@packall/core 0.0.1 → 0.1.0
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/dist/{Archive.d.ts → archive.d.ts} +5 -5
- package/dist/archive.d.ts.map +1 -0
- package/dist/{Bundle.d.ts → bundle.d.ts} +51 -36
- package/dist/bundle.d.ts.map +1 -0
- package/dist/{DependencyRange.d.ts → dependency-range.d.ts} +2 -2
- package/dist/dependency-range.d.ts.map +1 -0
- package/dist/{Download.d.ts → download.d.ts} +7 -7
- package/dist/download.d.ts.map +1 -0
- package/dist/enums/artifact-kind.d.ts +8 -0
- package/dist/enums/artifact-kind.d.ts.map +1 -0
- package/dist/enums/edge-kind.d.ts +29 -0
- package/dist/enums/edge-kind.d.ts.map +1 -0
- package/dist/enums/input-file-kind.d.ts +12 -0
- package/dist/enums/input-file-kind.d.ts.map +1 -0
- package/dist/enums/layout.d.ts +23 -0
- package/dist/enums/layout.d.ts.map +1 -0
- package/dist/enums/lockfile-format.d.ts +9 -0
- package/dist/enums/lockfile-format.d.ts.map +1 -0
- package/dist/enums/phase.d.ts +11 -0
- package/dist/enums/phase.d.ts.map +1 -0
- package/dist/{Errors.d.ts → errors.d.ts} +49 -7
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +44 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2416 -1180
- package/dist/index.js.map +1 -1
- package/dist/input-file.d.ts +77 -0
- package/dist/input-file.d.ts.map +1 -0
- package/dist/{Integrity.d.ts → integrity.d.ts} +3 -3
- package/dist/integrity.d.ts.map +1 -0
- package/dist/{Layout.d.ts → layout.d.ts} +1 -1
- package/dist/{Layout.d.ts.map → layout.d.ts.map} +1 -1
- package/dist/locked-resolve.d.ts +47 -0
- package/dist/locked-resolve.d.ts.map +1 -0
- package/dist/lockfile/bun.d.ts +3 -0
- package/dist/lockfile/bun.d.ts.map +1 -0
- package/dist/lockfile/detect.d.ts +31 -0
- package/dist/lockfile/detect.d.ts.map +1 -0
- package/dist/lockfile/json.d.ts +23 -0
- package/dist/lockfile/json.d.ts.map +1 -0
- package/dist/lockfile/names.d.ts +16 -0
- package/dist/lockfile/names.d.ts.map +1 -0
- package/dist/lockfile/npm.d.ts +3 -0
- package/dist/lockfile/npm.d.ts.map +1 -0
- package/dist/lockfile/parse.d.ts +23 -0
- package/dist/lockfile/parse.d.ts.map +1 -0
- package/dist/lockfile/pnpm.d.ts +20 -0
- package/dist/lockfile/pnpm.d.ts.map +1 -0
- package/dist/lockfile/tree-builder.d.ts +70 -0
- package/dist/lockfile/tree-builder.d.ts.map +1 -0
- package/dist/lockfile/types.d.ts +70 -0
- package/dist/lockfile/types.d.ts.map +1 -0
- package/dist/{Manifest.d.ts → manifest.d.ts} +7 -15
- package/dist/manifest.d.ts.map +1 -0
- package/dist/{Options.d.ts → options.d.ts} +9 -27
- package/dist/options.d.ts.map +1 -0
- package/dist/{Platform.d.ts → platform.d.ts} +9 -9
- package/dist/platform.d.ts.map +1 -0
- package/dist/{Progress.d.ts → progress.d.ts} +10 -9
- package/dist/progress.d.ts.map +1 -0
- package/dist/{Registry.d.ts → registry.d.ts} +42 -44
- package/dist/registry.d.ts.map +1 -0
- package/dist/{Resolve.d.ts → resolve.d.ts} +25 -26
- package/dist/resolve.d.ts.map +1 -0
- package/dist/schemas/lenient.d.ts +38 -0
- package/dist/schemas/lenient.d.ts.map +1 -0
- package/dist/schemas/package-json.d.ts +44 -0
- package/dist/schemas/package-json.d.ts.map +1 -0
- package/dist/{Spec.d.ts → spec.d.ts} +4 -4
- package/dist/spec.d.ts.map +1 -0
- package/dist/types/value-of.d.ts +9 -0
- package/dist/types/value-of.d.ts.map +1 -0
- package/dist/utils/is-record.d.ts +8 -0
- package/dist/utils/is-record.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/{Archive.ts → archive.ts} +92 -92
- package/src/{Bundle.ts → bundle.ts} +616 -570
- package/src/{DependencyRange.ts → dependency-range.ts} +115 -115
- package/src/{Download.ts → download.ts} +133 -132
- package/src/enums/artifact-kind.ts +9 -0
- package/src/enums/edge-kind.ts +32 -0
- package/src/enums/input-file-kind.ts +13 -0
- package/src/enums/layout.ts +25 -0
- package/src/enums/lockfile-format.ts +10 -0
- package/src/enums/phase.ts +12 -0
- package/src/{Errors.ts → errors.ts} +87 -5
- package/src/index.ts +133 -35
- package/src/{InputFile.ts → input-file.ts} +127 -44
- package/src/{Integrity.ts → integrity.ts} +121 -122
- package/src/locked-resolve.ts +461 -0
- package/src/lockfile/bun.ts +207 -0
- package/src/lockfile/detect.ts +80 -0
- package/src/lockfile/json.ts +132 -0
- package/src/lockfile/names.ts +20 -0
- package/src/lockfile/npm.ts +308 -0
- package/src/lockfile/parse.ts +56 -0
- package/src/lockfile/pnpm.ts +225 -0
- package/src/lockfile/tree-builder.ts +166 -0
- package/src/lockfile/types.ts +74 -0
- package/src/{Manifest.ts → manifest.ts} +133 -133
- package/src/{Options.ts → options.ts} +7 -27
- package/src/{Platform.ts → platform.ts} +23 -23
- package/src/{Progress.ts → progress.ts} +106 -106
- package/src/{Registry.ts → registry.ts} +115 -121
- package/src/{Resolve.ts → resolve.ts} +537 -528
- package/src/schemas/lenient.ts +123 -0
- package/src/schemas/package-json.ts +27 -0
- package/src/{Spec.ts → spec.ts} +11 -19
- package/src/types/value-of.ts +8 -0
- package/src/utils/is-record.ts +8 -0
- package/dist/Archive.d.ts.map +0 -1
- package/dist/Bundle.d.ts.map +0 -1
- package/dist/DependencyRange.d.ts.map +0 -1
- package/dist/Download.d.ts.map +0 -1
- package/dist/Errors.d.ts.map +0 -1
- package/dist/InputFile.d.ts +0 -46
- package/dist/InputFile.d.ts.map +0 -1
- package/dist/Integrity.d.ts.map +0 -1
- package/dist/Manifest.d.ts.map +0 -1
- package/dist/Options.d.ts.map +0 -1
- package/dist/Platform.d.ts.map +0 -1
- package/dist/Progress.d.ts.map +0 -1
- package/dist/Registry.d.ts.map +0 -1
- package/dist/Resolve.d.ts.map +0 -1
- package/dist/Spec.d.ts.map +0 -1
- /package/src/{Layout.ts → layout.ts} +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution driven by a lockfile instead of by semver.
|
|
3
|
+
*
|
|
4
|
+
* The distinction matters more than it first looks. Feeding a lockfile's
|
|
5
|
+
* versions in as exact root specs and letting the ordinary walk run would still
|
|
6
|
+
* produce the wrong bundle: every *transitive* edge is a range, and
|
|
7
|
+
* `Resolve.walk` answers a range with whatever is newest today. Lock `tslib` at
|
|
8
|
+
* 2.6.0 and let something depending on `tslib@^2` be re-resolved, and the
|
|
9
|
+
* bundle grows a second copy at 2.8.1 that CI never installed.
|
|
10
|
+
*
|
|
11
|
+
* So the lockfile is treated as the graph, not as a set of hints. The package
|
|
12
|
+
* set *is* what the file pins, and every edge is satisfied from inside it.
|
|
13
|
+
* Nothing here calls `selectVersions`; the only thing the registry is asked for
|
|
14
|
+
* is the metadata that a lockfile does not record — `dist`, and the `os`/`cpu`
|
|
15
|
+
* constraints that `--platform` narrows on.
|
|
16
|
+
*
|
|
17
|
+
* The result is an ordinary `Resolution`, so layout, archiving, the manifest
|
|
18
|
+
* and the summary cannot tell which path produced it.
|
|
19
|
+
*/
|
|
20
|
+
import * as Effect from "effect/Effect"
|
|
21
|
+
import semver from "semver"
|
|
22
|
+
import type { EdgeKind } from "./enums/edge-kind.js"
|
|
23
|
+
import { LockedRootKind } from "./enums/edge-kind.js"
|
|
24
|
+
import { Phase } from "./enums/phase.js"
|
|
25
|
+
import type { BundlerError } from "./errors.js"
|
|
26
|
+
import { LockfileIncompleteError, LockfileOutOfDateError } from "./errors.js"
|
|
27
|
+
import type { LockedRoot, LockedTree } from "./lockfile/types.js"
|
|
28
|
+
import type { ResolveOptions } from "./options.js"
|
|
29
|
+
import { isIncluded } from "./platform.js"
|
|
30
|
+
import * as Progress from "./progress.js"
|
|
31
|
+
import type { PackageManifest } from "./registry.js"
|
|
32
|
+
import { Registry } from "./registry.js"
|
|
33
|
+
import type {
|
|
34
|
+
PackageKey,
|
|
35
|
+
Reason,
|
|
36
|
+
Resolution,
|
|
37
|
+
ResolutionWarning,
|
|
38
|
+
ResolvedPackage,
|
|
39
|
+
RootResolution
|
|
40
|
+
} from "./resolve.js"
|
|
41
|
+
import { packageKey } from "./resolve.js"
|
|
42
|
+
import type { PackageSpec } from "./spec.js"
|
|
43
|
+
import { formatSpec } from "./spec.js"
|
|
44
|
+
|
|
45
|
+
/** Options for a lockfile-pinned resolution. */
|
|
46
|
+
export type LockedResolveOptions = ResolveOptions & {
|
|
47
|
+
/**
|
|
48
|
+
* Include the project's `devDependencies`.
|
|
49
|
+
*
|
|
50
|
+
* Matches `--file package.json`: on by default, because a project without its
|
|
51
|
+
* dev tooling does not build behind the firewall either.
|
|
52
|
+
*/
|
|
53
|
+
readonly includeDev: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Direct dependency names declared by the accompanying package.json.
|
|
56
|
+
*
|
|
57
|
+
* When present, anything declared there but absent from the lockfile's roots
|
|
58
|
+
* means the two have drifted — which is exactly the silent-mismatch case this
|
|
59
|
+
* whole feature exists to catch, so it is reported rather than papered over.
|
|
60
|
+
*/
|
|
61
|
+
readonly declared?: ReadonlyArray<string> | undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Turns a parsed lockfile into a resolution, pinning every version. */
|
|
65
|
+
export const resolveLocked = (
|
|
66
|
+
lock: LockedTree,
|
|
67
|
+
options: LockedResolveOptions
|
|
68
|
+
): Effect.Effect<Resolution, BundlerError, Registry | Progress.Progress> =>
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const roots = selectRoots(lock, options)
|
|
71
|
+
|
|
72
|
+
yield* Progress.emit({ _tag: "PhaseStarted", phase: Phase.Resolve, total: roots.length })
|
|
73
|
+
|
|
74
|
+
const gap = findGaps(lock, roots, options.declared)
|
|
75
|
+
if (gap !== null) return yield* Effect.fail(gap)
|
|
76
|
+
|
|
77
|
+
// Everything the lockfile can reach, before the platform filter has had a
|
|
78
|
+
// say. Cheap — no network at all — and it is what decides the fetch list.
|
|
79
|
+
const reachable = reachableKeys(lock, roots, options)
|
|
80
|
+
const manifests = yield* fetchManifests(lock, reachable, options)
|
|
81
|
+
|
|
82
|
+
const warnings: Array<ResolutionWarning> = lock.warnings.map((message) => ({ message }))
|
|
83
|
+
const resolved = new Map<PackageKey, ResolvedPackage>()
|
|
84
|
+
const seenWarnings = new Set<string>()
|
|
85
|
+
|
|
86
|
+
const addWarning = (warning: ResolutionWarning): void => {
|
|
87
|
+
const key = `${warning.from ?? ""}|${warning.message}`
|
|
88
|
+
if (seenWarnings.has(key)) return
|
|
89
|
+
seenWarnings.add(key)
|
|
90
|
+
warnings.push(warning)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rootResolutions: Array<RootResolution> = []
|
|
94
|
+
|
|
95
|
+
for (const root of roots) {
|
|
96
|
+
const key = packageKey(root.name, root.version)
|
|
97
|
+
if (!manifests.has(key)) continue
|
|
98
|
+
|
|
99
|
+
const spec: PackageSpec = {
|
|
100
|
+
name: root.name,
|
|
101
|
+
selector: { _tag: "Exact", version: root.version },
|
|
102
|
+
raw: `${root.name}@${root.version}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Platform narrowing applies to optional roots for the same reason it
|
|
106
|
+
// applies to optional edges: a native binding for another OS is exactly
|
|
107
|
+
// what --platform is there to drop.
|
|
108
|
+
if (root.kind === LockedRootKind.Optional && !platformAllows(manifests, key, options)) continue
|
|
109
|
+
|
|
110
|
+
const closure = walkLocked({
|
|
111
|
+
lock,
|
|
112
|
+
manifests,
|
|
113
|
+
options,
|
|
114
|
+
seed: key,
|
|
115
|
+
rootSpec: formatSpec(spec),
|
|
116
|
+
resolved,
|
|
117
|
+
addWarning
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
rootResolutions.push({
|
|
121
|
+
spec,
|
|
122
|
+
versions: [root.version],
|
|
123
|
+
closures: new Map([[root.version, closure]]),
|
|
124
|
+
closure
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const pkg of resolved.values()) {
|
|
129
|
+
const deprecated = pkg.manifest.deprecated
|
|
130
|
+
if (deprecated !== undefined && deprecated !== "") {
|
|
131
|
+
addWarning({
|
|
132
|
+
from: packageKey(pkg.name, pkg.version),
|
|
133
|
+
message: `${packageKey(pkg.name, pkg.version)} is deprecated: ${deprecated}`
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
yield* Progress.emit({ _tag: "PhaseCompleted", phase: Phase.Resolve })
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
roots: rootResolutions,
|
|
142
|
+
packages: [...resolved.values()].toSorted(compareResolved),
|
|
143
|
+
warnings
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/* -------------------------------------------------------------------------- */
|
|
148
|
+
/* Roots */
|
|
149
|
+
/* -------------------------------------------------------------------------- */
|
|
150
|
+
|
|
151
|
+
/** Which of the project's direct dependencies this run should bundle. */
|
|
152
|
+
const selectRoots = (
|
|
153
|
+
lock: LockedTree,
|
|
154
|
+
options: LockedResolveOptions
|
|
155
|
+
): ReadonlyArray<LockedRoot> =>
|
|
156
|
+
lock.roots
|
|
157
|
+
.filter((root) => {
|
|
158
|
+
if (root.kind === LockedRootKind.Dev) return options.includeDev
|
|
159
|
+
if (root.kind === LockedRootKind.Optional) return options.scope.optional
|
|
160
|
+
if (root.kind === LockedRootKind.Peer) return options.scope.peer
|
|
161
|
+
return true
|
|
162
|
+
})
|
|
163
|
+
// Sorted so the artifact order is a property of the dependency set rather
|
|
164
|
+
// than of which manager wrote the file.
|
|
165
|
+
.toSorted((a, b) => (a.name === b.name ? 0 : a.name < b.name ? -1 : 1))
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The two ways a lockfile can be partial, checked before anything is fetched.
|
|
169
|
+
*
|
|
170
|
+
* Both mean the same thing to whoever is waiting on the bundle — a package they
|
|
171
|
+
* expected will not be in it — and both are invisible unless something says so,
|
|
172
|
+
* because the run otherwise succeeds and simply produces less.
|
|
173
|
+
*/
|
|
174
|
+
const findGaps = (
|
|
175
|
+
lock: LockedTree,
|
|
176
|
+
roots: ReadonlyArray<LockedRoot>,
|
|
177
|
+
declared: ReadonlyArray<string> | undefined
|
|
178
|
+
): LockfileIncompleteError | null => {
|
|
179
|
+
const remedy = `Run \`${lock.format} install\` to bring the lockfile up to date.`
|
|
180
|
+
|
|
181
|
+
// A dependency added to package.json without re-running the install. The
|
|
182
|
+
// commonest way the two drift, and the cheapest to detect.
|
|
183
|
+
//
|
|
184
|
+
// Checked against everything the file pins rather than against its roots,
|
|
185
|
+
// because the managers disagree about which blocks appear in the root entry —
|
|
186
|
+
// pnpm's importers omit peers, npm's `packages[""]` includes them — and a
|
|
187
|
+
// package that is pinned somewhere will be in the bundle either way.
|
|
188
|
+
if (declared !== undefined && declared.length > 0) {
|
|
189
|
+
const locked = new Set<string>()
|
|
190
|
+
for (const pkg of lock.packages.values()) locked.add(pkg.name)
|
|
191
|
+
for (const root of roots) locked.add(root.name)
|
|
192
|
+
|
|
193
|
+
const missing = declared.filter((name) => !locked.has(name))
|
|
194
|
+
if (missing.length > 0) {
|
|
195
|
+
return new LockfileIncompleteError(
|
|
196
|
+
lock.path,
|
|
197
|
+
missing,
|
|
198
|
+
`the package.json declares ${missing.length} ` +
|
|
199
|
+
`dependenc${missing.length === 1 ? "y" : "ies"} it does not pin`,
|
|
200
|
+
remedy
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// A required edge inside the file pointing at nothing the file pins.
|
|
206
|
+
if (lock.incomplete.length > 0) {
|
|
207
|
+
return new LockfileIncompleteError(
|
|
208
|
+
lock.path,
|
|
209
|
+
lock.incomplete,
|
|
210
|
+
`${lock.incomplete.length} required ` +
|
|
211
|
+
`dependenc${lock.incomplete.length === 1 ? "y is" : "ies are"} not pinned`,
|
|
212
|
+
remedy
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* -------------------------------------------------------------------------- */
|
|
220
|
+
/* Reachability */
|
|
221
|
+
/* -------------------------------------------------------------------------- */
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Every package the roots can reach through the locked edges.
|
|
225
|
+
*
|
|
226
|
+
* Scope filters apply here — `--no-optional` genuinely removes a subtree — but
|
|
227
|
+
* the platform filter does not, because deciding it needs the `os`/`cpu` fields
|
|
228
|
+
* that only the registry has.
|
|
229
|
+
*/
|
|
230
|
+
const reachableKeys = (
|
|
231
|
+
lock: LockedTree,
|
|
232
|
+
roots: ReadonlyArray<LockedRoot>,
|
|
233
|
+
options: LockedResolveOptions
|
|
234
|
+
): ReadonlySet<PackageKey> => {
|
|
235
|
+
const seen = new Set<PackageKey>()
|
|
236
|
+
const queue = roots.map((root) => packageKey(root.name, root.version))
|
|
237
|
+
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const key = queue.pop()
|
|
240
|
+
if (key === undefined || seen.has(key)) continue
|
|
241
|
+
seen.add(key)
|
|
242
|
+
|
|
243
|
+
for (const edge of lock.packages.get(key)?.dependencies ?? []) {
|
|
244
|
+
if (!followsEdge(edge.kind, options)) continue
|
|
245
|
+
queue.push(packageKey(edge.name, edge.version))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return seen
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const followsEdge = (kind: EdgeKind, options: LockedResolveOptions): boolean => {
|
|
253
|
+
if (kind === LockedRootKind.Optional) return options.scope.optional
|
|
254
|
+
if (kind === LockedRootKind.Peer) return options.scope.peer
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* -------------------------------------------------------------------------- */
|
|
259
|
+
/* Metadata */
|
|
260
|
+
/* -------------------------------------------------------------------------- */
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Fetches the published manifest for every pinned version.
|
|
264
|
+
*
|
|
265
|
+
* One packument per distinct *name*, not per version — a lockfile holding four
|
|
266
|
+
* versions of `tslib` costs one request. Every pinned version that the registry
|
|
267
|
+
* does not serve is collected and reported together, because "refresh your
|
|
268
|
+
* lockfile" is a single action and finding out about the gaps one run at a time
|
|
269
|
+
* would be miserable.
|
|
270
|
+
*/
|
|
271
|
+
const fetchManifests = (
|
|
272
|
+
lock: LockedTree,
|
|
273
|
+
reachable: ReadonlySet<PackageKey>,
|
|
274
|
+
options: LockedResolveOptions
|
|
275
|
+
): Effect.Effect<
|
|
276
|
+
ReadonlyMap<PackageKey, PackageManifest>,
|
|
277
|
+
BundlerError,
|
|
278
|
+
Registry | Progress.Progress
|
|
279
|
+
> =>
|
|
280
|
+
Effect.gen(function* () {
|
|
281
|
+
const registry = yield* Registry
|
|
282
|
+
|
|
283
|
+
const wanted = new Map<string, Array<string>>()
|
|
284
|
+
for (const key of reachable) {
|
|
285
|
+
const pkg = lock.packages.get(key)
|
|
286
|
+
if (pkg === undefined) continue
|
|
287
|
+
const versions = wanted.get(pkg.name)
|
|
288
|
+
if (versions === undefined) wanted.set(pkg.name, [pkg.version])
|
|
289
|
+
else versions.push(pkg.version)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const manifests = new Map<PackageKey, PackageManifest>()
|
|
293
|
+
const missing: Array<{ name: string; version: string; detail: string }> = []
|
|
294
|
+
|
|
295
|
+
const fetched = yield* Effect.forEach(
|
|
296
|
+
[...wanted.entries()],
|
|
297
|
+
([name, versions]) =>
|
|
298
|
+
registry.packument(name).pipe(
|
|
299
|
+
Effect.map((packument) => ({ name, versions, packument })),
|
|
300
|
+
// A package the registry has never heard of is the same problem as a
|
|
301
|
+
// version it no longer serves, and belongs in the same report.
|
|
302
|
+
Effect.catch((error) =>
|
|
303
|
+
error._tag === "PackageNotFoundError"
|
|
304
|
+
? Effect.succeed({ name, versions, packument: null })
|
|
305
|
+
: Effect.fail(error)
|
|
306
|
+
)
|
|
307
|
+
),
|
|
308
|
+
{ concurrency: options.concurrency }
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
for (const { name, versions, packument } of fetched) {
|
|
312
|
+
for (const version of versions) {
|
|
313
|
+
if (packument === null) {
|
|
314
|
+
missing.push({ name, version, detail: "package not found" })
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const manifest = packument.versions[version]
|
|
319
|
+
if (manifest === undefined) {
|
|
320
|
+
missing.push({ name, version, detail: describeAvailable(Object.keys(packument.versions)) })
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
manifests.set(packageKey(name, version), manifest)
|
|
325
|
+
yield* Progress.emit({
|
|
326
|
+
_tag: "PackageResolved",
|
|
327
|
+
name,
|
|
328
|
+
version,
|
|
329
|
+
resolvedCount: manifests.size,
|
|
330
|
+
pendingCount: reachable.size - manifests.size
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (missing.length > 0) {
|
|
336
|
+
return yield* Effect.fail(
|
|
337
|
+
new LockfileOutOfDateError(lock.path, registry.registryFor(missing[0]?.name ?? ""), missing)
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return manifests
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
const describeAvailable = (available: ReadonlyArray<string>): string => {
|
|
345
|
+
const recent = available
|
|
346
|
+
.toSorted((a, b) =>
|
|
347
|
+
semver.valid(a) && semver.valid(b) ? semver.compare(a, b) : a.localeCompare(b)
|
|
348
|
+
)
|
|
349
|
+
.slice(-3)
|
|
350
|
+
return recent.length === 0
|
|
351
|
+
? "no published versions"
|
|
352
|
+
: `no longer published (latest: ${recent.join(", ")})`
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* -------------------------------------------------------------------------- */
|
|
356
|
+
/* The walk */
|
|
357
|
+
/* -------------------------------------------------------------------------- */
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Collects one root's closure by following locked edges.
|
|
361
|
+
*
|
|
362
|
+
* Pure and synchronous — every manifest is already in hand — so a per-root
|
|
363
|
+
* closure costs nothing beyond the traversal, which is what makes `per-spec`
|
|
364
|
+
* layout as cheap here as it is for a range-resolved run.
|
|
365
|
+
*/
|
|
366
|
+
const walkLocked = (input: {
|
|
367
|
+
readonly lock: LockedTree
|
|
368
|
+
readonly manifests: ReadonlyMap<PackageKey, PackageManifest>
|
|
369
|
+
readonly options: LockedResolveOptions
|
|
370
|
+
readonly seed: PackageKey
|
|
371
|
+
readonly rootSpec: string
|
|
372
|
+
readonly resolved: Map<PackageKey, ResolvedPackage>
|
|
373
|
+
readonly addWarning: (warning: ResolutionWarning) => void
|
|
374
|
+
}): ReadonlyArray<PackageKey> => {
|
|
375
|
+
const closure = new Set<PackageKey>()
|
|
376
|
+
const queue: Array<{ key: PackageKey; reason: Reason }> = [
|
|
377
|
+
{ key: input.seed, reason: { _tag: "Root", spec: input.rootSpec } }
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
while (queue.length > 0) {
|
|
381
|
+
const next = queue.pop()
|
|
382
|
+
if (next === undefined) continue
|
|
383
|
+
|
|
384
|
+
const manifest = input.manifests.get(next.key)
|
|
385
|
+
const locked = input.lock.packages.get(next.key)
|
|
386
|
+
if (manifest === undefined || locked === undefined) continue
|
|
387
|
+
|
|
388
|
+
record(input.resolved, next.key, manifest, locked, next.reason)
|
|
389
|
+
|
|
390
|
+
if (closure.has(next.key)) continue
|
|
391
|
+
closure.add(next.key)
|
|
392
|
+
|
|
393
|
+
for (const edge of locked.dependencies) {
|
|
394
|
+
if (!followsEdge(edge.kind, input.options)) continue
|
|
395
|
+
|
|
396
|
+
const target = packageKey(edge.name, edge.version)
|
|
397
|
+
if (!input.manifests.has(target)) continue
|
|
398
|
+
|
|
399
|
+
if (edge.kind === LockedRootKind.Optional && !platformAllows(input.manifests, target, input.options)) {
|
|
400
|
+
input.addWarning({
|
|
401
|
+
from: next.key,
|
|
402
|
+
message:
|
|
403
|
+
`Skipped optional ${target} — it does not build for the selected platforms.`
|
|
404
|
+
})
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
queue.push({ key: target, reason: { _tag: "Edge", from: next.key, kind: edge.kind } })
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [...closure]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Adds a package, or merges one more justification into an existing entry. */
|
|
416
|
+
const record = (
|
|
417
|
+
resolved: Map<PackageKey, ResolvedPackage>,
|
|
418
|
+
key: PackageKey,
|
|
419
|
+
manifest: PackageManifest,
|
|
420
|
+
locked: { readonly name: string; readonly version: string },
|
|
421
|
+
reason: Reason
|
|
422
|
+
): void => {
|
|
423
|
+
const existing = resolved.get(key)
|
|
424
|
+
if (existing === undefined) {
|
|
425
|
+
resolved.set(key, {
|
|
426
|
+
name: locked.name,
|
|
427
|
+
version: locked.version,
|
|
428
|
+
manifest,
|
|
429
|
+
reasons: [reason]
|
|
430
|
+
})
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
if (hasReason(existing.reasons, reason)) return
|
|
434
|
+
resolved.set(key, { ...existing, reasons: [...existing.reasons, reason] })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const hasReason = (reasons: ReadonlyArray<Reason>, candidate: Reason): boolean =>
|
|
438
|
+
reasons.some((reason) => {
|
|
439
|
+
if (reason._tag === "Root" && candidate._tag === "Root") return reason.spec === candidate.spec
|
|
440
|
+
if (reason._tag === "Edge" && candidate._tag === "Edge") {
|
|
441
|
+
return reason.from === candidate.from && reason.kind === candidate.kind
|
|
442
|
+
}
|
|
443
|
+
return false
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const platformAllows = (
|
|
447
|
+
manifests: ReadonlyMap<PackageKey, PackageManifest>,
|
|
448
|
+
key: PackageKey,
|
|
449
|
+
options: LockedResolveOptions
|
|
450
|
+
): boolean => {
|
|
451
|
+
const manifest = manifests.get(key)
|
|
452
|
+
if (manifest === undefined) return false
|
|
453
|
+
return isIncluded(manifest, options.scope.platforms)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const compareResolved = (a: ResolvedPackage, b: ResolvedPackage): number => {
|
|
457
|
+
if (a.name !== b.name) return a.name < b.name ? -1 : 1
|
|
458
|
+
return semver.valid(a.version) && semver.valid(b.version)
|
|
459
|
+
? semver.compare(a.version, b.version)
|
|
460
|
+
: a.version.localeCompare(b.version)
|
|
461
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun.lock`.
|
|
3
|
+
*
|
|
4
|
+
* Like npm, bun records ranges on its edges and resolves them through the
|
|
5
|
+
* install-path nesting — but it nests with a plain `/`, and package names
|
|
6
|
+
* contain `/` too, so the parent of a key can only be found by asking which
|
|
7
|
+
* prefixes are themselves entries.
|
|
8
|
+
*/
|
|
9
|
+
import { EdgeKind, LockedRootKind } from "../enums/edge-kind.js"
|
|
10
|
+
import { LockfileFormat } from "../enums/lockfile-format.js"
|
|
11
|
+
import { optionalPeerNames, parseJsonObject, stringRecord } from "./json.js"
|
|
12
|
+
import { isRecord } from "../utils/is-record.js"
|
|
13
|
+
import { asExactVersion, fieldByRootKind, importerKey, notCovered, TreeBuilder } from "./tree-builder.js"
|
|
14
|
+
import type { LockedEdge, LockedTree } from "./types.js"
|
|
15
|
+
|
|
16
|
+
/** One narrowed `packages` entry, keyed by its install path. */
|
|
17
|
+
type BunEntry = {
|
|
18
|
+
readonly key: string
|
|
19
|
+
readonly name: string
|
|
20
|
+
readonly version: string
|
|
21
|
+
readonly dependencies: Readonly<Record<string, string>>
|
|
22
|
+
readonly optionalDependencies: Readonly<Record<string, string>>
|
|
23
|
+
readonly peerDependencies: Readonly<Record<string, string>>
|
|
24
|
+
readonly optionalPeers: ReadonlySet<string>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Root blocks in bun's precedence order — the first mention of a name wins. */
|
|
28
|
+
const ROOT_KINDS = [
|
|
29
|
+
LockedRootKind.Prod,
|
|
30
|
+
LockedRootKind.Optional,
|
|
31
|
+
LockedRootKind.Dev,
|
|
32
|
+
LockedRootKind.Peer
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
export const parseBunLockfile = (
|
|
36
|
+
path: string,
|
|
37
|
+
content: string,
|
|
38
|
+
importer: string | undefined
|
|
39
|
+
): LockedTree => {
|
|
40
|
+
const root = parseJsonObject(path, content)
|
|
41
|
+
const version = String(root["lockfileVersion"] ?? "?")
|
|
42
|
+
const builder = new TreeBuilder()
|
|
43
|
+
|
|
44
|
+
const packages = root["packages"]
|
|
45
|
+
const entries = isRecord(packages)
|
|
46
|
+
? collectEntries(builder, packages)
|
|
47
|
+
: new Map<string, BunEntry>()
|
|
48
|
+
|
|
49
|
+
const keys = new Set(entries.keys())
|
|
50
|
+
for (const entry of entries.values()) {
|
|
51
|
+
builder.add({
|
|
52
|
+
name: entry.name,
|
|
53
|
+
version: entry.version,
|
|
54
|
+
dependencies: edgesOf(builder, entries, keys, entry)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
collectRoots(builder, entries, keys, root["workspaces"], path, importer)
|
|
59
|
+
|
|
60
|
+
return builder.finish({ format: LockfileFormat.Bun, lockfileVersion: version, path, importer })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Narrows bun's tuple entries.
|
|
65
|
+
*
|
|
66
|
+
* The value is a positional array whose shape depends on where the package came
|
|
67
|
+
* from. A registry package is `[id, registry, info, integrity]`; a workspace
|
|
68
|
+
* member or a git dependency is shorter and its `id` carries the protocol.
|
|
69
|
+
*/
|
|
70
|
+
const collectEntries = (
|
|
71
|
+
builder: TreeBuilder,
|
|
72
|
+
packages: Record<string, unknown>
|
|
73
|
+
): Map<string, BunEntry> => {
|
|
74
|
+
const entries = new Map<string, BunEntry>()
|
|
75
|
+
|
|
76
|
+
for (const [key, raw] of Object.entries(packages)) {
|
|
77
|
+
if (!Array.isArray(raw)) continue
|
|
78
|
+
|
|
79
|
+
const [id, , info] = raw
|
|
80
|
+
if (typeof id !== "string") continue
|
|
81
|
+
|
|
82
|
+
const at = id.lastIndexOf("@")
|
|
83
|
+
if (at <= 0) continue
|
|
84
|
+
const name = id.slice(0, at)
|
|
85
|
+
const spec = id.slice(at + 1)
|
|
86
|
+
|
|
87
|
+
const version = asExactVersion(name, spec)
|
|
88
|
+
if (version._tag === "Unsupported") {
|
|
89
|
+
builder.skip(name, spec, version.reason)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const details = isRecord(info) ? info : {}
|
|
94
|
+
|
|
95
|
+
entries.set(key, {
|
|
96
|
+
key,
|
|
97
|
+
name,
|
|
98
|
+
version: version.version,
|
|
99
|
+
dependencies: stringRecord(details["dependencies"]),
|
|
100
|
+
optionalDependencies: stringRecord(details["optionalDependencies"]),
|
|
101
|
+
peerDependencies: stringRecord(details["peerDependencies"]),
|
|
102
|
+
optionalPeers: collectOptionalPeers(details)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return entries
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** bun lists optional peers as a name array rather than npm's metadata object. */
|
|
110
|
+
const collectOptionalPeers = (details: Record<string, unknown>): ReadonlySet<string> => {
|
|
111
|
+
const listed = details["optionalPeers"]
|
|
112
|
+
const names = Array.isArray(listed)
|
|
113
|
+
? listed.flatMap((entry) => (typeof entry === "string" ? entry : []))
|
|
114
|
+
: []
|
|
115
|
+
return new Set([...names, ...optionalPeerNames(details["peerDependenciesMeta"])])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const edgesOf = (
|
|
119
|
+
builder: TreeBuilder,
|
|
120
|
+
entries: ReadonlyMap<string, BunEntry>,
|
|
121
|
+
keys: ReadonlySet<string>,
|
|
122
|
+
entry: BunEntry
|
|
123
|
+
): ReadonlyArray<LockedEdge> => {
|
|
124
|
+
const edges: Array<LockedEdge> = []
|
|
125
|
+
|
|
126
|
+
const add = (names: ReadonlyArray<string>, kind: EdgeKind): void => {
|
|
127
|
+
for (const name of names) {
|
|
128
|
+
const target = findEntry(entries, keys, entry.key, name)
|
|
129
|
+
if (target === undefined) {
|
|
130
|
+
if (kind === EdgeKind.Prod) builder.dangling(`${entry.name}@${entry.version}`, name)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
edges.push({ name: target.name, version: target.version, kind })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
add(Object.keys(entry.dependencies), EdgeKind.Prod)
|
|
138
|
+
add(Object.keys(entry.optionalDependencies), EdgeKind.Optional)
|
|
139
|
+
add(
|
|
140
|
+
Object.keys(entry.peerDependencies).filter((name) => !entry.optionalPeers.has(name)),
|
|
141
|
+
EdgeKind.Peer
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return edges
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const collectRoots = (
|
|
148
|
+
builder: TreeBuilder,
|
|
149
|
+
entries: ReadonlyMap<string, BunEntry>,
|
|
150
|
+
keys: ReadonlySet<string>,
|
|
151
|
+
workspaces: unknown,
|
|
152
|
+
path: string,
|
|
153
|
+
member: string | undefined
|
|
154
|
+
): void => {
|
|
155
|
+
if (!isRecord(workspaces)) return
|
|
156
|
+
|
|
157
|
+
const selected =
|
|
158
|
+
member === undefined
|
|
159
|
+
? Object.values(workspaces)
|
|
160
|
+
: Object.hasOwn(workspaces, importerKey(member))
|
|
161
|
+
? [workspaces[importerKey(member)]]
|
|
162
|
+
: notCovered(path, member, workspacePaths(workspaces))
|
|
163
|
+
|
|
164
|
+
for (const workspace of selected) {
|
|
165
|
+
if (!isRecord(workspace)) continue
|
|
166
|
+
for (const kind of ROOT_KINDS) {
|
|
167
|
+
const declared = stringRecord(workspace[fieldByRootKind[kind]])
|
|
168
|
+
for (const [name, specifier] of Object.entries(declared)) {
|
|
169
|
+
const target = findEntry(entries, keys, "", name)
|
|
170
|
+
if (target === undefined) continue
|
|
171
|
+
builder.addRoot({ name: target.name, version: target.version, kind, specifier })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const workspacePaths = (workspaces: Record<string, unknown>): ReadonlyArray<string> =>
|
|
178
|
+
Object.keys(workspaces).map((key) => (key === "" ? "." : key))
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* bun nests with a plain `/`, and package names contain `/` too, so a parent
|
|
182
|
+
* key cannot be found by string-splitting alone — `@babel/core` would split
|
|
183
|
+
* into a scope that is not a key. `parentKey` only accepts a prefix that is
|
|
184
|
+
* itself an entry, which resolves the ambiguity.
|
|
185
|
+
*/
|
|
186
|
+
const findEntry = (
|
|
187
|
+
entries: ReadonlyMap<string, BunEntry>,
|
|
188
|
+
keys: ReadonlySet<string>,
|
|
189
|
+
fromKey: string,
|
|
190
|
+
name: string
|
|
191
|
+
): BunEntry | undefined => {
|
|
192
|
+
let scope = fromKey
|
|
193
|
+
for (;;) {
|
|
194
|
+
const found = entries.get(scope === "" ? name : `${scope}/${name}`)
|
|
195
|
+
if (found !== undefined) return found
|
|
196
|
+
if (scope === "") return undefined
|
|
197
|
+
scope = parentKey(keys, scope)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const parentKey = (keys: ReadonlySet<string>, key: string): string => {
|
|
202
|
+
for (let at = key.lastIndexOf("/"); at > 0; at = key.lastIndexOf("/", at - 1)) {
|
|
203
|
+
const prefix = key.slice(0, at)
|
|
204
|
+
if (keys.has(prefix)) return prefix
|
|
205
|
+
}
|
|
206
|
+
return ""
|
|
207
|
+
}
|