@vzn/vx 0.0.591 → 0.0.594

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.
Files changed (55) hide show
  1. package/launcher.cjs +9 -2
  2. package/package.json +5 -5
  3. package/src/cache/archive.ts +16 -5
  4. package/src/cache/cache.ts +41 -11
  5. package/src/cache/git-inputs.ts +14 -9
  6. package/src/cache/inputs.ts +47 -17
  7. package/src/cache/tar-stream.ts +3 -1
  8. package/src/cli/cache.ts +3 -5
  9. package/src/cli/foreign-flags.ts +33 -4
  10. package/src/cli/help.ts +22 -5
  11. package/src/cli/info.ts +4 -6
  12. package/src/cli/init.ts +3 -3
  13. package/src/cli/last.ts +49 -20
  14. package/src/cli/lock.ts +3 -3
  15. package/src/cli/run.ts +65 -37
  16. package/src/cli/select.ts +14 -2
  17. package/src/cli/show.ts +5 -6
  18. package/src/cli/upgrade.ts +2 -3
  19. package/src/cli/watch-filter.ts +7 -6
  20. package/src/cli/watch-fs.ts +17 -1
  21. package/src/cli/watch.ts +22 -4
  22. package/src/cli/why.ts +4 -13
  23. package/src/cli/workspace-config.ts +1 -1
  24. package/src/exec/env.ts +3 -5
  25. package/src/exec/runner.ts +28 -7
  26. package/src/exec/sandbox-deny-scan.ts +2 -2
  27. package/src/exec/sandbox-runtime.ts +69 -19
  28. package/src/exec/sandbox-violations.ts +23 -9
  29. package/src/graph/scheduler.ts +13 -3
  30. package/src/graph/task-graph.ts +3 -2
  31. package/src/index.ts +3 -0
  32. package/src/orchestrator/affected-tasks.ts +34 -10
  33. package/src/orchestrator/doctor.ts +2 -1
  34. package/src/orchestrator/execute-task.ts +14 -3
  35. package/src/orchestrator/hit-restore.ts +3 -2
  36. package/src/orchestrator/index.ts +1 -0
  37. package/src/orchestrator/lockfile-claim.ts +2 -3
  38. package/src/orchestrator/logger.ts +4 -1
  39. package/src/orchestrator/miss-save.ts +10 -3
  40. package/src/orchestrator/placement.ts +17 -3
  41. package/src/orchestrator/plugin-host.ts +16 -0
  42. package/src/orchestrator/plugin.ts +9 -13
  43. package/src/orchestrator/projects.ts +18 -9
  44. package/src/orchestrator/run-context.ts +2 -2
  45. package/src/orchestrator/run-lock.ts +14 -4
  46. package/src/orchestrator/run.ts +42 -8
  47. package/src/orchestrator/sandbox-request.ts +6 -10
  48. package/src/workspace/affected.ts +17 -11
  49. package/src/workspace/config-eval.ts +4 -0
  50. package/src/workspace/config-imports.ts +1 -1
  51. package/src/workspace/config-schema.ts +2 -7
  52. package/src/workspace/filter.ts +5 -4
  53. package/src/workspace/migrate-scripts.ts +22 -5
  54. package/src/workspace/project-loader.ts +17 -2
  55. package/src/workspace/workspace.ts +40 -4
@@ -269,6 +269,9 @@ interface BuildPosition {
269
269
  column?: number
270
270
  }
271
271
 
272
+ /** A stack frame naming a file path, as a config's own throw has. */
273
+ const NAMED_FRAME = /\n\s+at .*[\\/]/
274
+
272
275
  /**
273
276
  * Turn the two errors Bun's own loader throws into user errors naming the
274
277
  * file the user wrote; every other throw is the config's own and already
@@ -301,6 +304,13 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
301
304
  return configLoadError(errors[0], configPath, kind)
302
305
  }
303
306
  if (typeof message !== 'string') return null
307
+ // Bun's JSON loader throws a `SyntaxError` whose stack, when it has one,
308
+ // holds no file frame: a malformed `import data from './data.json'`
309
+ // printed `vx: JSON Parse error: Expected '}'` and named nothing. A
310
+ // config's own `JSON.parse` has its line in the stack and passes through.
311
+ if (name === 'SyntaxError' && !NAMED_FRAME.test(String((err as { stack?: unknown }).stack))) {
312
+ return new UserError(`${kind} config ${configPath}: an import does not parse: ${message}`)
313
+ }
304
314
  if (name === 'ResolveMessage') {
305
315
  const spec = /Cannot find (?:package|module) ['"]([^'"]+)['"]/.exec(message)?.[1]
306
316
  const what = spec === undefined ? message.replace(BUST_QUERY, '') : `cannot find '${spec}'`
@@ -665,14 +675,19 @@ export async function loadProjectConfigs(
665
675
  */
666
676
  // `Bun` and `Bun.hash` are what vx itself runs on: `Bun.hash.xxHash3 = ()
667
677
  // => 7n` gave every task the key 00000000, and a changed command replayed
