Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Changelog

- **Added** `dependsOn` can now run the specified task in each direct workspace package listed in `dependencies`, `devDependencies`, or `peerDependencies`; for example, `{ "task": "build", "from": ["dependencies", "devDependencies"] }` runs `build` in each direct dependency and dev dependency ([#467](https://github.com/voidzero-dev/vite-task/pull/467), [#469](https://github.com/voidzero-dev/vite-task/pull/469)).
- **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** Object-form `dependsOn` entries can select direct workspace package dependencies by `dependencies`, `devDependencies`, and `peerDependencies`, and are materialized as task graph edges.
- **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)).
- **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)).
- **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)).
Expand Down
4 changes: 2 additions & 2 deletions crates/vite_task/docs/boolean-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `dependsOn` dependencies
- `--ignore-depends-on` — Skip explicit `dependsOn` dependencies
- `--verbose` / `-v` — Show full detailed summary after execution
- `--cache` / `--no-cache` — Force caching on or off for all tasks and scripts

Expand Down Expand Up @@ -39,7 +39,7 @@ vp run build -t
# Run in workspace root
vp run build -w

# Skip dependsOn dependencies
# Skip explicit dependsOn edges
vp run build --ignore-depends-on

# Verbose output
Expand Down
72 changes: 35 additions & 37 deletions crates/vite_task/docs/task-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ 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 string-form `dependsOn` relationships. Built from `vite.config.*` and `package.json` scripts.
2. **Task graph** — which tasks exist and their explicit `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. String-form
`dependsOn` declarations become edges in this graph:
The task graph contains a node for every task in every package, and edges only for explicit `dependsOn` declarations.

```jsonc
// packages/app/vite.config.*
Expand All @@ -38,8 +37,33 @@ 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.

Object-form `dependsOn` entries are also explicit task dependencies. At startup,
they are resolved against the declaring package's direct `package.json`
dependency fields and materialized as task graph edges:

```jsonc
// packages/app/vite.config.*
{
"tasks": {
"test": {
"command": "vitest run",
"dependsOn": [{ "task": "build", "from": ["dependencies", "devDependencies"] }],
},
},
}
```

If `app` directly depends on `ui` and `shared`, and both packages have `build`,
the task graph contains:

```
app#test ──dependsOn──> ui#build
app#test ──dependsOn──> shared#build
```

Dependency packages without the requested task are skipped. Recursive expansion
comes from dependency tasks declaring their own `dependsOn` entries.

## What happens when you run a query

Expand Down Expand Up @@ -77,7 +101,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. Expand `dependsOn` from these tasks (may pull in tasks from outside the selected packages).
4. Follow explicit `dependsOn` edges outward 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.

Expand Down Expand Up @@ -172,11 +196,9 @@ The package subgraph is already a lightweight `DiGraphMap<PackageNodeIndex, ()>`

So we clone the `DiGraphMap` once and mutate the clone. We iterate the original (stable node order) while modifying the clone.

## `dependsOn` expansion
## Explicit dependency expansion

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.
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.

```jsonc
// packages/app/vite.config.*
Expand All @@ -193,31 +215,7 @@ 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`.)

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.
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.

## Nested `vp run`

