@vzn/vx 0.0.366 → 0.0.368

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.366",
3
+ "version": "0.0.368",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.366",
25
- "@vzn/vx-linux-arm64": "0.0.366",
26
- "@vzn/vx-darwin-x64": "0.0.366",
27
- "@vzn/vx-darwin-arm64": "0.0.366"
24
+ "@vzn/vx-linux-x64": "0.0.368",
25
+ "@vzn/vx-linux-arm64": "0.0.368",
26
+ "@vzn/vx-darwin-x64": "0.0.368",
27
+ "@vzn/vx-darwin-arm64": "0.0.368"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -399,6 +399,22 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
399
399
  try {
400
400
  await spawn.ready
401
401
  } catch (err) {
402
+ // A server the run's stop killed while it started is aborted, as any
403
+ // task the stop kills (item 962): it read `failed (never ready:
404
+ // exited, exit 130)` with a recap after every Ctrl-C (C-62). The stop
405
+ // aborts before it kills, so it is set by the time the child is gone.
406
+ // Read through a call: the early return above narrows `aborted` to
407
+ // false, but the stop can land during `spawn.ready`.
408
+ if (isAborted(args.stopSignal)) {
409
+ return {
410
+ node,
411
+ status: 'aborted',
412
+ exitCode: err instanceof PersistentReadyError ? (err.exitCode ?? 1) : 1,
413
+ durationMs: spawn.readyMs(),
414
+ wallclockStartNs,
415
+ wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
416
+ }
417
+ }
402
418
  const message = err instanceof Error ? err.message : String(err)
403
419
  // The task's OWN stream, not the process's: the frame is where a
404
420
  // reader looks for why a task failed, and a run with a custom logger
@@ -1300,3 +1316,7 @@ function taskEnv(node: TaskNode, step: ExecConfig, workspaceRoot: string): NodeJ
1300
1316
  env[VX_RUN_TASK_ENV] = node.id
1301
1317
  return env
1302
1318
  }
1319
+
1320
+ function isAborted(signal: AbortSignal | undefined): boolean {
1321
+ return signal?.aborted === true
1322
+ }
@@ -674,11 +674,27 @@ function revParse(workspaceRoot: string, ref: string): string | undefined {
674
674
  'ignore',
675
675
  )
676
676
  const sha = new TextDecoder().decode(proc.stdout).trim()
677
- return proc.exitCode === 0 && sha.length > 0 ? sha : undefined
677
+ if (proc.exitCode !== 0 || sha.length === 0) return undefined
678
+ resolvedRefs.set(`${workspaceRoot}\0${ref}`, sha)
679
+ return sha
678
680
  }
679
681
 
682
+ /**
683
+ * Refs `revParse` resolved to a commit, by workspace: the default base is
684
+ * found that way, and `verifyRef` asked git about it again, a synchronous
685
+ * spawn (~3.5 ms) of every bare `--affected`. A ref is fixed for a run.
686
+ */
687
+ const resolvedRefs = new Map<string, string>()
688
+
689
+ /** A ref that names an ancestor of HEAD by its own spelling (`HEAD~1`, `HEAD^`). */
690
+ const HEAD_ANCESTOR = /^HEAD(?:~\d*|\^\d*)+$/
691
+
680
692
  /** `git merge-base <ref> HEAD`, or `ref` itself when the two share no ancestor. */
681
693
  async function mergeBase(workspaceRoot: string, ref: string): Promise<string> {
694
+ // HEAD's own ancestor is its merge base with HEAD: the commit it names,
695
+ // which the default base's search already resolved (`HEAD~1`).
696
+ const known = HEAD_ANCESTOR.test(ref) ? resolvedRefs.get(`${workspaceRoot}\0${ref}`) : undefined
697
+ if (known !== undefined) return known
682
698
  const proc = spawnGit(['merge-base', '--end-of-options', ref, 'HEAD'], workspaceRoot)
683
699
  const [out, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited])
684
700
  const sha = out.trim()
@@ -686,6 +702,7 @@ async function mergeBase(workspaceRoot: string, ref: string): Promise<string> {
686
702
  }
687
703
 
