diff --git a/packages/client/hmr/src/index.ts b/packages/client/hmr/src/index.ts index 1c169936bc..b06d3fa418 100644 --- a/packages/client/hmr/src/index.ts +++ b/packages/client/hmr/src/index.ts @@ -1,7 +1,7 @@ /** * HMR plugin, node half: the host end of the dev reload chain. One interval - * stat-polls every graph row's client bundle and optional source map (polling - * by design: network mounts deliver no inotify events), reports changes through + * stat-polls every graph row's client bundle (polling by design: network mounts + * deliver no inotify events), reports changes through * `clientModuleHost.rebuilt(id)`, and serves the `/plugins/events` SSE channel * broadcasting graph/rebuilt frames to the browser half (src/client/). * The web bundle mounts this row unconditionally: without a rebuild @@ -42,30 +42,22 @@ function sseData(frame: PluginsEventFrame): string { return `data: ${JSON.stringify(frame)}\n\n` } -type WatchedArtifactStat = Omit +type WatchedBundleStat = Omit type WatchedBundle = { -readonly [K in keyof ClientArtifactBaseline]: ClientArtifactBaseline[K] } & { dirty: boolean } -/** Snapshot the bundle plus its optional development source map. */ -function artifactStat(path: string): WatchedArtifactStat { +/** Snapshot the executable bundle metadata that drives reloads. */ +function bundleStat(path: string): WatchedBundleStat { const bundle = statSync(path) - try { - const map = statSync(`${path}.map`) - return { mtimeMs: bundle.mtimeMs, size: bundle.size, mapMtimeMs: map.mtimeMs, mapSize: map.size } - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - return { mtimeMs: bundle.mtimeMs, size: bundle.size, mapMtimeMs: null, mapSize: null } - } + return { mtimeMs: bundle.mtimeMs, size: bundle.size } } -/** Whether neither served artifact changed since the last successful re-hash. */ -function sameArtifactStat(left: WatchedArtifactStat, right: WatchedArtifactStat): boolean { +/** Whether the executable bundle is unchanged since the last successful re-hash. */ +function sameBundleStat(left: WatchedBundleStat, right: WatchedBundleStat): boolean { return left.mtimeMs === right.mtimeMs && left.size === right.size - && left.mapMtimeMs === right.mapMtimeMs - && left.mapSize === right.mapSize } /** @@ -80,7 +72,7 @@ export function apply(ctx: Context, config: Config): void { // --- bundle watch: one HMR-owned stat poll ------------------------------ const watched = new Map() - const rehash = (id: string, watch: WatchedBundle, current: WatchedArtifactStat): void => { + const rehash = (id: string, watch: WatchedBundle, current: WatchedBundleStat): void => { try { // rebuilt() replaces the opaque startup rev on its first call; later // calls stay silent when the content hash is unchanged. @@ -95,17 +87,15 @@ export function apply(ctx: Context, config: Config): void { } watch.mtimeMs = current.mtimeMs watch.size = current.size - watch.mapMtimeMs = current.mapMtimeMs - watch.mapSize = current.mapSize watch.dirty = false } const watchRow = (id: string, baseline: ClientArtifactBaseline): void => { const watch: WatchedBundle = { ...baseline, dirty: false } watched.set(id, watch) - let current: WatchedArtifactStat + let current: WatchedBundleStat try { - current = artifactStat(baseline.path) + current = bundleStat(baseline.path) } catch (error) { watch.dirty = true if ((error as NodeJS.ErrnoException).code !== 'ENOENT') ctx.logger.warn(error) @@ -113,20 +103,20 @@ export function apply(ctx: Context, config: Config): void { } // The module host captured its baseline before reading the bytes in the // startup batch. Only a mismatch crosses into the content-hash path. - if (!sameArtifactStat(current, watch)) rehash(id, watch, current) + if (!sameBundleStat(current, watch)) rehash(id, watch, current) } const pollWatches = (): void => { for (const [id, watch] of watched) { - let current: WatchedArtifactStat + let current: WatchedBundleStat try { - current = artifactStat(watch.path) + current = bundleStat(watch.path) } catch (error) { watch.dirty = true if ((error as NodeJS.ErrnoException).code !== 'ENOENT') ctx.logger.warn(error) continue } - if (!watch.dirty && sameArtifactStat(current, watch)) continue + if (!watch.dirty && sameBundleStat(current, watch)) continue // Stat-before-hash preserves a detectable older baseline for writes that // land during hashing. Repeated stat changes heal a torn read. rehash(id, watch, current) diff --git a/packages/client/hmr/tests/node-half.client.spec.ts b/packages/client/hmr/tests/node-half.client.spec.ts index 46bcd708d9..990eb6c4f6 100644 --- a/packages/client/hmr/tests/node-half.client.spec.ts +++ b/packages/client/hmr/tests/node-half.client.spec.ts @@ -31,19 +31,7 @@ interface FakeHostOptions { function artifactBaseline(path: string): ClientArtifactBaseline { const bundle = statSync(path) - try { - const sourceMap = statSync(`${path}.map`) - return { - path, - mtimeMs: bundle.mtimeMs, - size: bundle.size, - mapMtimeMs: sourceMap.mtimeMs, - mapSize: sourceMap.size, - } - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - return { path, mtimeMs: bundle.mtimeMs, size: bundle.size, mapMtimeMs: null, mapSize: null } - } + return { path, mtimeMs: bundle.mtimeMs, size: bundle.size } } function fakeClientModuleHost(rows: Map, options: FakeHostOptions = {}): FakeHost { @@ -111,7 +99,7 @@ async function mount(clientModuleHost: FakeHost, webServer: WebServer) { } describe('hmr node half', () => { - it('watches graph bundles, reports stat changes, and unwatches on dispose', async () => { + it('watches graph bundles, ignores map-only changes, and unwatches on dispose', async () => { const bundle = join(dir, 'a.js') writeFileSync(bundle, 'v1') const clientModuleHost = fakeClientModuleHost(new Map([['pkg-a', bundle]])) @@ -130,13 +118,17 @@ describe('hmr node half', () => { clientModuleHost.rebuiltCalls.length = 0 await new Promise(resolve => setTimeout(resolve, POLL_MS * 2)) writeFileSync(`${bundle}.map`, '{"version":3}') + await new Promise(resolve => setTimeout(resolve, POLL_MS * 3)) + expect(clientModuleHost.rebuiltCalls).toEqual([]) + + writeFileSync(bundle, 'v3-even-longer') await vi.waitFor(() => { expect(clientModuleHost.rebuiltCalls).toContain('pkg-a') }, { timeout: 3_000 }) await fiber.dispose() expect(routes).toHaveLength(0) // Watcher gone: further file changes report nothing. clientModuleHost.rebuiltCalls.length = 0 - writeFileSync(bundle, 'v3-even-longer') + writeFileSync(bundle, 'v4-after-dispose') await new Promise(resolve => setTimeout(resolve, POLL_MS * 4)) expect(clientModuleHost.rebuiltCalls).toHaveLength(0) }) diff --git a/packages/client/modules/src/index.ts b/packages/client/modules/src/index.ts index 27f2d07363..44bfa54f61 100644 --- a/packages/client/modules/src/index.ts +++ b/packages/client/modules/src/index.ts @@ -23,7 +23,7 @@ */ import { createHash, randomBytes } from 'node:crypto' -import { readFileSync, statSync, type Stats } from 'node:fs' +import { readFileSync, statSync } from 'node:fs' import type { IncomingMessage, ServerResponse } from 'node:http' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' @@ -78,10 +78,6 @@ export interface ClientArtifactBaseline { readonly mtimeMs: number /** Bundle size in bytes. */ readonly size: number - /** Source-map modification time, or null when no map was observable. */ - readonly mapMtimeMs: number | null - /** Source-map size in bytes, or null when no map was observable. */ - readonly mapSize: number | null } /** Resolved package metadata for one `dsh.client` package (cached per name, never expires). */ @@ -756,22 +752,13 @@ export class ClientModuleRegistry extends Service { return meta } - /** Capture the bundle and optional-map stats before reading their bytes. */ + /** Capture the bundle stats before reading its bytes. */ private captureArtifactBaseline(clientPath: string): ClientArtifactBaseline { const bundle = statSync(clientPath) - let sourceMap: Stats | undefined - try { - sourceMap = statSync(`${clientPath}.map`) - } catch { - // Optional map metadata only seeds HMR; the following map read reports - // malformed or inaccessible bytes and a later stat change self-heals. - } return { path: clientPath, mtimeMs: bundle.mtimeMs, size: bundle.size, - mapMtimeMs: sourceMap?.mtimeMs ?? null, - mapSize: sourceMap?.size ?? null, } } diff --git a/packages/client/modules/tests/node-half.client.spec.ts b/packages/client/modules/tests/node-half.client.spec.ts index 8452d6cd04..f55da4e017 100644 --- a/packages/client/modules/tests/node-half.client.spec.ts +++ b/packages/client/modules/tests/node-half.client.spec.ts @@ -378,8 +378,6 @@ describe('client bundle activation', () => { path: firstPath, mtimeMs: firstStat.mtimeMs, size: firstStat.size, - mapMtimeMs: null, - mapSize: null, }) expect(service.artifactBaseline('@fixture/unknown')).toBeUndefined() }) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 746467db37..4f5da0ccc8 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3434,7 +3434,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ClientArtifactBaseline', - declaration: 'export interface ClientArtifactBaseline {\n readonly path: string;\n readonly mtimeMs: number;\n readonly size: number;\n readonly mapMtimeMs: number | null;\n readonly mapSize: number | null;\n}', + declaration: 'export interface ClientArtifactBaseline {\n readonly path: string;\n readonly mtimeMs: number;\n readonly size: number;\n}', }, { name: 'CodeBindingErrorClass',