@ttsc/playground 0.20.1 → 0.21.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 +8 -0
- package/lib/src/npm/installPlaygroundDependencies.js +31 -11
- package/lib/src/npm/installPlaygroundDependencies.js.map +1 -1
- package/lib/src/npm/internal/npmRegistry.d.ts +15 -2
- package/lib/src/npm/internal/npmRegistry.js +332 -27
- package/lib/src/npm/internal/npmRegistry.js.map +1 -1
- package/lib/src/sandbox/createSandboxRequire.js +254 -105
- package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
- package/lib/src/structures/IPlaygroundDependencyInstallOptions.d.ts +13 -0
- package/package.json +3 -3
- package/src/npm/installPlaygroundDependencies.ts +42 -16
- package/src/npm/internal/npmRegistry.ts +431 -31
- package/src/sandbox/createSandboxRequire.ts +313 -136
- package/src/structures/IPlaygroundDependencyInstallOptions.ts +13 -0
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Resolution algorithm (minimal):
|
|
6
6
|
// - Bare specifier `typia/lib/X`:
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// giving up.
|
|
7
|
+
// honor the package's `exports` boundary when declared; otherwise try
|
|
8
|
+
// `typia/lib/X.js`, then `typia/lib/X/index.js`.
|
|
10
9
|
// - Relative `./Y` / `../Y` (encountered when one pack module requires a
|
|
11
10
|
// sibling): resolved against the caller's pack key.
|
|
12
|
-
// - Bare `name` (no subpath): honors
|
|
13
|
-
// to `name/index.js`.
|
|
11
|
+
// - Bare `name` (no subpath): honors package `exports`, then (only when
|
|
12
|
+
// exports is absent) `main`, falling back to `name/index.js`.
|
|
14
13
|
//
|
|
15
14
|
// Every successful load is cached so cyclic graphs settle. Module evaluation
|
|
16
15
|
// wraps the source text in the standard CJS wrapper:
|
|
@@ -36,6 +35,15 @@ interface ISandboxRequireOptions {
|
|
|
36
35
|
// neither Node's runtime nor an ESM evaluator.
|
|
37
36
|
const ACTIVE_EXPORT_CONDITIONS = new Set(["require", "default"]);
|
|
38
37
|
|
|
38
|
+
type ExportTargetResolution =
|
|
39
|
+
| { type: "resolved"; key: string }
|
|
40
|
+
| { type: "blocked" }
|
|
41
|
+
| { type: "unresolved" };
|
|
42
|
+
|
|
43
|
+
class InvalidPackageTargetError extends Error {}
|
|
44
|
+
class InvalidPackageTargetLoadError extends Error {}
|
|
45
|
+
class InvalidPackageConfigError extends Error {}
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* Build a sandboxed `require` function over a runtime pack. Resolves typia /
|
|
41
49
|
* `@typia/*` / randexp specifiers from the pack; throws on anything else so the
|
|
@@ -56,97 +64,104 @@ export function createSandboxRequire(
|
|
|
56
64
|
return null;
|
|
57
65
|
};
|
|
58
66
|
|
|
59
|
-
const
|
|
60
|
-
const key = `${
|
|
61
|
-
if (!has(key)) return
|
|
67
|
+
const readPackageJson = (mount: string): IPackJson | null => {
|
|
68
|
+
const key = `${mount}/package.json`;
|
|
69
|
+
if (!has(key)) return null;
|
|
62
70
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const parsed = JSON.parse(pack[key]!) as unknown;
|
|
72
|
+
if (
|
|
73
|
+
parsed === null ||
|
|
74
|
+
typeof parsed !== "object" ||
|
|
75
|
+
Array.isArray(parsed)
|
|
76
|
+
) {
|
|
77
|
+
throw new Error("package.json must contain an object");
|
|
78
|
+
}
|
|
79
|
+
return parsed as IPackJson;
|
|
67
80
|
} catch {
|
|
68
|
-
|
|
81
|
+
throw new InvalidPackageConfigError(
|
|
82
|
+
`invalid package configuration in ${key}`,
|
|
83
|
+
);
|
|
69
84
|
}
|
|
70
85
|
};
|
|
71
86
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const packageDeclaresExports = (mount: string): boolean => {
|
|
88
|
+
const manifest = readPackageJson(mount);
|
|
89
|
+
return manifest?.exports !== null && manifest?.exports !== undefined;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const resolveLegacyFile = (candidate: string): string | null =>
|
|
93
|
+
tryPaths(
|
|
94
|
+
candidate,
|
|
95
|
+
`${candidate}.js`,
|
|
96
|
+
`${candidate}.cjs`,
|
|
97
|
+
`${candidate}.mjs`,
|
|
98
|
+
`${candidate}.json`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const resolveLegacyIndex = (candidate: string): string | null =>
|
|
102
|
+
tryPaths(
|
|
103
|
+
`${candidate}/index.js`,
|
|
104
|
+
`${candidate}/index.cjs`,
|
|
105
|
+
`${candidate}/index.json`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/** Resolve one legacy CommonJS file-or-directory candidate. */
|
|
109
|
+
const resolveLegacyPath = (candidate: string): string | null => {
|
|
110
|
+
const file = resolveLegacyFile(candidate);
|
|
111
|
+
if (file !== null) return file;
|
|
112
|
+
|
|
113
|
+
const manifest = readPackageJson(candidate);
|
|
114
|
+
if (typeof manifest?.main === "string" && manifest.main.length !== 0) {
|
|
115
|
+
const main = posixJoin(candidate, manifest.main);
|
|
116
|
+
if (main !== candidate) {
|
|
117
|
+
// Node's legacy tryPackage resolves the selected main as a file, then
|
|
118
|
+
// as that directory's index. It does not recursively interpret a
|
|
119
|
+
// second package.json below the selected main directory.
|
|
120
|
+
const resolvedMain =
|
|
121
|
+
resolveLegacyFile(main) ?? resolveLegacyIndex(main);
|
|
122
|
+
if (resolvedMain !== null) return resolvedMain;
|
|
123
|
+
}
|
|
84
124
|
}
|
|
85
|
-
return
|
|
125
|
+
return resolveLegacyIndex(candidate);
|
|
86
126
|
};
|
|
87
127
|
|
|
88
128
|
// Read package.json from pack and resolve via main/exports.
|
|
89
129
|
const resolvePackageEntry = (
|
|
90
|
-
|
|
130
|
+
mount: string,
|
|
91
131
|
subpath: string | null,
|
|
92
132
|
): string | null => {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pj = JSON.parse(pack[pjKey]!) as IPackJson;
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
133
|
+
const pj = readPackageJson(mount);
|
|
134
|
+
if (pj?.exports !== null && pj?.exports !== undefined) {
|
|
135
|
+
const resolution = resolvePackageExports(mount, pj.exports, subpath);
|
|
136
|
+
return resolution.type === "resolved" ? resolution.key : null;
|
|
100
137
|
}
|
|
101
138
|
if (subpath === null) {
|
|
102
|
-
|
|
103
|
-
// "." key, or bare condition map) → CJS target, else main, else index.
|
|
104
|
-
if (pj.exports !== undefined)
|
|
105
|
-
return resolveExportTarget(pkg, rootExportTarget(pj.exports));
|
|
106
|
-
if (typeof pj.main === "string") {
|
|
107
|
-
return tryPaths(
|
|
108
|
-
`${pkg}/${stripDotSlash(pj.main)}`,
|
|
109
|
-
`${pkg}/${stripDotSlash(pj.main)}.js`,
|
|
110
|
-
`${pkg}/${stripDotSlash(pj.main)}.cjs`,
|
|
111
|
-
`${pkg}/${stripDotSlash(pj.main)}.mjs`,
|
|
112
|
-
`${pkg}/${stripDotSlash(pj.main)}.json`,
|
|
113
|
-
`${pkg}/${stripDotSlash(pj.main)}/index.js`,
|
|
114
|
-
`${pkg}/${stripDotSlash(pj.main)}/index.cjs`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
return tryPaths(`${pkg}/index.js`, `${pkg}/index.cjs`);
|
|
139
|
+
return resolveLegacyPath(mount);
|
|
118
140
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(entry): entry is typeof entry & { replacement: string } =>
|
|
144
|
-
entry.replacement !== undefined,
|
|
145
|
-
)
|
|
146
|
-
.sort((a, b) => compareExportPatternKeys(a.pattern, b.pattern));
|
|
147
|
-
for (const { replacement, target } of patterns) {
|
|
148
|
-
return resolveExportTarget(pkg, target, replacement);
|
|
149
|
-
}
|
|
141
|
+
return null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Locate a package self-reference by walking from the calling module to its
|
|
146
|
+
* nearest manifest. Node enables self-reference only when that manifest has
|
|
147
|
+
* both the requested `name` and an `exports` field; the pack mount itself may
|
|
148
|
+
* be an npm alias whose key differs from `name`.
|
|
149
|
+
*/
|
|
150
|
+
const selfReferenceMount = (
|
|
151
|
+
fromKey: string | null,
|
|
152
|
+
requestedPackage: string,
|
|
153
|
+
): string | null => {
|
|
154
|
+
if (fromKey === null) return null;
|
|
155
|
+
const parts = dirname(fromKey).split("/").filter(Boolean);
|
|
156
|
+
for (let length = parts.length; length > 0; --length) {
|
|
157
|
+
const mount = parts.slice(0, length).join("/");
|
|
158
|
+
const manifest = readPackageJson(mount);
|
|
159
|
+
if (manifest === null) continue;
|
|
160
|
+
return manifest.name === requestedPackage &&
|
|
161
|
+
manifest.exports !== null &&
|
|
162
|
+
manifest.exports !== undefined
|
|
163
|
+
? mount
|
|
164
|
+
: null;
|
|
150
165
|
}
|
|
151
166
|
return null;
|
|
152
167
|
};
|
|
@@ -161,18 +176,14 @@ export function createSandboxRequire(
|
|
|
161
176
|
if (!fromKey) return null;
|
|
162
177
|
const baseDir = dirname(fromKey);
|
|
163
178
|
const joined = posixJoin(baseDir, specifier);
|
|
164
|
-
return
|
|
165
|
-
joined,
|
|
166
|
-
`${joined}.js`,
|
|
167
|
-
`${joined}.cjs`,
|
|
168
|
-
`${joined}.mjs`,
|
|
169
|
-
`${joined}.json`,
|
|
170
|
-
`${joined}/index.js`,
|
|
171
|
-
`${joined}/index.cjs`,
|
|
172
|
-
);
|
|
179
|
+
return resolveLegacyPath(joined);
|
|
173
180
|
}
|
|
174
181
|
// Bare specifier. Split into package name + subpath.
|
|
175
182
|
const { pkg, subpath } = splitBareSpecifier(specifier);
|
|
183
|
+
const selfMount = selfReferenceMount(fromKey, pkg);
|
|
184
|
+
if (selfMount !== null) {
|
|
185
|
+
return resolvePackageEntry(selfMount, subpath);
|
|
186
|
+
}
|
|
176
187
|
if (subpath === null) {
|
|
177
188
|
return resolvePackageEntry(pkg, null);
|
|
178
189
|
}
|
|
@@ -180,15 +191,7 @@ export function createSandboxRequire(
|
|
|
180
191
|
// retain the historical packed-file fallback.
|
|
181
192
|
if (packageDeclaresExports(pkg)) return resolvePackageEntry(pkg, subpath);
|
|
182
193
|
// First try direct paths (covers packages that do not declare exports).
|
|
183
|
-
const direct =
|
|
184
|
-
`${pkg}/${subpath}`,
|
|
185
|
-
`${pkg}/${subpath}.js`,
|
|
186
|
-
`${pkg}/${subpath}.cjs`,
|
|
187
|
-
`${pkg}/${subpath}.mjs`,
|
|
188
|
-
`${pkg}/${subpath}.json`,
|
|
189
|
-
`${pkg}/${subpath}/index.js`,
|
|
190
|
-
`${pkg}/${subpath}/index.cjs`,
|
|
191
|
-
);
|
|
194
|
+
const direct = resolveLegacyPath(`${pkg}/${subpath}`);
|
|
192
195
|
if (direct) return direct;
|
|
193
196
|
// Fall back to package.json exports map.
|
|
194
197
|
return resolvePackageEntry(pkg, subpath);
|
|
@@ -208,7 +211,7 @@ export function createSandboxRequire(
|
|
|
208
211
|
}
|
|
209
212
|
const localRequire = (specifier: string): unknown => {
|
|
210
213
|
const resolved = resolveSpecifier(specifier, key);
|
|
211
|
-
if (!resolved) {
|
|
214
|
+
if (!resolved || !has(resolved)) {
|
|
212
215
|
throw new Error(
|
|
213
216
|
`require("${specifier}") is not available in the playground sandbox (from ${key})`,
|
|
214
217
|
);
|
|
@@ -257,7 +260,7 @@ export function createSandboxRequire(
|
|
|
257
260
|
|
|
258
261
|
return (specifier: string): unknown => {
|
|
259
262
|
const resolved = resolveSpecifier(specifier, null);
|
|
260
|
-
if (!resolved) {
|
|
263
|
+
if (!resolved || !has(resolved)) {
|
|
261
264
|
throw new Error(
|
|
262
265
|
`require("${specifier}") is not available in the playground sandbox`,
|
|
263
266
|
);
|
|
@@ -302,52 +305,226 @@ function compareExportPatternKeys(left: string, right: string): number {
|
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
/**
|
|
305
|
-
*
|
|
306
|
-
* (".") entry, ready for {@link conditionalExportTargets}. Node accepts three
|
|
307
|
-
* valid root shapes and they must all resolve consistently:
|
|
308
|
-
*
|
|
309
|
-
* - A bare string target — `"exports": "./index.cjs"`;
|
|
310
|
-
* - A subpath table keyed by "." — `{ ".": <target>, "./sub": ... }`;
|
|
311
|
-
* - A bare condition map whose keys are all conditions, not subpaths — `{
|
|
312
|
-
* "require": "./index.cjs", "default": "./index.cjs" }`.
|
|
308
|
+
* Resolve one package `exports` request without consulting pack existence.
|
|
313
309
|
*
|
|
314
|
-
* Node
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
310
|
+
* Node chooses one target first and performs file loading afterward. Keeping
|
|
311
|
+
* those phases separate is essential: a valid first array target that names a
|
|
312
|
+
* missing file must fail at load time rather than fall through to a later
|
|
313
|
+
* target.
|
|
318
314
|
*/
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
315
|
+
function resolvePackageExports(
|
|
316
|
+
mount: string,
|
|
317
|
+
exportsField: unknown,
|
|
318
|
+
subpath: string | null,
|
|
319
|
+
): ExportTargetResolution {
|
|
320
|
+
let target: unknown;
|
|
321
|
+
if (
|
|
322
|
+
exportsField !== null &&
|
|
323
|
+
typeof exportsField === "object" &&
|
|
324
|
+
!Array.isArray(exportsField)
|
|
325
|
+
) {
|
|
326
|
+
const entries = exportsField as Record<string, unknown>;
|
|
327
|
+
const kind = classifyExportsObject(mount, entries);
|
|
328
|
+
if (kind === "conditions") {
|
|
329
|
+
if (subpath !== null) return { type: "unresolved" };
|
|
330
|
+
target = entries;
|
|
331
|
+
} else {
|
|
332
|
+
const request = subpath === null ? "." : `./${subpath}`;
|
|
333
|
+
if (
|
|
334
|
+
Object.prototype.hasOwnProperty.call(entries, request) &&
|
|
335
|
+
!request.includes("*") &&
|
|
336
|
+
!request.endsWith("/")
|
|
337
|
+
) {
|
|
338
|
+
target = entries[request];
|
|
339
|
+
} else {
|
|
340
|
+
const patterns = Object.entries(entries)
|
|
341
|
+
.map(([pattern, candidate]) => ({
|
|
342
|
+
pattern,
|
|
343
|
+
replacement: exportPatternReplacement(pattern, request),
|
|
344
|
+
target: candidate,
|
|
345
|
+
}))
|
|
346
|
+
.filter(
|
|
347
|
+
(entry): entry is typeof entry & { replacement: string } =>
|
|
348
|
+
entry.replacement !== undefined,
|
|
349
|
+
)
|
|
350
|
+
.sort((a, b) => compareExportPatternKeys(a.pattern, b.pattern));
|
|
351
|
+
const selected = patterns[0];
|
|
352
|
+
if (selected === undefined) return { type: "unresolved" };
|
|
353
|
+
return resolvePackageTarget(
|
|
354
|
+
mount,
|
|
355
|
+
selected.target,
|
|
356
|
+
selected.replacement,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
if (subpath !== null) return { type: "unresolved" };
|
|
362
|
+
target = exportsField;
|
|
363
|
+
}
|
|
364
|
+
return resolvePackageTarget(mount, target, "");
|
|
327
365
|
}
|
|
328
366
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
367
|
+
/** Classify and validate a top-level exports object. */
|
|
368
|
+
function classifyExportsObject(
|
|
369
|
+
mount: string,
|
|
370
|
+
entries: Record<string, unknown>,
|
|
371
|
+
): "subpaths" | "conditions" {
|
|
372
|
+
const keys = Object.keys(entries);
|
|
373
|
+
const subpathKeys = keys.filter((key) => key.startsWith("."));
|
|
374
|
+
if (subpathKeys.length !== 0 && subpathKeys.length !== keys.length) {
|
|
375
|
+
throw new InvalidPackageConfigError(
|
|
376
|
+
`invalid package configuration for ${mount}: exports cannot mix subpath and condition keys`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (subpathKeys.length !== 0) {
|
|
380
|
+
if (subpathKeys.some((key) => key !== "." && !key.startsWith("./"))) {
|
|
381
|
+
throw new InvalidPackageConfigError(
|
|
382
|
+
`invalid package configuration for ${mount}: invalid exports subpath key`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return "subpaths";
|
|
386
|
+
}
|
|
387
|
+
validateConditionKeys(mount, keys);
|
|
388
|
+
return "conditions";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Resolve a string, array, conditional object, or null exports target. */
|
|
392
|
+
function resolvePackageTarget(
|
|
393
|
+
mount: string,
|
|
394
|
+
target: unknown,
|
|
395
|
+
replacement: string,
|
|
396
|
+
): ExportTargetResolution {
|
|
397
|
+
if (typeof target === "string") {
|
|
398
|
+
const substituted = target.split("*").join(replacement);
|
|
399
|
+
return {
|
|
400
|
+
type: "resolved",
|
|
401
|
+
key: resolvePackageTargetKey(mount, substituted),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (target === null) return { type: "blocked" };
|
|
405
|
+
if (Array.isArray(target)) {
|
|
406
|
+
if (target.length === 0) return { type: "blocked" };
|
|
407
|
+
let lastInvalid: InvalidPackageTargetError | undefined;
|
|
408
|
+
let lastBlocked = false;
|
|
409
|
+
for (const candidate of target) {
|
|
410
|
+
try {
|
|
411
|
+
const resolution = resolvePackageTarget(mount, candidate, replacement);
|
|
412
|
+
if (resolution.type === "blocked") {
|
|
413
|
+
lastBlocked = true;
|
|
414
|
+
lastInvalid = undefined;
|
|
415
|
+
} else if (resolution.type === "resolved") {
|
|
416
|
+
// File existence is a later phase and cannot trigger fallback.
|
|
417
|
+
return resolution;
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (!(error instanceof InvalidPackageTargetError)) throw error;
|
|
421
|
+
lastInvalid = error;
|
|
422
|
+
lastBlocked = false;
|
|
423
|
+
}
|
|
336
424
|
}
|
|
337
|
-
|
|
425
|
+
if (lastInvalid !== undefined) throw lastInvalid;
|
|
426
|
+
if (lastBlocked) return { type: "blocked" };
|
|
427
|
+
return { type: "unresolved" };
|
|
338
428
|
}
|
|
339
|
-
if (
|
|
340
|
-
const
|
|
341
|
-
|
|
429
|
+
if (target !== null && typeof target === "object") {
|
|
430
|
+
const conditions = target as Record<string, unknown>;
|
|
431
|
+
const keys = Object.keys(conditions);
|
|
432
|
+
validateConditionKeys(mount, keys);
|
|
433
|
+
for (const [condition, candidate] of Object.entries(conditions)) {
|
|
342
434
|
if (!ACTIVE_EXPORT_CONDITIONS.has(condition)) continue;
|
|
343
|
-
|
|
435
|
+
const resolution = resolvePackageTarget(mount, candidate, replacement);
|
|
436
|
+
if (resolution.type === "unresolved") continue;
|
|
437
|
+
return resolution;
|
|
344
438
|
}
|
|
439
|
+
return { type: "unresolved" };
|
|
440
|
+
}
|
|
441
|
+
throw new InvalidPackageTargetError(
|
|
442
|
+
`invalid package target for ${mount}: expected a relative ./ target`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Reject integer-like condition keys, whose enumeration order is ambiguous. */
|
|
447
|
+
function validateConditionKeys(mount: string, keys: string[]): void {
|
|
448
|
+
if (keys.some(isArrayIndexKey)) {
|
|
449
|
+
throw new InvalidPackageConfigError(
|
|
450
|
+
`invalid package configuration for ${mount}: numeric exports condition keys are not allowed`,
|
|
451
|
+
);
|
|
345
452
|
}
|
|
346
|
-
return null;
|
|
347
453
|
}
|
|
348
454
|
|
|
349
|
-
function
|
|
350
|
-
|
|
455
|
+
function isArrayIndexKey(key: string): boolean {
|
|
456
|
+
if (!/^(?:0|[1-9]\d*)$/.test(key)) return false;
|
|
457
|
+
const value = Number(key);
|
|
458
|
+
return value >= 0 && value < 0xffffffff && String(value) === key;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Resolve one URL-like package target into a normalized pack key.
|
|
463
|
+
*
|
|
464
|
+
* Targets are URL-like package-relative paths. Dot segments, `node_modules`,
|
|
465
|
+
* and encoded path separators cannot escape or reinterpret the mount. URL
|
|
466
|
+
* query/hash components do not participate in filesystem lookup, and pathname
|
|
467
|
+
* percent escapes are decoded exactly once.
|
|
468
|
+
*/
|
|
469
|
+
function resolvePackageTargetKey(mount: string, target: string): string {
|
|
470
|
+
if (!target.startsWith("./")) {
|
|
471
|
+
throw new InvalidPackageTargetError(
|
|
472
|
+
`invalid package target for ${mount}: ${JSON.stringify(target)}`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const pathnameTarget = target.split(/[?#]/, 1)[0]!;
|
|
476
|
+
if (/%(?:2f|5c)/i.test(pathnameTarget)) {
|
|
477
|
+
throw new InvalidPackageTargetLoadError(
|
|
478
|
+
`invalid module specifier for ${mount}: ${JSON.stringify(target)}`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
for (const rawSegment of pathnameTarget.slice(2).split(/[\\/]/)) {
|
|
482
|
+
let decoded: string;
|
|
483
|
+
try {
|
|
484
|
+
decoded = decodeURIComponent(rawSegment);
|
|
485
|
+
} catch {
|
|
486
|
+
throw new InvalidPackageTargetLoadError(
|
|
487
|
+
`invalid module specifier for ${mount}: ${JSON.stringify(target)}`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const normalized = decoded.toLowerCase();
|
|
491
|
+
if (
|
|
492
|
+
normalized === "." ||
|
|
493
|
+
normalized === ".." ||
|
|
494
|
+
normalized === "node_modules" ||
|
|
495
|
+
decoded.includes("/") ||
|
|
496
|
+
decoded.includes("\\")
|
|
497
|
+
) {
|
|
498
|
+
throw new InvalidPackageTargetError(
|
|
499
|
+
`invalid package target for ${mount}: ${JSON.stringify(target)}`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const base = new URL(`https://sandbox.invalid/${mount}/`);
|
|
505
|
+
const resolved = new URL(target, base);
|
|
506
|
+
const basePath = decodeURIComponent(base.pathname);
|
|
507
|
+
const resolvedPath = decodeURIComponent(resolved.pathname).replace(
|
|
508
|
+
/\/+/g,
|
|
509
|
+
"/",
|
|
510
|
+
);
|
|
511
|
+
if (!resolvedPath.startsWith(basePath)) {
|
|
512
|
+
throw new InvalidPackageTargetError(
|
|
513
|
+
`invalid package target for ${mount}: ${JSON.stringify(target)}`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const relative = resolvedPath.slice(basePath.length);
|
|
517
|
+
return `${mount}/${relative}`;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (
|
|
520
|
+
error instanceof InvalidPackageTargetError ||
|
|
521
|
+
error instanceof InvalidPackageTargetLoadError
|
|
522
|
+
)
|
|
523
|
+
throw error;
|
|
524
|
+
throw new InvalidPackageTargetLoadError(
|
|
525
|
+
`invalid module specifier for ${mount}: ${JSON.stringify(target)}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
351
528
|
}
|
|
352
529
|
|
|
353
530
|
function dirname(p: string): string {
|
|
@@ -22,6 +22,19 @@ export interface IPlaygroundDependencyInstallOptions {
|
|
|
22
22
|
ignoredPackages?: Iterable<string>;
|
|
23
23
|
/** Safety cap: error out after installing this many packages. */
|
|
24
24
|
maxPackages?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum compressed bytes accepted for one npm tarball.
|
|
27
|
+
*
|
|
28
|
+
* Defaults to 16 MiB and is enforced while streaming, independent of the
|
|
29
|
+
* response's `Content-Length`.
|
|
30
|
+
*/
|
|
31
|
+
maxTarballBytes?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Maximum expanded tar bytes accepted for one npm package.
|
|
34
|
+
*
|
|
35
|
+
* Defaults to 64 MiB and is enforced while gzip output is streamed.
|
|
36
|
+
*/
|
|
37
|
+
maxUnpackedBytes?: number;
|
|
25
38
|
/** Aborts the install when triggered. */
|
|
26
39
|
signal?: AbortSignal;
|
|
27
40
|
/** Fires for each phase transition during the install. */
|