/** * 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. * * The pass runs on a clean tree, as a static gate must. The shell's Vite config * aliases a few workspace packages to source; every other workspace name would * resolve through `node_modules` to a `lib/` entry the real build has emitted but * a clean checkout has not, so this module resolves those names to their own * source instead. `lib/` is compiled from `src/`, so the third-party edges the * pass records are the same either way. * * 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 { existsSync, 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] } /** * Source aliases for the workspace packages the shell does not already alias. * * A clean checkout has no `lib/`, so a workspace name would otherwise resolve * through `node_modules` to an entry that does not exist yet. Aliases are the * right seam rather than a plugin hook, because Vite resolves a stylesheet * `@import` through them too — the theme package publishes its stylesheets from * `lib/styles/`. `lib/` is compiled from `src/`, so the third-party edges the * pass records are the same either way. * @param root - repository root. * @param existing - the shell's own alias patterns, whose entry choices win. * @returns alias entries mapping each remaining workspace name to its source. */ function workspaceSourceAliases(root: string, existing: readonly string[]): { find: RegExp | string; replacement: string }[] { const aliases: { find: RegExp | string; replacement: string }[] = [] for (const pattern of ['packages/*/*/package.json', 'vendor/*/package.json']) { for (const relative of globSync(pattern, { cwd: root })) { const dir = join(root, dirname(relative)) const manifest = JSON.parse(readFileSync(join(root, relative), 'utf8')) as Manifest & { name?: string } const name = manifest.name if (name === undefined || !existsSync(join(dir, 'src'))) continue if (existing.some(find => find.includes(name))) continue const root_ = manifest.exports?.['.'] const target = typeof root_ === 'string' ? root_ : root_?.default const stem = (target ?? './lib/index.js') .replace(/^\.\/lib\/types\//, '').replace(/^\.\/lib\//, '').replace(/\.js$/, '') const entry = [`${stem}.ts`, `${stem}.tsx`, `${stem}/index.ts`, `${stem}/index.tsx`] .map(candidate => join(dir, 'src', candidate)) .find(candidate => existsSync(candidate)) // The subpath prefix carries `./client`, `./types`, and `./styles/*` alike: // each published subpath mirrors a path under `src/`. aliases.push({ find: `${name}/`, replacement: `${join(dir, 'src')}/` }) if (entry !== undefined) aliases.push({ find: new RegExp(`^${name.replaceAll('/', '\\/')}$`), replacement: entry }) } } return aliases } /** * Build the plugin that records bare specifiers and stops the walk at them. * @param seen - set the recorder adds package names to. * @returns the recording plugin. */ function recorder(seen: Set): 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 // A workspace name that reaches here is one no alias mapped to source, so // nothing of ours is left to walk; it is never a third-party disclosure. if (source.startsWith('@deepseek-ai/')) return { id: source, external: true } 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), ...(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, resolveConfig } = await import(createRequire(join(dir, 'package.json')).resolve('vite')) as { build: (options: Record) => Promise resolveConfig: (options: Record, command: string) => Promise<{ resolve: { alias: { find: string | RegExp }[] } }> } // The shell already aliases some workspace names to source, and its entry // choices win: a stylesheet `@import` resolves through aliases rather than a // plugin hook, so only the names it leaves out get one from here. const resolved = await resolveConfig({ root: dir, logLevel: 'error' }, 'build') await build({ root: dir, logLevel: 'error', plugins: [recorder(seen)], resolve: { alias: workspaceSourceAliases(root, resolved.resolve.alias.map(entry => String(entry.find))) }, 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 }