Skip to content

Commit 846885e

Browse files
authored
fix: synthetic tasks inherit parent task's cache config (#150)
# Synthetic tasks (e.g., `vp lint` → `oxlint`) now respect the parent task's cache configuration Synthetic tasks (e.g., `vp lint` → `oxlint`) now respect the parent task's cache configuration instead of always enabling their own cache. Cache is enabled only when both parent and synthetic want it, and parent settings like `passThroughEnvs` are properly propagated. - Add `ParentCacheConfig` enum (None/Disabled/Inherited) to represent parent cache state passed to synthetic task planning - Add `resolve_synthetic_cache_config` to merge parent and synthetic cache configs with clear precedence rules - Change `EnvConfig.pass_through_envs` from `Arc<[Str]>` to `FxHashSet<Str>` for natural deduplication during merge - Sort pass-through envs for deterministic cache fingerprinting - Add `synthetic-cache-disabled` test fixture with 6 cases covering disabled scripts, disabled tasks, enabled tasks, env inheritance, and expanded query task independence
1 parent 043c86e commit 846885e

16 files changed

+854
-22
lines changed

crates/vite_task_graph/src/config/mod.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,19 @@ impl ResolvedTaskOptions {
5151
let cache_config = match user_options.cache_config {
5252
UserCacheConfig::Disabled { cache: MustBe!(false) } => None,
5353
UserCacheConfig::Enabled { cache: _, enabled_cache_config } => {
54-
let mut pass_through_envs =
55-
enabled_cache_config.pass_through_envs.unwrap_or_default();
54+
let mut pass_through_envs: FxHashSet<Str> = enabled_cache_config
55+
.pass_through_envs
56+
.unwrap_or_default()
57+
.into_iter()
58+
.collect();
5659
pass_through_envs.extend(DEFAULT_PASSTHROUGH_ENVS.iter().copied().map(Str::from));
5760
Some(CacheConfig {
5861
env_config: EnvConfig {
5962
fingerprinted_envs: enabled_cache_config
6063
.envs
6164
.map(|e| e.into_vec().into_iter().collect())
6265
.unwrap_or_default(),
63-
pass_through_envs: pass_through_envs.into(),
66+
pass_through_envs,
6467
},
6568
})
6669
}
@@ -69,17 +72,17 @@ impl ResolvedTaskOptions {
6972
}
7073
}
7174

72-
#[derive(Debug, Serialize)]
75+
#[derive(Debug, Clone, Serialize)]
7376
pub struct CacheConfig {
7477
pub env_config: EnvConfig,
7578
}
7679

77-
#[derive(Debug, Serialize)]
80+
#[derive(Debug, Clone, Serialize)]
7881
pub struct EnvConfig {
7982
/// environment variable names to be fingerprinted and passed to the task, with defaults populated
8083
pub fingerprinted_envs: FxHashSet<Str>,
8184
/// environment variable names to be passed to the task without fingerprinting, with defaults populated
82-
pub pass_through_envs: Arc<[Str]>,
85+
pub pass_through_envs: FxHashSet<Str>,
8386
}
8487

8588
#[derive(Debug, thiserror::Error)]

crates/vite_task_plan/src/envs.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,12 @@ impl EnvFingerprints {
121121

122122
Ok(Self {
123123
fingerprinted_envs,
124-
// Save pass_through_envs names as-is, so any changes to it will invalidate the cache
125-
pass_through_env_config: Arc::clone(&env_config.pass_through_envs),
124+
// Save pass_through_envs names sorted for deterministic cache fingerprinting
125+
pass_through_env_config: {
126+
let mut sorted: Vec<Str> = env_config.pass_through_envs.iter().cloned().collect();
127+
sorted.sort();
128+
sorted.into()
129+
},
126130
})
127131
}
128132
}
@@ -194,9 +198,7 @@ mod tests {
194198
fn create_env_config(fingerprinted: &[&str], pass_through: &[&str]) -> EnvConfig {
195199
EnvConfig {
196200
fingerprinted_envs: fingerprinted.iter().map(|s| Str::from(*s)).collect(),
197-
pass_through_envs: Arc::from(
198-
pass_through.iter().map(|s| Str::from(*s)).collect::<Vec<_>>(),
199-
),
201+
pass_through_envs: pass_through.iter().map(|s| Str::from(*s)).collect(),
200202
}
201203
}
202204

crates/vite_task_plan/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub use error::{Error, TaskPlanErrorKind};
1616
use execution_graph::ExecutionGraph;
1717
use in_process::InProcessExecution;
1818
pub use path_env::{get_path_env, prepend_path_env};
19-
use plan::{plan_query_request, plan_synthetic_request};
19+
use plan::{ParentCacheConfig, plan_query_request, plan_synthetic_request};
2020
use plan_request::{PlanRequest, SyntheticPlanRequest};
2121
use rustc_hash::FxHashMap;
2222
use serde::{Serialize, ser::SerializeMap as _};
@@ -230,6 +230,7 @@ impl ExecutionPlan {
230230
synthetic_plan_request,
231231
None,
232232
cwd,
233+
ParentCacheConfig::None,
233234
)
234235
.with_empty_call_stack()?;
235236
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution))
@@ -256,6 +257,7 @@ impl ExecutionPlan {
256257
synthetic_plan_request,
257258
Some(execution_cache_key),
258259
cwd,
260+
ParentCacheConfig::None,
259261
)
260262
.with_empty_call_stack()?;
261263
Ok(Self { root_node: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution)) })

