Skip to content
Merged
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
15 changes: 9 additions & 6 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,19 @@ impl ResolvedTaskOptions {
let cache_config = match user_options.cache_config {
UserCacheConfig::Disabled { cache: MustBe!(false) } => None,
UserCacheConfig::Enabled { cache: _, enabled_cache_config } => {
let mut pass_through_envs =
enabled_cache_config.pass_through_envs.unwrap_or_default();
let mut pass_through_envs: FxHashSet<Str> = enabled_cache_config
.pass_through_envs
.unwrap_or_default()
.into_iter()
.collect();
pass_through_envs.extend(DEFAULT_PASSTHROUGH_ENVS.iter().copied().map(Str::from));
Some(CacheConfig {
env_config: EnvConfig {
fingerprinted_envs: enabled_cache_config
.envs
.map(|e| e.into_vec().into_iter().collect())
.unwrap_or_default(),
pass_through_envs: pass_through_envs.into(),
pass_through_envs,
},
})
}
Expand All @@ -69,17 +72,17 @@ impl ResolvedTaskOptions {
}
}

#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize)]
pub struct CacheConfig {
pub env_config: EnvConfig,
}

#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize)]
pub struct EnvConfig {
/// environment variable names to be fingerprinted and passed to the task, with defaults populated
pub fingerprinted_envs: FxHashSet<Str>,
/// environment variable names to be passed to the task without fingerprinting, with defaults populated
pub pass_through_envs: Arc<[Str]>,
pub pass_through_envs: FxHashSet<Str>,
}

#[derive(Debug, thiserror::Error)]
Expand Down
12 changes: 7 additions & 5 deletions crates/vite_task_plan/src/envs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,12 @@ impl EnvFingerprints {

Ok(Self {
fingerprinted_envs,
// Save pass_through_envs names as-is, so any changes to it will invalidate the cache
pass_through_env_config: Arc::clone(&env_config.pass_through_envs),
// Save pass_through_envs names sorted for deterministic cache fingerprinting
pass_through_env_config: {
let mut sorted: Vec<Str> = env_config.pass_through_envs.iter().cloned().collect();
sorted.sort();
sorted.into()
},
})
}
}
Expand Down Expand Up @@ -194,9 +198,7 @@ mod tests {
fn create_env_config(fingerprinted: &[&str], pass_through: &[&str]) -> EnvConfig {
EnvConfig {
fingerprinted_envs: fingerprinted.iter().map(|s| Str::from(*s)).collect(),
pass_through_envs: Arc::from(
pass_through.iter().map(|s| Str::from(*s)).collect::<Vec<_>>(),
),
pass_through_envs: pass_through.iter().map(|s| Str::from(*s)).collect(),
}
}

Expand Down
4 changes: 3 additions & 1 deletion crates/vite_task_plan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub use error::{Error, TaskPlanErrorKind};
use execution_graph::ExecutionGraph;
use in_process::InProcessExecution;
pub use path_env::{get_path_env, prepend_path_env};
use plan::{plan_query_request, plan_synthetic_request};
use plan::{ParentCacheConfig, plan_query_request, plan_synthetic_request};
use plan_request::{PlanRequest, SyntheticPlanRequest};
use rustc_hash::FxHashMap;
use serde::{Serialize, ser::SerializeMap as _};
Expand Down Expand Up @@ -230,6 +230,7 @@ impl ExecutionPlan {
synthetic_plan_request,
None,
cwd,
ParentCacheConfig::None,
)
.with_empty_call_stack()?;
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution))
Expand All @@ -256,6 +257,7 @@ impl ExecutionPlan {
synthetic_plan_request,
Some(execution_cache_key),
cwd,
ParentCacheConfig::None,
)
.with_empty_call_stack()?;
Ok(Self { root_node: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution)) })
Expand Down
97 changes: 87 additions & 10 deletions crates/vite_task_plan/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ use vite_shell::try_parse_as_and_list;
use vite_str::Str;
use vite_task_graph::{
TaskNodeIndex,
config::{ResolvedTaskOptions, user::UserTaskOptions},
config::{
CacheConfig, ResolvedTaskOptions,
user::{UserCacheConfig, UserTaskOptions},
},
};

