From 5a89f9b119d5c03a2b0fefe99bf2638ab01645c9 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 15 May 2026 15:03:00 +0800 Subject: [PATCH 1/7] fix bootstrap cli on Windows --- packages/core/build.ts | 2 +- packages/tools/src/install-global-cli.ts | 112 +++++++++++++++++++++-- 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/packages/core/build.ts b/packages/core/build.ts index 3741f7029d..4959f3d5a9 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -26,7 +26,7 @@ import { type ReplacementRule, } from './build-support/rewrite-module-specifiers.js'; import pkgJson from './package.json' with { type: 'json' }; -import viteRolldownConfig from './vite-rolldown.config.js'; +import viteRolldownConfig from '../../vite/packages/vite/rolldown.config.js'; const projectDir = join(fileURLToPath(import.meta.url), '..'); diff --git a/packages/tools/src/install-global-cli.ts b/packages/tools/src/install-global-cli.ts index b71f00bf54..fd3e6a721b 100644 --- a/packages/tools/src/install-global-cli.ts +++ b/packages/tools/src/install-global-cli.ts @@ -1,11 +1,14 @@ -import { execSync } from 'node:child_process'; +import { execFileSync, execSync } from 'node:child_process'; import { existsSync, + lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, + realpathSync, rmSync, + rmdirSync, symlinkSync, writeFileSync, } from 'node:fs'; @@ -105,11 +108,20 @@ export function installGlobalCli() { // Clean up old local-dev directories to avoid accumulation if (existsSync(installDir)) { + const currentInstallPath = getCurrentInstallPath(installDir); for (const entry of readdirSync(installDir)) { if (entry.startsWith(LOCAL_DEV_PREFIX)) { + const entryPath = path.join(installDir, entry); + if (pathsEqual(entryPath, currentInstallPath)) { + continue; + } try { - rmSync(path.join(installDir, entry), { recursive: true, force: true }); + removeInstallPath(entryPath); } catch (err) { + if (isWindowsLockedExecutableError(err)) { + console.log(`Skipping old ${entry} because its vp.exe is still running.`); + continue; + } console.warn(`Warning: failed to remove old ${entry}: ${(err as Error).message}`); } } @@ -132,12 +144,17 @@ export function installGlobalCli() { // Run platform-specific install script (use absolute paths) const installScriptDir = path.join(repoRoot, 'packages/cli'); if (isWindows) { - // Use pwsh (PowerShell Core) for better UTF-8 handling + // Prefer PowerShell Core for better UTF-8 handling, but fall back to + // Windows PowerShell because pwsh is not available on every Windows host. const ps1Path = path.join(installScriptDir, 'install.ps1'); - execSync(`pwsh -ExecutionPolicy Bypass -File "${ps1Path}"`, { - stdio: 'inherit', - env, - }); + execFileSync( + getWindowsPowerShellCommand(), + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps1Path], + { + stdio: 'inherit', + env, + }, + ); } else { const shPath = path.join(installScriptDir, 'install.sh'); execSync(`bash "${shPath}"`, { @@ -171,6 +188,67 @@ function getTargetDirs(): string[] { return dirs; } +function removeInstallPath(targetPath: string) { + if (!isWindows) { + rmSync(targetPath, { recursive: true, force: true }); + return; + } + + removeWindowsPath(targetPath); +} + +function getCurrentInstallPath(installDir: string): string | undefined { + const currentPath = path.join(installDir, 'current'); + if (!existsSync(currentPath)) { + return undefined; + } + + try { + return realpathSync(currentPath); + } catch { + return undefined; + } +} + +function pathsEqual(a: string, b: string | undefined): boolean { + if (!b) { + return false; + } + + const resolvedA = path.resolve(a); + const resolvedB = path.resolve(b); + return isWindows + ? resolvedA.toLowerCase() === resolvedB.toLowerCase() + : resolvedA === resolvedB; +} + +function isWindowsLockedExecutableError(err: unknown): boolean { + if (!isWindows || !(err instanceof Error)) { + return false; + } + + const code = (err as NodeJS.ErrnoException).code; + return code === 'EPERM' && err.message.includes(`${path.sep}bin${path.sep}vp.exe`); +} + +function removeWindowsPath(targetPath: string) { + if (!existsSync(targetPath)) { + return; + } + + const stat = lstatSync(targetPath); + if (!stat.isDirectory() || stat.isSymbolicLink()) { + rmSync(targetPath, { force: true }); + return; + } + + for (const entry of readdirSync(targetPath)) { + removeWindowsPath(path.join(targetPath, entry)); + } + + rmdirSync(targetPath); +} + // Find the vp binary in the target directory. // Checks target/release/ first (local builds), then target//release/ (cross-compiled CI builds). function findVpBinary(binaryName: string) { @@ -195,6 +273,26 @@ function findVpBinary(binaryName: string) { return null; } +function getWindowsPowerShellCommand(): string { + for (const command of ['pwsh', 'powershell.exe', 'powershell']) { + try { + const resolved = execFileSync('where.exe', [command], { + encoding: 'utf-8', + stdio: 'pipe', + }) + .split(/\r?\n/) + .find(Boolean); + if (resolved) { + return resolved; + } + } catch { + // Try the next candidate. + } + } + + return 'powershell.exe'; +} + /** * Install dependencies for CI by generating a wrapper package.json with file: protocol * references to the main tgz and sibling @voidzero-dev/* tgz files, then running npm install. From 3e7219fe98ef4b93e68f6c92c42d0482844062b3 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 15 May 2026 20:45:32 +0800 Subject: [PATCH 2/7] format Windows bootstrap fixes --- packages/core/build.ts | 2 +- packages/tools/src/install-global-cli.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/build.ts b/packages/core/build.ts index 4959f3d5a9..f0839dafde 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -16,6 +16,7 @@ import { dts } from 'rolldown-plugin-dts'; import { glob } from 'tinyglobby'; import { generateLicenseFile } from '../../scripts/generate-license.js'; +import viteRolldownConfig from '../../vite/packages/vite/rolldown.config.js'; import { buildCjsDeps } from './build-support/build-cjs-deps.js'; import { replaceThirdPartyCjsRequires } from './build-support/find-create-require.js'; import { RewriteImportsPlugin } from './build-support/rewrite-imports.js'; @@ -26,7 +27,6 @@ import { type ReplacementRule, } from './build-support/rewrite-module-specifiers.js'; import pkgJson from './package.json' with { type: 'json' }; -import viteRolldownConfig from '../../vite/packages/vite/rolldown.config.js'; const projectDir = join(fileURLToPath(import.meta.url), '..'); diff --git a/packages/tools/src/install-global-cli.ts b/packages/tools/src/install-global-cli.ts index fd3e6a721b..b97d9502a7 100644 --- a/packages/tools/src/install-global-cli.ts +++ b/packages/tools/src/install-global-cli.ts @@ -217,9 +217,7 @@ function pathsEqual(a: string, b: string | undefined): boolean { const resolvedA = path.resolve(a); const resolvedB = path.resolve(b); - return isWindows - ? resolvedA.toLowerCase() === resolvedB.toLowerCase() - : resolvedA === resolvedB; + return isWindows ? resolvedA.toLowerCase() === resolvedB.toLowerCase() : resolvedA === resolvedB; } function isWindowsLockedExecutableError(err: unknown): boolean { From 4bc75dba9349e354ff851e1c7cb553e385b47357 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 15 May 2026 20:52:11 +0800 Subject: [PATCH 3/7] ignore symlink placeholders in formatter --- vite.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index 73d6ccca86..3bedd5788d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -70,6 +70,8 @@ export default defineConfig({ 'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored', 'packages/cli/snap-tests-global/migration-lint-staged-ts-config', 'ecosystem-ci/*/**', + 'packages/core/rollupLicensePlugin.ts', + 'packages/core/vite-rolldown.config.ts', 'packages/test/**.cjs', 'packages/test/**.cts', 'packages/test/**.d.mjs', From b4e350042d4814814102c46344d010714df0ae97 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Sat, 16 May 2026 12:53:12 +0800 Subject: [PATCH 4/7] fix test recipe crate scope --- justfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 5410c99096..c1746269c2 100644 --- a/justfile +++ b/justfile @@ -74,8 +74,13 @@ check: watch-check: just watch "'cargo check; cargo clippy'" +[unix] +test: + cargo test $(for d in crates/*/; do echo -n "-p $(basename $d) "; done) -p vite-plus-cli + +[windows] test: - cargo test + $packages = Get-ChildItem -Path crates -Directory | ForEach-Object { '-p'; $_.Name }; $Env:__COMPAT_LAYER='RunAsInvoker'; cargo test @packages -p vite-plus-cli lint: cargo clippy --workspace --all-targets --all-features -- --deny warnings From 1e2a4e6512a8a41fc144b36211040d90d0905d2a Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 18 May 2026 11:41:10 +0800 Subject: [PATCH 5/7] chore: remove unused vite source symlinks from packages/core After routing build.ts to import vite/packages/vite/rolldown.config.js directly, the two symlinks under packages/core (rollupLicensePlugin.ts, vite-rolldown.config.ts) are no longer referenced by any build code. Drop them along with the now-dead ignore/exclude entries in vite.config.ts and tsconfig.json. --- packages/core/rollupLicensePlugin.ts | 1 - packages/core/vite-rolldown.config.ts | 1 - tsconfig.json | 5 +---- vite.config.ts | 6 ------ 4 files changed, 1 insertion(+), 12 deletions(-) delete mode 120000 packages/core/rollupLicensePlugin.ts delete mode 120000 packages/core/vite-rolldown.config.ts diff --git a/packages/core/rollupLicensePlugin.ts b/packages/core/rollupLicensePlugin.ts deleted file mode 120000 index 8d86c68561..0000000000 --- a/packages/core/rollupLicensePlugin.ts +++ /dev/null @@ -1 +0,0 @@ -../../vite/packages/vite/rollupLicensePlugin.ts \ No newline at end of file diff --git a/packages/core/vite-rolldown.config.ts b/packages/core/vite-rolldown.config.ts deleted file mode 120000 index 30b06ff591..0000000000 --- a/packages/core/vite-rolldown.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../vite/packages/vite/rolldown.config.ts \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8e6d9d4fd8..2e23b82eba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,11 +19,8 @@ "packages/cli/docs", "packages/cli/snap-tests", "packages/cli/snap-tests-global", - // Symlinks to vite submodule files that don't use .js extensions; - // excluding build.ts too because it imports vite-rolldown.config.ts + // build.ts imports vite/packages/vite/rolldown.config.ts which doesn't use .js extensions "packages/core/build.ts", - "packages/core/rollupLicensePlugin.ts", - "packages/core/vite-rolldown.config.ts", "vite", "rolldown", "target" diff --git a/vite.config.ts b/vite.config.ts index 3bedd5788d..cffb971e8e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,8 +28,6 @@ export default defineConfig({ 'bench/**/*.ts', 'ecosystem-ci/**/*', 'packages/*/build.ts', - 'packages/core/rollupLicensePlugin.ts', - 'packages/core/vite-rolldown.config.ts', 'packages/tools/**/*.ts', ], rules: { @@ -48,8 +46,6 @@ export default defineConfig({ '**/snap-tests-global/**', '**/snap-tests-todo/**', 'packages/*/binding/**', - 'packages/core/rollupLicensePlugin.ts', - 'packages/core/vite-rolldown.config.ts', ], }, test: { @@ -70,8 +66,6 @@ export default defineConfig({ 'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored', 'packages/cli/snap-tests-global/migration-lint-staged-ts-config', 'ecosystem-ci/*/**', - 'packages/core/rollupLicensePlugin.ts', - 'packages/core/vite-rolldown.config.ts', 'packages/test/**.cjs', 'packages/test/**.cts', 'packages/test/**.d.mjs', From 5c9d967100221dbe02f9751f9e4f6b399b1dfac8 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Mon, 18 May 2026 13:37:23 +0800 Subject: [PATCH 6/7] handle busy locked vp cleanup --- packages/tools/src/install-global-cli.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/install-global-cli.ts b/packages/tools/src/install-global-cli.ts index b97d9502a7..07bdf307b5 100644 --- a/packages/tools/src/install-global-cli.ts +++ b/packages/tools/src/install-global-cli.ts @@ -226,7 +226,10 @@ function isWindowsLockedExecutableError(err: unknown): boolean { } const code = (err as NodeJS.ErrnoException).code; - return code === 'EPERM' && err.message.includes(`${path.sep}bin${path.sep}vp.exe`); + return ( + (code === 'EPERM' || code === 'EBUSY') && + err.message.includes(`${path.sep}bin${path.sep}vp.exe`) + ); } function removeWindowsPath(targetPath: string) { From 39ebb629e45d73c52a4dcf0d0e6e0f844a2a54a4 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Mon, 18 May 2026 13:54:40 +0800 Subject: [PATCH 7/7] handle dangling Windows install junctions --- packages/tools/src/install-global-cli.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/tools/src/install-global-cli.ts b/packages/tools/src/install-global-cli.ts index 07bdf307b5..cb359f9b28 100644 --- a/packages/tools/src/install-global-cli.ts +++ b/packages/tools/src/install-global-cli.ts @@ -233,11 +233,16 @@ function isWindowsLockedExecutableError(err: unknown): boolean { } function removeWindowsPath(targetPath: string) { - if (!existsSync(targetPath)) { - return; + let stat; + try { + stat = lstatSync(targetPath); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return; + } + throw err; } - const stat = lstatSync(targetPath); if (!stat.isDirectory() || stat.isSymbolicLink()) { rmSync(targetPath, { force: true }); return;