dev-web.spec.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { expect, it } from 'vitest'
  5. import type { TsdownBundle } from 'tsdown'
  6. import { writeClientBuildRecord } from './client-build-environment.ts'
  7. import {
  8. devWebBuildEnvironment,
  9. discoverLibraryDirs,
  10. discoverPluginDirs,
  11. watchClientPlugins,
  12. } from './dev-web.ts'
  13. it('reuses the verified complete-build environment for every watcher stage', async () => {
  14. const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-environment-'))
  15. try {
  16. await mkdir(join(root, 'apps/web/dist'), { recursive: true })
  17. await mkdir(join(root, 'packages/client/example/lib'), { recursive: true })
  18. await writeFile(join(root, 'apps/web/dist/index.html'), '<main></main>')
  19. await writeFile(join(root, 'packages/client/example/lib/client.js'), 'module.exports = {}\n')
  20. writeClientBuildRecord(root, {
  21. DSH_CLIENT_COMMIT_HASH: 'abc1234',
  22. DSH_CLIENT_GIT_DIRTY: 'true',
  23. DSH_CLIENT_VERSION: '1.2.3',
  24. })
  25. expect(devWebBuildEnvironment(root, {
  26. PATH: '/bin',
  27. DSH_BUILD_CLIENT_PROFILE: 'official',
  28. DSH_CLIENT_COMMIT_HASH: 'stale',
  29. DSH_CLIENT_EXTRA: 'stale',
  30. })).toEqual({
  31. PATH: '/bin',
  32. DSH_CLIENT_COMMIT_HASH: 'abc1234',
  33. DSH_CLIENT_GIT_DIRTY: 'true',
  34. DSH_CLIENT_VERSION: '1.2.3',
  35. })
  36. } finally {
  37. await rm(root, { recursive: true, force: true })
  38. }
  39. })
  40. it('discovers dsh.client packages with sibling roles', async () => {
  41. const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-discovery-'))
  42. try {
  43. const current = join(root, 'packages', 'client', 'current')
  44. await mkdir(current, { recursive: true })
  45. await writeFile(join(current, 'package.json'), JSON.stringify({
  46. dsh: {
  47. bundle: { patch: './cordis.patch.yml' },
  48. client: { platform: 'web' },
  49. profile: { bundles: [] },
  50. },
  51. }))
  52. expect(discoverPluginDirs(root)).toEqual(['packages/client/current'])
  53. } finally {
  54. await rm(root, { recursive: true, force: true })
  55. }
  56. })
  57. it('discovers client-preset packages the shell links, excluding loader-delivered and test infrastructure', async () => {
  58. const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-library-'))
  59. try {
  60. const write = async (dir: string, manifest: unknown, config: string): Promise<void> => {
  61. await mkdir(join(root, dir), { recursive: true })
  62. await writeFile(join(root, dir, 'package.json'), JSON.stringify(manifest))
  63. await writeFile(join(root, dir, 'tsdown.config.ts'), config)
  64. }
  65. const clientPreset = "import { clientLibrary } from '../tsdown.client.ts'\nexport default clientLibrary('x', [])\n"
  66. // Linked by the compile shell: client preset, no loader-delivered half.
  67. await write('packages/client/linked', {}, clientPreset)
  68. // Loader-delivered: discoverPluginDirs owns it, so it must not appear twice.
  69. await write('packages/client/delivered', { dsh: { client: { platform: 'web' } } }, clientPreset)
  70. // Test infrastructure builds through the preset but never enters the shell graph.
  71. await write('packages/test-support/harness', {}, clientPreset)
  72. // Host package with its own config: not a client-face build at all.
  73. await write('packages/host/server', {}, "import { defineConfig } from 'tsdown'\nexport default defineConfig({})\n")
  74. expect(discoverLibraryDirs(root)).toEqual(['packages/client/linked'])
  75. } finally {
  76. await rm(root, { recursive: true, force: true })
  77. }
  78. })
  79. it('rebuilds a client-plugin bundle after its source changes', async () => {
  80. const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-watch-'))
  81. let bundles: TsdownBundle[] = []
  82. try {
  83. await symlink(join(import.meta.dirname, '..', 'node_modules'), join(root, 'node_modules'), 'dir')
  84. await writeFile(join(root, 'package.json'), JSON.stringify({ name: '@dsh-test/dev-web-watch', private: true, type: 'module' }))
  85. await writeFile(join(root, 'tsdown.config.ts'), `
  86. import { defineConfig } from 'tsdown'
  87. export default defineConfig({
  88. entry: { client: 'src.ts' }, outDir: 'lib', format: 'cjs', platform: 'browser', dts: false, clean: false,
  89. outputOptions: { entryFileNames: 'client.js' },
  90. })
  91. `)
  92. const sourcePath = join(root, 'src.ts')
  93. const bundlePath = join(root, 'lib/client.js')
  94. await writeFile(sourcePath, 'export const version = "watch-v1"\n')
  95. bundles = await watchClientPlugins(root, ['.'], 50)
  96. expect(await readFile(bundlePath, 'utf8')).toContain('watch-v1')
  97. await new Promise(resolve => setTimeout(resolve, 1_000))
  98. await writeFile(sourcePath, `export const version = "watch-v2-${'x'.repeat(100)}"\n`)
  99. await expect.poll(async () => (await readFile(bundlePath, 'utf8')).includes('watch-v2-'), {
  100. timeout: 10_000,
  101. }).toBe(true)
  102. } finally {
  103. for (const bundle of bundles) await bundle[Symbol.asyncDispose]()
  104. await rm(root, { recursive: true, force: true })
  105. }
  106. }, 20_000)