From f545d01c6df3811d46fb4c360ec4a0c4fa3bce11 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 09:20:10 +0000 Subject: [PATCH 1/3] fix: make TS definition collector visit recursive dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DeclCollector in generate_ts_definition only called visit_dependencies on UserRunConfig, collecting direct dependencies but missing indirect ones. This meant any type referenced through a chain of dependencies (e.g. a type used by EnabledCacheConfig, which is flattened into UserTaskConfig) would be referenced in the generated TypeScript but never declared. Fix the collector to recursively visit each type's dependencies, using a HashSet to prevent infinite loops from circular references. Also add CacheOutputs as an indirect type (referenced through the flatten chain) that confirms the fix works — it's now properly declared in the generated run-config.ts. https://claude.ai/code/session_01VQvSgXNnYL8tNWydvtbCvg --- crates/vite_task_bin/src/lib.rs | 7 +++- crates/vite_task_bin/src/main.rs | 1 + crates/vite_task_graph/run-config.ts | 15 +++++++++ crates/vite_task_graph/src/config/mod.rs | 6 +++- crates/vite_task_graph/src/config/user.rs | 41 ++++++++++++++++++++--- 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index c2560653..a0530d18 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -64,6 +64,7 @@ fn synthesize_node_modules_bin_task( cache_config: UserCacheConfig::with_config(EnabledCacheConfig { envs: None, pass_through_envs: None, + outputs: None, }), envs: Arc::clone(envs), }) @@ -125,7 +126,11 @@ impl vite_task::CommandHandler for CommandHandler { program: find_executable(get_path_env(&envs), &command.cwd, "print-env")?, args: [name.clone()].into(), cache_config: UserCacheConfig::with_config({ - EnabledCacheConfig { envs: None, pass_through_envs: Some(vec![name]) } + EnabledCacheConfig { + envs: None, + pass_through_envs: Some(vec![name]), + outputs: None, + } }), envs: Arc::new(envs), })) diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index b4a1320b..766bcaef 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -35,6 +35,7 @@ async fn run() -> anyhow::Result { EnabledCacheConfig { envs: Some(Box::from([Str::from("FOO")])), pass_through_envs: None, + outputs: None, } }), envs: Arc::clone(envs), diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 4b0c4c48..533b3213 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -1,3 +1,14 @@ +export type CacheOutputs = { + /** + * Directories to cache (relative to the package root). + */ + include?: Array; + /** + * Directories to exclude from the cache. + */ + exclude?: Array; +}; + export type Task = { /** * The command to run for the task. @@ -27,6 +38,10 @@ export type Task = { * Environment variable names to be passed to the task without fingerprinting. */ passThroughEnvs?: Array; + /** + * Output artifacts to cache. + */ + outputs?: CacheOutputs; } | { /** diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index 6512c3f2..d8a83d2d 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -114,7 +114,11 @@ impl ResolvedTaskConfig { let cache_config = if cache_scripts { UserCacheConfig::Enabled { cache: None, - enabled_cache_config: EnabledCacheConfig { envs: None, pass_through_envs: None }, + enabled_cache_config: EnabledCacheConfig { + envs: None, + pass_through_envs: None, + outputs: None, + }, } } else { UserCacheConfig::Disabled { cache: MustBe!(false) } diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index d8d784d6..0708d555 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -47,6 +47,19 @@ impl UserCacheConfig { } } +/// Configuration for cache output artifacts +#[derive(Debug, Deserialize, PartialEq, Eq)] +// TS derive macro generates code using std types that clippy disallows; skip derive during linting +#[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields, rename = "CacheOutputs"))] +#[serde(rename_all = "camelCase")] +pub struct CacheOutputs { + /// Directories to cache (relative to the package root). + pub include: Option>, + + /// Directories to exclude from the cache. + pub exclude: Option>, +} + /// Cache configuration fields when caching is enabled #[derive(Debug, Deserialize, PartialEq, Eq)] // TS derive macro generates code using std types that clippy disallows; skip derive during linting @@ -58,6 +71,9 @@ pub struct EnabledCacheConfig { /// Environment variable names to be passed to the task without fingerprinting. pub pass_through_envs: Option>, + + /// Output artifacts to cache. + pub outputs: Option, } /// Options for user-defined tasks in `vite.config.*`, excluding the command. @@ -89,7 +105,11 @@ impl Default for UserTaskOptions { // Caching enabled with no fingerprinted envs cache_config: UserCacheConfig::Enabled { cache: None, - enabled_cache_config: EnabledCacheConfig { envs: None, pass_through_envs: None }, + enabled_cache_config: EnabledCacheConfig { + envs: None, + pass_through_envs: None, + outputs: None, + }, }, } } @@ -204,25 +224,35 @@ impl UserRunConfig { #[expect(clippy::disallowed_types, reason = "test code uses std types for convenience")] pub fn generate_ts_definition() -> String { use std::{ + any::TypeId, + collections::HashSet, io::Write, process::{Command, Stdio}, }; use ts_rs::TypeVisitor; - struct DeclCollector(Vec); + struct DeclCollector { + decls: Vec, + visited: HashSet, + } impl TypeVisitor for DeclCollector { fn visit(&mut self) { + if !self.visited.insert(TypeId::of::()) { + return; + } // Only collect declarations from types that are exportable // (i.e., have an output path - built-in types like HashMap don't) if T::output_path().is_some() { - self.0.push(T::decl()); + self.decls.push(T::decl()); } + // Recursively visit dependencies of T + T::visit_dependencies(self); } } - let mut collector = DeclCollector(Vec::new()); + let mut collector = DeclCollector { decls: Vec::new(), visited: HashSet::new() }; Self::visit_dependencies(&mut collector); // Sort declarations for deterministic output order @@ -230,7 +260,7 @@ impl UserRunConfig { // Export all types let mut types: String = collector - .0 + .decls .iter() .map(|decl| vite_str::format!("export {decl}")) .collect::>() @@ -358,6 +388,7 @@ mod tests { enabled_cache_config: EnabledCacheConfig { envs: Some(std::iter::once("NODE_ENV".into()).collect()), pass_through_envs: Some(std::iter::once("FOO".into()).collect()), + outputs: None, } }, ); From b0f2f4b50909b172d3dc9ea6c5ebef57cf810e03 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 09:30:29 +0000 Subject: [PATCH 2/3] revert: remove CacheOutputs test type, keep only DeclCollector fix Remove the CacheOutputs indirect type that was added to demonstrate the bug. The recursive DeclCollector fix is retained. https://claude.ai/code/session_01VQvSgXNnYL8tNWydvtbCvg --- crates/vite_task_bin/src/lib.rs | 7 +------ crates/vite_task_bin/src/main.rs | 1 - crates/vite_task_graph/run-config.ts | 15 --------------- crates/vite_task_graph/src/config/mod.rs | 6 +----- crates/vite_task_graph/src/config/user.rs | 23 +---------------------- 5 files changed, 3 insertions(+), 49 deletions(-) diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index a0530d18..c2560653 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -64,7 +64,6 @@ fn synthesize_node_modules_bin_task( cache_config: UserCacheConfig::with_config(EnabledCacheConfig { envs: None, pass_through_envs: None, - outputs: None, }), envs: Arc::clone(envs), }) @@ -126,11 +125,7 @@ impl vite_task::CommandHandler for CommandHandler { program: find_executable(get_path_env(&envs), &command.cwd, "print-env")?, args: [name.clone()].into(), cache_config: UserCacheConfig::with_config({ - EnabledCacheConfig { - envs: None, - pass_through_envs: Some(vec![name]), - outputs: None, - } + EnabledCacheConfig { envs: None, pass_through_envs: Some(vec![name]) } }), envs: Arc::new(envs), })) diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index 766bcaef..b4a1320b 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -35,7 +35,6 @@ async fn run() -> anyhow::Result { EnabledCacheConfig { envs: Some(Box::from([Str::from("FOO")])), pass_through_envs: None, - outputs: None, } }), envs: Arc::clone(envs), diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 533b3213..4b0c4c48 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -1,14 +1,3 @@ -export type CacheOutputs = { - /** - * Directories to cache (relative to the package root). - */ - include?: Array; - /** - * Directories to exclude from the cache. - */ - exclude?: Array; -}; - export type Task = { /** * The command to run for the task. @@ -38,10 +27,6 @@ export type Task = { * Environment variable names to be passed to the task without fingerprinting. */ passThroughEnvs?: Array; - /** - * Output artifacts to cache. - */ - outputs?: CacheOutputs; } | { /** diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index d8a83d2d..6512c3f2 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -114,11 +114,7 @@ impl ResolvedTaskConfig { let cache_config = if cache_scripts { UserCacheConfig::Enabled { cache: None, - enabled_cache_config: EnabledCacheConfig { - envs: None, - pass_through_envs: None, - outputs: None, - }, + enabled_cache_config: EnabledCacheConfig { envs: None, pass_through_envs: None }, } } else { UserCacheConfig::Disabled { cache: MustBe!(false) } diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index 0708d555..beabb43f 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -47,19 +47,6 @@ impl UserCacheConfig { } } -/// Configuration for cache output artifacts -#[derive(Debug, Deserialize, PartialEq, Eq)] -// TS derive macro generates code using std types that clippy disallows; skip derive during linting -#[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields, rename = "CacheOutputs"))] -#[serde(rename_all = "camelCase")] -pub struct CacheOutputs { - /// Directories to cache (relative to the package root). - pub include: Option>, - - /// Directories to exclude from the cache. - pub exclude: Option>, -} - /// Cache configuration fields when caching is enabled #[derive(Debug, Deserialize, PartialEq, Eq)] // TS derive macro generates code using std types that clippy disallows; skip derive during linting @@ -71,9 +58,6 @@ pub struct EnabledCacheConfig { /// Environment variable names to be passed to the task without fingerprinting. pub pass_through_envs: Option>, - - /// Output artifacts to cache. - pub outputs: Option, } /// Options for user-defined tasks in `vite.config.*`, excluding the command. @@ -105,11 +89,7 @@ impl Default for UserTaskOptions { // Caching enabled with no fingerprinted envs cache_config: UserCacheConfig::Enabled { cache: None, - enabled_cache_config: EnabledCacheConfig { - envs: None, - pass_through_envs: None, - outputs: None, - }, + enabled_cache_config: EnabledCacheConfig { envs: None, pass_through_envs: None }, }, } } @@ -388,7 +368,6 @@ mod tests { enabled_cache_config: EnabledCacheConfig { envs: Some(std::iter::once("NODE_ENV".into()).collect()), pass_through_envs: Some(std::iter::once("FOO".into()).collect()), - outputs: None, } }, ); From d9b1bd9f373c0f5eb8ea282606548bfb65a74482 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Mar 2026 16:00:41 +0000 Subject: [PATCH 3/3] feat: add auto-generated header to run-config.ts https://claude.ai/code/session_01VQvSgXNnYL8tNWydvtbCvg --- crates/vite_task_graph/run-config.ts | 2 ++ crates/vite_task_graph/src/config/user.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 4b0c4c48..0ab58364 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by `cargo test`. Do not edit manually. + export type Task = { /** * The command to run for the task. diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index beabb43f..60feb652 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -236,15 +236,20 @@ impl UserRunConfig { Self::visit_dependencies(&mut collector); // Sort declarations for deterministic output order - collector.0.sort(); + collector.decls.sort(); + + // Header + let mut types: String = + "// This file is auto-generated by `cargo test`. Do not edit manually.\n\n".into(); // Export all types - let mut types: String = collector + let dep_types: String = collector .decls .iter() .map(|decl| vite_str::format!("export {decl}")) .collect::>() .join("\n\n"); + types.push_str(&dep_types); // Export the main type types.push_str("\n\nexport ");