/** Node-half composition diagnostics for package metadata and built client bundles. */ import { Hash } from 'node:crypto' import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs' import { SourceMap } from 'node:module' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { pathToFileURL } from 'node:url' import { runInNewContext } from 'node:vm' import { Context, FiberState, type Fiber } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' import { renderIndexInjections, type WebServer, type WebResourceRoute, type WebRoute } from '@deepseek-ai/dsh-host-webserver' import * as modulesClient from '../src/client/index.ts' import { ClientModuleRegistry, bootInjections, orderByModuleGraph } from '../src/index.ts' import type { ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '../src/client/index.ts' /** * Paths whose synchronous read refuses, as a browser Worker refuses a file * whose bytes arrive only through an asynchronous fetch. */ const syncRefused = vi.hoisted(() => new Set()) vi.mock('node:fs', async (importOriginal) => { const actual = await importOriginal() return { ...actual, readFileSync: ((path: Parameters[0], ...rest: unknown[]) => { if (syncRefused.has(String(path))) { throw Object.assign(new Error(`synchronous read refused for ${String(path)}`), { code: 'ERR_DSH_VFS_DEFERRED' }) } return (actual.readFileSync as (...args: unknown[]) => unknown)(path, ...rest) }) as typeof actual.readFileSync, } }) /** Paths whose next asynchronous read fails once, as a lazily fetched file whose download fails. */ const asyncFailOnce = vi.hoisted(() => new Set()) vi.mock('node:fs/promises', async (importOriginal) => { const actual = await importOriginal() return { ...actual, readFile: ((path: Parameters[0], ...rest: unknown[]) => { if (typeof path === 'string' && asyncFailOnce.delete(path)) return Promise.reject(new Error(`download failed for ${path}`)) return (actual.readFile as (...args: unknown[]) => unknown)(path, ...rest) }) as typeof actual.readFile, } }) const MODULES_ID = '@deepseek-ai/dsh-client-modules' const UI_RENDERER_ID = '@deepseek-ai/dsh-client-ui-renderer' // Graph rows and batch descriptors carry app-directory-relative references; // a host request reaches the backend route once the document's directory // resolves them, which these origin-root fixtures reduce to a leading slash. const comboReference = (ids: readonly string[], rev: string): string => `plugins/??${ids.map(id => `${id}/client.js`).join(',')}&rev=${rev}` const mapUrl = (url: string): string => url.replace(/\/client\.js(?=,|&rev=)/g, '/client.js.map') const mapReference = (url: string): string => mapUrl(url).replace(/^\/?plugins\//u, '') // A chunk script is served from its package's own directory, so its // source-map reference resolves against the chunk's own URL: the bare map // file name, not the route key the response table holds. const chunkMapReference = (fileName: string, rev: string): string => `${fileName}.map?rev=${rev}` const chunkReference = (id: string, fileName: string, rev: string): string => `plugins/${id}/${fileName}?rev=${rev}` const BOOTSTRAP_URL = comboReference([MODULES_ID], 'boot') const APPLICATION_URL = comboReference([UI_RENDERER_ID], 'app') let root: string | undefined const contexts: { ctx: Context; ready?: Promise }[] = [] afterEach(async () => { await Promise.all(contexts.splice(0).map(async ({ ctx, ready }) => { await ready await ctx.fiber.dispose() })) if (root !== undefined) rmSync(root, { recursive: true, force: true }) root = undefined syncRefused.clear() }) it.each([false, true])('tracks the Web carrier lifetime when server-first is %s', async (serverFirst) => { const ctx = new Context() contexts.push({ ctx }) ctx.provide('loader', { entries: () => [] }) const routes = new Set() const mountServer = () => ctx.plugin((serverCtx) => { serverCtx.provide('webServer', { register: (route: WebRoute) => { routes.add(route) return () => { routes.delete(route) } }, } as WebServer) }) let server = serverFirst ? await mountServer() : undefined const modules = await ctx.plugin(ClientModuleRegistry) const service = ctx.get('clientModules')! expect(service.graph().entries).toEqual([]) expect((await service.fetchBundle(new Request('http://localhost/plugins/missing'))).status).toBe(404) if (!serverFirst) { expect(routes.size).toBe(0) server = await mountServer() } await expect.poll(() => routes.size).toBe(1) await server!.dispose() await expect.poll(() => routes.size).toBe(0) expect(modules.state).toBe(FiberState.ACTIVE) expect((await service.fetchBundle(new Request('http://localhost/plugins/missing'))).status).toBe(404) await mountServer() await expect.poll(() => routes.size).toBe(1) await modules.dispose() expect(routes.size).toBe(0) }) it('publishes this package version and the build commit as the Host release in every composed graph', async () => { const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string } const mount = async (commit: string | undefined) => { vi.stubEnv('DSH_CLIENT_COMMIT_HASH', commit) const ctx = new Context() contexts.push({ ctx }) ctx.provide('loader', { entries: () => [] }) await ctx.plugin(ClientModuleRegistry) vi.unstubAllEnvs() return ctx.get('clientModules')!.graph() } const graph = await mount('abc1234') expect(graph.release).toEqual({ version, commit: 'abc1234' }) expect(bootInjections(graph).at(-1)).toEqual({ kind: 'global', name: '__DSH_BOOT__', value: graph }) expect(modulesClient.parseBootManifest(graph).release).toEqual({ version, commit: 'abc1234' }) expect((await mount(undefined)).release).toEqual({ version }) }) /** Create a resolvable package whose client export points at the returned path. */ function writePackage( packageName: string, metadata: Record = { dsh: { client: { platform: 'web' } } }, ): string { root ??= realpathSync(mkdtempSync(join(tmpdir(), 'dsh-client-modules-'))) const pkgRoot = join(root, 'node_modules', ...packageName.split('/')) const clientPath = join(pkgRoot, 'lib', 'client.js') mkdirSync(pkgRoot, { recursive: true }) writeFileSync(join(pkgRoot, 'package.json'), JSON.stringify({ name: packageName, exports: { './client': './lib/client.js', './package.json': './package.json', }, ...metadata, })) return clientPath } /** Create a built package with the supplied client declaration. */ function writeBuiltPackage(packageName: string, client: Record): void { const clientPath = writePackage(packageName, { dsh: { client: { platform: 'web', ...client } } }) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') } /** Construct the node-half service and capture its plugin-bundle route. */ function constructWithRoute( packageNames: string[], options: { contextBaseUrl?: string entryBaseUrl?: string internal?: NonNullable onEntries?: () => void } = {}, ): { context: Context; service: ClientModuleRegistry; route: Promise } { const ctx = new Context() const owned: typeof contexts[number] = { ctx } contexts.push(owned) ctx.baseUrl = options.contextBaseUrl ?? pathToFileURL(root!).href + '/' ctx.provide('loader', { internal: options.internal, *entries() { options.onEntries?.() for (const packageName of packageNames) { yield { options: { name: packageName }, fiber: {}, disabled: false, parent: { tree: { ctx: { baseUrl: options.entryBaseUrl ?? ctx.baseUrl } } }, } } }, }) const route = Promise.withResolvers() const webServer: Pick = { port: 0, register: (candidate) => { if (candidate.path === '/plugins' && 'resource' in candidate) route.resolve(candidate) return () => {} }, tapIndex: () => () => {}, } ctx.provide('webServer', webServer as WebServer) const service = new ClientModuleRegistry(ctx) owned.ready = route.promise return { context: ctx, service, route: route.promise } } /** Construct the node-half service over the enabled fixture entries. */ function construct(packageNames: string[]): ClientModuleRegistry { return constructWithRoute(packageNames).service } /** Invoke the registered plugin resource and capture status, headers, and bytes. */ async function routeRequest(route: Promise, url: string, method = 'GET'): Promise<{ status: number headers: Record body: Buffer }> { const { resource } = await route const response = await resource(new Request(new URL(url, 'http://localhost/'), { method })) return { status: response.status, headers: Object.fromEntries(response.headers), body: Buffer.from(await response.arrayBuffer()) } } /** Execute the exact first inline script emitted by the Host boot rows. */ function injectedFacade(graph: WebBootGraph): { html: string; target: ClientModuleLoaderTarget } { const html = renderIndexInjections( '', bootInjections(graph), ) const source = /`) const graphAt = html.indexOf('globalThis["__DSH_BOOT__"] = ') const entryAt = html.indexOf('') expect([facadeAt, applicationAt, bootstrapAt, graphAt, entryAt]).toEqual([...new Set([ facadeAt, applicationAt, bootstrapAt, graphAt, entryAt, ])].sort((a, b) => a - b)) target.load({ id: MODULES_ID, factory: () => modulesClient }) const system = target.create({ boot: graph, staticModules: {}, loadBundle: async (url) => { expect(url).toBe(APPLICATION_URL) target.load({ id: UI_RENDERER_ID, factory: () => ({ marker: 'ui-renderer' }) }) }, }) expect(target.mode).toBe('live') expect(target.pendingQueue).toEqual([]) expect(system.manifest.rev).toBe('graph') expect(await system.import(MODULES_ID)).toBe(modulesClient) expect(await system.import(`${UI_RENDERER_ID}/client`)).toEqual({ marker: 'ui-renderer' }) expect(() => target.create({ boot: graph, staticModules: {} })) .toThrow('create called after module-system boot') }) it('preloads every application combo', () => { const graph = bootGraph() const secondId = '@fixture/second-application-combo' const secondUrl = comboReference([secondId], 'app-2') graph.entries.push({ id: secondId, url: comboReference([secondId], 'row-2'), rev: 'row-2' }) graph.batches.push({ phase: 'application', url: secondUrl, rev: 'app-2', entries: [secondId] }) expect(bootInjections(graph).flatMap(row => row.kind === 'script-preload' ? [row.src] : [])) .toEqual([APPLICATION_URL, secondUrl]) }) it('rejects a page that did not preload the modules bundle', () => { const graph = bootGraph() const { target } = injectedFacade(graph) expect(() => target.create({ boot: graph, staticModules: {} })) .toThrow(`HTML did not preload ${MODULES_ID}/client.js`) }) it('rejects a bootstrap bundle with a runtime external', () => { const graph = bootGraph() const { target } = injectedFacade(graph) target.load({ id: MODULES_ID, factory: (require) => { require('react') return modulesClient }, }) expect(() => target.create({ boot: graph, staticModules: {} })) .toThrow(`${MODULES_ID}/client.js requested external "react"`) }) it.each([ null, { ...modulesClient, createClientModuleSystem: undefined }, { ...modulesClient, apply: undefined }, ])('rejects a bootstrap bundle without the complete module face', (exports) => { const graph = bootGraph() const { target } = injectedFacade(graph) target.load({ id: MODULES_ID, factory: () => exports as unknown as Record }) expect(() => target.create({ boot: graph, staticModules: {} })) .toThrow(`${MODULES_ID}/client.js did not export the bootstrap module face`) }) }) describe('client bundle activation', () => { it('enumerates Loader entries once per dirty-name batch and skips empty batches', async () => { const names = ['@fixture/scan-first', '@fixture/scan-second', '@fixture/scan-retained'] for (const name of names) writeBuiltPackage(name, {}) const scans = vi.fn() const { context, service } = constructWithRoute(names, { onEntries: scans }) expect(scans).toHaveBeenCalledTimes(2) expect(service.graph().entries).toHaveLength(3) scans.mockClear() const removed = names.splice(0, 2) for (const name of [...removed, removed[0]!]) emitLoaderEntryChange(context, name) await Promise.resolve() expect(scans).toHaveBeenCalledTimes(1) expect(service.graph().entries.map(entry => entry.id)).toEqual(names) for (const name of removed) expect(service.clientPath(name)).toBeUndefined() scans.mockClear() context.emit('internal/plugin', context.fiber) await Promise.resolve() expect(scans).not.toHaveBeenCalled() const emptyScans = vi.fn() const empty = constructWithRoute([], { onEntries: emptyScans }) expect(empty.service.graph().entries).toEqual([]) expect(emptyScans).toHaveBeenCalledTimes(1) }) it('keeps a shared source until its last same-name Loader entry unloads', async () => { const name = '@fixture/shared-source-owners' writeBuiltPackage(name, {}) const names = [name, name] const { context, service } = constructWithRoute(names) const initial = service.graph() names.pop() emitLoaderEntryChange(context, name) await Promise.resolve() expect(service.graph()).toBe(initial) expect(service.clientPath(name)).toBeDefined() names.pop() emitLoaderEntryChange(context, name) await Promise.resolve() expect(service.graph().entries).toEqual([]) expect(service.clientPath(name)).toBeUndefined() }) it('reports an activation flush enumeration failure as a composition error', () => { const name = '@fixture/failed-activation-scan' writeBuiltPackage(name, {}) const scans = vi.fn() .mockImplementationOnce(() => undefined) .mockImplementationOnce(() => { throw new Error('Loader enumeration failed') }) expect(() => constructWithRoute([name], { onEntries: scans })).toThrow(AggregateError) }) it.each([new Error('Loader enumeration failed'), 'Loader enumeration failed'])( 'retains the graph and consumes the failed batch when Loader enumeration throws %s', async (failure) => { const names = ['@fixture/failed-scan-first', '@fixture/failed-scan-second', '@fixture/failed-scan-retained'] for (const name of names) writeBuiltPackage(name, {}) const scans = vi.fn() const { context, service, route } = constructWithRoute(names, { onEntries: scans }) await route const initial = service.graph() const warning = vi.spyOn(context.logger, 'warn').mockImplementation(() => undefined) const removed = names.splice(0, 2) scans.mockImplementationOnce(() => { throw failure }) const callbacks: VoidFunction[] = [] const enqueue = vi.spyOn(globalThis, 'queueMicrotask').mockImplementation(callback => callbacks.push(callback)) try { for (const name of removed) emitLoaderEntryChange(context, name) expect(callbacks).toHaveLength(1) expect(callbacks[0]!).not.toThrow() } finally { enqueue.mockRestore() } expect(warning).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Loader enumeration failed' })) expect(service.graph()).toBe(initial) for (const name of removed) expect(service.clientPath(name)).toBeDefined() emitLoaderEntryChange(context, names[0]!) await Promise.resolve() expect(service.graph()).toBe(initial) for (const name of removed) emitLoaderEntryChange(context, name) await Promise.resolve() expect(service.graph().entries.map(entry => entry.id)).toEqual(names) for (const name of removed) expect(service.clientPath(name)).toBeUndefined() }, ) it.each(['processing', 'processed'] as const)('consumes pending names once when a %s name is marked again', async (timing) => { const firstName = '@fixture/reentrant-first' const secondName = '@fixture/reentrant-second' const thirdName = '@fixture/reentrant-third' const names = [firstName, secondName, thirdName] const hostUrls = new Map() for (const name of names) { writeBuiltPackage(name, {}) const hostPath = join(root!, 'node_modules', ...name.split('/'), 'lib', 'index.js') writeFileSync(hostPath, 'export default {}\n') hostUrls.set(name, pathToFileURL(hostPath).href) } const resolved: string[] = [] let duringResolve = (_name: string) => {} const internal = { version: 'v2' as const, resolveSync: (_baseUrl: string, { specifier }: { specifier: string }) => { resolved.push(specifier) duringResolve(specifier) return { format: 'module' as const, url: hostUrls.get(specifier)! } }, } const { context, service } = constructWithRoute(names, { internal: internal as NonNullable, }) const initial = service.graph() resolved.length = 0 duringResolve = (name) => { if (name === firstName) { emitLoaderEntryChange(context, secondName) emitLoaderEntryChange(context, thirdName) if (timing === 'processing') emitLoaderEntryChange(context, firstName) } else if (name === secondName) { if (timing === 'processed') emitLoaderEntryChange(context, firstName) duringResolve = () => {} } } emitLoaderEntryChange(context, firstName) emitLoaderEntryChange(context, secondName) await Promise.resolve() expect(resolved.splice(0)).toEqual([firstName, secondName]) await Promise.resolve() expect(resolved).toEqual([thirdName, firstName]) expect(service.graph()).toBe(initial) }) it.each(['v1', 'v2'] as const)( 'resolves %s package metadata from the owning entry tree', (version) => { const packageName = `@fixture/entry-base-${version}` const clientPath = writePackage(packageName) const hostPath = join(dirname(clientPath), 'index.js') mkdirSync(dirname(hostPath), { recursive: true }) writeFileSync(hostPath, 'export default {}\n') writeFileSync(clientPath, 'module.exports = {}\n') const contextBaseUrl = pathToFileURL(join(root!, 'profile')).href + '/' const entryBaseUrl = pathToFileURL(join(root!, 'overlay')).href + '/' const calls: unknown[][] = [] const resolveSync = (...args: unknown[]) => { calls.push(args) return { format: 'module' as const, url: pathToFileURL(hostPath).href } } const internal = { version, resolveSync } const { service } = constructWithRoute([packageName], { contextBaseUrl, entryBaseUrl, internal: internal as NonNullable, }) expect(calls).toEqual(version === 'v2' ? [[entryBaseUrl, { specifier: packageName, attributes: {} }]] : [[packageName, entryBaseUrl, {}]]) expect(service.clientPath(packageName)).toBe(clientPath) expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) }, ) it('derives the browser module id from a file entry owning manifest', () => { const packageName = '@fixture/file-entry' const clientPath = writePackage(packageName) const hostPath = join(dirname(clientPath), 'index.js') mkdirSync(dirname(hostPath), { recursive: true }) writeFileSync(hostPath, 'export default {}\n') writeFileSync(clientPath, 'module.exports = {}\n') const service = construct([pathToFileURL(hostPath).href]) expect(service.clientPath(packageName)).toBe(clientPath) expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) }) it.each(['relative', 'absolute'] as const)( 'finds the owning manifest through the %s-path fallback without Node loader internals', (kind) => { const packageName = `@fixture/${kind}-fallback-entry` const clientPath = writePackage(packageName) const packageRoot = dirname(dirname(clientPath)) const hostPath = join(packageRoot, 'index.js') mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(hostPath, 'export default {}\n') writeFileSync(clientPath, 'module.exports = {}\n') const loaderName = kind === 'relative' ? './index.js' : hostPath const { service } = constructWithRoute([loaderName], { entryBaseUrl: pathToFileURL(packageRoot).href + '/', }) expect(service.clientPath(packageName)).toBe(clientPath) expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) }, ) it.each(['v1', 'v2', 'worker'] as const)( 'derives a file entry package id through the %s Loader resolver', (version) => { const packageName = `@fixture/file-entry-${version}` const clientPath = writePackage(packageName) const hostPath = join(dirname(clientPath), 'index.js') mkdirSync(dirname(hostPath), { recursive: true }) writeFileSync(hostPath, 'export default {}\n') writeFileSync(clientPath, 'module.exports = {}\n') const loaderName = pathToFileURL(hostPath).href const entryBaseUrl = pathToFileURL(join(root!, 'overlay')).href + '/' const calls: unknown[][] = [] const resolveSync = (...args: unknown[]) => { calls.push(args) return { format: 'module' as const, url: loaderName } } const internal = { version, resolveSync } const { service } = constructWithRoute([loaderName], { entryBaseUrl, internal: internal as NonNullable, }) expect(calls).toEqual(version === 'v2' ? [[entryBaseUrl, { specifier: loaderName, attributes: {} }]] : [[loaderName, entryBaseUrl, {}]]) expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) }, ) it('rejects distinct active Loader sources for one browser package', () => { const packageName = '@fixture/duplicate-source' const clientPath = writePackage(packageName) const hostPath = join(dirname(clientPath), 'index.js') mkdirSync(dirname(hostPath), { recursive: true }) writeFileSync(hostPath, 'export default {}\n') writeFileSync(clientPath, 'module.exports = {}\n') const alias = './duplicate-source.js' const internal = { version: 'v2' as const, resolveSync: () => ({ format: 'module' as const, url: pathToFileURL(hostPath).href }), } expect(() => constructWithRoute([packageName, alias], { internal: internal as unknown as NonNullable, })).toThrow( `client-modules: package ${packageName} resolves from multiple active Loader sources:`, ) }) it('promotes the remaining Loader source after the selected alias unloads', async () => { const packageName = '@fixture/duplicate-source-recovery' const clientPath = writePackage(packageName) const hostPath = join(dirname(clientPath), 'index.js') mkdirSync(dirname(hostPath), { recursive: true }) writeFileSync(hostPath, 'export default {}\n') writeFileSync(clientPath, 'module.exports = {}\n') const alias = './duplicate-source-recovery.js' const entries = [packageName] const internal = { version: 'v2' as const, resolveSync: () => ({ format: 'module' as const, url: pathToFileURL(hostPath).href }), } const { context, service } = constructWithRoute(entries, { internal: internal as unknown as NonNullable, }) const firstRevision = service.graph().entries[0]!.rev const warning = vi.spyOn(context.logger, 'warn').mockImplementation(() => undefined) entries.push(alias) emitLoaderEntryChange(context, alias) await Promise.resolve() expect(warning).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining(`package ${packageName} resolves from multiple active Loader sources`) as string, })) expect(service.graph().entries[0]!.rev).toBe(firstRevision) entries.splice(entries.indexOf(packageName), 1) emitLoaderEntryChange(context, packageName) await Promise.resolve() expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) expect(service.graph().entries[0]!.rev).toBe(firstRevision) expect(service.clientPath(packageName)).toBe(clientPath) }) it('uses owning-tree package resolution for an import-only Worker module loader', () => { const packageName = '@fixture/worker-loader' writeBuiltPackage(packageName, {}) const internal = { version: 'worker', import: async () => ({}), } as unknown as NonNullable const { service } = constructWithRoute([packageName], { internal }) expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) }) it('allows sibling dsh roles', () => { const currentName = '@fixture/current-client-field' const clientPath = writePackage(currentName, { dsh: { bundle: { patch: './cordis.patch.yml' }, client: { platform: 'web' }, profile: { bundles: [] }, }, }) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') expect(construct([currentName]).graph().entries.map(entry => entry.id)).toEqual([currentName]) }) it('groups missing bundles under one source-build instruction with a package/path list', () => { const firstName = '@fixture/missing-first' const secondName = '@fixture/missing-second' const firstPath = writePackage(firstName) const secondPath = writePackage(secondName) expect(() => construct([firstName, secondName])).toThrow([ 'client-modules: 2 client packages failed to compose:', ' client bundles not found; run `pnpm run build` before launch:', ` - package: ${firstName}`, ` path: ${firstPath}`, ` - package: ${secondName}`, ` path: ${secondPath}`, ].join('\n')) }) it('does not report other bundle read failures as missing builds', () => { const packageName = '@fixture/unreadable-client' const clientPath = writePackage(packageName) mkdirSync(clientPath, { recursive: true }) let thrown: unknown try { construct([packageName]) } catch (error) { thrown = error } expect(String(thrown)).toContain('client-modules: 1 client package failed to compose:') expect(String(thrown)).toContain(' other failures:') expect(String(thrown)).toContain('EISDIR') expect(String(thrown)).not.toContain('pnpm run build') }) it('falls back to a generated-file map when an authored map is malformed', async () => { const packageName = '@fixture/malformed-source-map' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') writeFileSync(`${clientPath}.map`, '{') const torn = constructWithRoute([packageName]) const tornRow = torn.service.graph().entries[0]! expect((await routeRequest(torn.route, tornRow.url)).body.toString('utf8')) .toContain(`sourceMappingURL=${mapReference(tornRow.url)}`) const fallback = await routeRequest(torn.route, mapUrl(torn.service.graph().batches[0]!.url)) expect(JSON.parse(fallback.body.toString('utf8'))).toMatchObject({ sections: [{ map: { sources: [`/plugins/${packageName}/client.js`] } }], }) writeFileSync(`${clientPath}.map`, '{"version":3,"sources":[null]}\n') expect(() => construct([packageName])).not.toThrow() writeFileSync(`${clientPath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sourceRoot: 'http://[', sources: ['src/index.ts'], })) const invalidUrl = constructWithRoute([packageName]) const invalidMapUrl = mapUrl(invalidUrl.service.graph().batches[0]!.url) expect(JSON.parse((await routeRequest(invalidUrl.route, invalidMapUrl)).body.toString('utf8'))).toMatchObject({ sections: [{ map: { sources: [`/plugins/${packageName}/client.js`] } }], }) }) it('reads a source map only on its first map GET', async () => { const packageName = '@fixture/lazy-source-map' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n//# sourceMappingURL=client.js.map') writeFileSync(`${clientPath}.map`, '{') const { service, route } = constructWithRoute([packageName]) const batch = service.graph().batches[0]! const sourceMapUrl = mapUrl(batch.url) const script = await routeRequest(route, batch.url) expect(script.status).toBe(200) expect((await routeRequest(route, sourceMapUrl, 'HEAD')).body).toHaveLength(0) writeFileSync(`${clientPath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: ['src/first.ts'], })) const first = await routeRequest(route, sourceMapUrl) expect(JSON.parse(first.body.toString('utf8'))).toMatchObject({ sections: [{ map: { sources: [`/plugins/${packageName}/src/first.ts`] } }], }) writeFileSync(`${clientPath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: ['src/second.ts'], })) expect((await routeRequest(route, sourceMapUrl)).body).toEqual(first.body) expect((await routeRequest(route, batch.url)).body).toEqual(script.body) }) it('retains a materialized resource when an unrelated row recomposes the graph', async () => { const stablePackage = '@fixture/stable-source-map' const rebuiltPackage = '@fixture/rebuilt-neighbor' const stablePath = writePackage(stablePackage) const rebuiltPath = writePackage(rebuiltPackage) for (const clientPath of [stablePath, rebuiltPath]) { mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n//# sourceMappingURL=client.js.map') writeFileSync(`${clientPath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: ['src/first.ts'], })) } const { service, route } = constructWithRoute([stablePackage, rebuiltPackage]) const stableUrl = service.graph().entries.find(entry => entry.id === stablePackage)!.url const stableMapUrl = mapUrl(stableUrl) const first = await routeRequest(route, stableMapUrl) const stableChunkPath = join(dirname(stablePath), 'client.stable.js') writeFileSync(stableChunkPath, 'module.exports = { generation: 1 }\n') const stableRow = service.graph().entries.find(entry => entry.id === stablePackage)! const stableChunkUrl = chunkReference(stablePackage, 'client.stable.js', stableRow.rev) const firstChunk = await routeRequest(route, stableChunkUrl) writeFileSync(`${stablePath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: ['src/second.ts'], })) writeFileSync(stableChunkPath, 'module.exports = { generation: 2 }\n') writeFileSync(rebuiltPath, 'module.exports = { rebuilt: true }\n') service.rebuilt(rebuiltPackage) expect((await routeRequest(route, stableMapUrl)).body).toEqual(first.body) expect((await routeRequest(route, stableChunkUrl)).body).toEqual(firstChunk.body) }) it('maps packed combo sections back to each generated client bundle', async () => { const names = ['@fixture/generated-first', '@fixture/generated-second'] for (const [index, packageName] of names.entries()) { const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync( clientPath, `window.generation = ${String(index)}\n//# sourceURL=packages/client/generated-${String(index)}/lib/client.js`, ) } const { service, route } = constructWithRoute(names) const batch = service.graph().batches[0]! const script = (await routeRequest(route, batch.url)).body.toString('utf8') expect(script).not.toContain('//# sourceURL=') expect(script).toContain(`//# sourceMappingURL=${mapReference(batch.url)}`) const payload = JSON.parse((await routeRequest(route, mapUrl(batch.url))).body.toString('utf8')) as { sections: { map: { mappings: string; sources: string[]; sourcesContent: string[] } }[] } expect(payload.sections.map(section => section.map)).toEqual([ { version: 3, names: [], mappings: 'AAAA', sources: ['/packages/client/generated-0/lib/client.js'], sourcesContent: ['window.generation = 0\n'], }, { version: 3, names: [], mappings: 'AAAA', sources: ['/packages/client/generated-1/lib/client.js'], sourcesContent: ['window.generation = 1\n'], }, ]) const consumer = new SourceMap(payload as unknown as ConstructorParameters[0]) expect(consumer.findEntry(0, 0)).toMatchObject({ originalSource: '/packages/client/generated-0/lib/client.js', }) expect(consumer.findEntry(2, 0)).toMatchObject({ originalSource: '/packages/client/generated-1/lib/client.js', }) }) it('retains one prior immutable batch generation across rebuild recomposition', async () => { const packageName = '@fixture/batch-rebuild-race' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) // Distinct sizes keep all three revisions distinct even within one filesystem clock tick. writeFileSync(clientPath, 'module.exports = { generation: 1 }\n') const { service, route } = constructWithRoute([packageName]) const first = service.graph().batches[0]!.url const firstSize = service.artifactBaseline(packageName)!.size writeFileSync(clientPath, 'module.exports = { generation: 200 }\n') service.rebuilt(packageName) const second = service.graph().batches[0]!.url const secondSize = service.artifactBaseline(packageName)!.size expect(second).not.toBe(first) expect(secondSize).toBeGreaterThan(firstSize) expect((await routeRequest(route, first)).status).toBe(200) expect((await routeRequest(route, second)).status).toBe(200) writeFileSync(clientPath, 'module.exports = { generation: 30000 }\n') service.rebuilt(packageName) const third = service.graph().batches[0]!.url expect(third).not.toBe(first) expect(third).not.toBe(second) expect(service.artifactBaseline(packageName)!.size).toBeGreaterThan(secondSize) expect((await routeRequest(route, first)).status).toBe(404) expect((await routeRequest(route, second)).status).toBe(200) expect((await routeRequest(route, third)).status).toBe(200) }) it('preserves artifact revisions across registry restarts and scan order changes', () => { const firstName = '@fixture/startup-revision-first' const secondName = '@fixture/startup-revision-second' writeBuiltPackage(firstName, {}) writeBuiltPackage(secondName, {}) const service = construct([firstName, secondName]) expect(construct([firstName, secondName]).graph()).toEqual(service.graph()) const reversed = construct([secondName, firstName]).graph() for (const entry of service.graph().entries) { expect(reversed.entries.find(row => row.id === entry.id)).toEqual(entry) } const firstPath = service.clientPath(firstName)! const firstStat = statSync(firstPath) expect(service.artifactBaseline(firstName)).toEqual({ path: firstPath, mtimeMs: firstStat.mtimeMs, ctimeMs: firstStat.ctimeMs, size: firstStat.size, }) expect(service.artifactBaseline('@fixture/unknown')).toBeUndefined() }) it('keeps the initial graph and subscribers unchanged when a rebuild has identical artifacts', () => { const packageName = '@fixture/unchanged-rebuild' writeBuiltPackage(packageName, {}) const service = construct([packageName]) const graph = service.graph() const onRebuilt = vi.fn() const onGraphChanged = vi.fn() service.onRebuilt(onRebuilt) service.onGraphChanged(onGraphChanged) expect(service.rebuilt(packageName)).toBe(graph.entries[0]!.rev) expect(service.graph()).toBe(graph) expect(onRebuilt).not.toHaveBeenCalled() expect(onGraphChanged).not.toHaveBeenCalled() }) it('hashes only metadata when publishing initial and rebuilt artifact revisions', () => { const packageName = '@fixture/metadata-revision' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) const bundle = `module.exports = ${JSON.stringify('x'.repeat(64 * 1024))}\n` writeFileSync(clientPath, bundle) const update = vi.spyOn(Hash.prototype, 'update') try { const service = construct([packageName]) const first = service.graph().entries[0]!.rev const before = statSync(clientPath) writeFileSync(clientPath, `module.exports = ${JSON.stringify('y'.repeat(64 * 1024))}\n`) utimesSync(clientPath, before.atime, new Date(before.mtimeMs + 1_000)) expect(service.rebuilt(packageName)).not.toBe(first) const inputBytes = update.mock.calls.map(([input]) => Buffer.byteLength(input)) expect(inputBytes.length).toBeGreaterThan(0) expect(inputBytes.reduce((total, bytes) => total + bytes, 0)).toBeLessThan(4 * 1024) } finally { update.mockRestore() } }) it.each(['bytes', 'build stamp'])('agrees with a fresh registry after rebuilding changed %s', (change) => { const packageName = '@fixture/changed-rebuild' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') const timestamp = new Date(1_700_000_000_000) utimesSync(clientPath, timestamp, timestamp) const service = construct([packageName]) const before = service.graph() const initialStat = statSync(clientPath) if (change === 'bytes') { writeFileSync(clientPath, 'module.exports = { changed: true }\n') utimesSync(clientPath, initialStat.atime, initialStat.mtime) } else { utimesSync(clientPath, initialStat.atime, new Date(initialStat.mtimeMs + 1_000)) } const rev = service.rebuilt(packageName) expect(rev).not.toBe(before.entries[0]!.rev) expect(construct([packageName]).graph()).toEqual(service.graph()) }) it('changes revisions when a same-size rewrite preserves mtime', async () => { const packageName = '@fixture/restored-mtime' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = { generation: 1 }\n') const timestamp = new Date(1_700_000_000_000) utimesSync(clientPath, timestamp, timestamp) const service = construct([packageName]) const before = service.artifactBaseline(packageName)! const first = service.graph() // Filesystem ctime can advance more coarsely than Date.now(); the fixture needs a distinct value. await expect.poll(() => { writeFileSync(clientPath, 'module.exports = { generation: 2 }\n') utimesSync(clientPath, timestamp, timestamp) return statSync(clientPath).ctimeMs }).not.toBe(before.ctimeMs) expect(statSync(clientPath)).toMatchObject({ mtimeMs: before.mtimeMs, size: before.size }) expect(service.rebuilt(packageName)).not.toBe(first.entries[0]!.rev) expect(construct([packageName]).graph()).toEqual(service.graph()) }) it('splits startup combos before the map-form URL exceeds 3 KiB', async () => { const packageNames = Array.from({ length: 48 }, (_, index) => ( `@fixture/combo-url-${String(index).padStart(3, '0')}-${'x'.repeat(40)}` )) const sourceMap = JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: ['src/index.ts'], }) for (const packageName of packageNames) { const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') writeFileSync(`${clientPath}.map`, sourceMap) } const { service, route } = constructWithRoute(packageNames) const batches = service.graph().batches.filter(batch => batch.phase === 'application') expect(batches.length).toBeGreaterThan(1) expect(batches.flatMap(batch => batch.entries)).toEqual(packageNames) for (const batch of batches) { expect(Buffer.byteLength(batch.url)).toBeLessThanOrEqual(3 * 1024) expect(Buffer.byteLength(mapUrl(batch.url))).toBeLessThanOrEqual(3 * 1024) expect((await routeRequest(route, batch.url)).status).toBe(200) expect((await routeRequest(route, mapUrl(batch.url))).status).toBe(200) } for (let index = 0; index < batches.length - 1; index += 1) { const entries = [...batches[index]!.entries, batches[index + 1]!.entries[0]!] expect(Buffer.byteLength(mapUrl(comboReference(entries, '0'.repeat(12))))).toBeGreaterThan(3 * 1024) } }) it('serves the source map beside a registered client bundle', async () => { const packageName = '@fixture/source-map' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n//# sourceMappingURL=client.js.map') const map = '{"version":3,"names":[],"mappings":"AAAA","sources":["../../../packages/client/demo/src/index.tsx","https://cdn.example.test/library.js"]}\n' writeFileSync(`${clientPath}.map`, map) const { service, route } = constructWithRoute([packageName]) const row = service.graph().entries[0]! const singleScript = await routeRequest(route, row.url) expect(singleScript.body.toString('utf8')).toContain(`sourceMappingURL=${mapReference(row.url)}`) const singleMap = await routeRequest(route, mapUrl(row.url)) expect(singleMap.status).toBe(200) expect(singleMap.headers).toEqual({ 'content-type': 'application/json; charset=utf-8', 'cache-control': 'public, max-age=31536000, immutable', }) expect(JSON.parse(singleMap.body.toString('utf8'))).toMatchObject({ version: 3, file: 'client.js', sections: [{ offset: { line: 0, column: 0 }, map: { ...(JSON.parse(map) as Record), sources: ['/packages/client/demo/src/index.tsx', 'https://cdn.example.test/library.js'], }, }], }) const batch = service.graph().batches[0]! expect(batch).toMatchObject({ phase: 'application', entries: [packageName] }) const batchScript = await routeRequest(route, batch.url) expect(batchScript.status).toBe(200) expect(batchScript.headers['cache-control']).toBe('public, max-age=31536000, immutable') expect(batchScript.body.toString('utf8')).toContain(`//# sourceMappingURL=${mapReference(batch.url)}`) const shellResponse = await service.fetchBundle(new Request(`dsh-app://app/${batch.url}`)) expect(shellResponse.status).toBe(200) expect(shellResponse.headers.get('cache-control')).toBe('public, max-age=31536000, immutable') expect(await shellResponse.text()).toBe(batchScript.body.toString('utf8')) expect((await routeRequest(route, batch.url, 'HEAD')).body).toHaveLength(0) expect((await routeRequest(route, batch.url, 'POST')).status).toBe(405) const batchMap = await routeRequest(route, mapUrl(batch.url)) const parsedBatchMap: unknown = JSON.parse(batchMap.body.toString('utf8')) const parsedPluginMap = JSON.parse(map) as Record expect(parsedBatchMap).toMatchObject({ version: 3, file: 'client.js', sections: [{ offset: { line: 0, column: 0 }, map: { ...parsedPluginMap, sources: ['/packages/client/demo/src/index.tsx', 'https://cdn.example.test/library.js'], }, }], }) expect((await routeRequest(route, `${row.url}&stale=1`.replace(`rev=${row.rev}`, 'rev=stale'))).status).toBe(404) writeFileSync(`${clientPath}.map`, '{"version":3,"names":[],"mappings":"AAAA","sources":["src/changed.tsx"]}\n') const entryStat = statSync(clientPath) utimesSync(clientPath, entryStat.atime, new Date(entryStat.mtimeMs + 1_000)) const nextRev = service.rebuilt(packageName) expect(nextRev).not.toBe(row.rev) const nextRow = service.graph().entries[0]! expect(nextRow.rev).toBe(nextRev) const nextMap = await routeRequest(route, mapUrl(nextRow.url)) expect(JSON.parse(nextMap.body.toString('utf8'))).toMatchObject({ sections: [{ map: { sources: ['/plugins/@fixture/source-map/src/changed.tsx'] } }], }) }) it('serves a package-local chunk only after its versioned URL is requested', async () => { const packageName = '@fixture/chunked' const clientPath = writePackage(packageName) const chunkPath = join(dirname(clientPath), 'client.terminal.js') mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = { load: () => require.async("./client.terminal.js") }\n') const { service, route } = constructWithRoute([packageName]) const row = service.graph().entries[0]! const startup = await routeRequest(route, row.url) expect(startup.status).toBe(200) expect(startup.body.toString('utf8')).not.toContain('terminal loaded') expect((await routeRequest(route, chunkReference(packageName, 'client.terminal.js', row.rev))).status).toBe(404) writeFileSync(chunkPath, 'module.exports = { marker: "terminal loaded" }\n') const url = chunkReference(packageName, 'client.terminal.js', row.rev) const head = await routeRequest(route, url, 'HEAD') expect(head.status).toBe(200) expect(head.body).toHaveLength(0) const chunk = await routeRequest(route, url) expect(chunk.status).toBe(200) expect(chunk.body.toString('utf8')).toContain('terminal loaded') expect(chunk.body.toString('utf8')).toContain(`sourceMappingURL=${chunkMapReference('client.terminal.js', row.rev)}`) expect((await routeRequest(route, url.replace('.js?', '.js.map?'))).status).toBe(200) expect((await routeRequest(route, url.replace(`rev=${row.rev}`, 'rev=stale'))).status).toBe(404) }) it('serves a chunk of a package named @scope/assets instead of treating it as an asset request', async () => { const packageName = '@fixture/assets' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') writeFileSync(join(dirname(clientPath), 'client.extra.js'), 'module.exports = { marker: "extra chunk" }\n') const { service, route } = constructWithRoute([packageName]) const row = service.graph().entries[0]! const chunk = await routeRequest(route, chunkReference(packageName, 'client.extra.js', row.rev)) expect(chunk.status).toBe(200) expect(chunk.body.toString('utf8')).toContain('extra chunk') }) it('reads a package-local chunk and its map body only through the asynchronous filesystem face', async () => { const packageName = '@fixture/async-chunk' const clientPath = writePackage(packageName) const chunkPath = join(dirname(clientPath), 'client.pdf.js') mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = { load: () => require.async("./client.pdf.js") }\n') writeFileSync(chunkPath, 'module.exports = { marker: "pdf loaded" }\n') syncRefused.add(chunkPath) const { service, route } = constructWithRoute([packageName]) const row = service.graph().entries[0]! const url = chunkReference(packageName, 'client.pdf.js', row.rev) const chunk = await routeRequest(route, url) expect(chunk.status).toBe(200) expect(chunk.body.toString('utf8')).toContain('pdf loaded') const map = await routeRequest(route, url.replace('.js?', '.js.map?')) expect(map.status).toBe(200) expect(JSON.parse(map.body.toString('utf8'))).toMatchObject({ version: 3 }) }) it('reads a package-local chunk again after its first asynchronous read fails', async () => { const packageName = '@fixture/retried-chunk' const clientPath = writePackage(packageName) const chunkPath = join(dirname(clientPath), 'client.excel.js') mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = { load: () => require.async("./client.excel.js") }\n') writeFileSync(chunkPath, 'module.exports = { marker: "excel loaded" }\n') asyncFailOnce.add(chunkPath) const { service, route } = constructWithRoute([packageName]) const url = chunkReference(packageName, 'client.excel.js', service.graph().entries[0]!.rev) await expect(routeRequest(route, url)).rejects.toThrow(`download failed for ${chunkPath}`) const retried = await routeRequest(route, url) expect(retried.status).toBe(200) expect(retried.body.toString('utf8')).toContain('excel loaded') }) it('loads a renamed browser export at the logical URL without exposing Node bundles as chunks', async () => { const packageName = '@fixture/dual-client' const legacyPath = writePackage(packageName, { dsh: { client: { platform: 'web' } }, exports: { './client': { types: './lib/types/client/index.d.ts', node: './lib/client.node.js', browser: './lib/client.browser.js', default: './lib/client.browser.js', }, './package.json': './package.json', }, }) mkdirSync(dirname(legacyPath), { recursive: true }) writeFileSync(join(dirname(legacyPath), 'client.browser.js'), 'module.exports = { browser: true }\n') for (const name of ['client.node.js', 'client.node.helper.js']) { writeFileSync(join(dirname(legacyPath), name), 'throw new Error("Node code must not reach a browser")\n') } const { service, route } = constructWithRoute([packageName]) const row = service.graph().entries[0]! expect(row.url).toContain(`${packageName}/client.js`) expect((await routeRequest(route, row.url)).body.toString('utf8')).toContain('browser: true') for (const name of ['client.node.js', 'client.node.helper.js']) { for (const suffix of ['', '.map']) { expect((await routeRequest(route, chunkReference(packageName, name + suffix, row.rev))).status).toBe(404) } } }) it('publishes a new chunk revision when a completed build rewrites only the entry timestamp', async () => { const packageName = '@fixture/chunk-only-rebuild' const clientPath = writePackage(packageName) const chunkPath = join(dirname(clientPath), 'client.terminal.js') mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = { load: () => require.async("./client.terminal.js") }\n') writeFileSync(chunkPath, 'module.exports = { generation: 1 }\n') const { service, route } = constructWithRoute([packageName]) const firstRow = service.graph().entries[0]! const firstUrl = chunkReference(packageName, 'client.terminal.js', firstRow.rev) expect((await routeRequest(route, firstUrl)).body.toString('utf8')).toContain('generation: 1') writeFileSync(chunkPath, 'module.exports = { generation: 2 }\n') const entryStat = statSync(clientPath) const completed = new Date(entryStat.mtimeMs + 1_000) utimesSync(clientPath, entryStat.atime, completed) const nextRev = service.rebuilt(packageName)! expect(nextRev).not.toBe(firstRow.rev) expect((await routeRequest(route, firstUrl)).status).toBe(404) const nextUrl = chunkReference(packageName, 'client.terminal.js', nextRev) expect((await routeRequest(route, nextUrl)).body.toString('utf8')).toContain('generation: 2') }) it('applies sourceRoot before relocating absolute-looking section sources', async () => { const packageName = '@fixture/source-root' const clientPath = writePackage(packageName) mkdirSync(dirname(clientPath), { recursive: true }) writeFileSync(clientPath, 'module.exports = {}\n') writeFileSync(`${clientPath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sourceRoot: '../root', sources: ['/absolute.ts'], })) const { service, route } = constructWithRoute([packageName]) const response = await routeRequest(route, mapUrl(service.graph().batches[0]!.url)) const map = JSON.parse(response.body.toString('utf8')) as { sections: { map: { sourceRoot?: string; sources: string[] } }[] } expect(map.sections[0]?.map).toMatchObject({ sources: ['/plugins/@fixture/root/absolute.ts'], }) expect(map.sections[0]?.map).not.toHaveProperty('sourceRoot') }) it('maps a non-zero second batch section through a standard source-map consumer', async () => { const firstName = '@fixture/offset-first' const secondName = '@fixture/offset-second' const firstPath = writePackage(firstName) const secondPath = writePackage(secondName) for (const [path, source] of [ [firstPath, '../../../packages/demo/first.ts'], [secondPath, '../../../packages/demo/second.ts'], ] as const) { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, 'window.first = true\nwindow.second = true\n') writeFileSync(`${path}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: [source], sourcesContent: ['export {}\n'], })) } const { service, route } = constructWithRoute([firstName, secondName]) const response = await routeRequest(route, mapUrl(service.graph().batches[0]!.url)) const payload = JSON.parse(response.body.toString('utf8')) as ConstructorParameters[0] const sections = (payload as unknown as { sections: { offset: { line: number; column: number } }[] }).sections expect(sections.map(section => section.offset)).toEqual([ { line: 0, column: 0 }, { line: 3, column: 0 }, ]) const consumer = new SourceMap(payload) expect(consumer.findEntry(0, 0)).toMatchObject({ originalSource: '/packages/demo/first.ts' }) expect(consumer.findEntry(3, 0)).toMatchObject({ originalSource: '/packages/demo/second.ts' }) }) it('combines a generated-file fallback with a later authored map', async () => { const unmappedName = '@fixture/unmapped-first' const mappedName = '@fixture/mapped-second' const unmappedPath = writePackage(unmappedName) const mappedPath = writePackage(mappedName) mkdirSync(dirname(unmappedPath), { recursive: true }) mkdirSync(dirname(mappedPath), { recursive: true }) writeFileSync(unmappedPath, 'window.unmapped = true\n') writeFileSync(mappedPath, 'window.mapped = true\n') writeFileSync(`${mappedPath}.map`, JSON.stringify({ version: 3, names: [], mappings: 'AAAA', sources: ['../../../packages/demo/mapped.ts'], sourcesContent: ['export {}\n'], })) const { service, route } = constructWithRoute([unmappedName, mappedName]) const response = await routeRequest(route, mapUrl(service.graph().batches[0]!.url)) const payload = JSON.parse(response.body.toString('utf8')) as ConstructorParameters[0] const consumer = new SourceMap(payload) expect(consumer.findEntry(0, 0)).toMatchObject({ originalSource: `/plugins/${unmappedName}/client.js`, }) expect(consumer.findEntry(2, 0)).toMatchObject({ originalSource: '/packages/demo/mapped.ts' }) }) }) function emitLoaderEntryChange(context: Context, name: string): void { context.emit('internal/plugin', { entry: { options: { name } }, } as unknown as Fiber) } describe('shared module declarations', () => { it('accepts external requests and carries them onto the graph row', () => { const packageName = '@fixture/shared-declared' writeBuiltPackage(packageName, { external: ['react'] }) const { service } = constructWithRoute([packageName]) const row = service.graph().entries[0] if (row === undefined) throw new Error('shared-declared graph row missing') const revisionMatcher: unknown = expect.any(String) // The browser resolves this reference against its own document; the // response table still answers the absolute route it resolves to. expect(row).toEqual({ id: packageName, url: `plugins/??${packageName}/client.js&rev=${row.rev}`, rev: revisionMatcher, external: ['react'], }) }) it('omits external when the package declares no requests', () => { const packageName = '@fixture/shared-absent' writeBuiltPackage(packageName, {}) const [row] = construct([packageName]).graph().entries expect(row).not.toHaveProperty('external') }) it('rejects a non-array external', () => { const packageName = '@fixture/external-not-array' writeBuiltPackage(packageName, { external: 'react' }) expect(() => construct([packageName])) .toThrow(`client-modules: ${packageName} dsh.client.external must be a string array`) }) }) describe('module graph order', () => { const entry = (id: string, fields: Partial = {}): WebBootEntry => ({ id, url: comboReference([id], '0'), rev: '0', ...fields }) const ids = (entries: readonly WebBootEntry[]): string[] => entries.map(row => row.id) it('places every requested package row before its consumers along a chain', () => { expect(ids(orderByModuleGraph([ entry('ui', { external: ['slots'] }), entry('slots', { external: ['render'] }), entry('render'), ]))).toEqual(['render', 'slots', 'ui']) }) it('places a shared package row before both arms of a diamond', () => { expect(ids(orderByModuleGraph([ entry('app', { external: ['left', 'right'] }), entry('left', { external: ['vendor'] }), entry('right', { external: ['vendor'] }), entry('vendor'), ]))).toEqual(['vendor', 'left', 'right', 'app']) }) it('resolves a /client request onto the requested package row', () => { expect(ids(orderByModuleGraph([ entry('ui', { external: ['runtime/client'] }), entry('runtime'), ]))).toEqual(['runtime', 'ui']) }) it('leaves a request no row answers to the static assembly channel', () => { expect(ids(orderByModuleGraph([ entry('consumer', { external: ['@deepseek-ai/cordis'] }), entry('other'), ]))).toEqual(['consumer', 'other']) }) it('rejects a cycle and names the packages on it', () => { expect(() => orderByModuleGraph([ entry('a', { external: ['b'] }), entry('b', { external: ['a'] }), ])).toThrow('client-modules: module graph cycle a -> b -> a') }) it('rejects a row requesting its own package name', () => { expect(() => orderByModuleGraph([entry('solo', { external: ['solo'] })])) .toThrow('client-modules: "solo" requests module "solo" that it answers itself') }) it('composes the served graph in module-graph order', () => { const consumerName = '@fixture/order-consumer' const dependencyName = '@fixture/order-dependency' writeBuiltPackage(consumerName, { external: [dependencyName] }) writeBuiltPackage(dependencyName, {}) expect(ids(construct([consumerName, dependencyName]).graph().entries)) .toEqual([dependencyName, consumerName]) }) it('fails activation loud when scanned packages form a module cycle', () => { writeBuiltPackage('@fixture/cycle-a', { external: ['@fixture/cycle-b'] }) writeBuiltPackage('@fixture/cycle-b', { external: ['@fixture/cycle-a'] }) expect(() => construct(['@fixture/cycle-a', '@fixture/cycle-b'])) .toThrow('module graph cycle @fixture/cycle-a -> @fixture/cycle-b -> @fixture/cycle-a') }) }) describe('package manifest refresh', () => { /** Rewrite a fixture package's manifest; its client bundle is left as it is. */ const writeManifest = (packageName: string, manifest: Record): void => { writeFileSync(join(root!, 'node_modules', ...packageName.split('/'), 'package.json'), JSON.stringify({ name: packageName, ...manifest })) } const clientManifest = (client: Record, clientExport = './lib/client.js'): Record => ({ exports: { './client': clientExport, './package.json': './package.json' }, dsh: { client: { platform: 'web', ...client } }, }) it('publishes a reinstalled declaration when its Loader entry restarts, and nothing for an unchanged restart', async () => { const packageName = '@fixture/refresh-declaration' writeBuiltPackage(packageName, { external: ['react'] }) const { context, service } = constructWithRoute([packageName]) const before = service.graph().entries[0]! const changed = vi.fn() service.onGraphChanged(changed) const warning = vi.spyOn(context.logger, 'warn') // An unchanged restart compares bundle metadata and never reads the bundle. syncRefused.add(service.clientPath(packageName)!) emitLoaderEntryChange(context, packageName) await Promise.resolve() expect(changed).not.toHaveBeenCalled() expect(warning).not.toHaveBeenCalled() syncRefused.delete(service.clientPath(packageName)!) writeManifest(packageName, clientManifest({ external: ['scheduler'], inject: ['@fixture/slots'] })) emitLoaderEntryChange(context, packageName) await Promise.resolve() expect(changed).toHaveBeenCalledTimes(1) expect(service.graph().entries[0]).toMatchObject({ rev: before.rev, external: ['scheduler'], inject: ['@fixture/slots'] }) }) it('reads a moved client export as a new bundle generation when its Loader entry restarts', async () => { const packageName = '@fixture/refresh-moved' writeBuiltPackage(packageName, {}) const { context, service } = constructWithRoute([packageName]) const before = service.graph().entries[0]! const moved = join(root!, 'node_modules', '@fixture', 'refresh-moved', 'dist', 'client.js') mkdirSync(dirname(moved), { recursive: true }) writeFileSync(moved, 'module.exports = { moved: true }\n') writeManifest(packageName, clientManifest({}, './dist/client.js')) emitLoaderEntryChange(context, packageName) await Promise.resolve() expect(service.clientPath(packageName)).toBe(moved) expect(service.graph().entries[0]!.rev).not.toBe(before.rev) }) it('adds or removes the row when a restarted package gains or loses its client declaration', async () => { const packageName = '@fixture/refresh-membership' writeBuiltPackage(packageName, {}) writeManifest(packageName, { exports: { './package.json': './package.json' } }) const { context, service } = constructWithRoute([packageName]) expect(service.graph().entries).toEqual([]) writeManifest(packageName, clientManifest({})) emitLoaderEntryChange(context, packageName) await Promise.resolve() expect(service.graph().entries.map(entry => entry.id)).toEqual([packageName]) writeManifest(packageName, { exports: { './package.json': './package.json' } }) emitLoaderEntryChange(context, packageName) await Promise.resolve() expect(service.graph().entries).toEqual([]) }) })