@packall/core 0.1.0 → 0.2.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/archive-6sqTn1C4.js +274 -0
  4. package/dist/archive-6sqTn1C4.js.map +1 -0
  5. package/dist/archive.d.ts +47 -15
  6. package/dist/archive.d.ts.map +1 -1
  7. package/dist/bundle.d.ts +6 -3
  8. package/dist/bundle.d.ts.map +1 -1
  9. package/dist/dependency-range.d.ts.map +1 -1
  10. package/dist/download.d.ts +2 -1
  11. package/dist/download.d.ts.map +1 -1
  12. package/dist/enums/artifact-kind.d.ts.map +1 -1
  13. package/dist/enums/edge-kind.d.ts.map +1 -1
  14. package/dist/enums/input-file-kind.d.ts.map +1 -1
  15. package/dist/enums/layout.d.ts.map +1 -1
  16. package/dist/enums/lockfile-format.d.ts.map +1 -1
  17. package/dist/enums/phase.d.ts.map +1 -1
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/index.d.ts +12 -10
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +169 -344
  22. package/dist/index.js.map +1 -1
  23. package/dist/input-file.d.ts.map +1 -1
  24. package/dist/integrity.d.ts +37 -5
  25. package/dist/integrity.d.ts.map +1 -1
  26. package/dist/layout.d.ts +41 -7
  27. package/dist/layout.d.ts.map +1 -1
  28. package/dist/locked-resolve.d.ts.map +1 -1
  29. package/dist/lockfile/bun.d.ts.map +1 -1
  30. package/dist/lockfile/detect.d.ts.map +1 -1
  31. package/dist/lockfile/json.d.ts.map +1 -1
  32. package/dist/lockfile/names.d.ts.map +1 -1
  33. package/dist/lockfile/npm.d.ts.map +1 -1
  34. package/dist/lockfile/parse.d.ts.map +1 -1
  35. package/dist/lockfile/pnpm.d.ts.map +1 -1
  36. package/dist/lockfile/tree-builder.d.ts.map +1 -1
  37. package/dist/lockfile/types.d.ts.map +1 -1
  38. package/dist/manifest.d.ts.map +1 -1
  39. package/dist/node/archiver.d.ts +5 -0
  40. package/dist/node/archiver.d.ts.map +1 -0
  41. package/dist/node/index.d.ts +13 -0
  42. package/dist/node/index.d.ts.map +1 -0
  43. package/dist/node/index.js +55 -0
  44. package/dist/node/index.js.map +1 -0
  45. package/dist/options.d.ts +3 -3
  46. package/dist/options.d.ts.map +1 -1
  47. package/dist/platform.d.ts +5 -10
  48. package/dist/platform.d.ts.map +1 -1
  49. package/dist/progress.d.ts.map +1 -1
  50. package/dist/registry.d.ts.map +1 -1
  51. package/dist/resolve.d.ts.map +1 -1
  52. package/dist/schemas/lenient.d.ts.map +1 -1
  53. package/dist/schemas/package-json.d.ts.map +1 -1
  54. package/dist/spec.d.ts.map +1 -1
  55. package/dist/types/value-of.d.ts.map +1 -1
  56. package/dist/utils/is-record.d.ts.map +1 -1
  57. package/package.json +11 -10
  58. package/src/archive.ts +95 -92
  59. package/src/bundle.ts +736 -616
  60. package/src/dependency-range.ts +121 -115
  61. package/src/download.ts +155 -133
  62. package/src/enums/artifact-kind.ts +5 -5
  63. package/src/enums/edge-kind.ts +13 -13
  64. package/src/enums/input-file-kind.ts +9 -9
  65. package/src/enums/layout.ts +20 -20
  66. package/src/enums/lockfile-format.ts +6 -6
  67. package/src/enums/phase.ts +8 -8
  68. package/src/errors.ts +200 -198
  69. package/src/index.ts +126 -113
  70. package/src/input-file.ts +246 -234
  71. package/src/integrity.ts +165 -121
  72. package/src/layout.ts +63 -29
  73. package/src/locked-resolve.ts +471 -461
  74. package/src/lockfile/bun.ts +214 -207
  75. package/src/lockfile/detect.ts +80 -80
  76. package/src/lockfile/json.ts +91 -92
  77. package/src/lockfile/names.ts +9 -9
  78. package/src/lockfile/npm.ts +328 -308
  79. package/src/lockfile/parse.ts +32 -32
  80. package/src/lockfile/pnpm.ts +230 -225
  81. package/src/lockfile/tree-builder.ts +116 -116
  82. package/src/lockfile/types.ts +52 -52
  83. package/src/manifest.ts +129 -133
  84. package/src/node/archiver.ts +85 -0
  85. package/src/node/index.ts +13 -0
  86. package/src/options.ts +84 -84
  87. package/src/platform.ts +97 -101
  88. package/src/progress.ts +105 -106
  89. package/src/registry.ts +116 -115
  90. package/src/resolve.ts +560 -537
  91. package/src/schemas/lenient.ts +67 -60
  92. package/src/schemas/package-json.ts +11 -10
  93. package/src/spec.ts +123 -119
  94. package/src/types/value-of.ts +1 -1
  95. package/src/utils/is-record.ts +1 -1
