diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b93a693c..3d23ab117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Added** Object-form `dependsOn` entries can now run a task in direct workspace dependency packages selected by package.json fields, e.g. `{ "task": "build", "from": ["dependencies", "devDependencies"] }`. - **Added** First-party support for caching `vite build` with zero cache config, giving Vite projects correct cache hits out of the box ([vitejs/vite#22453](https://github.com/vitejs/vite/pull/22453)). - **Added** [`@voidzero-dev/vite-task-client`](https://npmx.dev/package/@voidzero-dev/vite-task-client), allowing tools to report cache information to Vite Task at runtime so users do not need to configure it manually ([#441](https://github.com/voidzero-dev/vite-task/pull/441), [#454](https://github.com/voidzero-dev/vite-task/pull/454), [#449](https://github.com/voidzero-dev/vite-task/pull/449), [#450](https://github.com/voidzero-dev/vite-task/pull/450), [#458](https://github.com/voidzero-dev/vite-task/pull/458), [#431](https://github.com/voidzero-dev/vite-task/pull/431), [#459](https://github.com/voidzero-dev/vite-task/pull/459)). - **Changed** Cached tasks now restore automatically tracked output files by default; use `output: []` to disable restoration ([#460](https://github.com/voidzero-dev/vite-task/pull/460), [#461](https://github.com/voidzero-dev/vite-task/pull/461)). diff --git a/Cargo.lock b/Cargo.lock index 38769ec58..b0c1967bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4356,6 +4356,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "ts-rs", + "vec1", "vite_graph_ser", "vite_path", "vite_str", diff --git a/crates/vite_task/docs/boolean-flags.md b/crates/vite_task/docs/boolean-flags.md index 2c1062e10..bd8e0fa98 100644 --- a/crates/vite_task/docs/boolean-flags.md +++ b/crates/vite_task/docs/boolean-flags.md @@ -9,7 +9,7 @@ This document describes how boolean flags work in `vp` commands. - `--recursive` / `-r` — Run task in all packages in the workspace - `--transitive` / `-t` — Run task in the current package and its transitive dependencies - `--workspace-root` / `-w` — Run task in the workspace root package -- `--ignore-depends-on` — Skip explicit `dependsOn` dependencies +- `--ignore-depends-on` — Skip `dependsOn` dependencies - `--verbose` / `-v` — Show full detailed summary after execution - `--cache` / `--no-cache` — Force caching on or off for all tasks and scripts @@ -39,7 +39,7 @@ vp run build -t # Run in workspace root vp run build -w -# Skip explicit dependsOn edges +# Skip dependsOn dependencies vp run build --ignore-depends-on # Verbose output diff --git a/crates/vite_task/docs/task-query.md b/crates/vite_task/docs/task-query.md index 0399638a6..20a4069f3 100644 --- a/crates/vite_task/docs/task-query.md +++ b/crates/vite_task/docs/task-query.md @@ -7,16 +7,17 @@ How `vp run` decides which tasks to run and in what order. When `vp` starts, it builds two data structures from the workspace: 1. **Package graph** — which packages depend on which. Built from `package.json` dependency fields. -2. **Task graph** — which tasks exist and their explicit `dependsOn` relationships. Built from `vite-task.json` and `package.json` scripts. +2. **Task graph** — which tasks exist and their string-form `dependsOn` relationships. Built from `vite.config.*` and `package.json` scripts. Both are built once and reused for every query, including nested `vp run` calls inside task scripts. ### What goes into the task graph -The task graph contains a node for every task in every package, and edges only for explicit `dependsOn` declarations: +The task graph contains a node for every task in every package. String-form +`dependsOn` declarations become edges in this graph: ```jsonc -// packages/app/vite-task.json +// packages/app/vite.config.* { "tasks": { "build": { @@ -37,6 +38,8 @@ Task graph: ``` Package dependency ordering (app depends on lib) is NOT stored as edges in the task graph. Why not is explained below. +Object-form `dependsOn` entries are also not stored as global task graph edges; +they are kept on the declaring task and expanded for each query. ## What happens when you run a query @@ -74,7 +77,7 @@ Given the package subgraph and a task name, we build the execution plan: 1. Find which selected packages have the requested task. 2. For packages that don't have it, reconnect their predecessors to their successors (skip-intermediate, explained below). 3. Map the remaining package nodes to task nodes — this gives us topological ordering. -4. Follow explicit `dependsOn` edges outward from these tasks (may pull in tasks from outside the selected packages). +4. Expand `dependsOn` from these tasks (may pull in tasks from outside the selected packages). The result is the execution plan: which tasks to run and in what order. @@ -169,12 +172,14 @@ The package subgraph is already a lightweight `DiGraphMap` So we clone the `DiGraphMap` once and mutate the clone. We iterate the original (stable node order) while modifying the clone. -## Explicit dependency expansion +## `dependsOn` expansion -After mapping the package subgraph to tasks, we follow explicit `dependsOn` edges from the task graph. This can pull in tasks from packages outside the selected set. +After mapping the package subgraph to tasks, we expand `dependsOn` entries. +String-form entries follow task graph edges and can pull in tasks from packages +outside the selected set. ```jsonc -// packages/app/vite-task.json +// packages/app/vite.config.* { "tasks": { "build": { @@ -188,7 +193,33 @@ If you run `vp run --filter app build`, the package subgraph contains only `app` This is intentional — `dependsOn` is an explicit declaration that a task can't run without its dependency. Ignoring it would break the build. (Users can skip this with `--ignore-depends-on`.) -The expansion only follows explicit edges, not topological ones. Topological ordering comes from the package subgraph — it's already baked into the task execution graph by Stage 2. +Object-form entries select direct package dependencies from the declaring task: + +```jsonc +// packages/app/vite.config.* +{ + "tasks": { + "test": { + "command": "vitest run", + "dependsOn": [{ "task": "build", "from": ["dependencies", "devDependencies"] }], + }, + }, +} +``` + +For `app#test`, this runs `build` in direct workspace dependency packages selected +by the listed package.json fields. If a selected package lacks `build`, expansion +walks through its matching dependency edges until it finds the nearest packages +with `build`; it stops at packages that already have `build`. Supported fields +are `dependencies`, `devDependencies`, and `peerDependencies`. + +Recursive expansion comes from dependency tasks declaring their own `dependsOn` +entries. For example, if `ui#build` also has `{ "task": "build", "from": "dependencies" }`, +then `tokens#build` is selected while expanding `ui#build`. + +The expansion follows `dependsOn` entries, not every topological edge. +Topological ordering comes from the package subgraph — it's already baked into +the task execution graph by Stage 2. ## Nested `vp run` @@ -213,7 +244,7 @@ The nested query produces its own execution subgraph, which gets embedded inside ``` Startup (once): workspace files ──> package graph ──> task graph - (dependencies) (tasks + dependsOn edges) + (dependencies) (tasks + string dependsOn edges) Per query: CLI flags ──> PackageQuery @@ -225,7 +256,7 @@ Per query: task graph ────> task execution graph (map packages to tasks, skip-intermediate reconnection, - explicit dep expansion) + dependsOn expansion) │ ▼ execution plan diff --git a/crates/vite_task_graph/Cargo.toml b/crates/vite_task_graph/Cargo.toml index cbab8e919..7cb5e400b 100644 --- a/crates/vite_task_graph/Cargo.toml +++ b/crates/vite_task_graph/Cargo.toml @@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +vec1 = { workspace = true, features = ["serde"] } vite_graph_ser = { workspace = true } vite_path = { workspace = true } vite_str = { workspace = true } diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 280b9adc1..5dcd95024 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -8,6 +8,12 @@ auto: boolean, }; export type Command = string | Array; +export type DependencyType = "dependencies" | "devDependencies" | "peerDependencies"; + +export type DependsOnEntry = string | UserPackageDependency; + +export type DependsOnFrom = DependencyType | Array; + export type GlobWithBase = { /** * The glob pattern (positive or negative starting with `!`) @@ -30,9 +36,13 @@ command: Command, */ cwd?: string, /** - * Dependencies of this task. Use `package-name#task-name` to refer to tasks in other packages. + * Dependencies of this task. + * + * String entries keep same-package / `package-name#task-name` behavior. + * Object entries run a task name in direct workspace dependency + * packages selected by package.json dependency fields. */ -dependsOn?: Array, } & ({ +dependsOn?: Array, } & ({ /** * Whether to cache the task */ @@ -95,6 +105,16 @@ scripts?: boolean, */ tasks?: boolean, }; +export type UserPackageDependency = { +/** + * Task name to run in dependency packages. + */ +task: string, +/** + * Package.json dependency field or fields to use when selecting direct dependency packages. + */ +from: DependsOnFrom, }; + export type RunConfig = { /** * Root-level cache configuration. diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index a36ca1def..d5f5f8cb9 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -7,8 +7,9 @@ use rustc_hash::FxHashSet; use serde::Serialize; pub use user::{ AutoTracking, Command, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig, - UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry, - UserRunConfig, UserTaskConfig, UserTaskDefinition, + UserCacheConfig, UserDependencyType, UserDependsOnEntry, UserDependsOnFrom, + UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry, + UserPackageDependency, UserRunConfig, UserTaskConfig, UserTaskDefinition, }; use vite_path::AbsolutePath; use vite_str::Str; @@ -25,7 +26,8 @@ use crate::config::user::UserTaskOptions; /// For example, `cwd` is resolved to absolute ones (no external factor can change it), /// but `command` is not parsed into program and args yet because environment variables in it may need to be expanded. /// -/// `depends_on` is not included here because it's represented by the edges of the task graph. +/// `depends_on` is not included here because string-form entries are represented by task graph +/// edges, and package dependency entries are stored separately on the indexed task graph. #[derive(Debug, Serialize)] pub struct ResolvedTaskConfig { /// The command or commands to run for this task. diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index d7bb938c6..3caf95b8e 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -7,6 +7,7 @@ use rustc_hash::FxHashMap; use serde::Deserialize; #[cfg(all(test, not(clippy)))] use ts_rs::TS; +use vec1::Vec1; use vite_path::RelativePathBuf; use vite_str::Str; @@ -65,6 +66,70 @@ pub enum UserInputEntry { /// Default (when field omitted): `[{auto: true}]` - infer from file accesses. pub type UserInputsConfig = Vec; +/// A supported package.json dependency field for package dependency selection. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)] +// TS derive macro generates code using std types that clippy disallows; skip derive during linting +#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependencyType"))] +#[serde(rename_all = "camelCase")] +pub enum UserDependencyType { + /// Traverse dependencies declared in the package.json `dependencies` field. + Dependencies, + /// Traverse dependencies declared in the package.json `devDependencies` field. + DevDependencies, + /// Traverse dependencies declared in the package.json `peerDependencies` field. + PeerDependencies, +} + +/// The `from` selector for object-form `dependsOn` entries. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +// TS derive macro generates code using std types that clippy disallows; skip derive during linting +#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependsOnFrom"))] +#[serde(untagged)] +pub enum UserDependsOnFrom { + /// Traverse one package.json dependency field. + Single(UserDependencyType), + /// Traverse the union of multiple package.json dependency fields. + Multiple( + #[cfg_attr(all(test, not(clippy)), ts(as = "Vec"))] + Vec1, + ), +} + +impl UserDependsOnFrom { + #[must_use] + pub fn as_slice(&self) -> &[UserDependencyType] { + match self { + Self::Single(dependency_type) => std::slice::from_ref(dependency_type), + Self::Multiple(dependency_types) => dependency_types, + } + } +} + +/// Object form for `dependsOn` entries that select workspace package dependencies. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +// TS derive macro generates code using std types that clippy disallows; skip derive during linting +#[cfg_attr(all(test, not(clippy)), derive(TS))] +#[serde(deny_unknown_fields)] +pub struct UserPackageDependency { + /// Task name to run in dependency packages. + pub task: Str, + + /// Package.json dependency field or fields to use when selecting direct dependency packages. + pub from: UserDependsOnFrom, +} + +/// A single `dependsOn` entry. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +// TS derive macro generates code using std types that clippy disallows; skip derive during linting +#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependsOnEntry"))] +#[serde(untagged)] +pub enum UserDependsOnEntry { + /// Same-package task or `package#task` specifier. + Task(Str), + /// Direct package dependency selection entry. + Package(UserPackageDependency), +} + /// A single output entry in the `output` array. /// /// Outputs can be: @@ -168,8 +233,12 @@ pub struct UserTaskOptions { #[serde(rename = "cwd")] pub cwd_relative_to_package: Option, - /// Dependencies of this task. Use `package-name#task-name` to refer to tasks in other packages. - pub depends_on: Option>, + /// Dependencies of this task. + /// + /// String entries keep same-package / `package-name#task-name` behavior. + /// Object entries run a task name in direct workspace dependency + /// packages selected by package.json dependency fields. + pub depends_on: Option>, /// Cache-related fields #[serde(flatten)] @@ -510,10 +579,107 @@ mod tests { ); let options = user_config.options; assert_eq!(options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src"); - assert_eq!(options.depends_on.as_ref().unwrap().as_ref(), [Str::from("build")]); + assert_eq!( + options.depends_on.as_ref().unwrap().as_ref(), + [UserDependsOnEntry::Task(Str::from("build"))] + ); assert_eq!(options.cache_config, UserCacheConfig::Disabled { cache: MustBe!(false) }); } + #[test] + fn test_depends_on_package_dependency_single_from() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "build", "from": "dependencies" }] + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + user_config.options.depends_on.as_ref().unwrap().as_ref(), + [UserDependsOnEntry::Package(UserPackageDependency { + task: "build".into(), + from: UserDependsOnFrom::Single(UserDependencyType::Dependencies), + })] + ); + } + + #[test] + fn test_depends_on_package_dependency_array_from() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ + "task": "build", + "from": ["dependencies", "devDependencies", "peerDependencies"] + }] + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + user_config.options.depends_on.as_ref().unwrap().as_ref(), + [UserDependsOnEntry::Package(UserPackageDependency { + task: "build".into(), + from: UserDependsOnFrom::Multiple( + Vec1::try_from_vec(vec![ + UserDependencyType::Dependencies, + UserDependencyType::DevDependencies, + UserDependencyType::PeerDependencies, + ]) + .unwrap() + ), + })] + ); + } + + #[test] + fn test_depends_on_package_dependency_empty_from_error() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "build", "from": [] }] + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + + #[test] + fn test_depends_on_package_dependency_missing_from_error() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "build" }] + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + + #[test] + fn test_depends_on_package_dependency_unknown_from_error() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "build", "from": "runtimeDependencies" }] + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + + #[test] + fn test_depends_on_package_dependency_optional_from_error() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "build", "from": "optionalDependencies" }] + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + + #[test] + fn test_depends_on_package_dependency_task_name_allows_hash() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "@scope/pkg#build", "from": "dependencies" }] + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + user_config.options.depends_on.as_ref().unwrap().as_ref(), + [UserDependsOnEntry::Package(UserPackageDependency { + task: "@scope/pkg#build".into(), + from: UserDependsOnFrom::Single(UserDependencyType::Dependencies), + })] + ); + } + #[test] fn test_task_invalid_shorthand_error() { let user_config_json = json!({ diff --git a/crates/vite_task_graph/src/lib.rs b/crates/vite_task_graph/src/lib.rs index fad227926..025b6fa69 100644 --- a/crates/vite_task_graph/src/lib.rs +++ b/crates/vite_task_graph/src/lib.rs @@ -16,9 +16,16 @@ use serde::Serialize; pub use specifier::TaskSpecifier; use vite_path::AbsolutePath; use vite_str::Str; -use vite_workspace::{PackageNodeIndex, WorkspaceRoot, package_graph::IndexedPackageGraph}; +use vite_workspace::{ + DependencyType, PackageNodeIndex, WorkspaceRoot, package_graph::IndexedPackageGraph, +}; -use crate::{config::user::UserTaskOptions, display::TaskDisplay}; +use crate::{ + config::user::{ + UserDependencyType, UserDependsOnEntry, UserPackageDependency, UserTaskOptions, + }, + display::TaskDisplay, +}; /// The type of a task dependency edge in the task graph. /// @@ -177,6 +184,38 @@ unsafe impl IndexType for TaskIx { pub type TaskNodeIndex = NodeIndex; pub type TaskEdgeIndex = EdgeIndex; +/// A `dependsOn` entry that selects direct package dependencies from a source task. +#[derive(Debug, Clone)] +pub(crate) struct PackageDependencyEntry { + pub task_name: Str, + pub dependency_types: Arc<[DependencyType]>, +} + +impl PackageDependencyEntry { + fn from_user_config(entry: &UserPackageDependency) -> Self { + Self { + task_name: entry.task.clone(), + dependency_types: entry + .from + .as_slice() + .iter() + .copied() + .map(DependencyType::from) + .collect(), + } + } +} + +impl From for DependencyType { + fn from(value: UserDependencyType) -> Self { + match value { + UserDependencyType::Dependencies => Self::Normal, + UserDependencyType::DevDependencies => Self::Dev, + UserDependencyType::PeerDependencies => Self::Peer, + } + } +} + /// Full task graph of a workspace, with necessary hash maps for quick task lookup /// /// It's immutable after created. The task nodes contain resolved task configurations and their dependencies. @@ -196,6 +235,15 @@ pub struct IndexedTaskGraph { /// Reverse map: task node index → task id (for hook lookup) task_ids_by_node_index: FxHashMap, + /// Object-form `dependsOn` entries keyed by the task that declared them. + /// + /// These stay anchored to their source task and are materialized when + /// `query_tasks` builds a per-query `TaskExecutionGraph`. Keeping them out + /// of the global `task_graph` avoids leaking package dependency selection + /// into direct runs of dependency tasks. + pub(crate) package_dependency_entries_by_node_index: + FxHashMap>, + /// Global cache configuration resolved from the workspace root config. resolved_global_cache: ResolvedGlobalCacheConfig, @@ -226,8 +274,10 @@ impl IndexedTaskGraph { let package_graph = vite_workspace::load_package_graph(workspace_root)?; - // Record dependency specifiers for each task node to add explicit dependencies later - let mut task_ids_with_dependency_specifiers: Vec<(TaskId, Option>)> = Vec::new(); + // Record `dependsOn` entries for each task node to add dependencies after all + // tasks are indexed. + let mut task_ids_with_depends_on_entries: Vec<(TaskId, Option>)> = + Vec::new(); // index tasks by ids let mut node_indices_by_task_id: FxHashMap = @@ -312,7 +362,7 @@ impl IndexedTaskGraph { UserTaskConfig { command, options: UserTaskOptions::default() } } }; - let dependency_specifiers = task_user_config.options.depends_on.clone(); + let depends_on_entries = task_user_config.options.depends_on.clone(); // Resolve the task configuration from the user config let resolved_config = ResolvedTaskConfig::resolve( @@ -340,7 +390,7 @@ impl IndexedTaskGraph { }; let node_index = task_graph.add_node(task_node); - task_ids_with_dependency_specifiers.push((task_id.clone(), dependency_specifiers)); + task_ids_with_depends_on_entries.push((task_id.clone(), depends_on_entries)); task_ids_by_node_index.insert(node_index, task_id.clone()); node_indices_by_task_id.insert(task_id, node_index); } @@ -381,25 +431,46 @@ impl IndexedTaskGraph { indexed_package_graph: IndexedPackageGraph::index(package_graph), node_indices_by_task_id, task_ids_by_node_index, + package_dependency_entries_by_node_index: FxHashMap::default(), resolved_global_cache, pre_post_scripts_enabled: root_pre_post_scripts_enabled.unwrap_or(true), }; - // Add explicit dependencies - for (from_task_id, dependency_specifiers) in task_ids_with_dependency_specifiers { + // Add string-form dependencies as explicit task graph edges, and keep + // object-form package dependency entries anchored to their source task. + for (from_task_id, depends_on_entries) in task_ids_with_depends_on_entries { let from_node_index = me.node_indices_by_task_id[&from_task_id]; - for specifier in dependency_specifiers.iter().flat_map(|s| s.iter()).cloned() { - let to_node_index = me - .get_task_index_by_specifier::( - TaskSpecifier::parse_raw(&specifier), - || Ok(from_task_id.package_index), - ) - .map_err(|error| TaskGraphLoadError::DependencySpecifierLookupError { - error, - specifier, - task_display: me.display_task(from_node_index), - })?; - me.task_graph.update_edge(from_node_index, to_node_index, TaskDependencyType); + let mut package_dependency_entries = Vec::::new(); + for entry in depends_on_entries.iter().flat_map(|entries| entries.iter()) { + match entry { + UserDependsOnEntry::Task(specifier) => { + let to_node_index = me + .get_task_index_by_specifier::( + TaskSpecifier::parse_raw(specifier), + || Ok(from_task_id.package_index), + ) + .map_err(|error| { + TaskGraphLoadError::DependencySpecifierLookupError { + error, + specifier: specifier.clone(), + task_display: me.display_task(from_node_index), + } + })?; + me.task_graph.update_edge( + from_node_index, + to_node_index, + TaskDependencyType, + ); + } + UserDependsOnEntry::Package(entry) => { + package_dependency_entries + .push(PackageDependencyEntry::from_user_config(entry)); + } + } + } + if !package_dependency_entries.is_empty() { + me.package_dependency_entries_by_node_index + .insert(from_node_index, Arc::from(package_dependency_entries)); } } diff --git a/crates/vite_task_graph/src/query/mod.rs b/crates/vite_task_graph/src/query/mod.rs index 9478a4b18..54649ef65 100644 --- a/crates/vite_task_graph/src/query/mod.rs +++ b/crates/vite_task_graph/src/query/mod.rs @@ -15,14 +15,16 @@ //! contains only task-having packages; edges map directly to task dependency edges. //! //! Explicit `dependsOn` dependencies are then added on top by `add_dependencies`. +//! String-form entries are followed as task graph edges; object-form entries +//! select direct package dependencies from the source task for the concrete query. use petgraph::{Direction, prelude::DiGraphMap, visit::EdgeRef}; use rustc_hash::{FxHashMap, FxHashSet}; use vite_str::Str; -use vite_workspace::PackageNodeIndex; pub use vite_workspace::package_graph::{PackageQuery, PackageQueryResolveError}; +use vite_workspace::{DependencyType, PackageNodeIndex}; -use crate::{IndexedTaskGraph, TaskDependencyType, TaskId, TaskNodeIndex}; +use crate::{IndexedTaskGraph, PackageDependencyEntry, TaskDependencyType, TaskId, TaskNodeIndex}; /// A task execution graph queried from a `TaskQuery`. /// @@ -148,6 +150,63 @@ impl IndexedTaskGraph { }) } + /// Resolve each package to its `task_name` task node, dropping packages that + /// don't define the task. Duplicate packages collapse to a single entry. + fn resolve_packages_to_tasks( + &self, + packages: impl Iterator, + task_name: &Str, + ) -> FxHashMap { + packages + .filter_map(|pkg| { + self.node_indices_by_task_id + .get(&TaskId { package_index: pkg, task_name: task_name.clone() }) + .map(|&task_idx| (pkg, task_idx)) + }) + .collect() + } + + /// Resolve each package to the nearest reachable `task_name` task node. + /// + /// Traversal starts from `packages`, follows package dependency edges whose + /// type is in `dependency_types`, and stops at a package once it defines the + /// requested task. This lets object-form `dependsOn` skip through direct + /// dependencies that lack the task without pulling in dependencies behind a + /// package that already has it. + fn resolve_nearest_packages_to_tasks( + &self, + packages: impl Iterator, + dependency_types: &[DependencyType], + task_name: &Str, + ) -> FxHashMap { + let package_graph = self.indexed_package_graph.package_graph(); + let mut pkg_to_task = FxHashMap::default(); + let mut seen = FxHashSet::default(); + let mut frontier: Vec<_> = packages.collect(); + + while let Some(pkg) = frontier.pop() { + if !seen.insert(pkg) { + continue; + } + if let Some(&task_idx) = self + .node_indices_by_task_id + .get(&TaskId { package_index: pkg, task_name: task_name.clone() }) + { + pkg_to_task.insert(pkg, task_idx); + continue; + } + + frontier.extend( + package_graph + .edges(pkg) + .filter(|edge| dependency_types.contains(edge.weight())) + .map(|edge| edge.target()), + ); + } + + pkg_to_task + } + /// Map a package subgraph to a task execution graph. /// /// For packages **with** the task: add the corresponding `TaskNodeIndex`. @@ -168,14 +227,7 @@ impl IndexedTaskGraph { execution_graph: &mut TaskExecutionGraph, ) { // Build the task-lookup map for all packages that have the requested task. - let pkg_to_task: FxHashMap = package_subgraph - .nodes() - .filter_map(|pkg| { - self.node_indices_by_task_id - .get(&TaskId { package_index: pkg, task_name: task_name.clone() }) - .map(|&task_idx| (pkg, task_idx)) - }) - .collect(); + let pkg_to_task = self.resolve_packages_to_tasks(package_subgraph.nodes(), task_name); // Clone the subgraph so that reconnection edits are visible in subsequent iterations. let mut subgraph = package_subgraph.clone(); @@ -214,11 +266,11 @@ impl IndexedTaskGraph { } } - /// Recursively add dependencies to the execution graph based on filtered edges. + /// Recursively add `dependsOn` dependencies to the execution graph. /// - /// Starts from the current nodes in `execution_graph` and follows outgoing edges - /// that match `filter_edge`, adding new nodes to the frontier until no new nodes - /// are discovered. + /// Starts from the current nodes in `execution_graph`, follows string-form + /// task graph edges that match `filter_edge`, and expands object-form package + /// dependency entries anchored at each visited source task. fn add_dependencies( &self, execution_graph: &mut TaskExecutionGraph, @@ -235,6 +287,18 @@ impl IndexedTaskGraph { let mut next_frontier = FxHashSet::::default(); for from_node in frontier { + if let Some(entries) = self.package_dependency_entries_by_node_index.get(&from_node) + { + for entry in entries.iter() { + self.add_package_dependency_entry( + execution_graph, + from_node, + entry, + &mut next_frontier, + ); + } + } + for edge_ref in self.task_graph.edges(from_node) { let to_node = edge_ref.target(); let dep_type = *edge_ref.weight(); @@ -251,4 +315,49 @@ impl IndexedTaskGraph { frontier = next_frontier; } } + + fn add_package_dependency_entry( + &self, + execution_graph: &mut TaskExecutionGraph, + from_node: TaskNodeIndex, + entry: &PackageDependencyEntry, + next_frontier: &mut FxHashSet, + ) { + let from_task_id = &self.task_ids_by_node_index[&from_node]; + let origin_package = from_task_id.package_index; + let package_graph = self.indexed_package_graph.package_graph(); + + // Select nearest dependency packages with `task_name`, starting from the + // origin's direct dependency packages whose edge matches one of the + // requested dependency fields. + let pkg_to_task = self.resolve_nearest_packages_to_tasks( + package_graph + .edges(origin_package) + .filter(|edge| entry.dependency_types.contains(edge.weight())) + .map(|edge| edge.target()), + &entry.dependency_types, + &entry.task_name, + ); + + // Connect the source task to each selected dependency task, recording newly + // discovered nodes for the next frontier (`add_edge` inserts the node too). + for &task_idx in pkg_to_task.values() { + if !execution_graph.graph.contains_node(task_idx) { + next_frontier.insert(task_idx); + } + execution_graph.graph.add_edge(from_node, task_idx, ()); + } + + // Preserve dependency ordering between the selected packages themselves. + for (&src_package, &src_task) in &pkg_to_task { + for edge in package_graph.edges(src_package) { + if !entry.dependency_types.contains(edge.weight()) { + continue; + } + if let Some(&dst_task) = pkg_to_task.get(&edge.target()) { + execution_graph.graph.add_edge(src_task, dst_task, ()); + } + } + } + } } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/package.json @@ -0,0 +1 @@ +{} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/package.json new file mode 100644 index 000000000..f0030c94e --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "@test/app", + "version": "1.0.0", + "dependencies": { + "@test/prod-a": "workspace:*" + }, + "devDependencies": { + "@test/dev-a": "workspace:*" + }, + "peerDependencies": { + "@test/peer-a": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/vite-task.json new file mode 100644 index 000000000..0b5b33c8b --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/vite-task.json @@ -0,0 +1,24 @@ +{ + "tasks": { + "test_dependencies": { + "command": "vtt test dependencies", + "dependsOn": [{ "task": "build", "from": "dependencies" }] + }, + "test_dev": { + "command": "vtt test dev", + "dependsOn": [{ "task": "build", "from": "devDependencies" }] + }, + "test_peer": { + "command": "vtt test peer", + "dependsOn": [{ "task": "build", "from": "peerDependencies" }] + }, + "test_union": { + "command": "vtt test union", + "dependsOn": [{ "task": "build", "from": ["dependencies", "devDependencies"] }] + }, + "test_recursive": { + "command": "vtt test recursive", + "dependsOn": [{ "task": "build_recursive", "from": "devDependencies" }] + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/package.json new file mode 100644 index 000000000..f262872d7 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/dev-a", + "version": "1.0.0", + "scripts": { + "build": "vtt build dev-a" + }, + "devDependencies": { + "@test/dev-b": "workspace:*", + "@test/shared": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/vite-task.json new file mode 100644 index 000000000..2410dd913 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/vite-task.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "build_recursive": { + "command": "vtt build recursive dev-a", + "dependsOn": [{ "task": "build_recursive", "from": "devDependencies" }] + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-b/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-b/package.json new file mode 100644 index 000000000..c98213c47 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/dev-b", + "version": "1.0.0", + "scripts": { + "build": "vtt build dev-b", + "build_recursive": "vtt build recursive dev-b" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/package.json new file mode 100644 index 000000000..a92699265 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/nearest-order-app", + "version": "1.0.0", + "dependencies": { + "@test/nearest-order-direct": "workspace:*", + "@test/nearest-order-shared": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/vite-task.json new file mode 100644 index 000000000..8db70fe8d --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/vite-task.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "build": { + "command": "vtt build nearest-order-app", + "dependsOn": [{ "task": "build", "from": "dependencies" }] + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-direct/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-direct/package.json new file mode 100644 index 000000000..14092fc25 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-direct/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/nearest-order-direct", + "version": "1.0.0", + "scripts": { + "build": "vtt build nearest-order-direct" + }, + "dependencies": { + "@test/nearest-order-skip": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-shared/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-shared/package.json new file mode 100644 index 000000000..6eb9cf2e6 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-shared/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/nearest-order-shared", + "version": "1.0.0", + "scripts": { + "build": "vtt build nearest-order-shared" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-skip/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-skip/package.json new file mode 100644 index 000000000..646b75bbc --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-skip/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/nearest-order-skip", + "version": "1.0.0", + "scripts": { + "lint": "vtt lint nearest-order-skip" + }, + "dependencies": { + "@test/nearest-order-shared": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/package.json new file mode 100644 index 000000000..163dcb838 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/nearest-stop-app", + "version": "1.0.0", + "dependencies": { + "@test/nearest-stop-foo": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/vite-task.json new file mode 100644 index 000000000..6cc6f233f --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/vite-task.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "build": { + "command": "vtt build nearest-stop-app", + "dependsOn": [{ "task": "build", "from": "dependencies" }] + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-bar/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-bar/package.json new file mode 100644 index 000000000..8f0a7a2fb --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-bar/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/nearest-stop-bar", + "version": "1.0.0", + "scripts": { + "build": "vtt build nearest-stop-bar" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-foo/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-foo/package.json new file mode 100644 index 000000000..589389b78 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-foo/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/nearest-stop-foo", + "version": "1.0.0", + "scripts": { + "build": "vtt build nearest-stop-foo" + }, + "dependencies": { + "@test/nearest-stop-bar": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/package.json new file mode 100644 index 000000000..fb8bef543 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/nearest-through-app", + "version": "1.0.0", + "dependencies": { + "@test/nearest-through-foo": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/vite-task.json new file mode 100644 index 000000000..a322eee3a --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/vite-task.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "build": { + "command": "vtt build nearest-through-app", + "dependsOn": [{ "task": "build", "from": "dependencies" }] + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-bar/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-bar/package.json new file mode 100644 index 000000000..ed46cdec6 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-bar/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/nearest-through-bar", + "version": "1.0.0", + "scripts": { + "build": "vtt build nearest-through-bar" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-foo/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-foo/package.json new file mode 100644 index 000000000..28bf0e39f --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-foo/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/nearest-through-foo", + "version": "1.0.0", + "scripts": { + "lint": "vtt lint nearest-through-foo" + }, + "dependencies": { + "@test/nearest-through-bar": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-a/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-a/package.json new file mode 100644 index 000000000..5d515c477 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-a/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/peer-a", + "version": "1.0.0", + "scripts": { + "build": "vtt build peer-a" + }, + "peerDependencies": { + "@test/peer-b": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-b/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-b/package.json new file mode 100644 index 000000000..999be6fe2 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/peer-b", + "version": "1.0.0", + "scripts": { + "build": "vtt build peer-b" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-a/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-a/package.json new file mode 100644 index 000000000..1a250e023 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-a/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/prod-a", + "version": "1.0.0", + "scripts": { + "build": "vtt build prod-a" + }, + "dependencies": { + "@test/prod-b": "workspace:*", + "@test/shared": "workspace:*" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-b/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-b/package.json new file mode 100644 index 000000000..dd7553eea --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/prod-b", + "version": "1.0.0", + "scripts": { + "build": "vtt build prod-b" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/shared/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/shared/package.json new file mode 100644 index 000000000..b03843ded --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/shared/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/shared", + "version": "1.0.0", + "scripts": { + "build": "vtt build shared", + "build_recursive": "vtt build recursive shared" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/pnpm-workspace.yaml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/pnpm-workspace.yaml new file mode 100644 index 000000000..18ec407ef --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml new file mode 100644 index 000000000..e20c1e1f7 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml @@ -0,0 +1,67 @@ +# Object-form dependsOn selects direct package.json dependency fields. + +[[plan]] +compact = true +name = "dependencies_from" +args = ["run", "test_dependencies"] +cwd = "packages/app" + +[[plan]] +compact = true +name = "dev_dependencies_from" +args = ["run", "test_dev"] +cwd = "packages/app" + +[[plan]] +compact = true +name = "peer_dependencies_from" +args = ["run", "test_peer"] +cwd = "packages/app" + +[[plan]] +compact = true +name = "multiple_from_fields" +args = ["run", "test_union"] +cwd = "packages/app" + +[[plan]] +compact = true +name = "recursive_through_dependency_task" +args = ["run", "test_recursive"] +cwd = "packages/app" + +[[plan]] +compact = true +name = "nearest_task_through_dependency_without_task" +args = ["run", "@test/nearest-through-app#build"] + +[[plan]] +compact = true +name = "nearest_task_stops_at_dependency_with_task" +args = ["run", "@test/nearest-stop-app#build"] + +# A selected package (`nearest-order-direct`) transitively depends on another +# selected package (`nearest-order-shared`) only through a task-less package +# (`nearest-order-skip`). No implicit recursion: the two selected `build` tasks +# stay unordered because there is no direct package edge between them. +[[plan]] +compact = true +name = "nearest_task_no_transitive_ordering" +args = ["run", "@test/nearest-order-app#build"] + +[[plan]] +compact = true +name = "direct_dependency_task_does_not_expand" +args = ["run", "@test/dev-a#build"] + +[[plan]] +compact = true +name = "ignore_depends_on_omits_object" +args = ["run", "--ignore-depends-on", "test_union"] +cwd = "packages/app" + +[[plan]] +compact = true +name = "extra_args_only_reach_requested_object_task" +args = ["run", "test_union", "extra"] +cwd = "packages/app" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dependencies_from.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dependencies_from.jsonc new file mode 100644 index 000000000..2ee5ef3ca --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dependencies_from.jsonc @@ -0,0 +1,7 @@ +// run test_dependencies +{ + "packages/app#test_dependencies": [ + "packages/prod-a#build" + ], + "packages/prod-a#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dev_dependencies_from.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dev_dependencies_from.jsonc new file mode 100644 index 000000000..e85b87375 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dev_dependencies_from.jsonc @@ -0,0 +1,7 @@ +// run test_dev +{ + "packages/app#test_dev": [ + "packages/dev-a#build" + ], + "packages/dev-a#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_direct_dependency_task_does_not_expand.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_direct_dependency_task_does_not_expand.jsonc new file mode 100644 index 000000000..40ad1c002 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_direct_dependency_task_does_not_expand.jsonc @@ -0,0 +1,4 @@ +// run @test/dev-a#build +{ + "packages/dev-a#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_extra_args_only_reach_requested_object_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_extra_args_only_reach_requested_object_task.jsonc new file mode 100644 index 000000000..9ab04f3fb --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_extra_args_only_reach_requested_object_task.jsonc @@ -0,0 +1,9 @@ +// run test_union extra +{ + "packages/app#test_union": [ + "packages/dev-a#build", + "packages/prod-a#build" + ], + "packages/dev-a#build": [], + "packages/prod-a#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_ignore_depends_on_omits_object.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_ignore_depends_on_omits_object.jsonc new file mode 100644 index 000000000..001458484 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_ignore_depends_on_omits_object.jsonc @@ -0,0 +1,4 @@ +// run --ignore-depends-on test_union +{ + "packages/app#test_union": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_multiple_from_fields.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_multiple_from_fields.jsonc new file mode 100644 index 000000000..d3f86018e --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_multiple_from_fields.jsonc @@ -0,0 +1,9 @@ +// run test_union +{ + "packages/app#test_union": [ + "packages/dev-a#build", + "packages/prod-a#build" + ], + "packages/dev-a#build": [], + "packages/prod-a#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_no_transitive_ordering.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_no_transitive_ordering.jsonc new file mode 100644 index 000000000..6e744951f --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_no_transitive_ordering.jsonc @@ -0,0 +1,9 @@ +// run @test/nearest-order-app#build +{ + "packages/nearest-order-app#build": [ + "packages/nearest-order-direct#build", + "packages/nearest-order-shared#build" + ], + "packages/nearest-order-direct#build": [], + "packages/nearest-order-shared#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_stops_at_dependency_with_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_stops_at_dependency_with_task.jsonc new file mode 100644 index 000000000..186614a39 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_stops_at_dependency_with_task.jsonc @@ -0,0 +1,7 @@ +// run @test/nearest-stop-app#build +{ + "packages/nearest-stop-app#build": [ + "packages/nearest-stop-foo#build" + ], + "packages/nearest-stop-foo#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_through_dependency_without_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_through_dependency_without_task.jsonc new file mode 100644 index 000000000..1ae2c4b31 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_through_dependency_without_task.jsonc @@ -0,0 +1,7 @@ +// run @test/nearest-through-app#build +{ + "packages/nearest-through-app#build": [ + "packages/nearest-through-bar#build" + ], + "packages/nearest-through-bar#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_peer_dependencies_from.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_peer_dependencies_from.jsonc new file mode 100644 index 000000000..3879257b3 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_peer_dependencies_from.jsonc @@ -0,0 +1,7 @@ +// run test_peer +{ + "packages/app#test_peer": [ + "packages/peer-a#build" + ], + "packages/peer-a#build": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_recursive_through_dependency_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_recursive_through_dependency_task.jsonc new file mode 100644 index 000000000..6ac032c0e --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_recursive_through_dependency_task.jsonc @@ -0,0 +1,12 @@ +// run test_recursive +{ + "packages/app#test_recursive": [ + "packages/dev-a#build_recursive" + ], + "packages/dev-a#build_recursive": [ + "packages/dev-b#build_recursive", + "packages/shared#build_recursive" + ], + "packages/dev-b#build_recursive": [], + "packages/shared#build_recursive": [] +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc new file mode 100644 index 000000000..0a2e54760 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc @@ -0,0 +1,1028 @@ +// task graph +[ + { + "key": [ + "/packages/app", + "test_dependencies" + ], + "node": { + "task_display": { + "package_name": "@test/app", + "task_name": "test_dependencies", + "package_path": "/packages/app" + }, + "resolved_config": { + "commands": [ + "vtt test dependencies" + ], + "resolved_options": { + "cwd": "/packages/app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/app", + "test_dev" + ], + "node": { + "task_display": { + "package_name": "@test/app", + "task_name": "test_dev", + "package_path": "/packages/app" + }, + "resolved_config": { + "commands": [ + "vtt test dev" + ], + "resolved_options": { + "cwd": "/packages/app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/app", + "test_peer" + ], + "node": { + "task_display": { + "package_name": "@test/app", + "task_name": "test_peer", + "package_path": "/packages/app" + }, + "resolved_config": { + "commands": [ + "vtt test peer" + ], + "resolved_options": { + "cwd": "/packages/app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/app", + "test_recursive" + ], + "node": { + "task_display": { + "package_name": "@test/app", + "task_name": "test_recursive", + "package_path": "/packages/app" + }, + "resolved_config": { + "commands": [ + "vtt test recursive" + ], + "resolved_options": { + "cwd": "/packages/app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/app", + "test_union" + ], + "node": { + "task_display": { + "package_name": "@test/app", + "task_name": "test_union", + "package_path": "/packages/app" + }, + "resolved_config": { + "commands": [ + "vtt test union" + ], + "resolved_options": { + "cwd": "/packages/app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/dev-a", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/dev-a", + "task_name": "build", + "package_path": "/packages/dev-a" + }, + "resolved_config": { + "commands": [ + "vtt build dev-a" + ], + "resolved_options": { + "cwd": "/packages/dev-a", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/dev-a", + "build_recursive" + ], + "node": { + "task_display": { + "package_name": "@test/dev-a", + "task_name": "build_recursive", + "package_path": "/packages/dev-a" + }, + "resolved_config": { + "commands": [ + "vtt build recursive dev-a" + ], + "resolved_options": { + "cwd": "/packages/dev-a", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/dev-b", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/dev-b", + "task_name": "build", + "package_path": "/packages/dev-b" + }, + "resolved_config": { + "commands": [ + "vtt build dev-b" + ], + "resolved_options": { + "cwd": "/packages/dev-b", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/dev-b", + "build_recursive" + ], + "node": { + "task_display": { + "package_name": "@test/dev-b", + "task_name": "build_recursive", + "package_path": "/packages/dev-b" + }, + "resolved_config": { + "commands": [ + "vtt build recursive dev-b" + ], + "resolved_options": { + "cwd": "/packages/dev-b", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-order-app", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-order-app", + "task_name": "build", + "package_path": "/packages/nearest-order-app" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-order-app" + ], + "resolved_options": { + "cwd": "/packages/nearest-order-app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-order-direct", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-order-direct", + "task_name": "build", + "package_path": "/packages/nearest-order-direct" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-order-direct" + ], + "resolved_options": { + "cwd": "/packages/nearest-order-direct", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-order-shared", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-order-shared", + "task_name": "build", + "package_path": "/packages/nearest-order-shared" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-order-shared" + ], + "resolved_options": { + "cwd": "/packages/nearest-order-shared", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-order-skip", + "lint" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-order-skip", + "task_name": "lint", + "package_path": "/packages/nearest-order-skip" + }, + "resolved_config": { + "commands": [ + "vtt lint nearest-order-skip" + ], + "resolved_options": { + "cwd": "/packages/nearest-order-skip", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-stop-app", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-stop-app", + "task_name": "build", + "package_path": "/packages/nearest-stop-app" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-stop-app" + ], + "resolved_options": { + "cwd": "/packages/nearest-stop-app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-stop-bar", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-stop-bar", + "task_name": "build", + "package_path": "/packages/nearest-stop-bar" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-stop-bar" + ], + "resolved_options": { + "cwd": "/packages/nearest-stop-bar", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-stop-foo", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-stop-foo", + "task_name": "build", + "package_path": "/packages/nearest-stop-foo" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-stop-foo" + ], + "resolved_options": { + "cwd": "/packages/nearest-stop-foo", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-through-app", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-through-app", + "task_name": "build", + "package_path": "/packages/nearest-through-app" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-through-app" + ], + "resolved_options": { + "cwd": "/packages/nearest-through-app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-through-bar", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-through-bar", + "task_name": "build", + "package_path": "/packages/nearest-through-bar" + }, + "resolved_config": { + "commands": [ + "vtt build nearest-through-bar" + ], + "resolved_options": { + "cwd": "/packages/nearest-through-bar", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/nearest-through-foo", + "lint" + ], + "node": { + "task_display": { + "package_name": "@test/nearest-through-foo", + "task_name": "lint", + "package_path": "/packages/nearest-through-foo" + }, + "resolved_config": { + "commands": [ + "vtt lint nearest-through-foo" + ], + "resolved_options": { + "cwd": "/packages/nearest-through-foo", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/peer-a", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/peer-a", + "task_name": "build", + "package_path": "/packages/peer-a" + }, + "resolved_config": { + "commands": [ + "vtt build peer-a" + ], + "resolved_options": { + "cwd": "/packages/peer-a", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/peer-b", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/peer-b", + "task_name": "build", + "package_path": "/packages/peer-b" + }, + "resolved_config": { + "commands": [ + "vtt build peer-b" + ], + "resolved_options": { + "cwd": "/packages/peer-b", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/prod-a", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/prod-a", + "task_name": "build", + "package_path": "/packages/prod-a" + }, + "resolved_config": { + "commands": [ + "vtt build prod-a" + ], + "resolved_options": { + "cwd": "/packages/prod-a", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/prod-b", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/prod-b", + "task_name": "build", + "package_path": "/packages/prod-b" + }, + "resolved_config": { + "commands": [ + "vtt build prod-b" + ], + "resolved_options": { + "cwd": "/packages/prod-b", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/shared", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/shared", + "task_name": "build", + "package_path": "/packages/shared" + }, + "resolved_config": { + "commands": [ + "vtt build shared" + ], + "resolved_options": { + "cwd": "/packages/shared", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/packages/shared", + "build_recursive" + ], + "node": { + "task_display": { + "package_name": "@test/shared", + "task_name": "build_recursive", + "package_path": "/packages/shared" + }, + "resolved_config": { + "commands": [ + "vtt build recursive shared" + ], + "resolved_options": { + "cwd": "/packages/shared", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/vite-task.json new file mode 100644 index 000000000..d548edfac --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/vite-task.json @@ -0,0 +1,3 @@ +{ + "cache": true +} diff --git a/docs/rfcs/depends-on-package-dependencies.md b/docs/rfcs/depends-on-package-dependencies.md new file mode 100644 index 000000000..dc7fc293e --- /dev/null +++ b/docs/rfcs/depends-on-package-dependencies.md @@ -0,0 +1,154 @@ +# RFC: `dependsOn` Package Dependency Selection + +## Summary + +Add an object form for `dependsOn` that runs a task in direct workspace dependency packages without using special-character task syntax. + +```ts +type DependencyType = 'dependencies' | 'devDependencies' | 'peerDependencies'; + +type DependsOnEntry = + | string + | { + task: string; + from: DependencyType | DependencyType[]; + }; +``` + +String entries keep their current behavior: + +```jsonc +{ + "dependsOn": ["build"], +} +``` + +Object entries select direct dependency packages from `package.json` fields: + +```jsonc +{ + "dependsOn": [{ "task": "build", "from": "dependencies" }], +} +``` + +## Motivation + +This is feature parity with Turborepo and Nx. + +Both tools have a common way to say: before running this task, run a task with the same or chosen name in dependency packages. That pattern is used for builds, type generation, tests that need built libraries, and other upstream artifact pipelines. + +Without it, migrating a repo from Turbo or Nx requires replacing a common task pipeline feature with manual package-specific `dependsOn` entries. That is noisy and easy to get wrong. For many repos, this is essential migration coverage. + +The proposed form keeps the behavior explicit while avoiding special characters like `^` in task config. + +## Comparison + +| Case | Turborepo | Nx | This proposal | +| ---------------------------------------------------------- | ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- | +| Run `build` in direct dependency packages before this task | `"^build"` | `"^build"` or `{ "dependencies": true, "target": "build" }` | `{ "task": "build", "from": "dependencies" }` | +| Dependency field selection | Not expressed in `^build` | Not expressed in `^build` / `dependencies: true` | `from`: `dependencies`, `devDependencies`, `peerDependencies` | + +## Semantics + +An object entry runs a task in direct dependency packages of the package that declares the source task. + +For: + +```jsonc +{ + "tasks": { + "test": { + "command": "vitest run", + "dependsOn": [{ "task": "build", "from": "dependencies" }], + }, + }, +} +``` + +running `app#test` means: + +1. Start at package `app`. +2. Select its direct workspace dependencies declared in `dependencies`. +3. In each selected package, run `build` if that package has a `build` task. +4. If a selected package lacks `build`, walk through its matching dependencies + until the nearest package or packages with `build` are found. + +The source package itself is not selected by the object entry. + +Package dependencies: + +```mermaid +flowchart LR + appPkg["@app"] --> uiPkg["@ui"] + appPkg --> utilsPkg["@utils"] + uiPkg --> tokensPkg["@tokens"] +``` + +Task dependencies: + +```mermaid +flowchart LR + appTask["app#test
{ task: build, from: dependencies }"] --> uiTask["ui#build"] + appTask --> utilsTask["utils#build"] +``` + +In this example: + +- `app#test` depends on `ui#build`. +- `app#test` depends on `utils#build`. +- `tokens#build` is not selected by this entry because `@tokens` is not a direct dependency of `@app`. +- `app#test` does not imply `app#build`; same-package dependencies use string form. + +## Nearest Task Selection + +An object entry is not recursive past packages that define the requested task. +It starts from direct dependency packages and stops at the nearest matching task +on each dependency path. + +In the graph above, `tokens#build` is not selected by `app#test` because `@tokens` is a dependency of `@ui`, not `@app`. + +If `ui#build` declares its own `{ "task": "build", "from": "dependencies" }`, then `tokens#build` can be selected from `ui#build`. That is a separate dependency declaration, not recursion from `app#test`. + +Package dependencies: + +```mermaid +flowchart LR + appPkg["@app"] --> uiPkg["@ui"] + uiPkg --> tokensPkg["@tokens"] +``` + +Task dependencies: + +```mermaid +flowchart LR + appTask["app#test"] --> uiTask["ui#build
{ task: build, from: dependencies }"] + uiTask --> tokensTask["tokens#build"] +``` + +If `@ui` did not define `build`, then `tokens#build` could be selected directly +from `app#test` by skipping through `@ui`. + +## `from` + +`from` names the package.json dependency fields used to select direct dependency packages +and to walk through packages that lack the requested task. + +```jsonc +{ "task": "build", "from": "dependencies" } +``` + +This selects direct dependencies from `dependencies`. + +```jsonc +{ "task": "build", "from": ["dependencies", "devDependencies"] } +``` + +This selects the union of direct dependencies from `dependencies` and `devDependencies`. + +Supported values are: + +- `dependencies` +- `devDependencies` +- `peerDependencies` + +If the same direct dependency package is selected through more than one allowed field, it is included once.