crates/vite_task_plan/src/plan.rs

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ use vite_shell::try_parse_as_and_list;
1818
use vite_str::Str;
1919
use vite_task_graph::{
2020
TaskNodeIndex,
21-
config::{ResolvedTaskOptions, user::UserTaskOptions},
21+
config::{
22+
CacheConfig, ResolvedTaskOptions,
23+
user::{UserCacheConfig, UserTaskOptions},
24+
},
2225
};
2326

2427
use crate::{
@@ -199,12 +202,21 @@ async fn plan_task_as_execution_node(
199202
}
200203
// Synthetic task (from CommandHandler)
201204
Some(PlanRequest::Synthetic(synthetic_plan_request)) => {
205+
let parent_cache_config = task_node
206+
.resolved_config
207+
.resolved_options
208+
.cache_config
209+
.as_ref()
210+
.map_or(ParentCacheConfig::Disabled, |config| {
211+
ParentCacheConfig::Inherited(config.clone())
212+
});
202213
let spawn_execution = plan_synthetic_request(
203214
context.workspace_path(),
204215
&and_item.envs,
205216
synthetic_plan_request,
206217
Some(task_execution_cache_key),
207218
&cwd,
219+
parent_cache_config,
208220
)
209221
.with_plan_context(&context)?;
210222
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution))
@@ -298,26 +310,91 @@ async fn plan_task_as_execution_node(
298310
Ok(TaskExecution { task_display: task_node.task_display.clone(), items })
299311
}
300312

