DeepSeek Harness 构建链路重构:以 tsdown 替换 dumble 的打包方案选型与落地实践
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
本文围绕 DeepSeek Harness(下称 dsh)仓库中一份已归档的 Agent Note(2026-06-11-tsdown-over-dumble.md)展开,完整还原一次关键的基础设施决策:为什么放弃 Cordis 生态自带的 dumble 打包器,改用 rolldown 内核的 tsdown,以及这套方案在 monorepo 中的具体配置、双格式覆盖、命令编排与后续演进。读完本文,你将掌握在大型 pnpm + TypeScript monorepo 中如何为 "everything is a plugin" 的包树设计统一打包基线、如何为特殊包做逐包覆盖(dual ESM/CJS、多入口内联),以及构建工具选型时该从哪些维度做权衡。
一、决策背景:dumble 为什么不再适合作为承重工具
dsh 仓库采用"一切皆插件"的架构(项目根 README.md 即宣言 "Everything is a Plugin"),全部业务包分布在packages/*/*与vendor/*两大目录树中。构建初期,仓库直接沿用了 Cordis 上游自己使用的打包器dumble——它是 cordiverse 的零配置 esbuild 封装,会逐个读取每个包的package.json,从exports字段推断入口与产物格式,从而与 vendor 化(source-vendored)的 Cordis 系列包的约定保持最大一致。
但根据该 Agent Note 的记录,dumble 作为承重工具存在明确风险:
- 社区活跃度低:当时为 v0.2.x 版本,npm 周下载量约 530 次,实际维护者只有一人("bus factor" 过高);
- 无 workspace 模式:仓库不得不为此维护一个自定义编排脚本
scripts/build.ts来逐包驱动它,增加了额外的心智负担; - 切换窗口正当时:构建产物当时只服务于
pnpm run build+ publint(仓库尚未发布任何包,dev/test/demo 都通过 tsx 直接跑源码),切换成本处于历史最低点,而一旦开始发布包,迁移成本只会不断上升。
这条"现在不换、以后更贵"的判断,是这次决策最核心的时间窗论证。
二、决策:采用 tsdown,并确立统一共享构建形状
最终决定是用tsdown替换 dumble。tsdown 基于 rolldown(Rust 内核),由 VoidZero 团队维护、发布节奏活跃,当时周下载量约 250 万。除社区与维护面优势外,tsdown 原生支持 workspace 模式,可以替代原先手写的逐包编排脚本。
2.1 workspace 通配:显式 glob 控制打包边界
根 tsdown.config.ts 中通过workspace字段声明需要打包的包集合:
workspace: ['vendor/*', 'packages/*/*', 'apps/cli'],Note 中特别说明了选择显式 glob 而非workspace: true的原因:workspace: true会顺带发现 example 清单和非打包型 workspace 成员(例如native/landlock-run、website等),而显式 glob 把打包范围精确收敛到vendor 化的 Cordis 框架层 + TypeScript 包树,避免误打包与产物膨胀。
2.2 共享形状:一份配置管住所有普通包
对绝大多数包,根配置给出统一形状(Note 中的核心参数,当前仓库配置逐项一致):
| 配置项 | 值 | 说明 |
|---|---|---|
entry | lib/types/{index,invariant,startup}.js | 打包入口为TSC 先编译产出的 JS(详见第四节演进),而非src/index.ts |
outDir | 'lib' | 发布产物输出目录,与lib/types声明树共存 |
format | ['esm'] | 默认 ESM 单格式 |
platform | 'node' | 面向 Node 运行时 |
target | 'es2024' | 编译目标,充分利用现代 Node(根 package.json 要求node ^22.19.0 \|\| >=24.0.0) |
fixedExtension | false | 保留.js扩展名,兼容"type": "module"包 |
dts | false | 声明文件交给tsc -b全权负责,tsdown 不再产出.d.ts |
clean | false | 不清理lib/,因为其中还驻留 TSC 的lib/types中间树 |
这一"打包器只管运行时 bundle、编译器管声明"的分工,是理解整套构建体系的关键。
三、逐包覆盖:vendor 层两个特殊形态
共享形状无法覆盖所有包的发布形态,因此在 vendor 目录内保留了两份仓库自有的逐包 tsdown 配置(不属于上游同步面,vendor/README.md 的本地修改日志第 5 条有明确记录)。
3.1 schemastery:dual ESM/CJS 双格式输出
schemastery向上游发布的形态是main → lib/index.cjs、module → lib/index.mjs的双格式包。它的 vendor/schemastery/tsdown.config.ts 通过outExtensions精确控制扩展名映射:
export default defineConfig({ entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm', 'cjs'], platform: 'node', target: 'es2024', outExtensions: ({ format }) => ({ js: format === 'es' ? '.mjs' : '.cjs' }), dts: false, clean: false, })对应的 vendor/schemastery/package.json 以条件 exports 声明import → ./lib/index.mjs、require → ./lib/index.cjs,types指向lib/types/index.d.ts,并在files中精确列出两个 bundle 与lib/types/**/*.d.ts。
3.2 logger-console:两次单入口打包,内联共享基类
logger-console需要同时发布 Node 导出(index)与浏览器导出(browser),由 package.json 的exports条件(node/default)切换。其 vendor/logger-console/tsdown.config.ts 采用了数组形式的两次单入口 pass:
export default defineConfig([ { ...shared, entry: ['lib/types/index.js'] }, { ...shared, entry: ['lib/types/browser.js'] }, ])这样做的目的是:让两个入口各自内联共享基类,而不是把公共代码拆成一个哈希命名的 chunk——这与上游发布的产物形状保持一致,避免浏览器与 Node 场景下出现包级 chunk 加载问题。发布面由 vendor/logger-console/package.json 的files(lib/index.js、lib/browser.js+ 声明树)钉死。
四、命令编排与后续演进:从 tsc+tsdown 串行到 TSC-first
4.1 迁移当时的命令形态
Note 记录:dumble 时代的scripts/build.ts被删除,pnpm run build简化为tsc -b && tsdown——根 solution 拥有产物图(emit graph),tsc 负责编译与声明,tsdown 只做打包。
4.2 TSC-first 演进:让一个编译器说了算
随后的一份 Agent Note(2026-06-17-ts-build-config.md,实现于implemented/,未归档)进一步把这一分工推到极致:tsdown 不再负责任何 TypeScript 变换,入口从src/index.ts改为TSC 产出的lib/types/*.js。原因有三:
- tsdown 底层用 oxc 做 TypeScript 变换,行为与
tsc不一致(如装饰器变换、逐文件 emit 与 bundle 的差异); - tsdown 产出的 bundled
.d.ts与 Cordis 内部相对模块增强(module augmentation)形态冲突; - 源码内使用显式
.ts相对导入说明符,配合rewriteRelativeImportExtensions让 JS 输出改写为.js、声明输出保留 NodeNext 可解析的.ts说明符。
当前根 package.json 的脚本即体现这套顺序:
"build:lib:host": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -b tsconfig.host.json && tsdown --env.DSH_BUILD_FACE host", "build:lib:client": "tsc -b tsconfig.client.json && tsdown --env.DSH_BUILD_FACE client"Host 相位先tsc -b(产出lib/types下逐模块的.js/.d.ts/.js.map/.d.ts.map),再由 tsdown 读取该 JS 产出发布入口并运行 Typert;Client 相位在 Host 生成 Remote Client 声明后编译,tsdown 负责产出 Node loader 入口与浏览器 bundle。
4.3 当前根配置:host/client 双面与 Typert 插件
当前根 tsdown.config.ts 已演化为函数式配置,通过--env.DSH_BUILD_FACE区分 host/client 两趟构建:
export default defineConfig(({ env }) => { const client = isBuildFaceClient(env?.DSH_BUILD_FACE) return { workspace: ['vendor/*', 'packages/*/*', 'apps/cli'], entry: client ? '' : ['lib/types/{index,invariant,startup}.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, plugins: client ? [] : [typertPlugin({ mode: 'workspace', faces: ['host'] })], } })Client 面只让"声明了浏览器 bundle 的包"通过包级配置产出 Node loader 入口与浏览器产物;Host 面额外接入仓库自研的typertPlugin(声明图生成,源自packages/typert的 generator 产物)。而完整pnpm run build目前由 scripts/build.ts 编排:先build:lib(host + client 两相位),再build:web,最后写入客户端构建记录——这是 Note 归档之后为绑定完整发布产物而重新引入的顶层编排,与 dumble 时代为弥补"无 workspace 模式"而写的编排脚本职责不同。
五、候选方案对比:为什么不是 esbuild 脚本,也不是 pkgroll
Note 完整记录了三个被否掉的备选方案,对做构建选型的读者很有参考价值:
| 方案 | 优势 | 被否理由 |
|---|---|---|
| 直接写 esbuild 脚本 | 引擎最成熟、零封装风险 | 必须手工维护逐包规格表,而 tsdown 的 workspace 模式免费提供了这份规格推导 |
| pkgroll | 理念上最接近的 drop-in 替代 | 周下载量约 7.8 万,且基于 Rollup,维护故事弱于 tsdown |
| 继续使用 dumble | 与上游 Cordis 完全对齐 | 上游对齐完美,但维护者风险(bus factor)不可接受 |
六、迁移后果:输出形态不变,代价是失去 exports 推导
6.1 运行时产物保持 dumble 时代公开入口形状
迁移不改变包的公开入口契约:
- 普通包:
lib/index.js; schemastery:lib/index.mjs/lib/index.cjs双格式;logger-console:lib/index.js(node)+lib/browser.js(browser);- 声明统一移至
lib/types(遵循 TSC-first 约定); - externals 依旧取自各包的
dependencies/peerDependencies(如 vendor/schemastery/package.json 声明@standard-schema/spec与 workspace 内@deepseek-ai/cosmokit)。
6.2 让渡的能力与未来选项
迁移付出的明确代价是:放弃了 dumble 的exports字段自动推导。今后新增非默认形态的包,需要像 schemastery、logger-console 那样补一份包级tsdown.config.ts,而不能只改 package.json 字段。vendor/README.md 的同步流程(第 5 步要求pnpm install && pnpm run test && pnpm run build)与 docs/cookbook/adding-a-vendored-package.md 新包添加指南中都体现了这一约束。
Note 同时为未来留了一个选项:如果将来tsc -b成为构建瓶颈,可以让 tsdown 通过isolatedDeclarations接管声明打包——但这是一个需要新开 Agent Note 的独立决策,当前不启用。
七、小结
这次迁移的本质是一次典型的承重工具替换决策:在"上游对齐"与"可维护性"之间,dsh 选择把打包器从单维护者的 dumble 换成生态更强、维护面更稳的 tsdown,同时通过workspace显式 glob、共享形状 + 逐包覆盖两层配置模型,把"统一基线"和"特殊形态"两条路同时走通;随后又以 TSC-first 演进,把 TypeScript 变换收敛到单一编译器,让 tsdown 只承担"把 TSC 产物打成发布 bundle"这一纯粹职责。这套"编译器管声明、打包器管 bundle、配置分层共享 + 覆盖"的模式,可以直接复用到其他大型 TypeScript monorepo 的构建体系设计中。
延伸阅读:本决策的中文版笔记见 2026-06-11-tsdown-over-dumble.zh.md;TSC-first 构建分工的完整推导见 2026-06-17-ts-build-config.md;vendor 包同步与本地修改日志见 vendor/README.md。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考