Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3b74c7f
feat(dev): reapply clean bootstrap workflow
Salnika Apr 3, 2026
d29d41d
fix(cli): resolve local path package names
Salnika Apr 3, 2026
366d915
test(cli): update env install snapshots
Salnika Apr 3, 2026
4e5fce2
chore(dev): align local cli vite type build
Salnika May 21, 2026
cfc8187
fix(global): materialize local package installs
Salnika May 21, 2026
785ad0d
test(cli): update local package snapshots
Salnika May 21, 2026
ccb3b44
fix(cli): merge upstream bootstrap and parser updates
Salnika May 26, 2026
8effcd5
merge upstream/main into fix/bootrap-cli
Salnika May 26, 2026
42fa42a
fix(ci): satisfy shear and check
Salnika May 26, 2026
3674d44
fix(cli): handle pnpm_execpath binaries
Salnika May 26, 2026
52cce68
feat(dev): reapply clean bootstrap workflow
Salnika Apr 3, 2026
cef37e7
fix(cli): resolve local path package names
Salnika Apr 3, 2026
68c9353
test(cli): update env install snapshots
Salnika Apr 3, 2026
97971d5
chore(dev): align local cli vite type build
Salnika May 21, 2026
d910a37
fix(global): materialize local package installs
Salnika May 21, 2026
f242baa
test(cli): update local package snapshots
Salnika May 21, 2026
adcb7b1
fix(cli): merge upstream bootstrap and parser updates
Salnika May 26, 2026
a19668b
merge upstream/main into fix/bootrap-cli
Salnika May 26, 2026
91a6846
fix(ci): satisfy shear and check
Salnika May 26, 2026
cdc2df3
fix(cli): handle pnpm_execpath binaries
Salnika May 26, 2026
57b9925
fix(ci): bump varlet fixture
Salnika May 26, 2026
7cddfe9
Merge remote-tracking branch 'upstream/main' into signed-final
Salnika May 28, 2026
c374a19
Merge branch 'signed-final' into fix/bootrap-cli
Salnika May 28, 2026
7bb921b
Fix local bootstrap after upstream sync
Salnika May 28, 2026
aaef2d3
Merge remote-tracking branch 'upstream/main' into fix/bootrap-cli
Salnika May 28, 2026
a4ff066
Fix mock registry proxy bypass in snap tests
Salnika May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install cargo-binstall
```

Initial setup to install dependencies for Vite+:
Initial setup to prepare the repo for local development:

```
just init
pnpm install:dev
```

### Windows
Expand All @@ -37,10 +37,10 @@ Install Rust & Cargo from [rustup.rs](https://rustup.rs/), then install `cargo-b
cargo install cargo-binstall
```

Initial setup to install dependencies for Vite+:
Initial setup to prepare the repo for local development:

```powershell
just init
pnpm install:dev
```

**Note:** Run commands in PowerShell or Windows Terminal. Some commands may require elevated permissions.
Expand All @@ -53,21 +53,38 @@ To create a release build of Vite+ and all upstream dependencies, run:
just build
```

## Local CLI workflow

```
pnpm bootstrap:dev
pnpm test
```

This prepares the local `rolldown/` and `vite/` checkouts, installs dependencies, builds the repo-local CLI artifacts, and runs tests without reading `~/.vite-plus`.

If you only want to prepare the repo after cloning it, run:

```
pnpm install:dev
```

If you prefer the existing Just-based setup, `just init` now delegates to the same repo-local install flow.

## Install the Vite+ Global CLI from source code

```
pnpm bootstrap-cli
vp --version
```

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

## Workflow for build and test

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

```
pnpm bootstrap-cli && pnpm test && git status
pnpm build:cli && pnpm test && git status
```

