Skip to content

Commit 5a628c0

Browse files
committed
feat(dev): simplify local cli workflow
1 parent 5f52b6c commit 5a628c0

File tree

9 files changed

+317
-15
lines changed

9 files changed

+317
-15
lines changed

CLAUDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,13 @@ All user-facing output must go through shared output modules instead of raw prin
108108

109109
## Build
110110

111-
- Run `pnpm bootstrap-cli` from the project root to build all packages and install the global CLI
111+
- Run `pnpm build:cli` from the project root for normal local development
112112
- This builds all `@voidzero-dev/*` and `vite-plus` packages
113-
- Compiles the Rust NAPI bindings and the `vp` Rust binary
114-
- Installs the CLI globally to `~/.vite-plus/`
113+
- Compiles the Rust `vp` and `vite_trampoline` binaries in `debug`
114+
- Uses only repo-local artifacts (`packages/cli/dist`, `packages/test/dist`, `target/debug/vp`)
115+
- Assumes the local `rolldown/` and `vite/` checkouts already exist; use `just init` or `node packages/tools/src/index.ts sync-remote` to prepare them
116+
- Run `pnpm bootstrap-cli` only when you need to validate the global install flow
117+
- This performs the release build and installs the CLI globally to `~/.vite-plus/`
115118

116119
## Snap Tests
117120

@@ -135,4 +138,6 @@ pnpm -F vite-plus snap-test-global
135138
pnpm -F vite-plus snap-test-global <name-filter>
136139
```
137140

141+
Global CLI snap tests use the repo-local debug binary and do not require `~/.vite-plus/bin`.
142+
138143
The snap test will automatically generate/update the `snap.txt` file with the command outputs. It exits with zero status even if there are output differences; you need to manually check the diffs(`git diff`) to verify correctness.

CONTRIBUTING.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,32 @@ To create a release build of Vite+ and all upstream dependencies, run:
5353
just build
5454
```
5555

56+
## Local CLI workflow
57+
58+
```
59+
pnpm install
60+
pnpm build:cli
61+
pnpm test
62+
```
63+
64+
This installs dependencies, builds the repo-local CLI artifacts, and runs tests without reading `~/.vite-plus`.
65+
If you have not prepared the local `rolldown/` and `vite/` checkouts yet, run `just init` or `node packages/tools/src/index.ts sync-remote` first.
66+
5667
## Install the Vite+ Global CLI from source code
5768

5869
```
5970
pnpm bootstrap-cli
6071
vp --version
6172
```
6273

63-
This builds all packages, compiles the Rust `vp` binary, and installs the CLI to `~/.vite-plus`.
74+
Use this only when you specifically want to validate the install flow or the globally installed CLI.
6475

6576
## Workflow for build and test
6677

6778
You can run this command to build, test and check if there are any snapshot changes:
6879

6980
```
70-
pnpm bootstrap-cli && pnpm test && git status
81+
pnpm build:cli && pnpm test && git status
7182
```
7283

