@vzn/vx 0.0.635 → 0.0.637

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.
Files changed (141) hide show
  1. package/README.md +47 -19
  2. package/docs/caching.md +222 -210
  3. package/docs/cli.md +699 -664
  4. package/docs/execution.md +67 -75
  5. package/docs/features.md +44 -40
  6. package/docs/patterns.md +31 -32
  7. package/docs/schema.md +418 -202
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +2 -2
  16. package/schemas/show.json +13 -4
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +16 -12
  19. package/src/bin.ts +17 -2
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +105 -53
  22. package/src/cache/git-inputs.ts +277 -63
  23. package/src/cache/index.ts +7 -1
  24. package/src/cache/inputs.ts +125 -35
  25. package/src/cache/key-fold.ts +8 -7
  26. package/src/cache/layer.ts +8 -17
  27. package/src/cache/layered-cache.ts +16 -102
  28. package/src/cache/policy.ts +25 -29
  29. package/src/cache/run-history.ts +36 -14
  30. package/src/cache/schema.ts +21 -23
  31. package/src/cache/scoped-cache.ts +224 -0
  32. package/src/cli/cache.ts +150 -28
  33. package/src/cli/completions.ts +2 -2
  34. package/src/cli/core-alias.ts +1 -1
  35. package/src/cli/foreign-flags.ts +13 -12
  36. package/src/cli/help.ts +94 -61
  37. package/src/cli/index.ts +42 -13
  38. package/src/cli/info.ts +38 -23
  39. package/src/cli/init.ts +115 -406
  40. package/src/cli/last.ts +145 -4
  41. package/src/cli/lock.ts +333 -111
  42. package/src/cli/plan-format.ts +2 -2
  43. package/src/cli/plugin-commands.ts +5 -9
  44. package/src/cli/plugin-templates.ts +10 -10
  45. package/src/cli/run.ts +123 -144
  46. package/src/cli/select.ts +33 -39
  47. package/src/cli/show.ts +17 -19
  48. package/src/cli/task-verb.ts +2 -9
  49. package/src/cli/upgrade.ts +402 -145
  50. package/src/cli/watch-cycle.ts +9 -2
  51. package/src/cli/watch-filter.ts +33 -60
  52. package/src/cli/watch-fs.ts +29 -16
  53. package/src/cli/watch-judge.ts +12 -3
  54. package/src/cli/watch-set.ts +16 -42
  55. package/src/cli/watch.ts +188 -215
  56. package/src/cli/why.ts +28 -17
  57. package/src/cli/workspace-config.ts +31 -68
  58. package/src/config.ts +191 -71
  59. package/src/exec/env.ts +1 -31
  60. package/src/exec/executor.ts +1 -5
  61. package/src/exec/kill-tree.ts +2 -1
  62. package/src/exec/sandbox-runtime.ts +12 -1
  63. package/src/exec/sandbox-violations.ts +9 -1
  64. package/src/graph/dependency-spec.ts +1 -1
  65. package/src/graph/index.ts +1 -0
  66. package/src/graph/priorities.ts +1 -1
  67. package/src/graph/scheduler.ts +19 -7
  68. package/src/graph/task-graph.ts +44 -20
  69. package/src/handoff.ts +169 -0
  70. package/src/index.ts +25 -57
  71. package/src/orchestrator/affected-tasks.ts +7 -5
  72. package/src/orchestrator/command-windows.ts +44 -0
  73. package/src/orchestrator/deferred-outputs.ts +12 -10
  74. package/src/orchestrator/doctor.ts +98 -110
  75. package/src/orchestrator/download-policy.ts +3 -3
  76. package/src/orchestrator/events.ts +19 -3
  77. package/src/orchestrator/excluded-keys.ts +0 -2
  78. package/src/orchestrator/execute-task.ts +192 -70
  79. package/src/orchestrator/fingerprint-watch.ts +18 -15
  80. package/src/orchestrator/forecast.ts +10 -2
  81. package/src/orchestrator/framed-output.ts +8 -3
  82. package/src/orchestrator/graph-stage.ts +5 -5
  83. package/src/orchestrator/history.ts +70 -30
  84. package/src/orchestrator/hit-restore.ts +15 -3
  85. package/src/orchestrator/index.ts +18 -4
  86. package/src/orchestrator/local-shortcircuit.ts +2 -1
  87. package/src/orchestrator/lockfile-claim.ts +79 -20
  88. package/src/orchestrator/logger.ts +71 -39
  89. package/src/orchestrator/metrics.ts +71 -6
  90. package/src/orchestrator/miss-save.ts +3 -0
  91. package/src/orchestrator/options.ts +43 -28
  92. package/src/orchestrator/output-host.ts +72 -0
  93. package/src/orchestrator/plan.ts +6 -8
  94. package/src/orchestrator/plugin-host.ts +87 -63
  95. package/src/orchestrator/plugin.ts +94 -60
  96. package/src/orchestrator/prepare.ts +114 -73
  97. package/src/orchestrator/projects.ts +102 -99
  98. package/src/orchestrator/remote-prefetch.ts +2 -1
  99. package/src/orchestrator/run-context.ts +25 -6
  100. package/src/orchestrator/run-failures.ts +17 -8
  101. package/src/orchestrator/run-lock.ts +1 -1
  102. package/src/orchestrator/run-report.ts +9 -9
  103. package/src/orchestrator/run-settings.ts +48 -0
  104. package/src/orchestrator/run.ts +127 -88
  105. package/src/orchestrator/sandbox-fix.ts +39 -20
  106. package/src/orchestrator/sandbox-request.ts +84 -13
  107. package/src/orchestrator/signals.ts +1 -1
  108. package/src/orchestrator/stable-keys.ts +13 -11
  109. package/src/orchestrator/status-line.ts +48 -6
  110. package/src/orchestrator/summary.ts +46 -62
  111. package/src/orchestrator/task-hash.ts +12 -83
  112. package/src/orchestrator/telemetry.ts +3 -3
  113. package/src/settings.ts +417 -0
  114. package/src/util/errors.ts +1 -1
  115. package/src/util/essential-env.ts +31 -0
  116. package/src/util/hide-env.ts +107 -0
  117. package/src/util/index.ts +8 -2
  118. package/src/util/paths.ts +10 -0
  119. package/src/util/settle.ts +49 -30
  120. package/src/util/size.ts +23 -4
  121. package/src/util/timing.ts +3 -2
  122. package/src/util/ulid.ts +1 -1
  123. package/src/util/verbs.ts +0 -15
  124. package/src/workspace/affected.ts +77 -222
  125. package/src/workspace/config-eval.ts +15 -20
  126. package/src/workspace/config-imports.ts +78 -3
  127. package/src/workspace/config-schema.ts +251 -241
  128. package/src/workspace/dependency-rules.ts +57 -0
  129. package/src/workspace/filter.ts +2 -2
  130. package/src/workspace/fingerprint.ts +11 -121
  131. package/src/workspace/index.ts +22 -27
  132. package/src/workspace/load-reads.ts +4 -4
  133. package/src/workspace/lockfile.ts +96 -140
  134. package/src/workspace/migration.ts +242 -146
  135. package/src/workspace/package-graph.ts +30 -216
  136. package/src/workspace/project-loader.ts +80 -285
  137. package/src/workspace/repo-id.ts +2 -1
  138. package/src/workspace/workspace.ts +362 -831
  139. package/src/cache/config-evals.ts +0 -130
  140. package/src/workspace/config-cache.ts +0 -661
  141. package/src/workspace/migrate-scripts.ts +0 -1121
