From 8a90e1550a78677243dc37387ff77d75a50f548d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 09:50:35 +0000 Subject: [PATCH 1/5] docs(rfc): add run-many concurrency RFC --- docs/rfcs/run-many.md | 126 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/rfcs/run-many.md diff --git a/docs/rfcs/run-many.md b/docs/rfcs/run-many.md new file mode 100644 index 00000000..cca2ec3c --- /dev/null +++ b/docs/rfcs/run-many.md @@ -0,0 +1,126 @@ +# RFC: `run-many` + +## Two scheduling structures + +vite-task schedules work using two structures nested inside each other: a **graph** (what the scheduler runs) and a **tree** (recursion of graphs). + +### Graph + +An `ExecutionGraph` is a DAG of tasks with its own concurrency limit and its own semaphore. The scheduler runs nodes whose dependencies have finished, up to the limit. Dependency-aware parallelism lives here. + +`vp run -r build` when `app` depends on `lib1` and `lib2`: + +```mermaid +flowchart TD + subgraph G["ExecutionGraph · one semaphore"] + direction TB + L1[lib1#build] --> A[app#build] + L2[lib2#build] --> A + end +``` + +Arrows mean "runs before". `lib1#build` and `lib2#build` run in parallel; `app#build` waits for both. + +### Tree + +A task's `command` splits on `&&` into **items** that run in order. An item is either a leaf process or `Expanded`: a nested `ExecutionGraph` built from a `vp run` inside the command. Every `Expanded` item gets its own graph with its own semaphore. + +After #381, `command: ["vp run build", "vp run test"]` is shorthand for `"vp run build && vp run test"`. So `string[]` is how you sequence siblings in the tree. + +`"ci": { "command": ["vp run build", "vp run test"] }`: + +```mermaid +flowchart TD + root["ci · items run sequentially"] + root -. item 1 .-> G1 + root -. item 2 .-> G2 + + subgraph G1["ExecutionGraph · vp run build"] + direction TB + b1[lib1#build] --> ba[app#build] + b2[lib2#build] --> ba + end + + subgraph G2["ExecutionGraph · vp run test"] + direction TB + t1[lib1#test] --> ta[app#test] + t2[lib2#test] --> ta + end +``` + +`G1` finishes before `G2` starts. The two graphs are isolated: separate semaphores, nothing connects them. + +## What's missing + +You can get dependency-aware parallelism inside one graph, and you can sequence siblings in the tree. What you can't say today is: + +> Run several tasks plus their dependencies as one DAG. Fan out where they're independent, serialize where they actually depend on each other. + +Each `vp run` produces a graph with a single requested root, so this can't happen inside one tree node. Siblings under `items` run sequentially, so it can't happen across tree nodes either. `--parallel` works around it by dropping every dependency edge, which is too blunt when some deps are real. + +## `run-many` + +`vp run-many ...` builds one graph with multiple requested roots. All `run` flags apply. The graph is the union of the per-task graphs, dedup'd by node. + +`vp run-many build test` where `test` depends on `build`: + +```mermaid +flowchart TD + subgraph G["ExecutionGraph · vp run-many build test"] + direction TB + lb[lib1#build] --> la[app#build] + lb --> lt[lib1#test] + la --> at[app#test] + lt --> at + end +``` + +`lib1#test` starts the moment `lib1#build` finishes. It doesn't wait for `app#build`. `--parallel` can't give you this schedule because it would drop the `test → build` edge entirely. `["vp run build", "vp run test"]` can't either, because no test starts until every build is done. + +## Composition + +`string[]` for sequencing and `run-many` for fan-out compose cleanly: + +```jsonc +{ + "ci": { + "command": [ + "vp run prepare", + "vp run-many lint test build typecheck", + "vp run publish" + ] + } +} +``` + +```mermaid +flowchart TD + ci["ci · items run sequentially"] + ci -. 1 .-> P + ci -. 2 .-> M + ci -. 3 .-> Pub + + subgraph P["ExecutionGraph · prepare"] + p1[prepare] + end + + subgraph M["ExecutionGraph · run-many (wide)"] + direction TB + b[build] --> t[test] + b --> l[lint] + b --> ty[typecheck] + end + + subgraph Pub["ExecutionGraph · publish"] + pub[publish] + end +``` + +`prepare` runs alone. Then the wide graph: `lint`, `test`, `typecheck` start the moment `build` finishes, all sharing one semaphore. Then `publish`. + +| Primitive | Effect | +| ----------------- | ------------------------------------------------- | +| `ExecutionGraph` | Dependency-aware parallelism within one semaphore | +| `command: [...]` | Sequence sibling graphs in the tree | +| `vp run` | Spawn one child graph | +| `vp run-many` | Spawn one wide child graph (multiple roots) | From 90622922c77385797e436ad9797cff54aa9efd8b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 10:04:21 +0000 Subject: [PATCH 2/5] docs(rfc): rewrite run-many with vite-plus task names, clearer sequence/parallel callouts --- docs/rfcs/run-many.md | 146 +++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 53 deletions(-) diff --git a/docs/rfcs/run-many.md b/docs/rfcs/run-many.md index cca2ec3c..d1249193 100644 --- a/docs/rfcs/run-many.md +++ b/docs/rfcs/run-many.md @@ -6,88 +6,108 @@ vite-task schedules work using two structures nested inside each other: a **grap ### Graph -An `ExecutionGraph` is a DAG of tasks with its own concurrency limit and its own semaphore. The scheduler runs nodes whose dependencies have finished, up to the limit. Dependency-aware parallelism lives here. +An `ExecutionGraph` is a DAG of tasks. The scheduler walks it: a task starts once all of its dependencies have finished, and the number of tasks running at the same time is capped by the concurrency limit (default 4). -`vp run -r build` when `app` depends on `lib1` and `lib2`: +`vp run-many vite#build vite#build-types`, where both depend on `rolldown#build-node`: ```mermaid flowchart TD - subgraph G["ExecutionGraph · one semaphore"] + subgraph G["ExecutionGraph"] direction TB - L1[lib1#build] --> A[app#build] - L2[lib2#build] --> A + rn[rolldown#build-node] --> vb[vite#build] + rn --> vbt[vite#build-types] end ``` -Arrows mean "runs before". `lib1#build` and `lib2#build` run in parallel; `app#build` waits for both. +Arrows mean "runs before". + +- **In sequence:** `rolldown#build-node` runs first. +- **In parallel:** once it's done, `vite#build` and `vite#build-types` run together. + +The two vite tasks are independent of each other, but the scheduler still honors their shared dependency on `rolldown#build-node`. ### Tree -A task's `command` splits on `&&` into **items** that run in order. An item is either a leaf process or `Expanded`: a nested `ExecutionGraph` built from a `vp run` inside the command. Every `Expanded` item gets its own graph with its own semaphore. +A task's `command` splits on `&&` into **items** that run one after another. An item is either a leaf process, or `Expanded`: a nested `ExecutionGraph` built from a `vp run` inside the command. Every `Expanded` item is its own independent scheduling unit. -After #381, `command: ["vp run build", "vp run test"]` is shorthand for `"vp run build && vp run test"`. So `string[]` is how you sequence siblings in the tree. +After #381, `command: ["vp run a", "vp run b"]` is shorthand for `"vp run a && vp run b"`. So `string[]` is how you sequence siblings in the tree. -`"ci": { "command": ["vp run build", "vp run test"] }`: +```jsonc +"build-vite": { + "command": [ + "vp run vite#build", + "vp run vite#build-types" + ] +} +``` ```mermaid flowchart TD - root["ci · items run sequentially"] + root["build-vite · items run one at a time"] root -. item 1 .-> G1 root -. item 2 .-> G2 - subgraph G1["ExecutionGraph · vp run build"] + subgraph G1["ExecutionGraph · vp run vite#build"] direction TB - b1[lib1#build] --> ba[app#build] - b2[lib2#build] --> ba + rn1[rolldown#build-node] --> vb[vite#build] end - subgraph G2["ExecutionGraph · vp run test"] + subgraph G2["ExecutionGraph · vp run vite#build-types"] direction TB - t1[lib1#test] --> ta[app#test] - t2[lib2#test] --> ta + rn2[rolldown#build-node] --> vbt[vite#build-types] end ``` -`G1` finishes before `G2` starts. The two graphs are isolated: separate semaphores, nothing connects them. +- **In sequence:** item 1 finishes completely, then item 2 starts. +- **In parallel:** nothing. `vite#build` and `vite#build-types` are independent, but `&&` walls them off from each other. + +(Cache will spare you the repeated `rolldown#build-node` work, but the two vite tasks themselves still wait for each other.) ## What's missing -You can get dependency-aware parallelism inside one graph, and you can sequence siblings in the tree. What you can't say today is: +You can get dependency-aware parallelism inside one graph. You can sequence siblings in the tree. What you can't say today is: -> Run several tasks plus their dependencies as one DAG. Fan out where they're independent, serialize where they actually depend on each other. +> Run several tasks plus their dependencies as one graph. Run them at the same time where they're independent, in order where they actually depend on each other. -Each `vp run` produces a graph with a single requested root, so this can't happen inside one tree node. Siblings under `items` run sequentially, so it can't happen across tree nodes either. `--parallel` works around it by dropping every dependency edge, which is too blunt when some deps are real. +Each `vp run` only takes one task, so you can't fan out inside a tree node. `&&` makes siblings sequential, so you can't fan out across tree nodes either. `--parallel` works around it by ignoring every dependency edge, which is too blunt when some of those edges matter. ## `run-many` -`vp run-many ...` builds one graph with multiple requested roots. All `run` flags apply. The graph is the union of the per-task graphs, dedup'd by node. +`vp run-many ...` builds one graph with multiple requested tasks. All `run` flags apply. The graph is the union of the per-task graphs, deduped by node. -`vp run-many build test` where `test` depends on `build`: +`vp run-many vite#build vite#build-types @voidzero-dev/vite-plus-test#build`: ```mermaid flowchart TD - subgraph G["ExecutionGraph · vp run-many build test"] + subgraph G["ExecutionGraph"] direction TB - lb[lib1#build] --> la[app#build] - lb --> lt[lib1#test] - la --> at[app#test] - lt --> at + rn[rolldown#build-node] --> vb[vite#build] + rn --> vbt[vite#build-types] + rn --> vpt["@voidzero-dev/vite-plus-test#build"] end ``` -`lib1#test` starts the moment `lib1#build` finishes. It doesn't wait for `app#build`. `--parallel` can't give you this schedule because it would drop the `test → build` edge entirely. `["vp run build", "vp run test"]` can't either, because no test starts until every build is done. +- **In sequence:** `rolldown#build-node` runs first. +- **In parallel:** once it's done, all three of `vite#build`, `vite#build-types`, `@voidzero-dev/vite-plus-test#build` run together (up to the concurrency limit). + +For comparison: + +- `["vp run vite#build", "vp run vite#build-types", "vp run @voidzero-dev/vite-plus-test#build"]` would serialize the three. +- `vp run --parallel ...` only takes one task and would drop dependency edges entirely. ## Composition -`string[]` for sequencing and `run-many` for fan-out compose cleanly: +`string[]` for sequencing, `run-many` for fan-out: ```jsonc { - "ci": { + "build:source": { "command": [ - "vp run prepare", - "vp run-many lint test build typecheck", - "vp run publish" + "vp run-many @rolldown/pluginutils#build rolldown#build-binding:release", + "vp run rolldown#build-node", + "vp run-many vite#build vite#build-types @voidzero-dev/vite-plus-test#build", + "vp run @voidzero-dev/vite-plus-core#build", + "vp run-many vite-plus#build vite-plus-cli#build" ] } } @@ -95,32 +115,52 @@ flowchart TD ```mermaid flowchart TD - ci["ci · items run sequentially"] - ci -. 1 .-> P - ci -. 2 .-> M - ci -. 3 .-> Pub + bs["build:source · items run one at a time"] + bs -. 1 .-> S1 + bs -. 2 .-> S2 + bs -. 3 .-> S3 + bs -. 4 .-> S4 + bs -. 5 .-> S5 + + subgraph S1["1 · run-many"] + direction TB + a1["@rolldown/pluginutils#build"] + a2["rolldown#build-binding:release"] + end - subgraph P["ExecutionGraph · prepare"] - p1[prepare] + subgraph S2["2 · run"] + b1[rolldown#build-node] end - subgraph M["ExecutionGraph · run-many (wide)"] + subgraph S3["3 · run-many"] direction TB - b[build] --> t[test] - b --> l[lint] - b --> ty[typecheck] + c1[vite#build] + c2[vite#build-types] + c3["@voidzero-dev/vite-plus-test#build"] end - subgraph Pub["ExecutionGraph · publish"] - pub[publish] + subgraph S4["4 · run"] + d1["@voidzero-dev/vite-plus-core#build"] end -``` -`prepare` runs alone. Then the wide graph: `lint`, `test`, `typecheck` start the moment `build` finishes, all sharing one semaphore. Then `publish`. + subgraph S5["5 · run-many"] + direction TB + e1[vite-plus#build] + e2[vite-plus-cli#build] + end +``` -| Primitive | Effect | -| ----------------- | ------------------------------------------------- | -| `ExecutionGraph` | Dependency-aware parallelism within one semaphore | -| `command: [...]` | Sequence sibling graphs in the tree | -| `vp run` | Spawn one child graph | -| `vp run-many` | Spawn one wide child graph (multiple roots) | +- **In sequence:** stages 1 → 2 → 3 → 4 → 5. Each stage finishes completely before the next begins. +- **In parallel within each stage:** + - Stage 1: `@rolldown/pluginutils#build` and `rolldown#build-binding:release` run together. + - Stage 2: one task on its own. + - Stage 3: `vite#build`, `vite#build-types`, and `@voidzero-dev/vite-plus-test#build` run together. + - Stage 4: one task on its own. + - Stage 5: `vite-plus#build` and `vite-plus-cli#build` run together. + +| Primitive | Effect | +| ----------------- | ------------------------------------------------------ | +| `ExecutionGraph` | Dependency-aware parallelism within one scheduler | +| `command: [...]` | Sequence sibling graphs in the tree | +| `vp run` | Spawn one child graph | +| `vp run-many` | Spawn one wide child graph (multiple requested tasks) | From 529904a99fa132c454a78aa10d4963d6da9a14f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 10:12:47 +0000 Subject: [PATCH 3/5] docs(rfc): lead with run-many, switch composition diagram to left-to-right --- docs/rfcs/run-many.md | 70 ++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/docs/rfcs/run-many.md b/docs/rfcs/run-many.md index d1249193..7a255465 100644 --- a/docs/rfcs/run-many.md +++ b/docs/rfcs/run-many.md @@ -1,5 +1,31 @@ # RFC: `run-many` +## What `run-many` does + +`vp run-many ...` builds one graph with multiple requested tasks. All `run` flags apply. The graph is the union of the per-task graphs, deduped by node. + +`vp run-many vite#build vite#build-types @voidzero-dev/vite-plus-test#build`, where all three depend on `rolldown#build-node`: + +```mermaid +flowchart TD + subgraph G["ExecutionGraph"] + direction TB + rn[rolldown#build-node] --> vb[vite#build] + rn --> vbt[vite#build-types] + rn --> vpt["@voidzero-dev/vite-plus-test#build"] + end +``` + +- **In sequence:** `rolldown#build-node` runs first. +- **In parallel:** once it's done, all three vite tasks run together (up to the concurrency limit). + +This is the schedule you can't get from existing primitives: + +- `["vp run vite#build", "vp run vite#build-types", "vp run @voidzero-dev/vite-plus-test#build"]` serializes the three. +- `vp run --parallel ...` only takes one task and would drop the dependency edges entirely. + +The rest of this RFC explains where `run-many` fits in the existing scheduling model. + ## Two scheduling structures vite-task schedules work using two structures nested inside each other: a **graph** (what the scheduler runs) and a **tree** (recursion of graphs). @@ -8,7 +34,7 @@ vite-task schedules work using two structures nested inside each other: a **grap An `ExecutionGraph` is a DAG of tasks. The scheduler walks it: a task starts once all of its dependencies have finished, and the number of tasks running at the same time is capped by the concurrency limit (default 4). -`vp run-many vite#build vite#build-types`, where both depend on `rolldown#build-node`: +`vp run vite-plus#build`, where `vite-plus#build` depends on `vite#build` and `vite#build-types`, both of which depend on `rolldown#build-node`: ```mermaid flowchart TD @@ -16,15 +42,17 @@ flowchart TD direction TB rn[rolldown#build-node] --> vb[vite#build] rn --> vbt[vite#build-types] + vb --> vp[vite-plus#build] + vbt --> vp end ``` Arrows mean "runs before". -- **In sequence:** `rolldown#build-node` runs first. -- **In parallel:** once it's done, `vite#build` and `vite#build-types` run together. +- **In sequence:** `rolldown#build-node` first, then the vite tasks, then `vite-plus#build`. +- **In parallel:** `vite#build` and `vite#build-types` run together after `rolldown#build-node`. -The two vite tasks are independent of each other, but the scheduler still honors their shared dependency on `rolldown#build-node`. +`run-many` widens this kind of graph by letting several tasks be the requested roots instead of one. ### Tree @@ -63,38 +91,6 @@ flowchart TD (Cache will spare you the repeated `rolldown#build-node` work, but the two vite tasks themselves still wait for each other.) -## What's missing - -You can get dependency-aware parallelism inside one graph. You can sequence siblings in the tree. What you can't say today is: - -> Run several tasks plus their dependencies as one graph. Run them at the same time where they're independent, in order where they actually depend on each other. - -Each `vp run` only takes one task, so you can't fan out inside a tree node. `&&` makes siblings sequential, so you can't fan out across tree nodes either. `--parallel` works around it by ignoring every dependency edge, which is too blunt when some of those edges matter. - -## `run-many` - -`vp run-many ...` builds one graph with multiple requested tasks. All `run` flags apply. The graph is the union of the per-task graphs, deduped by node. - -`vp run-many vite#build vite#build-types @voidzero-dev/vite-plus-test#build`: - -```mermaid -flowchart TD - subgraph G["ExecutionGraph"] - direction TB - rn[rolldown#build-node] --> vb[vite#build] - rn --> vbt[vite#build-types] - rn --> vpt["@voidzero-dev/vite-plus-test#build"] - end -``` - -- **In sequence:** `rolldown#build-node` runs first. -- **In parallel:** once it's done, all three of `vite#build`, `vite#build-types`, `@voidzero-dev/vite-plus-test#build` run together (up to the concurrency limit). - -For comparison: - -- `["vp run vite#build", "vp run vite#build-types", "vp run @voidzero-dev/vite-plus-test#build"]` would serialize the three. -- `vp run --parallel ...` only takes one task and would drop dependency edges entirely. - ## Composition `string[]` for sequencing, `run-many` for fan-out: @@ -114,7 +110,7 @@ For comparison: ``` ```mermaid -flowchart TD +flowchart LR bs["build:source · items run one at a time"] bs -. 1 .-> S1 bs -. 2 .-> S2 From 0509bf29c5e80f3d7e823c131090feec585a3c95 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 10:18:49 +0000 Subject: [PATCH 4/5] docs(rfc): show rolldown#build-node reappearing across graphs, link #381 --- docs/rfcs/run-many.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/rfcs/run-many.md b/docs/rfcs/run-many.md index 7a255465..4345d5ff 100644 --- a/docs/rfcs/run-many.md +++ b/docs/rfcs/run-many.md @@ -58,7 +58,7 @@ Arrows mean "runs before". A task's `command` splits on `&&` into **items** that run one after another. An item is either a leaf process, or `Expanded`: a nested `ExecutionGraph` built from a `vp run` inside the command. Every `Expanded` item is its own independent scheduling unit. -After #381, `command: ["vp run a", "vp run b"]` is shorthand for `"vp run a && vp run b"`. So `string[]` is how you sequence siblings in the tree. +After [#381](https://github.com/voidzero-dev/vite-task/issues/381), `command: ["vp run a", "vp run b"]` is shorthand for `"vp run a && vp run b"`. So `string[]` is how you sequence siblings in the tree. ```jsonc "build-vite": { @@ -130,9 +130,9 @@ flowchart LR subgraph S3["3 · run-many"] direction TB - c1[vite#build] - c2[vite#build-types] - c3["@voidzero-dev/vite-plus-test#build"] + rn3[rolldown#build-node] --> c1[vite#build] + rn3 --> c2[vite#build-types] + rn3 --> c3["@voidzero-dev/vite-plus-test#build"] end subgraph S4["4 · run"] @@ -150,10 +150,12 @@ flowchart LR - **In parallel within each stage:** - Stage 1: `@rolldown/pluginutils#build` and `rolldown#build-binding:release` run together. - Stage 2: one task on its own. - - Stage 3: `vite#build`, `vite#build-types`, and `@voidzero-dev/vite-plus-test#build` run together. + - Stage 3: `rolldown#build-node` gets pulled in as a dep first (a cache hit, since stage 2 already ran it), then `vite#build`, `vite#build-types`, and `@voidzero-dev/vite-plus-test#build` run together. - Stage 4: one task on its own. - Stage 5: `vite-plus#build` and `vite-plus-cli#build` run together. +Notice that `rolldown#build-node` shows up in both stage 2 and stage 3. Each `vp run` / `vp run-many` builds its own isolated graph and pulls in whatever dependencies its requested tasks declare. The same task can appear in many graphs; cache makes sure the duplicate appearance doesn't mean duplicate work. + | Primitive | Effect | | ----------------- | ------------------------------------------------------ | | `ExecutionGraph` | Dependency-aware parallelism within one scheduler | From cdfaac755c213354a4d6c608c7ed23db1d15a4fd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 10:55:37 +0000 Subject: [PATCH 5/5] docs(rfc): mark run-many flag set as TBD --- docs/rfcs/run-many.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfcs/run-many.md b/docs/rfcs/run-many.md index 4345d5ff..93bdd88c 100644 --- a/docs/rfcs/run-many.md +++ b/docs/rfcs/run-many.md @@ -2,7 +2,7 @@ ## What `run-many` does -`vp run-many ...` builds one graph with multiple requested tasks. All `run` flags apply. The graph is the union of the per-task graphs, deduped by node. +`vp run-many ...` builds one graph with multiple requested tasks. Which `run` flags are accepted is TBD. The graph is the union of the per-task graphs, deduped by node. `vp run-many vite#build vite#build-types @voidzero-dev/vite-plus-test#build`, where all three depend on `rolldown#build-node`: