client-bundle-purity.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /**
  2. * Pins shared client-bundle preset rules: module-edge purity, source-map
  3. * chaining, and physical watch dependencies hidden behind virtual CSS Modules.
  4. */
  5. import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
  6. import { tmpdir } from 'node:os'
  7. import { dirname, join, relative } from 'node:path'
  8. import { fileURLToPath } from 'node:url'
  9. import { build, type TsdownBundle, type UserConfig } from 'tsdown'
  10. import { describe, expect, it, onTestFinished, vi } from 'vitest'
  11. import { clientBundle, requestedExternals, staticLinked } from '../packages/client/tsdown.client.ts'
  12. type ResolveId = (source: string) => null | { id: string; external: boolean }
  13. interface CssModulePlugin {
  14. name: string
  15. resolveId?: (source: string, importer: string | undefined) => null | string
  16. load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
  17. }
  18. interface SourceMapPlugin {
  19. name: string
  20. load?: (id: string) => Promise<unknown>
  21. }
  22. interface InputIsolationPlugin {
  23. name: string
  24. generateBundle: (this: {
  25. getModuleInfo(id: string): { importedIds: string[]; dynamicallyImportedIds: string[] } | null
  26. }, options: unknown, bundle: Record<string, {
  27. type: 'chunk'
  28. modules: Record<string, object>
  29. imports: string[]
  30. dynamicImports: string[]
  31. }>) => void
  32. }
  33. /** A representative dynamic bundle using the shared client baseline. */
  34. const REQUESTING_PACKAGE = '@deepseek-ai/dsh-client-ui-conversation'
  35. function clientConfigs(id = REQUESTING_PACKAGE) {
  36. return clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])(
  37. { env: { DSH_BUILD_FACE: 'client' } },
  38. ).filter(config => config.platform === 'browser')
  39. }
  40. describe('client bundle build faces', () => {
  41. it('watches source in development and consumes emitted JavaScript in the Client build', () => {
  42. const bundle = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js'])
  43. const development = bundle({ env: {} }).find(config => config.platform === 'browser')
  44. const artifact = bundle({ env: { DSH_BUILD_FACE: 'client' } })
  45. .find(config => config.platform === 'browser')
  46. expect(development?.entry).toEqual({ client: 'src/client/index.ts' })
  47. expect(artifact?.entry).toEqual({ client: 'lib/types/client/index.js' })
  48. })
  49. })
  50. describe('client bundle dynamic imports', () => {
  51. it('compiles import() to the module loader asynchronous operation', async () => {
  52. const root = mkdtempSync(join(tmpdir(), 'dsh-client-dynamic-import-'))
  53. onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
  54. const entry = join(root, 'lib/types/client/index.js')
  55. mkdirSync(dirname(entry), { recursive: true })
  56. writeFileSync(join(root, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
  57. writeFileSync(entry, 'export const load = () => import("./terminal.js")\n')
  58. writeFileSync(join(dirname(entry), 'terminal.js'), 'export const marker = "terminal"\n')
  59. const config = clientConfigs()[0]
  60. if (config === undefined) throw new Error('client config missing')
  61. let builds: TsdownBundle[] = []
  62. try {
  63. builds = await build({
  64. ...config, cwd: root, config: false, tsconfig: false,
  65. write: false, clean: false, exports: false, report: false, logLevel: 'silent',
  66. })
  67. const chunks = builds.flatMap(bundle => bundle.chunks).filter(chunk => chunk.type === 'chunk')
  68. const output = chunks.find(chunk => chunk.fileName === 'client.js')?.code
  69. expect(output).toContain('require.async("./client.terminal.js")')
  70. expect(output).not.toContain('Promise.resolve().then(() => require("./client.terminal.js"))')
  71. } finally {
  72. for (const bundle of builds) await bundle[Symbol.asyncDispose]()
  73. }
  74. })
  75. })
  76. function clientSourceMapPath(packagePath: string): string {
  77. return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
  78. }
  79. function purityResolveId(id = REQUESTING_PACKAGE): ResolveId {
  80. // libEntry is spelled at every call site (no default) so the
  81. // package-invariants text check can see the invariant entry per package.
  82. const configs = clientConfigs(id)
  83. const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
  84. const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
  85. if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
  86. return gate.resolveId as ResolveId
  87. }
  88. function cssModulePlugin(): CssModulePlugin {
  89. const configs = clientConfigs()
  90. const plugins = (configs[0] as { plugins: CssModulePlugin[] }).plugins
  91. const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
  92. if (plugin?.resolveId === undefined || plugin.load === undefined) {
  93. throw new Error('CSS Modules plugin missing from client config')
  94. }
  95. return plugin
  96. }
  97. function sourceMapPlugin(): SourceMapPlugin {
  98. const configs = clientConfigs()
  99. const plugins = (configs[0] as { plugins: SourceMapPlugin[] }).plugins
  100. const plugin = plugins.find(candidate => candidate.name === 'dsh-tsc-sourcemap')
  101. if (plugin?.load === undefined) throw new Error('tsc sourcemap plugin missing from client config')
  102. return plugin
  103. }
  104. describe('client bundle purity gate', () => {
  105. const resolveId = purityResolveId()
  106. it('leaves default externals and non-scoped specifiers alone', () => {
  107. expect(resolveId('@deepseek-ai/dsh-client-store')).toBeNull()
  108. expect(resolveId('@deepseek-ai/dsh-client-ui-slots')).toBeNull()
  109. expect(resolveId('@deepseek-ai/dsh-client-ui-primitives')).toBeNull()
  110. expect(resolveId('react')).toBeNull()
  111. expect(resolveId('zod')).toBeNull()
  112. })
  113. it('rejects the retired web-react platform package', () => {
  114. expect(() => resolveId('@deepseek-ai/dsh-client-web-react')).toThrow(/purity/)
  115. expect(() => resolveId('@deepseek-ai/dsh-client-web-react/store')).toThrow(/purity/)
  116. })
  117. it('lets inline-safe libraries inline', () => {
  118. expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull()
  119. expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
  120. expect(resolveId('@deepseek-ai/dsh-deque')).toBeNull()
  121. expect(resolveId('@deepseek-ai/dsh-util-values')).toBeNull()
  122. expect(resolveId('@deepseek-ai/dsh-token-meter/client')).toBeNull()
  123. expect(() => resolveId('@deepseek-ai/dsh-token-meter')).toThrow(/purity/)
  124. expect(() => resolveId('@deepseek-ai/dsh-token-meter/client/internal')).toThrow(/purity/)
  125. expect(resolveId('@deepseek-ai/dsh-host-open-in-app/shared')).toBeNull()
  126. expect(() => resolveId('@deepseek-ai/dsh-host-open-in-app')).toThrow(/purity/)
  127. })
  128. it('admits only the pure spill notice entry, not its Host policy', () => {
  129. expect(resolveId('@deepseek-ai/dsh-spill-policy/notice')).toBeNull()
  130. expect(resolveId('@deepseek-ai/dsh-output-retention')).toBeNull()
  131. expect(() => resolveId('@deepseek-ai/dsh-spill-policy')).toThrow(/purity/)
  132. expect(() => resolveId('@deepseek-ai/dsh-spill-policy/notice/internal')).toThrow(/purity/)
  133. })
  134. it('lets exact generated Remote contributions inline without admitting their package implementation', () => {
  135. expect(resolveId('@deepseek-ai/dsh-goal/remote')).toBeNull()
  136. expect(() => resolveId('@deepseek-ai/dsh-goal')).toThrow(/purity/)
  137. expect(() => resolveId('@deepseek-ai/dsh-goal/client')).toThrow(/purity/)
  138. expect(() => resolveId('@deepseek-ai/dsh-goal/remote/nested')).toThrow(/purity/)
  139. })
  140. it('throws on any other @deepseek-ai leak', () => {
  141. expect(() => resolveId('@deepseek-ai/dsh-agent')).toThrow(/purity/)
  142. expect(() => resolveId('@deepseek-ai/dsh-client-web')).toThrow(/purity/)
  143. })
  144. it('throws on cross-plugin value imports — bare plugin names and /client subpaths alike', () => {
  145. expect(() => resolveId('@deepseek-ai/dsh-client-connection')).toThrow(/purity/)
  146. expect(() => resolveId('@deepseek-ai/dsh-client-ui-session')).toThrow(/purity/)
  147. expect(() => resolveId('@deepseek-ai/dsh-client-ui-layout/client')).toThrow(/purity/)
  148. })
  149. it('admits package-specific requests only for the declaring bundle', () => {
  150. const requesting = purityResolveId('@deepseek-ai/dsh-api-session-controller')
  151. expect(requesting('@deepseek-ai/dsh-api-gateway/client')).toBeNull()
  152. expect(() => resolveId('@deepseek-ai/dsh-api-gateway/client')).toThrow(/purity/)
  153. })
  154. it('externalizes the baseline independently of each package manifest', () => {
  155. const requesting = clientConfigs()[0]?.deps as { neverBundle: (specifier: string) => boolean }
  156. const plain = clientConfigs('@deepseek-ai/dsh-client-connection')[0]?.deps as {
  157. neverBundle: (specifier: string) => boolean
  158. }
  159. expect(requesting.neverBundle('react')).toBe(true)
  160. expect(requesting.neverBundle('zod')).toBe(false)
  161. expect(plain.neverBundle('react')).toBe(true)
  162. expect(plain.neverBundle('@deepseek-ai/dsh-client-store')).toBe(true)
  163. })
  164. })
  165. describe('client bundle experimental input isolation', () => {
  166. const experimental = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
  167. function fixture() {
  168. const root = mkdtempSync(join(tmpdir(), 'dsh-client-inputs-'))
  169. onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
  170. const owner = join(root, 'client')
  171. const entry = join(owner, 'lib/types/client/index.js')
  172. const prototype = join(root, 'prototype/src/index.js')
  173. for (const file of [entry, prototype]) mkdirSync(dirname(file), { recursive: true })
  174. writeFileSync(join(owner, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
  175. writeFileSync(join(root, 'prototype/package.json'), JSON.stringify({ name: experimental, type: 'module' }))
  176. writeFileSync(prototype, 'export const marker = "experimental sentinel"\n')
  177. return { root, owner, entry, prototype }
  178. }
  179. function config(kind: 'static' | 'dynamic', id = REQUESTING_PACKAGE): UserConfig {
  180. const configs = kind === 'static'
  181. ? staticLinked(id, ['lib/types/client/index.js'])({ env: { DSH_BUILD_FACE: 'client' } })
  182. : clientConfigs(id)
  183. const browser = configs.find(config => config.platform === 'browser')
  184. if (browser === undefined) throw new Error('client config missing')
  185. return browser
  186. }
  187. async function bundle(owner: string, config: UserConfig): Promise<string> {
  188. let builds: TsdownBundle[] = []
  189. try {
  190. builds = await build({
  191. ...config, cwd: owner, config: false, tsconfig: false,
  192. write: false, clean: false, exports: false, report: false, logLevel: 'silent',
  193. })
  194. return builds.flatMap(build => build.chunks.filter(chunk => chunk.type === 'chunk').map(chunk => chunk.code)).join('\n')
  195. } finally {
  196. for (const build of builds) await build[Symbol.asyncDispose]()
  197. }
  198. }
  199. function importPath(from: string, to: string): string {
  200. return relative(dirname(from), to).replaceAll('\\', '/')
  201. }
  202. function checkCompilerModule(module: string, imports?: string[]): void {
  203. const plugins = config('dynamic').plugins as InputIsolationPlugin[]
  204. const plugin = plugins.find(plugin => plugin.name === 'dsh-client-input-isolation')
  205. if (plugin === undefined) throw new Error('client input isolation plugin missing')
  206. plugin.generateBundle.call({
  207. getModuleInfo: () => imports === undefined ? null : { importedIds: imports, dynamicallyImportedIds: [] },
  208. }, {}, {
  209. 'client.js': { type: 'chunk', modules: { [module]: {} }, imports: [], dynamicImports: [] },
  210. })
  211. }
  212. it('accepts the exact compiler runtime helper without a source module record', () => {
  213. expect(() => { checkCompilerModule('\0rolldown/runtime.js') }).not.toThrow()
  214. })
  215. it.each(['\0rolldown/other.js', '\0rolldown/runtime.js?user', '\0other/runtime.js'])(
  216. 'rejects the unrecorded module %j',
  217. (module) => {
  218. expect(() => { checkCompilerModule(module) }).toThrow(/has no bundler module record/)
  219. },
  220. )
  221. it('checks runtime helper dependencies when the compiler supplies a module record', () => {
  222. expect(() => { checkCompilerModule('\0rolldown/runtime.js', [experimental]) })
  223. .toThrow(/client bundle isolation.*experimental/)
  224. })
  225. it.each(['static', 'dynamic'] as const)('rejects experimental files folded into a %s client artifact', async (kind) => {
  226. const { owner, entry, prototype } = fixture()
  227. writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
  228. await expect(bundle(owner, config(kind))).rejects.toThrow(/client bundle isolation.*experimental/)
  229. expect(existsSync(join(owner, 'lib/client.js'))).toBe(false)
  230. expect(existsSync(join(owner, 'lib/index.js'))).toBe(false)
  231. })
  232. it('proves the static library otherwise hides the experimental input in its emitted JavaScript', async () => {
  233. const { owner, entry, prototype } = fixture()
  234. writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
  235. const guarded = config('static')
  236. const plugins = guarded.plugins as Array<{ name: string }>
  237. const unguarded: UserConfig = {
  238. ...guarded,
  239. plugins: plugins.filter(plugin => plugin.name !== 'dsh-client-input-isolation'),
  240. outputOptions: { sourcemapExcludeSources: false },
  241. }
  242. await expect(bundle(owner, unguarded)).resolves.toContain('experimental sentinel')
  243. await expect(bundle(owner, guarded)).rejects.toThrow(/client bundle isolation.*experimental/)
  244. })
  245. it.each(['static', 'dynamic'] as const)('allows an experimental %s artifact to keep its own inputs', async (kind) => {
  246. const { owner, entry, prototype } = fixture()
  247. writeFileSync(entry, `export { marker } from ${JSON.stringify(importPath(entry, prototype))}\n`)
  248. await expect(bundle(owner, config(kind, experimental))).resolves.toContain('experimental sentinel')
  249. })
  250. it('rejects experimental ownership preserved only by an emitted compiler source map', async () => {
  251. const { owner, entry, prototype } = fixture()
  252. writeFileSync(entry, 'export const marker = "experimental sentinel"\n//# sourceMappingURL=index.js.map\n')
  253. writeFileSync(`${entry}.map`, JSON.stringify({
  254. version: 3, names: [], mappings: 'AAAA', sources: [importPath(entry, prototype)],
  255. sourcesContent: ['export const marker = "experimental sentinel"\n'],
  256. }))
  257. await expect(bundle(owner, config('static'))).rejects.toThrow(/client bundle isolation.*experimental/)
  258. })
  259. it('checks the resolved module when an import alias conceals its experimental package', async () => {
  260. const { owner, entry, prototype } = fixture()
  261. writeFileSync(entry, 'export { marker } from "./ordinary.js"\n')
  262. await expect(bundle(owner, { ...config('static'), alias: { './ordinary.js': prototype } }))
  263. .rejects.toThrow(/client bundle isolation.*experimental/)
  264. })
  265. it('rejects an experimental runtime import left external by a static client library', async () => {
  266. const { owner, entry } = fixture()
  267. writeFileSync(entry, `export * from ${JSON.stringify(experimental)}\n`)
  268. await expect(bundle(owner, config('static'))).rejects.toThrow(/client bundle isolation.*experimental/)
  269. })
  270. it.each([false, true])('checks omitted source-map files against their package owner (experimental: %s)', async (experimentalSource) => {
  271. const { root, owner, entry } = fixture()
  272. const dependency = join(root, 'dependency')
  273. mkdirSync(dependency)
  274. writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: experimentalSource ? experimental : 'ordinary-library' }))
  275. writeFileSync(entry, 'export const marker = "mapped sentinel"\n//# sourceMappingURL=index.js.map\n')
  276. writeFileSync(`${entry}.map`, JSON.stringify({
  277. version: 3, names: [], mappings: 'AAAA', sourceRoot: importPath(entry, dependency),
  278. sources: ['unshipped.ts'], sourcesContent: ['export const marker = "mapped sentinel"\n'],
  279. }))
  280. const result = bundle(owner, config('static'))
  281. if (experimentalSource) await expect(result).rejects.toThrow(/client bundle isolation.*experimental/)
  282. else await expect(result).resolves.toContain('mapped sentinel')
  283. })
  284. it.each(['.css', '.module.css', '.css?inline'])('rejects experimental %s inputs behind client CSS virtual loaders', async (extension) => {
  285. const { owner, entry, prototype } = fixture()
  286. const css = join(dirname(prototype), `style${extension.replace('?inline', '')}`)
  287. writeFileSync(css, 'body { color: red; }\n')
  288. const specifier = `${importPath(entry, css)}${extension.endsWith('?inline') ? '?inline' : ''}`
  289. writeFileSync(entry, extension === '.css'
  290. ? `import ${JSON.stringify(specifier)}\nexport const marker = true\n`
  291. : `export { default as style } from ${JSON.stringify(specifier)}\n`)
  292. await expect(bundle(owner, config('dynamic'))).rejects.toThrow(/client bundle isolation.*experimental/)
  293. })
  294. it.each(['static', 'dynamic'] as const)('keeps ordinary %s bundles and source maps buildable', async (kind) => {
  295. const { owner, entry } = fixture()
  296. const source = join(owner, 'src/client/index.ts')
  297. mkdirSync(dirname(source), { recursive: true })
  298. writeFileSync(source, 'export const marker = "stable sentinel"\n')
  299. writeFileSync(entry, 'export const marker = "stable sentinel"\n//# sourceMappingURL=index.js.map\n')
  300. writeFileSync(`${entry}.map`, JSON.stringify({
  301. version: 3, names: [], mappings: 'AAAA', sources: [importPath(entry, source)],
  302. sourcesContent: ['export const marker = "stable sentinel"\n'],
  303. }))
  304. await expect(bundle(owner, config(kind))).resolves.toContain('stable sentinel')
  305. })
  306. })
  307. describe('client bundle module requests', () => {
  308. it('requests what the declaration lists', () => {
  309. const requests = requestedExternals('@deepseek-ai/dsh-client-fixture', {
  310. external: ['react', 'react/jsx-runtime', '@deepseek-ai/dsh-client-ui-slots'],
  311. })
  312. expect([...requests].sort()).toEqual([
  313. '@deepseek-ai/dsh-client-ui-slots', 'react', 'react/jsx-runtime',
  314. ])
  315. })
  316. it('requests nothing when the declaration is absent', () => {
  317. expect(requestedExternals('@deepseek-ai/dsh-client-fixture', {}).size).toBe(0)
  318. })
  319. it('rejects a malformed declaration instead of reading past it', () => {
  320. expect(() => requestedExternals('@deepseek-ai/dsh-client-fixture', { external: 'react' }))
  321. .toThrow(/dsh\.client\.external must be a string array/)
  322. })
  323. })
  324. describe('client bundle debug artifacts', () => {
  325. it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
  326. const configs = clientConfigs()
  327. expect(configs[0]?.sourcemap).toBe(true)
  328. expect(configs[0]?.outputOptions).toMatchObject({ sourcemapExcludeSources: false })
  329. })
  330. it('chains emitted tsc maps when the production Client build consumes lib/types', async () => {
  331. const root = mkdtempSync(join(tmpdir(), 'dsh-client-sourcemap-'))
  332. try {
  333. const entry = join(root, 'lib', 'types', 'client', 'index.js')
  334. const source = join(root, 'src', 'client', 'index.ts')
  335. const map = { version: 3, names: [], mappings: 'AAAA', sources: ['../../../src/client/index.ts'] }
  336. mkdirSync(join(root, 'lib', 'types', 'client'), { recursive: true })
  337. mkdirSync(join(root, 'src', 'client'), { recursive: true })
  338. writeFileSync(entry, 'export const marker = true\n//# sourceMappingURL=index.js.map\n')
  339. writeFileSync(`${entry}.map`, JSON.stringify(map))
  340. writeFileSync(source, 'export const marker: true = true\n')
  341. await expect(sourceMapPlugin().load!(entry)).resolves.toEqual({
  342. code: 'export const marker = true',
  343. map: { ...map, sourcesContent: ['export const marker: true = true\n'] },
  344. })
  345. } finally {
  346. rmSync(root, { recursive: true, force: true })
  347. }
  348. })
  349. it('maps first-party sources to their repository package paths', () => {
  350. const configs = clientConfigs('@deepseek-ai/dsh-client-ui-goal')
  351. const outputOptions = configs[0]?.outputOptions
  352. if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
  353. const transform = outputOptions.sourcemapPathTransform
  354. if (transform === undefined) throw new Error('client sourcemap path transform missing')
  355. const source = transform('../src/client/GoalBar.tsx', clientSourceMapPath('client/ui-goal'))
  356. expect(source).toBe('../../../packages/client/ui-goal/src/client/GoalBar.tsx')
  357. const resolved = new URL(source, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-ui-goal/client.js.map')
  358. expect(resolved.pathname).toBe('/packages/client/ui-goal/src/client/GoalBar.tsx')
  359. })
  360. it('maps dual-face host sources to the host package group', () => {
  361. const configs = clientConfigs('@deepseek-ai/dsh-host-directory-picker-native')
  362. const outputOptions = configs[0]?.outputOptions
  363. if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
  364. const transform = outputOptions.sourcemapPathTransform
  365. if (transform === undefined) throw new Error('client sourcemap path transform missing')
  366. const source = transform('../src/client/index.ts', clientSourceMapPath('host/directory-picker-native'))
  367. expect(source).toBe('../../../packages/host/directory-picker-native/src/client/index.ts')
  368. })
  369. it('maps inlined workspace sources to packages and leaves dependencies outside it unchanged', () => {
  370. const configs = clientConfigs('@deepseek-ai/dsh-client-connection')
  371. const outputOptions = configs[0]?.outputOptions
  372. if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
  373. const transform = outputOptions.sourcemapPathTransform
  374. if (transform === undefined) throw new Error('client sourcemap path transform missing')
  375. const sourceMapPath = clientSourceMapPath('client/connection')
  376. const workspaceSource = transform('../src/rpc.ts', sourceMapPath)
  377. expect(workspaceSource).toBe('../../../packages/client/connection/src/rpc.ts')
  378. const resolved = new URL(workspaceSource, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-connection/client.js.map')
  379. expect(resolved.pathname).toBe('/packages/client/connection/src/rpc.ts')
  380. const dependencySource = '../../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js'
  381. expect(transform(dependencySource, sourceMapPath)).toBe(dependencySource)
  382. })
  383. })
  384. describe('client bundle CSS Modules watch graph', () => {
  385. it('registers the physical stylesheet read behind a virtual module', async () => {
  386. const plugin = cssModulePlugin()
  387. const importer = fileURLToPath(new URL(
  388. '../packages/client/ui-conversation/src/client/queue/QueueDock.tsx',
  389. import.meta.url,
  390. ))
  391. const stylesheet = fileURLToPath(new URL(
  392. '../packages/client/ui-conversation/src/client/queue/QueueDock.module.css',
  393. import.meta.url,
  394. ))
  395. const virtualId = plugin.resolveId?.('./QueueDock.module.css', importer)
  396. if (virtualId === null || virtualId === undefined) throw new Error('CSS Modules import was not resolved')
  397. const addWatchFile = vi.fn()
  398. await plugin.load?.call({ addWatchFile }, virtualId)
  399. expect(addWatchFile).toHaveBeenCalledExactlyOnceWith(stylesheet)
  400. })
  401. })