Skip to content

src/orchestrator/prepare.ts — shared run/planRun setup

run() and planRun() both go through an identical workspace- discovery → config-load → graph-build → cache-open sequence before they diverge into “execute” vs “predict”. prepareRun centralises that shared work; the two callers stay thin.

export interface PreparedRun {
workspaceRoot: string
workspaceConfig: WorkspaceConfig | null
cacheDir: string
cache: CacheLayer // caller owns close()
projects: Map<string, ProjectEntry>
packageGraph: PackageGraph
nodes: Map<string, TaskNode> // empty if `empty !== null`
workspaceFingerprint: string
nestedDirsByProject: Map<string, string[]>
/**
* Reason `nodes` is empty:
* - `null` — graph is non-empty, ready to execute.
* - `'no-tasks-declared'` — `requested.length === 0` after
* resolving the user's task names
* against `projects`. CI footgun;
* `run()` returns NOT-ok.
* - `'empty-graph'` — `requested` was non-empty but
* `buildTaskGraph` produced no nodes.
* Defensive; unreachable under current
* builder semantics.
*/
empty: null | 'no-tasks-declared' | 'empty-graph'
}
export function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun>
  1. Workspace discoveryfindWorkspaceRoot, loadWorkspace, loadWorkspaceConfig, listProjects.
  2. Project config loadloadProjectConfig per project that has a vx.config.* sibling. Projects without configs are kept in the workspace graph (for cross-package dep edges) but contribute no tasks.
  3. Package + task structurebuildPackageGraph, computeNestedProjectDirs, expandRequested.
  4. Cache + fingerprintnew Cache(resolveCacheDir(root, workspaceConfig)), optionally wrapWithRemoteCache(local, log), computeWorkspaceFingerprint.
  5. Build the task graphbuildTaskGraph(...) with optional excludeDependencies filter.

The cache + fingerprint are constructed even when the result will be empty so callers always have a uniform try { ... } finally { cache.close() } shape.

Before: run() and planRun() each duplicated ~50 lines of setup, slowly diverging (different error messages, different defaults, planRun opened the cache without a logger context for the remote wrap, etc.). After: one function, one path; the two callers handle only what’s actually different (execution vs prediction).

  • Named inputs / target defaults. A workspace-level transformation that rewrites each ProjectEntry.config (e.g. expanding $inputs:default references) should run between project-config load and graph build. Add it as a step here.
  • globalInputs / globalEnv. Workspace-level cache-key contributions would land in the fingerprint step.
  • Telemetry handle. A Telemetry sink could be constructed here and added to PreparedRun, with default a no-op. Both callers would receive it via the context.

Covered transitively by every orchestrator e2e test (tests/orchestrator.test.ts) and by tests/cli.test.ts’s end-to-end fixtures. No dedicated unit-test file; the function has no externally-visible behaviour beyond what the integration tests exercise.