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