Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 71 additions & 0 deletions .claude/skills/spawn-process/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: spawn-process
description: Guide for writing subprocess execution code using the vite_command crate
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
---

# Add Subprocess Execution Code

When writing Rust code that needs to spawn subprocesses (resolve binaries, build commands, execute programs), always use the `vite_command` crate. Never use `which`, `tokio::process::Command::new`, or `std::process::Command::new` directly.

## Available APIs

### `vite_command::resolve_bin(name, path_env, cwd)` — Resolve a binary name to an absolute path

Handles PATHEXT (`.cmd`/`.bat`) on Windows. Pass `None` for `path_env` to search the current process PATH.

```rust
// Resolve using current PATH
let bin = vite_command::resolve_bin("node", None, &cwd)?;

// Resolve using a custom PATH
let custom_path = std::ffi::OsString::from(&path_env_str);
let bin = vite_command::resolve_bin("eslint", Some(&custom_path), &cwd)?;
```

### `vite_command::build_command(bin_path, cwd)` — Build a command for a pre-resolved binary

Returns `tokio::process::Command` with cwd, inherited stdio, and `fix_stdio_streams` on Unix already configured. Add args, envs, or override stdio as needed.

```rust
let bin = vite_command::resolve_bin("eslint", None, &cwd)?;
let mut cmd = vite_command::build_command(&bin, &cwd);
cmd.args(&[".", "--fix"]);
cmd.env("NODE_ENV", "production");
let mut child = cmd.spawn()?;
let status = child.wait().await?;
```

### `vite_command::build_shell_command(shell_cmd, cwd)` — Build a shell command

Uses `/bin/sh -c` on Unix, `cmd.exe /C` on Windows. Same stdio and `fix_stdio_streams` setup as `build_command`.

```rust
let mut cmd = vite_command::build_shell_command("echo hello && ls", &cwd);
let mut child = cmd.spawn()?;
let status = child.wait().await?;
```

### `vite_command::run_command(bin_name, args, envs, cwd)` — Resolve + build + run in one call

Combines resolve_bin, build_command, and status().await. The `envs` HashMap must include `"PATH"` if you want custom PATH resolution.

```rust
let envs = HashMap::from([("PATH".to_string(), path_value)]);
let status = vite_command::run_command("node", &["--version"], &envs, &cwd).await?;
```

## Dependency Setup

Add `vite_command` to the crate's `Cargo.toml`:

```toml
[dependencies]
vite_command = { workspace = true }
```

Do NOT add `which` as a direct dependency — binary resolution goes through `vite_command::resolve_bin`.

## Exception

`crates/vite_global_cli/src/shim/exec.rs` uses synchronous `std::process::Command` with Unix `exec()` for process replacement. This is the only place that bypasses `vite_command`.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 75 additions & 25 deletions crates/vite_command/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{
use fspy::AccessMode;
use tokio::process::Command;
use vite_error::Error;
use vite_path::{AbsolutePath, RelativePathBuf};
use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf};

/// Result of running a command with fspy tracking.
#[derive(Debug)]
Expand All @@ -18,6 +18,76 @@ pub struct FspyCommandResult {
pub path_accesses: HashMap<RelativePathBuf, AccessMode>,
}

/// Resolve a binary name to a full path using the `which` crate.
/// Handles PATHEXT (`.cmd`/`.bat`) resolution natively on Windows.
///
/// If `path_env` is `None`, searches the process's current `PATH`.
pub fn resolve_bin(
bin_name: &str,
path_env: Option<&OsStr>,
cwd: impl AsRef<AbsolutePath>,
) -> Result<AbsolutePathBuf, Error> {
let current_path;
let path_env = match path_env {
Some(p) => p,
None => {
current_path = std::env::var_os("PATH").unwrap_or_default();
&current_path
}
};
let path = which::which_in(bin_name, Some(path_env), cwd.as_ref())
.map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?;
AbsolutePathBuf::new(path).ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into()))
}

