@vzn/vx 0.0.595 → 0.0.596

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 (50) hide show
  1. package/README.md +1 -1
  2. package/package.json +5 -5
  3. package/src/cache/cache.ts +139 -84
  4. package/src/cache/chained-cache.ts +25 -7
  5. package/src/cache/index.ts +1 -0
  6. package/src/cache/inputs.ts +12 -12
  7. package/src/cache/layer.ts +7 -5
  8. package/src/cache/policy.ts +5 -0
  9. package/src/cache/zstd.ts +10 -4
  10. package/src/cli/cache.ts +10 -4
  11. package/src/cli/foreign-flags.ts +1 -1
  12. package/src/cli/last.ts +4 -1
  13. package/src/cli/run.ts +31 -15
  14. package/src/cli/watch-filter.ts +63 -12
  15. package/src/cli/watch-judge.ts +15 -8
  16. package/src/cli/watch.ts +4 -4
  17. package/src/config.ts +21 -21
  18. package/src/exec/proc-sample.ts +1 -1
  19. package/src/exec/sandbox-paths.ts +1 -1
  20. package/src/exec/sandbox-runtime.ts +116 -40
  21. package/src/exec/sandbox-violations.ts +58 -11
  22. package/src/graph/scheduler.ts +28 -12
  23. package/src/graph/task-graph.ts +134 -25
  24. package/src/orchestrator/admission.ts +12 -3
  25. package/src/orchestrator/events.ts +10 -1
  26. package/src/orchestrator/execute-task.ts +36 -13
  27. package/src/orchestrator/failure-mode.ts +8 -2
  28. package/src/orchestrator/framed-output.ts +3 -3
  29. package/src/orchestrator/logger.ts +11 -6
  30. package/src/orchestrator/metrics.ts +19 -8
  31. package/src/orchestrator/miss-save.ts +2 -2
  32. package/src/orchestrator/options.ts +7 -0
  33. package/src/orchestrator/plan.ts +3 -3
  34. package/src/orchestrator/plugin.ts +37 -16
  35. package/src/orchestrator/prepare.ts +13 -4
  36. package/src/orchestrator/run-artifacts.ts +2 -1
  37. package/src/orchestrator/run.ts +20 -4
  38. package/src/orchestrator/sandbox-request.ts +65 -9
  39. package/src/orchestrator/save-lane.ts +5 -5
  40. package/src/orchestrator/summary.ts +2 -4
  41. package/src/orchestrator/telemetry.ts +16 -7
  42. package/src/util/paths.ts +23 -3
  43. package/src/workspace/config-imports.ts +7 -5
  44. package/src/workspace/config-schema.ts +26 -3
  45. package/src/workspace/filter.ts +19 -0
  46. package/src/workspace/migrate-scripts.ts +4 -2
  47. package/src/workspace/migration.ts +1 -1
  48. package/src/workspace/project-loader.ts +4 -2
  49. package/src/workspace/repo-id.ts +42 -4
  50. package/src/workspace/workspace.ts +17 -10
@@ -5,6 +5,8 @@
5
5
  // {<dir>} same as ./<dir>
6
6
  // // the workspace-root project only (Turbo's name for the root)
7
7
  // tag:<pattern> the projects whose config `tags` hold a match (Nx's `tag:`)
8
+ // <dir>/<glob> a name with a `/` outside a scope that names no project:
9
+ // ./<dir>/<glob> (Nx's `--projects 'apps/*'`)
8
10
  // <pattern>... pattern + its transitive workspace dependencies
9
11
  // ...<pattern> pattern + its transitive workspace dependents
10
12
  // <pattern>^... only the transitive deps of pattern (excluding the matched package)
@@ -58,6 +60,11 @@ export interface ParsedFilter {
58
60
  exactDir?: true
59
61
  /** `tag:<pattern>`: `matcher` is a glob over the projects' tags, not their names. */
60
62
  tag?: true
63
+ /**
64
+ * `apps/*`: a name pattern holding a `/` outside a scope, read as
65
+ * `./apps/*` when it names no project, as Nx's `--projects` reads it.
66
+ */
67
+ dirFallback?: ParsedFilter
61
68
  }