Expand All @@ -242,7 +240,7 @@ The nested query produces its own execution subgraph, which gets embedded inside
```
Startup (once):
workspace files ──> package graph ──> task graph
(dependencies) (tasks + string dependsOn edges)
(dependencies) (tasks + dependsOn edges)

Per query:
CLI flags ──> PackageQuery
Expand All @@ -254,7 +252,7 @@ Per query:
task graph ────> task execution graph
(map packages to tasks,
skip-intermediate reconnection,
dependsOn expansion)
explicit dep expansion)
execution plan
Expand Down
3 changes: 1 addition & 2 deletions crates/vite_task_graph/run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ cwd?: string,
* Dependencies of this task.
*
* String entries keep same-package / `package-name#task-name` behavior.
* Object entries run a task name in direct workspace dependency
* packages selected by package.json dependency fields.
* Object entries select direct workspace dependency packages from package.json fields.
*/
dependsOn?: Array<DependsOnEntry>, } & ({
/**
Expand Down
3 changes: 1 addition & 2 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ 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 string-form entries are represented by task graph
/// edges, and package dependency entries are stored separately on the indexed task graph.
/// `depends_on` is not included here because it's represented by the edges of the task graph.
#[derive(Debug, Serialize)]
pub struct ResolvedTaskConfig {
/// The command or commands to run for this task.
Expand Down
43 changes: 4 additions & 39 deletions crates/vite_task_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ pub enum UserDependencyType {
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "DependsOnFrom"))]
#[serde(untagged)]
pub enum UserDependsOnFrom {
/// Traverse one package.json dependency field.
/// Select one package.json dependency field.
Single(UserDependencyType),
/// Traverse the union of multiple package.json dependency fields.
/// Select the union of multiple package.json dependency fields.
Multiple(
#[cfg_attr(all(test, not(clippy)), ts(as = "Vec<UserDependencyType>"))]
Vec1<UserDependencyType>,
Expand All @@ -105,7 +105,7 @@ impl UserDependsOnFrom {
}
}

/// Object form for `dependsOn` entries that select workspace package dependencies.
/// Object form for `dependsOn` entries that select direct workspace package dependencies.
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
#[cfg_attr(all(test, not(clippy)), derive(TS))]
Expand Down Expand Up @@ -236,8 +236,7 @@ pub struct UserTaskOptions {
/// Dependencies of this task.
///
/// String entries keep same-package / `package-name#task-name` behavior.
/// Object entries run a task name in direct workspace dependency
/// packages selected by package.json dependency fields.
/// Object entries select direct workspace dependency packages from package.json fields.
pub depends_on: Option<Arc<[UserDependsOnEntry]>>,

/// Cache-related fields
Expand Down Expand Up @@ -637,49 +636,15 @@ mod tests {
assert!(serde_json::from_value::<UserTaskConfig>(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::<UserTaskConfig>(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::<UserTaskConfig>(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::<UserTaskConfig>(user_config_json).is_err());
}

#[test]
fn test_depends_on_package_dependency_task_name_allows_hash() {
let user_config_json = json!({
"command": "echo test",
"dependsOn": [{ "task": "@scope/pkg#build", "from": "dependencies" }]
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config.options.depends_on.as_ref().unwrap().as_ref(),
[UserDependsOnEntry::Package(UserPackageDependency {
task: "@scope/pkg#build".into(),
from: UserDependsOnFrom::Single(UserDependencyType::Dependencies),
})]
);
}

#[test]
fn test_task_invalid_shorthand_error() {
let user_config_json = json!({
Expand Down
71 changes: 42 additions & 29 deletions crates/vite_task_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use config::{
ResolvedGlobalCacheConfig, ResolvedTaskConfig, UserRunConfig, UserTaskConfig,
UserTaskDefinition,
};
use petgraph::graph::{DefaultIx, DiGraph, EdgeIndex, IndexType, NodeIndex};
use petgraph::{
graph::{DefaultIx, DiGraph, EdgeIndex, IndexType, NodeIndex},
visit::EdgeRef as _,
};
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::Serialize;
pub use specifier::TaskSpecifier;
Expand All @@ -29,9 +32,10 @@ use crate::{

/// The type of a task dependency edge in the task graph.
///
/// Currently only `Explicit` is produced (from `dependsOn` in the task config).
/// Topological ordering is handled at query time via the package subgraph rather
/// than by pre-computing edges in the task graph.
/// All edges are produced from `dependsOn` in task config, including string-form
/// task specifiers and object-form package dependency selections. Topological
/// ordering is handled at query time via the package subgraph rather than by
/// pre-computing package edges in the task graph.
#[derive(Debug, Clone, Copy, Serialize)]
pub struct TaskDependencyType;

Expand Down Expand Up @@ -185,10 +189,9 @@ pub type TaskNodeIndex = NodeIndex<TaskIx>;
pub type TaskEdgeIndex = EdgeIndex<TaskIx>;

/// 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]>,
struct PackageDependencyEntry {
task_name: Str,
dependency_types: Box<[DependencyType]>,
}

impl PackageDependencyEntry {
Expand Down Expand Up @@ -235,15 +238,6 @@ pub struct IndexedTaskGraph {
/// Reverse map: task node index → task id (for hook lookup)
task_ids_by_node_index: FxHashMap<TaskNodeIndex, TaskId>,

/// Object-form `dependsOn` entries keyed by the task that declared them.
///
/// These stay anchored to their source task and are materialized when
/// `query_tasks` builds a per-query `TaskExecutionGraph`. Keeping them out
/// of the global `task_graph` avoids leaking package dependency selection
/// into direct runs of dependency tasks.
pub(crate) package_dependency_entries_by_node_index:
FxHashMap<TaskNodeIndex, Arc<[PackageDependencyEntry]>>,

/// Global cache configuration resolved from the workspace root config.
resolved_global_cache: ResolvedGlobalCacheConfig,

Expand Down Expand Up @@ -274,8 +268,7 @@ impl IndexedTaskGraph {

let package_graph = vite_workspace::load_package_graph(workspace_root)?;

// Record `dependsOn` entries for each task node to add dependencies after all
// tasks are indexed.
// Record dependency declarations for each task node to add explicit dependencies later.
let mut task_ids_with_depends_on_entries: Vec<(TaskId, Option<Arc<[UserDependsOnEntry]>>)> =
Vec::new();

Expand Down Expand Up @@ -431,16 +424,15 @@ 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 string-form dependencies as explicit task graph edges, and keep
// object-form package dependency entries anchored to their source task.
// Add explicit dependencies. String-form entries resolve to fixed task
// specifier edges. Object-form entries select direct package dependency
// tasks and materialize those selections as global task graph edges.
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];
let mut package_dependency_entries = Vec::<PackageDependencyEntry>::new();
for entry in depends_on_entries.iter().flat_map(|entries| entries.iter()) {
match entry {
UserDependsOnEntry::Task(specifier) => {
Expand All @@ -463,15 +455,11 @@ impl IndexedTaskGraph {
);
}
UserDependsOnEntry::Package(entry) => {
package_dependency_entries
.push(PackageDependencyEntry::from_user_config(entry));
let entry = PackageDependencyEntry::from_user_config(entry);
me.add_package_dependency_edges(from_node_index, &from_task_id, &entry);
}
}
}
if !package_dependency_entries.is_empty() {
me.package_dependency_entries_by_node_index
.insert(from_node_index, Arc::from(package_dependency_entries));
}
}

// Topological dependency edges are no longer pre-computed here.
Expand Down Expand Up @@ -531,6 +519,31 @@ impl IndexedTaskGraph {
Ok(*node_index)
}

fn add_package_dependency_edges(
&mut self,
from_node_index: TaskNodeIndex,
from_task_id: &TaskId,
entry: &PackageDependencyEntry,
) {
let package_graph = self.indexed_package_graph.package_graph();
let dependency_tasks = package_graph
.edges(from_task_id.package_index)
.filter(|edge| entry.dependency_types.contains(edge.weight()))
.filter_map(|edge| {
self.node_indices_by_task_id
.get(&TaskId {
package_index: edge.target(),
task_name: entry.task_name.clone(),
})
.copied()
})
.collect::<Vec<_>>();

for to_node_index in dependency_tasks {
self.task_graph.update_edge(from_node_index, to_node_index, TaskDependencyType);
Comment on lines +542 to +543

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve ordering between selected dependency tasks

When an object-form dependsOn selects multiple direct dependencies that themselves have a package edge between them (for example, app depends on both ui and shared, and ui depends on shared), this loop only materializes app#task -> ui#build and app#task -> shared#build. The query-time expansion this replaces also added the dependency-task edge between selected packages, so now those builds can run concurrently and ui#build can start before shared#build has produced its artifacts. Please keep the package ordering for the dependency tasks, or keep that part of the expansion query-scoped.

Useful? React with 👍 / 👎.

}
}

#[must_use]
pub const fn task_graph(&self) -> &TaskGraph {
&self.task_graph
Expand Down
Loading
Loading