## Running Snap Tests
Expand All @@ -87,6 +104,8 @@ pnpm -F vite-plus snap-test-global
pnpm -F vite-plus snap-test-global <name-filter>
```

Global CLI snap tests use the repo-local debug binary and `packages/cli/dist`; they do not require `~/.vite-plus/bin`.

Snap tests auto-generate `snap.txt` files. Check `git diff` to verify output changes are correct.

## Verified Commits
Expand Down
125 changes: 124 additions & 1 deletion crates/vite_global_cli/src/commands/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,14 @@ async fn install_one(

// 4. Run npm install with prefix set to staging directory
// Pipe stdout/stderr so npm output is hidden on success, shown on failure
let mut install_args = vec!["install", "-g", "--no-fund"];
if is_local_package_spec(package_spec) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes unrelated to the PR content?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, --install-links is required for local installs to work correctly

But I had to do some other changes to fix the test/CI and could use a little help if something isn't right 😅

install_args.push("--install-links");
}
install_args.push(package_spec);

let output = Command::new(npm_path.as_path())
.args(["install", "-g", "--no-fund", &package_spec])
.args(install_args)
.env("npm_config_prefix", staging_dir.as_path())
.env("PATH", format_path_prepended(node_bin_dir.as_path()))
.stdout(Stdio::piped())
Expand Down Expand Up @@ -463,6 +469,86 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> {
Ok(())
}

/// Resolve the version currently published for a package spec.
///
/// `package_spec` may be a bare package name (`typescript`) or include a
/// version/tag (`typescript@beta`, `@scope/pkg@1.0.0`). The command returns the
/// version that npm resolves for that spec.
#[expect(dead_code)]
pub(crate) async fn latest_package_version(package_spec: &str) -> Result<String, Error> {
// Resolve from current directory
let node_version = {
let cwd = match current_dir() {
Ok(cwd) => cwd,
Err(error) => {
let error =
Error::ConfigError(format!("Cannot get current directory: {}", error).into());
return Err(error);
}
};
let resolution = match resolve_version(&cwd).await {
Ok(resolution) => resolution,
Err(error) => return Err(error),
};
resolution.version
};

// Ensure Node.js is installed
let runtime = match vite_js_runtime::download_runtime(
vite_js_runtime::JsRuntimeType::Node,
&node_version,
)
.await
{
Ok(runtime) => runtime,
Err(error) => {
let error = Error::RuntimeDownload(error);
return Err(error);
}
};

let node_bin_dir = runtime.get_bin_prefix();
let npm_path =
if cfg!(windows) { node_bin_dir.join("npm.cmd") } else { node_bin_dir.join("npm") };

let output = Command::new(npm_path.as_path())
.args(["view", package_spec, "version", "--json"])
.env("PATH", format_path_prepended(node_bin_dir.as_path()))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(Error::ConfigError(
format!("npm view failed for {package_spec}: {stderr}").into(),
));
}

parse_npm_view_version(&output.stdout)
}

#[expect(dead_code)]
fn parse_npm_view_version(stdout: &[u8]) -> Result<String, Error> {
let raw = String::from_utf8_lossy(stdout);
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(Error::ConfigError("npm view returned an empty version".into()));
}

match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(serde_json::Value::String(version)) => Ok(version),
Ok(serde_json::Value::Array(versions)) => versions
.iter()
.rev()
.find_map(|version| version.as_str())
.map(str::to_string)
.ok_or_else(|| Error::ConfigError("npm view returned an empty version list".into())),
_ => Ok(trimmed.to_string()),
}
}

/// Binary info extracted from package.json.
struct BinaryInfo {
/// Binary name (the command users will run)
Expand Down Expand Up @@ -646,6 +732,22 @@ mod tests {
use super::*;
use crate::commands::global::is_local_package_spec;

struct CurrentDirGuard(std::path::PathBuf);

impl CurrentDirGuard {
fn change_to(path: &std::path::Path) -> Self {
let previous = std::env::current_dir().unwrap();
std::env::set_current_dir(path).unwrap();
Self(previous)
}
}

impl Drop for CurrentDirGuard {
fn drop(&mut self) {
std::env::set_current_dir(&self.0).unwrap();
}
}

/// RAII guard that sets `VP_TRAMPOLINE_PATH` to a fake binary on creation
/// and clears it on drop. Ensures cleanup even on test panics.
#[cfg(windows)]
Expand Down Expand Up @@ -922,6 +1024,27 @@ mod tests {
assert_eq!(version, Some("20.0.0".to_string()));
}

#[test]
#[serial_test::serial]
fn test_parse_package_spec_local_path_uses_package_name() {
use tempfile::TempDir;

let temp_dir = TempDir::new().unwrap();
let _cwd_guard = CurrentDirGuard::change_to(temp_dir.path());
let package_dir = temp_dir.path().join("fixture-pkg");

std::fs::create_dir_all(&package_dir).unwrap();
std::fs::write(
package_dir.join("package.json"),
r#"{ "name": "resolved-local-package", "version": "1.0.0" }"#,
)
.unwrap();

let (name, version) = parse_package_spec("./fixture-pkg").unwrap();
assert_eq!(name, "resolved-local-package");
assert_eq!(version, None);
}

#[test]
fn test_is_javascript_binary_with_js_extension() {
use tempfile::TempDir;
Expand Down
2 changes: 2 additions & 0 deletions crates/vite_global_cli/src/commands/global/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ pub(crate) fn is_local_package_spec(spec: &str) -> bool {
|| spec == ".."
|| spec.starts_with("./")
|| spec.starts_with("../")
|| spec.starts_with(".\\")
|| spec.starts_with("..\\")
|| spec.starts_with('/')
|| spec.starts_with("file:")
|| (cfg!(windows)
Expand Down
24 changes: 15 additions & 9 deletions crates/vite_global_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,17 @@ mod tests {
v.iter().map(|s| s.to_string()).collect()
}

fn try_parse_args_from_test(args: Vec<String>) -> Result<crate::cli::Args, clap::Error> {
let handle = std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(move || try_parse_args_from(args))
.expect("Expected parser test thread to spawn");
match handle.join() {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
}
}

#[test]
fn normalize_args_rewrites_vp_node_to_env_exec_node() {
let input = s(&["vp", "node", "script.js", "foo", "--flag"]);
Expand Down Expand Up @@ -485,22 +496,17 @@ mod tests {

#[test]
fn unknown_argument_detected_without_pass_as_value_hint() {
let error = try_parse_args_from(["vp".to_string(), "--cache".to_string()])
.expect_err("Expected parse error");
let error =
try_parse_args_from_test(s(&["vp", "--cache"])).expect_err("Expected parse error");
assert_eq!(error.kind(), ErrorKind::UnknownArgument);
assert_eq!(extract_unknown_argument(&error).as_deref(), Some("--cache"));
assert!(!has_pass_as_value_suggestion(&error));
}

#[test]
fn unknown_argument_detected_with_pass_as_value_hint() {
let error = try_parse_args_from([
"vp".to_string(),
"remove".to_string(),
"--stream".to_string(),
"foo".to_string(),
])
.expect_err("Expected parse error");
let error = try_parse_args_from_test(s(&["vp", "remove", "--stream", "foo"]))
.expect_err("Expected parse error");
assert_eq!(error.kind(), ErrorKind::UnknownArgument);
assert_eq!(extract_unknown_argument(&error).as_deref(), Some("--stream"));
assert!(has_pass_as_value_suggestion(&error));
Expand Down
12 changes: 11 additions & 1 deletion crates/vite_global_cli/src/tips/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,17 @@ pub fn tip_context_from_command(command: &str) -> TipContext {
// Split simulates what the OS does with command line args
let args: Vec<String> = command.split_whitespace().map(String::from).collect();

let (exit_code, clap_error) = match crate::try_parse_args_from(args.iter().cloned()) {
let parse_args = args.clone();
let handle = std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(move || crate::try_parse_args_from(parse_args))
.expect("Expected parser test thread to spawn");
let parse_result = match handle.join() {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
};

let (exit_code, clap_error) = match parse_result {
Ok(_) => (0, None),
Err(e) => (e.exit_code(), Some(e)),
};
Expand Down
9 changes: 8 additions & 1 deletion crates/vite_global_cli/src/upgrade_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,14 @@ mod tests {
fn parse_args(args: &[&str]) -> crate::cli::Args {
let full: Vec<String> =
std::iter::once("vp").chain(args.iter().copied()).map(String::from).collect();
crate::try_parse_args_from(full).unwrap()
let handle = std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(move || crate::try_parse_args_from(full))
.expect("Expected parser test thread to spawn");
match handle.join() {
Ok(result) => result.unwrap(),
Err(payload) => std::panic::resume_unwind(payload),
}
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion ecosystem-ci/repo.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"varlet": {
"repository": "https://github.com/varletjs/varlet.git",
"branch": "dev",
"hash": "83f6c6a418ab9319e07d719d86d4fa952f99e266",
"hash": "175f02cc0e4978d211a57b13d9438b1ac44b50d6",
"forceFreshMigration": true
}
}
3 changes: 1 addition & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ _clean_dist:

init: _clean_dist _fix_symlinks
cargo binstall watchexec-cli cargo-insta typos-cli cargo-shear dprint taplo-cli -y
node packages/tools/src/index.ts sync-remote
pnpm install
pnpm install:dev
pnpm -C docs install

[unix]
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
"type": "module",
"scripts": {
"build": "pnpm -F rolldown build-binding:release && pnpm -F rolldown build-node && pnpm -F vite build-types && pnpm -F @voidzero-dev/* -F vite-plus build",
"bootstrap-cli": "pnpm build && cargo build -p vite_global_cli -p vite_trampoline --release && pnpm install-global-cli",
"install:dev": "node scripts/setup-local-dev.mjs",
"bootstrap:dev": "pnpm install:dev && pnpm build:cli",
"build:cli": "tool build-local-cli",
"bootstrap-cli": "pnpm install:dev && tool build-local-cli --release-rust && pnpm install-global-cli",
"bootstrap-cli:ci": "pnpm install-global-cli",
"install-global-cli": "tool install-global-cli",
"tsgo": "tsgo -b tsconfig.json",
"lint": "vp lint --type-aware --type-check --threads 4",
"test": "vp test run && pnpm -r snap-test",
"test": "pnpm build:cli && pnpm -F vite-plus test && pnpm -F vite-plus snap-test",
"fmt": "vp fmt",
"test:unit": "vp test run",
"test:unit": "pnpm build:cli && pnpm -F vite-plus test",
"docs:dev": "pnpm -C docs dev",
"docs:build": "pnpm -C docs build",
"docs:update-trusted-stack-stats": "pnpm -C docs update-trusted-stack-stats",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,9 @@
"build-native": "oxnode -C dev ./build.ts --skip-ts",
"snap-test": "pnpm snap-test-local && pnpm snap-test-global",
"snap-test-local": "tool snap-test",
"snap-test-global": "tool snap-test --dir snap-tests-global --bin-dir ~/.vite-plus/bin",
"snap-test-global": "tool snap-test-global-local",
"publish-native": "node ./publish-native-addons.ts",
"test": "vitest run"
"test": "tool local-cli test run"
},
"dependencies": {
"@oxc-project/types": "catalog:",
Expand Down
Empty file.
Empty file.
Loading
Loading