62
69
 
63
70
  export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
@@ -187,6 +194,15 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
187
194
  }
188
195
  }
189
196
 
197
+ // A package name holds a `/` only after its `@scope`, so `apps/*` is a
198
+ // directory for every project named after its package.json; Nx matches
199
+ // a `--projects` entry by name, then by root (find-matching-projects,
200
+ // 23.2.1), and `-p 'apps/*'` matched nothing here.
201
+ const dirFallback =
202
+ !isPath && s.includes('/') && !s.startsWith('@')
203
+ ? parseFilter(`./${s}`, workspaceRoot)
204
+ : undefined
205
+
190
206
  return {
191
207
  raw,
192
208
  negate,
@@ -196,6 +212,7 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
196
212
  onlyDependents,
197
213
  isPath,
198
214
  matcher,
215
+ ...(dirFallback !== undefined ? { dirFallback } : {}),
199
216
  ...(pathGlob !== undefined ? { pathGlob, pathRoot: workspaceRoot } : {}),
200
217
  ...(pathGlobBase !== undefined ? { pathGlobBase } : {}),
201
218
  ...(gitSince !== undefined ? { gitSince } : {}),
@@ -278,6 +295,8 @@ function matchSelector(
278
295
  for (const p of projects) {
279
296
  if (re.test(p.name)) out.push(p.name)
280
297
  }
298
+ if (out.length === 0 && filter.dirFallback !== undefined)
299
+ return matchSelector(filter.dirFallback, projects, tags)
281
300
  if (out.length > 0 || filter.matcher.includes('/')) return out
282
301
  // pnpm's rule: the scope may be left out (`--filter core` is
283
302
  // `@babel/core`), an exact name only when one package carries it. Nx
@@ -429,7 +429,8 @@ function pmRunsMembers(script: string): boolean {
429
429
  return false
430
430
  }
431
431
 
432
- const CD = /(?:^|[\s;&|(])cd\s+("[^"]*"|'[^']*'|[^\s;&|()]+)/g
432
+ // A bare target may escape a space or paren (`cd packages/my\ app`).
433
+ const CD = /(?:^|[\s;&|(])cd\s+("[^"]*"|'[^']*'|(?:\\.|[^\s;&|()\\])+)/g
433
434
 
434
435
  /**
435
436
  * A `cd` into a member's directory, or one holding members, runs that
@@ -439,7 +440,8 @@ const CD = /(?:^|[\s;&|(])cd\s+("[^"]*"|'[^']*'|[^\s;&|()]+)/g
439
440
  */
440
441
  function cdsToMembers(script: string, rootDir: string, memberDirs: readonly string[]): boolean {
441
442
  for (const m of script.matchAll(CD)) {
442
- const target = m[1]!.replace(/^(["'])(.*)\1$/, '$2')
443
+ const quoted = /^(["'])(.*)\1$/.exec(m[1]!)
444
+ const target = quoted ? quoted[2]! : m[1]!.replace(/\\(.)/g, '$1')
443
445
  if (/[$`~*?]/.test(target) || target === '-') return true
444
446
  const dir = path.resolve(rootDir, target)
445
447
  const inside = (a: string, b: string): boolean => a === b || a.startsWith(b + path.sep)
@@ -406,7 +406,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
406
406
  : []
407
407
  if (empty && unreached.length > 0) {
408
408
  report.push(
409
- `${verb}: ${unreachedHint(unreached)}`,
409
+ `${verb}: ${unreachedHint(unreached, root)}`,
410
410
  hasWorkspaceFile
411
411
  ? `${workspaceName} already exists.`
412
412
  : dry
@@ -127,10 +127,12 @@ function servableSource(bytes: Uint8Array, loader: 'ts' | 'js'): string | null {
127
127
  }
128
128
 
129
129
  // What makes Bun run a file as CommonJS is one of these names at the top
130
- // level (an escaped `\u006dodule` too, so any backslash counts). Source
130
+ // level (an escaped `\u006dodule` too, so any backslash counts), or
131
+ // TypeScript's `export =`, which spells none of them. Source
131
132
  // with none of them runs as a module whichever path loads it, so it skips
132
133
  // the parse: 16–20 µs a config, 1,000 cold configs (2026-10-03).
133
- const COMMONJS_HINT = /\b(?:module|exports|require|this|__dirname|__filename)\b|\\/
134
+ const COMMONJS_HINT =
135
+ /\b(?:module|exports|require|this|__dirname|__filename)\b|\\|\bexport\s*=(?!=)/
134
136
 
135
137
  /** vx's module-cache query, which no user wrote: stripped from anything shown to them. */
136
138
  const BUST_QUERY = /\?vx-(?:bust|held)=[^'"\s]*/g
@@ -13,6 +13,7 @@ import {
13
13
  lstatSync,
14
14
  openSync,
15
15
  readFileSync,
16
+ realpathSync,
16
17
  statSync,
17
18
  } from 'node:fs'
18
19
  import path from 'node:path'
@@ -25,12 +26,20 @@ import path from 'node:path'
25
26
  * git: those share nothing.
26
27
  */
27
28
  export async function repoIdOf(root: string): Promise<string | null> {
28
- const located = locateGitDir(root)
29
+ // Git walks the physical path; the logical one found no `.git` through a
30
+ // symlink from outside the repository, and an alias inside it another id.
31
+ let real: string
32
+ try {
33
+ real = realpathSync(root)
34
+ } catch {
35
+ real = path.resolve(root)
36
+ }
37
+ const located = locateGitDir(real)
29
38
  if (located === null) return null
30
39
  const identity =
31
40
  (await remoteIdentity(root, located.commonDir)) ?? (await firstCommit(located.commonDir, root))
32
41
  if (identity === null) return null
33
- const rel = path.relative(located.gitRoot, path.resolve(root)).split(path.sep).join('/')
42
+ const rel = path.relative(located.gitRoot, real).split(path.sep).join('/')
34
43
  const key = sha256(`${identity}#${rel}`)
35
44
  return sha256(key).slice(0, 16)
36
45
  }
@@ -108,11 +117,40 @@ function readOwnedFile(p: string): string | null {
108
117
  }
109
118
  }
110
119
 
111
- /** The remote's identity from the config file, else from `git remote -v`. */
120
+ /**
121
+ * The remote's identity from the config file, else from `git remote -v`
122
+ * when the file cannot settle it: a remote none of whose urls parse (a
123
+ * rewrite may live in a file vx does not read, as Nx asks git), or
124
+ * remotes git could read from `config.worktree`.
125
+ */
112
126
  async function remoteIdentity(root: string, commonDir: string): Promise<string | null> {
113
127
  const config = readOwnedFile(path.join(commonDir, 'config'))
114
128
  const remotes = config === null ? null : parseGitConfigRemotes(config)
115
- const urls = remotes ?? (await remotesFromGit(root))
129
+ if (remotes !== null) {
130
+ const picked = pickRemote(remotes)
131
+ // A remote-less repo with nothing to pull one from elsewhere spawns
132
+ // nothing: git would answer the same, and that spawn was a cost per run.
133
+ if (picked !== null || (remotes.length === 0 && !worktreeConfigOn(config!))) return picked
134
+ }
135
+ return pickRemote(await remotesFromGit(root))
136
+ }
137
+
138
+ /** `extensions.worktreeConfig` set: git reads `config.worktree` too. */
139
+ function worktreeConfigOn(contents: string): boolean {
140
+ let inExtensions = false
141
+ for (const raw of contents.split('\n')) {
142
+ const line = raw.trim()
143
+ if (line.startsWith('[')) {
144
+ inExtensions = /^\[\s*extensions\s*\]/i.test(line)
145
+ continue
146
+ }
147
+ const m = /^worktreeconfig\s*(?:=\s*(\S*))?/i.exec(line)
148
+ if (inExtensions && m !== null && !/^(false|no|off|0)$/i.test(m[1] ?? 'true')) return true
149
+ }
150
+ return false
151
+ }
152
+
153
+ function pickRemote(urls: [string, string][]): string | null {
116
154
  const found = new Map<string, string>()
117
155
  let first: string | null = null
118
156
  for (const [name, url] of urls) {
@@ -1,4 +1,4 @@
1
- import { constants, type Dirent } from 'node:fs'
1
+ import { constants, existsSync, type Dirent } from 'node:fs'
2
2
  import { access, readdir, realpath, stat } from 'node:fs/promises'
3
3
  import { homedir } from 'node:os'
4
4
  import path from 'node:path'
@@ -267,11 +267,13 @@ async function readRootManifests(
267
267
  }
268
268
  catalogs = catalogsOf([parsed])
269
269
  const packages = (parsed as { packages?: unknown }).packages
270
- if (packages !== undefined) {
270
+ // A list commented out leaves `packages:` null, which pnpm reads as
271
+ // absent; vx refused it as "must be an array" (D-149).
272
+ if (packages !== undefined && packages !== null) {
271
273
  return { globs: assertGlobList(packages, yamlPath, 'packages'), catalogs }
272
274
  }
273
275
  }
274
- // No `packages:`: pnpm 10 keeps its settings and catalogs in this file
276
+ // No `packages:` (or an empty one): pnpm 10 keeps its settings and catalogs in this file
275
277
  // for a single-package repo too. The root's package.json decides, as it
276
278
  // would without the file; read as an empty list, the root found zero
277
279
  // projects and every verb ran nothing and exited 0 (item 984).
@@ -348,8 +350,13 @@ export async function unreachedPackages(workspace: Workspace): Promise<string[]>
348
350
  return found.sort()
349
351
  }
350
352
 
351
- /** The line for `unreachedPackages`' finding: the cause, the packages, the glob to add. */
352
- export function unreachedHint(unreached: readonly string[]): string {
353
+ /**
354
+ * The line for `unreachedPackages`' finding: the cause, the packages, the
355
+ * glob to add, and where. A `pnpm-workspace.yaml` with no `packages:` (pnpm
356
+ * keeps its settings there) is where pnpm reads the globs: the hint said
357
+ * there was no such file and named package.json, beside ngrx's (2026-10-07).
358
+ */
359
+ export function unreachedHint(unreached: readonly string[], root: string): string {
353
360
  const n = unreached.length
354
361
  const shown = unreached.slice(0, 3).join(', ') + (n > 3 ? ` and ${n - 3} more` : '')
355
362
  const globs = [
@@ -357,11 +364,11 @@ export function unreachedHint(unreached: readonly string[]): string {
357
364
  ]
358
365
  .map((g) => `"${g}"`)
359
366
  .join(', ')
360
- return (
361
- `package.json declares no \`workspaces\` (and there is no pnpm-workspace.yaml), so the root is the only project ` +
362
- `and ${n} package.json below it ${n === 1 ? 'is' : 'are'} not: ${shown}. ` +
363
- `Add \`"workspaces": [${globs}]\` to package.json and re-run.`
364
- )
367
+ const rest = `so the root is the only project and ${n} package.json below it ${n === 1 ? 'is' : 'are'} not: ${shown}.`
368
+ return existsSync(path.join(root, 'pnpm-workspace.yaml'))
369
+ ? `pnpm-workspace.yaml lists no \`packages\`, ${rest} Add \`packages: [${globs}]\` to pnpm-workspace.yaml and re-run.`
370
+ : `package.json declares no \`workspaces\` (and there is no pnpm-workspace.yaml), ${rest} ` +
371
+ `Add \`"workspaces": [${globs}]\` to package.json and re-run.`
365
372
  }
366
373
 
367
374
  /**