Skip to content

Commit 9e1287e

Browse files
branchseerclaude
andauthored
feat: filter packages with --filter (#176)
## Summary Add pnpm-compatible `--filter` flag to `vp run` for selecting which packages to run tasks in. Supports: - **Package name**: exact (`--filter @test/app`) and glob (`--filter @test/*`) - **Directory**: exact (`--filter ./packages/app`), glob (`--filter ./packages/*`), relative (`.`, `..`) - **Braced paths**: `{./path}` for traversal on paths, `name{./dir}` for name + directory intersection - **Dependency traversal**: `foo...` (dependencies), `...foo` (dependents), `^` to exclude self, `...foo...` (both) - **Exclusion**: `!foo` to exclude packages from the result - **Multiple filters**: `--filter a --filter b` (union); `--filter "a b"` (whitespace split, pnpm compat) - **`-w` / `--workspace-root`**: select workspace root; additive with `--filter`, redundant with `-r`, supports `-t` for root + transitive deps - **Nested expansion**: `vp run --filter .... build` inside package scripts is expanded in the plan ### Architecture: `PackageQueryArgs` → `PackageQuery` → `FilterResolution` Package selection lives in `vite_workspace`, not in the task runner. Any command that needs to pick packages can reuse it. **Step 1 — Parse CLI flags into a query.** `PackageQueryArgs` is a clap struct with `-r`, `-t`, `-w`, and `--filter`. Embed it via `#[clap(flatten)]`, then call `into_package_query()`: ```rust // in any command definition (e.g. `vp exec`, `vp run`) #[derive(clap::Args)] struct MyCommand { #[clap(flatten)] packages: PackageQueryArgs, } // at runtime — package_name comes from `pkg#task` syntax (e.g. "app" in `vp run app#build`). // commands that don't use `pkg#task` (like `vp exec`) pass None. let query: PackageQuery = args.packages.into_package_query(package_name, &cwd)?; ``` `into_package_query` validates flag combinations (e.g. `--filter` + `-r` is an error), splits whitespace, parses each `--filter` token, and returns an opaque `PackageQuery`. **Step 2 — Resolve the query against the package graph.** ```rust let resolution: FilterResolution = indexed_package_graph.resolve_query(&query); // resolution.package_subgraph — the selected packages + edges between them // resolution.unmatched_selectors — filter strings that matched nothing (for warnings) ``` `PackageQuery` is opaque — callers don't inspect its internals. They just pass it to `resolve_query` and get back the selected subgraph. Future commands like `vp exec` only need steps 1 and 2 — they get the selected packages without depending on the task graph at all. **Step 3 (task runner only) — Map packages to tasks.** The task-graph layer maps the package subgraph to task nodes, reconnecting across packages that lack the requested task. Only `vp run` needs this step. ### `unmatched_selectors` tracks original filter strings Each `--filter` token is stored as a `source: Option<Str>` inside the parsed filter. Synthetic filters (implicit cwd, `-w`) get `source: None` since the user didn't type them. When a filter matches nothing, `resolve_query` collects the original string into `unmatched_selectors: Vec<Str>` so the caller can show a warning like: ``` warn: --filter "typo-pkg" matched no packages ``` This works correctly even with whitespace splitting (`--filter "a b"` → two filters, each with its own source string). ## Test plan - [x] 48 unit tests in `package_filter` for `parse_filter`, `into_package_query`, `resolve_directory_pattern`, and source tracking — covering all selector types, traversal modes, flag combinations, path normalization, and error cases - [x] 40 plan snapshot tests across `filter-workspace` (35) and `transitive-skip-intermediate` (5) fixtures - [x] Existing plan and e2e snapshot tests still pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1128edc commit 9e1287e

File tree

107 files changed

+4278
-900
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+4278
-900
lines changed

Cargo.lock

Lines changed: 13 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ os_str_bytes = "7.1.1"
9191
ouroboros = "0.18.5"
9292
owo-colors = { version = "4.1.0", features = ["supports-colors"] }
9393
passfd = { git = "https://github.com/polachok/passfd", rev = "d55881752c16aced1a49a75f9c428d38d3767213", default-features = false }
94+
path-clean = "1.0.1"
9495
pathdiff = "0.2.3"
9596
petgraph = "0.8.2"
9697
phf = { version = "0.11.3", features = ["macros"] }
@@ -145,7 +146,7 @@ vite_task_graph = { path = "crates/vite_task_graph" }
145146
vite_task_plan = { path = "crates/vite_task_plan" }
146147
vite_workspace = { path = "crates/vite_workspace" }
147148
vt100 = "0.16.2"
148-
wax = "0.6.0"
149+
wax = "0.7.0"
149150
which = "8.0.0"
150151
widestring = "1.2.0"
151152
winapi = "0.3.9"

crates/vite_glob/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod error;
44
use std::path::Path;
55

66
pub use error::Error;
7-
use wax::{Glob, Pattern};
7+
use wax::{Glob, Program};
88

99
/// If there are no negated patterns, it will follow the first match wins semantics.
1010
/// Otherwise, it will follow the last match wins semantics.

crates/vite_graph_ser/src/lib.rs

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ pub trait GetKey {
1717
}
1818

1919
#[derive(Serialize)]
20-
#[serde(bound = "E: Serialize, N: Serialize")]
21-
struct DiGraphNodeItem<'a, N: GetKey, E> {
20+
#[serde(bound = "N: Serialize")]
21+
struct DiGraphNodeItem<'a, N: GetKey> {
2222
key: N::Key<'a>,
2323
node: &'a N,
24-
neighbors: Vec<(N::Key<'a>, &'a E)>,
24+
neighbors: Vec<N::Key<'a>>,
2525
}
2626

