Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/build-upstream/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ runs:
id: cache-key
shell: bash
run: |
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
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

# Resolve the Rust target directory (CARGO_TARGET_DIR from setup-rust, or default "target")
- name: Resolve Rust target directory
Expand Down
79 changes: 39 additions & 40 deletions packages/cli/BUNDLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This document explains how `vite-plus` is built and how it re-exports from both

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

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

## Build Steps

### Step 1: TypeScript Compilation (`buildCli`)
### Step 1: tsdown Build (`buildWithTsdown`)

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

```typescript
const program = createProgram({
rootNames: fileNames,
options,
host,
});
program.emit();
```
**ESM build** — bundles all entry points to `dist/`:

- Public API entries: `bin`, `index`, `define-config`, `fmt`, `lint`, `pack`, `pack-bin`
- Global command entries: `create`, `migrate`, `version`, `config`, `mcp`, `staged`
- All third-party dependencies are inlined at build time
- Only packages that must be resolved at runtime stay external (NAPI binding, `@voidzero-dev/vite-plus-core`, `@voidzero-dev/vite-plus-test`, `oxfmt`, `oxlint`)
- Code splitting creates shared chunks for code used by multiple entries
- DTS (`.d.ts`) files are generated for all entries

**CJS build** — produces dual-format output for:

- `define-config.ts` → `dist/define-config.cjs`
- `index.cts` → `dist/index.cjs`

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

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

Expand Down Expand Up @@ -103,43 +108,37 @@ export * from '@voidzero-dev/vite-plus-test/browser-playwright';
```
packages/cli/
├── dist/
│ ├── index.js # Main entry (ESM)
│ ├── bin.js # CLI entry point (bundled)
│ ├── index.js # Main entry (ESM, bundled)
│ ├── index.cjs # Main entry (CJS)
│ ├── index.d.ts # Type declarations
│ ├── bin.js # CLI entry point
│ ├── define-config.js # Config helper (ESM)
│ ├── define-config.cjs # Config helper (CJS)
│ ├── define-config.d.ts
│ ├── fmt.js # Re-exports oxfmt
│ ├── lint.js # Re-exports oxlint types
│ ├── pack.js # Re-exports vite-plus-core/pack
│ ├── pack-bin.js # tsdown CLI for `vp pack`
│ ├── create.js # Global command: vp create
│ ├── migrate.js # Global command: vp migrate
│ ├── version.js # Global command: vp --version
│ ├── config.js # Global command: vp config
│ ├── mcp.js # Global command: vp mcp
│ ├── staged.js # Global command: vp staged
│ ├── *-<hash>.js # Shared chunks (code splitting)
│ ├── versions.js # Generated tool versions
│ ├── client.d.ts # ./client types (triple-slash ref)
│ ├── module-runner.js # ./module-runner shim
│ ├── module-runner.d.ts
│ ├── internal.js # ./internal shim
│ ├── internal.d.ts
│ ├── client/ # Synced client runtime files
│ │ ├── client.mjs # ESM client shim
│ │ ├── client.d.ts
│ │ ├── env.mjs
│ │ └── ...
│ ├── types/ # Synced type definitions
│ │ ├── importMeta.d.ts # Type shims (export type *)
│ │ ├── importGlob.d.ts
│ │ ├── customEvent.d.ts
│ │ └── ...
│ └── test/ # Synced test exports
│ ├── index.js # Re-exports @voidzero-dev/vite-plus-test
│ ├── index.cjs
│ ├── index.d.ts
│ ├── browser-playwright.js
│ ├── browser-playwright.d.ts
│ ├── plugins/
│ │ ├── runner.js
│ │ ├── utils.js
│ │ ├── spy.js
│ │ └── ... (33+ plugin shims)
│ └── ...
├── binding/
│ ├── index.js # NAPI binding JS wrapper
│ ├── index.d.ts # NAPI type declarations
│ └── *.node # Platform-specific binaries
└── bin/
└── vite # Shell entry point
└── vp # Shell entry point
```

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

---

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

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

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

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

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