688
704
  async function verifyRef(workspaceRoot: string, ref: string): Promise<void> {
705
+ if (resolvedRefs.has(`${workspaceRoot}\0${ref}`)) return
689
706
  const proc = spawnGitSync(
690
707
  ['rev-parse', '--verify', '--quiet', '--end-of-options', ref],
691
708
  workspaceRoot,
@@ -676,23 +676,49 @@ export function migrateScripts(
676
676
  .filter(([, v]) => typeof v === 'string' && v !== '')
677
677
  .map(([n]) => n),
678
678
  )
679
- const rootScripts = (meta: ProjectMeta): Record<string, unknown> => {
679
+ // The root scripts left out, by why: jest's root `build` (every package's)
680
+ // went unnamed beside the website's, and `vx run build --all` built the
681
+ // website alone (D-85). A left-out script's `pre` / `post` hook goes with
682
+ // it where the manager runs hooks: kept, it became a task of its own.
683
+ const leftOut = (meta: ProjectMeta): { runs: string[]; shared: string[]; out: Set<string> } => {
680
684
  const scripts = scriptsOf(meta)
681
685
  const runs = runningMembers(
682
686
  scripts,
683
687
  meta.dir,
684
688
  metas.filter((m) => m !== root).map((m) => m.dir),
685
689
  )
690
+ const hooks = runsScriptHooks(meta.dir, hookMemo) !== null
691
+ const hookOf = (n: string): string | undefined => {
692
+ const base = hooks ? /^(?:pre|post)(.+)$/.exec(n)?.[1] : undefined
693
+ return base !== undefined && typeof scripts[base] === 'string' ? base : undefined
694
+ }
695
+ // A hook rides in its script's command, so it is judged with it.
696
+ const named = Object.keys(scripts).filter(
697
+ (n) => typeof scripts[n] === 'string' && !LIFECYCLE.test(n) && hookOf(n) === undefined,
698
+ )
699
+ const left = {
700
+ runs: named.filter((n) => runs.has(n)),
701
+ shared: named.filter((n) => memberTasks.has(n) && !runs.has(n)),
702
+ }
703
+ const outNames = new Set([...left.runs, ...left.shared])
704
+ const out = new Set(
705
+ Object.keys(scripts).filter((n) => outNames.has(n) || outNames.has(hookOf(n) ?? '')),
706
+ )
707
+ return { ...left, out }
708
+ }
709
+ const rootScripts = (meta: ProjectMeta): Record<string, unknown> => {
710
+ const scripts = scriptsOf(meta)
711
+ const { out } = leftOut(meta)
686
712
  return Object.fromEntries(
687
- Object.entries(scripts).filter(
688
- ([n, v]) => typeof v === 'string' && !memberTasks.has(n) && !runs.has(n),
689
- ),
713
+ Object.entries(scripts).filter(([n, v]) => typeof v === 'string' && !out.has(n)),
690
714
  )
691
715
  }
692
716
  const rootMapped =
693
717
  rootMeta === undefined
694
718
  ? 0
695
719
  : Object.keys(rootScripts(rootMeta)).filter((n) => !LIFECYCLE.test(n)).length
720
+ const listed = (names: readonly string[]): string =>
721
+ names.slice(0, 8).join(', ') + (names.length > 8 ? ` and ${names.length - 8} more` : '')
696
722
  const outsideScripts = outside?.['scripts']
697
723
  const outsideRuns =
698
724
  typeof outsideScripts === 'object' &&
@@ -720,8 +746,15 @@ export function migrateScripts(
720
746
  : 'package.json'
721
747
  : undefined)
722
748
  if (rootName !== undefined && rootMapped > 0) {
749
+ const { runs, shared } = leftOut(rootMeta!)
723
750
  notes.push(
724
- `${rootName} (the workspace root): its scripts that check the whole repo are its tasks; those that run the members (${membersExample(rootMeta && rootManager(rootMeta.dir, hookMemo))}, a runner) or share a member's task name are left out`,
751
+ `${rootName} (the workspace root): its scripts that check the whole repo are its tasks` +
752
+ (runs.length > 0
753
+ ? `; left out as running the members (${membersExample(rootMeta && rootManager(rootMeta.dir, hookMemo))}, a runner): ${listed(runs)}`
754
+ : '') +
755
+ (shared.length > 0
756
+ ? `; left out as a member's task name, so \`--all\` never runs one twice: ${listed(shared)} — one that does other work maps by hand under a name of its own`
757
+ : ''),
725
758
  )
726
759
  } else if (rootName === 'package.json' && outsideDir !== undefined && unnamedMaps().length > 0) {
727
760
  // react's nameless root: "its scripts run the workspace" was not why,