@packall/core 0.0.1 → 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 (157) 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 +64 -0
  6. package/dist/archive.d.ts.map +1 -0
  7. package/dist/{Bundle.d.ts → bundle.d.ts} +56 -38
  8. package/dist/bundle.d.ts.map +1 -0
  9. package/dist/{DependencyRange.d.ts → dependency-range.d.ts} +2 -2
  10. package/dist/dependency-range.d.ts.map +1 -0
  11. package/dist/{Download.d.ts → download.d.ts} +9 -8
  12. package/dist/download.d.ts.map +1 -0
  13. package/dist/enums/artifact-kind.d.ts +8 -0
  14. package/dist/enums/artifact-kind.d.ts.map +1 -0
  15. package/dist/enums/edge-kind.d.ts +29 -0
  16. package/dist/enums/edge-kind.d.ts.map +1 -0
  17. package/dist/enums/input-file-kind.d.ts +12 -0
  18. package/dist/enums/input-file-kind.d.ts.map +1 -0
  19. package/dist/enums/layout.d.ts +23 -0
  20. package/dist/enums/layout.d.ts.map +1 -0
  21. package/dist/enums/lockfile-format.d.ts +9 -0
  22. package/dist/enums/lockfile-format.d.ts.map +1 -0
  23. package/dist/enums/phase.d.ts +11 -0
  24. package/dist/enums/phase.d.ts.map +1 -0
  25. package/dist/{Errors.d.ts → errors.d.ts} +49 -7
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/index.d.ts +46 -28
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2121 -1060
  30. package/dist/index.js.map +1 -1
  31. package/dist/input-file.d.ts +77 -0
  32. package/dist/input-file.d.ts.map +1 -0
  33. package/dist/integrity.d.ts +80 -0
  34. package/dist/integrity.d.ts.map +1 -0
  35. package/dist/layout.d.ts +87 -0
  36. package/dist/layout.d.ts.map +1 -0
  37. package/dist/locked-resolve.d.ts +47 -0
  38. package/dist/locked-resolve.d.ts.map +1 -0
  39. package/dist/lockfile/bun.d.ts +3 -0
  40. package/dist/lockfile/bun.d.ts.map +1 -0
  41. package/dist/lockfile/detect.d.ts +31 -0
  42. package/dist/lockfile/detect.d.ts.map +1 -0
  43. package/dist/lockfile/json.d.ts +23 -0
  44. package/dist/lockfile/json.d.ts.map +1 -0
  45. package/dist/lockfile/names.d.ts +16 -0
  46. package/dist/lockfile/names.d.ts.map +1 -0
  47. package/dist/lockfile/npm.d.ts +3 -0
  48. package/dist/lockfile/npm.d.ts.map +1 -0
  49. package/dist/lockfile/parse.d.ts +23 -0
  50. package/dist/lockfile/parse.d.ts.map +1 -0
  51. package/dist/lockfile/pnpm.d.ts +20 -0
  52. package/dist/lockfile/pnpm.d.ts.map +1 -0
  53. package/dist/lockfile/tree-builder.d.ts +70 -0
  54. package/dist/lockfile/tree-builder.d.ts.map +1 -0
  55. package/dist/lockfile/types.d.ts +70 -0
  56. package/dist/lockfile/types.d.ts.map +1 -0
  57. package/dist/{Manifest.d.ts → manifest.d.ts} +7 -15
  58. package/dist/manifest.d.ts.map +1 -0
  59. package/dist/node/archiver.d.ts +5 -0
  60. package/dist/node/archiver.d.ts.map +1 -0
  61. package/dist/node/index.d.ts +13 -0
  62. package/dist/node/index.d.ts.map +1 -0
  63. package/dist/node/index.js +55 -0
  64. package/dist/node/index.js.map +1 -0
  65. package/dist/{Options.d.ts → options.d.ts} +12 -30
  66. package/dist/options.d.ts.map +1 -0
  67. package/dist/{Platform.d.ts → platform.d.ts} +14 -19
  68. package/dist/platform.d.ts.map +1 -0
  69. package/dist/{Progress.d.ts → progress.d.ts} +10 -9
  70. package/dist/progress.d.ts.map +1 -0
  71. package/dist/{Registry.d.ts → registry.d.ts} +42 -44
  72. package/dist/registry.d.ts.map +1 -0
  73. package/dist/{Resolve.d.ts → resolve.d.ts} +25 -26
  74. package/dist/resolve.d.ts.map +1 -0
  75. package/dist/schemas/lenient.d.ts +38 -0
  76. package/dist/schemas/lenient.d.ts.map +1 -0
  77. package/dist/schemas/package-json.d.ts +44 -0
  78. package/dist/schemas/package-json.d.ts.map +1 -0
  79. package/dist/{Spec.d.ts → spec.d.ts} +4 -4
  80. package/dist/spec.d.ts.map +1 -0
  81. package/dist/types/value-of.d.ts +9 -0
  82. package/dist/types/value-of.d.ts.map +1 -0
  83. package/dist/utils/is-record.d.ts +8 -0
  84. package/dist/utils/is-record.d.ts.map +1 -0
  85. package/package.json +13 -11
  86. package/src/archive.ts +95 -0
  87. package/src/bundle.ts +736 -0
  88. package/src/dependency-range.ts +121 -0
  89. package/src/download.ts +155 -0
  90. package/src/enums/artifact-kind.ts +9 -0
  91. package/src/enums/edge-kind.ts +32 -0
  92. package/src/enums/input-file-kind.ts +13 -0
  93. package/src/enums/layout.ts +25 -0
  94. package/src/enums/lockfile-format.ts +10 -0
  95. package/src/enums/phase.ts +12 -0
  96. package/src/errors.ts +300 -0
  97. package/src/index.ts +146 -35
  98. package/src/input-file.ts +324 -0
  99. package/src/integrity.ts +165 -0
  100. package/src/layout.ts +137 -0
  101. package/src/locked-resolve.ts +471 -0
  102. package/src/lockfile/bun.ts +214 -0
  103. package/src/lockfile/detect.ts +80 -0
  104. package/src/lockfile/json.ts +131 -0
  105. package/src/lockfile/names.ts +20 -0
  106. package/src/lockfile/npm.ts +328 -0
  107. package/src/lockfile/parse.ts +56 -0
  108. package/src/lockfile/pnpm.ts +230 -0
  109. package/src/lockfile/tree-builder.ts +166 -0
  110. package/src/lockfile/types.ts +74 -0
  111. package/src/manifest.ts +129 -0
  112. package/src/node/archiver.ts +85 -0
  113. package/src/node/index.ts +13 -0
  114. package/src/options.ts +103 -0
  115. package/src/platform.ts +178 -0
  116. package/src/progress.ts +105 -0
  117. package/src/registry.ts +116 -0
  118. package/src/resolve.ts +560 -0
  119. package/src/schemas/lenient.ts +130 -0
  120. package/src/schemas/package-json.ts +28 -0
  121. package/src/spec.ts +182 -0
  122. package/src/types/value-of.ts +8 -0
  123. package/src/utils/is-record.ts +8 -0
  124. package/dist/Archive.d.ts +0 -32
  125. package/dist/Archive.d.ts.map +0 -1
  126. package/dist/Bundle.d.ts.map +0 -1
  127. package/dist/DependencyRange.d.ts.map +0 -1
  128. package/dist/Download.d.ts.map +0 -1
  129. package/dist/Errors.d.ts.map +0 -1
  130. package/dist/InputFile.d.ts +0 -46
  131. package/dist/InputFile.d.ts.map +0 -1
  132. package/dist/Integrity.d.ts +0 -48
  133. package/dist/Integrity.d.ts.map +0 -1
  134. package/dist/Layout.d.ts +0 -53
  135. package/dist/Layout.d.ts.map +0 -1
  136. package/dist/Manifest.d.ts.map +0 -1
  137. package/dist/Options.d.ts.map +0 -1
  138. package/dist/Platform.d.ts.map +0 -1
  139. package/dist/Progress.d.ts.map +0 -1
  140. package/dist/Registry.d.ts.map +0 -1
  141. package/dist/Resolve.d.ts.map +0 -1
  142. package/dist/Spec.d.ts.map +0 -1
  143. package/src/Archive.ts +0 -92
  144. package/src/Bundle.ts +0 -570
  145. package/src/DependencyRange.ts +0 -115
  146. package/src/Download.ts +0 -132
  147. package/src/Errors.ts +0 -216
  148. package/src/InputFile.ts +0 -229
  149. package/src/Integrity.ts +0 -122
  150. package/src/Layout.ts +0 -103
  151. package/src/Manifest.ts +0 -133
  152. package/src/Options.ts +0 -123
  153. package/src/Platform.ts +0 -182
  154. package/src/Progress.ts +0 -106
  155. package/src/Registry.ts +0 -121
  156. package/src/Resolve.ts +0 -528
  157. package/src/Spec.ts +0 -186
package/dist/index.js CHANGED
@@ -1,585 +1,396 @@
1
+ import { _ as OutputError, a as layerCallback, b as RegistryUnreachableError, c as ArchiveError, d as InvalidInputFileError, f as InvalidSpecError, g as NoMatchingVersionsError, h as LockfileOutOfDateError, i as emit, l as AuthenticationError, m as LockfileIncompleteError, n as createArchive, o as layerSilent, p as LockfileError, r as Progress, s as makeCollector, t as Archiver, u as IntegrityError, v as PackageNotFoundError, x as VersionNotFoundError, y as RegistryResponseError } from "./archive-6sqTn1C4.js";
1
2
  import * as Effect from "effect/Effect";
2
- import * as FileSystem from "effect/FileSystem";
3
- import { create } from "tar";
3
+ import * as Option from "effect/Option";
4
+ import * as Schema from "effect/Schema";
5
+ import * as Getter from "effect/SchemaGetter";
6
+ import semver from "semver";
4
7
  import * as Context from "effect/Context";
5
- import * as Layer from "effect/Layer";
6
- import * as Ref from "effect/Ref";
8
+ import * as Crypto from "effect/Crypto";
9
+ import * as Encoding from "effect/Encoding";
10
+ import { parse } from "yaml";
11
+ import * as FileSystem from "effect/FileSystem";
7
12
  import * as Path from "effect/Path";
8
- import { createHash, timingSafeEqual } from "node:crypto";
9
- import semver from "semver";
10
13
 
11
- //#region rolldown:runtime
12
- var __defProp = Object.defineProperty;
13
- var __export = (all) => {
14
- let target = {};
15
- for (var name in all) __defProp(target, name, {
16
- get: all[name],
17
- enumerable: true
18
- });
19
- return target;
14
+ //#region src/enums/artifact-kind.ts
15
+ /** What a bundle run wrote to the output directory. */
16
+ const ArtifactKind = {
17
+ Archive: "archive",
18
+ Directory: "directory"
20
19
  };
21
20
 
22
21
  //#endregion
23
- //#region src/Errors.ts
24
- var BundlerErrorBase = class extends Error {
25
- constructor(message, options) {
26
- super(message, options);
27
- this.name = new.target.name;
28
- }
29
- };
30
- /** A package spec on the command line or in an input file could not be parsed. */
31
- var InvalidSpecError = class extends BundlerErrorBase {
32
- _tag = "InvalidSpecError";
33
- spec;
34
- reason;
35
- constructor(spec, reason) {
36
- super(`Invalid package spec ${JSON.stringify(spec)}: ${reason}`);
37
- this.spec = spec;
38
- this.reason = reason;
39
- }
40
- };
41
- /** `--file` pointed at something that is neither a usable package.json nor a spec list. */
42
- var InvalidInputFileError = class extends BundlerErrorBase {
43
- _tag = "InvalidInputFileError";
44
- path;
45
- reason;
46
- constructor(path, reason, options) {
47
- super(`Cannot read specs from ${path}: ${reason}`, options);
48
- this.path = path;
49
- this.reason = reason;
50
- }
51
- };
22
+ //#region src/enums/edge-kind.ts
52
23
  /**
53
- * The registry could not be reached at all — DNS failure, refused connection,
54
- * proxy blackhole, or a preflight that timed out.
24
+ * Why one package depends on another.
55
25
  *
56
- * This is the error that fixes "hangs forever with no network": we surface it
57
- * quickly and say which host we could not reach.
26
+ * `EdgeKind` and `LockedRootKind` are one concept in two parts, which is why
27
+ * they share a file: `Dev` exists only at the top level, because lockfiles
28
+ * record dev dependencies for the project alone — a transitive devDependency is
29
+ * never installed, so it is never locked.
58
30
  */
59
- var RegistryUnreachableError = class extends BundlerErrorBase {
60
- _tag = "RegistryUnreachableError";
61
- registry;
62
- timeoutMillis;
63
- constructor(registry, detail, options) {
64
- super(`Cannot reach registry ${registry}: ${detail}.\n Check your network connection, VPN, and the proxy settings in your .npmrc.`, options);
65
- this.registry = registry;
66
- this.timeoutMillis = options?.timeoutMillis;
67
- }
68
- };
69
- /** The registry answered, but has never heard of this package. */
70
- var PackageNotFoundError = class extends BundlerErrorBase {
71
- _tag = "PackageNotFoundError";
72
- packageName;
73
- registry;
74
- constructor(packageName, registry) {
75
- super(`Package "${packageName}" was not found on ${registry}`);
76
- this.packageName = packageName;
77
- this.registry = registry;
78
- }
79
- };
80
- /** The package exists but the exact version requested does not. */
81
- var VersionNotFoundError = class extends BundlerErrorBase {
82
- _tag = "VersionNotFoundError";
83
- packageName;
84
- version;
85
- available;
86
- constructor(packageName, version, available) {
87
- const tail = available.slice(-5).join(", ");
88
- super(`${packageName}@${version} does not exist.` + (tail.length > 0 ? ` Most recent published versions: ${tail}` : ""));
89
- this.packageName = packageName;
90
- this.version = version;
91
- this.available = available;
92
- }
31
+ const EdgeKind = {
32
+ Prod: "prod",
33
+ Optional: "optional",
34
+ Peer: "peer"
93
35
  };
94
- /** A range (or dist-tag) matched nothing that is actually published. */
95
- var NoMatchingVersionsError = class extends BundlerErrorBase {
96
- _tag = "NoMatchingVersionsError";
97
- packageName;
98
- selector;
99
- available;
100
- constructor(packageName, selector, available) {
101
- const tail = available.slice(-5).join(", ");
102
- super(`No published version of ${packageName} satisfies "${selector}".` + (tail.length > 0 ? ` Most recent published versions: ${tail}` : ""));
103
- this.packageName = packageName;
104
- this.selector = selector;
105
- this.available = available;
106
- }
107
- };
108
- /** The registry responded, but with something we cannot use. */
109
- var RegistryResponseError = class extends BundlerErrorBase {
110
- _tag = "RegistryResponseError";
111
- url;
112
- status;
113
- constructor(url, detail, options) {
114
- super(`Unexpected response from ${url}: ${detail}`, options);
115
- this.url = url;
116
- this.status = options?.status;
117
- }
118
- };
119
- /** 401/403 — almost always a missing or stale token in `.npmrc`. */
120
- var AuthenticationError = class extends BundlerErrorBase {
121
- _tag = "AuthenticationError";
122
- registry;
123
- status;
124
- constructor(registry, status, packageName) {
125
- super(`Registry ${registry} rejected the request with HTTP ${status}` + (packageName ? ` while fetching "${packageName}"` : "") + `.\n Add credentials to your .npmrc, e.g.\n //${safeHost(registry)}/:_authToken=\${NPM_TOKEN}`);
126
- this.registry = registry;
127
- this.status = status;
128
- }
36
+ /** The kind that only a direct dependency of the locked project can have. */
37
+ const DirectOnlyKind = { Dev: "dev" };
38
+ /** Why a direct dependency of the locked project is a root. */
39
+ const LockedRootKind = {
40
+ ...EdgeKind,
41
+ ...DirectOnlyKind
129
42
  };
130
- /**
131
- * A downloaded tarball did not match the checksum the registry advertised.
132
- *
133
- * Never soft-fail this: a bundle is a supply-chain artifact and a corrupt or
134
- * substituted tarball is exactly what integrity checking exists to catch.
135
- */
136
- var IntegrityError = class extends BundlerErrorBase {
137
- _tag = "IntegrityError";
138
- packageName;
139
- version;
140
- expected;
141
- actual;
142
- constructor(packageName, version, expected, actual) {
143
- super(`Integrity check failed for ${packageName}@${version}.\n expected: ${expected}\n actual: ${actual}\n The download was discarded. This is either corruption in transit or a tampered artifact.`);
144
- this.packageName = packageName;
145
- this.version = version;
146
- this.expected = expected;
147
- this.actual = actual;
148
- }
43
+
44
+ //#endregion
45
+ //#region src/enums/input-file-kind.ts
46
+ /** Which of the three shapes a `--file` input turned out to be. */
47
+ const InputFileKind = {
48
+ PackageJson: "package.json",
49
+ Lockfile: "lockfile",
50
+ List: "list"
149
51
  };
150
- /** Something went wrong writing the `.tgz`. */
151
- var ArchiveError = class extends BundlerErrorBase {
152
- _tag = "ArchiveError";
153
- path;
154
- constructor(path, detail, options) {
155
- super(`Failed to create archive ${path}: ${detail}`, options);
156
- this.path = path;
157
- }
52
+
53
+ //#endregion
54
+ //#region src/enums/layout.ts
55
+ /** How the resulting tarball(s) are shaped. */
56
+ const Layout = {
57
+ PerSpec: "per-spec",
58
+ Single: "single",
59
+ Dir: "dir"
158
60
  };
159
- /** Something went wrong preparing or writing to the output directory. */
160
- var OutputError = class extends BundlerErrorBase {
161
- _tag = "OutputError";
162
- path;
163
- constructor(path, detail, options) {
164
- super(`Output error at ${path}: ${detail}`, options);
165
- this.path = path;
166
- }
61
+ const Layouts = Object.values(Layout);
62
+
63
+ //#endregion
64
+ //#region src/enums/lockfile-format.ts
65
+ /** Which package manager wrote a lockfile. */
66
+ const LockfileFormat = {
67
+ Npm: "npm",
68
+ Pnpm: "pnpm",
69
+ Bun: "bun"
167
70
  };
168
- const safeHost = (registry) => {
169
- try {
170
- const url = new URL(registry);
171
- return url.host + url.pathname.replace(/\/+$/, "");
172
- } catch {
173
- return registry;
174
- }
71
+
72
+ //#endregion
73
+ //#region src/enums/phase.ts
74
+ /** The phases a bundle run moves through, in order. */
75
+ const Phase = {
76
+ Preflight: "preflight",
77
+ Resolve: "resolve",
78
+ Download: "download",
79
+ Archive: "archive",
80
+ Done: "done"
175
81
  };
176
82
 
177
83
  //#endregion
178
- //#region src/Progress.ts
179
- var Progress_exports = /* @__PURE__ */ __export({
180
- Progress: () => Progress,
181
- emit: () => emit,
182
- layerCallback: () => layerCallback,
183
- layerSilent: () => layerSilent,
184
- makeCollector: () => makeCollector
185
- });
186
- /** Service tag for progress reporting. */
187
- var Progress = class extends Context.Service()("@packall/core/Progress") {};
188
- /** Emits an event to whichever reporter is installed. */
189
- const emit = (event) => Effect.gen(function* () {
190
- yield* (yield* Progress).emit(event);
191
- });
84
+ //#region src/utils/is-record.ts
192
85
  /**
193
- * Discards every event.
86
+ * Narrows an unknown value to a plain object.
194
87
  *
195
- * The default for library consumers and for tests that do not care about
196
- * progresssilence should never require ceremony.
88
+ * The entry point for reading anything the tool does not control lockfiles,
89
+ * package.json files, registry responses without reaching for a cast.
197
90
  */
198
- const layerSilent = Layer.succeed(Progress)({ emit: () => Effect.void });
199
- /** Sends every event to a callback. Used by the CLI renderer. */
200
- const layerCallback = (onEvent) => Layer.succeed(Progress)({ emit: (event) => Effect.sync(() => onEvent(event)) });
201
- /**
202
- * Accumulates every event into a `Ref`, for assertions.
203
- *
204
- * Returned as `[layer, ref]` so a test can provide the layer and then read the
205
- * transcript afterwards.
206
- */
207
- const makeCollector = Effect.gen(function* () {
208
- const ref = yield* Ref.make([]);
209
- return {
210
- layer: Layer.succeed(Progress)({ emit: (event) => Ref.update(ref, (events) => [...events, event]) }),
211
- events: Ref.get(ref)
212
- };
213
- });
91
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
214
92
 
215
93
  //#endregion
216
- //#region src/Archive.ts
217
- var Archive_exports = /* @__PURE__ */ __export({ createArchive: () => createArchive });
94
+ //#region src/schemas/lenient.ts
218
95
  /**
219
- * Packs `entries` (paths relative to `cwd`) into a gzipped tar at `outPath`.
96
+ * An optional field that goes *absent* rather than failing when it is
97
+ * malformed.
220
98
  *
221
- * `portable` normalises uid/gid/mtime so the same inputs produce byte-identical
222
- * output which matters when a security team wants to diff two bundles or
223
- * re-derive one from a manifest.
99
+ * This is the difference between "we do not understand this field" and "we do
100
+ * not understand this document". An `os: "linux"` where an array was expected
101
+ * costs the platform filter one signal; it should not cost the run the package.
224
102
  */
225
- const createArchive = (options) => Effect.gen(function* () {
226
- const fs = yield* FileSystem.FileSystem;
227
- yield* emit({
228
- _tag: "ArchiveStarted",
229
- path: options.outPath,
230
- entryCount: options.entries.length
231
- });
232
- yield* Effect.tryPromise({
233
- try: () => create({
234
- gzip: options.gzipLevel === void 0 ? true : { level: options.gzipLevel },
235
- file: options.outPath,
236
- cwd: options.cwd,
237
- portable: true,
238
- onWriteEntry: (entry) => {
239
- entry.path = entry.path.replace(/^\.\//, "");
240
- }
241
- }, [...options.entries].sort().map(dotSlash)),
242
- catch: (cause) => new ArchiveError(options.outPath, describe$1(cause), { cause })
243
- });
244
- const info = yield* fs.stat(options.outPath);
245
- const bytes = Number(info.size);
246
- yield* emit({
247
- _tag: "ArchiveCompleted",
248
- path: options.outPath,
249
- bytes
250
- });
251
- return {
252
- path: options.outPath,
253
- bytes
254
- };
255
- });
103
+ const tolerant = (schema) => Schema.optional(Schema.UndefinedOr(schema).pipe(Schema.catchDecoding(() => Effect.succeed(Option.some(void 0)))));
256
104
  /**
257
- * Guards every entry against tar's `@` convention.
105
+ * Keeps only the string-valued members of an untyped object.
258
106
  *
259
- * In a tar file list, a leading `@` means "splice in the entries of this other
260
- * archive" a GNU convention `node-tar` implements by stripping the `@` and
261
- * looking for what is left. Every scoped package is a top-level entry starting
262
- * with `@`, so this hit the bundler in both possible ways:
263
- *
264
- * - `@oxc-project` became `oxc-project`, which does not exist, and the run died
265
- * with an ENOENT naming a path that appears nowhere in the staging tree.
266
- * - `@esbuild` became `esbuild`, which *does* exist the unscoped package of
267
- * the same name sitting right next to it. No error, and every scoped tarball
268
- * silently missing from a bundle that reported success. That is the dangerous
269
- * one: you would not find out until the install failed behind the firewall.
107
+ * `{ "lodash": "^4.0.0", "broken": 3 }` decodes to `{ "lodash": "^4.0.0" }`.
108
+ * Per-entry rather than whole-record tolerance, because one unreadable edge in
109
+ * a dependency block is no reason to forget the others.
110
+ */
111
+ const StringRecordSchema = Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.decodeTo(Schema.Record(Schema.String, Schema.String), {
112
+ decode: Getter.transform((raw) => {
113
+ const out = {};
114
+ for (const [key, value] of Object.entries(raw)) if (typeof value === "string") out[key] = value;
115
+ return out;
116
+ }),
117
+ encode: Getter.passthroughSubtype()
118
+ }));
119
+ /** The same per-entry tolerance for a list: `["linux", 7]` decodes to `["linux"]`. */
120
+ const StringArraySchema = Schema.Array(Schema.Unknown).pipe(Schema.decodeTo(Schema.Array(Schema.String), {
121
+ decode: Getter.transform((raw) => raw.flatMap((entry) => typeof entry === "string" ? entry : [])),
122
+ encode: Getter.passthroughSubtype()
123
+ }));
124
+ /**
125
+ * `peerDependenciesMeta`, normalised.
270
126
  *
271
- * `./@scope` is not subject to the convention and resolves identically.
127
+ * Only `optional` is read, and only its `true` is meaningful — npm writes the
128
+ * flag as a boolean, a hand-edited file may carry anything, and every other
129
+ * value means "not optional".
272
130
  */
273
- const dotSlash = (entry) => entry.startsWith("./") ? entry : `./${entry}`;
274
- const describe$1 = (cause) => cause instanceof Error ? cause.message : String(cause);
131
+ const OptionalFlagRecordSchema = Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.decodeTo(Schema.Record(Schema.String, Schema.Struct({ optional: Schema.Boolean })), {
132
+ decode: Getter.transform((raw) => {
133
+ const out = {};
134
+ for (const [key, value] of Object.entries(raw)) if (isRecord(value)) out[key] = { optional: value["optional"] === true };
135
+ return out;
136
+ }),
137
+ encode: Getter.passthroughSubtype()
138
+ }));
139
+ const absentWhenEmpty = (value) => Object.keys(value).length > 0 ? value : void 0;
140
+ const OptionalStringRecordSchema = StringRecordSchema.pipe(Schema.decodeTo(Schema.UndefinedOr(Schema.Record(Schema.String, Schema.String)), {
141
+ decode: Getter.transform(absentWhenEmpty),
142
+ encode: Getter.transform((value) => value ?? {})
143
+ }));
144
+ const OptionalStringArraySchema = StringArraySchema.pipe(Schema.decodeTo(Schema.UndefinedOr(Schema.Array(Schema.String)), {
145
+ decode: Getter.transform((value) => value.length > 0 ? value : void 0),
146
+ encode: Getter.transform((value) => value ?? [])
147
+ }));
148
+ const OptionalFlagRecordOrAbsentSchema = OptionalFlagRecordSchema.pipe(Schema.decodeTo(Schema.UndefinedOr(Schema.Record(Schema.String, Schema.Struct({ optional: Schema.Boolean }))), {
149
+ decode: Getter.transform(absentWhenEmpty),
150
+ encode: Getter.transform((value) => value ?? {})
151
+ }));
275
152
 
276
153
  //#endregion
277
- //#region src/Integrity.ts
278
- var Integrity_exports = /* @__PURE__ */ __export({
279
- digestOf: () => digestOf,
280
- hexDigestOf: () => hexDigestOf,
281
- parseIntegrity: () => parseIntegrity,
282
- verify: () => verify
283
- });
284
- const SUPPORTED = new Set([
285
- "sha512",
286
- "sha384",
287
- "sha256",
288
- "sha1"
289
- ]);
154
+ //#region src/spec.ts
155
+ const MAX_NAME_LENGTH = 214;
156
+ const SEGMENT_RE = /^[a-z0-9\-._~]+$/;
157
+ const DIST_TAG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
158
+ /** Renders a spec back into the canonical `name@selector` form. */
159
+ const formatSpec = (spec) => `${spec.name}@${formatSelector(spec.selector)}`;
160
+ /** Human-readable form of just the selector part. */
161
+ const formatSelector = (selector) => {
162
+ switch (selector._tag) {
163
+ case "Exact": return selector.version;
164
+ case "Range": return selector.range;
165
+ case "Tag": return selector.tag;
166
+ }
167
+ };
290
168
  /**
291
- * Parses an SRI string such as `sha512-abc...==`.
292
- *
293
- * npm permits several space-separated hashes; we keep every one we can verify
294
- * and ignore algorithms Node does not implement.
169
+ * Validates an npm package name against the rules the registry actually
170
+ * enforces. Returns `null` when valid, or the reason it is not.
295
171
  */
296
- const parseIntegrity = (integrity) => {
297
- const hashes = [];
298
- for (const token of integrity.trim().split(/\s+/)) {
299
- if (token.length === 0) continue;
300
- const dash = token.indexOf("-");
301
- if (dash <= 0) continue;
302
- const algorithm = token.slice(0, dash).toLowerCase();
303
- const digest = token.slice(dash + 1);
304
- if (!SUPPORTED.has(algorithm) || digest.length === 0) continue;
305
- hashes.push({
306
- algorithm,
307
- digest
308
- });
172
+ const validatePackageName = (name) => {
173
+ if (name.length === 0) return "name is empty";
174
+ if (name.length > MAX_NAME_LENGTH) return `name is longer than ${MAX_NAME_LENGTH} characters`;
175
+ if (name.trim() !== name) return "name has leading or trailing whitespace";
176
+ if (name.startsWith(".")) return "name cannot start with a period";
177
+ if (name.startsWith("_")) return "name cannot start with an underscore";
178
+ if (name !== name.toLowerCase()) return "name cannot contain capital letters";
179
+ if (name.startsWith("@")) {
180
+ const slash = name.indexOf("/");
181
+ if (slash === -1) return "scoped name is missing the '/name' part";
182
+ const scope = name.slice(1, slash);
183
+ const rest = name.slice(slash + 1);
184
+ if (scope.length === 0) return "scope is empty";
185
+ if (rest.length === 0) return "name after the scope is empty";
186
+ if (rest.includes("/")) return "name contains more than one '/'";
187
+ if (!SEGMENT_RE.test(scope)) return `scope "${scope}" contains illegal characters`;
188
+ if (!SEGMENT_RE.test(rest)) return `name "${rest}" contains illegal characters`;
189
+ return null;
309
190
  }
310
- return hashes;
311
- };
312
- /** Computes the base64 digest of some bytes under one algorithm. */
313
- const digestOf = (data, algorithm) => createHash(algorithm).update(data).digest("base64");
314
- /** Computes the hex digest of some bytes — the form legacy `shasum` uses. */
315
- const hexDigestOf = (data, algorithm) => createHash(algorithm).update(data).digest("hex");
316
- /** Constant-time comparison of two digest strings of equal length. */
317
- const digestsEqual = (a, b) => {
318
- const left = Buffer.from(a);
319
- const right = Buffer.from(b);
320
- return left.length === right.length && timingSafeEqual(left, right);
191
+ if (name.includes("/")) return "unscoped name cannot contain '/'";
192
+ if (!SEGMENT_RE.test(name)) return "name contains illegal characters";
193
+ return null;
321
194
  };
322
195
  /**
323
- * Checks bytes against whichever checksums the registry supplied.
196
+ * Parses a single spec string.
324
197
  *
325
- * SRI is preferred; `shasum` is the fallback for versions published before
326
- * integrity strings existed. If the registry gave us neither, that is reported
327
- * as `Unverifiable` rather than silently treated as a pass — the caller decides
328
- * whether to tolerate it.
198
+ * Splitting on `@` is the fiddly part: a scoped name *starts* with `@`, so we
199
+ * look for the last `@` that is not at index 0.
329
200
  */
330
- const verify = (data, dist) => {
331
- if (dist.integrity !== void 0 && dist.integrity.length > 0) {
332
- const hashes = parseIntegrity(dist.integrity);
333
- if (hashes.length === 0) return {
334
- _tag: "Unverifiable",
335
- reason: `no supported algorithm in "${dist.integrity}"`
336
- };
337
- const best = [...hashes].sort((a, b) => strength(b.algorithm) - strength(a.algorithm))[0];
338
- const actual = digestOf(data, best.algorithm);
339
- return digestsEqual(actual, best.digest) ? {
340
- _tag: "Verified",
341
- using: best.algorithm
342
- } : {
343
- _tag: "Mismatch",
344
- expected: `${best.algorithm}-${best.digest}`,
345
- actual: `${best.algorithm}-${actual}`
346
- };
347
- }
348
- if (dist.shasum !== void 0 && dist.shasum.length > 0) {
349
- const actual = hexDigestOf(data, "sha1");
350
- return digestsEqual(actual.toLowerCase(), dist.shasum.toLowerCase()) ? {
351
- _tag: "Verified",
352
- using: "sha1"
353
- } : {
354
- _tag: "Mismatch",
355
- expected: `sha1-${dist.shasum}`,
356
- actual: `sha1-${actual}`
357
- };
358
- }
201
+ const parseSpec = (raw) => {
202
+ const input = raw.trim();
203
+ if (input.length === 0) throw new InvalidSpecError(raw, "spec is empty");
204
+ const at = input.lastIndexOf("@");
205
+ const hasSelector = at > 0;
206
+ const name = hasSelector ? input.slice(0, at) : input;
207
+ const selectorText = hasSelector ? input.slice(at + 1) : "";
208
+ const nameProblem = validatePackageName(name);
209
+ if (nameProblem !== null) throw new InvalidSpecError(raw, nameProblem);
210
+ if (!hasSelector || selectorText.length === 0) return {
211
+ name,
212
+ selector: {
213
+ _tag: "Tag",
214
+ tag: "latest"
215
+ },
216
+ raw: input
217
+ };
359
218
  return {
360
- _tag: "Unverifiable",
361
- reason: "registry advertised no integrity or shasum"
219
+ name,
220
+ selector: parseSelector(raw, selectorText),
221
+ raw: input
362
222
  };
363
223
  };
364
- const strength = (algorithm) => {
365
- switch (algorithm) {
366
- case "sha512": return 4;
367
- case "sha384": return 3;
368
- case "sha256": return 2;
369
- default: return 1;
370
- }
371
- };
372
-
373
- //#endregion
374
- //#region src/Layout.ts
375
- var Layout_exports = /* @__PURE__ */ __export({
376
- MANIFEST_FILE: () => MANIFEST_FILE,
377
- README_FILE: () => README_FILE,
378
- importGuide: () => importGuide,
379
- packagePath: () => packagePath,
380
- perSpecArchiveName: () => perSpecArchiveName,
381
- singleArchiveName: () => singleArchiveName,
382
- splitName: () => splitName,
383
- tarballFileName: () => tarballFileName
384
- });
385
- /**
386
- * The on-disk shape of a bundle.
387
- *
388
- * Everything is laid out exactly the way a registry serves it:
389
- *
390
- * lodash/-/lodash-4.17.21.tgz
391
- * @babel/core/-/core-7.24.0.tgz
392
- *
393
- * That is not an Artifactory convention — it is the path structure in every
394
- * `dist.tarball` URL npm publishes, which is why the same tree imports into
395
- * Artifactory, Nexus and Verdaccio without translation. Note that a scoped
396
- * package's file name drops the scope: `@babel/core` becomes `core-7.24.0.tgz`
397
- * under an `@babel/core/-/` directory.
398
- */
399
- /** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
400
- const splitName = (name) => {
401
- if (!name.startsWith("@")) return {
402
- scope: void 0,
403
- bare: name
224
+ /** Classifies the part after the `@` as an exact version, a range, or a dist-tag. */
225
+ const parseSelector = (raw, text) => {
226
+ const exact = semver.valid(text, { loose: false });
227
+ if (exact !== null) return {
228
+ _tag: "Exact",
229
+ version: exact
404
230
  };
405
- const slash = name.indexOf("/");
406
- if (slash === -1) return {
407
- scope: void 0,
408
- bare: name
231
+ if (semver.validRange(text, { loose: false }) !== null) return {
232
+ _tag: "Range",
233
+ range: text
409
234
  };
235
+ if (!DIST_TAG_RE.test(text)) throw new InvalidSpecError(raw, `"${text}" is not a valid version, semver range, or dist-tag`);
410
236
  return {
411
- scope: name.slice(0, slash),
412
- bare: name.slice(slash + 1)
237
+ _tag: "Tag",
238
+ tag: text
413
239
  };
414
240
  };
415
- /** `@babel/core` + `7.24.0` -> `core-7.24.0.tgz` */
416
- const tarballFileName = (name, version) => {
417
- const { bare } = splitName(name);
418
- return `${bare}-${version}.tgz`;
419
- };
420
- /**
421
- * The path of one package tarball within a bundle, using `/` separators
422
- * regardless of host platform — these become tar entry names, and tar entries
423
- * are always POSIX.
424
- */
425
- const packagePath = (name, version) => `${name}/-/${tarballFileName(name, version)}`;
426
- /** File name of the manifest that ships inside every bundle. */
427
- const MANIFEST_FILE = "bundle-manifest.json";
428
- /** File name of the short import guide that ships inside every bundle. */
429
- const README_FILE = "IMPORT.md";
430
241
  /**
431
- * Name of the archive produced for one root spec in `per-spec` layout.
432
- *
433
- * Scoped names are flattened (`@babel/core` -> `babel-core`) because a `/` in a
434
- * file name is not a thing, and `@` confuses enough shells and web UIs to be
435
- * worth avoiding.
242
+ * Parses many specs, collecting *every* failure rather than stopping at the
243
+ * first. Somebody bundling forty packages should be told about all four typos
244
+ * in one go, not made to re-run four times.
436
245
  */
437
- const perSpecArchiveName = (name, version) => {
438
- const { scope, bare } = splitName(name);
439
- return `${scope === void 0 ? bare : `${scope.slice(1)}-${bare}`}-${version}.tgz`;
246
+ const parseSpecs = (inputs) => {
247
+ const specs = [];
248
+ const errors = [];
249
+ for (const input of inputs) try {
250
+ specs.push(parseSpec(input));
251
+ } catch (error) {
252
+ if (error instanceof InvalidSpecError) errors.push(error);
253
+ else throw error;
254
+ }
255
+ return {
256
+ specs,
257
+ errors
258
+ };
440
259
  };
441
- /** Name of the archive produced in `single` layout. */
442
- const singleArchiveName = (base = "bundle") => `${base}.tgz`;
443
260
  /**
444
- * The import guide written into each bundle.
261
+ * Collapses duplicate specs, keeping the first occurrence.
445
262
  *
446
- * Kept short and copy-pasteable on purpose: whoever opens this is mid-task on a
447
- * restricted network and does not want prose.
263
+ * `npmb react react@latest` is a plausible thing to type and should not
264
+ * download React twice.
448
265
  */
449
- const importGuide = (options) => `# Importing this bundle
450
-
451
- ${options.packageCount} package tarball(s), laid out exactly as an npm registry serves them:
452
-
453
- <package-name>/-/<file>.tgz
454
- @<scope>/<name>/-/<file>.tgz
455
-
456
- Created ${options.createdAt} by packall ${options.toolVersion}.
457
- See ${MANIFEST_FILE} for the full list with checksums.
458
-
459
- ## JFrog Artifactory
460
-
461
- Unpack, then upload the tree into a **local npm** repository. The layout already
462
- matches what Artifactory expects, so no path rewriting is needed:
463
-
464
- tar -xzf <this-bundle>.tgz -C ./bundle
465
- jf rt upload "bundle/(**)" "<npm-local-repo>/{1}" --flat=false
466
-
467
- ## Verdaccio / Nexus / any npm registry
468
-
469
- Publish each tarball individually:
470
-
471
- find ./bundle -name '*.tgz' -exec npm publish --registry <url> {} \\;
472
-
473
- ## Verifying before import
474
-
475
- node -e "const m=require('./bundle/${MANIFEST_FILE}');console.log(m.packages.length+' packages')"
476
-
477
- Every entry in ${MANIFEST_FILE} carries the integrity string the source registry
478
- advertised, and each tarball was checked against it at download time.
479
- `;
266
+ const dedupeSpecs = (specs) => {
267
+ const seen = /* @__PURE__ */ new Set();
268
+ const out = [];
269
+ for (const spec of specs) {
270
+ const key = formatSpec(spec);
271
+ if (seen.has(key)) continue;
272
+ seen.add(key);
273
+ out.push(spec);
274
+ }
275
+ return out;
276
+ };
480
277
 
481
278
  //#endregion
482
- //#region src/Registry.ts
279
+ //#region src/dependency-range.ts
280
+ const UNSUPPORTED_PROTOCOLS = [
281
+ ["file:", "local file path"],
282
+ ["link:", "local link"],
283
+ ["workspace:", "workspace protocol"],
284
+ ["portal:", "portal protocol"],
285
+ ["patch:", "patch protocol"],
286
+ ["git:", "git dependency"],
287
+ ["git+", "git dependency"],
288
+ ["github:", "GitHub shorthand"],
289
+ ["gitlab:", "GitLab shorthand"],
290
+ ["bitbucket:", "Bitbucket shorthand"],
291
+ ["http:", "remote tarball URL"],
292
+ ["https:", "remote tarball URL"]
293
+ ];
483
294
  /**
484
- * Service tag for the registry backend.
485
- *
486
- * @example
487
- * ```ts
488
- * import { Effect } from "effect"
489
- * import { Registry } from "@packall/core"
295
+ * Classifies one `name -> range` entry.
490
296
  *
491
- * const program = Effect.gen(function*() {
492
- * const registry = yield* Registry
493
- * return yield* registry.packument("lodash")
494
- * })
495
- * ```
297
+ * An empty range, `*`, and `latest` all mean "any published version"; npm
298
+ * treats them interchangeably and so do we.
496
299
  */
497
- var Registry = class extends Context.Service()("@packall/core/Registry") {};
498
-
499
- //#endregion
500
- //#region src/Download.ts
501
- var Download_exports = /* @__PURE__ */ __export({ downloadAll: () => downloadAll });
300
+ const parseDependencyTarget = (name, raw) => {
301
+ const text = raw.trim();
302
+ if (text.length === 0 || text === "*" || text === "x" || text === "latest") return {
303
+ _tag: "Registry",
304
+ name,
305
+ selector: {
306
+ _tag: "Range",
307
+ range: "*"
308
+ }
309
+ };
310
+ if (text.startsWith("npm:")) return parseAlias(name, text);
311
+ for (const [prefix, reason] of UNSUPPORTED_PROTOCOLS) if (text.startsWith(prefix)) return {
312
+ _tag: "Unsupported",
313
+ name,
314
+ raw: text,
315
+ reason
316
+ };
317
+ if (semver.validRange(text, { loose: true }) === null && /^[\w.-]+\/[\w.-]+/.test(text)) return {
318
+ _tag: "Unsupported",
319
+ name,
320
+ raw: text,
321
+ reason: "git shorthand"
322
+ };
323
+ const exact = semver.valid(text, { loose: true });
324
+ if (exact !== null) return {
325
+ _tag: "Registry",
326
+ name,
327
+ selector: {
328
+ _tag: "Exact",
329
+ version: exact
330
+ }
331
+ };
332
+ if (semver.validRange(text, { loose: true }) !== null) return {
333
+ _tag: "Registry",
334
+ name,
335
+ selector: {
336
+ _tag: "Range",
337
+ range: text
338
+ }
339
+ };
340
+ if (/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(text)) return {
341
+ _tag: "Registry",
342
+ name,
343
+ selector: {
344
+ _tag: "Tag",
345
+ tag: text
346
+ }
347
+ };
348
+ return {
349
+ _tag: "Unsupported",
350
+ name,
351
+ raw: text,
352
+ reason: "unrecognised version specifier"
353
+ };
354
+ };
502
355
  /**
503
- * Downloads every package into `destDir`, laid out the way a registry serves
504
- * them.
356
+ * Parses `npm:<name>[@<range>]`.
505
357
  *
506
- * `verifyIntegrity` defaulting to on is deliberate: a corrupt tarball that
507
- * makes it into a corporate registry is far more expensive than a failed run.
358
+ * The nested name may itself be scoped, so the `@` split has the same
359
+ * "not at index 0" caveat as top-level spec parsing.
508
360
  */
509
- const downloadAll = (packages, destDir, options) => Effect.gen(function* () {
510
- const fs = yield* FileSystem.FileSystem;
511
- const path = yield* Path.Path;
512
- const registry = yield* Registry;
513
- yield* emit({
514
- _tag: "PhaseStarted",
515
- phase: "download",
516
- total: packages.length
517
- });
518
- let completed = 0;
519
- const sizes = /* @__PURE__ */ new Map();
520
- const unverified = [];
521
- const results = yield* Effect.forEach(packages, (pkg) => Effect.gen(function* () {
522
- const key = `${pkg.name}@${pkg.version}`;
523
- yield* emit({
524
- _tag: "DownloadStarted",
525
- name: pkg.name,
526
- version: pkg.version
527
- });
528
- const bytes = yield* registry.download(pkg.manifest);
529
- if (options.verifyIntegrity) {
530
- const result = verify(bytes, pkg.manifest.dist);
531
- if (result._tag === "Mismatch") return yield* Effect.fail(new IntegrityError(pkg.name, pkg.version, result.expected, result.actual));
532
- if (result._tag === "Unverifiable") yield* emit({
533
- _tag: "Warning",
534
- message: `${key} could not be verified (${result.reason})`
535
- });
536
- }
537
- const verifiable = (pkg.manifest.dist.integrity?.length ?? 0) > 0 || (pkg.manifest.dist.shasum?.length ?? 0) > 0;
538
- const relative = packagePath(pkg.name, pkg.version);
539
- const target = path.join(destDir, ...relative.split("/"));
540
- yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.mapError((cause) => new OutputError(path.dirname(target), "could not create directory", { cause })));
541
- yield* fs.writeFile(target, bytes).pipe(Effect.mapError((cause) => new OutputError(target, "could not write tarball", { cause })));
542
- completed += 1;
543
- yield* emit({
544
- _tag: "DownloadCompleted",
545
- name: pkg.name,
546
- version: pkg.version,
547
- bytes: bytes.byteLength,
548
- completedCount: completed,
549
- totalCount: packages.length
550
- });
551
- return {
552
- key,
553
- bytes: bytes.byteLength,
554
- verifiable
555
- };
556
- }), { concurrency: options.concurrency });
557
- for (const result of results) {
558
- sizes.set(result.key, result.bytes);
559
- if (!result.verifiable) unverified.push(result.key);
560
- }
561
- yield* emit({
562
- _tag: "PhaseCompleted",
563
- phase: "download"
564
- });
361
+ const parseAlias = (key, text) => {
362
+ const body = text.slice(4);
363
+ if (body.length === 0) return {
364
+ _tag: "Unsupported",
365
+ name: key,
366
+ raw: text,
367
+ reason: "empty npm: alias"
368
+ };
369
+ const at = body.lastIndexOf("@");
370
+ const target = at > 0 ? body.slice(0, at) : body;
371
+ const rangeText = at > 0 ? body.slice(at + 1) : "";
372
+ if (target.length === 0) return {
373
+ _tag: "Unsupported",
374
+ name: key,
375
+ raw: text,
376
+ reason: "empty npm: alias target"
377
+ };
378
+ const inner = parseDependencyTarget(target, rangeText);
379
+ if (inner._tag === "Unsupported") return {
380
+ ...inner,
381
+ name: key,
382
+ raw: text
383
+ };
565
384
  return {
566
- sizes,
567
- unverified,
568
- totalBytes: results.reduce((sum, r) => sum + r.bytes, 0)
385
+ _tag: "Registry",
386
+ name: target,
387
+ selector: inner.selector,
388
+ aliasOf: key
569
389
  };
570
- });
390
+ };
571
391
 
572
392
  //#endregion
573
- //#region src/Platform.ts
574
- var Platform_exports = /* @__PURE__ */ __export({
575
- allPlatforms: () => allPlatforms,
576
- currentPlatform: () => currentPlatform,
577
- formatPlatformFilter: () => formatPlatformFilter,
578
- isIncluded: () => isIncluded,
579
- matchesTarget: () => matchesTarget,
580
- parsePlatformTarget: () => parsePlatformTarget,
581
- platformTargets: () => platformTargets
582
- });
393
+ //#region src/platform.ts
583
394
  const allPlatforms = { _tag: "All" };
584
395
  const platformTargets = (targets) => ({
585
396
  _tag: "Targets",
@@ -598,7 +409,7 @@ const currentPlatform = () => ({
598
409
  * and installs nothing. `--platform windows` silently matching no binding at
599
410
  * all would be precisely that, so it is corrected rather than accepted.
600
411
  */
601
- const OS_ALIASES = {
412
+ const osByAlias = {
602
413
  win: "win32",
603
414
  windows: "win32",
604
415
  mac: "darwin",
@@ -632,27 +443,24 @@ const LIBC = new Set(["musl", "glibc"]);
632
443
  * once alongside the list of accepted forms.
633
444
  */
634
445
  const parsePlatformTarget = (input) => {
635
- const parts = input.trim().toLowerCase().split("-").filter((p) => p.length > 0);
636
- if (parts.length === 0 || parts.length > 3) return null;
637
- const rawOs = parts[0];
638
- const os = OS_ALIASES[rawOs] ?? rawOs;
446
+ const parts = input.trim().toLowerCase().split("-").filter((part) => part.length > 0);
447
+ const [rawOs, second, third] = parts;
448
+ if (rawOs === void 0 || parts.length > 3) return null;
449
+ const os = osByAlias[rawOs] ?? rawOs;
639
450
  if (!KNOWN_OS.has(os)) return null;
640
- if (parts.length === 1) return { os };
641
- if (parts.length === 2 && LIBC.has(parts[1])) return {
451
+ if (second === void 0) return { os };
452
+ if (third === void 0) return LIBC.has(second) ? {
642
453
  os,
643
- libc: parts[1]
644
- };
645
- const cpu = parts[1];
646
- if (parts.length === 2) return {
454
+ libc: second
455
+ } : {
647
456
  os,
648
- cpu
457
+ cpu: second
649
458
  };
650
- const libc = parts[2];
651
- if (!LIBC.has(libc)) return null;
459
+ if (!LIBC.has(third)) return null;
652
460
  return {
653
461
  os,
654
- cpu,
655
- libc
462
+ cpu: second,
463
+ libc: third
656
464
  };
657
465
  };
658
466
  /**
@@ -698,168 +506,299 @@ const isIncluded = (constraints, filter) => {
698
506
  /** Renders a filter for the manifest and for `--dry-run` output. */
699
507
  const formatPlatformFilter = (filter) => {
700
508
  if (filter._tag === "All") return "all";
701
- return filter.targets.map((t) => [
702
- t.os,
703
- t.cpu,
704
- t.libc
509
+ return filter.targets.map((target) => [
510
+ target.os,
511
+ target.cpu,
512
+ target.libc
705
513
  ].filter((part) => part !== void 0).join("-")).join(",");
706
514
  };
707
515
 
708
516
  //#endregion
709
- //#region src/Spec.ts
710
- var Spec_exports = /* @__PURE__ */ __export({
711
- dedupeSpecs: () => dedupeSpecs,
712
- formatSelector: () => formatSelector,
713
- formatSpec: () => formatSpec,
714
- parseSelector: () => parseSelector,
715
- parseSpec: () => parseSpec,
716
- parseSpecs: () => parseSpecs,
717
- validatePackageName: () => validatePackageName
718
- });
719
- /** Renders a spec back into the canonical `name@selector` form. */
720
- const formatSpec = (spec) => {
721
- switch (spec.selector._tag) {
722
- case "Exact": return `${spec.name}@${spec.selector.version}`;
723
- case "Range": return `${spec.name}@${spec.selector.range}`;
724
- case "Tag": return `${spec.name}@${spec.selector.tag}`;
725
- }
517
+ //#region src/options.ts
518
+ const defaultScope = {
519
+ optional: true,
520
+ peer: true,
521
+ platforms: allPlatforms
726
522
  };
727
- /** Human-readable form of just the selector part. */
728
- const formatSelector = (selector) => {
729
- switch (selector._tag) {
730
- case "Exact": return selector.version;
731
- case "Range": return selector.range;
732
- case "Tag": return selector.tag;
733
- }
523
+ const defaultResolveOptions = {
524
+ scope: defaultScope,
525
+ allVersions: false,
526
+ includePrerelease: false,
527
+ concurrency: 10
734
528
  };
735
- const MAX_NAME_LENGTH = 214;
529
+ const defaultBundleOptions = {
530
+ ...defaultResolveOptions,
531
+ layout: Layout.PerSpec,
532
+ dryRun: false,
533
+ verifyIntegrity: true,
534
+ force: false
535
+ };
536
+
537
+ //#endregion
538
+ //#region src/registry.ts
736
539
  /**
737
- * Validates an npm package name against the rules the registry actually
738
- * enforces. Returns `null` when valid, or the reason it is not.
739
- */
740
- const validatePackageName = (name) => {
741
- if (name.length === 0) return "name is empty";
742
- if (name.length > MAX_NAME_LENGTH) return `name is longer than ${MAX_NAME_LENGTH} characters`;
743
- if (name.trim() !== name) return "name has leading or trailing whitespace";
744
- if (name.startsWith(".")) return "name cannot start with a period";
745
- if (name.startsWith("_")) return "name cannot start with an underscore";
746
- if (name !== name.toLowerCase()) return "name cannot contain capital letters";
747
- if (name.startsWith("@")) {
748
- const slash = name.indexOf("/");
749
- if (slash === -1) return "scoped name is missing the '/name' part";
750
- const scope = name.slice(1, slash);
751
- const rest = name.slice(slash + 1);
752
- if (scope.length === 0) return "scope is empty";
753
- if (rest.length === 0) return "name after the scope is empty";
754
- if (rest.includes("/")) return "name contains more than one '/'";
755
- if (!SEGMENT_RE.test(scope)) return `scope "${scope}" contains illegal characters`;
756
- if (!SEGMENT_RE.test(rest)) return `name "${rest}" contains illegal characters`;
757
- return null;
540
+ * Service tag for the registry backend.
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * import { Effect } from "effect"
545
+ * import { Registry } from "@packall/core"
546
+ *
547
+ * const program = Effect.gen(function*() {
548
+ * const registry = yield* Registry
549
+ * return yield* registry.packument("lodash")
550
+ * })
551
+ * ```
552
+ */
553
+ var Registry = class extends Context.Service()("@packall/core/Registry") {};
554
+
555
+ //#endregion
556
+ //#region src/integrity.ts
557
+ /**
558
+ * The digests npm publishes, mapped to what `Crypto.digest` calls them.
559
+ *
560
+ * Doubles as the supported-algorithm list: an SRI token whose algorithm is not
561
+ * a key here is one we cannot check, and is dropped during parsing.
562
+ */
563
+ const digestAlgorithmByName = {
564
+ sha512: "SHA-512",
565
+ sha384: "SHA-384",
566
+ sha256: "SHA-256",
567
+ sha1: "SHA-1"
568
+ };
569
+ const isSupported = (algorithm) => Object.hasOwn(digestAlgorithmByName, algorithm);
570
+ /**
571
+ * Parses an SRI string such as `sha512-abc...==`.
572
+ *
573
+ * npm permits several space-separated hashes; we keep every one we can verify
574
+ * and ignore algorithms we cannot.
575
+ */
576
+ const parseIntegrity = (integrity) => {
577
+ const hashes = [];
578
+ for (const token of integrity.trim().split(/\s+/)) {
579
+ if (token.length === 0) continue;
580
+ const dash = token.indexOf("-");
581
+ if (dash <= 0) continue;
582
+ const algorithm = token.slice(0, dash).toLowerCase();
583
+ const digest = token.slice(dash + 1);
584
+ if (!isSupported(algorithm) || digest.length === 0) continue;
585
+ hashes.push({
586
+ algorithm,
587
+ digest
588
+ });
758
589
  }
759
- if (name.includes("/")) return "unscoped name cannot contain '/'";
760
- if (!SEGMENT_RE.test(name)) return "name contains illegal characters";
761
- return null;
590
+ return hashes;
762
591
  };
763
- const SEGMENT_RE = /^[a-z0-9\-._~]+$/;
592
+ const digestBytes = (data, algorithm) => Effect.gen(function* () {
593
+ return yield* (yield* Crypto.Crypto).digest(digestAlgorithmByName[algorithm], data);
594
+ });
595
+ /** Computes the base64 digest of some bytes under one algorithm. */
596
+ const digestOf = (data, algorithm) => Effect.map(digestBytes(data, algorithm), Encoding.encodeBase64);
597
+ /** Computes the hex digest of some bytes — the form legacy `shasum` uses. */
598
+ const hexDigestOf = (data, algorithm) => Effect.map(digestBytes(data, algorithm), Encoding.encodeHex);
764
599
  /**
765
- * Parses a single spec string.
600
+ * Constant-time comparison of two digest strings.
766
601
  *
767
- * Splitting on `@` is the fiddly part: a scoped name *starts* with `@`, so we
768
- * look for the last `@` that is not at index 0.
602
+ * Written by hand rather than with `node:crypto`'s `timingSafeEqual`, which
603
+ * needs `Buffer` and would pin this module to Node. Both digests here are
604
+ * public knowledge, so the timing property is close to theatre — but it costs
605
+ * four lines, and the alternative is explaining in a review why it was dropped.
769
606
  */
770
- const parseSpec = (raw) => {
771
- const input = raw.trim();
772
- if (input.length === 0) throw new InvalidSpecError(raw, "spec is empty");
773
- const at = input.lastIndexOf("@");
774
- const hasSelector = at > 0;
775
- const name = hasSelector ? input.slice(0, at) : input;
776
- const selectorText = hasSelector ? input.slice(at + 1) : "";
777
- const nameProblem = validatePackageName(name);
778
- if (nameProblem !== null) throw new InvalidSpecError(raw, nameProblem);
779
- if (!hasSelector || selectorText.length === 0) return {
780
- name,
781
- selector: {
782
- _tag: "Tag",
783
- tag: "latest"
784
- },
785
- raw: input
786
- };
607
+ const digestsEqual = (a, b) => {
608
+ if (a.length !== b.length) return false;
609
+ let difference = 0;
610
+ for (let index = 0; index < a.length; index++) difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
611
+ return difference === 0;
612
+ };
613
+ /**
614
+ * Checks bytes against whichever checksums the registry supplied.
615
+ *
616
+ * SRI is preferred; `shasum` is the fallback for versions published before
617
+ * integrity strings existed. If the registry gave us neither, that is reported
618
+ * as `Unverifiable` rather than silently treated as a pass — the caller decides
619
+ * whether to tolerate it.
620
+ */
621
+ const verify = (data, dist) => Effect.gen(function* () {
622
+ if (dist.integrity !== void 0 && dist.integrity.length > 0) {
623
+ const [best] = parseIntegrity(dist.integrity).toSorted((a, b) => strengthByAlgorithm[b.algorithm] - strengthByAlgorithm[a.algorithm]);
624
+ if (best === void 0) return {
625
+ _tag: "Unverifiable",
626
+ reason: `no supported algorithm in "${dist.integrity}"`
627
+ };
628
+ const actual = yield* digestOf(data, best.algorithm);
629
+ return digestsEqual(actual, best.digest) ? {
630
+ _tag: "Verified",
631
+ using: best.algorithm
632
+ } : {
633
+ _tag: "Mismatch",
634
+ expected: `${best.algorithm}-${best.digest}`,
635
+ actual: `${best.algorithm}-${actual}`
636
+ };
637
+ }
638
+ if (dist.shasum !== void 0 && dist.shasum.length > 0) {
639
+ const actual = yield* hexDigestOf(data, "sha1");
640
+ return digestsEqual(actual.toLowerCase(), dist.shasum.toLowerCase()) ? {
641
+ _tag: "Verified",
642
+ using: "sha1"
643
+ } : {
644
+ _tag: "Mismatch",
645
+ expected: `sha1-${dist.shasum}`,
646
+ actual: `sha1-${actual}`
647
+ };
648
+ }
787
649
  return {
788
- name,
789
- selector: parseSelector(raw, selectorText),
790
- raw: input
650
+ _tag: "Unverifiable",
651
+ reason: "registry advertised no integrity or shasum"
791
652
  };
653
+ });
654
+ const strengthByAlgorithm = {
655
+ sha512: 4,
656
+ sha384: 3,
657
+ sha256: 2,
658
+ sha1: 1
792
659
  };
793
- /** Classifies the part after the `@` as an exact version, a range, or a dist-tag. */
794
- const parseSelector = (raw, text) => {
795
- const exact = semver.valid(text, { loose: false });
796
- if (exact !== null) return {
797
- _tag: "Exact",
798
- version: exact
660
+
661
+ //#endregion
662
+ //#region src/layout.ts
663
+ /**
664
+ * The on-disk shape of a bundle.
665
+ *
666
+ * Everything is laid out exactly the way a registry serves it:
667
+ *
668
+ * lodash/-/lodash-4.17.21.tgz
669
+ * @babel/core/-/core-7.24.0.tgz
670
+ *
671
+ * That is not an Artifactory convention — it is the path structure in every
672
+ * `dist.tarball` URL npm publishes. See `notes/requirements.md` § FR-12 for
673
+ * what that buys.
674
+ *
675
+ * The one shape worth noticing before editing anything here: a scoped
676
+ * package's file name drops the scope, so `@babel/core` becomes
677
+ * `core-7.24.0.tgz` under an `@babel/core/-/` directory.
678
+ */
679
+ /** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
680
+ const splitName = (name) => {
681
+ if (!name.startsWith("@")) return {
682
+ scope: void 0,
683
+ bare: name
799
684
  };
800
- if (semver.validRange(text, { loose: false }) !== null) return {
801
- _tag: "Range",
802
- range: text
685
+ const slash = name.indexOf("/");
686
+ if (slash === -1) return {
687
+ scope: void 0,
688
+ bare: name
803
689
  };
804
- if (!DIST_TAG_RE.test(text)) throw new InvalidSpecError(raw, `"${text}" is not a valid version, semver range, or dist-tag`);
805
690
  return {
806
- _tag: "Tag",
807
- tag: text
691
+ scope: name.slice(0, slash),
692
+ bare: name.slice(slash + 1)
808
693
  };
809
694
  };
810
- const DIST_TAG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
695
+ /** `@babel/core` + `7.24.0` -> `core-7.24.0.tgz` */
696
+ const tarballFileName = (name, version) => {
697
+ const { bare } = splitName(name);
698
+ return `${bare}-${version}.tgz`;
699
+ };
811
700
  /**
812
- * Parses many specs, collecting *every* failure rather than stopping at the
813
- * first. Somebody bundling forty packages should be told about all four typos
814
- * in one go, not made to re-run four times.
701
+ * The path of one package tarball within a bundle, using `/` separators
702
+ * regardless of host platform these become tar entry names, and tar entries
703
+ * are always POSIX.
815
704
  */
816
- const parseSpecs = (inputs) => {
817
- const specs = [];
818
- const errors = [];
819
- for (const input of inputs) try {
820
- specs.push(parseSpec(input));
821
- } catch (error) {
822
- if (error instanceof InvalidSpecError) errors.push(error);
823
- else throw error;
824
- }
825
- return {
826
- specs,
827
- errors
828
- };
829
- };
705
+ const packagePath = (name, version) => `${name}/-/${tarballFileName(name, version)}`;
706
+ /** File name of the manifest that ships inside every bundle. */
707
+ const MANIFEST_FILE = "bundle-manifest.json";
708
+ /** File name of the short import guide that ships inside every bundle. */
709
+ const README_FILE = "IMPORT.md";
830
710
  /**
831
- * Collapses duplicate specs, keeping the first occurrence.
711
+ * Name of the archive produced for one root spec in `per-spec` layout.
832
712
  *
833
- * `npmb react react@latest` is a plausible thing to type and should not
834
- * download React twice.
713
+ * It is the spec, with the one character a file name cannot hold taken out:
714
+ * `@types/react@19.2.18` becomes `@types-react@19.2.18.tgz`. So the name of the
715
+ * file answers the question somebody has when they are looking at a directory
716
+ * of them — which package, and which version — in the notation they would have
717
+ * typed to ask for it.
718
+ *
719
+ * The version is joined with `@` rather than `-` for the same reason. A
720
+ * hyphen made `foo-bar-1.0.0` ambiguous by eye: package `foo` at `bar-1.0.0`,
721
+ * or `foo-bar` at `1.0.0`? npm's own syntax has no such problem, because a
722
+ * package name may not contain `@` beyond its scope.
723
+ *
724
+ * **The scope's `@` stays**, and it is the difference between a correct bundle
725
+ * and a quietly incomplete one.
726
+ *
727
+ * Dropping it — which this did, matching what `npm pack` produces — meant
728
+ * `@something/somewhat` and `something-somewhat` were both
729
+ * `something-somewhat-1.0.0.tgz`. Two different packages, one path: the second
730
+ * landed on the first, and the run reported two artifacts for the one file that
731
+ * survived. A bundle short a package, called complete, found at the far end of
732
+ * an air gap.
733
+ *
734
+ * Keeping the `@` makes that impossible by construction rather than by a check,
735
+ * because an unscoped npm name may not begin with one. It is also the more
736
+ * honest name: the scope is part of what the package *is*, and a directory of
737
+ * archives that has forgotten which ones were scoped is a directory somebody
738
+ * has to cross-reference against a manifest.
739
+ *
740
+ * It does not make every collision impossible — `@a/b-c` and `@a-b/c` still
741
+ * meet — so `assertDistinctOutputs` still runs. That pair is pathological; the
742
+ * scoped-against-unscoped one is `@babel/core` beside `babel-core`, which is a
743
+ * real migration somebody could be bundling.
744
+ *
745
+ * The leading `@` is safe here in a way it is not inside the archive: tar's
746
+ * splice convention applies to the *file list* it is given, which is what
747
+ * `archive.ts` guards with `./`. This is the output path, not an entry.
835
748
  */
836
- const dedupeSpecs = (specs) => {
837
- const seen = /* @__PURE__ */ new Set();
838
- const out = [];
839
- for (const spec of specs) {
840
- const key = formatSpec(spec);
841
- if (seen.has(key)) continue;
842
- seen.add(key);
843
- out.push(spec);
844
- }
845
- return out;
749
+ const perSpecArchiveName = (name, version) => {
750
+ const { scope, bare } = splitName(name);
751
+ return `${scope === void 0 ? bare : `${scope}-${bare}`}@${version}.tgz`;
846
752
  };
753
+ /** Name of the archive produced in `single` layout. */
754
+ const singleArchiveName = (base = "bundle") => `${base}.tgz`;
755
+ /**
756
+ * The import guide written into each bundle.
757
+ *
758
+ * Kept short and copy-pasteable on purpose: whoever opens this is mid-task on a
759
+ * restricted network and does not want prose.
760
+ */
761
+ const importGuide = (options) => `# Importing this bundle
762
+
763
+ ${options.packageCount} package tarball(s), laid out exactly as an npm registry serves them:
764
+
765
+ <package-name>/-/<file>.tgz
766
+ @<scope>/<name>/-/<file>.tgz
767
+
768
+ Created ${options.createdAt} by packall ${options.toolVersion}.
769
+ See ${MANIFEST_FILE} for the full list with checksums.
770
+
771
+ ## JFrog Artifactory
772
+
773
+ Unpack, then upload the tree into a **local npm** repository. The layout already
774
+ matches what Artifactory expects, so no path rewriting is needed:
775
+
776
+ tar -xzf <this-bundle>.tgz -C ./bundle
777
+ jf rt upload "bundle/(**)" "<npm-local-repo>/{1}" --flat=false
778
+
779
+ ## Verdaccio / Nexus / any npm registry
780
+
781
+ Publish each tarball individually:
782
+
783
+ find ./bundle -name '*.tgz' -exec npm publish --registry <url> {} \\;
784
+
785
+ ## Verifying before import
786
+
787
+ node -e "const m=require('./bundle/${MANIFEST_FILE}');console.log(m.packages.length+' packages')"
788
+
789
+ Every entry in ${MANIFEST_FILE} carries the integrity string the source registry
790
+ advertised, and each tarball was checked against it at download time.
791
+ `;
847
792
 
848
793
  //#endregion
849
- //#region src/Manifest.ts
850
- var Manifest_exports = /* @__PURE__ */ __export({
851
- MANIFEST_VERSION: () => MANIFEST_VERSION,
852
- buildManifest: () => buildManifest,
853
- formatReasons: () => formatReasons,
854
- serializeManifest: () => serializeManifest
855
- });
794
+ //#region src/manifest.ts
856
795
  /** Schema version, bumped when the shape changes incompatibly. */
857
796
  const MANIFEST_VERSION = 1;
858
797
  /** Renders a package's reasons into short, greppable strings. */
859
798
  const formatReasons = (pkg) => pkg.reasons.map((reason) => reason._tag === "Root" ? `root:${reason.spec}` : `${reason.kind}:${reason.from}`);
860
799
  /** Builds the manifest for a completed (or dry) run. */
861
800
  const buildManifest = (input) => {
862
- const includedNames = new Set(input.included.map((p) => `${p.name}@${p.version}`));
801
+ const includedNames = new Set(input.included.map((pkg) => `${pkg.name}@${pkg.version}`));
863
802
  const packages = input.included.map((pkg) => {
864
803
  const key = `${pkg.name}@${pkg.version}`;
865
804
  return {
@@ -909,132 +848,10 @@ const buildManifest = (input) => {
909
848
  const serializeManifest = (manifest) => `${JSON.stringify(manifest, null, 2)}\n`;
910
849
 
911
850
  //#endregion
912
- //#region src/DependencyRange.ts
913
- var DependencyRange_exports = /* @__PURE__ */ __export({ parseDependencyTarget: () => parseDependencyTarget });
914
- const UNSUPPORTED_PROTOCOLS = [
915
- ["file:", "local file path"],
916
- ["link:", "local link"],
917
- ["workspace:", "workspace protocol"],
918
- ["portal:", "portal protocol"],
919
- ["patch:", "patch protocol"],
920
- ["git:", "git dependency"],
921
- ["git+", "git dependency"],
922
- ["github:", "GitHub shorthand"],
923
- ["gitlab:", "GitLab shorthand"],
924
- ["bitbucket:", "Bitbucket shorthand"],
925
- ["http:", "remote tarball URL"],
926
- ["https:", "remote tarball URL"]
927
- ];
928
- /**
929
- * Classifies one `name -> range` entry.
930
- *
931
- * An empty range, `*`, and `latest` all mean "any published version"; npm
932
- * treats them interchangeably and so do we.
933
- */
934
- const parseDependencyTarget = (name, raw) => {
935
- const text = raw.trim();
936
- if (text.length === 0 || text === "*" || text === "x" || text === "latest") return {
937
- _tag: "Registry",
938
- name,
939
- selector: {
940
- _tag: "Range",
941
- range: "*"
942
- }
943
- };
944
- if (text.startsWith("npm:")) return parseAlias(name, text);
945
- for (const [prefix, reason] of UNSUPPORTED_PROTOCOLS) if (text.startsWith(prefix)) return {
946
- _tag: "Unsupported",
947
- name,
948
- raw: text,
949
- reason
950
- };
951
- if (semver.validRange(text, { loose: true }) === null && /^[\w.-]+\/[\w.-]+/.test(text)) return {
952
- _tag: "Unsupported",
953
- name,
954
- raw: text,
955
- reason: "git shorthand"
956
- };
957
- const exact = semver.valid(text, { loose: true });
958
- if (exact !== null) return {
959
- _tag: "Registry",
960
- name,
961
- selector: {
962
- _tag: "Exact",
963
- version: exact
964
- }
965
- };
966
- if (semver.validRange(text, { loose: true }) !== null) return {
967
- _tag: "Registry",
968
- name,
969
- selector: {
970
- _tag: "Range",
971
- range: text
972
- }
973
- };
974
- if (/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(text)) return {
975
- _tag: "Registry",
976
- name,
977
- selector: {
978
- _tag: "Tag",
979
- tag: text
980
- }
981
- };
982
- return {
983
- _tag: "Unsupported",
984
- name,
985
- raw: text,
986
- reason: "unrecognised version specifier"
987
- };
988
- };
989
- /**
990
- * Parses `npm:<name>[@<range>]`.
991
- *
992
- * The nested name may itself be scoped, so the `@` split has the same
993
- * "not at index 0" caveat as top-level spec parsing.
994
- */
995
- const parseAlias = (key, text) => {
996
- const body = text.slice(4);
997
- if (body.length === 0) return {
998
- _tag: "Unsupported",
999
- name: key,
1000
- raw: text,
1001
- reason: "empty npm: alias"
1002
- };
1003
- const at = body.lastIndexOf("@");
1004
- const target = at > 0 ? body.slice(0, at) : body;
1005
- const rangeText = at > 0 ? body.slice(at + 1) : "";
1006
- if (target.length === 0) return {
1007
- _tag: "Unsupported",
1008
- name: key,
1009
- raw: text,
1010
- reason: "empty npm: alias target"
1011
- };
1012
- const inner = parseDependencyTarget(target, rangeText);
1013
- if (inner._tag === "Unsupported") return {
1014
- ...inner,
1015
- name: key,
1016
- raw: text
1017
- };
1018
- return {
1019
- _tag: "Registry",
1020
- name: target,
1021
- selector: inner.selector,
1022
- aliasOf: key
1023
- };
1024
- };
1025
-
1026
- //#endregion
1027
- //#region src/Resolve.ts
1028
- var Resolve_exports = /* @__PURE__ */ __export({
1029
- edgesOf: () => edgesOf,
1030
- indexPackages: () => indexPackages,
1031
- packageKey: () => packageKey,
1032
- resolve: () => resolve,
1033
- selectVersions: () => selectVersions
1034
- });
851
+ //#region src/resolve.ts
1035
852
  const packageKey = (name, version) => `${name}@${version}`;
1036
853
  /** Index of a resolution by `name@version`, for lookups during bundling. */
1037
- const indexPackages = (resolution) => new Map(resolution.packages.map((p) => [packageKey(p.name, p.version), p]));
854
+ const indexPackages = (resolution) => new Map(resolution.packages.map((pkg) => [packageKey(pkg.name, pkg.version), pkg]));
1038
855
  /**
1039
856
  * Turns a selector into the concrete version(s) it names.
1040
857
  *
@@ -1054,17 +871,18 @@ const selectVersions = (packument, selector, options) => {
1054
871
  return [version];
1055
872
  }
1056
873
  case "Range": {
1057
- const satisfying = available.filter((v) => semver.satisfies(v, selector.range, {
874
+ const satisfying = available.filter((version) => semver.satisfies(version, selector.range, {
1058
875
  loose: true,
1059
876
  includePrerelease: options.includePrerelease
1060
- })).sort(semver.rcompare);
1061
- if (satisfying.length === 0) throw new NoMatchingVersionsError(packument.name, selector.range, sortVersions(available));
1062
- if (!options.all) return [satisfying[0]];
877
+ })).toSorted(semver.rcompare);
878
+ const [best] = satisfying;
879
+ if (best === void 0) throw new NoMatchingVersionsError(packument.name, selector.range, sortVersions(available));
880
+ if (!options.all) return [best];
1063
881
  return options.maxVersions !== void 0 && options.maxVersions > 0 ? satisfying.slice(0, options.maxVersions) : satisfying;
1064
882
  }
1065
883
  }
1066
884
  };
1067
- const sortVersions = (versions) => [...versions].sort((a, b) => semver.valid(a) && semver.valid(b) ? semver.compare(a, b) : a.localeCompare(b));
885
+ const sortVersions = (versions) => versions.toSorted((a, b) => semver.valid(a) && semver.valid(b) ? semver.compare(a, b) : a.localeCompare(b));
1068
886
  const makeWalkState = () => ({
1069
887
  resolved: /* @__PURE__ */ new Map(),
1070
888
  edges: /* @__PURE__ */ new Map(),
@@ -1092,8 +910,19 @@ const getPackument = (state, name) => Effect.gen(function* () {
1092
910
  state.packuments.set(name, packument);
1093
911
  return packument;
1094
912
  });
913
+ /**
914
+ * Re-fails a thrown selection error into the Effect error channel.
915
+ *
916
+ * `selectVersions` throws `BundlerError` subclasses and nothing else, so this
917
+ * narrows rather than casting; anything unexpected is rethrown as a defect,
918
+ * which is what an unexpected exception actually is.
919
+ */
920
+ const asBundlerError = (error) => {
921
+ if (error instanceof VersionNotFoundError || error instanceof NoMatchingVersionsError) return error;
922
+ throw error;
923
+ };
1095
924
  /** Expands one resolved manifest into the edges that follow from it. */
1096
- const edgesOf = (manifest, options) => {
925
+ const edgesOf$3 = (manifest, options) => {
1097
926
  const from = packageKey(manifest.name, manifest.version);
1098
927
  const tasks = [];
1099
928
  const warnings = [];
@@ -1121,11 +950,11 @@ const edgesOf = (manifest, options) => {
1121
950
  });
1122
951
  }
1123
952
  };
1124
- add(manifest.dependencies, "prod", false);
1125
- if (options.scope.optional) add(manifest.optionalDependencies, "optional", true);
953
+ add(manifest.dependencies, EdgeKind.Prod, false);
954
+ if (options.scope.optional) add(manifest.optionalDependencies, EdgeKind.Optional, true);
1126
955
  if (options.scope.peer) {
1127
956
  const meta = manifest.peerDependenciesMeta ?? {};
1128
- add(manifest.peerDependencies, "peer", false, (name) => meta[name]?.optional === true);
957
+ add(manifest.peerDependencies, EdgeKind.Peer, false, (name) => meta[name]?.optional === true);
1129
958
  }
1130
959
  return {
1131
960
  tasks,
@@ -1137,7 +966,7 @@ const edgesFor = (state, manifest, options) => {
1137
966
  const key = packageKey(manifest.name, manifest.version);
1138
967
  const cached = state.edges.get(key);
1139
968
  if (cached !== void 0) return cached;
1140
- const { tasks, warnings } = edgesOf(manifest, options);
969
+ const { tasks, warnings } = edgesOf$3(manifest, options);
1141
970
  for (const warning of warnings) addWarning(state, warning);
1142
971
  state.edges.set(key, tasks);
1143
972
  return tasks;
@@ -1168,7 +997,8 @@ const walk = (state, seeds, options) => Effect.gen(function* () {
1168
997
  if (result === null) continue;
1169
998
  for (const { manifest, reason } of result) {
1170
999
  const key = packageKey(manifest.name, manifest.version);
1171
- if (!state.resolved.has(key)) {
1000
+ const existing = state.resolved.get(key);
1001
+ if (existing === void 0) {
1172
1002
  state.resolved.set(key, {
1173
1003
  name: manifest.name,
1174
1004
  version: manifest.version,
@@ -1186,13 +1016,10 @@ const walk = (state, seeds, options) => Effect.gen(function* () {
1186
1016
  from: key,
1187
1017
  message: `${key} is deprecated: ${manifest.deprecated}`
1188
1018
  });
1189
- } else {
1190
- const existing = state.resolved.get(key);
1191
- if (!hasReason(existing.reasons, reason)) state.resolved.set(key, {
1192
- ...existing,
1193
- reasons: [...existing.reasons, reason]
1194
- });
1195
- }
1019
+ } else if (!hasReason$1(existing.reasons, reason)) state.resolved.set(key, {
1020
+ ...existing,
1021
+ reasons: [...existing.reasons, reason]
1022
+ });
1196
1023
  if (!discovered.has(key)) {
1197
1024
  discovered.add(key);
1198
1025
  next.push(...edgesFor(state, manifest, options));
@@ -1203,7 +1030,7 @@ const walk = (state, seeds, options) => Effect.gen(function* () {
1203
1030
  }
1204
1031
  return [...discovered];
1205
1032
  });
1206
- const hasReason = (reasons, candidate) => reasons.some((reason) => {
1033
+ const hasReason$1 = (reasons, candidate) => reasons.some((reason) => {
1207
1034
  if (reason._tag === "Root" && candidate._tag === "Root") return reason.spec === candidate.spec;
1208
1035
  if (reason._tag === "Edge" && candidate._tag === "Edge") return reason.from === candidate.from && reason.kind === candidate.kind;
1209
1036
  return false;
@@ -1217,24 +1044,21 @@ const hasReason = (reasons, candidate) => reasons.some((reason) => {
1217
1044
  */
1218
1045
  const resolveTask = (state, task, options) => Effect.gen(function* () {
1219
1046
  const packument = yield* getPackument(state, task.name);
1220
- const versions = yield* Effect.try({
1047
+ return (yield* Effect.try({
1221
1048
  try: () => selectVersions(packument, task.selector, {
1222
1049
  all: false,
1223
1050
  includePrerelease: options.includePrerelease
1224
1051
  }),
1225
- catch: (error) => error
1226
- });
1227
- const out = [];
1228
- for (const version of versions) {
1052
+ catch: asBundlerError
1053
+ })).flatMap((version) => {
1229
1054
  const manifest = packument.versions[version];
1230
- if (manifest === void 0) continue;
1231
- if (task.reason._tag === "Edge" && task.reason.kind === "optional" && !isIncluded(manifest, options.scope.platforms)) continue;
1232
- out.push({
1055
+ if (manifest === void 0) return [];
1056
+ if (task.reason._tag === "Edge" && task.reason.kind === EdgeKind.Optional && !isIncluded(manifest, options.scope.platforms)) return [];
1057
+ return {
1233
1058
  manifest,
1234
1059
  reason: task.reason
1235
- });
1236
- }
1237
- return out;
1060
+ };
1061
+ });
1238
1062
  }).pipe(Effect.catch((error) => {
1239
1063
  if (!task.tolerateFailure) return Effect.fail(error);
1240
1064
  return Effect.sync(() => {
@@ -1252,76 +1076,1395 @@ const resolveTask = (state, task, options) => Effect.gen(function* () {
1252
1076
  const resolve = (specs, options) => Effect.gen(function* () {
1253
1077
  yield* emit({
1254
1078
  _tag: "PhaseStarted",
1255
- phase: "resolve",
1256
- total: specs.length
1079
+ phase: Phase.Resolve,
1080
+ total: specs.length
1081
+ });
1082
+ const state = makeWalkState();
1083
+ const roots = [];
1084
+ for (const spec of specs) {
1085
+ const packument = yield* getPackument(state, spec.name);
1086
+ const versions = yield* Effect.try({
1087
+ try: () => selectVersions(packument, spec.selector, {
1088
+ all: options.allVersions,
1089
+ maxVersions: options.maxVersions,
1090
+ includePrerelease: options.includePrerelease
1091
+ }),
1092
+ catch: asBundlerError
1093
+ });
1094
+ const closures = /* @__PURE__ */ new Map();
1095
+ const union = /* @__PURE__ */ new Set();
1096
+ for (const version of versions) {
1097
+ const closure = yield* walk(state, [{
1098
+ name: spec.name,
1099
+ selector: {
1100
+ _tag: "Exact",
1101
+ version
1102
+ },
1103
+ reason: {
1104
+ _tag: "Root",
1105
+ spec: formatSpec(spec)
1106
+ },
1107
+ tolerateFailure: false
1108
+ }], options);
1109
+ closures.set(version, closure);
1110
+ for (const key of closure) union.add(key);
1111
+ }
1112
+ roots.push({
1113
+ spec,
1114
+ versions,
1115
+ closures,
1116
+ closure: [...union]
1117
+ });
1118
+ }
1119
+ yield* emit({
1120
+ _tag: "PhaseCompleted",
1121
+ phase: Phase.Resolve
1122
+ });
1123
+ return {
1124
+ roots,
1125
+ packages: [...state.resolved.values()].toSorted(compareResolved$1),
1126
+ warnings: state.warnings
1127
+ };
1128
+ });
1129
+ const compareResolved$1 = (a, b) => {
1130
+ if (a.name !== b.name) return a.name < b.name ? -1 : 1;
1131
+ return semver.valid(a.version) && semver.valid(b.version) ? semver.compare(a.version, b.version) : a.version.localeCompare(b.version);
1132
+ };
1133
+
1134
+ //#endregion
1135
+ //#region src/locked-resolve.ts
1136
+ /** Turns a parsed lockfile into a resolution, pinning every version. */
1137
+ const resolveLocked = (lock, options) => Effect.gen(function* () {
1138
+ const roots = selectRoots(lock, options);
1139
+ yield* emit({
1140
+ _tag: "PhaseStarted",
1141
+ phase: Phase.Resolve,
1142
+ total: roots.length
1143
+ });
1144
+ const gap = findGaps(lock, roots, options.declared);
1145
+ if (gap !== null) return yield* Effect.fail(gap);
1146
+ const manifests = yield* fetchManifests(lock, reachableKeys(lock, roots, options), options);
1147
+ const warnings = lock.warnings.map((message) => ({ message }));
1148
+ const resolved = /* @__PURE__ */ new Map();
1149
+ const seenWarnings = /* @__PURE__ */ new Set();
1150
+ const addWarning$1 = (warning) => {
1151
+ const key = `${warning.from ?? ""}|${warning.message}`;
1152
+ if (seenWarnings.has(key)) return;
1153
+ seenWarnings.add(key);
1154
+ warnings.push(warning);
1155
+ };
1156
+ const rootResolutions = [];
1157
+ for (const root of roots) {
1158
+ const key = packageKey(root.name, root.version);
1159
+ if (!manifests.has(key)) continue;
1160
+ const spec = {
1161
+ name: root.name,
1162
+ selector: {
1163
+ _tag: "Exact",
1164
+ version: root.version
1165
+ },
1166
+ raw: `${root.name}@${root.version}`
1167
+ };
1168
+ if (root.kind === LockedRootKind.Optional && !platformAllows(manifests, key, options)) continue;
1169
+ const closure = walkLocked({
1170
+ lock,
1171
+ manifests,
1172
+ options,
1173
+ seed: key,
1174
+ rootSpec: formatSpec(spec),
1175
+ resolved,
1176
+ addWarning: addWarning$1
1177
+ });
1178
+ rootResolutions.push({
1179
+ spec,
1180
+ versions: [root.version],
1181
+ closures: new Map([[root.version, closure]]),
1182
+ closure
1183
+ });
1184
+ }
1185
+ for (const pkg of resolved.values()) {
1186
+ const deprecated = pkg.manifest.deprecated;
1187
+ if (deprecated !== void 0 && deprecated !== "") addWarning$1({
1188
+ from: packageKey(pkg.name, pkg.version),
1189
+ message: `${packageKey(pkg.name, pkg.version)} is deprecated: ${deprecated}`
1190
+ });
1191
+ }
1192
+ yield* emit({
1193
+ _tag: "PhaseCompleted",
1194
+ phase: Phase.Resolve
1195
+ });
1196
+ return {
1197
+ roots: rootResolutions,
1198
+ packages: [...resolved.values()].toSorted(compareResolved),
1199
+ warnings
1200
+ };
1201
+ });
1202
+ /** Which of the project's direct dependencies this run should bundle. */
1203
+ const selectRoots = (lock, options) => lock.roots.filter((root) => {
1204
+ if (root.kind === LockedRootKind.Dev) return options.includeDev;
1205
+ if (root.kind === LockedRootKind.Optional) return options.scope.optional;
1206
+ if (root.kind === LockedRootKind.Peer) return options.scope.peer;
1207
+ return true;
1208
+ }).toSorted((a, b) => a.name === b.name ? 0 : a.name < b.name ? -1 : 1);
1209
+ /**
1210
+ * The two ways a lockfile can be partial, checked before anything is fetched.
1211
+ *
1212
+ * Both mean the same thing to whoever is waiting on the bundle — a package they
1213
+ * expected will not be in it — and both are invisible unless something says so,
1214
+ * because the run otherwise succeeds and simply produces less.
1215
+ */
1216
+ const findGaps = (lock, roots, declared) => {
1217
+ const remedy = `Run \`${lock.format} install\` to bring the lockfile up to date.`;
1218
+ if (declared !== void 0 && declared.length > 0) {
1219
+ const locked = /* @__PURE__ */ new Set();
1220
+ for (const pkg of lock.packages.values()) locked.add(pkg.name);
1221
+ for (const root of roots) locked.add(root.name);
1222
+ const missing = declared.filter((name) => !locked.has(name));
1223
+ if (missing.length > 0) return new LockfileIncompleteError(lock.path, missing, `the package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not pin`, remedy);
1224
+ }
1225
+ if (lock.incomplete.length > 0) return new LockfileIncompleteError(lock.path, lock.incomplete, `${lock.incomplete.length} required dependenc${lock.incomplete.length === 1 ? "y is" : "ies are"} not pinned`, remedy);
1226
+ return null;
1227
+ };
1228
+ /**
1229
+ * Every package the roots can reach through the locked edges.
1230
+ *
1231
+ * Scope filters apply here — `--no-optional` genuinely removes a subtree — but
1232
+ * the platform filter does not, because deciding it needs the `os`/`cpu` fields
1233
+ * that only the registry has.
1234
+ */
1235
+ const reachableKeys = (lock, roots, options) => {
1236
+ const seen = /* @__PURE__ */ new Set();
1237
+ const queue = roots.map((root) => packageKey(root.name, root.version));
1238
+ while (queue.length > 0) {
1239
+ const key = queue.pop();
1240
+ if (key === void 0 || seen.has(key)) continue;
1241
+ seen.add(key);
1242
+ for (const edge of lock.packages.get(key)?.dependencies ?? []) {
1243
+ if (!followsEdge(edge.kind, options)) continue;
1244
+ queue.push(packageKey(edge.name, edge.version));
1245
+ }
1246
+ }
1247
+ return seen;
1248
+ };
1249
+ const followsEdge = (kind, options) => {
1250
+ if (kind === LockedRootKind.Optional) return options.scope.optional;
1251
+ if (kind === LockedRootKind.Peer) return options.scope.peer;
1252
+ return true;
1253
+ };
1254
+ /**
1255
+ * Fetches the published manifest for every pinned version.
1256
+ *
1257
+ * One packument per distinct *name*, not per version — a lockfile holding four
1258
+ * versions of `tslib` costs one request. Every pinned version that the registry
1259
+ * does not serve is collected and reported together, because "refresh your
1260
+ * lockfile" is a single action and finding out about the gaps one run at a time
1261
+ * would be miserable.
1262
+ */
1263
+ const fetchManifests = (lock, reachable, options) => Effect.gen(function* () {
1264
+ const registry = yield* Registry;
1265
+ const wanted = /* @__PURE__ */ new Map();
1266
+ for (const key of reachable) {
1267
+ const pkg = lock.packages.get(key);
1268
+ if (pkg === void 0) continue;
1269
+ const versions = wanted.get(pkg.name);
1270
+ if (versions === void 0) wanted.set(pkg.name, [pkg.version]);
1271
+ else versions.push(pkg.version);
1272
+ }
1273
+ const manifests = /* @__PURE__ */ new Map();
1274
+ const missing = [];
1275
+ const fetched = yield* Effect.forEach([...wanted.entries()], ([name, versions]) => registry.packument(name).pipe(Effect.map((packument) => ({
1276
+ name,
1277
+ versions,
1278
+ packument
1279
+ })), Effect.catch((error) => error._tag === "PackageNotFoundError" ? Effect.succeed({
1280
+ name,
1281
+ versions,
1282
+ packument: null
1283
+ }) : Effect.fail(error))), { concurrency: options.concurrency });
1284
+ for (const { name, versions, packument } of fetched) for (const version of versions) {
1285
+ if (packument === null) {
1286
+ missing.push({
1287
+ name,
1288
+ version,
1289
+ detail: "package not found"
1290
+ });
1291
+ continue;
1292
+ }
1293
+ const manifest = packument.versions[version];
1294
+ if (manifest === void 0) {
1295
+ missing.push({
1296
+ name,
1297
+ version,
1298
+ detail: describeAvailable(Object.keys(packument.versions))
1299
+ });
1300
+ continue;
1301
+ }
1302
+ manifests.set(packageKey(name, version), manifest);
1303
+ yield* emit({
1304
+ _tag: "PackageResolved",
1305
+ name,
1306
+ version,
1307
+ resolvedCount: manifests.size,
1308
+ pendingCount: reachable.size - manifests.size
1309
+ });
1310
+ }
1311
+ if (missing.length > 0) return yield* Effect.fail(new LockfileOutOfDateError(lock.path, registry.registryFor(missing[0]?.name ?? ""), missing));
1312
+ return manifests;
1313
+ });
1314
+ const describeAvailable = (available) => {
1315
+ const recent = available.toSorted((a, b) => semver.valid(a) && semver.valid(b) ? semver.compare(a, b) : a.localeCompare(b)).slice(-3);
1316
+ return recent.length === 0 ? "no published versions" : `no longer published (latest: ${recent.join(", ")})`;
1317
+ };
1318
+ /**
1319
+ * Collects one root's closure by following locked edges.
1320
+ *
1321
+ * Pure and synchronous — every manifest is already in hand — so a per-root
1322
+ * closure costs nothing beyond the traversal, which is what makes `per-spec`
1323
+ * layout as cheap here as it is for a range-resolved run.
1324
+ */
1325
+ const walkLocked = (input) => {
1326
+ const closure = /* @__PURE__ */ new Set();
1327
+ const queue = [{
1328
+ key: input.seed,
1329
+ reason: {
1330
+ _tag: "Root",
1331
+ spec: input.rootSpec
1332
+ }
1333
+ }];
1334
+ while (queue.length > 0) {
1335
+ const next = queue.pop();
1336
+ if (next === void 0) continue;
1337
+ const manifest = input.manifests.get(next.key);
1338
+ const locked = input.lock.packages.get(next.key);
1339
+ if (manifest === void 0 || locked === void 0) continue;
1340
+ record(input.resolved, next.key, manifest, locked, next.reason);
1341
+ if (closure.has(next.key)) continue;
1342
+ closure.add(next.key);
1343
+ for (const edge of locked.dependencies) {
1344
+ if (!followsEdge(edge.kind, input.options)) continue;
1345
+ const target = packageKey(edge.name, edge.version);
1346
+ if (!input.manifests.has(target)) continue;
1347
+ if (edge.kind === LockedRootKind.Optional && !platformAllows(input.manifests, target, input.options)) {
1348
+ input.addWarning({
1349
+ from: next.key,
1350
+ message: `Skipped optional ${target} — it does not build for the selected platforms.`
1351
+ });
1352
+ continue;
1353
+ }
1354
+ queue.push({
1355
+ key: target,
1356
+ reason: {
1357
+ _tag: "Edge",
1358
+ from: next.key,
1359
+ kind: edge.kind
1360
+ }
1361
+ });
1362
+ }
1363
+ }
1364
+ return [...closure];
1365
+ };
1366
+ /** Adds a package, or merges one more justification into an existing entry. */
1367
+ const record = (resolved, key, manifest, locked, reason) => {
1368
+ const existing = resolved.get(key);
1369
+ if (existing === void 0) {
1370
+ resolved.set(key, {
1371
+ name: locked.name,
1372
+ version: locked.version,
1373
+ manifest,
1374
+ reasons: [reason]
1375
+ });
1376
+ return;
1377
+ }
1378
+ if (hasReason(existing.reasons, reason)) return;
1379
+ resolved.set(key, {
1380
+ ...existing,
1381
+ reasons: [...existing.reasons, reason]
1382
+ });
1383
+ };
1384
+ const hasReason = (reasons, candidate) => reasons.some((reason) => {
1385
+ if (reason._tag === "Root" && candidate._tag === "Root") return reason.spec === candidate.spec;
1386
+ if (reason._tag === "Edge" && candidate._tag === "Edge") return reason.from === candidate.from && reason.kind === candidate.kind;
1387
+ return false;
1388
+ });
1389
+ const platformAllows = (manifests, key, options) => {
1390
+ const manifest = manifests.get(key);
1391
+ if (manifest === void 0) return false;
1392
+ return isIncluded(manifest, options.scope.platforms);
1393
+ };
1394
+ const compareResolved = (a, b) => {
1395
+ if (a.name !== b.name) return a.name < b.name ? -1 : 1;
1396
+ return semver.valid(a.version) && semver.valid(b.version) ? semver.compare(a.version, b.version) : a.version.localeCompare(b.version);
1397
+ };
1398
+
1399
+ //#endregion
1400
+ //#region src/lockfile/json.ts
1401
+ const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
1402
+ const decodeStringRecord = Schema.decodeUnknownOption(StringRecordSchema);
1403
+ /** Keeps only the string-valued members of an untyped object. */
1404
+ const stringRecord = (value) => Option.getOrElse(decodeStringRecord(value), () => ({}));
1405
+ const decodeOptionalFlags = Schema.decodeUnknownOption(OptionalFlagRecordSchema);
1406
+ /** Names a package marked its peers optional with. */
1407
+ const optionalPeerNames = (value) => {
1408
+ const flags = Option.getOrElse(decodeOptionalFlags(value), () => ({}));
1409
+ return new Set(Object.entries(flags).flatMap(([name, meta]) => meta.optional ? name : []));
1410
+ };
1411
+ const parseJsonObject = (path, content) => {
1412
+ const parsed = tryParseJsonc(content);
1413
+ if (parsed === void 0) throw new LockfileError(path, "not valid JSON");
1414
+ if (!isRecord(parsed)) throw new LockfileError(path, "top level is not an object");
1415
+ return parsed;
1416
+ };
1417
+ /**
1418
+ * Parses JSON, tolerating the comments and trailing commas bun writes.
1419
+ *
1420
+ * `bun.lock` is JSONC by design — bun puts explanatory comments in it — so
1421
+ * plain `JSON.parse` rejects real lockfiles. Returns `undefined` rather than
1422
+ * throwing so detection can use it as a probe.
1423
+ */
1424
+ const tryParseJsonc = (content) => {
1425
+ try {
1426
+ return JSON.parse(content);
1427
+ } catch {
1428
+ try {
1429
+ return JSON.parse(stripJsonc(content));
1430
+ } catch {
1431
+ return;
1432
+ }
1433
+ }
1434
+ };
1435
+ /**
1436
+ * Removes comments and trailing commas.
1437
+ *
1438
+ * String-aware, because a `//` inside a tarball URL is not a comment and an
1439
+ * escaped quote does not end a string. Both appear in every real lockfile.
1440
+ */
1441
+ const stripJsonc = (content) => {
1442
+ let out = "";
1443
+ let index = 0;
1444
+ let inString = false;
1445
+ while (index < content.length) {
1446
+ const char = content[index];
1447
+ if (inString) {
1448
+ out += char;
1449
+ if (char === "\\") {
1450
+ out += content[index + 1] ?? "";
1451
+ index += 2;
1452
+ continue;
1453
+ }
1454
+ if (char === "\"") inString = false;
1455
+ index += 1;
1456
+ continue;
1457
+ }
1458
+ if (char === "\"") {
1459
+ inString = true;
1460
+ out += char;
1461
+ index += 1;
1462
+ continue;
1463
+ }
1464
+ if (char === "/" && content[index + 1] === "/") {
1465
+ const end = content.indexOf("\n", index);
1466
+ index = end === -1 ? content.length : end;
1467
+ continue;
1468
+ }
1469
+ if (char === "/" && content[index + 1] === "*") {
1470
+ const end = content.indexOf("*/", index + 2);
1471
+ index = end === -1 ? content.length : end + 2;
1472
+ continue;
1473
+ }
1474
+ if (char === ",") {
1475
+ const next = content.slice(index + 1).search(/\S/);
1476
+ const following = next === -1 ? "" : content[index + 1 + next];
1477
+ if (following === "}" || following === "]") {
1478
+ index += 1;
1479
+ continue;
1480
+ }
1481
+ }
1482
+ out += char;
1483
+ index += 1;
1484
+ }
1485
+ return out;
1486
+ };
1487
+ const describeError$1 = (error) => error instanceof Error ? error.message : String(error);
1488
+
1489
+ //#endregion
1490
+ //#region src/lockfile/detect.ts
1491
+ /**
1492
+ * Deliberately cheap and total: it never throws, so `--file` can try this
1493
+ * first and fall through to its other shapes when the answer is `NotALockfile`.
1494
+ */
1495
+ const detectLockfile = (content) => {
1496
+ const trimmed = content.trimStart();
1497
+ if (trimmed.startsWith("bun-lockfile-format-v0")) return {
1498
+ _tag: "Unsupported",
1499
+ label: "a binary bun lockfile (bun.lockb)",
1500
+ hint: "Run `bun install --save-text-lockfile` to produce a bun.lock, and pass that instead."
1501
+ };
1502
+ if (/^__metadata:/m.test(trimmed) || /^# yarn lockfile v\d/m.test(trimmed)) return {
1503
+ _tag: "Unsupported",
1504
+ label: "a yarn lockfile",
1505
+ hint: "yarn is not supported yet. Point --file at the package.json instead, or pass --lockfile off."
1506
+ };
1507
+ if (trimmed.startsWith("{")) return detectJsonLockfile(trimmed);
1508
+ if (/^lockfileVersion:/m.test(trimmed) || /^(importers|snapshots):/m.test(trimmed)) return {
1509
+ _tag: "Supported",
1510
+ format: LockfileFormat.Pnpm
1511
+ };
1512
+ return { _tag: "NotALockfile" };
1513
+ };
1514
+ /**
1515
+ * Separates the two JSON lockfiles from each other and from a package.json.
1516
+ *
1517
+ * Both npm and bun write a numeric `lockfileVersion`, so that field alone
1518
+ * cannot decide it. bun is the one that carries a `workspaces` map keyed by the
1519
+ * empty string; npm keeps the root project under `packages[""]` instead.
1520
+ */
1521
+ const detectJsonLockfile = (trimmed) => {
1522
+ const parsed = tryParseJsonc(trimmed);
1523
+ if (!isRecord(parsed)) return { _tag: "NotALockfile" };
1524
+ const workspaces = parsed["workspaces"];
1525
+ if (isRecord(workspaces) && Object.hasOwn(workspaces, "")) return {
1526
+ _tag: "Supported",
1527
+ format: LockfileFormat.Bun
1528
+ };
1529
+ if (typeof parsed["lockfileVersion"] === "number") return {
1530
+ _tag: "Supported",
1531
+ format: LockfileFormat.Npm
1532
+ };
1533
+ return { _tag: "NotALockfile" };
1534
+ };
1535
+
1536
+ //#endregion
1537
+ //#region src/lockfile/names.ts
1538
+ /**
1539
+ * The file name each supported manager writes, in detection order.
1540
+ *
1541
+ * Ordered: `package-lock.json` before `npm-shrinkwrap.json` before pnpm's and
1542
+ * bun's, so a directory holding several is searched the way npm itself would.
1543
+ */
1544
+ const formatByLockfileName = {
1545
+ "package-lock.json": LockfileFormat.Npm,
1546
+ "npm-shrinkwrap.json": LockfileFormat.Npm,
1547
+ "pnpm-lock.yaml": LockfileFormat.Pnpm,
1548
+ "bun.lock": LockfileFormat.Bun
1549
+ };
1550
+ /** The names a given manager writes. */
1551
+ const lockfileNamesFor = (format) => Object.entries(formatByLockfileName).flatMap(([name, candidate]) => candidate === format ? name : []);
1552
+
1553
+ //#endregion
1554
+ //#region src/lockfile/tree-builder.ts
1555
+ /**
1556
+ * Which manifest block each root kind is declared in.
1557
+ *
1558
+ * The block names are the same across npm, pnpm and bun; only which of them a
1559
+ * given format writes differs, which is why each parser supplies its own order
1560
+ * rather than iterating this.
1561
+ */
1562
+ const fieldByRootKind = {
1563
+ [LockedRootKind.Prod]: "dependencies",
1564
+ [LockedRootKind.Optional]: "optionalDependencies",
1565
+ [LockedRootKind.Dev]: "devDependencies",
1566
+ [LockedRootKind.Peer]: "peerDependencies"
1567
+ };
1568
+ var TreeBuilder = class {
1569
+ packages = /* @__PURE__ */ new Map();
1570
+ roots = [];
1571
+ warnings = [];
1572
+ incomplete = [];
1573
+ /** Names dropped on purpose, so an edge into one is explained, not alarming. */
1574
+ skipped = /* @__PURE__ */ new Set();
1575
+ seenWarnings = /* @__PURE__ */ new Set();
1576
+ add(pkg) {
1577
+ const key = packageKey(pkg.name, pkg.version);
1578
+ const existing = this.packages.get(key);
1579
+ if (existing === void 0) {
1580
+ this.packages.set(key, pkg);
1581
+ return;
1582
+ }
1583
+ this.packages.set(key, {
1584
+ ...existing,
1585
+ dependencies: mergeEdges(existing.dependencies, pkg.dependencies)
1586
+ });
1587
+ }
1588
+ addRoot(root) {
1589
+ if (this.roots.some((existing) => existing.name === root.name)) return;
1590
+ this.roots.push(root);
1591
+ }
1592
+ warn(message) {
1593
+ if (this.seenWarnings.has(message)) return;
1594
+ this.seenWarnings.add(message);
1595
+ this.warnings.push(message);
1596
+ }
1597
+ /** Notes an entry that is real but cannot come from a registry. */
1598
+ skip(name, spec, reason) {
1599
+ this.skipped.add(name);
1600
+ this.warn(`skipped ${name}@${spec} (${reason}) — not fetchable from a registry`);
1601
+ }
1602
+ /**
1603
+ * Records a required edge that resolved to nothing.
1604
+ *
1605
+ * An edge into something skipped on purpose is expected — you cannot bundle a
1606
+ * `workspace:` sibling from a registry — so it stays a warning. Anything else
1607
+ * means the file does not pin what it claims to.
1608
+ */
1609
+ dangling(from, name) {
1610
+ if (this.skipped.has(name)) {
1611
+ this.warn(`${from} requires ${name}, which is not fetchable from a registry`);
1612
+ return;
1613
+ }
1614
+ this.incomplete.push(`${name} (required by ${from})`);
1615
+ }
1616
+ finish(input) {
1617
+ if (this.packages.size === 0) throw new LockfileError(input.path, "lockfile pins no packages that can be bundled");
1618
+ return {
1619
+ format: input.format,
1620
+ lockfileVersion: input.lockfileVersion,
1621
+ path: input.path,
1622
+ importer: input.importer,
1623
+ packages: this.packages,
1624
+ roots: this.roots,
1625
+ warnings: this.warnings,
1626
+ incomplete: this.incomplete
1627
+ };
1628
+ }
1629
+ };
1630
+ const mergeEdges = (a, b) => {
1631
+ const merged = /* @__PURE__ */ new Map();
1632
+ for (const edge of [...a, ...b]) merged.set(`${edge.name}@${edge.version}|${edge.kind}`, edge);
1633
+ return [...merged.values()];
1634
+ };
1635
+ /**
1636
+ * Reads the right-hand side of a locked entry as an exact version.
1637
+ *
1638
+ * Returns the version when it is one, or the reason it is not — `file:../x`,
1639
+ * `workspace:*`, a git URL. `parseDependencyTarget` already classifies every
1640
+ * one of those for package.json parsing, so the vocabulary of skip reasons
1641
+ * stays identical between the two paths.
1642
+ */
1643
+ const asExactVersion = (name, raw) => {
1644
+ const exact = semver.valid(raw, { loose: true });
1645
+ if (exact !== null) return {
1646
+ _tag: "Version",
1647
+ version: exact
1648
+ };
1649
+ const target = parseDependencyTarget(name, raw);
1650
+ if (target._tag === "Unsupported") return {
1651
+ _tag: "Unsupported",
1652
+ reason: target.reason
1653
+ };
1654
+ return {
1655
+ _tag: "Unsupported",
1656
+ reason: "not an exact version"
1657
+ };
1658
+ };
1659
+ /**
1660
+ * Raised when a lockfile found by walking up turns out not to cover the
1661
+ * package.json that went looking for it.
1662
+ *
1663
+ * Worth distinguishing from "no lockfile at all": the file exists and is
1664
+ * readable, it simply belongs to a different project, and bundling it would
1665
+ * produce someone else's dependency set.
1666
+ */
1667
+ const notCovered = (path, importer, known) => {
1668
+ const listed = known.slice(0, 8).map((entry) => ` ${entry}`).join("\n");
1669
+ throw new LockfileError(path, `it does not cover ${importer}.\n` + (known.length === 0 ? " It records no workspace members." : ` Workspace members it does record:\n${listed}` + (known.length > 8 ? `\n … and ${known.length - 8} more` : "")));
1670
+ };
1671
+ /** `""` is how npm and bun spell the project at the lockfile's own level. */
1672
+ const importerKey = (importer) => importer === "." ? "" : importer;
1673
+
1674
+ //#endregion
1675
+ //#region src/lockfile/bun.ts
1676
+ /** Root blocks in bun's precedence order — the first mention of a name wins. */
1677
+ const ROOT_KINDS$2 = [
1678
+ LockedRootKind.Prod,
1679
+ LockedRootKind.Optional,
1680
+ LockedRootKind.Dev,
1681
+ LockedRootKind.Peer
1682
+ ];
1683
+ const parseBunLockfile = (path, content, importer) => {
1684
+ const root = parseJsonObject(path, content);
1685
+ const version = String(root["lockfileVersion"] ?? "?");
1686
+ const builder = new TreeBuilder();
1687
+ const packages = root["packages"];
1688
+ const entries = isRecord(packages) ? collectEntries$1(builder, packages) : /* @__PURE__ */ new Map();
1689
+ const keys = new Set(entries.keys());
1690
+ for (const entry of entries.values()) builder.add({
1691
+ name: entry.name,
1692
+ version: entry.version,
1693
+ dependencies: edgesOf$2(builder, entries, keys, entry)
1694
+ });
1695
+ collectRoots$2(builder, entries, keys, root["workspaces"], path, importer);
1696
+ return builder.finish({
1697
+ format: LockfileFormat.Bun,
1698
+ lockfileVersion: version,
1699
+ path,
1700
+ importer
1701
+ });
1702
+ };
1703
+ /**
1704
+ * Narrows bun's tuple entries.
1705
+ *
1706
+ * The value is a positional array whose shape depends on where the package came
1707
+ * from. A registry package is `[id, registry, info, integrity]`; a workspace
1708
+ * member or a git dependency is shorter and its `id` carries the protocol.
1709
+ */
1710
+ const collectEntries$1 = (builder, packages) => {
1711
+ const entries = /* @__PURE__ */ new Map();
1712
+ for (const [key, raw] of Object.entries(packages)) {
1713
+ if (!Array.isArray(raw)) continue;
1714
+ const [id, , info] = raw;
1715
+ if (typeof id !== "string") continue;
1716
+ const at = id.lastIndexOf("@");
1717
+ if (at <= 0) continue;
1718
+ const name = id.slice(0, at);
1719
+ const spec = id.slice(at + 1);
1720
+ const version = asExactVersion(name, spec);
1721
+ if (version._tag === "Unsupported") {
1722
+ builder.skip(name, spec, version.reason);
1723
+ continue;
1724
+ }
1725
+ const details = isRecord(info) ? info : {};
1726
+ entries.set(key, {
1727
+ key,
1728
+ name,
1729
+ version: version.version,
1730
+ dependencies: stringRecord(details["dependencies"]),
1731
+ optionalDependencies: stringRecord(details["optionalDependencies"]),
1732
+ peerDependencies: stringRecord(details["peerDependencies"]),
1733
+ optionalPeers: collectOptionalPeers(details)
1734
+ });
1735
+ }
1736
+ return entries;
1737
+ };
1738
+ /** bun lists optional peers as a name array rather than npm's metadata object. */
1739
+ const collectOptionalPeers = (details) => {
1740
+ const listed = details["optionalPeers"];
1741
+ const names = Array.isArray(listed) ? listed.flatMap((entry) => typeof entry === "string" ? entry : []) : [];
1742
+ return new Set([...names, ...optionalPeerNames(details["peerDependenciesMeta"])]);
1743
+ };
1744
+ const edgesOf$2 = (builder, entries, keys, entry) => {
1745
+ const edges = [];
1746
+ const add = (names, kind) => {
1747
+ for (const name of names) {
1748
+ const target = findEntry(entries, keys, entry.key, name);
1749
+ if (target === void 0) {
1750
+ if (kind === EdgeKind.Prod) builder.dangling(`${entry.name}@${entry.version}`, name);
1751
+ continue;
1752
+ }
1753
+ edges.push({
1754
+ name: target.name,
1755
+ version: target.version,
1756
+ kind
1757
+ });
1758
+ }
1759
+ };
1760
+ add(Object.keys(entry.dependencies), EdgeKind.Prod);
1761
+ add(Object.keys(entry.optionalDependencies), EdgeKind.Optional);
1762
+ add(Object.keys(entry.peerDependencies).filter((name) => !entry.optionalPeers.has(name)), EdgeKind.Peer);
1763
+ return edges;
1764
+ };
1765
+ const collectRoots$2 = (builder, entries, keys, workspaces, path, member) => {
1766
+ if (!isRecord(workspaces)) return;
1767
+ const selected = member === void 0 ? Object.values(workspaces) : Object.hasOwn(workspaces, importerKey(member)) ? [workspaces[importerKey(member)]] : notCovered(path, member, workspacePaths$1(workspaces));
1768
+ for (const workspace of selected) {
1769
+ if (!isRecord(workspace)) continue;
1770
+ for (const kind of ROOT_KINDS$2) {
1771
+ const declared = stringRecord(workspace[fieldByRootKind[kind]]);
1772
+ for (const [name, specifier] of Object.entries(declared)) {
1773
+ const target = findEntry(entries, keys, "", name);
1774
+ if (target === void 0) continue;
1775
+ builder.addRoot({
1776
+ name: target.name,
1777
+ version: target.version,
1778
+ kind,
1779
+ specifier
1780
+ });
1781
+ }
1782
+ }
1783
+ }
1784
+ };
1785
+ const workspacePaths$1 = (workspaces) => Object.keys(workspaces).map((key) => key === "" ? "." : key);
1786
+ /**
1787
+ * bun nests with a plain `/`, and package names contain `/` too, so a parent
1788
+ * key cannot be found by string-splitting alone — `@babel/core` would split
1789
+ * into a scope that is not a key. `parentKey` only accepts a prefix that is
1790
+ * itself an entry, which resolves the ambiguity.
1791
+ */
1792
+ const findEntry = (entries, keys, fromKey, name) => {
1793
+ let scope = fromKey;
1794
+ for (;;) {
1795
+ const found = entries.get(scope === "" ? name : `${scope}/${name}`);
1796
+ if (found !== void 0) return found;
1797
+ if (scope === "") return void 0;
1798
+ scope = parentKey(keys, scope);
1799
+ }
1800
+ };
1801
+ const parentKey = (keys, key) => {
1802
+ for (let at = key.lastIndexOf("/"); at > 0; at = key.lastIndexOf("/", at - 1)) {
1803
+ const prefix = key.slice(0, at);
1804
+ if (keys.has(prefix)) return prefix;
1805
+ }
1806
+ return "";
1807
+ };
1808
+
1809
+ //#endregion
1810
+ //#region src/lockfile/npm.ts
1811
+ /** Root blocks in npm's precedence order — the first mention of a name wins. */
1812
+ const ROOT_KINDS$1 = [
1813
+ LockedRootKind.Prod,
1814
+ LockedRootKind.Optional,
1815
+ LockedRootKind.Dev,
1816
+ LockedRootKind.Peer
1817
+ ];
1818
+ /**
1819
+ * The node resolution walk-up, over a flat map of install paths.
1820
+ *
1821
+ * A package at `node_modules/a/node_modules/b` depending on `c` gets, in order,
1822
+ * `node_modules/a/node_modules/b/node_modules/c`, then
1823
+ * `node_modules/a/node_modules/c`, then `node_modules/c`. This is what makes a
1824
+ * range on an edge unambiguous: the nesting already recorded which copy won.
1825
+ */
1826
+ const NESTING = "/node_modules/";
1827
+ const parseNpmLockfile = (path, content, importer) => {
1828
+ const root = parseJsonObject(path, content);
1829
+ const version = String(root["lockfileVersion"] ?? "?");
1830
+ const builder = new TreeBuilder();
1831
+ const packages = root["packages"];
1832
+ const entries = isRecord(packages) ? collectEntries(builder, packages) : collectLegacyEntries(builder, root);
1833
+ for (const entry of entries.values()) builder.add({
1834
+ name: entry.realName,
1835
+ version: entry.version,
1836
+ dependencies: edgesOf$1(builder, entries, entry)
1837
+ });
1838
+ collectRoots$1(builder, entries, isRecord(packages) ? packages : root, path, importer);
1839
+ return builder.finish({
1840
+ format: LockfileFormat.Npm,
1841
+ lockfileVersion: version,
1842
+ path,
1843
+ importer
1844
+ });
1845
+ };
1846
+ /** Narrows the v2/v3 `packages` map, dropping what cannot be bundled. */
1847
+ const collectEntries = (builder, packages) => {
1848
+ const entries = /* @__PURE__ */ new Map();
1849
+ for (const [entryPath, raw] of Object.entries(packages)) {
1850
+ const installName = installNameFromPath(entryPath);
1851
+ if (installName === null) continue;
1852
+ if (!isRecord(raw)) continue;
1853
+ if (raw["link"] === true) {
1854
+ builder.skip(installName, stringOr(raw["resolved"], "link"), "workspace link");
1855
+ continue;
1856
+ }
1857
+ const rawVersion = raw["version"];
1858
+ if (typeof rawVersion !== "string") {
1859
+ builder.warn(`skipped ${installName} — its lockfile entry records no version, so there is nothing to pin`);
1860
+ continue;
1861
+ }
1862
+ const resolvedName = typeof raw["name"] === "string" ? raw["name"] : installName;
1863
+ const version = asExactVersion(resolvedName, rawVersion);
1864
+ if (version._tag === "Unsupported") {
1865
+ builder.skip(resolvedName, rawVersion, version.reason);
1866
+ continue;
1867
+ }
1868
+ const resolvedUrl = raw["resolved"];
1869
+ if (typeof resolvedUrl === "string" && !isRegistryUrl(resolvedUrl)) {
1870
+ builder.skip(resolvedName, resolvedUrl, "not a registry tarball");
1871
+ continue;
1872
+ }
1873
+ entries.set(entryPath, {
1874
+ path: entryPath,
1875
+ version: version.version,
1876
+ realName: resolvedName,
1877
+ dependencies: stringRecord(raw["dependencies"]),
1878
+ optionalDependencies: stringRecord(raw["optionalDependencies"]),
1879
+ peerDependencies: stringRecord(raw["peerDependencies"]),
1880
+ optionalPeers: optionalPeerNames(raw["peerDependenciesMeta"])
1881
+ });
1882
+ }
1883
+ return entries;
1884
+ };
1885
+ /**
1886
+ * Flattens a v1 lockfile's nested `dependencies` tree into the same
1887
+ * path-keyed shape v2 uses.
1888
+ *
1889
+ * v1 is old — npm 6 — but it is exactly the vintage still pinned inside a lot
1890
+ * of the locked-down environments this tool exists for, so it is worth the
1891
+ * thirty lines rather than an error telling somebody to upgrade npm.
1892
+ */
1893
+ const collectLegacyEntries = (builder, root) => {
1894
+ const entries = /* @__PURE__ */ new Map();
1895
+ const visit = (tree, prefix) => {
1896
+ if (!isRecord(tree)) return;
1897
+ for (const [name, raw] of Object.entries(tree)) {
1898
+ if (!isRecord(raw)) continue;
1899
+ const entryPath = prefix === "" ? `node_modules/${name}` : `${prefix}/node_modules/${name}`;
1900
+ const rawVersion = raw["version"];
1901
+ if (typeof rawVersion === "string") {
1902
+ const version = asExactVersion(name, rawVersion);
1903
+ if (version._tag === "Unsupported") builder.skip(name, rawVersion, version.reason);
1904
+ else entries.set(entryPath, {
1905
+ path: entryPath,
1906
+ version: version.version,
1907
+ realName: name,
1908
+ dependencies: stringRecord(raw["requires"]),
1909
+ optionalDependencies: {},
1910
+ peerDependencies: {},
1911
+ optionalPeers: /* @__PURE__ */ new Set()
1912
+ });
1913
+ }
1914
+ visit(raw["dependencies"], entryPath);
1915
+ }
1916
+ };
1917
+ visit(root["dependencies"], "");
1918
+ return entries;
1919
+ };
1920
+ /** Turns one entry's ranges into edges, resolved through the directory nesting. */
1921
+ const edgesOf$1 = (builder, entries, entry) => {
1922
+ const edges = [];
1923
+ const add = (names, kind) => {
1924
+ for (const name of names) {
1925
+ const targetPath = resolveNestedEdge(entries, entry.path, name);
1926
+ const target = targetPath === null ? void 0 : entries.get(targetPath);
1927
+ if (target === void 0) {
1928
+ if (kind === EdgeKind.Prod) builder.dangling(`${entry.realName}@${entry.version}`, name);
1929
+ continue;
1930
+ }
1931
+ edges.push({
1932
+ name: target.realName,
1933
+ version: target.version,
1934
+ kind
1935
+ });
1936
+ }
1937
+ };
1938
+ const optionalNames = Object.keys(entry.optionalDependencies);
1939
+ const optional = new Set(optionalNames);
1940
+ add(Object.keys(entry.dependencies).filter((name) => !optional.has(name)), EdgeKind.Prod);
1941
+ add(optionalNames, EdgeKind.Optional);
1942
+ add(Object.keys(entry.peerDependencies).filter((name) => !entry.optionalPeers.has(name)), EdgeKind.Peer);
1943
+ return edges;
1944
+ };
1945
+ /**
1946
+ * Reads the root project's direct dependencies.
1947
+ *
1948
+ * The lockfile records these under `packages[""]` for v2/v3. v1 has no such
1949
+ * entry, so every top-level `node_modules/x` is treated as direct — which
1950
+ * over-counts roots slightly on v1 and is the best that file can support.
1951
+ */
1952
+ const collectRoots$1 = (builder, entries, source, path, importer) => {
1953
+ const collectFrom = (from, project) => {
1954
+ for (const kind of ROOT_KINDS$1) {
1955
+ const field = fieldByRootKind[kind];
1956
+ for (const [name, specifier] of Object.entries(stringRecord(project[field]))) {
1957
+ const targetPath = resolveNestedEdge(entries, from, name);
1958
+ const target = targetPath === null ? void 0 : entries.get(targetPath);
1959
+ if (target === void 0) continue;
1960
+ builder.addRoot({
1961
+ name: target.realName,
1962
+ version: target.version,
1963
+ kind,
1964
+ specifier
1965
+ });
1966
+ }
1967
+ }
1968
+ };
1969
+ if (importer !== void 0) {
1970
+ const key = importerKey(importer);
1971
+ const project = source[key];
1972
+ if (!isRecord(project)) notCovered(path, importer, workspacePaths(source));
1973
+ else collectFrom(key, project);
1974
+ return;
1975
+ }
1976
+ const projects = Object.entries(source).flatMap(([key, value]) => isRecord(value) && !key.includes("node_modules/") ? [[key, value]] : []);
1977
+ if (projects.length > 0) {
1978
+ for (const [key, project] of projects) collectFrom(key, project);
1979
+ return;
1980
+ }
1981
+ for (const entry of entries.values()) {
1982
+ if (entry.path.includes(NESTING)) continue;
1983
+ builder.addRoot({
1984
+ name: entry.realName,
1985
+ version: entry.version,
1986
+ kind: LockedRootKind.Prod
1987
+ });
1988
+ }
1989
+ };
1990
+ /** The workspace paths a v2/v3 lockfile records, for a "does not cover" message. */
1991
+ const workspacePaths = (source) => Object.keys(source).flatMap((key) => key === "" ? "." : key.includes("node_modules/") ? [] : key);
1992
+ /**
1993
+ * npm records non-registry sources as a URL in `resolved`. Registry tarballs
1994
+ * are ordinary http(s); anything with a `git+`, `file:` or bare path form is
1995
+ * something we cannot fetch.
1996
+ */
1997
+ const isRegistryUrl = (url) => url.startsWith("http://") || url.startsWith("https://");
1998
+ const resolveNestedEdge = (entries, fromPath, name) => {
1999
+ let scope = fromPath;
2000
+ for (;;) {
2001
+ const candidate = scope === "" ? `node_modules/${name}` : `${scope}${NESTING}${name}`;
2002
+ if (entries.has(candidate)) return candidate;
2003
+ if (scope === "") return null;
2004
+ const at = scope.lastIndexOf(NESTING);
2005
+ scope = at === -1 ? "" : scope.slice(0, at);
2006
+ }
2007
+ };
2008
+ /** `node_modules/@babel/core` -> `@babel/core`; a workspace path -> `null`. */
2009
+ const installNameFromPath = (entryPath) => {
2010
+ const at = entryPath.lastIndexOf("node_modules/");
2011
+ if (at === -1) return null;
2012
+ const name = entryPath.slice(at + 13);
2013
+ return name.length === 0 ? null : name;
2014
+ };
2015
+
2016
+ //#endregion
2017
+ //#region src/lockfile/pnpm.ts
2018
+ /** pnpm never records peer dependencies against an importer. */
2019
+ const ROOT_KINDS = [
2020
+ LockedRootKind.Prod,
2021
+ LockedRootKind.Optional,
2022
+ LockedRootKind.Dev
2023
+ ];
2024
+ const parsePnpmLockfile = (path, content, importer) => {
2025
+ let root;
2026
+ try {
2027
+ root = parse(content);
2028
+ } catch (error) {
2029
+ throw new LockfileError(path, `not valid YAML — ${describeError$1(error)}`, { cause: error });
2030
+ }
2031
+ if (!isRecord(root)) throw new LockfileError(path, "top level is not a mapping");
2032
+ const version = String(root["lockfileVersion"] ?? "?");
2033
+ const builder = new TreeBuilder();
2034
+ const packages = isRecord(root["packages"]) ? root["packages"] : {};
2035
+ const snapshots = isRecord(root["snapshots"]) ? root["snapshots"] : packages;
2036
+ const known = /* @__PURE__ */ new Map();
2037
+ for (const key of Object.keys(packages)) {
2038
+ const parsed = parsePnpmKey(key);
2039
+ if (parsed === null) continue;
2040
+ known.set(key, parsed);
2041
+ }
2042
+ for (const [key, raw] of Object.entries(snapshots)) {
2043
+ const parsed = parsePnpmKey(key) ?? known.get(key);
2044
+ if (parsed === void 0 || parsed === null) {
2045
+ const described = describeKey(key);
2046
+ if (described !== null) builder.skip(described.name, described.spec, described.reason);
2047
+ continue;
2048
+ }
2049
+ builder.add({
2050
+ name: parsed.name,
2051
+ version: parsed.version,
2052
+ dependencies: edgesOf(builder, parsed, isRecord(raw) ? raw : {})
2053
+ });
2054
+ }
2055
+ collectRoots(builder, root, path, importer);
2056
+ return builder.finish({
2057
+ format: LockfileFormat.Pnpm,
2058
+ lockfileVersion: version,
2059
+ path,
2060
+ importer
2061
+ });
2062
+ };
2063
+ const edgesOf = (builder, from, snapshot) => {
2064
+ const edges = [];
2065
+ const add = (field, kind) => {
2066
+ const entries = snapshot[field];
2067
+ if (!isRecord(entries)) return;
2068
+ for (const [name, raw] of Object.entries(entries)) {
2069
+ if (typeof raw !== "string") continue;
2070
+ const target = parsePnpmReference(name, raw);
2071
+ if (target === null) {
2072
+ if (kind === EdgeKind.Prod) builder.dangling(`${from.name}@${from.version}`, name);
2073
+ continue;
2074
+ }
2075
+ edges.push({
2076
+ ...target,
2077
+ kind
2078
+ });
2079
+ }
2080
+ };
2081
+ add(fieldByRootKind[LockedRootKind.Prod], EdgeKind.Prod);
2082
+ add(fieldByRootKind[LockedRootKind.Optional], EdgeKind.Optional);
2083
+ add(fieldByRootKind[LockedRootKind.Peer], EdgeKind.Peer);
2084
+ return edges;
2085
+ };
2086
+ /**
2087
+ * The importers' direct dependencies become the roots.
2088
+ *
2089
+ * pnpm is the format that makes this easy: `importers` is already keyed by the
2090
+ * path each package.json sits at, so scoping to one member is a lookup rather
2091
+ * than a reconstruction.
2092
+ */
2093
+ const collectRoots = (builder, root, path, member) => {
2094
+ const importers = isRecord(root["importers"]) ? root["importers"] : { ".": root };
2095
+ const selected = member === void 0 ? Object.values(importers) : Object.hasOwn(importers, member) ? [importers[member]] : notCovered(path, member, Object.keys(importers));
2096
+ for (const importer of selected) {
2097
+ if (!isRecord(importer)) continue;
2098
+ for (const kind of ROOT_KINDS) {
2099
+ const entries = importer[fieldByRootKind[kind]];
2100
+ if (!isRecord(entries)) continue;
2101
+ for (const [name, raw] of Object.entries(entries)) {
2102
+ const value = typeof raw === "string" ? raw : isRecord(raw) ? raw["version"] : void 0;
2103
+ if (typeof value !== "string") continue;
2104
+ const target = parsePnpmReference(name, value);
2105
+ if (target === null) continue;
2106
+ const specifier = isRecord(raw) && typeof raw["specifier"] === "string" ? raw["specifier"] : void 0;
2107
+ builder.addRoot({
2108
+ ...target,
2109
+ kind,
2110
+ specifier
2111
+ });
2112
+ }
2113
+ }
2114
+ }
2115
+ };
2116
+ /**
2117
+ * Reads a dependency value from a snapshot or importer.
2118
+ *
2119
+ * The value is either a bare version (`1.4.0`) or a full package key
2120
+ * (`/lodash@4.17.21`, used when the install name is an alias of another
2121
+ * package). Both may carry a peer-resolution suffix in parentheses.
2122
+ */
2123
+ const parsePnpmReference = (name, raw) => {
2124
+ const value = stripSuffix(raw);
2125
+ const exact = semver.valid(value, { loose: true });
2126
+ if (exact !== null) return {
2127
+ name,
2128
+ version: exact
2129
+ };
2130
+ return parsePnpmKey(raw);
2131
+ };
2132
+ /**
2133
+ * Splits a package key into name and version.
2134
+ *
2135
+ * Three encodings across the versions still in the wild:
2136
+ * v5 `/@babel/core/7.0.0`
2137
+ * v6 `/@babel/core@7.0.0`
2138
+ * v9 `@babel/core@7.0.0`
2139
+ *
2140
+ * plus an optional `(peer@1.0.0)` or `(patch_hash=…)` suffix on any of them.
2141
+ */
2142
+ const parsePnpmKey = (key) => {
2143
+ const body = stripSuffix(key.startsWith("/") ? key.slice(1) : key);
2144
+ const at = body.lastIndexOf("@");
2145
+ const cut = at > 0 ? at : body.lastIndexOf("/");
2146
+ if (cut <= 0) return null;
2147
+ const name = body.slice(0, cut);
2148
+ const version = semver.valid(body.slice(cut + 1), { loose: true });
2149
+ if (version === null || name.length === 0) return null;
2150
+ return {
2151
+ name,
2152
+ version
2153
+ };
2154
+ };
2155
+ /** Describes a key we cannot pin, so the skip warning can say why. */
2156
+ const describeKey = (key) => {
2157
+ const body = key.startsWith("/") ? key.slice(1) : key;
2158
+ const at = body.lastIndexOf("@");
2159
+ if (at <= 0) return null;
2160
+ const name = body.slice(0, at);
2161
+ const spec = body.slice(at + 1);
2162
+ const target = parseDependencyTarget(name, spec);
2163
+ return {
2164
+ name,
2165
+ spec,
2166
+ reason: target._tag === "Unsupported" ? target.reason : "not an exact version"
2167
+ };
2168
+ };
2169
+ /**
2170
+ * Drops the parenthesised peer-resolution suffix.
2171
+ *
2172
+ * pnpm distinguishes the same version installed against different peers —
2173
+ * `react-dom@18.3.1(react@18.3.1)`. Those are the same published tarball, which
2174
+ * is all this tool downloads, so the suffix is noise here.
2175
+ */
2176
+ const stripSuffix = (value) => {
2177
+ const open = value.indexOf("(");
2178
+ return open === -1 ? value : value.slice(0, open);
2179
+ };
2180
+
2181
+ //#endregion
2182
+ //#region src/lockfile/parse.ts
2183
+ /**
2184
+ * Parses lockfile content into a flat graph.
2185
+ *
2186
+ * Throws `LockfileError`; the caller is expected to be inside an `Effect.try`.
2187
+ */
2188
+ const parseLockfile = (path, content, format, options = {}) => {
2189
+ const resolved = format ?? resolveFormat(path, content);
2190
+ const importer = options.importer;
2191
+ switch (resolved) {
2192
+ case LockfileFormat.Npm: return parseNpmLockfile(path, content, importer);
2193
+ case LockfileFormat.Pnpm: return parsePnpmLockfile(path, content, importer);
2194
+ case LockfileFormat.Bun: return parseBunLockfile(path, content, importer);
2195
+ }
2196
+ };
2197
+ const resolveFormat = (path, content) => {
2198
+ const detected = detectLockfile(content);
2199
+ if (detected._tag === "Supported") return detected.format;
2200
+ if (detected._tag === "Unsupported") throw new LockfileError(path, `${detected.label} — ${detected.hint}`);
2201
+ throw new LockfileError(path, "content does not look like a package-lock.json, pnpm-lock.yaml or bun.lock");
2202
+ };
2203
+
2204
+ //#endregion
2205
+ //#region src/schemas/package-json.ts
2206
+ const PackageJsonSchema = Schema.Struct({
2207
+ dependencies: tolerant(StringRecordSchema),
2208
+ devDependencies: tolerant(StringRecordSchema),
2209
+ optionalDependencies: tolerant(StringRecordSchema),
2210
+ peerDependencies: tolerant(StringRecordSchema),
2211
+ peerDependenciesMeta: tolerant(OptionalFlagRecordSchema)
2212
+ });
2213
+ const decodePackageJson = Schema.decodeUnknownOption(PackageJsonSchema);
2214
+
2215
+ //#endregion
2216
+ //#region src/input-file.ts
2217
+ const defaultInputFileOptions = { includeDev: true };
2218
+ /** Reads and parses an input file. */
2219
+ const readInputFile = (filePath, options = defaultInputFileOptions) => Effect.gen(function* () {
2220
+ const fs = yield* FileSystem.FileSystem;
2221
+ if (!(yield* fs.exists(filePath).pipe(Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause }))))) return yield* Effect.fail(new InvalidInputFileError(filePath, "file does not exist"));
2222
+ const content = yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause })));
2223
+ return yield* Effect.try({
2224
+ try: () => parseInputFile(filePath, content, options),
2225
+ catch: (error) => error instanceof InvalidInputFileError || isLockfileError(error) ? error : new InvalidInputFileError(filePath, describeError(error), { cause: error })
2226
+ });
2227
+ });
2228
+ const isLockfileError = (error) => error instanceof Error && "_tag" in error && error._tag === "LockfileError";
2229
+ /**
2230
+ * Parses file content that has already been read.
2231
+ *
2232
+ * Split out from the IO so the whole of this logic is testable with plain
2233
+ * strings and no file system at all.
2234
+ */
2235
+ const parseInputFile = (filePath, content, options = defaultInputFileOptions) => {
2236
+ const trimmed = content.trim();
2237
+ if (trimmed.length === 0) throw new InvalidInputFileError(filePath, "file is empty");
2238
+ const detected = detectLockfile(trimmed);
2239
+ if (detected._tag === "Unsupported") throw new InvalidInputFileError(filePath, `${detected.label} — ${detected.hint}`);
2240
+ if (detected._tag === "Supported") return fromLockfile(filePath, content, detected.format, options);
2241
+ return looksLikeJson(trimmed) ? fromPackageJson(filePath, trimmed, options) : fromList(filePath, content);
2242
+ };
2243
+ const looksLikeJson = (trimmed) => trimmed.startsWith("{");
2244
+ const fromLockfile = (filePath, content, format, options) => {
2245
+ const lockfile = parseLockfile(filePath, content, format, { importer: options.importer });
2246
+ const roots = lockfile.roots.filter((root) => options.includeDev || root.kind !== "dev");
2247
+ return {
2248
+ kind: InputFileKind.Lockfile,
2249
+ specs: roots.map((root) => ({
2250
+ name: root.name,
2251
+ selector: {
2252
+ _tag: "Exact",
2253
+ version: root.version
2254
+ },
2255
+ raw: `${root.name}@${root.version}`
2256
+ })),
2257
+ warnings: lockfile.warnings,
2258
+ lockfile
2259
+ };
2260
+ };
2261
+ const fromPackageJson = (filePath, content, options) => {
2262
+ const parsed = parsePackageJson(filePath, content);
2263
+ const specs = [];
2264
+ const warnings = [];
2265
+ const required = [];
2266
+ const seen = /* @__PURE__ */ new Set();
2267
+ const collect = (entries, label, skip) => {
2268
+ if (entries === void 0) return;
2269
+ for (const [name, raw] of Object.entries(entries)) {
2270
+ if (skip?.(name)) continue;
2271
+ const target = parseDependencyTarget(name, raw);
2272
+ if (target._tag === "Unsupported") {
2273
+ warnings.push(`${label}: skipped ${name}@${target.raw} (${target.reason}) — not fetchable from a registry`);
2274
+ continue;
2275
+ }
2276
+ if (label === "dependencies" || label === "devDependencies") required.push(target.name);
2277
+ const key = `${target.name}|${JSON.stringify(target.selector)}`;
2278
+ if (seen.has(key)) continue;
2279
+ seen.add(key);
2280
+ specs.push({
2281
+ name: target.name,
2282
+ selector: target.selector,
2283
+ raw: `${target.name}@${raw}`
2284
+ });
2285
+ }
2286
+ };
2287
+ collect(parsed.dependencies, "dependencies");
2288
+ collect(parsed.optionalDependencies, "optionalDependencies");
2289
+ if (options.includeDev) collect(parsed.devDependencies, "devDependencies");
2290
+ const meta = parsed.peerDependenciesMeta ?? {};
2291
+ collect(parsed.peerDependencies, "peerDependencies", (name) => meta[name]?.optional === true);
2292
+ if (specs.length === 0) throw new InvalidInputFileError(filePath, options.includeDev ? "package.json declares no dependencies to bundle" : "package.json declares no non-dev dependencies to bundle (drop --prod to include devDependencies)");
2293
+ return {
2294
+ kind: InputFileKind.PackageJson,
2295
+ specs,
2296
+ warnings,
2297
+ required
2298
+ };
2299
+ };
2300
+ /**
2301
+ * Reads a package.json, or gives up on its contents rather than on the run.
2302
+ *
2303
+ * Invalid JSON is an error worth raising — somebody pointed `--file` at a
2304
+ * broken file. A well-formed JSON document this schema cannot recognise is
2305
+ * not: it decodes to no blocks, and the caller reports "declares no
2306
+ * dependencies to bundle", which is both true and more useful.
2307
+ */
2308
+ const parsePackageJson = (filePath, content) => {
2309
+ let parsed;
2310
+ try {
2311
+ parsed = JSON.parse(content);
2312
+ } catch (error) {
2313
+ throw new InvalidInputFileError(filePath, `not valid JSON — ${describeError(error)}`, { cause: error });
2314
+ }
2315
+ return Option.getOrElse(decodePackageJson(parsed), () => ({}));
2316
+ };
2317
+ const fromList = (filePath, content) => {
2318
+ const rawLines = content.split(/\r?\n/);
2319
+ const lines = rawLines.flatMap((line) => {
2320
+ const withoutComment = stripComment(line).trim();
2321
+ return withoutComment.length === 0 ? [] : withoutComment;
2322
+ });
2323
+ if (lines.length === 0) throw new InvalidInputFileError(filePath, "no package specs found (only blank lines and comments)");
2324
+ const { specs, errors } = parseSpecs(lines);
2325
+ if (errors.length > 0) throw new InvalidInputFileError(filePath, formatSpecErrors(errors, rawLines));
2326
+ return {
2327
+ kind: InputFileKind.List,
2328
+ specs,
2329
+ warnings: []
2330
+ };
2331
+ };
2332
+ /**
2333
+ * Strips `#` and `//` comments.
2334
+ *
2335
+ * `//` only counts when it is not part of a URL, so a future line containing
2336
+ * `https://…` does not get silently truncated.
2337
+ */
2338
+ const stripComment = (line) => {
2339
+ const hash = line.indexOf("#");
2340
+ const result = hash === -1 ? line : line.slice(0, hash);
2341
+ const slashes = result.indexOf("//");
2342
+ if (slashes === 0) return "";
2343
+ if (slashes > 0 && result[slashes - 1] !== ":") return result.slice(0, slashes);
2344
+ return result;
2345
+ };
2346
+ const formatSpecErrors = (errors, rawLines) => {
2347
+ const details = errors.map((error) => {
2348
+ const index = rawLines.findIndex((line) => stripComment(line).trim() === error.spec);
2349
+ const where = index === -1 ? "" : ` (line ${index + 1})`;
2350
+ return ` ${error.spec}${where}: ${error.reason}`;
2351
+ });
2352
+ return `${errors.length} invalid spec(s):\n${details.join("\n")}`;
2353
+ };
2354
+ const describeError = (error) => error instanceof Error ? error.message : String(error);
2355
+
2356
+ //#endregion
2357
+ //#region src/download.ts
2358
+ /**
2359
+ * Downloads every package into `destDir`, laid out the way a registry serves
2360
+ * them.
2361
+ *
2362
+ * `verifyIntegrity` defaulting to on is deliberate: a corrupt tarball that
2363
+ * makes it into a corporate registry is far more expensive than a failed run.
2364
+ */
2365
+ const downloadAll = (packages, destDir, options) => Effect.gen(function* () {
2366
+ const fs = yield* FileSystem.FileSystem;
2367
+ const path = yield* Path.Path;
2368
+ const registry = yield* Registry;
2369
+ yield* emit({
2370
+ _tag: "PhaseStarted",
2371
+ phase: Phase.Download,
2372
+ total: packages.length
1257
2373
  });
1258
- const state = makeWalkState();
1259
- const roots = [];
1260
- for (const spec of specs) {
1261
- const packument = yield* getPackument(state, spec.name);
1262
- const versions = yield* Effect.try({
1263
- try: () => selectVersions(packument, spec.selector, {
1264
- all: options.allVersions,
1265
- maxVersions: options.maxVersions,
1266
- includePrerelease: options.includePrerelease
1267
- }),
1268
- catch: (error) => error
2374
+ let completed = 0;
2375
+ const sizes = /* @__PURE__ */ new Map();
2376
+ const unverified = [];
2377
+ const results = yield* Effect.forEach(packages, (pkg) => Effect.gen(function* () {
2378
+ const key = `${pkg.name}@${pkg.version}`;
2379
+ yield* emit({
2380
+ _tag: "DownloadStarted",
2381
+ name: pkg.name,
2382
+ version: pkg.version
1269
2383
  });
1270
- const closures = /* @__PURE__ */ new Map();
1271
- const union = /* @__PURE__ */ new Set();
1272
- for (const version of versions) {
1273
- const closure = yield* walk(state, [{
1274
- name: spec.name,
1275
- selector: {
1276
- _tag: "Exact",
1277
- version
1278
- },
1279
- reason: {
1280
- _tag: "Root",
1281
- spec: formatSpec(spec)
1282
- },
1283
- tolerateFailure: false
1284
- }], options);
1285
- closures.set(version, closure);
1286
- for (const key of closure) union.add(key);
2384
+ const bytes = yield* registry.download(pkg.manifest);
2385
+ if (options.verifyIntegrity) {
2386
+ const result = yield* verify(bytes, pkg.manifest.dist);
2387
+ if (result._tag === "Mismatch") return yield* Effect.fail(new IntegrityError(pkg.name, pkg.version, result.expected, result.actual));
2388
+ if (result._tag === "Unverifiable") yield* emit({
2389
+ _tag: "Warning",
2390
+ message: `${key} could not be verified (${result.reason})`
2391
+ });
1287
2392
  }
1288
- roots.push({
1289
- spec,
1290
- versions,
1291
- closures,
1292
- closure: [...union]
2393
+ const verifiable = (pkg.manifest.dist.integrity?.length ?? 0) > 0 || (pkg.manifest.dist.shasum?.length ?? 0) > 0;
2394
+ const relative = packagePath(pkg.name, pkg.version);
2395
+ const target = path.join(destDir, ...relative.split("/"));
2396
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.mapError((cause) => new OutputError(path.dirname(target), "could not create directory", { cause })));
2397
+ yield* fs.writeFile(target, bytes).pipe(Effect.mapError((cause) => new OutputError(target, "could not write tarball", { cause })));
2398
+ completed += 1;
2399
+ yield* emit({
2400
+ _tag: "DownloadCompleted",
2401
+ name: pkg.name,
2402
+ version: pkg.version,
2403
+ bytes: bytes.byteLength,
2404
+ completedCount: completed,
2405
+ totalCount: packages.length
1293
2406
  });
2407
+ return {
2408
+ key,
2409
+ bytes: bytes.byteLength,
2410
+ verifiable
2411
+ };
2412
+ }), { concurrency: options.concurrency });
2413
+ for (const result of results) {
2414
+ sizes.set(result.key, result.bytes);
2415
+ if (!result.verifiable) unverified.push(result.key);
1294
2416
  }
1295
2417
  yield* emit({
1296
2418
  _tag: "PhaseCompleted",
1297
- phase: "resolve"
2419
+ phase: Phase.Download
1298
2420
  });
1299
2421
  return {
1300
- roots,
1301
- packages: [...state.resolved.values()].sort(compareResolved),
1302
- warnings: state.warnings
2422
+ sizes,
2423
+ unverified,
2424
+ totalBytes: results.reduce((sum, result) => sum + result.bytes, 0)
1303
2425
  };
1304
2426
  });
1305
- const compareResolved = (a, b) => {
1306
- if (a.name !== b.name) return a.name < b.name ? -1 : 1;
1307
- return semver.valid(a.version) && semver.valid(b.version) ? semver.compare(a.version, b.version) : a.version.localeCompare(b.version);
1308
- };
1309
2427
 
1310
2428
  //#endregion
1311
- //#region src/Bundle.ts
1312
- var Bundle_exports = /* @__PURE__ */ __export({
1313
- bundle: () => bundle,
1314
- bundleResolved: () => bundleResolved,
1315
- plan: () => plan,
1316
- plannedOutputs: () => plannedOutputs,
1317
- summarize: () => summarize,
1318
- withLayout: () => withLayout
1319
- });
2429
+ //#region src/bundle.ts
2430
+ /**
2431
+ * The archives `per-spec` emits — one per distinct root package version.
2432
+ *
2433
+ * Roots are *specs*, and several specs can name one package: `react`,
2434
+ * `react@latest` and `react@19.2.8` are three ways of asking for the same
2435
+ * thing, and after resolution they are indistinguishable. Without this they
2436
+ * were three archives — the same bytes, under the same file name, written over
2437
+ * each other. The run then reported three artifacts and three times the size
2438
+ * for one file on disk, and the duplication ratio it computes to decide whether
2439
+ * to ask about the layout counted them too, so the tool would offer to
2440
+ * deduplicate a bundle it had duplicated itself.
2441
+ *
2442
+ * Keyed by name and version rather than by the archive's file name. A scoped
2443
+ * name is flattened to make a file name, so `@foo/bar` and `foo-bar` can spell
2444
+ * the same one — and those are two different packages, which should collide
2445
+ * loudly rather than being merged here.
2446
+ *
2447
+ * Roots only. The packages *inside* an archive were deduplicated by resolution
2448
+ * long before this, which is why the same closure fetched from twenty specs is
2449
+ * fetched once.
2450
+ */
2451
+ const perSpecTargets = (resolution) => {
2452
+ const seen = /* @__PURE__ */ new Set();
2453
+ return resolution.roots.flatMap((root) => root.versions.flatMap((version) => {
2454
+ const key = `${root.spec.name}@${version}`;
2455
+ if (seen.has(key)) return [];
2456
+ seen.add(key);
2457
+ return {
2458
+ root,
2459
+ version
2460
+ };
2461
+ }));
2462
+ };
1320
2463
  /** Computes the plan summary for a resolution. */
1321
2464
  const summarize = (resolution) => {
1322
2465
  let perSpecArchives = 0;
1323
2466
  let perSpecEntries = 0;
1324
- for (const root of resolution.roots) for (const version of root.versions) {
2467
+ for (const { root, version } of perSpecTargets(resolution)) {
1325
2468
  perSpecArchives += 1;
1326
2469
  perSpecEntries += (root.closures.get(version) ?? []).length;
1327
2470
  }
@@ -1345,17 +2488,33 @@ const withLayout = (options, layout) => ({
1345
2488
  * expected.
1346
2489
  */
1347
2490
  const plan = (specs, options) => Effect.gen(function* () {
2491
+ yield* preflight;
2492
+ return yield* resolve(specs, options);
2493
+ });
2494
+ /**
2495
+ * The lockfile counterpart of `plan`.
2496
+ *
2497
+ * Same preflight, same `Resolution` out; the difference is entirely in how the
2498
+ * package set is arrived at. Kept as a separate entry point rather than an
2499
+ * option on `plan` because the two take genuinely different inputs — a set of
2500
+ * specs to satisfy versus a graph to reproduce — and blurring that is how a
2501
+ * "pinned" run quietly starts resolving ranges again.
2502
+ */
2503
+ const planLocked = (lockfile, options) => Effect.gen(function* () {
2504
+ yield* preflight;
2505
+ return yield* resolveLocked(lockfile, options);
2506
+ });
2507
+ const preflight = Effect.gen(function* () {
1348
2508
  const registry = yield* Registry;
1349
2509
  yield* emit({
1350
2510
  _tag: "PhaseStarted",
1351
- phase: "preflight"
2511
+ phase: Phase.Preflight
1352
2512
  });
1353
2513
  yield* registry.preflight;
1354
2514
  yield* emit({
1355
2515
  _tag: "PhaseCompleted",
1356
- phase: "preflight"
2516
+ phase: Phase.Preflight
1357
2517
  });
1358
- return yield* resolve(specs, options);
1359
2518
  });
1360
2519
  /**
1361
2520
  * Resolves, downloads and packages a set of specs.
@@ -1367,6 +2526,10 @@ const plan = (specs, options) => Effect.gen(function* () {
1367
2526
  const bundle = (specs, options) => Effect.gen(function* () {
1368
2527
  return yield* bundleResolved(yield* plan(specs, options), options);
1369
2528
  });
2529
+ /** `bundle`, pinned to a lockfile. */
2530
+ const bundleLocked = (lockfile, options) => Effect.gen(function* () {
2531
+ return yield* bundleResolved(yield* planLocked(lockfile, options), options);
2532
+ });
1370
2533
  /**
1371
2534
  * Downloads and packages an already-computed resolution.
1372
2535
  *
@@ -1384,6 +2547,21 @@ const bundleResolved = (resolution, options) => Effect.gen(function* () {
1384
2547
  };
1385
2548
  return yield* Effect.scoped(runBundle(resolution, options));
1386
2549
  });
2550
+ /**
2551
+ * The files a run will write.
2552
+ *
2553
+ * Computed before anything is downloaded, so a collision is reported in the
2554
+ * first second rather than after a twenty-minute transfer.
2555
+ */
2556
+ const plannedOutputs = (resolution, options) => {
2557
+ if (options.layout === Layout.Dir) return [{ file: MANIFEST_FILE }, { file: README_FILE }];
2558
+ if (options.layout === Layout.Single) return [{ file: singleArchiveName(options.archiveName ?? "bundle") }];
2559
+ return perSpecTargets(resolution).map(({ root, version }) => ({
2560
+ file: perSpecArchiveName(root.spec.name, version),
2561
+ name: root.spec.name,
2562
+ version
2563
+ }));
2564
+ };
1387
2565
  const runBundle = (resolution, options) => Effect.gen(function* () {
1388
2566
  const fs = yield* FileSystem.FileSystem;
1389
2567
  const path = yield* Path.Path;
@@ -1398,42 +2576,25 @@ const runBundle = (resolution, options) => Effect.gen(function* () {
1398
2576
  });
1399
2577
  yield* emit({
1400
2578
  _tag: "PhaseStarted",
1401
- phase: "archive"
2579
+ phase: Phase.Archive
1402
2580
  });
1403
- const registryInfo = {
1404
- kind: registry.kind,
1405
- url: registry.registryFor(resolution.roots[0]?.spec.name ?? "")
1406
- };
1407
- const index = indexPackages(resolution);
1408
- const artifacts = options.layout === "single" ? yield* emitSingle({
1409
- resolution,
1410
- options,
1411
- staging,
1412
- packagesDir,
1413
- report,
1414
- registryInfo
1415
- }) : options.layout === "dir" ? yield* emitDirectory({
1416
- resolution,
1417
- options,
1418
- packagesDir,
1419
- report,
1420
- registryInfo
1421
- }) : yield* emitPerSpec({
2581
+ const artifacts = yield* emit$1({
1422
2582
  resolution,
1423
2583
  options,
1424
- staging,
1425
2584
  packagesDir,
1426
2585
  report,
1427
- registryInfo,
1428
- index
1429
- });
2586
+ registryInfo: {
2587
+ kind: registry.kind,
2588
+ url: registry.registryFor(resolution.roots[0]?.spec.name ?? "")
2589
+ }
2590
+ }, staging);
1430
2591
  yield* emit({
1431
2592
  _tag: "PhaseCompleted",
1432
- phase: "archive"
2593
+ phase: Phase.Archive
1433
2594
  });
1434
2595
  yield* emit({
1435
2596
  _tag: "PhaseStarted",
1436
- phase: "done"
2597
+ phase: Phase.Done
1437
2598
  });
1438
2599
  return {
1439
2600
  resolution,
@@ -1443,6 +2604,13 @@ const runBundle = (resolution, options) => Effect.gen(function* () {
1443
2604
  dryRun: false
1444
2605
  };
1445
2606
  });
2607
+ const emit$1 = (input, staging) => {
2608
+ switch (input.options.layout) {
2609
+ case Layout.Single: return emitSingle(input);
2610
+ case Layout.Dir: return emitDirectory(input);
2611
+ case Layout.PerSpec: return emitPerSpec(input, staging);
2612
+ }
2613
+ };
1446
2614
  /** One tarball containing the deduplicated union of every closure. */
1447
2615
  const emitSingle = (input) => Effect.gen(function* () {
1448
2616
  const path = yield* Path.Path;
@@ -1463,7 +2631,7 @@ const emitSingle = (input) => Effect.gen(function* () {
1463
2631
  outPath
1464
2632
  });
1465
2633
  return [{
1466
- kind: "archive",
2634
+ kind: ArtifactKind.Archive,
1467
2635
  path: result.path,
1468
2636
  bytes: result.bytes,
1469
2637
  packageCount: input.resolution.packages.length
@@ -1483,7 +2651,7 @@ const emitDirectory = (input) => Effect.gen(function* () {
1483
2651
  yield* fs.copy(input.packagesDir, input.options.outDir, { overwrite: true }).pipe(Effect.mapError((cause) => new OutputError(input.options.outDir, "could not write output tree", { cause })));
1484
2652
  const bytes = [...input.report.sizes.values()].reduce((a, b) => a + b, 0);
1485
2653
  return [{
1486
- kind: "directory",
2654
+ kind: ArtifactKind.Directory,
1487
2655
  path: input.options.outDir,
1488
2656
  bytes,
1489
2657
  packageCount: input.resolution.packages.length
@@ -1496,15 +2664,16 @@ const emitDirectory = (input) => Effect.gen(function* () {
1496
2664
  * `react-18.1.0.tgz` and so on, each independently importable — which is the
1497
2665
  * whole reason to prefer this layout.
1498
2666
  */
1499
- const emitPerSpec = (input) => Effect.gen(function* () {
2667
+ const emitPerSpec = (input, staging) => Effect.gen(function* () {
1500
2668
  const fs = yield* FileSystem.FileSystem;
1501
2669
  const path = yield* Path.Path;
2670
+ const index = indexPackages(input.resolution);
1502
2671
  const artifacts = [];
1503
2672
  let counter = 0;
1504
- for (const root of input.resolution.roots) for (const version of root.versions) {
2673
+ for (const { root, version } of perSpecTargets(input.resolution)) {
1505
2674
  if (input.options.skipExisting?.has(perSpecArchiveName(root.spec.name, version)) === true) continue;
1506
- const included = (root.closures.get(version) ?? []).map((key) => input.index.get(key)).filter((pkg) => pkg !== void 0);
1507
- const rootDir = path.join(input.staging, "roots", String(counter++));
2675
+ const included = includedPackages(index, root.closures.get(version) ?? []);
2676
+ const rootDir = path.join(staging, "roots", String(counter++));
1508
2677
  yield* fs.makeDirectory(rootDir, { recursive: true });
1509
2678
  for (const pkg of included) {
1510
2679
  const parts = packagePath(pkg.name, pkg.version).split("/");
@@ -1534,7 +2703,7 @@ const emitPerSpec = (input) => Effect.gen(function* () {
1534
2703
  outPath
1535
2704
  });
1536
2705
  artifacts.push({
1537
- kind: "archive",
2706
+ kind: ArtifactKind.Archive,
1538
2707
  path: result.path,
1539
2708
  bytes: result.bytes,
1540
2709
  packageCount: included.length,
@@ -1544,6 +2713,7 @@ const emitPerSpec = (input) => Effect.gen(function* () {
1544
2713
  }
1545
2714
  return artifacts;
1546
2715
  });
2716
+ const includedPackages = (index, closure) => closure.flatMap((key) => index.get(key) ?? []);
1547
2717
  /**
1548
2718
  * Hard-links a file, falling back to a copy.
1549
2719
  *
@@ -1578,17 +2748,50 @@ const writeBundleMetadata = (input) => Effect.gen(function* () {
1578
2748
  });
1579
2749
  /** Sorted top-level entries of a directory, used as the tar entry list. */
1580
2750
  const topLevelEntries = (dir) => Effect.gen(function* () {
1581
- return [...yield* (yield* FileSystem.FileSystem).readDirectory(dir)].sort();
2751
+ return (yield* (yield* FileSystem.FileSystem).readDirectory(dir)).toSorted();
2752
+ });
2753
+ /**
2754
+ * Two different packages must not want the same file name.
2755
+ *
2756
+ * Most of what this used to catch is now impossible: an archive is named for
2757
+ * the spec, `@types-react@19.2.18.tgz`, and an unscoped npm name may not begin
2758
+ * with `@`, so it cannot spell a scoped one. What survives is **two scoped
2759
+ * packages whose scope boundary falls in a different place**, because that
2760
+ * boundary is the one character the file name drops:
2761
+ *
2762
+ * @a/b-c -> @a-b-c@1.0.0.tgz
2763
+ * @a-b/c -> @a-b-c@1.0.0.tgz
2764
+ *
2765
+ * Pathological rather than plausible — it needs somebody to hold the `@a-b`
2766
+ * scope as well as `@a`, and both packages in one run at one version. It is
2767
+ * kept anyway because of what it costs to be wrong: they are *different
2768
+ * packages*, so deduplicating them would be wrong, and writing both is worse —
2769
+ * the second lands on the first and the run reports two artifacts for the one
2770
+ * file that survived. A bundle short a package, called complete, found at the
2771
+ * far end of an air gap by somebody who cannot go and fetch the missing one.
2772
+ *
2773
+ * Nothing else would catch it. The overwrite check below compares this run's
2774
+ * planned files against what is *already on disk*, and both of these are new.
2775
+ *
2776
+ * So it refuses, and names both specs. Not silently renamed: an archive whose
2777
+ * name nobody chose is a file somebody has to identify later.
2778
+ *
2779
+ * Unconditional, unlike the overwrite check: `--force` is permission to replace
2780
+ * what was already in the directory, not to let one run overwrite itself.
2781
+ */
2782
+ const assertDistinctOutputs = (resolution, options) => Effect.gen(function* () {
2783
+ const owners = /* @__PURE__ */ new Map();
2784
+ for (const planned of plannedOutputs(resolution, options)) {
2785
+ const claimed = owners.get(planned.file);
2786
+ const spec = planned.name === void 0 ? planned.file : `${planned.name}@${planned.version}`;
2787
+ if (claimed === void 0) owners.set(planned.file, [spec]);
2788
+ else claimed.push(spec);
2789
+ }
2790
+ const collisions = [...owners].filter(([, specs]) => specs.length > 1);
2791
+ if (collisions.length === 0) return;
2792
+ const described = collisions.map(([file, specs]) => `${specs.join(" and ")} would both be ${file}`).join("; ");
2793
+ return yield* Effect.fail(new OutputError(options.outDir, `two packages want the same archive name — ${described}. The \`/\` in a scope becomes a \`-\` to make a file name, so \`@a/b-c\` and \`@a-b/c\` meet. Use --layout single, or bundle them into separate --out directories.`));
1582
2794
  });
1583
- const plannedOutputs = (resolution, options) => {
1584
- if (options.layout === "dir") return [{ file: MANIFEST_FILE }, { file: README_FILE }];
1585
- if (options.layout === "single") return [{ file: singleArchiveName(options.archiveName ?? "bundle") }];
1586
- return resolution.roots.flatMap((root) => root.versions.map((version) => ({
1587
- file: perSpecArchiveName(root.spec.name, version),
1588
- name: root.spec.name,
1589
- version
1590
- })));
1591
- };
1592
2795
  /**
1593
2796
  * Fails if the run would overwrite something, unless `--force`.
1594
2797
  *
@@ -1599,12 +2802,14 @@ const plannedOutputs = (resolution, options) => {
1599
2802
  * default `.` unusable.
1600
2803
  *
1601
2804
  * Checked before anything is downloaded. Discovering the collision *after* a
1602
- * twenty-minute download would be a uniquely irritating way to fail.
2805
+ * twenty-minute download would be a uniquely irritating way to fail — which is
2806
+ * also why `assertDistinctOutputs` runs from here rather than at emit time.
1603
2807
  */
1604
2808
  const prepareOutputDir = (resolution, options) => Effect.gen(function* () {
1605
2809
  const fs = yield* FileSystem.FileSystem;
1606
2810
  const path = yield* Path.Path;
1607
2811
  const outDir = options.outDir;
2812
+ yield* assertDistinctOutputs(resolution, options);
1608
2813
  if (!options.force) {
1609
2814
  const clashes = [];
1610
2815
  for (const planned of plannedOutputs(resolution, options)) {
@@ -1618,149 +2823,5 @@ const prepareOutputDir = (resolution, options) => Effect.gen(function* () {
1618
2823
  });
1619
2824
 
1620
2825
  //#endregion
1621
- //#region src/InputFile.ts
1622
- var InputFile_exports = /* @__PURE__ */ __export({
1623
- defaultInputFileOptions: () => defaultInputFileOptions,
1624
- parseInputFile: () => parseInputFile,
1625
- readInputFile: () => readInputFile
1626
- });
1627
- const defaultInputFileOptions = { includeDev: true };
1628
- /** Reads and parses an input file. */
1629
- const readInputFile = (filePath, options = defaultInputFileOptions) => Effect.gen(function* () {
1630
- const fs = yield* FileSystem.FileSystem;
1631
- if (!(yield* fs.exists(filePath).pipe(Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause }))))) return yield* Effect.fail(new InvalidInputFileError(filePath, "file does not exist"));
1632
- const content = yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause })));
1633
- return yield* Effect.try({
1634
- try: () => parseInputFile(filePath, content, options),
1635
- catch: (error) => error instanceof InvalidInputFileError ? error : new InvalidInputFileError(filePath, describe(error), { cause: error })
1636
- });
1637
- });
1638
- /**
1639
- * Parses file content that has already been read.
1640
- *
1641
- * Split out from the IO so the whole of this logic is testable with plain
1642
- * strings and no file system at all.
1643
- */
1644
- const parseInputFile = (filePath, content, options = defaultInputFileOptions) => {
1645
- const trimmed = content.trim();
1646
- if (trimmed.length === 0) throw new InvalidInputFileError(filePath, "file is empty");
1647
- return looksLikeJson(trimmed) ? fromPackageJson(filePath, trimmed, options) : fromList(filePath, content);
1648
- };
1649
- const looksLikeJson = (trimmed) => trimmed.startsWith("{");
1650
- const fromPackageJson = (filePath, content, options) => {
1651
- let parsed;
1652
- try {
1653
- parsed = JSON.parse(content);
1654
- } catch (error) {
1655
- throw new InvalidInputFileError(filePath, `not valid JSON — ${describe(error)}`, { cause: error });
1656
- }
1657
- const specs = [];
1658
- const warnings = [];
1659
- const seen = /* @__PURE__ */ new Set();
1660
- const collect = (entries, label, skip) => {
1661
- if (entries === void 0) return;
1662
- for (const [name, raw] of Object.entries(entries)) {
1663
- if (skip?.(name)) continue;
1664
- const target = parseDependencyTarget(name, raw);
1665
- if (target._tag === "Unsupported") {
1666
- warnings.push(`${label}: skipped ${name}@${target.raw} (${target.reason}) — not fetchable from a registry`);
1667
- continue;
1668
- }
1669
- const key = `${target.name}|${JSON.stringify(target.selector)}`;
1670
- if (seen.has(key)) continue;
1671
- seen.add(key);
1672
- specs.push({
1673
- name: target.name,
1674
- selector: target.selector,
1675
- raw: `${target.name}@${raw}`
1676
- });
1677
- }
1678
- };
1679
- collect(parsed.dependencies, "dependencies");
1680
- collect(parsed.optionalDependencies, "optionalDependencies");
1681
- if (options.includeDev) collect(parsed.devDependencies, "devDependencies");
1682
- const meta = parsed.peerDependenciesMeta ?? {};
1683
- collect(parsed.peerDependencies, "peerDependencies", (name) => meta[name]?.optional === true);
1684
- if (specs.length === 0) throw new InvalidInputFileError(filePath, options.includeDev ? "package.json declares no dependencies to bundle" : "package.json declares no non-dev dependencies to bundle (drop --prod to include devDependencies)");
1685
- return {
1686
- kind: "package.json",
1687
- specs,
1688
- warnings
1689
- };
1690
- };
1691
- const fromList = (filePath, content) => {
1692
- const lines = [];
1693
- const rawLines = content.split(/\r?\n/);
1694
- for (const line of rawLines) {
1695
- const withoutComment = stripComment(line).trim();
1696
- if (withoutComment.length === 0) continue;
1697
- lines.push(withoutComment);
1698
- }
1699
- if (lines.length === 0) throw new InvalidInputFileError(filePath, "no package specs found (only blank lines and comments)");
1700
- const { specs, errors } = parseSpecs(lines);
1701
- if (errors.length > 0) throw new InvalidInputFileError(filePath, formatSpecErrors(errors, rawLines));
1702
- return {
1703
- kind: "list",
1704
- specs,
1705
- warnings: []
1706
- };
1707
- };
1708
- /**
1709
- * Strips `#` and `//` comments.
1710
- *
1711
- * `//` only counts when it is not part of a URL, so a future line containing
1712
- * `https://…` does not get silently truncated.
1713
- */
1714
- const stripComment = (line) => {
1715
- const hash = line.indexOf("#");
1716
- let result = hash === -1 ? line : line.slice(0, hash);
1717
- const slashes = result.indexOf("//");
1718
- if (slashes > 0 && result[slashes - 1] !== ":") result = result.slice(0, slashes);
1719
- else if (slashes === 0) result = "";
1720
- return result;
1721
- };
1722
- const formatSpecErrors = (errors, rawLines) => {
1723
- const details = errors.map((error) => {
1724
- const index = rawLines.findIndex((line) => stripComment(line).trim() === error.spec);
1725
- const where = index === -1 ? "" : ` (line ${index + 1})`;
1726
- return ` ${error.spec}${where}: ${error.reason}`;
1727
- });
1728
- return `${errors.length} invalid spec(s):\n${details.join("\n")}`;
1729
- };
1730
- const describe = (error) => error instanceof Error ? error.message : String(error);
1731
-
1732
- //#endregion
1733
- //#region src/Options.ts
1734
- var Options_exports = /* @__PURE__ */ __export({
1735
- defaultBundleOptions: () => defaultBundleOptions,
1736
- defaultResolveOptions: () => defaultResolveOptions,
1737
- defaultScope: () => defaultScope,
1738
- layouts: () => layouts
1739
- });
1740
- const layouts = [
1741
- "per-spec",
1742
- "single",
1743
- "dir"
1744
- ];
1745
- const defaultScope = {
1746
- optional: true,
1747
- peer: true,
1748
- platforms: allPlatforms
1749
- };
1750
- const defaultResolveOptions = {
1751
- scope: defaultScope,
1752
- allVersions: false,
1753
- includePrerelease: false,
1754
- concurrency: 10
1755
- };
1756
- const defaultBundleOptions = {
1757
- ...defaultResolveOptions,
1758
- layout: "per-spec",
1759
- dryRun: false,
1760
- verifyIntegrity: true,
1761
- force: false
1762
- };
1763
-
1764
- //#endregion
1765
- export { Archive_exports as Archive, ArchiveError, AuthenticationError, Bundle_exports as Bundle, DependencyRange_exports as DependencyRange, Download_exports as Download, InputFile_exports as InputFile, Integrity_exports as Integrity, IntegrityError, InvalidInputFileError, InvalidSpecError, Layout_exports as Layout, Manifest_exports as Manifest, NoMatchingVersionsError, Options_exports as Options, OutputError, PackageNotFoundError, Platform_exports as Platform, Progress_exports as Progress, Progress as ProgressTag, Registry, RegistryResponseError, RegistryUnreachableError, Resolve_exports as Resolve, Spec_exports as Spec, VersionNotFoundError, allPlatforms, bundle, bundleResolved, currentPlatform, defaultBundleOptions, defaultResolveOptions, defaultScope, layouts, parsePlatformTarget, parseSpec, parseSpecs, plan, plannedOutputs, platformTargets, resolve, summarize, withLayout };
2826
+ export { ArchiveError, Archiver, ArtifactKind, AuthenticationError, DirectOnlyKind, EdgeKind, InputFileKind, IntegrityError, InvalidInputFileError, InvalidSpecError, Layout, Layouts, LockedRootKind, LockfileError, LockfileFormat, LockfileIncompleteError, LockfileOutOfDateError, MANIFEST_FILE, MANIFEST_VERSION, NoMatchingVersionsError, OptionalFlagRecordOrAbsentSchema, OptionalFlagRecordSchema, OptionalStringArraySchema, OptionalStringRecordSchema, OutputError, PackageNotFoundError, Phase, Progress, README_FILE, Registry, RegistryResponseError, RegistryUnreachableError, StringArraySchema, StringRecordSchema, VersionNotFoundError, allPlatforms, buildManifest, bundle, bundleLocked, bundleResolved, createArchive, currentPlatform, dedupeSpecs, defaultBundleOptions, defaultInputFileOptions, defaultResolveOptions, defaultScope, detectLockfile, digestOf, emit as emitProgress, formatByLockfileName, formatPlatformFilter, formatSelector, formatSpec, hexDigestOf, importGuide, indexPackages, isIncluded, isRecord, layerCallback as layerCallbackProgress, layerSilent as layerSilentProgress, lockfileNamesFor, makeCollector as makeProgressCollector, packageKey, packagePath, parseDependencyTarget, parseInputFile, parseIntegrity, parseLockfile, parsePlatformTarget, parseSpec, parseSpecs, perSpecArchiveName, plan, planLocked, plannedOutputs, platformTargets, readInputFile, resolve, resolveLocked, selectVersions, serializeManifest, singleArchiveName, splitName, summarize, tarballFileName, tolerant, verify as verifyIntegrity, withLayout };
1766
2827
  //# sourceMappingURL=index.js.map