@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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm-lock.yaml`.
|
|
3
|
+
*
|
|
4
|
+
* The easy one: pnpm's snapshots already name exact versions, so there is no
|
|
5
|
+
* nesting walk-up to reimplement. The fiddly parts are the three package-key
|
|
6
|
+
* encodings still in the wild and the parenthesised peer-resolution suffix.
|
|
7
|
+
*/
|
|
8
|
+
import semver from "semver"
|
|
9
|
+
import { parse as parseYaml } from "yaml"
|
|
10
|
+
import { parseDependencyTarget } from "../dependency-range.js"
|
|
11
|
+
import { EdgeKind, LockedRootKind } from "../enums/edge-kind.js"
|
|
12
|
+
import { LockfileFormat } from "../enums/lockfile-format.js"
|
|
13
|
+
import { LockfileError } from "../errors.js"
|
|
14
|
+
import { describeError } from "./json.js"
|
|
15
|
+
import { isRecord } from "../utils/is-record.js"
|
|
16
|
+
import { fieldByRootKind, notCovered, TreeBuilder } from "./tree-builder.js"
|
|
17
|
+
import type { LockedEdge, LockedTree } from "./types.js"
|
|
18
|
+
|
|
19
|
+
/** `name@version`, as a snapshot or importer entry resolves to. */
|
|
20
|
+
type PnpmReference = {
|
|
21
|
+
readonly name: string
|
|
22
|
+
readonly version: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** pnpm never records peer dependencies against an importer. */
|
|
26
|
+
const ROOT_KINDS = [
|
|
27
|
+
LockedRootKind.Prod,
|
|
28
|
+
LockedRootKind.Optional,
|
|
29
|
+
LockedRootKind.Dev
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
export const parsePnpmLockfile = (
|
|
33
|
+
path: string,
|
|
34
|
+
content: string,
|
|
35
|
+
importer: string | undefined
|
|
36
|
+
): LockedTree => {
|
|
37
|
+
let root: unknown
|
|
38
|
+
try {
|
|
39
|
+
root = parseYaml(content)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new LockfileError(path, `not valid YAML — ${describeError(error)}`, { cause: error })
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(root)) {
|
|
44
|
+
throw new LockfileError(path, "top level is not a mapping")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const version = String(root["lockfileVersion"] ?? "?")
|
|
48
|
+
const builder = new TreeBuilder()
|
|
49
|
+
|
|
50
|
+
// v9 split the graph in two: `packages` keeps resolution metadata, and
|
|
51
|
+
// `snapshots` keeps the edges. Before that both lived in `packages`.
|
|
52
|
+
const packages = isRecord(root["packages"]) ? root["packages"] : {}
|
|
53
|
+
const snapshots = isRecord(root["snapshots"]) ? root["snapshots"] : packages
|
|
54
|
+
|
|
55
|
+
const known = new Map<string, PnpmReference>()
|
|
56
|
+
for (const key of Object.keys(packages)) {
|
|
57
|
+
const parsed = parsePnpmKey(key)
|
|
58
|
+
if (parsed === null) continue
|
|
59
|
+
known.set(key, parsed)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const [key, raw] of Object.entries(snapshots)) {
|
|
63
|
+
const parsed = parsePnpmKey(key) ?? known.get(key)
|
|
64
|
+
if (parsed === undefined || parsed === null) {
|
|
65
|
+
const described = describeKey(key)
|
|
66
|
+
if (described !== null) builder.skip(described.name, described.spec, described.reason)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
builder.add({
|
|
70
|
+
name: parsed.name,
|
|
71
|
+
version: parsed.version,
|
|
72
|
+
dependencies: edgesOf(builder, parsed, isRecord(raw) ? raw : {})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
collectRoots(builder, root, path, importer)
|
|
77
|
+
|
|
78
|
+
return builder.finish({ format: LockfileFormat.Pnpm, lockfileVersion: version, path, importer })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const edgesOf = (
|
|
82
|
+
builder: TreeBuilder,
|
|
83
|
+
from: PnpmReference,
|
|
84
|
+
snapshot: Record<string, unknown>
|
|
85
|
+
): ReadonlyArray<LockedEdge> => {
|
|
86
|
+
const edges: Array<LockedEdge> = []
|
|
87
|
+
|
|
88
|
+
const add = (field: string, kind: EdgeKind): void => {
|
|
89
|
+
const entries = snapshot[field]
|
|
90
|
+
if (!isRecord(entries)) return
|
|
91
|
+
for (const [name, raw] of Object.entries(entries)) {
|
|
92
|
+
if (typeof raw !== "string") continue
|
|
93
|
+
const target = parsePnpmReference(name, raw)
|
|
94
|
+
if (target === null) {
|
|
95
|
+
if (kind === EdgeKind.Prod) builder.dangling(`${from.name}@${from.version}`, name)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
edges.push({ ...target, kind })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
add(fieldByRootKind[LockedRootKind.Prod], EdgeKind.Prod)
|
|
103
|
+
add(fieldByRootKind[LockedRootKind.Optional], EdgeKind.Optional)
|
|
104
|
+
add(fieldByRootKind[LockedRootKind.Peer], EdgeKind.Peer)
|
|
105
|
+
|
|
106
|
+
return edges
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The importers' direct dependencies become the roots.
|
|
111
|
+
*
|
|
112
|
+
* pnpm is the format that makes this easy: `importers` is already keyed by the
|
|
113
|
+
* path each package.json sits at, so scoping to one member is a lookup rather
|
|
114
|
+
* than a reconstruction.
|
|
115
|
+
*/
|
|
116
|
+
const collectRoots = (
|
|
117
|
+
builder: TreeBuilder,
|
|
118
|
+
root: Record<string, unknown>,
|
|
119
|
+
path: string,
|
|
120
|
+
member: string | undefined
|
|
121
|
+
): void => {
|
|
122
|
+
// A single-project lockfile has no `importers` block; its dependencies sit at
|
|
123
|
+
// the top level, which is the same thing as an importer called ".".
|
|
124
|
+
const importers = isRecord(root["importers"]) ? root["importers"] : { ".": root }
|
|
125
|
+
|
|
126
|
+
const selected =
|
|
127
|
+
member === undefined
|
|
128
|
+
? Object.values(importers)
|
|
129
|
+
: Object.hasOwn(importers, member)
|
|
130
|
+
? [importers[member]]
|
|
131
|
+
: notCovered(path, member, Object.keys(importers))
|
|
132
|
+
|
|
133
|
+
for (const importer of selected) {
|
|
134
|
+
if (!isRecord(importer)) continue
|
|
135
|
+
for (const kind of ROOT_KINDS) {
|
|
136
|
+
const entries = importer[fieldByRootKind[kind]]
|
|
137
|
+
if (!isRecord(entries)) continue
|
|
138
|
+
|
|
139
|
+
for (const [name, raw] of Object.entries(entries)) {
|
|
140
|
+
// v5 wrote a bare version string; v6 and v9 write
|
|
141
|
+
// `{specifier, version}`.
|
|
142
|
+
const value = typeof raw === "string" ? raw : isRecord(raw) ? raw["version"] : undefined
|
|
143
|
+
if (typeof value !== "string") continue
|
|
144
|
+
|
|
145
|
+
const target = parsePnpmReference(name, value)
|
|
146
|
+
if (target === null) continue
|
|
147
|
+
|
|
148
|
+
const specifier =
|
|
149
|
+
isRecord(raw) && typeof raw["specifier"] === "string" ? raw["specifier"] : undefined
|
|
150
|
+
builder.addRoot({ ...target, kind, specifier })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Reads a dependency value from a snapshot or importer.
|
|
158
|
+
*
|
|
159
|
+
* The value is either a bare version (`1.4.0`) or a full package key
|
|
160
|
+
* (`/lodash@4.17.21`, used when the install name is an alias of another
|
|
161
|
+
* package). Both may carry a peer-resolution suffix in parentheses.
|
|
162
|
+
*/
|
|
163
|
+
const parsePnpmReference = (name: string, raw: string): PnpmReference | null => {
|
|
164
|
+
const value = stripSuffix(raw)
|
|
165
|
+
|
|
166
|
+
const exact = semver.valid(value, { loose: true })
|
|
167
|
+
if (exact !== null) return { name, version: exact }
|
|
168
|
+
|
|
169
|
+
return parsePnpmKey(raw)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Splits a package key into name and version.
|
|
174
|
+
*
|
|
175
|
+
* Three encodings across the versions still in the wild:
|
|
176
|
+
* v5 `/@babel/core/7.0.0`
|
|
177
|
+
* v6 `/@babel/core@7.0.0`
|
|
178
|
+
* v9 `@babel/core@7.0.0`
|
|
179
|
+
*
|
|
180
|
+
* plus an optional `(peer@1.0.0)` or `(patch_hash=…)` suffix on any of them.
|
|
181
|
+
*/
|
|
182
|
+
export const parsePnpmKey = (key: string): PnpmReference | null => {
|
|
183
|
+
const body = stripSuffix(key.startsWith("/") ? key.slice(1) : key)
|
|
184
|
+
|
|
185
|
+
// A scoped name's own `@` sits at index 0, so only a later one separates the
|
|
186
|
+
// version. When there is none, this is the v5 form and the last `/` does it.
|
|
187
|
+
const at = body.lastIndexOf("@")
|
|
188
|
+
const cut = at > 0 ? at : body.lastIndexOf("/")
|
|
189
|
+
if (cut <= 0) return null
|
|
190
|
+
|
|
191
|
+
const name = body.slice(0, cut)
|
|
192
|
+
const version = semver.valid(body.slice(cut + 1), { loose: true })
|
|
193
|
+
if (version === null || name.length === 0) return null
|
|
194
|
+
|
|
195
|
+
return { name, version }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Describes a key we cannot pin, so the skip warning can say why. */
|
|
199
|
+
const describeKey = (
|
|
200
|
+
key: string
|
|
201
|
+
): { readonly name: string; readonly spec: string; readonly reason: string } | null => {
|
|
202
|
+
const body = key.startsWith("/") ? key.slice(1) : key
|
|
203
|
+
const at = body.lastIndexOf("@")
|
|
204
|
+
if (at <= 0) return null
|
|
205
|
+
const name = body.slice(0, at)
|
|
206
|
+
const spec = body.slice(at + 1)
|
|
207
|
+
const target = parseDependencyTarget(name, spec)
|
|
208
|
+
return {
|
|
209
|
+
name,
|
|
210
|
+
spec,
|
|
211
|
+
reason: target._tag === "Unsupported" ? target.reason : "not an exact version"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Drops the parenthesised peer-resolution suffix.
|
|
217
|
+
*
|
|
218
|
+
* pnpm distinguishes the same version installed against different peers —
|
|
219
|
+
* `react-dom@18.3.1(react@18.3.1)`. Those are the same published tarball, which
|
|
220
|
+
* is all this tool downloads, so the suffix is noise here.
|
|
221
|
+
*/
|
|
222
|
+
const stripSuffix = (value: string): string => {
|
|
223
|
+
const open = value.indexOf("(")
|
|
224
|
+
return open === -1 ? value : value.slice(0, open)
|
|
225
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bookkeeping every lockfile parser shares.
|
|
3
|
+
*
|
|
4
|
+
* Packages, edges and skip warnings accumulate here while a parser walks a
|
|
5
|
+
* file, which is what lets each parser stay focused on its own encoding.
|
|
6
|
+
*/
|
|
7
|
+
import semver from "semver"
|
|
8
|
+
import { parseDependencyTarget } from "../dependency-range.js"
|
|
9
|
+
import { LockedRootKind } from "../enums/edge-kind.js"
|
|
10
|
+
import type { LockfileFormat } from "../enums/lockfile-format.js"
|
|
11
|
+
import { LockfileError } from "../errors.js"
|
|
12
|
+
import { packageKey } from "../resolve.js"
|
|
13
|
+
import type { LockedEdge, LockedPackage, LockedRoot, LockedTree } from "./types.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Which manifest block each root kind is declared in.
|
|
17
|
+
*
|
|
18
|
+
* The block names are the same across npm, pnpm and bun; only which of them a
|
|
19
|
+
* given format writes differs, which is why each parser supplies its own order
|
|
20
|
+
* rather than iterating this.
|
|
21
|
+
*/
|
|
22
|
+
export const fieldByRootKind = {
|
|
23
|
+
[LockedRootKind.Prod]: "dependencies",
|
|
24
|
+
[LockedRootKind.Optional]: "optionalDependencies",
|
|
25
|
+
[LockedRootKind.Dev]: "devDependencies",
|
|
26
|
+
[LockedRootKind.Peer]: "peerDependencies"
|
|
27
|
+
} as const satisfies Record<LockedRootKind, string>
|
|
28
|
+
|
|
29
|
+
export class TreeBuilder {
|
|
30
|
+
readonly packages = new Map<string, LockedPackage>()
|
|
31
|
+
readonly roots: Array<LockedRoot> = []
|
|
32
|
+
readonly warnings: Array<string> = []
|
|
33
|
+
readonly incomplete: Array<string> = []
|
|
34
|
+
/** Names dropped on purpose, so an edge into one is explained, not alarming. */
|
|
35
|
+
private readonly skipped = new Set<string>()
|
|
36
|
+
private readonly seenWarnings = new Set<string>()
|
|
37
|
+
|
|
38
|
+
add(pkg: LockedPackage): void {
|
|
39
|
+
const key = packageKey(pkg.name, pkg.version)
|
|
40
|
+
const existing = this.packages.get(key)
|
|
41
|
+
if (existing === undefined) {
|
|
42
|
+
this.packages.set(key, pkg)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
// The same package at the same version can appear at several paths in a
|
|
46
|
+
// nested layout. They are the same bytes; merge the edge sets so a copy
|
|
47
|
+
// recorded with fewer dependencies does not lose any.
|
|
48
|
+
this.packages.set(key, {
|
|
49
|
+
...existing,
|
|
50
|
+
dependencies: mergeEdges(existing.dependencies, pkg.dependencies)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addRoot(root: LockedRoot): void {
|
|
55
|
+
if (this.roots.some((existing) => existing.name === root.name)) return
|
|
56
|
+
this.roots.push(root)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
warn(message: string): void {
|
|
60
|
+
if (this.seenWarnings.has(message)) return
|
|
61
|
+
this.seenWarnings.add(message)
|
|
62
|
+
this.warnings.push(message)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Notes an entry that is real but cannot come from a registry. */
|
|
66
|
+
skip(name: string, spec: string, reason: string): void {
|
|
67
|
+
this.skipped.add(name)
|
|
68
|
+
this.warn(`skipped ${name}@${spec} (${reason}) — not fetchable from a registry`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Records a required edge that resolved to nothing.
|
|
73
|
+
*
|
|
74
|
+
* An edge into something skipped on purpose is expected — you cannot bundle a
|
|
75
|
+
* `workspace:` sibling from a registry — so it stays a warning. Anything else
|
|
76
|
+
* means the file does not pin what it claims to.
|
|
77
|
+
*/
|
|
78
|
+
dangling(from: string, name: string): void {
|
|
79
|
+
if (this.skipped.has(name)) {
|
|
80
|
+
this.warn(`${from} requires ${name}, which is not fetchable from a registry`)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
this.incomplete.push(`${name} (required by ${from})`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
finish(input: {
|
|
87
|
+
readonly format: LockfileFormat
|
|
88
|
+
readonly lockfileVersion: string
|
|
89
|
+
readonly path: string
|
|
90
|
+
readonly importer?: string | undefined
|
|
91
|
+
}): LockedTree {
|
|
92
|
+
if (this.packages.size === 0) {
|
|
93
|
+
throw new LockfileError(input.path, "lockfile pins no packages that can be bundled")
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
format: input.format,
|
|
97
|
+
lockfileVersion: input.lockfileVersion,
|
|
98
|
+
path: input.path,
|
|
99
|
+
importer: input.importer,
|
|
100
|
+
packages: this.packages,
|
|
101
|
+
roots: this.roots,
|
|
102
|
+
warnings: this.warnings,
|
|
103
|
+
incomplete: this.incomplete
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const mergeEdges = (
|
|
109
|
+
a: ReadonlyArray<LockedEdge>,
|
|
110
|
+
b: ReadonlyArray<LockedEdge>
|
|
111
|
+
): ReadonlyArray<LockedEdge> => {
|
|
112
|
+
const merged = new Map<string, LockedEdge>()
|
|
113
|
+
for (const edge of [...a, ...b]) {
|
|
114
|
+
merged.set(`${edge.name}@${edge.version}|${edge.kind}`, edge)
|
|
115
|
+
}
|
|
116
|
+
return [...merged.values()]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reads the right-hand side of a locked entry as an exact version.
|
|
121
|
+
*
|
|
122
|
+
* Returns the version when it is one, or the reason it is not — `file:../x`,
|
|
123
|
+
* `workspace:*`, a git URL. `parseDependencyTarget` already classifies every
|
|
124
|
+
* one of those for package.json parsing, so the vocabulary of skip reasons
|
|
125
|
+
* stays identical between the two paths.
|
|
126
|
+
*/
|
|
127
|
+
export const asExactVersion = (
|
|
128
|
+
name: string,
|
|
129
|
+
raw: string
|
|
130
|
+
):
|
|
131
|
+
| { readonly _tag: "Version"; readonly version: string }
|
|
132
|
+
| { readonly _tag: "Unsupported"; readonly reason: string } => {
|
|
133
|
+
const exact = semver.valid(raw, { loose: true })
|
|
134
|
+
if (exact !== null) return { _tag: "Version", version: exact }
|
|
135
|
+
|
|
136
|
+
const target = parseDependencyTarget(name, raw)
|
|
137
|
+
if (target._tag === "Unsupported") return { _tag: "Unsupported", reason: target.reason }
|
|
138
|
+
return { _tag: "Unsupported", reason: "not an exact version" }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Raised when a lockfile found by walking up turns out not to cover the
|
|
143
|
+
* package.json that went looking for it.
|
|
144
|
+
*
|
|
145
|
+
* Worth distinguishing from "no lockfile at all": the file exists and is
|
|
146
|
+
* readable, it simply belongs to a different project, and bundling it would
|
|
147
|
+
* produce someone else's dependency set.
|
|
148
|
+
*/
|
|
149
|
+
export const notCovered = (
|
|
150
|
+
path: string,
|
|
151
|
+
importer: string,
|
|
152
|
+
known: ReadonlyArray<string>
|
|
153
|
+
): never => {
|
|
154
|
+
const listed = known.slice(0, 8).map((entry) => ` ${entry}`).join("\n")
|
|
155
|
+
throw new LockfileError(
|
|
156
|
+
path,
|
|
157
|
+
`it does not cover ${importer}.\n` +
|
|
158
|
+
(known.length === 0
|
|
159
|
+
? " It records no workspace members."
|
|
160
|
+
: ` Workspace members it does record:\n${listed}` +
|
|
161
|
+
(known.length > 8 ? `\n … and ${known.length - 8} more` : ""))
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `""` is how npm and bun spell the project at the lockfile's own level. */
|
|
166
|
+
export const importerKey = (importer: string): string => (importer === "." ? "" : importer)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one output shape every lockfile parser produces.
|
|
3
|
+
*
|
|
4
|
+
* npm, pnpm and bun encode "package X at path P depends on Y" in three
|
|
5
|
+
* different ways; once flattened into this graph, nothing downstream has to
|
|
6
|
+
* know which manager wrote the file.
|
|
7
|
+
*/
|
|
8
|
+
import type { EdgeKind, LockedRootKind } from "../enums/edge-kind.js"
|
|
9
|
+
import type { LockfileFormat } from "../enums/lockfile-format.js"
|
|
10
|
+
|
|
11
|
+
/** One edge out of a locked package, already pinned to an exact version. */
|
|
12
|
+
export type LockedEdge = {
|
|
13
|
+
readonly name: string
|
|
14
|
+
readonly version: string
|
|
15
|
+
readonly kind: EdgeKind
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One package pinned by a lockfile. */
|
|
19
|
+
export type LockedPackage = {
|
|
20
|
+
readonly name: string
|
|
21
|
+
readonly version: string
|
|
22
|
+
readonly dependencies: ReadonlyArray<LockedEdge>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A direct dependency of the locked project. */
|
|
26
|
+
export type LockedRoot = {
|
|
27
|
+
readonly name: string
|
|
28
|
+
readonly version: string
|
|
29
|
+
readonly kind: LockedRootKind
|
|
30
|
+
/** The range the manifest asked for, kept for reporting. */
|
|
31
|
+
readonly specifier?: string | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Options for reading a lockfile. */
|
|
35
|
+
export type LockfileParseOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Which workspace member's dependencies become the roots, as a path relative
|
|
38
|
+
* to the lockfile in POSIX form — `"packages/core"`, or `"."` for the project
|
|
39
|
+
* at the lockfile's own level.
|
|
40
|
+
*
|
|
41
|
+
* Left undefined, every workspace member in the file contributes, which is
|
|
42
|
+
* what pointing `--file` straight at a lockfile should mean. It matters when
|
|
43
|
+
* a package.json found a lockfile by walking *up*: the file then pins the
|
|
44
|
+
* whole repository, and only one member of it was asked for.
|
|
45
|
+
*/
|
|
46
|
+
readonly importer?: string | undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A whole lockfile, flattened. */
|
|
50
|
+
export type LockedTree = {
|
|
51
|
+
readonly format: LockfileFormat
|
|
52
|
+
/** As the file declares it: `3`, `'9.0'`, `1`. */
|
|
53
|
+
readonly lockfileVersion: string
|
|
54
|
+
/** Where it was read from, for error messages. */
|
|
55
|
+
readonly path: string
|
|
56
|
+
/** The workspace member the roots came from, when one was selected. */
|
|
57
|
+
readonly importer?: string | undefined
|
|
58
|
+
/** Every pinned package, keyed by `name@version`. */
|
|
59
|
+
readonly packages: ReadonlyMap<string, LockedPackage>
|
|
60
|
+
/** The project's direct dependencies. */
|
|
61
|
+
readonly roots: ReadonlyArray<LockedRoot>
|
|
62
|
+
/** Entries that were recognised but cannot be fetched from a registry. */
|
|
63
|
+
readonly warnings: ReadonlyArray<string>
|
|
64
|
+
/**
|
|
65
|
+
* Required edges pointing at nothing the file pins.
|
|
66
|
+
*
|
|
67
|
+
* Distinct from `warnings`: a `workspace:` dependency that cannot be bundled
|
|
68
|
+
* is expected and gets a warning, while an ordinary registry dependency with
|
|
69
|
+
* no entry means the lockfile is genuinely partial and the bundle it produces
|
|
70
|
+
* would be missing a package. The caller decides what to do about it, but the
|
|
71
|
+
* two must not be confused.
|
|
72
|
+
*/
|
|
73
|
+
readonly incomplete: ReadonlyArray<string>
|
|
74
|
+
}
|