From 6d9d369a78994ef97890729973e22a9c1e32f7a1 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 15 Jun 2026 09:41:41 +0800 Subject: [PATCH 1/4] feat: add dependsOn package dependency selection Co-authored-by: GPT-5 Codex --- CHANGELOG.md | 1 + crates/vite_task/docs/boolean-flags.md | 4 +- crates/vite_task/docs/task-query.md | 49 +- crates/vite_task_graph/run-config.ts | 24 +- crates/vite_task_graph/src/config/mod.rs | 8 +- crates/vite_task_graph/src/config/user.rs | 189 +++++- crates/vite_task_graph/src/lib.rs | 110 +++- crates/vite_task_graph/src/query/mod.rs | 75 ++- .../package.json | 1 + .../packages/app/package.json | 13 + .../packages/app/vite-task.json | 24 + .../packages/dev-a/package.json | 11 + .../packages/dev-a/vite-task.json | 8 + .../packages/dev-b/package.json | 8 + .../packages/peer-a/package.json | 10 + .../packages/peer-b/package.json | 7 + .../packages/prod-a/package.json | 11 + .../packages/prod-b/package.json | 7 + .../packages/shared/package.json | 8 + .../pnpm-workspace.yaml | 2 + .../snapshots.toml | 48 ++ .../snapshots/query_dependencies_from.jsonc | 7 + .../query_dev_dependencies_from.jsonc | 7 + ...rect_dependency_task_does_not_expand.jsonc | 4 + ...rgs_only_reach_requested_object_task.jsonc | 9 + ...query_ignore_depends_on_omits_object.jsonc | 4 + .../query_multiple_from_fields.jsonc | 9 + .../query_peer_dependencies_from.jsonc | 7 + ...ry_recursive_through_dependency_task.jsonc | 12 + .../snapshots/task_graph.jsonc | 618 ++++++++++++++++++ .../vite-task.json | 3 + docs/rfcs/depends-on-package-dependencies.md | 146 +++++ 32 files changed, 1398 insertions(+), 46 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/app/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-a/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/dev-b/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-a/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/peer-b/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-a/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/prod-b/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/shared/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/pnpm-workspace.yaml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dependencies_from.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_dev_dependencies_from.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_direct_dependency_task_does_not_expand.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_extra_args_only_reach_requested_object_task.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_ignore_depends_on_omits_object.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_multiple_from_fields.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_peer_dependencies_from.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_recursive_through_dependency_task.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/vite-task.json create mode 100644 docs/rfcs/depends-on-package-dependencies.md 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/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..7dce7b296 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,31 @@ 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. Packages without `build` are skipped. 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 +242,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 +254,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/run-config.ts b/crates/vite_task_graph/run-config.ts index 280b9adc1..00466f9e6 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 bare 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 = { +/** + * Bare 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..53c2cee1e 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use monostate::MustBe; use rustc_hash::FxHashMap; -use serde::Deserialize; +use serde::{Deserialize, de}; #[cfg(all(test, not(clippy)))] use ts_rs::TS; use vite_path::RelativePathBuf; @@ -65,6 +65,96 @@ 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(Arc<[UserDependencyType]>), +} + +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, + } + } + + #[must_use] + pub fn is_empty(&self) -> bool { + matches!(self, Self::Multiple(dependency_types) if dependency_types.is_empty()) + } +} + +/// Object form for `dependsOn` entries that select workspace package dependencies. +#[derive(Debug, 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))] +pub struct UserPackageDependency { + /// Bare 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, +} + +impl<'de> Deserialize<'de> for UserPackageDependency { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Raw { + task: Str, + from: UserDependsOnFrom, + } + + let raw = Raw::deserialize(deserializer)?; + if raw.task.as_str().contains('#') { + return Err(de::Error::custom( + "`dependsOn[].task` must be a bare task name without `#`", + )); + } + if raw.from.is_empty() { + return Err(de::Error::custom("`dependsOn[].from` must not be an empty array")); + } + Ok(Self { task: raw.task, from: raw.from }) + } +} + +/// 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 +258,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 bare task name in direct workspace dependency + /// packages selected by package.json dependency fields. + pub depends_on: Option>, /// Cache-related fields #[serde(flatten)] @@ -510,10 +604,97 @@ 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(Arc::from([ + UserDependencyType::Dependencies, + UserDependencyType::DevDependencies, + UserDependencyType::PeerDependencies, + ])), + })] + ); + } + + #[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_package_task_error() { + let user_config_json = json!({ + "command": "echo test", + "dependsOn": [{ "task": "@scope/pkg#build", "from": "dependencies" }] + }); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + #[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..774e0e007 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,14 @@ 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 expanded only for a + /// concrete query. Keeping them out of `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 +273,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 +361,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 +389,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 +430,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..3bf4aa5d3 100644 --- a/crates/vite_task_graph/src/query/mod.rs +++ b/crates/vite_task_graph/src/query/mod.rs @@ -15,6 +15,8 @@ //! 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}; @@ -22,7 +24,7 @@ use vite_str::Str; use vite_workspace::PackageNodeIndex; pub use vite_workspace::package_graph::{PackageQuery, PackageQueryResolveError}; -use crate::{IndexedTaskGraph, TaskDependencyType, TaskId, TaskNodeIndex}; +use crate::{IndexedTaskGraph, PackageDependencyEntry, TaskDependencyType, TaskId, TaskNodeIndex}; /// A task execution graph queried from a `TaskQuery`. /// @@ -214,11 +216,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 +237,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 +265,55 @@ 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(); + let mut selected_packages = FxHashSet::::default(); + for edge in package_graph.edges(origin_package) { + if entry.dependency_types.contains(edge.weight()) { + selected_packages.insert(edge.target()); + } + } + + let pkg_to_task: FxHashMap = selected_packages + .iter() + .copied() + .filter_map(|pkg| { + self.node_indices_by_task_id + .get(&TaskId { package_index: pkg, task_name: entry.task_name.clone() }) + .map(|&task_idx| (pkg, task_idx)) + }) + .collect(); + + for &task_idx in pkg_to_task.values() { + let is_new = !execution_graph.graph.contains_node(task_idx); + execution_graph.graph.add_node(task_idx); + if is_new { + next_frontier.insert(task_idx); + } + } + + for &task_idx in pkg_to_task.values() { + execution_graph.graph.add_edge(from_node, task_idx, ()); + } + + 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/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..0482e2414 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml @@ -0,0 +1,48 @@ +# 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 = "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_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..77015d4ba --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc @@ -0,0 +1,618 @@ +// 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/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..1340e4ecf --- /dev/null +++ b/docs/rfcs/depends-on-package-dependencies.md @@ -0,0 +1,146 @@ +# 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. + +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. + +## Not Recursive + +An object entry is not recursive. It selects only direct dependency packages. + +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"] +``` + +## `from` + +`from` names the package.json dependency fields used to select direct dependency packages. + +```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. From c796d440869a2e7cefeb98658a6370322aca8cee Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 15 Jun 2026 15:11:00 +0800 Subject: [PATCH 2/4] refactor(graph): simplify package dependency expansion Fold the redundant intermediate set and the duplicated value-loop in add_package_dependency_entry into a single pass, and extract the shared package-to-task lookup into resolve_packages_to_tasks, now reused by map_subgraph_to_tasks. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/vite_task_graph/src/query/mod.rs | 59 +++++++++++++------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/crates/vite_task_graph/src/query/mod.rs b/crates/vite_task_graph/src/query/mod.rs index 3bf4aa5d3..797d4b0f2 100644 --- a/crates/vite_task_graph/src/query/mod.rs +++ b/crates/vite_task_graph/src/query/mod.rs @@ -150,6 +150,22 @@ 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() + } + /// Map a package subgraph to a task execution graph. /// /// For packages **with** the task: add the corresponding `TaskNodeIndex`. @@ -170,14 +186,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(); @@ -276,35 +285,27 @@ impl IndexedTaskGraph { 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(); - let mut selected_packages = FxHashSet::::default(); - for edge in package_graph.edges(origin_package) { - if entry.dependency_types.contains(edge.weight()) { - selected_packages.insert(edge.target()); - } - } - let pkg_to_task: FxHashMap = selected_packages - .iter() - .copied() - .filter_map(|pkg| { - self.node_indices_by_task_id - .get(&TaskId { package_index: pkg, task_name: entry.task_name.clone() }) - .map(|&task_idx| (pkg, task_idx)) - }) - .collect(); + // Select the origin's direct dependency packages whose edge matches one of + // the requested dependency fields, mapped to their `task_name` task nodes. + let pkg_to_task = self.resolve_packages_to_tasks( + package_graph + .edges(origin_package) + .filter(|edge| entry.dependency_types.contains(edge.weight())) + .map(|edge| edge.target()), + &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() { - let is_new = !execution_graph.graph.contains_node(task_idx); - execution_graph.graph.add_node(task_idx); - if is_new { + if !execution_graph.graph.contains_node(task_idx) { next_frontier.insert(task_idx); } - } - - for &task_idx in pkg_to_task.values() { 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()) { From c042c52995f2baf7acbff608a167b2b58c4501c7 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 15 Jun 2026 15:46:26 +0800 Subject: [PATCH 3/4] feat(task-graph): resolve nearest package dependency tasks Co-authored-by: GPT-5 Codex --- Cargo.lock | 1 + crates/vite_task/docs/task-query.md | 6 +- crates/vite_task_graph/Cargo.toml | 1 + crates/vite_task_graph/run-config.ts | 4 +- crates/vite_task_graph/src/config/user.rs | 69 ++--- crates/vite_task_graph/src/lib.rs | 7 +- crates/vite_task_graph/src/query/mod.rs | 51 +++- .../packages/nearest-stop-app/package.json | 7 + .../packages/nearest-stop-app/vite-task.json | 8 + .../packages/nearest-stop-bar/package.json | 7 + .../packages/nearest-stop-foo/package.json | 10 + .../packages/nearest-through-app/package.json | 7 + .../nearest-through-app/vite-task.json | 8 + .../packages/nearest-through-bar/package.json | 7 + .../packages/nearest-through-foo/package.json | 10 + .../snapshots.toml | 10 + ...t_task_stops_at_dependency_with_task.jsonc | 7 + ...task_through_dependency_without_task.jsonc | 7 + .../snapshots/task_graph.jsonc | 246 ++++++++++++++++++ docs/rfcs/depends-on-package-dependencies.md | 14 +- 20 files changed, 431 insertions(+), 56 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-app/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-bar/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-stop-foo/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-app/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-bar/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-through-foo/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_stops_at_dependency_with_task.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_through_dependency_without_task.jsonc 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/task-query.md b/crates/vite_task/docs/task-query.md index 7dce7b296..20a4069f3 100644 --- a/crates/vite_task/docs/task-query.md +++ b/crates/vite_task/docs/task-query.md @@ -208,8 +208,10 @@ Object-form entries select direct package dependencies from the declaring task: ``` For `app#test`, this runs `build` in direct workspace dependency packages selected -by the listed package.json fields. Packages without `build` are skipped. Supported -fields are `dependencies`, `devDependencies`, and `peerDependencies`. +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" }`, 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 00466f9e6..5dcd95024 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -39,7 +39,7 @@ cwd?: string, * Dependencies of this task. * * String entries keep same-package / `package-name#task-name` behavior. - * Object entries run a bare task name in direct workspace dependency + * Object entries run a task name in direct workspace dependency * packages selected by package.json dependency fields. */ dependsOn?: Array, } & ({ @@ -107,7 +107,7 @@ tasks?: boolean, }; export type UserPackageDependency = { /** - * Bare task name to run in dependency packages. + * Task name to run in dependency packages. */ task: string, /** diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index 53c2cee1e..3caf95b8e 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -4,9 +4,10 @@ use std::sync::Arc; use monostate::MustBe; use rustc_hash::FxHashMap; -use serde::{Deserialize, de}; +use serde::Deserialize; #[cfg(all(test, not(clippy)))] use ts_rs::TS; +use vec1::Vec1; use vite_path::RelativePathBuf; use vite_str::Str; @@ -88,7 +89,10 @@ pub enum UserDependsOnFrom { /// Traverse one package.json dependency field. Single(UserDependencyType), /// Traverse the union of multiple package.json dependency fields. - Multiple(Arc<[UserDependencyType]>), + Multiple( + #[cfg_attr(all(test, not(clippy)), ts(as = "Vec"))] + Vec1, + ), } impl UserDependsOnFrom { @@ -99,50 +103,21 @@ impl UserDependsOnFrom { Self::Multiple(dependency_types) => dependency_types, } } - - #[must_use] - pub fn is_empty(&self) -> bool { - matches!(self, Self::Multiple(dependency_types) if dependency_types.is_empty()) - } } /// Object form for `dependsOn` entries that select workspace package dependencies. -#[derive(Debug, PartialEq, Eq, Clone)] +#[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 { - /// Bare task name to run in dependency packages. + /// 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, } -impl<'de> Deserialize<'de> for UserPackageDependency { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct Raw { - task: Str, - from: UserDependsOnFrom, - } - - let raw = Raw::deserialize(deserializer)?; - if raw.task.as_str().contains('#') { - return Err(de::Error::custom( - "`dependsOn[].task` must be a bare task name without `#`", - )); - } - if raw.from.is_empty() { - return Err(de::Error::custom("`dependsOn[].from` must not be an empty array")); - } - Ok(Self { task: raw.task, from: raw.from }) - } -} - /// 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 @@ -261,7 +236,7 @@ pub struct UserTaskOptions { /// Dependencies of this task. /// /// String entries keep same-package / `package-name#task-name` behavior. - /// Object entries run a bare task name in direct workspace dependency + /// Object entries run a task name in direct workspace dependency /// packages selected by package.json dependency fields. pub depends_on: Option>, @@ -641,11 +616,14 @@ mod tests { user_config.options.depends_on.as_ref().unwrap().as_ref(), [UserDependsOnEntry::Package(UserPackageDependency { task: "build".into(), - from: UserDependsOnFrom::Multiple(Arc::from([ - UserDependencyType::Dependencies, - UserDependencyType::DevDependencies, - UserDependencyType::PeerDependencies, - ])), + from: UserDependsOnFrom::Multiple( + Vec1::try_from_vec(vec![ + UserDependencyType::Dependencies, + UserDependencyType::DevDependencies, + UserDependencyType::PeerDependencies, + ]) + .unwrap() + ), })] ); } @@ -687,12 +665,19 @@ mod tests { } #[test] - fn test_depends_on_package_dependency_package_task_error() { + 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" }] }); - assert!(serde_json::from_value::(user_config_json).is_err()); + 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] diff --git a/crates/vite_task_graph/src/lib.rs b/crates/vite_task_graph/src/lib.rs index 774e0e007..025b6fa69 100644 --- a/crates/vite_task_graph/src/lib.rs +++ b/crates/vite_task_graph/src/lib.rs @@ -237,9 +237,10 @@ pub struct IndexedTaskGraph { /// Object-form `dependsOn` entries keyed by the task that declared them. /// - /// These stay anchored to their source task and are expanded only for a - /// concrete query. Keeping them out of `task_graph` avoids leaking package - /// dependency selection into direct runs of dependency tasks. + /// 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>, diff --git a/crates/vite_task_graph/src/query/mod.rs b/crates/vite_task_graph/src/query/mod.rs index 797d4b0f2..54649ef65 100644 --- a/crates/vite_task_graph/src/query/mod.rs +++ b/crates/vite_task_graph/src/query/mod.rs @@ -21,8 +21,8 @@ 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, PackageDependencyEntry, TaskDependencyType, TaskId, TaskNodeIndex}; @@ -166,6 +166,47 @@ impl IndexedTaskGraph { .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`. @@ -286,13 +327,15 @@ impl IndexedTaskGraph { let origin_package = from_task_id.package_index; let package_graph = self.indexed_package_graph.package_graph(); - // Select the origin's direct dependency packages whose edge matches one of - // the requested dependency fields, mapped to their `task_name` task nodes. - let pkg_to_task = self.resolve_packages_to_tasks( + // 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, ); 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/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml index 0482e2414..17a3c734b 100644 --- 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 @@ -30,6 +30,16 @@ 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"] + [[plan]] compact = true name = "direct_dependency_task_does_not_expand" 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/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc index 77015d4ba..0441f1b7c 100644 --- 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 @@ -369,6 +369,252 @@ }, "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", diff --git a/docs/rfcs/depends-on-package-dependencies.md b/docs/rfcs/depends-on-package-dependencies.md index 1340e4ecf..dc7fc293e 100644 --- a/docs/rfcs/depends-on-package-dependencies.md +++ b/docs/rfcs/depends-on-package-dependencies.md @@ -70,6 +70,8 @@ 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. @@ -97,9 +99,11 @@ In this example: - `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. -## Not Recursive +## Nearest Task Selection -An object entry is not recursive. It selects only direct dependency packages. +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`. @@ -121,9 +125,13 @@ flowchart LR 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. +`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" } From b79c3388846c54a275b157bbc2b4bbfe52746e42 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 15 Jun 2026 15:57:24 +0800 Subject: [PATCH 4/4] test(graph): pin no implicit recursion for nearest dependsOn Add a plan-snapshot fixture where a selected package transitively depends on another selected package only through a task-less intermediate. The two build tasks stay unordered, locking in that nearest object-form dependsOn selection does not create transitive ordering edges. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../packages/nearest-order-app/package.json | 8 + .../packages/nearest-order-app/vite-task.json | 8 + .../nearest-order-direct/package.json | 10 ++ .../nearest-order-shared/package.json | 7 + .../packages/nearest-order-skip/package.json | 10 ++ .../snapshots.toml | 9 + ..._nearest_task_no_transitive_ordering.jsonc | 9 + .../snapshots/task_graph.jsonc | 164 ++++++++++++++++++ 8 files changed, 225 insertions(+) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-app/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-direct/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-shared/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/packages/nearest-order-skip/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/query_nearest_task_no_transitive_ordering.jsonc 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/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots.toml index 17a3c734b..e20c1e1f7 100644 --- 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 @@ -40,6 +40,15 @@ 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" 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/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/depends_on_package_dependencies/snapshots/task_graph.jsonc index 0441f1b7c..0a2e54760 100644 --- 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 @@ -369,6 +369,170 @@ }, "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",