@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.
@@ -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 falls back to `HEAD~1`. A clone with no `origin/HEAD` and
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 [, manager, run, name] = m
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
- if (typeof pm === 'string')
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 ([...v.matchAll(RUNS_SCRIPT)].some((m) => out.has(m[1]!))) {
413
+ if (scriptRefs(v, text).some((r) => out.has(r))) {
371
414
  out.add(n)
372
415
  grew = true
373
416
  }