Description
The current vp install -g, and vp update -g will just install / reinstall packages one by one when multiple packages provided. It takes a lot of time, as well as multiple node-resolving and processes creating are also not good for performance.
We should follow the similar behavior like npm and pnpm, process the download in parallel instead.
Suggested solution
We have two ways to improve it, vp update should follow similar pipeline as well.
Delegate installing to npm
The current vp essentially spawns processes running npm install <pkg> for every single package provided. But actually npm itself can process parallel installing, we can just spawn one single npm process and create shim unitedly after npm's work gets done.
For example:
Before: vp install -g oxlint oxfmt -> npm install oxlint && npm install oxfmt
After: vp install -g oxlint oxfmt -> npm install oxlint oxfmt
🤖 A flow chart generated by Claude
# How current vp works
$ vp install -g pkg1 pkg2 pkg3
│
└─ cli.rs:1622 Commands::Install { global: true, packages: Some([pkg1, pkg2, pkg3]), … }
│
└─ cli.rs:1655-1666 if global { for package in &pkgs { … } } ← SERIAL LOOP
│ (no join_all,
│ no concurrency,
│ first failure aborts the rest)
│
├─ iter 1 ─ global_install::install("pkg1", node, force).await
├─ iter 2 ─ global_install::install("pkg2", node, force).await ← waits for iter 1
└─ iter 3 ─ global_install::install("pkg3", node, force).await ← waits for iter 2
Per-iteration (global_install.rs:28-209)
─────────────────────────────────────────
install(package_spec, node_version, force)
│
├─ 1. Resolve Node version
│ ├─ if --node X → resolve_version_alias(X)
│ └─ else → resolve_version(cwd) (reads engines / .nvmrc)
│
├─ 2. Ensure Node runtime installed
│ └─ vite_js_runtime::download_runtime(Node, version)
│
├─ 3. Stage on disk
│ ├─ tmp_dir = ~/.vite-plus/tmp
│ ├─ staging = tmp/packages/<pkg> ← per-package dir, no cross-pkg collision
│ └─ rm staging if leftover, mkdir -p staging
│
├─ 4. Run npm
│ └─ npm install -g --no-fund <pkg_spec>
│ env: npm_config_prefix=<staging> ← isolates each install
│ PATH=<node_bin_dir>:$PATH
│ stdout/stderr piped (shown only on failure)
│
├─ 5. Read package.json from staging
│ ├─ installed_version
│ └─ extract_binaries() → [BinaryInfo { name, path }, …]
│ ├─ "bin": "x" → one bin, name = pkg basename
│ └─ "bin": { … } → N bins
│ └─ is_javascript_binary() → classify each (.js/.mjs/.cjs OR shebang `node`)
│
├─ 5b. CONFLICT CHECK (the only guard)
│ │
│ for bin in bins {
│ BinConfig::load(bin) → Some(cfg) if another package already owns this bin
│ if cfg.package != self → push (bin, cfg.package) to conflicts
│ }
│ │
│ ├─ no conflicts → continue
│ ├─ conflicts && !force → rm staging, return Error::BinaryConflict (only first reported)
│ └─ conflicts && force → for each owner pkg:
│ output "Uninstalling <owner> (conflicts with <new>)"
│ Box::pin(uninstall(owner, false)).await ← recursive
│
│ ⚠ load+save is NOT atomic / not locked
│ → safe today (serial loop) but breaks the moment iter 1 & iter 2 run in parallel
│
├─ 6. Commit to final location
│ ├─ final = ~/.vite-plus/packages/<pkg>
│ ├─ rm final if exists
│ ├─ mkdir -p final.parent (handles @scope/pkg)
│ └─ rename(staging → final)
│
├─ 7. Save package metadata
│ └─ PackageMetadata { name, version, node_version, bins, js_bins, "npm" }
│ .save() → ~/.vite-plus/metadata/<pkg>.json
│
├─ 8. Create shims + per-bin configs
│ │
│ bin_dir = ~/.vite-plus/bin
│ for bin in bins {
│ create_package_shim(bin_dir, bin, pkg)
│ ├─ if bin ∈ CORE_SHIMS=["node","npm","npx","vp"] → warn + skip
│ ├─ unix → symlink bin_dir/<bin> → ../current/bin/vp
│ └─ windows → copy trampoline.exe to bin_dir/<bin>.exe
│ + cleanup_legacy_windows_shim()
│ BinConfig { bin, package, version, node_version }.save()
│ → ~/.vite-plus/bins/<bin>.json ← used by shim::dispatch at runtime
│ }
│
└─ 9. Print "Installed <pkg> v<ver>" / "Binaries: …"
# Ideal approach
$ vp install -g pkg1 pkg2 pkg3
│
└─ cli.rs Commands::Install { global: true, packages: Some(pkgs), … }
│
└─ global_install::install_many(pkgs, node, force).await ← single entry, batch-aware
│
│ ───────────── PHASE A · ONCE, UP FRONT ─────────────
│
├─ A1. Resolve Node version (ONE TIME)
│ ├─ if --node X → resolve_version_alias(X)
│ └─ else → resolve_version(cwd)
│
├─ A2. Ensure Node runtime (ONE TIME)
│ └─ download_runtime(Node, version)
│ → node_bin_dir, npm_path
│
├─ A3. Parse + dedupe specs
│ ├─ parse_package_spec for each → [(name, version_spec)]
│ ├─ dedupe by name (keep last spec, warn on conflict)
│ └─ (early) intra-batch sanity:
│ same name twice with diff versions → error before touching disk
│
└─ A4. Prepare a single shared staging root
└─ batch_staging = tmp/install-<uuid>/ ← one prefix for the whole batch
(gets cleaned on success or failure)
│ ───────────── PHASE B · ONE NPM CALL ──────────
│ (npm itself parallelises downloads & extraction)
│
└─ B1. npm install -g --no-fund pkg1 pkg2 pkg3
env: npm_config_prefix = <batch_staging>
PATH = <node_bin_dir>:$PATH
stdio: streamed live (or piped + shown on
│
on non-zero exit → rm batch_staging, surfa
(no partial commit, no shim writes)
│ ───────────── PHASE C · POST-INSTALL, PER PACKAGE ─────────────
│ (cheap CPU/FS work; can be a bound
│ but the COMMIT step in C4 must hold a global lock)
│
├─ C1. Discover what npm actually installed
│ └─ scan batch_staging/lib/node_modules (ha
│ → [{name, dir, package_json}] (source of truth, not the input list)
│
├─ C2. For each installed package — read metadata
│ ├─ installed_version ← package_json.version
│ ├─ bins ← extract_binaries(package_json)
│ └─ js_bins ← is_javascript_binary
│
├─ C3. Pre-flight conflict check (whole batch, bef
│ │
│ ├─ build new_owner: HashMap<bin_name, pkg>
│ ├─ intra-batch conflict:
│ │ two pkgs in the batch claim the same
│ │ abort with batch_staging untouched on disk side
│ ├─ existing-owner conflict:
│ │ for bin in new_owner.keys() {
│ │ BinConfig::load(bin) → Some(cfg) wh
│ │ }
│ │ ├─ none → proceed
│ │ ├─ any && !force → Error::BinaryConflict (list ALL, not just first)
│ │ └─ any && force → record set of own
│ │
│ └─ also flag: bin ∈ CORE_SHIMS → warn now,
│
├─ C4. Write metadata + shims (still under the lock, or per-pkg after rename)
│ │
│ for pkg in batch:
│ ├─ PackageMetadata{…}.save() → metadat
│ └─ for bin in pkg.bins:
│ ├─ if bin ∈ CORE_SHIMS → warn +
│ ├─ unix → symlink bin_dir/<bin> → ../current/bin/vp
│ ├─ windows → copy trampoline.exe + c
│ └─ BinConfig{ bin, pkg, version, node_version }.save()
│
└─ C6. release lock → print summary
├─ "Installed pkg1 v… (bins: a, b)"
├─ "Installed pkg2 v… (bins: c)"
└─ "Installed pkg3 v… (no bins)"
Make Vite+ itself Control Parallel Installing
It still spawns multiple npm processes, but let them run at the same time, controlled unitedly by Vite+.
I think the first approach is easy to implement, I personally prefer the first approach as well. By the way, no matter which way to use, we need to adjust terminal output format for it as it's currently mainly designed for single package installing.
Validations
Description
The current
vp install -g, andvp update -gwill just install / reinstall packages one by one when multiple packages provided. It takes a lot of time, as well as multiple node-resolving and processes creating are also not good for performance.We should follow the similar behavior like npm and pnpm, process the download in parallel instead.
Suggested solution
We have two ways to improve it,
vp updateshould follow similar pipeline as well.Delegate installing to
npmThe current vp essentially spawns processes running
npm install <pkg>for every single package provided. But actuallynpmitself can process parallel installing, we can just spawn one singlenpmprocess and create shim unitedly after npm's work gets done.For example:
Before:
vp install -g oxlint oxfmt->npm install oxlint && npm install oxfmtAfter:
vp install -g oxlint oxfmt->npm install oxlint oxfmt🤖 A flow chart generated by Claude
Make Vite+ itself Control Parallel Installing
It still spawns multiple npm processes, but let them run at the same time, controlled unitedly by Vite+.
I think the first approach is easy to implement, I personally prefer the first approach as well. By the way, no matter which way to use, we need to adjust terminal output format for it as it's currently mainly designed for single package installing.
Validations