@@ -12,13 +12,13 @@
12
12
  * version. Once that is done, nothing downstream has to know which manager
13
13
  * wrote the file.
14
14
  */
15
- import { LockfileFormat } from "../enums/lockfile-format.js"
16
- import { LockfileError } from "../errors.js"
17
- import { parseBunLockfile } from "./bun.js"
18
- import { detectLockfile } from "./detect.js"
19
- import { parseNpmLockfile } from "./npm.js"
20
- import { parsePnpmLockfile } from "./pnpm.js"
21
- import type { LockedTree, LockfileParseOptions } from "./types.js"
15
+ import { LockfileFormat } from "../enums/lockfile-format.js";
16
+ import { LockfileError } from "../errors.js";
17
+ import { parseBunLockfile } from "./bun.js";
18
+ import { detectLockfile } from "./detect.js";
19
+ import { parseNpmLockfile } from "./npm.js";
20
+ import { parsePnpmLockfile } from "./pnpm.js";
21
+ import type { LockedTree, LockfileParseOptions } from "./types.js";
22
22
 
23
23
  /**
24
24
  * Parses lockfile content into a flat graph.
@@ -26,31 +26,31 @@ import type { LockedTree, LockfileParseOptions } from "./types.js"
26
26
  * Throws `LockfileError`; the caller is expected to be inside an `Effect.try`.
27
27
  */
28
28
  export const parseLockfile = (
29
- path: string,
30
- content: string,
31
- format?: LockfileFormat,
32
- options: LockfileParseOptions = {}
29
+ path: string,
30
+ content: string,
31
+ format?: LockfileFormat,
32
+ options: LockfileParseOptions = {},
33
33
  ): LockedTree => {
34
- const resolved = format ?? resolveFormat(path, content)
35
- const importer = options.importer
36
- switch (resolved) {
37
- case LockfileFormat.Npm:
38
- return parseNpmLockfile(path, content, importer)
39
- case LockfileFormat.Pnpm:
40
- return parsePnpmLockfile(path, content, importer)
41
- case LockfileFormat.Bun:
42
- return parseBunLockfile(path, content, importer)
43
- }
44
- }
34
+ const resolved = format ?? resolveFormat(path, content);
35
+ const importer = options.importer;
36
+ switch (resolved) {
37
+ case LockfileFormat.Npm:
38
+ return parseNpmLockfile(path, content, importer);
39
+ case LockfileFormat.Pnpm:
40
+ return parsePnpmLockfile(path, content, importer);
41
+ case LockfileFormat.Bun:
42
+ return parseBunLockfile(path, content, importer);
43
+ }
44
+ };
45
45
 