/// Build a `tokio::process::Command` for a pre-resolved binary path.
/// Sets inherited stdio and `fix_stdio_streams` (Unix pre_exec).
/// Callers can further customize (add args, envs, override stdio, etc.).
pub fn build_command(bin_path: &AbsolutePath, cwd: &AbsolutePath) -> Command {
let mut cmd = Command::new(bin_path.as_path());
cmd.current_dir(cwd).stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit());

#[cfg(unix)]
unsafe {
cmd.pre_exec(|| {
fix_stdio_streams();
Ok(())
});
}

cmd
}

/// Build a `tokio::process::Command` for shell execution.
/// Uses `/bin/sh -c` on Unix, `cmd.exe /C` on Windows.
pub fn build_shell_command(shell_cmd: &str, cwd: &AbsolutePath) -> Command {
#[cfg(unix)]
let mut cmd = {
let mut cmd = Command::new("/bin/sh");
cmd.arg("-c").arg(shell_cmd);
cmd
};

#[cfg(windows)]
let mut cmd = {
let mut cmd = Command::new("cmd.exe");
cmd.arg("/C").arg(shell_cmd);
cmd
};

cmd.current_dir(cwd).stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit());

#[cfg(unix)]
unsafe {
cmd.pre_exec(|| {
fix_stdio_streams();
Ok(())
});
}

cmd
}

/// Run a command with the given bin name, arguments, environment variables, and current working directory.
///
/// # Arguments
Expand All @@ -40,31 +110,11 @@ where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
// Resolve the command path using which crate
// If PATH is provided in envs, use which_in to search in custom paths
// Otherwise, use which to search in system PATH
let paths = envs.get("PATH");
let cwd = cwd.as_ref();
let bin_path = which::which_in(bin_name, paths, cwd)
.map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?;

let mut cmd = Command::new(bin_path);
cmd.args(args)
.envs(envs)
.current_dir(cwd)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());

// fix stdio streams on unix
#[cfg(unix)]
unsafe {
cmd.pre_exec(|| {
fix_stdio_streams();
Ok(())
});
}

let paths = envs.get("PATH");
let bin_path = resolve_bin(bin_name, paths.map(|p| OsStr::new(p.as_str())), cwd)?;
let mut cmd = build_command(&bin_path, cwd);
cmd.args(args).envs(envs);
let status = cmd.status().await?;
Ok(status)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/vite_global_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ vite_error = { workspace = true }
vite_install = { workspace = true }
vite_js_runtime = { workspace = true }
vite_path = { workspace = true }
vite_command = { workspace = true }
vite_shared = { workspace = true }
vite_str = { workspace = true }
vite_workspace = { workspace = true }
which = { workspace = true }

[target.'cfg(windows)'.dependencies]
junction = { workspace = true }
Expand Down
13 changes: 12 additions & 1 deletion crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,14 @@ pub enum Commands {
args: Vec<String>,
},

