browser-bundled-externals.spec.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join, resolve } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts'
  6. const roots: string[] = []
  7. const repositoryRoot = resolve(import.meta.dirname, '..')
  8. afterEach(() => {
  9. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  10. })
  11. function fixture(): string {
  12. const root = mkdtempSync(join(tmpdir(), 'dsh-browser-notices-'))
  13. roots.push(root)
  14. write(root, 'package.json', '{"type":"module"}')
  15. write(root, 'tsconfig.base.json', JSON.stringify({
  16. compilerOptions: {
  17. module: 'esnext', target: 'es2022', jsx: 'react-jsx',
  18. paths: { '@fixture/static': ['./packages/client/static/src/index.ts'] },
  19. },
  20. }))
  21. write(root, 'tsconfig.base.client.json', '{"extends":"./tsconfig.base.json"}')
  22. return root
  23. }
  24. function write(root: string, path: string, text: string): void {
  25. const target = join(root, path)
  26. mkdirSync(dirname(target), { recursive: true })
  27. writeFileSync(target, text)
  28. }
  29. function library(root: string, name: string, source = 'export const value = 1'): void {
  30. write(root, `node_modules/${name}/package.json`, JSON.stringify({ name, type: 'module', exports: './index.js' }))
  31. write(root, `node_modules/${name}/index.js`, source)
  32. }
  33. function dynamicPlugin(root: string, source: string): void {
  34. write(root, 'packages/client/dynamic/package.json', JSON.stringify({
  35. name: '@fixture/dynamic', dsh: { client: { platform: 'web' } },
  36. }))
  37. write(root, 'packages/client/dynamic/tsdown.config.ts', [
  38. 'export default {',
  39. ' name: "@fixture/dynamic/client",',
  40. ' entry: { client: "src/client/index.ts" },',
  41. ' inputOptions: { resolve: { conditionNames: ["browser", "import", "default"] } },',
  42. '}',
  43. ].join('\n'))
  44. write(root, 'packages/client/dynamic/src/client/index.ts', source)
  45. }
  46. describe('browser dependency discovery', () => {
  47. it('records runtime imports through the client config without following upstream dependencies or erased types', async () => {
  48. const root = fixture()
  49. library(root, 'browser-lib', 'export { value } from "transitive-lib"')
  50. library(root, 'transitive-lib')
  51. dynamicPlugin(root, [
  52. 'import type { MissingType } from "type-only-lib"',
  53. 'import { value } from "browser-lib"',
  54. 'export const output: MissingType = value',
  55. ].join('\n'))
  56. expect(await browserBundledExternals(root)).toEqual(new Set(['browser-lib']))
  57. expect(existsSync(join(root, 'packages/client/dynamic/lib'))).toBe(false)
  58. })
  59. it('rejects an unresolved runtime library', async () => {
  60. const root = fixture()
  61. dynamicPlugin(root, 'export { value } from "missing-browser-lib"')
  62. await expect(browserBundledExternals(root)).rejects.toThrow('cannot resolve missing-browser-lib')
  63. })
  64. it('rejects a declared client without its browser config', async () => {
  65. const root = fixture()
  66. dynamicPlugin(root, 'export const value = 1')
  67. write(root, 'packages/client/dynamic/tsdown.config.ts', 'export default { name: "@fixture/dynamic", entry: "src/index.ts" }')
  68. await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config')
  69. })
  70. it('follows shell workspace aliases, CSS assets and lazy imports without writing output', async () => {
  71. const root = fixture()
  72. library(root, 'shell-lib')
  73. library(root, 'lazy-lib')
  74. library(root, 'asset-lib')
  75. write(root, 'node_modules/asset-lib/package.json', JSON.stringify({
  76. name: 'asset-lib', exports: { './theme.css': './theme.css' },
  77. }))
  78. write(root, 'node_modules/asset-lib/theme.css', '.fixture { color: red }')
  79. write(root, 'packages/client/static/package.json', '{"name":"@fixture/static"}')
  80. write(root, 'packages/client/static/src/index.ts', [
  81. 'import { value } from "shell-lib"',
  82. 'import "asset-lib/theme.css"',
  83. 'export const output = value',
  84. 'export const lazy = () => import("lazy-lib")',
  85. ].join('\n'))
  86. const app = join(root, 'apps/web')
  87. write(root, 'apps/web/package.json', '{"name":"@fixture/web","type":"module","exports":{"./dist/*":"./dist/*"}}')
  88. symlinkSync(resolve(repositoryRoot, 'apps/web/node_modules'), join(app, 'node_modules'), 'junction')
  89. write(root, 'apps/web/index.html', '<script type="module" src="./main.ts"></script>')
  90. write(root, 'apps/web/main.ts', 'import { output, lazy } from "@fixture/static"; console.log(output); void lazy()')
  91. write(root, 'apps/web/vite.config.ts', `export default {
  92. build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } }
  93. }`)
  94. write(root, 'apps/web/dist/sentinel.txt', 'untouched')
  95. expect(await browserBundledExternals(root)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib']))
  96. expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched')
  97. expect(existsSync(join(app, 'dist/index.html'))).toBe(false)
  98. expect(existsSync(join(root, 'packages/client/static/lib'))).toBe(false)
  99. })
  100. it('normalizes installed module ids and excludes virtual/workspace modules', () => {
  101. expect(browserPackageOfFile('/repo/node_modules/.pnpm/pkg@1/node_modules/pkg/a.js')).toBe('pkg')
  102. expect(browserPackageOfFile('C:\\repo\\node_modules\\@scope\\pkg\\a.css?url')).toBe('@scope/pkg')
  103. expect(browserPackageOfFile('/repo/packages/client/a.ts')).toBeUndefined()
  104. expect(browserPackageOfFile('\0vite/modulepreload-polyfill')).toBeUndefined()
  105. })
  106. it('maps exact names and subpaths from the source facade', () => {
  107. const root = fixture()
  108. const aliases = browserSourceAliases(root)
  109. expect('@fixture/static'.replace(aliases[0]!.find, aliases[0]!.replacement))
  110. .toBe(join(root, 'packages/client/static/src/index.ts'))
  111. expect(aliases[0]!.find.test('@fixture/static-extra')).toBe(false)
  112. })
  113. })