/** * The external packages a published browser artifact carries a copy of. * * Read from the real build configurations rather than declared by hand: each * `lib/client.js` plugin bundle is driven through its own `tsdown.config.ts`, and * the shell `dist` through `apps/web`'s Vite config. A recording plugin resolves * every bare specifier as external and notes it, so the pass walks our own source * and stops at the package boundary — which is both fast (about two seconds for * the whole repository) and exactly the direct-dependency granularity * THIRD_PARTY_NOTICES.md discloses. Erased type imports never appear, because the * transform drops them before resolution. * * Workspace names are followed only on the Vite side, where the shell's aliases * map them to source: that is how a browser-only library's own third-party * imports — katex and shiki through `ui-primitives`, for one — become visible. A * plugin bundle keeps them external, matching the frozen module table it is built * against; the wire layers it inlines are host packages that declare their own * dependencies, so nothing goes undisclosed. * * A specifier is recorded only once the host resolves it to a file inside a * package. A bundler's own virtual module has no package behind it — * `vite/modulepreload-polyfill` is generated by a Vite plugin rather than shipped * as a file, so the polyfill in the published `dist` is build glue in the same * category as an emitted TypeScript helper, not a redistributed copy of Vite. * * rolldown is resolved through tsdown deliberately: the dry run must use the * exact bundler the real build uses, which a separate root pin could drift from. */ import { globSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' /** The plugin-context member the recorder needs to resolve before recording. */ interface ResolveContext { resolve: ( source: string, importer: string, options: { skipSelf: boolean }, ) => Promise<{ id: string } | null> } /** A rolldown/Vite plugin shape, narrowed to what the recorder needs. */ interface RecorderPlugin { name: string enforce?: 'pre' resolveId: ( this: ResolveContext, source: string, importer: string | undefined, ) => Promise<{ id: string; external: true } | null> } /** * The package a resolved module file belongs to. * @param file - absolute path of a resolved module. * @returns the package name, or undefined when the file is not inside a package. */ function packageOfFile(file: string): string | undefined { const marker = file.lastIndexOf('node_modules/') if (marker < 0) return undefined const rest = file.slice(marker + 'node_modules/'.length) const parts = rest.split('/') return rest.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] } /** * Build the plugin that records bare specifiers and stops the walk at them. * @param seen - set the recorder adds package names to. * @param followWorkspace - leave `@deepseek-ai/*` to the host resolver instead of * externalizing it, so the walk continues into our own source. * @returns the recording plugin. */ function recorder(seen: Set, followWorkspace: boolean): RecorderPlugin { return { name: 'dsh-record-direct-externals', enforce: 'pre', async resolveId(source, importer) { if (importer === undefined) return null // the entry itself if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0')) return null if (source.startsWith('virtual:') || source.includes('?')) return null if (followWorkspace && source.startsWith('@deepseek-ai/')) return null if (source.startsWith('node:')) return { id: source, external: true } if (!source.startsWith('@deepseek-ai/')) { const resolved = await this.resolve(source, importer, { skipSelf: true }) const name = resolved === null ? undefined : packageOfFile(resolved.id) if (name !== undefined) seen.add(name) } return { id: source, external: true } }, } } interface Manifest { exports?: Record files?: string[] } /** Read one workspace manifest. */ function manifestOf(dir: string): Manifest { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as Manifest } /** Whether a manifest publishes a tsdown browser bundle at `lib/client.js`. */ function publishesClientBundle(manifest: Manifest): boolean { const target = manifest.exports?.['./client'] return typeof target === 'object' && target !== null && target.default === './lib/client.js' } /** * Record every external package the plugin client bundles carry. * @param root - repository root. * @param seen - set the recorder adds package names to. */ async function collectFromClientBundles(root: string, seen: Set): Promise { const requireFromTsdown = createRequire(createRequire(import.meta.url).resolve('tsdown')) const { rolldown } = await import(requireFromTsdown.resolve('rolldown')) as { rolldown: (options: Record) => Promise<{ generate: (output: Record) => Promise close: () => Promise }> } for (const relative of globSync('packages/*/*/tsdown.config.ts', { cwd: root }).sort()) { const dir = join(root, dirname(relative)) if (!publishesClientBundle(manifestOf(dir))) continue const loaded = await import(join(root, relative)) as { default: unknown } const factory = loaded.default const configs = (typeof factory === 'function' ? (factory as (inline: { env: Record }) => unknown[])({ env: {} }) : [factory]) as { name?: string; entry?: unknown; plugins?: unknown[] }[] // The `/client` config is the browser bundle; its siblings emit the node half. const client = configs.find(config => config.name?.endsWith('/client') === true) if (client === undefined) continue const bundle = await rolldown({ cwd: dir, input: client.entry, plugins: [recorder(seen, false), ...(client.plugins ?? [])], platform: 'browser', }) await bundle.generate({ format: 'cjs', minify: false, sourcemap: false }) await bundle.close() } } /** * Record every external package the prebuilt shell bundle carries. * @param root - repository root. * @param seen - set the recorder adds package names to. */ async function collectFromShellBundle(root: string, seen: Set): Promise { for (const relative of globSync('apps/*/vite.config.ts', { cwd: root }).sort()) { const dir = join(root, dirname(relative)) // Vite belongs to the app that builds with it, so it resolves from there. const { build } = await import(createRequire(join(dir, 'package.json')).resolve('vite')) as { build: (options: Record) => Promise } await build({ root: dir, logLevel: 'error', plugins: [recorder(seen, true)], build: { write: false, minify: false, sourcemap: false, reportCompressedSize: false }, }) } } /** * The external packages a published browser artifact carries a copy of. * @param root - repository root. * @returns package names, workspace names excluded. */ export async function browserBundledExternals(root: string): Promise> { const seen = new Set() await collectFromClientBundles(root, seen) await collectFromShellBundle(root, seen) return seen }