build: migrate tsup to tsdown, mirroring the official client-bundle preset
- banner/intro/footer three-part closure-factory wrap (verbatim from packages/client/tsdown.client.ts), intro now owns var module/exports - deps.neverBundle/alwaysBundle guards: host keeps production deps external, client keeps only module-table specifiers external - bundle purity gate: any non-requested @deepseek-ai/* value import is a build error (INLINE_SAFE / vendored-library / generated-remote allowances mirrored) - NODE_ENV + import.meta.env defines for inlined node-idiom deps - client sourcemap on, matching the official artifact contract - client smoke test now runs the bundle verbatim through the __ModuleLoader__ handoff instead of slicing the factory body by exact output strings (decoupled from bundler layout)
This commit is contained in:
1 parent
7df335114a
commit
021db5f2a2
8 files changed
+636
-814
No files matched your search
+3
-4
@@ -25,8 +25,8 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"watch": "tsup --watch",
|
||||
"build": "tsdown",
|
||||
"watch": "tsdown --watch",
|
||||
"register": "bash scripts/register.sh",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "npm run typecheck && node tests/host.test.mjs && node tests/client.test.mjs",
|
||||
@@ -59,7 +59,6 @@
|
||||
"ui"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
||||
@@ -88,7 +87,7 @@
|
||||
"playwright-core": "^1.62.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tsup": "^8.5.1",
|
||||
"tsdown": "^0.22.14",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
|
||||
Generated
+500
-720
File diff suppressed because it is too large.
Load diff
@@ -4,8 +4,8 @@
|
||||
* entire key/value cell is one hit target. Row 1 (Plugin name + version) →
|
||||
* the repo's releases page, row 2 (the short owner/repo label) → the repo
|
||||
* root; hovering a row underlines its value. Metadata is baked into the
|
||||
* bundle from package.json via tsup `define` (see src/client/meta.ts +
|
||||
* tsup.config.ts). One live check rides on top: the card asks the npm
|
||||
* bundle from package.json via tsdown `define` (see src/client/meta.ts +
|
||||
* tsdown.config.ts). One live check rides on top: the card asks the npm
|
||||
* registry for the latest published version (cached with a 1-hour TTL, see
|
||||
* ../latestVersion.ts) and shows an `↑ vX.Y.Z` chip when it is newer.
|
||||
*
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
* standard prop on every session-scope slot component). No polling, no RPC,
|
||||
* no client-side cache.
|
||||
*
|
||||
* This module is the body of the package's `./client` bundle: tsup
|
||||
* (tsup.config.ts) bundles it (external `react` — the browser module table
|
||||
* This module is the body of the package's `./client` bundle: tsdown
|
||||
* (tsdown.config.ts) bundles it (external `react` — the browser module table
|
||||
* supplies it via the injected `require`) into the web boot handoff
|
||||
* (`window.__ModuleLoader__.load({id, factory})`). All imports from other
|
||||
* client modules are inlined by the bundler; everything here is zero-runtime
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Plugin self-metadata for the Plugin info card. Version is substituted at
|
||||
* build time (tsup `define` in tsup.config.ts, read from package.json);
|
||||
* build time (tsdown `define` in tsdown.config.ts, read from package.json);
|
||||
* the typeof guard keeps a dev/test bundle built without defines working.
|
||||
*/
|
||||
|
||||
|
||||
+9
-11
@@ -68,15 +68,14 @@ const require = (spec) => {
|
||||
}
|
||||
|
||||
// ---- materialize the bundle the way the loader does ----
|
||||
// Extract the factory body (between the handoff's factory opening and the
|
||||
// loader-facing `return module.exports;`) and evaluate it with our own
|
||||
// module/exports so the closure shape matches the real bundle.
|
||||
const start = bundle.indexOf('factory: (require) => {') + 'factory: (require) => {'.length
|
||||
const end = bundle.indexOf(' return module.exports;')
|
||||
const factory = new Function('require', 'module', 'window', 'document',
|
||||
bundle.slice(start, end) + '\n return module.exports;')
|
||||
const m = { exports: {} }
|
||||
const pluginExports = factory(require, m, globalThis.window, fakeDoc)
|
||||
// Run the bundle verbatim: it registers itself through the boot handoff
|
||||
// (window.__ModuleLoader__.load), and the loader materializes the plugin by
|
||||
// invoking the captured factory with the module-table require. The factory
|
||||
// body declares its own module/exports (the bundle intro), so no string
|
||||
// surgery on the artifact — the test stays decoupled from bundler layout.
|
||||
new Function(bundle)()
|
||||
assert.ok(handoff !== null, 'bundle must register through __ModuleLoader__.load')
|
||||
const pluginExports = handoff.factory(require)
|
||||
|
||||
assert.equal(pluginExports.name, 'dsh-context')
|
||||
assert.deepEqual(pluginExports.inject, ['slots', 'locale'])
|
||||
@@ -241,8 +240,7 @@ const requireStateful = (spec) => {
|
||||
`bundle must only require platform modules (got "${spec}")`)
|
||||
return spec === 'react' ? statefulReact : fakePrimitives
|
||||
}
|
||||
const m2 = { exports: {} }
|
||||
const pluginExports2 = factory(requireStateful, m2, globalThis.window, fakeDoc)
|
||||
const pluginExports2 = handoff.factory(requireStateful)
|
||||
|
||||
const DICT_FOR_TEST = { 'tab': 'Context', 'loading': '…', 'error': 'ERR:', 'error.retry': 'retry', 'detail.step': 'Turn {t} · Step {s}', 'gran.step': 'Step', 'gran.turn': 'Turn', 'detail.turn': 'Turn {t} · {n} steps', 'detail.lastStep': 'last step', 'overview.used': 'of context used', 'overview.free': 'Free window', 'events.at': 'Turn {t} · Step {s}', 'events.range': 'Turn {t} · Step {a}→{b}', 'events.rangeTo': 'Turn {a} · Step {as} → Turn {b} · Step {bs}', 'stats.recycleSub': '{c} compactions · {p} prunes', 'tip.step': 'Turn {t} · Step {s}', 'tip.turn': 'Turn {t} · {n} steps', 'tip.total': 'total ≈ {n}', 'tip.actual': ' (actual {n})', 'trend.title': 'History', 'trend.empty': 'empty trend', 'cmd.close': 'Close', 'cat.user': 'User', 'browser.live': 'Live (next request)', 'browser.liveNow': 'Live · next request', 'browser.items': '{n} items', 'browser.noContent': 'outside the loaded window', 'browser.loading': 'loading older history', 'browser.preview': 'preview', 'browser.noHeader': 'older plugin build',
|
||||
'browser.deltaHint': 'vs previous turn', 'overview.compactReserve': 'compact reserve {pct}%', 'tool.desc': 'Description', 'tool.params': 'Parameters', 'tool.paramsEmpty': '(no parameters)', 'tool.jsonToggle': 'View Raw JSON', 'tool.jsonHide': 'Collapse', 'rich.raw': 'Raw', 'rich.md': 'Markdown', 'rich.toMd': 'View as Markdown', 'rich.toRaw': 'View Raw Text', 'attach.images': 'Images', 'attach.other': 'Other content', 'attach.image': 'Image', 'attach.open': 'Open full image', 'attach.loading': '…', 'attach.loadFailed': 'Load failed · click to retry', 'attach.orig': 'original {d}' }
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
// Read the manifest from cwd: the config file's own URL is not guaranteed to
|
||||
// sit at the package root under every loader, and `npm run build` runs here.
|
||||
const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
|
||||
|
||||
// Mirrors packages/client/web/src/platform.ts in deepseek-harness: the shell
|
||||
// seeds these specifiers into the frozen browser module table, so client
|
||||
// bundles leave them to the injected `require` instead of inlining.
|
||||
const PLATFORM_MODULES = [
|
||||
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
||||
'@deepseek-ai/dsh-client-ui-slots',
|
||||
'@deepseek-ai/dsh-client-ui-primitives',
|
||||
]
|
||||
const PRELOADED_CLIENT_EXTERNALS = ['@deepseek-ai/dsh-client-runtime/client']
|
||||
|
||||
// Mirrors the purity-gate allowances in packages/client/tsdown.client.ts:
|
||||
// wire/type layers with no shared runtime identity may inline; every other
|
||||
// @deepseek-ai/* value import is a build error (cross-plugin collaboration
|
||||
// goes through cordis services).
|
||||
const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand)(\/|$)/
|
||||
const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/
|
||||
const GENERATED_REMOTE = /^@deepseek-ai\/dsh-[a-z0-9]+(?:-[a-z0-9]+)*\/remote$/
|
||||
|
||||
const requested = new Set([
|
||||
...PLATFORM_MODULES,
|
||||
...PRELOADED_CLIENT_EXTERNALS,
|
||||
...(pkg.dsh?.client?.external ?? []),
|
||||
])
|
||||
const isRequested = (specifier: string): boolean => requested.has(specifier)
|
||||
|
||||
// Host half: a production dependency is on disk in a real install and stays
|
||||
// an import; everything else inlines. Both halves are stated so moving a
|
||||
// dependency between npm sections never silently re-bundles it.
|
||||
const productionDeps = new Set([
|
||||
...Object.keys(pkg.dependencies ?? {}),
|
||||
...Object.keys(pkg.peerDependencies ?? {}),
|
||||
...Object.keys(pkg.optionalDependencies ?? {}),
|
||||
])
|
||||
const escapeSpecifier = (name: string): string => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const productionPatterns = [...productionDeps].map(name => new RegExp(`^${escapeSpecifier(name)}(/|$)`))
|
||||
const isProductionDependency = (specifier: string): boolean =>
|
||||
productionPatterns.some(pattern => pattern.test(specifier))
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV ?? 'production'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: pkg.name,
|
||||
entry: { index: 'src/host/index.ts' },
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'es2024',
|
||||
fixedExtension: false,
|
||||
dts: false,
|
||||
clean: true,
|
||||
deps: {
|
||||
neverBundle: isProductionDependency,
|
||||
alwaysBundle: (specifier: string) => !isBuiltin(specifier) && !isProductionDependency(specifier),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `${pkg.name}/client`,
|
||||
entry: { client: 'src/client/index.ts' },
|
||||
outDir: 'lib',
|
||||
format: 'cjs',
|
||||
platform: 'browser',
|
||||
// dts would wrap the banner/footer into a .d.cts and break parsing;
|
||||
// browser profiling consumes the bundle's own sourcemap instead.
|
||||
dts: false,
|
||||
sourcemap: true,
|
||||
clean: false,
|
||||
deps: {
|
||||
// A require() the module table cannot answer is a guaranteed runtime
|
||||
// throw: requested specifiers stay imports, everything else inlines.
|
||||
neverBundle: isRequested,
|
||||
alwaysBundle: (specifier: string) => !isRequested(specifier),
|
||||
},
|
||||
// Browser bundles inline node-idiom deps that read process.env.NODE_ENV
|
||||
// or probe import.meta.env(.MODE); without these substitutions the
|
||||
// factory throws ReferenceError at boot.
|
||||
define: {
|
||||
'process.env': '{}',
|
||||
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
|
||||
'import.meta.env.MODE': JSON.stringify(NODE_ENV),
|
||||
'import.meta.env': JSON.stringify({ MODE: NODE_ENV }),
|
||||
__DSH_CTX_VERSION__: JSON.stringify(pkg.version),
|
||||
__DSH_CTX_REPO__: JSON.stringify(
|
||||
String((pkg.repository && pkg.repository.url) || '').replace(/^git\+/, '').replace(/\.git$/, ''),
|
||||
),
|
||||
},
|
||||
plugins: [{
|
||||
name: 'dsh-client-bundle-purity',
|
||||
resolveId(source: string) {
|
||||
if (!source.startsWith('@deepseek-ai/')) return null
|
||||
if (isRequested(source)) return null
|
||||
if (VENDORED_LIBRARY.test(source)) return null
|
||||
if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null
|
||||
throw new Error(
|
||||
`client bundle purity: "${source}" is not in the default client externals or ${pkg.name}'s dsh.client.external, an inline-safe wire layer, or a generated /remote contribution — `
|
||||
+ 'cross-plugin value imports are forbidden; declare a non-default module request or collaborate through cordis services '
|
||||
+ '(type-only imports are erased and never reach this gate)',
|
||||
)
|
||||
},
|
||||
}],
|
||||
outputOptions: {
|
||||
entryFileNames: 'client.js',
|
||||
// The closure-factory handoff every `dsh.client` package's ./client
|
||||
// export must use; mirrors tsdown.client.ts banner/intro/footer.
|
||||
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(pkg.name)}, factory: (require) => {`,
|
||||
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
||||
footer: 'return module.exports; } });',
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -1,74 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
// Read the manifest from cwd, not import.meta.url: tsup re-bundles this config
|
||||
// into a temp .mjs and runs it from there, so import.meta.url points at the
|
||||
// temp file, not this source. process.cwd() is the package root under
|
||||
// `npm run build`.
|
||||
const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
|
||||
|
||||
// Client bundle wrap: the closure-factory format every `dsh.client` package's
|
||||
// `./client` export must use. The browser loader answers `require` for the
|
||||
// platform modules (react, primitives) from its module table and reads
|
||||
// `module.exports` as the plugin surface.
|
||||
const CLIENT_BANNER = `window.__ModuleLoader__.load({
|
||||
id: ${JSON.stringify(pkg.name)},
|
||||
factory: (require) => {
|
||||
var module = { exports: {} };
|
||||
var exports = module.exports;
|
||||
`
|
||||
const CLIENT_FOOTER = `
|
||||
return module.exports;
|
||||
},
|
||||
});
|
||||
`
|
||||
|
||||
export default defineConfig([
|
||||
// ---- host half: bundled TS -> ESM. zod is a peer dependency;
|
||||
// @deepseek-ai/dsh-session is the harness-provided runtime contract
|
||||
// (declared as a peer), so both stay external and resolve at runtime from
|
||||
// the installation's single instance. ----
|
||||
{
|
||||
name: 'host',
|
||||
entry: { index: 'src/host/index.ts' },
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'es2022',
|
||||
external: ['zod', '@deepseek-ai/dsh-session'],
|
||||
outExtension: () => ({ js: '.js' }),
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
dts: false,
|
||||
},
|
||||
// ---- client half: bundled TS -> CJS, wrapped in the loader handoff -------
|
||||
// react and the primitives are peers the browser module table provides at
|
||||
// runtime; everything else is inlined by the bundler.
|
||||
{
|
||||
name: 'client',
|
||||
entry: { client: 'src/client/index.ts' },
|
||||
outDir: 'lib',
|
||||
format: ['cjs'],
|
||||
platform: 'browser',
|
||||
target: 'es2020',
|
||||
external: ['react', '@deepseek-ai/dsh-client-ui-primitives'],
|
||||
outExtension: () => ({ js: '.js' }),
|
||||
clean: false,
|
||||
sourcemap: false,
|
||||
dts: false,
|
||||
banner: { js: CLIENT_BANNER },
|
||||
footer: { js: CLIENT_FOOTER },
|
||||
// The source's `module.exports = {}` is the loader contract (the wrap
|
||||
// above provides a local `var module`); silence that one warning.
|
||||
esbuildOptions: (options) => {
|
||||
options.logOverride = { 'commonjs-variable-in-esm': 'silent' }
|
||||
},
|
||||
define: {
|
||||
__DSH_CTX_VERSION__: JSON.stringify(pkg.version),
|
||||
__DSH_CTX_REPO__: JSON.stringify(
|
||||
String((pkg.repository && pkg.repository.url) || '').replace(/^git\+/, '').replace(/\.git$/, ''),
|
||||
),
|
||||
},
|
||||
},
|
||||
])
|
||||
Reference in new issue
Block a user