Replace dumble with tsdown for JS bundling

dumble (0.2.x, ~530 dl/wk, single-maintainer) was a bus-factor risk as
the load-bearing bundler. tsdown (rolldown-based, ~2.5M dl/wk, actively
maintained) replaces it while output stays list-identical, verified by
snapshot diff: 17 JS bundles, externals preserved, schemastery dual
.mjs/.cjs and logger-console node+browser entries intact.

Root tsdown.config.ts uses workspace globs ['vendor/*', 'packages/*']
(explicit, so examples/* stays excluded); two per-package overrides in
vendor/ cover the special shapes and are logged in vendor/README.md as
ours (not upstream sync surface). scripts/build.ts (dumble
orchestration) is deleted; yarn build = tsc -b && tsdown. tsc -b keeps
owning declarations (dts: false, clean: false).

Rationale recorded in ADR 0008 (also covers the direct-esbuild and
pkgroll alternatives).

Gates: lint, typecheck, 134 tests, hygiene (knip/publint/constraints),
demo smoke all green.
This commit is contained in:
Tianyi Cui
2026-06-11 22:34:43 +08:00
parent 4dafad4db6
commit 630bbddf9a
11 changed files with 590 additions and 246 deletions
+4 -2
View File
@@ -32,7 +32,9 @@ examples/ Runnable demos (not workspaces). echo-agent = mock model + echo
docs/ architecture.md — the design doc. adr/ — decision records (the
why behind vendoring, event-sourcing, the schema DSL, …).
rfc/ — proposals for substantial future work.
scripts/ build.ts — dumble JS bundling for all packages.
scripts/ repo maintenance scripts (vendor-manifest guard, publint runner).
JS bundling is tsdown (root tsdown.config.ts + two per-package
overrides in vendor/).
```
## Commands
@@ -41,7 +43,7 @@ scripts/ build.ts — dumble JS bundling for all packages.
yarn install # Yarn 4 workspaces (node-modules linker), node >= 24
yarn test # vitest run (packages/*/tests/**/*.spec.ts)
yarn typecheck # tsc -b tsconfig.build.json (declarations only)
yarn build # typecheck + dumble JS bundles into each package's lib/
yarn build # typecheck + tsdown JS bundles into each package's lib/
yarn demo # run examples/echo-agent (needs --expose-internals, the
# script passes it; type "echo hi" to see a tool call)
```
+52
View File
@@ -0,0 +1,52 @@
# ADR 0008: tsdown for JS bundling instead of dumble
Status: accepted (2026-06-11)
## Context
The initial build used **dumble**, the cordiverse zero-config esbuild wrapper
that upstream Cordis itself builds with — maximum alignment with the vendored
packages' conventions (it reads each package.json and infers entries/formats
from the `exports` field). But dumble is a liability as a load-bearing tool in
this repo: v0.2.x, ~530 npm downloads/week, effectively one maintainer, and we
were invoking it through a custom orchestration script (`scripts/build.ts`)
because it has no workspace mode.
Build output currently matters only for `yarn build` + publint (nothing
publishes yet; dev/test/demo run unbuilt via tsx), so the switching cost is at
its lowest now and only grows once packages publish.
## Decision
Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week,
VoidZero-backed, actively released):
- Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*']`
(explicit globs, not `workspace: true`, which would also pick up
`examples/*` — they have package.json files but are not yarn workspaces).
- Shared shape: entry `src/index.ts`, `outDir: 'lib'`, ESM, `platform: node`,
`target: es2024`, `fixedExtension: false` (keeps `.js` for
`"type": "module"` packages), `dts: false` (tsc -b owns declarations),
`clean: false` (lib/ holds tsc's .d.ts output).
- Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs;
logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via
`outExtensions`), logger-console (two single-entry passes so the shared
base class is inlined into each entry instead of a hash-named chunk,
matching upstream's published shape).
- `scripts/build.ts` deleted; `yarn build` = `tsc -b && tsdown`.
Alternatives considered: **direct esbuild script** (most established engine,
zero wrapper risk, but hand-maintains the per-package spec table tsdown's
workspace mode gives us); **pkgroll** (closest drop-in philosophically, but
78k dl/wk and Rollup-based — strictly weaker maintenance story than tsdown);
**keep dumble** (perfect upstream alignment, unacceptable bus factor).
## Consequences
Output file lists are byte-for-byte-list identical to dumble's (verified by
snapshot diff at migration time); externals still come from each package's
dependencies/peerDependencies. We give up dumble's exports-field inference —
new packages with non-default shapes need a per-package `tsdown.config.ts`
instead of just package.json fields. Future option: tsdown could also absorb
declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the
bottleneck; that would be a new ADR.
+1
View File
@@ -17,3 +17,4 @@ with a new one and cross-link.
| [0005](0005-custom-schema-dsl-over-schemastery.md) | Custom typed tool-schema DSL instead of schemastery | accepted |
| [0006](0006-tool-schemas-in-prompt-assembly.md) | Tool schemas are part of the system-prompt assembly | accepted |
| [0007](0007-quality-gates.md) | Mechanical quality gates over prose guidelines | accepted |
| [0008](0008-tsdown-over-dumble.md) | tsdown for JS bundling instead of dumble | accepted |
+1
View File
@@ -24,6 +24,7 @@ export default tseslint.config(
'vendor/**', // vendored source keeps upstream style and idioms
'**/*.js',
'**/*.mjs',
'*.config.ts', // root tool configs (vitest, tsdown) — no project service
],
},
+2 -2
View File
@@ -12,7 +12,7 @@
"packages/*"
],
"scripts": {
"build": "tsc -b tsconfig.build.json && tsx scripts/build.ts",
"build": "tsc -b tsconfig.build.json && tsdown",
"typecheck": "tsc -b tsconfig.build.json && tsc -p tsconfig.typecheck.json",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
@@ -29,11 +29,11 @@
"@types/node": "^25.3.5",
"@vitest/coverage-v8": "^4.1.8",
"@yarnpkg/types": "^4.0.1",
"dumble": "^0.2.3",
"eslint": "^10.4.1",
"knip": "^6.16.1",
"lefthook": "^2.1.9",
"publint": "^0.3.21",
"tsdown": "^0.22.2",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.0",
-29
View File
@@ -1,29 +0,0 @@
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
// Bundle JS for every workspace package with dumble (tsc -b has already
// produced the .d.ts files; dumble reads each package's tsconfig/package.json).
const packages = [
'vendor/cosmokit',
'vendor/schemastery',
'vendor/cordis',
'vendor/loader',
'vendor/include',
'vendor/group',
'vendor/timer',
'vendor/hmr',
'vendor/logger-console',
'packages/llm',
'packages/session',
'packages/system-prompt',
'packages/tools',
'packages/agent',
'packages/agent-loop',
]
const root = resolve(import.meta.dirname, '..')
for (const path of packages) {
if (!existsSync(resolve(root, path, 'tsconfig.json'))) continue
execFileSync('node_modules/.bin/dumble', [path], { cwd: root, stdio: 'inherit' })
}
+26
View File
@@ -0,0 +1,26 @@
import { defineConfig } from 'tsdown'
/**
* JS bundling for all workspace packages (vendor/* + packages/*).
* Declarations are NOT produced here — `tsc -b tsconfig.build.json` owns
* .d.ts output (composite project references); hence `dts: false` and
* `clean: false` (lib/ already holds tsc's declarations).
*
* Per-package shape overrides live in `<package>/tsdown.config.ts`
* (schemastery: dual ESM+CJS; logger-console: extra browser entry).
*/
export default defineConfig({
// Explicit globs: `workspace: true` would also discover examples/* (any
// package.json), but only vendor/* and packages/* are yarn workspaces.
workspace: ['vendor/*', 'packages/*'],
entry: ['src/index.ts'],
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
// All packages set "type": "module"; fixedExtension false keeps ESM output
// at .js (not .mjs), matching the package.json main/exports fields.
fixedExtension: false,
dts: false,
clean: false,
})
+5
View File
@@ -49,6 +49,11 @@ Keep this log exhaustive — every divergence from upstream must be listed.
peer-dependency ranges preserved.
3. **All `tsconfig.json` files**: regenerated to extend the repo-root
`tsconfig.base.json` and declare project references.
4. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**:
ours, not upstream files — per-package build-shape overrides (dual
ESM+CJS output; separate node/browser entries) for the repo-root tsdown
build. Like the regenerated tsconfigs, they are not part of the upstream
sync surface.
## Sync procedure
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'tsdown'
/**
* logger-console ships two entries: the node exporter (index) and the
* browser exporter (browser), selected via package.json `exports`
* conditions. They are built as two single-entry passes so the shared
* base class is inlined into each (matching upstream's published shape)
* instead of split into a hash-named chunk.
*/
const shared = {
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
} as const
export default defineConfig([
{ ...shared, entry: ['src/index.ts'] },
{ ...shared, entry: ['src/browser.ts'] },
])
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'tsdown'
/**
* schemastery has no `"type": "module"` and publishes dual-format output
* (package.json: main → lib/index.cjs, module → lib/index.mjs). Pin the
* extensions explicitly — the defaults for a CommonJS package would emit
* .mjs/.js instead.
*/
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'lib',
format: ['esm', 'cjs'],
platform: 'node',
target: 'es2024',
outExtensions: ({ format }) => ({ js: format === 'es' ? '.mjs' : '.cjs' }),
dts: false,
clean: false,
})
+458 -213
View File
File diff suppressed because it is too large Load Diff