2727
/// A wrapper around `DiGraph` that serializes nodes by their keys.
28+
///
29+
/// Only node connectivity is recorded — edge weights are ignored in the output.
2830
#[derive(Serialize)]
2931
#[serde(transparent)]
30-
pub struct SerializeByKey<'a, N: GetKey + Serialize, E: Serialize, Ix: petgraph::graph::IndexType>(
32+
pub struct SerializeByKey<'a, N: GetKey + Serialize, E, Ix: petgraph::graph::IndexType>(
3133
#[serde(serialize_with = "serialize_by_key")] pub &'a DiGraph<N, E, Ix>,
3234
);
3335

@@ -45,25 +47,20 @@ pub struct SerializeByKey<'a, N: GetKey + Serialize, E: Serialize, Ix: petgraph:
4547
///
4648
/// # Panics
4749
/// Panics if an edge references a node index not present in the graph.
48-
pub fn serialize_by_key<
49-
N: GetKey + Serialize,
50-
E: Serialize,
51-
Ix: petgraph::graph::IndexType,
52-
S: Serializer,
53-
>(
50+
pub fn serialize_by_key<N: GetKey + Serialize, E, Ix: petgraph::graph::IndexType, S: Serializer>(
5451
graph: &DiGraph<N, E, Ix>,
5552
serializer: S,
5653
) -> Result<S::Ok, S::Error> {
57-
let mut items = Vec::<DiGraphNodeItem<'_, N, E>>::with_capacity(graph.node_count());
54+
let mut items = Vec::<DiGraphNodeItem<'_, N>>::with_capacity(graph.node_count());
5855
for (node_idx, node) in graph.node_references() {
59-
let mut neighbors = Vec::<(N::Key<'_>, &E)>::new();
56+
let mut neighbors = Vec::<N::Key<'_>>::new();
6057

6158
for edge in graph.edges(node_idx) {
6259
let target_idx = edge.target();
6360
let target_node = graph.node_weight(target_idx).unwrap();
64-
neighbors.push((target_node.key().map_err(serde::ser::Error::custom)?, edge.weight()));
61+
neighbors.push(target_node.key().map_err(serde::ser::Error::custom)?);
6562
}
66-
neighbors.sort_unstable_by(|a, b| a.0.cmp(&b.0));
63+
neighbors.sort_unstable();
6764
items.push(DiGraphNodeItem {
6865
key: node.key().map_err(serde::ser::Error::custom)?,
6966
node,
@@ -101,19 +98,19 @@ mod tests {
10198
#[derive(Serialize)]
10299
struct GraphWrapper {
103100
#[serde(serialize_with = "serialize_by_key")]
104-
graph: DiGraph<TestNode, &'static str>,
101+
graph: DiGraph<TestNode, ()>,
105102
}
106103

107104
#[test]
108105
fn test_serialize_graph_happy_path() {
109-
let mut graph = DiGraph::<TestNode, &'static str>::new();
106+
let mut graph = DiGraph::<TestNode, ()>::new();
110107
let a = graph.add_node(TestNode { id: "a", value: 1 });
111108
let b = graph.add_node(TestNode { id: "b", value: 2 });
112109
let c = graph.add_node(TestNode { id: "c", value: 3 });
113110

114-
graph.add_edge(a, b, "a->b");
115-
graph.add_edge(a, c, "a->c");
116-
graph.add_edge(b, c, "b->c");
111+
graph.add_edge(a, b, ());
112+
graph.add_edge(a, c, ());
113+
graph.add_edge(b, c, ());
117114

118115
let json = serde_json::to_value(GraphWrapper { graph }).unwrap();
119116
assert_eq!(
@@ -123,12 +120,12 @@ mod tests {
123120
{
124121
"key": "a",
125122
"node": {"id": "a", "value": 1},
126-
"neighbors": [["b", "a->b"], ["c", "a->c"]]
123+
"neighbors": ["b", "c"]
127124
},
128125
{
129126
"key": "b",
130127
"node": {"id": "b", "value": 2},
131-
"neighbors": [["c", "b->c"]]
128+
"neighbors": ["c"]
132129
},
133130
{
134131
"key": "c",

0 commit comments

Comments
 (0)