Files
deepseek-harness/scripts/client-bundle-purity.spec.ts
T
Turtle 38c7a9fbf4 feat(release): publish package source, declaration maps, and documentation (#5884)
* feat(release): publish package source, declaration maps, and documentation

Every release member publishes its TypeScript source and the declaration
maps that resolve into it, and @deepseek-ai/dsh publishes the repository
Markdown documentation staged from docs/ at prepack time.

The ./src/* export most members already declared now resolves for an
installed consumer, and a consumer editor follows a declaration to the
implementation. Bundled JavaScript maps stay unpublished; the vendored
family and the Desktop runtime file policy are unchanged.

Rationale: .agents/notes/implemented/process/2026-10-08-published-source-and-docs.md

* feat(release): publish JavaScript maps and compress bundles

Every emitted JavaScript map ships beside the bundle it describes, and the
workspace build and the client preset compress bundles without mangling
identifiers.

The documentation payload now mirrors the website publication manifest and
is removed again by `postpack`; Desktop and the Python executable exclude
the published source, documentation, and maps; `clean` removes a staged
copy. Typert's publication contract gains the Remote declaration map.

* fix(release): keep node bundles readable for the worker image

The browser worker image lowers `yield*` only as a whole statement and
rewrites `for await` loops by their source layout, so compressed node-side
entries either fail that lowering or lower to unparseable code. Only the
browser bundles keep compression, which removes about a third of their text.

The bundle-purity assertion now matches the module-loader call regardless of
the quote style compression picks, and the release-pack fixture exercises
the installed package's new documentation requirement.

* feat(release): publish the whole documentation tree

`@deepseek-ai/dsh` copies `docs/` in full instead of the website's page
set: the maintainer documents, the generated migration records, the
translation guidelines, and the `*.i18n.yaml` consistency records ship
with the installed package, so the documentation's own links resolve
without a checkout.

* feat(release): publish the upgrade guides with the documentation

The staged documentation keeps the website's page set and adds every
`docs/upgrade-guide/` document, which is what an installed reader consults
when a released change breaks something. The rest of the maintainer tree
stays out.

* test(release): expect source and maps in master's new payload cases

* feat(release): keep installed docs and source readable in Desktop

Desktop keeps the published src/ and docs/ trees inside app.asar, and
every model shell call receives DSH_INSTALL_DIR and DSH_NODE_EXECUTABLE
so an agent can read them through Electron's Node mode. Staging also
generates docs/upgrade-guide/README.md, an index of the upgrade guides
grouped by the release they upgrade from.

Minified client bundles print string literals with backticks: the
worker image packer now recognizes those chunk requests, client license
banners are prepended after minification so their text survives, and
the affected artifact tests accept either quote.

* test(desktop): read archived docs through the Windows PowerShell form

Regenerate the config catalog for the shifted shell-env source line.

* test(desktop): wait for the GUI-subsystem Electron process from PowerShell

* build(release): publish source and maps from the message-quotes package

* build(release): publish source and maps from packages new on master

* build(release): publish source and maps from the tool-office package
2026-11-05 00:22:36 +08:00

655 lines
33 KiB
TypeScript

/**
* Pins shared client-bundle preset rules: build modes, 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 { tmpdir } from 'node:os'
import { dirname, join, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
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' })
})
})
async function clientChunkOutput(root: string, config: UserConfig, fileName: string): Promise<string> {
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 output = chunks.find(chunk => chunk.fileName === fileName)?.code
if (output === undefined) throw new Error(`client output missing: ${fileName}`)
return output
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
}
describe('statically linked client build modes', () => {
it.each([
{ label: 'defaults to production', mode: undefined, expectedMode: 'production', frozen: false },
{ label: 'retains explicit development', mode: 'development', expectedMode: 'development', frozen: true },
])('$label when compiling conditional branches', async ({ mode, expectedMode, frozen }) => {
const root = mkdtempSync(join(tmpdir(), 'dsh-static-client-mode-'))
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 snapshot = { value: 1 }',
"if (process.env.NODE_ENV !== 'production') Object.freeze(snapshot)",
'export const mode = process.env.NODE_ENV',
'',
].join('\n'))
const previousMode = process.env.NODE_ENV
let config: UserConfig | undefined
try {
if (mode === undefined) Reflect.deleteProperty(process.env, 'NODE_ENV')
else process.env.NODE_ENV = mode
config = staticLinked(root, ['lib/types/client/index.js'])({ env: { DSH_BUILD_FACE: 'client' } })
.find(candidate => candidate.platform === 'browser')
} finally {
if (previousMode === undefined) Reflect.deleteProperty(process.env, 'NODE_ENV')
else process.env.NODE_ENV = previousMode
}
if (config === undefined) throw new Error('static client config missing')
const output = await clientChunkOutput(root, config, 'index.js')
const emitted = await import(`data:text/javascript,${encodeURIComponent(output)}`) as {
mode: string
snapshot: { value: number }
}
expect(emitted.mode).toBe(expectedMode)
expect(emitted.snapshot).toEqual({ value: 1 })
expect(Object.isFrozen(emitted.snapshot)).toBe(frozen)
})
})
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
}
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')
const output = await clientChunkOutput(root, config, 'client.js')
// The compressor rewrites string literals, so the quotes are not part of the contract.
expect(output).toMatch(/require\.async\((["'`])\.\/client\.terminal\.js\1\)/)
expect(output).not.toMatch(/Promise\.resolve\(\)\.then\(\(\)\s*=>\s*require\((["'`])\.\/client\.terminal\.js\1\)\)/)
})
})
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-time')).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('omits unused RRULE code from a Cron-only browser import', async () => {
const builds = await build({
config: false, platform: 'browser', format: ['esm'], entry: ['cron-only'],
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
plugins: [{
name: 'cron-only',
resolveId: id => id === 'cron-only' ? id : null,
load: id => id === 'cron-only'
? `export { Cron } from ${JSON.stringify(join(packageDir('util/time'), 'src/index.ts'))}` : null,
}],
})
try {
const modules = builds.flatMap(bundle => bundle.chunks).flatMap(chunk => chunk.type === 'chunk' ? chunk.moduleIds : [])
.map(id => id.replaceAll('\\', '/'))
expect(modules.some(id => id.endsWith('/time/src/cron.ts'))).toBe(true)
expect(modules.some(id => id.includes('/rrule'))).toBe(false)
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
})
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.each(['file-drag'])('inlines the stateless %s entry without admitting its subpaths or UI plugin', (name) => {
expect(resolveId(`@deepseek-ai/dsh-util-${name}`)).toBeNull()
expect(() => resolveId(`@deepseek-ai/dsh-util-${name}/internal`)).toThrow(/purity/)
expect(() => resolveId(`@deepseek-ai/dsh-client-ui-${name}`)).toThrow(/purity/)
const deps = clientConfigs()[0]?.deps as { neverBundle: (specifier: string) => boolean }
expect(deps.neverBundle(`@deepseek-ai/dsh-util-${name}`)).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-voice-input'
const experimentalDir = packageDir('experimental/client-ui-voice-input')
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)
})
})