@vzn/vx 0.0.636 → 0.0.638
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/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// What a config may SAY — the schema validators for `vx.config.*` and
|
|
2
|
-
//
|
|
2
|
+
// `.vx/workspace.*`, split from the loader (which decides HOW a file is
|
|
3
3
|
// evaluated) on 2026-09-10. Every object level refuses a key it does not
|
|
4
4
|
// know (tests/schema-unknown-keys), every message names the level and what
|
|
5
5
|
// it accepts, and `validateProjectConfig` is the one boundary the loader,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from '../config.js'
|
|
18
18
|
import {
|
|
19
19
|
CORE_VERBS,
|
|
20
|
-
MAX_TIMEOUT_MS,
|
|
21
20
|
nearest,
|
|
22
21
|
normalizeGlob,
|
|
23
22
|
parseDuration,
|
|
@@ -29,6 +28,14 @@ import {
|
|
|
29
28
|
asTrees,
|
|
30
29
|
isLiteralPattern,
|
|
31
30
|
} from '../util/index.js'
|
|
31
|
+
import {
|
|
32
|
+
parseSettingValue,
|
|
33
|
+
setting,
|
|
34
|
+
SETTING_GROUPS,
|
|
35
|
+
SETTINGS,
|
|
36
|
+
settingEnv,
|
|
37
|
+
settingRefusal,
|
|
38
|
+
} from '../settings.js'
|
|
32
39
|
import { compileNameGlob, parseFilter } from './filter.js'
|
|
33
40
|
import { nonJsonMessage, nonJsonPaths } from './json-data.js'
|
|
34
41
|
|
|
@@ -37,14 +44,14 @@ import { nonJsonMessage, nonJsonPaths } from './json-data.js'
|
|
|
37
44
|
// declared no plugins and ran the workspace bare, `cacheDirectory` left the
|
|
38
45
|
// cache where it was — a config that loads and quietly does nothing it says.
|
|
39
46
|
const CACHE_SCOPE_RULE =
|
|
40
|
-
"must be
|
|
47
|
+
"must be a scope name of letters, digits and . _ - / @ (at most 128), like 'pr-123'"
|
|
41
48
|
|
|
42
|
-
/** `
|
|
49
|
+
/** `cache.scope`'s rule, shared with `VX_CACHE_SCOPE`. */
|
|
43
50
|
export function isCacheScope(v: unknown): v is string {
|
|
44
51
|
return typeof v === 'string' && /^[\w.@/-]{1,128}$/.test(v)
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
/** Why a `VX_CACHE_SCOPE` or `--cache-scope` value is refused, in `
|
|
54
|
+
/** Why a `VX_CACHE_SCOPE` or `--cache-scope` value is refused, in `cache.scope`'s words. */
|
|
48
55
|
export function cacheScopeError(source = 'VX_CACHE_SCOPE'): string {
|
|
49
56
|
return `${source} ${CACHE_SCOPE_RULE}`
|
|
50
57
|
}
|
|
@@ -63,16 +70,6 @@ const oneOf = (values: readonly string[]): string =>
|
|
|
63
70
|
.join(', ')
|
|
64
71
|
.replace(/, ([^,]*)$/, ' or $1')}`
|
|
65
72
|
|
|
66
|
-
function envOneOf<T extends string>(
|
|
67
|
-
name: string,
|
|
68
|
-
raw: string | undefined,
|
|
69
|
-
values: readonly T[],
|
|
70
|
-
): T | undefined {
|
|
71
|
-
if (raw === undefined || raw === '') return undefined
|
|
72
|
-
if (!(values as readonly string[]).includes(raw)) throw new UserError(`${name} ${oneOf(values)}`)
|
|
73
|
-
return raw as T
|
|
74
|
-
}
|
|
75
|
-
|
|
76
73
|
function validateInputStability(stability: unknown, at: string): void {
|
|
77
74
|
if (typeof stability !== 'object' || stability === null || Array.isArray(stability)) {
|
|
78
75
|
throw new UserError(`${at} must be an object — \`inputStability: { strategy: 'mutable' }\``)
|
|
@@ -88,60 +85,132 @@ function validateInputStability(stability: unknown, at: string): void {
|
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
/**
|
|
91
|
-
* `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
88
|
+
* `VX_CACHE_INPUT_STABILITY_STRATEGY` and `_SEVERITY`, which override every
|
|
89
|
+
* task's `cache.inputStability` for the run: each undefined when unset or
|
|
90
|
+
* empty, refused when it names no value.
|
|
94
91
|
*/
|
|
95
92
|
export function inputStabilityEnv(): InputStability {
|
|
96
|
-
const strategy =
|
|
97
|
-
const severity =
|
|
98
|
-
'VX_INPUT_STABILITY_SEVERITY',
|
|
99
|
-
process.env['VX_INPUT_STABILITY_SEVERITY'],
|
|
100
|
-
SEVERITIES,
|
|
101
|
-
)
|
|
93
|
+
const strategy = settingEnv('cache.inputStability.strategy') as InputStability['strategy']
|
|
94
|
+
const severity = settingEnv('cache.inputStability.severity') as InputStability['severity']
|
|
102
95
|
return {
|
|
103
96
|
...(strategy !== undefined ? { strategy } : {}),
|
|
104
97
|
...(severity !== undefined ? { severity } : {}),
|
|
105
98
|
}
|
|
106
99
|
}
|
|
107
100
|
|
|
108
|
-
const WORKSPACE_FIELDS = new Set([
|
|
109
|
-
'concurrency',
|
|
110
|
-
'cacheDir',
|
|
111
|
-
'timeout',
|
|
112
|
-
'cacheRetention',
|
|
113
|
-
'affectedBase',
|
|
114
|
-
'cacheScope',
|
|
115
|
-
'rules',
|
|
116
|
-
'schedule',
|
|
117
|
-
'inputStability',
|
|
118
|
-
'plugins',
|
|
119
|
-
])
|
|
120
|
-
|
|
121
|
-
type Schedule = NonNullable<WorkspaceConfig['schedule']>
|
|
122
|
-
const SCHEDULES = {
|
|
123
|
-
'most-work': true,
|
|
124
|
-
'critical-path': true,
|
|
125
|
-
'direct-dependents': true,
|
|
126
|
-
'ready-order': true,
|
|
127
|
-
} satisfies Record<Schedule, true>
|
|
101
|
+
const WORKSPACE_FIELDS = new Set<string>([...SETTING_GROUPS, 'plugins'])
|
|
128
102
|
|
|
129
|
-
|
|
103
|
+
/**
|
|
104
|
+
* `.vx/workspace`'s settings, as `src/settings.ts` lists them: each group's
|
|
105
|
+
* fields, and the nested objects (`run.rules`) with theirs.
|
|
106
|
+
*/
|
|
107
|
+
const GROUP_FIELDS = new Map<string, Set<string>>()
|
|
108
|
+
for (const s of SETTINGS) {
|
|
109
|
+
if (s.config === false) continue
|
|
110
|
+
const parts = s.path.split('.')
|
|
111
|
+
for (let i = 1; i < parts.length; i++) {
|
|
112
|
+
const at = parts.slice(0, i).join('.')
|
|
113
|
+
if (!GROUP_FIELDS.has(at)) GROUP_FIELDS.set(at, new Set())
|
|
114
|
+
GROUP_FIELDS.get(at)!.add(parts[i]!)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// A list of objects, so no setting: no variable or flag can say it.
|
|
118
|
+
GROUP_FIELDS.get('run.rules')!.add('dependencies')
|
|
130
119
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
/** A setting's value or the level holding it, held to the schema. */
|
|
121
|
+
function validateSettingsAt(value: unknown, at: string, configPath: string): void {
|
|
122
|
+
const where = `${configPath}: \`${at}\``
|
|
123
|
+
const fields = GROUP_FIELDS.get(at)
|
|
124
|
+
if (fields !== undefined) {
|
|
125
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
126
|
+
if (at === 'cache')
|
|
127
|
+
// Turbo's and Nx's workspace `cache: true` is a task's own `cache` in vx (D-98).
|
|
128
|
+
throw new UserError(
|
|
129
|
+
`${where} must be an object like { dir, policy, scope }; a task caches through its own \`cache\` in its project's vx.config`,
|
|
130
|
+
)
|
|
131
|
+
throw new UserError(`${where} must be an object (fields: ${[...fields].join(', ')})`)
|
|
132
|
+
}
|
|
133
|
+
assertKnownFields(value, fields, where)
|
|
134
|
+
for (const [k, v] of Object.entries(value)) {
|
|
135
|
+
if (v !== undefined) validateSettingsAt(v, `${at}.${k}`, configPath)
|
|
136
|
+
}
|
|
137
|
+
return
|
|
135
138
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
if (at === 'run.rules.dependencies') validateDependencyRules(value, where)
|
|
140
|
+
else validateSettingValue(value, at, where)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const RULE_FIELDS = new Set(['from', 'allow', 'deny'])
|
|
144
|
+
|
|
145
|
+
function validateDependencyRules(value: unknown, where: string): void {
|
|
146
|
+
const shape = `an array of { from, allow?, deny? }, each a selector like 'tag:app' or '@acme/*'`
|
|
147
|
+
if (!Array.isArray(value)) throw new UserError(`${where} must be ${shape}`)
|
|
148
|
+
const selector = (v: unknown) => typeof v === 'string' && v.trim() !== '' && v !== 'tag:'
|
|
149
|
+
for (const [i, rule] of (value as unknown[]).entries()) {
|
|
150
|
+
const at = `${where}[${i}]`
|
|
151
|
+
if (typeof rule !== 'object' || rule === null || Array.isArray(rule))
|
|
152
|
+
throw new UserError(`${at} must be { from, allow?, deny? }`)
|
|
153
|
+
assertKnownFields(rule, RULE_FIELDS, at)
|
|
154
|
+
const { from, allow, deny } = rule as Record<string, unknown>
|
|
155
|
+
if (!selector(from)) throw new UserError(`${at}.from must be a selector like 'tag:app'`)
|
|
156
|
+
for (const [k, list] of [
|
|
157
|
+
['allow', allow],
|
|
158
|
+
['deny', deny],
|
|
159
|
+
] as const) {
|
|
160
|
+
if (list !== undefined && (!Array.isArray(list) || !list.every(selector)))
|
|
161
|
+
throw new UserError(`${at}.${k} must be an array of selectors like 'tag:lib'`)
|
|
140
162
|
}
|
|
163
|
+
// A rule that says nothing reads as a constraint and checks nothing.
|
|
164
|
+
if (allow === undefined && deny === undefined)
|
|
165
|
+
throw new UserError(`${at} names neither \`allow\` nor \`deny\``)
|
|
141
166
|
}
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
|
|
169
|
+
/** `value` as the setting at `path` takes it, refused in `where`'s words. */
|
|
170
|
+
function validateSettingValue(value: unknown, path: string, where: string): void {
|
|
171
|
+
const { kind } = setting(path)
|
|
172
|
+
// The bound `vx cache clean` refuses too: each run would empty the cache,
|
|
173
|
+
// the entries it just saved included, so nothing ever hits (item 969).
|
|
174
|
+
if (
|
|
175
|
+
path === 'cache.retention.olderThan' &&
|
|
176
|
+
typeof value === 'string' &&
|
|
177
|
+
parseDuration(value) === 0
|
|
178
|
+
) {
|
|
179
|
+
throw new UserError(`${where} of 0 evicts every entry after every run`)
|
|
180
|
+
}
|
|
181
|
+
// A file's values are typed: a count or a switch is no string (only a
|
|
182
|
+
// share of the cores is), as a variable's or a flag's must be.
|
|
183
|
+
const untyped =
|
|
184
|
+
(kind.type === 'concurrency' && typeof value === 'string' && !value.endsWith('%')) ||
|
|
185
|
+
(kind.type === 'int' && typeof value !== 'number') ||
|
|
186
|
+
(kind.type === 'bool' && typeof value !== 'boolean')
|
|
187
|
+
if (untyped || parseSettingValue(kind, value) === null) {
|
|
188
|
+
throw new UserError(settingRefusal(path, where, value, shown(value)))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The rules a setting's kind cannot say. */
|
|
193
|
+
function validateSettingsExtra(config: WorkspaceConfig, configPath: string): void {
|
|
194
|
+
const dir = config.cache?.dir
|
|
195
|
+
if (dir !== undefined && dir.includes('\0')) {
|
|
196
|
+
// No path can carry one: the mkdir's argument error was reported as an
|
|
197
|
+
// unwritable workspace.
|
|
198
|
+
throw new UserError(`${configPath}: \`cache.dir\` holds a NUL, which no path can carry`)
|
|
199
|
+
}
|
|
200
|
+
const base = config.run?.affectedBase
|
|
201
|
+
// A leading '-' would reach git as an option, not a ref; a NUL fails the
|
|
202
|
+
// spawn with an argument error naming nothing in this file.
|
|
203
|
+
if (base !== undefined && !/^[^-\s\0][^\s\0]*$/.test(base)) {
|
|
204
|
+
throw new UserError(`${configPath}: \`run.affectedBase\` must be a git ref like 'origin/main'`)
|
|
205
|
+
}
|
|
206
|
+
// `policy`'s grammar is the cache module's (`parseCachePolicy`), which
|
|
207
|
+
// this module may not import: the run parses it before any task (prepare.ts).
|
|
208
|
+
const scope = config.cache?.scope
|
|
209
|
+
if (scope !== undefined && !isCacheScope(scope)) {
|
|
210
|
+
throw new UserError(`${configPath}: \`cache.scope\` ${CACHE_SCOPE_RULE}`)
|
|
211
|
+
}
|
|
212
|
+
if (config.cache?.retention !== undefined) validateRetention(config.cache.retention, configPath)
|
|
213
|
+
}
|
|
145
214
|
|
|
146
215
|
/**
|
|
147
216
|
* A refused value as a message quotes it. A workspace file is evaluated
|
|
@@ -153,43 +222,26 @@ function shown(v: unknown): string {
|
|
|
153
222
|
return typeof v === 'bigint' ? `${v}n` : (JSON.stringify(v) ?? String(v))
|
|
154
223
|
}
|
|
155
224
|
|
|
156
|
-
function validateRetention(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
assertKnownFields(retention as Record<string, unknown>, RETENTION_FIELDS, where)
|
|
163
|
-
const { olderThan, maxSize } = retention as { olderThan?: unknown; maxSize?: unknown }
|
|
225
|
+
function validateRetention(
|
|
226
|
+
retention: NonNullable<NonNullable<WorkspaceConfig['cache']>['retention']>,
|
|
227
|
+
configPath: string,
|
|
228
|
+
): void {
|
|
229
|
+
const { olderThan, maxSize } = retention
|
|
164
230
|
// An empty object would load and evict nothing, which reads as a policy.
|
|
165
231
|
if (olderThan === undefined && maxSize === undefined) {
|
|
166
|
-
throw new UserError(`${where} names neither \`olderThan\` nor \`maxSize\``)
|
|
167
|
-
}
|
|
168
|
-
if (
|
|
169
|
-
olderThan !== undefined &&
|
|
170
|
-
(typeof olderThan !== 'string' || parseDuration(olderThan) === null)
|
|
171
|
-
) {
|
|
172
232
|
throw new UserError(
|
|
173
|
-
`${
|
|
233
|
+
`${configPath}: \`cache.retention\` names neither \`olderThan\` nor \`maxSize\``,
|
|
174
234
|
)
|
|
175
235
|
}
|
|
176
|
-
|
|
236
|
+
// As for olderThan (item 969).
|
|
237
|
+
if (maxSize !== undefined && parseSize(maxSize) === 0) {
|
|
177
238
|
throw new UserError(
|
|
178
|
-
`${
|
|
239
|
+
`${configPath}: \`cache.retention.maxSize\` of 0 evicts every entry after every run`,
|
|
179
240
|
)
|
|
180
241
|
}
|
|
181
|
-
// The bounds `vx cache prune` refuses, for the same reason: each evicts
|
|
182
|
-
// every entry, and here it would do so at the end of every run, the
|
|
183
|
-
// entries that run just saved included, so nothing ever hits (item 969).
|
|
184
|
-
if (olderThan !== undefined && parseDuration(olderThan) === 0) {
|
|
185
|
-
throw new UserError(`${field('olderThan')} of 0 evicts every entry after every run`)
|
|
186
|
-
}
|
|
187
|
-
if (maxSize !== undefined && parseSize(maxSize) === 0) {
|
|
188
|
-
throw new UserError(`${field('maxSize')} of 0 evicts every entry after every run`)
|
|
189
|
-
}
|
|
190
242
|
if (maxSize !== undefined && /^\d+$/.test(maxSize)) {
|
|
191
243
|
throw new UserError(
|
|
192
|
-
`${
|
|
244
|
+
`${configPath}: \`cache.retention.maxSize\` '${maxSize}' reads as ${maxSize} bytes — give a unit (e.g. '${maxSize}MB', '${maxSize}GB')`,
|
|
193
245
|
)
|
|
194
246
|
}
|
|
195
247
|
}
|
|
@@ -204,68 +256,10 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
204
256
|
// objects of functions by design, the file is evaluated in-process on
|
|
205
257
|
// every load (never through the config worker), and no key folds its JSON.
|
|
206
258
|
assertKnownFields(config, WORKSPACE_FIELDS, configPath)
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
typeof config.concurrency !== 'number' ||
|
|
210
|
-
!Number.isFinite(config.concurrency) ||
|
|
211
|
-
config.concurrency < 1 ||
|
|
212
|
-
!Number.isInteger(config.concurrency)
|
|
213
|
-
) {
|
|
214
|
-
throw new UserError(`${configPath}: \`concurrency\` must be a positive integer`)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (config.cacheDir !== undefined && typeof config.cacheDir !== 'string') {
|
|
218
|
-
throw new UserError(`${configPath}: \`cacheDir\` must be a string`)
|
|
219
|
-
}
|
|
220
|
-
// `' '` made a directory named three spaces at the root, hidden by the
|
|
221
|
-
// cache's own `.gitignore` (X-22).
|
|
222
|
-
if (
|
|
223
|
-
typeof config.cacheDir === 'string' &&
|
|
224
|
-
config.cacheDir !== '' &&
|
|
225
|
-
config.cacheDir.trim() === ''
|
|
226
|
-
) {
|
|
227
|
-
throw new UserError(`${configPath}: \`cacheDir\` is only whitespace — name a directory`)
|
|
228
|
-
}
|
|
229
|
-
// No path can carry one: the mkdir's argument error was reported as an
|
|
230
|
-
// unwritable workspace.
|
|
231
|
-
if (typeof config.cacheDir === 'string' && config.cacheDir.includes('\0')) {
|
|
232
|
-
throw new UserError(`${configPath}: \`cacheDir\` holds a NUL, which no path can carry`)
|
|
233
|
-
}
|
|
234
|
-
if (config.timeout !== undefined) {
|
|
235
|
-
if (
|
|
236
|
-
typeof config.timeout !== 'number' ||
|
|
237
|
-
!Number.isInteger(config.timeout) ||
|
|
238
|
-
config.timeout <= 0
|
|
239
|
-
) {
|
|
240
|
-
throw new UserError(`${configPath}: \`timeout\` must be a positive integer (milliseconds)`)
|
|
241
|
-
}
|
|
242
|
-
assertTimeoutInRange(config.timeout, `${configPath}: \`timeout\``)
|
|
243
|
-
}
|
|
244
|
-
if (config.cacheRetention !== undefined) validateRetention(config.cacheRetention, configPath)
|
|
245
|
-
if (config.affectedBase !== undefined) {
|
|
246
|
-
// A leading '-' would reach git as an option, not a ref; a NUL fails the
|
|
247
|
-
// spawn with an argument error naming nothing in this file.
|
|
248
|
-
if (
|
|
249
|
-
typeof config.affectedBase !== 'string' ||
|
|
250
|
-
!/^[^-\s\0][^\s\0]*$/.test(config.affectedBase)
|
|
251
|
-
) {
|
|
252
|
-
throw new UserError(`${configPath}: \`affectedBase\` must be a git ref like 'origin/main'`)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (config.cacheScope !== undefined && !isCacheScope(config.cacheScope)) {
|
|
256
|
-
throw new UserError(`${configPath}: \`cacheScope\` ${CACHE_SCOPE_RULE}`)
|
|
257
|
-
}
|
|
258
|
-
if (config.rules !== undefined) validateRules(config.rules, configPath)
|
|
259
|
-
if (config.inputStability !== undefined) {
|
|
260
|
-
validateInputStability(config.inputStability, `${configPath}: \`inputStability\``)
|
|
261
|
-
}
|
|
262
|
-
if (config.schedule !== undefined && !Object.hasOwn(SCHEDULES, config.schedule)) {
|
|
263
|
-
throw new UserError(
|
|
264
|
-
`${configPath}: \`schedule\` must be one of ${Object.keys(SCHEDULES)
|
|
265
|
-
.map((k) => `'${k}'`)
|
|
266
|
-
.join(', ')}`,
|
|
267
|
-
)
|
|
259
|
+
for (const group of SETTING_GROUPS) {
|
|
260
|
+
if (config[group] !== undefined) validateSettingsAt(config[group], group, configPath)
|
|
268
261
|
}
|
|
262
|
+
validateSettingsExtra(config, configPath)
|
|
269
263
|
if (config.plugins !== undefined) {
|
|
270
264
|
if (!Array.isArray(config.plugins)) {
|
|
271
265
|
throw new UserError(`${configPath}: \`plugins\` must be an array of plugin objects`)
|
|
@@ -288,7 +282,7 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
288
282
|
}
|
|
289
283
|
// The loose schema's shape (unknown-typed hooks) plus the one retired
|
|
290
284
|
// key this loader still refuses by name.
|
|
291
|
-
const plug = p as Partial<Plugin> & { name?: unknown
|
|
285
|
+
const plug = p as Partial<Plugin> & { name?: unknown }
|
|
292
286
|
// A plugin's name is its package name and nothing else. `definePlugin`
|
|
293
287
|
// reads it and stamps it under a registry symbol; a plain object, or
|
|
294
288
|
// one whose `name` was overwritten after the stamp, is refused here —
|
|
@@ -311,21 +305,6 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
311
305
|
`${configPath}: \`plugins[${i}].name\` overrides the package name ('${String(plug.name)}' over '${pkg}') — a plugin's name is its package name; drop the field`,
|
|
312
306
|
)
|
|
313
307
|
}
|
|
314
|
-
// The whole-run `backend` seam was REMOVED on 2026-08-23 with vx
|
|
315
|
-
// cloud: a run always executes in the `vx run` process. Nothing
|
|
316
|
-
// consults `.backend` any more, so leaving it in `caps` below meant a
|
|
317
|
-
// plugin declaring ONLY `backend` — a third-party one written against
|
|
318
|
-
// the old API, say — validated as "contributes a capability" and was
|
|
319
|
-
// then silently ignored. Refuse it by name and point at the seam that
|
|
320
|
-
// replaced it; a silent no-op is the failure this validation exists
|
|
321
|
-
// to prevent.
|
|
322
|
-
if (plug.backend !== undefined) {
|
|
323
|
-
throw new UserError(
|
|
324
|
-
`${configPath}: \`plugins[${i}].backend\` is no longer a capability — the whole-run ` +
|
|
325
|
-
`backend seam was removed in 2026-08. Use \`executor\` to change where a single ` +
|
|
326
|
-
`task's command runs (see docs/architecture.md § plugin capabilities).`,
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
308
|
// A misspelled hook (`excutor`, `setUp`) was never called and never
|
|
330
309
|
// said: tasks ran here with the plugin's executor unheard (X-19).
|
|
331
310
|
for (const key of Object.keys(plug)) {
|
|
@@ -412,11 +391,10 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
412
391
|
)
|
|
413
392
|
}
|
|
414
393
|
for (const file of claim.files as unknown[]) {
|
|
415
|
-
// A bare name at the workspace root: a lockfile
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// root arm `vx watch` keeps is not recursive.
|
|
394
|
+
// A bare name at the workspace root the plugin reads: a lockfile it
|
|
395
|
+
// folds, or a file its stages read (`turbo.json`; `--affected` and
|
|
396
|
+
// `vx watch` ask the claimant about it, item 961). A path is
|
|
397
|
+
// refused: the root arm `vx watch` keeps is not recursive.
|
|
420
398
|
if (
|
|
421
399
|
typeof file !== 'string' ||
|
|
422
400
|
file === '' ||
|
|
@@ -461,9 +439,15 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
461
439
|
* typechecked (plain .js, or TS with errors ignored). Catch the worst
|
|
462
440
|
* shape problems early with a clear message rather than letting them
|
|
463
441
|
* crash deeper in the orchestrator. Also applied to configs loaded
|
|
464
|
-
* back from
|
|
442
|
+
* back from `.vx/projects.lock.json` (a hand-editable file — same boundary).
|
|
443
|
+
* `ownConfig` is the config file outputs may not cover (null: none), apart
|
|
444
|
+
* from `configPath` when that is a label (`<path> (after plugin 'x')`).
|
|
465
445
|
*/
|
|
466
|
-
export function validateProjectConfig(
|
|
446
|
+
export function validateProjectConfig(
|
|
447
|
+
config: ProjectConfig,
|
|
448
|
+
configPath: string,
|
|
449
|
+
ownConfig: string | null = configPath,
|
|
450
|
+
): void {
|
|
467
451
|
// Before the schema, on the object as it stands: the key folds its JSON,
|
|
468
452
|
// so a value JSON cannot carry is refused here and in the config worker
|
|
469
453
|
// alike (json-data.ts). A plugin's `project` edit and a lock entry cross
|
|
@@ -473,6 +457,15 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
473
457
|
// The top level too: `task:` (singular) loaded as a project with no
|
|
474
458
|
// tasks and every request against it said "no projects declare".
|
|
475
459
|
assertKnownFields(config, PROJECT_FIELDS, configPath)
|
|
460
|
+
const name: unknown = config.name
|
|
461
|
+
if (name !== undefined) {
|
|
462
|
+
// A task is `<project>#<task>`, split at the first `#`.
|
|
463
|
+
if (typeof name !== 'string' || name === '' || name.trim() !== name || name.includes('#')) {
|
|
464
|
+
throw new UserError(
|
|
465
|
+
`${configPath}: \`name\` must be a non-empty string with no surrounding whitespace and no "#" (a task is <name>#<task>)`,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
476
469
|
const tags: unknown = config.tags
|
|
477
470
|
if (
|
|
478
471
|
tags !== undefined &&
|
|
@@ -534,10 +527,7 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
534
527
|
}
|
|
535
528
|
const timeout = (exec as { timeout?: unknown }).timeout
|
|
536
529
|
if (timeout !== undefined) {
|
|
537
|
-
|
|
538
|
-
throw new UserError(`${where}.exec.timeout must be a positive integer (milliseconds)`)
|
|
539
|
-
}
|
|
540
|
-
assertTimeoutInRange(timeout, `${where}.exec.timeout`)
|
|
530
|
+
validateSettingValue(timeout, 'run.timeout', `${where}.exec.timeout`)
|
|
541
531
|
}
|
|
542
532
|
const retries = (exec as { retries?: unknown }).retries
|
|
543
533
|
if (retries !== undefined) {
|
|
@@ -786,6 +776,10 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
786
776
|
)
|
|
787
777
|
}
|
|
788
778
|
assertKnownFields(outputs, CACHE_OUTPUT_FIELDS, `${where}.cache.outputs`)
|
|
779
|
+
const clean = (outputs as { clean?: unknown }).clean
|
|
780
|
+
if (clean !== undefined && typeof clean !== 'boolean') {
|
|
781
|
+
throw new UserError(`${where}.cache.outputs.clean must be true or false`)
|
|
782
|
+
}
|
|
789
783
|
const outFiles = (outputs as { files?: unknown }).files
|
|
790
784
|
if (!Array.isArray(outFiles)) {
|
|
791
785
|
throw new UserError(`${where}.cache.outputs.files must be an array of glob strings`)
|
|
@@ -834,12 +828,12 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
834
828
|
// Taken back by a `!`, the file is never cleaned or restored.
|
|
835
829
|
const own = g.startsWith('!')
|
|
836
830
|
? undefined
|
|
837
|
-
: ownFilesCovered(g,
|
|
831
|
+
: ownFilesCovered(g, ownConfig).find((f) => !takenBack(outFiles as unknown[], f))
|
|
838
832
|
if (own !== undefined) {
|
|
839
833
|
throw new UserError(
|
|
840
834
|
`${where}.cache.outputs.files: "${g}" covers the project's own ${own} — vx deletes a ` +
|
|
841
835
|
`task's outputs before it runs and restores them on a hit, so the project would lose ` +
|
|
842
|
-
`its
|
|
836
|
+
`its config. Name the directory the task writes, such as "dist/**", ` +
|
|
843
837
|
`or take the file back with "!${own}".`,
|
|
844
838
|
)
|
|
845
839
|
}
|
|
@@ -954,7 +948,7 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
954
948
|
// encodes. A new field must be added here deliberately.
|
|
955
949
|
// `tasks` first: `nearest` breaks a tie by order, and `taks` is one edit
|
|
956
950
|
// from both.
|
|
957
|
-
const PROJECT_FIELDS = new Set(['tasks', 'tags'])
|
|
951
|
+
const PROJECT_FIELDS = new Set(['name', 'tasks', 'tags'])
|
|
958
952
|
const TASK_FIELDS = new Set(['description', 'exec', 'dependsOn', 'cache'])
|
|
959
953
|
const EXEC_FIELDS = new Set([
|
|
960
954
|
'command',
|
|
@@ -977,50 +971,7 @@ const CACHE_INPUT_FIELDS = new Set([
|
|
|
977
971
|
'runtime',
|
|
978
972
|
'workspaceRuntime',
|
|
979
973
|
])
|
|
980
|
-
const CACHE_OUTPUT_FIELDS = new Set(['files', 'workspaceFiles'])
|
|
981
|
-
|
|
982
|
-
/** A field a release took out of the schema, and what took its place. */
|
|
983
|
-
interface Removal {
|
|
984
|
-
/** The vx version whose schema first refused it. */
|
|
985
|
-
removedIn: string
|
|
986
|
-
/** What to write instead, as a sentence fragment after "use". */
|
|
987
|
-
use: string
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/**
|
|
991
|
-
* Fields the schema once accepted, by the level that held them (keyed by
|
|
992
|
-
* that level's field set). A removed field is refused naming its
|
|
993
|
-
* replacement and the version that removed it, never as an unknown field a
|
|
994
|
-
* reader must research (docs/design/versioning-1.0.md § Deprecation). An
|
|
995
|
-
* entry stays for good: a config written against an old release meets it
|
|
996
|
-
* whenever it upgrades.
|
|
997
|
-
*/
|
|
998
|
-
export const REMOVED_FIELDS: ReadonlyMap<
|
|
999
|
-
ReadonlySet<string>,
|
|
1000
|
-
Readonly<Record<string, Removal>>
|
|
1001
|
-
> = new Map([
|
|
1002
|
-
[
|
|
1003
|
-
EXEC_FIELDS,
|
|
1004
|
-
{
|
|
1005
|
-
// Reservations left core with the `admit` seam (item 157).
|
|
1006
|
-
resources: {
|
|
1007
|
-
removedIn: '0.0.19',
|
|
1008
|
-
use:
|
|
1009
|
-
"`@vzn/vx-schedule-history`, which learns each task's reservation from its run " +
|
|
1010
|
-
"history (declare one by hand with its `reservations: { 'pkg#task': { cpus, memory } }`)",
|
|
1011
|
-
},
|
|
1012
|
-
},
|
|
1013
|
-
],
|
|
1014
|
-
[
|
|
1015
|
-
CACHE_FIELDS,
|
|
1016
|
-
{
|
|
1017
|
-
rewrittenInputs: {
|
|
1018
|
-
removedIn: '0.0.636',
|
|
1019
|
-
use: "`inputStability: { strategy: 'immutable' | 'mutable' }` ('withhold', 'compare')",
|
|
1020
|
-
},
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1023
|
-
])
|
|
974
|
+
const CACHE_OUTPUT_FIELDS = new Set(['files', 'workspaceFiles', 'clean'])
|
|
1024
975
|
|
|
1025
976
|
/**
|
|
1026
977
|
* Another runner's spelling of a field vx has under another name, by the
|
|
@@ -1052,7 +1003,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1052
1003
|
with: '`dependsOn` on each task it runs beside: a persistent one stays up, and this task starts once it is ready',
|
|
1053
1004
|
cwd: '`cd <dir> && …` in `exec.command`: a task runs in its project directory',
|
|
1054
1005
|
parallelism:
|
|
1055
|
-
'`concurrency: 1` in vx
|
|
1006
|
+
'`run.concurrency: 1` in .vx/workspace or `--concurrency 1` on the run: no task runs alone beside others',
|
|
1056
1007
|
configurations:
|
|
1057
1008
|
'one task per configuration (`build:production`, with its own `exec.command`)',
|
|
1058
1009
|
},
|
|
@@ -1109,14 +1060,13 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1109
1060
|
targets: '`tasks` (a target is a task: `exec.command`, `dependsOn`, `cache`)',
|
|
1110
1061
|
extends: 'an imported module spread into `tasks` (a vx.config is code; nothing is inherited)',
|
|
1111
1062
|
implicitDependencies: 'a `dependsOn` entry `pkg#task`, or a package.json dependency',
|
|
1112
|
-
name: 'the package.json `name` (a project is named by its package)',
|
|
1113
1063
|
// A package.json's, turbo.json's and project.json's keys, and a
|
|
1114
1064
|
// task's fields one level too high (D-99).
|
|
1115
1065
|
scripts:
|
|
1116
1066
|
"`tasks` (a script is a task's `exec.command`; `vx init` writes them from package.json)",
|
|
1117
1067
|
pipeline: '`tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)',
|
|
1118
1068
|
namedInputs: 'a module the vx.config imports, spread into `cache.inputs.files`',
|
|
1119
|
-
root: "no field: a project's directory is where its
|
|
1069
|
+
root: "no field: a project's directory is where its vx.config sits",
|
|
1120
1070
|
sourceRoot: "a task's `cache.inputs.files`",
|
|
1121
1071
|
projectType: 'no field: vx runs every project alike',
|
|
1122
1072
|
...Object.fromEntries(
|
|
@@ -1130,7 +1080,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1130
1080
|
env: '`tasks.<name>.cache.inputs.env` (to key a task on it) or `tasks.<name>.exec.env`',
|
|
1131
1081
|
},
|
|
1132
1082
|
],
|
|
1133
|
-
// Turbo's turbo.json and Nx's nx.json keys, written into vx
|
|
1083
|
+
// Turbo's turbo.json and Nx's nx.json keys, written into .vx/workspace
|
|
1134
1084
|
// (D-38).
|
|
1135
1085
|
[
|
|
1136
1086
|
WORKSPACE_FIELDS,
|
|
@@ -1146,33 +1096,47 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1146
1096
|
globalDependencies: '`cache.inputs.workspaceFiles` on the tasks it keys',
|
|
1147
1097
|
remoteCache:
|
|
1148
1098
|
'a cache plugin in `plugins` (`turboCache()` or `nxCache()` from `@vzn/vx-migrate`)',
|
|
1149
|
-
parallel: '`concurrency`',
|
|
1150
|
-
cacheDirectory: '`
|
|
1151
|
-
defaultBase: '`affectedBase`',
|
|
1099
|
+
parallel: '`run.concurrency`',
|
|
1100
|
+
cacheDirectory: '`cache.dir`',
|
|
1101
|
+
defaultBase: '`run.affectedBase`',
|
|
1152
1102
|
tasksRunnerOptions: 'a cache plugin in `plugins` (`nxCache()` from `@vzn/vx-migrate`)',
|
|
1153
1103
|
globalPassThroughEnv: '`exec.env.passThrough` on the tasks it passes to',
|
|
1154
1104
|
// pnpm-workspace.yaml's, Lerna's and the package.json's own keys
|
|
1155
|
-
// pasted into vx
|
|
1105
|
+
// pasted into .vx/workspace: refused with no word on where vx reads
|
|
1156
1106
|
// them (D-98).
|
|
1157
|
-
packages:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
"`workspaces` in package.json or pnpm-workspace.yaml's `packages`, where vx reads the members",
|
|
1161
|
-
projects:
|
|
1162
|
-
"`workspaces` in package.json or pnpm-workspace.yaml's `packages`, where vx reads the members",
|
|
1107
|
+
packages: 'a `vx.config` in each project directory: a project is a directory holding one',
|
|
1108
|
+
workspaces: 'a `vx.config` in each project directory: a project is a directory holding one',
|
|
1109
|
+
projects: 'a `vx.config` in each project directory: a project is a directory holding one',
|
|
1163
1110
|
ignore:
|
|
1164
|
-
|
|
1111
|
+
"no field: a directory with no `vx.config` is no project, and git's ignore rules apply",
|
|
1165
1112
|
catalog:
|
|
1166
1113
|
"pnpm-workspace.yaml's or package.json's `catalog`, where the package manager reads it",
|
|
1167
1114
|
npmClient: "no field: a task's command runs as written, with no package manager in between",
|
|
1168
|
-
cache:
|
|
1169
|
-
"each task's `cache` in its project's vx.config (a task caches only when it declares one)",
|
|
1170
1115
|
remote: 'a cache plugin in `plugins`',
|
|
1171
1116
|
env: '`cache.inputs.env` on the tasks it keys, or `exec.env` on the tasks it passes to',
|
|
1172
1117
|
},
|
|
1173
1118
|
],
|
|
1174
1119
|
])
|
|
1175
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* A top-level typo of a grouped setting, as its path: by its last name
|
|
1123
|
+
* (`concurency`) or its path run together (`cachedir`).
|
|
1124
|
+
*/
|
|
1125
|
+
function nearestSetting(key: string): string | undefined {
|
|
1126
|
+
const byName = new Map<string, string>()
|
|
1127
|
+
for (const s of SETTINGS) {
|
|
1128
|
+
if (s.config === false) continue
|
|
1129
|
+
const parts = s.path.split('.')
|
|
1130
|
+
byName.set(parts.at(-1)!, s.path)
|
|
1131
|
+
byName.set(
|
|
1132
|
+
parts.map((p, i) => (i === 0 ? p : p[0]!.toUpperCase() + p.slice(1))).join(''),
|
|
1133
|
+
s.path,
|
|
1134
|
+
)
|
|
1135
|
+
}
|
|
1136
|
+
const name = nearest(key, byName.keys())
|
|
1137
|
+
return name === undefined ? undefined : `\`${byName.get(name)}\``
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1176
1140
|
function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: string): void {
|
|
1177
1141
|
// `typeof [] === 'object'`, so an array reaches here and its indices read
|
|
1178
1142
|
// as fields: `outputs: ['dist/**']` (Turbo's spelling) was refused as
|
|
@@ -1185,16 +1149,11 @@ function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: s
|
|
|
1185
1149
|
}
|
|
1186
1150
|
for (const key of Object.keys(value)) {
|
|
1187
1151
|
if (!allowed.has(key)) {
|
|
1188
|
-
const removal = REMOVED_FIELDS.get(allowed)?.[key]
|
|
1189
|
-
if (removal !== undefined) {
|
|
1190
|
-
throw new UserError(
|
|
1191
|
-
`${where} has field "${key}", which vx ${removal.removedIn} removed — use ${removal.use}`,
|
|
1192
|
-
)
|
|
1193
|
-
}
|
|
1194
1152
|
// The nearest accepted spelling first: the list says what the level
|
|
1195
1153
|
// takes, the hint says which one was meant.
|
|
1196
1154
|
const foreign = FOREIGN_FIELDS.get(allowed)?.[key]
|
|
1197
|
-
const near =
|
|
1155
|
+
const near =
|
|
1156
|
+
nearest(key, allowed) ?? (allowed === WORKSPACE_FIELDS ? nearestSetting(key) : undefined)
|
|
1198
1157
|
throw new UserError(
|
|
1199
1158
|
`${where} has unknown field "${key}" (allowed: ${[...allowed].sort().join(', ')})` +
|
|
1200
1159
|
(foreign !== undefined
|
|
@@ -1208,35 +1167,17 @@ function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: s
|
|
|
1208
1167
|
}
|
|
1209
1168
|
|
|
1210
1169
|
/**
|
|
1211
|
-
*
|
|
1212
|
-
* `
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* back: silently substituting ~24.8 days for the 317 years they asked for would
|
|
1218
|
-
* trade one surprise for a quieter one. Costs nothing — such a config never
|
|
1219
|
-
* worked, it killed the task in milliseconds, so refusing reports a state that
|
|
1220
|
-
* already existed.
|
|
1221
|
-
*/
|
|
1222
|
-
function assertTimeoutInRange(ms: number, where: string): void {
|
|
1223
|
-
if (ms <= MAX_TIMEOUT_MS) return
|
|
1224
|
-
throw new UserError(
|
|
1225
|
-
`${where}: ${ms} ms exceeds the maximum timer delay (${MAX_TIMEOUT_MS} ms, ~24.8 days). ` +
|
|
1226
|
-
`Timers larger than this do NOT mean "no limit" — the platform reduces them to 1 ms, so ` +
|
|
1227
|
-
`the task would be killed the moment it starts. Omit \`timeout\` for no limit.`,
|
|
1228
|
-
)
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/**
|
|
1232
|
-
* The project file an output glob would take, or null. `outputs: ['**']`
|
|
1233
|
-
* loaded, and the clean before the run deleted the project's source,
|
|
1234
|
-
* `package.json` and `vx.config` while the run reported success (item
|
|
1235
|
-
* 1002); the message for `'.'` had even suggested `**`.
|
|
1170
|
+
* The project's config file if an output glob would take it. `outputs:
|
|
1171
|
+
* ['**']` loaded, and the clean before the run deleted the project's
|
|
1172
|
+
* source and `vx.config` while the run reported success (item 1002); the
|
|
1173
|
+
* message for `'.'` had even suggested `**`. Core names no other project
|
|
1174
|
+
* file: a file a plugin keys on is an input, and an output takes it out of
|
|
1175
|
+
* the input set like any other.
|
|
1236
1176
|
*/
|
|
1237
|
-
function ownFilesCovered(glob: string,
|
|
1238
|
-
|
|
1239
|
-
|
|
1177
|
+
function ownFilesCovered(glob: string, ownConfig: string | null): string[] {
|
|
1178
|
+
if (ownConfig === null) return []
|
|
1179
|
+
const own = path.basename(ownConfig)
|
|
1180
|
+
return new Bun.Glob(normalizeGlob(glob)).match(own) ? [own] : []
|
|
1240
1181
|
}
|
|
1241
1182
|
|
|
1242
1183
|
function takenBack(globs: readonly unknown[], file: string): boolean {
|