7384
## Running Snap Tests
@@ -87,6 +98,8 @@ pnpm -F vite-plus snap-test-global
8798
pnpm -F vite-plus snap-test-global <name-filter>
8899
```
89100

101+
Global CLI snap tests use the repo-local debug binary and `packages/cli/dist`; they do not require `~/.vite-plus/bin`.
102+
90103
Snap tests auto-generate `snap.txt` files. Check `git diff` to verify output changes are correct.
91104

92105
## Verified Commits

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
"type": "module",
66
"scripts": {
77
"build": "pnpm -F @voidzero-dev/* -F vite-plus build",
8-
"bootstrap-cli": "pnpm build && cargo build -p vite_global_cli -p vite_trampoline --release && pnpm install-global-cli",
8+
"build:cli": "tool build-local-cli",
9+
"bootstrap-cli": "tool build-local-cli --release-rust && pnpm install-global-cli",
910
"bootstrap-cli:ci": "pnpm install-global-cli",
1011
"install-global-cli": "tool install-global-cli",
1112
"tsgo": "tsgo -b tsconfig.json",
1213
"lint": "vp lint --type-aware --type-check --threads 4",
13-
"test": "vp test run && pnpm -r snap-test",
14+
"test": "pnpm build:cli && pnpm -F vite-plus test && pnpm -F vite-plus snap-test",
1415
"fmt": "vp fmt",
15-
"test:unit": "vp test run",
16+
"test:unit": "pnpm build:cli && pnpm -F vite-plus test",
1617
"docs:dev": "pnpm -C docs dev",
1718
"docs:build": "pnpm -C docs build",
1819
"prepare": "husky"

packages/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@
315315
"build-native": "oxnode -C dev ./build.ts --skip-ts",
316316
"snap-test": "pnpm snap-test-local && pnpm snap-test-global",
317317
"snap-test-local": "tool snap-test",
318-
"snap-test-global": "tool snap-test --dir snap-tests-global --bin-dir ~/.vite-plus/bin",
318+
"snap-test-global": "tool snap-test-global-local",
319319
"publish-native": "node ./publish-native-addons.ts",
320-
"test": "vitest run"
320+
"test": "tool local-cli test run"
321321
},
322322
"dependencies": {
323323
"@oxc-project/types": "catalog:",

packages/prompts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"fast-string-width": "^1.1.0",
3636
"fast-wrap-ansi": "^0.1.3",
3737
"is-unicode-supported": "^1.3.0",
38+
"rolldown": "workspace:*",
3839
"tsdown": "catalog:"
3940
}
4041
}

packages/tools/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,26 @@ switch (subcommand) {
2525
const { installGlobalCli } = await import('./install-global-cli.ts');
2626
installGlobalCli();
2727
break;
28+
case 'build-local-cli':
29+
const { runBuildLocalCli } = await import('./local-cli.ts');
30+
runBuildLocalCli(process.argv.slice(3));
31+
break;
32+
case 'local-cli':
33+
const { runLocalCli } = await import('./local-cli.ts');
34+
runLocalCli(process.argv.slice(3));
35+
break;
36+
case 'snap-test-global-local':
37+
const { runLocalGlobalSnapTest } = await import('./local-cli.ts');
38+
runLocalGlobalSnapTest(process.argv.slice(3));
39+
break;
2840
case 'brand-vite':
2941
const { brandVite } = await import('./brand-vite.ts');
3042
brandVite();
3143
break;
3244
default:
3345
console.error(`Unknown subcommand: ${subcommand}`);
3446
console.error(
35-
'Available subcommands: snap-test, replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, brand-vite',
47+
'Available subcommands: snap-test, replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, build-local-cli, local-cli, snap-test-global-local, brand-vite',
3648
);
3749
process.exit(1);
3850
}

packages/tools/src/local-cli.ts

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { existsSync, mkdirSync } from 'node:fs';
3+
import { createRequire } from 'node:module';
4+
import path from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
7+
const isWindows = process.platform === 'win32';
8+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
9+
const cliDistDir = path.join(repoRoot, 'packages', 'cli', 'dist');
10+
const cliBinPath = path.join(cliDistDir, 'bin.js');
11+
const testCliPath = path.join(repoRoot, 'packages', 'test', 'dist', 'cli.js');
12+
const localVpPath = path.join(repoRoot, 'target', 'debug', isWindows ? 'vp.exe' : 'vp');
13+
const localVpBinDir = path.dirname(localVpPath);
14+
const viteRepoDir = path.join(repoRoot, 'vite');
15+
const legacyViteRepoDir = path.join(repoRoot, 'rolldown-vite');
16+
const toolBinPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bin.js');
17+
const buildHint = 'pnpm build:cli';
18+
const pnpmExecPath = process.env.npm_execpath;
19+
const pnpmBin = isWindows ? 'pnpm.cmd' : 'pnpm';
20+
const cargoBin = isWindows ? 'cargo.exe' : 'cargo';
21+
const requireFromRolldown = createRequire(
22+
path.join(repoRoot, 'rolldown', 'packages', 'rolldown', 'package.json'),
23+
);
24+
25+
type CommandOptions = {
26+
cwd?: string;
27+
env?: NodeJS.ProcessEnv;
28+
hint?: string;
29+
};
30+
31+
function failMissing(pathname: string, description: string): never {
32+
console.error(`Missing ${description}: ${pathname}`);
33+
console.error(`Run "${buildHint}" first.`);
34+
process.exit(1);
35+
}
36+
37+
function ensureLocalCliReady(options?: { needsTestCli?: boolean }) {
38+
if (!existsSync(cliBinPath)) {
39+
failMissing(cliBinPath, 'local CLI bundle');
40+
}
41+
if (!existsSync(localVpPath)) {
42+
failMissing(localVpPath, 'local debug vp binary');
43+
}
44+
if (options?.needsTestCli && !existsSync(testCliPath)) {
45+
failMissing(testCliPath, 'local test CLI bundle');
46+
}
47+
}
48+
49+
function localCliEnv(): NodeJS.ProcessEnv {
50+
return {
51+
...process.env,
52+
VITE_GLOBAL_CLI_JS_SCRIPTS_DIR: cliDistDir,
53+
};
54+
}
55+
56+
function rolldownBindingCandidates() {
57+
switch (process.platform) {
58+
case 'android':
59+
if (process.arch === 'arm64') return ['@rolldown/binding-android-arm64/package.json'];
60+
if (process.arch === 'arm') return ['@rolldown/binding-android-arm-eabi/package.json'];
61+
return [];
62+
case 'darwin':
63+
if (process.arch === 'arm64') {
64+
return [
65+
'@rolldown/binding-darwin-universal/package.json',
66+
'@rolldown/binding-darwin-arm64/package.json',
67+
];
68+
}
69+
if (process.arch === 'x64') {
70+
return [
71+
'@rolldown/binding-darwin-universal/package.json',
72+
'@rolldown/binding-darwin-x64/package.json',
73+
];
74+
}
75+
return [];
76+
case 'freebsd':
77+
if (process.arch === 'arm64') return ['@rolldown/binding-freebsd-arm64/package.json'];
78+
if (process.arch === 'x64') return ['@rolldown/binding-freebsd-x64/package.json'];
79+
return [];
80+
case 'linux':
81+
if (process.arch === 'arm') {
82+
return [
83+
'@rolldown/binding-linux-arm-gnueabihf/package.json',
84+
'@rolldown/binding-linux-arm-musleabihf/package.json',
85+
];
86+
}
87+
if (process.arch === 'arm64') {
88+
return [
89+
'@rolldown/binding-linux-arm64-gnu/package.json',
90+
'@rolldown/binding-linux-arm64-musl/package.json',
91+
];
92+
}
93+
if (process.arch === 'loong64') {
94+
return [
95+
'@rolldown/binding-linux-loong64-gnu/package.json',
96+
'@rolldown/binding-linux-loong64-musl/package.json',
97+
];
98+
}
99+
if (process.arch === 'ppc64') return ['@rolldown/binding-linux-ppc64-gnu/package.json'];
100+
if (process.arch === 'riscv64') {
101+
return [
102+
'@rolldown/binding-linux-riscv64-gnu/package.json',
103+
'@rolldown/binding-linux-riscv64-musl/package.json',
104+
];
105+
}
106+
if (process.arch === 's390x') return ['@rolldown/binding-linux-s390x-gnu/package.json'];
107+
if (process.arch === 'x64') {
108+
return [
109+
'@rolldown/binding-linux-x64-gnu/package.json',
110+
'@rolldown/binding-linux-x64-musl/package.json',
111+
];
112+
}
113+
return [];
114+
case 'win32':
115+
if (process.arch === 'arm64') return ['@rolldown/binding-win32-arm64-msvc/package.json'];
116+
if (process.arch === 'ia32') return ['@rolldown/binding-win32-ia32-msvc/package.json'];
117+
if (process.arch === 'x64') {
118+
return [
119+
'@rolldown/binding-win32-x64-msvc/package.json',
120+
'@rolldown/binding-win32-x64-gnu/package.json',
121+
];
122+
}
123+
return [];
124+
default:
125+
return [];
126+
}
127+
}
128+
129+
function ensureBuildWorkspaceReady() {
130+
if (!existsSync(viteRepoDir)) {
131+
console.error(`Missing local vite checkout: ${viteRepoDir}`);
132+
if (existsSync(legacyViteRepoDir)) {
133+
console.error(
134+
`Found legacy checkout at ${legacyViteRepoDir}. This repo now expects the upstream Vite checkout at ./vite.`,
135+
);
136+
console.error(
137+
'Run "node packages/tools/src/index.ts sync-remote" to recreate the canonical layout.',
138+
);
139+
} else {
140+
console.error(
141+
'Run "node packages/tools/src/index.ts sync-remote" to fetch the local upstream checkouts required for development.',
142+
);
143+
}
144+
process.exit(1);
145+
}
146+
147+
const candidates = rolldownBindingCandidates();
148+
if (candidates.length === 0) {
149+
return;
150+
}
151+
152+
for (const candidate of candidates) {
153+
try {
154+
requireFromRolldown.resolve(candidate);
155+
return;
156+
} catch {
157+
continue;
158+
}
159+
}
160+
161+
console.error('Missing local rolldown native binding dependency.');
162+
console.error('Run "pnpm install" from the repo root to install workspace optional dependencies.');
163+
console.error('If your environment cannot download the prebuilt binding, install "cmake" to build rolldown from source.');
164+
process.exit(1);
165+
}
166+
167+
function runCommand(step: string, command: string, args: string[], options: CommandOptions = {}) {
168+
const result = spawnSync(command, args, {
169+
cwd: options.cwd ?? repoRoot,
170+
env: options.env ?? process.env,
171+
stdio: 'inherit',
172+
});
173+
174+
if (!result.error && result.status === 0) {
175+
return;
176+
}
177+
178+
console.error(`\n${step} failed.`);
179+
if (result.error) {
180+
console.error(result.error.message);
181+
}
182+
if (options.hint) {
183+
console.error(options.hint);
184+
}
185+
process.exit(result.status ?? 1);
186+
}
187+
188+
function runPnpmCommand(step: string, args: string[], options: CommandOptions = {}) {
189+
const baseArgs = pnpmExecPath ? [pnpmExecPath] : [];
190+
const command = pnpmExecPath ? process.execPath : pnpmBin;
191+
192+
runCommand(step, command, [...baseArgs, ...args], options);
193+
}
194+
195+
function exitWith(result: ReturnType<typeof spawnSync>): never {
196+
if (result.error) {
197+
console.error(result.error.message);
198+
process.exit(1);
199+
}
200+
process.exit(result.status ?? 1);
201+
}
202+
203+
export function runLocalCli(args: string[]) {
204+
ensureLocalCliReady({ needsTestCli: args[0] === 'test' });
205+
206+
const result = spawnSync(localVpPath, args, {
207+
cwd: process.cwd(),
208+
env: localCliEnv(),
209+
stdio: 'inherit',
210+
});
211+
exitWith(result);
212+
}
213+
214+
export function runLocalGlobalSnapTest(args: string[]) {
215+
ensureLocalCliReady();
216+
217+
const result = spawnSync(
218+
process.execPath,
219+
[toolBinPath, 'snap-test', '--dir', 'snap-tests-global', '--bin-dir', localVpBinDir, ...args],
220+
{
221+
cwd: process.cwd(),
222+
env: localCliEnv(),
223+
stdio: 'inherit',
224+
},
225+
);
226+
exitWith(result);
227+
}
228+
229+
export function runBuildLocalCli(args: string[]) {
230+
const releaseRust = args.includes('--release-rust');
231+
const localBuildEnv = {
232+
...process.env,
233+
VITE_PLUS_CLI_DEBUG: '1',
234+
};
235+
236+
mkdirSync(path.join(repoRoot, 'tmp'), { recursive: true });
237+
ensureBuildWorkspaceReady();
238+
239+
runPnpmCommand('Build @rolldown/pluginutils', ['--filter', '@rolldown/pluginutils', 'build']);
240+
runPnpmCommand('Build rolldown JS glue', ['--filter', 'rolldown', 'build-node'], {
241+
hint: 'If this fails with a missing rolldown native binding, rerun "pnpm install". If the error mentions "cmake", install cmake to build rolldown from source.',
242+
});
243+
runPnpmCommand('Build vite rolled-up types', ['-C', 'vite', '--filter', 'vite', 'build-types-roll'], {
244+
hint: 'If this fails because vite dependencies are missing, rerun "pnpm install" from the repo root.',
245+
});
246+
runPnpmCommand('Type-check vite declarations', ['-C', 'vite', '--filter', 'vite', 'build-types-check'], {
247+
hint: 'If this fails because vite dependencies are missing, rerun "pnpm install" from the repo root.',
248+
});
249+
runPnpmCommand('Build vite-plus core', ['--filter', '@voidzero-dev/vite-plus-core', 'build']);
250+
runPnpmCommand('Build vite-plus test', ['--filter', '@voidzero-dev/vite-plus-test', 'build']);
251+
runPnpmCommand('Build vite-plus prompts', ['--filter', '@voidzero-dev/vite-plus-prompts', 'build']);
252+
runPnpmCommand('Build vite-plus CLI', ['--filter', 'vite-plus', 'build'], {
253+
env: releaseRust ? process.env : localBuildEnv,
254+
});
255+
runCommand(
256+
'Build Rust CLI binaries',
257+
cargoBin,
258+
['build', '-p', 'vite_global_cli', '-p', 'vite_trampoline', ...(releaseRust ? ['--release'] : [])],
259+
);
260+
}

0 commit comments

Comments
 (0)