Skip to content

src/workspace/package-graph.ts — workspace dep graph

Build the workspace-internal dependency graph from each project’s package.json. Used by buildTaskGraph to resolve '^name' edges and by workspace/filter.ts for the pkg... / ...pkg filter traversals.

export interface PackageGraph {
directDeps: (name: string) => string[] // immediate workspace deps (sorted)
transitiveDeps: (name: string) => string[] // all transitive deps
transitiveDependents: (name: string) => string[] // all transitive dependents
}
export function buildPackageGraph(projects: ProjectMeta[]): PackageGraph

directDeps is the adjacency buildTaskGraph walks for '^name' frontier expansion; transitiveDeps / transitiveDependents serve workspace/filter.ts’s pkg... / ...pkg traversals.

For each project, scan all four dep buckets — dependencies, devDependencies, peerDependencies, optionalDependencies — and keep names that resolve to another workspace project (skipping self-references).

The graph is precomputed once per vx run invocation:

  • The direct-deps adjacency is materialized eagerly; directDeps reads straight from it.
  • transitiveDeps / transitiveDependents are memoized lazy functions backed by a DFS with cycle protection (a hypothetical cyclic workspace doesn’t loop forever; it just returns the reachable subset).

Output lists are sorted alphabetically for deterministic cache keys and graph traversal.

  • Doesn’t include external (non-workspace) deps. Those flow into the cache key via the workspace fingerprint (lockfile hash) and the project package.json hash.
  • Doesn’t classify dep types (dependencies vs devDependencies vs peerDependencies). For the task graph all four are equivalent — they all signal “this package needs that one to be present / built first.”
  • Doesn’t detect package-level cycles. Cycles within the workspace package graph itself are pathological but legal in package managers. The cycle protection above means traversal terminates; whether the task graph cycles is detected at task- graph build time.

tests/package-graph.test.ts:

  • empty workspace.
  • two projects, one depends on the other (directs + transitive).
  • directDeps returns immediate workspace deps only, sorted; [] for unknown names.
  • diamond (a → b, a → c, both → d). transitiveDeps(a) = [b, c, d].
  • transitiveDependents inverts correctly.
  • external (non-workspace) deps are ignored.
  • self-reference in a dep is skipped.
  • all four dep buckets are scanned.

The graph is consumed by name-lookup methods. Replacements should preserve PackageGraph shape. Reasonable extensions:

  • Edge metadata — annotate each edge with the source bucket so tooling can answer “is pkg-a’s dep on pkg-b a dev or runtime dep?”.
  • Conflict detection — flag inconsistent version ranges (e.g. pkg-a requires ^1.0 while pkg-b requires ^2.0 of a third workspace package). Belongs in lint, not the runner, but the data is here.