use crate::{
Expand Down Expand Up @@ -199,12 +202,21 @@ async fn plan_task_as_execution_node(
}
// Synthetic task (from CommandHandler)
Some(PlanRequest::Synthetic(synthetic_plan_request)) => {
let parent_cache_config = task_node
.resolved_config
.resolved_options
.cache_config
.as_ref()
.map_or(ParentCacheConfig::Disabled, |config| {
ParentCacheConfig::Inherited(config.clone())
});
let spawn_execution = plan_synthetic_request(
context.workspace_path(),
&and_item.envs,
synthetic_plan_request,
Some(task_execution_cache_key),
&cwd,
parent_cache_config,
)
.with_plan_context(&context)?;
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution))
Expand Down Expand Up @@ -298,26 +310,91 @@ async fn plan_task_as_execution_node(
Ok(TaskExecution { task_display: task_node.task_display.clone(), items })
}

/// Cache configuration inherited from the parent task that contains a synthetic command.
///
/// When a synthetic task (e.g., `vp lint` expanding to `oxlint`) appears inside a
/// user-defined task's script, the parent task's cache configuration should constrain
/// the synthetic task's caching behavior.
pub enum ParentCacheConfig {
/// No parent task (top-level synthetic command like `vp lint` run directly).
/// The synthetic task uses its own default cache configuration.
None,

/// Parent task has caching disabled (`cache: false` or `cacheScripts` not enabled).
/// The synthetic task should also have caching disabled.
Disabled,

/// Parent task has caching enabled with this configuration.
/// The synthetic task inherits this config, merged with its own additions.
Inherited(CacheConfig),
}

/// Resolves the effective cache configuration for a synthetic task by combining
/// the parent task's cache config with the synthetic command's own additions.
///
/// Synthetic tasks (e.g., `vp lint` → `oxlint`) may declare their own cache-related
/// env requirements (e.g., `pass_through_envs` for env-test). When a parent task
/// exists, its cache config takes precedence:
/// - If the parent disables caching, the synthetic task is also uncached.
/// - If the parent enables caching but the synthetic disables it, caching is disabled.
/// - If both parent and synthetic enable caching, the synthetic inherits the parent's
/// env config and merges in any additional envs the synthetic command needs.
/// - If there is no parent (top-level invocation), the synthetic task's own
/// [`UserCacheConfig`] is resolved with defaults.
fn resolve_synthetic_cache_config(
parent: ParentCacheConfig,
synthetic_cache_config: UserCacheConfig,
cwd: &Arc<AbsolutePath>,
) -> Option<CacheConfig> {
match parent {
ParentCacheConfig::None => {
// Top-level: resolve from synthetic's own config
ResolvedTaskOptions::resolve(
UserTaskOptions {
cache_config: synthetic_cache_config,
cwd_relative_to_package: None,
depends_on: None,
},
cwd,
)
.cache_config
}
ParentCacheConfig::Disabled => Option::None,
ParentCacheConfig::Inherited(mut parent_config) => {
// Cache is enabled only if both parent and synthetic want it.
// Merge synthetic's additions into parent's config.
match synthetic_cache_config {
UserCacheConfig::Disabled { .. } => Option::None,
UserCacheConfig::Enabled { enabled_cache_config, .. } => {
if let Some(extra_envs) = enabled_cache_config.envs {
parent_config.env_config.fingerprinted_envs.extend(extra_envs.into_vec());
}
if let Some(extra_pts) = enabled_cache_config.pass_through_envs {
parent_config.env_config.pass_through_envs.extend(extra_pts);
}
Some(parent_config)
}
}
}
}
}

