Files
deepseek-harness/scripts/client-bundle-purity.spec.ts
T

781 lines
39 KiB
TypeScript

/**
* Pins shared client-bundle preset rules: module-edge purity, source-map
* chaining, and physical watch dependencies hidden behind virtual CSS Modules.
*/
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { SourceMap } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
import { runInNewContext } from 'node:vm'
import { build, type Rolldown, type TsdownBundle, type UserConfig } from 'tsdown'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { clientBundle, requestedExternals, staticLinked } from '../packages/client/tsdown.client.ts'
type ResolveId = (source: string) => null | { id: string; external: boolean }
interface CssModulePlugin {
name: string
resolveId?: (source: string, importer: string | undefined) => null | string
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
}
interface SourceMapPlugin {
name: string
load?: (id: string) => Promise<unknown>
}
interface InputIsolationPlugin {
name: string
generateBundle: (this: {
getModuleInfo(id: string): { importedIds: string[]; dynamicallyImportedIds: string[] } | null
}, options: unknown, bundle: Record<string, {
type: 'chunk'
modules: Record<string, object>
imports: string[]
dynamicImports: string[]
}>) => void
}
function packageDir(packagePath: string): string {
return fileURLToPath(new URL(`../packages/${packagePath}`, import.meta.url))
}
/** A representative dynamic bundle using the shared client baseline. */
const REQUESTING_PACKAGE = '@deepseek-ai/dsh-client-ui-conversation'
const REQUESTING_PACKAGE_DIR = packageDir('client/ui-conversation')
function clientConfigs(dir = REQUESTING_PACKAGE_DIR) {
return clientBundle(dir, ['lib/types/index.js'])(
{ env: { DSH_BUILD_FACE: 'client' } },
).filter(config => config.platform === 'browser')
}
describe('client bundle build faces', () => {
it('names both configs and the loader registration after the package directory manifest', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-manifest-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-client-fixture' }))
const configs = clientBundle(root, ['lib/types/index.js'])({ env: { DSH_BUILD_FACE: 'client' } })
expect(configs.map(config => config.name)).toEqual([
'@deepseek-ai/dsh-client-fixture', '@deepseek-ai/dsh-client-fixture/client',
])
const outputOptions = configs[1]?.outputOptions
if (typeof outputOptions !== 'object' || typeof outputOptions.banner !== 'function') {
throw new Error('client banner missing')
}
expect(outputOptions.banner({ isEntry: true, fileName: 'client.js' } as Rolldown.RenderedChunk))
.toBe('window.__ModuleLoader__.load({ id: "@deepseek-ai/dsh-client-fixture", factory: (require) => {')
})
it('rejects a package directory without a named manifest', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-manifest-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
expect(() => clientBundle(root, ['lib/types/index.js']))
.toThrow(/cannot read .*package\.json; pass the package directory/)
writeFileSync(join(root, 'package.json'), JSON.stringify({ private: true }))
expect(() => staticLinked(root, ['lib/types/index.js'])).toThrow(/declares no string package name/)
})
it('opts shared Clients into independent Node ESM and browser factory entries', () => {
for (const dir of ['client/connection', 'api/gateway', 'typert/registry']) {
const configs = clientBundle(packageDir(dir), ['lib/types/index.js'], { dualClient: true })(
{ env: { DSH_BUILD_FACE: 'client' } },
)
const node = configs.find(config => config.name?.endsWith('/client-node'))
const browser = configs.find(config => config.name?.endsWith('/client'))
expect(node?.entry).toEqual({ 'client.node': 'lib/types/client/index.js' })
expect(node).toMatchObject({ platform: 'node', format: ['esm'], sourcemap: true })
expect(node?.inputOptions).toMatchObject({ resolve: { conditionNames: ['node', 'import', 'module', 'default'] } })
expect(node?.outputOptions).toMatchObject({ entryFileNames: 'client.node.js' })
expect(browser?.platform).toBe('browser')
expect(browser?.outputOptions).toMatchObject({ entryFileNames: 'client.browser.js' })
expect(configs.find(config => config.platform === 'node' && config.name?.endsWith('/client'))).toBeUndefined()
}
const ordinary = clientBundle(REQUESTING_PACKAGE_DIR, ['lib/types/index.js'])(
{ env: { DSH_BUILD_FACE: 'client' } },
)
expect(ordinary.some(config => config.name?.endsWith('/client-node'))).toBe(false)
expect(ordinary.find(config => config.platform === 'browser')?.outputOptions)
.toMatchObject({ entryFileNames: 'client.js' })
})
it('watches source in development and consumes emitted JavaScript in the Client build', () => {
const bundle = clientBundle(REQUESTING_PACKAGE_DIR, ['lib/types/index.js'])
const development = bundle({ env: {} }).find(config => config.platform === 'browser')
const artifact = bundle({ env: { DSH_BUILD_FACE: 'client' } })
.find(config => config.platform === 'browser')
expect(development?.entry).toEqual({ client: 'src/client/index.ts' })
expect(artifact?.entry).toEqual({ client: 'lib/types/client/index.js' })
})
})
function dynamicImportFixture(prefix: string): string {
const root = mkdtempSync(join(tmpdir(), prefix))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
const entry = join(root, 'lib/types/client/index.js')
mkdirSync(dirname(entry), { recursive: true })
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
writeFileSync(entry, 'export const load = () => import("./terminal.js")\n')
writeFileSync(join(dirname(entry), 'terminal.js'), 'export const marker = "terminal"\n')
return root
}
interface ClientFactoryRegistration<Value, Require> {
id: string
chunk?: string
factory(require: Require): Value
}
function registerClientFactory<Value, Require>(code: string): ClientFactoryRegistration<Value, Require> {
const registrations: ClientFactoryRegistration<Value, Require>[] = []
runInNewContext(code, { window: { __ModuleLoader__: { load(value: ClientFactoryRegistration<Value, Require>) {
registrations.push(value)
} } } })
expect(registrations).toHaveLength(1)
const registration = registrations[0]
if (registration === undefined) throw new Error('client factory registration missing')
expect(registration.id).toBe(REQUESTING_PACKAGE)
return registration
}
describe('client bundle dynamic imports', () => {
it('keeps a dual Client browser chunk disjoint from its Node output', async () => {
const root = dynamicImportFixture('dsh-dual-client-chunk-')
const configs = clientBundle(root, ['lib/types/index.js'], { dualClient: true })(
{ env: { DSH_BUILD_FACE: 'client' } },
)
for (const platform of ['node', 'browser'] as const) {
const config = configs.find(config => config.name?.endsWith(platform === 'node' ? '/client-node' : '/client'))
if (config === undefined) throw new Error(`${platform} config missing`)
const builds = await build({
...config, cwd: root, config: false, tsconfig: false,
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
})
try {
const names = builds.flatMap(bundle => bundle.chunks).map(chunk => chunk.fileName)
expect(names).toContain(platform === 'node' ? 'client.node.js' : 'client.browser.js')
expect(names).toContain(platform === 'node' ? 'client.node.terminal.js' : 'client.browser.terminal.js')
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
}
})
it('compiles import() to the module loader asynchronous operation', async () => {
const root = dynamicImportFixture('dsh-client-dynamic-import-')
const config = clientConfigs()[0]
if (config === undefined) throw new Error('client config missing')
let builds: TsdownBundle[] = []
try {
builds = await build({
...config, cwd: root, config: false, tsconfig: false,
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
})
const chunks = builds.flatMap(bundle => bundle.chunks).filter(chunk => chunk.type === 'chunk')
const entry = chunks.find(chunk => chunk.fileName === 'client.js')
const terminal = chunks.find(chunk => chunk.fileName === 'client.terminal.js')
if (entry === undefined || terminal === undefined) throw new Error('dynamic import fixture outputs missing')
type Terminal = { marker: string }
type Require = ((specifier: string) => never) & { async(specifier: string): Promise<Terminal> }
const requests: string[] = []
const require: Require = Object.assign((specifier: string): never => {
throw new Error(`Unexpected synchronous module request: ${specifier}`)
}, {
async(specifier: string): Promise<Terminal> {
requests.push(specifier)
expect(specifier).toBe('./client.terminal.js')
const registration = registerClientFactory<Terminal, Require>(terminal.code)
expect(registration.chunk).toBe('client.terminal.js')
return Promise.resolve(registration.factory(require))
},
})
const registration = registerClientFactory<{ load(): Promise<Terminal> }, Require>(entry.code)
expect(registration.chunk).toBeUndefined()
const exports = registration.factory(require)
expect(requests).toEqual([])
await expect(exports.load()).resolves.toEqual({ marker: 'terminal' })
expect(requests).toEqual(['./client.terminal.js'])
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
})
})
interface CompressionFixtureDetails {
describeScore(value: number): string
}
interface CompressionFixtureEntry {
calculateScore(values: readonly number[]): number
activate(): number
incrementScore(value: number): number
decrementScore(value: number): number
FixtureAccumulator: { new (seed: number): { total(values: readonly number[]): number }; readonly name: string }
loadDetails(): Promise<CompressionFixtureDetails>
}
type CompressionFixtureRequire = ((specifier: string) => never) & {
async(specifier: string): Promise<CompressionFixtureDetails>
}
describe('production client bundle compression', () => {
it('keeps executable exports, lazy chunks, names, notices and maps while compressing production only', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-compression-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
const directory = join(root, 'lib/types/client')
mkdirSync(directory, { recursive: true })
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
// Every long local contributes to a runtime result; comments and dead branches cannot earn the byte reduction.
const terms = Array.from({ length: 24 }, (_, index) => `intermediateContributionForInputPosition${index}`)
const entrySource = [
'/*! @license Fixture arithmetic license */',
'export function calculateScore(values) {',
...terms.map((name, index) => ` const ${name} = values[${index}] * ${index + 1} + ${index + 2};`),
` return ${terms.join(' + ')};`,
'}',
'export class FixtureAccumulator {',
' constructor(seed) { this.seed = seed; }',
' total(values) { return this.seed + calculateScore(values); }',
'}',
'export const activate = () => 7;',
'export { collisionLabel as incrementScore } from "./increment.js";',
'export { collisionLabel as decrementScore } from "./decrement.js";',
'export function loadDetails() { return import("./details.js"); }',
'',
].join('\n')
const detailSource = [
'/*! @license Fixture detail license */',
'export function describeScore(value) {',
' const descriptiveScorePrefix = "score";',
' return descriptiveScorePrefix + ":" + value;',
'}',
'',
].join('\n')
writeFileSync(join(directory, 'index.js'), entrySource)
writeFileSync(join(directory, 'details.js'), detailSource)
writeFileSync(join(directory, 'increment.js'), 'export function collisionLabel(value) { return value + 1; }\n')
writeFileSync(join(directory, 'decrement.js'), 'export function collisionLabel(value) { return value - 1; }\n')
const namesByMode = new Map<string, readonly string[]>()
for (const mode of ['production', 'development'] as const) {
const previousNodeEnv = process.env.NODE_ENV
const noticeBody = '// Copyright Fixture Authors\n// Permission is granted to use this fixture.'
let config: UserConfig | undefined
try {
process.env.NODE_ENV = mode
config = clientBundle(root, ['lib/types/index.js'], {
clientBanner: fileName => `//! Fixture banner for ${fileName}\n${noticeBody}`,
})({ env: { DSH_BUILD_FACE: 'client' } }).find(candidate => candidate.platform === 'browser')
} finally {
if (previousNodeEnv === undefined) delete process.env.NODE_ENV
else process.env.NODE_ENV = previousNodeEnv
}
if (config === undefined) throw new Error('client config missing')
const builds = await build({
...config, cwd: root, config: false, tsconfig: false,
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
})
try {
const chunks = builds.flatMap(bundle => bundle.chunks).filter(chunk => chunk.type === 'chunk')
expect(chunks.map(chunk => chunk.fileName).sort()).toEqual(['client.details.js', 'client.js'])
const entry = chunks.find(chunk => chunk.fileName === 'client.js')
const details = chunks.find(chunk => chunk.fileName === 'client.details.js')
if (entry === undefined || details === undefined) throw new Error('client fixture outputs missing')
const requests: string[] = []
const require: CompressionFixtureRequire = Object.assign((specifier: string): never => {
throw new Error(`Unexpected synchronous module request: ${specifier}`)
}, {
async(specifier: string): Promise<CompressionFixtureDetails> {
requests.push(specifier)
expect(specifier).toBe('./client.details.js')
const registration = registerClientFactory<CompressionFixtureDetails, CompressionFixtureRequire>(details.code)
expect(registration.chunk).toBe('client.details.js')
return Promise.resolve(registration.factory(require))
},
})
const registration = registerClientFactory<CompressionFixtureEntry, CompressionFixtureRequire>(entry.code)
expect(registration.chunk).toBeUndefined()
const exports = registration.factory(require)
const values = Array.from({ length: terms.length }, (_, index) => index * 3 - 7)
const expected = values.reduce((sum, value, index) => sum + value * (index + 1) + index + 2, 0)
expect(exports.calculateScore(values)).toBe(expected)
expect(new exports.FixtureAccumulator(9).total(values)).toBe(expected + 9)
expect(exports.incrementScore(expected)).toBe(expected + 1)
expect(exports.decrementScore(expected)).toBe(expected - 1)
expect(exports.calculateScore.name).toBe('calculateScore')
expect(exports.activate()).toBe(7)
expect(exports.activate.name).toBe('activate')
expect(exports.FixtureAccumulator.name).toBe('FixtureAccumulator')
namesByMode.set(mode, [exports.incrementScore.name, exports.decrementScore.name])
expect(requests).toEqual([])
const loaded = await exports.loadDetails()
expect(requests).toEqual(['./client.details.js'])
expect(loaded.describeScore(expected)).toBe(`score:${expected}`)
expect(loaded.describeScore.name).toBe('describeScore')
for (const [chunk, source, notice, name] of [
[entry, entrySource, 'Fixture arithmetic license', 'calculateScore'],
[details, detailSource, 'Fixture detail license', 'describeScore'],
] as const) {
expect(chunk.code).toContain(`@license ${notice}`)
expect(chunk.code).toContain(`//! Fixture banner for ${chunk.fileName}\n${noticeBody}`)
expect(chunk.map?.sourcesContent).toContain(source)
expect(chunk.map?.mappings.length).toBeGreaterThan(0)
expect(chunk.code.trimEnd().endsWith(`//# sourceMappingURL=${chunk.fileName}.map`)).toBe(true)
if (chunk.map === null) throw new Error('client fixture source map missing')
const map = new SourceMap({
...chunk.map, file: chunk.fileName, sourceRoot: '',
sourcesContent: chunk.map.sourcesContent.map(content => content ?? ''),
})
const marker = `function ${name}`
expect(chunk.code).toContain(marker)
// Rolldown maps a function declaration to the start of its export statement.
const generatedOffset = chunk.code.indexOf(marker)
const originalOffset = source.indexOf(`export ${marker}`)
const generatedLines = chunk.code.slice(0, generatedOffset).split('\n')
const originalLines = source.slice(0, originalOffset).split('\n')
expect(map.findEntry(generatedLines.length - 1, generatedLines.at(-1)!.length)).toMatchObject({
originalLine: originalLines.length - 1,
originalColumn: originalLines.at(-1)!.length,
})
}
if (mode === 'production') {
// These ceilings include the executable factory, loader call, legal notices and source-map reference.
expect(Buffer.byteLength(entry.code)).toBeLessThanOrEqual(1600)
expect(Buffer.byteLength(details.code)).toBeLessThanOrEqual(600)
} else {
expect(entry.code).toContain('intermediateContributionForInputPosition0')
}
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
}
// Minification preserves the bundler's names, including any static-module name collision suffixes.
expect(namesByMode.get('production')).toEqual(namesByMode.get('development'))
})
})
function clientSourceMapPath(packagePath: string): string {
return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
}
function purityResolveId(dir = REQUESTING_PACKAGE_DIR): ResolveId {
const configs = clientConfigs(dir)
const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
return gate.resolveId as ResolveId
}
function cssModulePlugin(): CssModulePlugin {
const configs = clientConfigs()
const plugins = (configs[0] as { plugins: CssModulePlugin[] }).plugins
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
if (plugin?.resolveId === undefined || plugin.load === undefined) {
throw new Error('CSS Modules plugin missing from client config')
}
return plugin
}
function sourceMapPlugin(): SourceMapPlugin {
const configs = clientConfigs()
const plugins = (configs[0] as { plugins: SourceMapPlugin[] }).plugins
const plugin = plugins.find(candidate => candidate.name === 'dsh-tsc-sourcemap')
if (plugin?.load === undefined) throw new Error('tsc sourcemap plugin missing from client config')
return plugin
}
describe('client bundle purity gate', () => {
const resolveId = purityResolveId()
it('leaves default externals and non-scoped specifiers alone', () => {
expect(resolveId('@deepseek-ai/dsh-client-store')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-client-ui-slots')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-client-ui-primitives')).toBeNull()
expect(resolveId('react')).toBeNull()
expect(resolveId('zod')).toBeNull()
})
it('rejects the retired web-react platform package', () => {
expect(() => resolveId('@deepseek-ai/dsh-client-web-react')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-web-react/store')).toThrow(/purity/)
})
it('lets inline-safe libraries inline', () => {
expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-deque')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-util-values')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-token-meter/client')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-token-meter')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-token-meter/client/internal')).toThrow(/purity/)
expect(resolveId('@deepseek-ai/dsh-host-open-in-app/shared')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-native-command/types')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-native-command')).toThrow('client bundle purity')
expect(() => resolveId('@deepseek-ai/dsh-host-open-in-app')).toThrow(/purity/)
expect(resolveId('@deepseek-ai/dsh-plugin-manager/registry')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-plugin-manager')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-plugin-manager/registry/internal')).toThrow(/purity/)
})
it('admits only the pure spill notice entry, not its Host policy', () => {
expect(resolveId('@deepseek-ai/dsh-spill-policy/notice')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-output-retention')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-spill-policy')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-spill-policy/notice/internal')).toThrow(/purity/)
})
it('inlines the stateless file-drag entry without admitting its subpaths or UI plugin', () => {
expect(resolveId('@deepseek-ai/dsh-util-file-drag')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-util-file-drag/internal')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-ui-file-drag')).toThrow(/purity/)
const deps = clientConfigs()[0]?.deps as { neverBundle: (specifier: string) => boolean }
expect(deps.neverBundle('@deepseek-ai/dsh-util-file-drag')).toBe(false)
})
it('inlines the browser-safe Timer entry while sharing Cordis through the module table', () => {
const timer = '@deepseek-ai/cordis-plugin-timer'
expect(resolveId(timer)).toBeNull()
expect(() => resolveId(`${timer}/src/index.ts`)).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/cordis-plugin-hmr')).toThrow(/purity/)
const config = clientConfigs(packageDir('extensions/cordis-client-runner'))[0]
const deps = config?.deps as { neverBundle: (specifier: string) => boolean }
expect(deps.neverBundle(timer)).toBe(false)
expect(deps.neverBundle('@deepseek-ai/cordis')).toBe(true)
})
it('lets exact generated Remote contributions inline without admitting their package implementation', () => {
expect(resolveId('@deepseek-ai/dsh-goal/remote')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-goal')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-goal/client')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-goal/remote/nested')).toThrow(/purity/)
})
it('throws on any other @deepseek-ai leak', () => {
expect(() => resolveId('@deepseek-ai/dsh-agent')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-web')).toThrow(/purity/)
})
it('throws on cross-plugin value imports — bare plugin names and /client subpaths alike', () => {
expect(() => resolveId('@deepseek-ai/dsh-client-connection')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-ui-session')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-ui-layout/client')).toThrow(/purity/)
})
it('admits package-specific requests only for the declaring bundle', () => {
const requesting = purityResolveId(packageDir('api/session-controller'))
expect(requesting('@deepseek-ai/dsh-api-gateway/client')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-api-gateway/client')).toThrow(/purity/)
})
it('externalizes the baseline independently of each package manifest', () => {
const requesting = clientConfigs()[0]?.deps as { neverBundle: (specifier: string) => boolean }
const plain = clientConfigs(packageDir('client/connection'))[0]?.deps as {
neverBundle: (specifier: string) => boolean
}
for (const bundle of [requesting, plain]) {
for (const specifier of ['react', 'zod', '@tanstack/react-virtual', '@deepseek-ai/dsh-client-store']) {
expect(bundle.neverBundle(specifier)).toBe(true)
}
expect(bundle.neverBundle('zod/v3')).toBe(false)
}
})
})
describe('client bundle experimental input isolation', () => {
const experimental = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
const experimentalDir = packageDir('experimental/client-ui-agent-team')
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-inputs-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
const owner = join(root, 'client')
const entry = join(owner, 'lib/types/client/index.js')
const prototype = join(root, 'prototype/src/index.js')
for (const file of [entry, prototype]) mkdirSync(dirname(file), { recursive: true })
writeFileSync(join(owner, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
writeFileSync(join(root, 'prototype/package.json'), JSON.stringify({ name: experimental, type: 'module' }))
writeFileSync(prototype, 'export const marker = "experimental sentinel"\n')
return { root, owner, entry, prototype }
}
function config(kind: 'static' | 'dynamic', dir = REQUESTING_PACKAGE_DIR): UserConfig {
const configs = kind === 'static'
? staticLinked(dir, ['lib/types/client/index.js'])({ env: { DSH_BUILD_FACE: 'client' } })
: clientConfigs(dir)
const browser = configs.find(config => config.platform === 'browser')
if (browser === undefined) throw new Error('client config missing')
return browser
}
async function bundle(owner: string, config: UserConfig): Promise<string> {
let builds: TsdownBundle[] = []
try {
builds = await build({
...config, cwd: owner, config: false, tsconfig: false,
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
})
return builds.flatMap(build => build.chunks.filter(chunk => chunk.type === 'chunk').map(chunk => chunk.code)).join('\n')
} finally {
for (const build of builds) await build[Symbol.asyncDispose]()
}
}
function importPath(from: string, to: string): string {
return relative(dirname(from), to).replaceAll('\\', '/')
}
function checkCompilerModule(module: string, imports?: string[]): void {
const plugins = config('dynamic').plugins as InputIsolationPlugin[]
const plugin = plugins.find(plugin => plugin.name === 'dsh-client-input-isolation')
if (plugin === undefined) throw new Error('client input isolation plugin missing')
plugin.generateBundle.call({
getModuleInfo: () => imports === undefined ? null : { importedIds: imports, dynamicallyImportedIds: [] },
}, {}, {
'client.js': { type: 'chunk', modules: { [module]: {} }, imports: [], dynamicImports: [] },
})
}
it('accepts the exact compiler runtime helper without a source module record', () => {
expect(() => { checkCompilerModule('\0rolldown/runtime.js') }).not.toThrow()
})
it.each(['\0rolldown/other.js', '\0rolldown/runtime.js?user', '\0other/runtime.js'])(
'rejects the unrecorded module %j',
(module) => {
expect(() => { checkCompilerModule(module) }).toThrow(/has no bundler module record/)
},
)
it('checks runtime helper dependencies when the compiler supplies a module record', () => {
expect(() => { checkCompilerModule('\0rolldown/runtime.js', [experimental]) })
.toThrow(/client bundle isolation.*experimental/)
})
it.each(['static', 'dynamic'] as const)('rejects experimental files folded into a %s client artifact', async (kind) => {
const { owner, entry, prototype } = fixture()
writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
await expect(bundle(owner, config(kind))).rejects.toThrow(/client bundle isolation.*experimental/)
expect(existsSync(join(owner, 'lib/client.js'))).toBe(false)
expect(existsSync(join(owner, 'lib/index.js'))).toBe(false)
})
it.each(['static', 'dynamic'] as const)('keeps retained-name experimental %s inputs outside ordinary artifacts', async (kind) => {
const { root, owner, entry, prototype } = fixture()
const name = '@deepseek-ai/dsh-tool-terminal'
writeFileSync(join(root, 'prototype/package.json'), JSON.stringify({ name, type: 'module' }))
writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
await expect(bundle(owner, config(kind))).rejects.toThrow(/client bundle isolation.*experimental/)
await expect(bundle(owner, config(kind, packageDir('experimental/tool-terminal')))).resolves.toContain('experimental sentinel')
})
it('proves the static library otherwise hides the experimental input in its emitted JavaScript', async () => {
const { owner, entry, prototype } = fixture()
writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
const guarded = config('static')
const plugins = guarded.plugins as Array<{ name: string }>
const unguarded: UserConfig = {
...guarded,
plugins: plugins.filter(plugin => plugin.name !== 'dsh-client-input-isolation'),
outputOptions: { sourcemapExcludeSources: false },
}
await expect(bundle(owner, unguarded)).resolves.toContain('experimental sentinel')
await expect(bundle(owner, guarded)).rejects.toThrow(/client bundle isolation.*experimental/)
})
it.each(['static', 'dynamic'] as const)('allows an experimental %s artifact to keep its own inputs', async (kind) => {
const { owner, entry, prototype } = fixture()
writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
await expect(bundle(owner, config(kind, experimentalDir))).resolves.toContain('experimental sentinel')
})
it('rejects experimental ownership preserved only by an emitted compiler source map', async () => {
const { owner, entry, prototype } = fixture()
writeFileSync(entry, 'export const marker = "experimental sentinel"\n//# sourceMappingURL=index.js.map\n')
writeFileSync(`${entry}.map`, JSON.stringify({
version: 3, names: [], mappings: 'AAAA', sources: [importPath(entry, prototype)],
sourcesContent: ['export const marker = "experimental sentinel"\n'],
}))
await expect(bundle(owner, config('static'))).rejects.toThrow(/client bundle isolation.*experimental/)
})
it('checks the resolved module when an import alias conceals its experimental package', async () => {
const { owner, entry, prototype } = fixture()
writeFileSync(entry, 'export { marker } from "./ordinary.js"\n')
await expect(bundle(owner, { ...config('static'), alias: { './ordinary.js': prototype } }))
.rejects.toThrow(/client bundle isolation.*experimental/)
})
it('rejects an experimental runtime import left external by a static client library', async () => {
const { owner, entry } = fixture()
writeFileSync(entry, `export * from ${JSON.stringify(experimental)}\n`)
await expect(bundle(owner, config('static'))).rejects.toThrow(/client bundle isolation.*experimental/)
})
it.each([false, true])('checks omitted source-map files against their package owner (experimental: %s)', async (experimentalSource) => {
const { root, owner, entry } = fixture()
const dependency = join(root, 'dependency')
mkdirSync(dependency)
writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: experimentalSource ? experimental : 'ordinary-library' }))
writeFileSync(entry, 'export const marker = "mapped sentinel"\n//# sourceMappingURL=index.js.map\n')
writeFileSync(`${entry}.map`, JSON.stringify({
version: 3, names: [], mappings: 'AAAA', sourceRoot: importPath(entry, dependency),
sources: ['unshipped.ts'], sourcesContent: ['export const marker = "mapped sentinel"\n'],
}))
const result = bundle(owner, config('static'))
if (experimentalSource) await expect(result).rejects.toThrow(/client bundle isolation.*experimental/)
else await expect(result).resolves.toContain('mapped sentinel')
})
it.each(['.css', '.module.css', '.css?inline'])('rejects experimental %s inputs behind client CSS virtual loaders', async (extension) => {
const { owner, entry, prototype } = fixture()
const css = join(dirname(prototype), `style${extension.replace('?inline', '')}`)
writeFileSync(css, 'body { color: red; }\n')
const specifier = `${importPath(entry, css)}${extension.endsWith('?inline') ? '?inline' : ''}`
writeFileSync(entry, extension === '.css'
? `import ${JSON.stringify(specifier)}\nexport const marker = true\n`
: `export { default as style } from ${JSON.stringify(specifier)}\n`)
await expect(bundle(owner, config('dynamic'))).rejects.toThrow(/client bundle isolation.*experimental/)
})
it.each(['static', 'dynamic'] as const)('keeps ordinary %s bundles and source maps buildable', async (kind) => {
const { owner, entry } = fixture()
const source = join(owner, 'src/client/index.ts')
mkdirSync(dirname(source), { recursive: true })
writeFileSync(source, 'export const marker = "stable sentinel"\n')
writeFileSync(entry, 'export const marker = "stable sentinel"\n//# sourceMappingURL=index.js.map\n')
writeFileSync(`${entry}.map`, JSON.stringify({
version: 3, names: [], mappings: 'AAAA', sources: [importPath(entry, source)],
sourcesContent: ['export const marker = "stable sentinel"\n'],
}))
await expect(bundle(owner, config(kind))).resolves.toContain('stable sentinel')
})
})
describe('client bundle module requests', () => {
it('requests what the declaration lists', () => {
const requests = requestedExternals('@deepseek-ai/dsh-client-fixture', {
external: ['react', 'react/jsx-runtime', '@deepseek-ai/dsh-client-ui-slots'],
})
expect([...requests].sort()).toEqual([
'@deepseek-ai/dsh-client-ui-slots', 'react', 'react/jsx-runtime',
])
})
it('requests nothing when the declaration is absent', () => {
expect(requestedExternals('@deepseek-ai/dsh-client-fixture', {}).size).toBe(0)
})
it('rejects a malformed declaration instead of reading past it', () => {
expect(() => requestedExternals('@deepseek-ai/dsh-client-fixture', { external: 'react' }))
.toThrow(/dsh\.client\.external must be a string array/)
})
})
describe('client bundle debug artifacts', () => {
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
const configs = clientConfigs()
expect(configs[0]?.sourcemap).toBe(true)
expect(configs[0]?.outputOptions).toMatchObject({ sourcemapExcludeSources: false })
})
it('chains emitted tsc maps when the production Client build consumes lib/types', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-sourcemap-'))
try {
const entry = join(root, 'lib', 'types', 'client', 'index.js')
const source = join(root, 'src', 'client', 'index.ts')
const map = { version: 3, names: [], mappings: 'AAAA', sources: ['../../../src/client/index.ts'] }
mkdirSync(join(root, 'lib', 'types', 'client'), { recursive: true })
mkdirSync(join(root, 'src', 'client'), { recursive: true })
writeFileSync(entry, 'export const marker = true\n//# sourceMappingURL=index.js.map\n')
writeFileSync(`${entry}.map`, JSON.stringify(map))
writeFileSync(source, 'export const marker: true = true\n')
await expect(sourceMapPlugin().load!(entry)).resolves.toEqual({
code: 'export const marker = true',
map: { ...map, sourcesContent: ['export const marker: true = true\n'] },
})
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('maps first-party sources to their repository package paths', () => {
const configs = clientConfigs(packageDir('client/ui-goal'))
const outputOptions = configs[0]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
const source = transform('../src/client/GoalBar.tsx', clientSourceMapPath('client/ui-goal'))
expect(source).toBe('../../../packages/client/ui-goal/src/client/GoalBar.tsx')
const resolved = new URL(source, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-ui-goal/client.js.map')
expect(resolved.pathname).toBe('/packages/client/ui-goal/src/client/GoalBar.tsx')
})
it('maps dual-face host sources to the host package group', () => {
const configs = clientConfigs(packageDir('host/directory-picker-native'))
const outputOptions = configs[0]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
const source = transform('../src/client/index.ts', clientSourceMapPath('host/directory-picker-native'))
expect(source).toBe('../../../packages/host/directory-picker-native/src/client/index.ts')
})
it('maps inlined workspace sources to packages and leaves dependencies outside it unchanged', () => {
const configs = clientConfigs(packageDir('client/connection'))
const outputOptions = configs[0]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
const sourceMapPath = clientSourceMapPath('client/connection')
const workspaceSource = transform('../src/rpc.ts', sourceMapPath)
expect(workspaceSource).toBe('../../../packages/client/connection/src/rpc.ts')
const resolved = new URL(workspaceSource, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-connection/client.js.map')
expect(resolved.pathname).toBe('/packages/client/connection/src/rpc.ts')
const dependencySource = '../../../../node_modules/.pnpm/[email protected]/node_modules/zod/index.js'
expect(transform(dependencySource, sourceMapPath)).toBe(dependencySource)
})
})
describe('client bundle CSS Modules watch graph', () => {
it('registers the physical stylesheet read behind a virtual module', async () => {
const plugin = cssModulePlugin()
const importer = fileURLToPath(new URL(
'../packages/client/ui-conversation/src/client/queue/QueueDock.tsx',
import.meta.url,
))
const stylesheet = fileURLToPath(new URL(
'../packages/client/ui-conversation/src/client/queue/QueueDock.module.css',
import.meta.url,
))
const virtualId = plugin.resolveId?.('./QueueDock.module.css', importer)
if (virtualId === null || virtualId === undefined) throw new Error('CSS Modules import was not resolved')
const addWatchFile = vi.fn()
await plugin.load?.call({ addWatchFile }, virtualId)
expect(addWatchFile).toHaveBeenCalledExactlyOnceWith(stylesheet)
})
})