@vzn/vx 0.0.651 → 0.0.652
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 +2 -3
- package/package.json +5 -5
- package/schemas/show.json +14 -20
- package/src/bin.ts +12 -0
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +19 -5
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/inputs.ts +74 -14
- package/src/cache/layer.ts +6 -6
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/run-history.ts +46 -17
- package/src/cache/schema.ts +4 -1
- package/src/cli/cache.ts +2 -2
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +24 -0
- package/src/cli/info.ts +1 -1
- package/src/cli/last.ts +34 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/show.ts +7 -10
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +11 -8
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +40 -1
- package/src/exec/index.ts +1 -0
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +314 -29
- package/src/exec/task-proxy.ts +93 -0
- package/src/graph/definition.ts +16 -3
- package/src/graph/scheduler.ts +3 -1
- package/src/graph/task-graph.ts +24 -23
- package/src/history/failure-mode.ts +24 -1
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +5 -5
- package/src/index.ts +4 -2
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +51 -18
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/miss-save.ts +27 -8
- package/src/orchestrator/persistent-task.ts +2 -0
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +2 -2
- package/src/orchestrator/plugin-host.ts +6 -2
- package/src/orchestrator/plugin.ts +6 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-failures.ts +61 -12
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +86 -67
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +92 -19
- package/src/orchestrator/sandbox-request.ts +50 -7
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +4 -4
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +3 -3
- package/src/report/framed-output.ts +16 -7
- package/src/report/logger.ts +1 -1
- package/src/report/plain-output.ts +2 -0
- package/src/report/summary.ts +5 -5
- package/src/settings.ts +8 -8
- package/src/util/config-frame.ts +128 -6
- package/src/util/paths.ts +1 -1
- package/src/util/settle.ts +2 -1
- package/src/util/tail.ts +4 -1
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +66 -61
- package/src/workspace/filter.ts +8 -1
- package/src/workspace/migration.ts +12 -6
|
@@ -268,13 +268,22 @@ const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
|
|
|
268
268
|
const OUT_COL = 7 // `outputs`: "12MB", "900B"
|
|
269
269
|
const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
|
|
270
270
|
|
|
271
|
+
// A row's padding, built once: `' '.repeat` per cell was ~7 ms of an
|
|
272
|
+
// 8,002-hit warm run's rows, the largest single cost of writing them.
|
|
273
|
+
const SPACES = Array.from({ length: 33 }, (_, n) => ' '.repeat(n))
|
|
274
|
+
|
|
275
|
+
/** `n` spaces (none below 1). */
|
|
276
|
+
function pad(n: number): string {
|
|
277
|
+
return n <= 0 ? '' : (SPACES[n] ?? ' '.repeat(n))
|
|
278
|
+
}
|
|
279
|
+
|
|
271
280
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
272
281
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
273
282
|
let raw = ms === null ? '' : formatElapsed(ms)
|
|
274
283
|
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
275
284
|
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
276
285
|
// The time is the row's fact (value level); the cells around it are meta.
|
|
277
|
-
return
|
|
286
|
+
return pad(TIME_COL - raw.length) + paint('value', raw, colors)
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
/**
|
|
@@ -284,7 +293,7 @@ function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
|
284
293
|
*/
|
|
285
294
|
function cell(text: string, color: Style | '', width: number, colors: ColorSupport): string {
|
|
286
295
|
const painted = text === '' ? '' : paint(color === '' ? 'muted' : color, text, colors)
|
|
287
|
-
return painted +
|
|
296
|
+
return painted + pad(width - text.length)
|
|
288
297
|
}
|
|
289
298
|
|
|
290
299
|
/** The cache-shape glyph (task-shape for non-success). */
|
|
@@ -364,7 +373,7 @@ export function formatTaskRow(
|
|
|
364
373
|
// `aborting` runs one into the cache cell, which is blank beside it.
|
|
365
374
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
366
375
|
const usual = cells.usual ?? { text: '', painted: '' }
|
|
367
|
-
const us = `${
|
|
376
|
+
const us = `${pad(USUAL_COL - usual.text.length)}${usual.painted}`
|
|
368
377
|
const cpu =
|
|
369
378
|
cells.cpu === undefined ? '' : cells.cpu === null ? UNKNOWN : `${Math.round(cells.cpu * 100)}%`
|
|
370
379
|
const mem =
|
|
@@ -379,8 +388,8 @@ export function formatTaskRow(
|
|
|
379
388
|
return (
|
|
380
389
|
` ${glyph} ${timeCell(ms, colors)} ${us} ${st} ${ca} ` +
|
|
381
390
|
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
382
|
-
`${
|
|
383
|
-
`${placeCell(worker, colors)}${
|
|
391
|
+
`${pad(BLOCKED_COL - blocks.length)}${paint(blocks === '0' ? 'muted' : 'value', blocks, colors)} ` +
|
|
392
|
+
`${placeCell(worker, colors)}${pad(WORKER_COL - worker.length)} ${paintedId}`
|
|
384
393
|
)
|
|
385
394
|
}
|
|
386
395
|
|
|
@@ -407,7 +416,7 @@ export interface RowCells {
|
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
function right(text: string, width: number, colors: ColorSupport): string {
|
|
410
|
-
return
|
|
419
|
+
return pad(width - text.length) + paint('muted', text, colors)
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
|
|
@@ -532,7 +541,7 @@ export function formatRecordedRow(
|
|
|
532
541
|
id: `${r.project}#${r.task}`,
|
|
533
542
|
projectName: r.project,
|
|
534
543
|
taskName: r.task,
|
|
535
|
-
config: r.cached === false ? {} : { cache:
|
|
544
|
+
config: r.cached === false ? {} : { cache: true },
|
|
536
545
|
} as unknown as TaskNode
|
|
537
546
|
const o = {
|
|
538
547
|
node,
|
package/src/report/logger.ts
CHANGED
|
@@ -373,7 +373,7 @@ export function defaultLogger(
|
|
|
373
373
|
let slots: (WorkerSlot | null)[] = []
|
|
374
374
|
const slotQueue: WorkerSlot[] = []
|
|
375
375
|
let succeeded = 0
|
|
376
|
-
// Executed
|
|
376
|
+
// Executed without `cache: true`: never a miss, counted apart.
|
|
377
377
|
let noCache = 0
|
|
378
378
|
let upToDate = 0
|
|
379
379
|
let restoredLocal = 0
|
|
@@ -57,6 +57,8 @@ export function plainOutput(sink: Logger): Logger {
|
|
|
57
57
|
if (text !== '' || chunk === '') sink.taskStderr(node, text)
|
|
58
58
|
},
|
|
59
59
|
taskComplete(node, outcome) {
|
|
60
|
+
// Nearly every task ends with no escape held: its keys go unbuilt.
|
|
61
|
+
if (held.size === 0) return sink.taskComplete(node, outcome)
|
|
60
62
|
flush(node, `${node.id}\0out`, (n, c) => sink.taskStdout(n, c))
|
|
61
63
|
flush(node, `${node.id}\0err`, (n, c) => sink.taskStderr(n, c))
|
|
62
64
|
sink.taskComplete(node, outcome)
|
package/src/report/summary.ts
CHANGED
|
@@ -62,11 +62,11 @@ export interface SummaryStats {
|
|
|
62
62
|
restoredRemote: number
|
|
63
63
|
/** Ran here and saved to the cache: the legend's `↑`. */
|
|
64
64
|
saved?: number
|
|
65
|
-
/** Ran here with
|
|
65
|
+
/** Ran here with `cache: true`, succeeded, and saved nothing. */
|
|
66
66
|
notSaved?: number
|
|
67
|
-
/** Tasks that executed (success + failed) with
|
|
67
|
+
/** Tasks that executed (success + failed) with `cache: true` — the misses. */
|
|
68
68
|
miss: number
|
|
69
|
-
/** Executed
|
|
69
|
+
/** Executed without `cache: true`: never consulted the cache, not a miss. */
|
|
70
70
|
noCache?: number
|
|
71
71
|
/** Still to run (live section only; 0 in the final summary) — renders
|
|
72
72
|
* as a gray ▱ remainder so the live meters FILL toward the final. */
|
|
@@ -582,7 +582,7 @@ function tableStatusColor(o: TaskOutcome): Style {
|
|
|
582
582
|
|
|
583
583
|
// Local byte formatter — the orchestrator can't import cli/format.ts
|
|
584
584
|
// (module boundary), and the footer only needs one compact form.
|
|
585
|
-
/** Ran here with
|
|
585
|
+
/** Ran here with `cache: true`, succeeded, and its result was not saved. */
|
|
586
586
|
export function ranUnsaved(o: TaskOutcome): boolean {
|
|
587
587
|
return o.status === 'success' && !ranNoCache(o) && o.saved !== true && o.where === undefined
|
|
588
588
|
}
|
|
@@ -593,7 +593,7 @@ export function formatRunSummary(
|
|
|
593
593
|
colors: ColorSupport = NO_COLOR,
|
|
594
594
|
context?: RunContext,
|
|
595
595
|
): string[] {
|
|
596
|
-
// Counted here, not in the Tally: a task
|
|
596
|
+
// Counted here, not in the Tally: a task without `cache: true` never
|
|
597
597
|
// consulted the cache, so it is not a miss.
|
|
598
598
|
const noCache = outcomes.filter(
|
|
599
599
|
(o) =>
|
package/src/settings.ts
CHANGED
|
@@ -118,7 +118,7 @@ export const SETTINGS: readonly Setting[] = [
|
|
|
118
118
|
{
|
|
119
119
|
path: 'cache.dir',
|
|
120
120
|
kind: { type: 'string' },
|
|
121
|
-
default: '.vx/cache, entries in ~/.vx/<
|
|
121
|
+
default: '.vx/cache, entries in ~/.vx/<id>/cache',
|
|
122
122
|
doc: 'Where the cache lives, relative to the workspace root; named, it holds the whole cache.',
|
|
123
123
|
flag: { verb: 'run', name: '--cache-dir' },
|
|
124
124
|
},
|
|
@@ -165,25 +165,25 @@ export const SETTINGS: readonly Setting[] = [
|
|
|
165
165
|
flag: { verb: 'run', name: '--cache-download' },
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
|
-
path: '
|
|
168
|
+
path: 'inputs.stability.strategy',
|
|
169
169
|
kind: { type: 'enum', values: ['immutable', 'mutable', 'unchecked'] },
|
|
170
170
|
default: 'immutable',
|
|
171
171
|
doc: 'What a write to an input during the command means for the save, for tasks without their own.',
|
|
172
|
-
task: '
|
|
172
|
+
task: 'inputs.stability.strategy',
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
|
-
path: '
|
|
175
|
+
path: 'inputs.stability.severity',
|
|
176
176
|
kind: { type: 'enum', values: ['warn', 'error'] },
|
|
177
177
|
default: 'warn',
|
|
178
178
|
doc: 'Whether a skipped save warns or fails the task, for tasks without their own.',
|
|
179
|
-
task: '
|
|
179
|
+
task: 'inputs.stability.severity',
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
|
-
path: '
|
|
182
|
+
path: 'outputs.clean',
|
|
183
183
|
kind: { type: 'bool' },
|
|
184
184
|
default: 'true',
|
|
185
185
|
doc: "Remove a task's declared outputs before it runs on a miss, for tasks without their own.",
|
|
186
|
-
task: '
|
|
186
|
+
task: 'outputs.clean',
|
|
187
187
|
env: false,
|
|
188
188
|
},
|
|
189
189
|
{
|
|
@@ -249,7 +249,7 @@ export const SETTINGS: readonly Setting[] = [
|
|
|
249
249
|
]
|
|
250
250
|
|
|
251
251
|
/** The `.vx/workspace` groups that hold settings; `baseBranch` and `plugins` are the other keys. */
|
|
252
|
-
export const SETTING_GROUPS = ['run', 'cache', 'lock', 'watch'] as const
|
|
252
|
+
export const SETTING_GROUPS = ['run', 'cache', 'inputs', 'outputs', 'lock', 'watch'] as const
|
|
253
253
|
|
|
254
254
|
const BY_PATH = new Map(SETTINGS.map((s) => [s.path, s]))
|
|
255
255
|
|
package/src/util/config-frame.ts
CHANGED
|
@@ -32,11 +32,25 @@ export function configErrorFrame(message: string, cwd: string): string | undefin
|
|
|
32
32
|
const rest = message.slice(head[0].length)
|
|
33
33
|
// The field the refusal is about, past the path that leads to it:
|
|
34
34
|
// `has unknown field "comand"`, or `tasks.build: \`cache\` is not allowed`.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// A refused key is quoted: `tasks.b.exec.env.define: "A=B" is not …`.
|
|
36
|
+
const said =
|
|
37
|
+
colon === ':'
|
|
38
|
+
? /^(?:`([^`]+)`|("(?:[^"\\\n]|\\.)*"))/.exec(rest)
|
|
39
|
+
: /^has (?:unknown )?field "([^"]+)"/.exec(rest)
|
|
40
|
+
const named = said === null ? null : [said[0], said[1] ?? JSON.parse(said[2]!)]
|
|
41
|
+
// `plugins[2].name`: an element of an array, framed at the element (a
|
|
42
|
+
// plugin is a call, so no key inside it is searched).
|
|
43
|
+
const element = setting === undefined ? null : /^([^[]+)\[(\d+)\]/.exec(setting)
|
|
37
44
|
const fieldPath =
|
|
38
|
-
taskName
|
|
39
|
-
|
|
45
|
+
taskName !== undefined
|
|
46
|
+
? ['tasks', JSON.parse(taskName)]
|
|
47
|
+
: element !== null
|
|
48
|
+
? element[1]!.split('.')
|
|
49
|
+
: (tasksPath ?? setting)?.split('.')
|
|
50
|
+
const segments = [
|
|
51
|
+
...(fieldPath ?? []),
|
|
52
|
+
...(named === null || element !== null ? [] : [named[1]!]),
|
|
53
|
+
]
|
|
40
54
|
if (segments.length === 0) return undefined
|
|
41
55
|
let source: string
|
|
42
56
|
try {
|
|
@@ -44,7 +58,7 @@ export function configErrorFrame(message: string, cwd: string): string | undefin
|
|
|
44
58
|
} catch {
|
|
45
59
|
return undefined
|
|
46
60
|
}
|
|
47
|
-
const at = locate(source, segments)
|
|
61
|
+
const at = locate(source, segments, element === null ? undefined : Number(element[2]))
|
|
48
62
|
if (at === undefined) return undefined
|
|
49
63
|
return framed(file, source, at, message.slice(file.length), cwd)
|
|
50
64
|
}
|
|
@@ -54,8 +68,70 @@ export function configErrorFrame(message: string, cwd: string): string | undefin
|
|
|
54
68
|
const POSITION =
|
|
55
69
|
/^(?:Project|Workspace) config (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s):(\d+):(\d+)(: [^]*)$/
|
|
56
70
|
|
|
71
|
+
// The loader's `Project config <file> threw: <message> (at <file>:<line>:<col>)`:
|
|
72
|
+
// the config's own throw, placed by its stack in the config itself.
|
|
73
|
+
const THREW =
|
|
74
|
+
/^(?:Project|Workspace) config (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s) threw: ([^\n]*?) \(at (\/[^\n]*?):(\d+):(\d+)\)([^]*)$/
|
|
75
|
+
|
|
76
|
+
// The loader's `<kind> config <file>: cannot find '<pkg>' …`: placed at the
|
|
77
|
+
// specifier. And `<kind> config at <file> did not export a default object`:
|
|
78
|
+
// at the `export default` that exported something else, when there is one.
|
|
79
|
+
const UNPROVIDED =
|
|
80
|
+
/^(?:Project|Workspace) config (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s): (cannot find '([^'\n]+)'[^]*)$/
|
|
81
|
+
const NOT_OBJECT =
|
|
82
|
+
/^(?:Project|Workspace) config at (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s) (did not export a default object[^]*)$/
|
|
83
|
+
|
|
84
|
+
/** The file, the source offset `find` places in it, and what follows `<file>:<line>:<col>`. */
|
|
85
|
+
function sourceFrame(
|
|
86
|
+
file: string,
|
|
87
|
+
find: (source: string) => number,
|
|
88
|
+
rest: string,
|
|
89
|
+
cwd: string,
|
|
90
|
+
): string | undefined {
|
|
91
|
+
let source: string
|
|
92
|
+
try {
|
|
93
|
+
source = readFileSync(file, 'utf8')
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
const at = find(source)
|
|
98
|
+
if (at < 0) return undefined
|
|
99
|
+
const before = source.slice(0, at)
|
|
100
|
+
const line = before.split('\n').length
|
|
101
|
+
return framed(file, source, { line, col: at - before.lastIndexOf('\n') }, rest, cwd)
|
|
102
|
+
}
|
|
103
|
+
|
|
57
104
|
function positionFrame(message: string, cwd: string): string | undefined {
|
|
58
|
-
const
|
|
105
|
+
const unprovided = UNPROVIDED.exec(message)
|
|
106
|
+
if (unprovided !== null) {
|
|
107
|
+
const [, file, said, pkg] = unprovided as unknown as [string, string, string, string]
|
|
108
|
+
const spec = new RegExp(
|
|
109
|
+
`(?<=\\b(?:from|import|import\\(|require\\()\\s*)(['"])${escapeRe(pkg)}\\1`,
|
|
110
|
+
)
|
|
111
|
+
return sourceFrame(file, (src) => spec.exec(src)?.index ?? -1, `: ${said}`, cwd)
|
|
112
|
+
}
|
|
113
|
+
const notObject = NOT_OBJECT.exec(message)
|
|
114
|
+
if (notObject !== null) {
|
|
115
|
+
const [, file, said] = notObject as unknown as [string, string, string]
|
|
116
|
+
return sourceFrame(
|
|
117
|
+
file,
|
|
118
|
+
(src) => /^export\s+default\b/m.exec(src)?.index ?? -1,
|
|
119
|
+
`: ${said}`,
|
|
120
|
+
cwd,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
const threw = THREW.exec(message)
|
|
124
|
+
const pos =
|
|
125
|
+
threw !== null && threw[3] === threw[1]
|
|
126
|
+
? [
|
|
127
|
+
threw[0],
|
|
128
|
+
threw[1],
|
|
129
|
+
threw[4],
|
|
130
|
+
threw[5],
|
|
131
|
+
// A cause's own `(at …)` names the same file: relative there too.
|
|
132
|
+
`: threw: ${threw[2]}${threw[6]!.replaceAll(threw[1]!, path.relative(cwd, threw[1]!))}`,
|
|
133
|
+
]
|
|
134
|
+
: POSITION.exec(message)
|
|
59
135
|
if (pos === null) return undefined
|
|
60
136
|
const [, file, line, col, rest] = pos as unknown as [string, string, string, string, string]
|
|
61
137
|
let source: string
|
|
@@ -105,6 +181,8 @@ function framed(
|
|
|
105
181
|
function locate(
|
|
106
182
|
source: string,
|
|
107
183
|
segments: readonly string[],
|
|
184
|
+
/** The element of the array the last segment holds; past its end, the key. */
|
|
185
|
+
index?: number,
|
|
108
186
|
): { line: number; col: number } | undefined {
|
|
109
187
|
let pos = 0
|
|
110
188
|
let end = source.length
|
|
@@ -137,6 +215,10 @@ function locate(
|
|
|
137
215
|
i = next
|
|
138
216
|
}
|
|
139
217
|
if (found < 0) return undefined
|
|
218
|
+
if (index !== undefined && i === segments.length) {
|
|
219
|
+
const el = elementStart(source, pos, index)
|
|
220
|
+
if (el !== undefined) found = el
|
|
221
|
+
}
|
|
140
222
|
const before = source.slice(0, found)
|
|
141
223
|
const line = before.split('\n').length
|
|
142
224
|
return { line, col: found - before.lastIndexOf('\n') }
|
|
@@ -172,6 +254,46 @@ function literalEnd(source: string, from: number): number {
|
|
|
172
254
|
return source.length
|
|
173
255
|
}
|
|
174
256
|
|
|
257
|
+
/** Where element `index` of the array literal at `from` (past blanks) starts, if it has one. */
|
|
258
|
+
function elementStart(source: string, from: number, index: number): number | undefined {
|
|
259
|
+
let i = from
|
|
260
|
+
while (i < source.length && /\s/.test(source[i]!)) i++
|
|
261
|
+
if (source[i] !== '[') return undefined
|
|
262
|
+
let depth = 0
|
|
263
|
+
let n = 0
|
|
264
|
+
// Each element starts at the first code past `[` or a top-level `,`.
|
|
265
|
+
let pending = true
|
|
266
|
+
for (i++; i < source.length; i++) {
|
|
267
|
+
const c = source[i]!
|
|
268
|
+
if (c === '/' && source[i + 1] === '/') {
|
|
269
|
+
i = source.indexOf('\n', i)
|
|
270
|
+
if (i < 0) return undefined
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (c === '/' && source[i + 1] === '*') {
|
|
274
|
+
i = source.indexOf('*/', i + 2)
|
|
275
|
+
if (i < 0) return undefined
|
|
276
|
+
i++
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
if (/\s/.test(c)) continue
|
|
280
|
+
if (depth === 0 && (c === ']' || c === ')' || c === '}')) return undefined
|
|
281
|
+
if (pending) {
|
|
282
|
+
if (n === index) return i
|
|
283
|
+
pending = false
|
|
284
|
+
}
|
|
285
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
286
|
+
for (i++; i < source.length && source[i] !== c; i++) if (source[i] === '\\') i++
|
|
287
|
+
} else if (c === '{' || c === '[' || c === '(') depth++
|
|
288
|
+
else if (c === '}' || c === ']' || c === ')') depth--
|
|
289
|
+
else if (c === ',' && depth === 0) {
|
|
290
|
+
n++
|
|
291
|
+
pending = true
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
175
297
|
function escapeRe(s: string): string {
|
|
176
298
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
177
299
|
}
|
package/src/util/paths.ts
CHANGED
|
@@ -202,7 +202,7 @@ export function isLiteralPattern(glob: string): boolean {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
|
-
* The alphabet of a TASK glob (`inputs.files`, `
|
|
205
|
+
* The alphabet of a TASK glob (`inputs.files`, `outputs.files`,
|
|
206
206
|
* `workspaceFiles`), behind {@link isLiteralPattern}, for the callers that
|
|
207
207
|
* need the POSITION of the first wildcard rather than a verdict. Every site
|
|
208
208
|
* that asks "must this declaration be matched?" reads this one regex: item
|
package/src/util/settle.ts
CHANGED
|
@@ -51,7 +51,8 @@ export function killGraceMs(defaultMs: number): number {
|
|
|
51
51
|
export async function settleWithin(p: Promise<unknown>, ms: number): Promise<boolean> {
|
|
52
52
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
53
53
|
const deadline = new Promise<false>((resolve) => {
|
|
54
|
-
|
|
54
|
+
// A delay past a 32-bit int fires at 1 ms: the longest the timer holds instead.
|
|
55
|
+
timer = setTimeout(() => resolve(false), Math.min(ms, MAX_TIMEOUT_MS))
|
|
55
56
|
})
|
|
56
57
|
try {
|
|
57
58
|
return (await Promise.race([p.then(() => true), deadline])) !== false
|
package/src/util/tail.ts
CHANGED
|
@@ -32,8 +32,11 @@ export function createTail(): Tail {
|
|
|
32
32
|
return { chunks: [], chars: 0, dropped: 0 }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function appendTail(t: Tail, chunk: string,
|
|
35
|
+
export function appendTail(t: Tail, chunk: string, cap = PERSISTENT_TAIL_CHARS): void {
|
|
36
36
|
if (chunk.length === 0) return
|
|
37
|
+
// Every bound below is `chars > limit`, which NaN never is: a NaN cap
|
|
38
|
+
// kept everything, the unbounded heap this exists to prevent.
|
|
39
|
+
const limit = Number.isNaN(cap) ? 0 : cap
|
|
37
40
|
t.chunks.push(chunk)
|
|
38
41
|
t.chars += chunk.length
|
|
39
42
|
// Evict WHOLE chunks from the head — no concatenation until the flush
|
|
@@ -65,8 +65,8 @@ export function checkBracketLiterals(root: string, projects: Iterable<ProjectEnt
|
|
|
65
65
|
const lists: [string, readonly string[] | undefined, string][] = [
|
|
66
66
|
['inputs.files', t.inputs?.files, p.dir],
|
|
67
67
|
['inputs.workspaceFiles', t.inputs?.workspaceFiles, root],
|
|
68
|
-
['
|
|
69
|
-
['
|
|
68
|
+
['outputs.files', t.outputs?.files, p.dir],
|
|
69
|
+
['outputs.workspaceFiles', t.outputs?.workspaceFiles, root],
|
|
70
70
|
]
|
|
71
71
|
for (const [field, globs, base] of lists) {
|
|
72
72
|
for (const glob of globs ?? []) {
|