@ttsc/playground 0.19.2 → 0.20.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/README.md +6 -0
- package/lib/src/npm/collectExternalPackageNames.js +436 -73
- package/lib/src/npm/collectExternalPackageNames.js.map +1 -1
- package/lib/src/npm/installPlaygroundDependencies.d.ts +5 -3
- package/lib/src/npm/installPlaygroundDependencies.js +230 -27
- package/lib/src/npm/installPlaygroundDependencies.js.map +1 -1
- package/lib/src/npm/internal/npmRegistry.d.ts +13 -2
- package/lib/src/npm/internal/npmRegistry.js +73 -50
- package/lib/src/npm/internal/npmRegistry.js.map +1 -1
- package/lib/src/npm/packageNameFromSpecifier.js +17 -4
- package/lib/src/npm/packageNameFromSpecifier.js.map +1 -1
- package/lib/src/react/PlaygroundShell.js +68 -61
- package/lib/src/react/PlaygroundShell.js.map +1 -1
- package/lib/src/react/createCompilerClient.d.ts +3 -3
- package/lib/src/react/createCompilerClient.js +32 -54
- package/lib/src/react/createCompilerClient.js.map +1 -1
- package/lib/src/sandbox/createSandboxRequire.js +117 -54
- package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
- package/lib/src/structures/IPlaygroundDependencyInstallOptions.d.ts +13 -1
- package/lib/src/structures/IPlaygroundDependencyInstallResult.d.ts +4 -0
- package/lib/src/structures/IPlaygroundDependencyPackage.d.ts +2 -0
- package/lib/src/structures/IPlaygroundDependencyRequest.d.ts +9 -0
- package/lib/src/structures/IPlaygroundDependencyRequest.js +3 -0
- package/lib/src/structures/IPlaygroundDependencyRequest.js.map +1 -0
- package/lib/src/structures/IPlaygroundInstalledDependency.d.ts +12 -0
- package/lib/src/structures/IPlaygroundInstalledDependency.js +3 -0
- package/lib/src/structures/IPlaygroundInstalledDependency.js.map +1 -0
- package/lib/src/structures/index.d.ts +2 -0
- package/lib/src/structures/index.js +2 -0
- package/lib/src/structures/index.js.map +1 -1
- package/package.json +6 -4
- package/src/npm/collectExternalPackageNames.ts +503 -68
- package/src/npm/installPlaygroundDependencies.ts +288 -29
- package/src/npm/internal/npmRegistry.ts +99 -38
- package/src/npm/packageNameFromSpecifier.ts +16 -3
- package/src/react/PlaygroundShell.tsx +86 -72
- package/src/react/createCompilerClient.ts +37 -51
- package/src/sandbox/createSandboxRequire.ts +140 -49
- package/src/structures/IPlaygroundDependencyInstallOptions.ts +13 -1
- package/src/structures/IPlaygroundDependencyInstallResult.ts +4 -0
- package/src/structures/IPlaygroundDependencyPackage.ts +2 -0
- package/src/structures/IPlaygroundDependencyRequest.ts +9 -0
- package/src/structures/IPlaygroundInstalledDependency.ts +13 -0
- package/src/structures/index.ts +2 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { IPlaygroundDependencyInstallOptions } from "../structures/IPlaygroundDependencyInstallOptions";
|
|
2
2
|
import type { IPlaygroundDependencyInstallResult } from "../structures/IPlaygroundDependencyInstallResult";
|
|
3
3
|
import type { IPlaygroundDependencyProgressPhase } from "../structures/IPlaygroundDependencyProgressPhase";
|
|
4
|
+
import type { IPlaygroundInstalledDependency } from "../structures/IPlaygroundInstalledDependency";
|
|
4
5
|
import { BUILT_IN_PLAYGROUND_PACKAGES } from "./BUILT_IN_PLAYGROUND_PACKAGES";
|
|
5
6
|
import {
|
|
6
7
|
DECLARATION_FILE_REGEXP,
|
|
7
8
|
type IQueueItem,
|
|
9
|
+
type IVersionRequest,
|
|
8
10
|
downloadTarball,
|
|
9
11
|
enqueuePackageDependencies,
|
|
10
12
|
fetchNpmMetadata,
|
|
@@ -23,9 +25,11 @@ const DEFAULT_MAX_PACKAGES = 48;
|
|
|
23
25
|
* extra-libs registry, and the in-page execute sandbox `require`.
|
|
24
26
|
*
|
|
25
27
|
* Transitive dependencies are followed via the resolved `package.json`'s
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* required, optional, and peer dependency fields. Passing a prior call's
|
|
29
|
+
* `resolvedDependencies` validates new edges against the exact mounted graph
|
|
30
|
+
* and reuses compatible packages without downloading their tarballs again. The
|
|
31
|
+
* walk is bounded by `maxPackages` to keep a single keystroke from exhausting
|
|
32
|
+
* the tab's network/memory budget.
|
|
29
33
|
*/
|
|
30
34
|
export async function installPlaygroundDependencies(
|
|
31
35
|
packageNames: Iterable<string>,
|
|
@@ -40,12 +44,40 @@ export async function installPlaygroundDependencies(
|
|
|
40
44
|
const ignored = new Set(
|
|
41
45
|
options.ignoredPackages ?? BUILT_IN_PLAYGROUND_PACKAGES,
|
|
42
46
|
);
|
|
43
|
-
const
|
|
47
|
+
const installedNames = new Set(options.installedPackages ?? []);
|
|
48
|
+
const installedDependencies = new Map<
|
|
49
|
+
string,
|
|
50
|
+
IPlaygroundInstalledDependency
|
|
51
|
+
>();
|
|
52
|
+
for (const dependency of options.installedDependencies ?? []) {
|
|
53
|
+
const previous = installedDependencies.get(dependency.name);
|
|
54
|
+
if (
|
|
55
|
+
previous !== undefined &&
|
|
56
|
+
(previous.registryName !== dependency.registryName ||
|
|
57
|
+
previous.version !== dependency.version)
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Conflicting mounted identities for ${dependency.name}: ${previous.registryName}@${previous.version} and ${dependency.registryName}@${dependency.version}.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (previous !== undefined) {
|
|
64
|
+
previous.requests.push(
|
|
65
|
+
...dependency.requests.map((request) => ({ ...request })),
|
|
66
|
+
);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
installedDependencies.set(dependency.name, {
|
|
70
|
+
...dependency,
|
|
71
|
+
requests: dependency.requests.map((request) => ({ ...request })),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
44
74
|
const maxPackages = options.maxPackages ?? DEFAULT_MAX_PACKAGES;
|
|
45
75
|
const queue: IQueueItem[] = [];
|
|
46
|
-
const queued = new
|
|
47
|
-
const done = new
|
|
76
|
+
const queued = new Map<string, IQueueItem>();
|
|
77
|
+
const done = new Map<string, string>();
|
|
78
|
+
const metadataByName = new Map<string, Parameters<typeof selectVersion>[0]>();
|
|
48
79
|
const result: IPlaygroundDependencyInstallResult = {
|
|
80
|
+
resolvedDependencies: [],
|
|
49
81
|
packages: [],
|
|
50
82
|
compilerFiles: {},
|
|
51
83
|
editorLibs: {},
|
|
@@ -53,11 +85,77 @@ export async function installPlaygroundDependencies(
|
|
|
53
85
|
};
|
|
54
86
|
|
|
55
87
|
const enqueue = (item: IQueueItem): void => {
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
queued.
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
const normalized = normalizeRegistryRequest(item);
|
|
89
|
+
if (ignored.has(normalized.name)) return;
|
|
90
|
+
const known = queued.get(normalized.name);
|
|
91
|
+
if (known) {
|
|
92
|
+
if (known.registryName !== normalized.registryName) {
|
|
93
|
+
if (normalized.optional) return;
|
|
94
|
+
const completed = done.get(normalized.name);
|
|
95
|
+
if (known.optional && (completed === undefined || completed === "")) {
|
|
96
|
+
known.registryName = normalized.registryName;
|
|
97
|
+
known.range = normalized.range;
|
|
98
|
+
known.requester = normalized.requester;
|
|
99
|
+
known.requests = [toVersionRequest(normalized)];
|
|
100
|
+
known.optional = false;
|
|
101
|
+
metadataByName.delete(normalized.name);
|
|
102
|
+
if (completed === "") {
|
|
103
|
+
done.delete(normalized.name);
|
|
104
|
+
queue.push(known);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
throw registryIdentityConflict(
|
|
109
|
+
normalized.name,
|
|
110
|
+
known.registryName!,
|
|
111
|
+
normalized,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
known.requests ??= [toVersionRequest(known)];
|
|
115
|
+
known.requests.push(toVersionRequest(normalized));
|
|
116
|
+
known.optional = known.requests.every((request) => request.optional);
|
|
117
|
+
const completed = done.get(normalized.name);
|
|
118
|
+
const metadata = metadataByName.get(normalized.name);
|
|
119
|
+
if (completed !== undefined && completed.length !== 0 && metadata) {
|
|
120
|
+
// A dependency can discover an additional range after this package has
|
|
121
|
+
// already been installed. Pin the installed version into the combined
|
|
122
|
+
// solve so a compatible late constraint is accepted, but a range that
|
|
123
|
+
// rejects the mounted version fails instead of being silently lost.
|
|
124
|
+
try {
|
|
125
|
+
selectQueuedVersion(metadata, known, [completed]);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const mounted = installedDependencies.get(normalized.name);
|
|
128
|
+
if (mounted) throw mountedVersionConflict(mounted, error);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
} else if (completed !== undefined && !known.optional) {
|
|
132
|
+
// An optional 404 may later be reached through a required edge. Retry
|
|
133
|
+
// that edge so the required dependency cannot remain silently absent.
|
|
134
|
+
done.delete(normalized.name);
|
|
135
|
+
queue.push(known);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const mounted = installedDependencies.get(normalized.name);
|
|
140
|
+
if (mounted && mounted.registryName !== normalized.registryName) {
|
|
141
|
+
if (normalized.optional) return;
|
|
142
|
+
throw registryIdentityConflict(
|
|
143
|
+
normalized.name,
|
|
144
|
+
mounted.registryName,
|
|
145
|
+
normalized,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (installedNames.has(normalized.name) && mounted === undefined) return;
|
|
149
|
+
normalized.requests = [
|
|
150
|
+
...(mounted?.requests.map((request) => ({ ...request })) ?? []),
|
|
151
|
+
toVersionRequest(normalized),
|
|
152
|
+
];
|
|
153
|
+
normalized.optional = normalized.requests.every(
|
|
154
|
+
(request) => request.optional,
|
|
155
|
+
);
|
|
156
|
+
queued.set(normalized.name, normalized);
|
|
157
|
+
queue.push(normalized);
|
|
158
|
+
report("queued", normalized, `Queued ${normalized.name}`);
|
|
61
159
|
};
|
|
62
160
|
|
|
63
161
|
const report = (
|
|
@@ -77,7 +175,7 @@ export async function installPlaygroundDependencies(
|
|
|
77
175
|
};
|
|
78
176
|
|
|
79
177
|
for (const name of packageNames) {
|
|
80
|
-
enqueue({ name, range: "
|
|
178
|
+
enqueue({ name, range: "*", optional: false, requester: "source" });
|
|
81
179
|
}
|
|
82
180
|
|
|
83
181
|
for (let index = 0; index < queue.length; index++) {
|
|
@@ -88,29 +186,72 @@ export async function installPlaygroundDependencies(
|
|
|
88
186
|
}
|
|
89
187
|
|
|
90
188
|
const item = queue[index]!;
|
|
91
|
-
if (
|
|
189
|
+
if (done.has(item.name)) continue;
|
|
92
190
|
throwIfAborted(options.signal);
|
|
93
191
|
|
|
94
192
|
report("resolve", item, `Resolving ${item.name}`);
|
|
95
193
|
const metadata = await fetchNpmMetadata(
|
|
96
194
|
fetchImpl,
|
|
97
|
-
item.name,
|
|
195
|
+
item.registryName ?? item.name,
|
|
98
196
|
item.optional,
|
|
99
197
|
options.signal,
|
|
100
198
|
);
|
|
101
199
|
throwIfAborted(options.signal);
|
|
102
200
|
if (!metadata) {
|
|
103
|
-
|
|
104
|
-
|
|
201
|
+
const mounted = installedDependencies.get(item.name);
|
|
202
|
+
if (mounted) {
|
|
203
|
+
// A missing registry entry gives us no way to validate a new optional
|
|
204
|
+
// range or tag. Preserve the already-published optional requests and
|
|
205
|
+
// omit the new edge instead of claiming the mounted version satisfies
|
|
206
|
+
// a constraint we never checked.
|
|
207
|
+
item.requests = mounted.requests.map((request) => ({ ...request }));
|
|
208
|
+
item.optional = item.requests.every((request) => request.optional);
|
|
209
|
+
}
|
|
210
|
+
done.set(item.name, mounted?.version ?? "");
|
|
211
|
+
report(
|
|
212
|
+
mounted ? "done" : "skip",
|
|
213
|
+
item,
|
|
214
|
+
mounted
|
|
215
|
+
? `Reused mounted ${item.name}@${mounted.version}`
|
|
216
|
+
: `Skipped optional ${item.name}`,
|
|
217
|
+
mounted?.version,
|
|
218
|
+
);
|
|
105
219
|
continue;
|
|
106
220
|
}
|
|
107
221
|
|
|
108
|
-
|
|
222
|
+
metadataByName.set(item.name, metadata);
|
|
223
|
+
const mounted = installedDependencies.get(item.name);
|
|
224
|
+
let version: string | null;
|
|
225
|
+
try {
|
|
226
|
+
version = selectQueuedVersion(
|
|
227
|
+
metadata,
|
|
228
|
+
item,
|
|
229
|
+
mounted ? [mounted.version] : [],
|
|
230
|
+
);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (mounted) throw mountedVersionConflict(mounted, error);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
if (version === null) {
|
|
236
|
+
done.set(item.name, mounted?.version ?? "");
|
|
237
|
+
report("skip", item, `Skipped optional ${item.name}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (mounted) {
|
|
241
|
+
done.set(item.name, mounted.version);
|
|
242
|
+
report(
|
|
243
|
+
"done",
|
|
244
|
+
item,
|
|
245
|
+
`Reused mounted ${item.name}@${mounted.version}`,
|
|
246
|
+
mounted.version,
|
|
247
|
+
);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
109
250
|
const versionMetadata = metadata.versions[version];
|
|
110
251
|
const tarball = versionMetadata?.dist?.tarball;
|
|
111
252
|
if (!versionMetadata || !tarball) {
|
|
112
253
|
if (item.optional) {
|
|
113
|
-
done.
|
|
254
|
+
done.set(item.name, "");
|
|
114
255
|
report("skip", item, `Skipped optional ${item.name}`);
|
|
115
256
|
continue;
|
|
116
257
|
}
|
|
@@ -127,37 +268,155 @@ export async function installPlaygroundDependencies(
|
|
|
127
268
|
...versionMetadata,
|
|
128
269
|
...unpacked.packageJson,
|
|
129
270
|
};
|
|
130
|
-
const
|
|
271
|
+
const mountedFiles = mountPackageFiles(item.name, unpacked.files);
|
|
131
272
|
|
|
132
|
-
Object.assign(result.compilerFiles,
|
|
133
|
-
Object.assign(result.editorLibs,
|
|
134
|
-
Object.assign(result.runtimeFiles,
|
|
273
|
+
Object.assign(result.compilerFiles, mountedFiles.compilerFiles);
|
|
274
|
+
Object.assign(result.editorLibs, mountedFiles.editorLibs);
|
|
275
|
+
Object.assign(result.runtimeFiles, mountedFiles.runtimeFiles);
|
|
135
276
|
|
|
136
|
-
const declarationCount = Object.keys(
|
|
137
|
-
DECLARATION_FILE_REGEXP.test(key),
|
|
277
|
+
const declarationCount = Object.keys(mountedFiles.editorLibs).filter(
|
|
278
|
+
(key) => DECLARATION_FILE_REGEXP.test(key),
|
|
138
279
|
).length;
|
|
139
280
|
result.packages.push({
|
|
140
281
|
name: item.name,
|
|
282
|
+
registryName: item.registryName ?? item.name,
|
|
141
283
|
version,
|
|
142
284
|
tarball,
|
|
143
|
-
fileCount: Object.keys(
|
|
285
|
+
fileCount: Object.keys(mountedFiles.compilerFiles).length,
|
|
144
286
|
declarationCount,
|
|
145
287
|
});
|
|
146
288
|
|
|
147
|
-
done.
|
|
148
|
-
|
|
289
|
+
done.set(item.name, version);
|
|
290
|
+
installedNames.add(item.name);
|
|
149
291
|
report("done", item, `Installed ${item.name}@${version}`, version);
|
|
150
292
|
|
|
151
|
-
enqueuePackageDependencies(packageJson, enqueue);
|
|
293
|
+
enqueuePackageDependencies(packageJson, enqueue, item.name);
|
|
152
294
|
if (declarationCount === 0 && !item.name.startsWith("@types/")) {
|
|
153
295
|
enqueue({
|
|
154
296
|
name: toTypesPackageName(item.name),
|
|
155
|
-
range: "
|
|
297
|
+
range: "*",
|
|
156
298
|
optional: true,
|
|
299
|
+
requester: item.name,
|
|
157
300
|
});
|
|
158
301
|
}
|
|
159
302
|
}
|
|
160
303
|
|
|
304
|
+
const resolved = new Map(installedDependencies);
|
|
305
|
+
for (const [name, version] of done) {
|
|
306
|
+
if (version.length === 0) continue;
|
|
307
|
+
const item = queued.get(name)!;
|
|
308
|
+
resolved.set(name, {
|
|
309
|
+
name,
|
|
310
|
+
registryName: item.registryName ?? name,
|
|
311
|
+
version,
|
|
312
|
+
requests: (item.requests ?? [toVersionRequest(item)]).map((request) => ({
|
|
313
|
+
...request,
|
|
314
|
+
})),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
result.resolvedDependencies = [...resolved.values()].map((dependency) => ({
|
|
318
|
+
...dependency,
|
|
319
|
+
requests: dependency.requests.map((request) => ({ ...request })),
|
|
320
|
+
}));
|
|
161
321
|
report("done", null, "Dependency install complete");
|
|
162
322
|
return result;
|
|
163
323
|
}
|
|
324
|
+
|
|
325
|
+
function normalizeRegistryRequest(item: IQueueItem): IQueueItem {
|
|
326
|
+
if (!item.range.startsWith("npm:"))
|
|
327
|
+
return { ...item, registryName: item.name };
|
|
328
|
+
const match = /^npm:((?:@[^/]+\/)?[^@/]+)(?:@(.+))?$/.exec(item.range);
|
|
329
|
+
if (!match) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Unsupported npm alias ${JSON.stringify(item.range)} for ${item.name}.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
...item,
|
|
336
|
+
registryName: match[1]!,
|
|
337
|
+
range: match[2] || "*",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function toVersionRequest(item: IQueueItem): IVersionRequest {
|
|
342
|
+
return {
|
|
343
|
+
optional: item.optional,
|
|
344
|
+
range: item.range,
|
|
345
|
+
requester: item.requester,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function selectQueuedVersion(
|
|
350
|
+
metadata: Parameters<typeof selectVersion>[0],
|
|
351
|
+
item: IQueueItem,
|
|
352
|
+
extraRanges: readonly string[] = [],
|
|
353
|
+
): string | null {
|
|
354
|
+
const requests = item.requests ?? [toVersionRequest(item)];
|
|
355
|
+
const required = requests.filter((request) => !request.optional);
|
|
356
|
+
const optional = requests.filter((request) => request.optional);
|
|
357
|
+
const accepted = [...required];
|
|
358
|
+
let selected: string | null = null;
|
|
359
|
+
|
|
360
|
+
if (required.length !== 0) {
|
|
361
|
+
try {
|
|
362
|
+
selected = selectVersion(metadata, [
|
|
363
|
+
...required.map((request) => request.range),
|
|
364
|
+
...extraRanges,
|
|
365
|
+
]);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw requestedVersionError(error, required);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const request of optional) {
|
|
372
|
+
try {
|
|
373
|
+
const candidate = selectVersion(metadata, [
|
|
374
|
+
...accepted.map((acceptedRequest) => acceptedRequest.range),
|
|
375
|
+
request.range,
|
|
376
|
+
...extraRanges,
|
|
377
|
+
]);
|
|
378
|
+
accepted.push(request);
|
|
379
|
+
selected = candidate;
|
|
380
|
+
} catch {
|
|
381
|
+
// Optional ranges refine the selected version only when they are
|
|
382
|
+
// compatible with every required edge and the mounted-version pin.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
item.requests = accepted;
|
|
386
|
+
item.optional = accepted.every((request) => request.optional);
|
|
387
|
+
return selected;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function requestedVersionError(
|
|
391
|
+
error: unknown,
|
|
392
|
+
requests: readonly IVersionRequest[],
|
|
393
|
+
): Error {
|
|
394
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
395
|
+
const requestedBy = requests
|
|
396
|
+
.map(
|
|
397
|
+
({ requester, range }) =>
|
|
398
|
+
`${requester} requests ${JSON.stringify(range)}`,
|
|
399
|
+
)
|
|
400
|
+
.join("; ");
|
|
401
|
+
return new Error(`${message} Requested by ${requestedBy}.`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function mountedVersionConflict(
|
|
405
|
+
mounted: IPlaygroundInstalledDependency,
|
|
406
|
+
error: unknown,
|
|
407
|
+
): Error {
|
|
408
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
409
|
+
return new Error(
|
|
410
|
+
`Mounted ${mounted.name}@${mounted.version} from ${mounted.registryName} is incompatible with the active dependency graph. ${message}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function registryIdentityConflict(
|
|
415
|
+
name: string,
|
|
416
|
+
registryName: string,
|
|
417
|
+
incoming: IQueueItem,
|
|
418
|
+
): Error {
|
|
419
|
+
return new Error(
|
|
420
|
+
`Conflicting registry identities for ${name}: mounted or queued from ${registryName}, but ${incoming.requester} requests ${JSON.stringify(incoming.range)} from ${incoming.registryName}.`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { maxSatisfying, satisfies, valid, validRange } from "semver";
|
|
2
|
+
|
|
1
3
|
// Internal npm-registry helpers used by `installPlaygroundDependencies`.
|
|
2
4
|
// Grouped in one file because every helper is privately coupled to the tar
|
|
3
5
|
// + version-resolve + tarball-extract flow; splitting them per the public
|
|
@@ -7,6 +9,7 @@ interface IPackageJson {
|
|
|
7
9
|
name?: string;
|
|
8
10
|
version?: string;
|
|
9
11
|
dependencies?: Record<string, string>;
|
|
12
|
+
optionalDependencies?: Record<string, string>;
|
|
10
13
|
peerDependencies?: Record<string, string>;
|
|
11
14
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
12
15
|
}
|
|
@@ -15,6 +18,7 @@ export interface INpmVersionMetadata {
|
|
|
15
18
|
name: string;
|
|
16
19
|
version: string;
|
|
17
20
|
dependencies?: Record<string, string>;
|
|
21
|
+
optionalDependencies?: Record<string, string>;
|
|
18
22
|
peerDependencies?: Record<string, string>;
|
|
19
23
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
20
24
|
dist?: {
|
|
@@ -37,6 +41,15 @@ export interface IQueueItem {
|
|
|
37
41
|
name: string;
|
|
38
42
|
range: string;
|
|
39
43
|
optional: boolean;
|
|
44
|
+
requester: string;
|
|
45
|
+
registryName?: string;
|
|
46
|
+
requests?: IVersionRequest[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface IVersionRequest {
|
|
50
|
+
optional: boolean;
|
|
51
|
+
range: string;
|
|
52
|
+
requester: string;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
export type FetchLike = (
|
|
@@ -79,19 +92,43 @@ export async function fetchNpmMetadata(
|
|
|
79
92
|
return (await response.json()) as INpmMetadata;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
|
-
export function selectVersion(
|
|
95
|
+
export function selectVersion(
|
|
96
|
+
metadata: INpmMetadata,
|
|
97
|
+
ranges: readonly string[] | string,
|
|
98
|
+
): string {
|
|
83
99
|
const versions = metadata.versions;
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
const requested = Array.isArray(ranges) ? ranges : [ranges];
|
|
101
|
+
const semverRanges = requested.filter((range) => validRange(range) !== null);
|
|
102
|
+
const tags = requested.filter((range) => validRange(range) === null);
|
|
103
|
+
const taggedVersions = new Set<string>();
|
|
104
|
+
for (const tag of tags) {
|
|
105
|
+
const version = metadata["dist-tags"]?.[tag];
|
|
106
|
+
if (!version || !versions[version]) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`No npm dist-tag ${JSON.stringify(tag)} exists for ${metadata.name}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
taggedVersions.add(version);
|
|
112
|
+
}
|
|
113
|
+
if (taggedVersions.size > 1) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Conflicting npm tags for ${metadata.name}: ${requested.join(", ")}.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const candidates = Object.keys(versions).filter(
|
|
120
|
+
(version) =>
|
|
121
|
+
valid(version) !== null &&
|
|
122
|
+
semverRanges.every((range) => satisfies(version, range)) &&
|
|
123
|
+
(taggedVersions.size === 0 || taggedVersions.has(version)),
|
|
124
|
+
);
|
|
125
|
+
const selected = maxSatisfying(candidates, "*");
|
|
126
|
+
if (selected) return selected;
|
|
127
|
+
throw new Error(
|
|
128
|
+
`No version of ${metadata.name} satisfies ${requested
|
|
129
|
+
.map((range) => JSON.stringify(range))
|
|
130
|
+
.join(", ")}.`,
|
|
131
|
+
);
|
|
95
132
|
}
|
|
96
133
|
|
|
97
134
|
export async function downloadTarball(
|
|
@@ -136,7 +173,7 @@ export async function unpackNpmTarball(
|
|
|
136
173
|
continue;
|
|
137
174
|
}
|
|
138
175
|
if (type === "x") {
|
|
139
|
-
paxPath = parsePaxPath(
|
|
176
|
+
paxPath = parsePaxPath(body);
|
|
140
177
|
continue;
|
|
141
178
|
}
|
|
142
179
|
if (type !== "0" && type !== "\0") {
|
|
@@ -209,13 +246,21 @@ export function mountPackageFiles(
|
|
|
209
246
|
export function enqueuePackageDependencies(
|
|
210
247
|
packageJson: {
|
|
211
248
|
dependencies?: Record<string, string>;
|
|
249
|
+
optionalDependencies?: Record<string, string>;
|
|
212
250
|
peerDependencies?: Record<string, string>;
|
|
213
251
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
214
252
|
},
|
|
215
253
|
enqueue: (item: IQueueItem) => void,
|
|
254
|
+
requester: string,
|
|
216
255
|
): void {
|
|
256
|
+
const optionalDependencies = packageJson.optionalDependencies ?? {};
|
|
217
257
|
for (const [name, range] of Object.entries(packageJson.dependencies ?? {})) {
|
|
218
|
-
if (
|
|
258
|
+
if (!(name in optionalDependencies) && isRegistryRange(range))
|
|
259
|
+
enqueue({ name, range, optional: false, requester });
|
|
260
|
+
}
|
|
261
|
+
for (const [name, range] of Object.entries(optionalDependencies)) {
|
|
262
|
+
if (isRegistryRange(range))
|
|
263
|
+
enqueue({ name, range, optional: true, requester });
|
|
219
264
|
}
|
|
220
265
|
for (const [name, range] of Object.entries(
|
|
221
266
|
packageJson.peerDependencies ?? {},
|
|
@@ -228,12 +273,26 @@ export function enqueuePackageDependencies(
|
|
|
228
273
|
// silent skip that leaves the wasm-side compile reporting a generic
|
|
229
274
|
// "Cannot find module" with no breadcrumb back to the dep installer.
|
|
230
275
|
if (!optional && isRegistryRange(range))
|
|
231
|
-
enqueue({ name, range, optional: false });
|
|
276
|
+
enqueue({ name, range, optional: false, requester });
|
|
232
277
|
}
|
|
233
278
|
}
|
|
234
279
|
|
|
235
280
|
function isRegistryRange(range: string): boolean {
|
|
236
|
-
|
|
281
|
+
const spec = range.trim();
|
|
282
|
+
// Match npm-package-arg's file classification before its registry fallback:
|
|
283
|
+
// dot/home/root/drive paths and tar archive names are source specs even
|
|
284
|
+
// though their characters could also form a URI-safe dist-tag.
|
|
285
|
+
if (
|
|
286
|
+
/^(?:\.|~[\\/]|[\\/]|[a-zA-Z]:)/.test(spec) ||
|
|
287
|
+
/\.(?:tgz|tar\.gz|tar)$/i.test(spec)
|
|
288
|
+
)
|
|
289
|
+
return false;
|
|
290
|
+
if (spec.startsWith("npm:")) return true;
|
|
291
|
+
if (validRange(spec) !== null) return true;
|
|
292
|
+
// npm accepts a dist-tag only when it is a non-empty URI-component-safe
|
|
293
|
+
// token. Git, hosted, URL, and local-path specs all require punctuation that
|
|
294
|
+
// encodeURIComponent escapes, so they cannot fall through as fake tags.
|
|
295
|
+
return spec.length > 0 && encodeURIComponent(spec) === spec;
|
|
237
296
|
}
|
|
238
297
|
|
|
239
298
|
export function toTypesPackageName(packageName: string): string {
|
|
@@ -256,19 +315,32 @@ function parseOctal(bytes: Uint8Array): number {
|
|
|
256
315
|
return text ? Number.parseInt(text, 8) : 0;
|
|
257
316
|
}
|
|
258
317
|
|
|
259
|
-
function parsePaxPath(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
318
|
+
function parsePaxPath(bytes: Uint8Array): string | null {
|
|
319
|
+
const decoder = new TextDecoder();
|
|
320
|
+
let offset = 0;
|
|
321
|
+
let path: string | null = null;
|
|
322
|
+
while (offset < bytes.length) {
|
|
323
|
+
let space = offset;
|
|
324
|
+
while (space < bytes.length && bytes[space] !== 0x20) space++;
|
|
325
|
+
if (space === bytes.length) throw new Error("Invalid PAX header length.");
|
|
326
|
+
const lengthText = decoder.decode(bytes.subarray(offset, space));
|
|
327
|
+
if (!/^[1-9][0-9]*$/.test(lengthText))
|
|
328
|
+
throw new Error("Invalid PAX header length.");
|
|
329
|
+
const length = Number(lengthText);
|
|
330
|
+
const end = offset + length;
|
|
331
|
+
if (
|
|
332
|
+
!Number.isSafeInteger(length) ||
|
|
333
|
+
length <= space - offset + 1 ||
|
|
334
|
+
end > bytes.length ||
|
|
335
|
+
bytes[end - 1] !== 0x0a
|
|
336
|
+
)
|
|
337
|
+
throw new Error("Invalid PAX header record.");
|
|
338
|
+
const record = decoder.decode(bytes.subarray(space + 1, end - 1));
|
|
267
339
|
const eq = record.indexOf("=");
|
|
268
|
-
if (eq > 0 && record.slice(0, eq) === "path")
|
|
269
|
-
|
|
340
|
+
if (eq > 0 && record.slice(0, eq) === "path") path = record.slice(eq + 1);
|
|
341
|
+
offset = end;
|
|
270
342
|
}
|
|
271
|
-
return
|
|
343
|
+
return path;
|
|
272
344
|
}
|
|
273
345
|
|
|
274
346
|
function stripTarRoot(path: string): string {
|
|
@@ -279,14 +351,3 @@ function stripTarRoot(path: string): string {
|
|
|
279
351
|
const slash = normalized.indexOf("/");
|
|
280
352
|
return slash < 0 ? normalized : normalized.slice(slash + 1);
|
|
281
353
|
}
|
|
282
|
-
|
|
283
|
-
function compareVersionDesc(a: string, b: string): number {
|
|
284
|
-
const pa = a.split(/[.-]/).map((part) => Number.parseInt(part, 10));
|
|
285
|
-
const pb = b.split(/[.-]/).map((part) => Number.parseInt(part, 10));
|
|
286
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
287
|
-
const av = Number.isFinite(pa[i]) ? pa[i]! : 0;
|
|
288
|
-
const bv = Number.isFinite(pb[i]) ? pb[i]! : 0;
|
|
289
|
-
if (av !== bv) return bv - av;
|
|
290
|
-
}
|
|
291
|
-
return b.localeCompare(a);
|
|
292
|
-
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
const BUILTIN_MODULES = new Set([
|
|
2
2
|
"assert",
|
|
3
|
+
"async_hooks",
|
|
3
4
|
"buffer",
|
|
4
5
|
"child_process",
|
|
6
|
+
"cluster",
|
|
5
7
|
"console",
|
|
6
8
|
"constants",
|
|
7
9
|
"crypto",
|
|
10
|
+
"dgram",
|
|
11
|
+
"diagnostics_channel",
|
|
8
12
|
"dns",
|
|
13
|
+
"domain",
|
|
9
14
|
"events",
|
|
10
15
|
"fs",
|
|
11
16
|
"http",
|
|
17
|
+
"http2",
|
|
12
18
|
"https",
|
|
19
|
+
"inspector",
|
|
13
20
|
"module",
|
|
14
21
|
"net",
|
|
15
22
|
"os",
|
|
@@ -19,14 +26,19 @@ const BUILTIN_MODULES = new Set([
|
|
|
19
26
|
"punycode",
|
|
20
27
|
"querystring",
|
|
21
28
|
"readline",
|
|
29
|
+
"repl",
|
|
22
30
|
"stream",
|
|
23
31
|
"string_decoder",
|
|
32
|
+
"sys",
|
|
24
33
|
"timers",
|
|
25
34
|
"tls",
|
|
35
|
+
"trace_events",
|
|
26
36
|
"tty",
|
|
27
37
|
"url",
|
|
28
38
|
"util",
|
|
39
|
+
"v8",
|
|
29
40
|
"vm",
|
|
41
|
+
"wasi",
|
|
30
42
|
"worker_threads",
|
|
31
43
|
"zlib",
|
|
32
44
|
]);
|
|
@@ -38,6 +50,10 @@ const BUILTIN_MODULES = new Set([
|
|
|
38
50
|
* built-in modules — the caller doesn't install those from npm.
|
|
39
51
|
*/
|
|
40
52
|
export function packageNameFromSpecifier(specifier: string): string | null {
|
|
53
|
+
const nodePrefixed = specifier.startsWith("node:");
|
|
54
|
+
const bare = nodePrefixed ? specifier.slice("node:".length) : specifier;
|
|
55
|
+
const first = bare.split("/")[0];
|
|
56
|
+
if (first && BUILTIN_MODULES.has(first)) return null;
|
|
41
57
|
if (
|
|
42
58
|
specifier.startsWith("#") ||
|
|
43
59
|
specifier.startsWith(".") ||
|
|
@@ -45,9 +61,6 @@ export function packageNameFromSpecifier(specifier: string): string | null {
|
|
|
45
61
|
/^[a-z][a-z0-9+.-]*:/i.test(specifier)
|
|
46
62
|
)
|
|
47
63
|
return null;
|
|
48
|
-
const bare = specifier.startsWith("node:") ? specifier.slice(5) : specifier;
|
|
49
|
-
const first = bare.split("/")[0];
|
|
50
|
-
if (first && BUILTIN_MODULES.has(first)) return null;
|
|
51
64
|
if (bare.startsWith("@")) {
|
|
52
65
|
const firstSlash = bare.indexOf("/");
|
|
53
66
|
if (firstSlash < 0) return null;
|