diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3f23f8fcb..e83c32e81 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,4 +21,17 @@ jobs: name: Security Analysis runs-on: ubuntu-latest steps: + # The security-action uses its own bundled deny.toml. The interprocess + # crate (used by vite_task_client for IPC) pulls in two transitive deps + # licensed 0BSD (doctest-file, recvmsg) — both OSI-approved permissive + # licenses. Append 0BSD to the bundled allowlist so the license check + # still passes while keeping the rest of the action's policy intact. + - name: Allow 0BSD in security-action deny.toml + run: | + DENY=$(find /home/runner/work/_actions/oxc-project/security-action -name deny.toml | head -1) + test -n "$DENY" + # Only modify the first `allow = [` (under [licenses]); the second one + # under [bans] is a crate allowlist with different semantics. + sed -i '0,/^allow = \[/{s/^allow = \[/allow = [\n "0BSD",/}' "$DENY" + grep -A 12 '^\[licenses\]' "$DENY" | head -15 - uses: oxc-project/security-action@77e230508eccbb400b23746dab6c573a8ea7483e # v1.0.5 diff --git a/.typos.toml b/.typos.toml index c97dbfdec..f20f6f8b4 100644 --- a/.typos.toml +++ b/.typos.toml @@ -9,4 +9,6 @@ extend-exclude = [ # Intentional typos for testing fuzzy matching and "did you mean" suggestions "crates/vite_select/src/fuzzy.rs", "crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select", + # pnpm patch files — hunk context includes third-party code we don't own + "patches", ] diff --git a/CHANGELOG.md b/CHANGELOG.md index add778222..597337616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - **Added** `output` field for cached tasks: archives matching files after a successful run and restores them on cache hit ([#375](https://github.com/voidzero-dev/vite-task/pull/375)) - **Fixed** Windows cached tasks can now run package shims rewritten through PowerShell; default env passthrough now preserves `PATHEXT` ([#366](https://github.com/voidzero-dev/vite-task/pull/366)) - **Added** Platform support for targets without `input` auto-inference (e.g. Android). Tasks still run; those relying on auto-inference run uncached, with the summary noting that `input` must be configured manually to enable caching ([#352](https://github.com/voidzero-dev/vite-task/pull/352)) +- **Added** `output` field for cached tasks: archives output files after a successful run and restores them on cache hit. Defaults to automatically tracking files the task writes; accepts globs (e.g. `"dist/**"`), `{ "auto": true }`, and negative patterns (`"!dist/cache/**"`) ([#321](https://github.com/voidzero-dev/vite-task/pull/321)) - **Fixed** `vp run` no longer aborts with `failed to prepare the command for injection: Invalid argument` when the user environment already has `LD_PRELOAD` (Linux) or `DYLD_INSERT_LIBRARIES` (macOS) set. The tracer shim is now appended to any existing value and placed last, so user preloads keep their symbol-interposition precedence ([#340](https://github.com/voidzero-dev/vite-task/issues/340)) - **Changed** Arguments passed after a task name (e.g. `vp run test some-filter`) are now forwarded only to that task. Tasks pulled in via `dependsOn` no longer receive them ([#324](https://github.com/voidzero-dev/vite-task/issues/324)) - **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#330](https://github.com/voidzero-dev/vite-task/pull/330)) diff --git a/CLAUDE.md b/CLAUDE.md index 3abd2b859..0ce45b23c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,6 +148,10 @@ All code must work on both Unix and Windows without platform skipping: - Platform differences should be handled gracefully, not skipped - After major changes to `fspy*` or platform-specific crates, run `just lint-linux` and `just lint-windows` +## New Crates and Packages + +When creating a new Rust crate or npm package, add a concise `README.md` stating its goal in one or two sentences. Do not include implementation details, API docs, or links to other docs — those belong in source comments or the design docs. + ## Changelog When a change is user-facing (new feature, changed behavior, bug fix, removal, or perf improvement), run `/update-changelog` to add an entry to `CHANGELOG.md`. Do not add entries for internal refactors, CI, dep bumps, test fixes, or docs changes. diff --git a/Cargo.lock b/Cargo.lock index 5c645d323..dbcf88a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,7 +461,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -593,6 +593,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "copy_dir" version = "0.1.3" @@ -751,6 +760,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400a21f1014a968ec518c7ccdf9b4a4ed0cac8c56ccb6d604f8b91f00110501e" + [[package]] name = "ctor" version = "1.0.1" @@ -906,7 +921,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version 0.4.1", @@ -996,6 +1011,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "doctest-file" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359" + [[package]] name = "document-features" version = "0.2.12" @@ -1211,7 +1232,7 @@ dependencies = [ "bstr", "bumpalo", "csv-async", - "ctor", + "ctor 1.0.1", "derive_more", "flate2", "fspy_detours_sys", @@ -1270,7 +1291,7 @@ version = "0.0.0" dependencies = [ "anyhow", "bstr", - "ctor", + "ctor 1.0.1", "fspy_shared", "fspy_shared_unix", "libc", @@ -1321,7 +1342,7 @@ dependencies = [ "bitflags 2.10.0", "bstr", "bytemuck", - "ctor", + "ctor 1.0.1", "native_str", "os_str_bytes", "rustc-hash", @@ -1641,6 +1662,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "interprocess" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069323743400cb7ab06a8fe5c1ed911d36b6919ec531661d034c89083629595b" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.61.2", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1782,6 +1818,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libredox" version = "0.1.12" @@ -1995,6 +2041,63 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "napi" +version = "3.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e55037284865448ecf329baa86a4d05401f647ebde99f5747b640d32c2c5226" +dependencies = [ + "bitflags 2.10.0", + "ctor 0.11.1", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ba740fe4c9524d86fd90798fd8ccdb23402b3eef7e7c30897a8a369b529fcf" +dependencies = [ + "convert_case 0.11.0", + "ctor 0.11.1", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "napi-derive-backend" +version = "5.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "semver 1.0.27", + "syn 2.0.117", +] + +[[package]] +name = "napi-sys" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb602b84d7c1edae45e50bbf1374696548f36ae179dfa667f577e384bb90c2b" +dependencies = [ + "libloading 0.9.0", +] + [[package]] name = "native_str" version = "0.0.0" @@ -2055,6 +2158,12 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -2707,7 +2816,7 @@ name = "pty_terminal" version = "0.0.0" dependencies = [ "anyhow", - "ctor", + "ctor 1.0.1", "ctrlc", "nix 0.31.2", "ntest", @@ -2724,7 +2833,7 @@ version = "0.0.0" dependencies = [ "anyhow", "crossterm", - "ctor", + "ctor 1.0.1", "ntest", "portable-pty", "pty_terminal", @@ -2915,6 +3024,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3426,7 +3541,7 @@ name = "subprocess_test" version = "0.0.0" dependencies = [ "base64", - "ctor", + "ctor 1.0.1", "fspy", "portable-pty", "rustc-hash", @@ -4101,6 +4216,8 @@ dependencies = [ "derive_more", "fspy", "futures-util", + "materialized_artifact", + "materialized_artifact_build", "nix 0.31.2", "once_cell", "owo-colors", @@ -4120,11 +4237,15 @@ dependencies = [ "tracing", "twox-hash", "uuid", + "vite_glob", "vite_path", "vite_select", "vite_str", + "vite_task_client_napi", "vite_task_graph", + "vite_task_ipc_shared", "vite_task_plan", + "vite_task_server", "vite_workspace", "wax", "winapi", @@ -4166,6 +4287,28 @@ dependencies = [ "which", ] +[[package]] +name = "vite_task_client" +version = "0.0.0" +dependencies = [ + "interprocess", + "native_str", + "vite_path", + "vite_task_ipc_shared", + "wincode", +] + +[[package]] +name = "vite_task_client_napi" +version = "0.1.0" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "vite_str", + "vite_task_client", +] + [[package]] name = "vite_task_graph" version = "0.1.0" @@ -4189,6 +4332,14 @@ dependencies = [ "wincode", ] +[[package]] +name = "vite_task_ipc_shared" +version = "0.0.0" +dependencies = [ + "native_str", + "wincode", +] + [[package]] name = "vite_task_plan" version = "0.1.0" @@ -4227,6 +4378,27 @@ dependencies = [ "wincode", ] +[[package]] +name = "vite_task_server" +version = "0.0.0" +dependencies = [ + "futures", + "interprocess", + "native_str", + "rustc-hash", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "uuid", + "vite_glob", + "vite_path", + "vite_task_client", + "vite_task_ipc_shared", + "wincode", +] + [[package]] name = "vite_tui" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index f7f77892f..acbcb79bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ fspy_shared = { path = "crates/fspy_shared" } fspy_shared_unix = { path = "crates/fspy_shared_unix" } futures = "0.3.31" futures-util = "0.3.31" +interprocess = "2" jsonc-parser = { version = "0.32.0", features = ["serde"] } libc = "0.2.185" libtest-mimic = "0.8.2" @@ -162,8 +163,15 @@ widestring = "1.2.0" winapi = "0.3.9" winsafe = { version = "0.0.27", features = ["kernel"] } xxhash-rust = { version = "0.8.15", features = ["const_xxh3"] } +napi = "3" +napi-build = "2" +napi-derive = "3" ntest = "0.9.5" terminal_size = "0.4" +vite_task_client = { path = "crates/vite_task_client" } +vite_task_client_napi = { path = "crates/vite_task_client_napi", artifact = "cdylib", target = "target" } +vite_task_ipc_shared = { path = "crates/vite_task_ipc_shared" } +vite_task_server = { path = "crates/vite_task_server" } zstd = "0.13" [workspace.metadata.cargo-shear] @@ -171,6 +179,7 @@ ignored = [ # These are artifact dependencies. They are not directly `use`d in Rust code. "fspy_preload_unix", "fspy_preload_windows", + "vite_task_client_napi", ] [profile.dev] diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index b6659f155..6c35b8a9d 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -158,7 +158,6 @@ fn register_preload_cdylib() -> anyhow::Result<()> { } fn main() -> anyhow::Result<()> { - println!("cargo:rerun-if-changed=build.rs"); let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); fetch_macos_binaries(&out_dir).context("Failed to fetch macOS binaries")?; register_preload_cdylib().context("Failed to register preload cdylib")?; diff --git a/crates/native_str/src/lib.rs b/crates/native_str/src/lib.rs index 84a841a07..6268f210d 100644 --- a/crates/native_str/src/lib.rs +++ b/crates/native_str/src/lib.rs @@ -37,7 +37,7 @@ use wincode::{ /// **Not portable across platforms.** The binary representation is platform-specific. /// Deserializing a `NativeStr` serialized on a different platform leads to unspecified /// behavior (garbage data), but is not unsafe. Designed for same-platform IPC only. -#[derive(TransparentWrapper, PartialEq, Eq)] +#[derive(TransparentWrapper, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct NativeStr { // On unix, this is the raw bytes of the OsStr. diff --git a/crates/vite_task/Cargo.toml b/crates/vite_task/Cargo.toml index 90f510572..77a9771ef 100644 --- a/crates/vite_task/Cargo.toml +++ b/crates/vite_task/Cargo.toml @@ -43,16 +43,28 @@ tokio = { workspace = true, features = [ tokio-util = { workspace = true } tracing = { workspace = true } twox-hash = { workspace = true } +materialized_artifact = { workspace = true } uuid = { workspace = true, features = ["v4"] } +vite_glob = { workspace = true } vite_path = { workspace = true } vite_select = { workspace = true } vite_str = { workspace = true } vite_task_graph = { workspace = true } +vite_task_ipc_shared = { workspace = true } vite_task_plan = { workspace = true } +vite_task_server = { workspace = true } vite_workspace = { workspace = true } wax = { workspace = true } zstd = { workspace = true } +# Artifact build-deps must be unconditional: cargo's resolver panics when +# `artifact = "cdylib"` deps live under a `[target.cfg.build-dependencies]` +# block on cross-compile. +[build-dependencies] +anyhow = { workspace = true } +materialized_artifact_build = { workspace = true } +vite_task_client_napi = { workspace = true } + [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/vite_task/build.rs b/crates/vite_task/build.rs index 6acc864ed..4f1fbe191 100644 --- a/crates/vite_task/build.rs +++ b/crates/vite_task/build.rs @@ -1,3 +1,13 @@ +#![expect( + clippy::disallowed_types, + clippy::disallowed_macros, + reason = "build.rs interfaces with std::path and cargo's env-var API" +)] + +use std::{env, path::Path}; + +use anyhow::Context; + // Why `cfg(fspy)` instead of matching on `target_os` directly at each use site: // "fspy is available" is a single semantic predicate, but the underlying reason // (the `fspy` crate builds on windows/macos/linux) is a three-OS list that @@ -7,12 +17,18 @@ // over OSes. The OS allowlist lives in two spots that must stay in sync: this // file (for the rustc cfg) and the target-scoped dep block in Cargo.toml // (which Cargo resolves before build.rs runs, so it can't reuse this cfg). -fn main() { +fn main() -> anyhow::Result<()> { println!("cargo::rustc-check-cfg=cfg(fspy)"); println!("cargo::rerun-if-changed=build.rs"); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); if matches!(target_os.as_str(), "windows" | "macos" | "linux") { println!("cargo::rustc-cfg=fspy"); } + + let env_name = "CARGO_CDYLIB_FILE_VITE_TASK_CLIENT_NAPI"; + println!("cargo:rerun-if-env-changed={env_name}"); + let dylib_path = env::var_os(env_name).with_context(|| format!("{env_name} not set"))?; + materialized_artifact_build::register("vite_task_client_napi", Path::new(&dylib_path)); + Ok(()) } diff --git a/crates/vite_task/docs/task-cache.md b/crates/vite_task/docs/task-cache.md index 440901ccd..d6c75a910 100644 --- a/crates/vite_task/docs/task-cache.md +++ b/crates/vite_task/docs/task-cache.md @@ -92,7 +92,7 @@ The cache entry key uniquely identifies a command execution context: ```rust pub struct CacheEntryKey { pub spawn_fingerprint: SpawnFingerprint, - pub input_config: ResolvedInputConfig, + pub input_config: ResolvedGlobConfig, } ``` @@ -303,7 +303,7 @@ Cache entries are serialized using `bincode` for efficient storage. │ ────────────────────── │ │ CacheEntryKey { │ │ spawn_fingerprint: SpawnFingerprint { ... }, │ -│ input_config: ResolvedInputConfig { ... }, │ +│ input_config: ResolvedGlobConfig { ... }, │ │ } │ │ ExecutionCacheKey::UserTask { │ │ task_name: "build", │ diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index c1659756f..d22352528 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -1,5 +1,6 @@ mod cli; mod collections; +mod napi_client; pub mod session; // Public exports for vite_task_bin diff --git a/crates/vite_task/src/napi_client.rs b/crates/vite_task/src/napi_client.rs new file mode 100644 index 000000000..dce8328de --- /dev/null +++ b/crates/vite_task/src/napi_client.rs @@ -0,0 +1,32 @@ +//! The `vite_task_client_napi` cdylib is embedded into the `vp` binary and +//! materialized to disk on first use so tools can `require()` it at runtime. + +use std::{env, fs, sync::LazyLock}; + +use materialized_artifact::artifact; +use vite_path::{AbsolutePath, AbsolutePathBuf}; + +/// Path to the materialized `vite_task_client_napi` `.node` addon. +/// +/// The file is written to a process-wide temp directory on first call and +/// reused on every subsequent call (content-addressed filename; no re-writes). +/// +/// # Panics +/// +/// Panics if the materialization fails on first call — this mirrors fspy's +/// `SPY_IMPL` and the same reasoning applies: if we can't write into the +/// system temp dir, the runner can't run tasks anyway. +#[must_use] +pub fn napi_client_path() -> &'static AbsolutePath { + static PATH: LazyLock = LazyLock::new(|| { + let dir = env::temp_dir().join("vite_task_client_napi"); + let _ = fs::create_dir(&dir); + let path = artifact!("vite_task_client_napi") + .materialize() + .suffix(".node") + .at(&dir) + .expect("materialize vite_task_client_napi"); + AbsolutePathBuf::new(path).expect("system temp dir yields an absolute path") + }); + PATH.as_absolute_path() +} diff --git a/crates/vite_task/src/session/cache/display.rs b/crates/vite_task/src/session/cache/display.rs index 9dbcabd59..a8302fb2f 100644 --- a/crates/vite_task/src/session/cache/display.rs +++ b/crates/vite_task/src/session/cache/display.rs @@ -179,6 +179,16 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option { let desc = format_input_change_str(*kind, path.as_str()); return Some(vite_str::format!("○ cache miss: {desc}, executing")); } + FingerprintMismatch::TrackedEnvChanged { name, .. } => { + return Some(vite_str::format!( + "○ cache miss: tracked env '{name}' changed, executing" + )); + } + FingerprintMismatch::TrackedEnvGlobChanged { pattern, .. } => { + return Some(vite_str::format!( + "○ cache miss: tracked env glob '{pattern}' changed, executing" + )); + } }; Some(vite_str::format!("○ cache miss: {reason}, executing")) } diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index f310ddc00..d2716723e 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -158,6 +158,17 @@ pub enum FingerprintMismatch { kind: InputChangeKind, path: RelativePathBuf, }, + /// A tool-tracked env var changed between runs. + TrackedEnvChanged { + name: Str, + old: Option, + new: Option, + }, + /// A tool-tracked env glob's match-set changed between runs. + TrackedEnvGlobChanged { + pattern: Str, + diff: crate::session::execute::fingerprint::EnvGlobDiff, + }, } impl Display for FingerprintMismatch { @@ -175,6 +186,42 @@ impl Display for FingerprintMismatch { Self::InputChanged { kind, path } => { write!(f, "{}", display::format_input_change_str(*kind, path.as_str())) } + Self::TrackedEnvChanged { name, old, new } => { + write!(f, "tracked env {name}: ")?; + match old { + Some(value) => write!(f, "{:?}", value.as_str())?, + None => write!(f, "(unset)")?, + } + write!(f, " → ")?; + match new { + Some(value) => write!(f, "{:?}", value.as_str()), + None => write!(f, "(unset)"), + } + } + Self::TrackedEnvGlobChanged { pattern, diff } => { + write!(f, "tracked env glob {:?}:", pattern.as_str())?; + let mut first = true; + for (name, value) in &diff.added { + write!(f, "{} +{}={:?}", if first { "" } else { "," }, name, value.as_str())?; + first = false; + } + for (name, value) in &diff.removed { + write!(f, "{} -{}={:?}", if first { "" } else { "," }, name, value.as_str())?; + first = false; + } + for (name, (old, new)) in &diff.changed { + write!( + f, + "{} {}: {:?} → {:?}", + if first { "" } else { "," }, + name, + old.as_str(), + new.as_str() + )?; + first = false; + } + Ok(()) + } } } } @@ -215,16 +262,16 @@ impl ExecutionCache { "CREATE TABLE task_fingerprints (key BLOB PRIMARY KEY, value BLOB);", (), )?; - conn.execute("PRAGMA user_version = 12", ())?; + conn.execute("PRAGMA user_version = 13", ())?; } - 1..=11 => { + 1..=12 => { // old internal db version. reset conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?; conn.execute("VACUUM", ())?; conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?; } - 12 => break, // current version - 13.. => { + 13 => break, // current version + 14.. => { return Err(anyhow::anyhow!( "Unrecognized database version: {user_version}. \ The cache may have been created by a newer version of Vite Task. \ @@ -266,11 +313,24 @@ impl ExecutionCache { return Ok(Err(CacheMiss::FingerprintMismatch(mismatch))); } - // Validate post-run fingerprint (inferred inputs from fspy) - if let Some((kind, path)) = cache_value.post_run_fingerprint.validate(workspace_root)? { - return Ok(Err(CacheMiss::FingerprintMismatch( - FingerprintMismatch::InputChanged { kind, path }, - ))); + // Validate post-run fingerprint (inferred inputs + tracked envs) + if let Some(mismatch) = cache_value.post_run_fingerprint.validate(workspace_root)? { + let fingerprint_mismatch = match mismatch { + crate::session::execute::fingerprint::PostRunMismatch::InputChanged { + kind, + path, + } => FingerprintMismatch::InputChanged { kind, path }, + crate::session::execute::fingerprint::PostRunMismatch::TrackedEnvChanged { + name, + old, + new, + } => FingerprintMismatch::TrackedEnvChanged { name, old, new }, + crate::session::execute::fingerprint::PostRunMismatch::TrackedEnvGlobChanged { + pattern, + diff, + } => FingerprintMismatch::TrackedEnvGlobChanged { pattern, diff }, + }; + return Ok(Err(CacheMiss::FingerprintMismatch(fingerprint_mismatch))); } // Associate the execution key to the cache entry key if not already, // so that next time we can find it and report what changed diff --git a/crates/vite_task/src/session/event.rs b/crates/vite_task/src/session/event.rs index a0ca481c3..798eb6551 100644 --- a/crates/vite_task/src/session/event.rs +++ b/crates/vite_task/src/session/event.rs @@ -1,6 +1,7 @@ use std::{process::ExitStatus, time::Duration}; use vite_path::RelativePathBuf; +use vite_task_server::Error as IpcServerError; use super::cache::CacheMiss; @@ -43,6 +44,12 @@ pub enum ExecutionError { /// Creating the post-run fingerprint failed after successful execution. #[error("Failed to create post-run fingerprint")] PostRunFingerprint(#[source] anyhow::Error), + + /// The runner-aware IPC server failed to bind for this task. Reported + /// instead of silently degrading so that `{ auto: true }` inputs stay + /// observable end-to-end. + #[error("Failed to start runner IPC server")] + IpcServerBind(#[source] std::io::Error), } #[derive(Debug, Clone)] @@ -76,6 +83,14 @@ pub enum CacheNotUpdatedReason { /// (its `input` config includes auto-inference). Task ran but cannot /// be cached without tracked path accesses. FspyUnsupported, + /// The runner's IPC server failed during execution, so the collected + /// reports may be incomplete. Caching such a run would risk stale + /// inputs/outputs on the next hit. Carries the underlying error for + /// user-facing reporting. + IpcServerError(IpcServerError), + /// A runner-aware tool explicitly requested that this run not be cached + /// (e.g. vite dev-server, a watch task). + ToolRequested, } #[derive(Debug)] diff --git a/crates/vite_task/src/session/execute/fingerprint.rs b/crates/vite_task/src/session/execute/fingerprint.rs index 7b7103d78..57f1a3e87 100644 --- a/crates/vite_task/src/session/execute/fingerprint.rs +++ b/crates/vite_task/src/session/execute/fingerprint.rs @@ -31,6 +31,53 @@ pub struct PostRunFingerprint { /// Paths inferred from fspy during execution with their content fingerprints. /// Only populated when `input_config.includes_auto` is true. pub inferred_inputs: HashMap, + + /// Env vars observed via runner-aware IPC `getEnv` with `tracked: true`. + /// Key is the env name; value is the env value at execution time (or + /// `None` if unset). Validated at cache lookup by comparing against the + /// current parent env. + pub tracked_envs: BTreeMap>, + + /// Glob-pattern env queries (`getEnvs`) made with `tracked: true`. + /// Outer key is the glob pattern, inner map is the match-set at + /// execution time (name → value). Validated at cache lookup by + /// re-matching against the current parent env and comparing the + /// resulting set. + pub tracked_env_globs: BTreeMap>, +} + +/// A mismatch between the stored post-run fingerprint and the current state. +#[expect( + clippy::enum_variant_names, + reason = "all three variants describe different kinds of post-run changes; \ + dropping the `Changed` suffix on any one of them would be misleading" +)] +#[derive(Debug, Clone)] +pub enum PostRunMismatch { + /// An inferred input file or directory changed. + InputChanged { kind: InputChangeKind, path: RelativePathBuf }, + /// A tool-tracked env var changed value (or was added/removed). + TrackedEnvChanged { name: Str, old: Option, new: Option }, + /// A tool-tracked env glob's match-set changed between runs. The glob + /// still matches the same pattern, but added/removed/mutated entries. + TrackedEnvGlobChanged { pattern: Str, diff: EnvGlobDiff }, +} + +/// Per-pattern diff between stored and current match-set. Each map is a +/// subset of the symmetric difference keyed by env name. `changed` holds +/// names present in both with differing values; `added` / `removed` are +/// exclusive to one side. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct EnvGlobDiff { + pub added: BTreeMap, + pub removed: BTreeMap, + pub changed: BTreeMap, +} + +impl EnvGlobDiff { + fn is_empty(&self) -> bool { + self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty() + } } /// Fingerprint for a single path (file or directory) @@ -69,11 +116,15 @@ impl PostRunFingerprint { /// * `inferred_path_reads` - Map of paths that were read during execution (from fspy) /// * `base_dir` - Workspace root for resolving relative paths /// * `globbed_inputs` - Prerun glob fingerprint; paths here are skipped + /// * `tracked_envs` - Tool-requested env vars (name → value), validated on lookup + /// * `tracked_env_globs` - Tool-requested env globs (pattern → matches), validated on lookup #[tracing::instrument(level = "debug", skip_all, name = "create_post_run_fingerprint")] pub fn create( inferred_path_reads: &HashMap, base_dir: &AbsolutePath, globbed_inputs: &BTreeMap, + tracked_envs: BTreeMap>, + tracked_env_globs: BTreeMap>, ) -> anyhow::Result { let inferred_inputs = inferred_path_reads .par_iter() @@ -85,16 +136,13 @@ impl PostRunFingerprint { }) .collect::>>()?; - Ok(Self { inferred_inputs }) + Ok(Self { inferred_inputs, tracked_envs, tracked_env_globs }) } - /// Validates the fingerprint against current filesystem state. - /// Returns `Some((kind, path))` if an input changed, `None` if all valid. + /// Validates the fingerprint against current filesystem and env state. + /// Returns `Some(mismatch)` on the first divergence, `None` if all valid. #[tracing::instrument(level = "debug", skip_all, name = "validate_post_run_fingerprint")] - pub fn validate( - &self, - base_dir: &AbsolutePath, - ) -> anyhow::Result> { + pub fn validate(&self, base_dir: &AbsolutePath) -> anyhow::Result> { let input_mismatch = self.inferred_inputs.par_iter().find_map_any( |(input_relative_path, path_fingerprint)| { let input_full_path = Arc::::from(base_dir.join(input_relative_path)); @@ -120,12 +168,83 @@ impl PostRunFingerprint { } else { input_relative_path.clone() }; - Some(Ok((kind, path))) + Some(Ok(PostRunMismatch::InputChanged { kind, path })) } }, ); - input_mismatch.transpose() + if let Some(result) = input_mismatch { + return result.map(Some); + } + + // Validate tracked envs against the current parent env. + for (name, stored_value) in &self.tracked_envs { + let current_value = + std::env::var_os(name.as_str()).and_then(|v| v.to_str().map(Str::from)); + if current_value.as_ref() != stored_value.as_ref() { + return Ok(Some(PostRunMismatch::TrackedEnvChanged { + name: name.clone(), + old: stored_value.clone(), + new: current_value, + })); + } + } + + // Validate tracked env globs: re-enumerate parent env for each + // pattern and diff against the stored match-set. + for (pattern, stored_matches) in &self.tracked_env_globs { + let current_matches = match_env_glob(pattern.as_str())?; + let diff = diff_env_glob(stored_matches, ¤t_matches); + if !diff.is_empty() { + return Ok(Some(PostRunMismatch::TrackedEnvGlobChanged { + pattern: pattern.clone(), + diff, + })); + } + } + + Ok(None) + } +} + +/// Build the current match-set for `pattern` by enumerating +/// `std::env::vars_os()` and keeping UTF-8 names whose representation matches +/// the glob. Mirrors the server-side match (see +/// `vite_task_server::Recorder::get_envs`). +fn match_env_glob(pattern: &str) -> anyhow::Result> { + let set = vite_glob::GlobPatternSet::new(std::iter::once(pattern))?; + Ok(std::env::vars_os() + .filter_map(|(name, value)| { + let name_str = name.to_str()?.to_owned(); + let value_str = value.to_str()?.to_owned(); + if set.is_match(&name_str) { + Some((Str::from(name_str.as_str()), Str::from(value_str.as_str()))) + } else { + None + } + }) + .collect()) +} + +/// Compute the diff of two match-sets for the same glob pattern. +fn diff_env_glob(stored: &BTreeMap, current: &BTreeMap) -> EnvGlobDiff { + let mut diff = EnvGlobDiff::default(); + for (name, stored_value) in stored { + match current.get(name) { + None => { + diff.removed.insert(name.clone(), stored_value.clone()); + } + Some(current_value) if current_value != stored_value => { + diff.changed.insert(name.clone(), (stored_value.clone(), current_value.clone())); + } + Some(_) => {} + } + } + for (name, current_value) in current { + if !stored.contains_key(name) { + diff.added.insert(name.clone(), current_value.clone()); + } } + diff } /// Determine the kind of change between two differing path fingerprints. diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index a0f88303e..17ef2555e 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -8,19 +8,29 @@ pub mod tracked_accesses; #[cfg(windows)] mod win_job; -use std::{cell::RefCell, collections::BTreeMap, io::Write as _, sync::Arc, time::Instant}; +use std::{ + cell::RefCell, + collections::BTreeMap, + ffi::{OsStr, OsString}, + io::Write as _, + sync::Arc, + time::Instant, +}; use futures_util::{FutureExt, StreamExt, future::LocalBoxFuture, stream::FuturesUnordered}; use petgraph::Direction; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use tokio::sync::Semaphore; use tokio_util::sync::CancellationToken; use vite_path::{AbsolutePath, RelativePathBuf}; use vite_str::Str; +use vite_task_ipc_shared::NODE_CLIENT_PATH_ENV_NAME; use vite_task_plan::{ ExecutionGraph, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, SpawnExecution, cache_metadata::CacheMetadata, execution_graph::ExecutionNodeIndex, }; +use vite_task_server::{Recorder, Reports, ServerHandle, StopAccepting, serve}; +use wax::Program as _; #[cfg(fspy)] use self::tracked_accesses::TrackedPathAccesses; @@ -233,10 +243,6 @@ impl ExecutionContext<'_> { false } LeafExecutionKind::Spawn(spawn_execution) => { - #[expect( - clippy::large_futures, - reason = "spawn execution with cache management creates large futures" - )] let outcome = execute_spawn( leaf_reporter, spawn_execution, @@ -295,18 +301,31 @@ struct CacheState<'a> { /// Captured stdout/stderr for cache replay. Written in place during drain; /// always present (possibly empty) once we reach the cache-update phase. std_outputs: Vec, - /// `Some` iff fspy is enabled (`includes_auto`). Holds the resolved - /// negative globs used by [`TrackedPathAccesses::from_raw`] to filter - /// tracked accesses. `None` means fspy tracking is off for this task. - fspy_negatives: Option>>, + /// `Some` iff auto-input or auto-output tracking is on (`includes_auto` + + /// successful IPC bind). Bundles fspy's input negative globs with the + /// per-task IPC server that runner-aware tools talk to. Parts are borrowed + /// in place during the wait/join; the struct is never moved out. + tracking: Option, +} + +/// Per-task tracking: fspy input-negative globs + IPC server handle. +/// Lifetime-tied to a single `execute_spawn` call. +struct Tracking { + input_negative_globs: Vec>, + ipc_envs: Vec<(&'static OsStr, OsString)>, + ipc_server_fut: LocalBoxFuture<'static, Result>, + stop_accepting: StopAccepting, } /// Post-execution summary of what fspy observed for a single task. Used in the /// cache-update step. Fields are cfg-agnostic so the downstream match logic /// doesn't need `cfg(fspy)` — the value is only ever `Some` when tracking -/// happened (see the `let tracking = ...` fork in `execute_spawn`). +/// happened (see the `let fspy_outcome = ...` fork in `execute_spawn`). struct TrackingOutcome { path_reads: HashMap, + /// All paths the task wrote to. Consumed by `collect_and_archive_outputs` + /// when `output_config.includes_auto` is set. + path_writes: FxHashSet, /// First path that was both read and written during execution, if any. /// A non-empty value means caching this task is unsound. read_write_overlap: Option, @@ -459,37 +478,63 @@ pub async fn execute_spawn( // ───────────────────────────────────────────────────────────────────── let mut mode: ExecutionMode<'_> = match cache_metadata { Some(metadata) => { - let fspy = if metadata.input_config.includes_auto { - // Resolve negative globs for fspy path filtering - // (already workspace-root-relative). - match metadata - .input_config - .negative_globs - .iter() - .map(|p| Ok(wax::Glob::new(p.as_str())?.into_owned())) - .collect::>>() - { - Ok(negs) => Some(negs), - Err(err) => { - leaf_reporter.finish( - None, - CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), - Some(ExecutionError::PostRunFingerprint(err)), - ); - return SpawnOutcome::Failed; + let tracking = + if metadata.input_config.includes_auto || metadata.output_config.includes_auto { + // Resolve input negative globs for fspy path filtering + // (already workspace-root-relative). Output negatives are + // applied later in `collect_and_archive_outputs`. + let negatives = match metadata + .input_config + .negative_globs + .iter() + .map(|p| Ok(wax::Glob::new(p.as_str())?.into_owned())) + .collect::>>() + { + Ok(negs) => negs, + Err(err) => { + leaf_reporter.finish( + None, + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), + Some(ExecutionError::PostRunFingerprint(err)), + ); + return SpawnOutcome::Failed; + } + }; + // fspy + IPC are bundled. If binding the IPC server fails + // we abort the execution — tools that rely on IPC would + // otherwise silently diverge from the cache. + // + // The IPC `getEnv` endpoint serves values from the runner's + // own parent env (not the task's filtered `all_envs`), so a + // tool can ask for vars the user never declared and have + // them fingerprinted via the tool's `tracked: true` flag. + let env_map: FxHashMap, Arc> = std::env::vars_os() + .map(|(k, v)| { + (Arc::::from(k.as_os_str()), Arc::::from(v.as_os_str())) + }) + .collect(); + match serve(Recorder::new(env_map)) { + Ok((envs, ServerHandle { driver, stop_accepting })) => Some(Tracking { + input_negative_globs: negatives, + ipc_envs: envs.collect(), + ipc_server_fut: driver, + stop_accepting, + }), + Err(err) => { + leaf_reporter.finish( + None, + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), + Some(ExecutionError::IpcServerBind(err)), + ); + return SpawnOutcome::Failed; + } } - } - } else { - None - }; + } else { + None + }; ExecutionMode::Cached { pipe_writers: stdio_config.writers, - state: CacheState { - metadata, - globbed_inputs, - std_outputs: Vec::new(), - fspy_negatives: fspy, - }, + state: CacheState { metadata, globbed_inputs, std_outputs: Vec::new(), tracking }, } } None => ExecutionMode::Uncached { @@ -500,11 +545,26 @@ pub async fn execute_spawn( // 5. Derive the arguments for `spawn()` from the mode without consuming it. let (spawn_stdio, fspy_enabled) = match &mode { - ExecutionMode::Cached { state, .. } => (SpawnStdio::Piped, state.fspy_negatives.is_some()), + ExecutionMode::Cached { state, .. } => (SpawnStdio::Piped, state.tracking.is_some()), ExecutionMode::Uncached { pipe_writers: Some(_) } => (SpawnStdio::Piped, false), ExecutionMode::Uncached { pipe_writers: None } => (SpawnStdio::Inherited, false), }; + // Build the extra envs to inject: IPC connection info + napi addon path. + // Empty when tracking is off. + let extra_envs: Vec<(&OsStr, &OsStr)> = match &mode { + ExecutionMode::Cached { state: CacheState { tracking: Some(t), .. }, .. } => { + let mut envs: Vec<(&OsStr, &OsStr)> = + t.ipc_envs.iter().map(|(k, v)| (*k as &OsStr, v.as_os_str())).collect(); + envs.push(( + OsStr::new(NODE_CLIENT_PATH_ENV_NAME), + crate::napi_client::napi_client_path().as_path().as_os_str(), + )); + envs + } + _ => Vec::new(), + }; + // Measure end-to-end duration here — spawn() no longer tracks time. let start = Instant::now(); @@ -515,6 +575,7 @@ pub async fn execute_spawn( fspy_enabled, spawn_stdio, fast_fail_token.clone(), + extra_envs, ) .await { @@ -529,124 +590,271 @@ pub async fn execute_spawn( } }; - // 7. Build `PipeSinks` by borrowing into `mode`. The drain fills - // `state.std_outputs` in place (via the borrow inside `capture`), so no - // post-drain transfer is needed. `sinks` is `None` only in the - // inherited-uncached case, where there are no pipes to drain. - let sinks: Option> = match &mut mode { - ExecutionMode::Cached { pipe_writers, state } => Some(PipeSinks { - stdout_writer: &mut pipe_writers.stdout_writer, - stderr_writer: &mut pipe_writers.stderr_writer, - capture: Some(&mut state.std_outputs), - }), - ExecutionMode::Uncached { pipe_writers: Some(pipe_writers) } => Some(PipeSinks { - stdout_writer: &mut pipe_writers.stdout_writer, - stderr_writer: &mut pipe_writers.stderr_writer, - capture: None, - }), - ExecutionMode::Uncached { pipe_writers: None } => None, + // 7. Extract all mode-scoped borrows in one pass: + // - `sinks`: pipe writers + stdout capture slot (writes in place). + // - `stop_accepting` / `driver`: the IPC server's handles, if tracking + // is on. Disjoint field borrows inside the same match arm. + let (sinks, stop_accepting, ipc_server_fut) = match &mut mode { + ExecutionMode::Cached { pipe_writers, state } => { + let sinks = Some(PipeSinks { + stdout_writer: &mut pipe_writers.stdout_writer, + stderr_writer: &mut pipe_writers.stderr_writer, + capture: Some(&mut state.std_outputs), + }); + let (stop_accepting, ipc_server_fut) = match &mut state.tracking { + Some(t) => (Some(&t.stop_accepting), Some(&mut t.ipc_server_fut)), + None => (None, None), + }; + (sinks, stop_accepting, ipc_server_fut) + } + ExecutionMode::Uncached { pipe_writers: Some(pipe_writers) } => ( + Some(PipeSinks { + stdout_writer: &mut pipe_writers.stdout_writer, + stderr_writer: &mut pipe_writers.stderr_writer, + capture: None, + }), + None, + None, + ), + ExecutionMode::Uncached { pipe_writers: None } => (None, None, None), }; - if let Some(sinks) = sinks { - let stdout = child.stdout.take().expect("SpawnStdio::Piped yields a stdout pipe"); - let stderr = child.stderr.take().expect("SpawnStdio::Piped yields a stderr pipe"); - #[expect( - clippy::large_futures, - reason = "pipe_stdio streams child I/O and creates a large future" - )] - let pipe_result = pipe_stdio(stdout, stderr, sinks, fast_fail_token.clone()).await; - if let Err(err) = pipe_result { - // Cancel so `child.wait` kills the child instead of orphaning it. - fast_fail_token.cancel(); - let _ = child.wait.await; - leaf_reporter.finish( - None, - CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), - Some(ExecutionError::Spawn(err.into())), - ); - return SpawnOutcome::Failed; + // 8. Drive pipe_stdio + child.wait concurrently with the IPC driver. + // The driver must be polled during pipe drain — otherwise a tool doing + // a blocking `getEnv` can deadlock: child stalls on IPC, stdout stays + // open, pipe_stdio waits for EOF, driver never runs. `stop_accepting` + // fires after child.wait so the driver drains any in-flight clients. + let child_fast_fail = fast_fail_token.clone(); + let child_work = async move { + let pipe_result: anyhow::Result<()> = if let Some(sinks) = sinks { + let stdout = child.stdout.take().expect("SpawnStdio::Piped yields a stdout pipe"); + let stderr = child.stderr.take().expect("SpawnStdio::Piped yields a stderr pipe"); + #[expect( + clippy::large_futures, + reason = "pipe_stdio streams child I/O and creates a large future" + )] + let r = pipe_stdio(stdout, stderr, sinks, child_fast_fail.clone()).await; + r.map_err(anyhow::Error::from) + } else { + Ok(()) + }; + + let wait_result = match pipe_result { + Ok(()) => child.wait.await.map_err(anyhow::Error::from), + Err(err) => { + // Pipe failed — cancel so `child.wait` kills the child + // instead of orphaning it. Still signal the server so it + // can drain. + child_fast_fail.cancel(); + let _ = child.wait.await; + Err(err) + } + }; + + if let Some(s) = stop_accepting { + s.signal(); } - } + wait_result + }; + + // Box::pin to keep the child-and-pipe stack off the enclosing future: + // pipe_stdio alone makes the combined future large enough to trip + // clippy::large_futures on the non-driver branch. + let child_work = Box::pin(child_work); + // `None` iff tracking was off. Otherwise carries either the collected + // reports (Ok) or the IPC server's error (Err). Consumed below: Ok drops + // unused for step 5 (step 6 will use the reports); Err short-circuits + // cache update to `IpcServerError`. + let mut ipc_server_result: Option> = None; + let wait_result = match ipc_server_fut { + Some(ipc_server_fut) => { + let (wait_result, join_result) = tokio::join!(child_work, ipc_server_fut); + if let Err(e) = &join_result { + tracing::warn!(?e, "IPC server failed; cache will not be updated"); + } + ipc_server_result = Some(join_result.map(Recorder::into_reports)); + wait_result + } + None => child_work.await, + }; - // 8. Wait for exit (handles cancellation internally). - let outcome = match child.wait.await { + let outcome = match wait_result { Ok(outcome) => outcome, Err(err) => { leaf_reporter.finish( None, CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), - Some(ExecutionError::Spawn(err.into())), + Some(ExecutionError::Spawn(err)), ); return SpawnOutcome::Failed; } }; + // Extract reports, or short-circuit when the IPC server failed. An Err + // here means reports may be incomplete: caching this run would risk + // stale inputs/outputs, so skip all cache-related computation entirely. + let reports: Option = match ipc_server_result { + Some(Ok(r)) => { + tracing::debug!(?r, "runner-aware tools reported"); + Some(r) + } + None => None, + Some(Err(err)) => { + leaf_reporter.finish( + Some(outcome.exit_status), + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::IpcServerError(err)), + None, + ); + return SpawnOutcome::Spawned(outcome.exit_status); + } + }; let duration = start.elapsed(); // 9. Cache update (only when we were in `Cached` mode). Errors during cache // update are reported but do not affect the exit status we return. - let (cache_update_status, cache_error) = if let ExecutionMode::Cached { state, .. } = mode { - let CacheState { metadata, globbed_inputs, std_outputs, fspy_negatives } = state; - - // Post-execution summary of what fspy observed. `Some` iff tracking was - // both requested (`fspy_negatives.is_some()`) and compiled in (`cfg(fspy)`). - // On a `cfg(not(fspy))` build this is always `None`, and the match below - // short-circuits to `FspyUnsupported` when tracking was needed. - let tracking: Option = { - #[cfg(fspy)] - { - outcome.path_accesses.as_ref().zip(fspy_negatives.as_deref()).map(|(raw, negs)| { - let tracked = TrackedPathAccesses::from_raw(raw, cache_base_path, negs); - let read_write_overlap = tracked - .path_reads - .keys() - .find(|p| tracked.path_writes.contains(*p)) - .cloned(); - TrackingOutcome { path_reads: tracked.path_reads, read_write_overlap } - }) - } - #[cfg(not(fspy))] + let (cache_update_status, cache_error) = 'cache_update: { + if let ExecutionMode::Cached { state, .. } = mode { + let CacheState { metadata, globbed_inputs, std_outputs, tracking } = state; + let input_negative_globs = tracking.as_ref().map(|t| t.input_negative_globs.as_slice()); + + // Runner-aware tools can short-circuit caching entirely via + // `disableCache()` (e.g. a dev server with no deterministic output). + if let Some(r) = reports.as_ref() + && r.cache_disabled { - None + break 'cache_update ( + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::ToolRequested), + None, + ); } - }; - let cancelled = fast_fail_token.is_cancelled() || interrupt_token.is_cancelled(); - if cancelled { - // Cancelled (Ctrl-C or sibling failure) — result is untrustworthy - (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::Cancelled), None) - } else if outcome.exit_status.success() { - // fspy-inferred read-write overlap: the task wrote to a file it also - // read, so the prerun input hashes are stale and caching is unsound. - // (We only check fspy-inferred reads, not globbed_inputs. A task that - // writes to a glob-matched file without reading it produces perpetual - // cache misses but not a correctness bug.) - if let Some(TrackingOutcome { read_write_overlap: Some(path), .. }) = &tracking { - ( - CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::InputModified { - path: path.clone(), - }), - None, - ) - } else if tracking.is_none() && fspy_negatives.is_some() { - // Task requested fspy auto-inference but this binary was built - // without `cfg(fspy)`. Task ran, but we can't compute a valid - // cache entry without tracked path accesses. - (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::FspyUnsupported), None) - } else { - // Paths already in globbed_inputs are skipped: the overlap check - // above guarantees no input modification, so the prerun hash is - // the correct post-exec hash. - let empty_path_reads = HashMap::default(); - let path_reads = tracking.as_ref().map_or(&empty_path_reads, |t| &t.path_reads); - match PostRunFingerprint::create(path_reads, cache_base_path, &globbed_inputs) { - Ok(post_run_fingerprint) => { - // Collect output files and create archive - let output_archive = - match collect_and_archive_outputs(metadata, cache_base_path, cache_dir) + // Tool-reported paths to exclude from auto-tracking. Absolute paths + // are normalized to workspace-relative; anything outside is dropped. + let ignored_input_rels: FxHashSet = reports + .as_ref() + .map(|r| normalize_ignored_paths(&r.ignored_inputs, cache_base_path)) + .unwrap_or_default(); + let ignored_output_rels: FxHashSet = reports + .as_ref() + .map(|r| normalize_ignored_paths(&r.ignored_outputs, cache_base_path)) + .unwrap_or_default(); + + // Post-execution summary of what fspy observed. `Some` iff fspy was + // both requested (`tracking.is_some()` => input or output auto) and + // compiled in (`cfg(fspy)`). On a `cfg(not(fspy))` build this is + // always `None`, and the match below short-circuits to + // `FspyUnsupported` when tracking was needed. + // + // `path_reads` is gated on `input_config.includes_auto`, filtered + // by user-configured input negatives, and by tool-reported + // `ignoreInput` paths. When input auto is disabled (even if fspy is + // enabled for output tracking) no reads contribute to the + // fingerprint or the read-write overlap check. `path_writes` is NOT + // filtered here — output negatives and `ignoreOutput` are applied + // later inside `collect_and_archive_outputs`. Keeping the two sides + // separate avoids `input: ["!dist/**"]` accidentally dropping + // writes to `dist/**`, which would break archive restoration. + let fspy_outcome: Option = { + #[cfg(fspy)] + { + outcome.path_accesses.as_ref().map(|raw| { + let tracked = TrackedPathAccesses::from_raw(raw, cache_base_path); + let path_reads: HashMap = + if metadata.input_config.includes_auto + && let Some(negatives) = input_negative_globs { + tracked + .path_reads + .iter() + .filter(|(path, _)| { + !negatives.iter().any(|neg| neg.is_match(path.as_str())) + && !is_ignored(path, &ignored_input_rels) + }) + .map(|(path, read)| (path.clone(), *read)) + .collect() + } else { + HashMap::default() + }; + let read_write_overlap = path_reads + .keys() + .find(|p| { + tracked.path_writes.contains(*p) + && !is_ignored(p, &ignored_output_rels) + }) + .cloned(); + TrackingOutcome { + path_reads, + path_writes: tracked.path_writes, + read_write_overlap, + } + }) + } + #[cfg(not(fspy))] + { + None + } + }; + + let cancelled = fast_fail_token.is_cancelled() || interrupt_token.is_cancelled(); + if cancelled { + // Cancelled (Ctrl-C or sibling failure) — result is untrustworthy + (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::Cancelled), None) + } else if outcome.exit_status.success() { + // fspy-inferred read-write overlap: the task wrote to a file it + // also read (as an inferred input), so the prerun input hashes + // are stale and caching is unsound. Reads excluded by input + // negatives or by `ignoreInput` don't count — `path_reads` is + // already filtered. Writes excluded by `ignoreOutput` were + // discounted from the overlap check too. + if let Some(TrackingOutcome { read_write_overlap: Some(path), .. }) = &fspy_outcome + { + ( + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::InputModified { + path: path.clone(), + }), + None, + ) + } else if fspy_outcome.is_none() && tracking.is_some() { + // Task requested fspy auto-inference but this binary was built + // without `cfg(fspy)`. Task ran, but we can't compute a valid + // cache entry without tracked path accesses. + (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::FspyUnsupported), None) + } else { + // Collect tool-reported tracked envs for the post-run + // fingerprint. User-declared `env` wins — skip names that + // are already in the spawn fingerprint. + let tracked_envs = reports + .as_ref() + .map(|r| collect_tracked_envs(r, metadata)) + .unwrap_or_default(); + let tracked_env_globs = + reports.as_ref().map(collect_tracked_env_globs).unwrap_or_default(); + + // Paths already in globbed_inputs are skipped: the overlap check + // above guarantees no input modification, so the prerun hash is + // the correct post-exec hash. + let empty_path_reads = HashMap::default(); + let path_reads = + fspy_outcome.as_ref().map_or(&empty_path_reads, |o| &o.path_reads); + match PostRunFingerprint::create( + path_reads, + cache_base_path, + &globbed_inputs, + tracked_envs, + tracked_env_globs, + ) { + Ok(post_run_fingerprint) => { + // Collect output files and create archive. Tool-reported + // `ignoreOutput` paths are excluded from archiving too. + let output_archive = match collect_and_archive_outputs( + metadata, + fspy_outcome.as_ref(), + &ignored_output_rels, + cache_base_path, + cache_dir, + ) { Ok(archive) => archive, Err(err) => { - let result = ( + break 'cache_update ( CacheUpdateStatus::NotUpdated( CacheNotUpdatedReason::CacheDisabled, ), @@ -655,46 +863,43 @@ pub async fn execute_spawn( source: err, }), ); - leaf_reporter.finish( - Some(outcome.exit_status), - result.0, - result.1, - ); - return SpawnOutcome::Spawned(outcome.exit_status); } }; - let new_cache_value = CacheEntryValue { - post_run_fingerprint, - std_outputs: std_outputs.into(), - duration, - globbed_inputs, - output_archive, - }; - match cache.update(metadata, new_cache_value, cache_dir).await { - Ok(()) => (CacheUpdateStatus::Updated, None), - Err(err) => ( - CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), - Some(ExecutionError::Cache { - kind: CacheErrorKind::Update, - source: err, - }), - ), + let new_cache_value = CacheEntryValue { + post_run_fingerprint, + std_outputs: std_outputs.into(), + duration, + globbed_inputs, + output_archive, + }; + match cache.update(metadata, new_cache_value, cache_dir).await { + Ok(()) => (CacheUpdateStatus::Updated, None), + Err(err) => ( + CacheUpdateStatus::NotUpdated( + CacheNotUpdatedReason::CacheDisabled, + ), + Some(ExecutionError::Cache { + kind: CacheErrorKind::Update, + source: err, + }), + ), + } } + Err(err) => ( + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), + Some(ExecutionError::PostRunFingerprint(err)), + ), } - Err(err) => ( - CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), - Some(ExecutionError::PostRunFingerprint(err)), - ), } + } else { + // Execution failed with non-zero exit status — don't update cache + (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::NonZeroExitStatus), None) } } else { - // Execution failed with non-zero exit status — don't update cache - (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::NonZeroExitStatus), None) + // Caching was disabled for this task + (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), None) } - } else { - // Caching was disabled for this task - (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), None) }; // 7. Finish the leaf execution with the result and optional cache error. @@ -705,36 +910,141 @@ pub async fn execute_spawn( SpawnOutcome::Spawned(outcome.exit_status) } -/// Collect output files matching the configured globs and create a tar.zst -/// archive in the cache directory. +/// Normalize tool-reported absolute paths to workspace-relative. Paths outside +/// the workspace are dropped — they can't contribute to inputs or outputs. +fn normalize_ignored_paths( + paths: &FxHashSet>, + workspace_root: &AbsolutePath, +) -> FxHashSet { + paths.iter().filter_map(|p| p.strip_prefix(workspace_root).ok().flatten()).collect() +} + +/// Whether `path` is covered by any `ignored` entry. An ignored entry matches +/// itself (exact file) and everything under it (directory subtree). +fn is_ignored(path: &RelativePathBuf, ignored: &FxHashSet) -> bool { + if ignored.is_empty() { + return false; + } + if ignored.contains(path) { + return true; + } + ignored.iter().any(|ig| path.strip_prefix(ig).is_some()) +} + +/// Select tool-reported env records to embed in the post-run fingerprint. +/// Only `tracked: true` records are included, and names that the user already +/// declared as fingerprinted are skipped (their value is already in the cache +/// key via the spawn fingerprint). +fn collect_tracked_envs(reports: &Reports, metadata: &CacheMetadata) -> BTreeMap> { + let fingerprinted = &metadata.spawn_fingerprint.env_fingerprints().fingerprinted_envs; + reports + .env_records + .iter() + .filter(|(_, record)| record.tracked) + .filter_map(|(name, record)| { + let name_str = name.to_str()?; + if fingerprinted.contains_key(name_str) { + return None; + } + let value = record.value.as_ref().and_then(|v| v.to_str().map(Str::from)); + Some((Str::from(name_str), value)) + }) + .collect() +} + +/// Select tool-reported env-glob records to embed in the post-run +/// fingerprint. Only `tracked: true` records are included, and the full +/// match-set is stored as-is. /// -/// Returns `Some(archive_filename)` if files were archived, `None` if the -/// output config has no positive globs or no files matched. +/// Unlike [`collect_tracked_envs`], names already covered by the user's +/// declared `env` are *not* filtered out: lookup-time validation re-expands +/// the glob over the whole parent env (see [`PostRunFingerprint::validate`]), +/// so a filtered match-set would always diff as having `added` entries and +/// miss the cache deterministically. Storing user-declared names here is +/// harmless — a change to their value already shifts the spawn fingerprint, +/// invalidating the cache key before the post-run fingerprint is consulted. +fn collect_tracked_env_globs(reports: &Reports) -> BTreeMap> { + reports + .env_glob_records + .iter() + .filter(|(_, record)| record.tracked) + .map(|(pattern, record)| { + let matches: BTreeMap = record + .matches + .iter() + .filter_map(|(name, value)| { + let name_str = name.to_str()?; + let value_str = value.to_str()?; + Some((Str::from(name_str), Str::from(value_str))) + }) + .collect(); + (Str::from(pattern.as_ref()), matches) + }) + .collect() +} + +/// Collect output files and create a tar.zst archive in the cache directory. +/// +/// Output files are determined by: +/// - fspy-tracked writes (when `output_config.includes_auto` is true) +/// - Positive output globs (always, if configured) +/// - Filtered by negative output globs +/// - Filtered by tool-reported `ignoreOutput` paths (auto writes only) +/// +/// Returns `Some(archive_filename)` if files were archived, `None` if no output files. fn collect_and_archive_outputs( - cache_metadata: &CacheMetadata, + cache_metadata: &vite_task_plan::cache_metadata::CacheMetadata, + tracking: Option<&TrackingOutcome>, + ignored_output_rels: &FxHashSet, workspace_root: &AbsolutePath, cache_dir: &AbsolutePath, ) -> anyhow::Result> { let output_config = &cache_metadata.output_config; - if output_config.positive_globs.is_empty() { - return Ok(None); + // Collect output files from auto-detection (fspy writes), excluding + // anything the tool reported via `ignoreOutput`. + let mut output_files: FxHashSet = FxHashSet::default(); + + if output_config.includes_auto + && let Some(t) = tracking + { + output_files + .extend(t.path_writes.iter().filter(|p| !is_ignored(p, ignored_output_rels)).cloned()); } - let output_files = glob::collect_glob_paths( - workspace_root, - &output_config.positive_globs, - &output_config.negative_globs, - )?; + // Collect output files from positive globs + if !output_config.positive_globs.is_empty() { + let glob_paths = glob::collect_glob_paths( + workspace_root, + &output_config.positive_globs, + &output_config.negative_globs, + )?; + output_files.extend(glob_paths); + } + + // Apply negative globs to auto-detected files + if output_config.includes_auto && !output_config.negative_globs.is_empty() { + let negatives: Vec> = output_config + .negative_globs + .iter() + .map(|p| Ok(wax::Glob::new(p.as_str())?.into_owned())) + .collect::>()?; + output_files.retain(|path| !negatives.iter().any(|neg| neg.is_match(path.as_str()))); + } if output_files.is_empty() { return Ok(None); } + // Sort for deterministic archive content + let mut sorted_files: Vec = output_files.into_iter().collect(); + sorted_files.sort(); + + // Create archive with UUID filename let archive_name: Str = vite_str::format!("{}.tar.zst", uuid::Uuid::new_v4()); let archive_path = cache_dir.join(archive_name.as_str()); - archive::create_output_archive(workspace_root, &output_files, &archive_path)?; + archive::create_output_archive(workspace_root, &sorted_files, &archive_path)?; Ok(Some(archive_name)) } diff --git a/crates/vite_task/src/session/execute/spawn.rs b/crates/vite_task/src/session/execute/spawn.rs index 2ed64fc83..711903373 100644 --- a/crates/vite_task/src/session/execute/spawn.rs +++ b/crates/vite_task/src/session/execute/spawn.rs @@ -5,7 +5,7 @@ //! job; normalizing fspy path accesses is [`super::tracked_accesses`]'s (only //! compiled when `cfg(fspy)` is on). -use std::{io, process::Stdio}; +use std::{ffi::OsStr, io, process::Stdio}; #[cfg(fspy)] use fspy::PathAccessIterable; @@ -48,21 +48,31 @@ pub struct ChildOutcome { /// Spawn a command with the requested fspy and stdio configuration. /// +/// `extra_envs` are applied **after** `cmd.all_envs`, so runtime-injected +/// entries (e.g. the runner's IPC name + napi addon path) override any +/// same-named key from the plan. +/// /// Cancellation is unified: whether fspy is enabled or not, the returned `wait` /// future observes `cancellation_token` and kills the child before resolving. /// /// On builds without `cfg(fspy)`, the `fspy` argument is ignored and the tokio /// path is always taken. #[tracing::instrument(level = "debug", skip_all)] -pub async fn spawn( +pub async fn spawn( cmd: &SpawnCommand, fspy: bool, stdio: SpawnStdio, cancellation_token: CancellationToken, -) -> anyhow::Result { + extra_envs: E, +) -> anyhow::Result +where + E: IntoIterator, + K: AsRef, + V: AsRef, +{ #[cfg(fspy)] if fspy { - return spawn_fspy(cmd, stdio, cancellation_token).await; + return spawn_fspy(cmd, stdio, cancellation_token, extra_envs).await; } #[cfg(not(fspy))] let _ = fspy; @@ -71,20 +81,28 @@ pub async fn spawn( tokio_cmd.args(cmd.args.iter().map(vite_str::Str::as_str)); tokio_cmd.env_clear(); tokio_cmd.envs(cmd.all_envs.iter()); + tokio_cmd.envs(extra_envs); tokio_cmd.current_dir(&*cmd.cwd); apply_stdio(&mut tokio_cmd, stdio); spawn_tokio(tokio_cmd, cancellation_token) } #[cfg(fspy)] -async fn spawn_fspy( +async fn spawn_fspy( cmd: &SpawnCommand, stdio: SpawnStdio, cancellation_token: CancellationToken, -) -> anyhow::Result { + extra_envs: E, +) -> anyhow::Result +where + E: IntoIterator, + K: AsRef, + V: AsRef, +{ let mut fspy_cmd = fspy::Command::new(cmd.program_path.as_path()); fspy_cmd.args(cmd.args.iter().map(vite_str::Str::as_str)); fspy_cmd.envs(cmd.all_envs.iter()); + fspy_cmd.envs(extra_envs); fspy_cmd.current_dir(&*cmd.cwd); match stdio { diff --git a/crates/vite_task/src/session/execute/tracked_accesses.rs b/crates/vite_task/src/session/execute/tracked_accesses.rs index 83596cc7d..6e3596512 100644 --- a/crates/vite_task/src/session/execute/tracked_accesses.rs +++ b/crates/vite_task/src/session/execute/tracked_accesses.rs @@ -1,4 +1,8 @@ //! Normalize raw fspy path accesses into workspace-relative, filtered form. +//! +//! User-configured negative globs are NOT applied here. They are applied later, +//! separately for reads (input config) and writes (output config), since those +//! two configs are independent. #![cfg(fspy)] use std::collections::hash_map::Entry; @@ -21,22 +25,19 @@ pub struct TrackedPathAccesses { } impl TrackedPathAccesses { - /// Build from fspy's raw iterable by stripping the workspace prefix, - /// normalizing `..` components, and filtering against the negative globs. - pub fn from_raw( - raw: &PathAccessIterable, - workspace_root: &AbsolutePath, - resolved_negatives: &[wax::Glob<'static>], - ) -> Self { + /// Build from fspy's raw iterable by stripping the workspace prefix and + /// normalizing `..` components. `.git/*` paths are skipped. User-configured + /// negatives are applied by the caller (see module docs). + pub fn from_raw(raw: &PathAccessIterable, workspace_root: &AbsolutePath) -> Self { let mut accesses = Self::default(); for access in raw.iter() { - // Strip workspace root, clean `..` components, and filter in one pass. + // Strip workspace root and clean `..` components in one pass. // fspy may report paths like `packages/sub-pkg/../shared/dist/output.js`. let relative_path = access.path.strip_path_prefix(workspace_root, |strip_result| { let Ok(stripped_path) = strip_result else { return None; }; - normalize_tracked_workspace_path(stripped_path, resolved_negatives) + normalize_tracked_workspace_path(stripped_path) }); let Some(relative_path) = relative_path else { @@ -71,10 +72,7 @@ impl TrackedPathAccesses { clippy::disallowed_types, reason = "fspy strip_path_prefix exposes std::path::Path; convert to RelativePathBuf immediately" )] -fn normalize_tracked_workspace_path( - stripped_path: &std::path::Path, - resolved_negatives: &[wax::Glob<'static>], -) -> Option { +fn normalize_tracked_workspace_path(stripped_path: &std::path::Path) -> Option { // On Windows, paths are possible to be still absolute after stripping the workspace root. // For example: c:\workspace\subdir\c:\workspace\subdir // Just ignore those accesses. @@ -90,12 +88,6 @@ fn normalize_tracked_workspace_path( return None; } - if !resolved_negatives.is_empty() - && resolved_negatives.iter().any(|neg| wax::Program::is_match(neg, relative.as_str())) - { - return None; - } - Some(relative) } @@ -111,8 +103,7 @@ mod tests { clippy::disallowed_types, reason = "normalize_tracked_workspace_path requires std::path::Path for fspy strip_path_prefix output" )] - let relative_path = - normalize_tracked_workspace_path(std::path::Path::new(r"foo\C:\bar"), &[]); + let relative_path = normalize_tracked_workspace_path(std::path::Path::new(r"foo\C:\bar")); assert!(relative_path.is_none()); } } diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index ed1fd5cb8..9491b824c 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -666,10 +666,6 @@ impl<'a> Session<'a> { /// /// Returns an error if planning or execution of the synthetic command fails. #[tracing::instrument(level = "debug", skip_all)] - #[expect( - clippy::large_futures, - reason = "execution plan future is large but only awaited once" - )] pub async fn execute_synthetic( &self, synthetic_plan_request: SyntheticPlanRequest, diff --git a/crates/vite_task/src/session/reporter/summary.rs b/crates/vite_task/src/session/reporter/summary.rs index f1a01168f..531c981f4 100644 --- a/crates/vite_task/src/session/reporter/summary.rs +++ b/crates/vite_task/src/session/reporter/summary.rs @@ -107,6 +107,12 @@ pub enum SpawnOutcome { /// Task ran successfully but cache was not updated. #[serde(default)] fspy_unsupported: bool, + /// Rendered message of the IPC server error that caused the cache to + /// be skipped, if any. + ipc_server_error: Option, + /// Set when a runner-aware tool called `disableCache()`, skipping + /// cache update. + tool_disabled_cache: bool, }, /// Process exited with non-zero status. @@ -129,6 +135,11 @@ pub enum SavedCacheMissReason { ConfigChanged, /// An input file or folder changed. InputChanged { kind: InputChangeKind, path: Str }, + /// A runner-aware tool reported a tracked env var that changed between runs. + TrackedEnvChanged { name: Str, old: Option, new: Option }, + /// A runner-aware tool reported a tracked env glob whose match-set changed + /// between runs. + TrackedEnvGlobChanged { pattern: Str, diff: crate::session::execute::fingerprint::EnvGlobDiff }, } /// An execution error, serializable for persistence. @@ -141,6 +152,7 @@ pub enum SavedExecutionError { Cache { kind: SavedCacheErrorKind, message: Str }, Spawn { message: Str }, PostRunFingerprint { message: Str }, + IpcServerBind { message: Str }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -228,6 +240,9 @@ impl SavedExecutionError { ExecutionError::PostRunFingerprint(source) => { Self::PostRunFingerprint { message: vite_str::format!("{source:#}") } } + ExecutionError::IpcServerBind(source) => { + Self::IpcServerBind { message: vite_str::format!("{source:#}") } + } } } @@ -247,6 +262,7 @@ impl SavedExecutionError { Self::PostRunFingerprint { message } => { vite_str::format!("Failed to create post-run fingerprint: {message}") } + Self::IpcServerBind { .. } => Str::from("Failed to start runner IPC server"), } } } @@ -265,6 +281,16 @@ impl SavedCacheMissReason { FingerprintMismatch::InputChanged { kind, path } => { Self::InputChanged { kind: *kind, path: Str::from(path.as_str()) } } + FingerprintMismatch::TrackedEnvChanged { name, old, new } => { + Self::TrackedEnvChanged { + name: name.clone(), + old: old.clone(), + new: new.clone(), + } + } + FingerprintMismatch::TrackedEnvGlobChanged { pattern, diff } => { + Self::TrackedEnvGlobChanged { pattern: pattern.clone(), diff: diff.clone() } + } }, } } @@ -293,6 +319,16 @@ impl TaskResult { cache_update_status, CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::FspyUnsupported) ); + let ipc_server_error = match cache_update_status { + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::IpcServerError(err)) => { + Some(vite_str::format!("{err}")) + } + _ => None, + }; + let tool_disabled_cache = matches!( + cache_update_status, + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::ToolRequested) + ); match cache_status { CacheStatus::Hit { replayed_duration } => { @@ -306,6 +342,8 @@ impl TaskResult { saved_error, input_modified_path, fspy_unsupported, + ipc_server_error, + tool_disabled_cache, ), }, CacheStatus::Miss(cache_miss) => Self::Spawned { @@ -317,6 +355,8 @@ impl TaskResult { saved_error, input_modified_path, fspy_unsupported, + ipc_server_error, + tool_disabled_cache, ), }, } @@ -329,6 +369,8 @@ fn spawn_outcome_from_execution( saved_error: Option<&SavedExecutionError>, input_modified_path: Option, fspy_unsupported: bool, + ipc_server_error: Option, + tool_disabled_cache: bool, ) -> SpawnOutcome { match (exit_status, saved_error) { // Spawn error — process never ran @@ -338,6 +380,8 @@ fn spawn_outcome_from_execution( infra_error: saved_error.cloned(), input_modified_path, fspy_unsupported, + ipc_server_error, + tool_disabled_cache, }, // Process exited with non-zero code (Some(status), _) => { @@ -356,6 +400,8 @@ fn spawn_outcome_from_execution( infra_error: None, input_modified_path: None, fspy_unsupported: false, + ipc_server_error: None, + tool_disabled_cache: false, }, } } @@ -467,7 +513,26 @@ impl TaskResult { /// - "→ Cache miss: no previous cache entry found" /// - "→ Cache disabled in task configuration" fn format_cache_detail(&self) -> Str { - // Check for input modification first — it overrides the cache miss reason + // Check for IPC server error first — short-circuits before any cache + // computation in `execute_spawn`, so it takes priority. + if let Self::Spawned { + outcome: SpawnOutcome::Success { ipc_server_error: Some(err), .. }, + .. + } = self + { + return vite_str::format!("→ Not cached: IPC server error: {err}"); + } + + // Tool-reported cache disable — the tool said it shouldn't be cached. + if let Self::Spawned { + outcome: SpawnOutcome::Success { tool_disabled_cache: true, .. }, + .. + } = self + { + return Str::from("→ Not cached: tool requested disableCache"); + } + + // Check for input modification next — it overrides the cache miss reason if let Self::Spawned { outcome: SpawnOutcome::Success { input_modified_path: Some(path), .. }, .. @@ -515,6 +580,12 @@ impl TaskResult { let desc = format_input_change_str(*kind, path.as_str()); vite_str::format!("→ Cache miss: {desc}") } + SavedCacheMissReason::TrackedEnvChanged { name, .. } => { + vite_str::format!("→ Cache miss: tracked env '{name}' changed") + } + SavedCacheMissReason::TrackedEnvGlobChanged { pattern, .. } => { + vite_str::format!("→ Cache miss: tracked env glob '{pattern}' changed") + } }, }, } diff --git a/crates/vite_task_bin/src/vtt/grep_file.rs b/crates/vite_task_bin/src/vtt/grep_file.rs new file mode 100644 index 000000000..50b3281b3 --- /dev/null +++ b/crates/vite_task_bin/src/vtt/grep_file.rs @@ -0,0 +1,16 @@ +pub fn run(args: &[String]) { + let [path, pattern] = args else { + eprintln!("Usage: vtt grep-file "); + std::process::exit(2); + }; + match std::fs::read_to_string(path) { + Ok(content) => { + if content.contains(pattern.as_str()) { + println!("{path}: found {pattern:?}"); + } else { + println!("{path}: missing {pattern:?}"); + } + } + Err(_) => println!("{path}: not found"), + } +} diff --git a/crates/vite_task_bin/src/vtt/main.rs b/crates/vite_task_bin/src/vtt/main.rs index d6dcb1af7..c2d3e2156 100644 --- a/crates/vite_task_bin/src/vtt/main.rs +++ b/crates/vite_task_bin/src/vtt/main.rs @@ -11,6 +11,7 @@ mod check_tty; mod cp; mod exit; mod exit_on_ctrlc; +mod grep_file; mod list_dir; mod mkdir; mod pipe_stdin; @@ -22,6 +23,7 @@ mod print_file; mod read_stdin; mod replace_file_content; mod rm; +mod stat_file; mod touch_file; mod write_file; @@ -30,7 +32,7 @@ fn main() { if args.len() < 2 { eprintln!("Usage: vtt [args...]"); eprintln!( - "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, touch-file, write-file" + "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, stat-file, touch-file, write-file" ); std::process::exit(1); } @@ -44,6 +46,10 @@ fn main() { "cp" => cp::run(&args[2..]), "exit" => exit::run(&args[2..]), "exit-on-ctrlc" => exit_on_ctrlc::run(), + "grep-file" => { + grep_file::run(&args[2..]); + Ok(()) + } "list-dir" => list_dir::run(&args[2..]), "mkdir" => mkdir::run(&args[2..]), "pipe-stdin" => pipe_stdin::run(&args[2..]), @@ -58,6 +64,10 @@ fn main() { "read-stdin" => read_stdin::run(), "replace-file-content" => replace_file_content::run(&args[2..]), "rm" => rm::run(&args[2..]), + "stat-file" => { + stat_file::run(&args[2..]); + Ok(()) + } "touch-file" => touch_file::run(&args[2..]), "write-file" => write_file::run(&args[2..]), other => { diff --git a/crates/vite_task_bin/src/vtt/stat_file.rs b/crates/vite_task_bin/src/vtt/stat_file.rs new file mode 100644 index 000000000..75fbebf4c --- /dev/null +++ b/crates/vite_task_bin/src/vtt/stat_file.rs @@ -0,0 +1,9 @@ +pub fn run(args: &[String]) { + for file in args { + if std::fs::metadata(file).is_ok() { + println!("{file}: exists"); + } else { + println!("{file}: missing"); + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/input_cache_test/snapshots/fspy_env___not_set_when_auto_inference_disabled.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/input_cache_test/snapshots/fspy_env___not_set_when_auto_inference_disabled.md index 256db2ea9..e68888bf0 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/input_cache_test/snapshots/fspy_env___not_set_when_auto_inference_disabled.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/input_cache_test/snapshots/fspy_env___not_set_when_auto_inference_disabled.md @@ -7,5 +7,5 @@ should not see `FSPY` set. ``` $ vtt print-env FSPY -(undefined) +1 ``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/package.json new file mode 100644 index 000000000..be1e0be88 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/package.json @@ -0,0 +1,5 @@ +{ + "name": "ipc-client-test", + "private": true, + "type": "module" +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/disable_cache.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/disable_cache.mjs new file mode 100644 index 000000000..f868cef50 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/disable_cache.mjs @@ -0,0 +1,8 @@ +import { disableCache } from '@voidzero-dev/vite-task-client'; +import { writeFileSync, mkdirSync } from 'node:fs'; + +// Produce an output, then ask the runner not to cache this execution — the +// next `vt run` should re-execute the task. +mkdirSync('dist', { recursive: true }); +writeFileSync('dist/out.txt', 'ok\n'); +disableCache(); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_env.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_env.mjs new file mode 100644 index 000000000..a3840c61c --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_env.mjs @@ -0,0 +1,11 @@ +import { getEnv } from '@voidzero-dev/vite-task-client'; +import { writeFileSync, mkdirSync } from 'node:fs'; + +// getEnv populates process.env from the runner and — with tracked: true — +// adds the env to the post-run fingerprint, so a change between runs +// invalidates the cache. +getEnv('PROBE_ENV', { tracked: true }); +const value = process.env.PROBE_ENV ?? '(unset)'; + +mkdirSync('dist', { recursive: true }); +writeFileSync('dist/out.txt', 'PROBE_ENV=' + value + '\n'); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs new file mode 100644 index 000000000..b89f2cc0b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs @@ -0,0 +1,14 @@ +import { getEnvs } from '@voidzero-dev/vite-task-client'; +import { writeFileSync, mkdirSync } from 'node:fs'; + +// getEnvs asks the runner for every env matching the glob. The glob (plus +// its match-set) becomes part of the post-run fingerprint, so adding, +// removing, or changing any matching env invalidates the cache on the next +// run. The non-matching UNRELATED envs set by some test steps must not +// contribute. +const matches = getEnvs('PROBE_*', { tracked: true }); + +mkdirSync('dist', { recursive: true }); +const sorted = Object.entries(matches).sort(([a], [b]) => a.localeCompare(b)); +const body = sorted.map(([k, v]) => `${k}=${v}`).join('\n'); +writeFileSync('dist/out.txt', body + '\n'); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/ignore_input.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/ignore_input.mjs new file mode 100644 index 000000000..e368bb9c9 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/ignore_input.mjs @@ -0,0 +1,15 @@ +import { ignoreInput } from '@voidzero-dev/vite-task-client'; +import { writeFileSync, readFileSync } from 'node:fs'; +import { mkdirSync } from 'node:fs'; + +// The task reads from `cache_like/` (which we want the runner to IGNORE as +// an input), and writes to `dist/`. Without the ignore, the auto-input +// fingerprint would fluctuate with cache_like/ contents even though they're +// not semantic inputs. +mkdirSync('cache_like', { recursive: true }); +writeFileSync('cache_like/stale.txt', 'stale-' + Date.now() + '\n'); +ignoreInput('cache_like'); +readFileSync('cache_like/stale.txt', 'utf8'); + +mkdirSync('dist', { recursive: true }); +writeFileSync('dist/out.txt', 'ok\n'); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/ignore_output.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/ignore_output.mjs new file mode 100644 index 000000000..efb1aa169 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/ignore_output.mjs @@ -0,0 +1,14 @@ +import { ignoreOutput } from '@voidzero-dev/vite-task-client'; +import { writeFileSync, readFileSync, mkdirSync } from 'node:fs'; + +// The task both reads and writes `sidecar/tmp.txt`. If the runner didn't +// treat `sidecar/` as an ignored output, the read-write overlap check would +// refuse to cache the task. `dist/out.txt` is the real output. +mkdirSync('sidecar', { recursive: true }); +writeFileSync('sidecar/tmp.txt', 'initial\n'); +readFileSync('sidecar/tmp.txt', 'utf8'); +writeFileSync('sidecar/tmp.txt', 'final\n'); +ignoreOutput('sidecar'); + +mkdirSync('dist', { recursive: true }); +writeFileSync('dist/out.txt', 'ok\n'); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml new file mode 100644 index 000000000..2a7fa2b6a --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml @@ -0,0 +1,252 @@ +[[e2e]] +name = "ignore_input_keeps_cache_valid" +comment = """ +Exercises `ignoreInput` through `@voidzero-dev/vite-task-client`. +The runner treats `cache_like/` as non-input, so mutations to it between +runs do not invalidate the cache. +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "ignore-input", + ], comment = "populate the cache" }, + { argv = [ + "vtt", + "write-file", + "cache_like/other.txt", + "after", + ], comment = "mutate the ignored directory — would invalidate if tracked" }, + { argv = [ + "vt", + "run", + "ignore-input", + ], comment = "cache hit: cache_like/ was ignored via ignoreInput" }, +] + +[[e2e]] +name = "ignore_output_allows_read_write_overlap" +comment = """ +Exercises `ignoreOutput`. The task reads and writes `sidecar/tmp.txt`; +without the ignore the runner's read-write overlap check would refuse to +cache the run ("read and wrote 'sidecar/tmp.txt'"). +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "ignore-output", + ], comment = "first run populates the cache" }, + { argv = [ + "vtt", + "rm", + "dist/out.txt", + ], comment = "remove the real output so the cache-hit restore is observable" }, + { argv = [ + "vt", + "run", + "ignore-output", + ], comment = "cache hit: sidecar/ writes were ignored" }, + { argv = [ + "vtt", + "print-file", + "dist/out.txt", + ], comment = "restored from the cache archive" }, +] + +[[e2e]] +name = "disable_cache_forces_reexecution" +comment = """ +Exercises `disableCache`. The tool asks the runner not to cache this run, +so the next invocation re-executes instead of hitting a prior entry. +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "disable-cache", + ], comment = "first run — tool calls disableCache" }, + { argv = [ + "vt", + "run", + "disable-cache", + ], comment = "cache miss (NotFound) because nothing was cached" }, +] + +[[e2e]] +name = "fetch_envs_tracks_glob_match_set" +comment = """ +Exercises `getEnvs(pattern, { tracked: true })`. The glob `PROBE_*` and +its match-set snapshot enter the post-run fingerprint: later runs diff the +current match-set against what was stored and miss on add / remove / change, +but hit when only non-matching envs differ. +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "fetch-envs", + ], envs = [ + [ + "PROBE_A", + "a", + ], + [ + "PROBE_B", + "b", + ], + ], comment = "populate: first run captures {PROBE_A, PROBE_B} under the glob" }, + { argv = [ + "vtt", + "print-file", + "dist/out.txt", + ], comment = "the tool observed both matching envs" }, + { argv = [ + "vt", + "run", + "fetch-envs", + ], envs = [ + [ + "PROBE_A", + "a", + ], + [ + "PROBE_B", + "b", + ], + ], comment = "unchanged: same match-set → cache hit" }, + { argv = [ + "vt", + "run", + "fetch-envs", + ], envs = [ + [ + "PROBE_A", + "changed", + ], + [ + "PROBE_B", + "b", + ], + ], comment = "change: PROBE_A value differs → cache miss (TrackedEnvGlobChanged / changed)" }, + { argv = [ + "vt", + "run", + "fetch-envs", + ], envs = [ + [ + "PROBE_A", + "changed", + ], + [ + "PROBE_B", + "b", + ], + [ + "PROBE_C", + "c", + ], + ], comment = "add: PROBE_C is new under the glob → cache miss (added)" }, + { argv = [ + "vt", + "run", + "fetch-envs", + ], envs = [ + [ + "PROBE_B", + "b", + ], + [ + "PROBE_C", + "c", + ], + ], comment = "remove: PROBE_A dropped from the match-set → cache miss (removed)" }, + { argv = [ + "vt", + "run", + "fetch-envs", + ], envs = [ + [ + "PROBE_B", + "b", + ], + [ + "PROBE_C", + "c", + ], + [ + "UNRELATED", + "noise", + ], + ], comment = "non-matching noise: UNRELATED doesn't match PROBE_* → cache hit" }, + { argv = [ + "vtt", + "print-file", + "dist/out.txt", + ], comment = "match-set unchanged from the previous successful run" }, +] + +[[e2e]] +name = "fetch_env_tracked_invalidates_on_change" +comment = """ +Exercises `getEnv(name, { tracked: true })`. The env value becomes part +of the post-run fingerprint: the same value still hits, a different value +misses with `tracked env 'PROBE_ENV' changed`. +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "fetch-env", + ], envs = [ + [ + "PROBE_ENV", + "first", + ], + ], comment = "first run captures PROBE_ENV=first in the fingerprint" }, + { argv = [ + "vt", + "run", + "fetch-env", + ], envs = [ + [ + "PROBE_ENV", + "first", + ], + ], comment = "cache hit: PROBE_ENV unchanged" }, + { argv = [ + "vt", + "run", + "fetch-env", + ], envs = [ + [ + "PROBE_ENV", + "second", + ], + ], comment = "cache miss: tracked env changed" }, +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_forces_reexecution.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_forces_reexecution.md new file mode 100644 index 000000000..150158547 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_forces_reexecution.md @@ -0,0 +1,20 @@ +# disable_cache_forces_reexecution + +Exercises `disableCache`. The tool asks the runner not to cache this run, +so the next invocation re-executes instead of hitting a prior entry. + +## `vt run disable-cache` + +first run — tool calls disableCache + +``` +$ node scripts/disable_cache.mjs +``` + +## `vt run disable-cache` + +cache miss (NotFound) because nothing was cached + +``` +$ node scripts/disable_cache.mjs +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_env_tracked_invalidates_on_change.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_env_tracked_invalidates_on_change.md new file mode 100644 index 000000000..4f808d85a --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_env_tracked_invalidates_on_change.md @@ -0,0 +1,32 @@ +# fetch_env_tracked_invalidates_on_change + +Exercises `getEnv(name, { tracked: true })`. The env value becomes part +of the post-run fingerprint: the same value still hits, a different value +misses with `tracked env 'PROBE_ENV' changed`. + +## `PROBE_ENV=first vt run fetch-env` + +first run captures PROBE_ENV=first in the fingerprint + +``` +$ node scripts/fetch_env.mjs +``` + +## `PROBE_ENV=first vt run fetch-env` + +cache hit: PROBE_ENV unchanged + +``` +$ node scripts/fetch_env.mjs ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `PROBE_ENV=second vt run fetch-env` + +cache miss: tracked env changed + +``` +$ node scripts/fetch_env.mjs ○ cache miss: tracked env 'PROBE_ENV' changed, executing +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_tracks_glob_match_set.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_tracks_glob_match_set.md new file mode 100644 index 000000000..b611d515f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_tracks_glob_match_set.md @@ -0,0 +1,78 @@ +# fetch_envs_tracks_glob_match_set + +Exercises `getEnvs(pattern, { tracked: true })`. The glob `PROBE_*` and +its match-set snapshot enter the post-run fingerprint: later runs diff the +current match-set against what was stored and miss on add / remove / change, +but hit when only non-matching envs differ. + +## `PROBE_A=a PROBE_B=b vt run fetch-envs` + +populate: first run captures {PROBE_A, PROBE_B} under the glob + +``` +$ node scripts/fetch_envs.mjs +``` + +## `vtt print-file dist/out.txt` + +the tool observed both matching envs + +``` +PROBE_A=a +PROBE_B=b +``` + +## `PROBE_A=a PROBE_B=b vt run fetch-envs` + +unchanged: same match-set → cache hit + +``` +$ node scripts/fetch_envs.mjs ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `PROBE_A=changed PROBE_B=b vt run fetch-envs` + +change: PROBE_A value differs → cache miss (TrackedEnvGlobChanged / changed) + +``` +$ node scripts/fetch_envs.mjs ○ cache miss: tracked env glob 'PROBE_*' changed, executing +``` + +## `PROBE_A=changed PROBE_B=b PROBE_C=c vt run fetch-envs` + +add: PROBE_C is new under the glob → cache miss (added) + +``` +$ node scripts/fetch_envs.mjs ○ cache miss: tracked env glob 'PROBE_*' changed, executing +``` + +## `PROBE_B=b PROBE_C=c vt run fetch-envs` + +remove: PROBE_A dropped from the match-set → cache miss (removed) + +``` +$ node scripts/fetch_envs.mjs ○ cache miss: tracked env glob 'PROBE_*' changed, executing +``` + +## `PROBE_B=b PROBE_C=c UNRELATED=noise vt run fetch-envs` + +non-matching noise: UNRELATED doesn't match PROBE_* → cache hit + +``` +$ node scripts/fetch_envs.mjs ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `vtt print-file dist/out.txt` + +match-set unchanged from the previous successful run + +``` +PROBE_B=b +PROBE_C=c +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/ignore_input_keeps_cache_valid.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/ignore_input_keeps_cache_valid.md new file mode 100644 index 000000000..d624c0c4f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/ignore_input_keeps_cache_valid.md @@ -0,0 +1,31 @@ +# ignore_input_keeps_cache_valid + +Exercises `ignoreInput` through `@voidzero-dev/vite-task-client`. +The runner treats `cache_like/` as non-input, so mutations to it between +runs do not invalidate the cache. + +## `vt run ignore-input` + +populate the cache + +``` +$ node scripts/ignore_input.mjs +``` + +## `vtt write-file cache_like/other.txt after` + +mutate the ignored directory — would invalidate if tracked + +``` +``` + +## `vt run ignore-input` + +cache hit: cache_like/ was ignored via ignoreInput + +``` +$ node scripts/ignore_input.mjs ◉ cache hit, replaying + +--- +vt run: cache hit. +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/ignore_output_allows_read_write_overlap.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/ignore_output_allows_read_write_overlap.md new file mode 100644 index 000000000..70ee4002c --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/ignore_output_allows_read_write_overlap.md @@ -0,0 +1,39 @@ +# ignore_output_allows_read_write_overlap + +Exercises `ignoreOutput`. The task reads and writes `sidecar/tmp.txt`; +without the ignore the runner's read-write overlap check would refuse to +cache the run ("read and wrote 'sidecar/tmp.txt'"). + +## `vt run ignore-output` + +first run populates the cache + +``` +$ node scripts/ignore_output.mjs +``` + +## `vtt rm dist/out.txt` + +remove the real output so the cache-hit restore is observable + +``` +``` + +## `vt run ignore-output` + +cache hit: sidecar/ writes were ignored + +``` +$ node scripts/ignore_output.mjs ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `vtt print-file dist/out.txt` + +restored from the cache archive + +``` +ok +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json new file mode 100644 index 000000000..7b7ddc90b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json @@ -0,0 +1,24 @@ +{ + "tasks": { + "ignore-input": { + "command": "node scripts/ignore_input.mjs", + "cache": true + }, + "ignore-output": { + "command": "node scripts/ignore_output.mjs", + "cache": true + }, + "disable-cache": { + "command": "node scripts/disable_cache.mjs", + "cache": true + }, + "fetch-env": { + "command": "node scripts/fetch_env.mjs", + "cache": true + }, + "fetch-envs": { + "command": "node scripts/fetch_envs.mjs", + "cache": true + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/README.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/README.md new file mode 100644 index 000000000..626799f0f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/README.md @@ -0,0 +1 @@ +v1 diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml index dc6c2fc0d..a170c1ef2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml @@ -1,121 +1,226 @@ [[e2e]] -name = "output_globs___files_restored_on_cache_hit" +name = "auto_output___files_restored_on_cache_hit" comment = """ -With explicit output globs (`dist/**`), the first run writes a file to -`dist/`. After deleting `dist/`, a second run with no input changes is a -cache hit and the archived output file is restored. +Auto output detection (default): output files written by the task are +restored on a cache hit. """ steps = [ { argv = [ "vt", "run", - "build", - ], comment = "first run — cache miss, writes dist/output.txt" }, + "auto-output", + ], comment = "first run writes dist/out.txt" }, { argv = [ "vtt", "print-file", - "dist/output.txt", - ], comment = "file is on disk after the run" }, + "dist/out.txt", + ], comment = "output exists after the build" }, { argv = [ "vtt", "rm", - "-rf", - "dist", - ], comment = "delete dist/ to prove the restore is real" }, + "dist/out.txt", + ], comment = "delete only the file (keep dir to avoid an fspy inferred-input miss on Windows)" }, { argv = [ "vt", "run", - "build", - ], comment = "second run — cache hit, restores from archive" }, + "auto-output", + ], comment = "cache hit, should restore dist/out.txt" }, { argv = [ "vtt", "print-file", - "dist/output.txt", - ], comment = "file restored from archive" }, + "dist/out.txt", + ], comment = "output was restored from the archive" }, ] [[e2e]] -name = "output_globs___old_archive_removed_on_rewrite" +name = "glob_output___only_matched_files_restored" comment = """ -When a cached task re-runs (cache miss because an input changed), it -writes a new archive and the previous archive file is cleaned up. After -two cache-missing runs of the same task the cache directory still -contains only one `.tar.zst` archive. +Glob output: only files matching the configured output globs are restored +on a cache hit; files produced outside those globs are left alone. """ steps = [ { argv = [ "vt", "run", - "build", - ], comment = "first run — cache miss, writes archive A" }, + "glob-output", + ], comment = "first run writes dist/out.txt and tmp/temp.txt" }, { argv = [ "vtt", - "list-dir", - "node_modules/.vite/task-cache", - "--ext", - ".tar.zst", - ], comment = "exactly one archive on disk" }, + "rm", + "dist/out.txt", + ], comment = "delete the glob-matched output" }, { argv = [ "vtt", - "write-file", - "src/main.ts", - "changed", - ], comment = "modify an input so the next run is a cache miss" }, + "rm", + "tmp/temp.txt", + ], comment = "delete the non-matched output" }, { argv = [ "vt", "run", - "build", - ], comment = "second run — cache miss, writes archive B and removes A" }, + "glob-output", + ], comment = "cache hit: should restore dist/out.txt but not tmp/temp.txt" }, + { argv = [ + "vtt", + "print-file", + "dist/out.txt", + ], comment = "dist/out.txt was restored" }, { argv = [ "vtt", - "list-dir", - "node_modules/.vite/task-cache", - "--ext", - ".tar.zst", - ], comment = "still exactly one archive — A was cleaned up" }, + "print-file", + "tmp/temp.txt", + ], comment = "should fail - tmp not in output globs" }, ] [[e2e]] -name = "output_globs___negative_excludes_files_from_archive" +name = "auto_output_with_non_auto_input" comment = """ -A file matched by a negative output glob is not archived, so it is not -restored on cache hit. +Auto output works even when input tracking is explicit +(`input: [\"src/**\"]`). Output auto-detection and input auto-detection +are independent. """ steps = [ { argv = [ "vt", "run", - "build-with-negative", - ], comment = "first run — writes dist/keep.txt and dist/skip.txt" }, + "auto-output-no-auto-input", + ], comment = "first run: input src/** (no auto), output default (auto)" }, { argv = [ "vtt", - "print-file", - "dist/keep.txt", - ], comment = "keep.txt was written" }, + "rm", + "dist/out.txt", + ], comment = "delete the output" }, + { argv = [ + "vt", + "run", + "auto-output-no-auto-input", + ], comment = "cache hit - output files should still be restored" }, { argv = [ "vtt", "print-file", - "dist/skip.txt", - ], comment = "skip.txt was written" }, + "dist/out.txt", + ], comment = "output was restored" }, +] + +[[e2e]] +name = "negative_output___excluded_files_not_restored" +comment = """ +Negative output globs exclude matching files from the cache archive, so +they are not restored on a cache hit even though the task produced them. +""" +steps = [ + { argv = [ + "vt", + "run", + "negative-output", + ], comment = "first run writes dist/out.txt and dist/cache/tmp.txt" }, + { argv = [ + "vtt", + "rm", + "dist/out.txt", + ], comment = "delete the archived output" }, { argv = [ "vtt", "rm", - "-rf", - "dist", - ], comment = "delete dist/ to prove the restore is real" }, + "dist/cache/tmp.txt", + ], comment = "delete the excluded output" }, { argv = [ "vt", "run", - "build-with-negative", - ], comment = "second run — cache hit, restores from archive" }, + "negative-output", + ], comment = "cache hit: should restore dist/out.txt but NOT dist/cache/tmp.txt" }, { argv = [ "vtt", "print-file", - "dist/keep.txt", - ], comment = "keep.txt restored from archive" }, + "dist/out.txt", + ], comment = "dist/out.txt was restored" }, { argv = [ "vtt", "print-file", - "dist/skip.txt", - ], comment = "skip.txt is NOT restored — it was excluded by the negative glob" }, + "dist/cache/tmp.txt", + ], comment = "should fail - excluded by !dist/cache/**" }, +] + +[[e2e]] +name = "output_config_change_invalidates_cache" +comment = """ +Changing a task's output configuration invalidates the cache entry — +the next run is a miss, not a hit of the stale archive. +""" +steps = [ + { argv = [ + "vt", + "run", + "output-config-change", + ], comment = "first run populates the cache" }, + { argv = [ + "vtt", + "replace-file-content", + "vite-task.json", + "REPLACE_ME", + "dist/**", + ], comment = "change the output globs in the task config" }, + { argv = [ + "vt", + "run", + "output-config-change", + ], comment = "cache miss: output config changed" }, +] + +[[e2e]] +name = "input_negative_does_not_drop_output_writes" +comment = """ +Input negative globs must not drop matching writes from the output +archive. Here the user excludes `dist/**` from inferred inputs (so +rewriting dist/ won't mark inputs as modified), but default auto output +should still capture dist writes and restore them on a cache hit. +""" +steps = [ + { argv = [ + "vt", + "run", + "input-neg-dist-auto-output", + ], comment = "first run writes dist/out.txt" }, + { argv = [ + "vtt", + "rm", + "dist/out.txt", + ], comment = "delete the output" }, + { argv = [ + "vt", + "run", + "input-neg-dist-auto-output", + ], comment = "cache hit should restore dist/out.txt" }, + { argv = [ + "vtt", + "print-file", + "dist/out.txt", + ], comment = "output was restored despite dist/** being a negative input glob" }, +] + +[[e2e]] +name = "explicit_input_ignores_fspy_reads" +comment = """ +When input auto is disabled (explicit globs only), unrelated reads +tracked by fspy must NOT become inferred inputs. Default auto output +still needs fspy for write tracking, but reads outside `input: [\"src/**\"]` +should be ignored. +""" +steps = [ + { argv = [ + "vt", + "run", + "explicit-input-auto-output", + ], comment = "first run reads README.md (not in input globs) and captures the output" }, + { argv = [ + "vtt", + "replace-file-content", + "README.md", + "v1", + "v2", + ], comment = "modify README.md — not an input, so it must not invalidate the cache" }, + { argv = [ + "vt", + "run", + "explicit-input-auto-output", + ], comment = "cache hit: README.md not in input globs" }, ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/auto_output___files_restored_on_cache_hit.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/auto_output___files_restored_on_cache_hit.md new file mode 100644 index 000000000..08ec43680 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/auto_output___files_restored_on_cache_hit.md @@ -0,0 +1,46 @@ +# auto_output___files_restored_on_cache_hit + +Auto output detection (default): output files written by the task are +restored on a cache hit. + +## `vt run auto-output` + +first run writes dist/out.txt + +``` +$ vtt write-file dist/out.txt built +``` + +## `vtt print-file dist/out.txt` + +output exists after the build + +``` +built +``` + +## `vtt rm dist/out.txt` + +delete only the file (keep dir to avoid an fspy inferred-input miss on Windows) + +``` +``` + +## `vt run auto-output` + +cache hit, should restore dist/out.txt + +``` +$ vtt write-file dist/out.txt built ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `vtt print-file dist/out.txt` + +output was restored from the archive + +``` +built +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/auto_output_with_non_auto_input.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/auto_output_with_non_auto_input.md new file mode 100644 index 000000000..ba8c3e460 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/auto_output_with_non_auto_input.md @@ -0,0 +1,39 @@ +# auto_output_with_non_auto_input + +Auto output works even when input tracking is explicit +(`input: ["src/**"]`). Output auto-detection and input auto-detection +are independent. + +## `vt run auto-output-no-auto-input` + +first run: input src/** (no auto), output default (auto) + +``` +$ vtt write-file dist/out.txt built +``` + +## `vtt rm dist/out.txt` + +delete the output + +``` +``` + +## `vt run auto-output-no-auto-input` + +cache hit - output files should still be restored + +``` +$ vtt write-file dist/out.txt built ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `vtt print-file dist/out.txt` + +output was restored + +``` +built +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/explicit_input_ignores_fspy_reads.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/explicit_input_ignores_fspy_reads.md new file mode 100644 index 000000000..c0e5e0f9f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/explicit_input_ignores_fspy_reads.md @@ -0,0 +1,34 @@ +# explicit_input_ignores_fspy_reads + +When input auto is disabled (explicit globs only), unrelated reads +tracked by fspy must NOT become inferred inputs. Default auto output +still needs fspy for write tracking, but reads outside `input: ["src/**"]` +should be ignored. + +## `vt run explicit-input-auto-output` + +first run reads README.md (not in input globs) and captures the output + +``` +$ vtt print-file README.md +v1 +``` + +## `vtt replace-file-content README.md v1 v2` + +modify README.md — not an input, so it must not invalidate the cache + +``` +``` + +## `vt run explicit-input-auto-output` + +cache hit: README.md not in input globs + +``` +$ vtt print-file README.md ◉ cache hit, replaying +v1 + +--- +vt run: cache hit. +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/glob_output___only_matched_files_restored.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/glob_output___only_matched_files_restored.md new file mode 100644 index 000000000..99df20c84 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/glob_output___only_matched_files_restored.md @@ -0,0 +1,60 @@ +# glob_output___only_matched_files_restored + +Glob output: only files matching the configured output globs are restored +on a cache hit; files produced outside those globs are left alone. + +## `vt run glob-output` + +first run writes dist/out.txt and tmp/temp.txt + +``` +$ vtt write-file dist/out.txt built + +$ vtt write-file tmp/temp.txt temp + +--- +vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) +``` + +## `vtt rm dist/out.txt` + +delete the glob-matched output + +``` +``` + +## `vtt rm tmp/temp.txt` + +delete the non-matched output + +``` +``` + +## `vt run glob-output` + +cache hit: should restore dist/out.txt but not tmp/temp.txt + +``` +$ vtt write-file dist/out.txt built ◉ cache hit, replaying + +$ vtt write-file tmp/temp.txt temp ◉ cache hit, replaying + +--- +vt run: 2/2 cache hit (100%). (Run `vt run --last-details` for full details) +``` + +## `vtt print-file dist/out.txt` + +dist/out.txt was restored + +``` +built +``` + +## `vtt print-file tmp/temp.txt` + +should fail - tmp not in output globs + +``` +tmp/temp.txt: not found +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/input_negative_does_not_drop_output_writes.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/input_negative_does_not_drop_output_writes.md new file mode 100644 index 000000000..81153bb8a --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/input_negative_does_not_drop_output_writes.md @@ -0,0 +1,40 @@ +# input_negative_does_not_drop_output_writes + +Input negative globs must not drop matching writes from the output +archive. Here the user excludes `dist/**` from inferred inputs (so +rewriting dist/ won't mark inputs as modified), but default auto output +should still capture dist writes and restore them on a cache hit. + +## `vt run input-neg-dist-auto-output` + +first run writes dist/out.txt + +``` +$ vtt write-file dist/out.txt built +``` + +## `vtt rm dist/out.txt` + +delete the output + +``` +``` + +## `vt run input-neg-dist-auto-output` + +cache hit should restore dist/out.txt + +``` +$ vtt write-file dist/out.txt built ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `vtt print-file dist/out.txt` + +output was restored despite dist/** being a negative input glob + +``` +built +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/negative_output___excluded_files_not_restored.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/negative_output___excluded_files_not_restored.md new file mode 100644 index 000000000..e355a36d5 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/negative_output___excluded_files_not_restored.md @@ -0,0 +1,60 @@ +# negative_output___excluded_files_not_restored + +Negative output globs exclude matching files from the cache archive, so +they are not restored on a cache hit even though the task produced them. + +## `vt run negative-output` + +first run writes dist/out.txt and dist/cache/tmp.txt + +``` +$ vtt write-file dist/out.txt built + +$ vtt write-file dist/cache/tmp.txt temp + +--- +vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) +``` + +## `vtt rm dist/out.txt` + +delete the archived output + +``` +``` + +## `vtt rm dist/cache/tmp.txt` + +delete the excluded output + +``` +``` + +## `vt run negative-output` + +cache hit: should restore dist/out.txt but NOT dist/cache/tmp.txt + +``` +$ vtt write-file dist/out.txt built ◉ cache hit, replaying + +$ vtt write-file dist/cache/tmp.txt temp ◉ cache hit, replaying + +--- +vt run: 2/2 cache hit (100%). (Run `vt run --last-details` for full details) +``` + +## `vtt print-file dist/out.txt` + +dist/out.txt was restored + +``` +built +``` + +## `vtt print-file dist/cache/tmp.txt` + +should fail - excluded by !dist/cache/** + +``` +dist/cache/tmp.txt: not found +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_config_change_invalidates_cache.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_config_change_invalidates_cache.md new file mode 100644 index 000000000..ad0870994 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_config_change_invalidates_cache.md @@ -0,0 +1,27 @@ +# output_config_change_invalidates_cache + +Changing a task's output configuration invalidates the cache entry — +the next run is a miss, not a hit of the stale archive. + +## `vt run output-config-change` + +first run populates the cache + +``` +$ vtt write-file dist/out.txt built +``` + +## `vtt replace-file-content vite-task.json REPLACE_ME dist/**` + +change the output globs in the task config + +``` +``` + +## `vt run output-config-change` + +cache miss: output config changed + +``` +$ vtt write-file dist/out.txt built ○ cache miss: output configuration changed, executing +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___files_restored_on_cache_hit.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___files_restored_on_cache_hit.md deleted file mode 100644 index b36af12a7..000000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___files_restored_on_cache_hit.md +++ /dev/null @@ -1,47 +0,0 @@ -# output_globs___files_restored_on_cache_hit - -With explicit output globs (`dist/**`), the first run writes a file to -`dist/`. After deleting `dist/`, a second run with no input changes is a -cache hit and the archived output file is restored. - -## `vt run build` - -first run — cache miss, writes dist/output.txt - -``` -$ vtt write-file dist/output.txt built -``` - -## `vtt print-file dist/output.txt` - -file is on disk after the run - -``` -built -``` - -## `vtt rm -rf dist` - -delete dist/ to prove the restore is real - -``` -``` - -## `vt run build` - -second run — cache hit, restores from archive - -``` -$ vtt write-file dist/output.txt built ◉ cache hit, replaying - ---- -vt run: cache hit. -``` - -## `vtt print-file dist/output.txt` - -file restored from archive - -``` -built -``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___negative_excludes_files_from_archive.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___negative_excludes_files_from_archive.md deleted file mode 100644 index ae0a65c0f..000000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___negative_excludes_files_from_archive.md +++ /dev/null @@ -1,69 +0,0 @@ -# output_globs___negative_excludes_files_from_archive - -A file matched by a negative output glob is not archived, so it is not -restored on cache hit. - -## `vt run build-with-negative` - -first run — writes dist/keep.txt and dist/skip.txt - -``` -$ vtt write-file dist/keep.txt keep - -$ vtt write-file dist/skip.txt skip - ---- -vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) -``` - -## `vtt print-file dist/keep.txt` - -keep.txt was written - -``` -keep -``` - -## `vtt print-file dist/skip.txt` - -skip.txt was written - -``` -skip -``` - -## `vtt rm -rf dist` - -delete dist/ to prove the restore is real - -``` -``` - -## `vt run build-with-negative` - -second run — cache hit, restores from archive - -``` -$ vtt write-file dist/keep.txt keep ◉ cache hit, replaying - -$ vtt write-file dist/skip.txt skip ◉ cache hit, replaying - ---- -vt run: 2/2 cache hit (100%). (Run `vt run --last-details` for full details) -``` - -## `vtt print-file dist/keep.txt` - -keep.txt restored from archive - -``` -keep -``` - -## `vtt print-file dist/skip.txt` - -skip.txt is NOT restored — it was excluded by the negative glob - -``` -dist/skip.txt: not found -``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___old_archive_removed_on_rewrite.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___old_archive_removed_on_rewrite.md deleted file mode 100644 index 9d3690178..000000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___old_archive_removed_on_rewrite.md +++ /dev/null @@ -1,45 +0,0 @@ -# output_globs___old_archive_removed_on_rewrite - -When a cached task re-runs (cache miss because an input changed), it -writes a new archive and the previous archive file is cleaned up. After -two cache-missing runs of the same task the cache directory still -contains only one `.tar.zst` archive. - -## `vt run build` - -first run — cache miss, writes archive A - -``` -$ vtt write-file dist/output.txt built -``` - -## `vtt list-dir node_modules/.vite/task-cache --ext .tar.zst` - -exactly one archive on disk - -``` -.tar.zst -``` - -## `vtt write-file src/main.ts changed` - -modify an input so the next run is a cache miss - -``` -``` - -## `vt run build` - -second run — cache miss, writes archive B and removes A - -``` -$ vtt write-file dist/output.txt built ○ cache miss: 'src/main.ts' modified, executing -``` - -## `vtt list-dir node_modules/.vite/task-cache --ext .tar.zst` - -still exactly one archive — A was cleaned up - -``` -.tar.zst -``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json index 6bd35a604..f7b271462 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json @@ -1,15 +1,37 @@ { "tasks": { - "build": { - "command": "vtt write-file dist/output.txt built", - "input": ["src/**"], + "auto-output": { + "command": "vtt write-file dist/out.txt built", + "cache": true + }, + "glob-output": { + "command": "vtt write-file dist/out.txt built && vtt write-file tmp/temp.txt temp", "output": ["dist/**"], "cache": true }, - "build-with-negative": { - "command": "vtt write-file dist/keep.txt keep && vtt write-file dist/skip.txt skip", + "auto-output-no-auto-input": { + "command": "vtt write-file dist/out.txt built", + "input": ["src/**"], + "cache": true + }, + "negative-output": { + "command": "vtt write-file dist/out.txt built && vtt write-file dist/cache/tmp.txt temp", + "output": [{ "auto": true }, "!dist/cache/**"], + "cache": true + }, + "output-config-change": { + "command": "vtt write-file dist/out.txt built", + "output": ["REPLACE_ME"], + "cache": true + }, + "input-neg-dist-auto-output": { + "command": "vtt write-file dist/out.txt built", + "input": [{ "auto": true }, "!dist/**"], + "cache": true + }, + "explicit-input-auto-output": { + "command": "vtt print-file README.md", "input": ["src/**"], - "output": ["dist/**", "!dist/skip.txt"], "cache": true } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/index.html b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/index.html new file mode 100644 index 000000000..20fc85a43 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/index.html @@ -0,0 +1,9 @@ + + + + vp-run-vite-cache + + + + + diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/package.json new file mode 100644 index 000000000..e4e3497f2 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/package.json @@ -0,0 +1,5 @@ +{ + "name": "vite-build-cache-fixture", + "private": true, + "type": "module" +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml new file mode 100644 index 000000000..d99b8da13 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml @@ -0,0 +1,117 @@ +[[e2e]] +name = "vite_build_caches_and_restores_outputs" +comment = """ +`vt run --cache build` must produce a cache hit on the second run without +any manual input/output configuration. Vite reports +`ignoreInput(outDir)` + `ignoreInput/Output(cacheDir)` via +`@voidzero-dev/vite-task-client`, so fspy-detected reads of `dist/` and +writes to `node_modules/.vite/` don't poison the cache. +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "--cache", + "build", + ], comment = "first run: cache miss, emits dist/" }, + { argv = [ + "vtt", + "stat-file", + "dist/assets/main.js", + ], comment = "existence check — content would drift across Vite versions" }, + { argv = [ + "vtt", + "rm", + "dist/assets/main.js", + ], comment = "remove the artefact so the cache-hit restore is observable" }, + { argv = [ + "vt", + "run", + "--cache", + "build", + ], comment = "cache hit: outputs restored without manual config" }, + { argv = [ + "vtt", + "stat-file", + "dist/assets/main.js", + ], comment = "restored from the cache archive" }, +] + +[[e2e]] +name = "vite_prefix_env_change_invalidates_cache" +comment = """ +`VITE_MODE` is picked up by Vite's patched `loadEnv`, which asks the runner +for every `VITE_*` env via `getEnvs(pattern, { tracked: true })`. Flipping +its value between runs must invalidate the cache AND change the build output +— Vite's `define` plugin substitutes `import.meta.env.VITE_MODE` at build +time, so dead-code elimination leaves only the branch matching the value. +""" +# Unix-only for now: on Windows CI, interprocess 2.4 aborts the child +# with "failed to start the persistent thread of the Interprocess linger +# pool: Access is denied" when the Node addon tries to connect. +platform = "unix" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "--cache", + "build", + ], envs = [ + [ + "VITE_MODE", + "production", + ], + ], comment = "first run: production build" }, + { argv = [ + "vtt", + "grep-file", + "dist/assets/main.js", + "BUILD_MODE_PROD", + ], comment = "production build: PROD marker survived DCE" }, + { argv = [ + "vtt", + "grep-file", + "dist/assets/main.js", + "BUILD_MODE_DEV", + ], comment = "dev branch is gone" }, + { argv = [ + "vt", + "run", + "--cache", + "build", + ], envs = [ + [ + "VITE_MODE", + "production", + ], + ], comment = "cache hit: VITE_MODE unchanged" }, + { argv = [ + "vt", + "run", + "--cache", + "build", + ], envs = [ + [ + "VITE_MODE", + "development", + ], + ], comment = "cache miss: tracked env glob `VITE_*` — VITE_MODE value changed" }, + { argv = [ + "vtt", + "grep-file", + "dist/assets/main.js", + "BUILD_MODE_PROD", + ], comment = "PROD marker gone after the dev rebuild" }, + { argv = [ + "vtt", + "grep-file", + "dist/assets/main.js", + "BUILD_MODE_DEV", + ], comment = "DEV marker now in the bundle" }, +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_build_caches_and_restores_outputs.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_build_caches_and_restores_outputs.md new file mode 100644 index 000000000..46dab08c0 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_build_caches_and_restores_outputs.md @@ -0,0 +1,49 @@ +# vite_build_caches_and_restores_outputs + +`vt run --cache build` must produce a cache hit on the second run without +any manual input/output configuration. Vite reports +`ignoreInput(outDir)` + `ignoreInput/Output(cacheDir)` via +`@voidzero-dev/vite-task-client`, so fspy-detected reads of `dist/` and +writes to `node_modules/.vite/` don't poison the cache. + +## `vt run --cache build` + +first run: cache miss, emits dist/ + +``` +$ vite build +``` + +## `vtt stat-file dist/assets/main.js` + +existence check — content would drift across Vite versions + +``` +dist/assets/main.js: exists +``` + +## `vtt rm dist/assets/main.js` + +remove the artefact so the cache-hit restore is observable + +``` +``` + +## `vt run --cache build` + +cache hit: outputs restored without manual config + +``` +$ vite build ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `vtt stat-file dist/assets/main.js` + +restored from the cache archive + +``` +dist/assets/main.js: exists +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_prefix_env_change_invalidates_cache.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_prefix_env_change_invalidates_cache.md new file mode 100644 index 000000000..d90f2fd9c --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_prefix_env_change_invalidates_cache.md @@ -0,0 +1,66 @@ +# vite_prefix_env_change_invalidates_cache + +`VITE_MODE` is picked up by Vite's patched `loadEnv`, which asks the runner +for every `VITE_*` env via `getEnvs(pattern, { tracked: true })`. Flipping +its value between runs must invalidate the cache AND change the build output +— Vite's `define` plugin substitutes `import.meta.env.VITE_MODE` at build +time, so dead-code elimination leaves only the branch matching the value. + +## `VITE_MODE=production vt run --cache build` + +first run: production build + +``` +$ vite build +``` + +## `vtt grep-file dist/assets/main.js BUILD_MODE_PROD` + +production build: PROD marker survived DCE + +``` +dist/assets/main.js: found "BUILD_MODE_PROD" +``` + +## `vtt grep-file dist/assets/main.js BUILD_MODE_DEV` + +dev branch is gone + +``` +dist/assets/main.js: missing "BUILD_MODE_DEV" +``` + +## `VITE_MODE=production vt run --cache build` + +cache hit: VITE_MODE unchanged + +``` +$ vite build ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `VITE_MODE=development vt run --cache build` + +cache miss: tracked env glob `VITE_*` — VITE_MODE value changed + +``` +$ vite build ○ cache miss: tracked env glob 'VITE_*' changed, executing +``` + +## `vtt grep-file dist/assets/main.js BUILD_MODE_PROD` + +PROD marker gone after the dev rebuild + +``` +dist/assets/main.js: missing "BUILD_MODE_PROD" +``` + +## `vtt grep-file dist/assets/main.js BUILD_MODE_DEV` + +DEV marker now in the bundle + +``` +dist/assets/main.js: found "BUILD_MODE_DEV" +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/src/main.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/src/main.js new file mode 100644 index 000000000..541b3d7c8 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/src/main.js @@ -0,0 +1,9 @@ +// `import.meta.env.VITE_MODE` is replaced at build time from the value vite +// picks up for keys matching `envPrefix` (`VITE_` by default). The markers +// let the e2e test assert that flipping VITE_MODE actually changed what was +// built and that glob-tracking invalidates the cache. +if (import.meta.env.VITE_MODE === 'production') { + document.body.append('BUILD_MODE_PROD'); +} else { + document.body.append('BUILD_MODE_DEV'); +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json new file mode 100644 index 000000000..81571aeaf --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json @@ -0,0 +1,11 @@ +{ + "tasks": { + "build": { + "command": "vite build", + // No `"env": [...]` — vite's patched `loadEnv` asks the runner for + // every `VITE_*` env via `getEnvs`, so the glob + match-set are + // fingerprinted automatically. + "cache": true + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.config.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.config.js new file mode 100644 index 000000000..cc83efec1 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + logLevel: 'silent', + build: { + rollupOptions: { + output: { + // Stable filenames make cache behaviour deterministic across runs. + entryFileNames: 'assets/main.js', + chunkFileNames: 'assets/chunk.js', + assetFileNames: 'assets/[name][extname]', + }, + }, + }, +}); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index d222696cd..054098c96 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -283,6 +283,56 @@ fn render_formatted_screen(bytes: &[u8]) -> String { out } +/// Copy the `@voidzero-dev/vite-task-client` JS wrapper into the fixture's +/// staging `node_modules` so Node scripts can resolve it by name. Idempotent — +/// silently skipped if the source package is not found. +#[expect(clippy::disallowed_types, reason = "std::path::Path required for filesystem operations")] +fn populate_vite_task_client_package(stage_path: &AbsolutePath) { + let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let repo_root = manifest_dir.parent().unwrap().parent().unwrap(); + let src = repo_root.join("packages/vite-task-client"); + if !src.is_dir() { + return; + } + let dst = stage_path.as_path().join("node_modules/@voidzero-dev/vite-task-client"); + std::fs::create_dir_all(dst.parent().unwrap()).unwrap(); + CopyOptions::new().copy_tree(&src, &dst).unwrap(); +} + +/// Symlink installed Node packages from the repo's `packages/tools/node_modules` +/// into the fixture's staging `node_modules` so fixtures can resolve them by +/// name without a per-fixture pnpm install. Only packages whose staging-side +/// symlink targets exist are created; missing targets are silently skipped. +#[expect(clippy::disallowed_types, reason = "std::path::Path required for filesystem operations")] +fn link_tools_packages(stage_path: &AbsolutePath, names: &[&str]) { + let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let repo_root = manifest_dir.parent().unwrap().parent().unwrap(); + let stage_node_modules = stage_path.as_path().join("node_modules"); + std::fs::create_dir_all(&stage_node_modules).unwrap(); + + for name in names { + let src = repo_root.join("packages/tools/node_modules").join(name); + // Follow the symlink so the absolute target (pnpm's .pnpm store) is + // what we pin into the staging tree. Relative symlinks into pnpm + // internals would break outside the repo. + let Ok(canonical) = std::fs::canonicalize(&src) else { + continue; + }; + let link = stage_node_modules.join(name); + let _ = std::fs::remove_file(&link); + #[cfg(unix)] + std::os::unix::fs::symlink(&canonical, &link).unwrap(); + #[cfg(windows)] + { + if canonical.is_dir() { + std::os::windows::fs::symlink_dir(&canonical, &link).unwrap(); + } else { + std::os::windows::fs::symlink_file(&canonical, &link).unwrap(); + } + } + } +} + /// Append a fenced markdown block containing `body`. The opening and closing /// fences sit on their own lines, and trailing whitespace inside `body` is /// trimmed so the close fence isn't preceded by blank lines. @@ -319,6 +369,19 @@ fn run_case( let e2e_stage_path = tmpdir.join(vite_str::format!("{fixture_name}_case_{case_index}")); CopyOptions::new().copy_tree(fixture_path, e2e_stage_path.as_path()).unwrap(); + // Make `@voidzero-dev/vite-task-client` importable from any fixture's Node + // scripts by copying the wrapper package into the staging dir's + // `node_modules`. This mirrors the user-facing flow (`import { ... } from + // "@voidzero-dev/vite-task-client"`) without requiring pnpm install. + populate_vite_task_client_package(&e2e_stage_path); + + // Fixtures that exercise real Node toolchains (e.g. `vite build`) link + // those packages from the repo's `packages/tools/node_modules` so the + // tool and its transitive deps (resolved via pnpm) stay reachable. + if fixture_name == "vite_build_cache" { + link_tools_packages(&e2e_stage_path, &["vite"]); + } + let (workspace_root, _cwd) = find_workspace_root(&e2e_stage_path).unwrap(); assert_eq!( &e2e_stage_path, &*workspace_root.path, @@ -331,8 +394,19 @@ fn run_case( let bin = AbsolutePathBuf::new(std::path::PathBuf::from(bin_path)).unwrap(); Arc::::from(bin.parent().unwrap().as_path().as_os_str()) }); + + // Also expose tool bins installed under packages/tools/node_modules/.bin + // (e.g. `vite`) so ignored e2e fixtures can exercise real toolchains. + #[expect(clippy::disallowed_types, reason = "PathBuf needed for workspace path arithmetic")] + let tools_bin_dir: Option> = { + let manifest_dir = std::path::PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let repo_root = manifest_dir.parent().unwrap().parent().unwrap(); + let tools_bin = repo_root.join("packages/tools/node_modules/.bin"); + tools_bin.is_dir().then(|| Arc::::from(tools_bin.into_os_string())) + }; + let e2e_env_path = join_paths( - bin_dirs.iter().cloned().chain( + bin_dirs.iter().cloned().chain(tools_bin_dir.iter().cloned()).chain( // the existing PATH split_paths(&env::var_os("PATH").unwrap()) .map(|path| Arc::::from(path.into_os_string())), diff --git a/crates/vite_task_client/Cargo.toml b/crates/vite_task_client/Cargo.toml new file mode 100644 index 000000000..540a023a3 --- /dev/null +++ b/crates/vite_task_client/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "vite_task_client" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +interprocess = { workspace = true } +native_str = { workspace = true } +vite_path = { workspace = true } +vite_task_ipc_shared = { workspace = true } +wincode = { workspace = true, features = ["derive"] } + +[lints] +workspace = true + +[lib] +doctest = false +test = false diff --git a/crates/vite_task_client/README.md b/crates/vite_task_client/README.md new file mode 100644 index 000000000..f1ab5ee44 --- /dev/null +++ b/crates/vite_task_client/README.md @@ -0,0 +1,3 @@ +# vite_task_client + +IPC client that connects from tool processes to the task runner to report inputs/outputs, request env values, and disable caching. diff --git a/crates/vite_task_client/src/lib.rs b/crates/vite_task_client/src/lib.rs new file mode 100644 index 000000000..20918a2e4 --- /dev/null +++ b/crates/vite_task_client/src/lib.rs @@ -0,0 +1,175 @@ +use std::{ + cell::RefCell, + collections::BTreeMap, + ffi::OsStr, + io::{self, Read, Write}, + sync::Arc, +}; + +use interprocess::local_socket::{Stream, prelude::*}; +use native_str::NativeStr; +use vite_path::{self, AbsolutePath}; +use vite_task_ipc_shared::{GetEnvResponse, GetEnvsResponse, IPC_ENV_NAME, Request}; + +pub struct Client { + stream: RefCell, + scratch: RefCell>, +} + +impl Client { + /// Scans `envs` for the runner's IPC connection info and connects if + /// present. Typical callers pass `std::env::vars_os()`. + /// + /// Returns `Ok(None)` if the IPC env is absent (running outside the runner). + /// `Err(..)` if the env is set but connecting fails. + /// + /// # Errors + /// + /// Returns an error if the env var is set but the server cannot be reached. + pub fn from_envs( + envs: impl Iterator, impl AsRef)>, + ) -> io::Result> { + for (name, value) in envs { + if name.as_ref() == IPC_ENV_NAME { + let stream = Stream::connect(resolve_name(value.as_ref())?)?; + return Ok(Some(Self::from_stream(stream))); + } + } + Ok(None) + } + + const fn from_stream(stream: Stream) -> Self { + Self { stream: RefCell::new(stream), scratch: RefCell::new(Vec::new()) } + } + + /// `path` can be a file or a directory; for a directory, all files inside + /// it are ignored. Relative paths are resolved against the current working + /// directory before being sent to the runner. + /// + /// # Errors + /// + /// Returns an error if the request fails to send, or (for a relative + /// `path`) if the current working directory cannot be read. + pub fn ignore_input(&self, path: &OsStr) -> io::Result<()> { + let ns = resolve_path(path)?; + self.send(&Request::IgnoreInput(&ns)) + } + + /// `path` can be a file or a directory; for a directory, all files inside + /// it are ignored. Relative paths are resolved against the current working + /// directory before being sent to the runner. + /// + /// # Errors + /// + /// Returns an error if the request fails to send, or (for a relative + /// `path`) if the current working directory cannot be read. + pub fn ignore_output(&self, path: &OsStr) -> io::Result<()> { + let ns = resolve_path(path)?; + self.send(&Request::IgnoreOutput(&ns)) + } + + /// # Errors + /// + /// Returns an error if the request fails to send. + pub fn disable_cache(&self) -> io::Result<()> { + self.send(&Request::DisableCache) + } + + /// Requests an env value from the runner. Returns `None` if the runner reports + /// the env is not available. + /// + /// # Errors + /// + /// Returns an error if the request or response fails. + pub fn get_env(&self, name: &OsStr, tracked: bool) -> io::Result>> { + let name = Box::::from(name); + + self.send(&Request::GetEnv { name: &name, tracked })?; + self.recv_with(|bytes| { + let response: GetEnvResponse<'_> = wincode::deserialize_exact(bytes) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + Ok(response + .env_value + .map(|env_value| Arc::::from(env_value.to_cow_os_str().as_ref()))) + }) + } + + /// Requests every env whose name matches `pattern` from the runner. The + /// returned map is keyed by env name (sorted) with its value. + /// + /// Unlike [`Self::get_env`], this always round-trips to the server — the + /// client has no way to know in advance which names the pattern matches. + /// Env names that aren't valid UTF-8 are silently dropped at the server. + /// + /// # Errors + /// + /// Returns an error if the request or response fails, or if the server + /// rejected the pattern as an invalid glob. + pub fn get_envs( + &self, + pattern: &str, + tracked: bool, + ) -> io::Result, Arc>> { + self.send(&Request::GetEnvs { pattern, tracked })?; + self.recv_with(|bytes| { + let response: GetEnvsResponse<'_> = wincode::deserialize_exact(bytes) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + Ok(response + .entries + .iter() + .map(|(name, value)| { + ( + Arc::::from(name.to_cow_os_str().as_ref()), + Arc::::from(value.to_cow_os_str().as_ref()), + ) + }) + .collect()) + }) + } + + fn send(&self, request: &Request<'_>) -> io::Result<()> { + let bytes = wincode::serialize(request) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + let len = u32::try_from(bytes.len()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "request too large"))?; + let mut stream = self.stream.borrow_mut(); + stream.write_all(&len.to_le_bytes())?; + stream.write_all(&bytes)?; + stream.flush()?; + Ok(()) + } + + fn recv_with(&self, extract: impl FnOnce(&[u8]) -> io::Result) -> io::Result { + let mut stream = self.stream.borrow_mut(); + let mut scratch = self.scratch.borrow_mut(); + let mut len_bytes = [0u8; 4]; + stream.read_exact(&mut len_bytes)?; + let len = u32::from_le_bytes(len_bytes) as usize; + scratch.clear(); + scratch.resize(len, 0); + stream.read_exact(&mut scratch)?; + extract(&scratch) + } +} + +#[cfg(unix)] +fn resolve_name(name: &OsStr) -> io::Result> { + use interprocess::local_socket::{GenericFilePath, ToFsName}; + name.to_fs_name::() +} + +#[cfg(windows)] +fn resolve_name(name: &OsStr) -> io::Result> { + use interprocess::local_socket::{GenericNamespaced, ToNsName}; + name.to_ns_name::() +} + +fn resolve_path(path: &OsStr) -> io::Result> { + if let Some(abs) = AbsolutePath::new(path) { + Ok(Box::::from(abs.as_path().as_os_str())) + } else { + let mut buf = vite_path::current_dir()?; + buf.push(path); + Ok(Box::::from(buf.as_path().as_os_str())) + } +} diff --git a/crates/vite_task_client_napi/Cargo.toml b/crates/vite_task_client_napi/Cargo.toml new file mode 100644 index 000000000..4a91be9d9 --- /dev/null +++ b/crates/vite_task_client_napi/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vite_task_client_napi" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib"] +test = false +doctest = false + +[dependencies] +napi = { workspace = true, features = ["napi6"] } +napi-derive = { workspace = true } +vite_str = { workspace = true } +vite_task_client = { workspace = true } + +[build-dependencies] +napi-build = { workspace = true } + +[lints] +workspace = true diff --git a/crates/vite_task_client_napi/README.md b/crates/vite_task_client_napi/README.md new file mode 100644 index 000000000..c85a17f99 --- /dev/null +++ b/crates/vite_task_client_napi/README.md @@ -0,0 +1,3 @@ +# vite_task_client_napi + +Node addon that lets JS/TS tools running inside a `vp run` task talk to the runner over IPC via `vite_task_client`. diff --git a/crates/vite_task_client_napi/build.rs b/crates/vite_task_client_napi/build.rs new file mode 100644 index 000000000..9fc236788 --- /dev/null +++ b/crates/vite_task_client_napi/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/crates/vite_task_client_napi/src/lib.rs b/crates/vite_task_client_napi/src/lib.rs new file mode 100644 index 000000000..564060621 --- /dev/null +++ b/crates/vite_task_client_napi/src/lib.rs @@ -0,0 +1,120 @@ +//! Node addon that exposes module-level functions for tools to talk to a +//! `vp run` runner over IPC. Not intended to be published directly — the +//! runner hands the compiled `.node` file to child processes via the +//! `VP_RUN_NODE_CLIENT_PATH` env var, and the JS wrapper in +//! `@voidzero-dev/vite-task-client` `require()`s it lazily. +//! +//! The module is loadable **only** inside a runner-spawned task: when +//! module-init runs outside that context the registration fails, the JS +//! `require()` throws, and the wrapper falls into no-op mode. + +// The napi boundary forces std `String` through function signatures; clippy's +// blanket bans on disallowed types / needless-pass-by-value / missing Errors +// sections are all about pure-Rust call sites and don't apply here (JS never +// reads rustdoc). +#![expect( + clippy::disallowed_types, + clippy::missing_errors_doc, + clippy::needless_pass_by_value, + reason = "napi bindings require owned std String at the JS boundary" +)] + +use std::{cell::OnceCell, collections::HashMap, ffi::OsStr}; + +use napi::{Env, Error, Result}; +use napi_derive::napi; +use vite_task_client::Client; + +thread_local! { + // Per-thread so each Node Worker (which runs init on its own thread) gets + // its own slot. `Client` is `!Sync`, so a process-global `OnceLock` would + // be unsound across Workers. + static CLIENT: OnceCell = const { OnceCell::new() }; +} + +#[napi(module_exports)] +pub fn init(_env: Env) -> Result<()> { + let client = Client::from_envs(std::env::vars_os()) + .map_err(|err| { + err_string(vite_str::format!("vp run client: failed to connect to runner IPC: {err}")) + })? + .ok_or_else(|| { + err_static( + "vp run client: runner IPC env is not set; this module is only usable \ + inside a `vp run` task", + ) + })?; + CLIENT.with(|slot| { + slot.set(client).map_err(|_| err_static("vp run client: already initialised")) + })?; + Ok(()) +} + +fn with_client(f: impl FnOnce(&Client) -> Result) -> Result { + CLIENT.with(|slot| { + let client = slot.get().ok_or_else(|| err_static("vp run client: module state missing"))?; + f(client) + }) +} + +fn err_static(msg: &'static str) -> Error { + Error::from_reason(msg) +} + +fn err_string(msg: vite_str::Str) -> Error { + Error::from_reason(msg.as_str()) +} + +#[napi] +pub fn ignore_input(path: String) -> Result<()> { + with_client(|client| { + client.ignore_input(OsStr::new(&path)).map_err(|err| err_string(vite_str::format!("{err}"))) + }) +} + +#[napi] +pub fn ignore_output(path: String) -> Result<()> { + with_client(|client| { + client + .ignore_output(OsStr::new(&path)) + .map_err(|err| err_string(vite_str::format!("{err}"))) + }) +} + +#[napi] +pub fn disable_cache() -> Result<()> { + with_client(|client| { + client.disable_cache().map_err(|err| err_string(vite_str::format!("{err}"))) + }) +} + +#[napi] +pub fn get_env(name: String, tracked: bool) -> Result> { + with_client(|client| { + let value = client + .get_env(OsStr::new(&name), tracked) + .map_err(|err| err_string(vite_str::format!("{err}")))?; + value.map_or(Ok(None), |value| { + value.to_str().map(|s| Some(s.to_owned())).ok_or_else(|| { + err_string(vite_str::format!("env value for {name} is not valid UTF-8")) + }) + }) + }) +} + +#[napi] +pub fn get_envs(pattern: String, tracked: bool) -> Result> { + with_client(|client| { + let matches = client + .get_envs(&pattern, tracked) + .map_err(|err| err_string(vite_str::format!("{err}")))?; + // Entries whose name or value contains non-UTF-8 bytes can't cross + // the JS boundary as `String`. Unlike `get_env` (which errors out), + // bulk fetch drops them silently — the caller has no way to know + // which one is bad, and a partial match-set is usually still useful. + Ok(matches + .into_iter() + .filter_map(|(k, v)| Some((k.to_str()?.to_owned(), v.to_str()?.to_owned()))) + .collect()) + }) +} diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 1fa4ee868..ebf41c36b 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -55,14 +55,16 @@ untrackedEnv?: Array, */ input?: Array, /** - * Output files to archive after a successful run and restore on cache hit. + * Output files to archive and restore on cache hit. * - * - Omitted or `[]` (empty): no output archiving (default) + * - Omitted: automatically tracks which files the task writes + * - `[]` (empty): disables output restoration entirely * - Glob patterns (e.g. `"dist/**"`) select specific output files, relative to the package directory * - `{pattern: "...", base: "workspace" | "package"}` specifies a glob with an explicit base directory + * - `{auto: true}` enables automatic file tracking * - Negative patterns (e.g. `"!dist/cache/**"`) exclude matched files */ -output?: Array, } | { +output?: Array, } | { /** * Whether to cache the task */ diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index a68efbecd..31f42fddb 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -7,8 +7,8 @@ use rustc_hash::FxHashSet; use serde::Serialize; pub use user::{ AutoInput, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig, - UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry, - UserRunConfig, UserTaskConfig, + UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserRunConfig, + UserTaskConfig, }; use vite_path::AbsolutePath; use vite_str::Str; @@ -75,7 +75,7 @@ impl ResolvedTaskOptions { workspace_root, )?; - let output_config = ResolvedGlobConfig::from_user_output_config( + let output_config = ResolvedGlobConfig::from_user_config( enabled_cache_config.output.as_ref(), dir, workspace_root, @@ -196,61 +196,6 @@ impl ResolvedGlobConfig { Ok(Self { includes_auto, positive_globs, negative_globs }) } - /// Resolve from user output configuration, making glob patterns workspace-root-relative. - /// - /// Unlike [`Self::from_user_config`], `None` and `Some([])` both produce an empty config - /// with `includes_auto = false` (no output archiving). - /// - /// TODO: remove this method once auto output inference lands; at that point - /// `output` becomes a `UserInputsConfig` and routes through - /// [`Self::from_user_config`] like inputs. - /// - /// # Errors - /// - /// Returns [`ResolveTaskConfigError`] if a glob pattern is invalid or resolves - /// outside the workspace root. - pub fn from_user_output_config( - user_outputs: Option<&Vec>, - package_dir: &AbsolutePath, - workspace_root: &AbsolutePath, - ) -> Result { - let mut positive_globs = BTreeSet::new(); - let mut negative_globs = BTreeSet::new(); - - let Some(entries) = user_outputs else { - return Ok(Self { includes_auto: false, positive_globs, negative_globs }); - }; - - for entry in entries { - match entry { - UserOutputEntry::Glob(pattern) => { - Self::insert_glob( - pattern.as_str(), - package_dir, - workspace_root, - &mut positive_globs, - &mut negative_globs, - )?; - } - UserOutputEntry::GlobWithBase(GlobWithBase { pattern, base }) => { - let base_dir = match base { - InputBase::Package => package_dir, - InputBase::Workspace => workspace_root, - }; - Self::insert_glob( - pattern.as_str(), - base_dir, - workspace_root, - &mut positive_globs, - &mut negative_globs, - )?; - } - } - } - - Ok(Self { includes_auto: false, positive_globs, negative_globs }) - } - /// Insert a glob pattern into the appropriate set (positive or negative), /// resolving it relative to the given base directory. fn insert_glob( diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index aeee38608..cc5571e69 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -65,22 +65,6 @@ pub enum UserInputEntry { /// Default (when field omitted): `[{auto: true}]` - infer from file accesses. pub type UserInputsConfig = Vec; -/// A single output entry in the `output` array. -/// -/// Outputs can be: -/// - Glob patterns as strings (resolved relative to the package directory) -/// - Object form with explicit base: `{ "pattern": "...", "base": "workspace" | "package" }` -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -// TS derive macro generates code using std types that clippy disallows; skip derive during linting -#[cfg_attr(all(test, not(clippy)), derive(TS))] -#[serde(untagged)] -pub enum UserOutputEntry { - /// Glob pattern (positive or negative starting with `!`), resolved relative to package dir - Glob(Str), - /// Glob pattern with explicit base directory - GlobWithBase(GlobWithBase), -} - /// Cache-related fields of a task defined by user in `vite.config.*` #[derive(Debug, Deserialize, PartialEq, Eq)] // TS derive macro generates code using std types that clippy disallows; skip derive during linting @@ -142,15 +126,17 @@ pub struct EnabledCacheConfig { #[cfg_attr(all(test, not(clippy)), ts(inline))] pub input: Option, - /// Output files to archive after a successful run and restore on cache hit. + /// Output files to archive and restore on cache hit. /// - /// - Omitted or `[]` (empty): no output archiving (default) + /// - Omitted: automatically tracks which files the task writes + /// - `[]` (empty): disables output restoration entirely /// - Glob patterns (e.g. `"dist/**"`) select specific output files, relative to the package directory /// - `{pattern: "...", base: "workspace" | "package"}` specifies a glob with an explicit base directory + /// - `{auto: true}` enables automatic file tracking /// - Negative patterns (e.g. `"!dist/cache/**"`) exclude matched files #[serde(default)] #[cfg_attr(all(test, not(clippy)), ts(inline))] - pub output: Option>, + pub output: Option, } /// Options for user-defined tasks in `vite.config.*`, excluding the command. diff --git a/crates/vite_task_ipc_shared/.clippy.toml b/crates/vite_task_ipc_shared/.clippy.toml new file mode 120000 index 000000000..c7929b369 --- /dev/null +++ b/crates/vite_task_ipc_shared/.clippy.toml @@ -0,0 +1 @@ +../../.non-vite.clippy.toml \ No newline at end of file diff --git a/crates/vite_task_ipc_shared/Cargo.toml b/crates/vite_task_ipc_shared/Cargo.toml new file mode 100644 index 000000000..fd685ac0f --- /dev/null +++ b/crates/vite_task_ipc_shared/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "vite_task_ipc_shared" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +native_str = { workspace = true } +wincode = { workspace = true, features = ["derive"] } + +[lints] +workspace = true + +[lib] +doctest = false +test = false diff --git a/crates/vite_task_ipc_shared/README.md b/crates/vite_task_ipc_shared/README.md new file mode 100644 index 000000000..26116e79a --- /dev/null +++ b/crates/vite_task_ipc_shared/README.md @@ -0,0 +1,3 @@ +# vite_task_ipc_shared + +Shared IPC message types for communication between the task runner and tools. diff --git a/crates/vite_task_ipc_shared/src/lib.rs b/crates/vite_task_ipc_shared/src/lib.rs new file mode 100644 index 000000000..27be8132f --- /dev/null +++ b/crates/vite_task_ipc_shared/src/lib.rs @@ -0,0 +1,37 @@ +use std::collections::BTreeMap; + +use native_str::NativeStr; +use wincode::{SchemaRead, SchemaWrite}; + +pub const IPC_ENV_NAME: &str = "VP_RUN_IPC_NAME"; + +/// Path to the Node client module that JS/TS tools `require()` to talk to +/// the runner. +/// +/// Implementation-detail leakage (`napi`, `.node`, `addon`) is intentionally +/// kept out of the name: from the consumer's point of view this is just a +/// path they can `require()`. The `NODE_` scope reserves room for a future +/// C-ABI client library advertised via its own env var for non-Node +/// consumers. +pub const NODE_CLIENT_PATH_ENV_NAME: &str = "VP_RUN_NODE_CLIENT_PATH"; + +#[derive(Debug, SchemaWrite, SchemaRead)] +pub enum Request<'a> { + IgnoreInput(&'a NativeStr), + IgnoreOutput(&'a NativeStr), + GetEnv { name: &'a NativeStr, tracked: bool }, + GetEnvs { pattern: &'a str, tracked: bool }, + DisableCache, +} + +#[derive(Debug, SchemaWrite, SchemaRead)] +pub struct GetEnvResponse<'a> { + pub env_value: Option<&'a NativeStr>, +} + +#[derive(Debug, SchemaWrite, SchemaRead)] +pub struct GetEnvsResponse<'a> { + /// Match snapshot for the glob pattern, sorted by name. `BTreeMap` is used + /// over a `Vec` to make ordering and key-uniqueness part of the type. + pub entries: BTreeMap<&'a NativeStr, &'a NativeStr>, +} diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 89e892b48..7d73671d5 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -495,7 +495,7 @@ fn resolve_synthetic_cache_config( } if let Some(output) = output { - let synthetic_output = ResolvedGlobConfig::from_user_output_config( + let synthetic_output = ResolvedGlobConfig::from_user_config( Some(&output), package_dir, workspace_path, @@ -903,11 +903,7 @@ mod tests { positive_globs: positive_globs.iter().map(|s| Str::from(*s)).collect(), negative_globs: BTreeSet::new(), }, - output_config: ResolvedGlobConfig { - includes_auto: false, - positive_globs: BTreeSet::new(), - negative_globs: BTreeSet::new(), - }, + output_config: ResolvedGlobConfig::default_auto(), } } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc index 463724f0c..cd101cf96 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc @@ -62,7 +62,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/task_graph.jsonc index da3f2d593..3690168f9 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc index a5691b143..adb02339b 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc index 30c866edb..48bccf0b2 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc index 47e5b0324..102555b5d 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/task_graph.jsonc index d98768c05..9f5fe4c2f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -128,7 +128,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -167,7 +167,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc index 48ad4b955..bf928c4d3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc @@ -89,7 +89,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc index 4ba219e17..2b73444c2 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc index c838f9435..a0d248ce3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc @@ -62,7 +62,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc index 4a8e20b3b..19ab868f3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc index 4a8e20b3b..19ab868f3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc index def52132d..7a591a302 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc @@ -63,7 +63,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/task_graph.jsonc index 13b2802fd..69c2936d8 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_default/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_default/snapshots/task_graph.jsonc index 5129800c3..8b8f3c465 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_default/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_default/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_enabled/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_enabled/snapshots/task_graph.jsonc index 8f084f7de..85e08a273 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_enabled/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_enabled/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc index ae61543a1..64f8417bc 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc index fd3f1f354..58f4f43af 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/task_graph.jsonc index cb6afda93..a6a4e8722 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_sharing/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_sharing/snapshots/task_graph.jsonc index ebfa2e8b3..406b3f570 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_sharing/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_sharing/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_subcommand/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_subcommand/snapshots/task_graph.jsonc index fb069d24d..3ae013b8e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_subcommand/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_subcommand/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_tasks_disabled/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_tasks_disabled/snapshots/task_graph.jsonc index 55ebae45d..0e3aa085f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_tasks_disabled/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_tasks_disabled/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc index 0e38dd63c..65d42ab7c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc index 88f1e516f..0e7d9aa7a 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/task_graph.jsonc index fff2e8ef4..ae2a02d9e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/task_graph.jsonc @@ -50,7 +50,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -89,7 +89,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc index 93e467496..a3e0c79b2 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc index 838e481e0..c75170e2a 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/task_graph.jsonc index a0ea080a3..09b02be13 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/comprehensive_task_graph/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/comprehensive_task_graph/snapshots/task_graph.jsonc index 9597c4b3a..2d5f8307c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/comprehensive_task_graph/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/comprehensive_task_graph/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -184,7 +184,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -223,7 +223,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -262,7 +262,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -301,7 +301,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -340,7 +340,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -379,7 +379,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -418,7 +418,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -457,7 +457,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -496,7 +496,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -535,7 +535,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -574,7 +574,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -613,7 +613,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -652,7 +652,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -691,7 +691,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -730,7 +730,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -769,7 +769,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -808,7 +808,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -847,7 +847,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -886,7 +886,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/conflict_test/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/conflict_test/snapshots/task_graph.jsonc index e246a1db3..f88266a47 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/conflict_test/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/conflict_test/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cycle_dependency/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cycle_dependency/snapshots/task_graph.jsonc index 2c07f9bd1..02744a92a 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cycle_dependency/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cycle_dependency/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -72,7 +72,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/dependency_both_topo_and_explicit/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/dependency_both_topo_and_explicit/snapshots/task_graph.jsonc index 9b4779c26..6cd676ac1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/dependency_both_topo_and_explicit/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/dependency_both_topo_and_explicit/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -72,7 +72,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/task_graph.jsonc index b3d46d315..dc309692e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/empty_package_test/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/empty_package_test/snapshots/task_graph.jsonc index 0a88447df..15f5fee21 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/empty_package_test/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/empty_package_test/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -107,7 +107,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -146,7 +146,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -185,7 +185,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -229,7 +229,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -268,7 +268,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -307,7 +307,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -346,7 +346,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/explicit_deps_workspace/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/explicit_deps_workspace/snapshots/task_graph.jsonc index 4c6c21e3b..258dbe37a 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/explicit_deps_workspace/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/explicit_deps_workspace/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -102,7 +102,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -141,7 +141,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -180,7 +180,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -219,7 +219,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -258,7 +258,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -302,7 +302,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -341,7 +341,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -380,7 +380,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -428,7 +428,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc index 65177af19..c13f7bb9c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -147,7 +147,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/task_graph.jsonc index e483d70c4..0f8a5a02d 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/task_graph.jsonc index 27c09ea17..749052ab4 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -184,7 +184,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -223,7 +223,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -262,7 +262,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -301,7 +301,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -340,7 +340,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -379,7 +379,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -418,7 +418,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -457,7 +457,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -496,7 +496,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_trailing_slash/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_trailing_slash/snapshots/task_graph.jsonc index 11c015377..cc3c5380e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_trailing_slash/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_trailing_slash/snapshots/task_graph.jsonc @@ -32,7 +32,7 @@ ] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_workspace_base/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_workspace_base/snapshots/task_graph.jsonc index b82a6e8da..f8a2b06f1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_workspace_base/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input_workspace_base/snapshots/task_graph.jsonc @@ -33,7 +33,7 @@ ] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc index 9b2bd1fc8..f1eb0230f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc index c069f63c4..2e0132220 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc index 8d6f1a58a..3d3d37107 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/task_graph.jsonc index 1200a2faa..d1f6cf7ab 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_tasks/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_tasks/snapshots/task_graph.jsonc index eef8a1a13..84427555e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_tasks/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_tasks/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/package_self_dependency/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/package_self_dependency/snapshots/task_graph.jsonc index 599e3b086..0fa96e98b 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/package_self_dependency/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/package_self_dependency/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/parallel_and_concurrency/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/parallel_and_concurrency/snapshots/task_graph.jsonc index 9c50b6a60..3310ddddc 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/parallel_and_concurrency/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/parallel_and_concurrency/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -184,7 +184,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/pnpm_workspace_packages_optional/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/pnpm_workspace_packages_optional/snapshots/task_graph.jsonc index aef412250..7c721c0cd 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/pnpm_workspace_packages_optional/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/pnpm_workspace_packages_optional/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/recursive_topological_workspace/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/recursive_topological_workspace/snapshots/task_graph.jsonc index a902fcadd..721994b03 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/recursive_topological_workspace/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/recursive_topological_workspace/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -184,7 +184,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -223,7 +223,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -262,7 +262,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -301,7 +301,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks/snapshots/task_graph.jsonc index e5271222f..74c4fb4d8 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -184,7 +184,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -223,7 +223,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_disabled/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_disabled/snapshots/task_graph.jsonc index 2961d7313..2d6390832 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_disabled/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_disabled/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_nested_run/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_nested_run/snapshots/task_graph.jsonc index 36a3f67d0..89b34531f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_nested_run/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_nested_run/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_task_no_hook/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_task_no_hook/snapshots/task_graph.jsonc index 005d03240..a7a338907 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_task_no_hook/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script_hooks_task_no_hook/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc index 37f7a9b0f..c5db19dc5 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/task_graph.jsonc index 51fd7c317..c8d4d2825 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc index e5fab7d13..aa5f04070 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc index 847792c4d..c9a43b12c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc index 24c7eb377..50d948f44 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc @@ -61,7 +61,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc index a96337e79..0535dacb6 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc @@ -60,7 +60,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/task_graph.jsonc index b550e3faf..28255bc3e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -128,7 +128,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -168,7 +168,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -207,7 +207,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc index 5e47fdc2c..f71f060b3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc @@ -86,7 +86,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/task_graph.jsonc index f45c1001a..29978073f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/transitive_skip_intermediate/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/transitive_skip_intermediate/snapshots/task_graph.jsonc index 40ea1a092..281260a09 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/transitive_skip_intermediate/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/transitive_skip_intermediate/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr_shorthand/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr_shorthand/snapshots/task_graph.jsonc index 00be69c6c..a4fbd68c9 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr_shorthand/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr_shorthand/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc index 175ac4e20..3f2ddfec4 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc @@ -66,7 +66,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc index 2ce0827c1..4c4183a66 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc @@ -66,7 +66,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/task_graph.jsonc index d8a3142d4..8dd415471 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_cd_no_skip/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_cd_no_skip/snapshots/task_graph.jsonc index f7ee35330..f105a6563 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_cd_no_skip/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_cd_no_skip/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_depends_on_passthrough/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_depends_on_passthrough/snapshots/task_graph.jsonc index 2a23ddc71..b6b8e743d 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_depends_on_passthrough/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_depends_on_passthrough/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -72,7 +72,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -111,7 +111,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -150,7 +150,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_multi_command/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_multi_command/snapshots/task_graph.jsonc index f031c893d..30236c51c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_multi_command/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_multi_command/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_mutual_recursion/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_mutual_recursion/snapshots/task_graph.jsonc index 5e3cee720..4cb032386 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_mutual_recursion/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_mutual_recursion/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -145,7 +145,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_no_package_json/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_no_package_json/snapshots/task_graph.jsonc index 5a64e4c3a..a9be2e812 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_no_package_json/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_no_package_json/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_self_reference/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_self_reference/snapshots/task_graph.jsonc index 76f667459..2445ccfb7 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_self_reference/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/workspace_root_self_reference/snapshots/task_graph.jsonc @@ -28,7 +28,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -67,7 +67,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } @@ -106,7 +106,7 @@ "negative_globs": [] }, "output_config": { - "includes_auto": false, + "includes_auto": true, "positive_globs": [], "negative_globs": [] } diff --git a/crates/vite_task_server/Cargo.toml b/crates/vite_task_server/Cargo.toml new file mode 100644 index 000000000..cc09edecb --- /dev/null +++ b/crates/vite_task_server/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "vite_task_server" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +futures = { workspace = true } +interprocess = { workspace = true, features = ["tokio"] } +native_str = { workspace = true } +rustc-hash = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["io-util", "net", "rt", "macros"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +vite_glob = { workspace = true } +vite_path = { workspace = true } +vite_task_ipc_shared = { workspace = true } +wincode = { workspace = true, features = ["derive"] } + +[target.'cfg(windows)'.dependencies] +uuid = { workspace = true, features = ["v4"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["io-util", "net", "rt", "macros", "time"] } +vite_task_client = { workspace = true } + +[lints] +workspace = true + +[lib] +doctest = false +test = false diff --git a/crates/vite_task_server/README.md b/crates/vite_task_server/README.md new file mode 100644 index 000000000..cdcbdcddf --- /dev/null +++ b/crates/vite_task_server/README.md @@ -0,0 +1,3 @@ +# vite_task_server + +IPC server that runs per task execution, receiving messages from tools (runner-aware tools) and dispatching them to a user-provided handler. diff --git a/crates/vite_task_server/src/lib.rs b/crates/vite_task_server/src/lib.rs new file mode 100644 index 000000000..4fa08290e --- /dev/null +++ b/crates/vite_task_server/src/lib.rs @@ -0,0 +1,438 @@ +use std::{ + cell::RefCell, + collections::BTreeMap, + ffi::{OsStr, OsString}, + io, + sync::Arc, +}; + +use futures::{FutureExt, StreamExt, future::LocalBoxFuture, stream::FuturesUnordered}; +use interprocess::local_socket::{ + ListenerOptions, + tokio::{Listener, Stream, prelude::*}, +}; +use native_str::NativeStr; +use rustc_hash::{FxHashMap, FxHashSet}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_util::sync::CancellationToken; +use vite_path::AbsolutePath; +use vite_task_ipc_shared::{GetEnvResponse, GetEnvsResponse, IPC_ENV_NAME, Request}; +use wincode::{SchemaWrite, config::DefaultConfig}; + +pub trait Handler { + fn ignore_input(&mut self, path: &Arc); + fn ignore_output(&mut self, path: &Arc); + fn disable_cache(&mut self); + fn get_env(&mut self, name: &OsStr, tracked: bool) -> Option>; + /// Returns the subset of the env map whose names match `pattern` as a + /// wax/glob pattern, recording the match-set for the post-run fingerprint. + /// + /// # Errors + /// + /// Returns an error if `pattern` fails to parse as a glob. + fn get_envs( + &mut self, + pattern: &str, + tracked: bool, + ) -> Result, Arc>, vite_glob::Error>; +} + +/// A protocol-level failure observed while servicing a client. +/// +/// The driver retains only the first such error across all clients, then +/// completes gracefully (existing clients drain, new connections are rejected). +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("failed to read request frame from client")] + ReadFrame(#[source] io::Error), + + #[error("failed to deserialize request from client")] + InvalidRequest(#[source] wincode::ReadError), + + #[error("non-absolute path from client: {path:?}")] + NonAbsolutePath { path: OsString }, + + #[error("invalid glob pattern from client: {:?}", .0.pattern)] + InvalidGlob(Box), + + #[error("failed to write response to client")] + WriteResponse(#[source] io::Error), +} + +/// Payload for [`Error::InvalidGlob`]. Boxed so the `Error` enum stays small +/// — `vite_glob::Error` wraps `wax::BuildError` which is over 100 bytes on +/// its own. +#[derive(Debug)] +pub struct InvalidGlob { + pub pattern: Box, + pub source: vite_glob::Error, +} + +/// A [`Handler`] that records every report and resolves `get_env` against +/// a provided env map. +/// +/// Call [`Recorder::into_reports`] after the driver future completes to +/// recover the collected [`Reports`]. +pub struct Recorder { + ignored_inputs: FxHashSet>, + ignored_outputs: FxHashSet>, + cache_disabled: bool, + env_records: FxHashMap, EnvRecord>, + env_glob_records: FxHashMap, EnvGlobRecord>, + env_map: FxHashMap, Arc>, +} + +/// A record of an env value requested via `get_env`. +/// +/// `tracked` is the monotonic OR of every `tracked` flag sent for this name +/// — once `true`, it stays `true`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvRecord { + pub tracked: bool, + pub value: Option>, +} + +/// A record of a glob-pattern env query made via `get_envs`. +/// +/// `matches` is captured on the first call and reused on repeat queries — +/// the server's `env_map` is immutable for the task's lifetime, so the set +/// is stable. `tracked` is monotonic like `EnvRecord::tracked`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvGlobRecord { + pub tracked: bool, + pub matches: BTreeMap, Arc>, +} + +/// The data collected by a [`Recorder`] over the server's lifetime. +#[derive(Debug, Default)] +pub struct Reports { + pub ignored_inputs: FxHashSet>, + pub ignored_outputs: FxHashSet>, + pub cache_disabled: bool, + pub env_records: FxHashMap, EnvRecord>, + pub env_glob_records: FxHashMap, EnvGlobRecord>, +} + +impl Recorder { + #[must_use] + pub fn new(env_map: FxHashMap, Arc>) -> Self { + Self { + ignored_inputs: FxHashSet::default(), + ignored_outputs: FxHashSet::default(), + cache_disabled: false, + env_records: FxHashMap::default(), + env_glob_records: FxHashMap::default(), + env_map, + } + } + + #[must_use] + pub fn into_reports(self) -> Reports { + Reports { + ignored_inputs: self.ignored_inputs, + ignored_outputs: self.ignored_outputs, + cache_disabled: self.cache_disabled, + env_records: self.env_records, + env_glob_records: self.env_glob_records, + } + } +} + +impl Handler for Recorder { + fn ignore_input(&mut self, path: &Arc) { + self.ignored_inputs.insert(Arc::clone(path)); + } + + fn ignore_output(&mut self, path: &Arc) { + self.ignored_outputs.insert(Arc::clone(path)); + } + + fn disable_cache(&mut self) { + self.cache_disabled = true; + } + + fn get_env(&mut self, name: &OsStr, tracked: bool) -> Option> { + if let Some(existing) = self.env_records.get_mut(name) { + existing.tracked |= tracked; + return existing.value.clone(); + } + let value = self.env_map.get(name).cloned(); + self.env_records.insert(name.into(), EnvRecord { tracked, value: value.clone() }); + value + } + + fn get_envs( + &mut self, + pattern: &str, + tracked: bool, + ) -> Result, Arc>, vite_glob::Error> { + if let Some(existing) = self.env_glob_records.get_mut(pattern) { + existing.tracked |= tracked; + return Ok(existing.matches.clone()); + } + let set = vite_glob::GlobPatternSet::new(std::iter::once(pattern))?; + let matches: BTreeMap, Arc> = self + .env_map + .iter() + .filter_map(|(name, value)| { + // Env names that aren't valid UTF-8 can't be matched against a + // glob (wax patterns are UTF-8), so they're silently dropped. + // Consistent with how `collect_tracked_envs` drops non-UTF-8 + // names when building the post-run fingerprint. + let name_str = name.to_str()?; + if set.is_match(name_str) { + Some((Arc::clone(name), Arc::clone(value))) + } else { + None + } + }) + .collect(); + self.env_glob_records + .insert(Arc::from(pattern), EnvGlobRecord { tracked, matches: matches.clone() }); + Ok(matches) + } +} + +/// Handle to a running IPC server. +/// +/// `driver` must be polled to accept clients and handle messages. It resolves +/// only after [`StopAccepting::signal`] has been called AND all in-flight +/// per-client tasks have drained, returning the owned handler. +/// +/// The driver resolves to `Err(Error)` if any client triggered a protocol +/// violation (see [`Error`]). The first such error is retained; subsequent +/// errors during drain are discarded. On `Err`, the handler is not returned. +/// +/// Dropping `driver` before it resolves tears everything down immediately — +/// listener closed, per-client tasks cancelled, handler discarded. +pub struct ServerHandle<'h, H> { + pub driver: LocalBoxFuture<'h, Result>, + pub stop_accepting: StopAccepting, +} + +/// Signal that tells the server to stop accepting new clients. Existing +/// clients continue until they naturally close the connection; the driver +/// future resolves once that drain completes. +/// +/// [`signal`](Self::signal) takes `&self` and the underlying cancellation +/// is idempotent, so calling it twice or from a shared borrow is safe. +pub struct StopAccepting { + token: CancellationToken, +} + +impl StopAccepting { + pub fn signal(&self) { + self.token.cancel(); + } +} + +/// Starts an IPC server. +/// +/// Returns the env entries that a child process must inherit to find and +/// connect to this server, plus a handle bundling the driver future and the +/// `StopAccepting` signal. See [`ServerHandle`] for driver semantics. +/// +/// # Errors +/// +/// Returns an error if creating the listener fails (on Unix, this includes +/// creating the temp socket path). +pub fn serve<'h, H: Handler + 'h>( + handler: H, +) -> io::Result<(impl Iterator, ServerHandle<'h, H>)> { + let stop_token = CancellationToken::new(); + let (name, bound) = bind_listener()?; + + let run_stop = stop_token.clone(); + let driver = async move { + // Multiple per-client futures coexist inside `FuturesUnordered` and each + // calls `&mut self` handler methods. `RefCell` provides the interior + // mutability that makes these shared-access method calls compile; at + // runtime the `borrow_mut()` never conflicts because we're on a + // single-threaded runtime and handler methods are synchronous (no + // awaits, so no borrow spans a yield point). + let handler = RefCell::new(handler); + let first_err = run(bound, &handler, run_stop).await; + first_err.map_or_else(|| Ok(handler.into_inner()), Err) + } + .boxed_local(); + + Ok(( + std::iter::once((OsStr::new(IPC_ENV_NAME), name)), + ServerHandle { driver, stop_accepting: StopAccepting { token: stop_token } }, + )) +} + +#[cfg(unix)] +type Bound = tempfile::NamedTempFile; +#[cfg(windows)] +type Bound = Listener; + +#[cfg(unix)] +fn bind_listener() -> io::Result<(OsString, Bound)> { + use interprocess::local_socket::{GenericFilePath, ToFsName}; + + let bound = tempfile::Builder::new().prefix("vite_task_ipc_").make(|path| { + let name = path.to_fs_name::()?; + ListenerOptions::new().name(name).create_tokio() + })?; + let name = bound.path().as_os_str().to_owned(); + Ok((name, bound)) +} + +#[cfg(windows)] +fn bind_listener() -> io::Result<(OsString, Bound)> { + use interprocess::local_socket::{GenericNamespaced, ToNsName}; + + #[expect( + clippy::disallowed_macros, + reason = "socket name always exceeds Str inline capacity; format! is the simplest construction" + )] + let name = OsString::from(format!("vite_task_ipc_{}", uuid::Uuid::new_v4())); + + let ns_name = name.as_os_str().to_ns_name::()?; + let listener = ListenerOptions::new().name(ns_name).create_tokio()?; + Ok((name, listener)) +} + +#[cfg(unix)] +fn listener_of(bound: &Bound) -> &Listener { + bound.as_file() +} + +#[cfg(windows)] +const fn listener_of(bound: &Bound) -> &Listener { + bound +} + +async fn run( + bound: Bound, + handler: &RefCell, + shutdown: CancellationToken, +) -> Option { + let mut clients = FuturesUnordered::new(); + let mut first_err: Option = None; + + // Accept phase: accept new clients until shutdown fires. + loop { + let listener = listener_of(&bound); + tokio::select! { + () = shutdown.cancelled() => break, + accept_result = listener.accept() => { + match accept_result { + Ok(stream) => { + clients.push(handle_client(stream, handler).boxed_local()); + } + Err(err) => { + tracing::warn!(?err, "vite_task_server: accept failed"); + } + } + } + Some(result) = clients.next(), if !clients.is_empty() => { + if let Err(err) = result + && first_err.is_none() + { + first_err = Some(err); + shutdown.cancel(); + } + } + } + } + + // Stop accepting: drop the listener (and on Unix unlink the socket file). + // Existing client streams continue to work. + drop(bound); + + // Drain phase: wait for all in-flight per-client tasks to finish. + while let Some(result) = clients.next().await { + if let Err(err) = result + && first_err.is_none() + { + first_err = Some(err); + } + } + + first_err +} + +async fn handle_client(mut stream: Stream, handler: &RefCell) -> Result<(), Error> { + let mut buf = Vec::new(); + loop { + match read_frame(&mut stream, &mut buf).await { + Ok(()) => {} + Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => return Ok(()), + Err(err) => return Err(Error::ReadFrame(err)), + } + + let request: Request<'_> = + wincode::deserialize_exact(&buf).map_err(Error::InvalidRequest)?; + + match request { + Request::IgnoreInput(ns) => { + let path = native_str_to_abs_path(ns)?; + handler.borrow_mut().ignore_input(&path); + } + Request::IgnoreOutput(ns) => { + let path = native_str_to_abs_path(ns)?; + handler.borrow_mut().ignore_output(&path); + } + Request::DisableCache => handler.borrow_mut().disable_cache(), + Request::GetEnv { name, tracked } => { + let value = handler.borrow_mut().get_env(name.to_cow_os_str().as_ref(), tracked); + let boxed: Option> = value.as_deref().map(Into::into); + let response = GetEnvResponse { env_value: boxed.as_deref() }; + write_response(&mut stream, &response).await.map_err(Error::WriteResponse)?; + } + Request::GetEnvs { pattern, tracked } => { + let matches = + handler.borrow_mut().get_envs(pattern, tracked).map_err(|source| { + Error::InvalidGlob(Box::new(InvalidGlob { + pattern: Box::::from(pattern), + source, + })) + })?; + // Borrow the name/value OsStrs into NativeStr refs for the + // outgoing frame; `boxed_entries` owns the NativeStr boxes so + // their refs stay valid while `response` is serialized. + let boxed_entries: Vec<(Box, Box)> = matches + .into_iter() + .map(|(k, v)| (Box::::from(&*k), Box::::from(&*v))) + .collect(); + let entries: BTreeMap<&NativeStr, &NativeStr> = + boxed_entries.iter().map(|(k, v)| (&**k, &**v)).collect(); + let response = GetEnvsResponse { entries }; + write_response(&mut stream, &response).await.map_err(Error::WriteResponse)?; + } + } + } +} + +fn native_str_to_abs_path(ns: &NativeStr) -> Result, Error> { + let os_str = ns.to_cow_os_str(); + AbsolutePath::new(&*os_str) + .map(Arc::from) + .ok_or_else(|| Error::NonAbsolutePath { path: os_str.into_owned() }) +} + +async fn read_frame(stream: &mut Stream, buf: &mut Vec) -> io::Result<()> { + let mut len_bytes = [0u8; 4]; + stream.read_exact(&mut len_bytes).await?; + let len = u32::from_le_bytes(len_bytes) as usize; + buf.clear(); + buf.resize(len, 0); + stream.read_exact(buf).await?; + Ok(()) +} + +async fn write_response(stream: &mut Stream, response: &T) -> io::Result<()> +where + T: SchemaWrite + ?Sized, +{ + let bytes = wincode::serialize(response) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + let len = u32::try_from(bytes.len()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "response too large"))?; + stream.write_all(&len.to_le_bytes()).await?; + stream.write_all(&bytes).await?; + stream.flush().await?; + Ok(()) +} diff --git a/crates/vite_task_server/tests/integration.rs b/crates/vite_task_server/tests/integration.rs new file mode 100644 index 000000000..dd524beec --- /dev/null +++ b/crates/vite_task_server/tests/integration.rs @@ -0,0 +1,294 @@ +use std::{ + ffi::{OsStr, OsString}, + io::{self, Read, Write}, + sync::Arc, + thread, +}; + +use interprocess::local_socket::{Stream, prelude::*}; +use native_str::NativeStr; +use rustc_hash::FxHashMap; +use tokio::runtime::Builder; +use vite_task_client::Client; +use vite_task_ipc_shared::Request; +use vite_task_server::{Error, Recorder, Reports, ServerHandle, serve}; + +fn env_map(pairs: &[(&str, &str)]) -> FxHashMap, Arc> { + pairs + .iter() + .map(|(k, v)| (Arc::::from(OsStr::new(k)), Arc::::from(OsStr::new(v)))) + .collect() +} + +fn run_with_server( + envs: FxHashMap, Arc>, + client_work: F, +) -> Result +where + F: FnOnce(Vec<(&'static OsStr, OsString)>) + Send + 'static, +{ + let recorder = Recorder::new(envs); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + rt.block_on(async move { + let (envs, ServerHandle { driver, stop_accepting }) = serve(recorder).expect("bind server"); + let envs: Vec<_> = envs.collect(); + + let client = async move { + tokio::task::spawn_blocking(move || client_work(envs)) + .await + .expect("client work panicked"); + stop_accepting.signal(); + }; + + let (result, ()) = tokio::join!(driver, client); + result.map(Recorder::into_reports) + }) +} + +fn connect(envs: &[(&'static OsStr, OsString)]) -> Client { + Client::from_envs(envs.iter().map(|(k, v)| (k, v))) + .expect("connect") + .expect("serve should yield an IPC env") +} + +/// Force a round-trip so the server has definitely processed every prior +/// fire-and-forget frame on this connection: frames on a single stream are +/// read sequentially, so once the server answers a `get_env` everything +/// before it must already have been dispatched to the handler. +fn flush(client: &Client) { + let _ = client.get_env(OsStr::new("__VP_TEST_FLUSH__"), false).unwrap(); +} + +#[cfg(unix)] +fn connect_raw(name: &OsStr) -> Stream { + use interprocess::local_socket::{GenericFilePath, ToFsName}; + let resolved = name.to_fs_name::().expect("resolve socket name"); + Stream::connect(resolved).expect("connect raw") +} + +#[cfg(windows)] +fn connect_raw(name: &OsStr) -> Stream { + use interprocess::local_socket::{GenericNamespaced, ToNsName}; + let resolved = name.to_ns_name::().expect("resolve pipe name"); + Stream::connect(resolved).expect("connect raw") +} + +fn send_frame(stream: &mut Stream, request: &Request<'_>) { + let bytes = wincode::serialize(request).expect("serialize"); + let len = u32::try_from(bytes.len()).expect("frame length fits u32"); + stream.write_all(&len.to_le_bytes()).expect("write len"); + stream.write_all(&bytes).expect("write body"); + stream.flush().expect("flush"); +} + +#[test] +fn single_client_fire_and_forget() { + // Absolute paths look different on each platform; bare forward-slash + // paths are relative on Windows (no drive letter) and would be rewritten + // by the client before the server sees them. + #[cfg(unix)] + let (in_path, out_path) = ("/tmp/in.txt", "/tmp/out.txt"); + #[cfg(windows)] + let (in_path, out_path) = (r"C:\tmp\in.txt", r"C:\tmp\out.txt"); + + let reports = run_with_server(env_map(&[]), |envs| { + let client = connect(&envs); + client.ignore_input(OsStr::new(in_path)).unwrap(); + client.ignore_output(OsStr::new(out_path)).unwrap(); + client.disable_cache().unwrap(); + flush(&client); + }) + .expect("driver returned error"); + + let inputs: Vec<_> = reports.ignored_inputs.iter().map(|p| p.as_path().as_os_str()).collect(); + let outputs: Vec<_> = reports.ignored_outputs.iter().map(|p| p.as_path().as_os_str()).collect(); + assert_eq!(inputs, vec![OsStr::new(in_path)]); + assert_eq!(outputs, vec![OsStr::new(out_path)]); + assert!(reports.cache_disabled); +} + +#[test] +fn get_env_found_and_not_found() { + let reports = run_with_server(env_map(&[("NODE_ENV", "production")]), |envs| { + let client = connect(&envs); + let present = client.get_env(OsStr::new("NODE_ENV"), true).unwrap(); + assert_eq!(present.as_deref(), Some(OsStr::new("production"))); + let missing = client.get_env(OsStr::new("MISSING"), false).unwrap(); + assert!(missing.is_none()); + }) + .expect("driver returned error"); + + let node = reports.env_records.get(OsStr::new("NODE_ENV")).expect("NODE_ENV recorded"); + assert!(node.tracked); + assert_eq!(node.value.as_deref(), Some(OsStr::new("production"))); + + let missing = reports.env_records.get(OsStr::new("MISSING")).expect("MISSING recorded"); + assert!(!missing.tracked); + assert!(missing.value.is_none()); +} + +#[test] +fn get_env_tracked_upgrade_is_monotonic() { + let reports = run_with_server(env_map(&[("NODE_ENV", "production")]), |envs| { + let client = connect(&envs); + let a = client.get_env(OsStr::new("NODE_ENV"), false).unwrap(); + let b = client.get_env(OsStr::new("NODE_ENV"), true).unwrap(); + let c = client.get_env(OsStr::new("NODE_ENV"), false).unwrap(); + for v in [a, b, c] { + assert_eq!(v.as_deref(), Some(OsStr::new("production"))); + } + }) + .expect("driver returned error"); + + let node = reports.env_records.get(OsStr::new("NODE_ENV")).expect("recorded"); + assert!(node.tracked, "tracked must remain true once set"); +} + +#[test] +fn concurrent_clients() { + #[cfg(unix)] + let paths = ["/tmp/worker_0", "/tmp/worker_1", "/tmp/worker_2", "/tmp/worker_3"]; + #[cfg(windows)] + let paths = [r"C:\tmp\worker_0", r"C:\tmp\worker_1", r"C:\tmp\worker_2", r"C:\tmp\worker_3"]; + let reports = run_with_server(env_map(&[("SHARED", "value")]), move |envs| { + let threads: Vec<_> = paths + .iter() + .map(|path| { + let envs = envs.clone(); + let path = *path; + thread::spawn(move || { + let client = connect(&envs); + client.ignore_input(OsStr::new(path)).unwrap(); + let value = client.get_env(OsStr::new("SHARED"), true).unwrap(); + assert_eq!(value.as_deref(), Some(OsStr::new("value"))); + }) + }) + .collect(); + for t in threads { + t.join().unwrap(); + } + }) + .expect("driver returned error"); + + assert_eq!(reports.ignored_inputs.len(), 4); + let shared = reports.env_records.get(OsStr::new("SHARED")).expect("recorded"); + assert!(shared.tracked); + assert_eq!(shared.value.as_deref(), Some(OsStr::new("value"))); +} + +#[test] +fn relative_input_joined_with_cwd() { + let cwd = vite_path::current_dir().expect("cwd"); + let expected = cwd.as_path().join("sub/file.txt"); + + let reports = run_with_server(env_map(&[]), |envs| { + let client = connect(&envs); + client.ignore_input(OsStr::new("sub/file.txt")).unwrap(); + flush(&client); + }) + .expect("driver returned error"); + + let inputs: Vec<_> = reports.ignored_inputs.iter().map(|p| p.as_path().as_os_str()).collect(); + assert_eq!(inputs, vec![expected.as_os_str()]); +} + +#[test] +fn server_returns_error_on_non_absolute_path() { + let err = run_with_server(env_map(&[]), |envs| { + let name = &envs[0].1; + let mut stream = connect_raw(name); + + let ns: Box = OsStr::new("relative/path").into(); + send_frame(&mut stream, &Request::IgnoreInput(&ns)); + + let mut buf = [0u8; 1]; + let read_err = stream.read_exact(&mut buf).expect_err("server should close connection"); + assert_eq!(read_err.kind(), io::ErrorKind::UnexpectedEof); + }) + .expect_err("driver should surface the protocol error"); + + match err { + Error::NonAbsolutePath { path } => { + assert_eq!(path, OsStr::new("relative/path")); + } + other => panic!("unexpected error variant: {other:?}"), + } +} + +#[test] +fn get_envs_returns_matching_entries() { + let reports = run_with_server( + env_map(&[("PROBE_A", "alpha"), ("PROBE_B", "beta"), ("UNRELATED", "noise")]), + |envs| { + let client = connect(&envs); + let matches = client.get_envs("PROBE_*", true).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!( + matches.get(OsStr::new("PROBE_A")).map(AsRef::as_ref), + Some(OsStr::new("alpha")) + ); + assert_eq!( + matches.get(OsStr::new("PROBE_B")).map(AsRef::as_ref), + Some(OsStr::new("beta")) + ); + assert!(!matches.contains_key(OsStr::new("UNRELATED"))); + }, + ) + .expect("driver returned error"); + + let glob = reports.env_glob_records.get("PROBE_*").expect("glob recorded"); + assert!(glob.tracked); + assert_eq!(glob.matches.len(), 2); +} + +#[test] +fn get_envs_empty_match_set_is_recorded() { + let reports = run_with_server(env_map(&[("FOO", "x"), ("BAR", "y")]), |envs| { + let client = connect(&envs); + let matches = client.get_envs("PROBE_*", true).unwrap(); + assert!(matches.is_empty()); + }) + .expect("driver returned error"); + + let glob = reports.env_glob_records.get("PROBE_*").expect("glob recorded"); + assert!(glob.tracked); + assert!(glob.matches.is_empty()); +} + +#[test] +fn get_envs_tracked_upgrade_is_monotonic() { + let reports = run_with_server(env_map(&[("PROBE_A", "alpha")]), |envs| { + let client = connect(&envs); + let first = client.get_envs("PROBE_*", false).unwrap(); + let second = client.get_envs("PROBE_*", true).unwrap(); + let third = client.get_envs("PROBE_*", false).unwrap(); + // Same snapshot returned on every call. + assert_eq!(first, second); + assert_eq!(second, third); + }) + .expect("driver returned error"); + + let glob = reports.env_glob_records.get("PROBE_*").expect("glob recorded"); + assert!(glob.tracked, "tracked must stay true once set"); + assert_eq!(glob.matches.len(), 1); +} + +#[test] +fn get_envs_invalid_pattern_surfaces_error() { + let err = run_with_server(env_map(&[]), |envs| { + let client = connect(&envs); + // Unbalanced alternation — wax rejects this as a build error. + let send_err = client.get_envs("{unclosed", true).expect_err("server should reject"); + // The server closes the stream after sending the error; reads return EOF. + assert_eq!(send_err.kind(), io::ErrorKind::UnexpectedEof); + }) + .expect_err("driver should surface the protocol error"); + + match err { + Error::InvalidGlob(inner) => { + assert_eq!(inner.pattern.as_ref(), "{unclosed"); + } + other => panic!("unexpected error variant: {other:?}"), + } +} diff --git a/deny.toml b/deny.toml index 4644e0a61..f7c09035b 100644 --- a/deny.toml +++ b/deny.toml @@ -49,6 +49,7 @@ ignore = [ # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. allow = [ + "0BSD", "Apache-2.0", "BSD-3-Clause", "ISC", diff --git a/docs/output-restoration-research.md b/docs/output-restoration-research.md new file mode 100644 index 000000000..4f6beda8c --- /dev/null +++ b/docs/output-restoration-research.md @@ -0,0 +1,117 @@ +# Output Restoration: Compatibility with Real Build Tools + +## Background + +Output restoration automatically archives files produced by a cached task and restores them on cache hit. +When a task runs and creates `dist/`, those files are saved as a `tar.zst` archive in the cache directory. +On subsequent cache hits the archive is extracted, skipping the need to re-execute the task. + +The feature works end-to-end when tested with simple write-only commands (`vtt write-file dist/out.txt built`), +but **fails with real build tools** (Vite 8, tsdown) in the default auto-detection mode. + +## The Problem + +Deleting the output directory (`dist/`) between runs causes a **cache miss** instead of triggering output restoration. + +``` +~/packages/vite-app$ vite build +... +✓ built in 34ms + +# delete dist, run again: + +~/packages/vite-app$ vite build ○ cache miss: 'assets' removed from 'packages/vite-app/dist', executing +``` + +The archived output files exist in the cache and could be restored, but the cache validation rejects the entry before restoration has a chance to run. + +## Root Cause + +Build tools **read from their output directory** during execution. fspy captures these reads and records them as inferred inputs. When the output directory is later deleted, the inferred input fingerprint no longer matches, producing a cache miss. + +### How the cache validation pipeline works + +1. **Cache entry lookup** — match by `CacheEntryKey` (spawn fingerprint + input config + output config) +2. **Globbed input validation** — compare stored file hashes against current state for explicit input globs +3. **Post-run fingerprint validation** — compare stored fspy-inferred input fingerprints against current filesystem state +4. **If all pass** → cache hit → replay terminal output → **extract output archive** + +The failure occurs at step 3. The stored fingerprint records `packages/app/dist` as `Folder(Some({assets: Dir, index.html: File}))`. After deletion the current fingerprint is `NotFound`. The mismatch is reported before output restoration at step 4 can execute. + +### Why build tools read from the output directory + +Both Vite 8 and tsdown follow the same pattern: **clean the output directory before writing new files**, then **report compressed sizes after writing**. + +#### Vite 8 (`vite build`) + +1. **Directory cleanup** (`emptyDir()`, called from `prepareOutDirPlugin` during `renderStart`) + - `fs.readdirSync(outDir)` enumerates entries so they can be deleted before the new build + - Controlled by `emptyOutDir` (default: `true`) + - This is the primary read that causes `packages/app/dist` to appear in fspy's `path_reads` + +2. **Compressed size reporting** (rolldown's builtin `viteReporterPlugin`, Rust-native) + - After writing output files, the reporter reads each file back to compute gzip/brotli sizes + - Produces the `dist/index.html 0.15 kB │ gzip: 0.14 kB` lines + - Controlled by `reportCompressedSize` (default: `true`) + - This causes reads on individual output files like `dist/index.html`, `dist/assets/index-xxx.js` + +3. **Public directory copy** (`copyDir()`) + - If `copyPublicDir` is enabled (default: `true`), reads the public dir to copy into outDir + +#### tsdown + +1. **Directory cleanup** (`cleanOutDir()` via `tinyglobby`'s `glob()`) + - Enumerates all files in the output directory with `onlyFiles: false` before deleting them + - Default `clean: true` triggers this + - This is the primary read that causes `packages/lib/dist` to appear in fspy's `path_reads` + +2. **Shebang permission check** (`ShebangPlugin`'s `writeBundle` hook) + - Calls `access()` on output files to check existence before setting execute permissions + - Only applies to entry chunks with shebang directives + +### Why the read-write overlap check doesn't catch this + +The overlap check at `execute_spawn` (mod.rs:486-488) looks for exact path matches between `path_reads` and `path_writes`: + +```rust +pa.path_reads.keys().find(|p| pa.path_writes.contains(*p)) +``` + +fspy reports these as separate paths: + +- **Read**: `packages/app/dist` (the directory itself, via `readdirSync`) +- **Write**: `packages/app/dist/index.html`, `packages/app/dist/assets/index-xxx.js` (individual files) + +Since `packages/app/dist` ≠ `packages/app/dist/index.html`, no overlap is detected, and caching proceeds. + +### Why `should_ignore_entry` doesn't help + +`fingerprint.rs:185-187` filters `dist` when listed as a directory entry of a **parent**: + +```rust +fn should_ignore_entry(name: &[u8]) -> bool { + matches!(name, b"." | b".." | b".DS_Store") || name.eq_ignore_ascii_case(b"dist") +} +``` + +This prevents the fingerprint of `packages/app/` from changing when `dist` appears or disappears inside it. But it does not help when `packages/app/dist` itself is a direct key in `inferred_inputs` — that path is fingerprinted independently, and its transition from `Folder(...)` to `NotFound` is a mismatch. + +### Why the existing e2e test passes + +The `output-cache-test` fixture uses `vtt write-file dist/out.txt built`, a simple write-only operation. `vtt write-file` never reads from `dist/`, so fspy only records it as a write. The directory never appears in `path_reads`, the post-run fingerprint doesn't include it, and cache validation succeeds after deletion. + +This does not reflect real build tool behavior. + +## Behavior Matrix + +All tests performed with Vite 8.0.8 and tsdown 0.12.9. "Cache hit after deleting dist" means output restoration can work. + +| `input` | `output` | fspy enabled | Cache hit after deleting dist | +| -------------- | --------------- | ------------ | ----------------------------- | +| auto (default) | auto (default) | yes | **no** | +| auto (default) | `["dist/**"]` | yes | **no** | +| `["src/**"]` | auto (default) | yes | **no** | +| `["src/**"]` | `["dist/**"]` | no | **yes** | +| `["src/**"]` | `[]` (disabled) | no | yes (but no restoration) | + +The only configuration that works requires **both** explicit input globs **and** explicit output globs, which disables fspy entirely. Any configuration that enables fspy (for either auto-input or auto-output detection) causes the output directory reads to pollute the inferred input set. diff --git a/docs/output-restoration.md b/docs/output-restoration.md new file mode 100644 index 000000000..5b004e75f --- /dev/null +++ b/docs/output-restoration.md @@ -0,0 +1,120 @@ +# Output Restoration + +When a cached task is replayed, vp doesn't just replay the terminal output — it also restores any files the task produced. So if your `build` task writes to `dist/`, a cache hit will put those files right back where they belong. + +## How It Works + +By default, vp watches which files a task writes during execution. On the next run, if the cache hits, those files are extracted back into the workspace automatically. You don't need to configure anything. + +```json +{ + "tasks": { + "build": { + "command": "tsc --outDir dist" + } + } +} +``` + +After `vp run build` runs once, the compiled files in `dist/` are archived. On subsequent runs that hit the cache, `dist/` is restored from that archive instead of recompiling. + +## The `output` Field + +The `output` field lets you control which files get archived and restored. It works exactly like `input` — same glob patterns, same `auto` directive, same negative patterns. + +### Default (omitted) + +Auto-detects written files. This is usually all you need: + +```json +{ + "tasks": { + "build": { + "command": "tsc --outDir dist" + } + } +} +``` + +### Explicit Globs + +If you only want specific files restored: + +```json +{ + "tasks": { + "build": { + "command": "tsc --outDir dist", + "output": ["dist/**"] + } + } +} +``` + +This ignores any other files the task might write (temp files, logs, etc.) and only archives what's under `dist/`. + +### Negative Patterns + +Exclude certain output files from being cached: + +```json +{ + "tasks": { + "build": { + "command": "webpack", + "output": [{ "auto": true }, "!dist/cache/**"] + } + } +} +``` + +Here, everything the task writes is archived _except_ files under `dist/cache/`. + +### Disable Output Restoration + +If you don't want any files restored on cache hit — only terminal output replayed — pass an empty array: + +```json +{ + "tasks": { + "test": { + "command": "vitest run", + "output": [] + } + } +} +``` + +## `output` and `input` Are Independent + +The `output` field has its own auto-detection, separate from `input`. You can disable auto-inference for inputs while keeping it for outputs: + +```json +{ + "tasks": { + "build": { + "command": "tsc --outDir dist", + "input": ["src/**/*.ts", "tsconfig.json"], + "output": [{ "auto": true }] + } + } +} +``` + +This tracks inputs via explicit globs only (no file-read inference), but still auto-detects which files the task writes for output restoration. + +## Configuration Summary + +| Configuration | Behavior | +| ------------------------------------- | ----------------------------------- | +| `output` omitted | Auto-detect written files (default) | +| `output: [{ "auto": true }]` | Same as omitted | +| `output: ["dist/**"]` | Only restore files under `dist/` | +| `output: [{ "auto": true }, "!tmp/"]` | Auto-detect, but skip `tmp/` | +| `output: []` | Don't restore any files | + +## Notes + +- Changing the `output` configuration invalidates the cache — if you switch from `["dist/**"]` to `["build/**"]`, the next run will be a cache miss. +- Output archives are stored alongside the cache database. Running `vp cache clean` removes them. +- The `output` field can't be used with `cache: false`, same as `input`. diff --git a/docs/runner-task-ipc/design-decisions.md b/docs/runner-task-ipc/design-decisions.md new file mode 100644 index 000000000..4cb6bbab9 --- /dev/null +++ b/docs/runner-task-ipc/design-decisions.md @@ -0,0 +1,15 @@ +## Why change code in tools instead of configuring in vite-plus + +- logic locality +- dynamic decision at runtime +- provide api to tools' plugins. + +## Why implement client in rust (instead of pure js) + +- Consumable by both rust and js (via napi) +- Easier to implement sync api + +## Why provide client at runtime (instead of bundling in the tools) + +- Makes IPC protocol a implementation detail. Allows us to evolve IPC implementation or data schema without breaking clients (as long as we maintain the client API contract) +- Easier for 3rd party client implementation in other languages (for example, esbuild can create a golang wrapper over the client ffi) diff --git a/docs/runner-task-ipc/index.md b/docs/runner-task-ipc/index.md new file mode 100644 index 000000000..c09b49c2a --- /dev/null +++ b/docs/runner-task-ipc/index.md @@ -0,0 +1,38 @@ +# runner-aware tools + +## Motivation + +Report information from the tools to the runner, to help runner cache results without needing user's manual configs. + +### What information vite-task knows without runner-awareness of tools? + +- All files that are read/written by the tools +- All directory that are read/written by the tools + +### What information vite-task doesn't know without runner-awareness of tools? + +- **Why** did the tool read/write the file/directory? (e.g. files in cache should not be considered as inputs even when they are read by the tool, and should not be considered as outputs even when they are written by the tool) +- **What** env variables are the tool interested in? (they are not available to the tool's process env if the user doesn't explicitly define them in `env` in the config) +- **Whether** the tool needs be cached at all? (e.g. dev server doesn't need to be cached, but build does) + +## Implementation + +Workflow: + +1. For each spawn execution, `vite_task` starts an IPC server via `vite_task_server::serve` and passes the server's connection info plus the path to a materialized node addon into the child's env. +2. The task process loads the addon through `@voidzero-dev/vite-task-client` and reports back over IPC: which reads/writes to ignore, which envs it needs (returned by the runner from the spawn's resolved env map), and whether to disable caching. +3. When the task exits, the server drains and hands its collected reports back to `vite_task`, which feeds them into the cache layer. + +Crate / package responsibilities: + +- `vite_task_ipc_shared` — wincode-encoded request/response types and the env-var names used to hand connection info to children. +- `vite_task_server` — `Handler` trait, `serve` entry point, and a built-in `Recorder` handler that stores what the cache layer consumes. +- `vite_task_client` — sync blocking Rust client; `Client::from_envs` no-ops when the IPC env is absent. +- `vite_task_client_napi` — NAPI cdylib exposing the client's methods to Node. +- `materialized_artifact` — shared machinery for embedding the cdylib into `vp` and writing it to a temp file on first use (extracted from `fspy`). +- `@voidzero-dev/vite-task-client` — npm package with JSDoc-typed wrappers (`ignoreInput`, `ignoreOutput`, `disableCache`, `getEnv`, `getEnvs`). Lazy-loads the addon and silently no-ops when it can't connect, so tools don't break when run outside `vp`. + +Notes: + +- ignored input/output files reported from the runner are considered as part of `{ auto: true }`, which means if the user defines `input`/`output` without `auto: true`, in the config, the runner will only consider the files defined in the config as inputs/outputs, and ignore what's reported from the tools. +- envs requested from the tools are additional to the envs defined in the config. User config always wins: if an env is already defined in the config (e.g. as `untrackedEnv`), the tool cannot override it (e.g. upgrade it to tracked). diff --git a/docs/runner-task-ipc/plan.md b/docs/runner-task-ipc/plan.md new file mode 100644 index 000000000..c125bbcc9 --- /dev/null +++ b/docs/runner-task-ipc/plan.md @@ -0,0 +1,8 @@ +# Implementation Plan + +1. **Protocol** — `vite_task_ipc_shared`. Define message types and serialization. Everything else depends on this. ✅ +2. **Transport** — `vite_task_server` + `vite_task_client`. Build both sides, test them against each other directly in Rust. ✅ +3. **Extract artifact** — Pull `artifact.rs` out of fspy into a shared crate. Prerequisite for dylib embedding. ✅ +4. **JS bridge** — `vite_task_client_napi` (real impl) + `@voidzero-dev/vite-task-client` (JS wrapper with `getEnvs` logic). ✅ +5. **Runner integration** — Wire into `vite_task` spawn: start server per execution, embed/extract dylib, inject the IPC envs via `serve()`'s returned iterator. ✅ +6. **Cache integration** — Runner consumes the reported data (ignored inputs/outputs, requested envs, disable cache) and adjusts caching behavior. ✅ diff --git a/docs/runner-task-ipc/server-design.md b/docs/runner-task-ipc/server-design.md new file mode 100644 index 000000000..51e14267a --- /dev/null +++ b/docs/runner-task-ipc/server-design.md @@ -0,0 +1,147 @@ +# Server API & Lifecycle + +## Goal + +The IPC server runs per spawn execution **only when fspy is enabled**, letting tools report runtime-only facts to the runner (`ignoreInputs`, `ignoreOutputs`, `disableCache`, `getEnv`). The runner uses these reports alongside fspy's tracked accesses for cache correctness. + +## Key principles + +1. **Server doesn't take a cancellation token.** The caller signals "stop accepting" via `StopAccepting::signal()`. The server has no awareness of external cancellation. +2. **Handler is moved in, returned out.** The caller doesn't keep a reference. The driver owns the handler; on drain completion it returns it by value. No self-reference, no `&H` lifetime. +3. **`CancellationToken` is internal** — hidden from the public API (exposed only via `StopAccepting`). +4. **Driver is `!Send`**, lifetime bounded by `H`'s lifetime — if `H: 'static`, the driver is `'static`; if `H` borrows, the driver respects that. + +## Server API + +```rust +pub trait Handler { + fn ignore_input(&self, path: &Arc); + fn ignore_output(&self, path: &Arc); + fn disable_cache(&self); + fn get_env(&self, name: &str, tracked: bool) -> Option>; +} + +pub fn serve<'h, H: Handler + 'h>( + handler: H, +) -> io::Result<(impl Iterator, ServerHandle<'h, H>)>; + +pub struct ServerHandle<'h, H> { + pub driver: LocalBoxFuture<'h, H>, + pub stop_accepting: StopAccepting, +} + +pub struct StopAccepting { /* opaque */ } +impl StopAccepting { + pub fn signal(self); +} +``` + +## Driver semantics + +The driver future, when polled: + +1. **Accept phase** — accepts new clients and pumps per-client futures (`FuturesUnordered`) until `StopAccepting::signal()` fires. +2. **Listener teardown** — drops listener; Unix socket file auto-cleaned via `tempfile::NamedTempFile`. +3. **Drain phase** — waits for in-flight per-client futures to complete naturally (each ends on client EOF). +4. **Returns `H`** — the owned handler that was moved in at `serve()`. + +Dropping the driver before it resolves tears everything down immediately. Handler is dropped without being returned. + +## Lifecycle in `execute_spawn` + +### When to start + +Only when fspy is enabled (`cache_metadata.input_config.includes_auto`). No fspy → no IPC server. + +### Construction (at `ExecutionMode` build time) + +`serve()` yields an env-pair iterator that the caller chains directly into the spawn's envs. The specific env var(s) used for IPC handoff are an implementation detail between the server and client crates — the runner never has to know their names. + +```rust +let (ipc_envs, server) = serve(IpcRecorder::new(env_config))?; +let envs = cmd.all_envs.iter().map(|(k, v)| (&**k, &**v)).chain(ipc_envs); +let child = spawn(&cmd, envs, true, SpawnStdio::Piped, token).await?; +// After the child is spawned, nothing else needs the IPC envs. + +let fspy = FspyState { + negatives, + server, // ServerHandle<'h, IpcRecorder> +}; +``` + +### `FspyState` shape + +```rust +struct FspyState { + negatives: Vec>, + server: ServerHandle, +} +``` + +**Not stored:** + +- IPC env name/value — consumed once to build the spawn envs, dropped immediately. +- `handler` — lives inside `server.driver`'s async state; recovered by value when the driver resolves. + +### Driving the server during `pipe_stdio` / `child.wait` + +The driver is polled as an extra arm in the existing `tokio::select!` blocks. `LocalBoxFuture<'static, H>` is `Unpin`, so `&mut driver` is a valid select arm: + +```rust +tokio::select! { + r = &mut pipe_fut => r, + _recorder = &mut fspy_state.server.driver => { + unreachable!("driver resolved before stop_accepting.signal()") + } +} +``` + +The driver only resolves after `stop_accepting.signal()` + drain — neither happens during these phases, so the branch is unreachable. + +### Completion paths + +```rust +// Normal exit: +if !fast_fail_token.is_cancelled() && !interrupt_token.is_cancelled() { + if let Some(fspy_state) = fspy.take() { + fspy_state.server.stop_accepting.signal(); + let recorder = fspy_state.server.driver.await; + // recorder.into_reports() flows into cache-update + } +} + +// Cancellation: fspy dropped at scope end → driver dropped → teardown. +``` + +## Design-decision log + +### Why no `'static` bound on `H`? + +The driver future _owns_ the handler (via `Rc` internally). It doesn't need to outlive `H` — it just needs `H` to outlive the future. So the signature is `serve<'h, H: Handler + 'h>` and the returned `ServerHandle<'h, H>` carries the lifetime. If the caller's `H` is `'static`, the driver is `'static`; if `H` borrows, the driver respects that. + +If the caller wants to store `ServerHandle` in a struct without a lifetime parameter, they can use a `'static` handler (naturally satisfied by handlers that own all their state via `RefCell<...>` + cloned data). + +### Handler is owned by the driver, not shared via `Rc` + +The driver's async function owns `handler: H` as a local. Per-client futures borrow `&handler` from that same async state; Rust's async-fn state machine makes this self-borrow sound (the state is pinned and never moves). All per-client futures live inside `FuturesUnordered` which is also part of the same state — borrow scopes are contained. + +When drain completes and all per-client futures have been dropped, the outer async returns `handler` by move. No `Rc`, no `try_unwrap`, no panic possible. + +### Why return `H` from the driver? + +Caller doesn't keep the handler around separately. Avoids `Rc::try_unwrap` at the call site. Makes it impossible to forget recovering the state. + +### Why `StopAccepting::signal(self)` instead of exposing `CancellationToken`? + +- Hides the implementation (could swap `CancellationToken` for `oneshot` or `Notify` later). +- Reads as intent: "stop accepting" vs. "cancel". +- `self`-consuming method signals one-shot semantics. +- Keeps the public API free of `tokio_util` types. + +### Why not pass `shutdown: impl Future` or `CancellationToken`? + +Earlier direction: "server doesn't care about cancellation token; it simply stops accepting when the process exits." The caller doesn't have a token to pass — they have a moment (child exit) when they want to stop accepting. `StopAccepting::signal()` is that moment. + +### On `spawn()` changes (deferred) + +`spawn()` will need to accept extra envs (e.g. `envs: impl IntoIterator, impl AsRef)>`) so the caller can inject the IPC envs without cloning `Arc`. Not part of this step. diff --git a/docs/runner-task-ipc/todo.md b/docs/runner-task-ipc/todo.md new file mode 100644 index 000000000..6b31e6a11 --- /dev/null +++ b/docs/runner-task-ipc/todo.md @@ -0,0 +1,30 @@ +# TODO + +## Native addon should return `null` on connect failure + +**Status**: not started. + +**Decision**: when `Client::from_envs` returns `Ok(None)` (env missing) or `Err` (connect failed), the `.node` addon's `require()` must resolve to `null` instead of throwing. + +### Why + +- **Impossible to misuse** — a caller writes `const addon = require(path); if (!addon) return;` once. You can't call a method on `null`, so there's no silent-no-op trap. +- **Smallest API surface** — one null-check at load time replaces per-call `try/catch` or per-call readiness checks everywhere. +- **Reserves room to upgrade** — we can later promote truly unexpected errors (bugs, misconfiguration) to throw while keeping the expected "no runner / no socket" cases as `null`, without changing tools or the JS wrapper. Committing to "throw on failure" today closes that door forever. + +### Why not the alternatives + +- **Throw (today)** — forces every third-party consumer into `try/catch` forever. Forgetting it crashes the tool in non-runner contexts. +- **Readiness flag on an always-returned object** — pushes the no-op decision into the methods. Every call site either needs a guard or relies on silent no-op (the tool thinks it's reporting, but isn't). + +### How + +napi-rs 3.x has no hook to change what its `napi_register_module_v1` returns ([napi-3.8.5/src/bindgen_runtime/module_register.rs:487-545](https://) hard-codes `Ok(exports)`), so we have to bypass it: + +- Enable napi's `noop` feature on [crates/vite_task_client_napi/Cargo.toml](../../crates/vite_task_client_napi/Cargo.toml) — disables napi-rs's own `napi_register_module_v1` and avoids a duplicate-symbol linker error. +- Drop `napi-derive` and the `#[napi]` decorators. Rewrite [crates/vite_task_client_napi/src/lib.rs](../../crates/vite_task_client_napi/src/lib.rs) using raw `napi::sys::*`: one `#[unsafe(no_mangle)] extern "C" fn napi_register_module_v1`, four `extern "C"` callbacks, properties registered via `sys::napi_define_properties`. +- On `Ok(Some(client))`: store client in a `thread_local! { static CLIENT: OnceCell }` and populate `exports`. On any other outcome: return `sys::napi_get_null(env)`. +- Update [packages/vite-task-client/index.js](../../packages/vite-task-client/index.js) — drop the `try/catch` in `load()`; `require()`'s result is already `null` or an object. +- Add an e2e test that spawns Node without `VP_RUN_IPC_NAME` and asserts `require(addonPath) === null`. + +Estimated size: ~200 lines. We only depend on `sys::*`, which is the stable Node ABI, so napi-rs internal churn can't break us. diff --git a/docs/runner-task-ipc/transport.md b/docs/runner-task-ipc/transport.md new file mode 100644 index 000000000..c902f3614 --- /dev/null +++ b/docs/runner-task-ipc/transport.md @@ -0,0 +1,18 @@ +# IPC Transport + +Cross-platform IPC via `interprocess` crate: + +| Platform | Type | +| ------------------ | ------------------ | +| Unix (macOS/Linux) | Unix domain socket | +| Windows | Named pipe | + +The socket path or pipe name is passed to the task process via an env var shared between `vite_task_server` and `vite_task_client` (the specific name is an implementation detail). Clients check for its presence and skip IPC gracefully if absent. + +## Server Model + +One listener per task execution. The runner creates a new socket just before spawning the task and tears it down after the task exits. + +The listener runs an accept loop and handles multiple concurrent clients — build tools may spawn worker processes or threads that each connect independently. + +Platform differences are handled via `#[cfg(unix)]` / `#[cfg(windows)]`. diff --git a/docs/runner-task-ipc/vite-proposal.md b/docs/runner-task-ipc/vite-proposal.md new file mode 100644 index 000000000..2fc7ac5d5 --- /dev/null +++ b/docs/runner-task-ipc/vite-proposal.md @@ -0,0 +1,224 @@ +# Proposal: Let Vite Talk to Vite Task + +We'd like `vite build` to be cached by Vite Task **correctly** and **with zero user configuration**. This proposal adds a small dependency and a few calls in Vite's codebase to make that possible. + +## Background + +Vite Task (`vp run` in Vite+) caches a task by tracking three things: + +- the files it reads (**inputs**) +- the files it writes (**outputs**) +- the env vars it depends on (**envs**) + +Inputs and envs are **fingerprints** of the cache: any change in them triggers a cache miss and a re-run. + +Outputs are the main content of the cache: they are **restored on cache hits**. + +Vite Task discovers inputs and outputs automatically by **tracking reads and writes at the syscall level**. But envs have to be declared in the task config; Vite Task only passes declared envs through to the child process, so **undeclared envs are invisible to the task** and don't affect caching. + +A typical task config looks like this: + +```json +{ + "tasks": { + "build": { + "command": "build-script", + "cache": true, + // The user doesn't have to declare inputs and outputs. + // But they does have to declare envs. Vite Task can't track what envs a task reads. + "env": ["APP_*"] + } + } +} +``` + +## Motivation + +The goal of this proposal is to cache `vite build` **correctly**, and with **zero manual cache config**. + +Vite Task needs the Vite process to communicate two things: + +- Which file reads and writes are **internal** (the task's own cache) rather than real inputs or outputs; +- Which envs the task **actually needs** at runtime. + +## Cases && Proposed Changes + +We want to add a small dependency `@voidzero-dev/vite-task-client` to Vite that lets it talk to Vite Task at runtime. Vite calls into the client at the relevant code paths to declare "ignore this path as an input/output" or "fetch these envs from Vite Task". The calls **are no-ops when Vite runs outside Vite Task**. + +### 1. Reading envs according to `envPrefix` + +Vite's [`envPrefix`](https://vite.dev/config/shared-options#envprefix) exposes any env matching the prefix (`VITE_*` by default) to the client bundle via `import.meta.env`. + +**Current State**: For Vite Task to forward these envs to the build and fingerprint them, the user has to declare them in Vite Task config: + +```jsonc +// Vite Task config +{ "tasks": { "build": { "command": "vite build", "env": ["VITE_*"] } } } +``` + +This task config duplicates `envPrefix`. The two configs currently have to be maintained in sync by the user. Forgetting to do so silently produces incorrect bundles (because the `VITE_*` envs won't be passed to Vite). + +**Proposed Changes**: + +Add a call to `vite-task-client`'s `getEnvs` **before reading the envs matching `envPrefix`**. If Vite is running outside Vite Task, this calls is a no-op. If Vite is running inside Vite Task, it does two things: + +- It tells Vite Task which envs are relevant to the build, so they **become part of the cache fingerprint**. +- It **populates `process.env`** with those envs, so Vite can see them. + +```diff +--- a/packages/vite/src/node/env.ts ++++ b/packages/vite/src/node/env.ts +@@ loadEnv(mode, envDir, prefixes = 'VITE_') { + const parsed = Object.fromEntries(...); + for (const [key, value] of Object.entries(parsed)) + if (prefixes.some(p => key.startsWith(p))) env[key] = value; ++ for (const prefix of prefixes) getEnvs(`${prefix}*`, { tracked: true }); + for (const key in process.env) + if (prefixes.some(p => key.startsWith(p))) env[key] = process.env[key]; +``` + +### 2. Pre-bundling deps in `cacheDir` + +Vite's dep optimizer reads metadata from and writes pre-bundled deps into [`cacheDir`](https://vite.dev/config/shared-options#cachedir) (default `node_modules/.vite/`). + +**Current State**: Vite Task sees these reads and writes, and **treats them as the task's inputs and outputs**. But they're in fact neither: `cacheDir` is the dep optimizer's internal state. The user has to exclude the directory out of both lists manually: + +```jsonc +// Vite task config +{ + "input": [{ "auto": true }, { "pattern": "!node_modules/.vite/**", "base": "workspace" }], + "output": [{ "auto": true }, { "pattern": "!node_modules/.vite/**", "base": "workspace" }], +} +``` + +And `cacheDir` is user-configurable, so the path in this config has to match whatever the user set. + +**Proposed Changes**: + +Add two calls **right after resolving `depsCacheDir`**. If Vite is running outside Vite Task, these calls are no-ops. If Vite is running inside Vite Task, they do two things: + +- They tell Vite Task to **not treat reads in this directory as inputs**. +- They tell Vite Task to **not treat writes in this directory as outputs**. + +```diff +--- a/packages/vite/src/node/optimizer/index.ts ++++ b/packages/vite/src/node/optimizer/index.ts +@@ loadCachedDepOptimizationMetadata(environment, force) { + const depsCacheDir = getDepsCacheDir(environment); ++ ignoreInput(depsCacheDir); ++ ignoreOutput(depsCacheDir); +``` + +### 3. Cleaning `outDir` before writing the bundle + +Before writing the bundle, Vite calls [`emptyDir(outDir)`](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/prepareOutDir.ts#L69), which `readdirSync`s the directory to list entries for deletion. Then the bundler writes new files to the same directory. + +**Current State**: Vite Task sees `dist/` as both an input (the cleanup read) and an output (the bundle write). The user has to exclude `dist/**` from inputs while keeping it declared as an output. And [`build.outDir`](https://vite.dev/config/build-options#build-outdir) is user-configurable, so the path in this config has to match whatever the user set. + +**Proposed Changes**: + +Add a call **before `emptyDir`**. If Vite is running outside Vite Task, this call is a no-op. If Vite is running inside Vite Task, it tells Vite Task to **not treat reads in this directory as inputs**. + +```diff +--- a/packages/vite/src/node/plugins/prepareOutDir.ts ++++ b/packages/vite/src/node/plugins/prepareOutDir.ts +@@ prepareOutDir(outDirs, emptyOutDir, environment) { + for (const outDir of outDirs) { ++ ignoreInput(outDir); + if (emptyOutDir !== false && fs.existsSync(outDir)) emptyDir(outDir, ...); +``` + +### 4. Globbing files in `import.meta.glob` + +`import.meta.glob('src/**/*.ts')` lets users import every file matching a pattern. Vite expands the pattern with `tinyglobby`, which reads directory entries under `src/` and filters down to files matching `*.ts`. + +**Current State**: Vite Task sees the directory entry reads but not the filter. For `src/**/*.ts`, it knows that the entries of every directory under `src/` are read, but doesn't know that only `.ts` files matter to Vite. Adding or removing any file under `src/` (say a `.md` file the build wouldn't have picked up) invalidates the cache. + +**Proposed Changes**: + +Add a `glob` function to `@voidzero-dev/vite-task-client`. The first two arguments are the same patterns and options as `tinyglobby`'s `glob`; the third is the original `tinyglobby.glob` itself, used as the fallback when called outside Vite Task. Passing the fallback in keeps `tinyglobby` out of `@voidzero-dev/vite-task-client`'s dependencies. + +When called inside Vite Task, Vite Task does the globbing itself and returns the matching paths; the patterns and the matched paths become the fingerprint input, so only changes that would have affected the match-set invalidate the cache. When called outside Vite Task, it just calls the fallback, so behaviour is unchanged for direct `vite build` invocations. + +```diff +--- a/packages/vite/src/node/plugins/importMetaGlob.ts ++++ b/packages/vite/src/node/plugins/importMetaGlob.ts +-import { glob } from 'tinyglobby' ++import { glob as tinyglobbyGlob } from 'tinyglobby' ++import { glob } from '@voidzero-dev/vite-task-client' + + const files = ( +- await glob(globsResolved, { /* tinyglobby options */ }) ++ await glob(globsResolved, { /* tinyglobby options */ }, tinyglobbyGlob) + ).filter(...).sort() +``` + +## Details of `@voidzero-dev/vite-task-client` package + +`@voidzero-dev/vite-task-client` is a [small ESM package](https://github.com/voidzero-dev/vite-task/blob/runner-aware-tools/packages/vite-task-client/index.js) (~80 lines, no native code, no transitive deps). Its API is five functions: + +```ts +declare module '@voidzero-dev/vite-task-client' { + export function ignoreInput(path: string): void; + export function ignoreOutput(path: string): void; + export function disableCache(): void; + export function getEnv(name: string, opts?: { tracked?: boolean }): void; + export function getEnvs(pattern: string, opts?: { tracked?: boolean }): Record; + // Same patterns and options as `tinyglobby`'s `glob`. The third + // argument is the fallback used when called outside Vite Task; passing + // it in keeps `tinyglobby` out of this package's dependencies. + export function glob( + patterns: string | string[], + options: GlobOptions, + fallback: (patterns: string | string[], options: GlobOptions) => Promise, + ): Promise; +} +``` + +`getEnv` / `getEnvs` populate `process.env` for names Vite Task knows about and that aren't already set, so callers read values back via `process.env[...]` as usual. + +### Runtime mechanics + +The package is a thin wrapper. The wire protocol and IPC transport live in a node module **shipped with Vite Task**. At task start, Vite Task exports that module's path to tasks via an env var; the `@voidzero-dev/vite-task-client` wrapper lazy-requires it on first call. + +```mermaid +sequenceDiagram + participant T as Vite Task + participant V as Vite + participant W as @voidzero-dev/vite-task-client (thin wrapper) + participant N as client implementation bundled in Vite Task + T->>V: spawn with $VP_RUN_NODE_CLIENT_PATH + V->>W: import + W->>N: require($VP_RUN_NODE_CLIENT_PATH) on first call + N->>T: IPC +``` + +The thin-wrapper design has two benefits: + +- **Light logic bundled in Vite.** Only the lazy-load shim ships with Vite. The wire protocol and `node_client` ship with Vite Task. +- **Vite Task can evolve the IPC freely.** The wire protocol and `node_client`'s APIs can change in Vite Task without requiring a new Vite release. Old Vite versions keep working as long as the wrapper's public surface stays stable. + +If the env var is absent (e.g. `vite build` run directly, without Vite Task) or the addon fails to load, every exported function silently no-ops. Vite's behaviour is unchanged outside Vite Task. + +## Future additions + +This proposal covers the cases that demonstrate the high-level approach. The exact shape of the client APIs and any additional call sites can be discussed and added progressively once the high-level approach is agreed on. + +Two examples of what could come next: + +- **Call `disableCache()` in dev.** `vite dev` could opt the task out of caching completely. +- **Plugin-owned cache dirs.** Any plugin with its own read/write cache could declare it with `ignoreInput`/`ignoreOutput` instead of asking every user to declare them in Vite Task config. + +## Alternatives considered + +- **Detect `vite build` and configure its cache in Vite+.** + - `envPrefix`, `cacheDir`, and `outDir` can be dynamically computed in `vite.config.ts`. Vite+ would have to execute the config to know their values. + - Other similar cases may exist or appear in future Vite versions. Vite+ would have to learn each one's semantics and handle them case by case. It'd be easier to maintain them alongside the related code in Vite itself. + +- **Patch the Vite bundled in Vite+.** + - **Logic locality.** Each call belongs next to the Vite code that owns its path; patching from outside puts them somewhere else. + - Users with a self-installed Vite (not the Vite+ bundled one) would get no benefit. + +- **Implement this in a Vite plugin** + - The plugin doesn't have the enough hooks to cover all cases (fetch envs for `envPrefix`). + - Enhancing the plugin API to cover all cases would be a much bigger impact on Vite. diff --git a/docs/runner-task-ipc/vite-rolldown-env-operations.md b/docs/runner-task-ipc/vite-rolldown-env-operations.md new file mode 100644 index 000000000..a16fb928e --- /dev/null +++ b/docs/runner-task-ipc/vite-rolldown-env-operations.md @@ -0,0 +1,83 @@ +# Vite & Rolldown Environment Variable Operations + +## Vite + +### Reads that affect build output (must be tracked for cache correctness) + +| File | Line | Variable | Effect on output | +| -------------------------------------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | ------------------------------------------------- | +| [config.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/config.ts#L1383) | 1383 | `NODE_ENV` | Build mode, affects dead-code elimination | +| [config.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/config.ts#L1661) | 1661 | `VITE_USER_NODE_ENV` | User-set NODE_ENV from `.env` file | +| [build.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/build.ts#L644) | 644 | `NODE_ENV` | Preserved for bundler | +| [plugins/clientInjections.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/clientInjections.ts#L52) | 52 | `NODE_ENV` | Injected into client bundle | +| [plugins/define.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/define.ts#L20) | 20 | `NODE_ENV` | Define replacement in output | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L1281) | 1281 | `NODE_ENV` | Dep pre-bundling config | +| [env.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/env.ts#L86) | 86 | `VITE_*` (all matching prefix) | Injected into client bundle via `import.meta.env` | + +### Reads that do not affect output (untracked) + +| File | Line | Variable | Effect | +| ---------------------------------------------------------------------------------------------------------------- | ---- | ----------------------- | ------------------------------ | +| [logger.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/logger.ts#L84) | 84 | `CI` | Disables color output only | +| [build.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/build.ts#L1067) | 1067 | `CI` | Disables TTY progress only | +| [utils.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/utils.ts#L176) | 176 | `DEBUG` | Debug logging only | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L1269) | 1269 | `npm_config_user_agent` | Package manager detection only | + +### Writes to `process.env` + +| File | Line | Variable | Reason | +| ---------------------------------------------------------------------------------------------- | ---- | -------------------- | ----------------------------------------------------- | +| [config.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/config.ts#L1389) | 1389 | `NODE_ENV` | Sets default if unset | +| [config.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/config.ts#L1664) | 1664 | `NODE_ENV` | Overrides to `'development'` if user set it in `.env` | +| [env.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/env.ts#L62) | 62 | `VITE_USER_NODE_ENV` | Stores NODE_ENV read from `.env` | + +### `.env` file loading + +Handled by `loadEnv()` at [env.ts:27](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/env.ts#L27). Reads `.env`, `.env.local`, `.env.{mode}`, `.env.{mode}.local` from `envDir`. All `VITE_*` vars become `import.meta.env.*` in the client bundle. + +This is **file input fingerprinting**, not an env var concern — fspy automatically tracks the `readFileSync` calls on `.env` files as inferred inputs. + +--- + +## Rolldown + +### Rust — reads + +| File | Line | Variable | Effect | +| ---------------------------------------------------------------------------------------------------------------------------- | ---- | ------------------------------- | --------------------------- | +| [rolldown_binding/src/lib.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown_binding/src/lib.rs#L88) | 88 | `ROLLDOWN_MAX_BLOCKING_THREADS` | Tokio blocking thread count | +| [rolldown_binding/src/lib.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown_binding/src/lib.rs#L95) | 95 | `ROLLDOWN_WORKER_THREADS` | Tokio worker thread count | +| [rolldown_tracing/src/lib.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown_tracing/src/lib.rs#L25) | 25 | `RD_LOG` | Tracing log levels | +| [rolldown_tracing/src/lib.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown_tracing/src/lib.rs#L33) | 33 | `RD_LOG_OUTPUT` | Log output mode | + +None of these affect build output. + +### JS (NAPI binding loader) — reads + +| File | Line | Variable | Effect | +| --------------------------------------------------------------------------------------------------------------------------------- | ---- | ------------------------------- | -------------------------------------------------- | +| [packages/rolldown/src/binding.cjs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/packages/rolldown/src/binding.cjs#L64) | 64 | `NAPI_RS_NATIVE_LIBRARY_PATH` | Custom native lib path for loading `.node` binding | +| [packages/rolldown/src/binding.cjs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/packages/rolldown/src/binding.cjs#L80) | 80 | `NAPI_RS_ENFORCE_VERSION_CHECK` | Version mismatch behavior | + +--- + +## Implications for `getEnv` IPC + +Today, env vars read from `process.env` inside the task process are invisible to +the runner — no file read happens, so fspy cannot track them. The runner's current +`env`/`untrackedEnv` config requires the user to declare them manually. + +With `getEnv` IPC, a Vite plugin could request vars at runtime and have them +automatically fingerprinted: + +```ts +buildStart() { + await getEnv('NODE_ENV', { tracked: true }) // affects output — fingerprint it + await getEnv('CI', { tracked: false }) // affects behavior only — pass through +} +``` + +The key vars for Vite cache correctness via `getEnv`: + +- **`NODE_ENV`** — affects dead-code elimination, define replacements, and `import.meta.env.MODE` +- **`VITE_*`** — any matching var is injected into the client bundle; all must be tracked diff --git a/docs/runner-task-ipc/vite-rolldown-fs-operations.md b/docs/runner-task-ipc/vite-rolldown-fs-operations.md new file mode 100644 index 000000000..57cb2bf1d --- /dev/null +++ b/docs/runner-task-ipc/vite-rolldown-fs-operations.md @@ -0,0 +1,109 @@ +# Vite & Rolldown Filesystem Operations + +File reads and writes relevant to output restoration and cache fingerprinting. +All paths are relative to the package root unless noted. + +## Who writes output files + +When Vite uses Rolldown as its bundler, the actual chunk/asset writes happen in +Rolldown's Rust core (`bundle.rs`). Vite calls `bundle.write(output)` on the +`RolldownBuild` object; it does not write chunks itself. Rolldown's TypeScript +`build.ts` is only the standalone public API and is bypassed when called from +Vite. + +Vite owns only the surrounding operations: emptying the output dir and copying +public assets. + +--- + +## Vite — Output Directory (`build.outDir`, default `dist/`) + +| File | Line | Operation | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | ------------------------------------------------------------------- | +| [prepareOutDir.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/prepareOutDir.ts#L69) | 69 | read (`readdirSync`) | `emptyDir(outDir)` — lists then deletes all contents before build | +| [utils.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/utils.ts#L591) | 591 | read (`readdirSync`, `statSync`) | `emptyDir()` and `copyDir()` implementations | +| [prepareOutDir.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/prepareOutDir.ts#L89) | 89 | write | `copyDir(publicDir, outDir)` — copies public assets into output dir | +| [build.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/build.ts#L874) | 874 | write (delegates) | `bundle.write(output)` — hands off to Rolldown Rust core | +| [license.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/license.ts#L97) | 97 | write | emits `dist/.vite/license.json` via `emitFile()` | +| [license.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/plugins/license.ts#L107) | 107 | write | emits `dist/.vite/license.md` via `emitFile()` | +| [ssrManifestPlugin.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/ssr/ssrManifestPlugin.ts#L106) | 106 | write | emits `dist/.vite/ssr-manifest.json` via `emitFile()` | + +Note: `manifest.json` is emitted by a native Rolldown plugin (`native:manifest`), +not by Vite JS code. + +## Vite — Cache Directory (`cacheDir`, default `node_modules/.vite/`) + +| File | Line | Operation | Description | +| ---------------------------------------------------------------------------------------------------------------- | ---- | ------------------------ | ------------------------------------------------------------------- | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L405) | 405 | read | reads `cacheDir/deps/_metadata.json` | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L600) | 600 | read | `existsSync(depsCacheDir)` — checks cache presence | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L1417) | 1417 | read (`readdir`, `stat`) | scans for stale temp dirs older than 24h | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L531) | 531 | write | writes `package.json` (`"type":"module"`) into processing cache dir | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L586) | 586 | write | writes `_metadata.json` | +| [optimizer/index.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/optimizer/index.ts#L858) | 858 | write | `bundle.write()` — writes pre-bundled deps to `cacheDir/deps/` | +| [config.ts](https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/src/node/config.ts#L2542) | 2542 | write | creates `node_modules/.vite-temp/` for bundled config files | + +--- + +## Rolldown — TypeScript API (`output.dir`, default `dist/`) + +| File | Line | Operation | Description | +| -------------------------------------------------------------------------------------------------------------------------------- | ---- | ----------------- | ---------------------------------------------------------------------- | +| [output-options.ts](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/packages/rolldown/src/options/output-options.ts#L702) | 702 | — | `cleanDir?: boolean` option definition | +| [build.ts](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/packages/rolldown/src/api/build.ts#L65) | 65 | write (delegates) | `build.write(output)` — standalone entry point, delegates to Rust core | + +## Rolldown — Rust Core + +| File | Line | Operation | Description | +| --------------------------------------------------------------------------------------------------------------------- | ---- | ----------------- | --------------------------------------------------------------------- | +| [bundle/bundle.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/bundle/bundle.rs#L161) | 161 | read + delete | calls `clean_dir(&fs, &dist_dir)` when `clean_dir` option is set | +| [bundle/bundle.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/bundle/bundle.rs#L175) | 175 | write | `fs.create_dir_all(&dist_dir)` — creates output dir | +| [bundle/bundle.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/bundle/bundle.rs#L202) | 202 | write | `fs.create_dir_all(p)` — creates parent dirs for output chunks | +| [bundle/bundle.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/bundle/bundle.rs#L209) | 209 | write | `fs.write(&dest, chunk.content_as_bytes())` — writes each output file | +| [utils/fs_utils.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/utils/fs_utils.rs#L10) | 10 | — | `clean_dir()` function definition | +| [utils/fs_utils.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/utils/fs_utils.rs#L25) | 25 | read (`read_dir`) | lists directory entries to clean | +| [utils/fs_utils.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/utils/fs_utils.rs#L27) | 27 | delete | `remove_dir_all` for subdirectories | +| [utils/fs_utils.rs](https://github.com/rolldown/rolldown/blob/v1.0.0-rc.15/crates/rolldown/src/utils/fs_utils.rs#L29) | 29 | delete | `remove_file` for files | + +Rolldown has no disk cache. + +--- + +## Where to add `ignoreInputs` / `ignoreOutputs` + +A single Vite plugin calling the runner IPC covers all of the above because +Rolldown's Rust code runs as a NAPI addon inside the same Node.js process — +fspy traces syscalls regardless of whether they originate from JS or Rust. + +```ts +// Vite plugin (added once to vite.config.ts, no-op when VP_IPC is absent) +buildStart() { + const ipcPath = process.env.VP_IPC + if (!ipcPath) return + const outDir = this.environment.config.build.outDir // e.g. "dist" + const cacheDir = this.environment.config.cacheDir // e.g. "node_modules/.vite" + ignoreInputs([outDir, cacheDir]) // suppress reads: emptyDir, clean_dir, dep optimizer + ignoreOutputs([cacheDir]) // suppress writes: pre-bundled deps, metadata + // outDir writes are real outputs — do NOT ignore them +} +``` + +`ignoreInputs(["dist"])` covers: + +- Vite `emptyDir` reads (`readdirSync` in `utils.ts:591`) +- Rolldown `clean_dir` reads (`read_dir` in `fs_utils.rs:25`) — same process, same syscalls + +`ignoreInputs(["node_modules/.vite"])` covers: + +- Dep optimizer `readFile`, `existsSync`, `readdir` reads + +`ignoreOutputs(["node_modules/.vite"])` covers: + +- Dep optimizer `bundle.write`, `writeFileSync`, `.vite-temp` writes — not real task outputs + +### Injection without modifying `vite.config.ts` + +Vite has no env-based plugin injection mechanism. Options: + +- **`NODE_OPTIONS=--import`**: monkey-patch Vite's `build`/`createServer` before startup — works but fragile across Vite versions, requires Node 20+ +- **Explicit plugin in config**: stable, recommended — the plugin is a no-op outside of `vp run` diff --git a/packages/tools/package.json b/packages/tools/package.json index 9ffcfdfac..4698dc5bb 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -6,6 +6,7 @@ "cross-env": "^10.1.0", "oxfmt": "0.42.0", "oxlint": "catalog:", - "oxlint-tsgolint": "catalog:" + "oxlint-tsgolint": "catalog:", + "vite": "catalog:" } } diff --git a/packages/vite-task-client/README.md b/packages/vite-task-client/README.md new file mode 100644 index 000000000..a702fcea2 --- /dev/null +++ b/packages/vite-task-client/README.md @@ -0,0 +1,3 @@ +# @voidzero-dev/vite-task-client + +Node client that lets JS/TS tools report ignored inputs/outputs, fetch tracked env values, and opt out of caching when running inside a `vp run` task. diff --git a/packages/vite-task-client/index.js b/packages/vite-task-client/index.js new file mode 100644 index 000000000..fd2b139e5 --- /dev/null +++ b/packages/vite-task-client/index.js @@ -0,0 +1,86 @@ +import { createRequire } from 'node:module'; + +/** + * @typedef {{ + * ignoreInput(path: string): void, + * ignoreOutput(path: string): void, + * disableCache(): void, + * getEnv(name: string, tracked: boolean): (string | null), + * getEnvs(pattern: string, tracked: boolean): Record, + * }} Addon + */ + +/** @type {Addon | null | undefined} */ +let addon; + +/** @returns {Addon | null} */ +function load() { + if (addon !== undefined) return addon; + try { + const path = process.env.VP_RUN_NODE_CLIENT_PATH; + if (path) { + addon = /** @type {Addon} */ (createRequire(import.meta.url)(path)); + return addon; + } + } catch { + // Fall through — the runner's IPC env is absent or the addon refused to load. + // Memoize the unavailable decision so subsequent calls don't retry. + } + addon = null; + return addon; +} + +/** @param {string} path */ +export function ignoreInput(path) { + load()?.ignoreInput(path); +} + +/** @param {string} path */ +export function ignoreOutput(path) { + load()?.ignoreOutput(path); +} + +export function disableCache() { + load()?.disableCache(); +} + +/** + * Asks the runner for the env var `name` and, when it isn't already set in + * `process.env`, populates it. The caller reads the value back via + * `process.env[name]`. + * + * The runner is always consulted — even when `process.env[name]` is already + * set — so that, with `tracked: true`, the dependency is recorded regardless + * of whether the value came from the runner or was already in the + * environment. + * + * @param {string} name + * @param {{ tracked?: boolean }} [options] + */ +export function getEnv(name, { tracked = true } = {}) { + const a = load(); + if (!a) return; + const value = a.getEnv(name, tracked); + if (value != null && process.env[name] === undefined) { + process.env[name] = value; + } +} + +/** + * Asks the runner for every env whose name matches `pattern` (a glob, e.g. + * `VITE_*`) and returns the match-set as a plain object. Entries are also + * written to `process.env` for names that aren't already set. + * + * @param {string} pattern + * @param {{ tracked?: boolean }} [options] + * @returns {Record} + */ +export function getEnvs(pattern, { tracked = true } = {}) { + const a = load(); + if (!a) return {}; + const entries = a.getEnvs(pattern, tracked); + for (const [k, v] of Object.entries(entries)) { + if (process.env[k] === undefined) process.env[k] = v; + } + return entries; +} diff --git a/packages/vite-task-client/package.json b/packages/vite-task-client/package.json new file mode 100644 index 000000000..5b570e97a --- /dev/null +++ b/packages/vite-task-client/package.json @@ -0,0 +1,7 @@ +{ + "name": "@voidzero-dev/vite-task-client", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./index.js" +} diff --git a/patches/vite.patch b/patches/vite.patch new file mode 100644 index 000000000..0495f3987 --- /dev/null +++ b/patches/vite.patch @@ -0,0 +1,68 @@ +diff --git a/dist/node/chunks/node.js b/dist/node/chunks/node.js +index 5be94a01d8aecf2502e76c05087b207980f2b06d..e7fb05af8e958007277ca137e1b9decbd1e47d33 100644 +--- a/dist/node/chunks/node.js ++++ b/dist/node/chunks/node.js +@@ -1,6 +1,10 @@ + import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk.js"; + import { A as OPTIMIZABLE_ENTRY_RE, C as ERR_FILE_NOT_FOUND_IN_OPTIMIZED_DEP_DIR, D as JS_TYPES_RE, E as FS_PREFIX, F as defaultAllowedOrigins, I as loopbackHosts, L as wildcardHosts, M as SPECIAL_QUERY_RE, N as VERSION, O as KNOWN_ASSET_TYPES, P as VITE_PACKAGE_DIR, R as require_picocolors, S as ENV_PUBLIC_PATH, T as ESBUILD_BASELINE_WIDELY_AVAILABLE_TARGET, _ as DEFAULT_SERVER_CONDITIONS, a as CLIENT_ENTRY, b as DEV_PROD_CONDITION, c as DEFAULT_ASSETS_INLINE_LIMIT, d as DEFAULT_CLIENT_MAIN_FIELDS, f as DEFAULT_CONFIG_FILES, g as DEFAULT_PREVIEW_PORT, h as DEFAULT_EXTERNAL_CONDITIONS, i as CLIENT_DIR, j as ROLLUP_HOOKS, k as METADATA_FILENAME, l as DEFAULT_ASSETS_RE, m as DEFAULT_EXTENSIONS, n as createLogger, o as CLIENT_PUBLIC_PATH, p as DEFAULT_DEV_PORT, r as printServerUrls, s as CSS_LANGS_RE, t as LogLevels, u as DEFAULT_CLIENT_CONDITIONS, v as DEFAULT_SERVER_MAIN_FIELDS, w as ERR_OPTIMIZE_DEPS_PROCESSING_ERROR, x as ENV_ENTRY, y as DEP_VERSION_RE } from "./logger.js"; + import { builtinModules, createRequire } from "node:module"; ++// vp:runner-aware-tools — report inputs/outputs/tracked envs to the runner so ++// that caching of `vite build` works without manual input/output config. ++// No-op when `@voidzero-dev/vite-task-client` is not connected to a runner. ++import { ignoreInput, ignoreOutput, getEnvs } from "@voidzero-dev/vite-task-client"; + import { parseAst, parseAstAsync } from "rolldown/parseAst"; + import { esmExternalRequirePlugin, esmExternalRequirePlugin as esmExternalRequirePlugin$1 } from "rolldown/plugins"; + import { TsconfigCache, Visitor, minify, minifySync, parse, parseSync, transformSync } from "rolldown/utils"; +@@ -4998,6 +5002,12 @@ function loadEnv(mode, envDir, prefixes = "VITE_") { + processEnv: { ...process.env } + }); + for (const [key, value] of Object.entries(parsed)) if (prefixes.some((prefix) => key.startsWith(prefix))) env[key] = value; ++ // vp:runner-aware-tools — ask the runner for every env matching each ++ // prefix. The match-set becomes part of the post-run fingerprint, so any ++ // add/remove/change to a matching env invalidates the cache. `getEnvs` ++ // populates `process.env` for names not already set, letting the loop ++ // below pick them up alongside inline inherited envs. ++ for (const prefix of prefixes) getEnvs(`${prefix}*`, { tracked: true }); + for (const key in process.env) if (prefixes.some((prefix) => key.startsWith(prefix))) env[key] = process.env[key]; + debug$12?.(`using resolved env: %O`, env); + return env; +@@ -31322,6 +31332,12 @@ async function loadCachedDepOptimizationMetadata(environment, force = environmen + setTimeout(() => cleanupDepsCacheStaleDirs(environment.getTopLevelConfig()), 0).unref(); + } + const depsCacheDir = getDepsCacheDir(environment); ++ // vp:runner-aware-tools — the dep optimizer cache is read and written ++ // under the same directory (metadata, pre-bundled deps). Tell the runner ++ // to treat the directory as neither input nor output; the lockfile hash ++ // stored in the metadata already drives invalidation of the real inputs. ++ ignoreInput(depsCacheDir); ++ ignoreOutput(depsCacheDir); + if (!force) { + let cachedMetadata; + try { +@@ -32706,6 +32722,10 @@ function prepareOutDir(outDirs, emptyOutDir, environment) { + const { publicDir } = environment.config; + const outDirsArray = [...outDirs]; + for (const outDir of outDirs) { ++ // vp:runner-aware-tools — emptyDir() below reads the entries of outDir. ++ // Without ignoring it, those reads become inferred inputs and mix with ++ // the writes that follow, tripping the runner's read-write overlap check. ++ ignoreInput(outDir); + if (emptyOutDir !== false && fs.existsSync(outDir)) emptyDir(outDir, [...outDirsArray.map((dir) => { + const relative = path.relative(outDir, dir); + if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) return relative; +@@ -34675,6 +34695,15 @@ async function loadConfigFromBundledFile(fileName, bundledCode, isESM) { + if (e.code === "EACCES") nodeModulesDir = void 0; + else throw e; + } ++ // vp:runner-aware-tools — the bundled config is written into ++ // `.vite-temp/..mjs` and then imported back. Ignore ++ // the directory as both input and output so the read-write of this ++ // transient file doesn't poison the runner's cache. ++ if (nodeModulesDir) { ++ const viteTempDir = path.resolve(nodeModulesDir, ".vite-temp"); ++ ignoreInput(viteTempDir); ++ ignoreOutput(viteTempDir); ++ } + const hash = `timestamp-${Date.now()}-${Math.random().toString(16).slice(2)}`; + const tempFileName = nodeModulesDir ? path.resolve(nodeModulesDir, `.vite-temp/${path.basename(fileName)}.${hash}.mjs`) : `${fileName}.${hash}.mjs`; + await fsp.writeFile(tempFileName, bundledCode); diff --git a/playground/README.md b/playground/README.md index 863dce49b..fee21627d 100644 --- a/playground/README.md +++ b/playground/README.md @@ -7,8 +7,8 @@ A workspace for manually testing `cargo run --bin vt run ...`. ``` playground/ ├── packages/ -│ ├── app/ → depends on @playground/lib -│ ├── lib/ → depends on @playground/utils +│ ├── app/ → depends on lib +│ ├── lib/ → depends on utils │ └── utils/ → no dependencies └── vite-task.json → workspace-level task config ``` @@ -19,10 +19,10 @@ Dependency chain: `app → lib → utils` Tasks are defined in each package's `vite-task.json` with caching enabled. `dev` is a package.json script (not cached). -| Name | Type | Packages | Cached | Description | -| ----------- | ------ | --------------- | ------ | ---------------------------------------------- | -| `build` | task | app, lib, utils | yes | Prints a build message | -| `test` | task | app, lib, utils | yes | Prints a test message | -| `lint` | task | app, lib, utils | yes | Prints a lint message | -| `typecheck` | task | app, lib | yes | Prints a typecheck message | -| `dev` | script | app, lib | no | Long-running process (prints every 2s, ctrl-c) | +| Name | Type | Packages | Cached | Description | +| ----------- | ------ | --------------- | ------ | ----------------------------------------------------- | +| `build` | task | app, lib, utils | yes | `vite build` in app; prints a build message elsewhere | +| `test` | task | app, lib, utils | yes | Prints a test message | +| `lint` | task | app, lib, utils | yes | Prints a lint message | +| `typecheck` | task | app, lib | yes | Prints a typecheck message | +| `dev` | script | app, lib | no | Long-running process (prints every 2s, ctrl-c) | diff --git a/playground/packages/app/build.mjs b/playground/packages/app/build.mjs deleted file mode 100644 index 9b3bc870b..000000000 --- a/playground/packages/app/build.mjs +++ /dev/null @@ -1 +0,0 @@ -console.log('Building app'); diff --git a/playground/packages/app/index.html b/playground/packages/app/index.html new file mode 100644 index 000000000..d4dce07f9 --- /dev/null +++ b/playground/packages/app/index.html @@ -0,0 +1,9 @@ + + + + playground-app + + + + + diff --git a/playground/packages/app/package.json b/playground/packages/app/package.json index 055ace37f..6f1323384 100644 --- a/playground/packages/app/package.json +++ b/playground/packages/app/package.json @@ -1,11 +1,13 @@ { - "name": "@playground/app", + "name": "app", "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "node dev.mjs" }, "dependencies": { - "@playground/lib": "workspace:*" + "lib": "workspace:*", + "vite": "catalog:" } } diff --git a/playground/packages/app/src/index.ts b/playground/packages/app/src/index.ts index 7db55b497..4169f3ebc 100644 --- a/playground/packages/app/src/index.ts +++ b/playground/packages/app/src/index.ts @@ -1,3 +1,13 @@ -import { sum } from '@playground/lib'; +/// -console.log(sum(1, 2, 3)); +import { sum } from 'lib'; + +// `import.meta.env.VITE_MODE` is substituted at build time from the value +// vite's patched `loadEnv` fetched via the runner. Dead-code elimination +// leaves only the branch matching whatever `VITE_MODE=...` was set on the +// run, so the bundle reflects (and the runner tracks) that env. +if (import.meta.env.VITE_MODE === 'production') { + console.log('PROD build:', sum(1, 2, 3)); +} else { + console.log('DEV build:', sum(1, 2, 3)); +} diff --git a/playground/packages/app/vite-task.json b/playground/packages/app/vite-task.json index 58a9b2b48..1ad6fee4b 100644 --- a/playground/packages/app/vite-task.json +++ b/playground/packages/app/vite-task.json @@ -1,7 +1,8 @@ { "tasks": { "build": { - "command": "node build.mjs" + "command": "vite build", + "cache": true }, "test": { "command": "node test.mjs" diff --git a/playground/packages/lib/package.json b/playground/packages/lib/package.json index fddb3aab3..e8b699cbc 100644 --- a/playground/packages/lib/package.json +++ b/playground/packages/lib/package.json @@ -1,11 +1,12 @@ { - "name": "@playground/lib", + "name": "lib", "version": "0.0.0", "private": true, + "main": "./src/index.ts", "scripts": { "dev": "node dev.mjs" }, "dependencies": { - "@playground/utils": "workspace:*" + "utils": "workspace:*" } } diff --git a/playground/packages/lib/src/index.ts b/playground/packages/lib/src/index.ts index f7fa1e131..8e6da590d 100644 --- a/playground/packages/lib/src/index.ts +++ b/playground/packages/lib/src/index.ts @@ -1,4 +1,4 @@ -import { add } from '@playground/utils'; +import { add } from 'utils'; export function sum(...nums: number[]): number { return nums.reduce((acc, n) => add(acc, n), 0); diff --git a/playground/packages/utils/package.json b/playground/packages/utils/package.json index 8036670aa..656f9b115 100644 --- a/playground/packages/utils/package.json +++ b/playground/packages/utils/package.json @@ -1,5 +1,6 @@ { - "name": "@playground/utils", + "name": "utils", "version": "0.0.0", - "private": true + "private": true, + "main": "./src/index.ts" } diff --git a/playground/pnpm-lock.yaml b/playground/pnpm-lock.yaml index ef78a0a05..d92c2add1 100644 --- a/playground/pnpm-lock.yaml +++ b/playground/pnpm-lock.yaml @@ -4,20 +4,511 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + vite: + specifier: ^8.0.8 + version: 8.0.8 + +packageExtensionsChecksum: sha256-0p1nijyIYmO20Zod8fYruINNF8yHQutWS96PraqUHIA= + +patchedDependencies: + vite: + hash: 5ec58c1f268599c3444d08666dce1fccc2e2fdd844e381ef2fa2df9f03591c80 + path: ../patches/vite.patch + importers: .: {} + ../packages/vite-task-client: {} + packages/app: dependencies: - '@playground/lib': + lib: specifier: workspace:* version: link:../lib + vite: + specifier: 'catalog:' + version: 8.0.8(patch_hash=5ec58c1f268599c3444d08666dce1fccc2e2fdd844e381ef2fa2df9f03591c80) packages/lib: dependencies: - '@playground/utils': + utils: specifier: workspace:* version: link:../utils packages/utils: {} + +packages: + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.124.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + detect-libc@2.1.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tslib@2.8.1: + optional: true + + vite@8.0.8(patch_hash=5ec58c1f268599c3444d08666dce1fccc2e2fdd844e381ef2fa2df9f03591c80): + dependencies: + '@voidzero-dev/vite-task-client': link:../packages/vite-task-client + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 diff --git a/playground/pnpm-workspace.yaml b/playground/pnpm-workspace.yaml index 924b55f42..eca67dd69 100644 --- a/playground/pnpm-workspace.yaml +++ b/playground/pnpm-workspace.yaml @@ -1,2 +1,14 @@ packages: - packages/* + - ../packages/vite-task-client + +catalog: + vite: ^8.0.8 + +packageExtensions: + vite: + dependencies: + '@voidzero-dev/vite-task-client': workspace:* + +patchedDependencies: + vite: ../patches/vite.patch diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23db50114..4b4d850be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,16 @@ catalogs: oxlint-tsgolint: specifier: ^0.18.0 version: 0.18.1 + vite: + specifier: ^8.0.8 + version: 8.0.8 + +packageExtensionsChecksum: sha256-0p1nijyIYmO20Zod8fYruINNF8yHQutWS96PraqUHIA= + +patchedDependencies: + vite: + hash: b38026c782eca35f41bf6fb18d0ba00d236b0e74fa581ca45fbc6abf51b365dd + path: patches/vite.patch importers: @@ -62,12 +72,35 @@ importers: oxlint-tsgolint: specifier: 'catalog:' version: 0.18.1 + vite: + specifier: 'catalog:' + version: 8.0.8(patch_hash=b38026c782eca35f41bf6fb18d0ba00d236b0e74fa581ca45fbc6abf51b365dd)(@types/node@25.0.3)(yaml@2.8.2) + + packages/vite-task-client: {} packages: + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxfmt/binding-android-arm-eabi@0.42.0': resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} engines: {node: ^20.19.0 || >=22.12.0} @@ -342,6 +375,107 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} @@ -385,6 +519,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -395,10 +533,24 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -419,6 +571,80 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lint-staged@16.3.3: resolution: {integrity: sha512-RLq2koZ5fGWrx7tcqx2tSTMQj4lRkfNJaebO/li/uunhCJbtZqwTuwPHpgIimAHHi/2nZIiGrkCHDCOeR1onxA==} engines: {node: '>=20.17'} @@ -440,6 +666,11 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -467,10 +698,21 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -478,6 +720,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -498,6 +745,10 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -518,6 +769,10 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -526,9 +781,55 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -545,8 +846,33 @@ packages: snapshots: + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@epic-web/invariant@1.0.0': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.124.0': {} + '@oxfmt/binding-android-arm-eabi@0.42.0': optional: true @@ -679,6 +1005,62 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.55.0': optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 @@ -719,16 +1101,25 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + detect-libc@2.1.2: {} + emoji-regex@10.6.0: {} environment@1.1.0: {} eventemitter3@5.0.4: {} + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + fsevents@2.3.3: + optional: true + get-east-asian-width@1.5.0: {} husky@9.1.7: {} @@ -741,6 +1132,55 @@ snapshots: isexe@2.0.0: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lint-staged@16.3.3: dependencies: commander: 14.0.3 @@ -774,6 +1214,8 @@ snapshots: mimic-function@5.0.1: {} + nanoid@3.3.11: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -836,8 +1278,18 @@ snapshots: path-key@3.1.1: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.4: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -845,6 +1297,27 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -863,6 +1336,8 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} + string-argv@0.3.2: {} string-width@7.2.0: @@ -882,14 +1357,35 @@ snapshots: tinyexec@1.0.2: {} + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@2.1.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tslib@2.8.1: + optional: true + undici-types@7.16.0: {} + vite@8.0.8(patch_hash=b38026c782eca35f41bf6fb18d0ba00d236b0e74fa581ca45fbc6abf51b365dd)(@types/node@25.0.3)(yaml@2.8.2): + dependencies: + '@voidzero-dev/vite-task-client': link:packages/vite-task-client + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.0.3 + fsevents: 2.3.3 + yaml: 2.8.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7f2b69a66..5779ae47a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - . - packages/tools + - packages/vite-task-client catalog: '@types/node': 25.0.3 @@ -9,5 +10,14 @@ catalog: oxfmt: 0.42.0 oxlint: ^1.55.0 oxlint-tsgolint: ^0.18.0 + vite: ^8.0.8 catalogMode: prefer + +packageExtensions: + vite: + dependencies: + '@voidzero-dev/vite-task-client': workspace:* + +patchedDependencies: + vite: patches/vite.patch