/// Execute a command from local node_modules/.bin
#[command(disable_help_flag = true)]
Exec {
/// Additional arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},

/// Preview production build
#[command(disable_help_flag = true)]
Preview {
Expand Down Expand Up @@ -1791,6 +1799,8 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result<ExitStatus,

Commands::Run { args } => commands::run_or_delegate::execute(cwd, &args).await,

Commands::Exec { args } => commands::delegate::execute(cwd, "exec", &args).await,

Commands::Preview { args } => commands::delegate::execute(cwd, "preview", &args).await,

Commands::Cache { args } => commands::delegate::execute(cwd, "cache", &args).await,
Expand All @@ -1814,7 +1824,7 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result<ExitStatus,
}

/// Create an exit status with the given code.
fn exit_status(code: i32) -> ExitStatus {
pub(crate) fn exit_status(code: i32) -> ExitStatus {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
Expand Down Expand Up @@ -1849,6 +1859,7 @@ fn apply_custom_help(cmd: clap::Command) -> clap::Command {
{bold}fmt{reset} Format code
{bold}pack{reset} Build library
{bold}run{reset} Run tasks
{bold}exec{reset} Execute a command from local node_modules/.bin
{bold}preview{reset} Preview production build
{bold}env{reset} Manage Node.js versions
{bold}migrate{reset} Migrate an existing project to Vite+
Expand Down
7 changes: 4 additions & 3 deletions crates/vite_global_cli/src/commands/env/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,9 @@ fn find_system_node() -> Option<std::path::PathBuf> {

let filtered_path = std::env::join_paths(filtered_paths).ok()?;

// Use which::which_in with filtered PATH - stops at first match
// Use vite_command::resolve_bin with filtered PATH - stops at first match
let cwd = current_dir().ok()?;
which::which_in("node", Some(filtered_path), cwd).ok()
vite_command::resolve_bin("node", Some(&filtered_path), &cwd).ok().map(|p| p.into_path_buf())
}

/// Check for active session override via VITE_PLUS_NODE_VERSION or session file.
Expand Down Expand Up @@ -393,7 +393,8 @@ async fn check_path() -> bool {

/// Find an executable in PATH.
fn find_in_path(name: &str) -> Option<std::path::PathBuf> {
which::which(name).ok()
let cwd = current_dir().ok()?;
vite_command::resolve_bin(name, None, &cwd).ok().map(|p| p.into_path_buf())
}

/// Print PATH fix instructions for shell setup.
Expand Down
3 changes: 1 addition & 2 deletions crates/vite_global_cli/src/commands/vpx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ fn find_on_path(cmd: &str) -> Option<AbsolutePathBuf> {

let filtered_path = std::env::join_paths(filtered_paths).ok()?;
let cwd = vite_path::current_dir().ok()?;
let path = which::which_in(cmd, Some(filtered_path), cwd).ok()?;
AbsolutePathBuf::new(path)
vite_command::resolve_bin(cmd, Some(&filtered_path), &cwd).ok()
}

/// Prepend all `node_modules/.bin` directories from cwd upward to PATH.
Expand Down
5 changes: 2 additions & 3 deletions crates/vite_global_cli/src/shim/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,9 @@ fn find_system_tool(tool: &str) -> Option<AbsolutePathBuf> {

let filtered_path = std::env::join_paths(filtered_paths).ok()?;

// Use which::which_in with filtered PATH - stops at first match
// Use vite_command::resolve_bin with filtered PATH - stops at first match
let cwd = current_dir().ok()?;
let path = which::which_in(tool, Some(filtered_path), cwd).ok()?;
AbsolutePathBuf::new(path)
vite_command::resolve_bin(tool, Some(&filtered_path), &cwd).ok()
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This project is using Vite+, a modern toolchain built on top of Vite, Rolldown,
- lib - Build library
- migrate - Migrate an existing project to Vite+
- new - Create a new monorepo package (in-project) or a new project (global)
- exec - Execute a command in workspace packages (supports `--filter`, `-r`, `--parallel`)
- run - Run tasks from `package.json` scripts

These commands map to their corresponding tools. For example, `vp dev --port 3000` runs Vite's dev server and works the same as Vite. `vp test` runs JavaScript tests through the bundled Vitest. The version of all tools can be checked using `vp --version`. This is useful when researching documentation, features, and bugs.
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/binding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ name = "vite-plus-cli"
version = "0.0.0"
edition.workspace = true

[[bin]]
name = "vite"
path = "src/main.rs"

[features]
rolldown = ["dep:rolldown_binding"]

Expand All @@ -15,9 +11,11 @@ anyhow = { workspace = true }
async-trait = { workspace = true }
clap = { workspace = true, features = ["derive"] }
fspy = { workspace = true }
glob = { workspace = true }
rustc-hash = { workspace = true }
napi = { workspace = true }
napi-derive = { workspace = true }
petgraph = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
Expand All @@ -31,8 +29,6 @@ vite_shared = { workspace = true }
vite_str = { workspace = true }
vite_task = { workspace = true }
vite_workspace = { workspace = true }
which = { workspace = true }

rolldown_binding = { workspace = true, optional = true }

[build-dependencies]
Expand Down
Loading
Loading