@vzn/vx 0.0.64 → 0.0.66

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.64",
3
+ "version": "0.0.66",
4
4
  "description": "An open, extensible monorepo task runner.",
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.64",
21
- "@vzn/vx-linux-arm64": "0.0.64",
22
- "@vzn/vx-darwin-x64": "0.0.64",
23
- "@vzn/vx-darwin-arm64": "0.0.64"
20
+ "@vzn/vx-linux-x64": "0.0.66",
21
+ "@vzn/vx-linux-arm64": "0.0.66",
22
+ "@vzn/vx-darwin-x64": "0.0.66",
23
+ "@vzn/vx-darwin-arm64": "0.0.66"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -495,11 +495,15 @@ export async function resolveOutputs(args: {
495
495
  nestedProjectDirs: string[]
496
496
  }): Promise<string[]> {
497
497
  if (args.outputs.length === 0) return []
498
- const excludeGlobs = [
499
- ...OUTPUT_NEVER,
500
- ...boundaryIgnorePatterns(args.projectDir, args.nestedProjectDirs),
501
- ].map(globFor)
502
- const scanned = [...(await scanUnion(asTrees(args.outputs), excludeGlobs, args.projectDir))]
498
+ const excludeGlobs = OUTPUT_NEVER.map(globFor)
499
+ const scanned = [
500
+ ...(await scanUnion(
501
+ asTrees(args.outputs),
502
+ excludeGlobs,
503
+ args.projectDir,
504
+ inNestedProject(args.projectDir, args.nestedProjectDirs),
505
+ )),
506
+ ]
503
507
  // Containment, enforced HERE and not only at the loader. `cleanOutputs`
504
508
  // DELETES whatever this returns, and `Bun.Glob.scan` happily walks `..` out
505
509
  // of its cwd — so the loader's `..`/absolute rejection alone was a single
@@ -891,13 +895,10 @@ async function resolveFiles(args: ResolveFilesArgs): Promise<string[]> {
891
895
 
892
896
  if (positive.length === 0) return []
893
897
 
894
- const boundaryIgnores = boundaryIgnorePatterns(args.projectDir, args.nestedProjectDirs)
895
- const excludeGlobs = [
896
- ...ALWAYS_IGNORE,
897
- ...boundaryIgnores,
898
- ...asTrees(args.ownOutputs),
899
- ...asTrees(negative),
900
- ].map(globFor)
898
+ const nested = inNestedProject(args.projectDir, args.nestedProjectDirs)
899
+ const excludeGlobs = [...ALWAYS_IGNORE, ...asTrees(args.ownOutputs), ...asTrees(negative)].map(
900
+ globFor,
901
+ )
901
902
 
902
903
  // Defer to git for the file set (Turbo / Nx parity). Nested .gitignore
903
904
  // files, .git/info/exclude, and global excludes all participate
@@ -910,7 +911,7 @@ async function resolveFiles(args: ResolveFilesArgs): Promise<string[]> {
910
911
  const positiveGlobs = asTrees(positive).map(globFor)
911
912
  // Everything below the snapshot that decides the result: the project, what
912
913
  // it declares, what it excludes as its own outputs, and the boundaries.
913
- const memoKey = `${args.projectDir}\0${positive.join('\u0001')}\0${negative.join('\u0001')}\0${args.ownOutputs.join('\u0001')}\0${boundaryIgnores.join('\u0001')}`
914
+ const memoKey = `${args.projectDir}\0${positive.join('\u0001')}\0${negative.join('\u0001')}\0${args.ownOutputs.join('\u0001')}\0${args.nestedProjectDirs.join('\u0001')}`
914
915
  let gitFiles = args.gitFilesCache?.snapshotFor(args.projectDir, positiveGlobs)
915
916
  let undecodable = args.gitFilesCache?.undecodableNames
916
917
  if (gitFiles !== undefined) {
@@ -962,7 +963,7 @@ async function resolveFiles(args: ResolveFilesArgs): Promise<string[]> {
962
963
  }
963
964
  }
964
965
  if (!matched) continue
965
- if (excludeGlobs.some((g) => g.match(rel))) continue
966
+ if (nested(rel) || excludeGlobs.some((g) => g.match(rel))) continue
966
967
  candidates.push(path.resolve(args.projectDir, rel))
967
968
  }
968
969
  if (unmatchedLiterals.size > 0) {
@@ -1061,6 +1062,7 @@ async function scanUnion(
1061
1062
  positive: readonly string[],
1062
1063
  excludeGlobs: readonly Bun.Glob[],
1063
1064
  cwd: string,
1065
+ nested: (rel: string) => boolean = () => false,
1064
1066
  ): Promise<Set<string>> {
1065
1067
  const matches = new Set<string>()
1066
1068
  // `Bun.Glob`'s scan finds nothing for a brace whose alternatives hold a
@@ -1069,7 +1071,7 @@ async function scanUnion(
1069
1071
  for (const pattern of positive.flatMap(slashBraceExpansions)) {
1070
1072
  const glob = globFor(pattern)
1071
1073
  for (const rel of glob.scanSync({ cwd, onlyFiles: false, followSymlinks: false, dot: true })) {
1072
- if (excludeGlobs.some((g) => g.match(rel))) continue
1074
+ if (nested(rel) || excludeGlobs.some((g) => g.match(rel))) continue
1073
1075
  const abs = path.resolve(cwd, rel)
1074
1076
  const st = lstatSync(abs, { throwIfNoEntry: false })
1075
1077
  if (st !== undefined && (st.isFile() || st.isSymbolicLink())) matches.add(abs)
@@ -1089,9 +1091,24 @@ function globFor(pattern: string): Bun.Glob {
1089
1091
  return g
1090
1092
  }
1091
1093
 
1092
- function boundaryIgnorePatterns(projectDir: string, nestedDirs: string[]): string[] {
1093
- return nestedDirs.map((d) => {
1094
- const rel = path.relative(projectDir, d).split(path.sep).join('/')
1095
- return `${rel}/**`
1096
- })
1094
+ /**
1095
+ * Whether a project-relative path lies inside a nested project: its
1096
+ * ancestor directories looked up in a set, O(depth). One `<nested>/**` glob
1097
+ * per nested project matched every file against every one, O(files ×
1098
+ * nested): 320 ms of astro's 834 ms warm no-op, 4,003 files against 330
1099
+ * fixture projects (I's log, A-11). A glob also read `*`, `?` or `{` in a
1100
+ * nested directory's NAME as syntax, so a project at `pkg*` took its
1101
+ * sibling `pkg-b`'s files out of the key; a name is a name.
1102
+ */
1103
+ function inNestedProject(projectDir: string, nestedDirs: string[]): (rel: string) => boolean {
1104
+ if (nestedDirs.length === 0) return () => false
1105
+ const dirs = new Set(
1106
+ nestedDirs.map((d) => path.relative(projectDir, d).split(path.sep).join('/')),
1107
+ )
1108
+ return (rel) => {
1109
+ for (let i = rel.indexOf('/'); i !== -1; i = rel.indexOf('/', i + 1)) {
1110
+ if (dirs.has(rel.slice(0, i))) return true
1111
+ }
1112
+ return false
1113
+ }
1097
1114
  }
@@ -68,7 +68,10 @@ export function createEventBus(): EventBus {
68
68
  // as wrong output, not a silent hang.
69
69
  for (const subscriber of subscribers) {
70
70
  try {
71
- subscriber(event)
71
+ // A subscriber typed `void` may still be async: its rejection is
72
+ // isolated as its throw is.
73
+ const ret: unknown = subscriber(event)
74
+ if (ret instanceof Promise) ret.catch(() => {})
72
75
  } catch {
73
76
  // isolate: a surface fault can't propagate into the orchestrator
74
77
  }
@@ -507,47 +507,53 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
507
507
  },
508
508
  },
509
509
  on(hook, handler) {
510
+ const fail = (err: unknown): void => {
511
+ if (disabled.has(plugin.name)) return
512
+ disabled.add(plugin.name)
513
+ warn(
514
+ `[vx] plugin '${plugin.name}' threw in ${hook}; disabled for this run: ${err instanceof Error ? err.message : String(err)}`,
515
+ )
516
+ }
510
517
  const dispose = bus.subscribe((event) => {
511
518
  if (disabled.has(plugin.name)) return
512
- // void each handler call: hooks may return Promise; we
513
- // intentionally don't await (the bus is synchronous;
514
- // long-running plugin work happens off the critical path).
519
+ // Not awaited: the bus is synchronous and plugin work runs off the
520
+ // critical path. A handler's rejection is caught like its throw.
521
+ let ret: unknown
515
522
  try {
516
523
  switch (hook) {
517
524
  case 'onRunStart':
518
525
  if (event.kind === 'run:start')
519
- void (handler as PluginHookHandlers['onRunStart'])(event.info)
526
+ ret = (handler as PluginHookHandlers['onRunStart'])(event.info)
520
527
  break
521
528
  case 'onTaskStart':
522
529
  if (event.kind === 'task:start')
523
- void (handler as PluginHookHandlers['onTaskStart'])(event.node)
530
+ ret = (handler as PluginHookHandlers['onTaskStart'])(event.node)
524
531
  break
525
532
  case 'onTaskStdout':
526
533
  if (event.kind === 'task:stdout')
527
- void (handler as PluginHookHandlers['onTaskStdout'])(event.node, event.chunk)
534
+ ret = (handler as PluginHookHandlers['onTaskStdout'])(event.node, event.chunk)
528
535
  break
529
536
  case 'onTaskStderr':
530
537
  if (event.kind === 'task:stderr')
531
- void (handler as PluginHookHandlers['onTaskStderr'])(event.node, event.chunk)
538
+ ret = (handler as PluginHookHandlers['onTaskStderr'])(event.node, event.chunk)
532
539
  break
533
540
  case 'onTaskComplete':
534
541
  if (event.kind === 'task:complete')
535
- void (handler as PluginHookHandlers['onTaskComplete'])(event.node, event.outcome)
542
+ ret = (handler as PluginHookHandlers['onTaskComplete'])(event.node, event.outcome)
536
543
  break
537
544
  case 'onRunStatus':
538
545
  if (event.kind === 'run:status')
539
- void (handler as PluginHookHandlers['onRunStatus'])(event.line)
546
+ ret = (handler as PluginHookHandlers['onRunStatus'])(event.line)
540
547
  break
541
548
  case 'onRunEnd':
542
- if (event.kind === 'run:end') void (handler as PluginHookHandlers['onRunEnd'])()
549
+ if (event.kind === 'run:end') ret = (handler as PluginHookHandlers['onRunEnd'])()
543
550
  break
544
551
  }
545
552
  } catch (err) {
546
- disabled.add(plugin.name)
547
- warn(
548
- `[vx] plugin '${plugin.name}' threw in ${hook}; disabled for this run: ${err instanceof Error ? err.message : String(err)}`,
549
- )
553
+ fail(err)
554
+ return
550
555
  }
556
+ if (ret instanceof Promise) ret.catch(fail)
551
557
  })
552
558
  disposers.push(dispose)
553
559
  },