@vzn/vx 0.0.37 → 0.0.39
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/package.json +5 -5
- package/src/cli/last.ts +2 -7
- package/src/orchestrator/summary.ts +2 -2
- package/src/util/index.ts +1 -1
- package/src/util/num.ts +14 -0
- package/src/workspace/config-schema.ts +16 -12
- package/src/workspace/migrate-scripts.ts +21 -2
- package/src/workspace/migration.ts +28 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.39",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.39",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.39",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.39"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/last.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
listInvocations,
|
|
15
15
|
type RunSummaryRow,
|
|
16
16
|
} from '../orchestrator/index.js'
|
|
17
|
-
import { UserError } from '../util/index.js'
|
|
17
|
+
import { formatElapsed, UserError } from '../util/index.js'
|
|
18
18
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
19
19
|
import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
20
20
|
|
|
@@ -78,12 +78,7 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
78
78
|
|
|
79
79
|
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
if (ms < 1000) return `${Math.round(ms)}ms`
|
|
83
|
-
if (ms < 60_000) return `${(ms / 1000).toFixed(2)}s`
|
|
84
|
-
const m = Math.floor(ms / 60_000)
|
|
85
|
-
return `${m}m ${Math.round((ms - m * 60_000) / 1000)}s`
|
|
86
|
-
}
|
|
81
|
+
const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
87
82
|
|
|
88
83
|
/**
|
|
89
84
|
* What an executed task used — its peak RSS and its CPU parallelism (CPU
|
|
@@ -8,6 +8,7 @@ import type { FlakyFinding } from './failure-mode.js'
|
|
|
8
8
|
import { paint, type ColorSupport } from './colors.js'
|
|
9
9
|
import { tallyOutcomes } from './tally.js'
|
|
10
10
|
import { isGroupTask } from '../graph/index.js'
|
|
11
|
+
import { formatElapsed } from '../util/index.js'
|
|
11
12
|
|
|
12
13
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
13
14
|
|
|
@@ -421,6 +422,5 @@ export function formatFlakySection(findings: readonly FlakyFinding[]): string[]
|
|
|
421
422
|
}
|
|
422
423
|
|
|
423
424
|
export function formatDuration(ms: number): string {
|
|
424
|
-
|
|
425
|
-
return `${(ms / 1000).toFixed(2)}s`
|
|
425
|
+
return formatElapsed(ms)
|
|
426
426
|
}
|
package/src/util/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export {
|
|
|
17
17
|
} from './errors.js'
|
|
18
18
|
export { xxh3, xxh3hex } from './hash.js'
|
|
19
19
|
export { mark, printTimings, span } from './timing.js'
|
|
20
|
-
export { clampInt, MAX_TIMEOUT_MS, parseDecimalInt } from './num.js'
|
|
20
|
+
export { clampInt, formatElapsed, MAX_TIMEOUT_MS, parseDecimalInt } from './num.js'
|
|
21
21
|
export {
|
|
22
22
|
asTrees,
|
|
23
23
|
BUN_GLOB_WILDCARDS,
|
package/src/util/num.ts
CHANGED
|
@@ -35,3 +35,17 @@ export function parseDecimalInt(input: string): number | null {
|
|
|
35
35
|
const n = Number(input)
|
|
36
36
|
return Number.isSafeInteger(n) ? n : null
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `ms` as vx prints a duration: whole milliseconds below a second, seconds
|
|
41
|
+
* to two places, and with `minutes` whole minutes and seconds from a
|
|
42
|
+
* minute on. Rounded to the unit shown BEFORE the unit is chosen: 999.6 ms
|
|
43
|
+
* printed `1000ms`, 59,996 ms `60.00s` and 119,600 ms `1m 60s` (item 1034).
|
|
44
|
+
*/
|
|
45
|
+
export function formatElapsed(ms: number, minutes = false): string {
|
|
46
|
+
if (Math.round(ms) < 1000) return `${Math.round(ms)}ms`
|
|
47
|
+
const hundredths = Math.round(ms / 10)
|
|
48
|
+
if (!minutes || hundredths < 6000) return `${(hundredths / 100).toFixed(2)}s`
|
|
49
|
+
const s = Math.round(ms / 1000)
|
|
50
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`
|
|
51
|
+
}
|
|
@@ -822,18 +822,7 @@ function isEnvName(name: unknown): name is string {
|
|
|
822
822
|
* trimmed away by the reader (item 1000).
|
|
823
823
|
*/
|
|
824
824
|
function assertTaskName(name: string, configPath: string): void {
|
|
825
|
-
const why =
|
|
826
|
-
name.trim().length === 0
|
|
827
|
-
? 'is empty'
|
|
828
|
-
: name.trim() !== name
|
|
829
|
-
? 'has surrounding whitespace'
|
|
830
|
-
: name.includes('#')
|
|
831
|
-
? "holds '#', which separates a project from its task"
|
|
832
|
-
: name.includes('*')
|
|
833
|
-
? "holds '*', which makes it a pattern"
|
|
834
|
-
: name.startsWith('^') || name.startsWith('!')
|
|
835
|
-
? `starts with '${name[0]}', which names dependencies' tasks or negates`
|
|
836
|
-
: null
|
|
825
|
+
const why = taskNameProblem(name)
|
|
837
826
|
if (why === null) return
|
|
838
827
|
throw new UserError(
|
|
839
828
|
`${configPath}: task name ${JSON.stringify(name)} ${why} — dependsOn, cache.inputs.tasks and ` +
|
|
@@ -841,6 +830,21 @@ function assertTaskName(name: string, configPath: string): void {
|
|
|
841
830
|
)
|
|
842
831
|
}
|
|
843
832
|
|
|
833
|
+
/** Why `name` cannot be a task name, or null; `vx init` skips such a script (item 1033). */
|
|
834
|
+
export function taskNameProblem(name: string): string | null {
|
|
835
|
+
return name.trim().length === 0
|
|
836
|
+
? 'is empty'
|
|
837
|
+
: name.trim() !== name
|
|
838
|
+
? 'has surrounding whitespace'
|
|
839
|
+
: name.includes('#')
|
|
840
|
+
? "holds '#', which separates a project from its task"
|
|
841
|
+
: name.includes('*')
|
|
842
|
+
? "holds '*', which makes it a pattern"
|
|
843
|
+
: name.startsWith('^') || name.startsWith('!')
|
|
844
|
+
? `starts with '${name[0]}', which names dependencies' tasks or negates`
|
|
845
|
+
: null
|
|
846
|
+
}
|
|
847
|
+
|
|
844
848
|
/** The graph's `*`-only task glob (`compileTaskPattern`), mirrored: `*` is the sole metacharacter. */
|
|
845
849
|
function taskPatternRegExp(pattern: string): RegExp {
|
|
846
850
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// `<pm> run <other>` becomes a GROUP over `<other>`, so the graph sees the
|
|
25
25
|
// dependency instead of a package-manager subprocess it cannot cache.
|
|
26
26
|
|
|
27
|
+
import { taskNameProblem } from './config-schema.js'
|
|
27
28
|
import type { ProjectMeta } from './workspace.js'
|
|
28
29
|
import {
|
|
29
30
|
foldScriptHooks,
|
|
@@ -116,10 +117,19 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
116
117
|
const projects: GeneratedProject[] = []
|
|
117
118
|
for (const meta of metas) {
|
|
118
119
|
const scripts = scriptsOf(meta)
|
|
119
|
-
const
|
|
120
|
+
const runnable = Object.keys(scripts).filter(
|
|
120
121
|
(n) => typeof scripts[n] === 'string' && scripts[n] !== '',
|
|
121
122
|
)
|
|
122
|
-
|
|
123
|
+
// A script whose name no task may carry (`lint#fix`, `^up`) was written
|
|
124
|
+
// as one, and the next run refused the whole config (item 1033). It is
|
|
125
|
+
// left out, with a TODO, and a script delegating to it keeps its
|
|
126
|
+
// command rather than naming it as an edge.
|
|
127
|
+
const refused = runnable.flatMap((n) => {
|
|
128
|
+
const why = taskNameProblem(n)
|
|
129
|
+
return why === null ? [] : [[n, why] as const]
|
|
130
|
+
})
|
|
131
|
+
const names = runnable.filter((n) => taskNameProblem(n) === null)
|
|
132
|
+
if (runnable.length === 0) continue
|
|
123
133
|
const hasBuild = names.includes('build')
|
|
124
134
|
const has = (n: string): boolean => names.includes(n)
|
|
125
135
|
const tasks: GeneratedTask[] = []
|
|
@@ -173,6 +183,15 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
173
183
|
}
|
|
174
184
|
tasks.push({ name, todos, task })
|
|
175
185
|
}
|
|
186
|
+
for (const [name, why] of refused) {
|
|
187
|
+
tasks.push({
|
|
188
|
+
name,
|
|
189
|
+
todos: [
|
|
190
|
+
`script ${JSON.stringify(name)} not migrated: its name ${why} — rename the script, or add it by hand under another name`,
|
|
191
|
+
],
|
|
192
|
+
task: null,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
176
195
|
upstreamBuildOnWorker(tasks)
|
|
177
196
|
if (tasks.length > 0) projects.push({ name: meta.name, dir: meta.dir, importLines: [], tasks })
|
|
178
197
|
}
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// tool wrote it. Core knows no source format here: a mapper returns a
|
|
6
6
|
// `MigrationPlan` and this file does the rest.
|
|
7
7
|
|
|
8
|
+
import { unlink } from 'node:fs/promises'
|
|
8
9
|
import path from 'node:path'
|
|
9
10
|
import { relPosix, UserError } from '../util/index.js'
|
|
10
11
|
import type { ProjectMeta } from './workspace.js'
|
|
11
12
|
import { loadWorkspace, unreachedHint, unreachedPackages } from './workspace.js'
|
|
13
|
+
import { WORKSPACE_CONFIG_FILENAMES } from './project-loader.js'
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Escape an arbitrary string into a single-quoted TS literal. Escapes
|
|
@@ -164,13 +166,10 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
164
166
|
}
|
|
165
167
|
// A migrated workspace must declare its executor and cache — nothing is
|
|
166
168
|
// applied by default. Emit the workspace file unless the repo already has
|
|
167
|
-
// one in any
|
|
169
|
+
// one in any extension the loader reads: a hand-written `.mts` was missed,
|
|
170
|
+
// and the `.ts` written beside it won by load order (item 1033).
|
|
168
171
|
const hasWorkspaceFile = (
|
|
169
|
-
await Promise.all(
|
|
170
|
-
['vx.workspace.ts', 'vx.workspace.mjs', 'vx.workspace.js'].map((n) =>
|
|
171
|
-
Bun.file(path.join(root, n)).exists(),
|
|
172
|
-
),
|
|
173
|
-
)
|
|
172
|
+
await Promise.all(WORKSPACE_CONFIG_FILENAMES.map((n) => Bun.file(path.join(root, n)).exists()))
|
|
174
173
|
).some(Boolean)
|
|
175
174
|
if (!hasWorkspaceFile) {
|
|
176
175
|
const abs = path.join(root, workspaceName)
|
|
@@ -201,12 +200,23 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
|
|
203
|
+
// Under --force a project's config of another extension is REPLACED: the
|
|
204
|
+
// new one was written beside it, the loader read one of the two by its
|
|
205
|
+
// order, and the report said "written" for a file the run never loaded
|
|
206
|
+
// (item 1033).
|
|
207
|
+
const replaced: string[] = []
|
|
208
|
+
for (const p of plan.projects) {
|
|
209
|
+
if (p.tasks.length === 0) continue
|
|
210
|
+
const existing = metas.find((m) => m.dir === p.dir)?.configPath
|
|
211
|
+
if (existing && existing !== path.join(p.dir, configName)) replaced.push(existing)
|
|
212
|
+
}
|
|
204
213
|
if (dry) {
|
|
205
214
|
for (const f of files) {
|
|
206
215
|
process.stdout.write(`── ${f.relPath} ──\n${f.contents}\n`)
|
|
207
216
|
}
|
|
208
217
|
} else {
|
|
209
218
|
for (const f of files) await Bun.write(f.abs, f.contents)
|
|
219
|
+
for (const f of replaced) await unlink(f)
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
const todoList: string[] = []
|
|
@@ -258,6 +268,10 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
258
268
|
report.push(...plan.notes)
|
|
259
269
|
report.push(dry ? 'files (dry run, nothing written):' : 'files written:')
|
|
260
270
|
for (const f of files) report.push(` ${f.relPath}`)
|
|
271
|
+
if (replaced.length > 0) {
|
|
272
|
+
report.push(dry ? 'would replace (dry run):' : 'replaced:')
|
|
273
|
+
for (const f of replaced) report.push(` ${relPosix(root, f)}`)
|
|
274
|
+
}
|
|
261
275
|
}
|
|
262
276
|
const firstTask =
|
|
263
277
|
plan.projects.flatMap((p) => p.tasks.map((t) => t.name)).find((n) => n === 'build') ??
|
|
@@ -345,7 +359,14 @@ function renderConfigFile(
|
|
|
345
359
|
for (const t of p.tasks) {
|
|
346
360
|
for (const todo of t.todos) lines.push(` // TODO(vx-migrate): ${todo}`)
|
|
347
361
|
if (t.task === null) continue // skipped target — the TODO above explains
|
|
348
|
-
|
|
362
|
+
// `__proto__: {…}` in a literal SETS the prototype, quoted or not, and
|
|
363
|
+
// the run refused the config (item 1033); a computed key is a property.
|
|
364
|
+
const key =
|
|
365
|
+
t.name === '__proto__'
|
|
366
|
+
? `[${quoteTsLiteral(t.name)}]`
|
|
367
|
+
: IDENT.test(t.name)
|
|
368
|
+
? t.name
|
|
369
|
+
: quoteTsLiteral(t.name)
|
|
349
370
|
lines.push(` ${key}: ${renderValue(t.task, ' ')},`)
|
|
350
371
|
}
|
|
351
372
|
lines.push(' },', format === 'ts' ? '} satisfies ProjectConfig' : '}', '')
|