-
Notifications
You must be signed in to change notification settings - Fork 39
Description
Problem
packages/cli uses a split build strategy that causes dependency instability:
- tsc compiles local CLI code (
src/**/*.tsminus global modules) →dist/ - rolldown bundles global CLI modules (
src/create/,src/migration/,src/init/,src/mcp/,src/config/,src/staged/) →dist/global/
Since tsc doesn't bundle, its output (dist/*.js) keeps bare import statements. Any package imported by tsc-compiled code must be in dependencies so it's available when vite-plus is installed in other projects.
Rolldown bundles everything inline (except explicitly externalized packages), so those same packages only need to be devDependencies for the bundled code.
This creates confusion and bugs:
-
detect-indent/detect-newlineincident: These were indevDependenciesbecause they're used by migration code (bundled by rolldown). Butsrc/utils/json.ts(compiled by tsc) also imports them, anddist/utils/json.jsis loaded at runtime bydist/init-config.js→dist/bin.js. Result:ERR_MODULE_NOT_FOUNDin the frm-stack E2E test. Fixed by moving todependencies, but the root cause is the build architecture. -
Shared utility modules (
src/utils/json.ts,src/utils/terminal.ts, etc.) are used by both tsc-compiled and rolldown-bundled code. The tsc path requires their transitive deps independencies; the rolldown path inlines them. This makes it hard to reason about which packages need to be runtime dependencies. -
validateGlobalBundleExternals()inbuild.tsexists specifically to catch cases where rolldown silently externalizes workspace packages. This is a band-aid for the split build problem.
Current Build Architecture
src/bin.ts ──────────────────────────────── tsc ──→ dist/bin.js
src/init-config.ts ──────────────────────── tsc ──→ dist/init-config.js
src/utils/*.ts ──────────────────────────── tsc ──→ dist/utils/*.js
src/resolve-*.ts ────────────────────────── tsc ──→ dist/resolve-*.js
src/define-config.ts ────────────────────── tsc ──→ dist/define-config.js + .cjs
src/create/bin.ts ───────────────────── rolldown ──→ dist/global/create.js
src/migration/bin.ts ────────────────── rolldown ──→ dist/global/migrate.js
src/config/bin.ts ───────────────────── rolldown ──→ dist/global/config.js
src/mcp/bin.ts ──────────────────────── rolldown ──→ dist/global/mcp.js
src/staged/bin.ts ───────────────────── rolldown ──→ dist/global/staged.js
src/version.ts ──────────────────────── rolldown ──→ dist/global/version.js
Problems with this split:
- Bare imports in tsc output require runtime
dependencies - Same utility imported by both paths → unclear dependency classification
- No tree-shaking for tsc output
- Two different bundler configs to maintain
- Hacks needed (
validateGlobalBundleExternals,fix-binding-pathplugin,inject-cjs-requireplugin)
Proposed Solution
Migrate the entire packages/cli build to tsdown (already used by packages/prompts). tsdown bundles all code, so:
- All third-party packages become
devDependencies(inlined at build time) - Only packages that must be resolved at runtime (NAPI binding, oxlint/oxfmt binaries, vite-plus-core/test re-exports) stay in
dependencies - No more confusion about dependency classification
- Single build tool, single config
- Tree-shaking for all outputs
Target Architecture
src/bin.ts ──────────────────────────── tsdown ──→ dist/bin.js
src/create/bin.ts ───────────────────── tsdown ──→ dist/global/create.js
src/migration/bin.ts ────────────────── tsdown ──→ dist/global/migrate.js
src/config/bin.ts ───────────────────── tsdown ──→ dist/global/config.js
src/mcp/bin.ts ──────────────────────── tsdown ──→ dist/global/mcp.js
src/staged/bin.ts ───────────────────── tsdown ──→ dist/global/staged.js
src/version.ts ──────────────────────── tsdown ──→ dist/global/version.js
src/define-config.ts ────────────────── tsdown ──→ dist/define-config.js + .cjs + .d.ts
Key Considerations
- NAPI binding: Must remain external (
../binding/index.js) — resolved at runtime - Binary packages (oxlint, oxfmt, oxlint-tsgolint): Must remain external — resolved at runtime for platform-specific binaries
- Re-export shims (vite-plus-core, vite-plus-test): Must remain external — the whole point is to delegate to these packages
- Type declarations: tsdown generates
.d.tsfiles, replacing the current tsc-generated types - CJS output:
define-config.tsneeds both ESM and CJS output (tsdown supports this) treeshake: false: Currently required for rolldown global modules (dynamic imports treated as pure) — verify if tsdown handles this differentlylint-stagedCJS compatibility: Theinject-cjs-requirerolldown plugin handles CJS deps — tsdown may handle this natively
Related
- PR refactor: move typeAware/typeCheck from CLI flags to vite.config.ts #739 —
detect-indentbug that exposed this issue packages/cli/build.ts— current build scriptpackages/cli/rolldown.config.ts— current rolldown config