@vzn/vx 0.0.326 → 0.0.328

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.326",
3
+ "version": "0.0.328",
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.326",
25
- "@vzn/vx-linux-arm64": "0.0.326",
26
- "@vzn/vx-darwin-x64": "0.0.326",
27
- "@vzn/vx-darwin-arm64": "0.0.326"
24
+ "@vzn/vx-linux-x64": "0.0.328",
25
+ "@vzn/vx-linux-arm64": "0.0.328",
26
+ "@vzn/vx-darwin-x64": "0.0.328",
27
+ "@vzn/vx-darwin-arm64": "0.0.328"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -522,10 +522,20 @@ export function translateForeign(args: readonly string[]): string[] | { error: s
522
522
  const sep = args.indexOf('--')
523
523
  const before = sep === -1 ? args : args.slice(0, sep)
524
524
  for (let i = 0; i < before.length; i++) {
525
- const a = before[i]!
525
+ let a = before[i]!
526
526
  const eq = a.indexOf('=')
527
- const name = eq === -1 ? a : a.slice(0, eq)
527
+ let name = eq === -1 ? a : a.slice(0, eq)
528
528
  const inline = eq === -1 ? undefined : a.slice(eq + 1)
529
+ // Nx's parser takes every long flag camelCased too (`--nxBail`,
530
+ // `--skipNxCache`), and its docs print that spelling; vx refused it
531
+ // as an unknown flag.
532
+ const kebab = name.startsWith('--')
533
+ ? name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
534
+ : name
535
+ if (kebab !== name && FOREIGN_FLAGS.some((f) => f.runner === 'nx' && f.names.includes(kebab))) {
536
+ name = kebab
537
+ a = inline === undefined ? kebab : `${kebab}=${inline}`
538
+ }
529
539
  const entries = FOREIGN_FLAGS.filter((f) => f.names.includes(name) || f.names.includes(a))
530
540
  const next = before[i + 1]
531
541
  const spaced = inline === undefined && next !== undefined && !next.startsWith('-')
@@ -4,7 +4,7 @@
4
4
  // does it. It stays a refusal: a verb that runs whatever task shares its
5
5
  // name would change meaning the day a plugin declares that verb.
6
6
 
7
- import { nxProjectTarget } from '../orchestrator/index.js'
7
+ import { nxProjectTarget, projectNamed } from '../orchestrator/index.js'
8
8
  import { findWorkspaceRoot, loadWorkspace } from '../workspace/index.js'
9
9
  import { findCwdProject } from './select.js'
10
10
  import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
@@ -20,7 +20,7 @@ export async function taskVerbHint(
20
20
  if (projects === null) return null
21
21
  if (![...projects.values()].some((p) => p.config.tasks?.[command] !== undefined)) return null
22
22
  // Nx's `nx build app`: the word after the target is its project.
23
- const project = rest[0] !== undefined && projects.has(rest[0]) ? rest[0] : undefined
23
+ const project = rest[0] === undefined ? undefined : projectNamed(rest[0], projects)
24
24
  const run =
25
25
  project !== undefined
26
26
  ? `vx run ${command} --filter ${project}`
@@ -1,7 +1,7 @@
1
1
  // Module contract for `orchestrator`. Cross-module imports must come
2
2
  // through here (enforced by tests/module-boundaries.test.ts).
3
3
 
4
- export { run, planRun, nxProjectTarget } from './run.js'
4
+ export { run, planRun, nxProjectTarget, projectNamed } from './run.js'
5
5
  export { prepareRun, type PreparedRun } from './prepare.js'
6
6
  export {
7
7
  discoverProjects,
@@ -111,8 +111,10 @@ export interface PreparedRun {
111
111
  * selection (the cwd's project, a `--filter`) declares: no typo, a scope.
112
112
  */
113
113
  declaredElsewhere: readonly string[]
114
- /** Every discovered project — the declared task names a typo is measured against. */
114
+ /** The loaded projects: the whole workspace, or a scoped run's closure. */
115
115
  projects: ReadonlyMap<string, ProjectEntry>
116
+ /** What a typo is measured against: `projects`, or every project when a `pkg#task` named none loaded. */
117
+ hintProjects: ReadonlyMap<string, ProjectEntry>
116
118
  /**
117
119
  * True iff at least one package in the workspace has a `vx.config.*` at
118
120
  * all — regardless of scope, so a `--filter` that matched nothing is not
@@ -353,6 +355,24 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
353
355
  }
354
356
  declaredElsewhere = bare.filter((t) => !nowhere.includes(t))
355
357
  }
358
+ // A `pkg#task` run loads pkg's closure alone, so a typo'd pkg (or Nx's
359
+ // short name for `@scope/pkg`) was measured against nothing and got no
360
+ // "did you mean". Only a failing run pays for the rest of the workspace.
361
+ let hintProjects: ReadonlyMap<string, ProjectEntry> = projects
362
+ if (
363
+ unresolvedTasks.some((t) => {
364
+ const at = t.indexOf('#')
365
+ return at > 0 && !projects.has(t.slice(0, at))
366
+ })
367
+ ) {
368
+ try {
369
+ hintProjects = (
370
+ await loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects })
371
+ ).projects
372
+ } catch {
373
+ // A config the scope skipped fails to load: no hint, the run's refusal stands.
374
+ }
375
+ }
356
376
 
357
377
  // Cache seam precedence: an EXPLICITLY injected remote layer
358
378
  // (RunOptions.remoteCache — a distribution agent or daemon that already
@@ -438,6 +458,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
438
458
  unresolvedTasks,
439
459
  declaredElsewhere,
440
460
  projects,
461
+ hintProjects,
441
462
  anyProjectConfig: projectsWithConfigs.length > 0,
442
463
  workspaceFingerprint,
443
464
  fingerprintWatch,
@@ -558,6 +579,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
558
579
  unresolvedTasks,
559
580
  declaredElsewhere,
560
581
  projects,
582
+ hintProjects,
561
583
  anyProjectConfig: projectsWithConfigs.length > 0,
562
584
  workspaceFingerprint,
563
585
  fingerprintWatch,
@@ -273,7 +273,7 @@ async function runOnBus(
273
273
  // case too; the message is identical, so that branch stays below.
274
274
  if (prepared.unresolvedTasks.length > 0) {
275
275
  log.status(
276
- `No projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.projects)}${await initHint(prepared)}`,
276
+ `No projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.hintProjects)}${await initHint(prepared)}`,
277
277
  )
278
278
  await teardown()
279
279
  prepared.cache.close()
@@ -1210,7 +1210,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
1210
1210
  return {
1211
1211
  tasks: [],
1212
1212
  unresolvedTasks: prepared.unresolvedTasks,
1213
- unresolvedHint: `${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.projects)}${await initHint(prepared)}`,
1213
+ unresolvedHint: `${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.hintProjects)}${await initHint(prepared)}`,
1214
1214
  }
1215
1215
  }
1216
1216
  if (prepared.empty === 'no-tasks-declared' && prepared.declaredElsewhere.length > 0) {
@@ -1323,7 +1323,7 @@ function didYouMean(
1323
1323
  }
1324
1324
  const [proj, task] = [spec.slice(0, at), spec.slice(at + 1)]
1325
1325
  if (!projects.has(proj)) {
1326
- const p = nearest(proj, projects.keys())
1326
+ const p = projectNamed(proj, projects) ?? nearest(proj, projects.keys())
1327
1327
  if (p !== undefined && tasksOf(projects.get(p)).includes(task)) hints.add(`${p}#${task}`)
1328
1328
  continue
1329
1329
  }
@@ -1344,12 +1344,28 @@ export function nxProjectTarget(
1344
1344
  ): string | undefined {
1345
1345
  const colon = spec.indexOf(':')
1346
1346
  if (colon <= 0 || spec.includes('#')) return undefined
1347
- const [project, task] = [spec.slice(0, colon), spec.slice(colon + 1)]
1348
- return projects.get(project)?.config.tasks?.[task] === undefined
1347
+ const [typed, task] = [spec.slice(0, colon), spec.slice(colon + 1)]
1348
+ const project = projectNamed(typed, projects)
1349
+ return project === undefined || projects.get(project)?.config.tasks?.[task] === undefined
1349
1350
  ? undefined
1350
1351
  : `${project}#${task}`
1351
1352
  }
1352
1353
 
1354
+ /**
1355
+ * The project a typed name means: itself, else the one project whose
1356
+ * scoped name ends in it. Nx names nx-examples' `@nx-example/cart` `cart`,
1357
+ * and `nx run cart:build` is what its users type.
1358
+ */
1359
+ export function projectNamed(
1360
+ typed: string,
1361
+ projects: ReadonlyMap<string, ProjectEntry>,
1362
+ ): string | undefined {
1363
+ if (projects.has(typed)) return typed
1364
+ if (typed.includes('/')) return undefined
1365
+ const scoped = [...projects.keys()].filter((n) => n.startsWith('@') && n.endsWith(`/${typed}`))
1366
+ return scoped.length === 1 ? scoped[0] : undefined
1367
+ }
1368
+
1353
1369
  /** The executed, keyed outcomes of a run — what flakiness is judged on. */
1354
1370
  function flakyCandidates(outcomes: readonly TaskOutcome[]): FlakyCandidate[] {
1355
1371
  const out: FlakyCandidate[] = []