Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()}`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "cache-negative-input-glob",
"private": true
}
Original file line number Diff line number Diff line change
@@ -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",
]
Original file line number Diff line number Diff line change
@@ -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, <duration> saved.
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const hello = 'world';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"cache": true,
"tasks": {
"build": {
"command": "node build.mjs",
"cache": true,
"input": [{ "auto": true }, "!node_modules/.vite-temp/**"]
}
}
}
229 changes: 228 additions & 1 deletion crates/vite_task_plan/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
})
Expand Down Expand Up @@ -749,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,
);
}
}
Loading