@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.
- package/README.md +1 -1
- package/package.json +5 -5
- package/src/cache/cache.ts +139 -84
- package/src/cache/chained-cache.ts +25 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/inputs.ts +12 -12
- package/src/cache/layer.ts +7 -5
- package/src/cache/policy.ts +5 -0
- package/src/cache/zstd.ts +10 -4
- package/src/cli/cache.ts +10 -4
- package/src/cli/foreign-flags.ts +1 -1
- package/src/cli/last.ts +4 -1
- package/src/cli/run.ts +31 -15
- package/src/cli/watch-filter.ts +63 -12
- package/src/cli/watch-judge.ts +15 -8
- package/src/cli/watch.ts +4 -4
- package/src/config.ts +21 -21
- package/src/exec/proc-sample.ts +1 -1
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +116 -40
- package/src/exec/sandbox-violations.ts +58 -11
- package/src/graph/scheduler.ts +28 -12
- package/src/graph/task-graph.ts +134 -25
- package/src/orchestrator/admission.ts +12 -3
- package/src/orchestrator/events.ts +10 -1
- package/src/orchestrator/execute-task.ts +36 -13
- package/src/orchestrator/failure-mode.ts +8 -2
- package/src/orchestrator/framed-output.ts +3 -3
- package/src/orchestrator/logger.ts +11 -6
- package/src/orchestrator/metrics.ts +19 -8
- package/src/orchestrator/miss-save.ts +2 -2
- package/src/orchestrator/options.ts +7 -0
- package/src/orchestrator/plan.ts +3 -3
- package/src/orchestrator/plugin.ts +37 -16
- package/src/orchestrator/prepare.ts +13 -4
- package/src/orchestrator/run-artifacts.ts +2 -1
- package/src/orchestrator/run.ts +20 -4
- package/src/orchestrator/sandbox-request.ts +65 -9
- package/src/orchestrator/save-lane.ts +5 -5
- package/src/orchestrator/summary.ts +2 -4
- package/src/orchestrator/telemetry.ts +16 -7
- package/src/util/paths.ts +23 -3
- package/src/workspace/config-imports.ts +7 -5
- package/src/workspace/config-schema.ts +26 -3
- package/src/workspace/filter.ts +19 -0
- package/src/workspace/migrate-scripts.ts +4 -2
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/project-loader.ts +4 -2
- package/src/workspace/repo-id.ts +42 -4
- package/src/workspace/workspace.ts +17 -10
package/src/workspace/filter.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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)
|
|
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 =
|
|
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
|
package/src/workspace/repo-id.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
352
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
`
|
|
363
|
-
`
|
|
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
|
/**
|