diff --git a/Cargo.lock b/Cargo.lock index 58bddf31..03e7c85d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,9 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -579,6 +582,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1437,17 +1446,20 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "insta" -version = "1.44.3" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" +checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c" dependencies = [ "console", "globset", "once_cell", "pest", "pest_derive", + "regex", + "ron", "serde", "similar", + "tempfile", "walkdir", ] @@ -1995,9 +2007,9 @@ checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", "ucd-trie", @@ -2005,9 +2017,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" dependencies = [ "pest", "pest_generator", @@ -2015,9 +2027,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" dependencies = [ "pest", "pest_meta", @@ -2028,9 +2040,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" dependencies = [ "pest", "sha2", @@ -2364,6 +2376,20 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags 2.10.0", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -3059,6 +3085,12 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" @@ -3186,6 +3218,15 @@ dependencies = [ "wax", ] +[[package]] +name = "vite_graph_ser" +version = "0.1.0" +dependencies = [ + "petgraph", + "serde", + "serde_json", +] + [[package]] name = "vite_path" version = "0.1.0" @@ -3231,14 +3272,12 @@ dependencies = [ "bstr", "clap", "compact_str 0.9.0", - "copy_dir", "dashmap", "derive_more", "diff-struct", "fspy", "futures-core", "futures-util", - "insta", "itertools 0.14.0", "nix 0.30.1", "owo-colors", @@ -3253,7 +3292,7 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "tokio", - "toml", + "tokio-stream", "tracing", "twox-hash", "uuid", @@ -3267,6 +3306,32 @@ dependencies = [ "wax", ] +[[package]] +name = "vite_task_bin" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "copy_dir", + "cow-utils", + "insta", + "petgraph", + "regex", + "serde", + "serde_json", + "tempfile", + "tokio", + "toml", + "vite_graph_ser", + "vite_path", + "vite_str", + "vite_task", + "vite_task_graph", + "vite_workspace", + "which", +] + [[package]] name = "vite_task_graph" version = "0.1.0" @@ -3281,6 +3346,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "vec1", + "vite_graph_ser", "vite_path", "vite_str", "vite_workspace", @@ -3292,17 +3358,22 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bincode", "futures-util", "petgraph", + "serde", "sha2", + "shell-escape", "supports-color", "thiserror 2.0.17", "tracing", "vite_glob", + "vite_graph_ser", "vite_path", "vite_shell", "vite_str", "vite_task_graph", + "which", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 814b4921..8c72e9dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ compact_str = "0.9.0" const_format = "0.2.34" constcat = "0.6.1" copy_dir = "0.1.3" +cow-utils = "0.1.3" crossterm = { version = "0.29.0", features = ["event-stream"] } csv-async = { version = "1.3.1", features = ["tokio"] } ctor = "0.6" @@ -91,6 +92,7 @@ rand = "0.9.1" ratatui = "0.29.0" rayon = "1.10.0" ref-cast = "1.0.24" +regex = "1.11.3" rusqlite = "0.37.0" rustc-hash = "2.1.1" seccompiler = { git = "https://github.com/branchseer/seccompiler", branch = "seccomp-action-raw" } @@ -109,6 +111,7 @@ tempfile = "3.14.0" test-log = { version = "0.2.18", features = ["trace"] } thiserror = "2" tokio = "1.48.0" +tokio-stream = "0.1.17" tokio-util = "0.7.17" toml = "0.9.5" tracing = "0.1.43" @@ -119,9 +122,11 @@ twox-hash = "2.1.1" uuid = "1.18.1" vec1 = "1.12.1" vite_glob = { path = "crates/vite_glob" } +vite_graph_ser = { path = "crates/vite_graph_ser" } vite_path = { path = "crates/vite_path" } vite_shell = { path = "crates/vite_shell" } vite_str = { path = "crates/vite_str" } +vite_task = { path = "crates/vite_task" } vite_task_graph = { path = "crates/vite_task_graph" } vite_task_plan = { path = "crates/vite_task_plan" } vite_workspace = { path = "crates/vite_workspace" } diff --git a/crates/vite_graph_ser/Cargo.toml b/crates/vite_graph_ser/Cargo.toml new file mode 100644 index 00000000..d149a13a --- /dev/null +++ b/crates/vite_graph_ser/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "vite_graph_ser" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +petgraph = { workspace = true } +serde = { workspace = true, features = ["derive"] } + +[dev-dependencies] +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/vite_graph_ser/src/lib.rs b/crates/vite_graph_ser/src/lib.rs new file mode 100644 index 00000000..edc4fd5e --- /dev/null +++ b/crates/vite_graph_ser/src/lib.rs @@ -0,0 +1,132 @@ +use petgraph::{ + graph::DiGraph, + visit::{EdgeRef as _, IntoNodeReferences}, +}; +use serde::{Serialize, Serializer}; + +/// Trait for getting a unique key for a node in the graph. +/// This key is used for serializing the graph with `serialize_by_key`. +pub trait GetKey { + type Key<'a>: Serialize + Ord + where + Self: 'a; + fn key(&self) -> Result, String>; +} + +#[derive(Serialize)] +#[serde(bound = "E: Serialize, N: Serialize")] +struct DiGraphNodeItem<'a, N: GetKey, E> { + key: N::Key<'a>, + node: &'a N, + neighbors: Vec<(N::Key<'a>, &'a E)>, +} + +/// A wrapper around `DiGraph` that serializes nodes by their keys. +#[derive(Serialize)] +#[serde(transparent)] +pub struct SerializeByKey<'a, N: GetKey + Serialize, E: Serialize, Ix: petgraph::graph::IndexType>( + #[serde(serialize_with = "serialize_by_key")] pub &'a DiGraph, +); + +/// Serialize a directed graph into a map from node keys to their values and neighbors by keys. +/// +/// Keys in nodes and edges are sorted lexicographically. +/// +/// If there are multiple nodes with the same key, or multiple edges between nodes with the same keys, +/// an error will be returned. +/// +/// This is useful for serializing graphs in a stable and human-readable way. +pub fn serialize_by_key< + N: GetKey + Serialize, + E: Serialize, + Ix: petgraph::graph::IndexType, + S: Serializer, +>( + graph: &DiGraph, + serializer: S, +) -> Result { + let mut items = Vec::>::with_capacity(graph.node_count()); + for (node_idx, node) in graph.node_references() { + let mut neighbors = Vec::<(N::Key<'_>, &E)>::new(); + + for edge in graph.edges(node_idx) { + let target_idx = edge.target(); + let target_node = graph.node_weight(target_idx).unwrap(); + neighbors.push((target_node.key().map_err(serde::ser::Error::custom)?, edge.weight())); + } + neighbors.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + items.push(DiGraphNodeItem { + key: node.key().map_err(serde::ser::Error::custom)?, + node, + neighbors, + }); + } + items.sort_unstable_by(|a, b| a.key.cmp(&b.key)); + items.serialize(serializer) +} + +#[cfg(test)] +mod tests { + use petgraph::graph::DiGraph; + + use super::*; + + #[derive(Debug, Clone, Serialize)] + struct TestNode { + id: &'static str, + value: i32, + } + + impl GetKey for TestNode { + type Key<'a> + = &'a str + where + Self: 'a; + + fn key(&self) -> Result, String> { + Ok(self.id) + } + } + + #[derive(Serialize)] + struct GraphWrapper { + #[serde(serialize_with = "serialize_by_key")] + graph: DiGraph, + } + + #[test] + fn test_serialize_graph_happy_path() { + let mut graph = DiGraph::::new(); + let a = graph.add_node(TestNode { id: "a", value: 1 }); + let b = graph.add_node(TestNode { id: "b", value: 2 }); + let c = graph.add_node(TestNode { id: "c", value: 3 }); + + graph.add_edge(a, b, "a->b"); + graph.add_edge(a, c, "a->c"); + graph.add_edge(b, c, "b->c"); + + let json = serde_json::to_value(GraphWrapper { graph }).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "graph": [ + { + "key": "a", + "node": {"id": "a", "value": 1}, + "neighbors": [["b", "a->b"], ["c", "a->c"]] + }, + { + "key": "b", + "node": {"id": "b", "value": 2}, + "neighbors": [["c", "b->c"]] + }, + { + "key": "c", + "node": {"id": "c", "value": 3}, + "neighbors": [] + } + ] + }) + ); + } +} diff --git a/crates/vite_path/src/absolute/mod.rs b/crates/vite_path/src/absolute/mod.rs index 928327cc..909a2d52 100644 --- a/crates/vite_path/src/absolute/mod.rs +++ b/crates/vite_path/src/absolute/mod.rs @@ -3,7 +3,7 @@ pub mod redaction; use std::{ ffi::OsStr, - fmt::Display, + fmt::{Debug, Display}, hash::Hash, ops::Deref, path::{Path, PathBuf}, @@ -16,7 +16,7 @@ use serde::Serialize; use crate::relative::{FromPathError, InvalidPathDataError, RelativePathBuf}; /// A path that is guaranteed to be absolute -#[derive(RefCastCustom, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(RefCastCustom, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct AbsolutePath(Path); impl AsRef for AbsolutePath { @@ -25,30 +25,27 @@ impl AsRef for AbsolutePath { } } +impl Debug for AbsolutePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug_tuple = f.debug_tuple("AbsolutePath"); + #[cfg(feature = "absolute-redaction")] + if let Some(redacted_path) = self.try_redact().unwrap() { + debug_tuple.field(&redacted_path); + return debug_tuple.finish(); + } + debug_tuple.field(&&self.0); + debug_tuple.finish() + } +} + impl Serialize for AbsolutePath { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { #[cfg(feature = "absolute-redaction")] - { - use redaction::REDACTION_PREFIX; - - if let Some(redaction_prefix) = REDACTION_PREFIX - .with(|redaction_prefix| redaction_prefix.borrow().as_ref().map(Arc::clone)) - { - match self.strip_prefix(redaction_prefix) { - Ok(Some(stripped_path)) => return stripped_path.serialize(serializer), - Err(strip_error) => { - return Err(serde::ser::Error::custom(format!( - "Failed to redact absolute path '{}': {}", - self.as_path().display(), - strip_error - ))); - } - Ok(None) => { /* continue to serialize full path */ } - } - } + if let Some(redacted_path) = self.try_redact().map_err(serde::ser::Error::custom)? { + return serializer.serialize_str(&redacted_path); } self.as_path().serialize(serializer) } @@ -94,6 +91,30 @@ impl AbsolutePath { if path.is_absolute() { Some(unsafe { Self::assume_absolute(path) }) } else { None } } + #[cfg(feature = "absolute-redaction")] + fn try_redact(&self) -> Result, String> { + use redaction::REDACTION_PREFIX; + + if let Some(redaction_prefix) = REDACTION_PREFIX + .with(|redaction_prefix| redaction_prefix.borrow().as_ref().map(Arc::clone)) + { + match self.strip_prefix(redaction_prefix) { + Ok(Some(stripped_path)) => { + return Ok(Some(format!("/{}", stripped_path.as_str()))); + } + Err(strip_error) => { + return Err(format!( + "Failed to redact absolute path '{}': {}", + self.as_path().display(), + strip_error + )); + } + Ok(None) => { /* continue to serialize full path */ } + } + } + Ok(None) + } + #[ref_cast_custom] pub(crate) unsafe fn assume_absolute(abs_path: &Path) -> &Self; @@ -188,7 +209,7 @@ impl AsRef for AbsolutePath { } /// An owned path buf that is guaranteed to be absolute -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct AbsolutePathBuf(PathBuf); impl From for Arc { diff --git a/crates/vite_task/Cargo.toml b/crates/vite_task/Cargo.toml index 25b22fd0..501965ea 100644 --- a/crates/vite_task/Cargo.toml +++ b/crates/vite_task/Cargo.toml @@ -19,7 +19,7 @@ bstr = { workspace = true } clap = { workspace = true, features = ["derive"] } compact_str = { workspace = true, features = ["serde"] } dashmap = { workspace = true } -derive_more = { workspace = true } +derive_more = { workspace = true, features = ["from"] } diff-struct = { workspace = true } fspy = { workspace = true } futures-core = { workspace = true } @@ -36,7 +36,8 @@ shell-escape = { workspace = true } supports-color = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] } +tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros", "sync"] } +tokio-stream = { workspace = true } tracing = { workspace = true } twox-hash = { workspace = true } uuid = { workspace = true, features = ["v4"] } @@ -51,10 +52,3 @@ wax = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true } - -[dev-dependencies] -copy_dir = { workspace = true } -insta = { workspace = true, features = ["glob", "json", "redactions"] } -tempfile = { workspace = true } -toml = { workspace = true } -vite_path = { workspace = true, features = ["absolute-redaction"] } diff --git a/crates/vite_task/src/bin/vite.rs b/crates/vite_task/src/bin/vite.rs deleted file mode 100644 index 42acd1ac..00000000 --- a/crates/vite_task/src/bin/vite.rs +++ /dev/null @@ -1,13 +0,0 @@ -use clap::Parser; -use vite_str::Str; -use vite_task::CLIArgs; - -#[derive(Debug, Parser)] -enum CustomTaskSubCommand { - /// oxlint - Lint { args: Vec }, -} - -fn main() { - let _subcommand = CLIArgs::::parse(); -} diff --git a/crates/vite_task/src/cli/mod.rs b/crates/vite_task/src/cli/mod.rs index 62b5736f..72b6afaa 100644 --- a/crates/vite_task/src/cli/mod.rs +++ b/crates/vite_task/src/cli/mod.rs @@ -1,25 +1,70 @@ -use std::sync::Arc; +use std::{ffi::OsStr, sync::Arc}; -use clap::Subcommand; +use clap::{Parser, Subcommand}; use vite_path::AbsolutePath; use vite_str::Str; use vite_task_graph::{TaskSpecifier, query::TaskQueryKind}; use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest}; -#[derive(Debug, clap::Parser)] -pub enum CLIArgs { - /// subcommands provided by vite task - #[command(flatten)] - ViteTaskSubCommand(ViteTaskSubCommand), +/// Represents the CLI arguments handled by vite-task, including both built-in and custom subcommands. +#[derive(Debug)] +pub struct TaskCLIArgs { + pub(crate) original: Arc<[Str]>, + pub(crate) parsed: ParsedTaskCLIArgs, +} +pub enum CLIArgs { + /// vite-task's own built-in subcommands + Task(TaskCLIArgs), /// custom subcommands provided by vite+ - #[command(flatten)] - Custom(CustomSubCommand), + NonTask(NonTaskSubcommand), +} + +impl + CLIArgs +{ + /// Get the original CLI arguments + pub fn try_parse_from( + args: impl Iterator>, + ) -> Result { + #[derive(Debug, clap::Parser)] + enum ParsedCLIArgs { + /// subcommands handled by vite task + #[command(flatten)] + Task(ParsedTaskCLIArgs), + + /// subcommands that are not handled by vite task + #[command(flatten)] + NonTask(NonTaskSubcommand), + } + + let args = args.map(|arg| Str::from(arg.as_ref())).collect::>(); + let parsed_cli_args = ParsedCLIArgs::::try_parse_from( + args.iter().map(|s| OsStr::new(s.as_str())), + )?; + + Ok(match parsed_cli_args { + ParsedCLIArgs::Task(parsed_task_cli_args) => { + Self::Task(TaskCLIArgs { original: args, parsed: parsed_task_cli_args }) + } + ParsedCLIArgs::NonTask(non_task_subcommand) => Self::NonTask(non_task_subcommand), + }) + } +} + +#[derive(Debug, Parser)] +pub(crate) enum ParsedTaskCLIArgs { + /// subcommands provided by vite task, like `run` + #[clap(flatten)] + BuiltIn(BuiltInCommand), + /// custom subcommands provided by vite+, like `lint` + #[clap(flatten)] + Custom(CustomSubcommand), } /// vite task CLI subcommands #[derive(Debug, Subcommand)] -pub enum ViteTaskSubCommand { +pub(crate) enum BuiltInCommand { /// Run tasks Run { /// `packageName#taskName` or `taskName`. @@ -38,7 +83,7 @@ pub enum ViteTaskSubCommand { ignore_depends_on: bool, /// Additional arguments to pass to the tasks - #[clap(trailing_var_arg = true)] + #[clap(trailing_var_arg = true, allow_hyphen_values = true)] additional_args: Vec, }, } @@ -52,7 +97,7 @@ pub enum CLITaskQueryError { PackageNameSpecifiedWithRecursive { package_name: Str, task_name: Str }, } -impl ViteTaskSubCommand { +impl BuiltInCommand { /// Convert to `TaskQuery`, or return an error if invalid. pub fn into_plan_request( self, diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index 88eb2f20..3845e7fb 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -14,10 +14,12 @@ mod ui; // Public exports for vite-plus-cli to use pub use cache::TaskCache; -pub use cli::CLIArgs; +pub use cli::{CLIArgs, TaskCLIArgs}; pub use config::{ResolvedTask, Workspace}; pub use error::Error; pub use execute::{CURRENT_EXECUTION_ID, EXECUTION_SUMMARY_DIR}; pub use schedule::{ExecutionPlan, ExecutionStatus, ExecutionSummary}; -pub use session::{Session, SessionCallbacks}; +pub use session::{Session, SessionCallbacks, TaskSynthesizer}; pub use types::ResolveCommandResult; +pub use vite_task_graph::loader; +pub use vite_task_plan::plan_request; diff --git a/crates/vite_task/src/session.rs b/crates/vite_task/src/session.rs deleted file mode 100644 index 207547a5..00000000 --- a/crates/vite_task/src/session.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::{ffi::OsStr, fmt::Debug, sync::Arc}; - -use clap::Parser; -use vite_path::AbsolutePath; -use vite_str::Str; -use vite_task_graph::{IndexedTaskGraph, TaskGraph, TaskGraphLoadError, loader::UserConfigLoader}; -use vite_task_plan::{ - ExecutionPlan, TaskGraphLoader, TaskPlanErrorKind, - plan_request::{PlanRequest, SyntheticPlanRequest}, -}; -use vite_workspace::{WorkspaceRoot, find_workspace_root}; - -use crate::{CLIArgs, collections::HashMap}; - -#[derive(Debug)] -enum LazyTaskGraph<'a> { - Uninitialized { workspace_root: WorkspaceRoot, config_loader: &'a dyn UserConfigLoader }, - Initialized(IndexedTaskGraph), -} - -#[async_trait::async_trait(?Send)] -impl TaskGraphLoader for LazyTaskGraph<'_> { - async fn load_task_graph( - &mut self, - ) -> Result<&vite_task_graph::IndexedTaskGraph, TaskGraphLoadError> { - Ok(match self { - Self::Uninitialized { workspace_root, config_loader } => { - let graph = IndexedTaskGraph::load(workspace_root, *config_loader).await?; - *self = Self::Initialized(graph); - match self { - Self::Initialized(graph) => &*graph, - _ => unreachable!(), - } - } - Self::Initialized(graph) => &*graph, - }) - } -} - -pub struct SessionCallbacks<'a, CustomSubCommand> { - task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), - user_config_loader: &'a mut (dyn UserConfigLoader + 'a), -} - -#[async_trait::async_trait(?Send)] -pub trait TaskSynthesizer: Debug { - fn should_synthesize_for_program(&self, program: &str) -> bool; - async fn synthesize_task( - &mut self, - subcommand: CustomSubCommand, - cwd: &Arc, - ) -> anyhow::Result; -} - -#[derive(derive_more::Debug)] -#[debug(bound())] // Avoid requiring CustomSubCommand: Debug -struct PlanRequestParser<'a, CustomSubCommand> { - task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), -} - -impl PlanRequestParser<'_, CustomSubCommand> { - async fn get_plan_request_from_cli_args( - &mut self, - cli_args: CLIArgs, - cwd: &Arc, - ) -> anyhow::Result { - match cli_args { - CLIArgs::ViteTaskSubCommand(vite_task_subcommand) => { - Ok(vite_task_subcommand.into_plan_request(cwd)?) - } - CLIArgs::Custom(custom_subcommand) => { - let synthetic_plan_request = - self.task_synthesizer.synthesize_task(custom_subcommand, cwd).await?; - Ok(PlanRequest::Synthetic(synthetic_plan_request)) - } - } - } -} - -#[async_trait::async_trait(?Send)] -impl vite_task_plan::PlanRequestParser - for PlanRequestParser<'_, CustomSubCommand> -{ - async fn get_plan_request( - &mut self, - program: &str, - args: &[Str], - cwd: &Arc, - ) -> anyhow::Result> { - if !self.task_synthesizer.should_synthesize_for_program(program) { - return Ok(None); - } - let cli_args = CLIArgs::::try_parse_from( - std::iter::once(program).chain(args.iter().map(Str::as_str)), - )?; - Ok(Some(self.get_plan_request_from_cli_args(cli_args, cwd).await?)) - } -} - -/// Represents a vite task session for planning and executing tasks. A process typically has one session. -/// -/// A session manages task graph loading internally and provides non-consuming methods to plan and/or execute tasks (allows multiple plans/executions per session). -pub struct Session<'a, CustomSubCommand> { - workspace_path: Arc, - /// A session doesn't necessarily load the task graph immediately. - /// The task graph is loaded on-demand and cached for future use. - lazy_task_graph: LazyTaskGraph<'a>, - - envs: HashMap, Arc>, - cwd: Arc, - - plan_request_parser: PlanRequestParser<'a, CustomSubCommand>, -} - -impl<'a, CustomSubCommand> Session<'a, CustomSubCommand> { - /// Initialize a session with real environment variables and cwd - pub fn init(callbacks: SessionCallbacks<'a, CustomSubCommand>) -> anyhow::Result { - let envs = std::env::vars_os() - .map(|(k, v)| (Arc::::from(k.as_os_str()), Arc::::from(v.as_os_str()))) - .collect(); - Self::init_with(envs, vite_path::current_dir()?.into(), callbacks) - } - - /// Initialize a session with custom cwd, environment variables. Useful for testing. - pub fn init_with( - envs: HashMap, Arc>, - cwd: Arc, - callbacks: SessionCallbacks<'a, CustomSubCommand>, - ) -> anyhow::Result { - let (workspace_root, _) = find_workspace_root(&cwd)?; - Ok(Self { - workspace_path: Arc::clone(&workspace_root.path), - lazy_task_graph: LazyTaskGraph::Uninitialized { - workspace_root, - config_loader: callbacks.user_config_loader, - }, - envs, - cwd, - plan_request_parser: PlanRequestParser { task_synthesizer: callbacks.task_synthesizer }, - }) - } - - pub fn task_graph(&self) -> Option<&TaskGraph> { - match &self.lazy_task_graph { - LazyTaskGraph::Initialized(graph) => Some(graph.task_graph()), - _ => None, - } - } -} - -impl<'a, CustomSubCommand: clap::Subcommand> Session<'a, CustomSubCommand> { - pub async fn plan( - &mut self, - cli_args: CLIArgs, - ) -> Result { - let plan_request = self - .plan_request_parser - .get_plan_request_from_cli_args(cli_args, &self.cwd) - .await - .map_err(|error| { - TaskPlanErrorKind::ParsePlanRequestError { error }.with_empty_call_stack() - })?; - ExecutionPlan::plan( - plan_request, - &self.workspace_path, - &self.cwd, - &self.envs, - &mut self.plan_request_parser, - &mut self.lazy_task_graph, - ) - .await - } -} diff --git a/crates/vite_task/src/session/cache/fingerprint.rs b/crates/vite_task/src/session/cache/fingerprint.rs new file mode 100644 index 00000000..c691ce59 --- /dev/null +++ b/crates/vite_task/src/session/cache/fingerprint.rs @@ -0,0 +1,70 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use bincode::{Decode, Encode}; +use diff::Diff; +use serde::{Deserialize, Serialize}; +use vite_path::RelativePathBuf; +use vite_str::Str; + +/// Fingerprint for command execution that affects caching. +/// +/// # Environment Variable Impact on Cache +/// +/// The `envs_without_pass_through` field is crucial for cache correctness: +/// - Only includes envs explicitly declared in the task's `envs` array +/// - Does NOT include pass-through envs (PATH, CI, etc.) +/// - These envs become part of the cache key +/// +/// When a task runs: +/// 1. All envs (including pass-through) are available to the process +/// 2. Only declared envs affect the cache key +/// 3. If a declared env changes value, cache will miss +/// 4. If a pass-through env changes, cache will still hit +/// +/// For built-in tasks (lint, build, etc): +/// - The resolver provides envs which become part of the fingerprint +/// - If resolver provides different envs between runs, cache breaks +/// - Each built-in task type must have unique task name to avoid cache collision +/// +/// # Fingerprint Ignores Impact on Cache +/// +/// The `fingerprint_ignores` field controls which files are tracked in `PostRunFingerprint`: +/// - Changes to this config must invalidate the cache +/// - Vec maintains insertion order (pattern order matters for last-match-wins semantics) +/// - Even though ignore patterns only affect `PostRunFingerprint`, the config itself is part of the cache key +#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)] +#[diff(attr(#[derive(Debug)]))] +pub struct SpawnFingerprint { + pub cwd: RelativePathBuf, + pub command_fingerprint: CommandFingerprint, + /// Environment variables that affect caching (excludes pass-through envs) + pub fingerprinted_envs: BTreeMap, // using BTreeMap to have a stable order in cache db + + /// even though value changes to `pass_through_envs` shouldn't invalidate the cache, + /// The names should still be fingerprinted so that the cache can be invalidated if the `pass_through_envs` config changes + pub pass_through_envs: BTreeSet, // using BTreeSet to have a stable order in cache db + + /// Glob patterns for fingerprint filtering. Order matters (last match wins). + /// Changes to this config invalidate the cache to ensure correct fingerprint tracking. + pub fingerprint_ignores: Option>, +} + +/// The program fingerprint used in `SpawnFingerprint` +#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)] +#[diff(attr(#[derive(Debug)]))] +enum ProgramFingerprint { + /// If the program is outside the workspace, fingerprint by its name only (like `node`, `npm`, etc) + OutsideWorkspace { program_name: Str }, + + /// If the program is inside the workspace, fingerprint by its path relative to the workspace root + InsideWorkspace { relative_path: RelativePathBuf }, +} + +#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)] +#[diff(attr(#[derive(Debug)]))] +enum CommandFingerprint { + /// A program with args to be executed directly + Program { program_fingerprint: ProgramFingerprint, args: Vec }, + /// A script to be executed by os shell (sh or cmd) + ShellScript { script: Str, extra_args: Vec }, +} diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs new file mode 100644 index 00000000..4b78eb22 --- /dev/null +++ b/crates/vite_task/src/session/cache/mod.rs @@ -0,0 +1,309 @@ +pub mod fingerprint; + +use std::{fmt::Display, io::Write, sync::Arc, time::Duration}; + +// use bincode::config::{Configuration, standard}; +use bincode::{Decode, Encode, decode_from_slice, encode_to_vec}; +use rusqlite::{Connection, OptionalExtension as _, config::DbConfig}; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; +use vite_str::Str; + +use crate::{ + Error, + config::{CommandFingerprint, ResolvedTask}, + execute::{ExecutedTask, StdOutput}, + fingerprint::{PostRunFingerprint, PostRunFingerprintMismatch}, + fs::FileSystem, +}; + +/// Command cache value, for validating post-run fingerprint after the command fingerprint is matched, +/// and replaying the std outputs if validated. +#[derive(Debug, Encode, Decode, Serialize)] +pub struct CommandCacheValue { + pub post_run_fingerprint: PostRunFingerprint, + pub std_outputs: Arc<[StdOutput]>, + pub duration: Duration, +} + +impl CommandCacheValue { + pub fn create( + executed_task: ExecutedTask, + fs: &impl FileSystem, + base_dir: &AbsolutePath, + fingerprint_ignores: Option<&[Str]>, + ) -> Result { + let post_run_fingerprint = + PostRunFingerprint::create(&executed_task, fs, base_dir, fingerprint_ignores)?; + Ok(Self { + post_run_fingerprint, + std_outputs: executed_task.std_outputs, + duration: executed_task.duration, + }) + } +} + +#[derive(Debug)] +pub struct ExecutionCache { + conn: Mutex, +} + +/// Cache key to associate an execution with a custom vite-task subcommand (like `vite lint`) directly run by the user. +#[derive(Debug, Encode, Decode, Serialize)] +pub struct DirectExecutionCacheKey { + /// The args after the program name, including the subcommand name. (like `["lint", "--fix"]` for `vite lint --fix`) + pub args_without_program: Arc<[Str]>, + + /// The cwd where the `vite [custom subcommand] ...` is run. + /// + /// This is not necessarily the actual cwd that the synthesized task runs in. + pub plan_cwd: RelativePathBuf, +} + +/// Cache key to associate an execution with a user-defined task (like `"lint-task": "vite lint" in package.json scripts`). +#[derive(Debug, Encode, Decode, Serialize)] +pub struct UserTaskExecutionCacheKey { + pub task_name: Str, + pub package_path: RelativePathBuf, + pub and_item_index: usize, +} + +/// Key to identify an execution. +#[derive(Debug, Encode, Decode, Serialize)] +pub enum ExecutionCacheKey { + /// This execution is directly from a custom vite-task subcommand (like `vite lint`). + /// + /// Note that this is only for the case where the subcommand is directly typed in the cli, + /// not from a task script (like `"lint-task": "vite lint"`), which is covered by the `Task` variant. + Direct(DirectExecutionCacheKey), + + /// This execution is from a task script. + UserTask(UserTaskExecutionCacheKey), +} + +const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard(); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum CacheMiss { + NotFound, + FingerprintMismatch(FingerprintMismatch), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum FingerprintMismatch { + /// Found the cache entry of the same task run, but the command fingerprint mismatches + /// this happens when the command itself or an env changes. + CommandFingerprintMismatch(CommandFingerprint), + /// Found the cache entry with the same command fingerprint, but the post-run fingerprint mismatches + PostRunFingerprintMismatch(PostRunFingerprintMismatch), +} + +impl Display for FingerprintMismatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CommandFingerprintMismatch(diff) => { + // TODO: improve the display of command fingerprint diff + write!(f, "Command fingerprint changed: {diff:?}") + } + Self::PostRunFingerprintMismatch(diff) => Display::fmt(diff, f), + } + } +} + +impl ExecutionCache { + pub fn load_from_path(cache_path: AbsolutePathBuf) -> Result { + let path: &AbsolutePath = cache_path.as_ref(); + tracing::info!("Creating task cache directory at {:?}", path); + std::fs::create_dir_all(path)?; + + let db_path = path.join("cache.db"); + let conn = Connection::open(db_path.as_path())?; + conn.execute_batch("PRAGMA journal_mode=WAL;")?; + loop { + let user_version: u32 = conn.query_one("PRAGMA user_version", (), |row| row.get(0))?; + match user_version { + 0 => { + // fresh new db + conn.execute( + "CREATE TABLE command_cache (key BLOB PRIMARY KEY, value BLOB);", + (), + )?; + conn.execute( + "CREATE TABLE taskrun_to_command (key BLOB PRIMARY KEY, value BLOB);", + (), + )?; + // Bump to version 3 to invalidate cache entries due to a change in the serialized cache key content + // (addition of the `fingerprint_ignores` field). No schema change was made. + conn.execute("PRAGMA user_version = 3", ())?; + } + 1..=2 => { + // old internal db version. reset + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?; + conn.execute("VACUUM", ())?; + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?; + } + 3 => break, // current version + 4.. => return Err(Error::UnrecognizedDbVersion(user_version)), + } + } + Ok(Self { conn: Mutex::new(conn) }) + } + + #[tracing::instrument] + pub async fn save(self) -> Result<(), Error> { + // do some cleanup in the future + Ok(()) + } + + pub async fn update( + &self, + resolved_task: &ResolvedTask, + cached_task: CommandCacheValue, + ) -> Result<(), Error> { + let task_run_key: ExecutionCacheKey = todo!(); + let command_fingerprint = &resolved_task.resolved_command.fingerprint; + self.upsert_command_cache(command_fingerprint, &cached_task).await?; + self.upsert_taskrun_to_command(&task_run_key, command_fingerprint).await?; + Ok(()) + } + + /// Tries to get the task cache if the fingerprint matches, otherwise returns why the cache misses + pub async fn try_hit( + &self, + task: &ResolvedTask, + fs: &impl FileSystem, + base_dir: &AbsolutePath, + ) -> Result, Error> { + let task_run_key: ExecutionCacheKey = todo!(); + let command_fingerprint = &task.resolved_command.fingerprint; + // Try to directly find the command cache by command fingerprint first, ignoring the task run key + if let Some(cache_value) = + self.get_command_cache_by_command_fingerprint(command_fingerprint).await? + { + if let Some(post_run_fingerprint_mismatch) = + cache_value.post_run_fingerprint.validate(fs, base_dir)? + { + // Found the command cache with the same command fingerprint, but the post-run fingerprint mismatches + Ok(Err(CacheMiss::FingerprintMismatch( + FingerprintMismatch::PostRunFingerprintMismatch(post_run_fingerprint_mismatch), + ))) + } else { + // Associate the task run key to the command fingerprint if not already, + // so that next time we can find it and report command fingerprint mismatch + self.upsert_taskrun_to_command(&task_run_key, command_fingerprint).await?; + Ok(Ok(cache_value)) + } + } else if let Some(command_fingerprint_in_cache) = + self.get_command_fingerprint_by_task_run_key(&task_run_key).await? + { + // No command cache found with the current command fingerprint, + // but found a command fingerprint associated with the same task run key, + // meaning the command or env has changed since last run + Ok(Err(CacheMiss::FingerprintMismatch( + FingerprintMismatch::CommandFingerprintMismatch(command_fingerprint_in_cache), + ))) + } else { + Ok(Err(CacheMiss::NotFound)) + } + } +} + +// basic database operations +impl ExecutionCache { + async fn get_key_by_value>( + &self, + table: &str, + key: &K, + ) -> Result, Error> { + let conn = self.conn.lock().await; + let mut select_stmt = + conn.prepare_cached(&format!("SELECT value FROM {table} WHERE key=?"))?; + let key_blob = encode_to_vec(key, BINCODE_CONFIG)?; + let Some(value_blob) = + select_stmt.query_row::, _, _>([key_blob], |row| row.get(0)).optional()? + else { + return Ok(None); + }; + let (value, _) = decode_from_slice::(&value_blob, BINCODE_CONFIG)?; + Ok(Some(value)) + } + + async fn get_command_cache_by_command_fingerprint( + &self, + command_fingerprint: &CommandFingerprint, + ) -> Result, Error> { + self.get_key_by_value("command_cache", command_fingerprint).await + } + + async fn get_command_fingerprint_by_task_run_key( + &self, + task_run_key: &ExecutionCacheKey, + ) -> Result, Error> { + self.get_key_by_value("taskrun_to_command", task_run_key).await + } + + async fn upsert( + &self, + table: &str, + key: &K, + value: &V, + ) -> Result<(), Error> { + let conn = self.conn.lock().await; + let key_blob = encode_to_vec(key, BINCODE_CONFIG)?; + let value_blob = encode_to_vec(value, BINCODE_CONFIG)?; + let mut update_stmt = conn.prepare_cached(&format!( + "INSERT INTO {table} (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value=?2" + ))?; + update_stmt.execute([key_blob, value_blob])?; + Ok(()) + } + + async fn upsert_command_cache( + &self, + command_fingerprint: &CommandFingerprint, + cached_task: &CommandCacheValue, + ) -> Result<(), Error> { + self.upsert("command_cache", command_fingerprint, cached_task).await + } + + async fn upsert_taskrun_to_command( + &self, + task_run_key: &ExecutionCacheKey, + command_fingerprint: &CommandFingerprint, + ) -> Result<(), Error> { + self.upsert("taskrun_to_command", task_run_key, command_fingerprint).await + } + + async fn list_table + Serialize, V: Decode<()> + Serialize>( + &self, + table: &str, + out: &mut impl Write, + ) -> Result<(), Error> { + let conn = self.conn.lock().await; + let mut select_stmt = conn.prepare_cached(&format!("SELECT key, value FROM {table}"))?; + let mut rows = select_stmt.query([])?; + while let Some(row) = rows.next()? { + let key_blob: Vec = row.get(0)?; + let value_blob: Vec = row.get(1)?; + let (key, _) = decode_from_slice::(&key_blob, BINCODE_CONFIG)?; + let (value, _) = decode_from_slice::(&value_blob, BINCODE_CONFIG)?; + writeln!( + out, + "{} => {}", + serde_json::to_string_pretty(&key)?, + serde_json::to_string_pretty(&value)? + )?; + } + Ok(()) + } + + pub async fn list(&self, mut out: impl Write) -> Result<(), Error> { + out.write_all(b"------- taskrun_to_command -------\n")?; + self.list_table::("taskrun_to_command", &mut out) + .await?; + out.write_all(b"------- command_cache -------\n")?; + self.list_table::("command_cache", &mut out).await?; + Ok(()) + } +} diff --git a/crates/vite_task/src/session/event.rs b/crates/vite_task/src/session/event.rs new file mode 100644 index 00000000..4cf6108f --- /dev/null +++ b/crates/vite_task/src/session/event.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; + +use bstr::BString; +use vite_path::AbsolutePath; +use vite_str::Str; + +#[derive(Clone, Debug)] +pub struct ExecutionStartInfo { + /// None if the execution is not associated with a specific task, but directly synthesized from CLI args, like `vite lint`/`vite exec ...` + pub task_display_name: Option, + pub command: Str, + pub cwd: Arc, +} + +#[derive(Debug)] +pub enum OutputKind { + Stdout, + Stderr, +} + +#[derive(Debug)] +pub enum CacheDisabledReason { + InProcessExecution, +} + +#[derive(Debug)] +pub enum CacheStatus { + Disabled(CacheDisabledReason), + Miss, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExecutionId(u32); + +impl ExecutionId { + pub(crate) fn zero() -> Self { + Self(0) + } + + pub(crate) fn next(&self) -> Self { + Self(self.0.checked_add(1).expect("ExecutionId overflow")) + } +} + +pub struct ExecutionStartedEvent { + pub execution_id: ExecutionId, + pub display: ExecutionStartInfo, +} + +pub struct ExecutionOutputEvent { + pub execution_id: ExecutionId, + pub kind: OutputKind, + pub content: BString, +} + +#[derive(Debug)] +pub struct ExecutionEvent { + pub execution_id: ExecutionId, + pub kind: ExecutionEventKind, +} + +#[derive(Debug)] +pub enum ExecutionEventKind { + Start(ExecutionStartInfo), + Output { kind: OutputKind, content: BString }, + Finish { status: Option, cache_status: CacheStatus }, +} diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs new file mode 100644 index 00000000..5569d23b --- /dev/null +++ b/crates/vite_task/src/session/execute/mod.rs @@ -0,0 +1,249 @@ +use std::{borrow::Cow, path::Path, sync::Arc}; + +use futures_util::FutureExt; +use petgraph::{ + algo::{Cycle, toposort}, + graph::DiGraph, +}; +use vite_path::{AbsolutePath, RelativePathBuf, relative::InvalidPathDataError}; +use vite_str::Str; +use vite_task_graph::{IndexedTaskGraph, TaskNodeIndex, display::TaskDisplay}; +use vite_task_plan::{ + ExecutionItem, ExecutionItemKind, ExecutionPlan, LeafExecutionKind, SpawnExecution, + TaskExecution, + execution_graph::{ExecutionIx, ExecutionNodeIndex}, +}; + +use super::{ + cache::{ExecutionCache, ExecutionCacheKey}, + event::{ + CacheDisabledReason, CacheStatus, ExecutionEvent, ExecutionEventKind, ExecutionId, + ExecutionStartInfo, OutputKind, + }, +}; +use crate::{ + Session, + session::cache::{DirectExecutionCacheKey, UserTaskExecutionCacheKey}, +}; + +#[derive(Debug, thiserror::Error)] +pub enum PathError { + #[error("Path {path:?} is outside of the workspace {workspace_path:?}")] + PathOutsideWorkspace { path: Arc, workspace_path: Arc }, + #[error("Path {path:?} contains characters that make it non-portable")] + NonPortableRelativePath { + path: Arc, + #[source] + error: InvalidPathDataError, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ExecuteError { + #[error("Cycle dependencies detected: {0:?}")] + CycleDependencies(Cycle), + + #[error(transparent)] + PathError(#[from] PathError), +} + +struct ExecutionContext<'a> { + indexed_task_graph: Option<&'a IndexedTaskGraph>, + event_handler: &'a mut (dyn FnMut(ExecutionEvent) + 'a), + current_execution_id: ExecutionId, + cache: &'a ExecutionCache, + /// All relative paths in cache are relative to this base path + cache_base_path: &'a Arc, +} + +/// The origin of the current execution item, either directly from CLI args, or from a task in the task graph +enum ExecutionOrigin<'a> { + CLIArgs { + args_without_program: &'a Arc<[Str]>, + cwd: &'a Arc, + }, + UserTask { + task_display: TaskDisplay, + item: &'a ExecutionItem, + item_index: usize, + item_count: usize, + }, +} + +impl ExecutionContext<'_> { + fn strip_prefix_for_cache( + &self, + path: &Arc, + ) -> Result { + match path.strip_prefix(&*self.cache_base_path) { + Ok(Some(rel_path)) => Ok(rel_path), + Ok(None) => Err(PathError::PathOutsideWorkspace { + path: Arc::clone(path), + workspace_path: Arc::clone(self.cache_base_path), + }), + Err(err) => Err(PathError::NonPortableRelativePath { + path: err.stripped_path.into(), + error: err.invalid_path_data_error, + }), + } + } + + async fn execute_item_kind( + &mut self, + item_kind: &ExecutionItemKind, + origin: ExecutionOrigin<'_>, + ) -> Result<(), ExecuteError> { + match item_kind { + ExecutionItemKind::Expanded(graph) => { + // clone for reversing edges and removing nodes + let mut graph: DiGraph<&TaskExecution, (), ExecutionIx> = + graph.map(|_, task_execution| task_execution, |_, ()| ()); + + // To be consistent with the package graph in vite_package_manager and the dependency graph definition in Wikipedia + // https://en.wikipedia.org/wiki/Dependency_graph, we construct the graph with edges from dependents to dependencies + // e.g. A -> B means A depends on B + // + // For execution we need to reverse the edges first before topological sorting, + // so that tasks without dependencies are executed first + graph.reverse(); // Run tasks without dependencies first + + // Always use topological sort to ensure the correct order of execution + // or the task dependencies declaration is meaningless + let node_indices = match toposort(&graph, None) { + Ok(ok) => ok, + Err(err) => return Err(ExecuteError::CycleDependencies(err)), + }; + + let ordered_executions = + node_indices.into_iter().map(|id| graph.remove_node(id).unwrap()); + for task_execution in ordered_executions { + let indexed_task_graph = self.indexed_task_graph.unwrap(); + let task_display = task_execution.task_display.clone(); + for (item_index, item) in task_execution.items.iter().enumerate() { + self.execute_item_kind( + &item.kind, + ExecutionOrigin::UserTask { + item, + item_index, + item_count: task_execution.items.len(), + task_display: task_execution.task_display.clone(), + }, + ) + .boxed_local() + .await?; + } + } + } + ExecutionItemKind::Leaf(leaf_execution_kind) => { + self.execute_leaf(leaf_execution_kind, origin).await?; + } + } + Ok(()) + } + + async fn execute_leaf( + &mut self, + leaf_execution_kind: &LeafExecutionKind, + origin: ExecutionOrigin<'_>, + ) -> Result<(), ExecuteError> { + let start_info: ExecutionStartInfo = todo!(); + + let execution_id = self.current_execution_id; + self.current_execution_id = self.current_execution_id.next(); + (self.event_handler)(ExecutionEvent { + execution_id, + kind: ExecutionEventKind::Start(start_info), + }); + + match leaf_execution_kind { + LeafExecutionKind::InProcess(in_process_execution) => { + let execution_output = in_process_execution.execute().await; + (self.event_handler)(ExecutionEvent { + execution_id, + kind: ExecutionEventKind::Output { + kind: OutputKind::Stdout, + content: execution_output.stdout.into(), + }, + }); + (self.event_handler)(ExecutionEvent { + execution_id, + kind: ExecutionEventKind::Finish { + status: Some(0), + cache_status: CacheStatus::Disabled( + CacheDisabledReason::InProcessExecution, + ), + }, + }); + } + LeafExecutionKind::Spawn(spawn_execution) => { + self.execute_spawn(execution_id, origin, spawn_execution).await?; + } + } + Ok(()) + } + + async fn execute_spawn( + &mut self, + execution_id: ExecutionId, + origin: ExecutionOrigin<'_>, + spawn_execution: &SpawnExecution, + ) -> Result<(), ExecuteError> { + let execution_cache_key: ExecutionCacheKey = todo!(); + + // let mut cmd = match &spawn_execution.command_kind { + // SpawnCommandKind::Program { program_path, args } => { + // let mut cmd = fspy::Command::new(program_path.as_path()); + // cmd.args(args.iter().map(|arg| arg.as_str())); + // cmd + // } + // SpawnCommandKind::ShellScript { script, args } => { + // let mut cmd = if cfg!(windows) { + // let mut cmd = fspy::Command::new("cmd.exe"); + // // https://github.com/nodejs/node/blob/dbd24b165128affb7468ca42f69edaf7e0d85a9a/lib/child_process.js#L633 + // cmd.args(["/d", "/s", "/c"]); + // cmd + // } else { + // let mut cmd = fspy::Command::new("sh"); + // cmd.args(["-c"]); + // cmd + // }; + + // let mut script = script.clone(); + // for arg in args.iter() { + // script.push(' '); + // script.push_str(shell_escape::escape(arg.as_str().into()).as_ref()); + // } + // cmd.arg(script); + // cmd + // } + // }; + // cmd.envs(spawn_execution.all_envs.iter()).current_dir(&*spawn_execution.cwd); + todo!() + } +} + +impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { + pub async fn execute( + &self, + plan: ExecutionPlan, + event_handler: &mut (dyn FnMut(ExecutionEvent) + '_), + ) -> Result<(), ExecuteError> { + let mut execution_context = ExecutionContext { + indexed_task_graph: self.lazy_task_graph.try_get(), + event_handler, + current_execution_id: ExecutionId::zero(), + cache: &self.cache, + cache_base_path: &self.workspace_path, + }; + Ok(()) + // execution_context + // .execute_item_kind( + // plan.plan.root_node(), + // ExecutionOrigin::CLIArgs { + // args_without_program: &plan.cli_args_without_program, + // cwd: &plan.cwd, + // }, + // ) + // .await + } +} diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs new file mode 100644 index 00000000..ae5d496a --- /dev/null +++ b/crates/vite_task/src/session/mod.rs @@ -0,0 +1,254 @@ +mod cache; +mod event; +mod execute; + +use std::{ffi::OsStr, fmt::Debug, sync::Arc}; + +use cache::ExecutionCache; +use clap::{Parser, Subcommand}; +use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_str::Str; +use vite_task_graph::{IndexedTaskGraph, TaskGraph, TaskGraphLoadError, loader::UserConfigLoader}; +use vite_task_plan::{ + ExecutionPlan, TaskGraphLoader, TaskPlanErrorKind, get_path_env, + plan_request::{PlanRequest, SyntheticPlanRequest}, +}; +use vite_workspace::{WorkspaceRoot, find_workspace_root}; + +use crate::{ + cli::{ParsedTaskCLIArgs, TaskCLIArgs}, + collections::HashMap, +}; + +#[derive(Debug)] +enum LazyTaskGraph<'a> { + Uninitialized { workspace_root: WorkspaceRoot, config_loader: &'a dyn UserConfigLoader }, + Initialized(IndexedTaskGraph), +} + +impl LazyTaskGraph<'_> { + fn try_get(&self) -> Option<&IndexedTaskGraph> { + match self { + Self::Initialized(graph) => Some(graph), + _ => None, + } + } +} + +#[async_trait::async_trait(?Send)] +impl TaskGraphLoader for LazyTaskGraph<'_> { + async fn load_task_graph( + &mut self, + ) -> Result<&vite_task_graph::IndexedTaskGraph, TaskGraphLoadError> { + Ok(match self { + Self::Uninitialized { workspace_root, config_loader } => { + let graph = IndexedTaskGraph::load(workspace_root, *config_loader).await?; + *self = Self::Initialized(graph); + match self { + Self::Initialized(graph) => &*graph, + _ => unreachable!(), + } + } + Self::Initialized(graph) => &*graph, + }) + } +} + +pub struct SessionCallbacks<'a, CustomSubcommand> { + pub task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), + pub user_config_loader: &'a mut (dyn UserConfigLoader + 'a), +} + +#[async_trait::async_trait(?Send)] +pub trait TaskSynthesizer: Debug { + fn should_synthesize_for_program(&self, program: &str) -> bool; + async fn synthesize_task( + &mut self, + subcommand: CustomSubcommand, + path_env: Option<&Arc>, + cwd: &Arc, + ) -> anyhow::Result; +} + +#[derive(derive_more::Debug)] +#[debug(bound())] // Avoid requiring CustomSubcommand: Debug +struct PlanRequestParser<'a, CustomSubcommand> { + task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), +} + +impl PlanRequestParser<'_, CustomSubcommand> { + async fn get_plan_request_from_cli_args( + &mut self, + cli_args: ParsedTaskCLIArgs, + path_env: Option<&Arc>, + cwd: &Arc, + ) -> anyhow::Result { + match cli_args { + ParsedTaskCLIArgs::BuiltIn(vite_task_subcommand) => { + Ok(vite_task_subcommand.into_plan_request(cwd)?) + } + ParsedTaskCLIArgs::Custom(custom_subcommand) => { + let synthetic_plan_request = + self.task_synthesizer.synthesize_task(custom_subcommand, path_env, cwd).await?; + Ok(PlanRequest::Synthetic(synthetic_plan_request)) + } + } + } +} + +#[async_trait::async_trait(?Send)] +impl vite_task_plan::PlanRequestParser + for PlanRequestParser<'_, CustomSubcommand> +{ + async fn get_plan_request( + &mut self, + program: &str, + args: &[Str], + path_env: Option<&Arc>, + cwd: &Arc, + ) -> anyhow::Result> { + Ok( + if self.task_synthesizer.should_synthesize_for_program(program) + && let Some(subcommand) = args.first() + && ParsedTaskCLIArgs::::has_subcommand(subcommand) + { + let cli_args = ParsedTaskCLIArgs::::try_parse_from( + std::iter::once(program).chain(args.iter().map(Str::as_str)), + )?; + Some(self.get_plan_request_from_cli_args(cli_args, path_env, cwd).await?) + } else { + None + }, + ) + } +} + +/// Represents a vite task session for planning and executing tasks. A process typically has one session. +/// +/// A session manages task graph loading internally and provides non-consuming methods to plan and/or execute tasks (allows multiple plans/executions per session). +pub struct Session<'a, CustomSubcommand> { + workspace_path: Arc, + /// A session doesn't necessarily load the task graph immediately. + /// The task graph is loaded on-demand and cached for future use. + lazy_task_graph: LazyTaskGraph<'a>, + + envs: HashMap, Arc>, + cwd: Arc, + + plan_request_parser: PlanRequestParser<'a, CustomSubcommand>, + + cache: ExecutionCache, +} + +fn get_cache_path_of_workspace(workspace_root: &AbsolutePath) -> AbsolutePathBuf { + if let Ok(env_cache_path) = std::env::var("VITE_CACHE_PATH") { + AbsolutePathBuf::new(env_cache_path.into()).expect("Cache path should be absolute") + } else { + workspace_root.join("node_modules/.vite/task-cache") + } +} + +impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { + /// Initialize a session with real environment variables and cwd + pub fn init(callbacks: SessionCallbacks<'a, CustomSubcommand>) -> anyhow::Result { + let envs = std::env::vars_os() + .map(|(k, v)| (Arc::::from(k.as_os_str()), Arc::::from(v.as_os_str()))) + .collect(); + Self::init_with(envs, vite_path::current_dir()?.into(), callbacks) + } + + pub async fn ensure_task_graph_loaded( + &mut self, + ) -> Result<&IndexedTaskGraph, TaskGraphLoadError> { + self.lazy_task_graph.load_task_graph().await + } + + /// Initialize a session with custom cwd, environment variables. Useful for testing. + pub fn init_with( + envs: HashMap, Arc>, + cwd: Arc, + callbacks: SessionCallbacks<'a, CustomSubcommand>, + ) -> anyhow::Result { + let (workspace_root, _) = find_workspace_root(&cwd)?; + let cache_path = get_cache_path_of_workspace(&workspace_root.path); + + if !cache_path.as_path().exists() + && let Some(cache_dir) = cache_path.as_path().parent() + { + tracing::info!("Creating task cache directory at {}", cache_dir.display()); + std::fs::create_dir_all(cache_dir)?; + } + let cache = ExecutionCache::load_from_path(cache_path)?; + Ok(Self { + workspace_path: Arc::clone(&workspace_root.path), + lazy_task_graph: LazyTaskGraph::Uninitialized { + workspace_root, + config_loader: callbacks.user_config_loader, + }, + envs, + cwd, + plan_request_parser: PlanRequestParser { task_synthesizer: callbacks.task_synthesizer }, + cache, + }) + } + + pub fn cache(&self) -> &ExecutionCache { + &self.cache + } + + pub fn task_graph(&self) -> Option<&TaskGraph> { + match &self.lazy_task_graph { + LazyTaskGraph::Initialized(graph) => Some(graph.task_graph()), + _ => None, + } + } +} + +impl<'a, CustomSubcommand: clap::Subcommand> Session<'a, CustomSubcommand> { + async fn plan_synthetic_task( + &mut self, + synthetic_plan_request: SyntheticPlanRequest, + ) -> Result { + let plan = ExecutionPlan::plan( + PlanRequest::Synthetic(synthetic_plan_request), + &self.workspace_path, + &self.cwd, + &self.envs, + &mut self.plan_request_parser, + &mut self.lazy_task_graph, + ) + .await?; + Ok(plan) + } + + pub async fn plan_from_cli( + &mut self, + cwd: Arc, + cli_args: TaskCLIArgs, + ) -> Result { + let path_env = get_path_env(&self.envs); + let plan_request = self + .plan_request_parser + .get_plan_request_from_cli_args(cli_args.parsed, path_env, &cwd) + .await + .map_err(|error| { + TaskPlanErrorKind::ParsePlanRequestError { + error, + program: cli_args.original[0].clone(), + args: cli_args.original.iter().skip(1).cloned().collect(), + cwd: Arc::clone(&cwd), + } + .with_empty_call_stack() + })?; + let plan = ExecutionPlan::plan( + plan_request, + &self.workspace_path, + &cwd, + &self.envs, + &mut self.plan_request_parser, + &mut self.lazy_task_graph, + ) + .await?; + Ok(plan) + } +} diff --git a/crates/vite_task/tests/fixtures/transitive-dependency-workspace/cli-queries.toml b/crates/vite_task/tests/fixtures/transitive-dependency-workspace/cli-queries.toml deleted file mode 100644 index 41491249..00000000 --- a/crates/vite_task/tests/fixtures/transitive-dependency-workspace/cli-queries.toml +++ /dev/null @@ -1,54 +0,0 @@ -[[query]] -name = "simple task by name" -cwd = "packages/a" -args = ["build"] - -[[query]] -name = "under subfolder of package" -cwd = "packages/a/src" -args = ["build"] - -[[query]] -name = "explicit package name under different package" -cwd = "packages/a" -args = ["@test/c#build"] - -[[query]] -name = "explicit package name under non-package cwd" -cwd = "" -args = ["@test/c#build"] - -[[query]] -name = "ambiguous task name" -cwd = "" -args = ["@test/a#build"] - -[[query]] -name = "ignore depends on" -cwd = "packages/a" -args = ["--ignore-depends-on", "build"] - -[[query]] -name = "transitive" -cwd = "packages/a" -args = ["--transitive", "build"] - -[[query]] -name = "transitive in package without the task" -cwd = "packages/a" -args = ["--transitive", "lint"] - -[[query]] -name = "transitive non existent task" -cwd = "packages/a" -args = ["--transitive", "non-existent-task"] - -[[query]] -name = "recursive" -cwd = "" -args = ["--recursive", "build"] - -[[query]] -name = "recursive and transitive" -cwd = "" -args = ["--recursive", "--transitive", "build"] diff --git a/crates/vite_task/tests/snapshots.rs b/crates/vite_task/tests/snapshots.rs deleted file mode 100644 index e003ca7d..00000000 --- a/crates/vite_task/tests/snapshots.rs +++ /dev/null @@ -1,189 +0,0 @@ -use core::panic; -use std::{path::Path, sync::Arc}; - -use clap::Parser; -use copy_dir::copy_dir; -use petgraph::visit::EdgeRef as _; -use tokio::runtime::Runtime; -use vite_path::{AbsolutePath, RelativePathBuf, redaction::redact_absolute_paths}; -use vite_str::Str; -use vite_task_graph::{ - IndexedTaskGraph, TaskDependencyType, TaskNodeIndex, - loader::JsonUserConfigLoader, - query::{TaskExecutionGraph, cli::CLITaskQuery}, -}; -use vite_workspace::find_workspace_root; - -#[derive(serde::Serialize, PartialEq, PartialOrd, Eq, Ord)] -struct TaskIdSnapshot { - package_dir: Arc, - task_name: Str, -} -impl TaskIdSnapshot { - fn new(task_index: TaskNodeIndex, indexed_task_graph: &IndexedTaskGraph) -> Self { - let task_id = &indexed_task_graph.task_graph()[task_index].task_id; - Self { - task_name: task_id.task_name.clone(), - package_dir: Arc::clone(&indexed_task_graph.get_package_path(task_id.package_index)), - } - } -} - -/// Create a stable json representation of the task graph for snapshot testing. -/// -/// All paths are relative to `base_dir`. -fn snapshot_task_graph(indexed_task_graph: &IndexedTaskGraph) -> impl serde::Serialize { - #[derive(serde::Serialize)] - struct TaskNodeSnapshot { - id: TaskIdSnapshot, - command: Str, - cwd: Arc, - depends_on: Vec<(TaskIdSnapshot, TaskDependencyType)>, - } - - let task_graph = indexed_task_graph.task_graph(); - let mut node_snapshots = Vec::::with_capacity(task_graph.node_count()); - for task_index in task_graph.node_indices() { - let task_node = &task_graph[task_index]; - let mut depends_on: Vec<(TaskIdSnapshot, TaskDependencyType)> = task_graph - .edges_directed(task_index, petgraph::Direction::Outgoing) - .map(|edge| (TaskIdSnapshot::new(edge.target(), indexed_task_graph), *edge.weight())) - .collect(); - depends_on.sort_unstable_by(|a, b| a.0.cmp(&b.0)); - node_snapshots.push(TaskNodeSnapshot { - id: TaskIdSnapshot::new(task_index, indexed_task_graph), - command: task_node.resolved_config.command.clone(), - cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), - depends_on, - }); - } - node_snapshots.sort_unstable_by(|a, b| a.id.cmp(&b.id)); - - node_snapshots -} - -/// Create a stable json representation of the task graph for snapshot testing. -/// -/// All paths are relative to `base_dir`. -fn snapshot_execution_graph( - execution_graph: &TaskExecutionGraph, - indexed_task_graph: &IndexedTaskGraph, -) -> impl serde::Serialize { - #[derive(serde::Serialize, PartialEq)] - struct ExecutionNodeSnapshot { - task: TaskIdSnapshot, - deps: Vec, - } - - let mut execution_node_snapshots = Vec::::new(); - for task_index in execution_graph.nodes() { - let mut deps = execution_graph - .neighbors(task_index) - .map(|dep_index| TaskIdSnapshot::new(dep_index, indexed_task_graph)) - .collect::>(); - deps.sort_unstable(); - - execution_node_snapshots.push(ExecutionNodeSnapshot { - task: TaskIdSnapshot::new(task_index, indexed_task_graph), - deps, - }); - } - execution_node_snapshots.sort_unstable_by(|a, b| a.task.cmp(&b.task)); - execution_node_snapshots -} - -#[derive(serde::Deserialize)] -struct CLIQuery { - pub name: Str, - pub args: Vec, - pub cwd: RelativePathBuf, -} - -#[derive(serde::Deserialize, Default)] -struct CLIQueriesFile { - #[serde(rename = "query")] // toml usually uses singular for arrays - pub queries: Vec, -} - -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, _cwd) = find_workspace_root(&case_stage_path).unwrap(); - - assert_eq!( - &case_stage_path, &*workspace_root.path, - "folder '{}' should be a workspace root", - case_name - ); - - let cli_queries_toml_path = case_path.join("cli-queries.toml"); - let cli_queries_file: CLIQueriesFile = match std::fs::read(&cli_queries_toml_path) { - Ok(content) => toml::from_slice(&content).unwrap(), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Default::default(), - Err(err) => panic!("Failed to read cli-queries.toml for case {}: {}", case_name, err), - }; - - runtime.block_on(async { - let _redaction_guard = redact_absolute_paths(&workspace_root.path); - - let indexed_task_graph = vite_task_graph::IndexedTaskGraph::load( - &workspace_root, - &JsonUserConfigLoader::default(), - ) - .await - .expect(&format!("Failed to load task graph for case {case_name}")); - - let task_graph_snapshot = snapshot_task_graph(&indexed_task_graph); - insta::assert_json_snapshot!("task graph", task_graph_snapshot); - - for cli_query in cli_queries_file.queries { - let snapshot_name = format!("query - {}", cli_query.name); - - let cli_task_query = CLITaskQuery::try_parse_from( - std::iter::once("vite-run") // dummy program name - .chain(cli_query.args.iter().map(|s| s.as_str())), - ) - .expect(&format!( - "Failed to parse CLI args for query '{}' in case '{}'", - cli_query.name, case_name - )); - - let cwd: Arc = case_stage_path.join(&cli_query.cwd).into(); - let task_query = match cli_task_query.into_task_query(&cwd) { - Ok(ok) => ok, - Err(err) => { - insta::assert_json_snapshot!(snapshot_name, err); - continue; - } - }; - - let execution_graph = match indexed_task_graph.query_tasks(task_query) { - Ok(ok) => ok, - Err(err) => { - insta::assert_json_snapshot!(snapshot_name, err); - continue; - } - }; - - let execution_graph_snapshot = - snapshot_execution_graph(&execution_graph, &indexed_task_graph); - insta::assert_json_snapshot!(snapshot_name, execution_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/tests/snapshots/snapshots__query - ambiguous task name@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - ambiguous task name@transitive-dependency-workspace.snap deleted file mode 100644 index eafd86a2..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - ambiguous task name@transitive-dependency-workspace.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: err -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -{ - "SpecifierLookupError": { - "specifier": { - "package_name": "@test/a", - "task_name": "build" - }, - "lookup_error": { - "AmbiguousPackageName": { - "package_name": "@test/a", - "package_paths": [ - "packages/a", - "packages/another-a" - ] - } - } - } -} diff --git a/crates/vite_task/tests/snapshots/snapshots__query - explicit package name under different package@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - explicit package name under different package@transitive-dependency-workspace.snap deleted file mode 100644 index 3ff92394..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - explicit package name under different package@transitive-dependency-workspace.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/c", - "task_name": "build" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - explicit package name under non-package cwd@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - explicit package name under non-package cwd@transitive-dependency-workspace.snap deleted file mode 100644 index 3ff92394..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - explicit package name under non-package cwd@transitive-dependency-workspace.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/c", - "task_name": "build" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - ignore depends on@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - ignore depends on@transitive-dependency-workspace.snap deleted file mode 100644 index 5a65e8f5..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - ignore depends on@transitive-dependency-workspace.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/a", - "task_name": "build" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - recursive and transitive@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - recursive and transitive@transitive-dependency-workspace.snap deleted file mode 100644 index c267178d..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - recursive and transitive@transitive-dependency-workspace.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: err -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -"RecursiveTransitiveConflict" diff --git a/crates/vite_task/tests/snapshots/snapshots__query - recursive@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - recursive@transitive-dependency-workspace.snap deleted file mode 100644 index fdc5c1a5..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - recursive@transitive-dependency-workspace.snap +++ /dev/null @@ -1,60 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/a", - "task_name": "build" - }, - "deps": [ - { - "package_dir": "packages/a", - "task_name": "test" - }, - { - "package_dir": "packages/b2", - "task_name": "build" - }, - { - "package_dir": "packages/c", - "task_name": "build" - } - ] - }, - { - "task": { - "package_dir": "packages/a", - "task_name": "test" - }, - "deps": [] - }, - { - "task": { - "package_dir": "packages/another-a", - "task_name": "build" - }, - "deps": [] - }, - { - "task": { - "package_dir": "packages/b2", - "task_name": "build" - }, - "deps": [ - { - "package_dir": "packages/c", - "task_name": "build" - } - ] - }, - { - "task": { - "package_dir": "packages/c", - "task_name": "build" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - simple task by name@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - simple task by name@transitive-dependency-workspace.snap deleted file mode 100644 index 41fc332c..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - simple task by name@transitive-dependency-workspace.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/a", - "task_name": "build" - }, - "deps": [ - { - "package_dir": "packages/a", - "task_name": "test" - } - ] - }, - { - "task": { - "package_dir": "packages/a", - "task_name": "test" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - transitive in package without the task@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - transitive in package without the task@transitive-dependency-workspace.snap deleted file mode 100644 index 95c858b1..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - transitive in package without the task@transitive-dependency-workspace.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/b1", - "task_name": "lint" - }, - "deps": [ - { - "package_dir": "packages/c", - "task_name": "lint" - } - ] - }, - { - "task": { - "package_dir": "packages/c", - "task_name": "lint" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - transitive non existent task@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - transitive non existent task@transitive-dependency-workspace.snap deleted file mode 100644 index 3e61cf81..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - transitive non existent task@transitive-dependency-workspace.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: err -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -{ - "SpecifierLookupError": { - "specifier": { - "package_name": null, - "task_name": "non-existent-task" - }, - "lookup_error": { - "TaskNameNotFound": { - "package_name": "@test/a", - "task_name": "non-existent-task" - } - } - } -} diff --git a/crates/vite_task/tests/snapshots/snapshots__query - transitive@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - transitive@transitive-dependency-workspace.snap deleted file mode 100644 index 826bebad..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - transitive@transitive-dependency-workspace.snap +++ /dev/null @@ -1,53 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/a", - "task_name": "build" - }, - "deps": [ - { - "package_dir": "packages/a", - "task_name": "test" - }, - { - "package_dir": "packages/b2", - "task_name": "build" - }, - { - "package_dir": "packages/c", - "task_name": "build" - } - ] - }, - { - "task": { - "package_dir": "packages/a", - "task_name": "test" - }, - "deps": [] - }, - { - "task": { - "package_dir": "packages/b2", - "task_name": "build" - }, - "deps": [ - { - "package_dir": "packages/c", - "task_name": "build" - } - ] - }, - { - "task": { - "package_dir": "packages/c", - "task_name": "build" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__query - under subfolder of package@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__query - under subfolder of package@transitive-dependency-workspace.snap deleted file mode 100644 index 41fc332c..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__query - under subfolder of package@transitive-dependency-workspace.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: execution_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "task": { - "package_dir": "packages/a", - "task_name": "build" - }, - "deps": [ - { - "package_dir": "packages/a", - "task_name": "test" - } - ] - }, - { - "task": { - "package_dir": "packages/a", - "task_name": "test" - }, - "deps": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__task graph@cache-sharing.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@cache-sharing.snap deleted file mode 100644 index 2f23f391..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@cache-sharing.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/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/tests/snapshots/snapshots__task graph@comprehensive-task-graph.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@comprehensive-task-graph.snap deleted file mode 100644 index d0e44840..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@comprehensive-task-graph.snap +++ /dev/null @@ -1,351 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/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": [ - [ - { - "package_dir": "packages/config", - "task_name": "build" - }, - "Topological" - ], - [ - { - "package_dir": "packages/shared", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/shared", - "task_name": "test" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/api", - "task_name": "build" - }, - "Topological" - ], - [ - { - "package_dir": "packages/pkg#special", - "task_name": "build" - }, - "Topological" - ], - [ - { - "package_dir": "packages/shared", - "task_name": "build" - }, - "Topological" - ], - [ - { - "package_dir": "packages/ui", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/api", - "task_name": "dev" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/api", - "task_name": "test" - }, - "Topological" - ], - [ - { - "package_dir": "packages/pkg#special", - "task_name": "test" - }, - "Topological" - ], - [ - { - "package_dir": "packages/shared", - "task_name": "test" - }, - "Topological" - ], - [ - { - "package_dir": "packages/ui", - "task_name": "test" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/shared", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/pkg#special", - "task_name": "test" - }, - "command": "echo Testing package with hash", - "cwd": "packages/pkg#special", - "depends_on": [ - [ - { - "package_dir": "packages/shared", - "task_name": "test" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/config", - "task_name": "validate" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/ui", - "task_name": "build" - }, - "command": "echo Compile styles && echo Build components && echo Generate types", - "cwd": "packages/ui", - "depends_on": [ - [ - { - "package_dir": "packages/shared", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/ui", - "task_name": "lint" - }, - "command": "echo Linting UI", - "cwd": "packages/ui", - "depends_on": [ - [ - { - "package_dir": "packages/shared", - "task_name": "lint" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/shared", - "task_name": "test" - }, - "Topological" - ] - ] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__task graph@conflict-test.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@conflict-test.snap deleted file mode 100644 index 80e5603f..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@conflict-test.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/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/tests/snapshots/snapshots__task graph@dependency-both-topo-and-explicit.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@dependency-both-topo-and-explicit.snap deleted file mode 100644 index b609a8d2..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@dependency-both-topo-and-explicit.snap +++ /dev/null @@ -1,33 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/tests/fixtures/dependency-both-topo-and-explicit ---- -[ - { - "id": { - "package_dir": "packages/a", - "task_name": "build" - }, - "command": "build a", - "cwd": "packages/a", - "depends_on": [ - [ - { - "package_dir": "packages/b", - "task_name": "build" - }, - "Both" - ] - ] - }, - { - "id": { - "package_dir": "packages/b", - "task_name": "build" - }, - "command": "build b", - "cwd": "packages/b", - "depends_on": [] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__task graph@empty-package-test.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@empty-package-test.snap deleted file mode 100644 index 5508e88e..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@empty-package-test.snap +++ /dev/null @@ -1,141 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/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": "build" - }, - "Topological" - ], - [ - { - "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": [ - [ - { - "package_dir": "packages/normal-package", - "task_name": "test" - }, - "Topological" - ] - ] - }, - { - "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/tests/snapshots/snapshots__task graph@explicit-deps-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@explicit-deps-workspace.snap deleted file mode 100644 index f2e12dd0..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@explicit-deps-workspace.snap +++ /dev/null @@ -1,190 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/tests/fixtures/explicit-deps-workspace ---- -[ - { - "id": { - "package_dir": "packages/app", - "task_name": "build" - }, - "command": "echo 'Building @test/app'", - "cwd": "packages/app", - "depends_on": [ - [ - { - "package_dir": "packages/utils", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/utils", - "task_name": "test" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/core", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "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/core", - "task_name": "lint" - }, - "Topological" - ], - [ - { - "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": [ - [ - { - "package_dir": "packages/core", - "task_name": "test" - }, - "Topological" - ] - ] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__task graph@fingerprint-ignore-test.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@fingerprint-ignore-test.snap deleted file mode 100644 index 67c9f908..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@fingerprint-ignore-test.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/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/tests/snapshots/snapshots__task graph@recursive-topological-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@recursive-topological-workspace.snap deleted file mode 100644 index fef43080..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@recursive-topological-workspace.snap +++ /dev/null @@ -1,126 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/tests/fixtures/recursive-topological-workspace ---- -[ - { - "id": { - "package_dir": "apps/web", - "task_name": "build" - }, - "command": "echo 'Building @test/web'", - "cwd": "apps/web", - "depends_on": [ - [ - { - "package_dir": "packages/app", - "task_name": "build" - }, - "Topological" - ], - [ - { - "package_dir": "packages/core", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/utils", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/app", - "task_name": "test" - }, - "command": "echo 'Testing @test/app'", - "cwd": "packages/app", - "depends_on": [ - [ - { - "package_dir": "packages/utils", - "task_name": "test" - }, - "Topological" - ] - ] - }, - { - "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": [ - [ - { - "package_dir": "packages/core", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/utils", - "task_name": "test" - }, - "command": "echo 'Testing @test/utils'", - "cwd": "packages/utils", - "depends_on": [ - [ - { - "package_dir": "packages/core", - "task_name": "test" - }, - "Topological" - ] - ] - } -] diff --git a/crates/vite_task/tests/snapshots/snapshots__task graph@transitive-dependency-workspace.snap b/crates/vite_task/tests/snapshots/snapshots__task graph@transitive-dependency-workspace.snap deleted file mode 100644 index 98b23222..00000000 --- a/crates/vite_task/tests/snapshots/snapshots__task graph@transitive-dependency-workspace.snap +++ /dev/null @@ -1,108 +0,0 @@ ---- -source: crates/vite_task/tests/snapshots.rs -expression: task_graph_snapshot -input_file: crates/vite_task/tests/fixtures/transitive-dependency-workspace ---- -[ - { - "id": { - "package_dir": "packages/a", - "task_name": "build" - }, - "command": "echo Building A", - "cwd": "packages/a", - "depends_on": [ - [ - { - "package_dir": "packages/a", - "task_name": "test" - }, - "Explicit" - ], - [ - { - "package_dir": "packages/b2", - "task_name": "build" - }, - "Topological" - ], - [ - { - "package_dir": "packages/c", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/a", - "task_name": "test" - }, - "command": "echo test a", - "cwd": "packages/a", - "depends_on": [] - }, - { - "id": { - "package_dir": "packages/another-a", - "task_name": "build" - }, - "command": "echo Building another A", - "cwd": "packages/another-a", - "depends_on": [] - }, - { - "id": { - "package_dir": "packages/b1", - "task_name": "lint" - }, - "command": "echo lint b1", - "cwd": "packages/b1", - "depends_on": [ - [ - { - "package_dir": "packages/c", - "task_name": "lint" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/b2", - "task_name": "build" - }, - "command": "echo build b2", - "cwd": "packages/b2", - "depends_on": [ - [ - { - "package_dir": "packages/c", - "task_name": "build" - }, - "Topological" - ] - ] - }, - { - "id": { - "package_dir": "packages/c", - "task_name": "build" - }, - "command": "echo Building C", - "cwd": "packages/c", - "depends_on": [] - }, - { - "id": { - "package_dir": "packages/c", - "task_name": "lint" - }, - "command": "echo lint c", - "cwd": "packages/c", - "depends_on": [] - } -] diff --git a/crates/vite_task_bin/Cargo.toml b/crates/vite_task_bin/Cargo.toml new file mode 100644 index 00000000..6a377b2a --- /dev/null +++ b/crates/vite_task_bin/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "vite_task_bin" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[[bin]] +name = "vite" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +clap = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["full"] } +vite_path = { workspace = true } +vite_str = { workspace = true } +vite_task = { workspace = true } +which = { workspace = true } + +[dev-dependencies] +copy_dir = { workspace = true } +cow-utils = { workspace = true } +insta = { workspace = true, features = ["glob", "json", "redactions", "filters", "ron"] } +petgraph = { workspace = true } +regex = { workspace = true } +serde = { workspace = true, features = ["derive", "rc"] } +serde_json = { workspace = true } +tempfile = { workspace = true } +toml = { workspace = true } +vite_graph_ser = { workspace = true } +vite_path = { workspace = true, features = ["absolute-redaction"] } +vite_task_graph = { workspace = true } +vite_workspace = { workspace = true } + +[lints] +workspace = true diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs new file mode 100644 index 00000000..ec51c189 --- /dev/null +++ b/crates/vite_task_bin/src/lib.rs @@ -0,0 +1,94 @@ +use std::{ + env::{self, join_paths}, + ffi::OsStr, + iter, + path::PathBuf, + sync::Arc, +}; + +use clap::Subcommand; +use vite_path::{AbsolutePath, current_dir}; +use vite_str::Str; +use vite_task::{CLIArgs, Session, SessionCallbacks, plan_request::SyntheticPlanRequest}; + +/// Theses are the custom subcommands that synthesize tasks for vite-task +#[derive(Debug, Subcommand)] +pub enum CustomTaskSubcommand { + /// oxlint + Lint { + #[clap(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, +} + +// These are the subcommands that is not handled by vite-task +#[derive(Debug, Subcommand)] +pub enum NonTaskSubcommand { + Version, +} + +#[derive(Debug, Default)] +pub struct TaskSynthesizer(()); + +fn find_executable( + path_env: Option<&Arc>, + cwd: &AbsolutePath, + executable: &str, +) -> anyhow::Result> { + let mut paths: Vec = + if let Some(path_env) = path_env { env::split_paths(path_env).collect() } else { vec![] }; + let mut current_cwd_parent = cwd; + loop { + let node_modules_bin = current_cwd_parent.join("node_modules").join(".bin"); + paths.push(node_modules_bin.as_path().to_path_buf()); + if let Some(parent) = current_cwd_parent.parent() { + current_cwd_parent = parent; + } else { + break; + } + } + let executable_path = which::which_in(executable, Some(join_paths(paths)?), cwd)?; + Ok(executable_path.into_os_string().into()) +} + +#[async_trait::async_trait(?Send)] +impl vite_task::TaskSynthesizer for TaskSynthesizer { + fn should_synthesize_for_program(&self, program: &str) -> bool { + program == "vite" + } + + async fn synthesize_task( + &mut self, + subcommand: CustomTaskSubcommand, + path_env: Option<&Arc>, + cwd: &Arc, + ) -> anyhow::Result { + match subcommand { + CustomTaskSubcommand::Lint { args } => { + let direct_execution_cache_key: Arc<[Str]> = + iter::once(Str::from("lint")).chain(args.iter().cloned()).collect(); + Ok(SyntheticPlanRequest { + program: find_executable(path_env, &*cwd, "oxlint")?, + args: args.into(), + task_options: Default::default(), + direct_execution_cache_key, + }) + } + } + } +} + +#[derive(Default)] +pub struct OwnedSessionCallbacks { + task_synthesizer: TaskSynthesizer, + user_config_loader: vite_task::loader::JsonUserConfigLoader, +} + +impl OwnedSessionCallbacks { + pub fn as_callbacks(&mut self) -> SessionCallbacks<'_, CustomTaskSubcommand> { + SessionCallbacks { + task_synthesizer: &mut self.task_synthesizer, + user_config_loader: &mut self.user_config_loader, + } + } +} diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs new file mode 100644 index 00000000..a5ef7028 --- /dev/null +++ b/crates/vite_task_bin/src/main.rs @@ -0,0 +1,35 @@ +use std::{env, sync::Arc}; + +use vite_path::{AbsolutePath, current_dir}; +use vite_task::{CLIArgs, Session, SessionCallbacks}; +use vite_task_bin::{ + CustomTaskSubcommand, NonTaskSubcommand, OwnedSessionCallbacks, TaskSynthesizer, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cwd: Arc = current_dir()?.into(); + // Parse the CLI arguments and see if they are for vite-task or not + let args = match CLIArgs::::try_parse_from(env::args()) + { + Ok(ok) => ok, + Err(err) => { + err.exit(); + } + }; + let task_cli_args = match args { + CLIArgs::Task(task_cli_args) => task_cli_args, + CLIArgs::NonTask(NonTaskSubcommand::Version) => { + // Non-task subcommands are not handled by vite-task's session. + println!("{}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + }; + + let mut owned_callbacks = OwnedSessionCallbacks::default(); + let mut session = Session::init(owned_callbacks.as_callbacks())?; + let plan = session.plan_from_cli(cwd, task_cli_args).await?; + dbg!(plan); + + Ok(()) +} diff --git a/crates/vite_task_bin/test_bins/README.md b/crates/vite_task_bin/test_bins/README.md new file mode 100644 index 00000000..40f338fd --- /dev/null +++ b/crates/vite_task_bin/test_bins/README.md @@ -0,0 +1 @@ +This package contains test binaries used in the tests for vite_task_bin crate. diff --git a/crates/vite_task_bin/test_bins/package.json b/crates/vite_task_bin/test_bins/package.json new file mode 100644 index 00000000..f52bdb2f --- /dev/null +++ b/crates/vite_task_bin/test_bins/package.json @@ -0,0 +1,13 @@ +{ + "name": "vite-task-test-bins", + "type": "module", + "private": true, + "bin": { + "print-file": "./src/print-file.ts", + "json-edit": "./src/json-edit.ts" + }, + "dependencies": { + "oxlint": "catalog:", + "vite-task-test-bins": "link:" + } +} diff --git a/crates/vite_task_bin/test_bins/src/json-edit.ts b/crates/vite_task_bin/test_bins/src/json-edit.ts new file mode 100755 index 00000000..d9593f21 --- /dev/null +++ b/crates/vite_task_bin/test_bins/src/json-edit.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from 'node:fs'; +import { parseArgs } from 'node:util'; + +const { positionals } = parseArgs({ + allowPositionals: true, +}); + +const filename = positionals[0]; +const script = positionals[1]; + +if (!filename || !script) { + console.error('Usage: json-edit