313+
/// Cache configuration inherited from the parent task that contains a synthetic command.
314+
///
315+
/// When a synthetic task (e.g., `vp lint` expanding to `oxlint`) appears inside a
316+
/// user-defined task's script, the parent task's cache configuration should constrain
317+
/// the synthetic task's caching behavior.
318+
pub enum ParentCacheConfig {
319+
/// No parent task (top-level synthetic command like `vp lint` run directly).
320+
/// The synthetic task uses its own default cache configuration.
321+
None,
322+
323+
/// Parent task has caching disabled (`cache: false` or `cacheScripts` not enabled).
324+
/// The synthetic task should also have caching disabled.
325+
Disabled,
326+
327+
/// Parent task has caching enabled with this configuration.
328+
/// The synthetic task inherits this config, merged with its own additions.
329+
Inherited(CacheConfig),
330+
}
331+
332+
/// Resolves the effective cache configuration for a synthetic task by combining
333+
/// the parent task's cache config with the synthetic command's own additions.
334+
///
335+
/// Synthetic tasks (e.g., `vp lint` → `oxlint`) may declare their own cache-related
336+
/// env requirements (e.g., `pass_through_envs` for env-test). When a parent task
337+
/// exists, its cache config takes precedence:
338+
/// - If the parent disables caching, the synthetic task is also uncached.
339+
/// - If the parent enables caching but the synthetic disables it, caching is disabled.
340+
/// - If both parent and synthetic enable caching, the synthetic inherits the parent's
341+
/// env config and merges in any additional envs the synthetic command needs.
342+
/// - If there is no parent (top-level invocation), the synthetic task's own
343+
/// [`UserCacheConfig`] is resolved with defaults.
344+
fn resolve_synthetic_cache_config(
345+
parent: ParentCacheConfig,
346+
synthetic_cache_config: UserCacheConfig,
347+
cwd: &Arc<AbsolutePath>,
348+
) -> Option<CacheConfig> {
349+
match parent {
350+
ParentCacheConfig::None => {
351+
// Top-level: resolve from synthetic's own config
352+
ResolvedTaskOptions::resolve(
353+
UserTaskOptions {
354+
cache_config: synthetic_cache_config,
355+
cwd_relative_to_package: None,
356+
depends_on: None,
357+
},
358+
cwd,
359+
)
360+
.cache_config
361+
}
362+
ParentCacheConfig::Disabled => Option::None,
363+
ParentCacheConfig::Inherited(mut parent_config) => {
364+
// Cache is enabled only if both parent and synthetic want it.
365+
// Merge synthetic's additions into parent's config.
366+
match synthetic_cache_config {
367+
UserCacheConfig::Disabled { .. } => Option::None,
368+
UserCacheConfig::Enabled { enabled_cache_config, .. } => {
369+
if let Some(extra_envs) = enabled_cache_config.envs {
370+
parent_config.env_config.fingerprinted_envs.extend(extra_envs.into_vec());
371+
}
372+
if let Some(extra_pts) = enabled_cache_config.pass_through_envs {
373+
parent_config.env_config.pass_through_envs.extend(extra_pts);
374+
}
375+
Some(parent_config)
376+
}
377+
}
378+
}
379+
}
380+
}
381+
301382
#[expect(clippy::result_large_err, reason = "TaskPlanErrorKind is large for diagnostics")]
302383
pub fn plan_synthetic_request(
303384
workspace_path: &Arc<AbsolutePath>,
304385
prefix_envs: &BTreeMap<Str, Str>,
305386
synthetic_plan_request: SyntheticPlanRequest,
306387
execution_cache_key: Option<ExecutionCacheKey>,
307388
cwd: &Arc<AbsolutePath>,
389+
parent_cache_config: ParentCacheConfig,
308390
) -> Result<SpawnExecution, TaskPlanErrorKind> {
309391
let SyntheticPlanRequest { program, args, cache_config, envs } = synthetic_plan_request;
310392

311393
let program_path = which(&program, &envs, cwd).map_err(TaskPlanErrorKind::ProgramNotFound)?;
312-
let resolved_options = ResolvedTaskOptions::resolve(
313-
UserTaskOptions {
314-
cache_config,
315-
// cwd_relative_to_package and depends_on don't make sense for synthetic tasks.
316-
cwd_relative_to_package: None,
317-
depends_on: None,
318-
},
319-
cwd,
320-
);
394+
let resolved_cache_config =
395+
resolve_synthetic_cache_config(parent_cache_config, cache_config, cwd);
396+
let resolved_options =
397+
ResolvedTaskOptions { cwd: Arc::clone(cwd), cache_config: resolved_cache_config };
321398

322399
plan_spawn_execution(
323400
workspace_path,

crates/vite_task_plan/src/plan_request.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ pub struct ScriptCommand {
1818
pub cwd: Arc<AbsolutePath>,
1919
}
2020

21+
impl ScriptCommand {
22+
/// Convert this `ScriptCommand` to a `SyntheticPlanRequest` with the given `cache_config`.
23+
#[must_use]
24+
pub fn to_synthetic_plan_request(&self, cache_config: UserCacheConfig) -> SyntheticPlanRequest {
25+
SyntheticPlanRequest {
26+
program: Arc::from(OsStr::new(&self.program)),
27+
args: self.args.clone(),
28+
cache_config,
29+
envs: self.envs.clone(),
30+
}
31+
}
32+
}
33+
2134
#[derive(Debug)]
2235
pub struct PlanOptions {
2336
pub extra_args: Arc<[Str]>,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@test/synthetic-cache-disabled",
3+
"scripts": {
4+
"lint": "vp lint",
5+
"run-build-cache-false": "vp run build"
6+
}
7+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[[plan]]
2+
name = "script without cacheScripts defaults to no cache"
3+
args = ["run", "lint"]
4+
5+
[[plan]]
6+
name = "task with cache false disables synthetic cache"
7+
args = ["run", "lint-no-cache"]
8+
9+
[[plan]]
10+
name = "task with cache true enables synthetic cache"
11+
args = ["run", "lint-with-cache"]
12+
13+
[[plan]]
14+
name = "task passThroughEnvs inherited by synthetic"
15+
args = ["run", "lint-with-pass-through-envs"]
16+
17+
[[plan]]
18+
name = "parent cache false does not affect expanded query tasks"
19+
args = ["run", "run-build-no-cache"]
20+
21+
[[plan]]
22+
name = "script cache false does not affect expanded synthetic cache"
23+
args = ["run", "run-build-cache-false"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
source: crates/vite_task_plan/tests/plan_snapshots/main.rs
3+
expression: "&plan_json"
4+
input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-cache-disabled
5+
---
6+
{
7+
"root_node": {
8+
"Expanded": [
9+
{
10+
"key": [
11+
"<workspace>/",
12+
"run-build-no-cache"
13+
],
14+
"node": {
15+
"task_display": {
16+
"package_name": "@test/synthetic-cache-disabled",
17+
"task_name": "run-build-no-cache",
18+
"package_path": "<workspace>/"
19+
},
20+
"items": [
21+
{
22+
"execution_item_display": {
23+
"task_display": {
24+
"package_name": "@test/synthetic-cache-disabled",
25+
"task_name": "run-build-no-cache",
26+
"package_path": "<workspace>/"
27+
},
28+
"command": "vp run build",
29+
"and_item_index": null,
30+
"cwd": "<workspace>/"
31+
},
32+
"kind": {
33+
"Expanded": [
34+
{
35+
"key": [
36+
"<workspace>/",
37+
"build"
38+
],
39+
"node": {
40+
"task_display": {
41+
"package_name": "@test/synthetic-cache-disabled",
42+
"task_name": "build",
43+
"package_path": "<workspace>/"
44+
},
45+
"items": [
46+
{
47+
"execution_item_display": {
48+
"task_display": {
49+
"package_name": "@test/synthetic-cache-disabled",
50+
"task_name": "build",
51+
"package_path": "<workspace>/"
52+
},
53+
"command": "vp lint",
54+
"and_item_index": null,
55+
"cwd": "<workspace>/"
56+
},
57+
"kind": {
58+
"Leaf": {
59+
"Spawn": {
60+
"cache_metadata": {
61+
"spawn_fingerprint": {
62+
"cwd": "",
63+
"program_fingerprint": {
64+
"OutsideWorkspace": {
65+
"program_name": "oxlint"
66+
}
67+
},
68+
"args": [],
69+
"env_fingerprints": {
70+
"fingerprinted_envs": {},
71+
"pass_through_env_config": [
72+
"<default pass-through envs>"
73+
]
74+
},
75+
"fingerprint_ignores": null
76+
},
77+
"execution_cache_key": {
78+
"UserTask": {
79+
"task_name": "build",
80+
"and_item_index": 0,
81+
"extra_args": [],
82+
"package_path": ""
83+
}
84+
}
85+
},
86+
"spawn_command": {
87+
"program_path": "<tools>/node_modules/.bin/oxlint",
88+
"args": [],
89+
"all_envs": {
90+
"NO_COLOR": "1",
91+
"PATH": "<workspace>/node_modules/.bin:<tools>/node_modules/.bin"
92+
},
93+
"cwd": "<workspace>/"
94+
}
95+
}
96+
}
97+
}
98+
}
99+
]
100+
},
101+
"neighbors": []
102+
}
103+
]
104+
}
105+
}
106+
]
107+
},
108+
"neighbors": []
109+
}
110+
]
111+
}
112+
}

0 commit comments

Comments
 (0)