@ttsc/playground 0.19.3 → 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.
Files changed (44) hide show
  1. package/README.md +6 -0
  2. package/lib/src/npm/collectExternalPackageNames.js +436 -73
  3. package/lib/src/npm/collectExternalPackageNames.js.map +1 -1
  4. package/lib/src/npm/installPlaygroundDependencies.d.ts +5 -3
  5. package/lib/src/npm/installPlaygroundDependencies.js +230 -27
  6. package/lib/src/npm/installPlaygroundDependencies.js.map +1 -1
  7. package/lib/src/npm/internal/npmRegistry.d.ts +13 -2
  8. package/lib/src/npm/internal/npmRegistry.js +73 -50
  9. package/lib/src/npm/internal/npmRegistry.js.map +1 -1
  10. package/lib/src/npm/packageNameFromSpecifier.js +17 -4
  11. package/lib/src/npm/packageNameFromSpecifier.js.map +1 -1
  12. package/lib/src/react/PlaygroundShell.js +68 -61
  13. package/lib/src/react/PlaygroundShell.js.map +1 -1
  14. package/lib/src/react/createCompilerClient.d.ts +3 -3
  15. package/lib/src/react/createCompilerClient.js +32 -54
  16. package/lib/src/react/createCompilerClient.js.map +1 -1
  17. package/lib/src/sandbox/createSandboxRequire.js +117 -54
  18. package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
  19. package/lib/src/structures/IPlaygroundDependencyInstallOptions.d.ts +13 -1
  20. package/lib/src/structures/IPlaygroundDependencyInstallResult.d.ts +4 -0
  21. package/lib/src/structures/IPlaygroundDependencyPackage.d.ts +2 -0
  22. package/lib/src/structures/IPlaygroundDependencyRequest.d.ts +9 -0
  23. package/lib/src/structures/IPlaygroundDependencyRequest.js +3 -0
  24. package/lib/src/structures/IPlaygroundDependencyRequest.js.map +1 -0
  25. package/lib/src/structures/IPlaygroundInstalledDependency.d.ts +12 -0
  26. package/lib/src/structures/IPlaygroundInstalledDependency.js +3 -0
  27. package/lib/src/structures/IPlaygroundInstalledDependency.js.map +1 -0
  28. package/lib/src/structures/index.d.ts +2 -0
  29. package/lib/src/structures/index.js +2 -0
  30. package/lib/src/structures/index.js.map +1 -1
  31. package/package.json +6 -4
  32. package/src/npm/collectExternalPackageNames.ts +503 -68
  33. package/src/npm/installPlaygroundDependencies.ts +288 -29
  34. package/src/npm/internal/npmRegistry.ts +99 -38
  35. package/src/npm/packageNameFromSpecifier.ts +16 -3
  36. package/src/react/PlaygroundShell.tsx +86 -72
  37. package/src/react/createCompilerClient.ts +37 -51
  38. package/src/sandbox/createSandboxRequire.ts +140 -49
  39. package/src/structures/IPlaygroundDependencyInstallOptions.ts +13 -1
  40. package/src/structures/IPlaygroundDependencyInstallResult.ts +4 -0
  41. package/src/structures/IPlaygroundDependencyPackage.ts +2 -0
  42. package/src/structures/IPlaygroundDependencyRequest.ts +9 -0
  43. package/src/structures/IPlaygroundInstalledDependency.ts +13 -0
  44. 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
- * `dependencies` and (non-optional) `peerDependencies` fields. The walk is
27
- * bounded by `maxPackages` to keep a single keystroke from exhausting the tab's
28
- * network/memory budget.
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 installed = new Set(options.installedPackages ?? []);
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 Set<string>();
47
- const done = new Set<string>();
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
- if (ignored.has(item.name) || installed.has(item.name)) return;
57
- if (queued.has(item.name) || done.has(item.name)) return;
58
- queued.add(item.name);
59
- queue.push(item);
60
- report("queued", item, `Queued ${item.name}`);
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: "latest", optional: false });
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 (installed.has(item.name) || done.has(item.name)) continue;
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
- done.add(item.name);
104
- report("skip", item, `Skipped optional ${item.name}`);
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
- const version = selectVersion(metadata, item.range);
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.add(item.name);
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 mounted = mountPackageFiles(item.name, unpacked.files);
271
+ const mountedFiles = mountPackageFiles(item.name, unpacked.files);
131
272
 
132
- Object.assign(result.compilerFiles, mounted.compilerFiles);
133
- Object.assign(result.editorLibs, mounted.editorLibs);
134
- Object.assign(result.runtimeFiles, mounted.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(mounted.editorLibs).filter((key) =>
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(mounted.compilerFiles).length,
285
+ fileCount: Object.keys(mountedFiles.compilerFiles).length,
144
286
  declarationCount,
145
287
  });
146
288
 
147
- done.add(item.name);
148
- installed.add(item.name);
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: "latest",
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(metadata: INpmMetadata, range: string): string {
95
+ export function selectVersion(
96
+ metadata: INpmMetadata,
97
+ ranges: readonly string[] | string,
98
+ ): string {
83
99
  const versions = metadata.versions;
84
- if (versions[range]) return range;
85
- const tag = metadata["dist-tags"]?.[range];
86
- if (tag && versions[tag]) return tag;
87
- const normalized = range.replace(/^[~^<>= ]+/, "").trim();
88
- if (normalized && versions[normalized]) return normalized;
89
- const latest = metadata["dist-tags"]?.latest;
90
- if (latest && versions[latest]) return latest;
91
- const all = Object.keys(versions).sort(compareVersionDesc);
92
- const fallback = all[0];
93
- if (!fallback) throw new Error(`No versions found for ${metadata.name}.`);
94
- return fallback;
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(decoder.decode(body));
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 (isRegistryRange(range)) enqueue({ name, range, optional: false });
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
- return !/^(file:|link:|workspace:|portal:|git\+|github:|https?:)/.test(range);
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(text: string): string | null {
260
- let rest = text;
261
- while (rest.length > 0) {
262
- const space = rest.indexOf(" ");
263
- if (space < 0) return null;
264
- const length = Number(rest.slice(0, space));
265
- if (!Number.isFinite(length) || length <= space) return null;
266
- const record = rest.slice(space + 1, length - 1);
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") return record.slice(eq + 1);
269
- rest = rest.slice(length);
340
+ if (eq > 0 && record.slice(0, eq) === "path") path = record.slice(eq + 1);
341
+ offset = end;
270
342
  }
271
- return null;
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;