From 7113fd34466db97f9e70b00e9ce144f32cc5e59a Mon Sep 17 00:00:00 2001 From: salnika Date: Sun, 22 Mar 2026 22:29:17 +0100 Subject: [PATCH 1/2] feat(dev): simplify local cli workflow --- CLAUDE.md | 11 +- CONTRIBUTING.md | 17 ++- package.json | 7 +- packages/cli/package.json | 4 +- packages/prompts/package.json | 1 + packages/tools/src/index.ts | 14 +- packages/tools/src/local-cli.ts | 260 ++++++++++++++++++++++++++++++++ packages/tools/src/snap-test.ts | 15 +- pnpm-lock.yaml | 3 + 9 files changed, 317 insertions(+), 15 deletions(-) create mode 100644 packages/tools/src/local-cli.ts diff --git a/CLAUDE.md b/CLAUDE.md index 10f95666d0..4cb09ac268 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,10 +108,13 @@ All user-facing output must go through shared output modules instead of raw prin ## Build -- Run `pnpm bootstrap-cli` from the project root to build all packages and install the global CLI +- Run `pnpm build:cli` from the project root for normal local development - This builds all `@voidzero-dev/*` and `vite-plus` packages - - Compiles the Rust NAPI bindings and the `vp` Rust binary - - Installs the CLI globally to `~/.vite-plus/` + - Compiles the Rust `vp` and `vite_trampoline` binaries in `debug` + - Uses only repo-local artifacts (`packages/cli/dist`, `packages/test/dist`, `target/debug/vp`) + - Assumes the local `rolldown/` and `vite/` checkouts already exist; use `just init` or `node packages/tools/src/index.ts sync-remote` to prepare them +- Run `pnpm bootstrap-cli` only when you need to validate the global install flow + - This performs the release build and installs the CLI globally to `~/.vite-plus/` ## Snap Tests @@ -135,4 +138,6 @@ pnpm -F vite-plus snap-test-global pnpm -F vite-plus snap-test-global ``` +Global CLI snap tests use the repo-local debug binary and do not require `~/.vite-plus/bin`. + 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1601369b37..41607f62e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,17 @@ To create a release build of Vite+ and all upstream dependencies, run: just build ``` +## Local CLI workflow + +``` +pnpm install +pnpm build:cli +pnpm test +``` + +This installs dependencies, builds the repo-local CLI artifacts, and runs tests without reading `~/.vite-plus`. +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. + ## Install the Vite+ Global CLI from source code ``` @@ -60,14 +71,14 @@ pnpm bootstrap-cli vp --version ``` -This builds all packages, compiles the Rust `vp` binary, and installs the CLI to `~/.vite-plus`. +Use this only when you specifically want to validate the install flow or the globally installed CLI. ## Workflow for build and test You can run this command to build, test and check if there are any snapshot changes: ``` -pnpm bootstrap-cli && pnpm test && git status +pnpm build:cli && pnpm test && git status ``` ## Running Snap Tests @@ -87,6 +98,8 @@ pnpm -F vite-plus snap-test-global pnpm -F vite-plus snap-test-global ``` +Global CLI snap tests use the repo-local debug binary and `packages/cli/dist`; they do not require `~/.vite-plus/bin`. + Snap tests auto-generate `snap.txt` files. Check `git diff` to verify output changes are correct. ## Verified Commits diff --git a/package.json b/package.json index 98018d0aad..b017122911 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,15 @@ "type": "module", "scripts": { "build": "pnpm -F @voidzero-dev/* -F vite-plus build", - "bootstrap-cli": "pnpm build && cargo build -p vite_global_cli -p vite_trampoline --release && pnpm install-global-cli", + "build:cli": "tool build-local-cli", + "bootstrap-cli": "tool build-local-cli --release-rust && pnpm install-global-cli", "bootstrap-cli:ci": "pnpm install-global-cli", "install-global-cli": "tool install-global-cli", "tsgo": "tsgo -b tsconfig.json", "lint": "vp lint --type-aware --type-check --threads 4", - "test": "vp test run && pnpm -r snap-test", + "test": "pnpm build:cli && pnpm -F vite-plus test && pnpm -F vite-plus snap-test", "fmt": "vp fmt", - "test:unit": "vp test run", + "test:unit": "pnpm build:cli && pnpm -F vite-plus test", "docs:dev": "pnpm -C docs dev", "docs:build": "pnpm -C docs build", "prepare": "husky" diff --git a/packages/cli/package.json b/packages/cli/package.json index 8a80c24255..26641dd8b8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -312,9 +312,9 @@ "build-native": "oxnode -C dev ./build.ts --skip-ts", "snap-test": "pnpm snap-test-local && pnpm snap-test-global", "snap-test-local": "tool snap-test", - "snap-test-global": "tool snap-test --dir snap-tests-global --bin-dir ~/.vite-plus/bin", + "snap-test-global": "tool snap-test-global-local", "publish-native": "node ./publish-native-addons.ts", - "test": "vitest run" + "test": "tool local-cli test run" }, "dependencies": { "@oxc-project/types": "catalog:", diff --git a/packages/prompts/package.json b/packages/prompts/package.json index 1b9de2eb29..5078fa81fd 100644 --- a/packages/prompts/package.json +++ b/packages/prompts/package.json @@ -35,6 +35,7 @@ "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "is-unicode-supported": "^1.3.0", + "rolldown": "workspace:*", "tsdown": "catalog:" } } diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 5a6fa9b58e..1aa106db9e 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -25,6 +25,18 @@ switch (subcommand) { const { installGlobalCli } = await import('./install-global-cli.ts'); installGlobalCli(); break; + case 'build-local-cli': + const { runBuildLocalCli } = await import('./local-cli.ts'); + runBuildLocalCli(process.argv.slice(3)); + break; + case 'local-cli': + const { runLocalCli } = await import('./local-cli.ts'); + runLocalCli(process.argv.slice(3)); + break; + case 'snap-test-global-local': + const { runLocalGlobalSnapTest } = await import('./local-cli.ts'); + runLocalGlobalSnapTest(process.argv.slice(3)); + break; case 'brand-vite': const { brandVite } = await import('./brand-vite.ts'); brandVite(); @@ -32,7 +44,7 @@ switch (subcommand) { default: console.error(`Unknown subcommand: ${subcommand}`); console.error( - 'Available subcommands: snap-test, replace-file-content, sync-remote, json-sort, merge-peer-deps, install-global-cli, brand-vite', + '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', ); process.exit(1); } diff --git a/packages/tools/src/local-cli.ts b/packages/tools/src/local-cli.ts new file mode 100644 index 0000000000..86559edee0 --- /dev/null +++ b/packages/tools/src/local-cli.ts @@ -0,0 +1,260 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const isWindows = process.platform === 'win32'; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); +const cliDistDir = path.join(repoRoot, 'packages', 'cli', 'dist'); +const cliBinPath = path.join(cliDistDir, 'bin.js'); +const testCliPath = path.join(repoRoot, 'packages', 'test', 'dist', 'cli.js'); +const localVpPath = path.join(repoRoot, 'target', 'debug', isWindows ? 'vp.exe' : 'vp'); +const localVpBinDir = path.dirname(localVpPath); +const viteRepoDir = path.join(repoRoot, 'vite'); +const legacyViteRepoDir = path.join(repoRoot, 'rolldown-vite'); +const toolBinPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bin.js'); +const buildHint = 'pnpm build:cli'; +const pnpmExecPath = process.env.npm_execpath; +const pnpmBin = isWindows ? 'pnpm.cmd' : 'pnpm'; +const cargoBin = isWindows ? 'cargo.exe' : 'cargo'; +const requireFromRolldown = createRequire( + path.join(repoRoot, 'rolldown', 'packages', 'rolldown', 'package.json'), +); + +type CommandOptions = { + cwd?: string; + env?: NodeJS.ProcessEnv; + hint?: string; +}; + +function failMissing(pathname: string, description: string): never { + console.error(`Missing ${description}: ${pathname}`); + console.error(`Run "${buildHint}" first.`); + process.exit(1); +} + +function ensureLocalCliReady(options?: { needsTestCli?: boolean }) { + if (!existsSync(cliBinPath)) { + failMissing(cliBinPath, 'local CLI bundle'); + } + if (!existsSync(localVpPath)) { + failMissing(localVpPath, 'local debug vp binary'); + } + if (options?.needsTestCli && !existsSync(testCliPath)) { + failMissing(testCliPath, 'local test CLI bundle'); + } +} + +function localCliEnv(): NodeJS.ProcessEnv { + return { + ...process.env, + VITE_GLOBAL_CLI_JS_SCRIPTS_DIR: cliDistDir, + }; +} + +function rolldownBindingCandidates() { + switch (process.platform) { + case 'android': + if (process.arch === 'arm64') return ['@rolldown/binding-android-arm64/package.json']; + if (process.arch === 'arm') return ['@rolldown/binding-android-arm-eabi/package.json']; + return []; + case 'darwin': + if (process.arch === 'arm64') { + return [ + '@rolldown/binding-darwin-universal/package.json', + '@rolldown/binding-darwin-arm64/package.json', + ]; + } + if (process.arch === 'x64') { + return [ + '@rolldown/binding-darwin-universal/package.json', + '@rolldown/binding-darwin-x64/package.json', + ]; + } + return []; + case 'freebsd': + if (process.arch === 'arm64') return ['@rolldown/binding-freebsd-arm64/package.json']; + if (process.arch === 'x64') return ['@rolldown/binding-freebsd-x64/package.json']; + return []; + case 'linux': + if (process.arch === 'arm') { + return [ + '@rolldown/binding-linux-arm-gnueabihf/package.json', + '@rolldown/binding-linux-arm-musleabihf/package.json', + ]; + } + if (process.arch === 'arm64') { + return [ + '@rolldown/binding-linux-arm64-gnu/package.json', + '@rolldown/binding-linux-arm64-musl/package.json', + ]; + } + if (process.arch === 'loong64') { + return [ + '@rolldown/binding-linux-loong64-gnu/package.json', + '@rolldown/binding-linux-loong64-musl/package.json', + ]; + } + if (process.arch === 'ppc64') return ['@rolldown/binding-linux-ppc64-gnu/package.json']; + if (process.arch === 'riscv64') { + return [ + '@rolldown/binding-linux-riscv64-gnu/package.json', + '@rolldown/binding-linux-riscv64-musl/package.json', + ]; + } + if (process.arch === 's390x') return ['@rolldown/binding-linux-s390x-gnu/package.json']; + if (process.arch === 'x64') { + return [ + '@rolldown/binding-linux-x64-gnu/package.json', + '@rolldown/binding-linux-x64-musl/package.json', + ]; + } + return []; + case 'win32': + if (process.arch === 'arm64') return ['@rolldown/binding-win32-arm64-msvc/package.json']; + if (process.arch === 'ia32') return ['@rolldown/binding-win32-ia32-msvc/package.json']; + if (process.arch === 'x64') { + return [ + '@rolldown/binding-win32-x64-msvc/package.json', + '@rolldown/binding-win32-x64-gnu/package.json', + ]; + } + return []; + default: + return []; + } +} + +function ensureBuildWorkspaceReady() { + if (!existsSync(viteRepoDir)) { + console.error(`Missing local vite checkout: ${viteRepoDir}`); + if (existsSync(legacyViteRepoDir)) { + console.error( + `Found legacy checkout at ${legacyViteRepoDir}. This repo now expects the upstream Vite checkout at ./vite.`, + ); + console.error( + 'Run "node packages/tools/src/index.ts sync-remote" to recreate the canonical layout.', + ); + } else { + console.error( + 'Run "node packages/tools/src/index.ts sync-remote" to fetch the local upstream checkouts required for development.', + ); + } + process.exit(1); + } + + const candidates = rolldownBindingCandidates(); + if (candidates.length === 0) { + return; + } + + for (const candidate of candidates) { + try { + requireFromRolldown.resolve(candidate); + return; + } catch { + continue; + } + } + + console.error('Missing local rolldown native binding dependency.'); + console.error('Run "pnpm install" from the repo root to install workspace optional dependencies.'); + console.error('If your environment cannot download the prebuilt binding, install "cmake" to build rolldown from source.'); + process.exit(1); +} + +function runCommand(step: string, command: string, args: string[], options: CommandOptions = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd ?? repoRoot, + env: options.env ?? process.env, + stdio: 'inherit', + }); + + if (!result.error && result.status === 0) { + return; + } + + console.error(`\n${step} failed.`); + if (result.error) { + console.error(result.error.message); + } + if (options.hint) { + console.error(options.hint); + } + process.exit(result.status ?? 1); +} + +function runPnpmCommand(step: string, args: string[], options: CommandOptions = {}) { + const baseArgs = pnpmExecPath ? [pnpmExecPath] : []; + const command = pnpmExecPath ? process.execPath : pnpmBin; + + runCommand(step, command, [...baseArgs, ...args], options); +} + +function exitWith(result: ReturnType): never { + if (result.error) { + console.error(result.error.message); + process.exit(1); + } + process.exit(result.status ?? 1); +} + +export function runLocalCli(args: string[]) { + ensureLocalCliReady({ needsTestCli: args[0] === 'test' }); + + const result = spawnSync(localVpPath, args, { + cwd: process.cwd(), + env: localCliEnv(), + stdio: 'inherit', + }); + exitWith(result); +} + +export function runLocalGlobalSnapTest(args: string[]) { + ensureLocalCliReady(); + + const result = spawnSync( + process.execPath, + [toolBinPath, 'snap-test', '--dir', 'snap-tests-global', '--bin-dir', localVpBinDir, ...args], + { + cwd: process.cwd(), + env: localCliEnv(), + stdio: 'inherit', + }, + ); + exitWith(result); +} + +export function runBuildLocalCli(args: string[]) { + const releaseRust = args.includes('--release-rust'); + const localBuildEnv = { + ...process.env, + VITE_PLUS_CLI_DEBUG: '1', + }; + + mkdirSync(path.join(repoRoot, 'tmp'), { recursive: true }); + ensureBuildWorkspaceReady(); + + runPnpmCommand('Build @rolldown/pluginutils', ['--filter', '@rolldown/pluginutils', 'build']); + runPnpmCommand('Build rolldown JS glue', ['--filter', 'rolldown', 'build-node'], { + 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.', + }); + runPnpmCommand('Build vite rolled-up types', ['-C', 'vite', '--filter', 'vite', 'build-types-roll'], { + hint: 'If this fails because vite dependencies are missing, rerun "pnpm install" from the repo root.', + }); + runPnpmCommand('Type-check vite declarations', ['-C', 'vite', '--filter', 'vite', 'build-types-check'], { + hint: 'If this fails because vite dependencies are missing, rerun "pnpm install" from the repo root.', + }); + runPnpmCommand('Build vite-plus core', ['--filter', '@voidzero-dev/vite-plus-core', 'build']); + runPnpmCommand('Build vite-plus test', ['--filter', '@voidzero-dev/vite-plus-test', 'build']); + runPnpmCommand('Build vite-plus prompts', ['--filter', '@voidzero-dev/vite-plus-prompts', 'build']); + runPnpmCommand('Build vite-plus CLI', ['--filter', 'vite-plus', 'build'], { + env: releaseRust ? process.env : localBuildEnv, + }); + runCommand( + 'Build Rust CLI binaries', + cargoBin, + ['build', '-p', 'vite_global_cli', '-p', 'vite_trampoline', ...(releaseRust ? ['--release'] : [])], + ); +} diff --git a/packages/tools/src/snap-test.ts b/packages/tools/src/snap-test.ts index 58a326736a..dd9fd40b9b 100755 --- a/packages/tools/src/snap-test.ts +++ b/packages/tools/src/snap-test.ts @@ -96,7 +96,8 @@ export async function snapTest() { } } - const vitePlusHome = path.join(homedir(), '.vite-plus'); + const vitePlusHome = path.join(tempTmpDir, 'vite-plus-home'); + fs.mkdirSync(vitePlusHome, { recursive: true }); // Remove .previous-version so command-upgrade-rollback snap test is stable const previousVersionPath = path.join(vitePlusHome, '.previous-version'); @@ -159,7 +160,7 @@ export async function snapTest() { } if (caseName.includes(filter)) { const steps: Steps = JSON.parse(readFileSync(stepsPath, 'utf-8')); - const task = () => runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir']); + const task = () => runTestCase(caseName, tempTmpDir, casesDir, vitePlusHome, values['bin-dir']); if (steps.serial) { serialTasks.push(task); } else { @@ -221,7 +222,13 @@ interface Steps { serial?: boolean; } -async function runTestCase(name: string, tempTmpDir: string, casesDir: string, binDir?: string) { +async function runTestCase( + name: string, + tempTmpDir: string, + casesDir: string, + vitePlusHome: string, + binDir?: string, +) { const steps: Steps = JSON.parse( await fsPromises.readFile(`${casesDir}/${name}/steps.json`, 'utf-8'), ); @@ -248,7 +255,7 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b NO_COLOR: 'true', // set CI=true make sure snap-tests are stable on GitHub Actions CI: 'true', - VITE_PLUS_HOME: path.join(homedir(), '.vite-plus'), + VITE_PLUS_HOME: vitePlusHome, // Set git identity so `git commit` works on CI runners without global git config GIT_AUTHOR_NAME: 'Test', GIT_COMMITTER_NAME: 'Test', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e037b04bde..30c0657d4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -587,6 +587,9 @@ importers: is-unicode-supported: specifier: ^1.3.0 version: 1.3.0 + rolldown: + specifier: workspace:rolldown@* + version: link:../../rolldown/packages/rolldown tsdown: specifier: 'catalog:' version: 0.21.4(@arethetypeswrong/core@0.18.2)(@tsdown/css@0.21.4)(@tsdown/exe@0.21.4)(@typescript/native-preview@7.0.0-dev.20260122.2)(@vitejs/devtools@0.1.3(@pnpm/logger@1001.0.1)(typescript@5.9.3)(vite@packages+core)(vue@3.5.27(typescript@5.9.3)))(oxc-resolver@11.14.0)(publint@0.3.18)(typescript@5.9.3)(unplugin-unused@0.5.6) From 63da70a73a966230418a2dc0079b872adace6478 Mon Sep 17 00:00:00 2001 From: salnika Date: Sun, 22 Mar 2026 23:03:00 +0100 Subject: [PATCH 2/2] feat(dev): add post-clone setup and bootstrap flow --- CONTRIBUTING.md | 22 ++- justfile | 3 +- package.json | 4 +- packages/tools/src/local-cli.ts | 89 ++++++++--- scripts/setup-local-dev.mjs | 255 ++++++++++++++++++++++++++++++++ 5 files changed, 338 insertions(+), 35 deletions(-) create mode 100644 scripts/setup-local-dev.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41607f62e2..8307ac84f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,10 +17,10 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh cargo install cargo-binstall ``` -Initial setup to install dependencies for Vite+: +Initial setup to prepare the repo for local development: ``` -just init +pnpm install:dev ``` ### Windows @@ -37,10 +37,10 @@ Install Rust & Cargo from [rustup.rs](https://rustup.rs/), then install `cargo-b cargo install cargo-binstall ``` -Initial setup to install dependencies for Vite+: +Initial setup to prepare the repo for local development: ```powershell -just init +pnpm install:dev ``` **Note:** Run commands in PowerShell or Windows Terminal. Some commands may require elevated permissions. @@ -56,13 +56,19 @@ just build ## Local CLI workflow ``` -pnpm install -pnpm build:cli +pnpm bootstrap:dev pnpm test ``` -This installs dependencies, builds the repo-local CLI artifacts, and runs tests without reading `~/.vite-plus`. -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. +This prepares the local `rolldown/` and `vite/` checkouts, installs dependencies, builds the repo-local CLI artifacts, and runs tests without reading `~/.vite-plus`. + +If you only want to prepare the repo after cloning it, run: + +``` +pnpm install:dev +``` + +If you prefer the existing Just-based setup, `just init` now delegates to the same repo-local install flow. ## Install the Vite+ Global CLI from source code diff --git a/justfile b/justfile index 67dd2e2203..b1ee9a82b7 100644 --- a/justfile +++ b/justfile @@ -18,8 +18,7 @@ _clean_dist: init: _clean_dist cargo binstall watchexec-cli cargo-insta typos-cli cargo-shear dprint taplo-cli -y - node packages/tools/src/index.ts sync-remote - pnpm install + pnpm install:dev pnpm -C docs install build: diff --git a/package.json b/package.json index b017122911..1f404924b2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "type": "module", "scripts": { "build": "pnpm -F @voidzero-dev/* -F vite-plus build", + "install:dev": "node scripts/setup-local-dev.mjs", + "bootstrap:dev": "pnpm install:dev && pnpm build:cli", "build:cli": "tool build-local-cli", - "bootstrap-cli": "tool build-local-cli --release-rust && pnpm install-global-cli", + "bootstrap-cli": "pnpm install:dev && tool build-local-cli --release-rust && pnpm install-global-cli", "bootstrap-cli:ci": "pnpm install-global-cli", "install-global-cli": "tool install-global-cli", "tsgo": "tsgo -b tsconfig.json", diff --git a/packages/tools/src/local-cli.ts b/packages/tools/src/local-cli.ts index 86559edee0..54813ab282 100644 --- a/packages/tools/src/local-cli.ts +++ b/packages/tools/src/local-cli.ts @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync } from 'node:fs'; +import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -13,8 +13,11 @@ const localVpPath = path.join(repoRoot, 'target', 'debug', isWindows ? 'vp.exe' const localVpBinDir = path.dirname(localVpPath); const viteRepoDir = path.join(repoRoot, 'vite'); const legacyViteRepoDir = path.join(repoRoot, 'rolldown-vite'); +const rolldownSrcDir = path.join(repoRoot, 'rolldown', 'packages', 'rolldown', 'src'); const toolBinPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bin.js'); const buildHint = 'pnpm build:cli'; +const bootstrapHint = 'pnpm bootstrap:dev'; +const installHint = 'pnpm install:dev'; const pnpmExecPath = process.env.npm_execpath; const pnpmBin = isWindows ? 'pnpm.cmd' : 'pnpm'; const cargoBin = isWindows ? 'cargo.exe' : 'cargo'; @@ -30,7 +33,7 @@ type CommandOptions = { function failMissing(pathname: string, description: string): never { console.error(`Missing ${description}: ${pathname}`); - console.error(`Run "${buildHint}" first.`); + console.error(`Run "${bootstrapHint}" from a fresh clone, or "${buildHint}" after setup.`); process.exit(1); } @@ -126,6 +129,51 @@ function rolldownBindingCandidates() { } } +function hasRolldownPackagedBinding() { + const candidates = rolldownBindingCandidates(); + if (candidates.length === 0) { + return true; + } + + for (const candidate of candidates) { + try { + requireFromRolldown.resolve(candidate); + return true; + } catch { + continue; + } + } + + return false; +} + +function materializeRolldownPackagedBindings() { + for (const candidate of rolldownBindingCandidates()) { + let packageJsonPath: string; + try { + packageJsonPath = requireFromRolldown.resolve(candidate); + } catch { + continue; + } + + const packageDir = path.dirname(packageJsonPath); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { + files?: string[]; + main?: string; + }; + const bindingFile = packageJson.main ?? packageJson.files?.find((file) => file.endsWith('.node')); + if (!bindingFile) { + continue; + } + + const sourcePath = path.join(packageDir, bindingFile); + const targetPath = path.join(rolldownSrcDir, path.basename(bindingFile)); + if (!existsSync(targetPath)) { + copyFileSync(sourcePath, targetPath); + } + } +} + function ensureBuildWorkspaceReady() { if (!existsSync(viteRepoDir)) { console.error(`Missing local vite checkout: ${viteRepoDir}`); @@ -134,34 +182,15 @@ function ensureBuildWorkspaceReady() { `Found legacy checkout at ${legacyViteRepoDir}. This repo now expects the upstream Vite checkout at ./vite.`, ); console.error( - 'Run "node packages/tools/src/index.ts sync-remote" to recreate the canonical layout.', + `Run "${installHint}" to recreate the canonical layout.`, ); } else { console.error( - 'Run "node packages/tools/src/index.ts sync-remote" to fetch the local upstream checkouts required for development.', + `Run "${installHint}" to fetch the local upstream checkouts, or "${bootstrapHint}" to prepare and build the local CLI.`, ); } process.exit(1); } - - const candidates = rolldownBindingCandidates(); - if (candidates.length === 0) { - return; - } - - for (const candidate of candidates) { - try { - requireFromRolldown.resolve(candidate); - return; - } catch { - continue; - } - } - - console.error('Missing local rolldown native binding dependency.'); - console.error('Run "pnpm install" from the repo root to install workspace optional dependencies.'); - console.error('If your environment cannot download the prebuilt binding, install "cmake" to build rolldown from source.'); - process.exit(1); } function runCommand(step: string, command: string, args: string[], options: CommandOptions = {}) { @@ -237,8 +266,20 @@ export function runBuildLocalCli(args: string[]) { ensureBuildWorkspaceReady(); runPnpmCommand('Build @rolldown/pluginutils', ['--filter', '@rolldown/pluginutils', 'build']); + const hasPackagedBinding = hasRolldownPackagedBinding(); + if (!hasPackagedBinding) { + runPnpmCommand( + 'Build rolldown native binding', + ['--filter', 'rolldown', releaseRust ? 'build-binding:release' : 'build-binding'], + { + hint: 'If this fails, install "cmake" so rolldown can build its native binding from source.', + }, + ); + } else { + materializeRolldownPackagedBindings(); + } runPnpmCommand('Build rolldown JS glue', ['--filter', 'rolldown', 'build-node'], { - 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.', + hint: 'If this fails with a missing rolldown native binding, rerun "pnpm install:dev". If the error mentions "cmake", install cmake to build rolldown from source.', }); runPnpmCommand('Build vite rolled-up types', ['-C', 'vite', '--filter', 'vite', 'build-types-roll'], { hint: 'If this fails because vite dependencies are missing, rerun "pnpm install" from the repo root.', diff --git a/scripts/setup-local-dev.mjs b/scripts/setup-local-dev.mjs new file mode 100644 index 0000000000..4cdeee07e7 --- /dev/null +++ b/scripts/setup-local-dev.mjs @@ -0,0 +1,255 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { existsSync, lstatSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const isWindows = process.platform === 'win32'; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const gitBin = isWindows ? 'git.exe' : 'git'; +const pnpmBin = isWindows ? 'pnpm.cmd' : 'pnpm'; +const pnpmLockfilePath = path.join(repoRoot, 'pnpm-lock.yaml'); +const upstreamVersions = JSON.parse( + readFileSync(path.join(repoRoot, 'packages', 'tools', '.upstream-versions.json'), 'utf-8'), +); + +function log(message) { + console.log(`[setup-dev] ${message}`); +} + +function fail(message) { + console.error(`[setup-dev] ${message}`); + process.exit(1); +} + +function canonicalRemote(url) { + return url + .trim() + .replace(/^git@github\.com:/, 'https://github.com/') + .replace(/^ssh:\/\/git@github\.com\//, 'https://github.com/') + .replace(/\.git$/, '') + .replace(/\/$/, ''); +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd ?? repoRoot, + stdio: options.stdio ?? 'inherit', + encoding: options.encoding ?? 'utf-8', + }); + + if (result.error) { + fail(result.error.message); + } + + if (result.status !== 0) { + const rendered = [command, ...args].join(' '); + fail(`Command failed (${result.status}): ${rendered}`); + } + + return result; +} + +function capture(command, args, cwd) { + return run(command, args, { + cwd, + stdio: 'pipe', + encoding: 'utf-8', + }).stdout.trim(); +} + +function isGitRepo(dir) { + const result = spawnSync(gitBin, ['rev-parse', '--git-dir'], { + cwd: dir, + stdio: 'ignore', + }); + return result.status === 0; +} + +function isDirty(dir) { + return capture(gitBin, ['status', '--porcelain'], dir) !== ''; +} + +function ensureExpectedRemote(name, dir, repoUrl) { + const actual = canonicalRemote(capture(gitBin, ['remote', 'get-url', 'origin'], dir)); + const expected = canonicalRemote(repoUrl); + if (actual !== expected) { + fail( + `Unexpected remote for ${name}: ${actual}. Expected ${expected}. Please fix the checkout or remove ${dir} and rerun this command.`, + ); + } +} + +function cloneCheckout(name, repoUrl, branch, hash) { + log(`Cloning ${name} from ${repoUrl} (${branch})...`); + run(gitBin, ['clone', '--branch', branch, repoUrl, name]); + if (hash) { + run(gitBin, ['reset', '--hard', hash], { + cwd: path.join(repoRoot, name), + }); + } +} + +function rolldownBindingCandidates() { + switch (process.platform) { + case 'android': + if (process.arch === 'arm64') return ['@rolldown/binding-android-arm64']; + if (process.arch === 'arm') return ['@rolldown/binding-android-arm-eabi']; + return []; + case 'darwin': + if (process.arch === 'arm64') { + return ['@rolldown/binding-darwin-universal', '@rolldown/binding-darwin-arm64']; + } + if (process.arch === 'x64') { + return ['@rolldown/binding-darwin-universal', '@rolldown/binding-darwin-x64']; + } + return []; + case 'freebsd': + if (process.arch === 'arm64') return ['@rolldown/binding-freebsd-arm64']; + if (process.arch === 'x64') return ['@rolldown/binding-freebsd-x64']; + return []; + case 'linux': + if (process.arch === 'arm') { + return ['@rolldown/binding-linux-arm-gnueabihf', '@rolldown/binding-linux-arm-musleabihf']; + } + if (process.arch === 'arm64') { + return ['@rolldown/binding-linux-arm64-gnu', '@rolldown/binding-linux-arm64-musl']; + } + if (process.arch === 'loong64') { + return ['@rolldown/binding-linux-loong64-gnu', '@rolldown/binding-linux-loong64-musl']; + } + if (process.arch === 'ppc64') return ['@rolldown/binding-linux-ppc64-gnu']; + if (process.arch === 'riscv64') { + return ['@rolldown/binding-linux-riscv64-gnu', '@rolldown/binding-linux-riscv64-musl']; + } + if (process.arch === 's390x') return ['@rolldown/binding-linux-s390x-gnu']; + if (process.arch === 'x64') { + return ['@rolldown/binding-linux-x64-gnu', '@rolldown/binding-linux-x64-musl']; + } + return []; + case 'win32': + if (process.arch === 'arm64') return ['@rolldown/binding-win32-arm64-msvc']; + if (process.arch === 'ia32') return ['@rolldown/binding-win32-ia32-msvc']; + if (process.arch === 'x64') { + return ['@rolldown/binding-win32-x64-msvc', '@rolldown/binding-win32-x64-gnu']; + } + return []; + default: + return []; + } +} + +function ensureRolldownHostBindings() { + const candidates = rolldownBindingCandidates(); + if (candidates.length === 0) { + return; + } + + const packageJsonPath = path.join(repoRoot, 'rolldown', 'packages', 'rolldown', 'package.json'); + const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const optionalDependencies = { + ...(pkg.optionalDependencies ?? {}), + }; + + let changed = false; + for (const candidate of candidates) { + if (!optionalDependencies[candidate]) { + optionalDependencies[candidate] = pkg.version; + changed = true; + } + } + + if (!changed) { + return; + } + + pkg.optionalDependencies = optionalDependencies; + writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); + log(`Added host rolldown bindings to ${packageJsonPath}`); +} + +function syncCleanCheckout(name, config) { + const dir = path.join(repoRoot, name); + + if (!existsSync(dir)) { + cloneCheckout(name, config.repo, config.branch, config.hash); + return; + } + + if (lstatSync(dir).isSymbolicLink()) { + log(`Using existing symlinked ${name} checkout at ${dir}`); + return; + } + + if (!isGitRepo(dir)) { + fail(`${dir} exists but is not a git repository.`); + } + + ensureExpectedRemote(name, dir, config.repo); + + if (isDirty(dir)) { + log(`Keeping existing dirty ${name} checkout at ${dir}`); + return; + } + + log(`Updating clean ${name} checkout...`); + run(gitBin, ['fetch', 'origin', '--tags'], { cwd: dir }); + run(gitBin, ['checkout', config.branch], { cwd: dir }); + + if (config.hash) { + run(gitBin, ['reset', '--hard', config.hash], { cwd: dir }); + } else { + run(gitBin, ['reset', '--hard', `origin/${config.branch}`], { cwd: dir }); + } +} + +function migrateLegacyViteCheckout() { + const viteDir = path.join(repoRoot, 'vite'); + const legacyDir = path.join(repoRoot, 'rolldown-vite'); + + if (existsSync(viteDir) || !existsSync(legacyDir)) { + return; + } + + if (lstatSync(legacyDir).isSymbolicLink()) { + fail(`Found legacy symlinked checkout at ${legacyDir}. Remove it and rerun this command.`); + } + + if (!isGitRepo(legacyDir)) { + fail(`Found legacy directory ${legacyDir}, but it is not a git repository.`); + } + + ensureExpectedRemote('rolldown-vite', legacyDir, upstreamVersions.vite.repo); + + if (isDirty(legacyDir)) { + fail( + `Found legacy checkout at ${legacyDir} with local changes. Rename it to ./vite or clean it before rerunning this command.`, + ); + } + + log(`Migrating legacy ${legacyDir} checkout to ${viteDir}...`); + renameSync(legacyDir, viteDir); +} + +function main() { + migrateLegacyViteCheckout(); + + syncCleanCheckout('rolldown', upstreamVersions.rolldown); + syncCleanCheckout('vite', upstreamVersions.vite); + ensureRolldownHostBindings(); + + const originalLockfile = existsSync(pnpmLockfilePath) + ? readFileSync(pnpmLockfilePath, 'utf-8') + : null; + log('Installing workspace dependencies...'); + try { + run(pnpmBin, ['install']); + } finally { + if (originalLockfile !== null) { + writeFileSync(pnpmLockfilePath, originalLockfile, 'utf-8'); + } + } +} + +main();