668
- // the old output (D-75).
678
+ // the old output (D-75). `Bun` is read by its identifier, not through
679
+ // globalThis: the docs playground bundles this file with the identifier
680
+ // rewritten to its shim, and a browser has no global `Bun`.
669
681
  const WATCHED_BUILTINS: ReadonlyArray<readonly [string, object]> = WATCHED_BUILTIN_NAMES.map(
670
682
  (name) =>
671
683
  [
672
684
  name,
673
685
  name
674
686
  .split('.')
675
- .reduce<unknown>((o, k) => (o as Record<string, unknown>)[k], globalThis) as object,
687
+ .reduce<unknown>(
688
+ (o, k) => (o === globalThis && k === 'Bun' ? Bun : (o as Record<string, unknown>)[k]),
689
+ globalThis,
690
+ ) as object,
676
691
  ] as const,
677
692
  )
678
693
 
@@ -115,8 +115,12 @@ export async function findWorkspaceRoot(
115
115
  // them: `packages/**` holding `packages/inner` (with `workspaces:
116
116
  // ['sub']`) and `packages/inner/sub` resolved `sub` to `inner` and
117
117
  // `inner` to the outer root, two roots and two keys for one tree.
118
- if (claimsMember(dir, [claimed], globs) && claimsMember(dir, members, globs)) claimed = dir
119
- } else if (claimsMember(dir, inner === null ? below : [inner], globs)) {
118
+ if (
119
+ (await claimsMember(dir, [claimed], globs)) &&
120
+ (await claimsMember(dir, members, globs))
121
+ )
122
+ claimed = dir
123
+ } else if (await claimsMember(dir, inner === null ? below : [inner], globs)) {
120
124
  claimed = dir
121
125
  members = inner === null ? [...below] : [inner]
122
126
  }
@@ -152,7 +156,11 @@ export async function findWorkspaceRoot(
152
156
  }
153
157
 
154
158
  /** True when one of `below` (dirs under `root`, toward `start`) is a member. */
155
- function claimsMember(root: string, below: readonly string[], globs: readonly string[]): boolean {
159
+ async function claimsMember(
160
+ root: string,
161
+ below: readonly string[],
162
+ globs: readonly string[],
163
+ ): Promise<boolean> {
156
164
  if (below.length === 0 || globs.length === 0) return false
157
165
  const { positive, negative } = splitPackageGlobs(globs)
158
166
  const rels = below.map((d) => relPosix(root, d)).filter((rel) => !excludedBy(rel, negative))
@@ -161,11 +169,21 @@ function claimsMember(root: string, below: readonly string[], globs: readonly st
161
169
  // `.` means the root itself is the project — never a directory below it.
162
170
  if (normalized === '' || normalized === '.') continue
163
171
  const glob = new Bun.Glob(normalized)
164
- if (rels.some((rel) => glob.match(rel))) return true
172
+ for (const rel of rels) {
173
+ if (!glob.match(rel)) continue
174
+ // `match` has no `dot: false`: `packages/*` matched `packages/.tpl`,
175
+ // which discovery skips, and a run from inside it found a workspace
176
+ // that does not list it. Such a path asks discovery's own walk.
177
+ if (!SKIPPED_SEGMENT.test(rel)) return true
178
+ const dirs = await memberDirs(root, pattern)
179
+ if (dirs.includes(path.join(root, rel))) return true
180
+ }
165
181
  }
166
182
  return false
167
183
  }
168
184
 
185
+ const SKIPPED_SEGMENT = /(^|\/)(\.|node_modules(\/|$))/
186
+
169
187
  /**
170
188
  * pnpm, npm, yarn and Bun all take `!packages/fixtures` in the package
171
189
  * list. Handed to `Bun.Glob` raw, a leading `!` negates the WHOLE pattern,
@@ -374,6 +392,11 @@ function extglobRefusal(pattern: string, file: string, field: string): UserError
374
392
  */
375
393
  export function resolveCacheDir(root: string, config: WorkspaceConfig | null): string {
376
394
  const rel = config?.cacheDir ?? (process.env['VX_CACHE_DIR'] || path.join('.vx', 'cache'))
395
+ // No shell expands `~` in a config string or a quoted variable, and
396
+ // `'~/.cache/vx'` made a directory named `~` in the workspace.
397
+ if (rel.startsWith('~/')) {
398
+ return path.join(process.env['HOME'] || homedir(), rel.slice(1))
399
+ }
377
400
  return path.resolve(root, rel)
378
401
  }
379
402
 
@@ -809,6 +832,19 @@ export async function namedProject(
809
832
  if (typeof named?.name !== 'string' || named.name === '' || typeof named.dir !== 'string') {
810
833
  throw new UserError(`${where} ${JSON.stringify(named)}: expected { dir: string, name: string }`)
811
834
  }
835
+ // The rules `parsePackageJson` holds a manifest's name to: a plugin's name
836
+ // for a directory with no `name` of its own skipped them, and `a#b` planned
837
+ // under `--all` while no run spec or dependsOn could reach it.
838
+ if (named.name.trim() !== named.name) {
839
+ throw new UserError(
840
+ `${where} ${JSON.stringify(named.name)}: the name has surrounding whitespace`,
841
+ )
842
+ }
843
+ if (named.name.includes('#')) {
844
+ throw new UserError(
845
+ `${where} "${named.name}": the name holds "#" — vx addresses a task as <name>#<task>`,
846
+ )
847
+ }
812
848
  const dir = path.resolve(workspace.root, named.dir)
813
849
  const rel = relPosix(workspace.root, dir)
814
850
  if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) {