diff --git a/Cargo.lock b/Cargo.lock index 3513f60c..f1fc6520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -473,6 +473,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "const_format" version = "0.2.34" @@ -509,6 +521,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -819,6 +840,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "env_filter" version = "0.1.3" @@ -1027,7 +1054,7 @@ dependencies = [ "fspy_detours_sys", "fspy_shared", "ntapi", - "smallvec 2.0.0-alpha.11", + "smallvec 2.0.0-alpha.12", "tempfile", "widestring", "winapi", @@ -1239,6 +1266,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1311,6 +1351,20 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "insta" +version = "1.44.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" +dependencies = [ + "console", + "globset", + "once_cell", + "serde", + "similar", + "walkdir", +] + [[package]] name = "instability" version = "0.3.9" @@ -1560,6 +1614,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "monostate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4cc965c89dd0615a9e822ff8002f7633d2466143d51bd58693e4b2c75aabad" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f5b99488110875b5904839d396c2cdfaf241ff6622638acb879cc7effad5de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "nix" version = "0.23.2" @@ -2456,6 +2532,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "siphasher" version = "1.0.1" @@ -2476,9 +2558,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smallvec" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b96efa4bd6bdd2ff0c6615cc36fc4970cbae63cfd46ddff5cee35a1b4df570" +checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857" [[package]] name = "socket2" @@ -2988,6 +3070,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec1" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322" +dependencies = [ + "smallvec 1.15.1", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3079,6 +3170,26 @@ dependencies = [ "wax", ] +[[package]] +name = "vite_task_graph" +version = "0.1.0" +dependencies = [ + "anyhow", + "copy_dir", + "insta", + "monostate", + "petgraph", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tokio", + "vec1", + "vite_path", + "vite_str", + "vite_workspace", +] + [[package]] name = "vite_tui" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4f4e55c5..cc1061af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ color-eyre = "0.6.5" compact_str = "0.9.0" const_format = "0.2.34" constcat = "0.6.1" +copy_dir = "0.1.3" crossterm = { version = "0.29.0", features = ["event-stream"] } csv-async = { version = "1.3.1", features = ["tokio"] } ctor = "0.6" @@ -70,6 +71,7 @@ fspy_test_utils = { path = "crates/fspy_test_utils" } futures = "0.3.31" futures-core = "0.3.31" futures-util = "0.3.31" +insta = "1.44.3" itertools = "0.14.0" libc = "0.2.172" memmap2 = "0.9.7" @@ -95,7 +97,7 @@ serde_yml = "0.0.12" sha2 = "0.10.9" shared_memory = "0.12.4" shell-escape = "0.1.5" -smallvec = { version = "2.0.0-alpha.11", features = ["std"] } +smallvec = { version = "2.0.0-alpha.12", features = ["std"] } stackalloc = "1.2.1" supports-color = "3.0.1" syscalls = { version = "0.6.18", default-features = false } @@ -113,6 +115,7 @@ tree-sitter-bash = "0.23.1" tui-term = "0.2.0" twox-hash = "2.1.1" uuid = "1.18.1" +vec1 = "1.12.1" vite_glob = { path = "crates/vite_glob" } vite_path = { path = "crates/vite_path" } vite_str = { path = "crates/vite_str" } diff --git a/crates/vite_path/src/absolute.rs b/crates/vite_path/src/absolute.rs index 77610108..fc85af8e 100644 --- a/crates/vite_path/src/absolute.rs +++ b/crates/vite_path/src/absolute.rs @@ -1,6 +1,7 @@ use std::{ ffi::OsStr, fmt::Display, + hash::Hash, ops::Deref, path::{Path, PathBuf}, sync::Arc, @@ -11,7 +12,7 @@ use ref_cast::{RefCastCustom, ref_cast_custom}; use crate::relative::{FromPathError, InvalidPathDataError, RelativePathBuf}; /// A path that is guaranteed to be absolute -#[derive(RefCastCustom, Debug, PartialEq, Eq)] +#[derive(RefCastCustom, Debug, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct AbsolutePath(Path); impl AsRef for AbsolutePath { @@ -31,6 +32,12 @@ impl PartialEq for &AbsolutePath { } } +impl Hash for AbsolutePath { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + impl AbsolutePath { /// Creates a [`AbsolutePath`] if the give path is absolute. pub fn new + ?Sized>(path: &P) -> Option<&Self> { diff --git a/crates/vite_task/src/config/mod.rs b/crates/vite_task/src/config/mod.rs index 009b5fbd..e7b16edf 100644 --- a/crates/vite_task/src/config/mod.rs +++ b/crates/vite_task/src/config/mod.rs @@ -282,1103 +282,3 @@ pub struct CommandFingerprint { /// Changes to this config invalidate the cache to ensure correct fingerprint tracking. pub fingerprint_ignores: Option>, } - -#[cfg(test)] -mod tests { - use petgraph::stable_graph::StableDiGraph; - - use super::*; - use crate::{ - Error, - test_utils::{get_fixture_path, with_unique_cache_path}, - }; - - #[test] - fn test_recursive_topological_build() { - with_unique_cache_path("recursive_topological_build", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test recursive topological build - let task_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve tasks"); - - // Verify that all build tasks are included - let task_names: Vec<_> = - task_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!(task_names.contains(&"@test/core#build".into())); - assert!(task_names.contains(&"@test/utils#build".into())); - assert!(task_names.contains(&"@test/app#build".into())); - assert!(task_names.contains(&"@test/web#build".into())); - - // Verify dependencies exist in the correct direction - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // With topological mode, edges go from dependencies to dependents - assert!( - has_edge("@test/utils#build(subcommand 0)", "@test/core#build"), - "Core should have edge to Utils (Utils depends on Core)" - ); - assert!( - has_edge("@test/app#build", "@test/utils#build"), - "Utils should have edge to App (App depends on Utils)" - ); - assert!( - has_edge("@test/web#build", "@test/app#build"), - "App should have edge to Web (Web depends on App)" - ); - assert!( - has_edge("@test/web#build", "@test/core#build"), - "Core should have edge to Web (Web depends on Core)" - ); - - // TODO: fix indirect dependencies - // assert!( - // !has_edge("@test/web#build", "@test/utils#build"), - // "Web should have edge to utils (It should be indirect via App)" - // ); - }); - } - - #[test] - fn test_topological_run_false_no_implicit_deps() { - with_unique_cache_path("topological_run_false", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - // Load with topological_run = false - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), false) - .expect("Failed to load workspace"); - - let task_graph = workspace - .build_task_subgraph(&["@test/web#build".into()], Arc::default(), false) - .expect("Failed to resolve tasks"); - - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // When topological_run is false, @test/web#build should NOT depend on @test/core#build - // even though @test/web depends on @test/core as a package dependency - assert!( - !has_edge("@test/core#build", "@test/web#build"), - "With topological_run=false, Core#build should NOT have edge to Web#build" - ); - }); - } - - #[test] - fn test_explicit_deps_with_topological_false() { - with_unique_cache_path("explicit_deps_topological_false", |cache_path| { - let fixture_path = get_fixture_path("fixtures/explicit-deps-workspace"); - - // Load with topological_run = false - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), false) - .expect("Failed to load workspace"); - - // Test @test/utils#lint which has explicit dependencies - let task_graph = workspace - .build_task_subgraph(&["@test/utils#lint".into()], Arc::default(), false) - .expect("Failed to resolve tasks"); - - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // Verify explicit dependencies are honored - assert!( - has_edge("@test/utils#lint", "@test/core#build"), - "Explicit dependency from utils#lint to core#build should exist" - ); - assert!( - has_edge("@test/utils#lint", "@test/utils#build"), - "Explicit dependency from utils#build to utils#lint should exist" - ); - - // Verify NO implicit dependencies from package dependencies - // Even though @test/utils depends on @test/core, utils#build should NOT depend on core#build - assert!( - !has_edge("@test/core#build", "@test/utils#build"), - "With topological_run=false, no implicit dependency should exist" - ); - }); - } - - #[test] - fn test_explicit_deps_with_topological_true() { - with_unique_cache_path("explicit_deps_topological_true", |cache_path| { - let fixture_path = get_fixture_path("fixtures/explicit-deps-workspace"); - - // Load with topological_run = true - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test @test/utils#lint which has explicit dependencies - let task_graph = workspace - .build_task_subgraph(&["@test/utils#lint".into()], Arc::default(), false) - .expect("Failed to resolve tasks"); - - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // Verify explicit dependencies are still honored - assert!( - has_edge("@test/utils#lint", "@test/core#build"), - "Explicit dependency from core#build to utils#lint should exist" - ); - assert!( - has_edge("@test/utils#lint", "@test/utils#build"), - "Explicit dependency from utils#build to utils#lint should exist" - ); - - // Verify implicit dependencies ARE added - assert!( - has_edge("@test/utils#build", "@test/core#build"), - "With topological_run=true, implicit dependency should exist" - ); - }); - } - - #[test] - fn test_recursive_with_topological_false() { - with_unique_cache_path("recursive_topological_false", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - // Load with topological_run = false - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), false) - .expect("Failed to load workspace"); - - // Test recursive build with topological_run=false - let task_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve tasks"); - - // Verify that all build tasks are included (recursive flag works) - let task_names: Vec<_> = - task_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!(task_names.contains(&"@test/core#build".into())); - assert!(task_names.contains(&"@test/utils#build".into())); - assert!(task_names.contains(&"@test/app#build".into())); - assert!(task_names.contains(&"@test/web#build".into())); - - // But verify NO implicit dependencies exist - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // With topological_run=false, these implicit dependencies should NOT exist - assert!( - !has_edge("@test/core#build", "@test/utils#build"), - "No implicit edge from core to utils" - ); - assert!( - !has_edge("@test/utils#build", "@test/app#build"), - "No implicit edge from utils to app" - ); - assert!( - !has_edge("@test/app#build", "@test/web#build"), - "No implicit edge from app to web" - ); - }); - } - - #[test] - fn test_topological_true_vs_false_comparison() { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - // Use separate cache paths to avoid database locking - with_unique_cache_path("topological_comparison_true", |cache_path_true| { - // Load with topological_run = true - let workspace_true = - Workspace::load_with_cache_path(fixture_path.clone(), Some(cache_path_true), true) - .expect("Failed to load workspace with topological=true"); - - let graph_true = workspace_true - .build_task_subgraph(&["@test/app#build".into()], Arc::default(), false) - .expect("Failed to resolve tasks"); - - with_unique_cache_path("topological_comparison_false", |cache_path_false| { - // Load with topological_run = false - let workspace_false = - Workspace::load_with_cache_path(fixture_path, Some(cache_path_false), false) - .expect("Failed to load workspace with topological=false"); - - let graph_false = workspace_false - .build_task_subgraph(&["@test/app#build".into()], Arc::default(), false) - .expect("Failed to resolve tasks"); - - // Count edges in each graph - let edge_count_true = graph_true.edge_count(); - let edge_count_false = graph_false.edge_count(); - - // With topological=true, there should be more edges due to implicit dependencies - assert!( - edge_count_true > edge_count_false, - "Graph with topological=true ({edge_count_true}) should have more edges than topological=false ({edge_count_false})" - ); - - // Verify specific edge differences - let has_edge = - |graph: &StableDiGraph, from: &str, to: &str| -> bool { - graph.edge_indices().any(|edge_idx| { - let (source, target) = graph.edge_endpoints(edge_idx).unwrap(); - graph[source].display_name() == from - && graph[target].display_name() == to - }) - }; - - // This edge should exist with topological=true but not with topological=false - assert!( - has_edge(&graph_true, "@test/app#build", "@test/utils#build"), - "Implicit edge should exist with topological=true" - ); - assert!( - !has_edge(&graph_false, "@test/app#build", "@test/utils#build"), - "Implicit edge should NOT exist with topological=false" - ); - }); - }); - } - - #[test] - fn test_recursive_without_topological() { - with_unique_cache_path("recursive_without_topological", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test recursive build without topological flag - // Note: Even without topological flag, cross-package dependencies are now always included - let task_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve tasks"); - - // Verify that all build tasks are included - let task_names: Vec<_> = - task_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!(task_names.contains(&"@test/core#build".into())); - assert!(task_names.contains(&"@test/utils#build".into())); - assert!(task_names.contains(&"@test/app#build".into())); - assert!(task_names.contains(&"@test/web#build".into())); - - // Cross-package dependencies should exist even without topological flag - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // Verify some cross-package dependencies exist - assert!( - has_edge("@test/utils#build(subcommand 0)", "@test/core#build"), - "utils should have edge to core" - ); - }); - } - - #[test] - fn test_recursive_run_with_scope_error() { - with_unique_cache_path("recursive_run_with_scope_error", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test that specifying a scoped task with recursive flag returns an error - let result = - workspace.build_task_subgraph(&["@test/core#build".into()], Arc::default(), true); - - assert!(result.is_err()); - match result { - Err(Error::RecursiveRunWithScope(task)) => { - assert_eq!(task, "@test/core#build"); - } - _ => panic!("Expected RecursiveRunWithScope error"), - } - }); - } - - #[test] - fn test_non_recursive_single_package() { - with_unique_cache_path("non_recursive_single_package", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test non-recursive build of a single package - let task_graph = workspace - .build_task_subgraph(&["@test/utils#build".into()], Arc::default(), false) - .expect("Failed to resolve tasks"); - - // @test/utils has compound commands (3 subtasks) plus dependencies on @test/core#build - let all_tasks: Vec<_> = - task_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Should include utils subtasks - assert!(all_tasks.contains(&"@test/utils#build(subcommand 0)".into())); - assert!(all_tasks.contains(&"@test/utils#build(subcommand 1)".into())); - assert!(all_tasks.contains(&"@test/utils#build".into())); - - // Should also include dependency on core - assert!(all_tasks.contains(&"@test/core#build".into())); - }); - } - - #[test] - fn test_recursive_topological_with_compound_commands() { - with_unique_cache_path("recursive_topological_with_compound_commands", |cache_path| { - let fixture_path = get_fixture_path("fixtures/recursive-topological-workspace"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test recursive topological build with compound commands - let task_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve tasks"); - - // Check all tasks including subcommands - let all_tasks: Vec<_> = - task_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Utils should have 3 subtasks (indices 0, 1, and None) - assert!(all_tasks.contains(&"@test/utils#build(subcommand 0)".into())); - assert!(all_tasks.contains(&"@test/utils#build(subcommand 1)".into())); - assert!(all_tasks.contains(&"@test/utils#build".into())); - - // Verify dependencies - let has_edge = |from_name: &str, to_name: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from_name - && task_graph[target].display_name() == to_name - }) - }; - - // Within-package dependencies for @test/utils compound command - assert!( - has_edge("@test/utils#build(subcommand 1)", "@test/utils#build(subcommand 0)"), - "Second subtask should have edge to first" - ); - assert!( - has_edge("@test/utils#build", "@test/utils#build(subcommand 1)"), - "Last subtask should have edge to second" - ); - - // Cross-package dependencies - // Core's LAST subtask should have edge to utils' FIRST subtask - assert!( - has_edge("@test/utils#build(subcommand 0)", "@test/core#build"), - "Utils' first subtask should have edge to core's last subtask" - ); - - // Utils' LAST subtask should have edge to app - assert!( - has_edge("@test/app#build", "@test/utils#build"), - "app should have edge to Utils' last subtask" - ); - }); - } - - #[test] - fn test_transitive_dependency_resolution() { - with_unique_cache_path("transitive_dependency_resolution", |cache_path| { - let fixture_path = get_fixture_path("fixtures/transitive-dependency-workspace"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test recursive topological build with transitive dependencies - let task_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve tasks"); - - // Verify that all build tasks are included - let task_names: Vec<_> = - task_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!( - task_names.contains(&"@test/a#build".into()), - "Package A build task should be included" - ); - assert!( - task_names.contains(&"@test/c#build".into()), - "Package C build task should be included" - ); - assert_eq!(task_names.len(), 2, "Only A and C should have build tasks"); - - // Verify dependencies exist in the correct direction - let has_edge = |from: &str, to: &str| -> bool { - task_graph.edge_indices().any(|edge_idx| { - let (source, target) = task_graph.edge_endpoints(edge_idx).unwrap(); - task_graph[source].display_name() == from - && task_graph[target].display_name() == to - }) - }; - - // With transitive dependency resolution, A should have edge to C (A depends on C transitively) - assert!( - has_edge("@test/a#build", "@test/c#build"), - "A should have edge to C (A depends on C transitively through B)" - ); - }); - } - - #[test] - fn test_comprehensive_task_graph() { - with_unique_cache_path("comprehensive_task_graph", |cache_path| { - let fixture_path = get_fixture_path("fixtures/comprehensive-task-graph"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test build task graph - let build_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve build tasks"); - - let build_tasks: Vec<_> = - build_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Verify all packages with build scripts are included - assert!(build_tasks.contains(&"@test/shared#build".into())); - assert!(build_tasks.contains(&"@test/ui#build".into())); - assert!(build_tasks.contains(&"@test/api#build".into())); - assert!(build_tasks.contains(&"@test/app#build".into())); - assert!(build_tasks.contains(&"@test/config#build".into())); - - // Tools doesn't have a build script - assert!(!build_tasks.iter().any(|task| task.starts_with("@test/tools#"))); - - let has_edge = - |graph: &StableDiGraph, from: &str, to: &str| -> bool { - graph.edge_indices().any(|edge_idx| { - let (source, target) = graph.edge_endpoints(edge_idx).unwrap(); - graph[source].display_name() == from && graph[target].display_name() == to - }) - }; - - // Verify dependency edges for build tasks (between last subtasks) - assert!(has_edge(&build_graph, "@test/ui#build(subcommand 0)", "@test/shared#build")); - assert!(has_edge(&build_graph, "@test/api#build(subcommand 0)", "@test/shared#build")); - assert!(has_edge(&build_graph, "@test/api#build(subcommand 0)", "@test/config#build")); - assert!(has_edge(&build_graph, "@test/app#build(subcommand 0)", "@test/ui#build")); - assert!(has_edge(&build_graph, "@test/app#build(subcommand 0)", "@test/api#build")); - assert!(has_edge(&build_graph, "@test/app#build(subcommand 0)", "@test/shared#build")); - - // Test that UI has compound commands (3 subtasks) - let ui_tasks: Vec<_> = build_graph - .node_weights() - .filter(|task| task.display_name().starts_with("@test/ui#build")) - .map(|task| task.name.subcommand_index) - .collect(); - assert_eq!(ui_tasks.len(), 3); - assert!(ui_tasks.contains(&Some(0))); - assert!(ui_tasks.contains(&Some(1))); - assert!(ui_tasks.contains(&None)); - - // Verify UI compound task internal dependencies - assert!(has_edge( - &build_graph, - "@test/ui#build(subcommand 1)", - "@test/ui#build(subcommand 0)", - )); - assert!(has_edge(&build_graph, "@test/ui#build", "@test/ui#build(subcommand 1)")); - - // Test that shared has compound commands (3 subtasks for build) - let shared_build_tasks: Vec<_> = build_graph - .node_weights() - .filter(|task| task.display_name().starts_with("@test/shared#build")) - .collect(); - assert_eq!(shared_build_tasks.len(), 3); - - // Test that API has compound commands (4 subtasks for build) - let api_build_tasks: Vec<_> = build_graph - .node_weights() - .filter(|task| task.display_name().starts_with("@test/api#build")) - .collect(); - assert_eq!(api_build_tasks.len(), 4); - - // Test that app has compound commands (5 subtasks for build) - let app_build_tasks: Vec<_> = build_graph - .node_weights() - .filter(|task| task.display_name().starts_with("@test/app#build")) - .collect(); - assert_eq!(app_build_tasks.len(), 5); - - // Verify cross-package dependencies connect to first subtask - assert!(has_edge(&build_graph, "@test/api#build(subcommand 0)", "@test/shared#build")); - assert!(has_edge(&build_graph, "@test/api#build(subcommand 0)", "@test/config#build")); - assert!(has_edge(&build_graph, "@test/app#build(subcommand 0)", "@test/api#build")); - - // Test test task graph - let test_graph = workspace - .build_task_subgraph(&["test".into()], Arc::default(), true) - .expect("Failed to resolve test tasks"); - - let test_tasks: Vec<_> = - test_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!(test_tasks.contains(&"@test/shared#test".into())); - assert!(test_tasks.contains(&"@test/ui#test".into())); - assert!(test_tasks.contains(&"@test/api#test".into())); - assert!(test_tasks.contains(&"@test/app#test".into())); - - // Config and tools don't have test scripts - assert!(!test_tasks.iter().any(|task| task == "@test/config#test")); - assert!(!test_tasks.iter().any(|task| task == "@test/tools#test")); - - // Verify shared#test has compound commands (3 subtasks) - let shared_test_tasks: Vec<_> = test_graph - .node_weights() - .filter(|task| task.display_name().starts_with("@test/shared#test")) - .collect(); - assert_eq!(shared_test_tasks.len(), 3); - - // Test specific package task - let api_build_graph = workspace - .build_task_subgraph(&["@test/api#build".into()], Arc::default(), false) - .expect("Failed to resolve api build task"); - - let api_deps: Vec<_> = - api_build_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Should include api and its dependencies - assert!(api_deps.contains(&"@test/api#build".into())); - assert!(api_deps.contains(&"@test/shared#build".into())); - assert!(api_deps.contains(&"@test/config#build".into())); - // Should not include app or ui - assert!(!api_deps.contains(&"@test/app#build".into())); - assert!(!api_deps.contains(&"@test/ui#build".into())); - }); - } - - #[test] - fn test_scripts_with_hash_in_names() { - with_unique_cache_path("scripts_with_hash_in_names", |cache_path| { - let fixture_path = get_fixture_path("fixtures/comprehensive-task-graph"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test that we can't use recursive with task names containing # (would be interpreted as scope) - let result = - workspace.build_task_subgraph(&["test#integration".into()], Arc::default(), true); - assert!(result.is_err(), "Recursive run with # in task name should fail"); - }); - } - - #[test] - fn test_task_graph_visualization() { - with_unique_cache_path("task_graph_visualization", |cache_path| { - let fixture_path = get_fixture_path("fixtures/comprehensive-task-graph"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test app build task graph - this should show the full dependency tree - let app_build_graph = workspace - .build_task_subgraph(&["@test/app#build".into()], Arc::default(), false) - .expect("Failed to resolve app build task"); - - // Expected task graph structure: - // - // @test/config#build ─────────────────┐ - // ▼ - // @test/shared#build[0] ──► [1] ──► [None] ──┐ - // │ │ - // ▼ ▼ - // @test/ui#build[0] ──► [1] ──► [None] ──► @test/app#build[0] ──► [1] ──► [2] ──► [3] ──► [None] - // ▲ - // @test/api#build[0] ──► [1] ──► [2] ──► [None] ──┘ - // ▲ - // └─────────────────────────────────────┘ - - let has_full_edge = - |graph: &StableDiGraph, from_name: &str, to_name: &str| -> bool { - graph.edge_indices().any(|edge_idx| { - let (source, target) = graph.edge_endpoints(edge_idx).unwrap(); - graph[source].display_name() == from_name - && graph[target].display_name() == to_name - }) - }; - - // Verify all tasks are present - let all_tasks: Vec<_> = - app_build_graph.node_weights().map(super::ResolvedTask::display_name).collect(); - - // App should have 5 subtasks (indices: 0, 1, 2, 3, None) - assert_eq!( - all_tasks.iter().filter(|name| name.starts_with("@test/app#build")).count(), - 5 - ); - // API should have 4 subtasks (indices: 0, 1, 2, None) - assert_eq!( - all_tasks.iter().filter(|name| name.starts_with("@test/api#build")).count(), - 4 - ); - // Shared should have 3 subtasks (indices: 0, 1, None) - assert_eq!( - all_tasks.iter().filter(|name| name.starts_with("@test/shared#build")).count(), - 3 - ); - // UI should have 3 subtasks (indices: 0, 1, None) - assert_eq!( - all_tasks.iter().filter(|name| name.starts_with("@test/ui#build")).count(), - 3 - ); - // Config should have 1 task (no &&) - assert_eq!( - all_tasks.iter().filter(|name| name.starts_with("@test/config#build")).count(), - 1 - ); - - // Verify internal task dependencies (within compound commands) - // App internal deps (5 commands => indices 0, 1, 2, 3, None) - assert!(has_full_edge( - &app_build_graph, - "@test/app#build(subcommand 1)", - "@test/app#build(subcommand 0)", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/app#build(subcommand 2)", - "@test/app#build(subcommand 1)", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/app#build(subcommand 3)", - "@test/app#build(subcommand 2)", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/app#build", - "@test/app#build(subcommand 3)", - )); - - // API internal deps (4 commands => indices 0, 1, 2, None) - assert!(has_full_edge( - &app_build_graph, - "@test/api#build(subcommand 1)", - "@test/api#build(subcommand 0)", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/api#build(subcommand 2)", - "@test/api#build(subcommand 1)", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/api#build", - "@test/api#build(subcommand 2)", - )); - - // Verify cross-package dependencies - // Dependencies TO app#build[0] (first subtask) - assert!(has_full_edge( - &app_build_graph, - "@test/app#build(subcommand 0)", - "@test/ui#build", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/app#build(subcommand 0)", - "@test/api#build", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/app#build(subcommand 0)", - "@test/shared#build", - )); - - // Dependencies TO api#build[0] - assert!(has_full_edge( - &app_build_graph, - "@test/api#build(subcommand 0)", - "@test/shared#build", - )); - assert!(has_full_edge( - &app_build_graph, - "@test/api#build(subcommand 0)", - "@test/config#build", - )); - - // Dependencies TO ui#build[0] - assert!(has_full_edge( - &app_build_graph, - "@test/ui#build(subcommand 0)", - "@test/shared#build", - )); - }); - } - - #[test] - fn test_cache_sharing_between_subtasks() { - with_unique_cache_path("cache_sharing_between_subtasks", |cache_path| { - let fixtures_dir = get_fixture_path("fixtures/cache-sharing"); - - let workspace = Workspace::load_with_cache_path( - fixtures_dir, - Some(cache_path), - false, // topological_run - ) - .unwrap(); - - let tasks = vec![ - "@test/cache-sharing#a".into(), - "@test/cache-sharing#b".into(), - "@test/cache-sharing#c".into(), - ]; - let task_graph = workspace.build_task_subgraph(&tasks, Arc::default(), false).unwrap(); - - // Get all tasks from the graph - let tasks: Vec<_> = task_graph - .node_weights() - .map(|task| (task.display_name(), task.name.subcommand_index)) - .collect(); - - // Task 'a' should have only one task (no &&) - assert_eq!( - tasks.iter().filter(|(name, _)| *name == "@test/cache-sharing#a").count(), - 1 - ); - - // Task 'b' should have 2 subtasks: 'echo a' (index 0) and main (None). - let b_tasks: Vec<_> = tasks - .iter() - .filter(|(name, _)| name.starts_with("@test/cache-sharing#b")) - .collect(); - assert_eq!(b_tasks.len(), 2, "Expected 2 subtasks for task 'b', got {}", b_tasks.len()); - - // Task 'c' should have 3 subtasks: 'echo a' (index 0), 'echo b' (index 1), and main (None) - assert_eq!( - tasks.iter().filter(|(name, _)| name.starts_with("@test/cache-sharing#c")).count(), - 3 - ); - - // Now verify that the cache keys are the same for "echo a" commands - // The first subtask of 'b' (echo a) should have the same cache key as task 'a' (echo a) - let task_a = task_graph - .node_weights() - .find(|t| { - t.display_name() == "@test/cache-sharing#a" && t.name.subcommand_index.is_none() - }) - .unwrap(); - - let task_b_subtask_0 = task_graph - .node_weights() - .find(|t| t.display_name() == "@test/cache-sharing#b(subcommand 0)") - .unwrap(); - - let task_c_subtask_0 = task_graph - .node_weights() - .find(|t| t.display_name() == "@test/cache-sharing#c(subcommand 0)") - .unwrap(); - - // All three should have command "echo a" - let task_a_command = &task_a.resolved_command.fingerprint.command; - let task_b_command = &task_b_subtask_0.resolved_command.fingerprint.command; - let task_c_command = &task_c_subtask_0.resolved_command.fingerprint.command; - - assert_eq!( - task_a_command.to_string(), - "echo a", - "Task 'a' should have command 'echo a'" - ); - assert_eq!( - task_b_command.to_string(), - "echo a", - "First subtask of 'b' should have command 'echo a'" - ); - assert_eq!( - task_c_command.to_string(), - "echo a", - "First subtask of 'c' should have command 'echo a'" - ); - - // The cache keys should be the same (same package, same command fingerprint, same args) - assert_eq!( - task_a.resolved_command.fingerprint, task_b_subtask_0.resolved_command.fingerprint, - "Task 'a' and first subtask of 'b' should have identical fingerprints for cache sharing" - ); - assert_eq!( - task_a.resolved_command.fingerprint, task_c_subtask_0.resolved_command.fingerprint, - "Task 'a' and first subtask of 'c' should have identical fingerprints for cache sharing" - ); - }); - } - - #[test] - fn test_empty_package_name_handling() { - with_unique_cache_path("empty_package_name", |cache_path| { - // Create a separate fixture directory for testing empty package names - // to avoid conflicts with the comprehensive-task-graph test - let fixture_path = get_fixture_path("fixtures/empty-package-test"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace with empty package name"); - - // Test that empty-name package is loaded correctly - let empty_name_package = - workspace.package_graph.node_weights().find(|p| p.package_json.name.is_empty()); - assert!(empty_name_package.is_some(), "Should find package with empty name"); - - // Test resolving build task recursively - should find both packages - let build_tasks = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve build tasks recursively"); - - let task_names: Vec<_> = - build_tasks.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!( - task_names.contains(&"build".into()), - "Should find empty-name package build task, found: {task_names:?}" - ); - assert!( - task_names.contains(&"normal-package#build".into()), - "Should find normal-package build task" - ); - - // Test that empty-name package internal dependencies work - let empty_build = workspace - .build_task_subgraph(&["#build".into()], Arc::default(), false) - .expect("Failed to resolve empty-name build"); - - let empty_build_tasks: Vec<_> = - empty_build.node_weights().map(super::ResolvedTask::display_name).collect(); - - assert!(empty_build_tasks.contains(&"build".into()), "Should have build task"); - assert!( - empty_build_tasks.contains(&"test".into()), - "Should have test task as dependency" - ); - - // Verify internal dependencies work correctly - let has_edge = - |graph: &StableDiGraph, from: &str, to: &str| -> bool { - graph.edge_indices().any(|edge_idx| { - let (source, target) = graph.edge_endpoints(edge_idx).unwrap(); - let source_task = &graph[source]; - let target_task = &graph[target]; - source_task.display_name() == from && target_task.display_name() == to - }) - }; - - assert!( - has_edge(&empty_build, "build", "test"), - "Empty-name build should depend on empty-name test (internal dependency)" - ); - }); - } - - #[test] - fn test_multiple_nameless_packages() { - with_unique_cache_path("multiple_nameless_packages", |cache_path| { - let fixture_path = get_fixture_path("fixtures/empty-package-test"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace with multiple nameless packages"); - - // Verify both nameless packages are loaded - let nameless_packages: Vec<_> = workspace - .package_graph - .node_weights() - .filter(|p| p.package_json.name.is_empty()) - .collect(); - - assert_eq!(nameless_packages.len(), 2, "Should find exactly 2 nameless packages"); - - // Test recursive build includes both nameless packages - let build_tasks = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve build tasks recursively"); - - let task_names: Vec<_> = - build_tasks.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Count build tasks from nameless packages (they appear as just "build") - let nameless_build_count = task_names.iter().filter(|name| *name == "build").count(); - - assert_eq!( - nameless_build_count, 2, - "Should find 2 'build' tasks from nameless packages, found tasks: {task_names:?}" - ); - - // Verify normal package build is also included - assert!( - task_names.contains(&"normal-package#build".into()), - "Should also include normal-package#build" - ); - - // Test that nameless packages can have different internal dependencies - // The second nameless package has more complex dependencies - let deploy_tasks = workspace - .build_task_subgraph(&["deploy".into()], Arc::default(), true) - .expect("Failed to resolve deploy tasks"); - - let deploy_task_names: Vec<_> = - deploy_tasks.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Check that deploy task and its dependencies are resolved - assert!( - deploy_task_names.contains(&"deploy".into()), - "Should find deploy task from second nameless package" - ); - assert!( - deploy_task_names.contains(&"lint".into()), - "Should include lint as dependency of build in second nameless package" - ); - assert!( - deploy_task_names.contains(&"normal-package#test".into()), - "Should include normal-package#test as dependency" - ); - - // Verify that dependencies between nameless packages don't interfere - let test_tasks = workspace - .build_task_subgraph(&["test".into()], Arc::default(), true) - .expect("Failed to resolve test tasks"); - - let test_task_names: Vec<_> = - test_tasks.node_weights().map(super::ResolvedTask::display_name).collect(); - - // Should have test tasks from both nameless packages and normal-package - let nameless_test_count = test_task_names.iter().filter(|name| *name == "test").count(); - - assert_eq!(nameless_test_count, 2, "Should find 2 'test' tasks from nameless packages"); - - // Test topological ordering with nameless packages - // The second nameless package depends on normal-package - // With topological ordering, build tasks should respect package dependencies - let build_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve build with topological"); - - // Helper to check edges - let has_edge = |graph: &StableDiGraph, - from_pattern: &str, - to_pattern: &str| - -> bool { - graph.edge_indices().any(|edge_idx| { - let (source, target) = graph.edge_endpoints(edge_idx).unwrap(); - let source_name = graph[source].display_name(); - let target_name = graph[target].display_name(); - - // For nameless packages, we need to check the package path - // Since both show as "build", we need another way to distinguish them - let source_matches = source_name == from_pattern; - let target_matches = target_name == to_pattern; - - source_matches && target_matches - }) - }; - - // The second nameless package depends on normal-package - // So with topological ordering, normal-package#build should run before the second nameless build - assert!( - has_edge(&build_graph, "build", "normal-package#build") - && has_edge(&build_graph, "build", "normal-package#test"), - "Should have dependency from normal-package to second nameless package due to topological ordering" - ); - }); - } - - #[test] - fn test_task_without_sharp_in_explicit_mode() { - with_unique_cache_path("task_without_sharp_explicit", |cache_path| { - let fixture_path = get_fixture_path("fixtures/comprehensive-task-graph"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), false) - .expect("Failed to load workspace"); - - // When in explicit mode (non-recursive), tasks without '#' should resolve to current package - // This test simulates being in a package directory - - // First, test that the original scoped task works - let api_build_scoped = workspace - .build_task_subgraph(&["@test/api#build".into()], Arc::default(), false) - .expect("Failed to resolve @test/api#build"); - - // Find the number of tasks for API build - let api_build_task_count = api_build_scoped.node_count(); - assert!(api_build_task_count > 0, "Should find API build task"); - - // Test that we can resolve task with '#' in package - let app_test_scoped = workspace - .build_task_subgraph(&["@test/app#test".into()], Arc::default(), false) - .expect("Failed to resolve @test/app#test"); - - // Should include dependencies - assert!(app_test_scoped.node_count() > 0, "Should find app test task"); - - // Verify task names in graph - let mut found_app_test = false; - for task in app_test_scoped.node_weights() { - if task.display_name() == "@test/app#test" { - found_app_test = true; - break; - } - } - assert!(found_app_test, "Should find @test/app#test task in graph"); - }); - } - - #[test] - fn test_dependency_resolution_with_ambiguous_names() { - with_unique_cache_path("dependency_ambiguous_names", |cache_path| { - let fixture_path = get_fixture_path("fixtures/conflict-test"); - - // This should fail with a TaskNameConflict error because the dependency - // "@test/scope-a#b#c" is ambiguous - it could mean: - // - Package "@test/scope-a" with task "b#c", or - // - Package "@test/scope-a#b" with task "c" - // And both packages exist in the fixture - let result = Workspace::load_with_cache_path(fixture_path, Some(cache_path), false); - - // The workspace loading should fail due to the conflict - assert!(result.is_err(), "Should fail to load workspace with conflicting task names"); - - if let Err(e) = result { - // Verify it's the expected error type - match e { - Error::AmbiguousTaskRequest { .. } => { - // This is the expected error - } - _ => panic!("Expected TaskNameConflict error, but got: {e:?}"), - } - } - }); - } -} diff --git a/crates/vite_task/src/config/workspace.rs b/crates/vite_task/src/config/workspace.rs index 3f22c3e2..70c6713f 100644 --- a/crates/vite_task/src/config/workspace.rs +++ b/crates/vite_task/src/config/workspace.rs @@ -131,7 +131,7 @@ impl Workspace { let (workspace_root, cwd, current_package_path) = Self::determine_current_package_path(&cwd)?; - let package_graph = vite_workspace::get_package_graph(workspace_root)?; + let package_graph = vite_workspace::discover_package_graph(workspace_root)?; // Load vite-task.json files for all packages let packages_with_task_jsons = Self::load_vite_task_jsons(&package_graph, workspace_root)?; diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index 4bc75d0c..5b1b7058 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -11,9 +11,6 @@ mod schedule; mod types; mod ui; -#[cfg(test)] -mod test_utils; - // Public exports for vite-plus-cli to use pub use cache::TaskCache; pub use config::{ResolvedTask, Workspace}; diff --git a/crates/vite_task/src/schedule.rs b/crates/vite_task/src/schedule.rs index f7040b2f..61cc6b98 100644 --- a/crates/vite_task/src/schedule.rs +++ b/crates/vite_task/src/schedule.rs @@ -247,46 +247,3 @@ async fn get_cached_or_execute<'a>( ), }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - Workspace, - test_utils::{get_fixture_path, with_unique_cache_path}, - }; - - #[track_caller] - fn assert_order(plan: &ExecutionPlan, before: &str, after: &str) { - let before_index = plan.steps.iter().position(|t| t.display_name() == before); - let after_index = plan.steps.iter().position(|t| t.display_name() == after); - assert!(before_index.is_some(), "Task {before} not found in plan"); - assert!(after_index.is_some(), "Task {after} not found in plan"); - assert!(before_index < after_index, "Task {before} should be before {after}"); - } - - #[test] - fn test_execution_non_parallel() { - with_unique_cache_path("comprehensive_task_graph", |cache_path| { - let fixture_path = get_fixture_path("fixtures/comprehensive-task-graph"); - - let workspace = Workspace::load_with_cache_path(fixture_path, Some(cache_path), true) - .expect("Failed to load workspace"); - - // Test build task graph - let build_graph = workspace - .build_task_subgraph(&["build".into()], Arc::default(), true) - .expect("Failed to resolve build tasks"); - - let plan = - ExecutionPlan::plan(build_graph, false).expect("Circular dependency detected"); - - assert_order(&plan, "@test/shared#build", "@test/ui#build(subcommand 0)"); - assert_order(&plan, "@test/shared#build", "@test/api#build(subcommand 0)"); - assert_order(&plan, "@test/config#build", "@test/api#build(subcommand 0)"); - assert_order(&plan, "@test/ui#build", "@test/app#build(subcommand 0)"); - assert_order(&plan, "@test/api#build", "@test/app#build(subcommand 0)"); - assert_order(&plan, "@test/shared#build", "@test/app#build(subcommand 0)"); - }); - } -} diff --git a/crates/vite_task/src/test_utils.rs b/crates/vite_task/src/test_utils.rs deleted file mode 100644 index a30f561a..00000000 --- a/crates/vite_task/src/test_utils.rs +++ /dev/null @@ -1,28 +0,0 @@ -use vite_path::{AbsolutePath, AbsolutePathBuf, current_dir}; -use vite_str::format; - -pub fn with_unique_cache_path(test_name: &str, f: F) -> R -where - F: FnOnce(AbsolutePathBuf) -> R, -{ - let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); - let cache_path = - AbsolutePath::new(temp_dir.path()).unwrap().join(format!("vite-test-{}", test_name)); - - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(cache_path))); - - // The temp directory and all its contents will be automatically cleaned up - // when temp_dir goes out of scope - - match result { - Ok(r) => r, - Err(panic) => std::panic::resume_unwind(panic), - } -} - -pub fn get_fixture_path(rel_path: &str) -> AbsolutePathBuf { - // The current dir is the manifest dir of the crate being tested. - // We don't use `env!("CARGO_MANIFEST_DIR")` because we want the test binary to be relocatable, - // so it can be cross-compiled and then run in a different os. - current_dir().unwrap().join(rel_path) -} diff --git a/crates/vite_task_graph/Cargo.toml b/crates/vite_task_graph/Cargo.toml new file mode 100644 index 00000000..03cee953 --- /dev/null +++ b/crates/vite_task_graph/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "vite_task_graph" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +monostate = "1.0.2" +petgraph = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs"] } +vec1 = { workspace = true, features = ["smallvec-v1"] } +vite_path = { workspace = true } +vite_str = { workspace = true } +vite_workspace = { workspace = true } + +[dev-dependencies] +copy_dir = { workspace = true } +insta = { workspace = true, features = ["glob", "json"] } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["fs", "rt-multi-thread"] } + +[lints] +workspace = true diff --git a/crates/vite_task_graph/README.md b/crates/vite_task_graph/README.md new file mode 100644 index 00000000..afd40419 --- /dev/null +++ b/crates/vite_task_graph/README.md @@ -0,0 +1,3 @@ +# vite_task_graph + +Crate for building task graphs based on package graphs and task configurations. diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs new file mode 100644 index 00000000..a4f09439 --- /dev/null +++ b/crates/vite_task_graph/src/config/mod.rs @@ -0,0 +1,88 @@ +mod user; + +use std::collections::HashSet; + +use monostate::MustBe; +pub use user::{UserCacheConfig, UserConfigFile, UserTaskConfig}; +use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_str::Str; + +/// Task configuration resolved from `package.json` scripts and/or `vite.config.ts` tasks, +/// without considering external factors like additional args from cli or environment variables. +/// +/// It should resolve as much as possible to the final form to save duplicated work when it's further resolved into a spawnable command later. +/// but must be independent of external factors. +/// +/// For example, `cwd` is resolved to absolute ones (no external factor can change it), +/// but `command` is not parsed into program and args yet because environment variables in it may need to be expanded. +/// +/// `depends_on` is not included here because it's represented in the task graph. +#[derive(Debug)] +pub struct ResolvedUserTaskConfig { + /// The command to run for this task + pub command: Str, + + /// The working directory for the task + pub cwd: AbsolutePathBuf, + + /// Cache-related config. None means caching is disabled. + pub cache_config: Option, +} + +#[derive(Debug)] +pub struct CacheConfig { + /// environment variable names to be fingerprinted and passed to the task, with defaults populated + pub envs: HashSet, + /// environment variable names to be passed to the task without fingerprinting, with defaults populated + pub pass_through_envs: HashSet, +} + +#[derive(Debug, thiserror::Error)] +pub enum ResolveTaskError { + /// Both package.json script and vite.config.* task define commands for the task + #[error("Both package.json script and vite.config.* task define commands for the task")] + CommandConflict, + + /// Neither package.json script nor vite.config.* task define a command for the task + #[error("Neither package.json script nor vite.config.* task define a command for the task")] + NoCommand, +} + +impl ResolvedUserTaskConfig { + pub fn resolve_package_json_script( + package_dir: &AbsolutePath, + package_json_script: &str, + ) -> Self { + Self::resolve( + UserTaskConfig::package_json_script_default(), + package_dir, + Some(package_json_script), + ) + .expect("Command conflict/missing for package.json script should never happen") + } + + /// Resolves from user config, package dir, and package.json script (if any). + pub fn resolve( + user_config: UserTaskConfig, + package_dir: &AbsolutePath, + package_json_script: Option<&str>, + ) -> Result { + let command = match (&user_config.command, package_json_script) { + (Some(_), Some(_)) => return Err(ResolveTaskError::CommandConflict), + (None, None) => return Err(ResolveTaskError::NoCommand), + (Some(cmd), None) => cmd.as_ref(), + (None, Some(script)) => script, + }; + let cwd = package_dir.join(user_config.cwd_relative_to_package); + let cache_config = match user_config.cache_config { + UserCacheConfig::Disabled { cache: MustBe!(false) } => None, + UserCacheConfig::Enabled { cache: MustBe!(true), envs, pass_through_envs } => { + Some(CacheConfig { + envs: envs.into_iter().collect(), + pass_through_envs: pass_through_envs.into_iter().collect(), + }) + } + }; + Ok(Self { command: command.into(), cwd, cache_config }) + } +} diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs new file mode 100644 index 00000000..e9f42dab --- /dev/null +++ b/crates/vite_task_graph/src/config/user.rs @@ -0,0 +1,154 @@ +//! Configuration structures for user-defined tasks in `vite.config.*` + +use std::{collections::HashMap, sync::Arc}; + +use monostate::MustBe; +use serde::Deserialize; +use vite_path::RelativePathBuf; +use vite_str::Str; + +/// Cache-related fields of a task defined by user in `vite.config.*` +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(untagged, deny_unknown_fields, rename_all = "camelCase")] +pub enum UserCacheConfig { + /// Cache is enabled + Enabled { + /// The `cache` field must be true or omitted + #[serde(default)] + cache: MustBe!(true), + + // Fields only relevant when cache is enabled + /// Environment variable names to be fingerprinted and passed to the task. + #[serde(default)] // default to empty if omitted + envs: Box<[Str]>, + + /// Environment variable names to be passed to the task without fingerprinting. + #[serde(default)] // default to empty if omitted + pass_through_envs: Box<[Str]>, + }, + /// Cache is disabled + Disabled { + /// The `cache` field must be false + cache: MustBe!(false), + }, +} + +/// Task configuration defined by user in `vite.config.*` +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UserTaskConfig { + /// If None, the script from `package.json` with the same name will be used + pub command: Option>, + + /// The working directory for the task, relative to the package root (not workspace root). + #[serde(default)] // default to empty if omitted + #[serde(rename = "cwd")] + pub cwd_relative_to_package: RelativePathBuf, + + /// Explicit dependencies of this task. + #[serde(default)] // default to empty if omitted + pub depends_on: Arc<[Str]>, + + /// Cache-related fields + #[serde(flatten)] + pub cache_config: UserCacheConfig, +} + +impl UserTaskConfig { + /// The default user task config for package.json scripts. + pub fn package_json_script_default() -> Self { + Self { + command: None, + cwd_relative_to_package: RelativePathBuf::default(), + depends_on: Arc::new([]), + cache_config: UserCacheConfig::Enabled { + cache: MustBe!(true), + envs: Box::new([]), + pass_through_envs: Box::new([]), + }, + } + } +} + +/// User configuration file structure for `vite.config.*` +#[derive(Debug, Deserialize)] +pub struct UserConfigFile { + pub tasks: HashMap, +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_defaults() { + let user_config_json = json!({}); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + user_config, + UserTaskConfig { + command: None, + cwd_relative_to_package: "".try_into().unwrap(), + depends_on: Default::default(), + cache_config: UserCacheConfig::Enabled { + cache: MustBe!(true), + envs: Default::default(), + pass_through_envs: Default::default(), + }, + } + ); + } + + #[test] + fn test_cwd_rename() { + let user_config_json = json!({ + "cwd": "src" + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!(user_config.cwd_relative_to_package.as_str(), "src"); + } + + #[test] + fn test_cache_disabled() { + let user_config_json = json!({ + "cache": false + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!(user_config.cache_config, UserCacheConfig::Disabled { cache: MustBe!(false) }); + } + + #[test] + fn test_cache_explicitly_enabled() { + let user_config_json = json!({ + "cache": true, + "envs": ["NODE_ENV"], + }); + assert_eq!( + serde_json::from_value::(user_config_json).unwrap(), + UserCacheConfig::Enabled { + cache: MustBe!(true), + envs: ["NODE_ENV".into()].into_iter().collect(), + pass_through_envs: Default::default(), + }, + ); + } + + #[test] + fn test_cache_disabled_but_with_fields() { + let user_config_json = json!({ + "cache": false, + "envs": ["NODE_ENV"], + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + + #[test] + fn test_deny_unknown_field() { + let user_config_json = json!({ + "foo": 42, + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } +} diff --git a/crates/vite_task_graph/src/lib.rs b/crates/vite_task_graph/src/lib.rs new file mode 100644 index 00000000..f990bd79 --- /dev/null +++ b/crates/vite_task_graph/src/lib.rs @@ -0,0 +1,341 @@ +pub mod config; +pub mod loader; + +use std::{ + collections::{HashMap, hash_map::Entry}, + sync::Arc, +}; + +use config::{ResolvedUserTaskConfig, UserConfigFile}; +use petgraph::graph::{DiGraph, NodeIndex}; +use serde::Serialize; +use vec1::smallvec_v1::SmallVec1; +use vite_path::AbsolutePath; +use vite_str::Str; +use vite_workspace::WorkspaceRoot; + +/// The type of a desk dependency, explaining why it's introduced. +#[derive(Debug, Clone, Copy, Serialize)] +pub enum TaskDependencyType { + /// The dependency is explicitly declared by user in `dependsOn`. + /// If a dependency is both explicit and topological, `TaskDependencyType::Explicit` takes precedenc + Explicit, + /// The dependency is added due to topological ordering based on package dependencies. + Topological, +} + +/// Uniquely identifies a task, by its name and the path where it's defined. +/// +/// We use package_dir instead of package_name because multiple packages can have the same name in a monorepo. +#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)] +pub struct TaskId { + /// This is the path of the package where the task is defined. + /// + /// Note that this is not always the cwd where the command is run, which is stored in `ResolvedUserTaskConfig`. + /// + /// `package_dir` is declared from `task_name` to make the `PartialOrd` implementation group tasks in same packages together. + pub package_dir: Arc, + + /// For user defined tasks, this is the name of the script or the entry in `vite-task.json`. + /// + /// For synthesized tasks, this is the program. + pub task_name: Str, +} + +/// A node in the task graph, representing a task with its resolved configuration. +#[derive(Debug)] +pub struct TaskNode { + /// The unique id of this task + pub task_id: TaskId, + + /// The name of the package where this task is defined. + /// It's used for matching task specifiers ('packageName#taskName') + /// + /// If package.json doesn't have a name field, this will be "", so the task can be matched by `#taskName`. + pub package_name: Str, + + /// The resolved configuration of this task. + /// + /// This contains information affecting how the task is spawn, + /// whereas `task_id` and `package_name` are for looking up the task. + /// + /// However, it does not contain external factors like additional args from cli and env vars. + pub resolved_config: ResolvedUserTaskConfig, +} + +#[derive(Debug, thiserror::Error)] +pub enum TaskGraphLoadError { + #[error("Failed to load package graph: {0}")] + PackageGraphLoadError(#[from] vite_workspace::Error), + + #[error("Failed to load task config file for package at {package_path:?}: {error}")] + ConfigLoadError { + #[source] + error: anyhow::Error, + package_path: Arc, + }, + + #[error("Failed to resolve task config for task {0} at {1:?}: {2}", task_id.task_name, task_id.package_dir, error)] + ResolveConfigError { + #[source] + error: crate::config::ResolveTaskError, + task_id: TaskId, + }, + + #[error("Failed to lookup dependency '{specifier}' of task {0} at {1:?}: {error}", origin_task_id.task_name, origin_task_id.task_name)] + DependencySpecifierLookupError { + #[source] + error: SpecifierLookupError, + specifier: Str, + // Where the dependency specifier is defined + origin_task_id: TaskId, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum SpecifierLookupError { + #[error( + "Package name '{package_name}' is ambiguous among multiple packages: {package_paths:?}" + )] + AmbiguousPackageName { package_name: Str, package_paths: Box<[Arc]> }, + + #[error("Package name '{package_name}' not found")] + PackageNameNotFound { package_name: Str }, + + #[error("Task name '{0}' not found in package {1:?}", task_id.task_name, task_id.package_dir)] + TaskNameNotFound { task_id: TaskId }, +} + +/// Full task graph of a workspace. +/// +/// It's immutable after created. The task nodes contain resolved task configurations and their dependencies. +/// External factors (e.g. additional args from cli, current working directory, environmental variables) are not stored here. +pub struct TaskGraph { + graph: DiGraph, + + /// Grouping package dirs by their package names. + /// Due to rare but possible name conflicts in monorepos, we use `SmallVec1` to store multiple dirs for same name. + package_dirs_by_name: HashMap; 1]>>, + + /// task indices by task id for quick lookup + node_indices_by_task_id: HashMap, +} + +impl TaskGraph { + /// Load the task graph from a discovered workspace using the provided config loader. + pub async fn load( + workspace_root: WorkspaceRoot<'_>, + config_loader: impl loader::UserConfigLoader, + ) -> Result { + let mut task_graph = DiGraph::::new(); + + let package_graph = vite_workspace::load_package_graph(&workspace_root)?; + + let mut dependency_specifiers_with_node_indices: Vec<(Arc<[Str]>, NodeIndex)> = Vec::new(); + + // Load task nodes into `task_graph` + for package in package_graph.node_weights() { + let package_dir: Arc = workspace_root.path.join(&package.path).into(); + + // Collect package.json scripts into a mutable map for draining lookup. + let mut package_json_scripts: HashMap<&str, &str> = package + .package_json + .scripts + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + .collect(); + + // Load vite.config.* for the package + let user_config: UserConfigFile = + config_loader.load_user_config_file(&package_dir).await.map_err(|error| { + TaskGraphLoadError::ConfigLoadError { error, package_path: package_dir.clone() } + })?; + + for (task_name, task_user_config) in user_config.tasks { + // For each task defined in vite.config.*, look up the corresponding package.json script (if any) + let package_json_script = package_json_scripts.remove(task_name.as_str()); + + let task_id = + TaskId { task_name: task_name.clone(), package_dir: Arc::clone(&package_dir) }; + + let dependency_specifiers = Arc::clone(&task_user_config.depends_on); + + // Resolve the task configuration combining vite.config.* and package.json script + let resolved_config = ResolvedUserTaskConfig::resolve( + task_user_config, + &package_dir, + package_json_script, + ) + .map_err(|err| TaskGraphLoadError::ResolveConfigError { + error: err, + task_id: task_id.clone(), + })?; + + let task_node = TaskNode { + task_id, + package_name: package.package_json.name.clone(), + resolved_config, + }; + + let node_index = task_graph.add_node(task_node); + dependency_specifiers_with_node_indices.push((dependency_specifiers, node_index)); + } + + // For remaining package.json scripts not defined in vite.config.*, create tasks with default config + for (script_name, package_json_script) in package_json_scripts.drain() { + let task_id = TaskId { + task_name: Str::from(script_name), + package_dir: Arc::clone(&package_dir), + }; + let resolved_config = ResolvedUserTaskConfig::resolve_package_json_script( + &package_dir, + package_json_script, + ); + task_graph.add_node(TaskNode { + task_id, + package_name: package.package_json.name.clone(), + resolved_config, + }); + } + } + + // index tasks by ids + let mut node_indices_by_task_id: HashMap = + HashMap::with_capacity(task_graph.node_count()); + for node_index in task_graph.node_indices() { + let task_node = &task_graph[node_index]; + + let existing_entry = + node_indices_by_task_id.insert(task_node.task_id.clone(), node_index); + if existing_entry.is_some() { + // This should never happen as we enforce unique task ids when adding nodes. + panic!("Duplicate task id found: {:?}", task_node.task_id); + } + } + + // Grouping package dirs by their package names. + let mut package_dirs_by_name: HashMap; 1]>> = + HashMap::new(); + for package in package_graph.node_weights() { + let package_dir: Arc = workspace_root.path.join(&package.path).into(); + match package_dirs_by_name.entry(package.package_json.name.clone()) { + Entry::Vacant(vacant) => { + vacant.insert(SmallVec1::new(package_dir)); + } + Entry::Occupied(occupied) => { + occupied.into_mut().push(package_dir); + } + } + } + + // Construct `Self` with task_graph with all task nodes ready and indexed, but no edges. + let mut me = Self { graph: task_graph, node_indices_by_task_id, package_dirs_by_name }; + + // Add explicit dependencies + for (dependency_specifiers, from_node_index) in dependency_specifiers_with_node_indices { + let from_task_id = me.graph[from_node_index].task_id.clone(); + + for specifier in dependency_specifiers.iter().cloned() { + let to_node_index = me + .get_task_index_by_specifier(&specifier, &from_task_id.package_dir) + .map_err(|error| TaskGraphLoadError::DependencySpecifierLookupError { + error, + specifier, + origin_task_id: from_task_id.clone(), + })?; + me.graph.update_edge(from_node_index, to_node_index, TaskDependencyType::Explicit); + } + } + + // TODO: Add topological dependencies based on package dependencies + + Ok(me) + } + + /// Lookup the node index of a task by its specifier. + /// + /// The specifier can be either 'packageName#taskName' or just 'taskName' (in which case the task in the origin package is looked up). + fn get_task_index_by_specifier( + &self, + specifier: &str, + package_origin: &Arc, + ) -> Result { + let (package_dir, task_name): (Arc, Str) = + if let Some((package_name, task_name)) = specifier.rsplit_once('#') { + // Lookup package path by the package name from '#' + let Some(package_paths) = self.package_dirs_by_name.get(package_name) else { + return Err(SpecifierLookupError::PackageNameNotFound { + package_name: package_name.into(), + }); + }; + if package_paths.len() > 1 { + return Err(SpecifierLookupError::AmbiguousPackageName { + package_name: package_name.into(), + package_paths: package_paths.iter().cloned().collect(), + }); + }; + (Arc::clone(package_paths.first()), task_name.into()) + } else { + // No '#', so the specifier only contains task name, look up in the origin path package + (Arc::clone(&package_origin), specifier.into()) + }; + let task_id = TaskId { task_name, package_dir }; + let Some(node_index) = self.node_indices_by_task_id.get(&task_id) else { + return Err(SpecifierLookupError::TaskNameNotFound { task_id }); + }; + Ok(*node_index) + } + + /// Create a stable json representation of the task graph for snapshot testing. + /// + /// All paths are relative to `base_dir`. + pub fn snapshot(&self, base_dir: &AbsolutePath) -> serde_json::Value { + use vite_path::RelativePathBuf; + + #[derive(serde::Serialize, PartialEq, PartialOrd, Eq, Ord)] + struct TaskIdSnapshot { + package_dir: RelativePathBuf, + task_name: Str, + } + impl TaskIdSnapshot { + fn from_task_id(task_id: &TaskId, base_dir: &AbsolutePath) -> Self { + Self { + task_name: task_id.task_name.clone(), + package_dir: task_id.package_dir.strip_prefix(base_dir).unwrap().unwrap(), + } + } + } + + #[derive(serde::Serialize)] + struct TaskNodeSnapshot { + id: TaskIdSnapshot, + command: Str, + cwd: RelativePathBuf, + depends_on: Vec<(TaskIdSnapshot, TaskDependencyType)>, + } + + let mut node_snapshots = Vec::::with_capacity(self.graph.node_count()); + for a in self.graph.node_indices() { + let node = &self.graph[a]; + let mut depends_on: Vec<(TaskIdSnapshot, TaskDependencyType)> = self + .graph + .edges_directed(a, petgraph::Direction::Outgoing) + .map(|edge| { + use petgraph::visit::EdgeRef as _; + let target_node = &self.graph[edge.target()]; + (TaskIdSnapshot::from_task_id(&target_node.task_id, base_dir), *edge.weight()) + }) + .collect(); + depends_on.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + node_snapshots.push(TaskNodeSnapshot { + id: TaskIdSnapshot::from_task_id(&node.task_id, base_dir), + command: node.resolved_config.command.clone(), + cwd: node.resolved_config.cwd.strip_prefix(base_dir).unwrap().unwrap(), + depends_on, + }); + } + node_snapshots.sort_unstable_by(|a, b| a.id.cmp(&b.id)); + + serde_json::to_value(&node_snapshots).unwrap() + } +} diff --git a/crates/vite_task_graph/src/loader.rs b/crates/vite_task_graph/src/loader.rs new file mode 100644 index 00000000..7a3c932b --- /dev/null +++ b/crates/vite_task_graph/src/loader.rs @@ -0,0 +1,37 @@ +use vite_path::AbsolutePath; + +use crate::config::UserConfigFile; + +/// Loader trait for loading user configuration files (vite.config.*). +pub trait UserConfigLoader { + fn load_user_config_file( + &self, + package_path: &AbsolutePath, + ) -> impl std::future::Future> + Send; +} + +/// A `UserConfigLoader` implementation that only loads `vite.config.json`. +/// +/// This is mainly for examples and testing as it does not require Node.js environment. +#[derive(Default, Debug)] +pub struct JsonUserConfigLoader(()); + +impl UserConfigLoader for JsonUserConfigLoader { + fn load_user_config_file( + &self, + package_path: &AbsolutePath, + ) -> impl std::future::Future> + Send { + async move { + let config_path = package_path.join("vite.config.json"); + let config_content = match tokio::fs::read_to_string(&config_path).await { + Ok(content) => content, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Ok(UserConfigFile { tasks: Default::default() }); + } + Err(err) => return Err(err.into()), + }; + let user_config: UserConfigFile = serde_json::from_str(&config_content)?; + Ok(user_config) + } + } +} diff --git a/crates/vite_task/fixtures/cache-sharing/package.json b/crates/vite_task_graph/tests/fixtures/cache-sharing/package.json similarity index 100% rename from crates/vite_task/fixtures/cache-sharing/package.json rename to crates/vite_task_graph/tests/fixtures/cache-sharing/package.json diff --git a/crates/vite_task/fixtures/cache-sharing/pnpm-lock.yaml b/crates/vite_task_graph/tests/fixtures/cache-sharing/pnpm-lock.yaml similarity index 99% rename from crates/vite_task/fixtures/cache-sharing/pnpm-lock.yaml rename to crates/vite_task_graph/tests/fixtures/cache-sharing/pnpm-lock.yaml index 9b60ae17..b6688e7c 100644 --- a/crates/vite_task/fixtures/cache-sharing/pnpm-lock.yaml +++ b/crates/vite_task_graph/tests/fixtures/cache-sharing/pnpm-lock.yaml @@ -5,5 +5,4 @@ settings: excludeLinksFromLockfile: false importers: - .: {} diff --git a/crates/vite_task/fixtures/cache-sharing/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/cache-sharing/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/cache-sharing/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/cache-sharing/pnpm-workspace.yaml diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/api/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/api/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/api/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/api/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/app/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/app/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/app/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/app/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/config/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/config/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/config/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/config/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/pkg#special/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/pkg#special/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/pkg#special/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/pkg#special/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/shared/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/shared/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/shared/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/shared/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/tools/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/tools/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/tools/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/tools/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/packages/ui/package.json b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/ui/package.json similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/packages/ui/package.json rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/packages/ui/package.json diff --git a/crates/vite_task/fixtures/comprehensive-task-graph/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/comprehensive-task-graph/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/comprehensive-task-graph/pnpm-workspace.yaml diff --git a/crates/vite_task/fixtures/conflict-test/package.json b/crates/vite_task_graph/tests/fixtures/conflict-test/package.json similarity index 100% rename from crates/vite_task/fixtures/conflict-test/package.json rename to crates/vite_task_graph/tests/fixtures/conflict-test/package.json diff --git a/crates/vite_task/fixtures/conflict-test/packages/scope-a-b/package.json b/crates/vite_task_graph/tests/fixtures/conflict-test/packages/scope-a-b/package.json similarity index 100% rename from crates/vite_task/fixtures/conflict-test/packages/scope-a-b/package.json rename to crates/vite_task_graph/tests/fixtures/conflict-test/packages/scope-a-b/package.json diff --git a/crates/vite_task/fixtures/conflict-test/packages/scope-a/package.json b/crates/vite_task_graph/tests/fixtures/conflict-test/packages/scope-a/package.json similarity index 100% rename from crates/vite_task/fixtures/conflict-test/packages/scope-a/package.json rename to crates/vite_task_graph/tests/fixtures/conflict-test/packages/scope-a/package.json diff --git a/crates/vite_task/fixtures/conflict-test/packages/test-package/package.json b/crates/vite_task_graph/tests/fixtures/conflict-test/packages/test-package/package.json similarity index 100% rename from crates/vite_task/fixtures/conflict-test/packages/test-package/package.json rename to crates/vite_task_graph/tests/fixtures/conflict-test/packages/test-package/package.json diff --git a/crates/vite_task/fixtures/conflict-test/packages/test-package/vite-task.json b/crates/vite_task_graph/tests/fixtures/conflict-test/packages/test-package/vite.config.json similarity index 82% rename from crates/vite_task/fixtures/conflict-test/packages/test-package/vite-task.json rename to crates/vite_task_graph/tests/fixtures/conflict-test/packages/test-package/vite.config.json index 3cbe3a64..aca6743c 100644 --- a/crates/vite_task/fixtures/conflict-test/packages/test-package/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/conflict-test/packages/test-package/vite.config.json @@ -2,7 +2,7 @@ "tasks": { "test": { "command": "echo Testing", - "cacheable": true, + "cache": true, "dependsOn": ["@test/scope-a#b#c"] } } diff --git a/crates/vite_task/fixtures/conflict-test/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/conflict-test/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/conflict-test/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/conflict-test/pnpm-workspace.yaml diff --git a/crates/vite_task/fixtures/empty-package-test/package.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/package.json similarity index 100% rename from crates/vite_task/fixtures/empty-package-test/package.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/package.json diff --git a/crates/vite_task/fixtures/empty-package-test/packages/another-empty/package.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/another-empty/package.json similarity index 100% rename from crates/vite_task/fixtures/empty-package-test/packages/another-empty/package.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/packages/another-empty/package.json diff --git a/crates/vite_task/fixtures/empty-package-test/packages/another-empty/vite-task.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/another-empty/vite.config.json similarity index 81% rename from crates/vite_task/fixtures/empty-package-test/packages/another-empty/vite-task.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/packages/another-empty/vite.config.json index dd531e4c..b69972a3 100644 --- a/crates/vite_task/fixtures/empty-package-test/packages/another-empty/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/another-empty/vite.config.json @@ -2,20 +2,20 @@ "tasks": { "build": { "command": "echo 'Building another-empty package'", - "cacheable": true, + "cache": true, "dependsOn": ["lint", "normal-package#test"] }, "test": { "command": "echo 'Testing another-empty package'", - "cacheable": true + "cache": true }, "lint": { "command": "echo 'Linting another-empty package'", - "cacheable": true + "cache": true }, "deploy": { "command": "echo 'Deploying another-empty package'", - "cacheable": false, + "cache": false, "dependsOn": ["build", "test"] } } diff --git a/crates/vite_task/fixtures/empty-package-test/packages/empty-name/package.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/empty-name/package.json similarity index 100% rename from crates/vite_task/fixtures/empty-package-test/packages/empty-name/package.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/packages/empty-name/package.json diff --git a/crates/vite_task/fixtures/empty-package-test/packages/empty-name/vite-task.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/empty-name/vite.config.json similarity index 79% rename from crates/vite_task/fixtures/empty-package-test/packages/empty-name/vite-task.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/packages/empty-name/vite.config.json index dc8c0a45..8ddea45b 100644 --- a/crates/vite_task/fixtures/empty-package-test/packages/empty-name/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/empty-name/vite.config.json @@ -2,16 +2,16 @@ "tasks": { "build": { "command": "echo 'Building empty-name package'", - "cacheable": true, + "cache": true, "dependsOn": ["test"] }, "test": { "command": "echo 'Testing empty-name package'", - "cacheable": true + "cache": true }, "lint": { "command": "echo 'Linting empty-name package'", - "cacheable": true + "cache": true } } } diff --git a/crates/vite_task/fixtures/empty-package-test/packages/normal-package/package.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/normal-package/package.json similarity index 100% rename from crates/vite_task/fixtures/empty-package-test/packages/normal-package/package.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/packages/normal-package/package.json diff --git a/crates/vite_task/fixtures/empty-package-test/packages/normal-package/vite-task.json b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/normal-package/vite.config.json similarity index 77% rename from crates/vite_task/fixtures/empty-package-test/packages/normal-package/vite-task.json rename to crates/vite_task_graph/tests/fixtures/empty-package-test/packages/normal-package/vite.config.json index 8554ee5a..26981cf1 100644 --- a/crates/vite_task/fixtures/empty-package-test/packages/normal-package/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/empty-package-test/packages/normal-package/vite.config.json @@ -2,11 +2,11 @@ "tasks": { "build": { "command": "echo 'Building normal-package'", - "cacheable": true + "cache": true }, "test": { "command": "echo 'Testing normal-package'", - "cacheable": true + "cache": true } } } diff --git a/crates/vite_task/fixtures/empty-package-test/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/empty-package-test/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/empty-package-test/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/empty-package-test/pnpm-workspace.yaml diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/package.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/package.json similarity index 100% rename from crates/vite_task/fixtures/explicit-deps-workspace/package.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/package.json diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/packages/app/package.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/app/package.json similarity index 100% rename from crates/vite_task/fixtures/explicit-deps-workspace/packages/app/package.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/app/package.json diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/packages/app/vite-task.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/app/vite.config.json similarity index 86% rename from crates/vite_task/fixtures/explicit-deps-workspace/packages/app/vite-task.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/app/vite.config.json index 451e2836..5f218989 100644 --- a/crates/vite_task/fixtures/explicit-deps-workspace/packages/app/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/app/vite.config.json @@ -2,7 +2,7 @@ "tasks": { "deploy": { "command": "deploy-script --prod", - "cacheable": false, + "cache": false, "dependsOn": ["@test/app#build", "@test/app#test", "@test/utils#lint"] } } diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/packages/core/package.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/core/package.json similarity index 100% rename from crates/vite_task/fixtures/explicit-deps-workspace/packages/core/package.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/core/package.json diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/packages/core/vite-task.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/core/vite.config.json similarity index 81% rename from crates/vite_task/fixtures/explicit-deps-workspace/packages/core/vite-task.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/core/vite.config.json index 3961abe0..1e27bd96 100644 --- a/crates/vite_task/fixtures/explicit-deps-workspace/packages/core/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/core/vite.config.json @@ -2,7 +2,7 @@ "tasks": { "lint": { "command": "eslint src", - "cacheable": true, + "cache": true, "dependsOn": ["@test/core#clean"] } } diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/packages/utils/package.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/utils/package.json similarity index 100% rename from crates/vite_task/fixtures/explicit-deps-workspace/packages/utils/package.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/utils/package.json diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/packages/utils/vite-task.json b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/utils/vite.config.json similarity index 84% rename from crates/vite_task/fixtures/explicit-deps-workspace/packages/utils/vite-task.json rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/utils/vite.config.json index ce6c551e..f974f9b0 100644 --- a/crates/vite_task/fixtures/explicit-deps-workspace/packages/utils/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/packages/utils/vite.config.json @@ -2,7 +2,7 @@ "tasks": { "lint": { "command": "eslint src", - "cacheable": true, + "cache": true, "dependsOn": ["@test/core#build", "@test/utils#build"] } } diff --git a/crates/vite_task/fixtures/explicit-deps-workspace/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/explicit-deps-workspace/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/explicit-deps-workspace/pnpm-workspace.yaml diff --git a/crates/vite_task/fixtures/fingerprint-ignore-test/README.md b/crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/README.md similarity index 100% rename from crates/vite_task/fixtures/fingerprint-ignore-test/README.md rename to crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/README.md diff --git a/crates/vite_task/fixtures/fingerprint-ignore-test/package.json b/crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/package.json similarity index 100% rename from crates/vite_task/fixtures/fingerprint-ignore-test/package.json rename to crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/package.json diff --git a/crates/vite_task/fixtures/fingerprint-ignore-test/vite-task.json b/crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/vite.config.json similarity index 63% rename from crates/vite_task/fixtures/fingerprint-ignore-test/vite-task.json rename to crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/vite.config.json index 17892d36..effd7a42 100644 --- a/crates/vite_task/fixtures/fingerprint-ignore-test/vite-task.json +++ b/crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test/vite.config.json @@ -2,12 +2,7 @@ "tasks": { "create-files": { "command": "mkdir -p node_modules/pkg-a && echo '{\"name\":\"pkg-a\"}' > node_modules/pkg-a/package.json && echo 'content' > node_modules/pkg-a/index.js && mkdir -p dist && echo 'output' > dist/bundle.js", - "cacheable": true, - "fingerprintIgnores": [ - "node_modules/**/*", - "!node_modules/**/package.json", - "dist/**/*" - ] + "cache": true } } } diff --git a/crates/vite_task/fixtures/recursive-topological-workspace/apps/web/package.json b/crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/apps/web/package.json similarity index 100% rename from crates/vite_task/fixtures/recursive-topological-workspace/apps/web/package.json rename to crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/apps/web/package.json diff --git a/crates/vite_task/fixtures/recursive-topological-workspace/package.json b/crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/package.json similarity index 100% rename from crates/vite_task/fixtures/recursive-topological-workspace/package.json rename to crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/package.json diff --git a/crates/vite_task/fixtures/recursive-topological-workspace/packages/app/package.json b/crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/packages/app/package.json similarity index 100% rename from crates/vite_task/fixtures/recursive-topological-workspace/packages/app/package.json rename to crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/packages/app/package.json diff --git a/crates/vite_task/fixtures/recursive-topological-workspace/packages/core/package.json b/crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/packages/core/package.json similarity index 100% rename from crates/vite_task/fixtures/recursive-topological-workspace/packages/core/package.json rename to crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/packages/core/package.json diff --git a/crates/vite_task/fixtures/recursive-topological-workspace/packages/utils/package.json b/crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/packages/utils/package.json similarity index 100% rename from crates/vite_task/fixtures/recursive-topological-workspace/packages/utils/package.json rename to crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/packages/utils/package.json diff --git a/crates/vite_task/fixtures/recursive-topological-workspace/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/recursive-topological-workspace/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/recursive-topological-workspace/pnpm-workspace.yaml diff --git a/crates/vite_task/fixtures/transitive-dependency-workspace/package.json b/crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/package.json similarity index 100% rename from crates/vite_task/fixtures/transitive-dependency-workspace/package.json rename to crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/package.json diff --git a/crates/vite_task/fixtures/transitive-dependency-workspace/packages/a/package.json b/crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/packages/a/package.json similarity index 100% rename from crates/vite_task/fixtures/transitive-dependency-workspace/packages/a/package.json rename to crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/packages/a/package.json diff --git a/crates/vite_task/fixtures/transitive-dependency-workspace/packages/b/package.json b/crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/packages/b/package.json similarity index 100% rename from crates/vite_task/fixtures/transitive-dependency-workspace/packages/b/package.json rename to crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/packages/b/package.json diff --git a/crates/vite_task/fixtures/transitive-dependency-workspace/packages/c/package.json b/crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/packages/c/package.json similarity index 100% rename from crates/vite_task/fixtures/transitive-dependency-workspace/packages/c/package.json rename to crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/packages/c/package.json diff --git a/crates/vite_task/fixtures/transitive-dependency-workspace/pnpm-workspace.yaml b/crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/pnpm-workspace.yaml similarity index 100% rename from crates/vite_task/fixtures/transitive-dependency-workspace/pnpm-workspace.yaml rename to crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace/pnpm-workspace.yaml diff --git a/crates/vite_task_graph/tests/snapshots.rs b/crates/vite_task_graph/tests/snapshots.rs new file mode 100644 index 00000000..48e0c037 --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots.rs @@ -0,0 +1,43 @@ +use std::path::Path; + +use copy_dir::copy_dir; +use tokio::runtime::Runtime; +use vite_path::AbsolutePath; +use vite_task_graph::loader::JsonUserConfigLoader; +use vite_workspace::find_workspace_root; + +fn run_case(runtime: &Runtime, tmpdir: &AbsolutePath, case_path: &Path) { + let case_name = case_path.file_name().unwrap().to_str().unwrap(); + if case_name.starts_with(".") { + return; // skip hidden files like .DS_Store + } + + // Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case. + let case_stage_path = tmpdir.join(case_name); + copy_dir(case_path, &case_stage_path).unwrap(); + + let workspace_root = find_workspace_root(&case_stage_path).unwrap(); + + assert_eq!( + &case_stage_path, workspace_root.path, + "folder '{}' should be a workspace root", + case_name + ); + + runtime.block_on(async { + let task_graph = + vite_task_graph::TaskGraph::load(workspace_root, JsonUserConfigLoader::default()) + .await + .expect(&format!("Failed to load task graph for case {case_name}")); + let task_graph_snapshot = task_graph.snapshot(&case_stage_path); + insta::assert_json_snapshot!("task graph", task_graph_snapshot); + }); +} + +#[test] +fn test_snapshots() { + let tokio_runtime = Runtime::new().unwrap(); + let tmp_dir = tempfile::tempdir().unwrap(); + let tmp_dir_path = AbsolutePath::new(tmp_dir.path()).unwrap(); + insta::glob!("fixtures/*", |case_path| run_case(&tokio_runtime, tmp_dir_path, case_path)); +} diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@cache-sharing.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@cache-sharing.snap new file mode 100644 index 00000000..684053b5 --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@cache-sharing.snap @@ -0,0 +1,34 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/cache-sharing +--- +[ + { + "id": { + "package_dir": "", + "task_name": "a" + }, + "command": "echo a", + "cwd": "", + "depends_on": [] + }, + { + "id": { + "package_dir": "", + "task_name": "b" + }, + "command": "echo a && echo b", + "cwd": "", + "depends_on": [] + }, + { + "id": { + "package_dir": "", + "task_name": "c" + }, + "command": "echo a && echo b && echo c", + "cwd": "", + "depends_on": [] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@comprehensive-task-graph.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@comprehensive-task-graph.snap new file mode 100644 index 00000000..81726275 --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@comprehensive-task-graph.snap @@ -0,0 +1,214 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/comprehensive-task-graph +--- +[ + { + "id": { + "package_dir": "packages/api", + "task_name": "build" + }, + "command": "echo Generate schemas && echo Compile TypeScript && echo Bundle API && echo Copy assets", + "cwd": "packages/api", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/api", + "task_name": "dev" + }, + "command": "echo Watch mode && echo Start dev server", + "cwd": "packages/api", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/api", + "task_name": "start" + }, + "command": "echo Starting API server", + "cwd": "packages/api", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/api", + "task_name": "test" + }, + "command": "echo Testing API", + "cwd": "packages/api", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "build" + }, + "command": "echo Clean dist && echo Build client && echo Build server && echo Generate manifest && echo Optimize assets", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "deploy" + }, + "command": "echo Validate && echo Upload && echo Verify", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "dev" + }, + "command": "echo Running dev server", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "preview" + }, + "command": "echo Preview build", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "test" + }, + "command": "echo Unit tests && echo Integration tests", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/config", + "task_name": "build" + }, + "command": "echo Building config", + "cwd": "packages/config", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/config", + "task_name": "validate" + }, + "command": "echo Validating config", + "cwd": "packages/config", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/pkg#special", + "task_name": "build" + }, + "command": "echo Building package with hash", + "cwd": "packages/pkg#special", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/pkg#special", + "task_name": "test" + }, + "command": "echo Testing package with hash", + "cwd": "packages/pkg#special", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/shared", + "task_name": "build" + }, + "command": "echo Cleaning && echo Compiling shared && echo Generating types", + "cwd": "packages/shared", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/shared", + "task_name": "lint" + }, + "command": "echo Linting shared", + "cwd": "packages/shared", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/shared", + "task_name": "test" + }, + "command": "echo Setting up test env && echo Running tests && echo Cleanup", + "cwd": "packages/shared", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/shared", + "task_name": "typecheck" + }, + "command": "echo Type checking shared", + "cwd": "packages/shared", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/tools", + "task_name": "generate" + }, + "command": "echo Generating tools", + "cwd": "packages/tools", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/tools", + "task_name": "validate" + }, + "command": "echo Validating", + "cwd": "packages/tools", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/ui", + "task_name": "build" + }, + "command": "echo Compile styles && echo Build components && echo Generate types", + "cwd": "packages/ui", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/ui", + "task_name": "lint" + }, + "command": "echo Linting UI", + "cwd": "packages/ui", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/ui", + "task_name": "storybook" + }, + "command": "echo Running storybook", + "cwd": "packages/ui", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/ui", + "task_name": "test" + }, + "command": "echo Testing UI", + "cwd": "packages/ui", + "depends_on": [] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@conflict-test.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@conflict-test.snap new file mode 100644 index 00000000..ad54a4dd --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@conflict-test.snap @@ -0,0 +1,42 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/conflict-test +--- +[ + { + "id": { + "package_dir": "packages/scope-a", + "task_name": "b#c" + }, + "command": "echo Task b#c in scope-a", + "cwd": "packages/scope-a", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/scope-a-b", + "task_name": "c" + }, + "command": "echo Task c in scope-a#b", + "cwd": "packages/scope-a-b", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/test-package", + "task_name": "test" + }, + "command": "echo Testing", + "cwd": "packages/test-package", + "depends_on": [ + [ + { + "package_dir": "packages/scope-a-b", + "task_name": "c" + }, + "Explicit" + ] + ] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@empty-package-test.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@empty-package-test.snap new file mode 100644 index 00000000..1e072f5b --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@empty-package-test.snap @@ -0,0 +1,126 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/empty-package-test +--- +[ + { + "id": { + "package_dir": "packages/another-empty", + "task_name": "build" + }, + "command": "echo 'Building another-empty package'", + "cwd": "packages/another-empty", + "depends_on": [ + [ + { + "package_dir": "packages/another-empty", + "task_name": "lint" + }, + "Explicit" + ], + [ + { + "package_dir": "packages/normal-package", + "task_name": "test" + }, + "Explicit" + ] + ] + }, + { + "id": { + "package_dir": "packages/another-empty", + "task_name": "deploy" + }, + "command": "echo 'Deploying another-empty package'", + "cwd": "packages/another-empty", + "depends_on": [ + [ + { + "package_dir": "packages/another-empty", + "task_name": "build" + }, + "Explicit" + ], + [ + { + "package_dir": "packages/another-empty", + "task_name": "test" + }, + "Explicit" + ] + ] + }, + { + "id": { + "package_dir": "packages/another-empty", + "task_name": "lint" + }, + "command": "echo 'Linting another-empty package'", + "cwd": "packages/another-empty", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/another-empty", + "task_name": "test" + }, + "command": "echo 'Testing another-empty package'", + "cwd": "packages/another-empty", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/empty-name", + "task_name": "build" + }, + "command": "echo 'Building empty-name package'", + "cwd": "packages/empty-name", + "depends_on": [ + [ + { + "package_dir": "packages/empty-name", + "task_name": "test" + }, + "Explicit" + ] + ] + }, + { + "id": { + "package_dir": "packages/empty-name", + "task_name": "lint" + }, + "command": "echo 'Linting empty-name package'", + "cwd": "packages/empty-name", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/empty-name", + "task_name": "test" + }, + "command": "echo 'Testing empty-name package'", + "cwd": "packages/empty-name", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/normal-package", + "task_name": "build" + }, + "command": "echo 'Building normal-package'", + "cwd": "packages/normal-package", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/normal-package", + "task_name": "test" + }, + "command": "echo 'Testing normal-package'", + "cwd": "packages/normal-package", + "depends_on": [] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@explicit-deps-workspace.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@explicit-deps-workspace.snap new file mode 100644 index 00000000..12da1ba8 --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@explicit-deps-workspace.snap @@ -0,0 +1,151 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/explicit-deps-workspace +--- +[ + { + "id": { + "package_dir": "packages/app", + "task_name": "build" + }, + "command": "echo 'Building @test/app'", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "deploy" + }, + "command": "deploy-script --prod", + "cwd": "packages/app", + "depends_on": [ + [ + { + "package_dir": "packages/app", + "task_name": "build" + }, + "Explicit" + ], + [ + { + "package_dir": "packages/app", + "task_name": "test" + }, + "Explicit" + ], + [ + { + "package_dir": "packages/utils", + "task_name": "lint" + }, + "Explicit" + ] + ] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "start" + }, + "command": "echo 'Starting @test/app'", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "test" + }, + "command": "echo 'Testing @test/app'", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/core", + "task_name": "build" + }, + "command": "echo 'Building @test/core'", + "cwd": "packages/core", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/core", + "task_name": "clean" + }, + "command": "echo 'Cleaning @test/core'", + "cwd": "packages/core", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/core", + "task_name": "lint" + }, + "command": "eslint src", + "cwd": "packages/core", + "depends_on": [ + [ + { + "package_dir": "packages/core", + "task_name": "clean" + }, + "Explicit" + ] + ] + }, + { + "id": { + "package_dir": "packages/core", + "task_name": "test" + }, + "command": "echo 'Testing @test/core'", + "cwd": "packages/core", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/utils", + "task_name": "build" + }, + "command": "echo 'Building @test/utils'", + "cwd": "packages/utils", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/utils", + "task_name": "lint" + }, + "command": "eslint src", + "cwd": "packages/utils", + "depends_on": [ + [ + { + "package_dir": "packages/core", + "task_name": "build" + }, + "Explicit" + ], + [ + { + "package_dir": "packages/utils", + "task_name": "build" + }, + "Explicit" + ] + ] + }, + { + "id": { + "package_dir": "packages/utils", + "task_name": "test" + }, + "command": "echo 'Testing @test/utils'", + "cwd": "packages/utils", + "depends_on": [] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@fingerprint-ignore-test.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@fingerprint-ignore-test.snap new file mode 100644 index 00000000..6e6714cd --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@fingerprint-ignore-test.snap @@ -0,0 +1,16 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/fingerprint-ignore-test +--- +[ + { + "id": { + "package_dir": "", + "task_name": "create-files" + }, + "command": "mkdir -p node_modules/pkg-a && echo '{\"name\":\"pkg-a\"}' > node_modules/pkg-a/package.json && echo 'content' > node_modules/pkg-a/index.js && mkdir -p dist && echo 'output' > dist/bundle.js", + "cwd": "", + "depends_on": [] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@recursive-topological-workspace.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@recursive-topological-workspace.snap new file mode 100644 index 00000000..c10c59b8 --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@recursive-topological-workspace.snap @@ -0,0 +1,79 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/recursive-topological-workspace +--- +[ + { + "id": { + "package_dir": "apps/web", + "task_name": "build" + }, + "command": "echo 'Building @test/web'", + "cwd": "apps/web", + "depends_on": [] + }, + { + "id": { + "package_dir": "apps/web", + "task_name": "dev" + }, + "command": "echo 'Running @test/web in dev mode'", + "cwd": "apps/web", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "build" + }, + "command": "echo 'Building @test/app'", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/app", + "task_name": "test" + }, + "command": "echo 'Testing @test/app'", + "cwd": "packages/app", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/core", + "task_name": "build" + }, + "command": "echo 'Building @test/core'", + "cwd": "packages/core", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/core", + "task_name": "test" + }, + "command": "echo 'Testing @test/core'", + "cwd": "packages/core", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/utils", + "task_name": "build" + }, + "command": "echo 'Preparing @test/utils' && echo 'Building @test/utils' && echo 'Done @test/utils'", + "cwd": "packages/utils", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/utils", + "task_name": "test" + }, + "command": "echo 'Testing @test/utils'", + "cwd": "packages/utils", + "depends_on": [] + } +] diff --git a/crates/vite_task_graph/tests/snapshots/snapshots__task graph@transitive-dependency-workspace.snap b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@transitive-dependency-workspace.snap new file mode 100644 index 00000000..623fd5de --- /dev/null +++ b/crates/vite_task_graph/tests/snapshots/snapshots__task graph@transitive-dependency-workspace.snap @@ -0,0 +1,25 @@ +--- +source: crates/vite_task_graph/tests/snapshots.rs +expression: task_graph_snapshot +input_file: crates/vite_task_graph/tests/fixtures/transitive-dependency-workspace +--- +[ + { + "id": { + "package_dir": "packages/a", + "task_name": "build" + }, + "command": "echo Building A", + "cwd": "packages/a", + "depends_on": [] + }, + { + "id": { + "package_dir": "packages/c", + "task_name": "build" + }, + "command": "echo Building C", + "cwd": "packages/c", + "depends_on": [] + } +] diff --git a/crates/vite_workspace/src/lib.rs b/crates/vite_workspace/src/lib.rs index 51d1cb6e..89a895c7 100644 --- a/crates/vite_workspace/src/lib.rs +++ b/crates/vite_workspace/src/lib.rs @@ -160,11 +160,19 @@ impl PackageGraphBuilder { } } -pub fn get_package_graph( +/// Discover the workspace from cwd and load the package graph. +pub fn discover_package_graph( cwd: impl AsRef, ) -> Result, Error> { - let mut graph_builder = PackageGraphBuilder::default(); let workspace_root = find_workspace_root(cwd.as_ref())?; + load_package_graph(&workspace_root) +} + +/// Load the package graph from a discovered workspace. +pub fn load_package_graph( + workspace_root: &WorkspaceRoot<'_>, +) -> Result, Error> { + let mut graph_builder = PackageGraphBuilder::default(); let workspaces = match &workspace_root.workspace_file { WorkspaceFile::PnpmWorkspaceYaml(file) => { let workspace: PnpmWorkspace = serde_yml::from_reader(file)?; @@ -242,7 +250,7 @@ mod tests { }); fs::write(temp_dir_path.join("package.json"), package_json.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have exactly 1 node (the single package) assert_eq!(graph.node_count(), 1); @@ -292,7 +300,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/pkg-b/package.json"), pkg_b.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have 3 nodes: root + pkg-a + pkg-b assert_eq!(graph.node_count(), 3); @@ -341,7 +349,7 @@ mod tests { fs::write(temp_dir_path.join("packages/excluded-test/package.json"), excluded.to_string()) .unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have the included package let mut found_included = false; @@ -396,7 +404,7 @@ mod tests { fs::write(temp_dir_path.join("packages/excluded/a/package.json"), excluded.to_string()) .unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have the included package let mut found_included = false; @@ -459,7 +467,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/pkg-c/package.json"), pkg_c.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have correct edge types let mut found_normal_dep = false; @@ -510,7 +518,7 @@ mod tests { fs::write(temp_dir_path.join("packages/pkg-2/package.json"), pkg_2.to_string()).unwrap(); // Should return an error for duplicate package names - let result = get_package_graph(temp_dir_path); + let result = discover_package_graph(temp_dir_path); assert!(result.is_err()); if let Err(Error::DuplicatedPackageName { name, .. }) = result { @@ -554,7 +562,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/pkg-a/package.json"), pkg_a.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have 2 nodes but no edges (nameless package can't be referenced) assert_eq!(graph.node_count(), 2); @@ -593,7 +601,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/pkg-b/package.json"), pkg_b.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should correctly parse workspace protocol with version let mut found_edge = false; @@ -641,7 +649,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/pkg-b/package.json"), pkg_b.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have 2 nodes and 2 edges (circular) assert_eq!(graph.node_count(), 2); @@ -689,7 +697,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/pkg-a/package.json"), pkg_a.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have both root and pkg-a (root added automatically) assert_eq!(graph.node_count(), 2); @@ -757,7 +765,7 @@ mod tests { }); fs::write(temp_dir_path.join("apps/web/package.json"), web_app.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have 4 nodes: root + shared + ui + web-app assert_eq!(graph.node_count(), 4); @@ -851,7 +859,7 @@ mod tests { }); fs::write(temp_dir_path.join("packages/cli/package.json"), cli_pkg.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Should have 4 nodes: root + core + utils + cli-tool assert_eq!(graph.node_count(), 4); @@ -941,7 +949,7 @@ mod tests { fs::write(temp_dir_path.join("packages/old.backup/package.json"), backup_pkg.to_string()) .unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Check which packages were included let mut packages_found = HashSet::::new(); @@ -993,7 +1001,7 @@ mod tests { }); fs::write(temp_dir_path.join("services/api/package.json"), api_pkg.to_string()).unwrap(); - let graph = get_package_graph(temp_dir_path).unwrap(); + let graph = discover_package_graph(temp_dir_path).unwrap(); // Verify packages assert_eq!(graph.node_count(), 3); // root + database + api diff --git a/crates/vite_workspace/src/package_manager.rs b/crates/vite_workspace/src/package_manager.rs index 2be76d1f..aba5dbb9 100644 --- a/crates/vite_workspace/src/package_manager.rs +++ b/crates/vite_workspace/src/package_manager.rs @@ -61,8 +61,11 @@ pub enum WorkspaceFile { /// If the workspace file is not found, but a package is found, `workspace_file` will be `NonWorkspacePackage` with the `package.json` File. #[derive(Debug)] pub struct WorkspaceRoot<'a> { + /// The absolute path of the workspace root directory. pub path: &'a AbsolutePath, + /// The cwd that the workspace was found from, relative to the workspace root. pub cwd: RelativePathBuf, + /// The workspace file. pub workspace_file: WorkspaceFile, }