@vzn/vx 0.0.635 → 0.0.637
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 +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- 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 +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- 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 +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- 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 +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- 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-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- 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 +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -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/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- 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 +362 -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,
|
|
@@ -10,13 +10,13 @@ import {
|
|
|
10
10
|
PLUGIN_FUNCTION_HOOKS,
|
|
11
11
|
PLUGIN_HOOKS,
|
|
12
12
|
PLUGIN_PACKAGE,
|
|
13
|
+
type InputStability,
|
|
13
14
|
type Plugin,
|
|
14
15
|
type ProjectConfig,
|
|
15
16
|
type WorkspaceConfig,
|
|
16
17
|
} from '../config.js'
|
|
17
18
|
import {
|
|
18
19
|
CORE_VERBS,
|
|
19
|
-
MAX_TIMEOUT_MS,
|
|
20
20
|
nearest,
|
|
21
21
|
normalizeGlob,
|
|
22
22
|
parseDuration,
|
|
@@ -28,6 +28,14 @@ import {
|
|
|
28
28
|
asTrees,
|
|
29
29
|
isLiteralPattern,
|
|
30
30
|
} from '../util/index.js'
|
|
31
|
+
import {
|
|
32
|
+
parseSettingValue,
|
|
33
|
+
setting,
|
|
34
|
+
SETTING_GROUPS,
|
|
35
|
+
SETTINGS,
|
|
36
|
+
settingEnv,
|
|
37
|
+
settingRefusal,
|
|
38
|
+
} from '../settings.js'
|
|
31
39
|
import { compileNameGlob, parseFilter } from './filter.js'
|
|
32
40
|
import { nonJsonMessage, nonJsonPaths } from './json-data.js'
|
|
33
41
|
|
|
@@ -36,54 +44,173 @@ import { nonJsonMessage, nonJsonPaths } from './json-data.js'
|
|
|
36
44
|
// declared no plugins and ran the workspace bare, `cacheDirectory` left the
|
|
37
45
|
// cache where it was — a config that loads and quietly does nothing it says.
|
|
38
46
|
const CACHE_SCOPE_RULE =
|
|
39
|
-
"must be
|
|
47
|
+
"must be a scope name of letters, digits and . _ - / @ (at most 128), like 'pr-123'"
|
|
40
48
|
|
|
41
|
-
/** `
|
|
49
|
+
/** `cache.scope`'s rule, shared with `VX_CACHE_SCOPE`. */
|
|
42
50
|
export function isCacheScope(v: unknown): v is string {
|
|
43
51
|
return typeof v === 'string' && /^[\w.@/-]{1,128}$/.test(v)
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
/** Why `VX_CACHE_SCOPE` is refused, in `
|
|
54
|
+
/** Why a `VX_CACHE_SCOPE` or `--cache-scope` value is refused, in `cache.scope`'s words. */
|
|
55
|
+
export function cacheScopeError(source = 'VX_CACHE_SCOPE'): string {
|
|
56
|
+
return `${source} ${CACHE_SCOPE_RULE}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Why `VX_CACHE_SCOPE` is refused, as the run's error. */
|
|
47
60
|
export function cacheScopeEnvError(): UserError {
|
|
48
|
-
return new UserError(
|
|
61
|
+
return new UserError(cacheScopeError())
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'schedule',
|
|
60
|
-
'plugins',
|
|
61
|
-
])
|
|
64
|
+
const STRATEGIES = ['immutable', 'mutable', 'unchecked'] as const
|
|
65
|
+
const SEVERITIES = ['warn', 'error'] as const
|
|
66
|
+
const INPUT_STABILITY_FIELDS = new Set(['strategy', 'severity'])
|
|
67
|
+
const oneOf = (values: readonly string[]): string =>
|
|
68
|
+
`must be ${values
|
|
69
|
+
.map((v) => `'${v}'`)
|
|
70
|
+
.join(', ')
|
|
71
|
+
.replace(/, ([^,]*)$/, ' or $1')}`
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
function validateInputStability(stability: unknown, at: string): void {
|
|
74
|
+
if (typeof stability !== 'object' || stability === null || Array.isArray(stability)) {
|
|
75
|
+
throw new UserError(`${at} must be an object — \`inputStability: { strategy: 'mutable' }\``)
|
|
76
|
+
}
|
|
77
|
+
assertKnownFields(stability, INPUT_STABILITY_FIELDS, at)
|
|
78
|
+
const { strategy, severity } = stability as Record<string, unknown>
|
|
79
|
+
if (strategy !== undefined && !STRATEGIES.includes(strategy as never)) {
|
|
80
|
+
throw new UserError(`${at}.strategy ${oneOf(STRATEGIES)}`)
|
|
81
|
+
}
|
|
82
|
+
if (severity !== undefined && !SEVERITIES.includes(severity as never)) {
|
|
83
|
+
throw new UserError(`${at}.severity ${oneOf(SEVERITIES)}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
/**
|
|
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.
|
|
91
|
+
*/
|
|
92
|
+
export function inputStabilityEnv(): InputStability {
|
|
93
|
+
const strategy = settingEnv('cache.inputStability.strategy') as InputStability['strategy']
|
|
94
|
+
const severity = settingEnv('cache.inputStability.severity') as InputStability['severity']
|
|
95
|
+
return {
|
|
96
|
+
...(strategy !== undefined ? { strategy } : {}),
|
|
97
|
+
...(severity !== undefined ? { severity } : {}),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
const WORKSPACE_FIELDS = new Set<string>([...SETTING_GROUPS, 'plugins'])
|
|
102
|
+
|
|
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]!)
|
|
77
115
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
116
|
+
}
|
|
117
|
+
// A list of objects, so no setting: no variable or flag can say it.
|
|
118
|
+
GROUP_FIELDS.get('run.rules')!.add('dependencies')
|
|
119
|
+
|
|
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)
|
|
82
136
|
}
|
|
137
|
+
return
|
|
83
138
|
}
|
|
139
|
+
if (at === 'run.rules.dependencies') validateDependencyRules(value, where)
|
|
140
|
+
else validateSettingValue(value, at, where)
|
|
84
141
|
}
|
|
85
142
|
|
|
86
|
-
const
|
|
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'`)
|
|
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\``)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
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
|
+
}
|
|
87
214
|
|
|
88
215
|
/**
|
|
89
216
|
* A refused value as a message quotes it. A workspace file is evaluated
|
|
@@ -95,43 +222,26 @@ function shown(v: unknown): string {
|
|
|
95
222
|
return typeof v === 'bigint' ? `${v}n` : (JSON.stringify(v) ?? String(v))
|
|
96
223
|
}
|
|
97
224
|
|
|
98
|
-
function validateRetention(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
assertKnownFields(retention as Record<string, unknown>, RETENTION_FIELDS, where)
|
|
105
|
-
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
|
|
106
230
|
// An empty object would load and evict nothing, which reads as a policy.
|
|
107
231
|
if (olderThan === undefined && maxSize === undefined) {
|
|
108
|
-
throw new UserError(`${where} names neither \`olderThan\` nor \`maxSize\``)
|
|
109
|
-
}
|
|
110
|
-
if (
|
|
111
|
-
olderThan !== undefined &&
|
|
112
|
-
(typeof olderThan !== 'string' || parseDuration(olderThan) === null)
|
|
113
|
-
) {
|
|
114
232
|
throw new UserError(
|
|
115
|
-
`${
|
|
233
|
+
`${configPath}: \`cache.retention\` names neither \`olderThan\` nor \`maxSize\``,
|
|
116
234
|
)
|
|
117
235
|
}
|
|
118
|
-
|
|
236
|
+
// As for olderThan (item 969).
|
|
237
|
+
if (maxSize !== undefined && parseSize(maxSize) === 0) {
|
|
119
238
|
throw new UserError(
|
|
120
|
-
`${
|
|
239
|
+
`${configPath}: \`cache.retention.maxSize\` of 0 evicts every entry after every run`,
|
|
121
240
|
)
|
|
122
241
|
}
|
|
123
|
-
// The bounds `vx cache prune` refuses, for the same reason: each evicts
|
|
124
|
-
// every entry, and here it would do so at the end of every run, the
|
|
125
|
-
// entries that run just saved included, so nothing ever hits (item 969).
|
|
126
|
-
if (olderThan !== undefined && parseDuration(olderThan) === 0) {
|
|
127
|
-
throw new UserError(`${field('olderThan')} of 0 evicts every entry after every run`)
|
|
128
|
-
}
|
|
129
|
-
if (maxSize !== undefined && parseSize(maxSize) === 0) {
|
|
130
|
-
throw new UserError(`${field('maxSize')} of 0 evicts every entry after every run`)
|
|
131
|
-
}
|
|
132
242
|
if (maxSize !== undefined && /^\d+$/.test(maxSize)) {
|
|
133
243
|
throw new UserError(
|
|
134
|
-
`${
|
|
244
|
+
`${configPath}: \`cache.retention.maxSize\` '${maxSize}' reads as ${maxSize} bytes — give a unit (e.g. '${maxSize}MB', '${maxSize}GB')`,
|
|
135
245
|
)
|
|
136
246
|
}
|
|
137
247
|
}
|
|
@@ -146,65 +256,10 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
146
256
|
// objects of functions by design, the file is evaluated in-process on
|
|
147
257
|
// every load (never through the config worker), and no key folds its JSON.
|
|
148
258
|
assertKnownFields(config, WORKSPACE_FIELDS, configPath)
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
typeof config.concurrency !== 'number' ||
|
|
152
|
-
!Number.isFinite(config.concurrency) ||
|
|
153
|
-
config.concurrency < 1 ||
|
|
154
|
-
!Number.isInteger(config.concurrency)
|
|
155
|
-
) {
|
|
156
|
-
throw new UserError(`${configPath}: \`concurrency\` must be a positive integer`)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (config.cacheDir !== undefined && typeof config.cacheDir !== 'string') {
|
|
160
|
-
throw new UserError(`${configPath}: \`cacheDir\` must be a string`)
|
|
161
|
-
}
|
|
162
|
-
// `' '` made a directory named three spaces at the root, hidden by the
|
|
163
|
-
// cache's own `.gitignore` (X-22).
|
|
164
|
-
if (
|
|
165
|
-
typeof config.cacheDir === 'string' &&
|
|
166
|
-
config.cacheDir !== '' &&
|
|
167
|
-
config.cacheDir.trim() === ''
|
|
168
|
-
) {
|
|
169
|
-
throw new UserError(`${configPath}: \`cacheDir\` is only whitespace — name a directory`)
|
|
170
|
-
}
|
|
171
|
-
// No path can carry one: the mkdir's argument error was reported as an
|
|
172
|
-
// unwritable workspace.
|
|
173
|
-
if (typeof config.cacheDir === 'string' && config.cacheDir.includes('\0')) {
|
|
174
|
-
throw new UserError(`${configPath}: \`cacheDir\` holds a NUL, which no path can carry`)
|
|
175
|
-
}
|
|
176
|
-
if (config.timeout !== undefined) {
|
|
177
|
-
if (
|
|
178
|
-
typeof config.timeout !== 'number' ||
|
|
179
|
-
!Number.isInteger(config.timeout) ||
|
|
180
|
-
config.timeout <= 0
|
|
181
|
-
) {
|
|
182
|
-
throw new UserError(`${configPath}: \`timeout\` must be a positive integer (milliseconds)`)
|
|
183
|
-
}
|
|
184
|
-
assertTimeoutInRange(config.timeout, `${configPath}: \`timeout\``)
|
|
185
|
-
}
|
|
186
|
-
if (config.cacheRetention !== undefined) validateRetention(config.cacheRetention, configPath)
|
|
187
|
-
if (config.affectedBase !== undefined) {
|
|
188
|
-
// A leading '-' would reach git as an option, not a ref; a NUL fails the
|
|
189
|
-
// spawn with an argument error naming nothing in this file.
|
|
190
|
-
if (
|
|
191
|
-
typeof config.affectedBase !== 'string' ||
|
|
192
|
-
!/^[^-\s\0][^\s\0]*$/.test(config.affectedBase)
|
|
193
|
-
) {
|
|
194
|
-
throw new UserError(`${configPath}: \`affectedBase\` must be a git ref like 'origin/main'`)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (config.cacheScope !== undefined && !isCacheScope(config.cacheScope)) {
|
|
198
|
-
throw new UserError(`${configPath}: \`cacheScope\` ${CACHE_SCOPE_RULE}`)
|
|
199
|
-
}
|
|
200
|
-
if (config.rules !== undefined) validateRules(config.rules, configPath)
|
|
201
|
-
if (config.schedule !== undefined && !Object.hasOwn(SCHEDULES, config.schedule)) {
|
|
202
|
-
throw new UserError(
|
|
203
|
-
`${configPath}: \`schedule\` must be one of ${Object.keys(SCHEDULES)
|
|
204
|
-
.map((k) => `'${k}'`)
|
|
205
|
-
.join(', ')}`,
|
|
206
|
-
)
|
|
259
|
+
for (const group of SETTING_GROUPS) {
|
|
260
|
+
if (config[group] !== undefined) validateSettingsAt(config[group], group, configPath)
|
|
207
261
|
}
|
|
262
|
+
validateSettingsExtra(config, configPath)
|
|
208
263
|
if (config.plugins !== undefined) {
|
|
209
264
|
if (!Array.isArray(config.plugins)) {
|
|
210
265
|
throw new UserError(`${configPath}: \`plugins\` must be an array of plugin objects`)
|
|
@@ -227,7 +282,7 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
227
282
|
}
|
|
228
283
|
// The loose schema's shape (unknown-typed hooks) plus the one retired
|
|
229
284
|
// key this loader still refuses by name.
|
|
230
|
-
const plug = p as Partial<Plugin> & { name?: unknown
|
|
285
|
+
const plug = p as Partial<Plugin> & { name?: unknown }
|
|
231
286
|
// A plugin's name is its package name and nothing else. `definePlugin`
|
|
232
287
|
// reads it and stamps it under a registry symbol; a plain object, or
|
|
233
288
|
// one whose `name` was overwritten after the stamp, is refused here —
|
|
@@ -250,21 +305,6 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
250
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`,
|
|
251
306
|
)
|
|
252
307
|
}
|
|
253
|
-
// The whole-run `backend` seam was REMOVED on 2026-08-23 with vx
|
|
254
|
-
// cloud: a run always executes in the `vx run` process. Nothing
|
|
255
|
-
// consults `.backend` any more, so leaving it in `caps` below meant a
|
|
256
|
-
// plugin declaring ONLY `backend` — a third-party one written against
|
|
257
|
-
// the old API, say — validated as "contributes a capability" and was
|
|
258
|
-
// then silently ignored. Refuse it by name and point at the seam that
|
|
259
|
-
// replaced it; a silent no-op is the failure this validation exists
|
|
260
|
-
// to prevent.
|
|
261
|
-
if (plug.backend !== undefined) {
|
|
262
|
-
throw new UserError(
|
|
263
|
-
`${configPath}: \`plugins[${i}].backend\` is no longer a capability — the whole-run ` +
|
|
264
|
-
`backend seam was removed in 2026-08. Use \`executor\` to change where a single ` +
|
|
265
|
-
`task's command runs (see docs/architecture.md § plugin capabilities).`,
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
308
|
// A misspelled hook (`excutor`, `setUp`) was never called and never
|
|
269
309
|
// said: tasks ran here with the plugin's executor unheard (X-19).
|
|
270
310
|
for (const key of Object.keys(plug)) {
|
|
@@ -351,11 +391,10 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
351
391
|
)
|
|
352
392
|
}
|
|
353
393
|
for (const file of claim.files as unknown[]) {
|
|
354
|
-
// A bare name at the workspace root: a lockfile
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// 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.
|
|
359
398
|
if (
|
|
360
399
|
typeof file !== 'string' ||
|
|
361
400
|
file === '' ||
|
|
@@ -400,9 +439,15 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
|
|
|
400
439
|
* typechecked (plain .js, or TS with errors ignored). Catch the worst
|
|
401
440
|
* shape problems early with a clear message rather than letting them
|
|
402
441
|
* crash deeper in the orchestrator. Also applied to configs loaded
|
|
403
|
-
* 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')`).
|
|
404
445
|
*/
|
|
405
|
-
export function validateProjectConfig(
|
|
446
|
+
export function validateProjectConfig(
|
|
447
|
+
config: ProjectConfig,
|
|
448
|
+
configPath: string,
|
|
449
|
+
ownConfig: string | null = configPath,
|
|
450
|
+
): void {
|
|
406
451
|
// Before the schema, on the object as it stands: the key folds its JSON,
|
|
407
452
|
// so a value JSON cannot carry is refused here and in the config worker
|
|
408
453
|
// alike (json-data.ts). A plugin's `project` edit and a lock entry cross
|
|
@@ -412,6 +457,15 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
412
457
|
// The top level too: `task:` (singular) loaded as a project with no
|
|
413
458
|
// tasks and every request against it said "no projects declare".
|
|
414
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
|
+
}
|
|
415
469
|
const tags: unknown = config.tags
|
|
416
470
|
if (
|
|
417
471
|
tags !== undefined &&
|
|
@@ -473,10 +527,7 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
473
527
|
}
|
|
474
528
|
const timeout = (exec as { timeout?: unknown }).timeout
|
|
475
529
|
if (timeout !== undefined) {
|
|
476
|
-
|
|
477
|
-
throw new UserError(`${where}.exec.timeout must be a positive integer (milliseconds)`)
|
|
478
|
-
}
|
|
479
|
-
assertTimeoutInRange(timeout, `${where}.exec.timeout`)
|
|
530
|
+
validateSettingValue(timeout, 'run.timeout', `${where}.exec.timeout`)
|
|
480
531
|
}
|
|
481
532
|
const retries = (exec as { retries?: unknown }).retries
|
|
482
533
|
if (retries !== undefined) {
|
|
@@ -668,10 +719,9 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
668
719
|
)
|
|
669
720
|
}
|
|
670
721
|
assertKnownFields(cache, CACHE_FIELDS, `${where}.cache`)
|
|
671
|
-
const
|
|
672
|
-
if (
|
|
673
|
-
|
|
674
|
-
}
|
|
722
|
+
const stability = (cache as { inputStability?: unknown }).inputStability
|
|
723
|
+
if (stability !== undefined)
|
|
724
|
+
validateInputStability(stability, `${where}.cache.inputStability`)
|
|
675
725
|
const inputs = (cache as { inputs?: unknown }).inputs
|
|
676
726
|
const outputs = (cache as { outputs?: unknown }).outputs
|
|
677
727
|
// A present field of the wrong shape (`outputs: 'dist'`) was told it
|
|
@@ -726,6 +776,10 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
726
776
|
)
|
|
727
777
|
}
|
|
728
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
|
+
}
|
|
729
783
|
const outFiles = (outputs as { files?: unknown }).files
|
|
730
784
|
if (!Array.isArray(outFiles)) {
|
|
731
785
|
throw new UserError(`${where}.cache.outputs.files must be an array of glob strings`)
|
|
@@ -774,12 +828,12 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
774
828
|
// Taken back by a `!`, the file is never cleaned or restored.
|
|
775
829
|
const own = g.startsWith('!')
|
|
776
830
|
? undefined
|
|
777
|
-
: ownFilesCovered(g,
|
|
831
|
+
: ownFilesCovered(g, ownConfig).find((f) => !takenBack(outFiles as unknown[], f))
|
|
778
832
|
if (own !== undefined) {
|
|
779
833
|
throw new UserError(
|
|
780
834
|
`${where}.cache.outputs.files: "${g}" covers the project's own ${own} — vx deletes a ` +
|
|
781
835
|
`task's outputs before it runs and restores them on a hit, so the project would lose ` +
|
|
782
|
-
`its
|
|
836
|
+
`its config. Name the directory the task writes, such as "dist/**", ` +
|
|
783
837
|
`or take the file back with "!${own}".`,
|
|
784
838
|
)
|
|
785
839
|
}
|
|
@@ -894,7 +948,7 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
894
948
|
// encodes. A new field must be added here deliberately.
|
|
895
949
|
// `tasks` first: `nearest` breaks a tie by order, and `taks` is one edit
|
|
896
950
|
// from both.
|
|
897
|
-
const PROJECT_FIELDS = new Set(['tasks', 'tags'])
|
|
951
|
+
const PROJECT_FIELDS = new Set(['name', 'tasks', 'tags'])
|
|
898
952
|
const TASK_FIELDS = new Set(['description', 'exec', 'dependsOn', 'cache'])
|
|
899
953
|
const EXEC_FIELDS = new Set([
|
|
900
954
|
'command',
|
|
@@ -908,7 +962,7 @@ const EXEC_FIELDS = new Set([
|
|
|
908
962
|
])
|
|
909
963
|
const PERSISTENT_FIELDS = new Set(['readyWhen'])
|
|
910
964
|
const ENV_FIELDS = new Set(['passThrough', 'define', 'secret'])
|
|
911
|
-
const CACHE_FIELDS = new Set(['inputs', 'outputs', '
|
|
965
|
+
const CACHE_FIELDS = new Set(['inputs', 'outputs', 'inputStability'])
|
|
912
966
|
const CACHE_INPUT_FIELDS = new Set([
|
|
913
967
|
'files',
|
|
914
968
|
'workspaceFiles',
|
|
@@ -917,41 +971,7 @@ const CACHE_INPUT_FIELDS = new Set([
|
|
|
917
971
|
'runtime',
|
|
918
972
|
'workspaceRuntime',
|
|
919
973
|
])
|
|
920
|
-
const CACHE_OUTPUT_FIELDS = new Set(['files', 'workspaceFiles'])
|
|
921
|
-
|
|
922
|
-
/** A field a release took out of the schema, and what took its place. */
|
|
923
|
-
interface Removal {
|
|
924
|
-
/** The vx version whose schema first refused it. */
|
|
925
|
-
removedIn: string
|
|
926
|
-
/** What to write instead, as a sentence fragment after "use". */
|
|
927
|
-
use: string
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Fields the schema once accepted, by the level that held them (keyed by
|
|
932
|
-
* that level's field set). A removed field is refused naming its
|
|
933
|
-
* replacement and the version that removed it, never as an unknown field a
|
|
934
|
-
* reader must research (docs/design/versioning-1.0.md § Deprecation). An
|
|
935
|
-
* entry stays for good: a config written against an old release meets it
|
|
936
|
-
* whenever it upgrades.
|
|
937
|
-
*/
|
|
938
|
-
export const REMOVED_FIELDS: ReadonlyMap<
|
|
939
|
-
ReadonlySet<string>,
|
|
940
|
-
Readonly<Record<string, Removal>>
|
|
941
|
-
> = new Map([
|
|
942
|
-
[
|
|
943
|
-
EXEC_FIELDS,
|
|
944
|
-
{
|
|
945
|
-
// Reservations left core with the `admit` seam (item 157).
|
|
946
|
-
resources: {
|
|
947
|
-
removedIn: '0.0.19',
|
|
948
|
-
use:
|
|
949
|
-
"`@vzn/vx-schedule-history`, which learns each task's reservation from its run " +
|
|
950
|
-
"history (declare one by hand with its `reservations: { 'pkg#task': { cpus, memory } }`)",
|
|
951
|
-
},
|
|
952
|
-
},
|
|
953
|
-
],
|
|
954
|
-
])
|
|
974
|
+
const CACHE_OUTPUT_FIELDS = new Set(['files', 'workspaceFiles', 'clean'])
|
|
955
975
|
|
|
956
976
|
/**
|
|
957
977
|
* Another runner's spelling of a field vx has under another name, by the
|
|
@@ -983,7 +1003,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
983
1003
|
with: '`dependsOn` on each task it runs beside: a persistent one stays up, and this task starts once it is ready',
|
|
984
1004
|
cwd: '`cd <dir> && …` in `exec.command`: a task runs in its project directory',
|
|
985
1005
|
parallelism:
|
|
986
|
-
'`concurrency: 1` in vx
|
|
1006
|
+
'`run.concurrency: 1` in .vx/workspace or `--concurrency 1` on the run: no task runs alone beside others',
|
|
987
1007
|
configurations:
|
|
988
1008
|
'one task per configuration (`build:production`, with its own `exec.command`)',
|
|
989
1009
|
},
|
|
@@ -1040,14 +1060,13 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1040
1060
|
targets: '`tasks` (a target is a task: `exec.command`, `dependsOn`, `cache`)',
|
|
1041
1061
|
extends: 'an imported module spread into `tasks` (a vx.config is code; nothing is inherited)',
|
|
1042
1062
|
implicitDependencies: 'a `dependsOn` entry `pkg#task`, or a package.json dependency',
|
|
1043
|
-
name: 'the package.json `name` (a project is named by its package)',
|
|
1044
1063
|
// A package.json's, turbo.json's and project.json's keys, and a
|
|
1045
1064
|
// task's fields one level too high (D-99).
|
|
1046
1065
|
scripts:
|
|
1047
1066
|
"`tasks` (a script is a task's `exec.command`; `vx init` writes them from package.json)",
|
|
1048
1067
|
pipeline: '`tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)',
|
|
1049
1068
|
namedInputs: 'a module the vx.config imports, spread into `cache.inputs.files`',
|
|
1050
|
-
root: "no field: a project's directory is where its
|
|
1069
|
+
root: "no field: a project's directory is where its vx.config sits",
|
|
1051
1070
|
sourceRoot: "a task's `cache.inputs.files`",
|
|
1052
1071
|
projectType: 'no field: vx runs every project alike',
|
|
1053
1072
|
...Object.fromEntries(
|
|
@@ -1061,7 +1080,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1061
1080
|
env: '`tasks.<name>.cache.inputs.env` (to key a task on it) or `tasks.<name>.exec.env`',
|
|
1062
1081
|
},
|
|
1063
1082
|
],
|
|
1064
|
-
// 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
|
|
1065
1084
|
// (D-38).
|
|
1066
1085
|
[
|
|
1067
1086
|
WORKSPACE_FIELDS,
|
|
@@ -1077,33 +1096,47 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1077
1096
|
globalDependencies: '`cache.inputs.workspaceFiles` on the tasks it keys',
|
|
1078
1097
|
remoteCache:
|
|
1079
1098
|
'a cache plugin in `plugins` (`turboCache()` or `nxCache()` from `@vzn/vx-migrate`)',
|
|
1080
|
-
parallel: '`concurrency`',
|
|
1081
|
-
cacheDirectory: '`
|
|
1082
|
-
defaultBase: '`affectedBase`',
|
|
1099
|
+
parallel: '`run.concurrency`',
|
|
1100
|
+
cacheDirectory: '`cache.dir`',
|
|
1101
|
+
defaultBase: '`run.affectedBase`',
|
|
1083
1102
|
tasksRunnerOptions: 'a cache plugin in `plugins` (`nxCache()` from `@vzn/vx-migrate`)',
|
|
1084
1103
|
globalPassThroughEnv: '`exec.env.passThrough` on the tasks it passes to',
|
|
1085
1104
|
// pnpm-workspace.yaml's, Lerna's and the package.json's own keys
|
|
1086
|
-
// pasted into vx
|
|
1105
|
+
// pasted into .vx/workspace: refused with no word on where vx reads
|
|
1087
1106
|
// them (D-98).
|
|
1088
|
-
packages:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
"`workspaces` in package.json or pnpm-workspace.yaml's `packages`, where vx reads the members",
|
|
1092
|
-
projects:
|
|
1093
|
-
"`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',
|
|
1094
1110
|
ignore:
|
|
1095
|
-
|
|
1111
|
+
"no field: a directory with no `vx.config` is no project, and git's ignore rules apply",
|
|
1096
1112
|
catalog:
|
|
1097
1113
|
"pnpm-workspace.yaml's or package.json's `catalog`, where the package manager reads it",
|
|
1098
1114
|
npmClient: "no field: a task's command runs as written, with no package manager in between",
|
|
1099
|
-
cache:
|
|
1100
|
-
"each task's `cache` in its project's vx.config (a task caches only when it declares one)",
|
|
1101
1115
|
remote: 'a cache plugin in `plugins`',
|
|
1102
1116
|
env: '`cache.inputs.env` on the tasks it keys, or `exec.env` on the tasks it passes to',
|
|
1103
1117
|
},
|
|
1104
1118
|
],
|
|
1105
1119
|
])
|
|
1106
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
|
+
|
|
1107
1140
|
function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: string): void {
|
|
1108
1141
|
// `typeof [] === 'object'`, so an array reaches here and its indices read
|
|
1109
1142
|
// as fields: `outputs: ['dist/**']` (Turbo's spelling) was refused as
|
|
@@ -1116,16 +1149,11 @@ function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: s
|
|
|
1116
1149
|
}
|
|
1117
1150
|
for (const key of Object.keys(value)) {
|
|
1118
1151
|
if (!allowed.has(key)) {
|
|
1119
|
-
const removal = REMOVED_FIELDS.get(allowed)?.[key]
|
|
1120
|
-
if (removal !== undefined) {
|
|
1121
|
-
throw new UserError(
|
|
1122
|
-
`${where} has field "${key}", which vx ${removal.removedIn} removed — use ${removal.use}`,
|
|
1123
|
-
)
|
|
1124
|
-
}
|
|
1125
1152
|
// The nearest accepted spelling first: the list says what the level
|
|
1126
1153
|
// takes, the hint says which one was meant.
|
|
1127
1154
|
const foreign = FOREIGN_FIELDS.get(allowed)?.[key]
|
|
1128
|
-
const near =
|
|
1155
|
+
const near =
|
|
1156
|
+
nearest(key, allowed) ?? (allowed === WORKSPACE_FIELDS ? nearestSetting(key) : undefined)
|
|
1129
1157
|
throw new UserError(
|
|
1130
1158
|
`${where} has unknown field "${key}" (allowed: ${[...allowed].sort().join(', ')})` +
|
|
1131
1159
|
(foreign !== undefined
|
|
@@ -1139,35 +1167,17 @@ function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: s
|
|
|
1139
1167
|
}
|
|
1140
1168
|
|
|
1141
1169
|
/**
|
|
1142
|
-
*
|
|
1143
|
-
* `
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
* back: silently substituting ~24.8 days for the 317 years they asked for would
|
|
1149
|
-
* trade one surprise for a quieter one. Costs nothing — such a config never
|
|
1150
|
-
* worked, it killed the task in milliseconds, so refusing reports a state that
|
|
1151
|
-
* already existed.
|
|
1152
|
-
*/
|
|
1153
|
-
function assertTimeoutInRange(ms: number, where: string): void {
|
|
1154
|
-
if (ms <= MAX_TIMEOUT_MS) return
|
|
1155
|
-
throw new UserError(
|
|
1156
|
-
`${where}: ${ms} ms exceeds the maximum timer delay (${MAX_TIMEOUT_MS} ms, ~24.8 days). ` +
|
|
1157
|
-
`Timers larger than this do NOT mean "no limit" — the platform reduces them to 1 ms, so ` +
|
|
1158
|
-
`the task would be killed the moment it starts. Omit \`timeout\` for no limit.`,
|
|
1159
|
-
)
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
* The project file an output glob would take, or null. `outputs: ['**']`
|
|
1164
|
-
* loaded, and the clean before the run deleted the project's source,
|
|
1165
|
-
* `package.json` and `vx.config` while the run reported success (item
|
|
1166
|
-
* 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.
|
|
1167
1176
|
*/
|
|
1168
|
-
function ownFilesCovered(glob: string,
|
|
1169
|
-
|
|
1170
|
-
|
|
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] : []
|
|
1171
1181
|
}
|
|
1172
1182
|
|
|
1173
1183
|
function takenBack(globs: readonly unknown[], file: string): boolean {
|