Skip to content

Commit 990cd71

Browse files
committed
refactor(cli): migrate build from tsc+rolldown to tsdown
Replace the split build strategy (tsc for local CLI code, rolldown for global modules) with a unified tsdown configuration that bundles all entry points to dist/. - All third-party deps are now inlined at build time, eliminating dependency classification confusion between dependencies/devDependencies - Move cac, cross-spawn, jsonc-parser, picocolors to devDependencies - Remove rolldown.config.ts and direct rolldown devDependency - Dynamic imports in bin.ts now use source-relative paths (./create/bin.js) which tsdown resolves and rewrites automatically - Remove @ts-ignore comments since TypeScript can now resolve the imports - Add cleanDistOutput() to remove stale tsc/rolldown artifacts before build Closes #744
1 parent ea02a9c commit 990cd71

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+295
-437
lines changed

.github/actions/build-upstream/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ runs:
2525
id: cache-key
2626
shell: bash
2727
run: |
28-
echo "key=napi-binding-v3-${{ inputs.target }}-${{ env.RELEASE_BUILD }}-${{ env.DEBUG }}-${{ env.VERSION }}-${{ env.NPM_TAG }}-${{ hashFiles('packages/tools/.upstream-versions.json', 'Cargo.lock', 'crates/**/*.rs', 'crates/*/Cargo.toml', 'packages/cli/binding/**/*.rs', 'packages/cli/binding/Cargo.toml', 'Cargo.toml', '.cargo/config.toml', 'packages/cli/package.json', 'packages/cli/build.ts') }}" >> $GITHUB_OUTPUT
28+
echo "key=napi-binding-v3-${{ inputs.target }}-${{ env.RELEASE_BUILD }}-${{ env.DEBUG }}-${{ env.VERSION }}-${{ env.NPM_TAG }}-${{ hashFiles('packages/tools/.upstream-versions.json', 'Cargo.lock', 'crates/**/*.rs', 'crates/*/Cargo.toml', 'packages/cli/binding/**/*.rs', 'packages/cli/binding/Cargo.toml', 'Cargo.toml', '.cargo/config.toml', 'packages/cli/package.json', 'packages/cli/build.ts', 'packages/cli/tsdown.config.ts') }}" >> $GITHUB_OUTPUT
2929
3030
# Resolve the Rust target directory (CARGO_TARGET_DIR from setup-rust, or default "target")
3131
- name: Resolve Rust target directory

packages/cli/BUNDLING.md

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This document explains how `vite-plus` is built and how it re-exports from both
66

77
The CLI package uses a **4-step build process**:
88