46
46
  const resolveFormat = (path: string, content: string): LockfileFormat => {
47
- const detected = detectLockfile(content)
48
- if (detected._tag === "Supported") return detected.format
49
- if (detected._tag === "Unsupported") {
50
- throw new LockfileError(path, `${detected.label} — ${detected.hint}`)
51
- }
52
- throw new LockfileError(
53
- path,
54
- "content does not look like a package-lock.json, pnpm-lock.yaml or bun.lock"
55
- )
56
- }
47
+ const detected = detectLockfile(content);
48
+ if (detected._tag === "Supported") return detected.format;
49
+ if (detected._tag === "Unsupported") {
50
+ throw new LockfileError(path, `${detected.label} — ${detected.hint}`);
51
+ }
52
+ throw new LockfileError(
53
+ path,
54
+ "content does not look like a package-lock.json, pnpm-lock.yaml or bun.lock",
55
+ );
56
+ };
@@ -1,225 +1,230 @@
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
- }
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
+
11
+ import { parseDependencyTarget } from "../dependency-range.js";
12
+ import { EdgeKind, LockedRootKind } from "../enums/edge-kind.js";
13
+ import { LockfileFormat } from "../enums/lockfile-format.js";
14
+ import { LockfileError } from "../errors.js";
15
+ import { isRecord } from "../utils/is-record.js";
16
+ import { describeError } from "./json.js";
17
+ import { fieldByRootKind, notCovered, TreeBuilder } from "./tree-builder.js";
18
+ import type { LockedEdge, LockedTree } from "./types.js";
19
+
20
+ /** `name@version`, as a snapshot or importer entry resolves to. */
21
+ type PnpmReference = {
22
+ readonly name: string;
23
+ readonly version: string;
24
+ };
25
+
26
+ /** pnpm never records peer dependencies against an importer. */
27
+ const ROOT_KINDS = [LockedRootKind.Prod, LockedRootKind.Optional, LockedRootKind.Dev] as const;
28
+
29
+ export const parsePnpmLockfile = (
30
+ path: string,
31
+ content: string,
32
+ importer: string | undefined,
33
+ ): LockedTree => {
34
+ let root: unknown;
35
+ try {
36
+ root = parseYaml(content);
37
+ } catch (error) {
38
+ throw new LockfileError(path, `not valid YAML — ${describeError(error)}`, { cause: error });
39
+ }
40
+ if (!isRecord(root)) {
41
+ throw new LockfileError(path, "top level is not a mapping");
42
+ }
43
+
44
+ const version = String(root["lockfileVersion"] ?? "?");
45
+ const builder = new TreeBuilder();
46
+
47
+ // v9 split the graph in two: `packages` keeps resolution metadata, and
48
+ // `snapshots` keeps the edges. Before that both lived in `packages`.
49
+ const packages = isRecord(root["packages"]) ? root["packages"] : {};
50
+ const snapshots = isRecord(root["snapshots"]) ? root["snapshots"] : packages;
51
+
52
+ const known = new Map<string, PnpmReference>();
53
+ for (const key of Object.keys(packages)) {
54
+ const parsed = parsePnpmKey(key);
55
+ if (parsed === null) continue;
56
+ known.set(key, parsed);
57
+ }
58
+
59
+ for (const [key, raw] of Object.entries(snapshots)) {
60
+ const parsed = parsePnpmKey(key) ?? known.get(key);
61
+ if (parsed === undefined || parsed === null) {
62
+ const described = describeKey(key);
63
+ if (described !== null) builder.skip(described.name, described.spec, described.reason);
64
+ continue;
65
+ }
66
+ builder.add({
67
+ name: parsed.name,
68
+ version: parsed.version,
69
+ dependencies: edgesOf(builder, parsed, isRecord(raw) ? raw : {}),
70
+ });
71
+ }
72
+
73
+ collectRoots(builder, root, path, importer);
74
+
75
+ return builder.finish({
76
+ format: LockfileFormat.Pnpm,
77
+ lockfileVersion: version,
78
+ path,
79
+ importer,
80
+ });
81
+ };
82
+
83
+ const edgesOf = (
84
+ builder: TreeBuilder,
85
+ from: PnpmReference,
86
+ snapshot: Record<string, unknown>,
87
+ ): ReadonlyArray<LockedEdge> => {
88
+ const edges: Array<LockedEdge> = [];
89
+
90
+ const add = (field: string, kind: EdgeKind): void => {
91
+ const entries = snapshot[field];
92
+ if (!isRecord(entries)) return;
93
+ for (const [name, raw] of Object.entries(entries)) {
94
+ if (typeof raw !== "string") continue;
95
+ const target = parsePnpmReference(name, raw);
96
+ if (target === null) {
97
+ if (kind === EdgeKind.Prod) builder.dangling(`${from.name}@${from.version}`, name);
98
+ continue;
99
+ }
100
+ edges.push({ ...target, kind });
101
+ }
102
+ };
103
+
104
+ add(fieldByRootKind[LockedRootKind.Prod], EdgeKind.Prod);
105
+ add(fieldByRootKind[LockedRootKind.Optional], EdgeKind.Optional);
106
+ add(fieldByRootKind[LockedRootKind.Peer], EdgeKind.Peer);
107
+
108
+ return edges;
109
+ };
110
+
111
+ /**
112
+ * The importers' direct dependencies become the roots.
113
+ *
114
+ * pnpm is the format that makes this easy: `importers` is already keyed by the
115
+ * path each package.json sits at, so scoping to one member is a lookup rather
116
+ * than a reconstruction.
117
+ */
118
+ const collectRoots = (
119
+ builder: TreeBuilder,
120
+ root: Record<string, unknown>,
121
+ path: string,
122
+ member: string | undefined,
123
+ ): void => {
124
+ // A single-project lockfile has no `importers` block; its dependencies sit at
125
+ // the top level, which is the same thing as an importer called ".".
126
+ const importers = isRecord(root["importers"]) ? root["importers"] : { ".": root };
127
+
128
+ const selected =
129
+ member === undefined
130
+ ? Object.values(importers)
131
+ : Object.hasOwn(importers, member)
132
+ ? [importers[member]]
133
+ : notCovered(path, member, Object.keys(importers));
134
+
135
+ for (const importer of selected) {
136
+ if (!isRecord(importer)) continue;
137
+ for (const kind of ROOT_KINDS) {
138
+ const entries = importer[fieldByRootKind[kind]];
139
+ if (!isRecord(entries)) continue;
140
+
141
+ for (const [name, raw] of Object.entries(entries)) {
142
+ // v5 wrote a bare version string; v6 and v9 write
143
+ // `{specifier, version}`.
144
+ const value =
145
+ typeof raw === "string" ? raw : isRecord(raw) ? raw["version"] : undefined;
146
+ if (typeof value !== "string") continue;
147
+
148
+ const target = parsePnpmReference(name, value);
149
+ if (target === null) continue;
150
+
151
+ const specifier =
152
+ isRecord(raw) && typeof raw["specifier"] === "string"
153
+ ? raw["specifier"]
154
+ : undefined;
155
+ builder.addRoot({ ...target, kind, specifier });
156
+ }
157
+ }
158
+ }
159
+ };
160
+
161
+ /**
162
+ * Reads a dependency value from a snapshot or importer.
163
+ *
164
+ * The value is either a bare version (`1.4.0`) or a full package key
165
+ * (`/lodash@4.17.21`, used when the install name is an alias of another
166
+ * package). Both may carry a peer-resolution suffix in parentheses.
167
+ */
168
+ const parsePnpmReference = (name: string, raw: string): PnpmReference | null => {
169
+ const value = stripSuffix(raw);
170
+
171
+ const exact = semver.valid(value, { loose: true });
172
+ if (exact !== null) return { name, version: exact };
173
+
174
+ return parsePnpmKey(raw);
175
+ };
176
+
177
+ /**
178
+ * Splits a package key into name and version.
179
+ *
180
+ * Three encodings across the versions still in the wild:
181
+ * v5 `/@babel/core/7.0.0`
182
+ * v6 `/@babel/core@7.0.0`
183
+ * v9 `@babel/core@7.0.0`
184
+ *
185
+ * plus an optional `(peer@1.0.0)` or `(patch_hash=…)` suffix on any of them.
186
+ */
187
+ export const parsePnpmKey = (key: string): PnpmReference | null => {
188
+ const body = stripSuffix(key.startsWith("/") ? key.slice(1) : key);
189
+
190
+ // A scoped name's own `@` sits at index 0, so only a later one separates the
191
+ // version. When there is none, this is the v5 form and the last `/` does it.
192
+ const at = body.lastIndexOf("@");
193
+ const cut = at > 0 ? at : body.lastIndexOf("/");
194
+ if (cut <= 0) return null;
195
+
196
+ const name = body.slice(0, cut);
197
+ const version = semver.valid(body.slice(cut + 1), { loose: true });
198
+ if (version === null || name.length === 0) return null;
199
+
200
+ return { name, version };
201
+ };
202
+
203
+ /** Describes a key we cannot pin, so the skip warning can say why. */
204
+ const describeKey = (
205
+ key: string,
206
+ ): { readonly name: string; readonly spec: string; readonly reason: string } | null => {
207
+ const body = key.startsWith("/") ? key.slice(1) : key;
208
+ const at = body.lastIndexOf("@");
209
+ if (at <= 0) return null;
210
+ const name = body.slice(0, at);
211
+ const spec = body.slice(at + 1);
212
+ const target = parseDependencyTarget(name, spec);
213
+ return {
214
+ name,
215
+ spec,
216
+ reason: target._tag === "Unsupported" ? target.reason : "not an exact version",
217
+ };
218
+ };
219
+
220
+ /**
221
+ * Drops the parenthesised peer-resolution suffix.
222
+ *
223
+ * pnpm distinguishes the same version installed against different peers —
224
+ * `react-dom@18.3.1(react@18.3.1)`. Those are the same published tarball, which
225
+ * is all this tool downloads, so the suffix is noise here.
226
+ */
227
+ const stripSuffix = (value: string): string => {
228
+ const open = value.indexOf("(");
229
+ return open === -1 ? value : value.slice(0, open);
230
+ };