@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { UserError } from '../util/index.js'
|
|
4
|
+
import type { ProjectEntry } from './workspace.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Each unescaped `[`…`]` class in a segment, or null when the segment holds
|
|
8
|
+
* none or another wildcard (`*`, `?`, `{`) a literal reading cannot settle.
|
|
9
|
+
*/
|
|
10
|
+
function classes(segment: string): [number, number][] | null {
|
|
11
|
+
const found: [number, number][] = []
|
|
12
|
+
for (let i = 0; i < segment.length; i++) {
|
|
13
|
+
const c = segment[i]!
|
|
14
|
+
if (c === '\\') i++
|
|
15
|
+
else if (c === '*' || c === '?' || c === '{') return null
|
|
16
|
+
else if (c === '[') {
|
|
17
|
+
const end = segment.indexOf(']', i + 2)
|
|
18
|
+
if (end === -1) return null
|
|
19
|
+
found.push([i, end])
|
|
20
|
+
i = end
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return found.length > 0 ? found : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const unescape = (s: string): string => s.replace(/\\(.)/g, '$1')
|
|
27
|
+
const literalSegment = (s: string): boolean => !/(?<!\\)[*?{[]/.test(s)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The glob with its first class segment escaped, when that segment taken
|
|
31
|
+
* literally names a path under `base`; else null. Only a literal prefix is
|
|
32
|
+
* followed: `**\/[id]` would need a walk, and this runs at `vx lock`.
|
|
33
|
+
*/
|
|
34
|
+
function literalBracketFix(glob: string, base: string): string | null {
|
|
35
|
+
const neg = glob.startsWith('!') ? '!' : ''
|
|
36
|
+
const segments = glob.slice(neg.length).split('/')
|
|
37
|
+
const prefix: string[] = []
|
|
38
|
+
for (const [i, s] of segments.entries()) {
|
|
39
|
+
if (literalSegment(s)) {
|
|
40
|
+
prefix.push(unescape(s))
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const spans = classes(s)
|
|
44
|
+
if (spans === null) return null
|
|
45
|
+
if (!existsSync(path.join(base, ...prefix, unescape(s)))) return null
|
|
46
|
+
let fixed = s
|
|
47
|
+
for (const [open, close] of spans.toReversed())
|
|
48
|
+
fixed = `${fixed.slice(0, open)}\\[${fixed.slice(open + 1, close)}\\]${fixed.slice(close + 1)}`
|
|
49
|
+
return neg + [...segments.slice(0, i), fixed, ...segments.slice(i + 1)].join('/')
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A bare `app/[id]/**` is the class matching `app/i` and `app/d`, never the
|
|
56
|
+
* route directory `app/[id]` (schema.md). Beside another glob it matched
|
|
57
|
+
* nothing with no warning, so an edit under the route replayed a stale hit
|
|
58
|
+
* (vercel/turborepo#14850 reads an existing bracketed path literally). vx
|
|
59
|
+
* keeps one reading and refuses the glob when that path exists. Checked by
|
|
60
|
+
* `vx lock` only: a run trusts the lock.
|
|
61
|
+
*/
|
|
62
|
+
export function checkBracketLiterals(root: string, projects: Iterable<ProjectEntry>): void {
|
|
63
|
+
for (const p of projects) {
|
|
64
|
+
for (const [task, t] of Object.entries(p.config.tasks ?? {})) {
|
|
65
|
+
const lists: [string, readonly string[] | undefined, string][] = [
|
|
66
|
+
['inputs.files', t.inputs?.files, p.dir],
|
|
67
|
+
['inputs.workspaceFiles', t.inputs?.workspaceFiles, root],
|
|
68
|
+
['cache.outputs.files', t.cache?.outputs?.files, p.dir],
|
|
69
|
+
['cache.outputs.workspaceFiles', t.cache?.outputs?.workspaceFiles, root],
|
|
70
|
+
]
|
|
71
|
+
for (const [field, globs, base] of lists) {
|
|
72
|
+
for (const glob of globs ?? []) {
|
|
73
|
+
if (!glob.includes('[')) continue
|
|
74
|
+
const fixed = literalBracketFix(glob, base)
|
|
75
|
+
if (fixed === null) continue
|
|
76
|
+
throw new UserError(
|
|
77
|
+
`${p.name}#${task} ${field}: "${glob}" reads its brackets as a character class, ` +
|
|
78
|
+
`so it never matches the path of that name, which exists: write "${fixed}"`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A directory at where it leads, spelled under the workspace root: its own
|
|
2
|
+
// module, as discovery and the filters both ask it and filter.ts is one
|
|
3
|
+
// discovery imports.
|
|
4
|
+
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { isOutOfFds, realPath } from '../util/index.js'
|
|
7
|
+
|
|
8
|
+
const realRoots = new Map<string, string>()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `dir` at the directory it leads to, spelled under `root` as given (a
|
|
12
|
+
* root reached through a link, as macOS's temp dir is, keeps its
|
|
13
|
+
* spelling); `dir` itself when that directory lies outside the root; null
|
|
14
|
+
* when `dir` is not there. A workspace glob over linked packages (pnpm's
|
|
15
|
+
* `widgets/**`, `widgets/a -> ../submodules/a`) names the link, and git
|
|
16
|
+
* lists the config at the real directory: matched by spelling, one
|
|
17
|
+
* directory was two projects, the config-less twin at the link taking
|
|
18
|
+
* every edge.
|
|
19
|
+
*/
|
|
20
|
+
export function canonicalDir(root: string, dir: string): string | null {
|
|
21
|
+
const real = realOrNull(dir)
|
|
22
|
+
if (real === null) return null
|
|
23
|
+
let realRoot = realRoots.get(root)
|
|
24
|
+
if (realRoot === undefined) realRoots.set(root, (realRoot = realOrNull(root) ?? root))
|
|
25
|
+
const rel = path.relative(realRoot, real)
|
|
26
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return dir
|
|
27
|
+
return path.join(root, rel)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function realOrNull(p: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
return realPath(p)
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (isOutOfFds(err)) throw err
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// Every run evaluates `.vx/workspace.ts`, and with it every plugin package
|
|
2
|
+
// it imports: Bun parses and compiles that TypeScript on each run, and a
|
|
3
|
+
// plugin's functions compile again on their first call, while core itself
|
|
4
|
+
// runs from the binary's bytecode. On a solid-sized workspace that was
|
|
5
|
+
// ~4 ms of a ~60 ms warm run (the `discover` stage alone 9 → 5 ms,
|
|
6
|
+
// 2026-10-22). So the config's module graph is bundled once into CommonJS
|
|
7
|
+
// with Bun's bytecode under `.vx/cache/compiled/`, and a later run whose
|
|
8
|
+
// every input still has the bytes it was built from requires that instead.
|
|
9
|
+
//
|
|
10
|
+
// The bundle is built from the bytes it records, file by file, and is used
|
|
11
|
+
// only while each recorded file still hashes the same and each import still
|
|
12
|
+
// resolves to the file it did (a relative one too: `./preset` finds a new
|
|
13
|
+
// `preset.ts` before the bundled `preset.js`); anything else loads the
|
|
14
|
+
// source as before and builds again. A graph the bundle could run differently is
|
|
15
|
+
// never built: an `import.meta` field beyond its location, a `require` or
|
|
16
|
+
// `import()` of a computed specifier (relative to the bundle, not the
|
|
17
|
+
// file), top-level await, a CommonJS config, a file this process already
|
|
18
|
+
// evaluated (a bundled copy would be a second state; run from source, core
|
|
19
|
+
// itself). The bundle goes
|
|
20
|
+
// into `.vx/cache` only once a run made it, so a reading verb on a fresh
|
|
21
|
+
// workspace writes nothing (item 900). `.vx/cache` is trusted as the rest
|
|
22
|
+
// of the cache is: what it holds is restored into the tree too.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readdirSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
realpathSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
} from 'node:fs'
|
|
34
|
+
import { writeFile } from 'node:fs/promises'
|
|
35
|
+
import { createRequire } from 'node:module'
|
|
36
|
+
import path from 'node:path'
|
|
37
|
+
import { xxh3hex } from '../util/index.js'
|
|
38
|
+
|
|
39
|
+
/** Bumped when what a bundle holds, or how it is checked, changes. */
|
|
40
|
+
const COMPILED_VERSION = 2
|
|
41
|
+
|
|
42
|
+
/** What `import.meta` becomes in a bundled file: an identifier no source spells. */
|
|
43
|
+
const META = '__vxImportMeta'
|
|
44
|
+
const META_FIELDS = new Set(['dir', 'dirname', 'path', 'filename', 'file', 'url', 'main'])
|
|
45
|
+
const META_USE = new RegExp(`\\b${META}\\b(\\.[\\w$]+)?`, 'g')
|
|
46
|
+
const COMPUTED_LOAD = /\b(?:require|import)\(\s*(?![\s'"])|\brequire\.resolve\b/
|
|
47
|
+
|
|
48
|
+
interface Manifest {
|
|
49
|
+
version: number
|
|
50
|
+
/** The config file by real path; its bytes are in the bundle's name, so it is not read again (principle 9). */
|
|
51
|
+
entry: string
|
|
52
|
+
/** Every file the bundle was built from, by real path, and its bytes' hash. */
|
|
53
|
+
inputs: Record<string, string>
|
|
54
|
+
/** Each import: the importing file's directory, the specifier, the real path it resolved to. */
|
|
55
|
+
imports: [string, string, string][]
|
|
56
|
+
/** The graph cannot be bundled faithfully: load the source while the inputs stay as they are. */
|
|
57
|
+
skip?: true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compiledDir(root: string): string {
|
|
61
|
+
return path.join(root, '.vx', 'cache', 'compiled')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nameOf(configPath: string, bytes: Uint8Array): string {
|
|
65
|
+
return `workspace-${xxh3hex(`${COMPILED_VERSION}\0${Bun.revision}\0${configPath}\0${xxh3hex(bytes)}`)}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fresh(manifest: Manifest): boolean {
|
|
69
|
+
for (const [file, hash] of Object.entries(manifest.inputs)) {
|
|
70
|
+
if (file === manifest.entry) continue
|
|
71
|
+
let bytes: Uint8Array
|
|
72
|
+
try {
|
|
73
|
+
bytes = readFileSync(file)
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
if (xxh3hex(bytes) !== hash) return false
|
|
78
|
+
}
|
|
79
|
+
for (const [from, spec, to] of manifest.imports) {
|
|
80
|
+
try {
|
|
81
|
+
if (realpathSync(Bun.resolveSync(spec, from)) !== to) return false
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The workspace config's module namespace from its bundle: `undefined` when
|
|
91
|
+
* there is none for these bytes or an input moved (`build` then makes one),
|
|
92
|
+
* `null` when the graph is one no bundle runs faithfully.
|
|
93
|
+
*/
|
|
94
|
+
export function loadCompiled(
|
|
95
|
+
root: string,
|
|
96
|
+
configPath: string,
|
|
97
|
+
bytes: Uint8Array,
|
|
98
|
+
): { default?: unknown } | null | undefined {
|
|
99
|
+
// A repeat load in this process (the run after the CLI's selection pass,
|
|
100
|
+
// a `vx watch` cycle) of the same bytes: the loader already refused an
|
|
101
|
+
// import edited since the first, so the answer stands (principle 9).
|
|
102
|
+
const seen = answered.get(configPath)
|
|
103
|
+
if (seen !== undefined && Buffer.from(seen.bytes).equals(bytes)) return seen.ns
|
|
104
|
+
const name = nameOf(configPath, bytes)
|
|
105
|
+
const dir = compiledDir(root)
|
|
106
|
+
let manifest: Manifest
|
|
107
|
+
try {
|
|
108
|
+
manifest = JSON.parse(readFileSync(path.join(dir, `${name}.json`), 'utf8')) as Manifest
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
if (manifest.version !== COMPILED_VERSION || !fresh(manifest)) return undefined
|
|
113
|
+
const ns =
|
|
114
|
+
manifest.skip === true
|
|
115
|
+
? null
|
|
116
|
+
: (createRequire(path.join(dir, 'index.js'))(path.join(dir, `${name}.js`)) as {
|
|
117
|
+
default?: unknown
|
|
118
|
+
})
|
|
119
|
+
answered.set(configPath, { bytes, ns })
|
|
120
|
+
return ns
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** What `loadCompiled` answered for each config, by the bytes it was asked with. */
|
|
124
|
+
const answered = new Map<string, { bytes: Uint8Array; ns: { default?: unknown } | null }>()
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The files this process has evaluated as modules. One the config imports
|
|
128
|
+
* by path (run from source, core's own `src/index.ts`) is shared with the
|
|
129
|
+
* process, and a bundle would run a second copy of its state.
|
|
130
|
+
*/
|
|
131
|
+
export function loadedModules(root: string): Set<string> {
|
|
132
|
+
return new Set(Object.keys(createRequire(path.join(root, 'index.js')).cache))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The loader Bun reads `file` with, or null for a file the bundle must not hold. */
|
|
136
|
+
function loaderOf(file: string): 'ts' | 'tsx' | 'js' | 'jsx' | null {
|
|
137
|
+
const ext = path.extname(file)
|
|
138
|
+
if (ext === '.ts' || ext === '.mts') return 'ts'
|
|
139
|
+
if (ext === '.tsx') return 'tsx'
|
|
140
|
+
if (ext === '.js' || ext === '.mjs') return 'js'
|
|
141
|
+
if (ext === '.jsx') return 'jsx'
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Bundle the workspace config's graph for the next run. A speed-up only:
|
|
147
|
+
* every failure leaves the next run loading the source.
|
|
148
|
+
*/
|
|
149
|
+
export async function buildCompiled(
|
|
150
|
+
root: string,
|
|
151
|
+
configPath: string,
|
|
152
|
+
bytes: Uint8Array,
|
|
153
|
+
/** `loadedModules()` before the config's source load. */
|
|
154
|
+
loaded: ReadonlySet<string>,
|
|
155
|
+
/** What that load evaluated: a repeat load in this process gets it, not a bundle or a second build. */
|
|
156
|
+
ns: { default?: unknown },
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
answered.set(configPath, { bytes, ns })
|
|
159
|
+
const dir = compiledDir(root)
|
|
160
|
+
if (!existsSync(path.dirname(dir))) return
|
|
161
|
+
const name = nameOf(configPath, bytes)
|
|
162
|
+
const entryFile = realpathSync(configPath)
|
|
163
|
+
const inputs: Record<string, string> = {}
|
|
164
|
+
let faithful = true
|
|
165
|
+
const transpilers = new Map<string, Bun.Transpiler>()
|
|
166
|
+
const tmp = path.join(dir, `.tmp-${process.pid}-${Date.now()}`)
|
|
167
|
+
try {
|
|
168
|
+
mkdirSync(tmp, { recursive: true })
|
|
169
|
+
// Ignored by git under a cache dir elsewhere, where no `.vx/.gitignore` is written.
|
|
170
|
+
try {
|
|
171
|
+
writeFileSync(path.join(dir, '.gitignore'), '*\n', { flag: 'wx' })
|
|
172
|
+
} catch {
|
|
173
|
+
// there already: the user's or an earlier run's
|
|
174
|
+
}
|
|
175
|
+
const built = await Bun.build({
|
|
176
|
+
entrypoints: [configPath],
|
|
177
|
+
target: 'bun',
|
|
178
|
+
format: 'cjs',
|
|
179
|
+
bytecode: true,
|
|
180
|
+
external: ['@vzn/vx'],
|
|
181
|
+
outdir: tmp,
|
|
182
|
+
naming: `${name}.js`,
|
|
183
|
+
metafile: true,
|
|
184
|
+
throw: false,
|
|
185
|
+
plugins: [
|
|
186
|
+
{
|
|
187
|
+
name: 'vx-config-bytecode',
|
|
188
|
+
setup(build) {
|
|
189
|
+
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
|
|
190
|
+
const file = realpathSync(args.path)
|
|
191
|
+
if (loaded.has(file)) {
|
|
192
|
+
faithful = false
|
|
193
|
+
inputs[file] = xxh3hex(await Bun.file(file).bytes())
|
|
194
|
+
return { contents: '', loader: 'js' }
|
|
195
|
+
}
|
|
196
|
+
const loader = loaderOf(file)
|
|
197
|
+
const source = file === entryFile ? bytes : await Bun.file(file).bytes()
|
|
198
|
+
inputs[file] = xxh3hex(source)
|
|
199
|
+
if (loader === null || /\.c[jt]s$/.test(file)) {
|
|
200
|
+
faithful = false
|
|
201
|
+
return { contents: source, loader: loader ?? 'js' }
|
|
202
|
+
}
|
|
203
|
+
const fileDir = path.dirname(file)
|
|
204
|
+
let t = transpilers.get(file)
|
|
205
|
+
if (t === undefined) {
|
|
206
|
+
t = new Bun.Transpiler({
|
|
207
|
+
loader,
|
|
208
|
+
define: {
|
|
209
|
+
'import.meta': META,
|
|
210
|
+
__dirname: JSON.stringify(fileDir),
|
|
211
|
+
__filename: JSON.stringify(file),
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
transpilers.set(file, t)
|
|
215
|
+
}
|
|
216
|
+
const js = t.transformSync(new TextDecoder().decode(source))
|
|
217
|
+
for (const m of js.matchAll(META_USE)) {
|
|
218
|
+
if (m[1] !== undefined && !META_FIELDS.has(m[1].slice(1))) faithful = false
|
|
219
|
+
}
|
|
220
|
+
if (COMPUTED_LOAD.test(js)) faithful = false
|
|
221
|
+
const meta = {
|
|
222
|
+
dir: fileDir,
|
|
223
|
+
dirname: fileDir,
|
|
224
|
+
path: file,
|
|
225
|
+
filename: file,
|
|
226
|
+
file: path.basename(file),
|
|
227
|
+
url: Bun.pathToFileURL(file).href,
|
|
228
|
+
main: false,
|
|
229
|
+
}
|
|
230
|
+
return { contents: `const ${META} = ${JSON.stringify(meta)};\n${js}`, loader: 'js' }
|
|
231
|
+
})
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
})
|
|
236
|
+
const out = path.join(tmp, `${name}.js`)
|
|
237
|
+
const bytecode = built.success && Bun.file(`${out}.jsc`).size > 0
|
|
238
|
+
const manifest: Manifest = {
|
|
239
|
+
version: COMPILED_VERSION,
|
|
240
|
+
entry: entryFile,
|
|
241
|
+
inputs,
|
|
242
|
+
imports: importsOf(built.metafile),
|
|
243
|
+
...(faithful && bytecode ? {} : { skip: true as const }),
|
|
244
|
+
}
|
|
245
|
+
// The metafile names every input; one no onLoad saw (a JSON import) is
|
|
246
|
+
// bytes the manifest does not check.
|
|
247
|
+
for (const [input, entry] of Object.entries(built.metafile?.inputs ?? {})) {
|
|
248
|
+
const file = realpathSync(path.resolve(input))
|
|
249
|
+
if (inputs[file] === undefined) manifest.skip = true
|
|
250
|
+
// A CommonJS config's export is `module.exports`, not a namespace's `default`.
|
|
251
|
+
if (file === entryFile && entry.format === 'cjs') manifest.skip = true
|
|
252
|
+
}
|
|
253
|
+
for (const old of readdirSync(dir)) {
|
|
254
|
+
if (old.startsWith('workspace-')) rmSync(path.join(dir, old), { force: true })
|
|
255
|
+
}
|
|
256
|
+
if (manifest.skip !== true) {
|
|
257
|
+
renameSync(`${out}.jsc`, path.join(dir, `${name}.js.jsc`))
|
|
258
|
+
renameSync(out, path.join(dir, `${name}.js`))
|
|
259
|
+
}
|
|
260
|
+
// Last: a manifest names a bundle already in place.
|
|
261
|
+
await writeFile(path.join(tmp, 'manifest.json'), JSON.stringify(manifest))
|
|
262
|
+
renameSync(path.join(tmp, 'manifest.json'), path.join(dir, `${name}.json`))
|
|
263
|
+
} catch {
|
|
264
|
+
// A read-only cache directory, a full disk, a graph Bun cannot bundle:
|
|
265
|
+
// the next run loads the source, as this one did.
|
|
266
|
+
} finally {
|
|
267
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function importsOf(metafile: Bun.BuildMetafile | undefined): [string, string, string][] {
|
|
272
|
+
const out: [string, string, string][] = []
|
|
273
|
+
for (const [input, entry] of Object.entries(metafile?.inputs ?? {})) {
|
|
274
|
+
const from = path.dirname(realpathSync(path.resolve(input)))
|
|
275
|
+
for (const imp of entry.imports) {
|
|
276
|
+
const spec = imp.original
|
|
277
|
+
if (imp.external === true || spec === undefined) continue
|
|
278
|
+
out.push([from, spec, realpathSync(path.resolve(imp.path))])
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return out
|
|
282
|
+
}
|
|
@@ -104,17 +104,19 @@ const nonJsonPaths = ${nonJsonPaths.toString()}
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
// Bun's resolver reads \\ as a separator and ? as a query, so a config under a\\b or a?b is served
|
|
107
|
-
// from the path as written (project-loader.ts's LITERAL_QUERY), read now.
|
|
107
|
+
// from the path as written (project-loader.ts's LITERAL_QUERY), read now. By the request's id:
|
|
108
|
+
// Bun 1.4.4 hands onLoad the path with \\ made /, and a\\b's config was read from a/b.
|
|
108
109
|
let literal = false
|
|
110
|
+
const literals = new Map()
|
|
109
111
|
const serveLiteral = () => {
|
|
110
112
|
if (literal) return
|
|
111
113
|
literal = true
|
|
112
114
|
globalThis.Bun.plugin({
|
|
113
115
|
name: 'vx-config-literal',
|
|
114
116
|
setup(build) {
|
|
115
|
-
build.onResolve({ filter: /\\?vx-literal
|
|
116
|
-
build.onLoad({ filter: /\\?vx-literal
|
|
117
|
-
const file = args.path
|
|
117
|
+
build.onResolve({ filter: /\\?vx-literal=\\d+$/ }, (args) => ({ path: args.path, namespace: 'file' }))
|
|
118
|
+
build.onLoad({ filter: /\\?vx-literal=\\d+$/ }, async (args) => {
|
|
119
|
+
const file = literals.get(/=(\\d+)$/.exec(args.path)[1])
|
|
118
120
|
return { contents: await globalThis.Bun.file(file).text(), loader: /\\.[cm]?ts$/.test(file) ? 'ts' : 'js' }
|
|
119
121
|
})
|
|
120
122
|
},
|
|
@@ -127,8 +129,6 @@ self.onmessage = async (e) => {
|
|
|
127
129
|
// env var set in-process evaluated with it on the first load and without
|
|
128
130
|
// it on every later one, and an embedder's second run() derived a
|
|
129
131
|
// different key (D-61). Each request carries the parent's env now.
|
|
130
|
-
// Through globalThis: the playground bundles this source as a string, and
|
|
131
|
-
// its free-global scan reads a bare process after a paren as code.
|
|
132
132
|
const live = globalThis.process.env
|
|
133
133
|
for (const k of Object.keys(live)) if (!(k in env)) delete live[k]
|
|
134
134
|
Object.assign(live, env)
|
|
@@ -173,8 +173,11 @@ self.onmessage = async (e) => {
|
|
|
173
173
|
}
|
|
174
174
|
try {
|
|
175
175
|
const literalPath = /[\\\\?]/.test(path)
|
|
176
|
-
if (literalPath)
|
|
177
|
-
|
|
176
|
+
if (literalPath) {
|
|
177
|
+
serveLiteral()
|
|
178
|
+
literals.set(String(id), path)
|
|
179
|
+
}
|
|
180
|
+
const ns = await import(literalPath ? path + '?vx-literal=' + id : path)
|
|
178
181
|
// Awaited as the in-process load's async return flattens it: a Promise
|
|
179
182
|
// default loaded on a run and was refused as "an instance of Promise"
|
|
180
183
|
// on every later evaluation in the process (D-5).
|
|
@@ -207,7 +210,16 @@ self.onmessage = async (e) => {
|
|
|
207
210
|
// A thrown string or plain object has no message, and String() of a
|
|
208
211
|
// null-prototype object throws here, inside the catch.
|
|
209
212
|
const isError = err !== null && typeof err === 'object' && typeof err.message === 'string'
|
|
213
|
+
// A cause holds the reason a wrapper names only by its own words.
|
|
214
|
+
const causes = []
|
|
215
|
+
const seen = new Set([err])
|
|
216
|
+
for (let c = isError ? err.cause : undefined; c !== undefined && c !== null && !seen.has(c) && causes.length < 8; c = c.cause) {
|
|
217
|
+
seen.add(c)
|
|
218
|
+
const isE = typeof c === 'object' && typeof c.message === 'string'
|
|
219
|
+
causes.push(isE ? (c.stack ?? (c.name + ': ' + c.message)) : globalThis.Bun.inspect(c, { compact: true }))
|
|
220
|
+
}
|
|
210
221
|
postMessage({
|
|
222
|
+
causes,
|
|
211
223
|
id,
|
|
212
224
|
ok: false,
|
|
213
225
|
thrown: isError ? null : globalThis.Bun.inspect(err, { compact: true }),
|
|
@@ -240,6 +252,8 @@ interface WorkerReply {
|
|
|
240
252
|
/** What a config threw that is not an Error, as `thrownValueMessage` shows it; null for an Error. */
|
|
241
253
|
thrown?: string | null
|
|
242
254
|
stack: string | null
|
|
255
|
+
/** Each `cause` under a thrown Error, outermost first, as its stack (or inspected value). */
|
|
256
|
+
causes?: string[]
|
|
243
257
|
/** A transpile error's location — `BuildMessage.position`, trimmed to what the loader reads. */
|
|
244
258
|
position: { file?: string; line?: number; column?: number } | null
|
|
245
259
|
}
|
|
@@ -333,6 +347,13 @@ function acquireWorker(): Worker {
|
|
|
333
347
|
if (msg.stack !== null) err.stack = msg.stack
|
|
334
348
|
else delete err.stack
|
|
335
349
|
if (msg.position !== null) err.position = msg.position
|
|
350
|
+
let cause: Error | undefined
|
|
351
|
+
for (const stack of (msg.causes ?? []).toReversed()) {
|
|
352
|
+
const c = new Error('', cause === undefined ? undefined : { cause })
|
|
353
|
+
c.stack = stack
|
|
354
|
+
cause = c
|
|
355
|
+
}
|
|
356
|
+
if (cause !== undefined) err.cause = cause
|
|
336
357
|
p.reject(err)
|
|
337
358
|
}
|
|
338
359
|
w.onerror = (event: ErrorEvent): void => {
|