@@ -1,19 +1,23 @@
1
- import path from 'node:path'
2
1
  import type { ProjectMeta } from './workspace.js'
3
2
 
4
3
  export interface PackageGraph {
5
- /** Immediate workspace deps the package builds AFTER (sorted): every bucket but peers. */
4
+ /**
5
+ * Some `discover` entry carried `deps`: a project with none is a leaf.
6
+ * False, no plugin supplies edges and a `^task` is refused.
7
+ */
8
+ readonly edgeSource: boolean
9
+ /** Projects the project builds AFTER (sorted): a `discover` plugin's `deps` and the task edges. */
6
10
  directDeps: (name: string) => string[]
7
- /** All transitive workspace deps a change can reach the package through — peers included. */
11
+ /** All projects a change can reach the project through: order edges and a plugin's `reach`. */
8
12
  transitiveDeps: (name: string) => string[]
9
- /** All transitive workspace dependents (packages a change here can reach) — peers included. */
13
+ /** All projects a change here can reach (the reverse). */
10
14
  transitiveDependents: (name: string) => string[]
11
15
  }
12
16
 
13
17
  /**
14
18
  * `taskEdges`: project → the projects its tasks name in a cross-project
15
19
  * `dependsOn` (`e2e` → `app` from `dependsOn: ['app#build']`). A dependent
16
- * the manifest does not know but the task graph does; without it
20
+ * no plugin edge names but the task graph does; without it
17
21
  * `--filter '...app'` never selected `e2e`, and a CI that runs "what
18
22
  * changed and everything depending on it" silently left it out
19
23
  * (2026-09-10). The `^task` walk still reads `directDeps`, which carries
@@ -24,112 +28,37 @@ export function buildPackageGraph(
24
28
  taskEdges?: ReadonlyMap<string, readonly string[]>,
25
29
  ): PackageGraph {
26
30
  const byName = new Map<string, ProjectMeta>()
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)
31
+ for (const p of projects) byName.set(p.name, p)
33
32
 
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.
33
+ // Two adjacencies. ORDER (`directDeps`, the `^task` walk) is the task
34
+ // edges and a `discover` plugin's `deps`. REACH (the transitive closures
35
+ // `--filter pkg...` and `--affected` read) is ORDER plus a plugin's
36
+ // `reach`, which never orders a build.
62
37
  const directDeps = new Map<string, string[]>()
63
38
  const order = new Map<string, Set<string>>()
64
- const peers = new Map<string, Set<string>>()
39
+ const reachOnly = new Map<string, readonly string[]>()
65
40
  const sorted = [...projects].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
41
+ let edgeSource = false
66
42
  for (const p of sorted) {
67
43
  const own = new Set<string>()
68
44
  for (const name of taskEdges?.get(p.name) ?? []) {
69
45
  if (name !== p.name && byName.has(name)) own.add(name)
70
46
  }
71
- const optional = bucket(p.packageJson.optionalDependencies)
72
- const prod = bucket(p.packageJson.dependencies)
73
- const dev = bucket(p.packageJson.devDependencies)
74
- const add = (key: string, spec: unknown, installed: boolean, into: Set<string>): void => {
75
- if (typeof spec !== 'string') return
76
- const target = linked(key, fromCatalog(p, key, spec.trim()), p.dir, installed)
77
- if (target !== undefined && target !== p.name) into.add(target)
78
- }
79
- // Which entry is installed when a key sits in more than one field,
80
- // measured with bun 1.4, npm 10, yarn 1 and pnpm 12 (2026-09-24): bun
81
- // and npm take devDependencies, then optionalDependencies, then
82
- // dependencies; yarn and pnpm take optionalDependencies, then
83
- // dependencies, then devDependencies. vx cannot tell which manager
84
- // installed the tree, so either winner linking is an edge: a spare
85
- // edge costs order, a missing one a stale hit. A dev or optional
86
- // entry wins under one order or the other; a prod entry loses to an
87
- // optional one under both.
88
- for (const key in dev) add(key, dev[key], true, own)
89
- for (const key in optional) add(key, optional[key], true, own)
90
- for (const key in prod) if (!Object.hasOwn(optional, key)) add(key, prod[key], true, own)
91
- order.set(p.name, own)
92
- // A peer is not installed by the package that declares it: when an
93
- // installed entry names the same key, that entry is what resolves
94
- // (turborepo#12640: a registry `buffer@^6` dev dependency beside a
95
- // `workspace:*` peer on it installs the registry copy in all four).
96
- // Alone, a peer on a sibling resolves to the sibling whatever its
97
- // range, since bun and yarn hoist the workspace copy (npm refuses an
98
- // unmet one; pnpm 12 fetched it from the registry).
99
- const peer = new Set<string>()
100
- const peerDeps = bucket(p.packageJson.peerDependencies)
101
- for (const key in peerDeps) {
102
- if (!Object.hasOwn(optional, key) && !Object.hasOwn(prod, key) && !Object.hasOwn(dev, key)) {
103
- add(key, peerDeps[key], false, peer)
104
- }
105
- }
106
- peers.set(p.name, peer)
107
- }
108
- // `to` reaches `from` through the order edges so far ⇒ from → to
109
- // would close a cycle.
110
- const reaches = (start: string, goal: string): boolean => {
111
- const seen = new Set<string>([start])
112
- const stack = [start]
113
- while (stack.length > 0) {
114
- for (const next of order.get(stack.pop()!) ?? []) {
115
- if (next === goal) return true
116
- if (!seen.has(next)) {
117
- seen.add(next)
118
- stack.push(next)
119
- }
120
- }
47
+ if (p.deps !== undefined) edgeSource = true
48
+ for (const name of p.deps ?? []) {
49
+ if (name !== p.name && byName.has(name)) own.add(name)
121
50
  }
122
- return false
123
- }
124
- for (const p of sorted) {
125
- const own = order.get(p.name)!
126
- for (const peer of [...peers.get(p.name)!].sort()) {
127
- if (!own.has(peer) && !reaches(peer, p.name)) own.add(peer)
51
+ if (p.reach !== undefined) {
52
+ reachOnly.set(
53
+ p.name,
54
+ p.reach.filter((name) => name !== p.name && byName.has(name)),
55
+ )
128
56
  }
57
+ order.set(p.name, own)
58
+ directDeps.set(p.name, [...own].sort())
129
59
  }
130
- for (const p of sorted) directDeps.set(p.name, [...order.get(p.name)!].sort())
131
60
 
132
- // REACH and its reverse (who declares X as a workspace dep) are read only
61
+ // REACH and its reverse (who has an edge to X) are read only
133
62
  // by the transitive questions below, which an unscoped run never asks:
134
63
  // built on the first one, as the closures are.
135
64
  const reachDeps = new Map<string, string[]>()
@@ -140,7 +69,7 @@ export function buildPackageGraph(
140
69
  if (reach === undefined) {
141
70
  const own = order.get(name)
142
71
  if (own === undefined) return []
143
- reach = [...new Set([...own, ...peers.get(name)!])].sort()
72
+ reach = [...new Set([...own, ...(reachOnly.get(name) ?? [])])].sort()
144
73
  reachDeps.set(name, reach)
145
74
  }
146
75
  return reach
@@ -151,7 +80,7 @@ export function buildPackageGraph(
151
80
  reachBuilt = true
152
81
  for (const p of sorted) {
153
82
  const own = order.get(p.name)!
154
- reachDeps.set(p.name, [...new Set([...own, ...peers.get(p.name)!])].sort())
83
+ reachDeps.set(p.name, [...new Set([...own, ...(reachOnly.get(p.name) ?? [])])].sort())
155
84
  }
156
85
  for (const [name, deps] of reachDeps) {
157
86
  for (const d of deps) {
@@ -294,124 +223,9 @@ export function buildPackageGraph(
294
223
  }
295
224
 
296
225
  return {
226
+ edgeSource,
297
227
  directDeps: (name) => directDeps.get(name) ?? [],
298
228
  transitiveDeps: makeAccessor(reachDeps, searchDeps),
299
229
  transitiveDependents: makeAccessor(directDependents),
300
230
  }
301
231
  }
302
-
303
- /**
304
- * The spec a `catalog:` / `catalog:<name>` entry stands for in the
305
- * workspace's catalog, so it is an edge by the rule its range or protocol
306
- * meets (zenstack's `nuxt: "catalog:"` is `4.3.1`, which the workspace's
307
- * versionless `nuxt` sample does not satisfy). A catalog the graph was not
308
- * given or that lacks the key leaves the spec, which keeps its edge.
309
- */
310
- function fromCatalog(p: ProjectMeta, key: string, spec: string): string {
311
- if (p.catalogs === undefined || !spec.startsWith('catalog:')) return spec
312
- const resolved = p.catalogs.get(spec.slice('catalog:'.length).trim() || 'default')?.[key]
313
- return typeof resolved === 'string' && !resolved.startsWith('catalog:') ? resolved.trim() : spec
314
- }
315
-
316
- function bucket(field: unknown): Readonly<Record<string, unknown>> {
317
- return typeof field === 'object' && field !== null ? (field as Record<string, unknown>) : {}
318
- }
319
-
320
- // The ranges a manifest carries, in the part of npm's grammar (node-semver)
321
- // where `Bun.semver.satisfies` answers as npm does: `||`-joined sets, each
322
- // a hyphen range, one comparator, or two or more `<`/`>` bounds, over
323
- // versions whose wildcards trail (`1.x`, `1.2.*`). Bun 1.4.2 answers true
324
- // for text that is no range at all (`latest`, `npm:foo@1`, `github:a/b`,
325
- // `../x`), ORs bare comparators npm ANDs (`1 2`), and reads `>x` and a
326
- // leading wildcard (`x.3.1`) its own way; a spec outside this grammar
327
- // counts as unmet. One corner inside it still differs (item 831): a
328
- // prerelease at a `<` bound over a partial (`<3.x`, `<3.1`) or at a hyphen
329
- // range's partial end, beside a lower bound naming that prerelease's own
330
- // version — `3.0.0-beta` against `>=3.0.0-alpha <3.x` — Bun admits and npm
331
- // does not. bun installs by the first answer and the others by the second,
332
- // so neither is right everywhere; the graph takes Bun's. The playground's
333
- // port (packages/vx-docs, src/playground/shim/semver.ts) is held to Bun
334
- // over this grammar.
335
- const W = String.raw`[xX*]`
336
- const PRE = String.raw`(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?`
337
- const NUMERIC = String.raw`[vV]?\d+(?:\.${W}(?:\.${W})?|\.\d+(?:\.${W}|\.\d+${PRE})?)?`
338
- const SINGLE = String.raw`(?:(?:[<>]=?|~>?|\^)\s*${NUMERIC}|=?\s*(?:${NUMERIC}|${W}(?:\.${W}){0,2}))`
339
- const BOUND = String.raw`[<>]=?\s*${NUMERIC}`
340
- const SET = String.raw`(?:${NUMERIC}\s+-\s+${NUMERIC}|${BOUND}(?:\s+${BOUND})+|${SINGLE})`
341
- export const SEMVER_RANGE = new RegExp(String.raw`^(?:${SET}(?:\s*\|\|\s*${SET})*)?$`)
342
- /** `<name>[@<range>]` after `npm:` or `workspace:`; the name may be scoped. */
343
- const ALIAS = /^(@[^@/]+\/[^@/]+|[^@/.][^@/]*)(?:@(.*))?$/
344
- const PATH_PROTOCOLS = ['file:', 'link:', 'portal:'] as const
345
- const MAY_POINT_ELSEWHERE = /^(?:workspace:|npm:|file:|link:|portal:|\.\.?\/|\/)/
346
-
347
- /**
348
- * The workspace package a manifest entry resolves to, or undefined when
349
- * the package manager installs it from elsewhere. `installed` is false
350
- * for a peer, which the hoisted workspace copy under its key provides
351
- * whatever the spec. The rule and its measurements are
352
- * docs/modules/package-graph.md § Which entries are edges.
353
- */
354
- function linkedTarget(
355
- byName: ReadonlyMap<string, ProjectMeta>,
356
- byDir: ReadonlyMap<string, ProjectMeta>,
357
- ): (key: string, spec: string, fromDir: string, installed: boolean) => string | undefined {
358
- // A monorepo repeats a few ranges and versions thousands of times, and
359
- // the grammar test and `Bun.semver` cost about a microsecond a call:
360
- // unmemoised they tripled the graph build at 1000 projects × 30 deps.
361
- const isRange = new Map<string, boolean>()
362
- const range = (spec: string): boolean => {
363
- let ok = isRange.get(spec)
364
- if (ok === undefined) isRange.set(spec, (ok = SEMVER_RANGE.test(spec)))
365
- return ok
366
- }
367
- const satisfiedBy = new Map<string, Map<string, boolean>>()
368
- // A dist-tag (`latest`) is no range, and bun, npm, yarn and pnpm all
369
- // installed it from the registry beside a matching workspace package.
370
- const satisfies = (p: ProjectMeta, spec: string): boolean => {
371
- if (spec === '*' || spec === '') return true
372
- const version = p.packageJson.version
373
- if (typeof version !== 'string' || !range(spec)) return false
374
- let memo = satisfiedBy.get(version)
375
- if (memo === undefined) satisfiedBy.set(version, (memo = new Map()))
376
- let ok = memo.get(spec)
377
- if (ok === undefined) memo.set(spec, (ok = Bun.semver.satisfies(version, spec)))
378
- return ok
379
- }
380
- const named = (name: string, spec: string): string | undefined => {
381
- const p = byName.get(name)
382
- return p !== undefined && satisfies(p, spec) ? name : undefined
383
- }
384
- const atPath = (fromDir: string, rel: string): string | undefined =>
385
- byDir.get(path.resolve(fromDir, rel))?.name
386
- return (key, spec, fromDir, installed) => {
387
- if (spec === 'workspace:*') return named(key, '*')
388
- // Only a protocol or a path points a key at another package; every
389
- // other spec (a range, a tag, a URL) is decided by the key.
390
- if (!MAY_POINT_ELSEWHERE.test(spec)) {
391
- const local = byName.get(key)
392
- if (local === undefined) return undefined
393
- // A catalog entry its workspace's catalog did not resolve keeps the edge.
394
- if (!installed || spec.startsWith('catalog:')) return key
395
- return satisfies(local, spec) ? key : undefined
396
- }
397
- if (spec.startsWith('workspace:')) {
398
- const rest = spec.slice('workspace:'.length)
399
- if (rest === '^' || rest === '~') return named(key, '*')
400
- if (range(rest)) return named(key, rest)
401
- const alias = ALIAS.exec(rest)
402
- if (alias !== null) return named(alias[1]!, alias[2] ?? '*')
403
- return atPath(fromDir, rest)
404
- }
405
- if (spec.startsWith('npm:')) {
406
- // bun links a satisfied alias; npm and yarn fetch it from the
407
- // registry. The edge is the spare one, as above.
408
- const alias = ALIAS.exec(spec.slice('npm:'.length))
409
- return alias === null ? undefined : named(alias[1]!, alias[2] ?? '*')
410
- }
411
- for (const protocol of PATH_PROTOCOLS) {
412
- if (spec.startsWith(protocol)) return atPath(fromDir, spec.slice(protocol.length))
413
- }
414
- // A bare `./`, `../` or `/` path is a directory to bun, npm and pnpm.
415
- return atPath(fromDir, spec)
416
- }
417
- }