#[expect(clippy::result_large_err, reason = "TaskPlanErrorKind is large for diagnostics")]
pub fn plan_synthetic_request(
workspace_path: &Arc<AbsolutePath>,
prefix_envs: &BTreeMap<Str, Str>,
synthetic_plan_request: SyntheticPlanRequest,
execution_cache_key: Option<ExecutionCacheKey>,
cwd: &Arc<AbsolutePath>,
parent_cache_config: ParentCacheConfig,
) -> Result<SpawnExecution, TaskPlanErrorKind> {
let SyntheticPlanRequest { program, args, cache_config, envs } = synthetic_plan_request;

let program_path = which(&program, &envs, cwd).map_err(TaskPlanErrorKind::ProgramNotFound)?;
let resolved_options = ResolvedTaskOptions::resolve(
UserTaskOptions {
cache_config,
// cwd_relative_to_package and depends_on don't make sense for synthetic tasks.
cwd_relative_to_package: None,
depends_on: None,
},
cwd,
);
let resolved_cache_config =
resolve_synthetic_cache_config(parent_cache_config, cache_config, cwd);
let resolved_options =
ResolvedTaskOptions { cwd: Arc::clone(cwd), cache_config: resolved_cache_config };

plan_spawn_execution(
workspace_path,
Expand Down
13 changes: 13 additions & 0 deletions crates/vite_task_plan/src/plan_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ pub struct ScriptCommand {
pub cwd: Arc<AbsolutePath>,
}

impl ScriptCommand {
/// Convert this `ScriptCommand` to a `SyntheticPlanRequest` with the given `cache_config`.
#[must_use]
pub fn to_synthetic_plan_request(&self, cache_config: UserCacheConfig) -> SyntheticPlanRequest {
SyntheticPlanRequest {
program: Arc::from(OsStr::new(&self.program)),
args: self.args.clone(),
cache_config,
envs: self.envs.clone(),
}
}
}

#[derive(Debug)]
pub struct PlanOptions {
pub extra_args: Arc<[Str]>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@test/synthetic-cache-disabled",
"scripts": {
"lint": "vp lint",
"run-build-cache-false": "vp run build"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[[plan]]
name = "script without cacheScripts defaults to no cache"
args = ["run", "lint"]

[[plan]]
name = "task with cache false disables synthetic cache"
args = ["run", "lint-no-cache"]

[[plan]]
name = "task with cache true enables synthetic cache"
args = ["run", "lint-with-cache"]

[[plan]]
name = "task passThroughEnvs inherited by synthetic"
args = ["run", "lint-with-pass-through-envs"]

[[plan]]
name = "parent cache false does not affect expanded query tasks"
args = ["run", "run-build-no-cache"]

[[plan]]
name = "script cache false does not affect expanded synthetic cache"
args = ["run", "run-build-cache-false"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
source: crates/vite_task_plan/tests/plan_snapshots/main.rs
expression: "&plan_json"
input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-cache-disabled
---
{
"root_node": {
"Expanded": [
{
"key": [
"<workspace>/",
"run-build-no-cache"
],
"node": {
"task_display": {
"package_name": "@test/synthetic-cache-disabled",
"task_name": "run-build-no-cache",
"package_path": "<workspace>/"
},
"items": [
{
"execution_item_display": {
"task_display": {
"package_name": "@test/synthetic-cache-disabled",
"task_name": "run-build-no-cache",
"package_path": "<workspace>/"
},
"command": "vp run build",
"and_item_index": null,
"cwd": "<workspace>/"
},
"kind": {
"Expanded": [
{
"key": [
"<workspace>/",
"build"
],
"node": {
"task_display": {
"package_name": "@test/synthetic-cache-disabled",
"task_name": "build",
"package_path": "<workspace>/"
},
"items": [
{
"execution_item_display": {
"task_display": {
"package_name": "@test/synthetic-cache-disabled",
"task_name": "build",
"package_path": "<workspace>/"
},
"command": "vp lint",
"and_item_index": null,
"cwd": "<workspace>/"
},
"kind": {
"Leaf": {
"Spawn": {
"cache_metadata": {
"spawn_fingerprint": {
"cwd": "",
"program_fingerprint": {
"OutsideWorkspace": {
"program_name": "oxlint"
}
},
"args": [],
"env_fingerprints": {
"fingerprinted_envs": {},
"pass_through_env_config": [
"<default pass-through envs>"
]
},
"fingerprint_ignores": null
},
"execution_cache_key": {
"UserTask": {
"task_name": "build",
"and_item_index": 0,
"extra_args": [],
"package_path": ""
}
}
},
"spawn_command": {
"program_path": "<tools>/node_modules/.bin/oxlint",
"args": [],
"all_envs": {
"NO_COLOR": "1",
"PATH": "<workspace>/node_modules/.bin:<tools>/node_modules/.bin"
},
"cwd": "<workspace>/"
}
}
}
}
}
]
},
"neighbors": []
}
]
}
}
]
},
"neighbors": []
}
]
}
}
Loading