9-
1. **TypeScript Compilation** - Compile TypeScript source to JavaScript
9+
1. **tsdown Build** - Bundle all CLI entry points via tsdown
1010
2. **NAPI Binding Build** - Compile Rust code to native Node.js bindings
1111
3. **Core Package Export Sync** - Re-export `@voidzero-dev/vite-plus-core` under `./client`, `./types/*`, etc.
1212
4. **Test Package Export Sync** - Re-export `@voidzero-dev/vite-plus-test` under `./test/*`
@@ -15,21 +15,26 @@ This architecture allows users to import everything from a single package (`vite
1515

1616
## Build Steps
1717

18-
### Step 1: TypeScript Compilation (`buildCli`)
18+
### Step 1: tsdown Build (`buildWithTsdown`)
1919

20-
Compiles TypeScript source files using the TypeScript compiler API:
20+
Bundles all CLI entry points using tsdown (configured in `tsdown.config.ts`). The config defines two builds:
2121

22-
```typescript
23-
const program = createProgram({
24-
rootNames: fileNames,
25-
options,
26-
host,
27-
});
28-
program.emit();
29-
```
22+
**ESM build** — bundles all entry points to `dist/`:
23+
24+
- Public API entries: `bin`, `index`, `define-config`, `fmt`, `lint`, `pack`, `pack-bin`
25+
- Global command entries: `create`, `migrate`, `version`, `config`, `mcp`, `staged`
26+
- All third-party dependencies are inlined at build time
27+
- Only packages that must be resolved at runtime stay external (NAPI binding, `@voidzero-dev/vite-plus-core`, `@voidzero-dev/vite-plus-test`, `oxfmt`, `oxlint`)
28+
- Code splitting creates shared chunks for code used by multiple entries
29+
- DTS (`.d.ts`) files are generated for all entries
30+
31+
**CJS build** — produces dual-format output for:
32+
33+
- `define-config.ts``dist/define-config.cjs`
34+
- `index.cts``dist/index.cjs`
3035

31-
**Input**: `src/*.ts` files
32-
**Output**: `dist/*.js`, `dist/*.d.ts`
36+
**Input**: `src/**/*.ts`, `src/**/*.cts`
37+
**Output**: `dist/*.js`, `dist/*.cjs`, `dist/*.d.ts`, `dist/*-<hash>.js` (shared chunks)
3338

3439
### Step 2: NAPI Binding Build (`buildNapiBinding`)
3540

@@ -103,43 +108,37 @@ export * from '@voidzero-dev/vite-plus-test/browser-playwright';
103108
```
104109
packages/cli/
105110
├── dist/
106-
│ ├── index.js # Main entry (ESM)
111+
│ ├── bin.js # CLI entry point (bundled)
112+
│ ├── index.js # Main entry (ESM, bundled)
107113
│ ├── index.cjs # Main entry (CJS)
108114
│ ├── index.d.ts # Type declarations
109-
│ ├── bin.js # CLI entry point
115+
│ ├── define-config.js # Config helper (ESM)
116+
│ ├── define-config.cjs # Config helper (CJS)
117+
│ ├── define-config.d.ts
118+
│ ├── fmt.js # Re-exports oxfmt
119+
│ ├── lint.js # Re-exports oxlint types
120+
│ ├── pack.js # Re-exports vite-plus-core/pack
121+
│ ├── pack-bin.js # tsdown CLI for `vp pack`
122+
│ ├── create.js # Global command: vp create
123+
│ ├── migrate.js # Global command: vp migrate
124+
│ ├── version.js # Global command: vp --version
125+
│ ├── config.js # Global command: vp config
126+
│ ├── mcp.js # Global command: vp mcp
127+
│ ├── staged.js # Global command: vp staged
128+
│ ├── *-<hash>.js # Shared chunks (code splitting)
129+
│ ├── versions.js # Generated tool versions
110130
│ ├── client.d.ts # ./client types (triple-slash ref)
111131
│ ├── module-runner.js # ./module-runner shim
112-
│ ├── module-runner.d.ts
113132
│ ├── internal.js # ./internal shim
114-
│ ├── internal.d.ts
115133
│ ├── client/ # Synced client runtime files
116-
│ │ ├── client.mjs # ESM client shim
117-
│ │ ├── client.d.ts
118-
│ │ ├── env.mjs
119-
│ │ └── ...
120134
│ ├── types/ # Synced type definitions
121-
│ │ ├── importMeta.d.ts # Type shims (export type *)
122-
│ │ ├── importGlob.d.ts
123-
│ │ ├── customEvent.d.ts
124-
│ │ └── ...
125135
│ └── test/ # Synced test exports
126-
│ ├── index.js # Re-exports @voidzero-dev/vite-plus-test
127-
│ ├── index.cjs
128-
│ ├── index.d.ts
129-
│ ├── browser-playwright.js
130-
│ ├── browser-playwright.d.ts
131-
│ ├── plugins/
132-
│ │ ├── runner.js
133-
│ │ ├── utils.js
134-
│ │ ├── spy.js
135-
│ │ └── ... (33+ plugin shims)
136-
│ └── ...
137136
├── binding/
138137
│ ├── index.js # NAPI binding JS wrapper
139138
│ ├── index.d.ts # NAPI type declarations
140139
│ └── *.node # Platform-specific binaries
141140
└── bin/
142-
└── vite # Shell entry point
141+
└── vp # Shell entry point
143142
```
144143

145144
---
@@ -420,7 +419,7 @@ Note: Type shims include a side-effect import to preserve module augmentations (
420419
| -------------- | -------------------------------- |
421420
| `@napi-rs/cli` | NAPI build toolchain for Rust |
422421
| `oxfmt` | Code formatting for generated JS |
423-
| `typescript` | TypeScript compilation |
422+
| `tsdown` | TypeScript bundling |
424423

425424
---
426425

@@ -480,7 +479,7 @@ See `package.json` for the complete list of exports.
480479
### Build Flow
481480

482481
```
483-
1. buildCli() TypeScript compilation -> dist/*.js
482+
1. buildWithTsdown() tsdown bundle -> dist/*.js, dist/*.d.ts
484483
2. buildNapiBinding() Rust -> binding/*.node (per platform)
485484
3. syncCorePackageExports() Read core pkg dist -> dist/client/, dist/types/
486485
├── createClientShim() Triple-slash reference for ./client
@@ -512,7 +511,7 @@ The `exports` field in `package.json` has two categories: **manual** and **autom
512511

513512
All non-`./test*` exports are manually maintained in `package.json`. These fall into two groups:
514513

515-
**CLI-native exports** — point to CLI's own compiled TypeScript (built by `buildCli()` via tsc):
514+
**CLI-native exports** — point to CLI's own bundled TypeScript (built by `buildWithTsdown()` via tsdown):
516515

517516
| Export | Description |
518517
| ---------------- | -------------------------- |

packages/cli/build.ts

Lines changed: 13 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
* Build script for vite-plus CLI package
33
*
44
* This script performs the following main tasks:
5-
* 1. buildCli() - Compiles TypeScript sources (local CLI) via tsc
6-
* 2. buildGlobalModules() - Bundles global CLI modules (create, migrate, init, mcp, version) via rolldown
7-
* 3. buildNapiBinding() - Builds the native Rust binding via NAPI
8-
* 4. syncCorePackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-core
9-
* 5. syncTestPackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-test
10-
* 6. syncVersionsExport() - Generates ./versions module with bundled tool versions
11-
* 7. copySkillDocs() - Copies docs into skills/vite-plus/docs for runtime MCP access
12-
* 8. syncReadmeFromRoot() - Keeps package README in sync
5+
* 1. buildWithTsdown() - Bundles all CLI entry points via tsdown
6+
* 2. buildNapiBinding() - Builds the native Rust binding via NAPI
7+
* 3. syncCorePackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-core
8+
* 4. syncTestPackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-test
9+
* 5. syncVersionsExport() - Generates ./versions module with bundled tool versions
10+
* 6. copySkillDocs() - Copies docs into skills/vite-plus/docs for runtime MCP access
11+
* 7. syncReadmeFromRoot() - Keeps package README in sync
1312
*
1413
* The sync functions allow this package to be a drop-in replacement for 'vite' by
1514
* re-exporting all the same subpaths (./client, ./types/*, etc.) while delegating
@@ -20,23 +19,14 @@
2019
*/
2120

2221
import { execSync } from 'node:child_process';
23-
import { existsSync, globSync, readFileSync, readdirSync, statSync } from 'node:fs';
24-
import { copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
22+
import { existsSync, globSync, readdirSync, statSync } from 'node:fs';
23+
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2524
import { dirname, join } from 'node:path';
2625
import { fileURLToPath } from 'node:url';
2726
import { parseArgs } from 'node:util';
2827

2928
import { createBuildCommand, NapiCli } from '@napi-rs/cli';
3029
import { format } from 'oxfmt';
31-
import {
32-
createCompilerHost,
33-
createProgram,
34-
formatDiagnostics,
35-
parseJsonSourceFileConfigFileContent,
36-
readJsonConfigFile,
37-
sys,
38-
ModuleKind,
39-
} from 'typescript';
4030

4131
import { generateLicenseFile } from '../../scripts/generate-license.js';
4232
import corePkg from '../core/package.json' with { type: 'json' };
@@ -62,14 +52,13 @@ const napiArgs = process.argv
6252
.filter((arg) => arg !== '--skip-native' && arg !== '--skip-ts');
6353

6454
if (!skipTs) {
65-
await buildCli();
66-
buildGlobalModules();
55+
buildWithTsdown();
6756
generateLicenseFile({
6857
title: 'Vite-Plus CLI license',
6958
packageName: 'Vite-Plus',
7059
outputPath: join(projectDir, 'LICENSE'),
7160
coreLicensePath: join(projectDir, '..', '..', 'LICENSE'),
72-
bundledPaths: [join(projectDir, 'dist', 'global')],
61+
bundledPaths: [join(projectDir, 'dist')],
7362
resolveFrom: [projectDir],
7463
});
7564
if (!existsSync(join(projectDir, 'LICENSE'))) {
@@ -133,112 +122,11 @@ async function buildNapiBinding() {
133122
}
134123
}
135124

136-
async function buildCli() {
137-
const tsconfig = readJsonConfigFile(join(projectDir, 'tsconfig.json'), sys.readFile.bind(sys));
138-
139-
const { options: initialOptions } = parseJsonSourceFileConfigFileContent(
140-
tsconfig,
141-
sys,
142-
projectDir,
143-
);
144-
145-
const options = {
146-
...initialOptions,
147-
noEmit: false,
148-
outDir: join(projectDir, 'dist'),
149-
};
150-
151-
const cjsHost = createCompilerHost({
152-
...options,
153-
module: ModuleKind.CommonJS,
154-
});
155-
156-
const cjsProgram = createProgram({
157-
rootNames: ['src/define-config.ts'],
158-
options: {
159-
...options,
160-
module: ModuleKind.CommonJS,
161-
},
162-
host: cjsHost,
163-
});
164-
165-
const { diagnostics: cjsDiagnostics } = cjsProgram.emit();
166-
167-
if (cjsDiagnostics.length > 0) {
168-
console.error(formatDiagnostics(cjsDiagnostics, cjsHost));
169-
process.exit(1);
170-
}
171-
await rename(
172-
join(projectDir, 'dist/define-config.js'),
173-
join(projectDir, 'dist/define-config.cjs'),
174-
);
175-
176-
const host = createCompilerHost(options);
177-
178-
const program = createProgram({
179-
rootNames: globSync('src/**/*.{ts,cts}', {
180-
cwd: projectDir,
181-
exclude: [
182-
'**/*/__tests__',
183-
// Global CLI modules — bundled by rolldown instead of tsc
184-
'src/create/**',
185-
'src/init/**',
186-
'src/mcp/**',
187-
'src/migration/**',
188-
'src/version.ts',
189-
'src/types/**',
190-
],
191-
}),
192-
options,
193-
host,
194-
});
195-
196-
const { diagnostics } = program.emit();
197-
198-
if (diagnostics.length > 0) {
199-
console.error(formatDiagnostics(diagnostics, host));
200-
process.exit(1);
201-
}
202-
}
203-
204-
function buildGlobalModules() {
205-
execSync('npx rolldown -c rolldown.config.ts', {
125+
function buildWithTsdown() {
126+
execSync('npx tsdown', {
206127
cwd: projectDir,
207128
stdio: 'inherit',
208129
});
209-
validateGlobalBundleExternals();
210-
}
211-
212-
/**
213-
* Scan rolldown output for unbundled workspace package imports.
214-
*
215-
* Rolldown silently externalizes imports it can't resolve (no error, no warning).
216-
* If a workspace package's dist doesn't exist at bundle time (build order race,
217-
* clean checkout, etc.), the bare specifier stays in the output. Since these
218-
* packages are devDependencies — not installed in the global CLI's node_modules —
219-
* this causes a runtime ERR_MODULE_NOT_FOUND crash.
220-
*
221-
* Fail the build loudly instead of producing a broken install.
222-
*/
223-
function validateGlobalBundleExternals() {
224-
const globalDir = join(projectDir, 'dist/global');
225-
const files = globSync('*.js', { cwd: globalDir });
226-
const errors: string[] = [];
227-
228-
for (const file of files) {
229-
const content = readFileSync(join(globalDir, file), 'utf8');
230-
const matches = content.matchAll(/\bimport\s.*?from\s+["'](@voidzero-dev\/[^"']+)["']/g);
231-
for (const match of matches) {
232-
errors.push(` ${file}: unbundled import of "${match[1]}"`);
233-
}
234-
}
235-
236-
if (errors.length > 0) {
237-
throw new Error(
238-
`Rolldown failed to bundle workspace packages in dist/global/:\n${errors.join('\n')}\n` +
239-
`Ensure these packages are built before running the CLI build.`,
240-
);
241-
}
242130
}
243131

244132
/**

packages/cli/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,9 @@
331331
"@oxc-project/types": "catalog:",
332332
"@voidzero-dev/vite-plus-core": "workspace:*",
333333
"@voidzero-dev/vite-plus-test": "workspace:*",
334-
"cac": "catalog:",
335-
"cross-spawn": "catalog:",
336-
"jsonc-parser": "catalog:",
337334
"oxfmt": "catalog:",
338335
"oxlint": "catalog:",
339-
"oxlint-tsgolint": "catalog:",
340-
"picocolors": "catalog:"
336+
"oxlint-tsgolint": "catalog:"
341337
},
342338
"devDependencies": {
343339
"@napi-rs/cli": "catalog:",
@@ -348,13 +344,16 @@
348344
"@types/validate-npm-package-name": "catalog:",
349345
"@voidzero-dev/vite-plus-prompts": "workspace:*",
350346
"@voidzero-dev/vite-plus-tools": "workspace:",
347+
"cac": "catalog:",
348+
"cross-spawn": "catalog:",
351349
"detect-indent": "catalog:",
352350
"detect-newline": "catalog:",
353351
"glob": "catalog:",
352+
"jsonc-parser": "catalog:",
354353
"lint-staged": "catalog:",
355354
"minimatch": "catalog:",
356355
"mri": "catalog:",
357-
"rolldown": "workspace:*",
356+
"picocolors": "catalog:",
358357
"rolldown-plugin-dts": "catalog:",
359358
"semver": "catalog:",
360359
"tsdown": "catalog:",

0 commit comments

Comments
 (0)