@vzn/vx 0.0.18 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
// try/finally around its plan() call.
|
|
9
9
|
|
|
10
10
|
import path from 'node:path'
|
|
11
|
-
import type {
|
|
11
|
+
import type { WorkspaceConfig } from '../config.js'
|
|
12
12
|
import { mark, UserError } from '../util/index.js'
|
|
13
13
|
import {
|
|
14
14
|
Cache,
|
|
15
|
+
noteSchemaReset,
|
|
15
16
|
type CacheLayer,
|
|
16
17
|
type CachePolicy,
|
|
17
18
|
FULL_CACHE_POLICY,
|
|
@@ -25,34 +26,35 @@ import {
|
|
|
25
26
|
import {
|
|
26
27
|
buildPackageGraph,
|
|
27
28
|
computeNestedProjectDirs,
|
|
28
|
-
|
|
29
|
+
computeWorkspaceFingerprints,
|
|
29
30
|
findWorkspaceRoot,
|
|
30
|
-
|
|
31
|
+
type LoadReads,
|
|
31
32
|
listProjects,
|
|
32
|
-
loadProjectConfigs,
|
|
33
33
|
loadWorkspace,
|
|
34
|
-
|
|
34
|
+
FROZEN_WITHOUT_LOCK,
|
|
35
35
|
readLockfile,
|
|
36
36
|
resolveCacheDir,
|
|
37
|
-
validateProjectConfig,
|
|
38
37
|
type ProjectEntry,
|
|
39
38
|
} from '../workspace/index.js'
|
|
40
39
|
import {
|
|
41
40
|
buildTaskGraph,
|
|
41
|
+
excludeDependencies,
|
|
42
42
|
expandRequested,
|
|
43
|
-
parseDependencySpec,
|
|
44
43
|
type TaskNode,
|
|
44
|
+
undeclaredDepsError,
|
|
45
45
|
unresolvedRequests,
|
|
46
46
|
} from '../graph/index.js'
|
|
47
47
|
import {
|
|
48
|
-
applyConfigHooks,
|
|
49
48
|
applyGraphHooks,
|
|
50
49
|
applyKeyHooks,
|
|
51
|
-
applyProjectHooks,
|
|
52
50
|
applyScheduleHooks,
|
|
51
|
+
fingerprintClaims,
|
|
53
52
|
hasHook,
|
|
54
53
|
resolveCache,
|
|
55
54
|
} from './plugin-host.js'
|
|
55
|
+
import { loadProjects, loadWorkspacePlugins, type LoadedProjects } from './projects.js'
|
|
56
|
+
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
57
|
+
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
56
58
|
import type { VxPlugin } from './plugin.js'
|
|
57
59
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
58
60
|
import type { Logger } from './logger.js'
|
|
@@ -76,8 +78,7 @@ export interface PreparedRun {
|
|
|
76
78
|
* injected remote layer, or one a plugin's `cache` capability built. The
|
|
77
79
|
* remote axes of the policy only mean anything then: without a remote
|
|
78
80
|
* layer a `remote:w` policy writes NOWHERE, so a task that believed it
|
|
79
|
-
* would be saved still cleans its outputs before executing
|
|
80
|
-
* `--verify`) restores an artifact that was never written.
|
|
81
|
+
* would be saved still cleans its outputs before executing.
|
|
81
82
|
*/
|
|
82
83
|
hasRemoteLayer: boolean
|
|
83
84
|
/**
|
|
@@ -96,7 +97,15 @@ export interface PreparedRun {
|
|
|
96
97
|
unresolvedTasks: readonly string[]
|
|
97
98
|
/** Every discovered project — the declared task names a typo is measured against. */
|
|
98
99
|
projects: ReadonlyMap<string, ProjectEntry>
|
|
100
|
+
/**
|
|
101
|
+
* True iff at least one package in the workspace has a `vx.config.*` at
|
|
102
|
+
* all — regardless of scope, so a `--filter` that matched nothing is not
|
|
103
|
+
* mistaken for a workspace vx was never set up in.
|
|
104
|
+
*/
|
|
105
|
+
anyProjectConfig: boolean
|
|
99
106
|
workspaceFingerprint: string
|
|
107
|
+
/** Whether a task has since rewritten what that fingerprint folded (fingerprint-watch.ts). */
|
|
108
|
+
fingerprintWatch: FingerprintWatch
|
|
100
109
|
nestedDirsByProject: Map<string, string[]>
|
|
101
110
|
/**
|
|
102
111
|
* Per-run memo for `git ls-files` output, keyed by project dir.
|
|
@@ -129,25 +138,6 @@ export interface PreparedRun {
|
|
|
129
138
|
empty: null | 'no-tasks-declared' | 'empty-graph'
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
/** Project names named by a `pkg#task` dependsOn entry anywhere in `config`. */
|
|
133
|
-
function crossDepProjects(config: ProjectConfig): string[] {
|
|
134
|
-
const out: string[] = []
|
|
135
|
-
for (const task of Object.values(config.tasks ?? {})) {
|
|
136
|
-
for (const raw of task.dependsOn ?? []) {
|
|
137
|
-
// A malformed spec is the graph builder's error to report — it names
|
|
138
|
-
// the offending task. Here it just contributes no project.
|
|
139
|
-
if (!raw.includes('#')) continue
|
|
140
|
-
try {
|
|
141
|
-
const spec = parseDependencySpec(raw)
|
|
142
|
-
if (spec.kind === 'cross') out.push(spec.project)
|
|
143
|
-
} catch {
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return out
|
|
149
|
-
}
|
|
150
|
-
|
|
151
141
|
/**
|
|
152
142
|
* Build the prepared-run context: workspace discovery, project-config
|
|
153
143
|
* load, package + task graph, cache handle (local, optionally wrapped
|
|
@@ -160,7 +150,10 @@ function crossDepProjects(config: ProjectConfig): string[] {
|
|
|
160
150
|
*/
|
|
161
151
|
export async function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun> {
|
|
162
152
|
mark('startup')
|
|
163
|
-
|
|
153
|
+
// The root manifest is read once for the root, the globs and the
|
|
154
|
+
// fingerprint. A watch cycle is a new run and reads it afresh.
|
|
155
|
+
const reads: LoadReads = new Map()
|
|
156
|
+
const workspaceRoot = await findWorkspaceRoot(options.cwd, reads)
|
|
164
157
|
// An UNSCOPED run (no explicit scope, at least one bare task name)
|
|
165
158
|
// enumerates the whole tree whatever the configs say, so git starts
|
|
166
159
|
// HERE — the walk needs only the root — and overlaps the workspace
|
|
@@ -177,17 +170,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
177
170
|
// handler keeps that from surfacing as an unhandled rejection (the real
|
|
178
171
|
// await further down still sees the error).
|
|
179
172
|
earlyGit?.catch(() => {})
|
|
180
|
-
const workspace = await loadWorkspace(workspaceRoot)
|
|
181
|
-
const workspaceConfig = await
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const plugins = (workspaceConfig?.plugins ?? []) as readonly VxPlugin[]
|
|
185
|
-
if (workspaceConfig !== null && hasHook(plugins, 'config')) {
|
|
186
|
-
await applyConfigHooks(plugins, workspaceConfig, {
|
|
187
|
-
workspaceRoot,
|
|
188
|
-
warn: (m) => log.status(m),
|
|
189
|
-
})
|
|
190
|
-
}
|
|
173
|
+
const workspace = await loadWorkspace(workspaceRoot, reads)
|
|
174
|
+
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, (m) =>
|
|
175
|
+
log.status(m),
|
|
176
|
+
)
|
|
191
177
|
mark('workspace config')
|
|
192
178
|
const projectMetas = await listProjects(workspace)
|
|
193
179
|
mark('discover projects')
|
|
@@ -201,17 +187,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
201
187
|
// Turbo-like: a broken config in an unrelated package no longer
|
|
202
188
|
// fails a scoped run — it surfaces when that package enters scope.
|
|
203
189
|
const packageGraph = buildPackageGraph(projectMetas)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Seeds: explicit scope, plus anchored pkg#task targets (which
|
|
211
|
-
// bypass scope by design). With no explicit scope, bare task names
|
|
212
|
-
// fan out across the whole workspace — but when EVERY spec is
|
|
213
|
-
// anchored, the anchors alone are the scope and nothing else needs
|
|
214
|
-
// its config evaluated.
|
|
190
|
+
// Seeds: explicit scope, plus anchored pkg#task targets (which bypass
|
|
191
|
+
// scope by design). With no explicit scope, bare task names fan out
|
|
192
|
+
// across the whole workspace — but when EVERY spec is anchored, the
|
|
193
|
+
// anchors alone are the scope and nothing else needs its config
|
|
194
|
+
// evaluated.
|
|
215
195
|
const anchored: string[] = []
|
|
216
196
|
let hasBare = false
|
|
217
197
|
for (const spec of options.tasks) {
|
|
@@ -219,16 +199,13 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
219
199
|
if (hashIdx > 0) anchored.push(spec.slice(0, hashIdx))
|
|
220
200
|
else hasBare = true
|
|
221
201
|
}
|
|
222
|
-
const seeds
|
|
223
|
-
options.projects
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
for (const project of anchored) {
|
|
230
|
-
if (haveConfig.has(project)) seeds.add(project)
|
|
231
|
-
}
|
|
202
|
+
const seeds: 'all' | string[] =
|
|
203
|
+
options.projects !== undefined ? [...options.projects, ...anchored] : hasBare ? 'all' : anchored
|
|
204
|
+
// Its own mark: the package graph is the bulk of what used to be charged
|
|
205
|
+
// to `open cache` (measured on a 1,000-project tree, 2026-09-20 — the
|
|
206
|
+
// graph 3-8 ms against the cache open's ~1 ms and the fingerprints' ~0.3),
|
|
207
|
+
// so a reader profiling that stage was sent to the wrong code.
|
|
208
|
+
mark('package graph')
|
|
232
209
|
|
|
233
210
|
// The local cache opens BEFORE the configs load: it is also where their
|
|
234
211
|
// cached evaluations live. `--cache-dir <path>` (RunOptions.cacheDir)
|
|
@@ -238,26 +215,26 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
238
215
|
const cacheDir = options.cacheDir
|
|
239
216
|
? path.resolve(options.cwd, options.cacheDir)
|
|
240
217
|
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
241
|
-
const localCache = new Cache(
|
|
242
|
-
|
|
218
|
+
const localCache = new Cache(
|
|
219
|
+
cacheDir,
|
|
220
|
+
{ read: policy.localRead, write: policy.localWrite },
|
|
221
|
+
workspaceRoot,
|
|
222
|
+
options.artifactCeiling,
|
|
223
|
+
)
|
|
224
|
+
localCache.assertWritable()
|
|
225
|
+
noteSchemaReset(localCache, (m) => log.status(m))
|
|
226
|
+
// Two digests from one read: the config-evaluation cache keys on every
|
|
227
|
+
// file (a config may import a dependency), the task keys on the files no
|
|
228
|
+
// plugin claims (`VxPlugin.fingerprint`).
|
|
229
|
+
const fingerprintsAt = Date.now()
|
|
230
|
+
const fingerprints = await computeWorkspaceFingerprints(
|
|
231
|
+
workspaceRoot,
|
|
232
|
+
new Set(fingerprintClaims(plugins).keys()),
|
|
233
|
+
reads,
|
|
234
|
+
)
|
|
235
|
+
const workspaceFingerprint = fingerprints.unclaimed
|
|
243
236
|
mark('open cache')
|
|
244
237
|
|
|
245
|
-
type ConfigMeta = (typeof projectsWithConfigs)[number]
|
|
246
|
-
const metaByName = new Map<string, ConfigMeta>(projectsWithConfigs.map((m) => [m.name, m]))
|
|
247
|
-
const needed = new Set<string>()
|
|
248
|
-
const pending: ConfigMeta[] = []
|
|
249
|
-
const consider = (name: string): void => {
|
|
250
|
-
if (needed.has(name)) return
|
|
251
|
-
needed.add(name)
|
|
252
|
-
const meta = metaByName.get(name)
|
|
253
|
-
if (meta) pending.push(meta)
|
|
254
|
-
}
|
|
255
|
-
const considerWithDeps = (name: string): void => {
|
|
256
|
-
consider(name)
|
|
257
|
-
for (const dep of packageGraph.transitiveDeps(name)) consider(dep)
|
|
258
|
-
}
|
|
259
|
-
for (const seed of seeds) considerWithDeps(seed)
|
|
260
|
-
|
|
261
238
|
// Frozen mode (--frozen, CI): configs load FROM vx-lock.json after a
|
|
262
239
|
// content-hash tripwire — no evaluation; env-dependent configs keep
|
|
263
240
|
// their locked values. Default (local) runs ALWAYS evaluate live:
|
|
@@ -267,55 +244,34 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
267
244
|
// See docs/design/config-lock-2026-06.md.
|
|
268
245
|
const lock = options.frozen === true ? await readLockfile(workspaceRoot) : null
|
|
269
246
|
if (options.frozen === true && lock === null) {
|
|
270
|
-
throw new UserError(
|
|
271
|
-
`--frozen requires vx-lock.json at the workspace root — run 'vx lock' and commit it`,
|
|
272
|
-
)
|
|
247
|
+
throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
273
248
|
}
|
|
274
249
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
250
|
+
const loadArgs = {
|
|
251
|
+
workspaceRoot,
|
|
252
|
+
cacheDir,
|
|
253
|
+
plugins,
|
|
254
|
+
projectMetas,
|
|
255
|
+
packageGraph,
|
|
256
|
+
lock,
|
|
257
|
+
evalCache: { store: localCache, workspaceFingerprint: fingerprints.all },
|
|
258
|
+
warn: (m: string) => log.status(m),
|
|
259
|
+
}
|
|
260
|
+
let loaded: LoadedProjects
|
|
282
261
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
{ evalCache: { store: localCache, workspaceFingerprint } },
|
|
290
|
-
)
|
|
291
|
-
for (let i = 0; i < round.length; i++) {
|
|
292
|
-
const meta = round[i]!
|
|
293
|
-
const config = configs[i] as ProjectConfig
|
|
294
|
-
if (projectStage) {
|
|
295
|
-
// The `project` stage edits the validated object in place; core
|
|
296
|
-
// then re-validates so a plugin can only produce what the loader
|
|
297
|
-
// accepts from a user. The message names the config file — the
|
|
298
|
-
// plugin's edit is a defect in THAT project's tasks.
|
|
299
|
-
await applyProjectHooks(plugins, config, {
|
|
300
|
-
workspaceRoot,
|
|
301
|
-
cacheDir,
|
|
302
|
-
warn: (m) => log.status(m),
|
|
303
|
-
name: meta.name,
|
|
304
|
-
dir: meta.dir,
|
|
305
|
-
packageJson: meta.packageJson as unknown as Readonly<Record<string, unknown>>,
|
|
306
|
-
})
|
|
307
|
-
validateProjectConfig(config, `${meta.configPath} (after plugins)`)
|
|
308
|
-
}
|
|
309
|
-
projects.set(meta.name, { name: meta.name, dir: meta.dir, config })
|
|
310
|
-
for (const name of crossDepProjects(config)) considerWithDeps(name)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
262
|
+
loaded = await loadProjects({
|
|
263
|
+
...loadArgs,
|
|
264
|
+
seeds,
|
|
265
|
+
closure: true,
|
|
266
|
+
...(options.staged !== undefined ? { staged: options.staged } : {}),
|
|
267
|
+
})
|
|
313
268
|
} catch (err) {
|
|
314
269
|
// The cache opened before the configs loaded (it holds their cached
|
|
315
270
|
// evaluations); a config error must not leak the handle.
|
|
316
271
|
localCache.close()
|
|
317
272
|
throw err
|
|
318
273
|
}
|
|
274
|
+
const { projects, configured: projectsWithConfigs } = loaded
|
|
319
275
|
mark('load configs')
|
|
320
276
|
|
|
321
277
|
// Boundary geometry considers every config-bearing project in the
|
|
@@ -333,24 +289,32 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
333
289
|
const unresolvedTasks = unresolvedRequests(options.tasks, candidateProjects, projects)
|
|
334
290
|
|
|
335
291
|
// Cache seam precedence: an EXPLICITLY injected remote layer
|
|
336
|
-
// (RunOptions.remoteCache — a distribution agent or
|
|
292
|
+
// (RunOptions.remoteCache — a distribution agent or daemon that already
|
|
337
293
|
// holds a wire client) wins outright; else a plugin's `cache` capability;
|
|
338
294
|
// else the local cache alone. Core ships no wire client — the remote
|
|
339
|
-
// cache is a plugin concern (
|
|
295
|
+
// cache is a plugin concern (docs/patterns.md § Remote cache wire). Injection
|
|
340
296
|
// winning prevents double-wrapping when the workspace also declares a
|
|
341
297
|
// cache plugin.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
298
|
+
let cache: CacheLayer
|
|
299
|
+
try {
|
|
300
|
+
cache = options.remoteCache
|
|
301
|
+
? new LayeredCache(localCache, options.remoteCache, {
|
|
302
|
+
policy,
|
|
303
|
+
onRemoteError: (err) => log.status(`[vx] remote cache: ${err.message}`),
|
|
304
|
+
})
|
|
305
|
+
: await resolveCache(plugins, {
|
|
306
|
+
workspaceRoot,
|
|
307
|
+
cacheDir,
|
|
308
|
+
warn: (m) => log.status(m),
|
|
309
|
+
localCache,
|
|
310
|
+
policy,
|
|
311
|
+
})
|
|
312
|
+
} catch (err) {
|
|
313
|
+
// A cache plugin that throws or returns something off-contract is
|
|
314
|
+
// refused by name; the local handle opened above must not leak with it.
|
|
315
|
+
localCache.close()
|
|
316
|
+
throw err
|
|
317
|
+
}
|
|
354
318
|
// Ask the LAYER, don't infer. Identity against `localCache` answers a
|
|
355
319
|
// DIFFERENT question — "did the plugin hand back something other than the
|
|
356
320
|
// handle I passed in?" — which an ordinary pass-through decorator (a
|
|
@@ -381,6 +345,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
381
345
|
applyGitEnumeration(enumeration, workspaceRoot, projectDirs, gitFilesCache, usesWorkspaceInputs)
|
|
382
346
|
mark('git enumeration')
|
|
383
347
|
const hashCache = createHashCache()
|
|
348
|
+
const fingerprintWatch = new FingerprintWatch(workspaceRoot, fingerprints, fingerprintsAt)
|
|
384
349
|
|
|
385
350
|
// Empty-cases bookkeeping. We still construct the cache + fingerprint
|
|
386
351
|
// so the caller's try/finally pattern can close it uniformly.
|
|
@@ -397,7 +362,9 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
397
362
|
nodes: new Map(),
|
|
398
363
|
unresolvedTasks,
|
|
399
364
|
projects,
|
|
365
|
+
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
400
366
|
workspaceFingerprint,
|
|
367
|
+
fingerprintWatch,
|
|
401
368
|
nestedDirsByProject,
|
|
402
369
|
gitFilesCache,
|
|
403
370
|
hashCache,
|
|
@@ -406,14 +373,31 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
406
373
|
}
|
|
407
374
|
}
|
|
408
375
|
|
|
376
|
+
// The whole graph, whatever `--exclude-dependencies` says: the stages
|
|
377
|
+
// below shape and key the tasks it drops as a full run would, because
|
|
378
|
+
// a dropped task is still keyed (excluded-keys.ts). A scoped load is not
|
|
379
|
+
// the whole workspace: a `^name` nothing loaded declares may be declared
|
|
380
|
+
// by a config the scope left out, so the builder hands it back instead of
|
|
381
|
+
// refusing it, and the rest decide.
|
|
382
|
+
const unproven: Array<[taskId: string, name: string]> = []
|
|
409
383
|
const nodes = buildTaskGraph({
|
|
410
384
|
projects,
|
|
411
385
|
packageGraph,
|
|
412
386
|
requested,
|
|
413
|
-
...(
|
|
414
|
-
? {
|
|
387
|
+
...(projects.size < projectsWithConfigs.length
|
|
388
|
+
? { undeclaredDeps: (id: string, name: string) => void unproven.push([id, name]) }
|
|
415
389
|
: {}),
|
|
416
390
|
})
|
|
391
|
+
if (unproven.length > 0) {
|
|
392
|
+
await refuseUndeclaredDeps(unproven, () =>
|
|
393
|
+
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
// The graph is built; what follows is the plugins' (graph, key,
|
|
397
|
+
// schedule). Two rows, so a plugin's key stage reads as its own cost
|
|
398
|
+
// and not as graph building — a lockfile plugin's 1000 stats per run
|
|
399
|
+
// hid inside one `prepare (graph)` row until 2026-09-10.
|
|
400
|
+
mark('build graph')
|
|
417
401
|
if (hasHook(plugins, 'graph')) {
|
|
418
402
|
await applyGraphHooks(plugins, nodes, {
|
|
419
403
|
workspaceRoot,
|
|
@@ -425,6 +409,24 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
425
409
|
if (hasHook(plugins, 'key')) {
|
|
426
410
|
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: (m) => log.status(m) })
|
|
427
411
|
}
|
|
412
|
+
const exclude = options.excludeDependencies
|
|
413
|
+
if (exclude !== undefined && (exclude === 'all' || exclude.length > 0)) {
|
|
414
|
+
const { keyOnly, dropped } = excludeDependencies(nodes, exclude)
|
|
415
|
+
if (dropped.size > 0) {
|
|
416
|
+
await keyExcludedDependencies({
|
|
417
|
+
nodes,
|
|
418
|
+
keyOnly,
|
|
419
|
+
dropped,
|
|
420
|
+
cache,
|
|
421
|
+
workspaceRoot,
|
|
422
|
+
workspaceFingerprint,
|
|
423
|
+
forwardArgs: options.forwardArgs,
|
|
424
|
+
nestedDirsByProject,
|
|
425
|
+
gitFilesCache,
|
|
426
|
+
hashCache,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
428
430
|
let priorities: ReadonlyMap<string, number> = new Map()
|
|
429
431
|
if (hasHook(plugins, 'schedule')) {
|
|
430
432
|
priorities = await applyScheduleHooks(plugins, nodes, {
|
|
@@ -447,7 +449,9 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
447
449
|
nodes,
|
|
448
450
|
unresolvedTasks,
|
|
449
451
|
projects,
|
|
452
|
+
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
450
453
|
workspaceFingerprint,
|
|
454
|
+
fingerprintWatch,
|
|
451
455
|
nestedDirsByProject,
|
|
452
456
|
gitFilesCache,
|
|
453
457
|
hashCache,
|
|
@@ -455,3 +459,30 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
455
459
|
empty: nodes.size === 0 ? 'empty-graph' : null,
|
|
456
460
|
}
|
|
457
461
|
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Refuses the first `^name` in `unproven` that no project in the whole
|
|
465
|
+
* workspace declares. `load` evaluates the configs a scoped run left out;
|
|
466
|
+
* it is reached only when a `^name` found no holder and nothing loaded
|
|
467
|
+
* declares it, so a run that names only declared tasks never pays for it.
|
|
468
|
+
* The rest failing to load leaves the names unjudged: an out-of-scope
|
|
469
|
+
* broken config does not fail a scoped run.
|
|
470
|
+
*/
|
|
471
|
+
async function refuseUndeclaredDeps(
|
|
472
|
+
unproven: ReadonlyArray<readonly [taskId: string, name: string]>,
|
|
473
|
+
load: () => Promise<LoadedProjects>,
|
|
474
|
+
): Promise<void> {
|
|
475
|
+
let all: LoadedProjects
|
|
476
|
+
try {
|
|
477
|
+
all = await load()
|
|
478
|
+
} catch {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
const declared = new Set<string>()
|
|
482
|
+
for (const p of all.projects.values()) {
|
|
483
|
+
for (const t of Object.keys(p.config.tasks ?? {})) declared.add(t)
|
|
484
|
+
}
|
|
485
|
+
for (const [id, name] of unproven) {
|
|
486
|
+
if (!declared.has(name)) throw undeclaredDepsError(id, name)
|
|
487
|
+
}
|
|
488
|
+
}
|