From ae93e93746e524661c7b0497a5fee7bc001b8209 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Wed, 18 Mar 2026 12:10:08 +0000 Subject: [PATCH 1/2] feat(env): use trampoline exe instead of .cmd wrappers on Windows (#981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replace Windows `.cmd` shim wrappers with lightweight trampoline `.exe` binaries - Eliminates the "Terminate batch job (Y/N)?" prompt on Ctrl+C - The trampoline detects its tool name from its own filename, sets `VITE_PLUS_SHIM_TOOL` env var, spawns `vp.exe`, ignores Ctrl+C (child handles it), and propagates the exit code - Thanks for the solution suggestion from @sunfkny ## Changes - Add `crates/vite_trampoline/` — minimal Windows trampoline binary (~100-150KB) - Update shim detection (`detect_shim_tool`) to check `VITE_PLUS_SHIM_TOOL` env var before `argv[0]` - Replace `.cmd`/`.sh` wrapper creation with trampoline `.exe` copying in `setup.rs` and `global_install.rs` - Add rename-before-copy pattern for refreshing `bin/vp.exe` while it's running - Add legacy `.cmd`/`.sh` cleanup during `vp env setup --refresh` - Update CI to build and distribute `vp-shim.exe` for Windows targets - Update `install.ps1`, `install.sh`, `publish-native-addons.ts` for trampoline distribution - Update `extract_platform_package` in upgrade path to also extract `vp-shim.exe` - Update npm-link conflict detection to recognize `.exe` shims - Add RFC document and update existing RFCs (`env-command`, `upgrade-command`, `vpx-command`) ## Manual testing (Windows) ### 1\. Fresh install via PowerShell ```powershell Remove-Item -Recurse -Force "$env:USERPROFILE\.vite-plus" -ErrorAction SilentlyContinue & ./packages/cli/install.ps1 dir "$env:USERPROFILE\.vite-plus\bin\*.exe" dir "$env:USERPROFILE\.vite-plus\bin\*.cmd" ``` - [x] Trampoline `.exe` shims created for vp, node, npm, npx, vpx - [x] No legacy `.cmd` wrappers for core tools (only `vp-use.cmd` if present) - [x] `node --version` works - [x] `npm --version` works - [x] `vp --version` works ### 2\. Fresh install via Git Bash ([install.sh](http://install.sh)) ```bash rm -rf ~/.vite-plus bash ./packages/cli/install.sh ls -la ~/.vite-plus/current/bin/vp-shim.exe ``` - [x] `vp-shim.exe` copied alongside `vp.exe` in `current/bin/` - [x] `node --version` works - [x] `npm --version` works ### 3\. Ctrl+C behavior (the main fix) ```powershell vp dev # Press Ctrl+C in each shell ``` - [x] cmd.exe: clean exit, NO "Terminate batch job (Y/N)?" prompt - [x] PowerShell: clean exit - [x] Git Bash: clean exit ### 4\. Exit code propagation ```powershell node -e "process.exit(42)"; echo $LASTEXITCODE npm run nonexistent-script; echo $LASTEXITCODE ``` - [x] Exit code 42 propagated correctly - [x] Non-zero exit code from failed npm command ### 5\. Nested shim invocations (recursion marker) ```powershell npm --version pnpm --version ``` - [x] `npm --version` prints version (npm spawns node internally) - [x] `pnpm --version` prints version (pnpm spawns node internally) ### 6\. `vp env setup --refresh` (running exe overwrite) ```powershell vp env setup --refresh node --version dir "$env:USERPROFILE\.vite-plus\bin\*.old" ``` - [x] Refresh succeeds without "sharing violation" error - [x] Shims still work after refresh - [x] `.old` files cleaned up (or only one recent if vp.exe was still running) ### 7\. `vp install -g` / `vp remove -g` (managed package shims) ```powershell vp install -g typescript where.exe tsc tsc --version vp remove -g typescript dir "$env:USERPROFILE\.vite-plus\bin\tsc*" ``` - [x] `tsc.exe` created (NOT `tsc.cmd`) - [x] `tsc --version` works - [x] After remove: no `tsc.exe`, `tsc.cmd`, or extensionless `tsc` left behind ### 8\. `npm install -g` auto-link (npm-managed packages) ```powershell npm install -g cowsay where.exe cowsay cowsay hello npm uninstall -g cowsay ``` - [x] npm packages use `.cmd` wrapper (NOT trampoline) - [x] `cowsay` works - [x] Uninstall removes the `.cmd` wrapper ### 9\. `vp upgrade` and rollback ```powershell vp --version vp upgrade node --version vp upgrade --rollback vp --version ``` - [x] Upgrade succeeds, shims still work - [x] Rollback succeeds, shims still work ### 10\. Install a pre-trampoline version (downgrade compatibility) ```powershell $env:VITE_PLUS_VERSION = "" & ./packages/cli/install.ps1 Remove-Item Env:VITE_PLUS_VERSION dir "$env:USERPROFILE\.vite-plus\bin\*.exe" dir "$env:USERPROFILE\.vite-plus\bin\*.cmd" ``` - [x] Falls back to `.cmd` wrappers - [x] Stale trampoline `.exe` shims removed (`node.exe`, `npm.exe`, etc.) - [x] `vp --version` works via `.cmd` wrapper ### 11\. `vp env doctor` ```powershell vp env doctor ``` - [x] Shows shims as working, no errors about missing files ### 12\. Cross-shell verification - [x] cmd.exe: `node --version`, `npm --version`, `vp --version` all work - [x] PowerShell: `node --version`, `npm --version`, `vp --version` all work - [x] Git Bash: `node --version`, `npm --version`, `vp --version` all work Closes #835 --- .github/actions/build-upstream/action.yml | 7 + .github/workflows/e2e-test.yml | 7 +- .github/workflows/release.yml | 1 + .github/workflows/test-standalone-install.yml | 33 +- .husky/pre-commit | 0 Cargo.lock | 4 + Cargo.toml | 5 + .../src/commands/env/doctor.rs | 12 +- .../src/commands/env/global_install.rs | 119 +++++--- .../vite_global_cli/src/commands/env/setup.rs | 216 +++++++++---- .../src/commands/upgrade/install.rs | 3 +- crates/vite_global_cli/src/shim/dispatch.rs | 25 +- crates/vite_global_cli/src/shim/mod.rs | 55 ++-- crates/vite_shared/src/env_vars.rs | 8 + crates/vite_trampoline/Cargo.toml | 27 ++ crates/vite_trampoline/src/main.rs | 98 ++++++ package.json | 6 +- packages/cli/install.ps1 | 68 ++++- packages/cli/install.sh | 68 ++++- packages/cli/publish-native-addons.ts | 19 +- packages/tools/src/install-global-cli.ts | 11 + rfcs/env-command.md | 131 ++------ rfcs/trampoline-exe-for-shims.md | 285 ++++++++++++++++++ rfcs/upgrade-command.md | 6 +- rfcs/vpx-command.md | 8 +- 25 files changed, 914 insertions(+), 308 deletions(-) mode change 100644 => 100755 .husky/pre-commit create mode 100644 crates/vite_trampoline/Cargo.toml create mode 100644 crates/vite_trampoline/src/main.rs create mode 100644 rfcs/trampoline-exe-for-shims.md diff --git a/.github/actions/build-upstream/action.yml b/.github/actions/build-upstream/action.yml index 78c88cb834..12b84bbb5b 100644 --- a/.github/actions/build-upstream/action.yml +++ b/.github/actions/build-upstream/action.yml @@ -40,6 +40,7 @@ runs: packages/cli/binding/index.d.cts target/${{ inputs.target }}/release/vp target/${{ inputs.target }}/release/vp.exe + target/${{ inputs.target }}/release/vp-shim.exe key: ${{ steps.cache-key.outputs.key }} # Apply Vite+ branding patches to rolldown-vite source (CI checks out @@ -111,6 +112,11 @@ runs: shell: bash run: cargo build --release --target ${{ inputs.target }} -p vite_global_cli + - name: Build trampoline shim binary (Windows only) + if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'windows') + shell: bash + run: cargo build --release --target ${{ inputs.target }} -p vite_trampoline + - name: Save NAPI binding cache if: steps.cache-restore.outputs.cache-hit != 'true' uses: actions/cache/save@94b89442628ad1d101e352b7ee38f30e1bef108e # v5 @@ -123,6 +129,7 @@ runs: packages/cli/binding/index.d.cts target/${{ inputs.target }}/release/vp target/${{ inputs.target }}/release/vp.exe + target/${{ inputs.target }}/release/vp-shim.exe key: ${{ steps.cache-key.outputs.key }} # Build vite-plus TypeScript after native bindings are ready diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index b8fa95cf73..0b765e458e 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -112,6 +112,7 @@ jobs: cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../.. # Copy vp binary for e2e-test job (findVpBinary expects it in target/) cp target/${{ matrix.target }}/release/vp tmp/tgz/vp 2>/dev/null || cp target/${{ matrix.target }}/release/vp.exe tmp/tgz/vp.exe 2>/dev/null || true + cp target/${{ matrix.target }}/release/vp-shim.exe tmp/tgz/vp-shim.exe 2>/dev/null || true ls -la tmp/tgz - name: Upload tgz artifacts @@ -289,12 +290,16 @@ jobs: # Place vp binary where install-global-cli.ts expects it (target/release/) mkdir -p target/release cp tmp/tgz/vp target/release/vp 2>/dev/null || cp tmp/tgz/vp.exe target/release/vp.exe 2>/dev/null || true + cp tmp/tgz/vp-shim.exe target/release/vp-shim.exe 2>/dev/null || true chmod +x target/release/vp 2>/dev/null || true node $GITHUB_WORKSPACE/packages/tools/src/install-global-cli.ts --tgz $GITHUB_WORKSPACE/tmp/tgz/vite-plus-0.0.0.tgz - echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH + # Use USERPROFILE (native Windows path) instead of HOME (Git Bash path /c/Users/...) + # so cmd.exe and Node.js execSync can resolve binaries in PATH + echo "${USERPROFILE:-$HOME}/.vite-plus/bin" >> $GITHUB_PATH - name: Migrate in ${{ matrix.project.name }} working-directory: ${{ runner.temp }}/vite-plus-ecosystem-ci/${{ matrix.project.name }}${{ matrix.project.directory && format('/{0}', matrix.project.directory) || '' }} + shell: bash run: | node $GITHUB_WORKSPACE/ecosystem-ci/patch-project.ts ${{ matrix.project.name }} vp install --no-frozen-lockfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13544a624a..e28d4a0fca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,6 +124,7 @@ jobs: path: | ./target/${{ matrix.settings.target }}/release/vp ./target/${{ matrix.settings.target }}/release/vp.exe + ./target/${{ matrix.settings.target }}/release/vp-shim.exe if-no-files-found: error - name: Remove .node files before upload dist diff --git a/.github/workflows/test-standalone-install.yml b/.github/workflows/test-standalone-install.yml index 0ce0c9a202..60765245b3 100644 --- a/.github/workflows/test-standalone-install.yml +++ b/.github/workflows/test-standalone-install.yml @@ -19,7 +19,7 @@ defaults: shell: bash env: - VITE_PLUS_VERSION: latest + VITE_PLUS_VERSION: alpha jobs: test-install-sh: @@ -131,7 +131,7 @@ jobs: run: | docker run --rm --platform linux/arm64 \ -v "${{ github.workspace }}:/workspace" \ - -e VITE_PLUS_VERSION=latest \ + -e VITE_PLUS_VERSION=alpha \ ubuntu:20.04 bash -c " ls -al ~/ apt-get update && apt-get install -y curl ca-certificates @@ -233,7 +233,7 @@ jobs: exit 1 } - $expectedShims = @("node.cmd", "npm.cmd", "npx.cmd") + $expectedShims = @("node.exe", "npm.exe", "npx.exe") foreach ($shim in $expectedShims) { $shimFile = Join-Path $binPath $shim if (-not (Test-Path $shimFile)) { @@ -300,7 +300,7 @@ jobs: exit 1 } - $expectedShims = @("node.cmd", "npm.cmd", "npx.cmd") + $expectedShims = @("node.exe", "npm.exe", "npx.exe") foreach ($shim in $expectedShims) { $shimFile = Join-Path $binPath $shim if (-not (Test-Path $shimFile)) { @@ -380,8 +380,8 @@ jobs: exit 1 } - # Verify shim executables exist (all use .cmd wrappers on Windows) - $expectedShims = @("node.cmd", "npm.cmd", "npx.cmd") + # Verify shim executables exist (trampoline .exe files on Windows) + $expectedShims = @("node.exe", "npm.exe", "npx.exe") foreach ($shim in $expectedShims) { $shimFile = Join-Path $binPath $shim if (-not (Test-Path $shimFile)) { @@ -419,8 +419,8 @@ jobs: set "BIN_PATH=%USERPROFILE%\.vite-plus\bin" dir "%BIN_PATH%" - REM Verify shim executables exist (Windows uses .cmd wrappers) - for %%s in (node.cmd npm.cmd npx.cmd vp.cmd) do ( + REM Verify shim executables exist (Windows uses trampoline .exe files) + for %%s in (node.exe npm.exe npx.exe vp.exe) do ( if not exist "%BIN_PATH%\%%s" ( echo Error: Shim not found: %BIN_PATH%\%%s exit /b 1 @@ -462,22 +462,13 @@ jobs: exit 1 fi - # Verify .cmd wrappers exist (for cmd.exe/PowerShell) - for shim in node.cmd npm.cmd npx.cmd vp.cmd; do + # Verify trampoline .exe files exist + for shim in node.exe npm.exe npx.exe vp.exe; do if [ ! -f "$BIN_PATH/$shim" ]; then - echo "Error: .cmd wrapper not found: $BIN_PATH/$shim" + echo "Error: Trampoline shim not found: $BIN_PATH/$shim" exit 1 fi - echo "Found .cmd wrapper: $BIN_PATH/$shim" - done - - # Verify shell scripts exist (for Git Bash) - for shim in node npm npx vp; do - if [ ! -f "$BIN_PATH/$shim" ]; then - echo "Error: Shell script not found: $BIN_PATH/$shim" - exit 1 - fi - echo "Found shell script: $BIN_PATH/$shim" + echo "Found trampoline shim: $BIN_PATH/$shim" done # Verify vp env doctor works diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/Cargo.lock b/Cargo.lock index 9c3b9e37ef..de3eccd83d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7529,6 +7529,10 @@ dependencies = [ "which", ] +[[package]] +name = "vite_trampoline" +version = "0.0.0" + [[package]] name = "vite_workspace" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 48ce30a032..3f6aaaccc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -318,3 +318,8 @@ codegen-units = 1 strip = "symbols" # set to `false` for debug information debug = false # set to `true` for debug information panic = "abort" # Let it crash and force ourselves to write safe Rust. + +# The trampoline binary is copied per shim tool (~5-10 copies), so optimize for +# size instead of speed. This reduces it from ~200KB to ~100KB on Windows. +[profile.release.package.vite_trampoline] +opt-level = "z" diff --git a/crates/vite_global_cli/src/commands/env/doctor.rs b/crates/vite_global_cli/src/commands/env/doctor.rs index 4f15b3c75c..b96d06579a 100644 --- a/crates/vite_global_cli/src/commands/env/doctor.rs +++ b/crates/vite_global_cli/src/commands/env/doctor.rs @@ -239,8 +239,8 @@ async fn check_bin_dir() -> bool { fn shim_filename(tool: &str) -> String { #[cfg(windows)] { - // All tools use .cmd wrappers on Windows (including node) - format!("{tool}.cmd") + // All tools use trampoline .exe files on Windows + format!("{tool}.exe") } #[cfg(not(windows))] @@ -739,10 +739,10 @@ mod tests { #[cfg(windows)] { - // All shims should use .cmd on Windows (matching setup.rs) - assert_eq!(node, "node.cmd"); - assert_eq!(npm, "npm.cmd"); - assert_eq!(npx, "npx.cmd"); + // All shims should use .exe on Windows (trampoline executables) + assert_eq!(node, "node.exe"); + assert_eq!(npm, "npm.exe"); + assert_eq!(npx, "npx.exe"); } #[cfg(not(windows))] diff --git a/crates/vite_global_cli/src/commands/env/global_install.rs b/crates/vite_global_cli/src/commands/env/global_install.rs index 563abf4420..963c1f06fc 100644 --- a/crates/vite_global_cli/src/commands/env/global_install.rs +++ b/crates/vite_global_cli/src/commands/env/global_install.rs @@ -368,7 +368,7 @@ pub(crate) const CORE_SHIMS: &[&str] = &["node", "npm", "npx", "vp"]; /// Create a shim for a package binary. /// /// On Unix: Creates a symlink to ../current/bin/vp -/// On Windows: Creates a .cmd wrapper that calls `vp env exec ` +/// On Windows: Creates a trampoline .exe that forwards to vp.exe async fn create_package_shim( bin_dir: &vite_path::AbsolutePath, bin_name: &str, @@ -406,40 +406,25 @@ async fn create_package_shim( #[cfg(windows)] { - let cmd_path = bin_dir.join(format!("{}.cmd", bin_name)); + let shim_path = bin_dir.join(format!("{}.exe", bin_name)); // Skip if already exists (e.g., re-installing the same package) - if tokio::fs::try_exists(&cmd_path).await.unwrap_or(false) { + if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) { return Ok(()); } - // Create .cmd wrapper that calls vp env exec . - // Use `--` so args like `--help` are forwarded to the package binary, - // not consumed by clap while parsing `vp env exec`. - // Set VITE_PLUS_HOME using %~dp0.. which resolves to the parent of bin/ - // This ensures the vp binary knows its home directory - let wrapper_content = format!( - "@echo off\r\nset VITE_PLUS_HOME=%~dp0..\r\nset VITE_PLUS_SHIM_WRAPPER=1\r\n\"%VITE_PLUS_HOME%\\current\\bin\\vp.exe\" env exec {} -- %*\r\nexit /b %ERRORLEVEL%\r\n", - bin_name - ); - tokio::fs::write(&cmd_path, wrapper_content).await?; - - // Also create shell script for Git Bash (bin_name without extension) - // Uses explicit "vp env exec " instead of symlink+argv[0] because - // Windows symlinks require admin privileges - let sh_path = bin_dir.join(bin_name); - let sh_content = format!( - r#"#!/bin/sh -VITE_PLUS_HOME="$(dirname "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")")" -export VITE_PLUS_HOME -export VITE_PLUS_SHIM_WRAPPER=1 -exec "$VITE_PLUS_HOME/current/bin/vp.exe" env exec {} -- "$@" -"#, - bin_name - ); - tokio::fs::write(&sh_path, sh_content).await?; + // Copy the trampoline binary as .exe. + // The trampoline detects the tool name from its own filename and sets + // VITE_PLUS_SHIM_TOOL env var before spawning vp.exe. + let trampoline_src = super::setup::get_trampoline_path()?; + tokio::fs::copy(trampoline_src.as_path(), &shim_path).await?; + + // Remove legacy .cmd and shell script wrappers from previous versions. + // In Git Bash/MSYS, the extensionless script takes precedence over .exe, + // so leftover wrappers would bypass the trampoline. + super::setup::cleanup_legacy_windows_shim(bin_dir, bin_name).await; - tracing::debug!("Created package shim wrappers for {} (.cmd and shell script)", bin_name); + tracing::debug!("Created package trampoline shim {:?}", shim_path); } Ok(()) @@ -466,16 +451,15 @@ async fn remove_package_shim( #[cfg(windows)] { - // Remove .cmd wrapper - let cmd_path = bin_dir.join(format!("{}.cmd", bin_name)); - if tokio::fs::try_exists(&cmd_path).await.unwrap_or(false) { - tokio::fs::remove_file(&cmd_path).await?; - } - - // Also remove shell script (for Git Bash) - let sh_path = bin_dir.join(bin_name); - if tokio::fs::try_exists(&sh_path).await.unwrap_or(false) { - tokio::fs::remove_file(&sh_path).await?; + // Remove trampoline .exe shim and legacy .cmd / shell script wrappers. + // Best-effort: ignore NotFound errors for files that don't exist. + for suffix in &[".exe", ".cmd", ""] { + let path = if suffix.is_empty() { + bin_dir.join(bin_name) + } else { + bin_dir.join(format!("{bin_name}{suffix}")) + }; + let _ = tokio::fs::remove_file(&path).await; } } @@ -486,13 +470,42 @@ async fn remove_package_shim( mod tests { use super::*; + /// RAII guard that sets `VITE_PLUS_TRAMPOLINE_PATH` to a fake binary on creation + /// and clears it on drop. Ensures cleanup even on test panics. + #[cfg(windows)] + struct FakeTrampolineGuard; + + #[cfg(windows)] + impl FakeTrampolineGuard { + fn new(dir: &std::path::Path) -> Self { + let trampoline = dir.join("vp-shim.exe"); + std::fs::write(&trampoline, b"fake-trampoline").unwrap(); + unsafe { + std::env::set_var(vite_shared::env_vars::VITE_PLUS_TRAMPOLINE_PATH, &trampoline); + } + Self + } + } + + #[cfg(windows)] + impl Drop for FakeTrampolineGuard { + fn drop(&mut self) { + unsafe { + std::env::remove_var(vite_shared::env_vars::VITE_PLUS_TRAMPOLINE_PATH); + } + } + } + #[tokio::test] + #[cfg_attr(windows, serial_test::serial)] async fn test_create_package_shim_creates_bin_dir() { use tempfile::TempDir; use vite_path::AbsolutePathBuf; // Create a temp directory but don't create the bin subdirectory let temp_dir = TempDir::new().unwrap(); + #[cfg(windows)] + let _guard = FakeTrampolineGuard::new(temp_dir.path()); let bin_dir = temp_dir.path().join("bin"); let bin_dir = AbsolutePathBuf::new(bin_dir).unwrap(); @@ -505,7 +518,7 @@ mod tests { // Verify bin directory was created assert!(bin_dir.as_path().exists()); - // Verify shim file was created (on Windows, shims have .cmd extension) + // Verify shim file was created (on Windows, shims have .exe extension) // On Unix, symlinks may be broken (target doesn't exist), so use symlink_metadata #[cfg(unix)] { @@ -517,7 +530,7 @@ mod tests { } #[cfg(windows)] { - let shim_path = bin_dir.join("test-shim.cmd"); + let shim_path = bin_dir.join("test-shim.exe"); assert!(shim_path.as_path().exists()); } } @@ -537,16 +550,19 @@ mod tests { #[cfg(unix)] let shim_path = bin_dir.join("node"); #[cfg(windows)] - let shim_path = bin_dir.join("node.cmd"); + let shim_path = bin_dir.join("node.exe"); assert!(!shim_path.as_path().exists()); } #[tokio::test] + #[cfg_attr(windows, serial_test::serial)] async fn test_remove_package_shim_removes_shim() { use tempfile::TempDir; use vite_path::AbsolutePathBuf; let temp_dir = TempDir::new().unwrap(); + #[cfg(windows)] + let _guard = FakeTrampolineGuard::new(temp_dir.path()); let bin_dir = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); // Create a shim @@ -573,7 +589,7 @@ mod tests { } #[cfg(windows)] { - let shim_path = bin_dir.join("tsc.cmd"); + let shim_path = bin_dir.join("tsc.exe"); assert!(shim_path.as_path().exists(), "Shim should exist after creation"); // Remove the shim @@ -597,13 +613,16 @@ mod tests { } #[tokio::test] + #[cfg_attr(windows, serial_test::serial)] async fn test_uninstall_removes_shims_from_metadata() { use tempfile::TempDir; use vite_path::AbsolutePathBuf; let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path().to_path_buf(); - let _guard = vite_shared::EnvConfig::test_guard( + #[cfg(windows)] + let _trampoline_guard = FakeTrampolineGuard::new(&temp_path); + let _env_guard = vite_shared::EnvConfig::test_guard( vite_shared::EnvConfig::for_test_with_home(&temp_path), ); @@ -630,10 +649,10 @@ mod tests { } #[cfg(windows)] { - assert!(bin_dir.join("tsc.cmd").as_path().exists(), "tsc.cmd shim should exist"); + assert!(bin_dir.join("tsc.exe").as_path().exists(), "tsc.exe shim should exist"); assert!( - bin_dir.join("tsserver.cmd").as_path().exists(), - "tsserver.cmd shim should exist" + bin_dir.join("tsserver.exe").as_path().exists(), + "tsserver.exe shim should exist" ); } @@ -674,10 +693,10 @@ mod tests { } #[cfg(windows)] { - assert!(!bin_dir.join("tsc.cmd").as_path().exists(), "tsc.cmd shim should be removed"); + assert!(!bin_dir.join("tsc.exe").as_path().exists(), "tsc.exe shim should be removed"); assert!( - !bin_dir.join("tsserver.cmd").as_path().exists(), - "tsserver.cmd shim should be removed" + !bin_dir.join("tsserver.exe").as_path().exists(), + "tsserver.exe shim should be removed" ); } } diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 29c784e55c..6ade827f6d 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -10,8 +10,10 @@ //! - Symlinks preserve argv[0], allowing tool detection via the symlink name //! //! On Windows: -//! - bin/vp.cmd is a wrapper script that calls ..\current\bin\vp.exe -//! - bin/node.cmd, bin/npm.cmd, bin/npx.cmd are wrappers calling `vp env exec ` +//! - bin/vp.exe, bin/node.exe, bin/npm.exe, bin/npx.exe are trampoline executables +//! - Each trampoline detects its tool name from its own filename and spawns +//! current\bin\vp.exe with VITE_PLUS_SHIM_TOOL env var set +//! - This avoids the "Terminate batch job (Y/N)?" prompt from .cmd wrappers use std::process::ExitStatus; @@ -77,6 +79,12 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result } } + // Best-effort cleanup of .old files from rename-before-copy on Windows + #[cfg(windows)] + if refresh { + cleanup_old_files(&bin_dir).await; + } + // Print results if !created.is_empty() { println!("{}", help::render_heading("Created Shims")); @@ -129,35 +137,27 @@ async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> R #[cfg(windows)] { - let bin_vp_cmd = bin_dir.join("vp.cmd"); - - // Create wrapper script bin/vp.cmd that calls current\bin\vp.exe - let should_create_wrapper = - refresh || !tokio::fs::try_exists(&bin_vp_cmd).await.unwrap_or(false); - - if should_create_wrapper { - // Set VITE_PLUS_HOME using a for loop to canonicalize the path. - // %~dp0.. would produce paths like C:\Users\x\.vite-plus\bin\.. - // The for loop resolves this to a clean C:\Users\x\.vite-plus - let cmd_content = "@echo off\r\nfor %%I in (\"%~dp0..\") do set VITE_PLUS_HOME=%%~fI\r\n\"%VITE_PLUS_HOME%\\current\\bin\\vp.exe\" %*\r\nexit /b %ERRORLEVEL%\r\n"; - tokio::fs::write(&bin_vp_cmd, cmd_content).await?; - tracing::debug!("Created wrapper script {:?}", bin_vp_cmd); - } + let bin_vp_exe = bin_dir.join("vp.exe"); + + // Create trampoline bin/vp.exe that forwards to current\bin\vp.exe + let should_create = refresh || !tokio::fs::try_exists(&bin_vp_exe).await.unwrap_or(false); + + if should_create { + let trampoline_src = get_trampoline_path()?; + // On refresh, the existing vp.exe may still be running (the trampoline + // that launched us). Windows prevents overwriting a running exe, so we + // rename it to a timestamped .old file first, then copy the new one. + if tokio::fs::try_exists(&bin_vp_exe).await.unwrap_or(false) { + rename_to_old(&bin_vp_exe).await; + } - // Also create shell script for Git Bash (vp without extension) - // Note: We call vp.exe directly, not via symlink, because Windows - // symlinks require admin privileges and Git Bash support is unreliable - let bin_vp = bin_dir.join("vp"); - let should_create_sh = refresh || !tokio::fs::try_exists(&bin_vp).await.unwrap_or(false); + tokio::fs::copy(trampoline_src.as_path(), &bin_vp_exe).await?; + tracing::debug!("Created trampoline {:?}", bin_vp_exe); + } - if should_create_sh { - let sh_content = r#"#!/bin/sh -VITE_PLUS_HOME="$(dirname "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")")" -export VITE_PLUS_HOME -exec "$VITE_PLUS_HOME/current/bin/vp.exe" "$@" -"#; - tokio::fs::write(&bin_vp, sh_content).await?; - tracing::debug!("Created shell wrapper script {:?}", bin_vp); + // Clean up legacy .cmd and shell script wrappers from previous versions + if refresh { + cleanup_legacy_windows_shim(bin_dir, "vp").await; } } @@ -189,8 +189,15 @@ async fn create_shim( if !refresh { return Ok(false); } - // Remove existing shim for refresh - tokio::fs::remove_file(&shim_path).await?; + // Remove existing shim for refresh. + // On Windows, .exe files may be locked (by antivirus, indexer, or + // still-running processes), so rename to .old first instead of deleting. + #[cfg(windows)] + rename_to_old(&shim_path).await; + #[cfg(not(windows))] + { + tokio::fs::remove_file(&shim_path).await?; + } } #[cfg(unix)] @@ -210,8 +217,8 @@ async fn create_shim( fn shim_filename(tool: &str) -> String { #[cfg(windows)] { - // All tools use .cmd wrappers on Windows (including node) - format!("{tool}.cmd") + // All tools use trampoline .exe files on Windows + format!("{tool}.exe") } #[cfg(not(windows))] @@ -237,50 +244,129 @@ async fn create_unix_shim( Ok(()) } -/// Create Windows shims using .cmd wrappers that call `vp env exec `. +/// Create Windows shims using trampoline `.exe` files. /// -/// All tools (node, npm, npx, vpx) get .cmd wrappers that invoke `vp env exec`. -/// Also creates shell scripts (without extension) for Git Bash compatibility. -/// This is consistent with Volta's Windows approach. +/// Each tool gets a copy of the trampoline binary renamed to `.exe`. +/// The trampoline detects its tool name from its own filename and spawns +/// vp.exe with `VITE_PLUS_SHIM_TOOL` set, avoiding the "Terminate batch job?" +/// prompt that `.cmd` wrappers cause on Ctrl+C. +/// +/// See: #[cfg(windows)] async fn create_windows_shim( _source: &std::path::Path, bin_dir: &vite_path::AbsolutePath, tool: &str, ) -> Result<(), Error> { - let cmd_path = bin_dir.join(format!("{tool}.cmd")); + let trampoline_src = get_trampoline_path()?; + let shim_path = bin_dir.join(format!("{tool}.exe")); + tokio::fs::copy(trampoline_src.as_path(), &shim_path).await?; - // Create .cmd wrapper that calls vp env exec . - // Use `--` so tool args like `--help` are forwarded to the tool, - // not consumed by clap while parsing `vp env exec`. - // Use a for loop to canonicalize VITE_PLUS_HOME path. - // %~dp0.. would produce paths like C:\Users\x\.vite-plus\bin\.. - // The for loop resolves this to a clean C:\Users\x\.vite-plus - let cmd_content = format!( - "@echo off\r\nfor %%I in (\"%~dp0..\") do set VITE_PLUS_HOME=%%~fI\r\nset VITE_PLUS_SHIM_WRAPPER=1\r\n\"%VITE_PLUS_HOME%\\current\\bin\\vp.exe\" env exec {} -- %*\r\nexit /b %ERRORLEVEL%\r\n", - tool - ); + // Clean up legacy .cmd and shell script wrappers from previous versions + cleanup_legacy_windows_shim(bin_dir, tool).await; - tokio::fs::write(&cmd_path, cmd_content).await?; + tracing::debug!("Created trampoline shim {:?}", shim_path); - // Also create shell script for Git Bash (tool without extension) - // Uses explicit "vp env exec " instead of symlink+argv[0] because - // Windows symlinks require admin privileges - let sh_path = bin_dir.join(tool); - let sh_content = format!( - r#"#!/bin/sh -VITE_PLUS_HOME="$(dirname "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")")" -export VITE_PLUS_HOME -export VITE_PLUS_SHIM_WRAPPER=1 -exec "$VITE_PLUS_HOME/current/bin/vp.exe" env exec {} -- "$@" -"#, - tool - ); - tokio::fs::write(&sh_path, sh_content).await?; + Ok(()) +} - tracing::debug!("Created Windows wrappers for {} (.cmd and shell script)", tool); +/// Get the path to the trampoline template binary (vp-shim.exe). +/// +/// The trampoline binary is distributed alongside vp.exe in the same directory. +/// In tests, `VITE_PLUS_TRAMPOLINE_PATH` can override the resolved path. +#[cfg(windows)] +pub(crate) fn get_trampoline_path() -> Result { + // Allow tests to override the trampoline path + if let Ok(override_path) = std::env::var(vite_shared::env_vars::VITE_PLUS_TRAMPOLINE_PATH) { + let path = std::path::PathBuf::from(override_path); + if path.exists() { + return vite_path::AbsolutePathBuf::new(path) + .ok_or_else(|| Error::ConfigError("Invalid trampoline override path".into())); + } + } - Ok(()) + let current_exe = std::env::current_exe() + .map_err(|e| Error::ConfigError(format!("Cannot find current executable: {e}").into()))?; + let bin_dir = current_exe + .parent() + .ok_or_else(|| Error::ConfigError("Cannot find parent directory of vp.exe".into()))?; + let trampoline = bin_dir.join("vp-shim.exe"); + + if !trampoline.exists() { + return Err(Error::ConfigError( + format!( + "Trampoline binary not found at {}. Re-install vite-plus to fix this.", + trampoline.display() + ) + .into(), + )); + } + + vite_path::AbsolutePathBuf::new(trampoline) + .ok_or_else(|| Error::ConfigError("Invalid trampoline path".into())) +} + +/// Rename an existing `.exe` to a timestamped `.old` file instead of deleting. +/// +/// On Windows, running `.exe` files can't be deleted or overwritten, but they can +/// be renamed. The `.old` files are cleaned up by `cleanup_old_files()`. +#[cfg(windows)] +async fn rename_to_old(path: &vite_path::AbsolutePath) { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + if let Some(name) = path.as_path().file_name().and_then(|n| n.to_str()) { + let old_name = format!("{name}.{timestamp}.old"); + let old_path = path.as_path().with_file_name(&old_name); + if let Err(e) = tokio::fs::rename(path, &old_path).await { + tracing::warn!("Failed to rename {} to {}: {}", name, old_name, e); + } + } +} + +/// Best-effort cleanup of accumulated `.old` files from previous rename-before-copy operations. +/// +/// When refreshing `bin/vp.exe` on Windows, the running trampoline is renamed to a +/// timestamped `.old` file. This function tries to delete all such files. Files still +/// in use by a running process will silently fail to delete and be cleaned up next time. +#[cfg(windows)] +async fn cleanup_old_files(bin_dir: &vite_path::AbsolutePath) { + let Ok(mut entries) = tokio::fs::read_dir(bin_dir).await else { + return; + }; + while let Ok(Some(entry)) = entries.next_entry().await { + let file_name = entry.file_name(); + let name = file_name.to_string_lossy(); + if name.ends_with(".old") { + let _ = tokio::fs::remove_file(entry.path()).await; + } + } +} + +/// Remove legacy `.cmd` and shell script wrappers from previous versions. +#[cfg(windows)] +pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vite_path::AbsolutePath, tool: &str) { + // Remove old .cmd wrapper (best-effort, ignore NotFound) + let cmd_path = bin_dir.join(format!("{tool}.cmd")); + let _ = tokio::fs::remove_file(&cmd_path).await; + + // Remove old shell script wrapper (extensionless, for Git Bash) + // Only remove if it starts with #!/bin/sh (not a binary or other file) + // Read only the first 9 bytes to avoid loading large files into memory + let sh_path = bin_dir.join(tool); + let is_shell_script = async { + use tokio::io::AsyncReadExt; + let mut file = tokio::fs::File::open(&sh_path).await.ok()?; + let mut buf = [0u8; 9]; // b"#!/bin/sh".len() + let n = file.read(&mut buf).await.ok()?; + Some(buf[..n].starts_with(b"#!/bin/sh")) + // file handle dropped here before remove_file + } + .await; + if is_shell_script == Some(true) { + let _ = tokio::fs::remove_file(&sh_path).await; + } } /// Create env files with PATH guard (prevents duplicate PATH entries). diff --git a/crates/vite_global_cli/src/commands/upgrade/install.rs b/crates/vite_global_cli/src/commands/upgrade/install.rs index f87226014f..29e38375e2 100644 --- a/crates/vite_global_cli/src/commands/upgrade/install.rs +++ b/crates/vite_global_cli/src/commands/upgrade/install.rs @@ -30,6 +30,7 @@ fn is_safe_tar_path(path: &Path) -> bool { /// /// From the platform tarball, extracts: /// - The `vp` binary → `{version_dir}/bin/vp` +/// - The `vp-shim.exe` trampoline → `{version_dir}/bin/vp-shim.exe` (Windows only) /// /// `.node` files are no longer extracted here — npm installs them /// via the platform package's optionalDependencies. @@ -62,7 +63,7 @@ pub async fn extract_platform_package( let file_name = relative.file_name().and_then(|n| n.to_str()).unwrap_or(""); - if file_name == "vp" || file_name == "vp.exe" { + if file_name == "vp" || file_name == "vp.exe" || file_name == "vp-shim.exe" { // Binary goes to bin/ let target = bin_dir_clone.join(file_name); let mut buf = Vec::new(); diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 94685f7e84..21a39a64a7 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -261,8 +261,19 @@ fn check_npm_global_install_result( } // Check if binary already exists in bin_dir (vite-plus bin) + // On Unix: symlinks (bin/tsc) + // On Windows: trampoline .exe (bin/tsc.exe) or legacy .cmd (bin/tsc.cmd) let shim_path = bin_dir.join(&bin_name); - if std::fs::symlink_metadata(shim_path.as_path()).is_ok() { + let shim_exists = std::fs::symlink_metadata(shim_path.as_path()).is_ok() || { + #[cfg(windows)] + { + let exe_path = bin_dir.join(vite_str::format!("{bin_name}.exe")); + std::fs::symlink_metadata(exe_path.as_path()).is_ok() + } + #[cfg(not(windows))] + false + }; + if shim_exists { if let Ok(Some(config)) = BinConfig::load_sync(&bin_name) { if config.source == BinSource::Vp { // Managed by vp install -g — warn about the conflict @@ -430,7 +441,9 @@ fn create_bin_link( #[cfg(windows)] { - // Create .cmd wrapper + // npm-installed packages use .cmd wrappers pointing to npm's generated script. + // Unlike vp-installed packages, these don't have PackageMetadata, so the + // trampoline approach won't work (dispatch_package_binary would fail). let cmd_path = bin_dir.join(vite_str::format!("{bin_name}.cmd")); let wrapper_content = vite_str::format!( "@echo off\r\n\"{source}\" %*\r\nexit /b %ERRORLEVEL%\r\n", @@ -523,13 +536,13 @@ fn remove_npm_global_uninstall_links(bin_entries: &[(String, String)], npm_prefi // Clean up the BinConfig let _ = BinConfig::delete_sync(bin_name); - // Also remove .cmd on Windows + // Also remove .cmd and .exe on Windows #[cfg(windows)] { let cmd_path = bin_dir.join(vite_str::format!("{bin_name}.cmd")); - if cmd_path.as_path().exists() { - let _ = std::fs::remove_file(cmd_path.as_path()); - } + let _ = std::fs::remove_file(cmd_path.as_path()); + let exe_path = bin_dir.join(vite_str::format!("{bin_name}.exe")); + let _ = std::fs::remove_file(exe_path.as_path()); } } else { // Owned by a different npm package — check if our link target is now broken diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index 6f658a1d2f..1b11304f04 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -5,8 +5,8 @@ //! //! Detection methods: //! - Unix: Symlinks to vp binary preserve argv[0], allowing tool detection -//! - Windows: .cmd wrappers call `vp env exec ` directly -//! - Legacy: VITE_PLUS_SHIM_TOOL env var (kept for backward compatibility) +//! - Windows: Trampoline `.exe` files set `VITE_PLUS_SHIM_TOOL` env var and spawn vp.exe +//! - Legacy: `.cmd` wrappers call `vp env exec ` directly (deprecated) mod cache; pub(crate) mod dispatch; @@ -77,10 +77,24 @@ fn is_potential_package_binary(tool: &str) -> bool { return false; }; - // Check if the shim exists in the configured bin directory - // Use symlink_metadata to detect symlinks (even broken ones) + // Check if the shim exists in the configured bin directory. + // Use symlink_metadata to detect symlinks (even broken ones). + // On Windows, check .exe first (trampoline shims, the common case), + // then fall back to extensionless (Unix symlinks or legacy). + #[cfg(windows)] + { + let exe_path = configured_bin.join(format!("{tool}.exe")); + if std::fs::symlink_metadata(&exe_path).is_ok() { + return true; + } + } + let shim_path = configured_bin.join(tool); - std::fs::symlink_metadata(&shim_path).is_ok() + if std::fs::symlink_metadata(&shim_path).is_ok() { + return true; + } + + false } /// Environment variable used for shim tool detection via shell wrapper scripts. @@ -89,12 +103,10 @@ const SHIM_TOOL_ENV_VAR: &str = env_vars::VITE_PLUS_SHIM_TOOL; /// Detect the shim tool from environment and argv. /// /// Detection priority: -/// 1. If argv[0] is "vp" or "vp.exe", this is a direct CLI invocation - NOT shim mode -/// 2. Check `VITE_PLUS_SHIM_TOOL` env var (for shell wrapper scripts) +/// 1. Check `VITE_PLUS_SHIM_TOOL` env var (set by trampoline exe on Windows) +/// 2. If argv[0] is "vp" or "vp.exe", this is a direct CLI invocation - NOT shim mode /// 3. Fall back to argv[0] detection (primary method on Unix with symlinks) /// -/// Note: Modern Windows wrappers use `vp env exec ` instead of env vars. -/// /// IMPORTANT: This function clears `VITE_PLUS_SHIM_TOOL` after reading it to /// prevent the env var from leaking to child processes. pub fn detect_shim_tool(argv0: &str) -> Option { @@ -106,17 +118,9 @@ pub fn detect_shim_tool(argv0: &str) -> Option { std::env::remove_var(SHIM_TOOL_ENV_VAR); } - // If argv[0] is explicitly "vp" or "vp.exe", this is a direct CLI invocation. - // Do NOT use the env var in this case - it may be stale from a parent process. - let argv0_tool = extract_tool_name(argv0); - if argv0_tool == "vp" { - return None; // Direct vp invocation, not shim mode - } - if argv0_tool == "vpx" { - return Some("vpx".to_string()); - } - - // Check VITE_PLUS_SHIM_TOOL env var (set by shell wrapper scripts) + // Check VITE_PLUS_SHIM_TOOL env var first (set by trampoline exe on Windows). + // This takes priority over argv[0] because the trampoline spawns vp.exe + // (so argv[0] would be "vp"), but the env var carries the real tool name. if let Some(tool) = env_tool { if !tool.is_empty() { let tool_lower = tool.to_lowercase(); @@ -127,7 +131,16 @@ pub fn detect_shim_tool(argv0: &str) -> Option { } } - // Fall back to argv[0] detection + // If argv[0] is explicitly "vp" or "vp.exe", this is a direct CLI invocation. + let argv0_tool = extract_tool_name(argv0); + if argv0_tool == "vp" { + return None; // Direct vp invocation, not shim mode + } + if argv0_tool == "vpx" { + return Some("vpx".to_string()); + } + + // Fall back to argv[0] detection (Unix symlinks) if is_shim_tool(&argv0_tool) { Some(argv0_tool) } else { None } } diff --git a/crates/vite_shared/src/env_vars.rs b/crates/vite_shared/src/env_vars.rs index 658d50a485..7b3d35f565 100644 --- a/crates/vite_shared/src/env_vars.rs +++ b/crates/vite_shared/src/env_vars.rs @@ -72,3 +72,11 @@ pub const VITE_PLUS_CLI_BIN: &str = "VITE_PLUS_CLI_BIN"; /// Global CLI version, passed from Rust binary to JS for --version display. pub const VITE_PLUS_GLOBAL_VERSION: &str = "VITE_PLUS_GLOBAL_VERSION"; + +// ── Testing / Development ─────────────────────────────────────────────── + +/// Override the trampoline binary path for tests. +/// +/// When set, `get_trampoline_path()` uses this path instead of resolving +/// relative to `current_exe()`. Only used in test environments. +pub const VITE_PLUS_TRAMPOLINE_PATH: &str = "VITE_PLUS_TRAMPOLINE_PATH"; diff --git a/crates/vite_trampoline/Cargo.toml b/crates/vite_trampoline/Cargo.toml new file mode 100644 index 0000000000..1b200492f0 --- /dev/null +++ b/crates/vite_trampoline/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "vite_trampoline" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +description = "Minimal Windows trampoline exe for vite-plus shims" + +[[bin]] +name = "vp-shim" +path = "src/main.rs" + +# No dependencies — the single Win32 FFI call (SetConsoleCtrlHandler) is +# declared inline to avoid pulling in the heavy `windows`/`windows-core` crates. + +# Override workspace lints: this is a standalone minimal binary that intentionally +# avoids dependencies on vite_shared, vite_path, vite_str, etc. to keep binary +# size small. It uses std types and macros directly. +[lints.clippy] +disallowed_macros = "allow" +disallowed_types = "allow" +disallowed_methods = "allow" + +# Note: Release profile is defined at workspace root (Cargo.toml). +# The workspace already sets lto="fat", codegen-units=1, strip="symbols", panic="abort". +# For even smaller binaries, consider building this crate separately with opt-level="z". diff --git a/crates/vite_trampoline/src/main.rs b/crates/vite_trampoline/src/main.rs new file mode 100644 index 0000000000..dec34097fc --- /dev/null +++ b/crates/vite_trampoline/src/main.rs @@ -0,0 +1,98 @@ +//! Minimal Windows trampoline for vite-plus shims. +//! +//! This binary is copied and renamed for each shim tool (node.exe, npm.exe, etc.). +//! It detects the tool name from its own filename, then spawns `vp.exe` with the +//! `VITE_PLUS_SHIM_TOOL` environment variable set, allowing `vp.exe` to enter +//! shim dispatch mode. +//! +//! On Ctrl+C, the trampoline ignores the signal (the child process handles it), +//! avoiding the "Terminate batch job (Y/N)?" prompt that `.cmd` wrappers produce. +//! +//! **Size optimization**: This binary avoids `core::fmt` (which adds ~100KB) by +//! never using `format!`, `eprintln!`, `println!`, or `.unwrap()`. All error +//! paths use `process::exit(1)` directly. +//! +//! See: + +use std::{ + env, + process::{self, Command}, +}; + +fn main() { + // 1. Determine tool name from our own executable filename + let exe_path = env::current_exe().unwrap_or_else(|_| process::exit(1)); + let tool_name = + exe_path.file_stem().and_then(|s| s.to_str()).unwrap_or_else(|| process::exit(1)); + + // 2. Locate vp.exe: /../current/bin/vp.exe + let bin_dir = exe_path.parent().unwrap_or_else(|| process::exit(1)); + let vp_home = bin_dir.parent().unwrap_or_else(|| process::exit(1)); + let vp_exe = vp_home.join("current").join("bin").join("vp.exe"); + + // 3. Install Ctrl+C handler that ignores signals (child will handle them). + // This prevents the "Terminate batch job (Y/N)?" prompt. + #[cfg(windows)] + install_ctrl_handler(); + + // 4. Spawn vp.exe + // - Always set VITE_PLUS_HOME so vp.exe uses the correct home directory + // (matches what the old .cmd wrappers did with %~dp0..) + // - If tool is "vp", run in normal CLI mode (no VITE_PLUS_SHIM_TOOL) + // - Otherwise, set VITE_PLUS_SHIM_TOOL so vp.exe enters shim dispatch + let mut cmd = Command::new(&vp_exe); + cmd.args(env::args_os().skip(1)); + cmd.env("VITE_PLUS_HOME", vp_home); + + if tool_name != "vp" { + cmd.env("VITE_PLUS_SHIM_TOOL", tool_name); + // Clear the recursion marker so nested shim invocations (e.g., npm + // spawning node) get fresh version resolution instead of falling + // through to passthrough mode. The old .cmd wrappers went through + // `vp env exec` which cleared this in exec.rs; the trampoline + // bypasses that path. + // Must match vite_shared::env_vars::VITE_PLUS_TOOL_RECURSION + cmd.env_remove("VITE_PLUS_TOOL_RECURSION"); + } + + // 5. Execute and propagate exit code. + // Use write_all instead of eprintln!/format! to avoid pulling in core::fmt (~100KB). + match cmd.status() { + Ok(s) => process::exit(s.code().unwrap_or(1)), + Err(_) => { + use std::io::Write; + let stderr = std::io::stderr(); + let mut handle = stderr.lock(); + let _ = handle.write_all(b"vite-plus: failed to execute "); + let _ = handle.write_all(vp_exe.as_os_str().as_encoded_bytes()); + let _ = handle.write_all(b"\n"); + process::exit(1); + } + } +} + +/// Install a console control handler that ignores Ctrl+C, Ctrl+Break, etc. +/// +/// When Ctrl+C is pressed, Windows sends the event to all processes in the +/// console group. By returning TRUE (1), we tell Windows we handled the event +/// (by ignoring it). The child process also receives the event and can +/// decide how to respond (typically by exiting gracefully). +/// +/// This is the same pattern used by uv-trampoline and Python's distlib launcher. +#[cfg(windows)] +fn install_ctrl_handler() { + // Raw FFI declaration to avoid pulling in the heavy `windows`/`windows-core` crates. + // Signature: https://learn.microsoft.com/en-us/windows/console/setconsolectrlhandler + type HandlerRoutine = unsafe extern "system" fn(ctrl_type: u32) -> i32; + unsafe extern "system" { + fn SetConsoleCtrlHandler(handler: Option, add: i32) -> i32; + } + + unsafe extern "system" fn handler(_ctrl_type: u32) -> i32 { + 1 // TRUE - signal handled (ignored) + } + + unsafe { + SetConsoleCtrlHandler(Some(handler), 1); + } +} diff --git a/package.json b/package.json index 23a4321c29..98018d0aad 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "pnpm -F @voidzero-dev/* -F vite-plus build", - "bootstrap-cli": "pnpm build && cargo build -p vite_global_cli --release && pnpm install-global-cli", + "bootstrap-cli": "pnpm build && cargo build -p vite_global_cli -p vite_trampoline --release && pnpm install-global-cli", "bootstrap-cli:ci": "pnpm install-global-cli", "install-global-cli": "tool install-global-cli", "tsgo": "tsgo -b tsconfig.json", @@ -13,8 +13,8 @@ "test": "vp test run && pnpm -r snap-test", "fmt": "vp fmt", "test:unit": "vp test run", - "docs:dev": "pnpm --filter=./docs dev", - "docs:build": "pnpm --filter=./docs build", + "docs:dev": "pnpm -C docs dev", + "docs:build": "pnpm -C docs build", "prepare": "husky" }, "devDependencies": { diff --git a/packages/cli/install.ps1 b/packages/cli/install.ps1 index b1f56462f1..10100ca920 100644 --- a/packages/cli/install.ps1 +++ b/packages/cli/install.ps1 @@ -183,6 +183,16 @@ function Configure-UserPath { return "true" } +# Run vp env setup --refresh, showing output only on failure +function Refresh-Shims { + param([string]$BinDir) + $setupOutput = & "$BinDir\vp.exe" env setup --refresh 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warn "Failed to refresh shims:" + Write-Host "$setupOutput" + } +} + # Setup Node.js version manager (node/npm/npx shims) # Returns: "true" = enabled, "false" = not enabled, "already" = already configured function Setup-NodeManager { @@ -193,13 +203,13 @@ function Setup-NodeManager { # Check if Vite+ is already managing Node.js (bin\node.exe exists) if (Test-Path "$binPath\node.exe") { # Already managing Node.js, just refresh shims - & "$BinDir\vp.exe" env setup --refresh | Out-Null + Refresh-Shims -BinDir $BinDir return "already" } # Auto-enable on CI environment if ($env:CI) { - & "$BinDir\vp.exe" env setup --refresh | Out-Null + Refresh-Shims -BinDir $BinDir return "true" } @@ -208,7 +218,7 @@ function Setup-NodeManager { # Auto-enable if no node available on system if (-not $nodeAvailable) { - & "$BinDir\vp.exe" env setup --refresh | Out-Null + Refresh-Shims -BinDir $BinDir return "true" } @@ -220,7 +230,7 @@ function Setup-NodeManager { $response = Read-Host "Press Enter to accept (Y/n)" if ($response -eq '' -or $response -eq 'y' -or $response -eq 'Y') { - & "$BinDir\vp.exe" env setup --refresh | Out-Null + Refresh-Shims -BinDir $BinDir return "true" } } @@ -272,6 +282,11 @@ function Main { # Copy binary from LOCAL_BINARY env var (set by install-global-cli.ts) if ($LocalBinary -and (Test-Path $LocalBinary)) { Copy-Item -Path $LocalBinary -Destination (Join-Path $BinDir $binaryName) -Force + # Also copy trampoline shim binary if available (sibling to vp.exe) + $shimSource = Join-Path (Split-Path $LocalBinary) "vp-shim.exe" + if (Test-Path $shimSource) { + Copy-Item -Path $shimSource -Destination (Join-Path $BinDir "vp-shim.exe") -Force + } } else { Write-Error-Exit "VITE_PLUS_LOCAL_BINARY must be set when using VITE_PLUS_LOCAL_TGZ" } @@ -293,10 +308,16 @@ function Main { & "$env:SystemRoot\System32\tar.exe" -xzf $platformTempFile -C $platformTempExtract # Copy binary to BinDir - $binarySource = Join-Path (Join-Path $platformTempExtract "package") $binaryName + $packageDir = Join-Path $platformTempExtract "package" + $binarySource = Join-Path $packageDir $binaryName if (Test-Path $binarySource) { Copy-Item -Path $binarySource -Destination $BinDir -Force } + # Also copy trampoline shim binary if present in the package + $shimSource = Join-Path $packageDir "vp-shim.exe" + if (Test-Path $shimSource) { + Copy-Item -Path $shimSource -Destination $BinDir -Force + } Remove-Item -Recurse -Force $platformTempExtract } finally { @@ -347,27 +368,46 @@ function Main { # Create new junction pointing to the version directory cmd /c mklink /J "$CurrentLink" "$VersionDir" | Out-Null - # Create bin directory and vp.cmd wrapper (always done) - # Set VITE_PLUS_HOME so the vp binary knows its home directory + # Create bin directory and vp wrapper (always done) New-Item -ItemType Directory -Force -Path "$InstallDir\bin" | Out-Null - $wrapperContent = @" + $trampolineSrc = "$VersionDir\bin\vp-shim.exe" + if (Test-Path $trampolineSrc) { + # New versions: use trampoline exe to avoid "Terminate batch job (Y/N)?" on Ctrl+C + Copy-Item -Path $trampolineSrc -Destination "$InstallDir\bin\vp.exe" -Force + # Remove legacy .cmd and shell script wrappers from previous versions + foreach ($legacy in @("$InstallDir\bin\vp.cmd", "$InstallDir\bin\vp")) { + if (Test-Path $legacy) { + Remove-Item -Path $legacy -Force -ErrorAction SilentlyContinue + } + } + } else { + # Pre-trampoline versions: fall back to legacy .cmd and shell script wrappers. + # Remove any stale trampoline .exe shims left by a newer install — .exe wins + # over .cmd on Windows PATH, so leftover trampolines would bypass the wrappers. + foreach ($stale in @("vp.exe", "node.exe", "npm.exe", "npx.exe", "vpx.exe")) { + $stalePath = Join-Path "$InstallDir\bin" $stale + if (Test-Path $stalePath) { + Remove-Item -Path $stalePath -Force -ErrorAction SilentlyContinue + } + } + # Keep consistent with the original install.ps1 wrapper format + $wrapperContent = @" @echo off set VITE_PLUS_HOME=%~dp0.. "%VITE_PLUS_HOME%\current\bin\vp.exe" %* exit /b %ERRORLEVEL% "@ - Set-Content -Path "$InstallDir\bin\vp.cmd" -Value $wrapperContent -NoNewline + Set-Content -Path "$InstallDir\bin\vp.cmd" -Value $wrapperContent -NoNewline - # Create shell script wrapper for Git Bash (vp without extension) - # Note: We call vp.exe directly (not via symlink) because Windows symlinks - # require admin privileges and Git Bash symlink support is unreliable - $shContent = @" + # Also create shell script wrapper for Git Bash/MSYS + $shContent = @" #!/bin/sh VITE_PLUS_HOME="`$(dirname "`$(dirname "`$(readlink -f "`$0" 2>/dev/null || echo "`$0")")")" export VITE_PLUS_HOME exec "`$VITE_PLUS_HOME/current/bin/vp.exe" "`$@" "@ - Set-Content -Path "$InstallDir\bin\vp" -Value $shContent -NoNewline + Set-Content -Path "$InstallDir\bin\vp" -Value $shContent -NoNewline + } # Cleanup old versions Cleanup-OldVersions -InstallDir $InstallDir diff --git a/packages/cli/install.sh b/packages/cli/install.sh index 8d4fd05fac..3f7b056254 100644 --- a/packages/cli/install.sh +++ b/packages/cli/install.sh @@ -413,6 +413,17 @@ configure_shell_path() { # If result is still 1, PATH_CONFIGURED remains "false" (set at function start) } +# Run vp env setup --refresh, showing output only on failure +# Arguments: vp_bin - path to the vp binary +refresh_shims() { + local vp_bin="$1" + local setup_output + if ! setup_output=$("$vp_bin" env setup --refresh 2>&1); then + warn "Failed to refresh shims:" + echo "$setup_output" >&2 + fi +} + # Setup Node.js version manager (node/npm/npx shims) # Sets NODE_MANAGER_ENABLED global # Arguments: bin_dir - path to the version's bin directory containing vp @@ -421,17 +432,22 @@ setup_node_manager() { local bin_path="$INSTALL_DIR/bin" NODE_MANAGER_ENABLED="false" - # Check if Vite+ is already managing Node.js (bin/node exists) - if [ -e "$bin_path/node" ]; then - # Already managing Node.js, just refresh shims - "$bin_dir/vp" env setup --refresh > /dev/null + # Resolve vp binary name (vp on Unix, vp.exe on Windows) + local vp_bin="$bin_dir/vp" + if [ -f "$bin_dir/vp.exe" ]; then + vp_bin="$bin_dir/vp.exe" + fi + + # Check if Vite+ is already managing Node.js (bin/node or bin/node.exe exists) + if [ -e "$bin_path/node" ] || [ -e "$bin_path/node.exe" ]; then + refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="already" return 0 fi # Auto-enable on CI environment if [ -n "$CI" ]; then - "$bin_dir/vp" env setup --refresh > /dev/null + refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" return 0 fi @@ -444,7 +460,7 @@ setup_node_manager() { # Auto-enable if no node available on system if [ "$node_available" = "false" ]; then - "$bin_dir/vp" env setup --refresh > /dev/null + refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" return 0 fi @@ -457,7 +473,7 @@ setup_node_manager() { read -r response < /dev/tty if [ -z "$response" ] || [ "$response" = "y" ] || [ "$response" = "Y" ]; then - "$bin_dir/vp" env setup --refresh > /dev/null + refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" fi fi @@ -554,6 +570,14 @@ main() { # Copy binary from LOCAL_BINARY env var (set by install-global-cli.ts) if [ -n "$LOCAL_BINARY" ]; then cp "$LOCAL_BINARY" "$BIN_DIR/$binary_name" + # On Windows, also copy the trampoline shim binary if available + if [[ "$platform" == win32* ]]; then + local shim_src + shim_src="$(dirname "$LOCAL_BINARY")/vp-shim.exe" + if [ -f "$shim_src" ]; then + cp "$shim_src" "$BIN_DIR/vp-shim.exe" + fi + fi else error "VITE_PLUS_LOCAL_BINARY must be set when using VITE_PLUS_LOCAL_TGZ" fi @@ -572,6 +596,10 @@ main() { # Copy binary to BIN_DIR cp "$platform_temp_dir/$binary_name" "$BIN_DIR/" chmod +x "$BIN_DIR/$binary_name" + # On Windows, also copy the trampoline shim binary if present in the package + if [[ "$platform" == win32* ]] && [ -f "$platform_temp_dir/vp-shim.exe" ]; then + cp "$platform_temp_dir/vp-shim.exe" "$BIN_DIR/" + fi rm -rf "$platform_temp_dir" fi @@ -600,7 +628,11 @@ NPMRC_EOF # e.g. during local dev where install-global-cli.ts handles deps separately) if [ -z "${VITE_PLUS_SKIP_DEPS_INSTALL:-}" ]; then local install_log="$VERSION_DIR/install.log" - if ! (cd "$VERSION_DIR" && CI=true "$BIN_DIR/vp" install --silent > "$install_log" 2>&1); then + local vp_install_bin="$BIN_DIR/vp" + if [ -f "$BIN_DIR/vp.exe" ]; then + vp_install_bin="$BIN_DIR/vp.exe" + fi + if ! (cd "$VERSION_DIR" && CI=true "$vp_install_bin" install --silent > "$install_log" 2>&1); then error "Failed to install dependencies. See log for details: $install_log" exit 1 fi @@ -609,15 +641,29 @@ NPMRC_EOF # Create/update current symlink (use relative path for portability) ln -sfn "$VITE_PLUS_VERSION" "$CURRENT_LINK" - # Create bin directory and vp symlink (always done) + # Create bin directory and vp entrypoint (always done) mkdir -p "$INSTALL_DIR/bin" - ln -sf "../current/bin/vp" "$INSTALL_DIR/bin/vp" + if [[ "$platform" == win32* ]]; then + # Windows: copy trampoline as vp.exe (matching install.ps1) + if [ -f "$INSTALL_DIR/current/bin/vp-shim.exe" ]; then + cp "$INSTALL_DIR/current/bin/vp-shim.exe" "$INSTALL_DIR/bin/vp.exe" + fi + else + # Unix: symlink to current/bin/vp + ln -sf "../current/bin/vp" "$INSTALL_DIR/bin/vp" + fi # Cleanup old versions cleanup_old_versions # Create env files with PATH guard (prevents duplicate PATH entries) - "$INSTALL_DIR/bin/vp" env setup --env-only > /dev/null + # Use current/bin/vp directly (the real binary) instead of bin/vp (trampoline) + # to avoid the self-overwrite issue on Windows during --refresh + local vp_bin="$INSTALL_DIR/current/bin/vp" + if [[ "$platform" == win32* ]]; then + vp_bin="$INSTALL_DIR/current/bin/vp.exe" + fi + "$vp_bin" env setup --env-only > /dev/null # Configure shell PATH (always attempted) configure_shell_path diff --git a/packages/cli/publish-native-addons.ts b/packages/cli/publish-native-addons.ts index 470a0e5024..6e1821d759 100644 --- a/packages/cli/publish-native-addons.ts +++ b/packages/cli/publish-native-addons.ts @@ -108,13 +108,30 @@ for (const [platform, rustTarget] of Object.entries(RUST_TARGETS)) { chmodSync(join(platformCliDir, binaryName), 0o755); } + // Copy trampoline shim binary for Windows (required) + // The trampoline is a small exe that replaces .cmd wrappers to avoid + // "Terminate batch job (Y/N)?" on Ctrl+C (see issue #835) + const shimName = 'vp-shim.exe'; + const files = [binaryName]; + if (isWindows) { + const shimSource = join(repoRoot, 'target', rustTarget, 'release', shimName); + if (!existsSync(shimSource)) { + console.error( + `Error: ${shimName} not found at ${shimSource}. Run "cargo build -p vite_trampoline --release --target ${rustTarget}" first.`, + ); + process.exit(1); + } + copyFileSync(shimSource, join(platformCliDir, shimName)); + files.push(shimName); + } + // Generate package.json const cliPackage = { name: `@voidzero-dev/vite-plus-cli-${platform}`, version: cliVersion, os: [meta.os], cpu: [meta.cpu], - files: [binaryName], + files, description: `Vite+ CLI binary for ${platform}`, repository: cliPackageJson.repository, }; diff --git a/packages/tools/src/install-global-cli.ts b/packages/tools/src/install-global-cli.ts index 68dd3e4b79..ed381af404 100644 --- a/packages/tools/src/install-global-cli.ts +++ b/packages/tools/src/install-global-cli.ts @@ -89,6 +89,17 @@ export function installGlobalCli() { process.exit(1); } + // On Windows, the trampoline shim binary is required for creating shims. + // Validate it exists beside the chosen vp.exe to avoid mismatched artifacts. + if (isWindows) { + const shimPath = path.join(path.dirname(binaryPath), 'vp-shim.exe'); + if (!existsSync(shimPath)) { + console.error(`Error: vp-shim.exe not found at ${shimPath}`); + console.error('Build it with: cargo build -p vite_trampoline --release'); + process.exit(1); + } + } + const localDevVer = localDevVersion(); // Clean up old local-dev directories to avoid accumulation diff --git a/rfcs/env-command.md b/rfcs/env-command.md index 062495e8cb..6aec2333ac 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -23,7 +23,7 @@ This RFC proposes adding a `vp env` command that provides system-wide, IDE-safe A shim-based approach where: - `VITE_PLUS_HOME/bin/` directory is added to PATH (system-level for IDE reliability) -- Shims (`node`, `npm`, `npx`) are symlinks to the `vp` binary (Unix) or `.cmd` wrappers (Windows) +- Shims (`node`, `npm`, `npx`) are symlinks to the `vp` binary (Unix) or trampoline `.exe` files (Windows) - The `vp` CLI itself is also in `VITE_PLUS_HOME/bin/`, so users only need one PATH entry - The binary detects invocation via `argv[0]` and dispatches accordingly - Version resolution and installation leverage existing `vite_js_runtime` infrastructure @@ -344,16 +344,11 @@ VITE_PLUS_HOME/ # Default: ~/.vite-plus │ ├── npm -> ../current/bin/vp # Symlink to vp binary (Unix) │ ├── npx -> ../current/bin/vp # Symlink to vp binary (Unix) │ ├── tsc -> ../current/bin/vp # Symlink for global package (Unix) -│ ├── vp # Shell script for Git Bash (Windows) -│ ├── vp.cmd # Wrapper calling ..\current\bin\vp.exe (Windows) -│ ├── node # Shell script for Git Bash (Windows) -│ ├── node.cmd # Wrapper calling vp env exec node (Windows) -│ ├── npm # Shell script for Git Bash (Windows) -│ ├── npm.cmd # Wrapper calling vp env exec npm (Windows) -│ ├── npx # Shell script for Git Bash (Windows) -│ ├── npx.cmd # Wrapper calling vp env exec npx (Windows) -│ ├── tsc # Shell script for global package Git Bash (Windows) -│ └── tsc.cmd # Wrapper for global package (Windows) +│ ├── vp.exe # Trampoline forwarding to current\bin\vp.exe (Windows) +│ ├── node.exe # Trampoline shim for node (Windows) +│ ├── npm.exe # Trampoline shim for npm (Windows) +│ ├── npx.exe # Trampoline shim for npx (Windows) +│ └── tsc.exe # Trampoline shim for global package (Windows) ├── current/ │ └── bin/ │ ├── vp # The actual vp CLI binary (Unix) @@ -734,17 +729,16 @@ fn execute_run_command() { - No binary accumulation issues (symlinks are just filesystem pointers) - Relative symlinks (e.g., `../current/bin/vp`) work within the same directory tree -### 3. Wrapper Scripts for Windows +### 3. Trampoline Executables for Windows -**Decision**: Use `.cmd` wrapper scripts on Windows that call `vp env exec `. +**Decision**: Use lightweight trampoline `.exe` files on Windows instead of `.cmd` wrappers. Each trampoline detects its tool name from its own filename, sets `VITE_PLUS_SHIM_TOOL`, and spawns `vp.exe`. See [RFC: Trampoline EXE for Shims](./trampoline-exe-for-shims.md). **Rationale**: -- Windows PATH resolution prefers `.cmd` over `.exe` for extensionless commands -- Simple wrapper format: `vp env exec npm %*` - no binary copies needed -- Same pattern as Volta (`volta run `) -- Single `vp.exe` binary to maintain in `current/bin/` -- No `VITE_PLUS_SHIM_TOOL` env var complexity - dispatch via `vp env exec` command +- `.cmd` wrappers cause "Terminate batch job (Y/N)?" prompt on Ctrl+C +- `.exe` files work in all shells (cmd.exe, PowerShell, Git Bash) without needing separate wrappers +- Single trampoline binary (~100-150KB) copied per tool — no `.cmd` + shell script pair needed +- Ctrl+C handled cleanly via `SetConsoleCtrlHandler` ### 4. execve on Unix, spawn on Windows @@ -1853,7 +1847,7 @@ This is useful for: - Testing code against different Node versions - Running one-off commands without changing project configuration - CI/CD scripts that need explicit version control -- Windows shims (`.cmd` wrappers and Git Bash shell scripts call `vp env exec `) +- Legacy Windows `.cmd` wrappers (deprecated in favor of trampoline `.exe` shims) ### Usage @@ -1897,7 +1891,7 @@ When `--node` is **not provided** and the first command is a shim tool: - **Core tools (node, npm, npx)**: Version resolved from `.node-version`, `package.json#engines.node`, or default - **Global packages (tsc, eslint, etc.)**: Uses the Node.js version that was used during `vp install -g` -Both use the **exact same code path** as Unix symlinks (`shim::dispatch()`), ensuring identical behavior across platforms. This is how Windows `.cmd` wrappers and Git Bash shell scripts work. +Both use the **exact same code path** as Unix symlinks (`shim::dispatch()`), ensuring identical behavior across platforms. On Windows, trampoline `.exe` shims set `VITE_PLUS_SHIM_TOOL` to enter shim dispatch mode. **Important**: The `VITE_PLUS_TOOL_RECURSION` environment variable is cleared before dispatch to ensure fresh version resolution, even when invoked from within a context where the variable is already set (e.g., when pnpm runs through the vite-plus shim). @@ -2123,24 +2117,20 @@ ln -sf ../current/bin/vp ~/.vite-plus/bin/tsc ``` VITE_PLUS_HOME\ ├── bin\ -│ ├── vp # Shell script for Git Bash (calls vp.exe directly) -│ ├── vp.cmd # Wrapper for cmd.exe/PowerShell -│ ├── node # Shell script for Git Bash (calls vp env exec node) -│ ├── node.cmd # Wrapper calling vp env exec node -│ ├── npm # Shell script for Git Bash (calls vp env exec npm) -│ ├── npm.cmd # Wrapper calling vp env exec npm -│ ├── npx # Shell script for Git Bash (calls vp env exec npx) -│ ├── npx.cmd # Wrapper calling vp env exec npx -│ ├── tsc # Shell script for global package (Git Bash) -│ └── tsc.cmd # Wrapper for global package (cmd.exe/PowerShell) +│ ├── vp.exe # Trampoline forwarding to current\bin\vp.exe +│ ├── node.exe # Trampoline shim (sets VITE_PLUS_SHIM_TOOL=node) +│ ├── npm.exe # Trampoline shim (sets VITE_PLUS_SHIM_TOOL=npm) +│ ├── npx.exe # Trampoline shim (sets VITE_PLUS_SHIM_TOOL=npx) +│ └── tsc.exe # Trampoline shim for global package └── current\ └── bin\ - └── vp.exe # The actual vp CLI binary + ├── vp.exe # The actual vp CLI binary + └── vp-shim.exe # Trampoline template (copied as shims) ``` -### Shell Scripts for Git Bash +### Trampoline Executables -Git Bash (MSYS2/MinGW) doesn't use Windows' PATHEXT mechanism, so it won't find `.cmd` files when you type a command without extension. Shell script wrappers (without extension) are created alongside all `.cmd` files. +Windows shims use lightweight trampoline `.exe` files (see [RFC: Trampoline EXE for Shims](./trampoline-exe-for-shims.md)). Each trampoline detects its tool name from its own filename, sets `VITE_PLUS_SHIM_TOOL`, and spawns `vp.exe`. This avoids the "Terminate batch job (Y/N)?" prompt from `.cmd` wrappers and works in all shells (cmd.exe, PowerShell, Git Bash) without needing separate wrapper formats. #### Why Not Symlinks? @@ -2148,68 +2138,14 @@ On Unix, shims are symlinks to the vp binary, which preserves argv[0] for tool d 1. **Admin privileges required**: Windows symlinks need admin rights or Developer Mode 2. **Unreliable Git Bash support**: Symlink emulation varies by Git for Windows version -3. **Consistent with .cmd approach**: Both .cmd and shell scripts use the same dispatch pattern -#### Wrapper Scripts - -**vp wrapper** (calls vp.exe directly): - -```sh -#!/bin/sh -VITE_PLUS_HOME="$(dirname "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")")" -export VITE_PLUS_HOME -exec "$VITE_PLUS_HOME/current/bin/vp.exe" "$@" -``` - -**Tool wrappers** (node, npm, npx - uses explicit dispatch): - -```sh -#!/bin/sh -VITE_PLUS_HOME="$(dirname "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")")" -export VITE_PLUS_HOME -exec "$VITE_PLUS_HOME/current/bin/vp.exe" env exec node "$@" -``` - -This ensures all commands work in: - -- Git Bash -- WSL (if accessing Windows paths) -- Any POSIX-compatible shell on Windows - -### Wrapper Script Template (vp.cmd) - -```batch -@echo off -set VITE_PLUS_HOME=%~dp0.. -"%VITE_PLUS_HOME%\current\bin\vp.exe" %* -exit /b %ERRORLEVEL% -``` - -The `vp.cmd` wrapper forwards all arguments to the actual `vp.exe` binary. - -### Wrapper Script Template (node.cmd, npm.cmd, npx.cmd) - -```batch -@echo off -set VITE_PLUS_HOME=%~dp0.. -"%VITE_PLUS_HOME%\current\bin\vp.exe" env exec node %* -exit /b %ERRORLEVEL% -``` - -For npm: - -```batch -@echo off -set VITE_PLUS_HOME=%~dp0.. -"%VITE_PLUS_HOME%\current\bin\vp.exe" env exec npm %* -exit /b %ERRORLEVEL% -``` +Instead, trampoline `.exe` files are used. See [RFC: Trampoline EXE for Shims](./trampoline-exe-for-shims.md) for the full design. **How it works**: 1. User runs `npm install` -2. Windows finds `~/.vite-plus/bin/npm.cmd` in PATH (cmd.exe/PowerShell) or `npm` (Git Bash) -3. Wrapper calls `vp.exe env exec npm install` +2. Windows finds `~/.vite-plus/bin/npm.exe` in PATH +3. Trampoline sets `VITE_PLUS_SHIM_TOOL=npm` and spawns `vp.exe` 4. `vp env exec` command handles version resolution and execution **Benefits of this approach**: @@ -2224,11 +2160,10 @@ exit /b %ERRORLEVEL% The Windows installer (`install.ps1`) follows this flow: -1. Download and install `vp.exe` to `~/.vite-plus/current/bin/` -2. Create `~/.vite-plus/bin/vp.cmd` wrapper script -3. Create `~/.vite-plus/bin/vp` shell script (for Git Bash) -4. Create shim wrappers: `node.cmd`, `npm.cmd`, `npx.cmd` (and corresponding shell scripts) -5. Configure User PATH to include `~/.vite-plus/bin` +1. Download and install `vp.exe` and `vp-shim.exe` to `~/.vite-plus/current/bin/` +2. Create `~/.vite-plus/bin/vp.exe` trampoline (copy of `vp-shim.exe`) +3. Create shim trampolines: `node.exe`, `npm.exe`, `npx.exe` (via `vp env setup`) +4. Configure User PATH to include `~/.vite-plus/bin` ## Testing Strategy @@ -2266,7 +2201,7 @@ env-doctor/ - ubuntu-latest: Full integration tests - macos-latest: Full integration tests -- windows-latest: Full integration tests with .cmd wrapper validation +- windows-latest: Full integration tests with trampoline `.exe` shim validation ## Security Considerations @@ -2282,7 +2217,7 @@ env-doctor/ 1. Add `vp env` command structure to CLI 2. Implement argv[0] detection in main.rs 3. Implement shim dispatch logic for `node` -4. Implement `vp env setup` (Unix symlinks, Windows .cmd wrappers) +4. Implement `vp env setup` (Unix symlinks, Windows trampoline `.exe` shims) 5. Implement `vp env doctor` basic diagnostics 6. Add resolution cache (persists across upgrades with version field) 7. Implement `vp env default [version]` to set/show global default Node.js version @@ -2338,7 +2273,7 @@ The following decisions have been made: 1. **VITE_PLUS_HOME Default Location**: `~/.vite-plus` - Simple, memorable path that's easy for users to find and configure. -2. **Windows Wrapper Strategy**: `.cmd` wrappers that call `vp env exec ` - Consistent with Volta, no binary copies needed. +2. **Windows Shim Strategy**: Trampoline `.exe` files that set `VITE_PLUS_SHIM_TOOL` and spawn `vp.exe` - Avoids "Terminate batch job?" prompt, works in all shells. See [RFC: Trampoline EXE for Shims](./trampoline-exe-for-shims.md). 3. **Corepack Handling**: Not included - vite-plus has integrated package manager functionality, making corepack shims unnecessary. diff --git a/rfcs/trampoline-exe-for-shims.md b/rfcs/trampoline-exe-for-shims.md new file mode 100644 index 0000000000..f6de78b2a4 --- /dev/null +++ b/rfcs/trampoline-exe-for-shims.md @@ -0,0 +1,285 @@ +# RFC: Windows Trampoline `.exe` for Shims + +## Status + +Implemented + +## Summary + +Replace Windows `.cmd` wrapper scripts with lightweight trampoline `.exe` binaries for all shim tools (`vp`, `node`, `npm`, `npx`, `vpx`, and globally installed package binaries). This eliminates the `Terminate batch job (Y/N)?` prompt that appears when users press Ctrl+C, providing the same clean signal behavior as direct `.exe` invocation. + +## Motivation + +### The Problem + +On Windows, the vite-plus CLI previously exposed tools through `.cmd` batch file wrappers: + +``` +~/.vite-plus/bin/ +├── vp.cmd → calls current\bin\vp.exe +├── node.cmd → calls vp.exe env exec node +├── npm.cmd → calls vp.exe env exec npm +├── npx.cmd → calls vp.exe env exec npx +└── ... +``` + +When a user presses Ctrl+C while a command is running through a `.cmd` wrapper, `cmd.exe` intercepts the signal and displays: + +``` +Terminate batch job (Y/N)? +``` + +This is a fundamental limitation of batch file execution on Windows. The prompt: + +- Interrupts the normal Ctrl+C workflow that users expect +- May appear multiple times (once per `.cmd` in the chain) +- Differs from Unix behavior where Ctrl+C cleanly terminates the process +- Cannot be suppressed from within the batch file + +### Confirmed Behavior + +As demonstrated in [issue #835](https://github.com/voidzero-dev/vite-plus/issues/835): + +1. Running `vp dev` (through `vp.cmd`) shows `Terminate batch job (Y/N)?` on Ctrl+C +2. Running `~/.vite-plus/current/bin/vp.exe dev` directly does **NOT** show the prompt +3. Running `npm.cmd run dev` shows the prompt; running `npm.ps1 run dev` does not +4. The prompt can appear multiple times when `.cmd` wrappers chain (e.g., `vp.cmd` → `npm.cmd`) + +### Why `.ps1` Scripts Are Not Sufficient + +PowerShell `.ps1` scripts avoid the Ctrl+C issue but have critical limitations: + +- `where.exe` and `which` do not discover `.ps1` files as executables +- Only work in PowerShell, not in `cmd.exe`, Git Bash, or other shells +- Cannot serve as universal shims + +## Architecture + +### Unix (Symlink-Based — Unchanged) + +On Unix, shims are symlinks to the `vp` binary. The binary detects the tool name from `argv[0]`: + +``` +~/.vite-plus/bin/ +├── vp → ../current/bin/vp (symlink) +├── node → ../current/bin/vp (symlink) +├── npm → ../current/bin/vp (symlink) +└── npx → ../current/bin/vp (symlink) +``` + +### Windows (Trampoline `.exe` Files) + +``` +~/.vite-plus/bin/ +├── vp.exe # Trampoline → spawns current\bin\vp.exe +├── node.exe # Trampoline → sets VITE_PLUS_SHIM_TOOL=node, spawns vp.exe +├── npm.exe # Trampoline → sets VITE_PLUS_SHIM_TOOL=npm, spawns vp.exe +├── npx.exe # Trampoline → sets VITE_PLUS_SHIM_TOOL=npx, spawns vp.exe +├── vpx.exe # Trampoline → sets VITE_PLUS_SHIM_TOOL=vpx, spawns vp.exe +└── tsc.exe # Trampoline → sets VITE_PLUS_SHIM_TOOL=tsc, spawns vp.exe (package shim) +``` + +Each trampoline is a copy of `vp-shim.exe` (the template binary distributed alongside `vp.exe`). + +**Note**: npm-installed packages (via `npm install -g`) still use `.cmd` wrappers because they lack `PackageMetadata` and need to point directly at npm's generated scripts. + +## Implementation + +### Crate Structure + +``` +crates/vite_trampoline/ +├── Cargo.toml # Zero external dependencies +├── src/ +│ └── main.rs # ~90 lines, single-file binary +``` + +### Trampoline Binary + +The trampoline has **zero external dependencies** — the Win32 FFI call (`SetConsoleCtrlHandler`) is declared inline to avoid the heavy `windows`/`windows-core` crates. It also avoids `core::fmt` (~100KB overhead) by never using `format!`, `eprintln!`, `println!`, or `.unwrap()`. + +```rust +use std::{env, process::{self, Command}}; + +fn main() { + // 1. Determine tool name from own filename (e.g., node.exe → "node") + let exe_path = env::current_exe().unwrap_or_else(|_| process::exit(1)); + let tool_name = exe_path.file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_else(|| process::exit(1)); + + // 2. Locate vp.exe at ../current/bin/vp.exe + let bin_dir = exe_path.parent().unwrap_or_else(|| process::exit(1)); + let vp_home = bin_dir.parent().unwrap_or_else(|| process::exit(1)); + let vp_exe = vp_home.join("current").join("bin").join("vp.exe"); + + // 3. Install Ctrl+C handler (ignores signal; child handles it) + install_ctrl_handler(); + + // 4. Spawn vp.exe with env vars + let mut cmd = Command::new(&vp_exe); + cmd.args(env::args_os().skip(1)); + cmd.env("VITE_PLUS_HOME", vp_home); + + if tool_name != "vp" { + cmd.env("VITE_PLUS_SHIM_TOOL", tool_name); + cmd.env_remove("VITE_PLUS_TOOL_RECURSION"); + } + + // 5. Propagate exit code (error message via write_all, not eprintln!) + match cmd.status() { + Ok(s) => process::exit(s.code().unwrap_or(1)), + Err(_) => { + use std::io::Write; + let mut stderr = std::io::stderr().lock(); + let _ = stderr.write_all(b"vite-plus: failed to execute "); + let _ = stderr.write_all(vp_exe.as_os_str().as_encoded_bytes()); + let _ = stderr.write_all(b"\n"); + process::exit(1); + } + } +} + +fn install_ctrl_handler() { + type HandlerRoutine = unsafe extern "system" fn(ctrl_type: u32) -> i32; + unsafe extern "system" { + fn SetConsoleCtrlHandler(handler: Option, add: i32) -> i32; + } + unsafe extern "system" fn handler(_ctrl_type: u32) -> i32 { 1 } + unsafe { SetConsoleCtrlHandler(Some(handler), 1); } +} +``` + +### Size Optimization + +| Technique | Savings | Status | +| ------------------------------------------------------------------------------------- | -------------------------- | ------ | +| Zero external dependencies (raw FFI) | ~20KB (vs `windows` crate) | Done | +| No direct `core::fmt` usage (avoid `eprintln!`/`format!`/`.unwrap()`) | Marginal | Done | +| Workspace profile: `lto="fat"`, `codegen-units=1`, `strip="symbols"`, `panic="abort"` | Inherited | Done | +| Per-package `opt-level="z"` (optimize for size) | ~5-10% | Done | + +**Binary size**: ~200KB on Windows. The floor is set by `std::process::Command` which internally pulls in `core::fmt` for error formatting regardless of whether our code uses it. Further reduction to ~40-50KB (matching uv-trampoline) would require replacing `Command` with raw `CreateProcessW` and using nightly Rust (see Future Optimizations). + +### Environment Variables + +The trampoline sets three env vars before spawning `vp.exe`: + +| Variable | When | Purpose | +| -------------------------- | -------------------------- | ------------------------------------------------------------------------------ | +| `VITE_PLUS_HOME` | Always | Tells vp.exe the install directory (derived from `bin_dir.parent()`) | +| `VITE_PLUS_SHIM_TOOL` | Tool shims only (not "vp") | Tells vp.exe to enter shim dispatch mode for the named tool | +| `VITE_PLUS_TOOL_RECURSION` | Removed for tool shims | Clears the recursion marker for fresh version resolution in nested invocations | + +### Ctrl+C Handling + +The trampoline installs a console control handler that returns `TRUE` (1): + +1. When Ctrl+C is pressed, Windows sends `CTRL_C_EVENT` to **all processes** in the console group +2. The trampoline's handler returns 1 (TRUE) → trampoline stays alive +3. The child process (`vp.exe` → Node.js) receives the **same** event +4. The child decides how to handle it (typically exits gracefully) +5. The trampoline detects the child's exit and propagates its exit code + +**No "Terminate batch job?" prompt** because there is no batch file involved. + +### Integration with Shim Detection + +`detect_shim_tool()` in `shim/mod.rs` checks `VITE_PLUS_SHIM_TOOL` env var **before** `argv[0]`: + +``` +Trampoline (node.exe) + → sets VITE_PLUS_SHIM_TOOL=node, VITE_PLUS_HOME=..., removes VITE_PLUS_TOOL_RECURSION + → spawns current/bin/vp.exe with original args + → detect_shim_tool() reads env var → "node" + → dispatch("node", args) + → resolves Node.js version, executes real node +``` + +### Running Exe Overwrite + +When `vp env setup --refresh` is invoked through the trampoline (`~/.vite-plus/bin/vp.exe`), the trampoline is still running. Windows prevents overwriting a running `.exe`. The solution: + +1. Rename existing `vp.exe` to `vp.exe..old` +2. Copy new trampoline to `vp.exe` +3. Best-effort cleanup of all `*.old` files in the bin directory + +### Distribution + +The trampoline binary (`vp-shim.exe`) is distributed alongside `vp.exe`: + +``` +~/.vite-plus/current/bin/ +├── vp.exe # Main CLI binary +└── vp-shim.exe # Trampoline template (copied as shims) +``` + +Included in: + +- Platform npm packages (`@voidzero-dev/vite-plus-cli-win32-x64-msvc`) +- Release artifacts (`.github/workflows/release.yml`) +- `install.ps1` and `install.sh` (both local dev and download paths) +- `extract_platform_package()` in the upgrade path + +### Legacy Fallback + +When installing a pre-trampoline version (no `vp-shim.exe` in the package): + +- `install.ps1` falls back to creating `.cmd` + shell script wrappers +- Stale trampoline `.exe` shims from a newer install are removed (`.exe` takes precedence over `.cmd` on Windows PATH) + +## Comparison with uv-trampoline + +| Aspect | uv-trampoline | vite-plus trampoline | +| ------------------- | ---------------------------------------- | ------------------------------------ | +| **Purpose** | Launch Python with embedded script | Forward to `vp.exe` | +| **Complexity** | High (PE resources, zipimport) | Low (filename + spawn) | +| **Data embedding** | PE resources (kind, path, script ZIP) | None (uses filename + relative path) | +| **Dependencies** | `windows` crate (unsafe, no CRT) | Zero (raw FFI declaration) | +| **Toolchain** | Nightly Rust (`panic="immediate-abort"`) | Stable Rust | +| **Binary size** | 39-47 KB | ~200 KB | +| **Entry point** | `#![no_main]` + `mainCRTStartup` | Standard `fn main()` | +| **Error output** | `ufmt` (no `core::fmt`) | `write_all` (no `core::fmt`) | +| **Ctrl+C handling** | `SetConsoleCtrlHandler` → ignore | Same approach | +| **Exit code** | `GetExitCodeProcess` → `exit()` | `Command::status()` → `exit()` | + +The vite-plus trampoline is significantly simpler because it doesn't need to embed data in PE resources — it just reads its own filename, finds `vp.exe` at a fixed relative path, and spawns it. The ~150KB size difference from uv-trampoline comes from `std::process::Command` (which internally pulls in `core::fmt`) versus raw `CreateProcessW` with nightly-only `#![no_main]`. + +## Alternatives Considered + +### 1. NTFS Hardlinks (Rejected) + +Hardlinks resolve to physical file inodes, not through directory junctions. After `vp` upgrade re-points `current`, hardlinks in `bin/` still reference the old binary. + +### 2. Windows Symbolic Links (Rejected) + +Requires administrator privileges or Developer Mode. Not reliable for all users. + +### 3. PowerShell `.ps1` Scripts (Rejected) + +`where.exe` and `which` do not find `.ps1` files. Only works in PowerShell. + +### 4. Copy `vp.exe` as Each Shim (Rejected) + +~5-10MB per copy. Trampoline achieves the same result at ~100KB. + +### 5. `windows` Crate for FFI (Rejected) + +Adds ~100KB to the binary for a single `SetConsoleCtrlHandler` call. Raw FFI declaration is sufficient. + +## Future Optimizations + +If the ~100KB binary size needs to be reduced further: + +1. **Switch to nightly Rust** with `panic="immediate-abort"` and `#![no_main]` + `mainCRTStartup` (~50KB savings) +2. **Use raw Win32 `CreateProcessW`** instead of `std::process::Command` (eliminates most of std's process machinery) +3. **Pre-build and check in** trampoline binaries (like uv does) to decouple the trampoline build from the workspace toolchain + +These would bring the binary to ~40-50KB, matching uv-trampoline, at the cost of requiring a nightly toolchain and more unsafe code. + +## References + +- [Issue #835](https://github.com/voidzero-dev/vite-plus/issues/835): Original feature request with video reproduction +- [uv-trampoline](https://github.com/astral-sh/uv/tree/main/crates/uv-trampoline): Reference implementation by astral-sh (~40KB with nightly Rust) +- [RFC: env-command](./env-command.md): Shim architecture documentation +- [RFC: upgrade-command](./upgrade-command.md): Upgrade/rollback flow diff --git a/rfcs/upgrade-command.md b/rfcs/upgrade-command.md index ba77076c96..7780e02342 100644 --- a/rfcs/upgrade-command.md +++ b/rfcs/upgrade-command.md @@ -31,7 +31,7 @@ A native `vp upgrade` command would allow users to update the CLI in-place with └── env.ps1 # PowerShell env ``` -Key invariant: `~/.vite-plus/bin/vp` is a symlink to `../current/bin/vp` (Unix) or a `.cmd` wrapper calling `current\bin\vp.exe` (Windows), and `current` is a symlink (Unix) or junction (Windows) to the active version directory. Upgrading swaps the `current` link — atomic on Unix, near-instant on Windows. +Key invariant: `~/.vite-plus/bin/vp` is a symlink to `../current/bin/vp` (Unix) or a trampoline `.exe` forwarding to `current\bin\vp.exe` (Windows), and `current` is a symlink (Unix) or junction (Windows) to the active version directory. Upgrading swaps the `current` link — atomic on Unix, near-instant on Windows. ## Goals @@ -295,7 +295,7 @@ Key differences on Windows: - **Junctions** (`mklink /J`) are used instead of symlinks — junctions don't require admin privileges - Junctions only work for directories (which `current` is), and use absolute paths internally - The swap is **not atomic** — there's a brief window (~milliseconds) where `current` doesn't exist -- `bin/vp` is a `.cmd` wrapper (not a symlink), so it doesn't need updating during upgrade +- `bin/vp.exe` is a trampoline (not a symlink) that resolves through `current`, so it doesn't need updating during upgrade - This matches the existing `install.ps1` behavior exactly #### Step 6: Post-Update (Non-Fatal) @@ -314,7 +314,7 @@ The running `vp` process is **not** the binary being replaced. The flow is: ~/.vite-plus/bin/vp → ../current/bin/vp → {old_version}/bin/vp # Windows -~/.vite-plus/bin/vp.cmd → current\bin\vp.exe → {old_version}\bin\vp.exe +~/.vite-plus/bin/vp.exe (trampoline) → current\bin\vp.exe → {old_version}\bin\vp.exe ``` After the `current` link swap, any **new** invocation of `vp` will use the new binary. The currently running process continues to execute from the old version's binary file on disk: diff --git a/rfcs/vpx-command.md b/rfcs/vpx-command.md index 5d6081ff89..1fc645f5c9 100644 --- a/rfcs/vpx-command.md +++ b/rfcs/vpx-command.md @@ -155,13 +155,7 @@ if tool == "vpx" { ### Windows -On Windows, `vpx.cmd` is a wrapper script (consistent with existing `node.cmd`, `npm.cmd`, `npx.cmd` wrappers): - -```cmd -@echo off -set "VITE_PLUS_SHIM_TOOL=vpx" -"%~dp0..\current\bin\vp.exe" %* -``` +On Windows, `vpx.exe` is a trampoline executable (consistent with existing `node.exe`, `npm.exe`, `npx.exe` shims). It detects its tool name from its own filename (`vpx`), sets `VITE_PLUS_SHIM_TOOL=vpx`, and spawns `vp.exe`. See [RFC: Trampoline EXE for Shims](./trampoline-exe-for-shims.md). ### Setup From bc1b785e9820bc326c1cc4b839efca401c7db057 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 18 Mar 2026 08:50:49 +0800 Subject: [PATCH 2/2] feat(install): add bun as a package manager (#557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add bun as the 4th supported package manager alongside pnpm, npm, and yarn. Bun is added only as a package manager — runtime support is not planned. Rust core: - Add `Bun` variant to `PackageManagerType` enum - Detect bun via `packageManager` field, `bun.lock`, `bun.lockb`, `bunfig.toml` - Download platform-specific native binary from `@oven/bun-{os}-{arch}` npm packages - Add native binary shim support (non-Node.js wrappers for sh/cmd/ps1) - Add `PackageManagerType::Bun` arms to all 30 command files - Add bun to interactive package manager selection menu Global CLI & NAPI: - Add `"bun"` to `PACKAGE_MANAGER_TOOLS` in shim dispatch - Add `"bun" => PackageManagerType::Bun` in NAPI binding TypeScript: - Add `bun` to `PackageManager` type and selection prompt - Add `bunx` as DLX command runner - Handle bun in monorepo templates and migration (overrides, no catalog) RFCs: - Update all 11 package-manager RFCs with bun command mappings --- crates/vite_global_cli/src/shim/dispatch.rs | 2 +- crates/vite_install/src/commands/add.rs | 42 ++ crates/vite_install/src/commands/audit.rs | 23 ++ crates/vite_install/src/commands/cache.rs | 22 ++ crates/vite_install/src/commands/config.rs | 26 ++ crates/vite_install/src/commands/dedupe.rs | 6 + crates/vite_install/src/commands/deprecate.rs | 11 +- crates/vite_install/src/commands/dist_tag.rs | 8 + crates/vite_install/src/commands/dlx.rs | 26 ++ crates/vite_install/src/commands/fund.rs | 11 +- crates/vite_install/src/commands/install.rs | 78 +++- crates/vite_install/src/commands/link.rs | 4 + crates/vite_install/src/commands/list.rs | 58 +++ crates/vite_install/src/commands/login.rs | 8 + crates/vite_install/src/commands/logout.rs | 8 + crates/vite_install/src/commands/outdated.rs | 43 +++ crates/vite_install/src/commands/owner.rs | 11 +- crates/vite_install/src/commands/pack.rs | 31 ++ crates/vite_install/src/commands/ping.rs | 11 +- crates/vite_install/src/commands/prune.rs | 6 + crates/vite_install/src/commands/publish.rs | 58 +++ crates/vite_install/src/commands/rebuild.rs | 4 + crates/vite_install/src/commands/remove.rs | 15 + crates/vite_install/src/commands/run.rs | 1 + crates/vite_install/src/commands/search.rs | 11 +- crates/vite_install/src/commands/token.rs | 11 +- crates/vite_install/src/commands/unlink.rs | 8 + crates/vite_install/src/commands/update.rs | 16 + crates/vite_install/src/commands/view.rs | 10 +- crates/vite_install/src/commands/whoami.rs | 5 + crates/vite_install/src/commands/why.rs | 46 +++ crates/vite_install/src/package_manager.rs | 361 +++++++++++++++++- crates/vite_install/src/shim.rs | 78 ++++ packages/cli/binding/src/package_manager.rs | 1 + packages/cli/src/create/command.ts | 4 +- packages/cli/src/create/templates/monorepo.ts | 2 +- packages/cli/src/migration/bin.ts | 5 +- packages/cli/src/migration/migrator.ts | 7 +- packages/cli/src/types/package.ts | 1 + packages/cli/src/utils/prompts.ts | 1 + rfcs/add-remove-package-commands.md | 145 ++++--- rfcs/dedupe-package-command.md | 26 +- rfcs/dlx-command.md | 34 +- rfcs/exec-command.md | 14 +- rfcs/install-command.md | 88 +++-- rfcs/link-unlink-package-commands.md | 57 ++- rfcs/outdated-package-command.md | 72 ++-- rfcs/pack-command.md | 10 +- rfcs/pm-command-group.md | 321 ++++++++-------- rfcs/update-package-command.md | 60 +-- rfcs/why-package-command.md | 65 ++-- 51 files changed, 1553 insertions(+), 419 deletions(-) diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 21a39a64a7..d0166ae90e 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -27,7 +27,7 @@ const RECURSION_ENV_VAR: &str = env_vars::VITE_PLUS_TOOL_RECURSION; /// Package manager tools that should resolve Node.js version from the project context /// rather than using the install-time version. -const PACKAGE_MANAGER_TOOLS: &[&str] = &["pnpm", "yarn"]; +const PACKAGE_MANAGER_TOOLS: &[&str] = &["pnpm", "yarn", "bun"]; fn is_package_manager_tool(tool: &str) -> bool { PACKAGE_MANAGER_TOOLS.contains(&tool) diff --git a/crates/vite_install/src/commands/add.rs b/crates/vite_install/src/commands/add.rs index 0a7285e10f..e223e5f517 100644 --- a/crates/vite_install/src/commands/add.rs +++ b/crates/vite_install/src/commands/add.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -195,6 +196,47 @@ impl PackageManager { args.push("--save-exact".into()); } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("add".into()); + + if let Some(save_dependency_type) = options.save_dependency_type { + match save_dependency_type { + SaveDependencyType::Production => { + // default, no flag needed + } + SaveDependencyType::Dev => { + args.push("--dev".into()); + } + SaveDependencyType::Peer => { + args.push("--peer".into()); + } + SaveDependencyType::Optional => { + args.push("--optional".into()); + } + } + } + if options.save_exact { + args.push("--exact".into()); + } + if let Some(filters) = options.filters { + if !filters.is_empty() { + output::warn("bun add does not support --filter"); + } + } + if options.workspace_root { + output::warn("bun add does not support --workspace-root"); + } + if options.workspace_only { + output::warn("bun add does not support --workspace-only"); + } + if options.save_catalog_name.is_some() { + output::warn("bun add does not support --save-catalog-name"); + } + if options.allow_build.is_some() { + output::warn("bun add does not support --allow-build"); + } + } } if let Some(pass_through_args) = options.pass_through_args { diff --git a/crates/vite_install/src/commands/audit.rs b/crates/vite_install/src/commands/audit.rs index 55722a2510..31dbc857f1 100644 --- a/crates/vite_install/src/commands/audit.rs +++ b/crates/vite_install/src/commands/audit.rs @@ -142,6 +142,29 @@ impl PackageManager { } } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("pm".into()); + args.push("audit".into()); + + if options.fix { + output::warn("bun pm audit does not support --fix"); + return None; + } + + if let Some(level) = options.level { + args.push("--level".into()); + args.push(level.to_string()); + } + + if options.production { + output::warn("--production not supported by bun pm audit, ignoring flag"); + } + + if options.json { + args.push("--json".into()); + } + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/cache.rs b/crates/vite_install/src/commands/cache.rs index c8402535a3..cd6aff3b31 100644 --- a/crates/vite_install/src/commands/cache.rs +++ b/crates/vite_install/src/commands/cache.rs @@ -117,6 +117,28 @@ impl PackageManager { } } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + + match options.subcommand { + "dir" | "path" => { + args.push("pm".into()); + args.push("cache".into()); + } + "clean" => { + args.push("pm".into()); + args.push("cache".into()); + args.push("rm".into()); + } + _ => { + output::warn(&format!( + "bun pm cache subcommand '{}' not supported", + options.subcommand + )); + return None; + } + } + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/config.rs b/crates/vite_install/src/commands/config.rs index 30f18489bc..9472fdaaf5 100644 --- a/crates/vite_install/src/commands/config.rs +++ b/crates/vite_install/src/commands/config.rs @@ -127,6 +127,32 @@ impl PackageManager { } } } + PackageManagerType::Bun => { + output::warn( + "bun uses bunfig.toml for configuration, not a config command. Falling back to npm config.", + ); + + // Fall back to npm config + args.push("config".into()); + args.push(options.subcommand.to_string()); + + if let Some(key) = options.key { + args.push(key.to_string()); + } + + if let Some(value) = options.value { + args.push(value.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + if let Some(location) = options.location { + args.push("--location".into()); + args.push(location.to_string()); + } + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/dedupe.rs b/crates/vite_install/src/commands/dedupe.rs index 616caea26a..05f73b6113 100644 --- a/crates/vite_install/src/commands/dedupe.rs +++ b/crates/vite_install/src/commands/dedupe.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -63,6 +64,11 @@ impl PackageManager { args.push("--dry-run".into()); } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + output::warn("bun does not support dedupe, falling back to bun install"); + args.push("install".into()); + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/deprecate.rs b/crates/vite_install/src/commands/deprecate.rs index 36e471f4fb..31ed407574 100644 --- a/crates/vite_install/src/commands/deprecate.rs +++ b/crates/vite_install/src/commands/deprecate.rs @@ -4,7 +4,9 @@ use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use vite_shared::output; + +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Options for the deprecate command. #[derive(Debug, Default)] @@ -31,6 +33,7 @@ impl PackageManager { /// Resolve the deprecate command. /// All package managers delegate to npm deprecate. + /// Bun does not support deprecate, falls back to npm. #[must_use] pub fn resolve_deprecate_command( &self, @@ -40,6 +43,12 @@ impl PackageManager { let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not support the deprecate command, falling back to npm deprecate", + ); + } + args.push("deprecate".into()); args.push(options.package.to_string()); args.push(options.message.to_string()); diff --git a/crates/vite_install/src/commands/dist_tag.rs b/crates/vite_install/src/commands/dist_tag.rs index e1b6258474..2e5080abd6 100644 --- a/crates/vite_install/src/commands/dist_tag.rs +++ b/crates/vite_install/src/commands/dist_tag.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -64,6 +65,13 @@ impl PackageManager { args.push("tag".into()); } } + PackageManagerType::Bun => { + output::warn( + "bun does not support dist-tag, falling back to npm dist-tag", + ); + bin_name = "npm".into(); + args.push("dist-tag".into()); + } } match &options.subcommand { diff --git a/crates/vite_install/src/commands/dlx.rs b/crates/vite_install/src/commands/dlx.rs index 4e4f357eba..3f23b9a73a 100644 --- a/crates/vite_install/src/commands/dlx.rs +++ b/crates/vite_install/src/commands/dlx.rs @@ -52,6 +52,7 @@ impl PackageManager { self.resolve_yarn_dlx(options, envs) } } + PackageManagerType::Bun => self.resolve_bun_dlx(options, envs), } } @@ -187,6 +188,31 @@ impl PackageManager { let args = build_npx_args(options); ResolveCommandResult { bin_path: "npx".into(), args, envs } } + + fn resolve_bun_dlx( + &self, + options: &DlxCommandOptions, + envs: HashMap, + ) -> ResolveCommandResult { + let mut args = Vec::new(); + + // bunx is the dlx equivalent, no subcommand needed + // Add package spec + args.push(options.package_spec.into()); + + // Add command arguments + args.extend(options.args.iter().cloned()); + + // Warn about unsupported flags + if !options.packages.is_empty() { + output::warn("bunx does not support --package"); + } + if options.shell_mode { + output::warn("bunx does not support shell mode (-c)"); + } + + ResolveCommandResult { bin_path: "bunx".into(), args, envs } + } } /// Build npx command-line arguments from dlx options. diff --git a/crates/vite_install/src/commands/fund.rs b/crates/vite_install/src/commands/fund.rs index 55bdb7ff08..b6f553435b 100644 --- a/crates/vite_install/src/commands/fund.rs +++ b/crates/vite_install/src/commands/fund.rs @@ -4,7 +4,9 @@ use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use vite_shared::output; + +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Options for the fund command. #[derive(Debug, Default)] @@ -28,12 +30,19 @@ impl PackageManager { /// Resolve the fund command. /// All package managers delegate to npm fund. + /// Bun does not support fund, falls back to npm. #[must_use] pub fn resolve_fund_command(&self, options: &FundCommandOptions) -> ResolveCommandResult { let bin_name: String = "npm".to_string(); let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not support the fund command, falling back to npm fund", + ); + } + args.push("fund".into()); if options.json { diff --git a/crates/vite_install/src/commands/install.rs b/crates/vite_install/src/commands/install.rs index ba38c7fc84..d0052a881e 100644 --- a/crates/vite_install/src/commands/install.rs +++ b/crates/vite_install/src/commands/install.rs @@ -1,9 +1,9 @@ use std::{collections::HashMap, iter, process::ExitStatus}; -use tracing::warn; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -165,8 +165,8 @@ impl PackageManager { args.push("--mode".into()); args.push("update-lockfile".into()); if options.ignore_scripts { - warn!( - "yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts" + output::warn( + "yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts", ); } } else if options.ignore_scripts { @@ -177,15 +177,15 @@ impl PackageManager { args.push("--refresh-lockfile".into()); } if options.silent { - warn!( - "yarn@2+ does not support --silent, use YARN_ENABLE_PROGRESS=false instead" + output::warn( + "yarn@2+ does not support --silent, use YARN_ENABLE_PROGRESS=false instead", ); } if options.prod { - warn!("yarn@2+ requires configuration in .yarnrc.yml for --prod behavior"); + output::warn("yarn@2+ requires configuration in .yarnrc.yml for --prod behavior"); } if options.resolution_only { - warn!("yarn@2+ does not support --resolution-only"); + output::warn("yarn@2+ does not support --resolution-only"); } } else { // yarn@1 (Classic) @@ -220,10 +220,10 @@ impl PackageManager { args.push("--no-lockfile".into()); } if options.fix_lockfile { - warn!("yarn@1 does not support --fix-lockfile"); + output::warn("yarn@1 does not support --fix-lockfile"); } if options.resolution_only { - warn!("yarn@1 does not support --resolution-only"); + output::warn("yarn@1 does not support --resolution-only"); } if options.workspace_root { args.push("-W".into()); @@ -269,10 +269,10 @@ impl PackageManager { args.push("--no-package-lock".into()); } if options.fix_lockfile { - warn!("npm does not support --fix-lockfile"); + output::warn("npm does not support --fix-lockfile"); } if options.resolution_only { - warn!("npm does not support --resolution-only"); + output::warn("npm does not support --resolution-only"); } if options.silent { args.push("--loglevel".into()); @@ -288,6 +288,62 @@ impl PackageManager { } } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("install".into()); + + if options.prod { + args.push("--production".into()); + } + // --no-frozen-lockfile takes higher priority over --frozen-lockfile + if options.no_frozen_lockfile { + args.push("--no-frozen-lockfile".into()); + } else if options.frozen_lockfile { + args.push("--frozen-lockfile".into()); + } + if options.force { + args.push("--force".into()); + } + if options.silent { + args.push("--silent".into()); + } + if options.no_optional { + output::warn("bun does not directly support --no-optional"); + } + if options.lockfile_only { + output::warn("bun does not support --lockfile-only"); + } + if options.prefer_offline { + output::warn("bun does not support --prefer-offline"); + } + if options.offline { + output::warn("bun does not support --offline"); + } + if options.ignore_scripts { + output::warn( + "bun uses trustedDependencies instead of --ignore-scripts", + ); + } + if options.no_lockfile { + output::warn("bun does not support --no-lockfile"); + } + if options.fix_lockfile { + output::warn("bun does not support --fix-lockfile"); + } + // shamefully-hoist: bun uses hoisted node_modules by default, skip silently + if options.resolution_only { + output::warn("bun does not support --resolution-only"); + } + if let Some(filters) = options.filters { + for filter in filters { + args.push("--filter".into()); + args.push(filter.clone()); + } + } + if options.workspace_root { + output::warn("bun does not support --workspace-root"); + } + } } if let Some(pass_through_args) = options.pass_through_args { diff --git a/crates/vite_install/src/commands/link.rs b/crates/vite_install/src/commands/link.rs index fa7196d319..ec68367c7f 100644 --- a/crates/vite_install/src/commands/link.rs +++ b/crates/vite_install/src/commands/link.rs @@ -49,6 +49,10 @@ impl PackageManager { bin_name = "npm".into(); args.push("link".into()); } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("link".into()); + } } // Add package/directory if specified diff --git a/crates/vite_install/src/commands/list.rs b/crates/vite_install/src/commands/list.rs index ffe3a020df..ed73bfd615 100644 --- a/crates/vite_install/src/commands/list.rs +++ b/crates/vite_install/src/commands/list.rs @@ -198,6 +198,64 @@ impl PackageManager { } } } + PackageManagerType::Bun => { + args.push("pm".into()); + args.push("ls".into()); + + if let Some(pattern) = options.pattern { + args.push(pattern.to_string()); + } + + if options.depth.is_some() { + output::warn("--depth not supported by bun pm ls, ignoring flag"); + } + + if options.json { + output::warn("--json not supported by bun pm ls, ignoring flag"); + } + + if options.long { + output::warn("--long not supported by bun pm ls, ignoring flag"); + } + + if options.parseable { + output::warn("--parseable not supported by bun pm ls, ignoring flag"); + } + + if options.prod { + output::warn("--prod not supported by bun pm ls, ignoring flag"); + } + + if options.dev { + output::warn("--dev not supported by bun pm ls, ignoring flag"); + } + + if options.no_optional { + output::warn("--no-optional not supported by bun pm ls, ignoring flag"); + } + + if options.exclude_peers { + output::warn("--exclude-peers not supported by bun pm ls, ignoring flag"); + } + + if options.only_projects { + output::warn("--only-projects not supported by bun pm ls, ignoring flag"); + } + + if options.find_by.is_some() { + output::warn("--find-by not supported by bun pm ls, ignoring flag"); + } + + if options.recursive { + output::warn("--recursive not supported by bun pm ls, ignoring flag"); + } + + if let Some(filters) = options.filters { + if !filters.is_empty() { + output::warn("--filter not supported by bun pm ls, ignoring flag"); + } + } + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/login.rs b/crates/vite_install/src/commands/login.rs index 3fbb05325f..81461ae205 100644 --- a/crates/vite_install/src/commands/login.rs +++ b/crates/vite_install/src/commands/login.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -55,6 +56,13 @@ impl PackageManager { args.push("login".into()); } } + PackageManagerType::Bun => { + output::warn( + "bun does not have a login command, falling back to npm login", + ); + bin_name = "npm".into(); + args.push("login".into()); + } } if let Some(registry) = options.registry { diff --git a/crates/vite_install/src/commands/logout.rs b/crates/vite_install/src/commands/logout.rs index b64ae70a58..6714355026 100644 --- a/crates/vite_install/src/commands/logout.rs +++ b/crates/vite_install/src/commands/logout.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -55,6 +56,13 @@ impl PackageManager { args.push("logout".into()); } } + PackageManagerType::Bun => { + output::warn( + "bun does not have a logout command, falling back to npm logout", + ); + bin_name = "npm".into(); + args.push("logout".into()); + } } if let Some(registry) = options.registry { diff --git a/crates/vite_install/src/commands/outdated.rs b/crates/vite_install/src/commands/outdated.rs index 2ff8b6c4d2..d06ecf137a 100644 --- a/crates/vite_install/src/commands/outdated.rs +++ b/crates/vite_install/src/commands/outdated.rs @@ -219,6 +219,49 @@ impl PackageManager { bin_name = "npm".into(); Self::format_npm_outdated_args(&mut args, options); } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("outdated".into()); + + if let Some(filters) = options.filters { + for filter in filters { + args.push("--filter".into()); + args.push(filter.clone()); + } + } + + if options.recursive { + args.push("--recursive".into()); + } + + // Add packages + args.extend_from_slice(options.packages); + + if let Some(format) = options.format { + if format == Format::Json { + output::warn("bun outdated does not support --format json"); + } + } + + if options.long { + output::warn("bun outdated does not support --long"); + } + if options.workspace_root { + output::warn("bun outdated does not support --workspace-root"); + } + if options.prod || options.dev { + output::warn("bun outdated does not support --prod/--dev"); + } + if options.no_optional { + output::warn("bun outdated does not support --no-optional"); + } + if options.compatible { + output::warn("bun outdated does not support --compatible"); + } + if options.sort_by.is_some() { + output::warn("bun outdated does not support --sort-by"); + } + } } } diff --git a/crates/vite_install/src/commands/owner.rs b/crates/vite_install/src/commands/owner.rs index 1b5b21c65b..537345ea4c 100644 --- a/crates/vite_install/src/commands/owner.rs +++ b/crates/vite_install/src/commands/owner.rs @@ -4,7 +4,9 @@ use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use vite_shared::output; + +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Owner subcommand type. #[derive(Debug, Clone)] @@ -29,12 +31,19 @@ impl PackageManager { /// Resolve the owner command. /// All package managers delegate to npm owner. + /// Bun does not support owner, falls back to npm. #[must_use] pub fn resolve_owner_command(&self, subcommand: &OwnerSubcommand) -> ResolveCommandResult { let bin_name: String = "npm".to_string(); let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not support the owner command, falling back to npm owner", + ); + } + args.push("owner".into()); match subcommand { diff --git a/crates/vite_install/src/commands/pack.rs b/crates/vite_install/src/commands/pack.rs index 166dd4156f..af77a95b2f 100644 --- a/crates/vite_install/src/commands/pack.rs +++ b/crates/vite_install/src/commands/pack.rs @@ -173,6 +173,37 @@ impl PackageManager { args.push("--json".into()); } } + PackageManagerType::Bun => { + args.push("pm".into()); + args.push("pack".into()); + + if options.recursive { + output::warn("--recursive not supported by bun pm pack, ignoring flag"); + } + + if let Some(filters) = options.filters { + if !filters.is_empty() { + output::warn("--filter not supported by bun pm pack, ignoring flag"); + } + } + + if options.out.is_some() { + output::warn("--out not supported by bun pm pack, ignoring flag"); + } + + if let Some(dest) = options.pack_destination { + args.push("--destination".into()); + args.push(dest.to_string()); + } + + if options.pack_gzip_level.is_some() { + output::warn("--pack-gzip-level not supported by bun pm pack, ignoring flag"); + } + + if options.json { + output::warn("--json not supported by bun pm pack, ignoring flag"); + } + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/ping.rs b/crates/vite_install/src/commands/ping.rs index d40bdafe34..90b16c9afa 100644 --- a/crates/vite_install/src/commands/ping.rs +++ b/crates/vite_install/src/commands/ping.rs @@ -4,7 +4,9 @@ use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use vite_shared::output; + +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Options for the ping command. #[derive(Debug, Default)] @@ -28,12 +30,19 @@ impl PackageManager { /// Resolve the ping command. /// All package managers delegate to npm ping. + /// Bun does not support ping, falls back to npm. #[must_use] pub fn resolve_ping_command(&self, options: &PingCommandOptions) -> ResolveCommandResult { let bin_name: String = "npm".to_string(); let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not support the ping command, falling back to npm ping", + ); + } + args.push("ping".into()); if let Some(registry_value) = options.registry { diff --git a/crates/vite_install/src/commands/prune.rs b/crates/vite_install/src/commands/prune.rs index f9edc3c40a..fa6c77cff0 100644 --- a/crates/vite_install/src/commands/prune.rs +++ b/crates/vite_install/src/commands/prune.rs @@ -75,6 +75,12 @@ impl PackageManager { ); return None; } + PackageManagerType::Bun => { + output::warn( + "bun does not have a 'prune' command. bun install will prune extraneous packages automatically.", + ); + return None; + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/publish.rs b/crates/vite_install/src/commands/publish.rs index 242086fc47..185c1d329f 100644 --- a/crates/vite_install/src/commands/publish.rs +++ b/crates/vite_install/src/commands/publish.rs @@ -168,6 +168,64 @@ impl PackageManager { output::warn("--json not supported by npm, ignoring flag"); } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + + args.push("publish".into()); + + if let Some(target) = options.target { + args.push(target.to_string()); + } + + if options.dry_run { + args.push("--dry-run".into()); + } + + if let Some(tag) = options.tag { + args.push("--tag".into()); + args.push(tag.to_string()); + } + + if let Some(access) = options.access { + args.push("--access".into()); + args.push(access.to_string()); + } + + if let Some(otp) = options.otp { + args.push("--otp".into()); + args.push(otp.to_string()); + } + + if options.no_git_checks { + output::warn("--no-git-checks not supported by bun, ignoring flag"); + } + + if options.publish_branch.is_some() { + output::warn("--publish-branch not supported by bun, ignoring flag"); + } + + if options.report_summary { + output::warn("--report-summary not supported by bun, ignoring flag"); + } + + if options.force { + output::warn("--force not supported by bun publish, ignoring flag"); + } + + if options.json { + output::warn("--json not supported by bun publish, ignoring flag"); + } + + if options.recursive { + output::warn("--recursive not supported by bun publish, ignoring flag"); + } + + if let Some(filters) = options.filters { + if !filters.is_empty() { + output::warn("--filter not supported by bun publish, ignoring flag"); + } + } + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/rebuild.rs b/crates/vite_install/src/commands/rebuild.rs index f0076e3ea1..b85725ef3b 100644 --- a/crates/vite_install/src/commands/rebuild.rs +++ b/crates/vite_install/src/commands/rebuild.rs @@ -63,6 +63,10 @@ impl PackageManager { return None; } + PackageManagerType::Bun => { + output::warn("bun does not support the rebuild command"); + return None; + } } // Add pass-through args diff --git a/crates/vite_install/src/commands/remove.rs b/crates/vite_install/src/commands/remove.rs index f7a9f51ff8..eb380760ee 100644 --- a/crates/vite_install/src/commands/remove.rs +++ b/crates/vite_install/src/commands/remove.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, @@ -127,6 +128,20 @@ impl PackageManager { } // not support: save_dev, save_optional, save_prod, just ignore them } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("remove".into()); + + if let Some(filters) = options.filters { + if !filters.is_empty() { + output::warn("bun remove does not support --filter"); + } + } + if options.workspace_root { + output::warn("bun remove does not support --workspace-root"); + } + // bun remove doesn't support save_dev, save_optional, save_prod flags + } } if let Some(pass_through_args) = options.pass_through_args { diff --git a/crates/vite_install/src/commands/run.rs b/crates/vite_install/src/commands/run.rs index 6ebfdd430c..b5bfb95d2d 100644 --- a/crates/vite_install/src/commands/run.rs +++ b/crates/vite_install/src/commands/run.rs @@ -31,6 +31,7 @@ impl PackageManager { PackageManagerType::Pnpm => "pnpm", PackageManagerType::Npm => "npm", PackageManagerType::Yarn => "yarn", + PackageManagerType::Bun => "bun", }; ResolveCommandResult { bin_path: bin_path.to_string(), args: cmd_args, envs } diff --git a/crates/vite_install/src/commands/search.rs b/crates/vite_install/src/commands/search.rs index 5375c3059d..ce6cc41233 100644 --- a/crates/vite_install/src/commands/search.rs +++ b/crates/vite_install/src/commands/search.rs @@ -4,7 +4,9 @@ use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use vite_shared::output; + +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Options for the search command. #[derive(Debug, Default)] @@ -31,12 +33,19 @@ impl PackageManager { /// Resolve the search command. /// All package managers delegate to npm search. + /// Bun does not support search, falls back to npm. #[must_use] pub fn resolve_search_command(&self, options: &SearchCommandOptions) -> ResolveCommandResult { let bin_name: String = "npm".to_string(); let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not support the search command, falling back to npm search", + ); + } + args.push("search".into()); for term in options.terms { diff --git a/crates/vite_install/src/commands/token.rs b/crates/vite_install/src/commands/token.rs index 1f314ae3fb..17f4bef007 100644 --- a/crates/vite_install/src/commands/token.rs +++ b/crates/vite_install/src/commands/token.rs @@ -4,7 +4,9 @@ use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use vite_shared::output; + +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Token subcommand type. #[derive(Debug, Clone)] @@ -43,12 +45,19 @@ impl PackageManager { /// Resolve the token command. /// All package managers delegate to npm token. + /// Bun does not support token, falls back to npm. #[must_use] pub fn resolve_token_command(&self, subcommand: &TokenSubcommand) -> ResolveCommandResult { let bin_name: String = "npm".to_string(); let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not support the token command, falling back to npm token", + ); + } + args.push("token".into()); match subcommand { diff --git a/crates/vite_install/src/commands/unlink.rs b/crates/vite_install/src/commands/unlink.rs index a71e956897..e69b56a49d 100644 --- a/crates/vite_install/src/commands/unlink.rs +++ b/crates/vite_install/src/commands/unlink.rs @@ -63,6 +63,14 @@ impl PackageManager { output::warn("npm doesn't support --recursive for unlink command"); } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("unlink".into()); + + if options.recursive { + output::warn("bun doesn't support --recursive for unlink command"); + } + } } // Add package if specified diff --git a/crates/vite_install/src/commands/update.rs b/crates/vite_install/src/commands/update.rs index 3f474bf419..12a267e45a 100644 --- a/crates/vite_install/src/commands/update.rs +++ b/crates/vite_install/src/commands/update.rs @@ -179,6 +179,22 @@ impl PackageManager { output::warn("npm doesn't support interactive mode. Running standard update."); } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("update".into()); + + if options.latest { + args.push("--latest".into()); + } + if options.interactive { + args.push("--interactive".into()); + } + if options.recursive { + output::warn( + "bun updates all workspaces by default, --recursive is not needed", + ); + } + } } if let Some(pass_through_args) = options.pass_through_args { diff --git a/crates/vite_install/src/commands/view.rs b/crates/vite_install/src/commands/view.rs index 0307f5323c..d86f3798ad 100644 --- a/crates/vite_install/src/commands/view.rs +++ b/crates/vite_install/src/commands/view.rs @@ -3,8 +3,9 @@ use std::{collections::HashMap, process::ExitStatus}; use vite_command::run_command; use vite_error::Error; use vite_path::AbsolutePath; +use vite_shared::output; -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; +use crate::package_manager::{PackageManager, PackageManagerType, ResolveCommandResult, format_path_env}; /// Options for the view command. #[derive(Debug, Default)] @@ -30,12 +31,19 @@ impl PackageManager { /// Resolve the view command. /// All package managers delegate to npm view (pnpm and yarn use npm internally). + /// Bun does not have a direct equivalent, so we fall back to npm view. #[must_use] pub fn resolve_view_command(&self, options: &ViewCommandOptions) -> ResolveCommandResult { let bin_name: String = "npm".to_string(); let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); + if self.client == PackageManagerType::Bun { + output::warn( + "bun does not have a view command, falling back to npm view", + ); + } + args.push("view".into()); args.push(options.package.to_string()); diff --git a/crates/vite_install/src/commands/whoami.rs b/crates/vite_install/src/commands/whoami.rs index a441c0fb60..15daa898c8 100644 --- a/crates/vite_install/src/commands/whoami.rs +++ b/crates/vite_install/src/commands/whoami.rs @@ -62,6 +62,11 @@ impl PackageManager { args.push("npm".into()); args.push("whoami".into()); } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("pm".into()); + args.push("whoami".into()); + } } if let Some(registry) = options.registry { diff --git a/crates/vite_install/src/commands/why.rs b/crates/vite_install/src/commands/why.rs index c835feb450..08f6058e63 100644 --- a/crates/vite_install/src/commands/why.rs +++ b/crates/vite_install/src/commands/why.rs @@ -200,6 +200,52 @@ impl PackageManager { output::warn("--find-by not supported by npm"); } } + PackageManagerType::Bun => { + bin_name = "bun".into(); + + // bun has a direct `why` subcommand (not `bun pm why`) + args.push("why".into()); + + // Add packages + args.extend_from_slice(options.packages); + + // Warn about unsupported flags + if options.json { + output::warn("--json not supported by bun why"); + } + if options.long { + output::warn("--long not supported by bun why"); + } + if options.parseable { + output::warn("--parseable not supported by bun why"); + } + if options.recursive { + output::warn("--recursive not supported by bun why"); + } + if let Some(filters) = options.filters { + if !filters.is_empty() { + output::warn("--filter not supported by bun why"); + } + } + if options.workspace_root { + output::warn("--workspace-root not supported by bun why"); + } + if options.prod || options.dev { + output::warn("--prod/--dev not supported by bun why"); + } + if options.depth.is_some() { + output::warn("--depth not supported by bun why"); + } + if options.no_optional { + output::warn("--no-optional not supported by bun why"); + } + if options.exclude_peers { + output::warn("--exclude-peers not supported by bun why"); + } + if options.find_by.is_some() { + output::warn("--find-by not supported by bun why"); + } + } } // Add pass-through args diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index 851c3ae007..5afcb21e87 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -44,6 +44,7 @@ pub enum PackageManagerType { Pnpm, Yarn, Npm, + Bun, } impl fmt::Display for PackageManagerType { @@ -52,6 +53,7 @@ impl fmt::Display for PackageManagerType { Self::Pnpm => write!(f, "pnpm"), Self::Yarn => write!(f, "yarn"), Self::Npm => write!(f, "npm"), + Self::Bun => write!(f, "bun"), } } } @@ -194,6 +196,11 @@ impl PackageManager { ignores.push("!**/package-lock.json".into()); ignores.push("!**/npm-shrinkwrap.json".into()); } + PackageManagerType::Bun => { + ignores.push("!**/bun.lock".into()); + ignores.push("!**/bun.lockb".into()); + ignores.push("!**/bunfig.toml".into()); + } } // if the workspace is a monorepo, keep workspace packages' parent directories to watch for new packages being added @@ -259,6 +266,7 @@ pub fn get_package_manager_type_and_version( "pnpm" => return Ok((PackageManagerType::Pnpm, version.into(), hash)), "yarn" => return Ok((PackageManagerType::Yarn, version.into(), hash)), "npm" => return Ok((PackageManagerType::Npm, version.into(), hash)), + "bun" => return Ok((PackageManagerType::Bun, version.into(), hash)), _ => return Err(Error::UnsupportedPackageManager(name.into())), } } @@ -291,6 +299,16 @@ pub fn get_package_manager_type_and_version( return Ok((PackageManagerType::Npm, version, None)); } + // if bun.lock (text format) or bun.lockb (binary format) exists, use bun@latest + let bun_lock_path = workspace_root.path.join("bun.lock"); + if is_exists_file(&bun_lock_path)? { + return Ok((PackageManagerType::Bun, version, None)); + } + let bun_lockb_path = workspace_root.path.join("bun.lockb"); + if is_exists_file(&bun_lockb_path)? { + return Ok((PackageManagerType::Bun, version, None)); + } + // if .pnpmfile.cjs exists, use pnpm@latest let pnpmfile_cjs_path = workspace_root.path.join(".pnpmfile.cjs"); if is_exists_file(&pnpmfile_cjs_path)? { @@ -303,6 +321,12 @@ pub fn get_package_manager_type_and_version( return Ok((PackageManagerType::Pnpm, version, None)); } + // if bunfig.toml exists, use bun@latest + let bunfig_toml_path = workspace_root.path.join("bunfig.toml"); + if is_exists_file(&bunfig_toml_path)? { + return Ok((PackageManagerType::Bun, version, None)); + } + // if yarn.config.cjs exists, use yarn@latest (yarn 2.0+) let yarn_config_cjs_path = workspace_root.path.join("yarn.config.cjs"); if is_exists_file(&yarn_config_cjs_path)? { @@ -372,9 +396,15 @@ pub async fn download_package_manager( } } - let tgz_url = get_npm_package_tgz_url(&package_name, &version); let home_dir = vite_shared::get_vite_plus_home()?; let bin_name = package_manager_type.to_string(); + + // For bun, use platform-specific download flow + if matches!(package_manager_type, PackageManagerType::Bun) { + return download_bun_package_manager(&version, &home_dir, expected_hash).await; + } + + let tgz_url = get_npm_package_tgz_url(&package_name, &version); // $VITE_PLUS_HOME/package_manager/pnpm/10.0.0 let target_dir = home_dir.join("package_manager").join(&bin_name).join(&version); let install_dir = target_dir.join(&bin_name); @@ -448,6 +478,133 @@ pub async fn download_package_manager( Ok((install_dir, package_name, version)) } +/// Get the platform-specific npm package name for bun. +/// Returns the `@oven/bun-{os}-{arch}` package name for the current platform. +fn get_bun_platform_package_name() -> Result<&'static str, Error> { + let name = match (env::consts::OS, env::consts::ARCH) { + ("macos", "aarch64") => "@oven/bun-darwin-aarch64", + ("macos", "x86_64") => "@oven/bun-darwin-x64", + ("linux", "aarch64") => "@oven/bun-linux-aarch64", + ("linux", "x86_64") => "@oven/bun-linux-x64", + ("windows", "x86_64") => "@oven/bun-windows-x64", + ("windows", "aarch64") => "@oven/bun-windows-aarch64", + (os, arch) => { + return Err(Error::UnsupportedPackageManager( + format!("bun (unsupported platform: {os}-{arch})").into(), + )); + } + }; + Ok(name) +} + +/// Download bun package manager (native binary) from npm. +/// +/// Unlike JS-based package managers (pnpm/npm/yarn), bun is a native binary +/// distributed via platform-specific npm packages (`@oven/bun-{os}-{arch}`). +/// +/// Layout: `$VITE_PLUS_HOME/package_manager/bun/{version}/bun/bin/bun.native` +async fn download_bun_package_manager( + version: &Str, + home_dir: &AbsolutePath, + expected_hash: Option<&str>, +) -> Result<(AbsolutePathBuf, Str, Str), Error> { + let package_name: Str = "bun".into(); + let platform_package_name = get_bun_platform_package_name()?; + + // $VITE_PLUS_HOME/package_manager/bun/{version} + let target_dir = home_dir.join("package_manager").join("bun").join(version.as_str()); + let install_dir = target_dir.join("bun"); + let bin_prefix = install_dir.join("bin"); + let bin_file = bin_prefix.join("bun"); + + // If shims already exist, return early + if is_exists_file(&bin_file)? + && is_exists_file(bin_file.with_extension("cmd"))? + && is_exists_file(bin_file.with_extension("ps1"))? + { + return Ok((install_dir, package_name, version.clone())); + } + + let parent_dir = target_dir.parent().unwrap(); + tokio::fs::create_dir_all(parent_dir).await?; + + // Download the platform-specific package directly + let platform_tgz_url = get_npm_package_tgz_url(platform_package_name, version); + let target_dir_tmp = tempfile::tempdir_in(parent_dir)?.path().to_path_buf(); + + download_and_extract_tgz_with_hash(&platform_tgz_url, &target_dir_tmp, expected_hash) + .await + .map_err(|err| { + if let Error::Reqwest(e) = &err + && let Some(status) = e.status() + && status == reqwest::StatusCode::NOT_FOUND + { + Error::PackageManagerVersionNotFound { + name: "bun".into(), + version: version.clone(), + url: platform_tgz_url.into(), + } + } else { + err + } + })?; + + // Create the expected directory structure: bun/bin/ + let tmp_bun_dir = target_dir_tmp.join("bun"); + let tmp_bin_dir = tmp_bun_dir.join("bin"); + tokio::fs::create_dir_all(&tmp_bin_dir).await?; + + // The platform package extracts to `package/` with the bun binary inside + // Find the native binary in the extracted package + let package_dir = target_dir_tmp.join("package"); + let native_bin_src = if cfg!(windows) { + package_dir.join("bun.exe") + } else { + package_dir.join("bun") + }; + + // Move native binary to bin/bun.native + let native_bin_dest = if cfg!(windows) { + tmp_bin_dir.join("bun.native.exe") + } else { + tmp_bin_dir.join("bun.native") + }; + tokio::fs::rename(&native_bin_src, &native_bin_dest).await?; + + // Set executable permission on the native binary + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + tokio::fs::set_permissions(&native_bin_dest, fs::Permissions::from_mode(0o755)).await?; + } + + // Clean up the extracted package directory + remove_dir_all_force(&package_dir).await?; + + // Acquire lock for atomic rename + let lock_path = parent_dir.join(format!("{version}.lock")); + tracing::debug!("Acquire lock file: {:?}", lock_path); + let lock_file = File::create(lock_path.as_path())?; + lock_file.lock()?; + tracing::debug!("Lock acquired: {:?}", lock_path); + + if is_exists_file(&bin_file)? { + tracing::debug!("bun bin_file already exists after lock acquisition, skip rename"); + return Ok((install_dir, package_name, version.clone())); + } + + // Rename temp dir to final location + tracing::debug!("Rename {:?} to {:?}", target_dir_tmp, target_dir); + remove_dir_all_force(&target_dir).await?; + tokio::fs::rename(&target_dir_tmp, &target_dir).await?; + + // Create native binary shims + tracing::debug!("Create shim files for bun"); + create_shim_files(PackageManagerType::Bun, &bin_prefix).await?; + + Ok((install_dir, package_name, version.clone())) +} + /// Remove the directory and all its contents. /// Ignore the error if the directory is not found. async fn remove_dir_all_force(path: impl AsRef) -> Result<(), std::io::Error> { @@ -494,6 +651,12 @@ async fn create_shim_files( bin_names.push(("npm", "npm-cli")); bin_names.push(("npx", "npx-cli")); } + PackageManagerType::Bun => { + // bun is a native binary, not a JS package. + // Create native binary shims instead of Node.js-based shims. + let bin_prefix = bin_prefix.as_ref(); + return create_bun_shim_files(bin_prefix).await; + } } let bin_prefix = bin_prefix.as_ref(); @@ -515,6 +678,37 @@ async fn create_shim_files( Ok(()) } +/// Create shim files for bun's native binary. +/// +/// Bun is a native binary distributed via platform-specific npm packages. +/// The native binary is placed at `bin_prefix/bun.native` (unix) or +/// `bin_prefix/bun.native.exe` (windows), and we create shim wrappers +/// that exec it directly (without Node.js). +async fn create_bun_shim_files(bin_prefix: &AbsolutePath) -> Result<(), Error> { + // The native binary should already be at bin_prefix/bun.native + // (placed there by download_bun_platform_binary) + let native_bin = bin_prefix.join("bun.native"); + if !is_exists_file(&native_bin)? { + // On Windows, check for bun.native.exe + let native_bin_exe = bin_prefix.join("bun.native.exe"); + if !is_exists_file(&native_bin_exe)? { + return Err(Error::CannotFindBinaryPath( + "bun native binary not found. Expected bin/bun.native".into(), + )); + } + } + + // Create bun shim -> bun.native + let bun_shim = bin_prefix.join("bun"); + shim::write_native_shims(&native_bin, &bun_shim).await?; + + // Create bunx shim -> bun.native (bunx is just bun with different argv[0]) + let bunx_shim = bin_prefix.join("bunx"); + shim::write_native_shims(&native_bin, &bunx_shim).await?; + + Ok(()) +} + async fn set_package_manager_field( package_json_path: impl AsRef, package_manager_type: PackageManagerType, @@ -570,6 +764,7 @@ fn interactive_package_manager_menu() -> Result { ("pnpm (recommended)", PackageManagerType::Pnpm), ("npm", PackageManagerType::Npm), ("yarn", PackageManagerType::Yarn), + ("bun", PackageManagerType::Bun), ]; let mut selected_index = 0; @@ -664,6 +859,9 @@ fn interactive_package_manager_menu() -> Result { KeyCode::Char('3') if options.len() > 2 => { break Ok(options[2].1); } + KeyCode::Char('4') if options.len() > 3 => { + break Ok(options[3].1); + } KeyCode::Esc | KeyCode::Char('q') => { // Exit on escape/quit terminal::disable_raw_mode()?; @@ -693,6 +891,7 @@ fn interactive_package_manager_menu() -> Result { PackageManagerType::Pnpm => "pnpm", PackageManagerType::Npm => "npm", PackageManagerType::Yarn => "yarn", + PackageManagerType::Bun => "bun", }; println!("\n✓ Selected package manager: {name}\n"); } @@ -733,6 +932,7 @@ fn simple_text_prompt() -> Result { ("pnpm", PackageManagerType::Pnpm), ("npm", PackageManagerType::Npm), ("yarn", PackageManagerType::Yarn), + ("bun", PackageManagerType::Bun), ]; println!("\nNo package manager detected. Please select one:"); @@ -2107,5 +2307,164 @@ mod tests { assert!(matcher.is_match("README.md"), "Should ignore docs"); assert!(matcher.is_match("src/app.ts"), "Should ignore source files"); } + + #[test] + fn test_bun_fingerprint_ignores() { + let temp_dir: TempDir = create_temp_dir(); + let pm = create_mock_package_manager(temp_dir, PackageManagerType::Bun, false); + let ignores = pm.get_fingerprint_ignores().expect("Should get fingerprint ignores"); + let matcher = GlobPatternSet::new(&ignores).expect("Should compile patterns"); + + // Should NOT ignore bun-specific files + assert!(!matcher.is_match("bun.lock"), "Should NOT ignore bun.lock"); + assert!(!matcher.is_match("bun.lockb"), "Should NOT ignore bun.lockb"); + assert!(!matcher.is_match("bunfig.toml"), "Should NOT ignore bunfig.toml"); + assert!(!matcher.is_match(".npmrc"), "Should NOT ignore .npmrc"); + assert!(!matcher.is_match("package.json"), "Should NOT ignore package.json"); + + // Should ignore other package manager files + assert!(matcher.is_match("pnpm-lock.yaml"), "Should ignore pnpm-lock.yaml"); + assert!(matcher.is_match("yarn.lock"), "Should ignore yarn.lock"); + assert!(matcher.is_match("package-lock.json"), "Should ignore package-lock.json"); + + // Regular files should be ignored + assert!(matcher.is_match("README.md"), "Should ignore docs"); + assert!(matcher.is_match("src/app.ts"), "Should ignore source files"); + } + } + + // Tests for bun package manager detection + #[tokio::test] + async fn test_detect_package_manager_with_bun_lock() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_content = r#"{"name": "test-package"}"#; + create_package_json(&temp_dir_path, package_content); + + // Create bun.lock (text format) + fs::write(temp_dir_path.join("bun.lock"), r#"# bun lockfile"#) + .expect("Failed to write bun.lock"); + + let (workspace_root, _) = + find_workspace_root(&temp_dir_path).expect("Should find workspace root"); + let (pm_type, version, hash) = + get_package_manager_type_and_version(&workspace_root, None) + .expect("Should detect bun"); + assert_eq!(pm_type, PackageManagerType::Bun); + assert_eq!(version.as_str(), "latest"); + assert!(hash.is_none()); + } + + #[tokio::test] + async fn test_detect_package_manager_with_bun_lockb() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_content = r#"{"name": "test-package"}"#; + create_package_json(&temp_dir_path, package_content); + + // Create bun.lockb (binary format) + fs::write(temp_dir_path.join("bun.lockb"), b"\x00\x01\x02") + .expect("Failed to write bun.lockb"); + + let (workspace_root, _) = + find_workspace_root(&temp_dir_path).expect("Should find workspace root"); + let (pm_type, version, hash) = + get_package_manager_type_and_version(&workspace_root, None) + .expect("Should detect bun"); + assert_eq!(pm_type, PackageManagerType::Bun); + assert_eq!(version.as_str(), "latest"); + assert!(hash.is_none()); + } + + #[tokio::test] + async fn test_detect_package_manager_with_bunfig_toml() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_content = r#"{"name": "test-package"}"#; + create_package_json(&temp_dir_path, package_content); + + // Create bunfig.toml + fs::write(temp_dir_path.join("bunfig.toml"), "[install]\noptional = true") + .expect("Failed to write bunfig.toml"); + + let (workspace_root, _) = + find_workspace_root(&temp_dir_path).expect("Should find workspace root"); + let (pm_type, version, hash) = + get_package_manager_type_and_version(&workspace_root, None) + .expect("Should detect bun"); + assert_eq!(pm_type, PackageManagerType::Bun); + assert_eq!(version.as_str(), "latest"); + assert!(hash.is_none()); + } + + #[tokio::test] + async fn test_detect_package_manager_with_package_manager_field_bun() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_content = r#"{"name": "test-package", "packageManager": "bun@1.2.0"}"#; + create_package_json(&temp_dir_path, package_content); + + let (workspace_root, _) = + find_workspace_root(&temp_dir_path).expect("Should find workspace root"); + let (pm_type, version, hash) = + get_package_manager_type_and_version(&workspace_root, None) + .expect("Should detect bun from packageManager field"); + assert_eq!(pm_type, PackageManagerType::Bun); + assert_eq!(version.as_str(), "1.2.0"); + assert!(hash.is_none()); + } + + #[tokio::test] + async fn test_detect_package_manager_with_package_manager_field_bun_with_hash() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_content = + r#"{"name": "test-package", "packageManager": "bun@1.2.0+sha512.abc123"}"#; + create_package_json(&temp_dir_path, package_content); + + let (workspace_root, _) = + find_workspace_root(&temp_dir_path).expect("Should find workspace root"); + let (pm_type, version, hash) = + get_package_manager_type_and_version(&workspace_root, None) + .expect("Should detect bun with hash"); + assert_eq!(pm_type, PackageManagerType::Bun); + assert_eq!(version.as_str(), "1.2.0"); + assert_eq!(hash.unwrap().as_str(), "sha512.abc123"); + } + + #[tokio::test] + async fn test_detect_package_manager_bun_lock_priority_over_pnpmfile() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_content = r#"{"name": "test-package"}"#; + create_package_json(&temp_dir_path, package_content); + + // Create both bun.lock and .pnpmfile.cjs + fs::write(temp_dir_path.join("bun.lock"), "# bun lockfile") + .expect("Failed to write bun.lock"); + fs::write(temp_dir_path.join(".pnpmfile.cjs"), "module.exports = {}") + .expect("Failed to write .pnpmfile.cjs"); + + let (workspace_root, _) = + find_workspace_root(&temp_dir_path).expect("Should find workspace root"); + let (pm_type, _, _) = get_package_manager_type_and_version(&workspace_root, None) + .expect("Should detect bun"); + assert_eq!( + pm_type, + PackageManagerType::Bun, + "bun.lock should be detected before .pnpmfile.cjs" + ); + } + + #[test] + fn test_get_bun_platform_package_name() { + // Just verify it returns a valid package name for the current platform + let result = get_bun_platform_package_name(); + assert!(result.is_ok(), "Should return a platform package name"); + let name = result.unwrap(); + assert!( + name.starts_with("@oven/bun-"), + "Package name should start with @oven/bun-, got: {name}" + ); } } diff --git a/crates/vite_install/src/shim.rs b/crates/vite_install/src/shim.rs index a19071b57f..23cad046fb 100644 --- a/crates/vite_install/src/shim.rs +++ b/crates/vite_install/src/shim.rs @@ -5,6 +5,84 @@ use pathdiff::diff_paths; use tokio::fs::write; use vite_error::Error; +/// Write cmd/sh/pwsh shim files for native (non-Node.js) binaries. +/// Unlike `write_shims` which wraps JS files with Node.js, this creates +/// wrappers that exec the native binary directly. +pub async fn write_native_shims( + source_file: impl AsRef, + to_bin: impl AsRef, +) -> Result<(), Error> { + let to_bin = to_bin.as_ref(); + let relative_path = diff_paths(source_file, to_bin.parent().unwrap()).unwrap(); + let relative_file = relative_path.to_str().unwrap(); + + write(to_bin, native_sh_shim(relative_file)).await?; + write(to_bin.with_extension("cmd"), native_cmd_shim(relative_file)).await?; + write(to_bin.with_extension("ps1"), native_pwsh_shim(relative_file)).await?; + + // set executable permission for unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + tokio::fs::set_permissions(to_bin, std::fs::Permissions::from_mode(0o755)).await?; + } + + tracing::debug!("write_native_shims: {:?} -> {:?}", to_bin, relative_file); + Ok(()) +} + +/// Unix shell shim for native binaries. +pub fn native_sh_shim(relative_file: &str) -> String { + formatdoc! { + r#" + #!/bin/sh + basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + + case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; + esac + + exec "$basedir/{relative_file}" "$@" + "# + } +} + +/// Windows Command Prompt shim for native binaries. +pub fn native_cmd_shim(relative_file: &str) -> String { + formatdoc! { + r#" + @SETLOCAL + "%~dp0\{relative_file}" %* + "#, + relative_file = relative_file.replace('/', "\\") + } + .replace('\n', "\r\n") +} + +/// `PowerShell` shim for native binaries. +pub fn native_pwsh_shim(relative_file: &str) -> String { + formatdoc! { + r#" + #!/usr/bin/env pwsh + $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + + $ret=0 + # Support pipeline input + if ($MyInvocation.ExpectingInput) {{ + $input | & "$basedir/{relative_file}" $args + }} else {{ + & "$basedir/{relative_file}" $args + }} + $ret=$LASTEXITCODE + exit $ret + "# + } +} + /// Write cmd/sh/pwsh shim files. pub async fn write_shims( source_file: impl AsRef, diff --git a/packages/cli/binding/src/package_manager.rs b/packages/cli/binding/src/package_manager.rs index 641f7016d8..41d9444805 100644 --- a/packages/cli/binding/src/package_manager.rs +++ b/packages/cli/binding/src/package_manager.rs @@ -62,6 +62,7 @@ pub async fn download_package_manager( "pnpm" => PackageManagerType::Pnpm, "yarn" => PackageManagerType::Yarn, "npm" => PackageManagerType::Npm, + "bun" => PackageManagerType::Bun, _ => { return Err(Error::from_reason(format!( "Invalid package manager name: {}", diff --git a/packages/cli/src/create/command.ts b/packages/cli/src/create/command.ts index 9cecc11ce7..5b42823a63 100644 --- a/packages/cli/src/create/command.ts +++ b/packages/cli/src/create/command.ts @@ -153,6 +153,8 @@ export function getPackageRunner(workspaceInfo: WorkspaceInfo) { command: 'yarn', args: ['dlx'], }; + case 'bun': + return { command: 'bunx', args: [] }; case 'npm': default: return { command: 'npx', args: [] }; @@ -166,7 +168,7 @@ export function formatDlxCommand( workspaceInfo: WorkspaceInfo, ) { const runner = getPackageRunner(workspaceInfo); - const dlxArgs = runner.command === 'npm' ? ['--', ...args] : args; + const dlxArgs = runner.command === 'npx' ? ['--', ...args] : args; return { command: runner.command, args: [...runner.args, packageName, ...dlxArgs], diff --git a/packages/cli/src/create/templates/monorepo.ts b/packages/cli/src/create/templates/monorepo.ts index 22392f02be..8706416946 100644 --- a/packages/cli/src/create/templates/monorepo.ts +++ b/packages/cli/src/create/templates/monorepo.ts @@ -89,7 +89,7 @@ export async function executeMonorepoTemplate( fs.unlinkSync(pnpmWorkspacePath); } } else { - // npm + // npm or bun: both use package.json workspaces field // remove pnpm field editJsonFile(path.join(fullPath, 'package.json'), (pkg) => { pkg.pnpm = undefined; diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 8606445634..2a24759e78 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -716,7 +716,10 @@ async function executeMigrationPlan( // 11. Reinstall after migration // npm needs --force to re-resolve packages with newly added overrides, // otherwise the stale lockfile prevents override resolution. - const installArgs = plan.packageManager === PackageManager.npm ? ['--force'] : undefined; + const installArgs = + plan.packageManager === PackageManager.npm || plan.packageManager === PackageManager.bun + ? ['--force'] + : undefined; updateMigrationProgress('Installing dependencies'); const finalInstallSummary = await runViteInstall( workspaceInfo.rootDir, diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index d6a5f3f60f..3b58253a1e 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -679,7 +679,7 @@ export function rewriteStandaloneProject( ...pkg.resolutions, ...VITE_PLUS_OVERRIDE_PACKAGES, }; - } else if (packageManager === PackageManager.npm) { + } else if (packageManager === PackageManager.npm || packageManager === PackageManager.bun) { pkg.overrides = { ...pkg.overrides, ...VITE_PLUS_OVERRIDE_PACKAGES, @@ -976,7 +976,7 @@ function rewriteRootWorkspacePackageJson( // https://github.com/yarnpkg/berry/issues/6979 ...VITE_PLUS_OVERRIDE_PACKAGES, }; - } else if (packageManager === PackageManager.npm) { + } else if (packageManager === PackageManager.npm || packageManager === PackageManager.bun) { pkg.overrides = { ...pkg.overrides, ...VITE_PLUS_OVERRIDE_PACKAGES, @@ -1078,7 +1078,8 @@ export function rewritePackageJson( const updated = rewriteScripts(JSON.stringify(config), readRulesYaml()); extractedStagedConfig = updated ? JSON.parse(updated) : config; } - const supportCatalog = isMonorepo && packageManager !== PackageManager.npm; + const supportCatalog = + isMonorepo && packageManager !== PackageManager.npm && packageManager !== PackageManager.bun; let needVitePlus = false; for (const [key, version] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { const value = supportCatalog && !version.startsWith('file:') ? 'catalog:' : version; diff --git a/packages/cli/src/types/package.ts b/packages/cli/src/types/package.ts index 8e55e3f48e..98594fe36b 100644 --- a/packages/cli/src/types/package.ts +++ b/packages/cli/src/types/package.ts @@ -2,6 +2,7 @@ export const PackageManager = { pnpm: 'pnpm', npm: 'npm', yarn: 'yarn', + bun: 'bun', } as const; export type PackageManager = (typeof PackageManager)[keyof typeof PackageManager]; diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts index b5a37ee1e4..75f8aa3abc 100644 --- a/packages/cli/src/utils/prompts.ts +++ b/packages/cli/src/utils/prompts.ts @@ -24,6 +24,7 @@ export async function selectPackageManager(interactive?: boolean, silent = false { value: PackageManager.pnpm, hint: 'recommended' }, { value: PackageManager.yarn }, { value: PackageManager.npm }, + { value: PackageManager.bun }, ], initialValue: PackageManager.pnpm, }); diff --git a/rfcs/add-remove-package-commands.md b/rfcs/add-remove-package-commands.md index 592aea39fa..9b2ee4d13d 100644 --- a/rfcs/add-remove-package-commands.md +++ b/rfcs/add-remove-package-commands.md @@ -2,7 +2,7 @@ ## Summary -Add `vp add` and `vp remove` commands that automatically adapt to the detected package manager (pnpm/yarn/npm) for adding and removing packages, with support for multiple packages, common flags, and workspace-aware operations based on pnpm's API design. +Add `vp add` and `vp remove` commands that automatically adapt to the detected package manager (pnpm/yarn/npm/bun) for adding and removing packages, with support for multiple packages, common flags, and workspace-aware operations based on pnpm's API design. ## Motivation @@ -12,6 +12,7 @@ Currently, developers must manually use package manager-specific commands: pnpm add react yarn add react npm install react +bun add react ``` This creates friction in monorepo workflows and requires remembering different syntaxes. A unified interface would: @@ -28,11 +29,13 @@ This creates friction in monorepo workflows and requires remembering different s pnpm add -D typescript # pnpm project yarn add --dev typescript # yarn project npm install --save-dev typescript # npm project +bun add --dev typescript # bun project # Different remove commands pnpm remove lodash yarn remove lodash npm uninstall lodash +bun remove lodash ``` ### Proposed Solution @@ -102,7 +105,7 @@ vp install ... [OPTIONS] ##### Install global packages with npm cli only -For global packages, we will use npm cli only. +For global packages, we will use npm cli only (except for bun, which natively supports `bun add -g`). > Because yarn do not support global packages install on [version>=2.x](https://yarnpkg.com/migration/guide#use-yarn-dlx-instead-of-yarn-global), and pnpm global install has some bugs like `wrong bin file` issues. @@ -145,22 +148,23 @@ vp remove -g typescript # Remove global package - https://pnpm.io/cli/add#options - https://yarnpkg.com/cli/add#options - https://docs.npmjs.com/cli/v11/commands/npm-install#description - -| Vite+ Flag | pnpm | yarn | npm | Description | -| ------------------------------------ | ------------------------ | ----------------------------------------------- | ------------------------------- | ------------------------------------------------------- | -| `` | `add ` | `add ` | `install ` | Add packages | -| `--filter ` | `--filter add` | `workspaces foreach -A --include add` | `install --workspace ` | Target specific workspace package(s) | -| `-w, --workspace-root` | `-w` | `-W` for v1, v2+ N/A | `--include-workspace-root` | Add to workspace root (ignore-workspace-root-check) | -| `--workspace` | `--workspace` | N/A | N/A | Only add if package exists in workspace (pnpm-specific) | -| `-P, --save-prod` | `--save-prod` / `-P` | N/A | `--save-prod` / `-P` | Save to `dependencies`. The default behavior | -| `-D, --save-dev` | `-D` | `--dev` / `-D` | `--save-dev` / `-D` | Save to `devDependencies` | -| `--save-peer` | `--save-peer` | `--peer` / `-P` | `--save-peer` | Save to `peerDependencies` and `devDependencies` | -| `-O, --save-optional` | `-O` | `--optional` / `-O` | `--save-optional` / `-O` | Save to `optionalDependencies` | -| `-E, --save-exact` | `-E` | `--exact` / `-E` | `--save-exact` / `-E` | Save exact version | -| `-g, --global` | `-g` | `global add` | `--global` / `-g` | Install globally | -| `--save-catalog` | pnpm@10+ only | N/A | N/A | Save the new dependency to the default catalog | -| `--save-catalog-name ` | pnpm@10+ only | N/A | N/A | Save the new dependency to the specified catalog | -| `--allow-build ` | pnpm@10+ only | N/A | N/A | A list of package names allowed to run postinstall | +- https://bun.sh/docs/cli/add + +| Vite+ Flag | pnpm | yarn | npm | bun | Description | +| ------------------------------------ | ------------------------ | ----------------------------------------------- | ------------------------------- | ----------------- | ------------------------------------------------------- | +| `` | `add ` | `add ` | `install ` | `add ` | Add packages | +| `--filter ` | `--filter add` | `workspaces foreach -A --include add` | `install --workspace ` | N/A | Target specific workspace package(s) | +| `-w, --workspace-root` | `-w` | `-W` for v1, v2+ N/A | `--include-workspace-root` | N/A | Add to workspace root (ignore-workspace-root-check) | +| `--workspace` | `--workspace` | N/A | N/A | N/A | Only add if package exists in workspace (pnpm-specific) | +| `-P, --save-prod` | `--save-prod` / `-P` | N/A | `--save-prod` / `-P` | N/A | Save to `dependencies`. The default behavior | +| `-D, --save-dev` | `-D` | `--dev` / `-D` | `--save-dev` / `-D` | `--dev` / `-d` | Save to `devDependencies` | +| `--save-peer` | `--save-peer` | `--peer` / `-P` | `--save-peer` | `--peer` | Save to `peerDependencies` and `devDependencies` | +| `-O, --save-optional` | `-O` | `--optional` / `-O` | `--save-optional` / `-O` | `--optional` | Save to `optionalDependencies` | +| `-E, --save-exact` | `-E` | `--exact` / `-E` | `--save-exact` / `-E` | `--exact` / `-E` | Save exact version | +| `-g, --global` | `-g` | `global add` | `--global` / `-g` | `--global` / `-g` | Install globally | +| `--save-catalog` | pnpm@10+ only | N/A | N/A | N/A | Save the new dependency to the default catalog | +| `--save-catalog-name ` | pnpm@10+ only | N/A | N/A | N/A | Save the new dependency to the specified catalog | +| `--allow-build ` | pnpm@10+ only | N/A | N/A | N/A | A list of package names allowed to run postinstall | **Note**: For pnpm, `--filter` must come before the command (e.g., `pnpm --filter app add react`). For yarn/npm, it's integrated into the command structure. @@ -169,17 +173,18 @@ vp remove -g typescript # Remove global package - https://pnpm.io/cli/remove#options - https://yarnpkg.com/cli/remove#options - https://docs.npmjs.com/cli/v11/commands/npm-uninstall#description - -| Vite+ Flag | pnpm | yarn | npm | Description | -| ---------------------- | --------------------------- | -------------------------------------------------- | --------------------------------- | ---------------------------------------------- | -| `` | `remove ` | `remove ` | `uninstall ` | Remove packages | -| `-D, --save-dev` | `-D` | N/A | `--save-dev` / `-D` | Only remove from `devDependencies` | -| `-O, --save-optional` | `-O` | N/A | `--save-optional` / `-O` | Only remove from `optionalDependencies` | -| `-P, --save-prod` | `-P` | N/A | `--save-prod` / `-P` | Only remove from `dependencies` | -| `--filter ` | `--filter remove` | `workspaces foreach -A --include remove` | `uninstall --workspace ` | Target specific workspace package(s) | -| `-w, --workspace-root` | `-w` | N/A | `--include-workspace-root` | Remove from workspace root | -| `-r, --recursive` | `-r, --recursive` | `-A, --all` | `--workspaces` | Remove recursively from all workspace packages | -| `-g, --global` | `-g` | N/A | `--global` / `-g` | Remove global packages | +- https://bun.sh/docs/cli/remove + +| Vite+ Flag | pnpm | yarn | npm | bun | Description | +| ---------------------- | --------------------------- | -------------------------------------------------- | --------------------------------- | ------------------- | ---------------------------------------------- | +| `` | `remove ` | `remove ` | `uninstall ` | `remove ` | Remove packages | +| `-D, --save-dev` | `-D` | N/A | `--save-dev` / `-D` | N/A | Only remove from `devDependencies` | +| `-O, --save-optional` | `-O` | N/A | `--save-optional` / `-O` | N/A | Only remove from `optionalDependencies` | +| `-P, --save-prod` | `-P` | N/A | `--save-prod` / `-P` | N/A | Only remove from `dependencies` | +| `--filter ` | `--filter remove` | `workspaces foreach -A --include remove` | `uninstall --workspace ` | N/A | Target specific workspace package(s) | +| `-w, --workspace-root` | `-w` | N/A | `--include-workspace-root` | N/A | Remove from workspace root | +| `-r, --recursive` | `-r, --recursive` | `-A, --all` | `--workspaces` | N/A | Remove recursively from all workspace packages | +| `-g, --global` | `-g` | N/A | `--global` / `-g` | `--global` / `-g` | Remove global packages | **Note**: Similar to add, `--filter` must precede the command for pnpm. @@ -221,6 +226,7 @@ vp add react --allow-build=react,napi -- --use-stderr -> pnpm add --allow-build=react,napi --use-stderr react -> yarn add --use-stderr react -> npm install --use-stderr react +-> bun add --use-stderr react ``` ### Implementation Architecture @@ -294,6 +300,7 @@ impl PackageManager { PackageManagerType::Pnpm => "add", PackageManagerType::Yarn => "add", PackageManagerType::Npm => "install", + PackageManagerType::Bun => "add", } } @@ -303,6 +310,7 @@ impl PackageManager { PackageManagerType::Pnpm => "remove", PackageManagerType::Yarn => "remove", PackageManagerType::Npm => "uninstall", + PackageManagerType::Bun => "remove", } } @@ -365,6 +373,12 @@ impl PackageManager { } args.extend_from_slice(extra_args); } + PackageManagerType::Bun => { + // bun: simple add command, no workspace filter support + args.push("add".to_string()); + args.extend_from_slice(packages); + args.extend_from_slice(extra_args); + } } args @@ -415,6 +429,12 @@ impl PackageManager { args.extend_from_slice(packages); args.extend_from_slice(extra_args); } + PackageManagerType::Bun => { + // bun: simple remove command, no workspace filter support + args.push("remove".to_string()); + args.extend_from_slice(packages); + args.extend_from_slice(extra_args); + } } args @@ -541,7 +561,7 @@ impl RemoveCommand { Yarn requires different command structure for global operations: ```rust -// pnpm/npm: add -g +// pnpm/npm/bun: add -g // yarn: global add fn handle_global_flag(args: &[String], pm_type: PackageManagerType) -> (Vec, bool) { @@ -599,6 +619,12 @@ fn build_workspace_command( } args } + PackageManagerType::Bun => { + // bun: no workspace filter support + let mut args = vec![operation.to_string()]; + args.extend_from_slice(packages); + args + } } } ``` @@ -652,6 +678,7 @@ vp add react --save-exact # → pnpm add react --save-exact # → yarn add react --save-exact # → npm install react --save-exact +# → bun add react --exact ``` ### 3. Common Flags Only @@ -661,7 +688,7 @@ vp add react --save-exact **Common Flags**: - `-D, --save-dev` - universally supported -- `-g, --global` - needs special handling for yarn +- `-g, --global` - needs special handling for yarn; bun uses `--global` / `-g` - `-E, --save-exact` - universally supported - `-P, --save-peer` - universally supported - `-O, --save-optional` - universally supported @@ -765,6 +792,7 @@ vp add react --dev # → pnpm add react -D # → yarn add react --dev # → npm install react --save-dev +# → bun add react --dev ``` **Rejected because**: @@ -780,6 +808,7 @@ vp add react --dev vp pnpm:add react vp yarn:add react vp npm:install react +vp bun:add react ``` **Rejected because**: @@ -841,6 +870,7 @@ $ vp add - yarn@4.x - npm@10.x - npm@11.x [WIP] +- bun@1.x ### Unit Tests @@ -852,6 +882,9 @@ fn test_add_command_resolution() { let pm = PackageManager::mock(PackageManagerType::Npm); assert_eq!(pm.resolve_add_command(), "install"); + + let pm = PackageManager::mock(PackageManagerType::Bun); + assert_eq!(pm.resolve_add_command(), "add"); } #[test] @@ -861,6 +894,9 @@ fn test_remove_command_resolution() { let pm = PackageManager::mock(PackageManagerType::Npm); assert_eq!(pm.resolve_remove_command(), "uninstall"); + + let pm = PackageManager::mock(PackageManagerType::Bun); + assert_eq!(pm.resolve_remove_command(), "remove"); } #[test] @@ -1050,8 +1086,11 @@ This is a new feature with no breaking changes: Users can start using immediately: ```bash -# Old way +# Old way (package manager specific) pnpm add react +yarn add react +npm install react +bun add react # New way (works with any package manager) vp add react @@ -1080,7 +1119,7 @@ vp add ... [OPTIONS] ``` ```` -Automatically uses the detected package manager (pnpm/yarn/npm). +Automatically uses the detected package manager (pnpm/yarn/npm/bun). **Basic Examples:** @@ -1137,13 +1176,13 @@ Aliases: `rm`, `un`, `uninstall` Document flag support matrix: -| Flag | pnpm | yarn | npm | -|------|------|------|-----| -| `-D` | ✅ | ✅ | ✅ | -| `-E` | ✅ | ✅ | ✅ | -| `-P` | ✅ | ✅ | ✅ | -| `-O` | ✅ | ✅ | ✅ | -| `-g` | ✅ | ⚠️ (use global) | ✅ | +| Flag | pnpm | yarn | npm | bun | +|------|------|------|-----|-----| +| `-D` | ✅ | ✅ | ✅ | ✅ | +| `-E` | ✅ | ✅ | ✅ | ✅ | +| `-P` | ✅ | ✅ | ✅ | ✅ | +| `-O` | ✅ | ✅ | ✅ | ✅ | +| `-g` | ✅ | ⚠️ (use global) | ✅ | ✅ | ## Workspace Operations Deep Dive @@ -1210,6 +1249,7 @@ vp add -D typescript -w # → pnpm add -D typescript -w (pnpm) # → yarn add -D typescript -W (yarn) # → npm install -D typescript -w (npm) +# → bun add --dev typescript (bun, no workspace root flag) ``` **Why needed**: By default, package managers prevent adding to workspace root to encourage proper package structure. @@ -1231,31 +1271,32 @@ vp add "@myorg/utils@workspace:^" --filter app ### Package Manager Compatibility -| Feature | pnpm | yarn | npm | Notes | -| -------------------------- | ------------------ | --------------------- | ----------------------- | ------------------------ | -| `--filter ` | ✅ Native | ⚠️ `workspace ` | ⚠️ `--workspace ` | Syntax differs | -| Multiple filters | ✅ Repeatable flag | ❌ Single only | ⚠️ Limited | pnpm most flexible | -| Wildcard patterns | ✅ Full support | ⚠️ Limited | ❌ No wildcards | pnpm best | -| Exclusion `!` | ✅ Supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Dependency selectors `...` | ✅ Supported | ❌ Not supported | ❌ Not supported | pnpm only | -| `-w` (root) | ✅ `-w` | ✅ `-W` | ✅ `-w` | Slightly different flags | -| `--workspace` protocol | ✅ Supported | ❌ Manual | ❌ Manual | pnpm feature | +| Feature | pnpm | yarn | npm | bun | Notes | +| -------------------------- | ------------------ | --------------------- | ----------------------- | ---------------- | ------------------------ | +| `--filter ` | ✅ Native | ⚠️ `workspace ` | ⚠️ `--workspace ` | ❌ Not supported | Syntax differs | +| Multiple filters | ✅ Repeatable flag | ❌ Single only | ⚠️ Limited | ❌ Not supported | pnpm most flexible | +| Wildcard patterns | ✅ Full support | ⚠️ Limited | ❌ No wildcards | ❌ Not supported | pnpm best | +| Exclusion `!` | ✅ Supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Dependency selectors `...` | ✅ Supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| `-w` (root) | ✅ `-w` | ✅ `-W` | ✅ `-w` | ❌ Not supported | Slightly different flags | +| `--workspace` protocol | ✅ Supported | ❌ Manual | ❌ Manual | ❌ Not supported | pnpm feature | **Graceful Degradation**: -- Advanced pnpm features (wildcard, exclusion, selectors) will error on yarn/npm with helpful message +- Advanced pnpm features (wildcard, exclusion, selectors) will error on yarn/npm/bun with helpful message - Basic `--filter ` works across all package managers ## Future Enhancements -### 1. Enhanced Filter Support for yarn/npm +### 1. Enhanced Filter Support for yarn/npm/bun -Implement wildcard translation for yarn/npm: +Implement wildcard translation for yarn/npm/bun: ```bash vp add react --filter "app*" # → For yarn: Run `yarn workspace app add react` for each matching package # → For npm: Run `npm install react --workspace app` for each matching package +# → For bun: Run `bun add react` in each matching package directory ``` ### 2. Interactive Mode diff --git a/rfcs/dedupe-package-command.md b/rfcs/dedupe-package-command.md index f9d6831ac7..a466a59663 100644 --- a/rfcs/dedupe-package-command.md +++ b/rfcs/dedupe-package-command.md @@ -2,7 +2,7 @@ ## Summary -Add `vp dedupe` command that automatically adapts to the detected package manager (pnpm/npm/yarn) for optimizing dependency trees by removing duplicate packages and upgrading older dependencies to newer compatible versions in the lockfile. This helps reduce redundancy and improve project efficiency. +Add `vp dedupe` command that automatically adapts to the detected package manager (pnpm/npm/yarn/bun) for optimizing dependency trees by removing duplicate packages and upgrading older dependencies to newer compatible versions in the lockfile. This helps reduce redundancy and improve project efficiency. ## Motivation @@ -84,15 +84,16 @@ vp dedupe --check - https://yarnpkg.com/cli/dedupe (yarn@2+) - Note: yarn@2+ has a dedicated `yarn dedupe` command with `--check` mode support -| Vite+ Flag | pnpm | npm | yarn@2+ | Description | -| ----------- | ------------- | ------------ | ------------- | ---------------------------- | -| `vp dedupe` | `pnpm dedupe` | `npm dedupe` | `yarn dedupe` | Deduplicate dependencies | -| `--check` | `--check` | `--dry-run` | `--check` | Check if changes would occur | +| Vite+ Flag | pnpm | npm | yarn@2+ | bun | Description | +| ----------- | ------------- | ------------ | ------------- | --- | ---------------------------- | +| `vp dedupe` | `pnpm dedupe` | `npm dedupe` | `yarn dedupe` | N/A | Deduplicate dependencies | +| `--check` | `--check` | `--dry-run` | `--check` | N/A | Check if changes would occur | **Note**: - pnpm uses `--check` for dry-run, npm uses `--dry-run`, yarn@2+ uses `--check` - yarn@1 does not have dedupe command and is not supported +- bun does not currently support a dedupe command ### Dedupe Behavior Differences Across Package Managers @@ -561,6 +562,7 @@ vp dedupe:run - yarn@4.x (yarn@2+) - npm@10.x - npm@11.x (WIP) +- bun@1.x (N/A - bun does not support dedupe) ### Unit Tests @@ -764,13 +766,13 @@ vp test ## Package Manager Compatibility -| Feature | pnpm | npm | yarn@2+ | Notes | -| ------------- | ------------ | -------------- | ------------ | ----------------------------------------- | -| Basic dedupe | ✅ `dedupe` | ✅ `dedupe` | ✅ `dedupe` | All use native dedupe command | -| Check/Dry-run | ✅ `--check` | ✅ `--dry-run` | ✅ `--check` | npm uses different flag name | -| Exit codes | ✅ Supported | ✅ Supported | ✅ Supported | All return non-zero on check with changes | +| Feature | pnpm | npm | yarn@2+ | bun | Notes | +| ------------- | ------------ | -------------- | ------------ | ---------------- | ----------------------------------------- | +| Basic dedupe | ✅ `dedupe` | ✅ `dedupe` | ✅ `dedupe` | ❌ Not supported | bun has no dedupe command | +| Check/Dry-run | ✅ `--check` | ✅ `--dry-run` | ✅ `--check` | ❌ Not supported | npm uses different flag name | +| Exit codes | ✅ Supported | ✅ Supported | ✅ Supported | ❌ Not supported | All return non-zero on check with changes | -**Note**: yarn@1 does not have a dedupe command and is not supported +**Note**: yarn@1 does not have a dedupe command and is not supported. bun does not currently support a dedupe command. ## Future Enhancements @@ -870,7 +872,7 @@ Recommendation: All can use lodash@4.17.21 ## Conclusion -This RFC proposes adding `vp dedupe` command to provide a unified interface for dependency deduplication across pnpm/npm/yarn@2+. The design: +This RFC proposes adding `vp dedupe` command to provide a unified interface for dependency deduplication across pnpm/npm/yarn@2+/bun. The design: - ✅ Automatically adapts to detected package manager - ✅ Supports check mode for validation (maps to --check for pnpm/yarn@2+, --dry-run for npm) diff --git a/rfcs/dlx-command.md b/rfcs/dlx-command.md index 418acf63dd..f41892f9b9 100644 --- a/rfcs/dlx-command.md +++ b/rfcs/dlx-command.md @@ -2,7 +2,7 @@ ## Summary -Add `vp dlx` command that fetches a package from the registry without installing it as a dependency, hotloads it, and runs whatever default command binary it exposes. This provides a unified interface across pnpm, npm, and yarn for executing remote packages temporarily. +Add `vp dlx` command that fetches a package from the registry without installing it as a dependency, hotloads it, and runs whatever default command binary it exposes. This provides a unified interface across pnpm, npm, yarn, and bun for executing remote packages temporarily. ## Motivation @@ -104,13 +104,14 @@ vp dlx -p typescript -p @types/node -c 'tsc --init && node -e "console.log(123)" - pnpm: https://pnpm.io/cli/dlx - npm: https://docs.npmjs.com/cli/v10/commands/npm-exec - yarn: https://yarnpkg.com/cli/dlx +- bun: https://bun.sh/docs/cli/bunx -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------------------- | ------------------ | ------------------- | ----------- | ---------------- | -------------------------- | -| `vp dlx ` | `pnpm dlx ` | `npm exec ` | `npx ` | `yarn dlx ` | Execute package binary | -| `--package `, `-p ` | `--package ` | `--package=` | N/A | `-p ` | Specify package to install | -| `--shell-mode`, `-c` | `-c` | `-c` | N/A | N/A | Execute in shell | -| `--silent`, `-s` | `--silent` | `--loglevel silent` | `--quiet` | `--quiet` | Suppress output | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------------------- | ------------------ | ------------------- | ----------- | ---------------- | ------------ | -------------------------- | +| `vp dlx ` | `pnpm dlx ` | `npm exec ` | `npx ` | `yarn dlx ` | `bunx ` | Execute package binary | +| `--package `, `-p ` | `--package ` | `--package=` | N/A | `-p ` | N/A | Specify package to install | +| `--shell-mode`, `-c` | `-c` | `-c` | N/A | N/A | N/A | Execute in shell | +| `--silent`, `-s` | `--silent` | `--loglevel silent` | `--quiet` | `--quiet` | N/A | Suppress output | **Notes:** @@ -119,6 +120,7 @@ vp dlx -p typescript -p @types/node -c 'tsc --init && node -e "console.log(123)" - **Shell mode**: Yarn 2+ does not support shell mode (`-c`), command will print a warning and try to execute anyway. - **--package flag position**: For pnpm, `--package` comes before `dlx`. For npm, `--package` can be anywhere. For yarn, `-p` comes after `dlx`. - **Auto-confirm prompts**: For npm and npx (yarn@1 fallback), `--yes` is automatically added to align with pnpm's behavior which doesn't require confirmation. +- **bun**: Uses `bunx` as a standalone binary (not a subcommand of `bun`). It does not support `--package`, `--shell-mode`, or `--silent` flags. ### Argument Handling @@ -888,14 +890,14 @@ Examples: ## Package Manager Compatibility -| Feature | pnpm | npm | yarn@1 | yarn@2+ | Notes | -| ----------------- | ------- | ------- | ------- | ------- | ------------------------ | -| Basic execution | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | yarn@1 uses npx fallback | -| Version specifier | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | | -| --package flag | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | | -| Shell mode (-c) | ✅ Full | ✅ Full | ⚠️ npx | ❌ N/A | yarn@2+ doesn't support | -| Silent mode | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | | -| Auto-confirm | ✅ N/A | ✅ Auto | ⚠️ Auto | ✅ N/A | --yes added for npm/npx | +| Feature | pnpm | npm | yarn@1 | yarn@2+ | bun | Notes | +| ----------------- | ------- | ------- | ------- | ------- | --------- | ------------------------- | +| Basic execution | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | ✅ `bunx` | yarn@1 uses npx fallback | +| Version specifier | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | ✅ Full | | +| --package flag | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | ❌ N/A | bunx doesn't support | +| Shell mode (-c) | ✅ Full | ✅ Full | ⚠️ npx | ❌ N/A | ❌ N/A | yarn@2+/bun don't support | +| Silent mode | ✅ Full | ✅ Full | ⚠️ npx | ✅ Full | ❌ N/A | bunx doesn't support | +| Auto-confirm | ✅ N/A | ✅ Auto | ⚠️ Auto | ✅ N/A | ✅ N/A | --yes added for npm/npx | ## Security Considerations @@ -1026,7 +1028,7 @@ vp dlx madge --image deps.svg src/ ## Conclusion -This RFC proposes adding `vp dlx` command to provide unified remote package execution across pnpm/npm/yarn. The design: +This RFC proposes adding `vp dlx` command to provide unified remote package execution across pnpm/npm/yarn/bun. The design: - ✅ Unified interface for all package managers - ✅ Intelligent fallback for yarn@1 diff --git a/rfcs/exec-command.md b/rfcs/exec-command.md index 28a20b4c49..f26d5c2701 100644 --- a/rfcs/exec-command.md +++ b/rfcs/exec-command.md @@ -2,15 +2,17 @@ ## Summary -Add `vp exec` as a subcommand that prepends `./node_modules/.bin` to PATH and executes a command. This is the equivalent of `pnpm exec`. +Add `vp exec` as a subcommand that prepends `./node_modules/.bin` to PATH and executes a command. This is the equivalent of `pnpm exec` or direct execution with `bun`. The command completes the execution story alongside existing commands: -| Command | Behavior | Analogy | -| ------------- | -------------------------------------------------------------- | --------------- | -| `vp dlx` | Always downloads from remote | `pnpm dlx` | -| `vpx` | Local → global → PATH → remote fallback | `npx` | -| **`vp exec`** | **Prepend `node_modules/.bin` to PATH, then execute normally** | **`pnpm exec`** | +| Command | Behavior | Analogy | +| ------------- | -------------------------------------------------------------- | --------------------------- | +| `vp dlx` | Always downloads from remote | `pnpm dlx` / `bunx` | +| `vpx` | Local → global → PATH → remote fallback | `npx` | +| **`vp exec`** | **Prepend `node_modules/.bin` to PATH, then execute normally** | **`pnpm exec`** / **`bun`** | + +**Note:** bun natively resolves binaries from local `node_modules/.bin`, so `bun ` or `bunx ` can serve a similar purpose to `vp exec`. ## Motivation diff --git a/rfcs/install-command.md b/rfcs/install-command.md index 859b2fc47c..11b77ecefc 100644 --- a/rfcs/install-command.md +++ b/rfcs/install-command.md @@ -2,7 +2,7 @@ ## Summary -Add `vp install` command (alias: `vp i`) that automatically adapts to the detected package manager (pnpm/yarn/npm) for installing all dependencies in a project, with support for common flags and workspace-aware operations based on pnpm's API design. +Add `vp install` command (alias: `vp i`) that automatically adapts to the detected package manager (pnpm/yarn/npm/bun) for installing all dependencies in a project, with support for common flags and workspace-aware operations based on pnpm's API design. ## Motivation @@ -28,6 +28,7 @@ This creates friction in monorepo workflows and requires remembering different s pnpm install --frozen-lockfile # pnpm project yarn install --frozen-lockfile # yarn project (v1) or --immutable (v2+) npm ci # npm project (clean install) +bun install --frozen-lockfile # bun project # Different flags for production install pnpm install --prod @@ -120,27 +121,28 @@ vp install --filter app # Install for specific package - https://yarnpkg.com/cli/install - https://classic.yarnpkg.com/en/docs/cli/install - https://docs.npmjs.com/cli/v11/commands/npm-install - -| Vite+ Flag | pnpm | yarn@1 | yarn@2+ | npm | Description | -| ---------------------- | ---------------------- | ---------------------- | ------------------------------------------- | --------------------------- | ------------------------------------ | -| `vp install` | `pnpm install` | `yarn install` | `yarn install` | `npm install` | Install all dependencies | -| `--prod, -P` | `--prod` | `--production` | N/A (use `.yarnrc.yml`) | `--omit=dev` | Skip devDependencies | -| `--dev, -D` | `--dev` | N/A | N/A | `--include=dev --omit=prod` | Only devDependencies | -| `--no-optional` | `--no-optional` | `--ignore-optional` | N/A | `--omit=optional` | Skip optionalDependencies | -| `--frozen-lockfile` | `--frozen-lockfile` | `--frozen-lockfile` | `--immutable` | `ci` (use `npm ci`) | Fail if lockfile outdated | -| `--no-frozen-lockfile` | `--no-frozen-lockfile` | `--no-frozen-lockfile` | `--no-immutable` | `install` (not `ci`) | Allow lockfile updates | -| `--lockfile-only` | `--lockfile-only` | N/A | `--mode update-lockfile` | `--package-lock-only` | Only update lockfile | -| `--prefer-offline` | `--prefer-offline` | `--prefer-offline` | N/A | `--prefer-offline` | Prefer cached packages | -| `--offline` | `--offline` | `--offline` | N/A | `--offline` | Only use cache | -| `--force, -f` | `--force` | `--force` | N/A | `--force` | Force reinstall | -| `--ignore-scripts` | `--ignore-scripts` | `--ignore-scripts` | `--mode skip-build` | `--ignore-scripts` | Skip lifecycle scripts | -| `--no-lockfile` | `--no-lockfile` | `--no-lockfile` | N/A | `--no-package-lock` | Skip lockfile | -| `--fix-lockfile` | `--fix-lockfile` | N/A | `--refresh-lockfile` | N/A | Fix broken lockfile entries | -| `--shamefully-hoist` | `--shamefully-hoist` | N/A | N/A | N/A | Flat node_modules (pnpm) | -| `--resolution-only` | `--resolution-only` | N/A | N/A | N/A | Re-run resolution only (pnpm) | -| `--silent` | `--silent` | `--silent` | N/A (use env var) | `--loglevel silent` | Suppress output | -| `--filter ` | `--filter ` | N/A | `workspaces foreach -A --include ` | `--workspace ` | Target specific workspace package(s) | -| `-w, --workspace-root` | `-w` | `-W` | N/A | `--include-workspace-root` | Install in root only | +- https://bun.sh/docs/cli/install + +| Vite+ Flag | pnpm | yarn@1 | yarn@2+ | npm | bun | Description | +| ---------------------- | ---------------------- | ---------------------- | ------------------------------------------- | --------------------------- | --------------------------- | ------------------------------------ | +| `vp install` | `pnpm install` | `yarn install` | `yarn install` | `npm install` | `bun install` | Install all dependencies | +| `--prod, -P` | `--prod` | `--production` | N/A (use `.yarnrc.yml`) | `--omit=dev` | `--production` | Skip devDependencies | +| `--dev, -D` | `--dev` | N/A | N/A | `--include=dev --omit=prod` | N/A | Only devDependencies | +| `--no-optional` | `--no-optional` | `--ignore-optional` | N/A | `--omit=optional` | N/A | Skip optionalDependencies | +| `--frozen-lockfile` | `--frozen-lockfile` | `--frozen-lockfile` | `--immutable` | `ci` (use `npm ci`) | `--frozen-lockfile` | Fail if lockfile outdated | +| `--no-frozen-lockfile` | `--no-frozen-lockfile` | `--no-frozen-lockfile` | `--no-immutable` | `install` (not `ci`) | `--no-frozen-lockfile` | Allow lockfile updates | +| `--lockfile-only` | `--lockfile-only` | N/A | `--mode update-lockfile` | `--package-lock-only` | N/A | Only update lockfile | +| `--prefer-offline` | `--prefer-offline` | `--prefer-offline` | N/A | `--prefer-offline` | N/A | Prefer cached packages | +| `--offline` | `--offline` | `--offline` | N/A | `--offline` | N/A | Only use cache | +| `--force, -f` | `--force` | `--force` | N/A | `--force` | `--force` | Force reinstall | +| `--ignore-scripts` | `--ignore-scripts` | `--ignore-scripts` | `--mode skip-build` | `--ignore-scripts` | N/A (`trustedDependencies`) | Skip lifecycle scripts | +| `--no-lockfile` | `--no-lockfile` | `--no-lockfile` | N/A | `--no-package-lock` | N/A | Skip lockfile | +| `--fix-lockfile` | `--fix-lockfile` | N/A | `--refresh-lockfile` | N/A | N/A | Fix broken lockfile entries | +| `--shamefully-hoist` | `--shamefully-hoist` | N/A | N/A | N/A | N/A (hoisted by default) | Flat node_modules (pnpm) | +| `--resolution-only` | `--resolution-only` | N/A | N/A | N/A | N/A | Re-run resolution only (pnpm) | +| `--silent` | `--silent` | `--silent` | N/A (use env var) | `--loglevel silent` | `--silent` | Suppress output | +| `--filter ` | `--filter ` | N/A | `workspaces foreach -A --include ` | `--workspace ` | `--filter ` | Target specific workspace package(s) | +| `-w, --workspace-root` | `-w` | `-W` | N/A | `--include-workspace-root` | N/A | Install in root only | **Notes:** @@ -151,6 +153,7 @@ vp install --filter app # Install for specific package - `--fix-lockfile`: Automatically fixes broken lockfile entries (pnpm and yarn@2+ only, npm does not support) - `--resolution-only`: Re-runs dependency resolution without installing packages. Useful for peer dependency analysis (pnpm only) - `--shamefully-hoist`: pnpm-specific, creates flat node_modules like npm/yarn +- `--ignore-scripts`: For bun, lifecycle scripts are not run by default (security-first). Use `trustedDependencies` in package.json to explicitly allow scripts. - `--silent`: Suppresses output. For yarn@2+, use `YARN_ENABLE_PROGRESS=false` environment variable instead. For npm, maps to `--loglevel silent` **Add Package Mode:** @@ -801,6 +804,7 @@ VITE_PM=pnpm vp install - yarn@4.x - npm@10.x - npm@11.x +- bun@1.x ### Unit Tests @@ -1002,25 +1006,25 @@ This is a new feature with no breaking changes: ## Package Manager Compatibility Matrix -| Feature | pnpm | yarn@1 | yarn@2+ | npm | Notes | -| ---------------------- | ---- | ------ | ----------------------- | --------------- | ------------------------- | -| Basic install | ✅ | ✅ | ✅ | ✅ | All supported | -| `--prod` | ✅ | ✅ | ⚠️ | ✅ | yarn@2+ needs .yarnrc.yml | -| `--dev` | ✅ | ❌ | ❌ | ✅ | Limited support | -| `--no-optional` | ✅ | ✅ | ⚠️ | ✅ | yarn@2+ needs .yarnrc.yml | -| `--frozen-lockfile` | ✅ | ✅ | ✅ `--immutable` | ✅ `ci` | npm uses `npm ci` | -| `--no-frozen-lockfile` | ✅ | ✅ | ✅ `--no-immutable` | ✅ `install` | Pass through to PM | -| `--lockfile-only` | ✅ | ❌ | ✅ | ✅ | yarn@1 not supported | -| `--prefer-offline` | ✅ | ✅ | ❌ | ✅ | yarn@2+ not supported | -| `--offline` | ✅ | ✅ | ❌ | ✅ | yarn@2+ not supported | -| `--force` | ✅ | ✅ | ❌ | ✅ | yarn@2+ not supported | -| `--ignore-scripts` | ✅ | ✅ | ✅ `--mode skip-build` | ✅ | All supported | -| `--no-lockfile` | ✅ | ✅ | ❌ | ✅ | yarn@2+ not supported | -| `--fix-lockfile` | ✅ | ❌ | ✅ `--refresh-lockfile` | ❌ | pnpm and yarn@2+ only | -| `--shamefully-hoist` | ✅ | ❌ | ❌ | ❌ | pnpm only | -| `--resolution-only` | ✅ | ❌ | ❌ | ❌ | pnpm only | -| `--silent` | ✅ | ✅ | ⚠️ (use env var) | ✅ `--loglevel` | yarn@2+ use env var | -| `--filter` | ✅ | ❌ | ✅ `workspaces foreach` | ✅ | yarn@1 not supported | +| Feature | pnpm | yarn@1 | yarn@2+ | npm | bun | Notes | +| ---------------------- | ---- | ------ | ----------------------- | --------------- | -------------------------- | ---------------------------- | +| Basic install | ✅ | ✅ | ✅ | ✅ | ✅ | All supported | +| `--prod` | ✅ | ✅ | ⚠️ | ✅ | ✅ | yarn@2+ needs .yarnrc.yml | +| `--dev` | ✅ | ❌ | ❌ | ✅ | ❌ | Limited support | +| `--no-optional` | ✅ | ✅ | ⚠️ | ✅ | ❌ | yarn@2+ needs .yarnrc.yml | +| `--frozen-lockfile` | ✅ | ✅ | ✅ `--immutable` | ✅ `ci` | ✅ | npm uses `npm ci` | +| `--no-frozen-lockfile` | ✅ | ✅ | ✅ `--no-immutable` | ✅ `install` | ✅ | Pass through to PM | +| `--lockfile-only` | ✅ | ❌ | ✅ | ✅ | ❌ | yarn@1, bun not supported | +| `--prefer-offline` | ✅ | ✅ | ❌ | ✅ | ❌ | yarn@2+, bun not supported | +| `--offline` | ✅ | ✅ | ❌ | ✅ | ❌ | yarn@2+, bun not supported | +| `--force` | ✅ | ✅ | ❌ | ✅ | ✅ | yarn@2+ not supported | +| `--ignore-scripts` | ✅ | ✅ | ✅ `--mode skip-build` | ✅ | ⚠️ (`trustedDependencies`) | bun uses trustedDependencies | +| `--no-lockfile` | ✅ | ✅ | ❌ | ✅ | ❌ | yarn@2+, bun not supported | +| `--fix-lockfile` | ✅ | ❌ | ✅ `--refresh-lockfile` | ❌ | ❌ | pnpm and yarn@2+ only | +| `--shamefully-hoist` | ✅ | ❌ | ❌ | ❌ | ❌ (hoisted by default) | pnpm only | +| `--resolution-only` | ✅ | ❌ | ❌ | ❌ | ❌ | pnpm only | +| `--silent` | ✅ | ✅ | ⚠️ (use env var) | ✅ `--loglevel` | ✅ | yarn@2+ use env var | +| `--filter` | ✅ | ❌ | ✅ `workspaces foreach` | ✅ | ✅ | yarn@1 not supported | ## Future Enhancements @@ -1139,7 +1143,7 @@ vp install --offline ## Conclusion -This RFC proposes adding `vp install` command to provide a unified interface for installing dependencies across pnpm/yarn/npm. The design: +This RFC proposes adding `vp install` command to provide a unified interface for installing dependencies across pnpm/yarn/npm/bun. The design: - ✅ Automatically adapts to detected package manager - ✅ Supports common installation flags diff --git a/rfcs/link-unlink-package-commands.md b/rfcs/link-unlink-package-commands.md index 9ef299bd80..39fa0aea44 100644 --- a/rfcs/link-unlink-package-commands.md +++ b/rfcs/link-unlink-package-commands.md @@ -2,7 +2,7 @@ ## Summary -Add `vp link` (alias: `vp ln`) and `vp unlink` commands that automatically adapt to the detected package manager (pnpm/yarn/npm) for creating and removing symlinks to local packages, making them accessible system-wide or in other locations. This enables local package development and testing workflows. +Add `vp link` (alias: `vp ln`) and `vp unlink` commands that automatically adapt to the detected package manager (pnpm/yarn/npm/bun) for creating and removing symlinks to local packages, making them accessible system-wide or in other locations. This enables local package development and testing workflows. ## Motivation @@ -139,11 +139,16 @@ vp unlink -r - https://docs.npmjs.com/cli/v11/commands/npm-link - npm link creates symlinks between packages -| Vite+ Command | pnpm | yarn@1 | yarn@2+ | npm | Description | -| --------------- | ----------------- | ----------------- | ----------------- | ---------------- | ------------------------------------------------------- | -| `vp link` | `pnpm link` | `yarn link` | `yarn link` | `npm link` | Register current package or link to local directory | -| `vp link ` | `pnpm link ` | `yarn link ` | `yarn link ` | `npm link ` | Links package to current project | -| `vp link ` | `pnpm link ` | `yarn link ` | `yarn link ` | `npm link ` | Links package from `` directory to current project | +**bun references:** + +- https://bun.sh/docs/cli/link +- bun link creates symlinks for local packages + +| Vite+ Command | pnpm | yarn@1 | yarn@2+ | npm | bun | Description | +| --------------- | ----------------- | ----------------- | ----------------- | ---------------- | ---------------- | ------------------------------------------------------- | +| `vp link` | `pnpm link` | `yarn link` | `yarn link` | `npm link` | `bun link` | Register current package or link to local directory | +| `vp link ` | `pnpm link ` | `yarn link ` | `yarn link ` | `npm link ` | `bun link ` | Links package to current project | +| `vp link ` | `pnpm link ` | `yarn link ` | `yarn link ` | `npm link ` | `bun link ` | Links package from `` directory to current project | #### Unlink Command Mapping @@ -163,11 +168,11 @@ vp unlink -r - https://docs.npmjs.com/cli/v11/commands/npm-uninstall - npm unlink removes symlinks -| Vite+ Command | pnpm | yarn@1 | yarn@2+ | npm | Description | -| ----------------------- | ------------------------- | ------------------- | ------------------- | ------------------ | ---------------------------------- | -| `vp unlink` | `pnpm unlink` | `yarn unlink` | `yarn unlink` | `npm unlink` | Unlinks current package | -| `vp unlink ` | `pnpm unlink ` | `yarn unlink ` | `yarn unlink ` | `npm unlink ` | Unlinks specific package | -| `vp unlink --recursive` | `pnpm unlink --recursive` | N/A | `yarn unlink --all` | N/A | Unlinks in every workspace package | +| Vite+ Command | pnpm | yarn@1 | yarn@2+ | npm | bun | Description | +| ----------------------- | ------------------------- | ------------------- | ------------------- | ------------------ | ------------ | ---------------------------------- | +| `vp unlink` | `pnpm unlink` | `yarn unlink` | `yarn unlink` | `npm unlink` | `bun unlink` | Unlinks current package | +| `vp unlink ` | `pnpm unlink ` | `yarn unlink ` | `yarn unlink ` | `npm unlink ` | `bun unlink` | Unlinks specific package | +| `vp unlink --recursive` | `pnpm unlink --recursive` | N/A | `yarn unlink --all` | N/A | N/A | Unlinks in every workspace package | ### Link/Unlink Behavior Differences Across Package Managers @@ -217,6 +222,18 @@ vp unlink -r - `npm unlink`: Removes global symlink for current package - `npm unlink `: Removes package from current project +#### bun + +**Link behavior:** + +- `bun link`: Registers current package as a linkable package +- `bun link `: Links a registered package to current project +- `--save`: Adds `link:` prefix to package.json dependency entry + +**Unlink behavior:** + +- `bun unlink`: Unlinks current package + ### Implementation Architecture #### 1. Command Structure @@ -740,6 +757,7 @@ $ vp link - yarn@4.x - npm@10.x - npm@11.x +- bun@1.x [WIP] ### Unit Tests @@ -992,13 +1010,14 @@ npm install my-lib@latest ## Package Manager Compatibility -| Feature | pnpm | yarn@1 | yarn@2+ | npm | Notes | -| -------------------- | ----------------------- | ---------------- | ----------------- | ---------------- | ---------------- | -| Link package/dir | `link` | `link` | `link` | `link` | All supported | -| Link with package | `link ` | `link ` | `link ` | `link ` | All supported | -| Link local directory | `link ` | `link ` | `link ` | `link ` | All supported | -| Unlink | `unlink` | `unlink` | `unlink` | `unlink` | All supported | -| Recursive unlink | ✅ `unlink --recursive` | ❌ Not supported | ✅ `unlink --all` | ❌ Not supported | pnpm and yarn@2+ | +| Feature | pnpm | yarn@1 | yarn@2+ | npm | bun | Notes | +| -------------------- | ----------------------- | ---------------- | ----------------- | ---------------- | ---------------- | ---------------- | +| Link package/dir | `link` | `link` | `link` | `link` | `link` | All supported | +| Link with package | `link ` | `link ` | `link ` | `link ` | `link ` | All supported | +| Link local directory | `link ` | `link ` | `link ` | `link ` | `link ` | All supported | +| Save to package.json | N/A | N/A | N/A | N/A | `--save` | bun-specific | +| Unlink | `unlink` | `unlink` | `unlink` | `unlink` | `unlink` | All supported | +| Recursive unlink | ✅ `unlink --recursive` | ❌ Not supported | ✅ `unlink --all` | ❌ Not supported | ❌ Not supported | pnpm and yarn@2+ | ## Future Enhancements @@ -1090,7 +1109,7 @@ vp link --verify ## Conclusion -This RFC proposes adding `vp link` and `vp unlink` commands to provide a unified interface for local package development across pnpm/yarn/npm. The design: +This RFC proposes adding `vp link` and `vp unlink` commands to provide a unified interface for local package development across pnpm/yarn/npm/bun. The design: - ✅ Automatically adapts to detected package manager - ✅ Supports both package and local directory linking diff --git a/rfcs/outdated-package-command.md b/rfcs/outdated-package-command.md index 13ecca0eb3..4d4b3254b0 100644 --- a/rfcs/outdated-package-command.md +++ b/rfcs/outdated-package-command.md @@ -2,7 +2,7 @@ ## Summary -Add `vite outdated` command that automatically adapts to the detected package manager (pnpm/npm/yarn) for checking outdated packages. This helps developers identify packages that have newer versions available, maintain up-to-date dependencies, and manage security vulnerabilities by showing which packages can be updated. +Add `vite outdated` command that automatically adapts to the detected package manager (pnpm/npm/yarn/bun) for checking outdated packages. This helps developers identify packages that have newer versions available, maintain up-to-date dependencies, and manage security vulnerabilities by showing which packages can be updated. ## Motivation @@ -147,21 +147,26 @@ vite outdated -g # Check globally installed packages - https://yarnpkg.com/cli/upgrade-interactive (yarn@2+) - Checks for outdated package dependencies -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ---------------------- | ---------------------- | ----------------------------------- | --------------- | -------------------------- | --------------------------------------------- | -| `vite outdated` | `pnpm outdated` | `npm outdated` | `yarn outdated` | `yarn upgrade-interactive` | Check for outdated packages | -| `...` | `...` | `[[@scope/]]` | `[]` | N/A | Package patterns to check | -| `--long` | `--long` | `--long` | N/A | N/A | Extended output format | -| `--format ` | `--format ` | json: `--json`/ list: `--parseable` | `--json` | N/A | Output format (table/list/json) | -| `-r, --recursive` | `-r, --recursive` | `--all` | N/A | N/A | Check across all workspaces | -| `--filter ` | `--filter ` | `--workspace ` | N/A | N/A | Target specific workspace | -| `-w, --workspace-root` | `-w, --workspace-root` | `--include-workspace-root` | N/A | N/A | Include workspace root | -| `-P, --prod` | `-P, --prod` | N/A | N/A | N/A | Only production dependencies (pnpm-specific) | -| `-D, --dev` | `-D, --dev` | N/A | N/A | N/A | Only dev dependencies (pnpm-specific) | -| `--no-optional` | `--no-optional` | N/A | N/A | N/A | Exclude optional dependencies (pnpm-specific) | -| `--compatible` | `--compatible` | N/A | N/A | N/A | Only show compatible versions (pnpm-specific) | -| `--sort-by ` | `--sort-by ` | N/A | N/A | N/A | Sort results by field (pnpm-specific) | -| `-g, --global` | `-g, --global` | `-g, --global` | N/A | N/A | Check globally installed packages | +**bun references:** + +- https://bun.sh/docs/cli/outdated +- Checks for outdated packages in the current project + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ---------------------- | ---------------------- | ----------------------------------- | --------------- | -------------------------- | -------------------- | --------------------------------------------- | +| `vite outdated` | `pnpm outdated` | `npm outdated` | `yarn outdated` | `yarn upgrade-interactive` | `bun outdated` | Check for outdated packages | +| `...` | `...` | `[[@scope/]]` | `[]` | N/A | N/A | Package patterns to check | +| `--long` | `--long` | `--long` | N/A | N/A | N/A | Extended output format | +| `--format ` | `--format ` | json: `--json`/ list: `--parseable` | `--json` | N/A | N/A | Output format (table/list/json) | +| `-r, --recursive` | `-r, --recursive` | `--all` | N/A | N/A | `-r` / `--recursive` | Check across all workspaces | +| `--filter ` | `--filter ` | `--workspace ` | N/A | N/A | `--filter` / `-F` | Target specific workspace | +| `-w, --workspace-root` | `-w, --workspace-root` | `--include-workspace-root` | N/A | N/A | N/A | Include workspace root | +| `-P, --prod` | `-P, --prod` | N/A | N/A | N/A | N/A | Only production dependencies (pnpm-specific) | +| `-D, --dev` | `-D, --dev` | N/A | N/A | N/A | N/A | Only dev dependencies (pnpm-specific) | +| `--no-optional` | `--no-optional` | N/A | N/A | N/A | N/A | Exclude optional dependencies (pnpm-specific) | +| `--compatible` | `--compatible` | N/A | N/A | N/A | N/A | Only show compatible versions (pnpm-specific) | +| `--sort-by ` | `--sort-by ` | N/A | N/A | N/A | N/A | Sort results by field (pnpm-specific) | +| `-g, --global` | `-g, --global` | `-g, --global` | N/A | N/A | N/A | Check globally installed packages | **Note:** @@ -170,6 +175,8 @@ vite outdated -g # Check globally installed packages - yarn@1 accepts package names but limited filtering options - yarn@2+ uses interactive mode (`upgrade-interactive`) instead of traditional `outdated` - pnpm has the most comprehensive filtering and output options +- bun supports `--filter` / `-F` for workspace filtering and `-r` / `--recursive` for checking across all workspaces +- bun does not support JSON output format (`--format json`) ### Outdated Behavior Differences Across Package Managers @@ -1002,6 +1009,7 @@ vp outdated --update - yarn@4.x - npm@10.x - npm@11.x +- bun@1.x [WIP] ### Unit Tests @@ -1286,21 +1294,21 @@ vite outdated -g typescript ## Package Manager Compatibility -| Feature | pnpm | npm | yarn@1 | yarn@2+ | Notes | -| ------------------- | ------------------ | ----------------------------- | ---------------- | ------------------- | ------------------------ | -| Basic command | ✅ `outdated` | ✅ `outdated` | ✅ `outdated` | ⚠️ `upgrade-int...` | yarn@2+ uses interactive | -| Pattern matching | ✅ Glob patterns | ⚠️ Package names | ⚠️ Package names | ❌ Not supported | pnpm supports globs | -| JSON output | ✅ `--format json` | ✅ `--json` | ❌ Not supported | ❌ Not supported | Different flags | -| Long output | ✅ `--long` | ✅ `--long` | ❌ Not supported | ❌ Not supported | pnpm and npm only | -| Parseable | ❌ Not supported | ✅ `--parseable` | ❌ Not supported | ❌ Not supported | npm only | -| Recursive | ✅ `-r` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Workspace filter | ✅ `--filter` | ✅ `--workspace` | ❌ Not supported | ❌ Not supported | Different flags | -| Workspace root | ✅ `-w` | ✅ `--include-workspace-root` | ❌ Not supported | ❌ Not supported | Different flags | -| Dep type filter | ✅ `--prod/--dev` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Compatible only | ✅ `--compatible` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Sort results | ✅ `--sort-by` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Global check | ✅ `-g` | ✅ `-g` | ❌ Not supported | ❌ Not supported | pnpm and npm | -| Show all transitive | ⚠️ Use `-r` | ✅ `--all` | ❌ Not supported | ❌ Not supported | Different approaches | +| Feature | pnpm | npm | yarn@1 | yarn@2+ | bun | Notes | +| ------------------- | ------------------ | ----------------------------- | ---------------- | ------------------- | -------------------- | ------------------------ | +| Basic command | ✅ `outdated` | ✅ `outdated` | ✅ `outdated` | ⚠️ `upgrade-int...` | ✅ `outdated` | yarn@2+ uses interactive | +| Pattern matching | ✅ Glob patterns | ⚠️ Package names | ⚠️ Package names | ❌ Not supported | ❌ Not supported | pnpm supports globs | +| JSON output | ✅ `--format json` | ✅ `--json` | ❌ Not supported | ❌ Not supported | ❌ Not supported | Different flags | +| Long output | ✅ `--long` | ✅ `--long` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm and npm only | +| Parseable | ❌ Not supported | ✅ `--parseable` | ❌ Not supported | ❌ Not supported | ❌ Not supported | npm only | +| Recursive | ✅ `-r` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ✅ `-r` | pnpm and bun | +| Workspace filter | ✅ `--filter` | ✅ `--workspace` | ❌ Not supported | ❌ Not supported | ✅ `--filter` / `-F` | Different flags | +| Workspace root | ✅ `-w` | ✅ `--include-workspace-root` | ❌ Not supported | ❌ Not supported | ❌ Not supported | Different flags | +| Dep type filter | ✅ `--prod/--dev` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Compatible only | ✅ `--compatible` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Sort results | ✅ `--sort-by` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Global check | ✅ `-g` | ✅ `-g` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm and npm | +| Show all transitive | ⚠️ Use `-r` | ✅ `--all` | ❌ Not supported | ❌ Not supported | ❌ Not supported | Different approaches | ## Future Enhancements @@ -1415,7 +1423,7 @@ Changes: ## Conclusion -This RFC proposes adding `vite outdated` command to provide a unified interface for checking outdated packages across pnpm/npm/yarn. The design: +This RFC proposes adding `vite outdated` command to provide a unified interface for checking outdated packages across pnpm/npm/yarn/bun. The design: - ✅ Automatically adapts to detected package manager - ✅ Supports pattern matching (pnpm) with graceful degradation diff --git a/rfcs/pack-command.md b/rfcs/pack-command.md index bb892dfca3..2a938f1e24 100644 --- a/rfcs/pack-command.md +++ b/rfcs/pack-command.md @@ -232,10 +232,12 @@ Node.js version v22.22.0 does not support `exe` option. Please upgrade to Node.j These are distinct commands: -| Command | Purpose | Output | -| ------------ | ----------------------------- | ------------------- | -| `vp pack` | Library bundling via tsdown | `dist/` directory | -| `vp pm pack` | Tarball creation via npm/pnpm | `.tgz` package file | +| Command | Purpose | Output | +| ------------ | --------------------------------- | ------------------- | +| `vp pack` | Library bundling via tsdown | `dist/` directory | +| `vp pm pack` | Tarball creation via npm/pnpm/bun | `.tgz` package file | + +**Note:** For tarball creation, bun uses `bun pm pack` (not `bun pack`). It supports `--destination` and `--dry-run` flags. See the [pm-command-group RFC](./pm-command-group.md) for the full command mapping. ## Design Decisions diff --git a/rfcs/pm-command-group.md b/rfcs/pm-command-group.md index 6f31931624..a7fad2aff5 100644 --- a/rfcs/pm-command-group.md +++ b/rfcs/pm-command-group.md @@ -2,7 +2,7 @@ ## Summary -Add `vp pm` command group that provides a set of utilities for working with package managers. The `pm` command group offers direct access to package manager utilities like cache management, package publishing, configuration, and more. These are pass-through commands that delegate to the detected package manager (pnpm/npm/yarn) with minimal processing, providing a unified interface across different package managers. +Add `vp pm` command group that provides a set of utilities for working with package managers. The `pm` command group offers direct access to package manager utilities like cache management, package publishing, configuration, and more. These are pass-through commands that delegate to the detected package manager (pnpm/npm/yarn/bun) with minimal processing, providing a unified interface across different package managers. ## Motivation @@ -730,6 +730,22 @@ vp pm ping --registry https://custom-registry.com - `--registry `: Registry URL to ping +### Bun-Specific Subcommands + +Bun provides several `bun pm` subcommands that may not have direct equivalents in other package managers: + +- `bun pm ls` / `bun list` - List installed packages +- `bun pm bin` - Show the bin directory for installed binaries +- `bun pm cache` / `bun pm cache rm` - Cache management (show cache path / remove cached packages) +- `bun pm whoami` - Show the currently logged-in npm registry username +- `bun pm pack` - Create a tarball of the package (supports `--destination`, `--dry-run`) +- `bun pm trust` / `bun pm untrusted` - Manage trusted dependencies (allow lifecycle scripts) +- `bun pm version` - Show the installed version of bun +- `bun pm pkg` - Manage package.json fields programmatically +- `bun publish` - Publish package to the npm registry (direct subcommand, not `bun pm publish`) + +**Note:** Many npm registry operations (login, logout, owner, dist-tag, deprecate, search, fund, ping, token) do not have native bun equivalents and delegate to `npm` when using bun as the package manager. + ### Command Mapping #### Prune Command @@ -747,16 +763,17 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/prune - The prune command isn't necessary. yarn install will prune extraneous packages. -| Vite+ Flag | pnpm | npm | yarn | Description | -| --------------- | --------------- | ----------------- | ---- | --------------------------- | -| `vp pm prune` | `pnpm prune` | `npm prune` | N/A | Remove unnecessary packages | -| `--prod` | `--prod` | `--omit=dev` | N/A | Remove devDependencies | -| `--no-optional` | `--no-optional` | `--omit=optional` | N/A | Remove optional deps | +| Vite+ Flag | pnpm | npm | yarn | bun | Description | +| --------------- | --------------- | ----------------- | ---- | --- | --------------------------- | +| `vp pm prune` | `pnpm prune` | `npm prune` | N/A | N/A | Remove unnecessary packages | +| `--prod` | `--prod` | `--omit=dev` | N/A | N/A | Remove devDependencies | +| `--no-optional` | `--no-optional` | `--omit=optional` | N/A | N/A | Remove optional deps | **Note:** - npm supports prune with `--omit=dev` (for prod) and `--omit=optional` (for no-optional) - yarn doesn't have a prune command (automatic during install) +- bun doesn't have a prune command #### Pack Command @@ -774,15 +791,16 @@ vp pm ping --registry https://custom-registry.com - https://yarnpkg.com/cli/pack - https://yarnpkg.com/cli/workspaces/foreach (for yarn@2+ recursive packing) -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| --------------------------- | -------------------- | -------------------- | ------------ | --------------------------------------------- | --------------------------------- | -| `vp pm pack` | `pnpm pack` | `npm pack` | `yarn pack` | `yarn pack` | Create package tarball | -| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | `workspaces foreach --all pack` | Pack all workspace packages | -| `--filter ` | `--filter` | `--workspace` | N/A | `workspaces foreach --include pack` | Filter packages to pack | -| `--out ` | `--out` | N/A | `--filename` | `--out` | Output file path (supports %s/%v) | -| `--pack-destination ` | `--pack-destination` | `--pack-destination` | N/A | N/A | Output directory | -| `--pack-gzip-level ` | `--pack-gzip-level` | N/A | N/A | N/A | Gzip compression level (0-9) | -| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| --------------------------- | -------------------- | -------------------- | ------------ | --------------------------------------------- | --------------- | --------------------------------- | +| `vp pm pack` | `pnpm pack` | `npm pack` | `yarn pack` | `yarn pack` | `bun pm pack` | Create package tarball | +| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | `workspaces foreach --all pack` | N/A | Pack all workspace packages | +| `--filter ` | `--filter` | `--workspace` | N/A | `workspaces foreach --include pack` | N/A | Filter packages to pack | +| `--out ` | `--out` | N/A | `--filename` | `--out` | N/A | Output file path (supports %s/%v) | +| `--pack-destination ` | `--pack-destination` | `--pack-destination` | N/A | N/A | `--destination` | Output directory | +| `--pack-gzip-level ` | `--pack-gzip-level` | N/A | N/A | N/A | N/A | Gzip compression level (0-9) | +| `--json` | `--json` | `--json` | `--json` | `--json` | N/A | JSON output | +| `--dry-run` | N/A | `--dry-run` | N/A | N/A | `--dry-run` | Preview without creating tarball | **Note:** @@ -805,7 +823,8 @@ vp pm ping --registry https://custom-registry.com - yarn does not support this option (prints warning and ignores) - `--pack-gzip-level `: Gzip compression level (0-9) - Only supported by pnpm - - npm and yarn do not support this option (prints warning and ignores) + - npm, yarn, and bun do not support this option (prints warning and ignores) +- bun uses `bun pm pack` (not `bun pack`), supports `--destination` and `--dry-run` flags #### List Command @@ -821,22 +840,22 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/list -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| -------------------- | ----------------- | ------------------------------- | ------------- | ------------- | --------------------------------------------- | -| `vp pm list` | `pnpm list` | `npm list` | `yarn list` | N/A | List installed packages | -| `--depth ` | `--depth ` | `--depth ` | `--depth ` | N/A | Limit tree depth | -| `--json` | `--json` | `--json` | `--json` | N/A | JSON output | -| `--long` | `--long` | `--long` | N/A | N/A | Extended info | -| `--parseable` | `--parseable` | `--parseable` | N/A | N/A | Parseable format | -| `-P, --prod` | `--prod` | `--include prod --include peer` | N/A | N/A | Production deps only | -| `-D, --dev` | `--dev` | `--include dev` | N/A | N/A | Dev deps only | -| `--no-optional` | `--no-optional` | `--omit optional` | N/A | N/A | Exclude optional deps | -| `--exclude-peers` | `--exclude-peers` | `--omit peer` | N/A | N/A | Exclude peer deps | -| `--only-projects` | `--only-projects` | N/A | N/A | N/A | Show only project packages (pnpm) | -| `--find-by ` | `--find-by` | N/A | N/A | N/A | Use finder function from .pnpmfile.cjs (pnpm) | -| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | N/A | List across workspaces | -| `--filter ` | `--filter` | `--workspace` | N/A | N/A | Filter workspace | -| `-g, --global` | `npm list -g` | `npm list -g` | `npm list -g` | `npm list -g` | List global packages | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| -------------------- | ----------------- | ------------------------------- | ------------- | ------------- | ------------- | --------------------------------------------- | +| `vp pm list` | `pnpm list` | `npm list` | `yarn list` | N/A | `bun pm ls` | List installed packages | +| `--depth ` | `--depth ` | `--depth ` | `--depth ` | N/A | N/A | Limit tree depth | +| `--json` | `--json` | `--json` | `--json` | N/A | N/A | JSON output | +| `--long` | `--long` | `--long` | N/A | N/A | N/A | Extended info | +| `--parseable` | `--parseable` | `--parseable` | N/A | N/A | N/A | Parseable format | +| `-P, --prod` | `--prod` | `--include prod --include peer` | N/A | N/A | N/A | Production deps only | +| `-D, --dev` | `--dev` | `--include dev` | N/A | N/A | N/A | Dev deps only | +| `--no-optional` | `--no-optional` | `--omit optional` | N/A | N/A | N/A | Exclude optional deps | +| `--exclude-peers` | `--exclude-peers` | `--omit peer` | N/A | N/A | N/A | Exclude peer deps | +| `--only-projects` | `--only-projects` | N/A | N/A | N/A | N/A | Show only project packages (pnpm) | +| `--find-by ` | `--find-by` | N/A | N/A | N/A | N/A | Use finder function from .pnpmfile.cjs (pnpm) | +| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | N/A | N/A | List across workspaces | +| `--filter ` | `--filter` | `--workspace` | N/A | N/A | N/A | Filter workspace | +| `-g, --global` | `npm list -g` | `npm list -g` | `npm list -g` | `npm list -g` | `npm list -g` | List global packages | **Note:** @@ -900,10 +919,10 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/info (delegates to npm view) - https://yarnpkg.com/cli/npm/info (delegates to npm view) -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------ | ---------- | ---------- | ---------- | ---------- | ----------------- | -| `vp pm view` | `npm view` | `npm view` | `npm view` | `npm view` | View package info | -| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------ | ---------- | ---------- | ---------- | ---------- | ---------- | ----------------- | +| `vp pm view` | `npm view` | `npm view` | `npm view` | `npm view` | `npm view` | View package info | +| `--json` | `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | **Note:** @@ -926,20 +945,20 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/publish (delegates to npm publish) - https://yarnpkg.com/cli/npm/publish (delegates to npm publish) -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| --------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | --------------------------- | -| `vp pm publish` | `pnpm publish` | `npm publish` | `npm publish` | `npm publish` | Publish package | -| `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | Preview without publishing | -| `--tag ` | `--tag ` | `--tag ` | `--tag ` | `--tag ` | Publish tag | -| `--access ` | `--access ` | `--access ` | `--access ` | `--access ` | Public/restricted | -| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | -| `--no-git-checks` | `--no-git-checks` | N/A | N/A | N/A | Skip git checks (pnpm) | -| `--publish-branch ` | `--publish-branch` | N/A | N/A | N/A | Set publish branch (pnpm) | -| `--report-summary` | `--report-summary` | N/A | N/A | N/A | Save publish summary (pnpm) | -| `--force` | `--force` | `--force` | `--force` | `--force` | Force publish | -| `--json` | `--json` | N/A | N/A | N/A | JSON output (pnpm) | -| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | N/A | Publish workspaces | -| `--filter ` | `--filter` | `--workspace` | N/A | N/A | Filter workspace | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| --------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------- | --------------------------- | +| `vp pm publish` | `pnpm publish` | `npm publish` | `npm publish` | `npm publish` | `bun publish` | Publish package | +| `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | Preview without publishing | +| `--tag ` | `--tag ` | `--tag ` | `--tag ` | `--tag ` | `--tag ` | Publish tag | +| `--access ` | `--access ` | `--access ` | `--access ` | `--access ` | `--access` | Public/restricted | +| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | N/A | One-time password | +| `--no-git-checks` | `--no-git-checks` | N/A | N/A | N/A | N/A | Skip git checks (pnpm) | +| `--publish-branch ` | `--publish-branch` | N/A | N/A | N/A | N/A | Set publish branch (pnpm) | +| `--report-summary` | `--report-summary` | N/A | N/A | N/A | N/A | Save publish summary (pnpm) | +| `--force` | `--force` | `--force` | `--force` | `--force` | N/A | Force publish | +| `--json` | `--json` | N/A | N/A | N/A | N/A | JSON output (pnpm) | +| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | N/A | N/A | Publish workspaces | +| `--filter ` | `--filter` | `--workspace` | N/A | N/A | N/A | Filter workspace | **Note:** @@ -986,12 +1005,12 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/owner (delegates to npm owner) - https://yarnpkg.com/cli/npm/owner (delegates to npm owner) -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------------- | ---------------- | ---------------- | ---------------- | ---------------- | ------------------- | -| `vp pm owner list ` | `npm owner list` | `npm owner list` | `npm owner list` | `npm owner list` | List package owners | -| `vp pm owner add

` | `npm owner add` | `npm owner add` | `npm owner add` | `npm owner add` | Add owner | -| `vp pm owner rm

` | `npm owner rm` | `npm owner rm` | `npm owner rm` | `npm owner rm` | Remove owner | -| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ------------------- | +| `vp pm owner list ` | `npm owner list` | `npm owner list` | `npm owner list` | `npm owner list` | `npm owner list` | List package owners | +| `vp pm owner add

` | `npm owner add` | `npm owner add` | `npm owner add` | `npm owner add` | `npm owner add` | Add owner | +| `vp pm owner rm

` | `npm owner rm` | `npm owner rm` | `npm owner rm` | `npm owner rm` | `npm owner rm` | Remove owner | +| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | **Note:** @@ -1014,11 +1033,11 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/cache - https://yarnpkg.com/cli/cache -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------- | ------------------ | ---------------------- | ------------------ | ----------------------------- | -------------------- | -| `vp pm cache dir` | `pnpm store path` | `npm config get cache` | `yarn cache dir` | `yarn config get cacheFolder` | Show cache directory | -| `vp pm cache path` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for dir | -| `vp pm cache clean` | `pnpm store prune` | `npm cache clean` | `yarn cache clean` | `yarn cache clean` | Clean cache | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------- | ------------------ | ---------------------- | ------------------ | ----------------------------- | ----------------- | -------------------- | +| `vp pm cache dir` | `pnpm store path` | `npm config get cache` | `yarn cache dir` | `yarn config get cacheFolder` | `bun pm cache` | Show cache directory | +| `vp pm cache path` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for dir | +| `vp pm cache clean` | `pnpm store prune` | `npm cache clean` | `yarn cache clean` | `yarn cache clean` | `bun pm cache rm` | Clean cache | **Note:** @@ -1044,15 +1063,15 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/config - https://yarnpkg.com/cli/config -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| --------------------------- | -------------------- | ------------------- | -------------------- | --------------------------- | ------------------ | -| `vp pm config list` | `pnpm config list` | `npm config list` | `yarn config list` | `yarn config` | List configuration | -| `vp pm config get ` | `pnpm config get` | `npm config get` | `yarn config get` | `yarn config get` | Get config value | -| `vp pm config set ` | `pnpm config set` | `npm config set` | `yarn config set` | `yarn config set` | Set config value | -| `vp pm config delete ` | `pnpm config delete` | `npm config delete` | `yarn config delete` | `yarn config unset` | Delete config key | -| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | -| `-g, --global` | `--global` | `--global` | `--global` | `--home` | Global config | -| `--location ` | `--location` | `--location` | N/A | Maps to `--home` for global | Config location | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| --------------------------- | -------------------- | ------------------- | -------------------- | --------------------------- | --- | ------------------ | +| `vp pm config list` | `pnpm config list` | `npm config list` | `yarn config list` | `yarn config` | N/A | List configuration | +| `vp pm config get ` | `pnpm config get` | `npm config get` | `yarn config get` | `yarn config get` | N/A | Get config value | +| `vp pm config set ` | `pnpm config set` | `npm config set` | `yarn config set` | `yarn config set` | N/A | Set config value | +| `vp pm config delete ` | `pnpm config delete` | `npm config delete` | `yarn config delete` | `yarn config unset` | N/A | Delete config key | +| `--json` | `--json` | `--json` | `--json` | `--json` | N/A | JSON output | +| `-g, --global` | `--global` | `--global` | `--global` | `--home` | N/A | Global config | +| `--location ` | `--location` | `--location` | N/A | Maps to `--home` for global | N/A | Config location | **Note:** @@ -1087,11 +1106,11 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/login - https://yarnpkg.com/cli/npm/login -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------ | ------------ | ------------ | ------------ | ---------------- | -------------------- | -| `vp pm login` | `npm login` | `npm login` | `yarn login` | `yarn npm login` | Log in to registry | -| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | -| `--scope ` | `--scope` | `--scope` | `--scope` | `--scope` | Associate with scope | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------ | ------------ | ------------ | ------------ | ---------------- | ------------ | -------------------- | +| `vp pm login` | `npm login` | `npm login` | `yarn login` | `yarn npm login` | `npm login` | Log in to registry | +| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | +| `--scope ` | `--scope` | `--scope` | `--scope` | `--scope` | `--scope` | Associate with scope | **Note:** @@ -1114,11 +1133,11 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/logout - https://yarnpkg.com/cli/npm/logout -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------ | ------------ | ------------ | ------------- | ----------------- | --------------------- | -| `vp pm logout` | `npm logout` | `npm logout` | `yarn logout` | `yarn npm logout` | Log out from registry | -| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | -| `--scope ` | `--scope` | `--scope` | `--scope` | `--scope` | Scoped registry | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------ | ------------ | ------------ | ------------- | ----------------- | ------------ | --------------------- | +| `vp pm logout` | `npm logout` | `npm logout` | `yarn logout` | `yarn npm logout` | `npm logout` | Log out from registry | +| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | +| `--scope ` | `--scope` | `--scope` | `--scope` | `--scope` | `--scope` | Scoped registry | **Note:** @@ -1140,10 +1159,10 @@ vp pm ping --registry https://custom-registry.com - https://yarnpkg.com/cli/npm/whoami -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------ | ------------ | ------------ | ---------- | ----------------- | ------------------- | -| `vp pm whoami` | `npm whoami` | `npm whoami` | N/A (warn) | `yarn npm whoami` | Show logged-in user | -| `--registry ` | `--registry` | `--registry` | N/A | `--registry` | Registry URL | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------ | ------------ | ------------ | ---------- | ----------------- | --------------- | ------------------- | +| `vp pm whoami` | `npm whoami` | `npm whoami` | N/A (warn) | `yarn npm whoami` | `bun pm whoami` | Show logged-in user | +| `--registry ` | `--registry` | `--registry` | N/A | `--registry` | N/A | Registry URL | **Note:** @@ -1157,13 +1176,13 @@ vp pm ping --registry https://custom-registry.com - https://docs.npmjs.com/cli/v11/commands/npm-token -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| -------------------- | ------------------ | ------------------ | ---------- | ---------- | ---------------- | -| `vp pm token list` | `npm token list` | `npm token list` | N/A (warn) | N/A (warn) | List tokens | -| `vp pm token create` | `npm token create` | `npm token create` | N/A (warn) | N/A (warn) | Create token | -| `vp pm token revoke` | `npm token revoke` | `npm token revoke` | N/A (warn) | N/A (warn) | Revoke token | -| `--read-only` | `--read-only` | `--read-only` | N/A | N/A | Read-only token | -| `--cidr ` | `--cidr` | `--cidr` | N/A | N/A | CIDR restriction | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| -------------------- | ------------------ | ------------------ | ---------- | ---------- | ---------- | ---------------- | +| `vp pm token list` | `npm token list` | `npm token list` | N/A (warn) | N/A (warn) | N/A (warn) | List tokens | +| `vp pm token create` | `npm token create` | `npm token create` | N/A (warn) | N/A (warn) | N/A (warn) | Create token | +| `vp pm token revoke` | `npm token revoke` | `npm token revoke` | N/A (warn) | N/A (warn) | N/A (warn) | Revoke token | +| `--read-only` | `--read-only` | `--read-only` | N/A | N/A | N/A | Read-only token | +| `--cidr ` | `--cidr` | `--cidr` | N/A | N/A | N/A | CIDR restriction | **Note:** @@ -1185,13 +1204,13 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/audit - https://yarnpkg.com/cli/npm/audit -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ----------------------- | --------------- | --------------- | --------------- | -------------------------- | ------------------ | -| `vp pm audit` | `pnpm audit` | `npm audit` | `yarn audit` | `yarn npm audit` | Run security audit | -| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | -| `--prod` | `--prod` | `--omit=dev` | `--groups prod` | `--environment production` | Production only | -| `--audit-level ` | `--audit-level` | `--audit-level` | `--level` | `--severity` | Minimum severity | -| `fix` | `--fix` | `npm audit fix` | N/A | N/A | Auto-fix | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ----------------------- | --------------- | --------------- | --------------- | -------------------------- | ---------- | ------------------ | +| `vp pm audit` | `pnpm audit` | `npm audit` | `yarn audit` | `yarn npm audit` | N/A (warn) | Run security audit | +| `--json` | `--json` | `--json` | `--json` | `--json` | N/A | JSON output | +| `--prod` | `--prod` | `--omit=dev` | `--groups prod` | `--environment production` | N/A | Production only | +| `--audit-level ` | `--audit-level` | `--audit-level` | `--level` | `--severity` | N/A | Minimum severity | +| `fix` | `--fix` | `npm audit fix` | N/A | N/A | N/A | Auto-fix | **Note:** @@ -1213,12 +1232,12 @@ vp pm ping --registry https://custom-registry.com - https://classic.yarnpkg.com/en/docs/cli/tag - https://yarnpkg.com/cli/npm/tag -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| -------------------------------- | ------------------- | ------------------- | --------------- | ------------------- | ----------------- | -| `vp pm dist-tag list ` | `npm dist-tag list` | `npm dist-tag list` | `yarn tag list` | `yarn npm tag list` | List tags | -| `vp pm dist-tag add ` | `npm dist-tag add` | `npm dist-tag add` | `yarn tag add` | `yarn npm tag add` | Add tag | -| `vp pm dist-tag rm ` | `npm dist-tag rm` | `npm dist-tag rm` | `yarn tag rm` | `yarn npm tag rm` | Remove tag | -| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| -------------------------------- | ------------------- | ------------------- | --------------- | ------------------- | ------------------- | ----------------- | +| `vp pm dist-tag list ` | `npm dist-tag list` | `npm dist-tag list` | `yarn tag list` | `yarn npm tag list` | `npm dist-tag list` | List tags | +| `vp pm dist-tag add ` | `npm dist-tag add` | `npm dist-tag add` | `yarn tag add` | `yarn npm tag add` | `npm dist-tag add` | Add tag | +| `vp pm dist-tag rm ` | `npm dist-tag rm` | `npm dist-tag rm` | `yarn tag rm` | `yarn npm tag rm` | `npm dist-tag rm` | Remove tag | +| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | **Note:** @@ -1233,11 +1252,11 @@ vp pm ping --registry https://custom-registry.com - https://docs.npmjs.com/cli/v11/commands/npm-deprecate -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ----------------------------- | --------------- | --------------- | --------------- | --------------- | ------------------- | -| `vp pm deprecate ` | `npm deprecate` | `npm deprecate` | `npm deprecate` | `npm deprecate` | Deprecate a package | -| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | -| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ----------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | ------------------- | +| `vp pm deprecate ` | `npm deprecate` | `npm deprecate` | `npm deprecate` | `npm deprecate` | `npm deprecate` | Deprecate a package | +| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | +| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | **Note:** @@ -1250,12 +1269,12 @@ vp pm ping --registry https://custom-registry.com - https://docs.npmjs.com/cli/v11/commands/npm-search -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ---------------------- | --------------- | --------------- | --------------- | --------------- | ------------------- | -| `vp pm search ` | `npm search` | `npm search` | `npm search` | `npm search` | Search for packages | -| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | -| `--long` | `--long` | `--long` | `--long` | `--long` | Extended info | -| `--searchlimit ` | `--searchlimit` | `--searchlimit` | `--searchlimit` | `--searchlimit` | Limit results | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ---------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | ------------------- | +| `vp pm search ` | `npm search` | `npm search` | `npm search` | `npm search` | `npm search` | Search for packages | +| `--json` | `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | +| `--long` | `--long` | `--long` | `--long` | `--long` | `--long` | Extended info | +| `--searchlimit ` | `--searchlimit` | `--searchlimit` | `--searchlimit` | `--searchlimit` | `--searchlimit` | Limit results | **Note:** @@ -1271,10 +1290,10 @@ vp pm ping --registry https://custom-registry.com - https://docs.npmjs.com/cli/v11/commands/npm-rebuild -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------------ | -------------------- | ------------------- | ---------- | ---------- | --------------------- | -| `vp pm rebuild` | `pnpm rebuild` | `npm rebuild` | N/A (warn) | N/A (warn) | Rebuild native addons | -| `vp pm rebuild ` | `pnpm rebuild ` | `npm rebuild ` | N/A (warn) | N/A (warn) | Rebuild specific pkgs | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------------ | -------------------- | ------------------- | ---------- | ---------- | ---------- | --------------------- | +| `vp pm rebuild` | `pnpm rebuild` | `npm rebuild` | N/A (warn) | N/A (warn) | N/A (warn) | Rebuild native addons | +| `vp pm rebuild ` | `pnpm rebuild ` | `npm rebuild ` | N/A (warn) | N/A (warn) | N/A (warn) | Rebuild specific pkgs | **Note:** @@ -1290,12 +1309,12 @@ vp pm ping --registry https://custom-registry.com - https://docs.npmjs.com/cli/v11/commands/npm-fund -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------ | ---------- | ---------- | ---------- | ---------- | --------------------- | -| `vp pm fund` | `npm fund` | `npm fund` | N/A (warn) | N/A (warn) | Show funding info | -| `vp pm fund ` | `npm fund` | `npm fund` | N/A (warn) | N/A (warn) | Fund for specific pkg | -| `--json` | `--json` | `--json` | N/A | N/A | JSON output | -| `--depth ` | `--depth` | `--depth` | N/A | N/A | Limit depth | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------ | ---------- | ---------- | ---------- | ---------- | ---------- | --------------------- | +| `vp pm fund` | `npm fund` | `npm fund` | N/A (warn) | N/A (warn) | N/A (warn) | Show funding info | +| `vp pm fund ` | `npm fund` | `npm fund` | N/A (warn) | N/A (warn) | N/A (warn) | Fund for specific pkg | +| `--json` | `--json` | `--json` | N/A | N/A | N/A | JSON output | +| `--depth ` | `--depth` | `--depth` | N/A | N/A | N/A | Limit depth | **Note:** @@ -1309,10 +1328,10 @@ vp pm ping --registry https://custom-registry.com - https://docs.npmjs.com/cli/v11/commands/npm-ping -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------ | ------------ | ------------ | ------------ | ------------ | ------------- | -| `vp pm ping` | `npm ping` | `npm ping` | `npm ping` | `npm ping` | Ping registry | -| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------- | +| `vp pm ping` | `npm ping` | `npm ping` | `npm ping` | `npm ping` | `npm ping` | Ping registry | +| `--registry ` | `--registry` | `--registry` | `--registry` | `--registry` | `--registry` | Registry URL | **Note:** @@ -2176,27 +2195,27 @@ Examples: ## Package Manager Compatibility -| Subcommand | pnpm | npm | yarn@1 | yarn@2+ | Notes | -| ---------- | ---------- | ------- | ---------- | ---------------- | --------------------------------------- | -| prune | ✅ Full | ✅ Full | ❌ N/A | ❌ N/A | npm uses --omit flags, yarn auto-prunes | -| pack | ✅ Full | ✅ Full | ✅ Full | ✅ Full | All supported | -| list/ls | ✅ Full | ✅ Full | ⚠️ Limited | ❌ N/A | yarn@1 no -r, yarn@2+ not supported | -| view | ✅ Full | ✅ Full | ⚠️ `info` | ⚠️ `info` | yarn uses different name | -| publish | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm publish` | yarn@2+ uses npm plugin | -| owner | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm owner` | yarn@2+ uses npm plugin | -| cache | ⚠️ `store` | ✅ Full | ✅ Full | ✅ Full | pnpm uses different command | -| config | ✅ Full | ✅ Full | ✅ Full | ⚠️ Different | yarn@2+ has different API | -| login | ✅ `npm` | ✅ Full | ✅ Full | ⚠️ `npm login` | pnpm delegates to npm | -| logout | ✅ `npm` | ✅ Full | ✅ Full | ⚠️ `npm logout` | pnpm delegates to npm | -| whoami | ✅ `npm` | ✅ Full | ❌ N/A | ⚠️ `npm whoami` | yarn@1 not supported | -| token | ✅ `npm` | ✅ Full | ❌ N/A | ❌ N/A | Always delegates to npm | -| audit | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm audit` | yarn@2+ uses npm plugin | -| dist-tag | ✅ `npm` | ✅ Full | ⚠️ `tag` | ⚠️ `npm tag` | Different command names | -| deprecate | ✅ `npm` | ✅ Full | ✅ `npm` | ✅ `npm` | Always delegates to npm | -| search | ✅ `npm` | ✅ Full | ✅ `npm` | ✅ `npm` | Always delegates to npm | -| rebuild | ✅ Full | ✅ Full | ❌ N/A | ❌ N/A | yarn does not support | -| fund | ✅ `npm` | ✅ Full | ❌ N/A | ❌ N/A | Always delegates to npm | -| ping | ✅ `npm` | ✅ Full | ✅ `npm` | ✅ `npm` | Always delegates to npm | +| Subcommand | pnpm | npm | yarn@1 | yarn@2+ | bun | Notes | +| ---------- | ---------- | ------- | ---------- | ---------------- | ------------------ | --------------------------------------- | +| prune | ✅ Full | ✅ Full | ❌ N/A | ❌ N/A | ❌ N/A | npm uses --omit flags, yarn auto-prunes | +| pack | ✅ Full | ✅ Full | ✅ Full | ✅ Full | ✅ `bun pm pack` | bun uses `bun pm pack` | +| list/ls | ✅ Full | ✅ Full | ⚠️ Limited | ❌ N/A | ⚠️ `bun pm ls` | bun has basic list support | +| view | ✅ Full | ✅ Full | ⚠️ `info` | ⚠️ `info` | ✅ `npm view` | delegates to npm | +| publish | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm publish` | ✅ `bun publish` | bun has native publish | +| owner | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm owner` | ✅ `npm owner` | delegates to npm | +| cache | ⚠️ `store` | ✅ Full | ✅ Full | ✅ Full | ⚠️ `bun pm cache` | bun uses `bun pm cache` | +| config | ✅ Full | ✅ Full | ✅ Full | ⚠️ Different | ❌ N/A | bun has no config command | +| login | ✅ `npm` | ✅ Full | ✅ Full | ⚠️ `npm login` | ✅ `npm login` | delegates to npm | +| logout | ✅ `npm` | ✅ Full | ✅ Full | ⚠️ `npm logout` | ✅ `npm logout` | delegates to npm | +| whoami | ✅ `npm` | ✅ Full | ❌ N/A | ⚠️ `npm whoami` | ✅ `bun pm whoami` | bun has native whoami | +| token | ✅ `npm` | ✅ Full | ❌ N/A | ❌ N/A | ❌ N/A | Always delegates to npm | +| audit | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm audit` | ❌ N/A | bun has no audit command | +| dist-tag | ✅ `npm` | ✅ Full | ⚠️ `tag` | ⚠️ `npm tag` | ✅ `npm dist-tag` | delegates to npm | +| deprecate | ✅ `npm` | ✅ Full | ✅ `npm` | ✅ `npm` | ✅ `npm deprecate` | Always delegates to npm | +| search | ✅ `npm` | ✅ Full | ✅ `npm` | ✅ `npm` | ✅ `npm search` | Always delegates to npm | +| rebuild | ✅ Full | ✅ Full | ❌ N/A | ❌ N/A | ❌ N/A | bun has no rebuild command | +| fund | ✅ `npm` | ✅ Full | ❌ N/A | ❌ N/A | ❌ N/A | Always delegates to npm | +| ping | ✅ `npm` | ✅ Full | ✅ `npm` | ✅ `npm` | ✅ `npm ping` | Always delegates to npm | ## Future Enhancements @@ -2308,7 +2327,7 @@ vp pm list --filter app ## Conclusion -This RFC proposes adding `vp pm` command group to provide unified access to package manager utilities across pnpm/npm/yarn. The design: +This RFC proposes adding `vp pm` command group to provide unified access to package manager utilities across pnpm/npm/yarn/bun. The design: - ✅ Pass-through architecture for maximum flexibility - ✅ Command name translation for common operations diff --git a/rfcs/update-package-command.md b/rfcs/update-package-command.md index a953e3c98f..f9ed8f54d0 100644 --- a/rfcs/update-package-command.md +++ b/rfcs/update-package-command.md @@ -2,7 +2,7 @@ ## Summary -Add `vp update` (alias: `vp up`) command that automatically adapts to the detected package manager (pnpm/yarn/npm) for updating packages to their latest versions within the specified semver range, with support for updating to absolute latest versions, workspace-aware operations, and interactive mode. +Add `vp update` (alias: `vp up`) command that automatically adapts to the detected package manager (pnpm/yarn/npm/bun) for updating packages to their latest versions within the specified semver range, with support for updating to absolute latest versions, workspace-aware operations, and interactive mode. ## Motivation @@ -106,21 +106,22 @@ vp update react --latest --no-save # Test latest version without saving - https://yarnpkg.com/cli/up (yarn@2+) - https://classic.yarnpkg.com/en/docs/cli/upgrade (yarn@1) - https://docs.npmjs.com/cli/v11/commands/npm-update - -| Vite+ Flag | pnpm | yarn@1 | yarn@2+ | npm | Description | -| ---------------------- | --------------------------- | -------------------- | ------------------------------------------- | ------------------------------ | ---------------------------------------------------------- | -| `[packages]` | `update [packages]` | `upgrade [packages]` | `up [packages]` | `update [packages]` | Update specific packages (or all if omitted) | -| `-L, --latest` | `--latest` / `-L` | `--latest` | N/A (default behavior) | N/A | Update to latest version (ignore semver range) | -| `-g, --global` | N/A | N/A | N/A | `--global` / `-g` | Update global packages | -| `-r, --recursive` | `-r, --recursive` | N/A | `--recursive` / `-R` | `--workspaces` | Update recursively in all workspace packages | -| `--filter ` | `--filter update` | N/A | `workspaces foreach --include up` | `update --workspace ` | Target specific workspace package(s) | -| `-w, --workspace-root` | `-w` | N/A | N/A | `--include-workspace-root` | Include workspace root | -| `-D, --dev` | `--dev` / `-D` | N/A | N/A | `--include=dev` | Update only devDependencies | -| `-P, --prod` | `--prod` / `-P` | N/A | N/A | `--include=prod` | Update only dependencies and optionalDependencies | -| `-i, --interactive` | `--interactive` / `-i` | N/A | `--interactive` / `-i` | N/A | Show outdated packages and choose which to update | -| `--no-optional` | `--no-optional` | N/A | N/A | `--no-optional` | Don't update optionalDependencies | -| `--no-save` | `--no-save` | N/A | N/A | `--no-save` | Update lockfile only, don't modify package.json | -| `--workspace` | `--workspace` | N/A | N/A | N/A | Only update if package exists in workspace (pnpm-specific) | +- https://bun.sh/docs/cli/update + +| Vite+ Flag | pnpm | yarn@1 | yarn@2+ | npm | bun | Description | +| ---------------------- | --------------------------- | -------------------- | ------------------------------------------- | ------------------------------ | --------------------------------------- | ---------------------------------------------------------- | +| `[packages]` | `update [packages]` | `upgrade [packages]` | `up [packages]` | `update [packages]` | `update [packages]` | Update specific packages (or all if omitted) | +| `-L, --latest` | `--latest` / `-L` | `--latest` | N/A (default behavior) | N/A | `--latest` | Update to latest version (ignore semver range) | +| `-g, --global` | N/A | N/A | N/A | `--global` / `-g` | N/A | Update global packages | +| `-r, --recursive` | `-r, --recursive` | N/A | `--recursive` / `-R` | `--workspaces` | N/A (updates all workspaces by default) | Update recursively in all workspace packages | +| `--filter ` | `--filter update` | N/A | `workspaces foreach --include up` | `update --workspace ` | N/A | Target specific workspace package(s) | +| `-w, --workspace-root` | `-w` | N/A | N/A | `--include-workspace-root` | N/A | Include workspace root | +| `-D, --dev` | `--dev` / `-D` | N/A | N/A | `--include=dev` | N/A | Update only devDependencies | +| `-P, --prod` | `--prod` / `-P` | N/A | N/A | `--include=prod` | N/A | Update only dependencies and optionalDependencies | +| `-i, --interactive` | `--interactive` / `-i` | N/A | `--interactive` / `-i` | N/A | `--interactive` / `-i` | Show outdated packages and choose which to update | +| `--no-optional` | `--no-optional` | N/A | N/A | `--no-optional` | N/A | Don't update optionalDependencies | +| `--no-save` | `--no-save` | N/A | N/A | `--no-save` | N/A | Update lockfile only, don't modify package.json | +| `--workspace` | `--workspace` | N/A | N/A | N/A | N/A | Only update if package exists in workspace (pnpm-specific) | **Note**: @@ -130,6 +131,8 @@ vp update react --latest --no-save # Test latest version without saving - npm doesn't support `--latest` flag, it always updates within semver range - `--no-optional` skips updating optional dependencies (pnpm/npm only) - `--no-save` updates lockfile without modifying package.json (pnpm/npm only) +- bun updates all workspaces by default; `--recursive` is not needed +- bun supports `--latest` and `--interactive` flags **Aliases:** @@ -648,6 +651,7 @@ vp update react --range # Updates within semver range - yarn@4.x - npm@10.x - npm@11.x [WIP] +- bun@1.x [WIP] ### Unit Tests @@ -777,17 +781,17 @@ vp update --no-optional ## Package Manager Compatibility -| Feature | pnpm | yarn@1 | yarn@2+ | npm | Notes | -| ---------------- | ------------------ | ---------------- | ---------------- | ---------------- | -------------------------- | -| Update command | `update` | `upgrade` | `up` | `update` | Different command names | -| Latest flag | `--latest` / `-L` | `--latest` | N/A (default) | ❌ Not supported | npm only updates in range | -| Interactive | `--interactive` | ❌ Not supported | `--interactive` | ❌ Not supported | Limited support | -| Workspace filter | `--filter` | ⚠️ Limited | ⚠️ Limited | `--workspace` | pnpm most flexible | -| Recursive | `--recursive` | ❌ Not supported | `--recursive` | `--workspaces` | Different flags | -| Dev/Prod filter | `--dev` / `--prod` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Global | `-g` | `global upgrade` | ❌ Not supported | `-g` | Use npm for global | -| No optional | `--no-optional` | ❌ Not supported | ❌ Not supported | `--no-optional` | Skip optional dependencies | -| No save | `--no-save` | ❌ Not supported | ❌ Not supported | `--no-save` | Lockfile only updates | +| Feature | pnpm | yarn@1 | yarn@2+ | npm | bun | Notes | +| ---------------- | ------------------ | ---------------- | ---------------- | ---------------- | ---------------------- | -------------------------- | +| Update command | `update` | `upgrade` | `up` | `update` | `update` | Different command names | +| Latest flag | `--latest` / `-L` | `--latest` | N/A (default) | ❌ Not supported | `--latest` | npm only updates in range | +| Interactive | `--interactive` | ❌ Not supported | `--interactive` | ❌ Not supported | `--interactive` / `-i` | Limited support | +| Workspace filter | `--filter` | ⚠️ Limited | ⚠️ Limited | `--workspace` | N/A | pnpm most flexible | +| Recursive | `--recursive` | ❌ Not supported | `--recursive` | `--workspaces` | N/A (default behavior) | bun updates all by default | +| Dev/Prod filter | `--dev` / `--prod` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Global | `-g` | `global upgrade` | ❌ Not supported | `-g` | ❌ Not supported | Use npm for global | +| No optional | `--no-optional` | ❌ Not supported | ❌ Not supported | `--no-optional` | ❌ Not supported | Skip optional dependencies | +| No save | `--no-save` | ❌ Not supported | ❌ Not supported | `--no-save` | ❌ Not supported | Lockfile only updates | ## Future Enhancements @@ -838,7 +842,7 @@ Continue? (Y/n) ## Conclusion -This RFC proposes adding `vp update` command to provide a unified interface for updating packages across pnpm/yarn/npm. The design: +This RFC proposes adding `vp update` command to provide a unified interface for updating packages across pnpm/yarn/npm/bun. The design: - ✅ Automatically adapts to detected package manager - ✅ Supports updating specific packages or all packages diff --git a/rfcs/why-package-command.md b/rfcs/why-package-command.md index b5153ce04d..28bc5c545f 100644 --- a/rfcs/why-package-command.md +++ b/rfcs/why-package-command.md @@ -2,7 +2,7 @@ ## Summary -Add `vp why` (alias: `vp explain`) command that automatically adapts to the detected package manager (pnpm/npm/yarn) for showing all packages that depend on a specified package. This helps developers understand dependency relationships, audit package usage, and debug dependency tree issues. +Add `vp why` (alias: `vp explain`) command that automatically adapts to the detected package manager (pnpm/npm/yarn/bun) for showing all packages that depend on a specified package. This helps developers understand dependency relationships, audit package usage, and debug dependency tree issues. ## Motivation @@ -138,22 +138,22 @@ vp why typescript -g # Check globally installed packages - https://yarnpkg.com/cli/why (yarn@2+) - Identifies why a package has been installed -| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | -| ------------------------- | ------------------------- | ----------------------- | ------------------- | ------------------------ | --------------------------------------------------------------- | -| `vp why ` | `pnpm why ` | `npm explain ` | `yarn why ` | `yarn why --peers` | Show why package is installed | -| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output format | -| `--long` | `--long` | N/A | N/A | N/A | Verbose output (pnpm only) | -| `--parseable` | `--parseable` | N/A | N/A | N/A | Parseable format (pnpm only) | -| `-r, --recursive` | `-r, --recursive` | N/A | N/A | `--recursive` | Check across all workspaces | -| `--filter ` | `--filter ` | `--workspace ` | N/A | N/A | Target specific workspace (pnpm/npm) | -| `-w, --workspace-root` | `-w` | N/A | N/A | N/A | Check in workspace root (pnpm-specific) | -| `-P, --prod` | `-P, --prod` | N/A | N/A | N/A | Only production dependencies (pnpm only) | -| `-D, --dev` | `-D, --dev` | N/A | N/A | N/A | Only dev dependencies (pnpm only) | -| `--depth ` | `--depth ` | N/A | N/A | N/A | Limit tree depth (pnpm only) | -| `--no-optional` | `--no-optional` | N/A | `--ignore-optional` | N/A | Exclude optional dependencies (pnpm only) | -| `-g, --global` | `-g, --global` | N/A | N/A | N/A | Check globally installed packages | -| `--exclude-peers` | `--exclude-peers` | N/A | N/A | Removes `--peers` flag | Exclude peer dependencies (yarn@2+ defaults to including peers) | -| `--find-by ` | `--find-by ` | N/A | N/A | N/A | Use finder function from .pnpmfile.cjs | +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | bun | Description | +| ------------------------- | ------------------------- | ----------------------- | ------------------- | ------------------------ | --------------- | --------------------------------------------------------------- | +| `vp why ` | `pnpm why ` | `npm explain ` | `yarn why ` | `yarn why --peers` | `bun why ` | Show why package is installed | +| `--json` | `--json` | `--json` | `--json` | `--json` | N/A | JSON output format | +| `--long` | `--long` | N/A | N/A | N/A | N/A | Verbose output (pnpm only) | +| `--parseable` | `--parseable` | N/A | N/A | N/A | N/A | Parseable format (pnpm only) | +| `-r, --recursive` | `-r, --recursive` | N/A | N/A | `--recursive` | N/A | Check across all workspaces | +| `--filter ` | `--filter ` | `--workspace ` | N/A | N/A | N/A | Target specific workspace (pnpm/npm) | +| `-w, --workspace-root` | `-w` | N/A | N/A | N/A | N/A | Check in workspace root (pnpm-specific) | +| `-P, --prod` | `-P, --prod` | N/A | N/A | N/A | N/A | Only production dependencies (pnpm only) | +| `-D, --dev` | `-D, --dev` | N/A | N/A | N/A | N/A | Only dev dependencies (pnpm only) | +| `--depth ` | `--depth ` | N/A | N/A | N/A | N/A | Limit tree depth (pnpm only) | +| `--no-optional` | `--no-optional` | N/A | `--ignore-optional` | N/A | N/A | Exclude optional dependencies (pnpm only) | +| `-g, --global` | `-g, --global` | N/A | N/A | N/A | N/A | Check globally installed packages | +| `--exclude-peers` | `--exclude-peers` | N/A | N/A | Removes `--peers` flag | N/A | Exclude peer dependencies (yarn@2+ defaults to including peers) | +| `--find-by ` | `--find-by ` | N/A | N/A | N/A | N/A | Use finder function from .pnpmfile.cjs | **Note:** @@ -162,6 +162,7 @@ vp why typescript -g # Check globally installed packages - yarn has `why` command in both v1 and v2+, but different output formats, only supports single package - pnpm has the most comprehensive filtering and output options - npm has simpler output focused on the dependency path +- bun uses `bun why ` as a direct subcommand (not `bun pm why`); it provides a tree visualization of dependency relationships **Aliases:** @@ -989,6 +990,7 @@ vp why react --json # On yarn - yarn@4.x - npm@10.x - npm@11.x +- bun@1.x [WIP] ### Unit Tests @@ -1253,19 +1255,20 @@ vp why package --prod --json ## Package Manager Compatibility -| Feature | pnpm | npm | yarn@1 | yarn@2+ | Notes | -| ---------------- | ----------------- | ---------------- | ---------------- | ---------------- | ----------------------- | -| Basic command | `why` | `explain` | `why` | `why` | npm uses different name | -| Multiple pkgs | ✅ Supported | ✅ Supported | ❌ Single only | ❌ Single only | pnpm and npm | -| Glob patterns | ✅ Supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| JSON output | ✅ `--json` | ✅ `--json` | ❌ Not supported | ❌ Not supported | pnpm and npm only | -| Long output | ✅ `--long` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Parseable | ✅ `--parseable` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Recursive | ✅ `-r` | ❌ Not supported | ❌ Not supported | ✅ `--recursive` | pnpm and yarn@2+ | -| Workspace filter | ✅ `--filter` | ✅ `--workspace` | ❌ Not supported | ❌ Not supported | pnpm and npm | -| Dep type filter | ✅ `--prod/--dev` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Depth limit | ✅ `--depth` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | -| Global check | ✅ `-g` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Feature | pnpm | npm | yarn@1 | yarn@2+ | bun | Notes | +| ---------------- | ----------------- | ---------------- | ---------------- | ---------------- | ---------------- | ----------------------- | +| Basic command | `why` | `explain` | `why` | `why` | `why` | npm uses different name | +| Multiple pkgs | ✅ Supported | ✅ Supported | ❌ Single only | ❌ Single only | ❌ Single only | pnpm and npm | +| Glob patterns | ✅ Supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| JSON output | ✅ `--json` | ✅ `--json` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm and npm only | +| Long output | ✅ `--long` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Parseable | ✅ `--parseable` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Recursive | ✅ `-r` | ❌ Not supported | ❌ Not supported | ✅ `--recursive` | ❌ Not supported | pnpm and yarn@2+ | +| Workspace filter | ✅ `--filter` | ✅ `--workspace` | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm and npm | +| Dep type filter | ✅ `--prod/--dev` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Depth limit | ✅ `--depth` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Global check | ✅ `-g` | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Tree view | ❌ Not supported | ❌ Not supported | ❌ Not supported | ❌ Not supported | ✅ Built-in | bun shows tree view | ## Future Enhancements @@ -1365,7 +1368,7 @@ Total impact: 23.3MB ## Conclusion -This RFC proposes adding `vp why` command to provide a unified interface for understanding dependency relationships across pnpm/npm/yarn. The design: +This RFC proposes adding `vp why` command to provide a unified interface for understanding dependency relationships across pnpm/npm/yarn/bun. The design: - ✅ Automatically adapts to detected package manager - ✅ Supports multiple packages (pnpm) with graceful degradation