@vzn/vx 0.0.349 → 0.0.352
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/cache/cache.ts +31 -1
- package/src/cache/file-hashes.ts +30 -0
- package/src/cache/git-inputs.ts +164 -10
- package/src/cache/index.ts +2 -0
- package/src/cache/inputs.ts +84 -22
- package/src/cache/key-fold.ts +5 -1
- package/src/cache/schema.ts +9 -0
- package/src/cli/show.ts +7 -1
- package/src/cli/workspace-config.ts +1 -1
- package/src/exec/index.ts +7 -1
- package/src/exec/sandbox-paths.ts +22 -0
- package/src/exec/sandbox-runtime.ts +71 -15
- package/src/exec/sandbox-violations.ts +41 -6
- package/src/graph/scheduler.ts +9 -1
- package/src/orchestrator/execute-task.ts +2 -0
- package/src/orchestrator/framed-output.ts +13 -0
- package/src/orchestrator/logger.ts +41 -2
- package/src/orchestrator/options.ts +6 -4
- package/src/orchestrator/plugin-host.ts +7 -6
- package/src/orchestrator/prepare.ts +36 -9
- package/src/orchestrator/projects.ts +9 -3
- package/src/orchestrator/run.ts +10 -2
- package/src/orchestrator/sandbox-request.ts +6 -5
- package/src/orchestrator/shell-verdict.ts +10 -1
- package/src/workspace/affected.ts +34 -1
- package/src/workspace/config-schema.ts +28 -0
- package/src/workspace/migrate-scripts.ts +49 -6
|
@@ -592,7 +592,8 @@ async function gitPaths(workspaceRoot: string, cmd: string[]): Promise<string[]>
|
|
|
592
592
|
/**
|
|
593
593
|
* Resolve the default base for `--affected` with no explicit value.
|
|
594
594
|
* Tries the remote's HEAD branch first (`origin/main`, `origin/master`,
|
|
595
|
-
* etc.), then
|
|
595
|
+
* etc.), then `origin/main`, `origin/master`, `main`, `master`, then
|
|
596
|
+
* falls back to `HEAD~1`. A clone with no `origin/HEAD` and
|
|
596
597
|
* no parent commit — a CI checkout at `fetch-depth: 1` — has no base at
|
|
597
598
|
* all, and says so here rather than failing on a ref nobody typed.
|
|
598
599
|
*/
|
|
@@ -609,6 +610,12 @@ export async function defaultAffectedBase(workspaceRoot: string): Promise<string
|
|
|
609
610
|
if (probe.exitCode === 0 && out.length > 0 && revParse(workspaceRoot, out) !== undefined) {
|
|
610
611
|
return out
|
|
611
612
|
}
|
|
613
|
+
// No origin/HEAD: `git remote add` + fetch, as actions/checkout does, sets
|
|
614
|
+
// none, and `HEAD~1` saw only a feature branch's last commit where Turbo
|
|
615
|
+
// and Nx compare with `main` (D-93). The usual trunk names, remote first;
|
|
616
|
+
// one that IS HEAD (a push to main) falls through to `HEAD~1` as before.
|
|
617
|
+
const trunk = trunkBase(workspaceRoot)
|
|
618
|
+
if (trunk !== undefined) return trunk
|
|
612
619
|
if (revParse(workspaceRoot, 'HEAD~1') === undefined) {
|
|
613
620
|
throw new UserError(
|
|
614
621
|
'--affected has no base here: origin/HEAD is not set (or names a branch that is gone) and HEAD has no parent to compare ' +
|
|
@@ -619,6 +626,32 @@ export async function defaultAffectedBase(workspaceRoot: string): Promise<string
|
|
|
619
626
|
return 'HEAD~1'
|
|
620
627
|
}
|
|
621
628
|
|
|
629
|
+
const TRUNKS = ['origin/main', 'origin/master', 'main', 'master']
|
|
630
|
+
|
|
631
|
+
/** The first of `TRUNKS` that exists and is not HEAD's commit, in one spawn. */
|
|
632
|
+
function trunkBase(workspaceRoot: string): string | undefined {
|
|
633
|
+
const proc = spawnGitSync(
|
|
634
|
+
[
|
|
635
|
+
'for-each-ref',
|
|
636
|
+
'--format=%(refname:short) %(objectname)',
|
|
637
|
+
'--end-of-options',
|
|
638
|
+
...TRUNKS.map((t) => (t.startsWith('origin/') ? `refs/remotes/${t}` : `refs/heads/${t}`)),
|
|
639
|
+
],
|
|
640
|
+
workspaceRoot,
|
|
641
|
+
'ignore',
|
|
642
|
+
)
|
|
643
|
+
if (proc.exitCode !== 0) return undefined
|
|
644
|
+
const at = new Map(
|
|
645
|
+
new TextDecoder()
|
|
646
|
+
.decode(proc.stdout)
|
|
647
|
+
.split('\n')
|
|
648
|
+
.filter((l) => l !== '')
|
|
649
|
+
.map((l) => l.split(' ') as [string, string]),
|
|
650
|
+
)
|
|
651
|
+
const head = revParse(workspaceRoot, 'HEAD')
|
|
652
|
+
return TRUNKS.find((t) => at.has(t) && at.get(t) !== head)
|
|
653
|
+
}
|
|
654
|
+
|
|
622
655
|
/**
|
|
623
656
|
* Does `ref` name the commit HEAD is on? A base that IS HEAD can never
|
|
624
657
|
* mark anything affected — the shape of a single-branch clone whose
|
|
@@ -742,6 +742,16 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
742
742
|
'`exec.command`: vx runs one shell command (`nx()` from `@vzn/vx-migrate` runs an Nx executor as one)',
|
|
743
743
|
options: '`exec.command` (a run-commands `options.command`) and `exec.env`',
|
|
744
744
|
outputLogs: 'the `--output-logs` flag of `vx run`',
|
|
745
|
+
// Turbo's `interactive` / `with`, Nx's `cwd` / `parallelism` /
|
|
746
|
+
// `configurations`: refused with no word on where they went (D-89).
|
|
747
|
+
interactive:
|
|
748
|
+
'a command run outside vx: a task never reads the terminal (its stdin is EOF, or a pipe vx holds under `exec.persistent`)',
|
|
749
|
+
with: '`dependsOn` on each task it runs beside: a persistent one stays up, and this task starts once it is ready',
|
|
750
|
+
cwd: '`cd <dir> && …` in `exec.command`: a task runs in its project directory',
|
|
751
|
+
parallelism:
|
|
752
|
+
'`concurrency: 1` in vx.workspace or `--concurrency 1` on the run: no task runs alone beside others',
|
|
753
|
+
configurations:
|
|
754
|
+
'one task per configuration (`build:production`, with its own `exec.command`)',
|
|
745
755
|
},
|
|
746
756
|
],
|
|
747
757
|
[EXEC_FIELDS, { cmd: '`command`', script: '`command`' }],
|
|
@@ -755,6 +765,24 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
755
765
|
implicitDependencies: 'a `dependsOn` entry `pkg#task`, or a package.json dependency',
|
|
756
766
|
name: 'the package.json `name` (a project is named by its package)',
|
|
757
767
|
tags: '`--filter` (a name glob or a directory) to select projects',
|
|
768
|
+
// A package.json's, turbo.json's and project.json's keys, and a
|
|
769
|
+
// task's fields one level too high (D-99).
|
|
770
|
+
scripts:
|
|
771
|
+
"`tasks` (a script is a task's `exec.command`; `vx init` writes them from package.json)",
|
|
772
|
+
pipeline: '`tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)',
|
|
773
|
+
namedInputs: 'a module the vx.config imports, spread into `cache.inputs.files`',
|
|
774
|
+
root: "no field: a project's directory is where its package.json sits",
|
|
775
|
+
sourceRoot: "a task's `cache.inputs.files`",
|
|
776
|
+
projectType: 'no field: vx runs every project alike',
|
|
777
|
+
...Object.fromEntries(
|
|
778
|
+
['dependsOn', 'cache', 'exec', 'description'].map((k) => [
|
|
779
|
+
k,
|
|
780
|
+
`\`tasks.<name>.${k}\`: a project holds tasks, and each task its own \`${k}\``,
|
|
781
|
+
]),
|
|
782
|
+
),
|
|
783
|
+
inputs: '`tasks.<name>.cache.inputs.files`',
|
|
784
|
+
outputs: '`tasks.<name>.cache.outputs.files`',
|
|
785
|
+
env: '`tasks.<name>.cache.inputs.env` (to key a task on it) or `tasks.<name>.exec.env`',
|
|
758
786
|
},
|
|
759
787
|
],
|
|
760
788
|
// Turbo's turbo.json and Nx's nx.json keys, written into vx.workspace
|
|
@@ -55,12 +55,14 @@ const LIFECYCLE = /^(pre|post)(install|publish|pack|version)$|^(prepare|prepubli
|
|
|
55
55
|
*/
|
|
56
56
|
export function delegatedScript(command: string): string | null {
|
|
57
57
|
const m =
|
|
58
|
-
/^(?:(?:npm run|(pnpm|yarn|bun)(?: (run))?) ([^\s&|;<>()$`'"\\]+)|npm (test|start))$/.exec(
|
|
58
|
+
/^(?:(?:npm run|(pnpm|pn|yarn|bun)(?: (run))?) ([^\s&|;<>()$`'"\\]+)|npm (test|start))$/.exec(
|
|
59
59
|
command.trim(),
|
|
60
60
|
)
|
|
61
61
|
if (m === null) return null
|
|
62
62
|
if (m[4] !== undefined) return m[4]
|
|
63
|
-
const [,
|
|
63
|
+
const [, alias, run, name] = m
|
|
64
|
+
// `pn` is pnpm's own short name (pnpm 11; pnpm/pnpm's scripts run it).
|
|
65
|
+
const manager = alias === 'pn' ? 'pnpm' : alias
|
|
64
66
|
// Bare, the manager's own command wins over a script of that name:
|
|
65
67
|
// `bun test` is Bun's test runner and `bun build` its bundler, never the
|
|
66
68
|
// `test` / `build` script, and a group over the script ran the wrong
|
|
@@ -155,7 +157,10 @@ function ownerOf(dir: string, memo: Map<string, Owner>): Owner {
|
|
|
155
157
|
try {
|
|
156
158
|
const pm = (JSON.parse(read('package.json') ?? '') as { packageManager?: unknown })
|
|
157
159
|
.packageManager
|
|
158
|
-
|
|
160
|
+
// A manager vx knows nothing of (zod's `nub@0.8.3`) says nothing about
|
|
161
|
+
// hooks: the lockfile beside it does, and "nub ran `postbuild`" was a
|
|
162
|
+
// claim nothing had checked (D-96).
|
|
163
|
+
if (typeof pm === 'string' && /^(npm|pnpm|yarn|bun)@/.test(pm))
|
|
159
164
|
manager = /^yarn@([2-9]|\d{2,})/.test(pm) ? 'berry' : pm.split('@')[0]
|
|
160
165
|
} catch {}
|
|
161
166
|
if (manager === undefined) {
|
|
@@ -302,7 +307,7 @@ const MEMBER_FLAG =
|
|
|
302
307
|
function pmRunsMembers(script: string): boolean {
|
|
303
308
|
for (const segment of script.split(/&&|\|\||[;|()]/)) {
|
|
304
309
|
const words = segment.trim().split(/\s+/)
|
|
305
|
-
let i = words.findIndex((w) => /^(pnpm|npm|yarn|bun)$/.test(w))
|
|
310
|
+
let i = words.findIndex((w) => /^(pnpm|pn|npm|yarn|bun)$/.test(w))
|
|
306
311
|
// npm/cli runs itself: `node . run test --workspaces`.
|
|
307
312
|
if (i < 0) i = words.findIndex((w, j) => w === 'run' && words[j - 2] === 'node')
|
|
308
313
|
if (i < 0) continue
|
|
@@ -340,7 +345,45 @@ function cdsToMembers(script: string, rootDir: string, memberDirs: readonly stri
|
|
|
340
345
|
|
|
341
346
|
/** A package manager running a script by name: `pnpm build`, `npm run x`, `bun run x`. */
|
|
342
347
|
const RUNS_SCRIPT =
|
|
343
|
-
/(?:^|[\s;&|(])(?:pnpm|npm|yarn|bun)\s+(?:run(?:-script)?\s+)?([^\s;&|()'"-][^\s;&|()'"]*)/g
|
|
348
|
+
/(?:^|[\s;&|(])(?:pnpm|pn|npm|yarn|bun)\s+(?:run(?:-script)?\s+)?([^\s;&|()'"-][^\s;&|()'"]*)/g
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The scripts a command runs by name: a package manager's (`pnpm x`, `npm
|
|
352
|
+
* run x`), `run-s` / `run-p` / `npm-run-all` (`build:*` is one segment,
|
|
353
|
+
* `build:**` any), and `concurrently "npm:x"`. A root `ci: run-s build:all
|
|
354
|
+
* lint` over `build:all: pnpm -r build` mapped as a root task (D-95).
|
|
355
|
+
*/
|
|
356
|
+
function scriptRefs(command: string, scripts: readonly (readonly [string, string])[]): string[] {
|
|
357
|
+
const refs = [...command.matchAll(RUNS_SCRIPT)].map((m) => m[1]!)
|
|
358
|
+
for (const m of command.matchAll(/["']?\b(?:npm|pnpm|yarn|bun):([^\s"']+)/g)) refs.push(m[1]!)
|
|
359
|
+
for (const segment of command.split(/&&|\|\||[;|()]/)) {
|
|
360
|
+
const words = segment.trim().split(/\s+/)
|
|
361
|
+
const at = words.findIndex((w) => /^(run-s|run-p|npm-run-all)$/.test(w))
|
|
362
|
+
if (at < 0) continue
|
|
363
|
+
for (const w of words.slice(at + 1)) {
|
|
364
|
+
if (w.startsWith('-')) continue
|
|
365
|
+
const name = w.replace(/^(["'])(.*)\1$/, '$2')
|
|
366
|
+
if (!name.includes('*')) {
|
|
367
|
+
refs.push(name)
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
const re = new RegExp(
|
|
371
|
+
`^${name
|
|
372
|
+
.split(/(\*\*|\*)/)
|
|
373
|
+
.map((part) =>
|
|
374
|
+
part === '**'
|
|
375
|
+
? '.*'
|
|
376
|
+
: part === '*'
|
|
377
|
+
? '[^:]*'
|
|
378
|
+
: part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'),
|
|
379
|
+
)
|
|
380
|
+
.join('')}$`,
|
|
381
|
+
)
|
|
382
|
+
for (const [n] of scripts) if (re.test(n)) refs.push(n)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return refs
|
|
386
|
+
}
|
|
344
387
|
|
|
345
388
|
/**
|
|
346
389
|
* The scripts that run the members, directly or through another of these
|
|
@@ -367,7 +410,7 @@ function runningMembers(
|
|
|
367
410
|
grew = false
|
|
368
411
|
for (const [n, v] of text) {
|
|
369
412
|
if (out.has(n)) continue
|
|
370
|
-
if (
|
|
413
|
+
if (scriptRefs(v, text).some((r) => out.has(r))) {
|
|
371
414
|
out.add(n)
|
|
372
415
|
grew = true
|
|
373
416
|
}
|