import { execSync } from 'node:child_process';
import { existsSync, globSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { existsSync, globSync, readdirSync, statSync } from 'node:fs';
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';

import { createBuildCommand, NapiCli } from '@napi-rs/cli';
import { format } from 'oxfmt';
import {
createCompilerHost,
createProgram,
formatDiagnostics,
parseJsonSourceFileConfigFileContent,
readJsonConfigFile,
sys,
ModuleKind,
} from 'typescript';

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

if (!skipTs) {
await buildCli();
buildGlobalModules();
buildWithTsdown();
generateLicenseFile({
title: 'Vite-Plus CLI license',
packageName: 'Vite-Plus',
outputPath: join(projectDir, 'LICENSE'),
coreLicensePath: join(projectDir, '..', '..', 'LICENSE'),
bundledPaths: [join(projectDir, 'dist', 'global')],
bundledPaths: [join(projectDir, 'dist')],
resolveFrom: [projectDir],
});
if (!existsSync(join(projectDir, 'LICENSE'))) {
Expand Down Expand Up @@ -133,112 +122,11 @@ async function buildNapiBinding() {
}
}

async function buildCli() {
const tsconfig = readJsonConfigFile(join(projectDir, 'tsconfig.json'), sys.readFile.bind(sys));

const { options: initialOptions } = parseJsonSourceFileConfigFileContent(
tsconfig,
sys,
projectDir,
);

const options = {
...initialOptions,
noEmit: false,
outDir: join(projectDir, 'dist'),
};

const cjsHost = createCompilerHost({
...options,
module: ModuleKind.CommonJS,
});

const cjsProgram = createProgram({
rootNames: ['src/define-config.ts'],
options: {
...options,
module: ModuleKind.CommonJS,
},
host: cjsHost,
});

const { diagnostics: cjsDiagnostics } = cjsProgram.emit();

if (cjsDiagnostics.length > 0) {
console.error(formatDiagnostics(cjsDiagnostics, cjsHost));
process.exit(1);
}
await rename(
join(projectDir, 'dist/define-config.js'),
join(projectDir, 'dist/define-config.cjs'),
);

const host = createCompilerHost(options);

const program = createProgram({
rootNames: globSync('src/**/*.{ts,cts}', {
cwd: projectDir,
exclude: [
'**/*/__tests__',
// Global CLI modules — bundled by rolldown instead of tsc
'src/create/**',
'src/init/**',
'src/mcp/**',
'src/migration/**',
'src/version.ts',
'src/types/**',
],
}),
options,
host,
});

const { diagnostics } = program.emit();

if (diagnostics.length > 0) {
console.error(formatDiagnostics(diagnostics, host));
process.exit(1);
}
}

function buildGlobalModules() {
execSync('npx rolldown -c rolldown.config.ts', {
function buildWithTsdown() {
execSync('npx tsdown', {
cwd: projectDir,
stdio: 'inherit',
});
validateGlobalBundleExternals();
}

/**
* Scan rolldown output for unbundled workspace package imports.
*
* Rolldown silently externalizes imports it can't resolve (no error, no warning).
* If a workspace package's dist doesn't exist at bundle time (build order race,
* clean checkout, etc.), the bare specifier stays in the output. Since these
* packages are devDependencies — not installed in the global CLI's node_modules —
* this causes a runtime ERR_MODULE_NOT_FOUND crash.
*
* Fail the build loudly instead of producing a broken install.
*/
function validateGlobalBundleExternals() {
const globalDir = join(projectDir, 'dist/global');
const files = globSync('*.js', { cwd: globalDir });
const errors: string[] = [];

for (const file of files) {
const content = readFileSync(join(globalDir, file), 'utf8');
const matches = content.matchAll(/\bimport\s.*?from\s+["'](@voidzero-dev\/[^"']+)["']/g);
for (const match of matches) {
errors.push(` ${file}: unbundled import of "${match[1]}"`);
}
}

if (errors.length > 0) {
throw new Error(
`Rolldown failed to bundle workspace packages in dist/global/:\n${errors.join('\n')}\n` +
`Ensure these packages are built before running the CLI build.`,
);
}
}

/**
Expand Down
11 changes: 5 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,9 @@
"@oxc-project/types": "catalog:",
"@voidzero-dev/vite-plus-core": "workspace:*",
"@voidzero-dev/vite-plus-test": "workspace:*",
"cac": "catalog:",
"cross-spawn": "catalog:",
"jsonc-parser": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"picocolors": "catalog:"
"oxlint-tsgolint": "catalog:"
},
"devDependencies": {
"@napi-rs/cli": "catalog:",
Expand All @@ -348,13 +344,16 @@
"@types/validate-npm-package-name": "catalog:",
"@voidzero-dev/vite-plus-prompts": "workspace:*",
"@voidzero-dev/vite-plus-tools": "workspace:",
"cac": "catalog:",
"cross-spawn": "catalog:",
"detect-indent": "catalog:",
"detect-newline": "catalog:",
"glob": "catalog:",
"jsonc-parser": "catalog:",
"lint-staged": "catalog:",
"minimatch": "catalog:",
"mri": "catalog:",
"rolldown": "workspace:*",
"picocolors": "catalog:",
"rolldown-plugin-dts": "catalog:",
"semver": "catalog:",
"tsdown": "catalog:",
Expand Down
Loading
Loading