@vzn/vx 0.0.305 → 0.0.307

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.305",
3
+ "version": "0.0.307",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
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.305",
21
- "@vzn/vx-linux-arm64": "0.0.305",
22
- "@vzn/vx-darwin-x64": "0.0.305",
23
- "@vzn/vx-darwin-arm64": "0.0.305"
20
+ "@vzn/vx-linux-x64": "0.0.307",
21
+ "@vzn/vx-linux-arm64": "0.0.307",
22
+ "@vzn/vx-darwin-x64": "0.0.307",
23
+ "@vzn/vx-darwin-arm64": "0.0.307"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/init.ts CHANGED
@@ -102,6 +102,7 @@ export async function initCmd(args: readonly string[]): Promise<number> {
102
102
  plan: migrateScripts(
103
103
  metas,
104
104
  metas.some((m) => m.dir === root) ? undefined : await readRootManifest(root),
105
+ root,
105
106
  ),
106
107
  source: 'package.json scripts',
107
108
  verb: 'vx init',
@@ -106,6 +106,11 @@ export interface PreparedRun {
106
106
  * than run the remainder silently.
107
107
  */
108
108
  unresolvedTasks: readonly string[]
109
+ /**
110
+ * The bare names in `unresolvedTasks` a project outside a scoped run's
111
+ * selection (the cwd's project, a `--filter`) declares: no typo, a scope.
112
+ */
113
+ declaredElsewhere: readonly string[]
109
114
  /** Every discovered project — the declared task names a typo is measured against. */
110
115
  projects: ReadonlyMap<string, ProjectEntry>
111
116
  /**
@@ -330,6 +335,24 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
330
335
  )
331
336
  }
332
337
  }
338
+ // Run inside a project that lacks the task (`vx run typecheck` at a
339
+ // root whose members declare it), the run said no project declares it.
340
+ // Only a failing run pays for the rest of the workspace.
341
+ let declaredElsewhere: string[] = []
342
+ // A scope with no project to ask (a member with no vx config) leaves
343
+ // every requested name unjudged.
344
+ const bare = (candidateProjects.length === 0 ? tasks : unresolvedTasks).filter(
345
+ (t) => !t.includes('#'),
346
+ )
347
+ if (options.projects !== undefined && options.selectedByDiff !== true && bare.length > 0) {
348
+ let nowhere = undeclaredIn(bare, projects)
349
+ if (nowhere.length > 0 && projectsWithConfigs.some((m) => !projects.has(m.name))) {
350
+ nowhere = await declaredNowhere(nowhere, () =>
351
+ loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
352
+ )
353
+ }
354
+ declaredElsewhere = bare.filter((t) => !nowhere.includes(t))
355
+ }
333
356
 
334
357
  // Cache seam precedence: an EXPLICITLY injected remote layer
335
358
  // (RunOptions.remoteCache — a distribution agent or daemon that already
@@ -413,6 +436,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
413
436
  nodes: new Map(),
414
437
  keyOnly: new Map(),
415
438
  unresolvedTasks,
439
+ declaredElsewhere,
416
440
  projects,
417
441
  anyProjectConfig: projectsWithConfigs.length > 0,
418
442
  workspaceFingerprint,
@@ -532,6 +556,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
532
556
  nodes,
533
557
  keyOnly,
534
558
  unresolvedTasks,
559
+ declaredElsewhere,
535
560
  projects,
536
561
  anyProjectConfig: projectsWithConfigs.length > 0,
537
562
  workspaceFingerprint,
@@ -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(', ')}.${didYouMean(prepared.unresolvedTasks, prepared.projects)}${await initHint(prepared)}`,
276
+ `No projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.projects)}${await initHint(prepared)}`,
277
277
  )
278
278
  await teardown()
279
279
  prepared.cache.close()
@@ -292,7 +292,7 @@ async function runOnBus(
292
292
  // buildTaskGraph semantics but logged just in case.
293
293
  const msg =
294
294
  prepared.empty === 'no-tasks-declared'
295
- ? `No projects declare task(s): ${options.tasks.join(', ')}.${await initHint(prepared)}`
295
+ ? `No projects declare task(s): ${options.tasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${await initHint(prepared)}`
296
296
  : 'No tasks to run.'
297
297
  log.status(msg)
298
298
  await teardown()
@@ -1210,7 +1210,14 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
1210
1210
  return {
1211
1211
  tasks: [],
1212
1212
  unresolvedTasks: prepared.unresolvedTasks,
1213
- unresolvedHint: `${didYouMean(prepared.unresolvedTasks, prepared.projects)}${await initHint(prepared)}`,
1213
+ unresolvedHint: `${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.projects)}${await initHint(prepared)}`,
1214
+ }
1215
+ }
1216
+ if (prepared.empty === 'no-tasks-declared' && prepared.declaredElsewhere.length > 0) {
1217
+ return {
1218
+ tasks: [],
1219
+ unresolvedTasks: prepared.declaredElsewhere,
1220
+ unresolvedHint: elsewhereHint(prepared.declaredElsewhere),
1214
1221
  }
1215
1222
  }
1216
1223
  if (prepared.empty !== null) return { tasks: [] }
@@ -1258,6 +1265,12 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
1258
1265
  }
1259
1266
  }
1260
1267
 
1268
+ /** The names a project outside the run's selection declares, and how to reach them. */
1269
+ function elsewhereHint(names: readonly string[]): string {
1270
+ if (names.length === 0) return ''
1271
+ return ` Only projects outside the selection declare ${names.join(', ')} — pass --all, or --filter to pick them.`
1272
+ }
1273
+
1261
1274
  /**
1262
1275
  * The first-run case, told apart from a typo: no package in the workspace
1263
1276
  * has a `vx.config.*` at all, so no name could have resolved. Reached only
@@ -244,24 +244,67 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
244
244
  if (!at.todos.includes(todo)) at.todos.push(todo)
245
245
  }
246
246
 
247
+ /**
248
+ * A root script that runs the members rather than checking the repo:
249
+ * pnpm's `-r` / `--filter` / `-C`, npm's and Yarn's workspace flags, Bun's
250
+ * `--filter`, and the other runners, vx itself included. Mapped, it ran
251
+ * every member's task again beside the member's own (D-45).
252
+ */
253
+ const RUNS_MEMBERS =
254
+ /(^|[\s;&|(])(turbo|nx|lerna|ultra|wireit|nps|moon|rush|vx)(\s|$)|\s(-r|--recursive|--filter|-F|--workspaces|-ws|--workspace|-w|-C|--dir|--if-present|--parallel|--stream)(\s|=|$)|\bworkspaces?\s+(foreach|run)\b|\bcd\s/
255
+
247
256
  /**
248
257
  * `outside`: the root manifest when the root is no member (pnpm's and
249
258
  * Yarn's default), whose own scripts (`lint: eslint .`) went unmapped
250
- * without a word.
259
+ * without a word; `outsideDir` is where it sits.
251
260
  */
252
261
  export function migrateScripts(
253
262
  metas: readonly ProjectMeta[],
254
263
  outside?: Readonly<Record<string, unknown>>,
264
+ outsideDir?: string,
255
265
  ): MigrationPlan {
256
266
  const projects: GeneratedProject[] = []
257
267
  const hookMemo = new Map<string, string | null>()
258
- // The workspace root among members: its scripts run the workspace
259
- // (`npm run build --workspaces`, `pnpm -r build`), and a root task made
260
- // of one ran every member's build again under `--all`. Since D-39 a
261
- // hand-written root config makes the root a project, and `--force`
262
- // replaced it so (D-45). A lone package is its repo's project and maps.
268
+ // The workspace root among members: many of its scripts run the
269
+ // workspace (`npm run build --workspaces`, `pnpm -r build`), and a root
270
+ // task made of one ran every member's build again under `--all`. Since
271
+ // D-39 a hand-written root config makes the root a project, and
272
+ // `--force` replaced it so (D-45). A lone package is its repo's project
273
+ // and maps.
263
274
  const root = metas.length > 1 ? workspaceRootOf(metas) : undefined
264
275
  const notes: string[] = []
276
+ // The rest check the whole repo (`lint: oxlint .`, `test: vitest`):
277
+ // `vx run lint` found no project in remix, wagmi or element-plus. Such a
278
+ // script maps onto the root, when the root has a name (vx skips a
279
+ // nameless one's config), no config of its own, and no member declares
280
+ // the task, so `--all` never runs one check twice.
281
+ const outsideName =
282
+ typeof outside?.['name'] === 'string' && outside['name'] !== '' ? outside['name'] : undefined
283
+ const rootMeta: ProjectMeta | undefined =
284
+ root !== undefined
285
+ ? root.configPath === null
286
+ ? root
287
+ : undefined
288
+ : outsideName !== undefined && outsideDir !== undefined
289
+ ? { name: outsideName, dir: outsideDir, packageJson: outside as never, configPath: null }
290
+ : undefined
291
+ const memberTasks = new Set(
292
+ metas
293
+ .filter((m) => m !== root)
294
+ .flatMap((m) => Object.entries(scriptsOf(m)))
295
+ .filter(([, v]) => typeof v === 'string' && v !== '')
296
+ .map(([n]) => n),
297
+ )
298
+ const rootScripts = (meta: ProjectMeta): Record<string, unknown> =>
299
+ Object.fromEntries(
300
+ Object.entries(scriptsOf(meta)).filter(
301
+ ([n, v]) => typeof v === 'string' && !memberTasks.has(n) && !RUNS_MEMBERS.test(` ${v}`),
302
+ ),
303
+ )
304
+ const rootMapped =
305
+ rootMeta === undefined
306
+ ? 0
307
+ : Object.keys(rootScripts(rootMeta)).filter((n) => !LIFECYCLE.test(n)).length
265
308
  const outsideScripts = outside?.['scripts']
266
309
  const outsideRuns =
267
310
  typeof outsideScripts === 'object' &&
@@ -274,7 +317,11 @@ export function migrateScripts(
274
317
  ? outside['name']
275
318
  : 'package.json'
276
319
  : undefined)
277
- if (rootName !== undefined) {
320
+ if (rootName !== undefined && rootMapped > 0) {
321
+ notes.push(
322
+ `${rootName} (the workspace root): its scripts that check the whole repo are its tasks; those that run the members (\`pnpm -r\`, \`--filter\`, a runner) or share a member's task name are left out`,
323
+ )
324
+ } else if (rootName !== undefined) {
278
325
  // A nameless root's vx.config is skipped (vx names projects by their
279
326
  // manifest's name), so the hand-written one needs a name first (vuejs/core).
280
327
  notes.push(
@@ -282,9 +329,10 @@ export function migrateScripts(
282
329
  (rootName === 'package.json' ? ', after giving its package.json a "name"' : ''),
283
330
  )
284
331
  }
285
- for (const meta of metas) {
286
- if (meta === root) continue
287
- const scripts = scriptsOf(meta)
332
+ const mapped = metas.filter((m) => m !== root)
333
+ if (rootMeta !== undefined && rootMapped > 0) mapped.push(rootMeta)
334
+ for (const meta of mapped) {
335
+ const scripts = meta === rootMeta ? rootScripts(meta) : scriptsOf(meta)
288
336
  const hooksBy = runsScriptHooks(meta.dir, hookMemo)
289
337
  const runsHooks = hooksBy !== null
290
338
  const runnable = Object.keys(scripts).filter(