Skip to content

Commit 46ddbb1

Browse files
wan9chicodex
andcommitted
feat(task-graph): materialize object dependsOn edges
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 6c9dc4a commit 46ddbb1

26 files changed

Lines changed: 901 additions & 36 deletions

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 can select direct workspace package dependencies by `dependencies`, `devDependencies`, and `peerDependencies`, and are materialized as task graph edges.
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: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ How `vp run` decides which tasks to run and in what order.
77
When `vp` starts, it builds two data structures from the workspace:
88

99
1. **Package graph** — which packages depend on which. Built from `package.json` dependency fields.
10-
2. **Task graph** — which tasks exist and their explicit `dependsOn` relationships. Built from `vite-task.json` and `package.json` scripts.
10+
2. **Task graph** — which tasks exist and their explicit `dependsOn` relationships. Built from `vite.config.*` and `package.json` scripts.
1111

1212
Both are built once and reused for every query, including nested `vp run` calls inside task scripts.
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
19-
// packages/app/vite-task.json
19+
// packages/app/vite.config.*
2020
{
2121
"tasks": {
2222
"build": {
@@ -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:
@@ -174,7 +201,7 @@ So we clone the `DiGraphMap` once and mutate the clone. We iterate the original
174201
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.
175202

176203
```jsonc
177-
// packages/app/vite-task.json
204+
// packages/app/vite.config.*
178205
{
179206
"tasks": {
180207
"build": {
@@ -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_graph_ser = { workspace = true }
2223
vite_path = { workspace = true }
2324
vite_str = { 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)