@vzn/vx 0.0.18 → 0.0.21
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/LICENSE +1 -1
- package/README.md +99 -65
- package/package.json +10 -20
- package/src/bin.ts +12 -2
- package/src/cache/archive.ts +120 -18
- package/src/cache/cache.ts +313 -1324
- package/src/cache/chained-cache.ts +0 -8
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +16 -7
- package/src/cache/inputs.ts +246 -733
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +12 -17
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +39 -11
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +64 -36
- package/src/cli/index.ts +54 -15
- package/src/cli/info.ts +120 -91
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +38 -6
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +14 -3
- package/src/cli/plugin-commands.ts +15 -8
- package/src/cli/run.ts +84 -302
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +2 -1
- package/src/cli/watch.ts +647 -97
- package/src/cli/why.ts +44 -14
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +156 -136
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +11 -26
- package/src/exec/index.ts +6 -2
- package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
- package/src/exec/runner.ts +57 -18
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +582 -370
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +1 -9
- package/src/graph/scheduler.ts +158 -203
- package/src/graph/task-graph.ts +2 -2
- package/src/index.ts +97 -102
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +3 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/events.ts +42 -8
- package/src/orchestrator/execute-task.ts +145 -554
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/framed-output.ts +41 -36
- package/src/orchestrator/history.ts +118 -98
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +35 -4
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +13 -22
- package/src/orchestrator/metrics.ts +53 -20
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +34 -31
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plugin-host.ts +195 -38
- package/src/orchestrator/plugin.ts +162 -9
- package/src/orchestrator/prepare.ts +77 -132
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-artifacts.ts +25 -3
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +2 -2
- package/src/orchestrator/run.ts +246 -597
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +60 -3
- package/src/orchestrator/status-line.ts +2 -1
- package/src/orchestrator/summary.ts +68 -25
- package/src/orchestrator/task-hash.ts +20 -18
- package/src/orchestrator/task-log-buffer.ts +8 -11
- package/src/orchestrator/telemetry-host.ts +11 -1
- package/src/orchestrator/telemetry.ts +3 -17
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/index.ts +11 -3
- package/src/util/paths.ts +23 -2
- package/src/util/settle.ts +19 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +111 -11
- package/src/workspace/config-cache.ts +11 -2
- package/src/workspace/config-eval.ts +11 -4
- package/src/workspace/config-imports.ts +5 -4
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +43 -6
- package/src/workspace/fingerprint.ts +37 -3
- package/src/workspace/index.ts +30 -3
- package/src/workspace/lockfile.ts +12 -7
- package/src/{cli → workspace}/migrate-scripts.ts +26 -19
- package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
- package/src/workspace/package-graph.ts +98 -28
- package/src/workspace/project-loader.ts +76 -746
- package/src/workspace/workspace.ts +98 -13
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/util/settle.ts
CHANGED
|
@@ -27,6 +27,25 @@ export function teardownTimeoutMs(): number {
|
|
|
27
27
|
return DEFAULT_TIMEOUT_MS
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The SIGTERM→SIGKILL grace a run grants a child that ignores SIGTERM: the
|
|
32
|
+
* one-shot timeout escalation (`exec/runner.ts`) and the end-of-run
|
|
33
|
+
* persistent shutdown (`orchestrator/persistent.ts`) share it. Two seconds
|
|
34
|
+
* is generous for a real server's cleanup and is what every run waits when
|
|
35
|
+
* a child wedges. `VX_KILL_GRACE_MS` overrides it, read per call and bounded
|
|
36
|
+
* like the teardown deadline (out of range falls back to the default): the
|
|
37
|
+
* tests that prove the escalation used to wait the full two seconds each,
|
|
38
|
+
* a third of the suite's wall time for a claim a 200 ms grace proves too.
|
|
39
|
+
*/
|
|
40
|
+
export function killGraceMs(defaultMs: number): number {
|
|
41
|
+
const raw = process.env['VX_KILL_GRACE_MS']
|
|
42
|
+
if (raw !== undefined && /^[0-9]+$/.test(raw)) {
|
|
43
|
+
const n = Number(raw)
|
|
44
|
+
if (n > 0 && n <= MAX_TIMEOUT_MS) return n
|
|
45
|
+
}
|
|
46
|
+
return defaultMs
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
/**
|
|
31
50
|
* Await `p`, giving up after `ms`. Returns true when `p` settled first,
|
|
32
51
|
* false when the deadline won — the caller decides whether a lost result
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbs the CLI dispatcher (`cli/index.ts`) owns; `stats` is a
|
|
3
|
+
* deprecated alias of `info` and stays out of hints but is still matched
|
|
4
|
+
* first. Lives in util rather than cli because the workspace VALIDATOR
|
|
5
|
+
* refuses a plugin verb that names one of these — a verb core matches
|
|
6
|
+
* first could never run — and the workspace module cannot import cli.
|
|
7
|
+
*/
|
|
8
|
+
export const CORE_VERBS = [
|
|
9
|
+
'run',
|
|
10
|
+
'watch',
|
|
11
|
+
'cache',
|
|
12
|
+
'lock',
|
|
13
|
+
'init',
|
|
14
|
+
'upgrade',
|
|
15
|
+
'show',
|
|
16
|
+
'info',
|
|
17
|
+
'why',
|
|
18
|
+
'last',
|
|
19
|
+
'completions',
|
|
20
|
+
'help',
|
|
21
|
+
'version',
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
/** Every word the dispatcher matches before consulting plugins. */
|
|
25
|
+
export const DISPATCHED_VERBS: readonly string[] = [...CORE_VERBS, 'stats']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Verbs core owned once and a package owns now. Not core verbs: a plugin
|
|
29
|
+
* may declare them, and the dispatcher prints the
|
|
30
|
+
* pointer only when no declared plugin claimed the name.
|
|
31
|
+
*/
|
|
32
|
+
export const MOVED_VERBS: Readonly<Record<string, string>> = {
|
|
33
|
+
migrate:
|
|
34
|
+
'vx migrate moved to @vzn/vx-migrate: run `bunx @vzn/vx-migrate` (turbo.json or an Nx ' +
|
|
35
|
+
'project graph → vx.config.ts); `vx init` reads package.json scripts',
|
|
36
|
+
prune:
|
|
37
|
+
'vx prune was removed (2026-09-11): the Docker-subset verb is not shipped; copy the ' +
|
|
38
|
+
'workspace and `--filter` the build instead',
|
|
39
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// `--affected` — git-relative project selection.
|
|
2
2
|
//
|
|
3
3
|
// Resolves the set of project names whose files have changed since a
|
|
4
|
-
// given git ref
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// We use `git diff --name-only <since>` which by default compares the
|
|
9
|
-
// `<since>` commit to the working tree — i.e. commits-from-`<since>`
|
|
10
|
-
// + index + unstaged. Matches Turbo's `[<since>]` filter semantics.
|
|
4
|
+
// given git ref: `git diff --name-only <merge-base(since, HEAD)>`, which
|
|
5
|
+
// compares that commit to the working tree — commits on this branch +
|
|
6
|
+
// index + unstaged — so it captures everything you touched and nothing
|
|
7
|
+
// the base branch moved on with. Matches Turbo's `[<since>]` semantics.
|
|
11
8
|
|
|
12
9
|
import path from 'node:path'
|
|
13
10
|
import { UserError } from '../util/index.js'
|
|
@@ -37,6 +34,23 @@ export interface AffectedArgs {
|
|
|
37
34
|
* — never invokes it and pays nothing.
|
|
38
35
|
*/
|
|
39
36
|
workspaceGlobOwners?: (orphanPaths: readonly string[]) => Promise<Iterable<string>>
|
|
37
|
+
/**
|
|
38
|
+
* The fingerprint files a plugin claims (`VxPlugin.fingerprint`) and its
|
|
39
|
+
* answer for a change to one. Resolved lazily: loading the workspace
|
|
40
|
+
* file costs an evaluation, and a diff that touches no fingerprint file
|
|
41
|
+
* — the common one — never needs it.
|
|
42
|
+
*/
|
|
43
|
+
fingerprintClaims?: () => Promise<FingerprintClaims>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FingerprintClaims {
|
|
47
|
+
readonly files: ReadonlySet<string>
|
|
48
|
+
/** Project names a change to a claimed file affects; `undefined` = all. */
|
|
49
|
+
affected(change: {
|
|
50
|
+
file: string
|
|
51
|
+
before: Uint8Array | null
|
|
52
|
+
after: Uint8Array | null
|
|
53
|
+
}): Promise<ReadonlySet<string> | undefined>
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
/**
|
|
@@ -45,7 +59,26 @@ export interface AffectedArgs {
|
|
|
45
59
|
* or `<since>` doesn't resolve to a commit.
|
|
46
60
|
*/
|
|
47
61
|
export async function affectedProjects(args: AffectedArgs): Promise<Set<string>> {
|
|
62
|
+
// The base reaches git as an argument, never through a shell, so `$(…)`
|
|
63
|
+
// is opaque — but an option-like value is not: `--output=<path>` is a
|
|
64
|
+
// real `git diff` option and an arbitrary file write. This is a security
|
|
65
|
+
// boundary, so it is a check that knows it is one, before any spawn, and
|
|
66
|
+
// every git call below also ends its options (`--end-of-options`) so a
|
|
67
|
+
// second caller cannot lose the guard by accident.
|
|
68
|
+
if (args.since.length === 0 || args.since.startsWith('-')) {
|
|
69
|
+
throw new UserError(
|
|
70
|
+
`git ref "${args.since}" is not a ref: a base cannot be empty or start with "-".`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
48
73
|
await verifyRef(args.workspaceRoot, args.since)
|
|
74
|
+
// Diff from the MERGE BASE of `since` and HEAD, not from `since` itself:
|
|
75
|
+
// on a branch whose base has moved on, `git diff <base>` reports every
|
|
76
|
+
// file OTHER people changed on the base (over-selection that defeats a
|
|
77
|
+
// CI `--affected`), and hides your own edit when the base later landed
|
|
78
|
+
// byte-identical content. Turbo and Nx both diff from the merge base;
|
|
79
|
+
// when there is none (unrelated histories, a detached probe) the ref
|
|
80
|
+
// itself is the base, as before.
|
|
81
|
+
const base = await mergeBase(args.workspaceRoot, args.since)
|
|
49
82
|
|
|
50
83
|
const [diffed, untracked] = await Promise.all([
|
|
51
84
|
// `--no-renames` is crucial for project-affected detection: with
|
|
@@ -73,7 +106,8 @@ export async function affectedProjects(args: AffectedArgs): Promise<Set<string>>
|
|
|
73
106
|
'--relative',
|
|
74
107
|
'--name-only',
|
|
75
108
|
'-z',
|
|
76
|
-
|
|
109
|
+
'--end-of-options',
|
|
110
|
+
base,
|
|
77
111
|
]),
|
|
78
112
|
// `git diff` never reports untracked-but-not-ignored files, but input
|
|
79
113
|
// enumeration does (`git ls-files --cached --others --exclude-standard`),
|
|
@@ -96,11 +130,31 @@ export async function affectedProjects(args: AffectedArgs): Promise<Set<string>>
|
|
|
96
130
|
// principle: "input hashing sees it, so `--affected` must too."
|
|
97
131
|
//
|
|
98
132
|
// Selection is not hashed, so widening it here changes no cache key.
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
//
|
|
134
|
+
// A file a plugin CLAIMS is the exception: the key folds what the plugin
|
|
135
|
+
// says per project, so selection asks the plugin the same question, with
|
|
136
|
+
// the bytes at the base ref and in the working tree. Its answer is
|
|
137
|
+
// unioned with the path-owned projects below; only "cannot tell" widens.
|
|
138
|
+
const fingerprintChanged = changed.filter((p) => FINGERPRINT_SET.has(p))
|
|
139
|
+
const claimedOwned = new Set<string>()
|
|
140
|
+
if (fingerprintChanged.length > 0) {
|
|
141
|
+
const claims = args.fingerprintClaims === undefined ? undefined : await args.fingerprintClaims()
|
|
142
|
+
for (const file of fingerprintChanged) {
|
|
143
|
+
if (claims === undefined || !claims.files.has(file)) {
|
|
144
|
+
return new Set(args.projects.map((p) => p.name))
|
|
145
|
+
}
|
|
146
|
+
const answer = await claims.affected({
|
|
147
|
+
file,
|
|
148
|
+
before: await gitBytesAt(args.workspaceRoot, base, file),
|
|
149
|
+
after: await bytesOrNull(path.join(args.workspaceRoot, file)),
|
|
150
|
+
})
|
|
151
|
+
if (answer === undefined) return new Set(args.projects.map((p) => p.name))
|
|
152
|
+
for (const name of answer) claimedOwned.add(name)
|
|
153
|
+
}
|
|
101
154
|
}
|
|
102
155
|
|
|
103
156
|
const { owned, orphans } = projectsContaining(args.workspaceRoot, changed, args.projects)
|
|
157
|
+
for (const name of claimedOwned) owned.add(name)
|
|
104
158
|
|
|
105
159
|
// THIRD CHANNEL: a project whose `vx.config.*` IMPORTS a changed file.
|
|
106
160
|
// Resolved-config hashing folds those values into the key, so the same
|
|
@@ -149,6 +203,39 @@ export function workspaceGlobsMatch(globs: readonly string[], rel: string): bool
|
|
|
149
203
|
*/
|
|
150
204
|
const FINGERPRINT_SET: ReadonlySet<string> = new Set(WORKSPACE_FINGERPRINT_FILES)
|
|
151
205
|
|
|
206
|
+
/**
|
|
207
|
+
* A root file's bytes at `ref`, or null when the ref has no such file.
|
|
208
|
+
* `./` anchors the path to the cwd (the workspace root) rather than the
|
|
209
|
+
* repository root, for a workspace that is a subdirectory of its repo.
|
|
210
|
+
*/
|
|
211
|
+
async function gitBytesAt(
|
|
212
|
+
workspaceRoot: string,
|
|
213
|
+
ref: string,
|
|
214
|
+
file: string,
|
|
215
|
+
): Promise<Uint8Array | null> {
|
|
216
|
+
const proc = Bun.spawn({
|
|
217
|
+
cmd: ['git', 'show', '--end-of-options', `${ref}:./${file}`],
|
|
218
|
+
cwd: workspaceRoot,
|
|
219
|
+
stdout: 'pipe',
|
|
220
|
+
stderr: 'pipe',
|
|
221
|
+
})
|
|
222
|
+
const [bytes, stderr, exit] = await Promise.all([
|
|
223
|
+
new Response(proc.stdout).bytes(),
|
|
224
|
+
new Response(proc.stderr).text(),
|
|
225
|
+
proc.exited,
|
|
226
|
+
])
|
|
227
|
+
if (exit === 0) return bytes
|
|
228
|
+
// "exists on disk but not in <ref>" and "path does not exist in <ref>"
|
|
229
|
+
// are the file being absent at the ref; anything else is git failing.
|
|
230
|
+
if (/does not exist in|exists on disk, but not in/.test(stderr)) return null
|
|
231
|
+
throw new UserError(`git show ${ref}:./${file} failed (exit ${exit}): ${stderr.trim()}`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function bytesOrNull(file: string): Promise<Uint8Array | null> {
|
|
235
|
+
const f = Bun.file(file)
|
|
236
|
+
return (await f.exists()) ? await f.bytes() : null
|
|
237
|
+
}
|
|
238
|
+
|
|
152
239
|
/** Run a NUL-separated path-listing git command from the workspace root. */
|
|
153
240
|
async function gitPaths(workspaceRoot: string, cmd: string[]): Promise<string[]> {
|
|
154
241
|
const proc = Bun.spawn({
|
|
@@ -184,9 +271,22 @@ export async function defaultAffectedBase(workspaceRoot: string): Promise<string
|
|
|
184
271
|
return 'HEAD~1'
|
|
185
272
|
}
|
|
186
273
|
|
|
274
|
+
/** `git merge-base <ref> HEAD`, or `ref` itself when the two share no ancestor. */
|
|
275
|
+
async function mergeBase(workspaceRoot: string, ref: string): Promise<string> {
|
|
276
|
+
const proc = Bun.spawn({
|
|
277
|
+
cmd: ['git', 'merge-base', '--end-of-options', ref, 'HEAD'],
|
|
278
|
+
cwd: workspaceRoot,
|
|
279
|
+
stdout: 'pipe',
|
|
280
|
+
stderr: 'pipe',
|
|
281
|
+
})
|
|
282
|
+
const [out, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited])
|
|
283
|
+
const sha = out.trim()
|
|
284
|
+
return exit === 0 && sha.length > 0 ? sha : ref
|
|
285
|
+
}
|
|
286
|
+
|
|
187
287
|
async function verifyRef(workspaceRoot: string, ref: string): Promise<void> {
|
|
188
288
|
const proc = Bun.spawnSync({
|
|
189
|
-
cmd: ['git', 'rev-parse', '--verify', '--quiet', ref],
|
|
289
|
+
cmd: ['git', 'rev-parse', '--verify', '--quiet', '--end-of-options', ref],
|
|
190
290
|
cwd: workspaceRoot,
|
|
191
291
|
stdout: 'pipe',
|
|
192
292
|
stderr: 'pipe',
|
|
@@ -52,6 +52,12 @@ export interface ConfigEvalStore {
|
|
|
52
52
|
* shadow, is never indexed.
|
|
53
53
|
*/
|
|
54
54
|
hashFile?(file: string): Promise<string>
|
|
55
|
+
/**
|
|
56
|
+
* `hashFile` over many paths with one memo query; a path that cannot be
|
|
57
|
+
* stat'ed is absent. The warm load identifies every indexed closure's
|
|
58
|
+
* files through this in a single call when the store offers it.
|
|
59
|
+
*/
|
|
60
|
+
hashFiles?(files: readonly string[]): Promise<Map<string, string>>
|
|
55
61
|
getConfigClosures?(configPaths: readonly string[]): Map<string, string[]>
|
|
56
62
|
putConfigClosure?(configPath: string, files: readonly string[]): void
|
|
57
63
|
getConfigEval(key: string): string | null
|
|
@@ -95,9 +101,12 @@ const MAX_CLOSURE_FILES = 32
|
|
|
95
101
|
// `global` and `self` are live objects in Bun (aliases of `globalThis`), so
|
|
96
102
|
// a computed `global['proc' + 'ess']` reaches process without ever
|
|
97
103
|
// spelling it; `Temporal` is a clock. All three were CACHED AS PURE before
|
|
98
|
-
// they were listed (2026-09-03).
|
|
104
|
+
// they were listed (2026-09-03). `constructor` reaches `Function` through a
|
|
105
|
+
// property name (`({}).constructor.constructor('return process')()`, the
|
|
106
|
+
// body hidden in a literal the strip removes) and `localeCompare` answers
|
|
107
|
+
// by the host locale; both listed 2026-09-09.
|
|
99
108
|
const IMPURE_RE =
|
|
100
|
-
/\b(?:process|Bun|globalThis|global|self|fetch|Date|Temporal|Intl|crypto|performance|navigator|require|eval|Function|await|toLocale\w*)\b|import\s*\.\s*meta|Math\s*\.\s*random|\bimport\s*\(/
|
|
109
|
+
/\b(?:process|Bun|globalThis|global|self|fetch|Date|Temporal|Intl|crypto|performance|navigator|require|eval|Function|constructor|localeCompare|await|toLocale\w*)\b|import\s*\.\s*meta|Math\s*\.\s*random|\bimport\s*\(/
|
|
101
110
|
|
|
102
111
|
// Static `import … from '…'` / `export … from '…'` / `import '…'` forms.
|
|
103
112
|
// `[^;'"]*?` spans newlines, so multi-line specifier lists match.
|
|
@@ -52,6 +52,10 @@ self.onmessage = async (e) => {
|
|
|
52
52
|
name: err?.name ?? 'Error',
|
|
53
53
|
message: err?.message ?? String(err),
|
|
54
54
|
stack: err?.stack ?? null,
|
|
55
|
+
position:
|
|
56
|
+
err?.position && typeof err.position === 'object'
|
|
57
|
+
? { file: err.position.file, line: err.position.line, column: err.position.column }
|
|
58
|
+
: null,
|
|
55
59
|
})
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -66,6 +70,8 @@ interface WorkerReply {
|
|
|
66
70
|
name: string
|
|
67
71
|
message: string
|
|
68
72
|
stack: string | null
|
|
73
|
+
/** A transpile error's location — `BuildMessage.position`, trimmed to what the loader reads. */
|
|
74
|
+
position: { file?: string; line?: number; column?: number } | null
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
interface Pending {
|
|
@@ -136,12 +142,13 @@ function acquireWorker(): Worker {
|
|
|
136
142
|
p.resolve(msg.json)
|
|
137
143
|
return
|
|
138
144
|
}
|
|
139
|
-
// Rebuild the error the config actually threw. Name, message
|
|
140
|
-
//
|
|
141
|
-
// text it would from an in-process import.
|
|
142
|
-
const err = new Error(msg.message)
|
|
145
|
+
// Rebuild the error the config actually threw. Name, message, stack
|
|
146
|
+
// and a transpile error's position all survive the hop, so a broken
|
|
147
|
+
// config reports the same text it would from an in-process import.
|
|
148
|
+
const err = new Error(msg.message) as Error & { position?: WorkerReply['position'] }
|
|
143
149
|
err.name = msg.name
|
|
144
150
|
if (msg.stack !== null) err.stack = msg.stack
|
|
151
|
+
if (msg.position !== null) err.position = msg.position
|
|
145
152
|
p.reject(err)
|
|
146
153
|
}
|
|
147
154
|
w.onerror = (event: ErrorEvent): void => {
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
// - RELATIVE specifiers only. A bare specifier is a package; it moves when
|
|
20
20
|
// the lockfile moves, which the workspace fingerprint already covers.
|
|
21
21
|
// - Descend only through files owned by NO project. A config reaching into
|
|
22
|
-
// another project (
|
|
23
|
-
//
|
|
24
|
-
// would drag substantially all of
|
|
25
|
-
// containment channel already selects the project that
|
|
22
|
+
// another project (say a site's `vx.config.ts` importing
|
|
23
|
+
// `../core/src/index.ts`) records that edge and STOPS there — following
|
|
24
|
+
// it would drag substantially all of that project's `src/` into the
|
|
25
|
+
// closure, and the containment channel already selects the project that
|
|
26
|
+
// owns it.
|
|
26
27
|
|
|
27
28
|
import { realpath } from 'node:fs/promises'
|
|
28
29
|
import path from 'node:path'
|