client-bundle-css.spec.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /**
  2. * Stylesheets enter client bundles through virtual modules, so the loader must
  3. * register their physical files as watch dependencies.
  4. */
  5. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  6. import { tmpdir } from 'node:os'
  7. import { join } from 'node:path'
  8. import { describe, expect, it } from 'vitest'
  9. import { clientBundle } from '../packages/client/tsdown.client.ts'
  10. interface CssPlugin {
  11. name: string
  12. resolveId?: (source: string, importer?: string) => string | null
  13. load?: (this: { addWatchFile(id: string): void }, id: string) => Promise<string | null>
  14. }
  15. function cssPlugin(name: 'dsh-css-modules-inline' | 'dsh-css-global-inline' | 'dsh-css-text-inline'): CssPlugin {
  16. const configs = clientBundle(
  17. '@deepseek-ai/dsh-client-test',
  18. ['lib/types/index.js', 'lib/types/invariant.js'],
  19. )({ env: { DSH_BUILD_FACE: 'client' } })
  20. const client = configs.find(config => config.platform === 'browser')
  21. if (client === undefined) throw new Error('client config missing')
  22. const plugins = (client as { plugins: CssPlugin[] }).plugins
  23. const plugin = plugins.find(candidate => candidate.name === name)
  24. if (plugin === undefined) throw new Error(`${name} missing from client config`)
  25. return plugin
  26. }
  27. describe('client bundle CSS Modules', () => {
  28. it('registers the source stylesheet as a watch dependency', async () => {
  29. const root = await mkdtemp(join(tmpdir(), 'dsh-client-css-watch-'))
  30. try {
  31. const stylesheet = join(root, 'Fixture.module.css')
  32. const importer = join(root, 'index.ts')
  33. await writeFile(stylesheet, '.root { color: red; }\n')
  34. const plugin = cssPlugin('dsh-css-modules-inline')
  35. const virtualId = plugin.resolveId?.('./Fixture.module.css', importer)
  36. if (typeof virtualId !== 'string' || plugin.load === undefined) {
  37. throw new Error('CSS Modules plugin hooks are incomplete')
  38. }
  39. const watched: string[] = []
  40. const output = await plugin.load.call({ addWatchFile: id => watched.push(id) }, virtualId)
  41. expect(watched).toEqual([stylesheet])
  42. expect(output).toContain('data-plugin-css')
  43. } finally {
  44. await rm(root, { recursive: true, force: true })
  45. }
  46. })
  47. })
  48. describe('client bundle global CSS', () => {
  49. it('compiles a side-effect stylesheet into a watched style injector', async () => {
  50. const root = await mkdtemp(join(tmpdir(), 'dsh-client-global-css-watch-'))
  51. try {
  52. const stylesheet = join(root, 'base.css')
  53. const importer = join(root, 'index.ts')
  54. await writeFile(stylesheet, 'body { color: red; }\n')
  55. const plugin = cssPlugin('dsh-css-global-inline')
  56. const virtualId = plugin.resolveId?.('./base.css', importer)
  57. if (typeof virtualId !== 'string' || plugin.load === undefined) {
  58. throw new Error('global CSS plugin hooks are incomplete')
  59. }
  60. const watched: string[] = []
  61. const output = await plugin.load.call({ addWatchFile: id => watched.push(id) }, virtualId)
  62. expect(watched).toEqual([stylesheet])
  63. expect(output).toContain('data-plugin-css')
  64. expect(output).toContain('body{color:red}')
  65. } finally {
  66. await rm(root, { recursive: true, force: true })
  67. }
  68. })
  69. it('compiles inline stylesheets as watched text without a module side effect', async () => {
  70. const root = await mkdtemp(join(tmpdir(), 'dsh-client-inline-css-watch-'))
  71. try {
  72. const stylesheet = join(root, 'base.css')
  73. const importer = join(root, 'index.ts')
  74. await writeFile(stylesheet, 'body { color: red; }\n')
  75. const plugin = cssPlugin('dsh-css-text-inline')
  76. const virtualId = plugin.resolveId?.('./base.css?inline', importer)
  77. if (typeof virtualId !== 'string' || plugin.load === undefined) {
  78. throw new Error('inline CSS plugin hooks are incomplete')
  79. }
  80. const watched: string[] = []
  81. const output = await plugin.load.call({ addWatchFile: id => watched.push(id) }, virtualId)
  82. expect(watched).toEqual([stylesheet])
  83. expect(output).toContain('export default "body{color:red}"')
  84. expect(output).not.toContain('data-plugin-css')
  85. } finally {
  86. await rm(root, { recursive: true, force: true })
  87. }
  88. })
  89. })