@@ -18,7 +18,10 @@ use vite_shell::try_parse_as_and_list;
1818use vite_str:: Str ;
1919use vite_task_graph:: {
2020 TaskNodeIndex ,
21- config:: { ResolvedTaskOptions , user:: UserTaskOptions } ,
21+ config:: {
22+ CacheConfig , ResolvedTaskOptions ,
23+ user:: { UserCacheConfig , UserTaskOptions } ,
24+ } ,
2225} ;
2326
2427use 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" ) ]
302383pub 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,
0 commit comments