@vzn/vx 0.0.18 → 0.0.22
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 +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- 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-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- 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/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
|
@@ -1,40 +1,142 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
1
2
|
import type { ProjectMeta } from './workspace.js'
|
|
2
3
|
|
|
3
4
|
export interface PackageGraph {
|
|
4
|
-
/** Immediate workspace deps (sorted). */
|
|
5
|
+
/** Immediate workspace deps the package builds AFTER (sorted): every bucket but peers. */
|
|
5
6
|
directDeps: (name: string) => string[]
|
|
6
|
-
/** All transitive workspace deps
|
|
7
|
+
/** All transitive workspace deps a change can reach the package through — peers included. */
|
|
7
8
|
transitiveDeps: (name: string) => string[]
|
|
8
|
-
/** All transitive workspace dependents (packages
|
|
9
|
+
/** All transitive workspace dependents (packages a change here can reach) — peers included. */
|
|
9
10
|
transitiveDependents: (name: string) => string[]
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* `taskEdges`: project → the projects its tasks name in a cross-project
|
|
15
|
+
* `dependsOn` (`e2e` → `app` from `dependsOn: ['app#build']`). A dependent
|
|
16
|
+
* the manifest does not know but the task graph does; without it
|
|
17
|
+
* `--filter '...app'` never selected `e2e`, and a CI that runs "what
|
|
18
|
+
* changed and everything depending on it" silently left it out
|
|
19
|
+
* (2026-09-10). The `^task` walk still reads `directDeps`, which carries
|
|
20
|
+
* both — a task edge IS a dependency.
|
|
21
|
+
*/
|
|
22
|
+
export function buildPackageGraph(
|
|
23
|
+
projects: ProjectMeta[],
|
|
24
|
+
taskEdges?: ReadonlyMap<string, readonly string[]>,
|
|
25
|
+
): PackageGraph {
|
|
13
26
|
const byName = new Map<string, ProjectMeta>()
|
|
14
|
-
|
|
27
|
+
const byDir = new Map<string, ProjectMeta>()
|
|
28
|
+
for (const p of projects) {
|
|
29
|
+
byName.set(p.name, p)
|
|
30
|
+
byDir.set(path.resolve(p.dir), p)
|
|
31
|
+
}
|
|
32
|
+
const linked = linkedTarget(byName, byDir)
|
|
15
33
|
|
|
34
|
+
// Two adjacencies. ORDER (`directDeps`, the `^task` walk) is what the
|
|
35
|
+
// package imports at build time: dependencies, devDependencies,
|
|
36
|
+
// optionalDependencies, the task edges, and a workspace peer that does
|
|
37
|
+
// not close a cycle. REACH (the transitive closures `--filter pkg...`
|
|
38
|
+
// and `--affected` read) is ORDER plus every workspace peer.
|
|
39
|
+
//
|
|
40
|
+
// A peer on a workspace sibling is an import that resolves to that
|
|
41
|
+
// sibling's build: every package manager links or hoists it (pnpm's
|
|
42
|
+
// `linkWorkspacePackages`, the hoisted root of bun/npm/yarn), and
|
|
43
|
+
// TanStack/router's `router-devtools-core` peers on `router-core` and
|
|
44
|
+
// type-checks against its `dist` — Nx orders `^build` on the peer,
|
|
45
|
+
// vx built the devtools first and failed (2026-09-11). But peers are
|
|
46
|
+
// the one bucket that routinely cycles (medusa's test-utils peers on
|
|
47
|
+
// medusa, which dev-depends on it through analytics; Turbo, which
|
|
48
|
+
// reads no peers, runs it; an unconditional order edge made `^build`
|
|
49
|
+
// a task cycle, 2026-09-11), so a peer edge that would close a cycle
|
|
50
|
+
// through the order graph is reach only: the consumer above provides
|
|
51
|
+
// that peer.
|
|
52
|
+
//
|
|
53
|
+
// Which edge of a MUTUAL two-peer cycle stays is stable across runs,
|
|
54
|
+
// and the PACKAGE sort below is what makes it so: the first package
|
|
55
|
+
// in name order keeps its edge, whatever order the projects arrive
|
|
56
|
+
// in. The per-package peer sort provides none of that — measured
|
|
57
|
+
// (item 571), both manifest orders give identical graphs in every
|
|
58
|
+
// arrangement tried, because `reaches(peer, p)` walks edges INTO `p`
|
|
59
|
+
// and adding an edge OUT of `p` cannot change it. It stays as cheap
|
|
60
|
+
// insurance against a future rule that does depend on it; it is not
|
|
61
|
+
// what the stability rests on.
|
|
16
62
|
const directDeps = new Map<string, string[]>()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
const reachDeps = new Map<string, string[]>()
|
|
64
|
+
const order = new Map<string, Set<string>>()
|
|
65
|
+
const peers = new Map<string, Set<string>>()
|
|
66
|
+
const sorted = [...projects].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
67
|
+
for (const p of sorted) {
|
|
68
|
+
const own = new Set<string>()
|
|
69
|
+
for (const name of taskEdges?.get(p.name) ?? []) {
|
|
70
|
+
if (name !== p.name && byName.has(name)) own.add(name)
|
|
71
|
+
}
|
|
72
|
+
const optional = bucket(p.packageJson.optionalDependencies)
|
|
73
|
+
const prod = bucket(p.packageJson.dependencies)
|
|
74
|
+
const dev = bucket(p.packageJson.devDependencies)
|
|
75
|
+
const add = (key: string, spec: unknown, installed: boolean, into: Set<string>): void => {
|
|
76
|
+
if (typeof spec !== 'string') return
|
|
77
|
+
const target = linked(key, spec.trim(), p.dir, installed)
|
|
78
|
+
if (target !== undefined && target !== p.name) into.add(target)
|
|
79
|
+
}
|
|
80
|
+
// Which entry is installed when a key sits in more than one field,
|
|
81
|
+
// measured with bun 1.4, npm 10, yarn 1 and pnpm 12 (2026-09-24): bun
|
|
82
|
+
// and npm take devDependencies, then optionalDependencies, then
|
|
83
|
+
// dependencies; yarn and pnpm take optionalDependencies, then
|
|
84
|
+
// dependencies, then devDependencies. vx cannot tell which manager
|
|
85
|
+
// installed the tree, so either winner linking is an edge: a spare
|
|
86
|
+
// edge costs order, a missing one a stale hit. A dev or optional
|
|
87
|
+
// entry wins under one order or the other; a prod entry loses to an
|
|
88
|
+
// optional one under both.
|
|
89
|
+
for (const key in dev) add(key, dev[key], true, own)
|
|
90
|
+
for (const key in optional) add(key, optional[key], true, own)
|
|
91
|
+
for (const key in prod) if (!Object.hasOwn(optional, key)) add(key, prod[key], true, own)
|
|
92
|
+
order.set(p.name, own)
|
|
93
|
+
// A peer is not installed by the package that declares it: when an
|
|
94
|
+
// installed entry names the same key, that entry is what resolves
|
|
95
|
+
// (turborepo#12640: a registry `buffer@^6` dev dependency beside a
|
|
96
|
+
// `workspace:*` peer on it installs the registry copy in all four).
|
|
97
|
+
// Alone, a peer on a sibling resolves to the sibling whatever its
|
|
98
|
+
// range, since bun and yarn hoist the workspace copy (npm refuses an
|
|
99
|
+
// unmet one; pnpm 12 fetched it from the registry).
|
|
100
|
+
const peer = new Set<string>()
|
|
101
|
+
const peerDeps = bucket(p.packageJson.peerDependencies)
|
|
102
|
+
for (const key in peerDeps) {
|
|
103
|
+
if (!Object.hasOwn(optional, key) && !Object.hasOwn(prod, key) && !Object.hasOwn(dev, key)) {
|
|
104
|
+
add(key, peerDeps[key], false, peer)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
peers.set(p.name, peer)
|
|
108
|
+
}
|
|
109
|
+
// `to` reaches `from` through the order edges so far ⇒ from → to
|
|
110
|
+
// would close a cycle.
|
|
111
|
+
const reaches = (start: string, goal: string): boolean => {
|
|
112
|
+
const seen = new Set<string>([start])
|
|
113
|
+
const stack = [start]
|
|
114
|
+
while (stack.length > 0) {
|
|
115
|
+
for (const next of order.get(stack.pop()!) ?? []) {
|
|
116
|
+
if (next === goal) return true
|
|
117
|
+
if (!seen.has(next)) {
|
|
118
|
+
seen.add(next)
|
|
119
|
+
stack.push(next)
|
|
120
|
+
}
|
|
30
121
|
}
|
|
31
122
|
}
|
|
32
|
-
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
for (const p of sorted) {
|
|
126
|
+
const own = order.get(p.name)!
|
|
127
|
+
for (const peer of [...peers.get(p.name)!].sort()) {
|
|
128
|
+
if (!own.has(peer) && !reaches(peer, p.name)) own.add(peer)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const p of sorted) {
|
|
132
|
+
const own = order.get(p.name)!
|
|
133
|
+
directDeps.set(p.name, [...own].sort())
|
|
134
|
+
reachDeps.set(p.name, [...new Set([...own, ...peers.get(p.name)!])].sort())
|
|
33
135
|
}
|
|
34
136
|
|
|
35
137
|
// Reverse adjacency: who declares X as a workspace dep.
|
|
36
138
|
const directDependents = new Map<string, string[]>()
|
|
37
|
-
for (const [name, deps] of
|
|
139
|
+
for (const [name, deps] of reachDeps) {
|
|
38
140
|
for (const d of deps) {
|
|
39
141
|
const arr = directDependents.get(d)
|
|
40
142
|
if (arr) arr.push(name)
|
|
@@ -110,22 +212,24 @@ export function buildPackageGraph(projects: ProjectMeta[]): PackageGraph {
|
|
|
110
212
|
return [...seen].sort()
|
|
111
213
|
}
|
|
112
214
|
|
|
215
|
+
// The closures are built on the FIRST query, not here: an unscoped run
|
|
216
|
+
// seeds every project and never asks for a transitive set, and neither
|
|
217
|
+
// does a `^task` walk (it reads `directDeps`). Building both closures
|
|
218
|
+
// eagerly was 12 ms of a 240 ms warm run at 1000 projects × 30 deps
|
|
219
|
+
// (profiled 2026-09-09) for answers nobody read.
|
|
113
220
|
function makeAccessor(edges: Map<string, string[]>): (name: string) => string[] {
|
|
114
|
-
const closures = bitsetClosures(edges)
|
|
115
221
|
const memo = new Map<string, string[]>()
|
|
116
|
-
|
|
117
|
-
return (name) => {
|
|
118
|
-
const cached = memo.get(name)
|
|
119
|
-
if (cached) return cached
|
|
120
|
-
const result = reachableFrom(name, edges)
|
|
121
|
-
memo.set(name, result)
|
|
122
|
-
return result
|
|
123
|
-
}
|
|
124
|
-
}
|
|
222
|
+
let closures: Uint32Array | null | undefined
|
|
125
223
|
const words = (names.length + 31) >>> 5
|
|
126
224
|
return (name) => {
|
|
127
225
|
const cached = memo.get(name)
|
|
128
226
|
if (cached) return cached
|
|
227
|
+
if (closures === undefined) closures = bitsetClosures(edges)
|
|
228
|
+
if (closures === null) {
|
|
229
|
+
const result = reachableFrom(name, edges)
|
|
230
|
+
memo.set(name, result)
|
|
231
|
+
return result
|
|
232
|
+
}
|
|
129
233
|
const i = index.get(name)
|
|
130
234
|
if (i === undefined) return []
|
|
131
235
|
const base = i * words
|
|
@@ -145,7 +249,111 @@ export function buildPackageGraph(projects: ProjectMeta[]): PackageGraph {
|
|
|
145
249
|
|
|
146
250
|
return {
|
|
147
251
|
directDeps: (name) => directDeps.get(name) ?? [],
|
|
148
|
-
transitiveDeps: makeAccessor(
|
|
252
|
+
transitiveDeps: makeAccessor(reachDeps),
|
|
149
253
|
transitiveDependents: makeAccessor(directDependents),
|
|
150
254
|
}
|
|
151
255
|
}
|
|
256
|
+
|
|
257
|
+
function bucket(field: unknown): Readonly<Record<string, unknown>> {
|
|
258
|
+
return typeof field === 'object' && field !== null ? (field as Record<string, unknown>) : {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The ranges a manifest carries, in the part of npm's grammar (node-semver)
|
|
262
|
+
// where `Bun.semver.satisfies` answers as npm does: `||`-joined sets, each
|
|
263
|
+
// a hyphen range, one comparator, or two or more `<`/`>` bounds, over
|
|
264
|
+
// versions whose wildcards trail (`1.x`, `1.2.*`). Bun 1.4.2 answers true
|
|
265
|
+
// for text that is no range at all (`latest`, `npm:foo@1`, `github:a/b`,
|
|
266
|
+
// `../x`), ORs bare comparators npm ANDs (`1 2`), and reads `>x` and a
|
|
267
|
+
// leading wildcard (`x.3.1`) its own way; a spec outside this grammar
|
|
268
|
+
// counts as unmet. One corner inside it still differs (item 831): a
|
|
269
|
+
// prerelease at a `<` bound over a partial (`<3.x`, `<3.1`) or at a hyphen
|
|
270
|
+
// range's partial end, beside a lower bound naming that prerelease's own
|
|
271
|
+
// version — `3.0.0-beta` against `>=3.0.0-alpha <3.x` — Bun admits and npm
|
|
272
|
+
// does not. bun installs by the first answer and the others by the second,
|
|
273
|
+
// so neither is right everywhere; the graph takes Bun's. The playground's
|
|
274
|
+
// port (packages/vx-docs, src/playground/shim/semver.ts) is held to Bun
|
|
275
|
+
// over this grammar.
|
|
276
|
+
const W = String.raw`[xX*]`
|
|
277
|
+
const PRE = String.raw`(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?`
|
|
278
|
+
const NUMERIC = String.raw`[vV]?\d+(?:\.${W}(?:\.${W})?|\.\d+(?:\.${W}|\.\d+${PRE})?)?`
|
|
279
|
+
const SINGLE = String.raw`(?:(?:[<>]=?|~>?|\^)\s*${NUMERIC}|=?\s*(?:${NUMERIC}|${W}(?:\.${W}){0,2}))`
|
|
280
|
+
const BOUND = String.raw`[<>]=?\s*${NUMERIC}`
|
|
281
|
+
const SET = String.raw`(?:${NUMERIC}\s+-\s+${NUMERIC}|${BOUND}(?:\s+${BOUND})+|${SINGLE})`
|
|
282
|
+
export const SEMVER_RANGE = new RegExp(String.raw`^(?:${SET}(?:\s*\|\|\s*${SET})*)?$`)
|
|
283
|
+
/** `<name>[@<range>]` after `npm:` or `workspace:`; the name may be scoped. */
|
|
284
|
+
const ALIAS = /^(@[^@/]+\/[^@/]+|[^@/.][^@/]*)(?:@(.*))?$/
|
|
285
|
+
const PATH_PROTOCOLS = ['file:', 'link:', 'portal:'] as const
|
|
286
|
+
const MAY_POINT_ELSEWHERE = /^(?:workspace:|npm:|file:|link:|portal:|\.\.?\/|\/)/
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The workspace package a manifest entry resolves to, or undefined when
|
|
290
|
+
* the package manager installs it from elsewhere. `installed` is false
|
|
291
|
+
* for a peer, which the hoisted workspace copy under its key provides
|
|
292
|
+
* whatever the spec. The rule and its measurements are
|
|
293
|
+
* docs/modules/package-graph.md § Which entries are edges.
|
|
294
|
+
*/
|
|
295
|
+
function linkedTarget(
|
|
296
|
+
byName: ReadonlyMap<string, ProjectMeta>,
|
|
297
|
+
byDir: ReadonlyMap<string, ProjectMeta>,
|
|
298
|
+
): (key: string, spec: string, fromDir: string, installed: boolean) => string | undefined {
|
|
299
|
+
// A monorepo repeats a few ranges and versions thousands of times, and
|
|
300
|
+
// the grammar test and `Bun.semver` cost about a microsecond a call:
|
|
301
|
+
// unmemoised they tripled the graph build at 1000 projects × 30 deps.
|
|
302
|
+
const isRange = new Map<string, boolean>()
|
|
303
|
+
const range = (spec: string): boolean => {
|
|
304
|
+
let ok = isRange.get(spec)
|
|
305
|
+
if (ok === undefined) isRange.set(spec, (ok = SEMVER_RANGE.test(spec)))
|
|
306
|
+
return ok
|
|
307
|
+
}
|
|
308
|
+
const satisfiedBy = new Map<string, Map<string, boolean>>()
|
|
309
|
+
// A dist-tag (`latest`) is no range, and bun, npm, yarn and pnpm all
|
|
310
|
+
// installed it from the registry beside a matching workspace package.
|
|
311
|
+
const satisfies = (p: ProjectMeta, spec: string): boolean => {
|
|
312
|
+
if (spec === '*' || spec === '') return true
|
|
313
|
+
const version = p.packageJson.version
|
|
314
|
+
if (typeof version !== 'string' || !range(spec)) return false
|
|
315
|
+
let memo = satisfiedBy.get(version)
|
|
316
|
+
if (memo === undefined) satisfiedBy.set(version, (memo = new Map()))
|
|
317
|
+
let ok = memo.get(spec)
|
|
318
|
+
if (ok === undefined) memo.set(spec, (ok = Bun.semver.satisfies(version, spec)))
|
|
319
|
+
return ok
|
|
320
|
+
}
|
|
321
|
+
const named = (name: string, spec: string): string | undefined => {
|
|
322
|
+
const p = byName.get(name)
|
|
323
|
+
return p !== undefined && satisfies(p, spec) ? name : undefined
|
|
324
|
+
}
|
|
325
|
+
const atPath = (fromDir: string, rel: string): string | undefined =>
|
|
326
|
+
byDir.get(path.resolve(fromDir, rel))?.name
|
|
327
|
+
return (key, spec, fromDir, installed) => {
|
|
328
|
+
if (spec === 'workspace:*') return named(key, '*')
|
|
329
|
+
// Only a protocol or a path points a key at another package; every
|
|
330
|
+
// other spec (a range, a tag, a URL) is decided by the key.
|
|
331
|
+
if (!MAY_POINT_ELSEWHERE.test(spec)) {
|
|
332
|
+
const local = byName.get(key)
|
|
333
|
+
if (local === undefined) return undefined
|
|
334
|
+
// A catalog entry's range lives where the graph does not read
|
|
335
|
+
// (`pnpm-workspace.yaml`, bun's root `catalog`): it keeps the edge.
|
|
336
|
+
if (!installed || spec.startsWith('catalog:')) return key
|
|
337
|
+
return satisfies(local, spec) ? key : undefined
|
|
338
|
+
}
|
|
339
|
+
if (spec.startsWith('workspace:')) {
|
|
340
|
+
const rest = spec.slice('workspace:'.length)
|
|
341
|
+
if (rest === '^' || rest === '~') return named(key, '*')
|
|
342
|
+
if (range(rest)) return named(key, rest)
|
|
343
|
+
const alias = ALIAS.exec(rest)
|
|
344
|
+
if (alias !== null) return named(alias[1]!, alias[2] ?? '*')
|
|
345
|
+
return atPath(fromDir, rest)
|
|
346
|
+
}
|
|
347
|
+
if (spec.startsWith('npm:')) {
|
|
348
|
+
// bun links a satisfied alias; npm and yarn fetch it from the
|
|
349
|
+
// registry. The edge is the spare one, as above.
|
|
350
|
+
const alias = ALIAS.exec(spec.slice('npm:'.length))
|
|
351
|
+
return alias === null ? undefined : named(alias[1]!, alias[2] ?? '*')
|
|
352
|
+
}
|
|
353
|
+
for (const protocol of PATH_PROTOCOLS) {
|
|
354
|
+
if (spec.startsWith(protocol)) return atPath(fromDir, spec.slice(protocol.length))
|
|
355
|
+
}
|
|
356
|
+
// A bare `./`, `../` or `/` path is a directory to bun, npm and pnpm.
|
|
357
|
+
return atPath(fromDir, spec)
|
|
358
|
+
}
|
|
359
|
+
}
|