From 8c3cc35e31713738f45756b42e80996d97ec6429 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 25 Mar 2026 11:07:52 +0800 Subject: [PATCH 1/2] fix: merge synthetic task's input config in inherited cache path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a synthetic task (e.g., vp pack, vp build) runs inside a parent script task, the parent's cache config is inherited. Previously, only env and untracked_env were merged from the synthetic — the input config was silently dropped, causing negative glob exclusions to be ignored. Now the synthetic's input globs are resolved relative to the workspace root and merged into the parent's input_config. This allows callers to pass negative globs (e.g., !node_modules/.vite-temp/**) that correctly exclude paths from fspy tracking. Closes https://github.com/voidzero-dev/vite-plus/issues/1095 --- .../cache-negative-input-glob/build.mjs | 18 +++++++++++++++++ .../cache-negative-input-glob/package.json | 4 ++++ .../cache-negative-input-glob/snapshots.toml | 18 +++++++++++++++++ ...glob excludes vite-temp from tracking.snap | 13 ++++++++++++ ...s cache miss with negative input glob.snap | 12 +++++++++++ .../cache-negative-input-glob/src/index.ts | 1 + .../cache-negative-input-glob/vite-task.json | 10 ++++++++++ crates/vite_task_plan/src/plan.rs | 20 ++++++++++++++++++- 8 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/build.mjs create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/package.json create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots.toml create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/negative input glob excludes vite-temp from tracking.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/source change still causes cache miss with negative input glob.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/src/index.ts create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/vite-task.json diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/build.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/build.mjs new file mode 100644 index 00000000..42982d61 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/build.mjs @@ -0,0 +1,18 @@ +// Simulates Vite's behavior during build: +// 1. Reads the source file (tracked as input by fspy) +// 2. Writes a temp config to node_modules/.vite-temp/ (also tracked by fspy) +// Without the negative input glob, step 2 causes a read-write overlap +// that prevents caching. + +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; + +// Read source (tracked as input) +const source = readFileSync('src/index.ts', 'utf-8'); + +// Simulate Vite writing temp config (read-write to .vite-temp) +const tempDir = 'node_modules/.vite-temp'; +mkdirSync(tempDir, { recursive: true }); +const tempFile = `${tempDir}/vite.config.ts.timestamp-${Date.now()}.mjs`; +writeFileSync(tempFile, `// compiled config\nexport default {};`); + +console.log(`built: ${source.trim()}`); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/package.json new file mode 100644 index 00000000..74a60e6e --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/package.json @@ -0,0 +1,4 @@ +{ + "name": "cache-negative-input-glob", + "private": true +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots.toml new file mode 100644 index 00000000..40a9c95e --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots.toml @@ -0,0 +1,18 @@ +# Tests that negative input globs exclude paths from fspy tracking, +# so tasks writing to excluded paths (like node_modules/.vite-temp) are still cached. +# See: https://github.com/voidzero-dev/vite-plus/issues/1095 + +[[e2e]] +name = "negative input glob excludes vite-temp from tracking" +steps = [ + "vt run build # first run: cache miss", + "vt run build # second run: should hit cache despite .vite-temp write", +] + +[[e2e]] +name = "source change still causes cache miss with negative input glob" +steps = [ + "vt run build # first run: cache miss", + "replace-file-content src/index.ts world universe # modify real source", + "vt run build # cache miss: source changed", +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/negative input glob excludes vite-temp from tracking.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/negative input glob excludes vite-temp from tracking.snap new file mode 100644 index 00000000..7b8ce5c4 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/negative input glob excludes vite-temp from tracking.snap @@ -0,0 +1,13 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +> vt run build # first run: cache miss +$ node build.mjs +built: export const hello = 'world'; +> vt run build # second run: should hit cache despite .vite-temp write +$ node build.mjs ◉ cache hit, replaying +built: export const hello = 'world'; + +--- +vt run: cache hit, saved. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/source change still causes cache miss with negative input glob.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/source change still causes cache miss with negative input glob.snap new file mode 100644 index 00000000..00dc2fd2 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/snapshots/source change still causes cache miss with negative input glob.snap @@ -0,0 +1,12 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +> vt run build # first run: cache miss +$ node build.mjs +built: export const hello = 'world'; +> replace-file-content src/index.ts world universe # modify real source + +> vt run build # cache miss: source changed +$ node build.mjs ○ cache miss: 'src/index.ts' modified, executing +built: export const hello = 'universe'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/src/index.ts b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/src/index.ts new file mode 100644 index 00000000..35f468bf --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/src/index.ts @@ -0,0 +1 @@ +export const hello = 'world'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/vite-task.json new file mode 100644 index 00000000..a6bd2951 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-negative-input-glob/vite-task.json @@ -0,0 +1,10 @@ +{ + "cache": true, + "tasks": { + "build": { + "command": "node build.mjs", + "cache": true, + "input": [{ "auto": true }, "!node_modules/.vite-temp/**"] + } + } +} diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index ebaaec25..db2cece9 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -20,7 +20,7 @@ use vite_str::Str; use vite_task_graph::{ TaskNodeIndex, TaskSource, config::{ - CacheConfig, ResolvedGlobalCacheConfig, ResolvedTaskOptions, + CacheConfig, ResolvedGlobalCacheConfig, ResolvedInputConfig, ResolvedTaskOptions, user::{UserCacheConfig, UserTaskOptions}, }, query::TaskQuery, @@ -464,6 +464,24 @@ fn resolve_synthetic_cache_config( if let Some(extra_pts) = enabled_cache_config.untracked_env { parent_config.env_config.untracked_env.extend(extra_pts); } + // Merge synthetic's input config (resolve relative to workspace root, + // since synthetic commands operate at CLI level, not package level) + if let Some(ref input) = enabled_cache_config.input { + let synthetic_input = ResolvedInputConfig::from_user_config( + Some(input), + workspace_path, + workspace_path, + ) + .map_err(Error::ResolveTaskConfig)?; + parent_config + .input_config + .negative_globs + .extend(synthetic_input.negative_globs); + parent_config + .input_config + .positive_globs + .extend(synthetic_input.positive_globs); + } Some(parent_config) } }) From 1fdb09a9fd7236bf9c5727402c8846a354cc40cb Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 25 Mar 2026 11:28:53 +0800 Subject: [PATCH 2/2] test: add unit tests for resolve_synthetic_cache_config input merging --- crates/vite_task_plan/src/plan.rs | 209 ++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index db2cece9..4ae66e43 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -767,3 +767,212 @@ pub async fn plan_query_request( Error::CycleDependencyDetected(displays) }) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use rustc_hash::FxHashSet; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + use vite_task_graph::config::{ + CacheConfig, EnvConfig, ResolvedInputConfig, + user::{EnabledCacheConfig, UserCacheConfig, UserInputEntry}, + }; + + use super::{ParentCacheConfig, resolve_synthetic_cache_config}; + + fn make_workspace_path() -> AbsolutePathBuf { + #[cfg(unix)] + { + AbsolutePathBuf::new("/workspace".into()).unwrap() + } + #[cfg(windows)] + { + AbsolutePathBuf::new("C:\\workspace".into()).unwrap() + } + } + + fn make_parent_config() -> CacheConfig { + CacheConfig { + env_config: EnvConfig { + fingerprinted_envs: FxHashSet::default(), + untracked_env: FxHashSet::default(), + }, + input_config: ResolvedInputConfig::default_auto(), + } + } + + #[test] + fn inherited_disabled_synthetic_returns_none() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(make_parent_config()), + UserCacheConfig::disabled(), + &cwd, + &workspace, + ) + .unwrap(); + assert!(result.is_none()); + } + + #[test] + fn disabled_parent_returns_none() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Disabled, + UserCacheConfig::with_config(EnabledCacheConfig { + env: None, + untracked_env: None, + input: None, + }), + &cwd, + &workspace, + ) + .unwrap(); + assert!(result.is_none()); + } + + #[test] + fn inherited_merges_env() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(make_parent_config()), + UserCacheConfig::with_config(EnabledCacheConfig { + env: Some(Box::new([Str::from("VITE_*")])), + untracked_env: Some(vec![Str::from("HOME")]), + input: None, + }), + &cwd, + &workspace, + ) + .unwrap() + .unwrap(); + assert!(result.env_config.fingerprinted_envs.contains("VITE_*")); + assert!(result.env_config.untracked_env.contains("HOME")); + } + + #[test] + fn inherited_merges_negative_input_globs() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(make_parent_config()), + UserCacheConfig::with_config(EnabledCacheConfig { + env: None, + untracked_env: None, + input: Some(vec![ + UserInputEntry::Auto { auto: true }, + UserInputEntry::Glob(Str::from("!node_modules/.vite-temp/**")), + ]), + }), + &cwd, + &workspace, + ) + .unwrap() + .unwrap(); + assert!(result.input_config.includes_auto); + assert!( + result.input_config.negative_globs.contains(&Str::from("node_modules/.vite-temp/**")), + "negative globs should contain the vite-temp exclusion, got: {:?}", + result.input_config.negative_globs, + ); + } + + #[test] + fn inherited_merges_positive_input_globs() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(make_parent_config()), + UserCacheConfig::with_config(EnabledCacheConfig { + env: None, + untracked_env: None, + input: Some(vec![UserInputEntry::Glob(Str::from("src/**"))]), + }), + &cwd, + &workspace, + ) + .unwrap() + .unwrap(); + assert!( + result.input_config.positive_globs.contains(&Str::from("src/**")), + "positive globs should contain src/**, got: {:?}", + result.input_config.positive_globs, + ); + } + + #[test] + fn inherited_input_none_preserves_parent_config() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let mut parent = make_parent_config(); + parent.input_config.negative_globs.insert(Str::from("existing/**")); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(parent), + UserCacheConfig::with_config(EnabledCacheConfig { + env: None, + untracked_env: None, + input: None, + }), + &cwd, + &workspace, + ) + .unwrap() + .unwrap(); + assert_eq!(result.input_config.negative_globs, BTreeSet::from([Str::from("existing/**")]),); + } + + #[test] + fn inherited_merges_input_with_existing_parent_globs() { + let workspace = make_workspace_path(); + let cwd = workspace.clone().into(); + let mut parent = make_parent_config(); + parent.input_config.negative_globs.insert(Str::from("dist/**")); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(parent), + UserCacheConfig::with_config(EnabledCacheConfig { + env: None, + untracked_env: None, + input: Some(vec![UserInputEntry::Glob(Str::from("!node_modules/.vite-temp/**"))]), + }), + &cwd, + &workspace, + ) + .unwrap() + .unwrap(); + assert_eq!( + result.input_config.negative_globs, + BTreeSet::from([Str::from("dist/**"), Str::from("node_modules/.vite-temp/**"),]), + ); + } + + #[test] + fn inherited_input_globs_resolved_relative_to_workspace_root() { + let workspace = make_workspace_path(); + // cwd is a subdirectory, but globs should resolve relative to workspace root + let cwd = workspace.join("packages/hello").into(); + let result = resolve_synthetic_cache_config( + ParentCacheConfig::Inherited(make_parent_config()), + UserCacheConfig::with_config(EnabledCacheConfig { + env: None, + untracked_env: None, + input: Some(vec![UserInputEntry::Glob(Str::from("!node_modules/.vite-temp/**"))]), + }), + &cwd, + &workspace, + ) + .unwrap() + .unwrap(); + // The glob should be workspace-root-relative, NOT cwd-relative + // (i.e., "node_modules/.vite-temp/**", NOT "packages/hello/node_modules/.vite-temp/**") + assert!( + result.input_config.negative_globs.contains(&Str::from("node_modules/.vite-temp/**")), + "glob should be workspace-root-relative, got: {:?}", + result.input_config.negative_globs, + ); + } +}