@vzn/vx 0.0.347 → 0.0.349
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.
|
|
3
|
+
"version": "0.0.349",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.349",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.349",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.349",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.349"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
|
@@ -35,16 +35,18 @@ type SrtModule = typeof import('@anthropic-ai/sandbox-runtime')
|
|
|
35
35
|
export function bindableWrites(paths: readonly string[]): string[] {
|
|
36
36
|
if (process.platform !== 'linux') return [...paths]
|
|
37
37
|
return unique(
|
|
38
|
-
paths
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
paths
|
|
39
|
+
.filter((p) => !holdsBracket(p))
|
|
40
|
+
.map((p) => {
|
|
41
|
+
if (!isMountableLiteral(p)) return p
|
|
42
|
+
try {
|
|
43
|
+
if (statSync(p).isDirectory()) return p
|
|
44
|
+
} catch {
|
|
45
|
+
// Does not exist yet: `prepareOutputsForBind` creates a file for a
|
|
46
|
+
// file-shaped grant, so treat it as one.
|
|
47
|
+
}
|
|
48
|
+
return path.dirname(p)
|
|
49
|
+
}),
|
|
48
50
|
)
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -188,6 +190,32 @@ export function punchWritePaths(readPath: string, writePaths: readonly string[])
|
|
|
188
190
|
return out
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
/** Grants already reported — once per process, not per spawn. */
|
|
194
|
+
const warnedBracket = new Set<string>()
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* SRT drops every Linux write path holding a bracket (it reads one as a
|
|
198
|
+
* glob, and a write path must be a path), and no spelling keeps it. Left
|
|
199
|
+
* in, the read grants were punched around a bind that never came, the
|
|
200
|
+
* directory vanished from the task's view, its write read "Directory
|
|
201
|
+
* nonexistent" with no word of the grant, and the refused write went
|
|
202
|
+
* unreported, judged against it. Dropped from the binds instead, and said
|
|
203
|
+
* once: the remedy is the deepest directory above it whose name holds none.
|
|
204
|
+
*/
|
|
205
|
+
function holdsBracket(grant: string): boolean {
|
|
206
|
+
const at = grant.search(/[[\]]/)
|
|
207
|
+
if (at === -1) return false
|
|
208
|
+
if (!warnedBracket.has(grant)) {
|
|
209
|
+
warnedBracket.add(grant)
|
|
210
|
+
process.stderr.write(
|
|
211
|
+
`[vx] sandbox: the write grant ${grant} holds a bracket, and the Linux sandbox mounts no ` +
|
|
212
|
+
`write path that does, so a write under it is refused. Grant the directory above it ` +
|
|
213
|
+
`instead: ${grant.slice(0, grant.lastIndexOf(path.sep, at) + 1)}\n`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
191
219
|
/**
|
|
192
220
|
* Merge the orchestrator-provided baseline (declared inputs / outputs /
|
|
193
221
|
* workspace-root anchor) with the user's resolved sandbox block to
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
38
38
|
import { killGraceMs, maskedEmitter, printable, relPosix, secretMask, span } from '../util/index.js'
|
|
39
39
|
import { SIGNAL_SHUTDOWN_GRACE_MS } from './signals.js'
|
|
40
|
-
import { executorLabel } from './plugin-host.js'
|
|
40
|
+
import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
41
41
|
import {
|
|
42
42
|
mayWriteFingerprint,
|
|
43
43
|
type Placeholder,
|
|
@@ -791,7 +791,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
791
791
|
assertExecuteResult(args.executor.name, node.id, r)
|
|
792
792
|
return r
|
|
793
793
|
})
|
|
794
|
-
.catch(async (
|
|
794
|
+
.catch(async (raw: unknown) => {
|
|
795
|
+
const err = nameExecutorFailure(args.executor, raw)
|
|
795
796
|
const message = err instanceof Error ? err.message : String(err)
|
|
796
797
|
log.taskStderr(node, `${message}\n`)
|
|
797
798
|
await sweepPlaceholders(placeholders)
|
|
@@ -439,6 +439,23 @@ export async function resolveCache(
|
|
|
439
439
|
/** The plugin each resolved executor came from, so a hook of it that throws is named. */
|
|
440
440
|
const executorPlugin = new WeakMap<TaskExecutor, string>()
|
|
441
441
|
|
|
442
|
+
/**
|
|
443
|
+
* A plugin executor's throw from `execute`, named as every other hook's
|
|
444
|
+
* is: `plugin 'p' (executor 'e') failed in execute: …`. It read `internal
|
|
445
|
+
* error in <task>: pool down`, naming neither (C-63). The error itself is
|
|
446
|
+
* kept, its class, cause and code with it, so the scheduler still tells a
|
|
447
|
+
* refusal from a bug; the floor's own throw is vx's and stays as it is.
|
|
448
|
+
*/
|
|
449
|
+
export function nameExecutorFailure(executor: TaskExecutor, err: unknown): unknown {
|
|
450
|
+
if (!(err instanceof Error) || !executorPlugin.has(executor)) return err
|
|
451
|
+
try {
|
|
452
|
+
err.message = `${executorLabel(executor)} failed in execute: ${err.message}`
|
|
453
|
+
} catch {
|
|
454
|
+
// A frozen error keeps its own words.
|
|
455
|
+
}
|
|
456
|
+
return err
|
|
457
|
+
}
|
|
458
|
+
|
|
442
459
|
/** How a message names `executor`: its plugin and its own name, or its name alone (the floor). */
|
|
443
460
|
export function executorLabel(executor: TaskExecutor): string {
|
|
444
461
|
const plugin = executorPlugin.get(executor)
|
|
@@ -107,7 +107,41 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
|
107
107
|
* `enablePrePostScripts: false` in `pnpm-workspace.yaml` (D-33); Bun and
|
|
108
108
|
* Yarn 1 run them whatever those say. No manager found is npm's default.
|
|
109
109
|
*/
|
|
110
|
-
function runsScriptHooks(dir: string, memo: Map<string,
|
|
110
|
+
function runsScriptHooks(dir: string, memo: Map<string, Owner>): string | null {
|
|
111
|
+
const { manager, at } = ownerOf(dir, memo)
|
|
112
|
+
const read = (f: string): string | undefined => {
|
|
113
|
+
try {
|
|
114
|
+
return readFileSync(path.join(at, f), 'utf8')
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const npmrc = read('.npmrc') ?? ''
|
|
120
|
+
const set = (key: string, value: string): boolean =>
|
|
121
|
+
new RegExp(`^\\s*${key}\\s*=\\s*${value}\\s*$`, 'm').test(npmrc)
|
|
122
|
+
const runs =
|
|
123
|
+
manager === 'berry'
|
|
124
|
+
? false
|
|
125
|
+
: manager === 'npm'
|
|
126
|
+
? !set('ignore-scripts', 'true')
|
|
127
|
+
: manager === 'pnpm'
|
|
128
|
+
? !set('enable-pre-post-scripts', 'false') &&
|
|
129
|
+
!/^enablePrePostScripts:\s*false\s*$/m.test(read('pnpm-workspace.yaml') ?? '')
|
|
130
|
+
: true
|
|
131
|
+
return runs ? manager : null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The package manager that owns a directory, and the directory that says so. */
|
|
135
|
+
interface Owner {
|
|
136
|
+
manager: string
|
|
137
|
+
at: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The nearest `packageManager` field or lockfile from `dir` up: `berry` for
|
|
142
|
+
* Yarn 2+, `yarn` for Yarn 1, else the manager's name; none found is npm.
|
|
143
|
+
*/
|
|
144
|
+
function ownerOf(dir: string, memo: Map<string, Owner>): Owner {
|
|
111
145
|
const known = memo.get(dir)
|
|
112
146
|
if (known !== undefined) return known
|
|
113
147
|
const read = (f: string): string | undefined => {
|
|
@@ -131,27 +165,26 @@ function runsScriptHooks(dir: string, memo: Map<string, string | null>): string
|
|
|
131
165
|
else if (existsSync(path.join(dir, 'package-lock.json'))) manager = 'npm'
|
|
132
166
|
else if (['bun.lock', 'bun.lockb'].some((f) => existsSync(path.join(dir, f)))) manager = 'bun'
|
|
133
167
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return answer
|
|
168
|
+
const up = path.dirname(dir)
|
|
169
|
+
const owner =
|
|
170
|
+
manager !== undefined
|
|
171
|
+
? { manager, at: dir }
|
|
172
|
+
: up === dir
|
|
173
|
+
? { manager: 'npm', at: dir }
|
|
174
|
+
: ownerOf(up, memo)
|
|
175
|
+
memo.set(dir, owner)
|
|
176
|
+
return owner
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Yarn 2+ runs a script in its own shell, where `run <script>` is `yarn
|
|
181
|
+
* run <script>`; vx's shell has no `run`, and 23 of berry's scripts
|
|
182
|
+
* (`run build:zip:worker`, `run test:unit packages/…`) mapped to tasks
|
|
183
|
+
* that failed "command not found" (D-92). Spelled out, at the head of
|
|
184
|
+
* each `&&` / `||` / `;` / `|` segment.
|
|
185
|
+
*/
|
|
186
|
+
function berryRun(command: string): string {
|
|
187
|
+
return command.replace(/(^|&&|\|\||[;|(])(\s*)run(?=\s)/g, '$1$2yarn run')
|
|
155
188
|
}
|
|
156
189
|
|
|
157
190
|
const MANIFEST_IMPORT = "import pkg from './package.json' with { type: 'json' }"
|
|
@@ -354,7 +387,7 @@ export function migrateScripts(
|
|
|
354
387
|
outsideDir?: string,
|
|
355
388
|
): MigrationPlan {
|
|
356
389
|
const projects: GeneratedProject[] = []
|
|
357
|
-
const hookMemo = new Map<string,
|
|
390
|
+
const hookMemo = new Map<string, Owner>()
|
|
358
391
|
// The workspace root among members: many of its scripts run the
|
|
359
392
|
// workspace (`npm run build --workspaces`, `pnpm -r build`), and a root
|
|
360
393
|
// task made of one ran every member's build again under `--all`. Since
|
|
@@ -454,6 +487,7 @@ export function migrateScripts(
|
|
|
454
487
|
const scripts = meta === rootMeta ? rootScripts(meta) : scriptsOf(meta)
|
|
455
488
|
const hooksBy = runsScriptHooks(meta.dir, hookMemo)
|
|
456
489
|
const runsHooks = hooksBy !== null
|
|
490
|
+
const berry = ownerOf(meta.dir, hookMemo).manager === 'berry'
|
|
457
491
|
const runnable = Object.keys(scripts).filter(
|
|
458
492
|
(n) => typeof scripts[n] === 'string' && scripts[n] !== '',
|
|
459
493
|
)
|
|
@@ -482,7 +516,7 @@ export function migrateScripts(
|
|
|
482
516
|
if (!isTask(name)) continue
|
|
483
517
|
|
|
484
518
|
const todos: string[] = []
|
|
485
|
-
const own = scripts[name] as string
|
|
519
|
+
const own = berry ? berryRun(scripts[name] as string) : (scripts[name] as string)
|
|
486
520
|
const delegate = delegatedScript(own)
|
|
487
521
|
// npm lifecycle hooks (`prepack`, `prepublishOnly`, …) belong to the
|
|
488
522
|
// package manager and never ride inside a task — `pack` stays alone.
|
|
@@ -81,12 +81,57 @@ const BUILD_OUTPUTS: readonly [RegExp, readonly string[]][] = [
|
|
|
81
81
|
[/\s(?:storybook build|build-storybook)\s/, ['storybook-static/**']],
|
|
82
82
|
]
|
|
83
83
|
|
|
84
|
+
/** A directory a TODO may name: relative, plain, no hidden or tool dir. */
|
|
85
|
+
const OUTPUT_DIR = /^(?:\.\/)?((?![./])[\w@-][\w@./-]*?)\/?$/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The directory the command says it writes, when no tool above names one:
|
|
89
|
+
* an out-dir flag (`tsc --outDir lib`, `babel -d lib`), else the
|
|
90
|
+
* directories it cleans first (`del-cli distribution`, `rimraf dist
|
|
91
|
+
* types`). ky's build writes `distribution/`, and the TODO said `dist/**`
|
|
92
|
+
* (D-90). A file (`tsconfig.tsbuildinfo`) and `node_modules` are no output.
|
|
93
|
+
*/
|
|
94
|
+
function namedOutputs(command: string): string[] | undefined {
|
|
95
|
+
const dir = (w: string | undefined): string | undefined => {
|
|
96
|
+
const m = w === undefined ? null : OUTPUT_DIR.exec(w.replace(/^(["'])(.*)\1$/, '$2'))
|
|
97
|
+
if (m === null) return undefined
|
|
98
|
+
const d = m[1]!
|
|
99
|
+
return /\.[a-z]+$/i.test(d) || d.split('/').includes('node_modules') ? undefined : d
|
|
100
|
+
}
|
|
101
|
+
const flag = /\s(?:--outDir|--out-dir|--outdir|-d)[\s=](\S+)/.exec(` ${command} `)
|
|
102
|
+
const out = dir(flag?.[1])
|
|
103
|
+
if (out !== undefined) return [`${out}/**`]
|
|
104
|
+
for (const segment of command.split(/[;&|()]+/)) {
|
|
105
|
+
const words = segment.trim().split(/\s+/)
|
|
106
|
+
const at =
|
|
107
|
+
words[0] === 'rimraf' || words[0] === 'del-cli' || words[0] === 'del'
|
|
108
|
+
? 1
|
|
109
|
+
: words[0] === 'rm' || (words[0] === 'shx' && words[1] === 'rm')
|
|
110
|
+
? words.indexOf('rm') + 1
|
|
111
|
+
: -1
|
|
112
|
+
if (at < 0) continue
|
|
113
|
+
const args = words.slice(at).filter((w) => !w.startsWith('-'))
|
|
114
|
+
// A dir the command makes again (kit's `rm -rf ./missing_ids && mkdir
|
|
115
|
+
// ./missing_ids && vite build`) is a scratch dir it prepares, not output.
|
|
116
|
+
const made = new Set([...command.matchAll(/\bmkdir\s+(?:-p\s+)?(\S+)/g)].map((m) => dir(m[1])))
|
|
117
|
+
const dirs = args
|
|
118
|
+
.map(dir)
|
|
119
|
+
.filter((d): d is string => d !== undefined)
|
|
120
|
+
.filter((d) => !made.has(d))
|
|
121
|
+
// Every cleaned path a directory vx can name, or the guess is not this.
|
|
122
|
+
return dirs.length > 0 && dirs.length === args.filter((w) => !/\.[a-z]+$/i.test(w)).length
|
|
123
|
+
? dirs.map((d) => `${d}/**`)
|
|
124
|
+
: undefined
|
|
125
|
+
}
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
84
129
|
/** The cache block the task that builds `command` should declare, as a TODO on it. */
|
|
85
130
|
export function cacheTodo(command: string): string {
|
|
86
131
|
// Padded, separators as spaces: `next build && …` and `(next build)` match.
|
|
87
132
|
const words = ` ${command.replace(/[;&|()]/g, ' ')} `
|
|
88
133
|
const hit = BUILD_OUTPUTS.find(([re]) => re.test(words))
|
|
89
|
-
const outputs = (hit?.[1] ?? ['dist/**']).map((o) => `'${o}'`).join(', ')
|
|
134
|
+
const outputs = (hit?.[1] ?? namedOutputs(command) ?? ['dist/**']).map((o) => `'${o}'`).join(', ')
|
|
90
135
|
return `cache: add \`cache: { inputs: { files: ['src/**'] }, outputs: { files: [${outputs}] } }\` with this package's real inputs and outputs — without it the task always runs and every file here, what it writes included, folds into the key its dependents fold; a block with EMPTY outputs would be a cached no-op, not an uncached task`
|
|
91
136
|
}
|
|
92
137
|
|