Files
deepseek-harness/scripts/browser-bundled-externals.spec.ts
T

128 lines
5.9 KiB
TypeScript

import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts'
const roots: string[] = []
const repositoryRoot = resolve(import.meta.dirname, '..')
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixture(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-browser-notices-'))
roots.push(root)
write(root, 'package.json', '{"type":"module"}')
write(root, 'tsconfig.base.json', JSON.stringify({
compilerOptions: {
module: 'esnext', target: 'es2022', jsx: 'react-jsx',
paths: { '@fixture/static': ['./packages/client/static/src/index.ts'] },
},
}))
write(root, 'tsconfig.base.client.json', '{"extends":"./tsconfig.base.json"}')
return root
}
function write(root: string, path: string, text: string): void {
const target = join(root, path)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, text)
}
function library(root: string, name: string, source = 'export const value = 1'): void {
write(root, `node_modules/${name}/package.json`, JSON.stringify({ name, type: 'module', exports: './index.js' }))
write(root, `node_modules/${name}/index.js`, source)
}
function dynamicPlugin(root: string, source: string): void {
write(root, 'packages/client/dynamic/package.json', JSON.stringify({
name: '@fixture/dynamic', dsh: { client: { platform: 'web' } },
}))
write(root, 'packages/client/dynamic/tsdown.config.ts', [
'export default {',
' name: "@fixture/dynamic/client",',
' entry: { client: "src/client/index.ts" },',
' inputOptions: { resolve: { conditionNames: ["browser", "import", "default"] } },',
'}',
].join('\n'))
write(root, 'packages/client/dynamic/src/client/index.ts', source)
}
describe('browser dependency discovery', () => {
it('records runtime imports through the client config without following upstream dependencies or erased types', async () => {
const root = fixture()
library(root, 'browser-lib', 'export { value } from "transitive-lib"')
library(root, 'transitive-lib')
dynamicPlugin(root, [
'import type { MissingType } from "type-only-lib"',
'import { value } from "browser-lib"',
'export const output: MissingType = value',
].join('\n'))
expect(await browserBundledExternals(root)).toEqual(new Set(['browser-lib']))
expect(existsSync(join(root, 'packages/client/dynamic/lib'))).toBe(false)
})
it('rejects an unresolved runtime library', async () => {
const root = fixture()
dynamicPlugin(root, 'export { value } from "missing-browser-lib"')
await expect(browserBundledExternals(root)).rejects.toThrow('cannot resolve missing-browser-lib')
})
it('rejects a declared client without its browser config', async () => {
const root = fixture()
dynamicPlugin(root, 'export const value = 1')
write(root, 'packages/client/dynamic/tsdown.config.ts', 'export default { name: "@fixture/dynamic", entry: "src/index.ts" }')
await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config')
})
it('follows shell workspace aliases, CSS assets and lazy imports without writing output', async () => {
const root = fixture()
library(root, 'shell-lib')
library(root, 'lazy-lib')
library(root, 'asset-lib')
write(root, 'node_modules/asset-lib/package.json', JSON.stringify({
name: 'asset-lib', exports: { './theme.css': './theme.css' },
}))
write(root, 'node_modules/asset-lib/theme.css', '.fixture { color: red }')
write(root, 'packages/client/static/package.json', '{"name":"@fixture/static"}')
write(root, 'packages/client/static/src/index.ts', [
'import { value } from "shell-lib"',
'import "asset-lib/theme.css"',
'export const output = value',
'export const lazy = () => import("lazy-lib")',
].join('\n'))
const app = join(root, 'apps/web')
write(root, 'apps/web/package.json', '{"name":"@fixture/web","type":"module","exports":{"./dist/*":"./dist/*"}}')
symlinkSync(resolve(repositoryRoot, 'apps/web/node_modules'), join(app, 'node_modules'), 'junction')
write(root, 'apps/web/index.html', '<script type="module" src="./main.ts"></script>')
write(root, 'apps/web/main.ts', 'import { output, lazy } from "@fixture/static"; console.log(output); void lazy()')
write(root, 'apps/web/vite.config.ts', `export default {
build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } }
}`)
write(root, 'apps/web/dist/sentinel.txt', 'untouched')
expect(await browserBundledExternals(root)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib']))
expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched')
expect(existsSync(join(app, 'dist/index.html'))).toBe(false)
expect(existsSync(join(root, 'packages/client/static/lib'))).toBe(false)
})
it('normalizes installed module ids and excludes virtual/workspace modules', () => {
expect(browserPackageOfFile('/repo/node_modules/.pnpm/pkg@1/node_modules/pkg/a.js')).toBe('pkg')
expect(browserPackageOfFile('C:\\repo\\node_modules\\@scope\\pkg\\a.css?url')).toBe('@scope/pkg')
expect(browserPackageOfFile('/repo/packages/client/a.ts')).toBeUndefined()
expect(browserPackageOfFile('\0vite/modulepreload-polyfill')).toBeUndefined()
})
it('maps exact names and subpaths from the source facade', () => {
const root = fixture()
const aliases = browserSourceAliases(root)
expect('@fixture/static'.replace(aliases[0]!.find, aliases[0]!.replacement))
.toBe(join(root, 'packages/client/static/src/index.ts'))
expect(aliases[0]!.find.test('@fixture/static-extra')).toBe(false)
})
})