Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5f91278
Port packages/tools Node.js bins to single Rust binary (vtt)
claude Mar 23, 2026
db0fe86
Fix formatting, duration redaction for nanoseconds, and snapshot rege…
claude Mar 23, 2026
b7b2e68
Strip ", <duration> saved" from cache hit summaries for stable snapshots
claude Mar 23, 2026
c8b2096
Fix Windows path redaction for verbatim (\\?\) prefix paths
claude Mar 23, 2026
f9a96a6
Fix Windows redaction for Debug-format paths with escaped backslashes
claude Mar 23, 2026
e6d907d
Collapse double forward slashes from Windows backslash normalization
claude Mar 23, 2026
7f11863
Fix Windows Debug-format path redaction and musl summary formatting
claude Mar 23, 2026
216e4fa
Fix Windows Debug-format path redaction in plan test error strings
claude Mar 23, 2026
f6c2f4c
ci: switch tests to --release to measure build+test time impact
claude Mar 24, 2026
94983a5
Revert --release tests; optimize insta/similar in dev profile instead
claude Mar 24, 2026
c5efaf7
Revert insta/similar opt-level — compilation overhead outweighs gains
claude Mar 24, 2026
65bf3ed
Disable debug info in test profile
claude Mar 24, 2026
53f839a
update
branchseer Mar 24, 2026
7cd9746
Rename `json-edit` to `json-patch` using the `json-patch` crate (RFC …
branchseer Mar 24, 2026
97fa56a
Resolve vt and vtt runtime paths using compile-time manifest diffing
branchseer Mar 24, 2026
be67459
Make vite_task_tools a library crate with vtt binary in vite_task_bin
branchseer Mar 24, 2026
966a18e
Disable test/doctest harness for vite_task_tools lib target
branchseer Mar 24, 2026
3f39751
ci: split test step into separate build and run steps
branchseer Mar 24, 2026
fe23bd3
Remove json-patch command; use replace-file-content instead
branchseer Mar 24, 2026
85a53ef
Simplify Args to Task/Tool; remove execute_synthetic and Lint/Test/En…
branchseer Mar 25, 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
20 changes: 17 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,20 @@ jobs:
# Must run after setup-node so correct native binaries are installed
- run: pnpm install

- run: cargo test --target ${{ matrix.target }}
- name: Build tests
run: cargo test --no-run --target ${{ matrix.target }}
if: ${{ matrix.os != 'ubuntu-latest' }}

- run: cargo-zigbuild test --target x86_64-unknown-linux-gnu.2.17
- name: Build tests
run: cargo-zigbuild test --no-run --target x86_64-unknown-linux-gnu.2.17
if: ${{ matrix.os == 'ubuntu-latest' }}

- name: Run tests
run: cargo test --target ${{ matrix.target }}
if: ${{ matrix.os != 'ubuntu-latest' }}

- name: Run tests
run: cargo-zigbuild test --target x86_64-unknown-linux-gnu.2.17
if: ${{ matrix.os == 'ubuntu-latest' }}

test-musl:
Expand Down Expand Up @@ -175,7 +185,11 @@ jobs:
corepack enable
pnpm install

- run: cargo test
- name: Build tests
run: cargo test --no-run

- name: Run tests
run: cargo test

fmt:
name: Format and Check Deps
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ ignored = [
# and we don't rely on it for debugging that much.
debug = false

[profile.test]
debug = false

[profile.release]
# Configurations explicitly listed here for clarity.
# Using the best options for performance.
Expand Down
4 changes: 4 additions & 0 deletions crates/vite_task_bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
name = "vt"
path = "src/main.rs"

[[bin]]
name = "vtt"
path = "src/vtt.rs"

[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
clap = { workspace = true, features = ["derive"] }
jsonc-parser = { workspace = true }
rustc-hash = { workspace = true }

Check failure on line 22 in crates/vite_task_bin/Cargo.toml

View workflow job for this annotation

GitHub Actions / Format and Check Deps

shear/unused_dependency

unused dependency `rustc-hash` (remove this dependency)
serde_json = { workspace = true }
tokio = { workspace = true, features = ["full"] }
vite_path = { workspace = true }
Expand Down
66 changes: 11 additions & 55 deletions crates/vite_task_bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::{
};

use clap::Parser;
use rustc_hash::FxHashMap;
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task::{
Expand Down Expand Up @@ -47,44 +46,14 @@ pub fn find_executable(
Ok(executable_path.into_os_string().into())
}

/// Create a synthetic plan request for running a tool from `node_modules/.bin`.
///
/// # Errors
///
/// Returns an error if the executable cannot be found.
fn synthesize_node_modules_bin_task(
executable_name: &str,
args: &[Str],
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
cwd: &Arc<AbsolutePath>,
) -> anyhow::Result<SyntheticPlanRequest> {
Ok(SyntheticPlanRequest {
program: find_executable(get_path_env(envs), cwd, executable_name)?,
args: args.into(),
cache_config: UserCacheConfig::with_config(EnabledCacheConfig {
env: None,
untracked_env: None,
input: None,
}),
envs: Arc::clone(envs),
})
}

#[derive(Debug, Parser)]
#[command(name = "vt", version)]
pub enum Args {
Lint {
/// Run a tool via vtt.
Tool {
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<Str>,
},
Test {
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<Str>,
},
EnvTest {
name: Str,
value: Str,
},
#[command(flatten)]
Task(Command),
}
Expand All @@ -109,30 +78,17 @@ impl vite_task::CommandHandler for CommandHandler {
std::iter::once(command.program.as_str()).chain(command.args.iter().map(Str::as_str)),
)?;
match args {
Args::Lint { args } => Ok(HandledCommand::Synthesized(
synthesize_node_modules_bin_task("oxlint", &args, &command.envs, &command.cwd)?,
)),
Args::Test { args } => Ok(HandledCommand::Synthesized(
synthesize_node_modules_bin_task("vitest", &args, &command.envs, &command.cwd)?,
)),
Args::EnvTest { name, value } => {
let mut envs = FxHashMap::clone(&command.envs);
envs.insert(
Arc::from(OsStr::new(name.as_str())),
Arc::from(OsStr::new(value.as_str())),
);

Args::Tool { args } => {
let program = find_executable(get_path_env(&command.envs), &command.cwd, "vtt")?;
Ok(HandledCommand::Synthesized(SyntheticPlanRequest {
program: find_executable(get_path_env(&envs), &command.cwd, "print-env")?,
args: [name.clone()].into(),
cache_config: UserCacheConfig::with_config({
EnabledCacheConfig {
env: None,
untracked_env: Some(vec![name]),
input: None,
}
program,
args: args.into_iter().filter(|a| a.as_str() != "--").collect(),
cache_config: UserCacheConfig::with_config(EnabledCacheConfig {
env: None,
untracked_env: None,
input: None,
}),
envs: Arc::new(envs),
envs: Arc::clone(&command.envs),
}))
}
Args::Task(parsed) => Ok(HandledCommand::ViteTaskCommand(parsed)),
Expand Down
40 changes: 4 additions & 36 deletions crates/vite_task_bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
use std::{process::ExitCode, sync::Arc};
use std::process::ExitCode;

use clap::Parser;
use vite_str::Str;
use vite_task::{
EnabledCacheConfig, ExitStatus, Session, UserCacheConfig, get_path_env,
plan_request::SyntheticPlanRequest,
};
use vite_task_bin::{Args, OwnedSessionConfig, find_executable};
use vite_task::{ExitStatus, Session};
use vite_task_bin::{Args, OwnedSessionConfig};

#[tokio::main]
async fn main() -> anyhow::Result<ExitCode> {
#[expect(clippy::large_futures, reason = "top-level await in main, no alternative")]
let exit_status = run().await?;
Ok(exit_status.0.into())
}
Expand All @@ -21,34 +16,7 @@ async fn run() -> anyhow::Result<ExitStatus> {
let session = Session::init(owned_config.as_config())?;
match args {
Args::Task(parsed) => session.main(parsed).await,
args => {
// If env FOO is set, run `print-env FOO` via Session::exec before proceeding.
// In vite-plus, Session::exec is used for auto-install.
let envs = session.envs();
if envs.contains_key(std::ffi::OsStr::new("FOO")) {
let program = find_executable(get_path_env(envs), session.cwd(), "print-env")?;
let request = SyntheticPlanRequest {
program,
args: [Str::from("FOO")].into(),
cache_config: UserCacheConfig::with_config({
EnabledCacheConfig {
env: Some(Box::from([Str::from("FOO")])),
untracked_env: None,
input: None,
}
}),
envs: Arc::clone(envs),
};
let cache_key: Arc<[Str]> = Arc::from([Str::from("print-env-foo")]);
#[expect(
clippy::large_futures,
reason = "execute_synthetic produces a large future"
)]
let status = session.execute_synthetic(request, cache_key, true).await?;
if status != ExitStatus::SUCCESS {
return Ok(status);
}
}
args @ Args::Tool { .. } => {
#[expect(clippy::print_stdout, reason = "CLI binary output for non-task commands")]
{
println!("{args:?}");
Expand Down
126 changes: 126 additions & 0 deletions crates/vite_task_bin/src/vtt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// This is a standalone test utility binary that deliberately uses std types
// rather than the project's custom types (vite_str, vite_path, etc.).
#![expect(clippy::disallowed_types, reason = "standalone test utility uses std types")]
#![expect(clippy::disallowed_macros, reason = "standalone test utility uses std macros")]
#![expect(clippy::disallowed_methods, reason = "standalone test utility uses std methods")]
#![expect(clippy::print_stderr, reason = "CLI tool error output")]
#![expect(clippy::print_stdout, reason = "CLI tool output")]

fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: vtt <subcommand> [args...]");
eprintln!(
"Subcommands: check-tty, print, print-cwd, print-env, print-file, read-stdin, replace-file-content, touch-file"
);
std::process::exit(1);
}

let result: Result<(), Box<dyn std::error::Error>> = match args[1].as_str() {
"check-tty" => {
cmd_check_tty();
Ok(())
}
"print" => {
cmd_print(&args[2..]);
Ok(())
}
"print-cwd" => cmd_print_cwd(),
"print-env" => cmd_print_env(&args[2..]),
"print-file" => cmd_print_file(&args[2..]),
"read-stdin" => cmd_read_stdin(),
"replace-file-content" => cmd_replace_file_content(&args[2..]),
"touch-file" => cmd_touch_file(&args[2..]),
other => {
eprintln!("Unknown subcommand: {other}");
std::process::exit(1);
}
};

if let Err(err) = result {
eprintln!("{err}");
std::process::exit(1);
}
}

fn cmd_check_tty() {
use std::io::IsTerminal as _;
let stdin_tty = if std::io::stdin().is_terminal() { "tty" } else { "not-tty" };
let stdout_tty = if std::io::stdout().is_terminal() { "tty" } else { "not-tty" };
let stderr_tty = if std::io::stderr().is_terminal() { "tty" } else { "not-tty" };
println!("stdin:{stdin_tty}");
println!("stdout:{stdout_tty}");
println!("stderr:{stderr_tty}");
}

fn cmd_print(args: &[String]) {
println!("{}", args.join(" "));
}

fn cmd_print_cwd() -> Result<(), Box<dyn std::error::Error>> {
let cwd = std::env::current_dir()?;
println!("{}", cwd.display());
Ok(())
}

fn cmd_print_env(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
if args.is_empty() {
return Err("Usage: vtt print-env <VAR_NAME>".into());
}
let value = std::env::var(&args[0]).unwrap_or_else(|_| "(undefined)".to_string());
println!("{value}");
Ok(())
}

fn cmd_print_file(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write as _;
let stdout = std::io::stdout();
let mut out = stdout.lock();
for file in args {
match std::fs::read(file) {
Ok(content) => out.write_all(&content)?,
Err(_) => eprintln!("{file}: not found"),
}
}
Ok(())
}

fn cmd_read_stdin() -> Result<(), Box<dyn std::error::Error>> {
use std::io::{Read as _, Write as _};
let mut stdin = std::io::stdin().lock();
let mut stdout = std::io::stdout().lock();
let mut buf = [0u8; 8192];
loop {
match stdin.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => stdout.write_all(&buf[..n])?,
}
}
Ok(())
}

fn cmd_replace_file_content(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
if args.len() < 3 {
return Err("Usage: vtt replace-file-content <filename> <searchValue> <newValue>".into());
}
let filename = &args[0];
let search_value = &args[1];
let new_value = &args[2];

let filepath = std::path::Path::new(filename).canonicalize()?;
let content = std::fs::read_to_string(&filepath)?;
if !content.contains(search_value) {
return Err(std::format!("searchValue not found in {filename}: {search_value:?}").into());
}
let new_content = content.replacen(search_value, new_value, 1);
std::fs::write(&filepath, new_content)?;
Ok(())
}

fn cmd_touch_file(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
if args.is_empty() {
return Err("Usage: vtt touch-file <filename>".into());
}
let _file = std::fs::OpenOptions::new().read(true).write(true).open(&args[0])?;
Ok(())
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"scripts": {
"script1": "print hello",
"script2": "print hello"
"script1": "vtt print hello",
"script2": "vtt print hello"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ name = "associate existing cache"
steps = [
"vt run script1 # cache miss",
"vt run script2 # cache hit, same command as script1",
"json-edit package.json '_.scripts.script2 = \"print world\"' # change script2",
"vtt replace-file-content package.json '\"script2\": \"vtt print hello\"' '\"script2\": \"vtt print world\"' # change script2",
"vt run script2 # cache miss",
]
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
> vt run script1 # cache miss
$ print hello
$ vtt print hello
hello
> vt run script2 # cache hit, same command as script1
$ print hello ◉ cache hit, replaying
$ vtt print hello ◉ cache hit, replaying
hello

---
vt run: cache hit, <duration> saved.
> json-edit package.json '_.scripts.script2 = "print world"' # change script2
vt run: cache hit.
> vtt replace-file-content package.json '"script2": "vtt print hello"' '"script2": "vtt print world"' # change script2

> vt run script2 # cache miss
$ print world ○ cache miss: args changed, executing
$ vtt print world ○ cache miss: args changed, executing
world

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading