Skip to content

Commit b281e54

Browse files
claudecodex
authored andcommitted
feat(task-graph): materialize object dependsOn edges
Object-form `dependsOn` entries — `{ "task": "build", "from": "dependencies" }` — run a task in the direct workspace packages listed under a package.json dependency field (`dependencies`, `devDependencies`, and/or `peerDependencies`). The earlier implementation expanded these selections at query time, so they never appeared in the global task graph. This reimplements the feature at graph-load time: each object entry is resolved against the package dependency graph and the matching `package#task` selections are added as ordinary task graph edges (`add_package_dependency_edges`). Only direct dependencies are followed, and an edge is added only when the dependency package defines the task. Because the selections are now plain task graph edges, they appear in the global graph and flow through the existing dependency machinery for free — including `--ignore-depends-on`, which drops them at query time like any other `dependsOn` edge. The fixture asserts edge construction through the rendered global `task_graph.md` (every `from` variant, recursive cross-package chains, and the exclusion of peer-only and missing-task dependencies). The only behavior the static graph cannot express — `--ignore-depends-on` removing the materialized edges at query time — is kept as the single per-case plan snapshot. Co-authored-by: GPT-5 Codex <codex@openai.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QowxsN8vDKKbQdaSMdxL67
1 parent 7a3bf10 commit b281e54

22 files changed

Lines changed: 817 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
- **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)).
4+
- **Added** Object-form `dependsOn` entries for direct workspace dependencies ([#479](https://github.com/voidzero-dev/vite-task/pull/479)).
45
- **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), [#472](https://github.com/voidzero-dev/vite-task/pull/472)).
56
- **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)).
67
- **Changed** Environment values in task cache fingerprints are now stored only as SHA-256 digests, and env-related cache miss details report names without values ([#455](https://github.com/voidzero-dev/vite-task/pull/455)).

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_task/docs/task-query.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Both are built once and reused for every query, including nested `vp run` calls
1313

1414
### What goes into the task graph
1515

16-
The task graph contains a node for every task in every package, and edges only for explicit `dependsOn` declarations:
16+
The task graph contains a node for every task in every package, and edges only for explicit `dependsOn` declarations.
1717

1818
```jsonc
1919
// packages/app/vite.config.*
@@ -38,6 +38,33 @@ Task graph:
3838

3939
Package dependency ordering (app depends on lib) is NOT stored as edges in the task graph. Why not is explained below.
4040

41+
Object-form `dependsOn` entries are also explicit task dependencies. At startup,
42+
they are resolved against the declaring package's direct `package.json`
43+
dependency fields and materialized as task graph edges:
44+
45+
```jsonc
46+
// packages/app/vite.config.*
47+
{
48+
"tasks": {
49+
"test": {
50+
"command": "vitest run",
51+
"dependsOn": [{ "task": "build", "from": ["dependencies", "devDependencies"] }],
52+
},
53+
},
54+
}
55+
```
56+
57+
If `app` directly depends on `ui` and `shared`, and both packages have `build`,
58+
the task graph contains:
59+
60+
```
61+
app#test ──dependsOn──> ui#build
62+
app#test ──dependsOn──> shared#build
63+
```
64+
65+
Dependency packages without the requested task are skipped. Recursive expansion
66+
comes from dependency tasks declaring their own `dependsOn` entries.
67+
4168
## What happens when you run a query
4269

4370
Every `vp run` command goes through two stages:
@@ -188,7 +215,7 @@ If you run `vp run --filter app build`, the package subgraph contains only `app`
188215

189216
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`.)
190217

191-
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.
218+
The expansion follows explicit `dependsOn` edges, including edges materialized from object-form entries. It does not follow topological package edges. Topological ordering comes from the package subgraph — it's already baked into the task execution graph by Stage 2.
192219

193220
## Nested `vp run`
194221

crates/vite_task_graph/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] }
1818
serde_json = { workspace = true }
1919
thiserror = { workspace = true }
2020
tracing = { workspace = true }
21+
vec1 = { workspace = true, features = ["serde"] }
2122
vite_path = { workspace = true }
2223
vite_str = { workspace = true }
2324
vite_workspace = { workspace = true }

crates/vite_task_graph/run-config.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ auto: boolean, };
88

99
export type Command = string | Array<string>;
1010

11+
export type DependencyType = "dependencies" | "devDependencies" | "peerDependencies";
12+
13+
export type DependsOnEntry = string | UserPackageDependency;
14+
15+
export type DependsOnFrom = DependencyType | Array<DependencyType>;
16+
1117
export type GlobWithBase = {
1218
/**
1319
* The glob pattern (positive or negative starting with `!`)
@@ -30,9 +36,12 @@ command: Command,
3036
*/
3137
cwd?: string,
3238
/**
33-
* Dependencies of this task. Use `package-name#task-name` to refer to tasks in other packages.
39+
* Dependencies of this task.
40+
*
41+
* String entries keep same-package / `package-name#task-name` behavior.
42+
* Object entries select direct workspace dependency packages from package.json fields.
3443
*/
35-
dependsOn?: Array<string>, } & ({
44+
dependsOn?: Array<DependsOnEntry>, } & ({
3645
/**
3746
* Whether to cache the task
3847
*/
@@ -95,6 +104,16 @@ scripts?: boolean,
95104
*/
96105
tasks?: boolean, };
97106

107+
export type UserPackageDependency = {
108+
/**
109+
* Task name to run in dependency packages.
110+
*/
111+
task: string,
112+
/**
113+
* Package.json dependency field or fields to use when selecting direct dependency packages.
114+
*/
115+
from: DependsOnFrom, };
116+
98117
export type RunConfig = {
99118
/**
100119
* Root-level cache configuration.

crates/vite_task_graph/src/config/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use rustc_hash::FxHashSet;
77
use serde::Serialize;
88
pub use user::{
99
AutoTracking, Command, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig,
10-
UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry,
11-
UserRunConfig, UserTaskConfig, UserTaskDefinition,
10+
UserCacheConfig, UserDependencyType, UserDependsOnEntry, UserDependsOnFrom,
11+
UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry,
12+
UserPackageDependency, UserRunConfig, UserTaskConfig, UserTaskDefinition,
1213
};
1314
use vite_path::AbsolutePath;
1415
use vite_str::Str;

crates/vite_task_graph/src/config/user.rs

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use rustc_hash::FxHashMap;
77
use serde::Deserialize;
88
#[cfg(all(test, not(clippy)))]
99
use ts_rs::TS;
10+
use vec1::Vec1;
1011
use vite_path::RelativePathBuf;
1112
use vite_str::Str;
1213

@@ -65,6 +66,70 @@ pub enum UserInputEntry {
6566
/// Default (when field omitted): `[{auto: true}]` - infer from file accesses.
6667
pub type UserInputsConfig = Vec<UserInputEntry>;
6768

69+
/// A supported package.json dependency field for package dependency selection.
70+
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
71+
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
72+
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependencyType"))]
73+
#[serde(rename_all = "camelCase")]
74+
pub enum UserDependencyType {
75+
/// Traverse dependencies declared in the package.json `dependencies` field.
76+
Dependencies,
77+
/// Traverse dependencies declared in the package.json `devDependencies` field.
78+
DevDependencies,
79+
/// Traverse dependencies declared in the package.json `peerDependencies` field.
80+
PeerDependencies,
81+
}
82+
83+
/// The `from` selector for object-form `dependsOn` entries.
84+
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
85+
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
86+
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependsOnFrom"))]
87+
#[serde(untagged)]
88+
pub enum UserDependsOnFrom {
89+
/// Select one package.json dependency field.
90+
Single(UserDependencyType),
91+
/// Select the union of multiple package.json dependency fields.
92+
Multiple(
93+
#[cfg_attr(all(test, not(clippy)), ts(as = "Vec<UserDependencyType>"))]
94+
Vec1<UserDependencyType>,
95+
),
96+
}
97+
98+
impl UserDependsOnFrom {
99+
#[must_use]
100+
pub fn as_slice(&self) -> &[UserDependencyType] {
101+
match self {
102+
Self::Single(dependency_type) => std::slice::from_ref(dependency_type),
103+
Self::Multiple(dependency_types) => dependency_types,
104+
}
105+
}
106+
}
107+
108+
/// Object form for `dependsOn` entries that select direct workspace package dependencies.
109+
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
110+
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
111+
#[cfg_attr(all(test, not(clippy)), derive(TS))]
112+
#[serde(deny_unknown_fields)]
113+
pub struct UserPackageDependency {
114+
/// Task name to run in dependency packages.
115+
pub task: Str,
116+
117+
/// Package.json dependency field or fields to use when selecting direct dependency packages.
118+
pub from: UserDependsOnFrom,
119+
}
120+
121+
/// A single `dependsOn` entry.
122+
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
123+
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
124+
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependsOnEntry"))]
125+
#[serde(untagged)]
126+
pub enum UserDependsOnEntry {
127+
/// Same-package task or `package#task` specifier.
128+
Task(Str),
129+
/// Direct package dependency selection entry.
130+
Package(UserPackageDependency),
131+
}
132+
68133
/// A single output entry in the `output` array.
69134
///
70135
/// Outputs can be:
@@ -168,8 +233,11 @@ pub struct UserTaskOptions {
168233
#[serde(rename = "cwd")]
169234
pub cwd_relative_to_package: Option<RelativePathBuf>,
170235

171-
/// Dependencies of this task. Use `package-name#task-name` to refer to tasks in other packages.
172-
pub depends_on: Option<Arc<[Str]>>,
236+
/// Dependencies of this task.
237+
///
238+
/// String entries keep same-package / `package-name#task-name` behavior.
239+
/// Object entries select direct workspace dependency packages from package.json fields.
240+
pub depends_on: Option<Arc<[UserDependsOnEntry]>>,
173241

174242
/// Cache-related fields
175243
#[serde(flatten)]
@@ -510,10 +578,73 @@ mod tests {
510578
);
511579
let options = user_config.options;
512580
assert_eq!(options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
513-
assert_eq!(options.depends_on.as_ref().unwrap().as_ref(), [Str::from("build")]);
581+
assert_eq!(
582+
options.depends_on.as_ref().unwrap().as_ref(),
583+
[UserDependsOnEntry::Task(Str::from("build"))]
584+
);
514585
assert_eq!(options.cache_config, UserCacheConfig::Disabled { cache: MustBe!(false) });
515586
}
516587

588+
#[test]
589+
fn test_depends_on_package_dependency_single_from() {
590+
let user_config_json = json!({
591+
"command": "echo test",
592+
"dependsOn": [{ "task": "build", "from": "dependencies" }]
593+
});
594+
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
595+
assert_eq!(
596+
user_config.options.depends_on.as_ref().unwrap().as_ref(),
597+
[UserDependsOnEntry::Package(UserPackageDependency {
598+
task: "build".into(),
599+
from: UserDependsOnFrom::Single(UserDependencyType::Dependencies),
600+
})]
601+
);
602+
}
603+
604+
#[test]
605+
fn test_depends_on_package_dependency_array_from() {
606+
let user_config_json = json!({
607+
"command": "echo test",
608+
"dependsOn": [{
609+
"task": "build",
610+
"from": ["dependencies", "devDependencies", "peerDependencies"]
611+
}]
612+
});
613+
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
614+
assert_eq!(
615+
user_config.options.depends_on.as_ref().unwrap().as_ref(),
616+
[UserDependsOnEntry::Package(UserPackageDependency {
617+
task: "build".into(),
618+
from: UserDependsOnFrom::Multiple(
619+
Vec1::try_from_vec(vec![
620+
UserDependencyType::Dependencies,
621+
UserDependencyType::DevDependencies,
622+
UserDependencyType::PeerDependencies,
623+
])
624+
.unwrap()
625+
),
626+
})]
627+
);
628+
}
629+
630+
#[test]
631+
fn test_depends_on_package_dependency_empty_from_error() {
632+
let user_config_json = json!({
633+
"command": "echo test",
634+
"dependsOn": [{ "task": "build", "from": [] }]
635+
});
636+
assert!(serde_json::from_value::<UserTaskConfig>(user_config_json).is_err());
637+
}
638+
639+
#[test]
640+
fn test_depends_on_package_dependency_unknown_from_error() {
641+
let user_config_json = json!({
642+
"command": "echo test",
643+
"dependsOn": [{ "task": "build", "from": "optionalDependencies" }]
644+
});
645+
assert!(serde_json::from_value::<UserTaskConfig>(user_config_json).is_err());
646+
}
647+
517648
#[test]
518649
fn test_task_invalid_shorthand_error() {
519650
let user_config_json = json!({

0 commit comments

Comments
 (0)