tsdown.config.ts
Rama main · réplica de lectura del repositorio original.
/**
* dsh-web-tools — tsdown build for the browser client bundle.
*
* DSH's web shell loads third-party client plugins as SINGLE script bundles
* served from /plugins/<package-name>/client.js and registered through the
* shared module loader (`window.__ModuleLoader__.load({ id, factory })`).
* This config replicates the official DSH client-bundle preset
* (packages/client/tsdown.client.ts in deepseek-harness, same shape as
* dsh-better-sidebar):
*
* - the host half stays on plain `tsc` (see tsconfig.build.json) — Node ESM;
* - the client half is bundled here as one CJS-closure script whose `require`
* resolves only the platform module table (react, cordis, the injected
* @deepseek-ai client services); everything else is inlined;
* - the purity gate rejects any @deepseek-ai value import that is not a
* platform module or an inline-safe wire layer: cross-plugin collaboration
* goes through cordis services, never value imports;
* - the bundle registers itself via window.__ModuleLoader__.load with the
* package-name id (`dsh-web-tools`; client-modules compose keys on the
* package name — keep it in sync with package.json `name`).
*
* Types ship from `tsc -p tsconfig.build.json` (lib/*.d.ts), not from tsdown.
*/
import { builtinModules } from 'node:module'
import type { UserConfig } from 'tsdown'
/** Node builtins must never survive into the browser module-loader factory. */
const NODE_BUILTINS = new Set([
...builtinModules,
...builtinModules.map(id => `node:${id}`),
])
/**
* Module specifiers the web shell seeds into its module table. This is the
* ACTUAL seed key set, taken from the served shell bundle's
* `staticModules` (`dsh-web-frontend/dist/assets/index-*.js`), which the loader
* installs verbatim as `new Map(Object.entries(options.staticModules))`.
*
* The previous list was inaccurate in both directions: it allowed four
* specifiers that are not table keys (`cordis` — the key is
* `@deepseek-ai/cordis` — plus dsh-client-web-react, dsh-client-schema-form and
* dsh-client-runtime/client, three packages that are not even published), and
* it omitted two real keys (dsh-client-store, dsh-client-ui-dockkit). An
* allowed-but-absent specifier compiles into a bare `require(...)` that throws
* the loader's "missed the module table" error at load, while an omitted-but-real
* key is rejected here at build time — so the list is only useful when it matches.
*/
const CLIENT_EXTERNALS = [
'react',
'react/jsx-runtime',
'react-dom',
'react-dom/client',
'@deepseek-ai/cordis',
'@deepseek-ai/dsh-client-store',
'@deepseek-ai/dsh-client-ui-slots',
'@deepseek-ai/dsh-client-ui-primitives',
'@deepseek-ai/dsh-client-ui-dockkit',
]
/** Wire/type layers a client bundle may inline (mirror of the official INLINE_SAFE list). */
const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/
/** A rolldown plugin as tsdown's config accepts it (contextual `this` for load/resolveId). */
type BuildPlugin = NonNullable<UserConfig['plugins']>
/** The official client-bundle purity gate: only platform modules + inline-safe wire layers may be imported from @deepseek-ai/*. */
function purityGatePlugin(): BuildPlugin {
return {
name: 'dsh-client-bundle-purity',
resolveId(source: string) {
if (NODE_BUILTINS.has(source)) {
throw new Error(
`client bundle purity: Node builtin "${source}" cannot run in the browser module table — `
+ 'select the dependency browser export or add an explicit browser implementation',
)
}
if (!source.startsWith('@deepseek-ai/')) return null
if (CLIENT_EXTERNALS.includes(source)) return null // platform module: external wins
if (INLINE_SAFE.test(source)) return null // wire/type layer: inline is the point
throw new Error(
`client bundle purity: "${source}" is not a platform module (CLIENT_EXTERNALS) and not an inline-safe wire layer — `
+ 'cross-plugin value imports are forbidden; collaborate through cordis services (type-only imports are erased and never reach this gate)',
)
},
}
}
export default [
// Official profile channel: bundle id = package name (package.json `name`).
{
entry: { client: 'src/client/index.ts' },
outDir: 'lib',
format: 'cjs',
platform: 'browser',
dts: false,
sourcemap: false,
// clean stays off: the build script removes lib/ wholesale before tsc, so
// a tsdown clean here would wipe the lib types tsc just emitted.
clean: false,
// Platform module-table entries stay external (resolved by the loader's
// `require` at runtime); every other dependency is inlined into the bundle.
deps: {
neverBundle: [...CLIENT_EXTERNALS],
alwaysBundle: (id: string) => (CLIENT_EXTERNALS.includes(id) ? false : true),
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
'import.meta.resolve': 'undefined',
},
// CJS output otherwise makes some transitive packages resolve their Node
// entry even though this bundle runs in the browser. Keep browser
// conditional exports authoritative for both source import() and
// generated require() edges.
inputOptions: {
resolve: {
conditionNames: ['browser', 'import', 'require', 'default'],
},
},
plugins: [purityGatePlugin()],
outputOptions: {
entryFileNames: 'client.js',
banner: `window.__ModuleLoader__.load({ id: "dsh-web-tools", factory: (require) => {`,
footer: `return module.exports; } });`,
intro: `var module = { exports: {} }; var exports = module.exports;`,
// The CJS wrapper factory's `require` only resolves module-table entries
// (react, cordis, ...); it cannot load relative chunk URLs in the
// browser. Disable code splitting so the artifact is one script.
codeSplitting: false,
},
},
] satisfies UserConfig[]