How Vite+ Workspace Configuration Loads Packages and Constructs Task Graphs

Vite+ discovers monorepo packages by traversing directory trees to locate workspace markers, indexes them for O(1) lookups, and delegates to the vite-task crate to build a directed task graph that is topologically sorted using petgraph with deterministic cycle handling via Tarjan’s algorithm.

The voidzero-dev/vite-plus repository provides a Rust-powered monorepo task runner that treats your codebase as a dynamic workspace configuration. This system operates in two distinct phases: first discovering and indexing all packages, then constructing a dependency-aware task graph for execution.

Discovering and Loading the Workspace Package Graph

When a workspace-aware command like vp run or vp exec is invoked, the CLI initializes the workspace configuration by locating the monorepo root and building an in-memory representation of all packages.

Locating the Workspace Root

The process begins in packages/cli/binding/src/exec/workspace.rs. The function vite_workspace::find_workspace_root(cwd) traverses upward from the current working directory to identify a workspace marker—either a pnpm-workspace.yaml file or a package.json containing a workspaces field. This function returns a WorkspaceRoot struct containing the absolute path and workspace file type.

Constructing the Package Graph

Once the root is identified, vite_workspace::load_package_graph(&workspace_root) parses every package.json within the workspace boundaries, including nested packages/** directories. This builds a directed graph where each node is a PackageInfo struct containing the package name, path, absolute path, and dependency metadata.

Indexing for Fast Lookups

To enable performant querying, IndexedPackageGraph::index(graph) creates an auxiliary PackageNodeIndex structure. This index allows the system to locate any package by name or path in O(1) time, which is critical when resolving filters across large monorepos.

Resolving Package Queries

The CLI parses flags such as --filter, --recursive, and --resume-from via args.packages.into_package_query(). This query is resolved against the indexed graph to produce a sub-graph containing only the selected packages and their implicit dependencies. If selectors match no packages, the system emits a warning (lines 48-54 in workspace.rs).

Building and Ordering the Task Graph

After establishing the package sub-graph, Vite+ constructs the executable task pipeline.

Delegating to vite-task

Vite+ does not embed task-graph logic directly in the CLI crate. Instead, it delegates to the vite-task crate via vite_task::TaskGraphBuilder::new(&workspace_root). This builder receives the PackageGraph produced in the discovery phase.

Defining Task Dependencies

For each package, the TaskGraphBuilder reads task definitions from vite-task.json or the run section of vite.config.ts. These definitions specify commands, explicit dependsOn arrays, and environment variables required for execution.

Implicit Dependencies and Topological Linking

When the --topological flag is active (default for --recursive), the builder automatically injects implicit edges based on package-level dependencies. For example, if app lists lib in its package.json dependencies, the task app#build implicitly depends on lib#build.

Cycle Detection and Deterministic Sorting

The builder produces a DiGraphMap<PackageNodeIndex, ()> from the petgraph library. The CLI then calls topological_sort_packages(&subgraph) (lines 59-66 in workspace.rs), which first attempts a standard petgraph::algo::toposort. If cycles are detected, it falls back to tarjan_scc (Tarjan’s Strongly Connected Components algorithm) to produce a deterministic execution order that respects all acyclic dependencies.

Execution Modes

The final sorted list is processed either sequentially or in parallel. The --parallel flag triggers the parallel branch in workspace.rs, spawning each task as a separate tokio::process::Command with prefixed output (e.g., pkg_name$).

Practical Implementation Examples

Detecting the Workspace via Node API

import { detectWorkspace } from 'vite-plus';

async function listPackages() {
  const info = await detectWorkspace(process.cwd());
  console.log('Root:', info.root);
  console.log('Monorepo?', info.isMonorepo);
}
listPackages();

Running Tasks Recursively


# Build all packages respecting dependency order

vp run build -r

Behind the scenes:

  1. -r (--recursive) selects all workspace packages via into_package_query()
  2. IndexedPackageGraph creates a sub-graph of all packages
  3. topological_sort_packages orders them (dependencies before dependents)
  4. Tasks execute according to vite-task.json definitions

Parallel Execution with Prefixing

vp exec echo "Hello" -p

The -p (--parallel) flag executes commands concurrently across packages, with each line prefixed by the package name unless only one package is selected.

Filtering Specific Packages


# Run tests only in packages matching "*ui*"

vp run test --filter "*ui*"

Unmatched filter patterns trigger warnings (lines 48-54 in workspace.rs), while matched packages form a filtered sub-graph that maintains topological integrity.

Summary

  • Workspace Discovery: find_workspace_root locates markers (pnpm-workspace.yaml or package.json with workspaces) and load_package_graph builds the initial directed graph in packages/cli/binding/src/exec/workspace.rs.
  • Fast Indexing: IndexedPackageGraph::index enables O(1) package lookups for query resolution.
  • Task Delegation: The vite-task crate's TaskGraphBuilder assembles the final execution graph from vite-task.json definitions and implicit package dependencies.
  • Deterministic Ordering: topological_sort_packages uses petgraph::algo::toposort with a tarjan_scc fallback to handle cycles deterministically.
  • Flexible Execution: Supports both sequential and parallel (--parallel) modes with filtered sub-graphs.

Frequently Asked Questions

How does Vite+ determine which directory is the workspace root?

Vite+ uses vite_workspace::find_workspace_root(cwd) to walk up the directory tree from the current working directory until it finds either a pnpm-workspace.yaml file or a package.json containing a workspaces array. This function returns a WorkspaceRoot struct with the absolute path and detected workspace type.

What happens if my monorepo has circular dependencies?

The system first attempts a standard topological sort via petgraph::algo::toposort. If cycles exist, it falls back to tarjan_scc (Tarjan’s Strongly Connected Components algorithm) to break the cycle deterministically while preserving all valid dependency constraints, ensuring stable execution order even in complex graphs.

Where does Vite+ store task definitions for each package?

Task definitions are read from vite-task.json in the package root or from the run section of vite.config.ts. The TaskGraphBuilder in the vite-task crate parses these files to extract command configurations, explicit dependsOn relationships, and environment variable requirements.

How does filtering affect the task graph construction?

When using --filter or similar flags, the CLI converts arguments into a PackageQuery via args.packages.into_package_query(). The IndexedPackageGraph resolves this to a sub-graph containing only matching packages (plus their implicit dependencies if --recursive is used). This filtered sub-graph is then topologically sorted independently, ensuring dependencies are still respected even for partial workspace execution.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →