@packall/core 0.0.1
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 +32 -0
- package/dist/Archive.d.ts.map +1 -0
- package/dist/Bundle.d.ts +123 -0
- package/dist/Bundle.d.ts.map +1 -0
- package/dist/DependencyRange.d.ts +26 -0
- package/dist/DependencyRange.d.ts.map +1 -0
- package/dist/Download.d.ts +35 -0
- package/dist/Download.d.ts.map +1 -0
- package/dist/Errors.d.ts +124 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/InputFile.d.ts +46 -0
- package/dist/InputFile.d.ts.map +1 -0
- package/dist/Integrity.d.ts +48 -0
- package/dist/Integrity.d.ts.map +1 -0
- package/dist/Layout.d.ts +53 -0
- package/dist/Layout.d.ts.map +1 -0
- package/dist/Manifest.d.ts +82 -0
- package/dist/Manifest.d.ts.map +1 -0
- package/dist/Options.d.ts +100 -0
- package/dist/Options.d.ts.map +1 -0
- package/dist/Platform.d.ts +69 -0
- package/dist/Platform.d.ts.map +1 -0
- package/dist/Progress.d.ts +107 -0
- package/dist/Progress.d.ts.map +1 -0
- package/dist/Registry.d.ts +106 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Resolve.d.ts +105 -0
- package/dist/Resolve.d.ts.map +1 -0
- package/dist/Spec.d.ts +62 -0
- package/dist/Spec.d.ts.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1766 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/Archive.ts +92 -0
- package/src/Bundle.ts +570 -0
- package/src/DependencyRange.ts +115 -0
- package/src/Download.ts +132 -0
- package/src/Errors.ts +216 -0
- package/src/InputFile.ts +229 -0
- package/src/Integrity.ts +122 -0
- package/src/Layout.ts +103 -0
- package/src/Manifest.ts +133 -0
- package/src/Options.ts +123 -0
- package/src/Platform.ts +182 -0
- package/src/Progress.ts +106 -0
- package/src/Registry.ts +121 -0
- package/src/Resolve.ts +528 -0
- package/src/Spec.ts +186 -0
- package/src/index.ts +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1766 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
import { create } from "tar";
|
|
4
|
+
import * as Context from "effect/Context";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Ref from "effect/Ref";
|
|
7
|
+
import * as Path from "effect/Path";
|
|
8
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
9
|
+
import semver from "semver";
|
|
10
|
+
|
|
11
|
+
//#region rolldown:runtime
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __export = (all) => {
|
|
14
|
+
let target = {};
|
|
15
|
+
for (var name in all) __defProp(target, name, {
|
|
16
|
+
get: all[name],
|
|
17
|
+
enumerable: true
|
|
18
|
+
});
|
|
19
|
+
return target;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/Errors.ts
|
|
24
|
+
var BundlerErrorBase = class extends Error {
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = new.target.name;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** A package spec on the command line or in an input file could not be parsed. */
|
|
31
|
+
var InvalidSpecError = class extends BundlerErrorBase {
|
|
32
|
+
_tag = "InvalidSpecError";
|
|
33
|
+
spec;
|
|
34
|
+
reason;
|
|
35
|
+
constructor(spec, reason) {
|
|
36
|
+
super(`Invalid package spec ${JSON.stringify(spec)}: ${reason}`);
|
|
37
|
+
this.spec = spec;
|
|
38
|
+
this.reason = reason;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/** `--file` pointed at something that is neither a usable package.json nor a spec list. */
|
|
42
|
+
var InvalidInputFileError = class extends BundlerErrorBase {
|
|
43
|
+
_tag = "InvalidInputFileError";
|
|
44
|
+
path;
|
|
45
|
+
reason;
|
|
46
|
+
constructor(path, reason, options) {
|
|
47
|
+
super(`Cannot read specs from ${path}: ${reason}`, options);
|
|
48
|
+
this.path = path;
|
|
49
|
+
this.reason = reason;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The registry could not be reached at all — DNS failure, refused connection,
|
|
54
|
+
* proxy blackhole, or a preflight that timed out.
|
|
55
|
+
*
|
|
56
|
+
* This is the error that fixes "hangs forever with no network": we surface it
|
|
57
|
+
* quickly and say which host we could not reach.
|
|
58
|
+
*/
|
|
59
|
+
var RegistryUnreachableError = class extends BundlerErrorBase {
|
|
60
|
+
_tag = "RegistryUnreachableError";
|
|
61
|
+
registry;
|
|
62
|
+
timeoutMillis;
|
|
63
|
+
constructor(registry, detail, options) {
|
|
64
|
+
super(`Cannot reach registry ${registry}: ${detail}.\n Check your network connection, VPN, and the proxy settings in your .npmrc.`, options);
|
|
65
|
+
this.registry = registry;
|
|
66
|
+
this.timeoutMillis = options?.timeoutMillis;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/** The registry answered, but has never heard of this package. */
|
|
70
|
+
var PackageNotFoundError = class extends BundlerErrorBase {
|
|
71
|
+
_tag = "PackageNotFoundError";
|
|
72
|
+
packageName;
|
|
73
|
+
registry;
|
|
74
|
+
constructor(packageName, registry) {
|
|
75
|
+
super(`Package "${packageName}" was not found on ${registry}`);
|
|
76
|
+
this.packageName = packageName;
|
|
77
|
+
this.registry = registry;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/** The package exists but the exact version requested does not. */
|
|
81
|
+
var VersionNotFoundError = class extends BundlerErrorBase {
|
|
82
|
+
_tag = "VersionNotFoundError";
|
|
83
|
+
packageName;
|
|
84
|
+
version;
|
|
85
|
+
available;
|
|
86
|
+
constructor(packageName, version, available) {
|
|
87
|
+
const tail = available.slice(-5).join(", ");
|
|
88
|
+
super(`${packageName}@${version} does not exist.` + (tail.length > 0 ? ` Most recent published versions: ${tail}` : ""));
|
|
89
|
+
this.packageName = packageName;
|
|
90
|
+
this.version = version;
|
|
91
|
+
this.available = available;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/** A range (or dist-tag) matched nothing that is actually published. */
|
|
95
|
+
var NoMatchingVersionsError = class extends BundlerErrorBase {
|
|
96
|
+
_tag = "NoMatchingVersionsError";
|
|
97
|
+
packageName;
|
|
98
|
+
selector;
|
|
99
|
+
available;
|
|
100
|
+
constructor(packageName, selector, available) {
|
|
101
|
+
const tail = available.slice(-5).join(", ");
|
|
102
|
+
super(`No published version of ${packageName} satisfies "${selector}".` + (tail.length > 0 ? ` Most recent published versions: ${tail}` : ""));
|
|
103
|
+
this.packageName = packageName;
|
|
104
|
+
this.selector = selector;
|
|
105
|
+
this.available = available;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
/** The registry responded, but with something we cannot use. */
|
|
109
|
+
var RegistryResponseError = class extends BundlerErrorBase {
|
|
110
|
+
_tag = "RegistryResponseError";
|
|
111
|
+
url;
|
|
112
|
+
status;
|
|
113
|
+
constructor(url, detail, options) {
|
|
114
|
+
super(`Unexpected response from ${url}: ${detail}`, options);
|
|
115
|
+
this.url = url;
|
|
116
|
+
this.status = options?.status;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/** 401/403 — almost always a missing or stale token in `.npmrc`. */
|
|
120
|
+
var AuthenticationError = class extends BundlerErrorBase {
|
|
121
|
+
_tag = "AuthenticationError";
|
|
122
|
+
registry;
|
|
123
|
+
status;
|
|
124
|
+
constructor(registry, status, packageName) {
|
|
125
|
+
super(`Registry ${registry} rejected the request with HTTP ${status}` + (packageName ? ` while fetching "${packageName}"` : "") + `.\n Add credentials to your .npmrc, e.g.\n //${safeHost(registry)}/:_authToken=\${NPM_TOKEN}`);
|
|
126
|
+
this.registry = registry;
|
|
127
|
+
this.status = status;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* A downloaded tarball did not match the checksum the registry advertised.
|
|
132
|
+
*
|
|
133
|
+
* Never soft-fail this: a bundle is a supply-chain artifact and a corrupt or
|
|
134
|
+
* substituted tarball is exactly what integrity checking exists to catch.
|
|
135
|
+
*/
|
|
136
|
+
var IntegrityError = class extends BundlerErrorBase {
|
|
137
|
+
_tag = "IntegrityError";
|
|
138
|
+
packageName;
|
|
139
|
+
version;
|
|
140
|
+
expected;
|
|
141
|
+
actual;
|
|
142
|
+
constructor(packageName, version, expected, actual) {
|
|
143
|
+
super(`Integrity check failed for ${packageName}@${version}.\n expected: ${expected}\n actual: ${actual}\n The download was discarded. This is either corruption in transit or a tampered artifact.`);
|
|
144
|
+
this.packageName = packageName;
|
|
145
|
+
this.version = version;
|
|
146
|
+
this.expected = expected;
|
|
147
|
+
this.actual = actual;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/** Something went wrong writing the `.tgz`. */
|
|
151
|
+
var ArchiveError = class extends BundlerErrorBase {
|
|
152
|
+
_tag = "ArchiveError";
|
|
153
|
+
path;
|
|
154
|
+
constructor(path, detail, options) {
|
|
155
|
+
super(`Failed to create archive ${path}: ${detail}`, options);
|
|
156
|
+
this.path = path;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
/** Something went wrong preparing or writing to the output directory. */
|
|
160
|
+
var OutputError = class extends BundlerErrorBase {
|
|
161
|
+
_tag = "OutputError";
|
|
162
|
+
path;
|
|
163
|
+
constructor(path, detail, options) {
|
|
164
|
+
super(`Output error at ${path}: ${detail}`, options);
|
|
165
|
+
this.path = path;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const safeHost = (registry) => {
|
|
169
|
+
try {
|
|
170
|
+
const url = new URL(registry);
|
|
171
|
+
return url.host + url.pathname.replace(/\/+$/, "");
|
|
172
|
+
} catch {
|
|
173
|
+
return registry;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/Progress.ts
|
|
179
|
+
var Progress_exports = /* @__PURE__ */ __export({
|
|
180
|
+
Progress: () => Progress,
|
|
181
|
+
emit: () => emit,
|
|
182
|
+
layerCallback: () => layerCallback,
|
|
183
|
+
layerSilent: () => layerSilent,
|
|
184
|
+
makeCollector: () => makeCollector
|
|
185
|
+
});
|
|
186
|
+
/** Service tag for progress reporting. */
|
|
187
|
+
var Progress = class extends Context.Service()("@packall/core/Progress") {};
|
|
188
|
+
/** Emits an event to whichever reporter is installed. */
|
|
189
|
+
const emit = (event) => Effect.gen(function* () {
|
|
190
|
+
yield* (yield* Progress).emit(event);
|
|
191
|
+
});
|
|
192
|
+
/**
|
|
193
|
+
* Discards every event.
|
|
194
|
+
*
|
|
195
|
+
* The default for library consumers and for tests that do not care about
|
|
196
|
+
* progress — silence should never require ceremony.
|
|
197
|
+
*/
|
|
198
|
+
const layerSilent = Layer.succeed(Progress)({ emit: () => Effect.void });
|
|
199
|
+
/** Sends every event to a callback. Used by the CLI renderer. */
|
|
200
|
+
const layerCallback = (onEvent) => Layer.succeed(Progress)({ emit: (event) => Effect.sync(() => onEvent(event)) });
|
|
201
|
+
/**
|
|
202
|
+
* Accumulates every event into a `Ref`, for assertions.
|
|
203
|
+
*
|
|
204
|
+
* Returned as `[layer, ref]` so a test can provide the layer and then read the
|
|
205
|
+
* transcript afterwards.
|
|
206
|
+
*/
|
|
207
|
+
const makeCollector = Effect.gen(function* () {
|
|
208
|
+
const ref = yield* Ref.make([]);
|
|
209
|
+
return {
|
|
210
|
+
layer: Layer.succeed(Progress)({ emit: (event) => Ref.update(ref, (events) => [...events, event]) }),
|
|
211
|
+
events: Ref.get(ref)
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/Archive.ts
|
|
217
|
+
var Archive_exports = /* @__PURE__ */ __export({ createArchive: () => createArchive });
|
|
218
|
+
/**
|
|
219
|
+
* Packs `entries` (paths relative to `cwd`) into a gzipped tar at `outPath`.
|
|
220
|
+
*
|
|
221
|
+
* `portable` normalises uid/gid/mtime so the same inputs produce byte-identical
|
|
222
|
+
* output — which matters when a security team wants to diff two bundles or
|
|
223
|
+
* re-derive one from a manifest.
|
|
224
|
+
*/
|
|
225
|
+
const createArchive = (options) => Effect.gen(function* () {
|
|
226
|
+
const fs = yield* FileSystem.FileSystem;
|
|
227
|
+
yield* emit({
|
|
228
|
+
_tag: "ArchiveStarted",
|
|
229
|
+
path: options.outPath,
|
|
230
|
+
entryCount: options.entries.length
|
|
231
|
+
});
|
|
232
|
+
yield* Effect.tryPromise({
|
|
233
|
+
try: () => create({
|
|
234
|
+
gzip: options.gzipLevel === void 0 ? true : { level: options.gzipLevel },
|
|
235
|
+
file: options.outPath,
|
|
236
|
+
cwd: options.cwd,
|
|
237
|
+
portable: true,
|
|
238
|
+
onWriteEntry: (entry) => {
|
|
239
|
+
entry.path = entry.path.replace(/^\.\//, "");
|
|
240
|
+
}
|
|
241
|
+
}, [...options.entries].sort().map(dotSlash)),
|
|
242
|
+
catch: (cause) => new ArchiveError(options.outPath, describe$1(cause), { cause })
|
|
243
|
+
});
|
|
244
|
+
const info = yield* fs.stat(options.outPath);
|
|
245
|
+
const bytes = Number(info.size);
|
|
246
|
+
yield* emit({
|
|
247
|
+
_tag: "ArchiveCompleted",
|
|
248
|
+
path: options.outPath,
|
|
249
|
+
bytes
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
path: options.outPath,
|
|
253
|
+
bytes
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
/**
|
|
257
|
+
* Guards every entry against tar's `@` convention.
|
|
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.
|
|
270
|
+
*
|
|
271
|
+
* `./@scope` is not subject to the convention and resolves identically.
|
|
272
|
+
*/
|
|
273
|
+
const dotSlash = (entry) => entry.startsWith("./") ? entry : `./${entry}`;
|
|
274
|
+
const describe$1 = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/Integrity.ts
|
|
278
|
+
var Integrity_exports = /* @__PURE__ */ __export({
|
|
279
|
+
digestOf: () => digestOf,
|
|
280
|
+
hexDigestOf: () => hexDigestOf,
|
|
281
|
+
parseIntegrity: () => parseIntegrity,
|
|
282
|
+
verify: () => verify
|
|
283
|
+
});
|
|
284
|
+
const SUPPORTED = new Set([
|
|
285
|
+
"sha512",
|
|
286
|
+
"sha384",
|
|
287
|
+
"sha256",
|
|
288
|
+
"sha1"
|
|
289
|
+
]);
|
|
290
|
+
/**
|
|
291
|
+
* Parses an SRI string such as `sha512-abc...==`.
|
|
292
|
+
*
|
|
293
|
+
* npm permits several space-separated hashes; we keep every one we can verify
|
|
294
|
+
* and ignore algorithms Node does not implement.
|
|
295
|
+
*/
|
|
296
|
+
const parseIntegrity = (integrity) => {
|
|
297
|
+
const hashes = [];
|
|
298
|
+
for (const token of integrity.trim().split(/\s+/)) {
|
|
299
|
+
if (token.length === 0) continue;
|
|
300
|
+
const dash = token.indexOf("-");
|
|
301
|
+
if (dash <= 0) continue;
|
|
302
|
+
const algorithm = token.slice(0, dash).toLowerCase();
|
|
303
|
+
const digest = token.slice(dash + 1);
|
|
304
|
+
if (!SUPPORTED.has(algorithm) || digest.length === 0) continue;
|
|
305
|
+
hashes.push({
|
|
306
|
+
algorithm,
|
|
307
|
+
digest
|
|
308
|
+
});
|
|
309
|
+
}
|
|
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
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* Checks bytes against whichever checksums the registry supplied.
|
|
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.
|
|
329
|
+
*/
|
|
330
|
+
const verify = (data, dist) => {
|
|
331
|
+
if (dist.integrity !== void 0 && dist.integrity.length > 0) {
|
|
332
|
+
const hashes = parseIntegrity(dist.integrity);
|
|
333
|
+
if (hashes.length === 0) return {
|
|
334
|
+
_tag: "Unverifiable",
|
|
335
|
+
reason: `no supported algorithm in "${dist.integrity}"`
|
|
336
|
+
};
|
|
337
|
+
const best = [...hashes].sort((a, b) => strength(b.algorithm) - strength(a.algorithm))[0];
|
|
338
|
+
const actual = digestOf(data, best.algorithm);
|
|
339
|
+
return digestsEqual(actual, best.digest) ? {
|
|
340
|
+
_tag: "Verified",
|
|
341
|
+
using: best.algorithm
|
|
342
|
+
} : {
|
|
343
|
+
_tag: "Mismatch",
|
|
344
|
+
expected: `${best.algorithm}-${best.digest}`,
|
|
345
|
+
actual: `${best.algorithm}-${actual}`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (dist.shasum !== void 0 && dist.shasum.length > 0) {
|
|
349
|
+
const actual = hexDigestOf(data, "sha1");
|
|
350
|
+
return digestsEqual(actual.toLowerCase(), dist.shasum.toLowerCase()) ? {
|
|
351
|
+
_tag: "Verified",
|
|
352
|
+
using: "sha1"
|
|
353
|
+
} : {
|
|
354
|
+
_tag: "Mismatch",
|
|
355
|
+
expected: `sha1-${dist.shasum}`,
|
|
356
|
+
actual: `sha1-${actual}`
|
|
357
|
+
};
|
|
358
|
+
}
|
|
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;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/Layout.ts
|
|
375
|
+
var Layout_exports = /* @__PURE__ */ __export({
|
|
376
|
+
MANIFEST_FILE: () => MANIFEST_FILE,
|
|
377
|
+
README_FILE: () => README_FILE,
|
|
378
|
+
importGuide: () => importGuide,
|
|
379
|
+
packagePath: () => packagePath,
|
|
380
|
+
perSpecArchiveName: () => perSpecArchiveName,
|
|
381
|
+
singleArchiveName: () => singleArchiveName,
|
|
382
|
+
splitName: () => splitName,
|
|
383
|
+
tarballFileName: () => tarballFileName
|
|
384
|
+
});
|
|
385
|
+
/**
|
|
386
|
+
* The on-disk shape of a bundle.
|
|
387
|
+
*
|
|
388
|
+
* Everything is laid out exactly the way a registry serves it:
|
|
389
|
+
*
|
|
390
|
+
* lodash/-/lodash-4.17.21.tgz
|
|
391
|
+
* @babel/core/-/core-7.24.0.tgz
|
|
392
|
+
*
|
|
393
|
+
* That is not an Artifactory convention — it is the path structure in every
|
|
394
|
+
* `dist.tarball` URL npm publishes, which is why the same tree imports into
|
|
395
|
+
* Artifactory, Nexus and Verdaccio without translation. Note that a scoped
|
|
396
|
+
* package's file name drops the scope: `@babel/core` becomes `core-7.24.0.tgz`
|
|
397
|
+
* under an `@babel/core/-/` directory.
|
|
398
|
+
*/
|
|
399
|
+
/** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
|
|
400
|
+
const splitName = (name) => {
|
|
401
|
+
if (!name.startsWith("@")) return {
|
|
402
|
+
scope: void 0,
|
|
403
|
+
bare: name
|
|
404
|
+
};
|
|
405
|
+
const slash = name.indexOf("/");
|
|
406
|
+
if (slash === -1) return {
|
|
407
|
+
scope: void 0,
|
|
408
|
+
bare: name
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
scope: name.slice(0, slash),
|
|
412
|
+
bare: name.slice(slash + 1)
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
/** `@babel/core` + `7.24.0` -> `core-7.24.0.tgz` */
|
|
416
|
+
const tarballFileName = (name, version) => {
|
|
417
|
+
const { bare } = splitName(name);
|
|
418
|
+
return `${bare}-${version}.tgz`;
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* The path of one package tarball within a bundle, using `/` separators
|
|
422
|
+
* regardless of host platform — these become tar entry names, and tar entries
|
|
423
|
+
* are always POSIX.
|
|
424
|
+
*/
|
|
425
|
+
const packagePath = (name, version) => `${name}/-/${tarballFileName(name, version)}`;
|
|
426
|
+
/** File name of the manifest that ships inside every bundle. */
|
|
427
|
+
const MANIFEST_FILE = "bundle-manifest.json";
|
|
428
|
+
/** File name of the short import guide that ships inside every bundle. */
|
|
429
|
+
const README_FILE = "IMPORT.md";
|
|
430
|
+
/**
|
|
431
|
+
* Name of the archive produced for one root spec in `per-spec` layout.
|
|
432
|
+
*
|
|
433
|
+
* Scoped names are flattened (`@babel/core` -> `babel-core`) because a `/` in a
|
|
434
|
+
* file name is not a thing, and `@` confuses enough shells and web UIs to be
|
|
435
|
+
* worth avoiding.
|
|
436
|
+
*/
|
|
437
|
+
const perSpecArchiveName = (name, version) => {
|
|
438
|
+
const { scope, bare } = splitName(name);
|
|
439
|
+
return `${scope === void 0 ? bare : `${scope.slice(1)}-${bare}`}-${version}.tgz`;
|
|
440
|
+
};
|
|
441
|
+
/** Name of the archive produced in `single` layout. */
|
|
442
|
+
const singleArchiveName = (base = "bundle") => `${base}.tgz`;
|
|
443
|
+
/**
|
|
444
|
+
* The import guide written into each bundle.
|
|
445
|
+
*
|
|
446
|
+
* Kept short and copy-pasteable on purpose: whoever opens this is mid-task on a
|
|
447
|
+
* restricted network and does not want prose.
|
|
448
|
+
*/
|
|
449
|
+
const importGuide = (options) => `# Importing this bundle
|
|
450
|
+
|
|
451
|
+
${options.packageCount} package tarball(s), laid out exactly as an npm registry serves them:
|
|
452
|
+
|
|
453
|
+
<package-name>/-/<file>.tgz
|
|
454
|
+
@<scope>/<name>/-/<file>.tgz
|
|
455
|
+
|
|
456
|
+
Created ${options.createdAt} by packall ${options.toolVersion}.
|
|
457
|
+
See ${MANIFEST_FILE} for the full list with checksums.
|
|
458
|
+
|
|
459
|
+
## JFrog Artifactory
|
|
460
|
+
|
|
461
|
+
Unpack, then upload the tree into a **local npm** repository. The layout already
|
|
462
|
+
matches what Artifactory expects, so no path rewriting is needed:
|
|
463
|
+
|
|
464
|
+
tar -xzf <this-bundle>.tgz -C ./bundle
|
|
465
|
+
jf rt upload "bundle/(**)" "<npm-local-repo>/{1}" --flat=false
|
|
466
|
+
|
|
467
|
+
## Verdaccio / Nexus / any npm registry
|
|
468
|
+
|
|
469
|
+
Publish each tarball individually:
|
|
470
|
+
|
|
471
|
+
find ./bundle -name '*.tgz' -exec npm publish --registry <url> {} \\;
|
|
472
|
+
|
|
473
|
+
## Verifying before import
|
|
474
|
+
|
|
475
|
+
node -e "const m=require('./bundle/${MANIFEST_FILE}');console.log(m.packages.length+' packages')"
|
|
476
|
+
|
|
477
|
+
Every entry in ${MANIFEST_FILE} carries the integrity string the source registry
|
|
478
|
+
advertised, and each tarball was checked against it at download time.
|
|
479
|
+
`;
|
|
480
|
+
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/Registry.ts
|
|
483
|
+
/**
|
|
484
|
+
* Service tag for the registry backend.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```ts
|
|
488
|
+
* import { Effect } from "effect"
|
|
489
|
+
* import { Registry } from "@packall/core"
|
|
490
|
+
*
|
|
491
|
+
* const program = Effect.gen(function*() {
|
|
492
|
+
* const registry = yield* Registry
|
|
493
|
+
* return yield* registry.packument("lodash")
|
|
494
|
+
* })
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
var Registry = class extends Context.Service()("@packall/core/Registry") {};
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/Download.ts
|
|
501
|
+
var Download_exports = /* @__PURE__ */ __export({ downloadAll: () => downloadAll });
|
|
502
|
+
/**
|
|
503
|
+
* Downloads every package into `destDir`, laid out the way a registry serves
|
|
504
|
+
* them.
|
|
505
|
+
*
|
|
506
|
+
* `verifyIntegrity` defaulting to on is deliberate: a corrupt tarball that
|
|
507
|
+
* makes it into a corporate registry is far more expensive than a failed run.
|
|
508
|
+
*/
|
|
509
|
+
const downloadAll = (packages, destDir, options) => Effect.gen(function* () {
|
|
510
|
+
const fs = yield* FileSystem.FileSystem;
|
|
511
|
+
const path = yield* Path.Path;
|
|
512
|
+
const registry = yield* Registry;
|
|
513
|
+
yield* emit({
|
|
514
|
+
_tag: "PhaseStarted",
|
|
515
|
+
phase: "download",
|
|
516
|
+
total: packages.length
|
|
517
|
+
});
|
|
518
|
+
let completed = 0;
|
|
519
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
520
|
+
const unverified = [];
|
|
521
|
+
const results = yield* Effect.forEach(packages, (pkg) => Effect.gen(function* () {
|
|
522
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
523
|
+
yield* emit({
|
|
524
|
+
_tag: "DownloadStarted",
|
|
525
|
+
name: pkg.name,
|
|
526
|
+
version: pkg.version
|
|
527
|
+
});
|
|
528
|
+
const bytes = yield* registry.download(pkg.manifest);
|
|
529
|
+
if (options.verifyIntegrity) {
|
|
530
|
+
const result = verify(bytes, pkg.manifest.dist);
|
|
531
|
+
if (result._tag === "Mismatch") return yield* Effect.fail(new IntegrityError(pkg.name, pkg.version, result.expected, result.actual));
|
|
532
|
+
if (result._tag === "Unverifiable") yield* emit({
|
|
533
|
+
_tag: "Warning",
|
|
534
|
+
message: `${key} could not be verified (${result.reason})`
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const verifiable = (pkg.manifest.dist.integrity?.length ?? 0) > 0 || (pkg.manifest.dist.shasum?.length ?? 0) > 0;
|
|
538
|
+
const relative = packagePath(pkg.name, pkg.version);
|
|
539
|
+
const target = path.join(destDir, ...relative.split("/"));
|
|
540
|
+
yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.mapError((cause) => new OutputError(path.dirname(target), "could not create directory", { cause })));
|
|
541
|
+
yield* fs.writeFile(target, bytes).pipe(Effect.mapError((cause) => new OutputError(target, "could not write tarball", { cause })));
|
|
542
|
+
completed += 1;
|
|
543
|
+
yield* emit({
|
|
544
|
+
_tag: "DownloadCompleted",
|
|
545
|
+
name: pkg.name,
|
|
546
|
+
version: pkg.version,
|
|
547
|
+
bytes: bytes.byteLength,
|
|
548
|
+
completedCount: completed,
|
|
549
|
+
totalCount: packages.length
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
key,
|
|
553
|
+
bytes: bytes.byteLength,
|
|
554
|
+
verifiable
|
|
555
|
+
};
|
|
556
|
+
}), { concurrency: options.concurrency });
|
|
557
|
+
for (const result of results) {
|
|
558
|
+
sizes.set(result.key, result.bytes);
|
|
559
|
+
if (!result.verifiable) unverified.push(result.key);
|
|
560
|
+
}
|
|
561
|
+
yield* emit({
|
|
562
|
+
_tag: "PhaseCompleted",
|
|
563
|
+
phase: "download"
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
sizes,
|
|
567
|
+
unverified,
|
|
568
|
+
totalBytes: results.reduce((sum, r) => sum + r.bytes, 0)
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/Platform.ts
|
|
574
|
+
var Platform_exports = /* @__PURE__ */ __export({
|
|
575
|
+
allPlatforms: () => allPlatforms,
|
|
576
|
+
currentPlatform: () => currentPlatform,
|
|
577
|
+
formatPlatformFilter: () => formatPlatformFilter,
|
|
578
|
+
isIncluded: () => isIncluded,
|
|
579
|
+
matchesTarget: () => matchesTarget,
|
|
580
|
+
parsePlatformTarget: () => parsePlatformTarget,
|
|
581
|
+
platformTargets: () => platformTargets
|
|
582
|
+
});
|
|
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
|
+
});
|
|
593
|
+
/**
|
|
594
|
+
* npm's documented `os` values, plus the names people actually type.
|
|
595
|
+
*
|
|
596
|
+
* An OS on its own is a valid target, so a typo has to be caught here — the
|
|
597
|
+
* whole failure mode this tool exists to prevent is a bundle that looks fine
|
|
598
|
+
* and installs nothing. `--platform windows` silently matching no binding at
|
|
599
|
+
* all would be precisely that, so it is corrected rather than accepted.
|
|
600
|
+
*/
|
|
601
|
+
const OS_ALIASES = {
|
|
602
|
+
win: "win32",
|
|
603
|
+
windows: "win32",
|
|
604
|
+
mac: "darwin",
|
|
605
|
+
macos: "darwin",
|
|
606
|
+
osx: "darwin"
|
|
607
|
+
};
|
|
608
|
+
const KNOWN_OS = new Set([
|
|
609
|
+
"aix",
|
|
610
|
+
"android",
|
|
611
|
+
"cygwin",
|
|
612
|
+
"darwin",
|
|
613
|
+
"freebsd",
|
|
614
|
+
"haiku",
|
|
615
|
+
"linux",
|
|
616
|
+
"netbsd",
|
|
617
|
+
"openbsd",
|
|
618
|
+
"sunos",
|
|
619
|
+
"win32"
|
|
620
|
+
]);
|
|
621
|
+
const LIBC = new Set(["musl", "glibc"]);
|
|
622
|
+
/**
|
|
623
|
+
* Parses `linux`, `linux-x64`, `darwin-arm64`, `win32-x64`, `linux-x64-musl`,
|
|
624
|
+
* `linux-musl`.
|
|
625
|
+
*
|
|
626
|
+
* An OS on its own means every architecture for that OS, which is almost always
|
|
627
|
+
* what you want: "the Windows and Linux bindings" is a far more natural way to
|
|
628
|
+
* describe a target set than enumerating four `os-arch` pairs, and it keeps
|
|
629
|
+
* working when a package adds an arm64 build.
|
|
630
|
+
*
|
|
631
|
+
* Returns `null` rather than throwing so the CLI can report every bad value at
|
|
632
|
+
* once alongside the list of accepted forms.
|
|
633
|
+
*/
|
|
634
|
+
const parsePlatformTarget = (input) => {
|
|
635
|
+
const parts = input.trim().toLowerCase().split("-").filter((p) => p.length > 0);
|
|
636
|
+
if (parts.length === 0 || parts.length > 3) return null;
|
|
637
|
+
const rawOs = parts[0];
|
|
638
|
+
const os = OS_ALIASES[rawOs] ?? rawOs;
|
|
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]
|
|
644
|
+
};
|
|
645
|
+
const cpu = parts[1];
|
|
646
|
+
if (parts.length === 2) return {
|
|
647
|
+
os,
|
|
648
|
+
cpu
|
|
649
|
+
};
|
|
650
|
+
const libc = parts[2];
|
|
651
|
+
if (!LIBC.has(libc)) return null;
|
|
652
|
+
return {
|
|
653
|
+
os,
|
|
654
|
+
cpu,
|
|
655
|
+
libc
|
|
656
|
+
};
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* npm's `os`/`cpu`/`libc` fields use a list of allowed values, where a leading
|
|
660
|
+
* `!` negates. An empty or absent list means "no constraint".
|
|
661
|
+
*
|
|
662
|
+
* The semantics npm implements: if any negated entry matches, reject. Otherwise
|
|
663
|
+
* if there are any positive entries, at least one must match.
|
|
664
|
+
*/
|
|
665
|
+
const listAllows = (list, value) => {
|
|
666
|
+
if (list === void 0 || list.length === 0) return true;
|
|
667
|
+
let hasPositive = false;
|
|
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;
|
|
681
|
+
};
|
|
682
|
+
/** Would this package install on this specific target? */
|
|
683
|
+
const matchesTarget = (constraints, target) => {
|
|
684
|
+
if (!listAllows(constraints.os, target.os)) return false;
|
|
685
|
+
if (target.cpu !== void 0 && !listAllows(constraints.cpu, target.cpu)) return false;
|
|
686
|
+
if (target.libc !== void 0 && !listAllows(constraints.libc, target.libc)) return false;
|
|
687
|
+
return true;
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Should this package be included, given the active filter?
|
|
691
|
+
*
|
|
692
|
+
* Under `All` this is unconditionally `true` — that is the whole point.
|
|
693
|
+
*/
|
|
694
|
+
const isIncluded = (constraints, filter) => {
|
|
695
|
+
if (filter._tag === "All") return true;
|
|
696
|
+
return filter.targets.some((target) => matchesTarget(constraints, target));
|
|
697
|
+
};
|
|
698
|
+
/** Renders a filter for the manifest and for `--dry-run` output. */
|
|
699
|
+
const formatPlatformFilter = (filter) => {
|
|
700
|
+
if (filter._tag === "All") return "all";
|
|
701
|
+
return filter.targets.map((t) => [
|
|
702
|
+
t.os,
|
|
703
|
+
t.cpu,
|
|
704
|
+
t.libc
|
|
705
|
+
].filter((part) => part !== void 0).join("-")).join(",");
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/Spec.ts
|
|
710
|
+
var Spec_exports = /* @__PURE__ */ __export({
|
|
711
|
+
dedupeSpecs: () => dedupeSpecs,
|
|
712
|
+
formatSelector: () => formatSelector,
|
|
713
|
+
formatSpec: () => formatSpec,
|
|
714
|
+
parseSelector: () => parseSelector,
|
|
715
|
+
parseSpec: () => parseSpec,
|
|
716
|
+
parseSpecs: () => parseSpecs,
|
|
717
|
+
validatePackageName: () => validatePackageName
|
|
718
|
+
});
|
|
719
|
+
/** Renders a spec back into the canonical `name@selector` form. */
|
|
720
|
+
const formatSpec = (spec) => {
|
|
721
|
+
switch (spec.selector._tag) {
|
|
722
|
+
case "Exact": return `${spec.name}@${spec.selector.version}`;
|
|
723
|
+
case "Range": return `${spec.name}@${spec.selector.range}`;
|
|
724
|
+
case "Tag": return `${spec.name}@${spec.selector.tag}`;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
/** Human-readable form of just the selector part. */
|
|
728
|
+
const formatSelector = (selector) => {
|
|
729
|
+
switch (selector._tag) {
|
|
730
|
+
case "Exact": return selector.version;
|
|
731
|
+
case "Range": return selector.range;
|
|
732
|
+
case "Tag": return selector.tag;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const MAX_NAME_LENGTH = 214;
|
|
736
|
+
/**
|
|
737
|
+
* Validates an npm package name against the rules the registry actually
|
|
738
|
+
* enforces. Returns `null` when valid, or the reason it is not.
|
|
739
|
+
*/
|
|
740
|
+
const validatePackageName = (name) => {
|
|
741
|
+
if (name.length === 0) return "name is empty";
|
|
742
|
+
if (name.length > MAX_NAME_LENGTH) return `name is longer than ${MAX_NAME_LENGTH} characters`;
|
|
743
|
+
if (name.trim() !== name) return "name has leading or trailing whitespace";
|
|
744
|
+
if (name.startsWith(".")) return "name cannot start with a period";
|
|
745
|
+
if (name.startsWith("_")) return "name cannot start with an underscore";
|
|
746
|
+
if (name !== name.toLowerCase()) return "name cannot contain capital letters";
|
|
747
|
+
if (name.startsWith("@")) {
|
|
748
|
+
const slash = name.indexOf("/");
|
|
749
|
+
if (slash === -1) return "scoped name is missing the '/name' part";
|
|
750
|
+
const scope = name.slice(1, slash);
|
|
751
|
+
const rest = name.slice(slash + 1);
|
|
752
|
+
if (scope.length === 0) return "scope is empty";
|
|
753
|
+
if (rest.length === 0) return "name after the scope is empty";
|
|
754
|
+
if (rest.includes("/")) return "name contains more than one '/'";
|
|
755
|
+
if (!SEGMENT_RE.test(scope)) return `scope "${scope}" contains illegal characters`;
|
|
756
|
+
if (!SEGMENT_RE.test(rest)) return `name "${rest}" contains illegal characters`;
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
if (name.includes("/")) return "unscoped name cannot contain '/'";
|
|
760
|
+
if (!SEGMENT_RE.test(name)) return "name contains illegal characters";
|
|
761
|
+
return null;
|
|
762
|
+
};
|
|
763
|
+
const SEGMENT_RE = /^[a-z0-9\-._~]+$/;
|
|
764
|
+
/**
|
|
765
|
+
* Parses a single spec string.
|
|
766
|
+
*
|
|
767
|
+
* Splitting on `@` is the fiddly part: a scoped name *starts* with `@`, so we
|
|
768
|
+
* look for the last `@` that is not at index 0.
|
|
769
|
+
*/
|
|
770
|
+
const parseSpec = (raw) => {
|
|
771
|
+
const input = raw.trim();
|
|
772
|
+
if (input.length === 0) throw new InvalidSpecError(raw, "spec is empty");
|
|
773
|
+
const at = input.lastIndexOf("@");
|
|
774
|
+
const hasSelector = at > 0;
|
|
775
|
+
const name = hasSelector ? input.slice(0, at) : input;
|
|
776
|
+
const selectorText = hasSelector ? input.slice(at + 1) : "";
|
|
777
|
+
const nameProblem = validatePackageName(name);
|
|
778
|
+
if (nameProblem !== null) throw new InvalidSpecError(raw, nameProblem);
|
|
779
|
+
if (!hasSelector || selectorText.length === 0) return {
|
|
780
|
+
name,
|
|
781
|
+
selector: {
|
|
782
|
+
_tag: "Tag",
|
|
783
|
+
tag: "latest"
|
|
784
|
+
},
|
|
785
|
+
raw: input
|
|
786
|
+
};
|
|
787
|
+
return {
|
|
788
|
+
name,
|
|
789
|
+
selector: parseSelector(raw, selectorText),
|
|
790
|
+
raw: input
|
|
791
|
+
};
|
|
792
|
+
};
|
|
793
|
+
/** Classifies the part after the `@` as an exact version, a range, or a dist-tag. */
|
|
794
|
+
const parseSelector = (raw, text) => {
|
|
795
|
+
const exact = semver.valid(text, { loose: false });
|
|
796
|
+
if (exact !== null) return {
|
|
797
|
+
_tag: "Exact",
|
|
798
|
+
version: exact
|
|
799
|
+
};
|
|
800
|
+
if (semver.validRange(text, { loose: false }) !== null) return {
|
|
801
|
+
_tag: "Range",
|
|
802
|
+
range: text
|
|
803
|
+
};
|
|
804
|
+
if (!DIST_TAG_RE.test(text)) throw new InvalidSpecError(raw, `"${text}" is not a valid version, semver range, or dist-tag`);
|
|
805
|
+
return {
|
|
806
|
+
_tag: "Tag",
|
|
807
|
+
tag: text
|
|
808
|
+
};
|
|
809
|
+
};
|
|
810
|
+
const DIST_TAG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
811
|
+
/**
|
|
812
|
+
* Parses many specs, collecting *every* failure rather than stopping at the
|
|
813
|
+
* first. Somebody bundling forty packages should be told about all four typos
|
|
814
|
+
* in one go, not made to re-run four times.
|
|
815
|
+
*/
|
|
816
|
+
const parseSpecs = (inputs) => {
|
|
817
|
+
const specs = [];
|
|
818
|
+
const errors = [];
|
|
819
|
+
for (const input of inputs) try {
|
|
820
|
+
specs.push(parseSpec(input));
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (error instanceof InvalidSpecError) errors.push(error);
|
|
823
|
+
else throw error;
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
specs,
|
|
827
|
+
errors
|
|
828
|
+
};
|
|
829
|
+
};
|
|
830
|
+
/**
|
|
831
|
+
* Collapses duplicate specs, keeping the first occurrence.
|
|
832
|
+
*
|
|
833
|
+
* `npmb react react@latest` is a plausible thing to type and should not
|
|
834
|
+
* download React twice.
|
|
835
|
+
*/
|
|
836
|
+
const dedupeSpecs = (specs) => {
|
|
837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
838
|
+
const out = [];
|
|
839
|
+
for (const spec of specs) {
|
|
840
|
+
const key = formatSpec(spec);
|
|
841
|
+
if (seen.has(key)) continue;
|
|
842
|
+
seen.add(key);
|
|
843
|
+
out.push(spec);
|
|
844
|
+
}
|
|
845
|
+
return out;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/Manifest.ts
|
|
850
|
+
var Manifest_exports = /* @__PURE__ */ __export({
|
|
851
|
+
MANIFEST_VERSION: () => MANIFEST_VERSION,
|
|
852
|
+
buildManifest: () => buildManifest,
|
|
853
|
+
formatReasons: () => formatReasons,
|
|
854
|
+
serializeManifest: () => serializeManifest
|
|
855
|
+
});
|
|
856
|
+
/** Schema version, bumped when the shape changes incompatibly. */
|
|
857
|
+
const MANIFEST_VERSION = 1;
|
|
858
|
+
/** Renders a package's reasons into short, greppable strings. */
|
|
859
|
+
const formatReasons = (pkg) => pkg.reasons.map((reason) => reason._tag === "Root" ? `root:${reason.spec}` : `${reason.kind}:${reason.from}`);
|
|
860
|
+
/** Builds the manifest for a completed (or dry) run. */
|
|
861
|
+
const buildManifest = (input) => {
|
|
862
|
+
const includedNames = new Set(input.included.map((p) => `${p.name}@${p.version}`));
|
|
863
|
+
const packages = input.included.map((pkg) => {
|
|
864
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
865
|
+
return {
|
|
866
|
+
name: pkg.name,
|
|
867
|
+
version: pkg.version,
|
|
868
|
+
path: packagePath(pkg.name, pkg.version),
|
|
869
|
+
tarball: pkg.manifest.dist.tarball,
|
|
870
|
+
integrity: pkg.manifest.dist.integrity,
|
|
871
|
+
shasum: pkg.manifest.dist.shasum,
|
|
872
|
+
bytes: input.sizes.get(key),
|
|
873
|
+
reasons: formatReasons(pkg)
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
const totalBytes = packages.reduce((sum, entry) => sum + (entry.bytes ?? 0), 0);
|
|
877
|
+
return {
|
|
878
|
+
manifestVersion: MANIFEST_VERSION,
|
|
879
|
+
tool: {
|
|
880
|
+
name: "packall",
|
|
881
|
+
version: input.toolVersion
|
|
882
|
+
},
|
|
883
|
+
createdAt: (input.createdAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
884
|
+
registry: input.registry,
|
|
885
|
+
requested: input.resolution.roots.map((root) => formatSpec(root.spec)),
|
|
886
|
+
roots: input.resolution.roots.map((root) => ({
|
|
887
|
+
spec: formatSpec(root.spec),
|
|
888
|
+
versions: root.versions,
|
|
889
|
+
packageCount: root.closure.filter((key) => includedNames.has(key)).length
|
|
890
|
+
})),
|
|
891
|
+
options: {
|
|
892
|
+
layout: input.options.layout,
|
|
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
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
};
|
|
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
|
+
/**
|
|
929
|
+
* Classifies one `name -> range` entry.
|
|
930
|
+
*
|
|
931
|
+
* An empty range, `*`, and `latest` all mean "any published version"; npm
|
|
932
|
+
* treats them interchangeably and so do we.
|
|
933
|
+
*/
|
|
934
|
+
const parseDependencyTarget = (name, raw) => {
|
|
935
|
+
const text = raw.trim();
|
|
936
|
+
if (text.length === 0 || text === "*" || text === "x" || text === "latest") return {
|
|
937
|
+
_tag: "Registry",
|
|
938
|
+
name,
|
|
939
|
+
selector: {
|
|
940
|
+
_tag: "Range",
|
|
941
|
+
range: "*"
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
if (text.startsWith("npm:")) return parseAlias(name, text);
|
|
945
|
+
for (const [prefix, reason] of UNSUPPORTED_PROTOCOLS) if (text.startsWith(prefix)) return {
|
|
946
|
+
_tag: "Unsupported",
|
|
947
|
+
name,
|
|
948
|
+
raw: text,
|
|
949
|
+
reason
|
|
950
|
+
};
|
|
951
|
+
if (semver.validRange(text, { loose: true }) === null && /^[\w.-]+\/[\w.-]+/.test(text)) return {
|
|
952
|
+
_tag: "Unsupported",
|
|
953
|
+
name,
|
|
954
|
+
raw: text,
|
|
955
|
+
reason: "git shorthand"
|
|
956
|
+
};
|
|
957
|
+
const exact = semver.valid(text, { loose: true });
|
|
958
|
+
if (exact !== null) return {
|
|
959
|
+
_tag: "Registry",
|
|
960
|
+
name,
|
|
961
|
+
selector: {
|
|
962
|
+
_tag: "Exact",
|
|
963
|
+
version: exact
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
if (semver.validRange(text, { loose: true }) !== null) return {
|
|
967
|
+
_tag: "Registry",
|
|
968
|
+
name,
|
|
969
|
+
selector: {
|
|
970
|
+
_tag: "Range",
|
|
971
|
+
range: text
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
if (/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(text)) return {
|
|
975
|
+
_tag: "Registry",
|
|
976
|
+
name,
|
|
977
|
+
selector: {
|
|
978
|
+
_tag: "Tag",
|
|
979
|
+
tag: text
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
return {
|
|
983
|
+
_tag: "Unsupported",
|
|
984
|
+
name,
|
|
985
|
+
raw: text,
|
|
986
|
+
reason: "unrecognised version specifier"
|
|
987
|
+
};
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* Parses `npm:<name>[@<range>]`.
|
|
991
|
+
*
|
|
992
|
+
* The nested name may itself be scoped, so the `@` split has the same
|
|
993
|
+
* "not at index 0" caveat as top-level spec parsing.
|
|
994
|
+
*/
|
|
995
|
+
const parseAlias = (key, text) => {
|
|
996
|
+
const body = text.slice(4);
|
|
997
|
+
if (body.length === 0) return {
|
|
998
|
+
_tag: "Unsupported",
|
|
999
|
+
name: key,
|
|
1000
|
+
raw: text,
|
|
1001
|
+
reason: "empty npm: alias"
|
|
1002
|
+
};
|
|
1003
|
+
const at = body.lastIndexOf("@");
|
|
1004
|
+
const target = at > 0 ? body.slice(0, at) : body;
|
|
1005
|
+
const rangeText = at > 0 ? body.slice(at + 1) : "";
|
|
1006
|
+
if (target.length === 0) return {
|
|
1007
|
+
_tag: "Unsupported",
|
|
1008
|
+
name: key,
|
|
1009
|
+
raw: text,
|
|
1010
|
+
reason: "empty npm: alias target"
|
|
1011
|
+
};
|
|
1012
|
+
const inner = parseDependencyTarget(target, rangeText);
|
|
1013
|
+
if (inner._tag === "Unsupported") return {
|
|
1014
|
+
...inner,
|
|
1015
|
+
name: key,
|
|
1016
|
+
raw: text
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
_tag: "Registry",
|
|
1020
|
+
name: target,
|
|
1021
|
+
selector: inner.selector,
|
|
1022
|
+
aliasOf: key
|
|
1023
|
+
};
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
//#endregion
|
|
1027
|
+
//#region src/Resolve.ts
|
|
1028
|
+
var Resolve_exports = /* @__PURE__ */ __export({
|
|
1029
|
+
edgesOf: () => edgesOf,
|
|
1030
|
+
indexPackages: () => indexPackages,
|
|
1031
|
+
packageKey: () => packageKey,
|
|
1032
|
+
resolve: () => resolve,
|
|
1033
|
+
selectVersions: () => selectVersions
|
|
1034
|
+
});
|
|
1035
|
+
const packageKey = (name, version) => `${name}@${version}`;
|
|
1036
|
+
/** Index of a resolution by `name@version`, for lookups during bundling. */
|
|
1037
|
+
const indexPackages = (resolution) => new Map(resolution.packages.map((p) => [packageKey(p.name, p.version), p]));
|
|
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
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
const sortVersions = (versions) => [...versions].sort((a, b) => semver.valid(a) && semver.valid(b) ? semver.compare(a, b) : a.localeCompare(b));
|
|
1068
|
+
const makeWalkState = () => ({
|
|
1069
|
+
resolved: /* @__PURE__ */ new Map(),
|
|
1070
|
+
edges: /* @__PURE__ */ new Map(),
|
|
1071
|
+
packuments: /* @__PURE__ */ new Map(),
|
|
1072
|
+
warnings: [],
|
|
1073
|
+
warningKeys: /* @__PURE__ */ new Set()
|
|
1074
|
+
});
|
|
1075
|
+
const addWarning = (state, warning) => {
|
|
1076
|
+
const key = `${warning.from ?? ""}|${warning.message}`;
|
|
1077
|
+
if (state.warningKeys.has(key)) return;
|
|
1078
|
+
state.warningKeys.add(key);
|
|
1079
|
+
state.warnings.push(warning);
|
|
1080
|
+
};
|
|
1081
|
+
/**
|
|
1082
|
+
* Fetches a packument, reusing anything already seen in this run.
|
|
1083
|
+
*
|
|
1084
|
+
* Two fibers in the same wave can race here and both fetch. That is one
|
|
1085
|
+
* duplicated GET at worst, and de-racing it would cost more complexity than it
|
|
1086
|
+
* saves.
|
|
1087
|
+
*/
|
|
1088
|
+
const getPackument = (state, name) => Effect.gen(function* () {
|
|
1089
|
+
const cached = state.packuments.get(name);
|
|
1090
|
+
if (cached !== void 0) return cached;
|
|
1091
|
+
const packument = yield* (yield* Registry).packument(name);
|
|
1092
|
+
state.packuments.set(name, packument);
|
|
1093
|
+
return packument;
|
|
1094
|
+
});
|
|
1095
|
+
/** Expands one resolved manifest into the edges that follow from it. */
|
|
1096
|
+
const edgesOf = (manifest, options) => {
|
|
1097
|
+
const from = packageKey(manifest.name, manifest.version);
|
|
1098
|
+
const tasks = [];
|
|
1099
|
+
const warnings = [];
|
|
1100
|
+
const add = (entries, kind, tolerateFailure, skip) => {
|
|
1101
|
+
if (entries === void 0) return;
|
|
1102
|
+
for (const [name, raw] of Object.entries(entries)) {
|
|
1103
|
+
if (skip?.(name)) continue;
|
|
1104
|
+
const target = parseDependencyTarget(name, raw);
|
|
1105
|
+
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
|
+
});
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
tasks.push({
|
|
1113
|
+
name: target.name,
|
|
1114
|
+
selector: target.selector,
|
|
1115
|
+
reason: {
|
|
1116
|
+
_tag: "Edge",
|
|
1117
|
+
from,
|
|
1118
|
+
kind
|
|
1119
|
+
},
|
|
1120
|
+
tolerateFailure
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
add(manifest.dependencies, "prod", false);
|
|
1125
|
+
if (options.scope.optional) add(manifest.optionalDependencies, "optional", true);
|
|
1126
|
+
if (options.scope.peer) {
|
|
1127
|
+
const meta = manifest.peerDependenciesMeta ?? {};
|
|
1128
|
+
add(manifest.peerDependencies, "peer", false, (name) => meta[name]?.optional === true);
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
tasks,
|
|
1132
|
+
warnings
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
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
|
+
/**
|
|
1146
|
+
* Resolves everything reachable from `seeds`, returning the closure.
|
|
1147
|
+
*
|
|
1148
|
+
* `state` is mutated with newly resolved packages and warnings; the returned
|
|
1149
|
+
* array is scoped to *this* walk, which is what makes per-root-version
|
|
1150
|
+
* closures possible.
|
|
1151
|
+
*/
|
|
1152
|
+
const walk = (state, seeds, options) => Effect.gen(function* () {
|
|
1153
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
1154
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
1155
|
+
let frontier = seeds;
|
|
1156
|
+
while (frontier.length > 0) {
|
|
1157
|
+
const wave = [];
|
|
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;
|
|
1203
|
+
}
|
|
1204
|
+
return [...discovered];
|
|
1205
|
+
});
|
|
1206
|
+
const hasReason = (reasons, candidate) => reasons.some((reason) => {
|
|
1207
|
+
if (reason._tag === "Root" && candidate._tag === "Root") return reason.spec === candidate.spec;
|
|
1208
|
+
if (reason._tag === "Edge" && candidate._tag === "Edge") return reason.from === candidate.from && reason.kind === candidate.kind;
|
|
1209
|
+
return false;
|
|
1210
|
+
});
|
|
1211
|
+
/**
|
|
1212
|
+
* Resolves one task to its manifest(s).
|
|
1213
|
+
*
|
|
1214
|
+
* Returns `null` when an optional edge could not be resolved — the caller
|
|
1215
|
+
* treats that as "skip quietly", which is what npm does for
|
|
1216
|
+
* `optionalDependencies`.
|
|
1217
|
+
*/
|
|
1218
|
+
const resolveTask = (state, task, options) => Effect.gen(function* () {
|
|
1219
|
+
const packument = yield* getPackument(state, task.name);
|
|
1220
|
+
const versions = yield* Effect.try({
|
|
1221
|
+
try: () => selectVersions(packument, task.selector, {
|
|
1222
|
+
all: false,
|
|
1223
|
+
includePrerelease: options.includePrerelease
|
|
1224
|
+
}),
|
|
1225
|
+
catch: (error) => error
|
|
1226
|
+
});
|
|
1227
|
+
const out = [];
|
|
1228
|
+
for (const version of versions) {
|
|
1229
|
+
const manifest = packument.versions[version];
|
|
1230
|
+
if (manifest === void 0) continue;
|
|
1231
|
+
if (task.reason._tag === "Edge" && task.reason.kind === "optional" && !isIncluded(manifest, options.scope.platforms)) continue;
|
|
1232
|
+
out.push({
|
|
1233
|
+
manifest,
|
|
1234
|
+
reason: task.reason
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
return out;
|
|
1238
|
+
}).pipe(Effect.catch((error) => {
|
|
1239
|
+
if (!task.tolerateFailure) return Effect.fail(error);
|
|
1240
|
+
return Effect.sync(() => {
|
|
1241
|
+
addWarning(state, { message: `Optional dependency ${task.name}@${formatSelector(task.selector)} could not be resolved and was skipped: ${error.message}` });
|
|
1242
|
+
return null;
|
|
1243
|
+
});
|
|
1244
|
+
}));
|
|
1245
|
+
/**
|
|
1246
|
+
* Resolves every requested spec.
|
|
1247
|
+
*
|
|
1248
|
+
* Each selected root *version* is walked separately so `per-spec` layout has an
|
|
1249
|
+
* exact closure per tarball, while all walks share one cache, so a package
|
|
1250
|
+
* reached from twenty roots is fetched once.
|
|
1251
|
+
*/
|
|
1252
|
+
const resolve = (specs, options) => Effect.gen(function* () {
|
|
1253
|
+
yield* emit({
|
|
1254
|
+
_tag: "PhaseStarted",
|
|
1255
|
+
phase: "resolve",
|
|
1256
|
+
total: specs.length
|
|
1257
|
+
});
|
|
1258
|
+
const state = makeWalkState();
|
|
1259
|
+
const roots = [];
|
|
1260
|
+
for (const spec of specs) {
|
|
1261
|
+
const packument = yield* getPackument(state, spec.name);
|
|
1262
|
+
const versions = yield* Effect.try({
|
|
1263
|
+
try: () => selectVersions(packument, spec.selector, {
|
|
1264
|
+
all: options.allVersions,
|
|
1265
|
+
maxVersions: options.maxVersions,
|
|
1266
|
+
includePrerelease: options.includePrerelease
|
|
1267
|
+
}),
|
|
1268
|
+
catch: (error) => error
|
|
1269
|
+
});
|
|
1270
|
+
const closures = /* @__PURE__ */ new Map();
|
|
1271
|
+
const union = /* @__PURE__ */ new Set();
|
|
1272
|
+
for (const version of versions) {
|
|
1273
|
+
const closure = yield* walk(state, [{
|
|
1274
|
+
name: spec.name,
|
|
1275
|
+
selector: {
|
|
1276
|
+
_tag: "Exact",
|
|
1277
|
+
version
|
|
1278
|
+
},
|
|
1279
|
+
reason: {
|
|
1280
|
+
_tag: "Root",
|
|
1281
|
+
spec: formatSpec(spec)
|
|
1282
|
+
},
|
|
1283
|
+
tolerateFailure: false
|
|
1284
|
+
}], options);
|
|
1285
|
+
closures.set(version, closure);
|
|
1286
|
+
for (const key of closure) union.add(key);
|
|
1287
|
+
}
|
|
1288
|
+
roots.push({
|
|
1289
|
+
spec,
|
|
1290
|
+
versions,
|
|
1291
|
+
closures,
|
|
1292
|
+
closure: [...union]
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
yield* emit({
|
|
1296
|
+
_tag: "PhaseCompleted",
|
|
1297
|
+
phase: "resolve"
|
|
1298
|
+
});
|
|
1299
|
+
return {
|
|
1300
|
+
roots,
|
|
1301
|
+
packages: [...state.resolved.values()].sort(compareResolved),
|
|
1302
|
+
warnings: state.warnings
|
|
1303
|
+
};
|
|
1304
|
+
});
|
|
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
|
+
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/Bundle.ts
|
|
1312
|
+
var Bundle_exports = /* @__PURE__ */ __export({
|
|
1313
|
+
bundle: () => bundle,
|
|
1314
|
+
bundleResolved: () => bundleResolved,
|
|
1315
|
+
plan: () => plan,
|
|
1316
|
+
plannedOutputs: () => plannedOutputs,
|
|
1317
|
+
summarize: () => summarize,
|
|
1318
|
+
withLayout: () => withLayout
|
|
1319
|
+
});
|
|
1320
|
+
/** Computes the plan summary for a resolution. */
|
|
1321
|
+
const summarize = (resolution) => {
|
|
1322
|
+
let perSpecArchives = 0;
|
|
1323
|
+
let perSpecEntries = 0;
|
|
1324
|
+
for (const root of resolution.roots) for (const version of root.versions) {
|
|
1325
|
+
perSpecArchives += 1;
|
|
1326
|
+
perSpecEntries += (root.closures.get(version) ?? []).length;
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
uniquePackages: resolution.packages.length,
|
|
1330
|
+
perSpecArchives,
|
|
1331
|
+
perSpecEntries
|
|
1332
|
+
};
|
|
1333
|
+
};
|
|
1334
|
+
/** Replaces the layout on a set of options. */
|
|
1335
|
+
const withLayout = (options, layout) => ({
|
|
1336
|
+
...options,
|
|
1337
|
+
layout
|
|
1338
|
+
});
|
|
1339
|
+
/**
|
|
1340
|
+
* Checks the registry is reachable, then resolves every spec.
|
|
1341
|
+
*
|
|
1342
|
+
* Split out from `bundle` so a caller can inspect the plan — how many packages,
|
|
1343
|
+
* how many archives — and act on it before any bytes move. The CLI uses this to
|
|
1344
|
+
* offer a different layout when a run turns out to be much larger than
|
|
1345
|
+
* expected.
|
|
1346
|
+
*/
|
|
1347
|
+
const plan = (specs, options) => Effect.gen(function* () {
|
|
1348
|
+
const registry = yield* Registry;
|
|
1349
|
+
yield* emit({
|
|
1350
|
+
_tag: "PhaseStarted",
|
|
1351
|
+
phase: "preflight"
|
|
1352
|
+
});
|
|
1353
|
+
yield* registry.preflight;
|
|
1354
|
+
yield* emit({
|
|
1355
|
+
_tag: "PhaseCompleted",
|
|
1356
|
+
phase: "preflight"
|
|
1357
|
+
});
|
|
1358
|
+
return yield* resolve(specs, options);
|
|
1359
|
+
});
|
|
1360
|
+
/**
|
|
1361
|
+
* Resolves, downloads and packages a set of specs.
|
|
1362
|
+
*
|
|
1363
|
+
* Under `dryRun` this stops after resolution and reports the plan — useful for
|
|
1364
|
+
* checking what a command *would* pull before committing to a multi-gigabyte
|
|
1365
|
+
* download over a slow VPN.
|
|
1366
|
+
*/
|
|
1367
|
+
const bundle = (specs, options) => Effect.gen(function* () {
|
|
1368
|
+
return yield* bundleResolved(yield* plan(specs, options), options);
|
|
1369
|
+
});
|
|
1370
|
+
/**
|
|
1371
|
+
* Downloads and packages an already-computed resolution.
|
|
1372
|
+
*
|
|
1373
|
+
* Everything from here on touches the disk, and all of it happens inside
|
|
1374
|
+
* `Effect.scoped` — so the staging tree's lifetime is exactly this call,
|
|
1375
|
+
* including when it fails partway through or the user hits Ctrl-C.
|
|
1376
|
+
*/
|
|
1377
|
+
const bundleResolved = (resolution, options) => Effect.gen(function* () {
|
|
1378
|
+
if (options.dryRun) return {
|
|
1379
|
+
resolution,
|
|
1380
|
+
artifacts: [],
|
|
1381
|
+
downloadedBytes: 0,
|
|
1382
|
+
unverified: [],
|
|
1383
|
+
dryRun: true
|
|
1384
|
+
};
|
|
1385
|
+
return yield* Effect.scoped(runBundle(resolution, options));
|
|
1386
|
+
});
|
|
1387
|
+
const runBundle = (resolution, options) => Effect.gen(function* () {
|
|
1388
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1389
|
+
const path = yield* Path.Path;
|
|
1390
|
+
const registry = yield* Registry;
|
|
1391
|
+
yield* prepareOutputDir(resolution, options);
|
|
1392
|
+
const staging = yield* fs.makeTempDirectoryScoped({ prefix: "packall-" });
|
|
1393
|
+
const packagesDir = path.join(staging, "packages");
|
|
1394
|
+
yield* fs.makeDirectory(packagesDir, { recursive: true });
|
|
1395
|
+
const report = yield* downloadAll(resolution.packages, packagesDir, {
|
|
1396
|
+
concurrency: options.concurrency,
|
|
1397
|
+
verifyIntegrity: options.verifyIntegrity
|
|
1398
|
+
});
|
|
1399
|
+
yield* emit({
|
|
1400
|
+
_tag: "PhaseStarted",
|
|
1401
|
+
phase: "archive"
|
|
1402
|
+
});
|
|
1403
|
+
const registryInfo = {
|
|
1404
|
+
kind: registry.kind,
|
|
1405
|
+
url: registry.registryFor(resolution.roots[0]?.spec.name ?? "")
|
|
1406
|
+
};
|
|
1407
|
+
const index = indexPackages(resolution);
|
|
1408
|
+
const artifacts = options.layout === "single" ? yield* emitSingle({
|
|
1409
|
+
resolution,
|
|
1410
|
+
options,
|
|
1411
|
+
staging,
|
|
1412
|
+
packagesDir,
|
|
1413
|
+
report,
|
|
1414
|
+
registryInfo
|
|
1415
|
+
}) : options.layout === "dir" ? yield* emitDirectory({
|
|
1416
|
+
resolution,
|
|
1417
|
+
options,
|
|
1418
|
+
packagesDir,
|
|
1419
|
+
report,
|
|
1420
|
+
registryInfo
|
|
1421
|
+
}) : yield* emitPerSpec({
|
|
1422
|
+
resolution,
|
|
1423
|
+
options,
|
|
1424
|
+
staging,
|
|
1425
|
+
packagesDir,
|
|
1426
|
+
report,
|
|
1427
|
+
registryInfo,
|
|
1428
|
+
index
|
|
1429
|
+
});
|
|
1430
|
+
yield* emit({
|
|
1431
|
+
_tag: "PhaseCompleted",
|
|
1432
|
+
phase: "archive"
|
|
1433
|
+
});
|
|
1434
|
+
yield* emit({
|
|
1435
|
+
_tag: "PhaseStarted",
|
|
1436
|
+
phase: "done"
|
|
1437
|
+
});
|
|
1438
|
+
return {
|
|
1439
|
+
resolution,
|
|
1440
|
+
artifacts,
|
|
1441
|
+
downloadedBytes: report.totalBytes,
|
|
1442
|
+
unverified: report.unverified,
|
|
1443
|
+
dryRun: false
|
|
1444
|
+
};
|
|
1445
|
+
});
|
|
1446
|
+
/** One tarball containing the deduplicated union of every closure. */
|
|
1447
|
+
const emitSingle = (input) => Effect.gen(function* () {
|
|
1448
|
+
const path = yield* Path.Path;
|
|
1449
|
+
const archiveName = singleArchiveName(input.options.archiveName ?? "bundle");
|
|
1450
|
+
if (input.options.skipExisting?.has(archiveName) === true) return [];
|
|
1451
|
+
yield* writeBundleMetadata({
|
|
1452
|
+
dir: input.packagesDir,
|
|
1453
|
+
resolution: input.resolution,
|
|
1454
|
+
included: input.resolution.packages,
|
|
1455
|
+
options: input.options,
|
|
1456
|
+
registryInfo: input.registryInfo,
|
|
1457
|
+
sizes: input.report.sizes
|
|
1458
|
+
});
|
|
1459
|
+
const outPath = path.join(input.options.outDir, archiveName);
|
|
1460
|
+
const result = yield* createArchive({
|
|
1461
|
+
cwd: input.packagesDir,
|
|
1462
|
+
entries: yield* topLevelEntries(input.packagesDir),
|
|
1463
|
+
outPath
|
|
1464
|
+
});
|
|
1465
|
+
return [{
|
|
1466
|
+
kind: "archive",
|
|
1467
|
+
path: result.path,
|
|
1468
|
+
bytes: result.bytes,
|
|
1469
|
+
packageCount: input.resolution.packages.length
|
|
1470
|
+
}];
|
|
1471
|
+
});
|
|
1472
|
+
/** The raw npm-layout tree, no archive. */
|
|
1473
|
+
const emitDirectory = (input) => Effect.gen(function* () {
|
|
1474
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1475
|
+
yield* writeBundleMetadata({
|
|
1476
|
+
dir: input.packagesDir,
|
|
1477
|
+
resolution: input.resolution,
|
|
1478
|
+
included: input.resolution.packages,
|
|
1479
|
+
options: input.options,
|
|
1480
|
+
registryInfo: input.registryInfo,
|
|
1481
|
+
sizes: input.report.sizes
|
|
1482
|
+
});
|
|
1483
|
+
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
|
+
const bytes = [...input.report.sizes.values()].reduce((a, b) => a + b, 0);
|
|
1485
|
+
return [{
|
|
1486
|
+
kind: "directory",
|
|
1487
|
+
path: input.options.outDir,
|
|
1488
|
+
bytes,
|
|
1489
|
+
packageCount: input.resolution.packages.length
|
|
1490
|
+
}];
|
|
1491
|
+
});
|
|
1492
|
+
/**
|
|
1493
|
+
* One tarball per resolved root *version*.
|
|
1494
|
+
*
|
|
1495
|
+
* Under `--all-versions react@^18` this produces `react-18.0.0.tgz`,
|
|
1496
|
+
* `react-18.1.0.tgz` and so on, each independently importable — which is the
|
|
1497
|
+
* whole reason to prefer this layout.
|
|
1498
|
+
*/
|
|
1499
|
+
const emitPerSpec = (input) => Effect.gen(function* () {
|
|
1500
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1501
|
+
const path = yield* Path.Path;
|
|
1502
|
+
const artifacts = [];
|
|
1503
|
+
let counter = 0;
|
|
1504
|
+
for (const root of input.resolution.roots) for (const version of root.versions) {
|
|
1505
|
+
if (input.options.skipExisting?.has(perSpecArchiveName(root.spec.name, version)) === true) continue;
|
|
1506
|
+
const included = (root.closures.get(version) ?? []).map((key) => input.index.get(key)).filter((pkg) => pkg !== void 0);
|
|
1507
|
+
const rootDir = path.join(input.staging, "roots", String(counter++));
|
|
1508
|
+
yield* fs.makeDirectory(rootDir, { recursive: true });
|
|
1509
|
+
for (const pkg of included) {
|
|
1510
|
+
const parts = packagePath(pkg.name, pkg.version).split("/");
|
|
1511
|
+
const source = path.join(input.packagesDir, ...parts);
|
|
1512
|
+
const target = path.join(rootDir, ...parts);
|
|
1513
|
+
yield* fs.makeDirectory(path.dirname(target), { recursive: true });
|
|
1514
|
+
yield* linkOrCopy(source, target);
|
|
1515
|
+
}
|
|
1516
|
+
yield* writeBundleMetadata({
|
|
1517
|
+
dir: rootDir,
|
|
1518
|
+
resolution: {
|
|
1519
|
+
...input.resolution,
|
|
1520
|
+
roots: [{
|
|
1521
|
+
...root,
|
|
1522
|
+
versions: [version]
|
|
1523
|
+
}]
|
|
1524
|
+
},
|
|
1525
|
+
included,
|
|
1526
|
+
options: input.options,
|
|
1527
|
+
registryInfo: input.registryInfo,
|
|
1528
|
+
sizes: input.report.sizes
|
|
1529
|
+
});
|
|
1530
|
+
const outPath = path.join(input.options.outDir, perSpecArchiveName(root.spec.name, version));
|
|
1531
|
+
const result = yield* createArchive({
|
|
1532
|
+
cwd: rootDir,
|
|
1533
|
+
entries: yield* topLevelEntries(rootDir),
|
|
1534
|
+
outPath
|
|
1535
|
+
});
|
|
1536
|
+
artifacts.push({
|
|
1537
|
+
kind: "archive",
|
|
1538
|
+
path: result.path,
|
|
1539
|
+
bytes: result.bytes,
|
|
1540
|
+
packageCount: included.length,
|
|
1541
|
+
spec: formatSpec(root.spec),
|
|
1542
|
+
version
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
return artifacts;
|
|
1546
|
+
});
|
|
1547
|
+
/**
|
|
1548
|
+
* Hard-links a file, falling back to a copy.
|
|
1549
|
+
*
|
|
1550
|
+
* Hard links make `per-spec` layout nearly free in the staging tree. They fail
|
|
1551
|
+
* across devices and on some Windows configurations, so the copy fallback is
|
|
1552
|
+
* not optional.
|
|
1553
|
+
*/
|
|
1554
|
+
const linkOrCopy = (source, target) => Effect.gen(function* () {
|
|
1555
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1556
|
+
yield* fs.link(source, target).pipe(Effect.catch(() => fs.copyFile(source, target)));
|
|
1557
|
+
});
|
|
1558
|
+
/** Writes the manifest and import guide into a staging directory. */
|
|
1559
|
+
const writeBundleMetadata = (input) => Effect.gen(function* () {
|
|
1560
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1561
|
+
const path = yield* Path.Path;
|
|
1562
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
1563
|
+
const manifest = buildManifest({
|
|
1564
|
+
resolution: input.resolution,
|
|
1565
|
+
included: input.included,
|
|
1566
|
+
options: input.options,
|
|
1567
|
+
registry: input.registryInfo,
|
|
1568
|
+
toolVersion: input.options.toolVersion,
|
|
1569
|
+
sizes: input.sizes,
|
|
1570
|
+
createdAt
|
|
1571
|
+
});
|
|
1572
|
+
yield* fs.writeFileString(path.join(input.dir, MANIFEST_FILE), serializeManifest(manifest));
|
|
1573
|
+
yield* fs.writeFileString(path.join(input.dir, README_FILE), importGuide({
|
|
1574
|
+
packageCount: input.included.length,
|
|
1575
|
+
createdAt: createdAt.toISOString(),
|
|
1576
|
+
toolVersion: input.options.toolVersion
|
|
1577
|
+
}));
|
|
1578
|
+
});
|
|
1579
|
+
/** Sorted top-level entries of a directory, used as the tar entry list. */
|
|
1580
|
+
const topLevelEntries = (dir) => Effect.gen(function* () {
|
|
1581
|
+
return [...yield* (yield* FileSystem.FileSystem).readDirectory(dir)].sort();
|
|
1582
|
+
});
|
|
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
|
+
/**
|
|
1593
|
+
* Fails if the run would overwrite something, unless `--force`.
|
|
1594
|
+
*
|
|
1595
|
+
* The guard is on the *files this run writes*, not on whether the directory has
|
|
1596
|
+
* anything in it. An output directory accumulating bundles is the normal way to
|
|
1597
|
+
* use this — bundling `esbuild` into an `out/` that already holds `tsdown` is
|
|
1598
|
+
* not a conflict, and refusing it made `--out` a single-use directory and the
|
|
1599
|
+
* default `.` unusable.
|
|
1600
|
+
*
|
|
1601
|
+
* Checked before anything is downloaded. Discovering the collision *after* a
|
|
1602
|
+
* twenty-minute download would be a uniquely irritating way to fail.
|
|
1603
|
+
*/
|
|
1604
|
+
const prepareOutputDir = (resolution, options) => Effect.gen(function* () {
|
|
1605
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1606
|
+
const path = yield* Path.Path;
|
|
1607
|
+
const outDir = options.outDir;
|
|
1608
|
+
if (!options.force) {
|
|
1609
|
+
const clashes = [];
|
|
1610
|
+
for (const planned of plannedOutputs(resolution, options)) {
|
|
1611
|
+
if (options.skipExisting?.has(planned.file) === true) continue;
|
|
1612
|
+
if (options.overwrite?.has(planned.file) === true) continue;
|
|
1613
|
+
if (yield* fs.exists(path.join(outDir, planned.file)).pipe(Effect.orElseSucceed(() => false))) clashes.push(planned.file);
|
|
1614
|
+
}
|
|
1615
|
+
if (clashes.length > 0) return yield* Effect.fail(new OutputError(outDir, `would overwrite ${clashes.length} existing file${clashes.length === 1 ? "" : "s"} (${clashes.slice(0, 3).join(", ")}${clashes.length > 3 ? ", …" : ""}). Pass --force true to replace ${clashes.length === 1 ? "it" : "them"}, or choose a different --out.`));
|
|
1616
|
+
}
|
|
1617
|
+
yield* fs.makeDirectory(outDir, { recursive: true }).pipe(Effect.mapError((cause) => new OutputError(outDir, "could not create output directory", { cause })));
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
//#endregion
|
|
1621
|
+
//#region src/InputFile.ts
|
|
1622
|
+
var InputFile_exports = /* @__PURE__ */ __export({
|
|
1623
|
+
defaultInputFileOptions: () => defaultInputFileOptions,
|
|
1624
|
+
parseInputFile: () => parseInputFile,
|
|
1625
|
+
readInputFile: () => readInputFile
|
|
1626
|
+
});
|
|
1627
|
+
const defaultInputFileOptions = { includeDev: true };
|
|
1628
|
+
/** Reads and parses an input file. */
|
|
1629
|
+
const readInputFile = (filePath, options = defaultInputFileOptions) => Effect.gen(function* () {
|
|
1630
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1631
|
+
if (!(yield* fs.exists(filePath).pipe(Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause }))))) return yield* Effect.fail(new InvalidInputFileError(filePath, "file does not exist"));
|
|
1632
|
+
const content = yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause })));
|
|
1633
|
+
return yield* Effect.try({
|
|
1634
|
+
try: () => parseInputFile(filePath, content, options),
|
|
1635
|
+
catch: (error) => error instanceof InvalidInputFileError ? error : new InvalidInputFileError(filePath, describe(error), { cause: error })
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
/**
|
|
1639
|
+
* Parses file content that has already been read.
|
|
1640
|
+
*
|
|
1641
|
+
* Split out from the IO so the whole of this logic is testable with plain
|
|
1642
|
+
* strings and no file system at all.
|
|
1643
|
+
*/
|
|
1644
|
+
const parseInputFile = (filePath, content, options = defaultInputFileOptions) => {
|
|
1645
|
+
const trimmed = content.trim();
|
|
1646
|
+
if (trimmed.length === 0) throw new InvalidInputFileError(filePath, "file is empty");
|
|
1647
|
+
return looksLikeJson(trimmed) ? fromPackageJson(filePath, trimmed, options) : fromList(filePath, content);
|
|
1648
|
+
};
|
|
1649
|
+
const looksLikeJson = (trimmed) => trimmed.startsWith("{");
|
|
1650
|
+
const fromPackageJson = (filePath, content, options) => {
|
|
1651
|
+
let parsed;
|
|
1652
|
+
try {
|
|
1653
|
+
parsed = JSON.parse(content);
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
throw new InvalidInputFileError(filePath, `not valid JSON — ${describe(error)}`, { cause: error });
|
|
1656
|
+
}
|
|
1657
|
+
const specs = [];
|
|
1658
|
+
const warnings = [];
|
|
1659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1660
|
+
const collect = (entries, label, skip) => {
|
|
1661
|
+
if (entries === void 0) return;
|
|
1662
|
+
for (const [name, raw] of Object.entries(entries)) {
|
|
1663
|
+
if (skip?.(name)) continue;
|
|
1664
|
+
const target = parseDependencyTarget(name, raw);
|
|
1665
|
+
if (target._tag === "Unsupported") {
|
|
1666
|
+
warnings.push(`${label}: skipped ${name}@${target.raw} (${target.reason}) — not fetchable from a registry`);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
const key = `${target.name}|${JSON.stringify(target.selector)}`;
|
|
1670
|
+
if (seen.has(key)) continue;
|
|
1671
|
+
seen.add(key);
|
|
1672
|
+
specs.push({
|
|
1673
|
+
name: target.name,
|
|
1674
|
+
selector: target.selector,
|
|
1675
|
+
raw: `${target.name}@${raw}`
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
collect(parsed.dependencies, "dependencies");
|
|
1680
|
+
collect(parsed.optionalDependencies, "optionalDependencies");
|
|
1681
|
+
if (options.includeDev) collect(parsed.devDependencies, "devDependencies");
|
|
1682
|
+
const meta = parsed.peerDependenciesMeta ?? {};
|
|
1683
|
+
collect(parsed.peerDependencies, "peerDependencies", (name) => meta[name]?.optional === true);
|
|
1684
|
+
if (specs.length === 0) throw new InvalidInputFileError(filePath, options.includeDev ? "package.json declares no dependencies to bundle" : "package.json declares no non-dev dependencies to bundle (drop --prod to include devDependencies)");
|
|
1685
|
+
return {
|
|
1686
|
+
kind: "package.json",
|
|
1687
|
+
specs,
|
|
1688
|
+
warnings
|
|
1689
|
+
};
|
|
1690
|
+
};
|
|
1691
|
+
const fromList = (filePath, content) => {
|
|
1692
|
+
const lines = [];
|
|
1693
|
+
const rawLines = content.split(/\r?\n/);
|
|
1694
|
+
for (const line of rawLines) {
|
|
1695
|
+
const withoutComment = stripComment(line).trim();
|
|
1696
|
+
if (withoutComment.length === 0) continue;
|
|
1697
|
+
lines.push(withoutComment);
|
|
1698
|
+
}
|
|
1699
|
+
if (lines.length === 0) throw new InvalidInputFileError(filePath, "no package specs found (only blank lines and comments)");
|
|
1700
|
+
const { specs, errors } = parseSpecs(lines);
|
|
1701
|
+
if (errors.length > 0) throw new InvalidInputFileError(filePath, formatSpecErrors(errors, rawLines));
|
|
1702
|
+
return {
|
|
1703
|
+
kind: "list",
|
|
1704
|
+
specs,
|
|
1705
|
+
warnings: []
|
|
1706
|
+
};
|
|
1707
|
+
};
|
|
1708
|
+
/**
|
|
1709
|
+
* Strips `#` and `//` comments.
|
|
1710
|
+
*
|
|
1711
|
+
* `//` only counts when it is not part of a URL, so a future line containing
|
|
1712
|
+
* `https://…` does not get silently truncated.
|
|
1713
|
+
*/
|
|
1714
|
+
const stripComment = (line) => {
|
|
1715
|
+
const hash = line.indexOf("#");
|
|
1716
|
+
let result = hash === -1 ? line : line.slice(0, hash);
|
|
1717
|
+
const slashes = result.indexOf("//");
|
|
1718
|
+
if (slashes > 0 && result[slashes - 1] !== ":") result = result.slice(0, slashes);
|
|
1719
|
+
else if (slashes === 0) result = "";
|
|
1720
|
+
return result;
|
|
1721
|
+
};
|
|
1722
|
+
const formatSpecErrors = (errors, rawLines) => {
|
|
1723
|
+
const details = errors.map((error) => {
|
|
1724
|
+
const index = rawLines.findIndex((line) => stripComment(line).trim() === error.spec);
|
|
1725
|
+
const where = index === -1 ? "" : ` (line ${index + 1})`;
|
|
1726
|
+
return ` ${error.spec}${where}: ${error.reason}`;
|
|
1727
|
+
});
|
|
1728
|
+
return `${errors.length} invalid spec(s):\n${details.join("\n")}`;
|
|
1729
|
+
};
|
|
1730
|
+
const describe = (error) => error instanceof Error ? error.message : String(error);
|
|
1731
|
+
|
|
1732
|
+
//#endregion
|
|
1733
|
+
//#region src/Options.ts
|
|
1734
|
+
var Options_exports = /* @__PURE__ */ __export({
|
|
1735
|
+
defaultBundleOptions: () => defaultBundleOptions,
|
|
1736
|
+
defaultResolveOptions: () => defaultResolveOptions,
|
|
1737
|
+
defaultScope: () => defaultScope,
|
|
1738
|
+
layouts: () => layouts
|
|
1739
|
+
});
|
|
1740
|
+
const layouts = [
|
|
1741
|
+
"per-spec",
|
|
1742
|
+
"single",
|
|
1743
|
+
"dir"
|
|
1744
|
+
];
|
|
1745
|
+
const defaultScope = {
|
|
1746
|
+
optional: true,
|
|
1747
|
+
peer: true,
|
|
1748
|
+
platforms: allPlatforms
|
|
1749
|
+
};
|
|
1750
|
+
const defaultResolveOptions = {
|
|
1751
|
+
scope: defaultScope,
|
|
1752
|
+
allVersions: false,
|
|
1753
|
+
includePrerelease: false,
|
|
1754
|
+
concurrency: 10
|
|
1755
|
+
};
|
|
1756
|
+
const defaultBundleOptions = {
|
|
1757
|
+
...defaultResolveOptions,
|
|
1758
|
+
layout: "per-spec",
|
|
1759
|
+
dryRun: false,
|
|
1760
|
+
verifyIntegrity: true,
|
|
1761
|
+
force: false
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
//#endregion
|
|
1765
|
+
export { Archive_exports as Archive, ArchiveError, AuthenticationError, Bundle_exports as Bundle, DependencyRange_exports as DependencyRange, Download_exports as Download, InputFile_exports as InputFile, Integrity_exports as Integrity, IntegrityError, InvalidInputFileError, InvalidSpecError, Layout_exports as Layout, Manifest_exports as Manifest, NoMatchingVersionsError, Options_exports as Options, OutputError, PackageNotFoundError, Platform_exports as Platform, Progress_exports as Progress, Progress as ProgressTag, Registry, RegistryResponseError, RegistryUnreachableError, Resolve_exports as Resolve, Spec_exports as Spec, VersionNotFoundError, allPlatforms, bundle, bundleResolved, currentPlatform, defaultBundleOptions, defaultResolveOptions, defaultScope, layouts, parsePlatformTarget, parseSpec, parseSpecs, plan, plannedOutputs, platformTargets, resolve, summarize, withLayout };
|
|
1766
|
+
//# sourceMappingURL=index.js.map
|