@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.
- 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
|
@@ -12,6 +12,7 @@ import { installPlaygroundDependencies } from "../npm/installPlaygroundDependenc
|
|
|
12
12
|
import type { ICompilerService } from "../structures/ICompilerService";
|
|
13
13
|
import type { IConsoleMessage } from "../structures/IConsoleMessage";
|
|
14
14
|
import type { IPlaygroundDependencyProgress } from "../structures/IPlaygroundDependencyProgress";
|
|
15
|
+
import type { IPlaygroundInstalledDependency } from "../structures/IPlaygroundInstalledDependency";
|
|
15
16
|
import type { IPlaygroundShellProps } from "../structures/IPlaygroundShellProps";
|
|
16
17
|
import type { ITransformOptions } from "../structures/ITransformOptions";
|
|
17
18
|
import { ConsoleViewer } from "./ConsoleViewer";
|
|
@@ -86,20 +87,14 @@ export function PlaygroundShell({
|
|
|
86
87
|
const dependencyProgressTimer = useRef<number | null>(null);
|
|
87
88
|
const dependencyInstallChain = useRef<Promise<void>>(Promise.resolve());
|
|
88
89
|
const dependencyAbort = useRef<AbortController | null>(null);
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
// has added across the session. A useEffect below merges fresh
|
|
98
|
-
// preinstalledPackages prop values into the ref so a parent that swaps
|
|
99
|
-
// the prop later does not race a now-stale Set.
|
|
100
|
-
const installedDependencyNames = useRef<Set<string>>(
|
|
101
|
-
new Set<string>(preinstalledPackages),
|
|
102
|
-
);
|
|
90
|
+
// Exact mounted identities and active requests. A name-only set cannot
|
|
91
|
+
// validate a later transitive range or distinguish an npm alias target.
|
|
92
|
+
const installedDependencies = useRef<
|
|
93
|
+
Map<string, IPlaygroundInstalledDependency>
|
|
94
|
+
>(new Map());
|
|
95
|
+
// Direct source roots selecting the mounted graph. Removing one requires a
|
|
96
|
+
// full solve and worker replacement so obsolete package files cannot survive.
|
|
97
|
+
const dependencyRoots = useRef<Set<string>>(new Set());
|
|
103
98
|
// Accumulated runtime-file map produced by every successful
|
|
104
99
|
// installPlaygroundDependencies call. Threaded through to executeBundle so
|
|
105
100
|
// the in-page Execute sandbox's require can resolve any npm package the
|
|
@@ -145,17 +140,6 @@ export function PlaygroundShell({
|
|
|
145
140
|
setSource(next);
|
|
146
141
|
}, []);
|
|
147
142
|
|
|
148
|
-
// ── Sync installedDependencyNames + preinstalledPackagesRef on prop change ──
|
|
149
|
-
// The refs capture the initial value on mount; without this effect a
|
|
150
|
-
// parent that swaps `preinstalledPackages` later would race a stale
|
|
151
|
-
// Set against the fresh prop used in `ignoredPackages` below.
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
preinstalledPackagesRef.current = preinstalledPackages;
|
|
154
|
-
for (const name of preinstalledPackages) {
|
|
155
|
-
installedDependencyNames.current.add(name);
|
|
156
|
-
}
|
|
157
|
-
}, [preinstalledPackages]);
|
|
158
|
-
|
|
159
143
|
// ── Decode source from URL on mount ──
|
|
160
144
|
useEffect(() => {
|
|
161
145
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -199,10 +183,11 @@ export function PlaygroundShell({
|
|
|
199
183
|
input,
|
|
200
184
|
preinstalledPackages,
|
|
201
185
|
);
|
|
202
|
-
|
|
203
|
-
(
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
if (
|
|
187
|
+
dependencyRootDelta(firstPassPackageNames, dependencyRoots.current)
|
|
188
|
+
.changed === false
|
|
189
|
+
)
|
|
190
|
+
return null;
|
|
206
191
|
|
|
207
192
|
await wait(DEPENDENCY_INSTALL_QUIET_MS);
|
|
208
193
|
if (sourceVersion.current !== version) return null;
|
|
@@ -211,28 +196,44 @@ export function PlaygroundShell({
|
|
|
211
196
|
latestSource.current,
|
|
212
197
|
preinstalledPackages,
|
|
213
198
|
);
|
|
214
|
-
const
|
|
215
|
-
|
|
199
|
+
const delta = dependencyRootDelta(
|
|
200
|
+
packageNames,
|
|
201
|
+
dependencyRoots.current,
|
|
216
202
|
);
|
|
217
|
-
if (
|
|
203
|
+
if (!delta.changed) return null;
|
|
204
|
+
const replacing = delta.removed.length !== 0;
|
|
205
|
+
const requested = replacing ? packageNames : delta.added;
|
|
218
206
|
|
|
219
207
|
if (dependencyProgressTimer.current !== null) {
|
|
220
208
|
window.clearTimeout(dependencyProgressTimer.current);
|
|
221
209
|
dependencyProgressTimer.current = null;
|
|
222
210
|
}
|
|
223
|
-
setDependencyPackageNames(
|
|
211
|
+
setDependencyPackageNames(requested);
|
|
224
212
|
const abort = new AbortController();
|
|
225
213
|
dependencyAbort.current = abort;
|
|
214
|
+
let workerMutated = false;
|
|
226
215
|
try {
|
|
227
|
-
const installed = await installPlaygroundDependencies(
|
|
228
|
-
|
|
216
|
+
const installed = await installPlaygroundDependencies(requested, {
|
|
217
|
+
installedDependencies: replacing
|
|
218
|
+
? []
|
|
219
|
+
: installedDependencies.current.values(),
|
|
229
220
|
ignoredPackages: preinstalledPackages,
|
|
230
221
|
signal: abort.signal,
|
|
231
222
|
onProgress: setDependencyProgress,
|
|
232
223
|
});
|
|
233
224
|
if (sourceVersion.current !== version) return null;
|
|
225
|
+
|
|
226
|
+
if (replacing) {
|
|
227
|
+
workerMutated = true;
|
|
228
|
+
await client.reset();
|
|
229
|
+
installedDependencies.current = new Map();
|
|
230
|
+
dependencyRoots.current = new Set();
|
|
231
|
+
runtimeDependencyFiles.current = {};
|
|
232
|
+
setEditorExtraLibs({});
|
|
233
|
+
}
|
|
234
234
|
if (Object.keys(installed.compilerFiles).length > 0) {
|
|
235
235
|
const service = await createCompilerService();
|
|
236
|
+
workerMutated = true;
|
|
236
237
|
await service.installDependencies({
|
|
237
238
|
files: installed.compilerFiles,
|
|
238
239
|
packages: installed.packages.map(({ name, version }) => ({
|
|
@@ -241,21 +242,23 @@ export function PlaygroundShell({
|
|
|
241
242
|
})),
|
|
242
243
|
});
|
|
243
244
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
runtimeDependencyFiles.current =
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
installedDependencies.current = new Map(
|
|
246
|
+
installed.resolvedDependencies.map(
|
|
247
|
+
(dependency) => [dependency.name, dependency] as const,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
dependencyRoots.current = new Set(packageNames);
|
|
251
|
+
setEditorExtraLibs((previous) =>
|
|
252
|
+
replacing
|
|
253
|
+
? installed.editorLibs
|
|
254
|
+
: { ...previous, ...installed.editorLibs },
|
|
255
|
+
);
|
|
256
|
+
runtimeDependencyFiles.current = replacing
|
|
257
|
+
? installed.runtimeFiles
|
|
258
|
+
: {
|
|
259
|
+
...runtimeDependencyFiles.current,
|
|
260
|
+
...installed.runtimeFiles,
|
|
261
|
+
};
|
|
259
262
|
dependencyProgressTimer.current = window.setTimeout(() => {
|
|
260
263
|
setDependencyProgress(null);
|
|
261
264
|
setDependencyPackageNames([]);
|
|
@@ -263,6 +266,13 @@ export function PlaygroundShell({
|
|
|
263
266
|
}, 350);
|
|
264
267
|
return null;
|
|
265
268
|
} catch (error) {
|
|
269
|
+
if (workerMutated) {
|
|
270
|
+
await client.reset();
|
|
271
|
+
installedDependencies.current = new Map();
|
|
272
|
+
dependencyRoots.current = new Set();
|
|
273
|
+
runtimeDependencyFiles.current = {};
|
|
274
|
+
setEditorExtraLibs({});
|
|
275
|
+
}
|
|
266
276
|
if (isAbortError(error)) {
|
|
267
277
|
setDependencyProgress(null);
|
|
268
278
|
setDependencyPackageNames([]);
|
|
@@ -270,9 +280,9 @@ export function PlaygroundShell({
|
|
|
270
280
|
}
|
|
271
281
|
setDependencyProgress({
|
|
272
282
|
phase: "error",
|
|
273
|
-
packageName:
|
|
283
|
+
packageName: requested[0],
|
|
274
284
|
completed: 0,
|
|
275
|
-
total:
|
|
285
|
+
total: requested.length,
|
|
276
286
|
message: describeUnknownError(error),
|
|
277
287
|
});
|
|
278
288
|
dependencyProgressTimer.current = window.setTimeout(() => {
|
|
@@ -288,7 +298,7 @@ export function PlaygroundShell({
|
|
|
288
298
|
dependencyInstallChain.current = task.then(() => {});
|
|
289
299
|
return task;
|
|
290
300
|
},
|
|
291
|
-
[createCompilerService, preinstalledPackages],
|
|
301
|
+
[client, createCompilerService, preinstalledPackages],
|
|
292
302
|
);
|
|
293
303
|
|
|
294
304
|
// ── Run compile when source / options change ──
|
|
@@ -412,28 +422,18 @@ export function PlaygroundShell({
|
|
|
412
422
|
// from the playground leaks one Worker per mount; a workerUrl swap
|
|
413
423
|
// leaks the previous Worker forever.
|
|
414
424
|
//
|
|
415
|
-
// Reset the dependency
|
|
416
|
-
//
|
|
417
|
-
// only `preinstalledPackages` mounted, so carrying the old names
|
|
418
|
-
// would make installDependenciesForSource skip the install (because
|
|
419
|
-
// `installedDependencyNames.current.has(name)` is still true) and
|
|
420
|
-
// the next compile would fail with `Cannot find module`.
|
|
425
|
+
// Reset the dependency graph too. Its exact versions, requests, roots, and
|
|
426
|
+
// runtime files describe only the worker generation that is being closed.
|
|
421
427
|
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// current value through `preinstalledPackagesRef`, which the sync
|
|
426
|
-
// effect above keeps up to date.
|
|
427
|
-
useEffect(
|
|
428
|
-
() => () => {
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
setEditorExtraLibs({});
|
|
430
|
+
return () => {
|
|
429
431
|
void client.reset();
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
432
|
+
installedDependencies.current = new Map();
|
|
433
|
+
dependencyRoots.current = new Set();
|
|
433
434
|
runtimeDependencyFiles.current = {};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
);
|
|
435
|
+
};
|
|
436
|
+
}, [client]);
|
|
437
437
|
|
|
438
438
|
const onPickExample = useCallback(
|
|
439
439
|
(id: string) => {
|
|
@@ -843,6 +843,20 @@ function wait(ms: number): Promise<void> {
|
|
|
843
843
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
function dependencyRootDelta(
|
|
847
|
+
current: readonly string[],
|
|
848
|
+
previous: ReadonlySet<string>,
|
|
849
|
+
): { added: string[]; changed: boolean; removed: string[] } {
|
|
850
|
+
const next = new Set(current);
|
|
851
|
+
const added = current.filter((name) => !previous.has(name));
|
|
852
|
+
const removed = [...previous].filter((name) => !next.has(name));
|
|
853
|
+
return {
|
|
854
|
+
added,
|
|
855
|
+
changed: added.length !== 0 || removed.length !== 0,
|
|
856
|
+
removed,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
846
860
|
function createAbortError(reason: string): Error {
|
|
847
861
|
const error = new Error(`Dependency install aborted: ${reason}.`);
|
|
848
862
|
error.name = "AbortError";
|
|
@@ -9,72 +9,58 @@ import type { ICreateCompilerClientOptions } from "../structures/ICreateCompiler
|
|
|
9
9
|
* UI-side singleton: connect to the playground worker over tgrid and return the
|
|
10
10
|
* typed `ICompilerService` driver.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* One connection generation owns the cached promise and connector. A reset
|
|
13
|
+
* invalidates that generation before awaiting it, so a late settlement cannot
|
|
14
|
+
* replace a newer connection or clear its retry state.
|
|
15
15
|
*/
|
|
16
16
|
export function createCompilerClient(options: ICreateCompilerClientOptions): {
|
|
17
17
|
connect(): Promise<ICompilerService>;
|
|
18
18
|
reset(): Promise<void>;
|
|
19
19
|
} {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let activeConnector: WorkerConnector<null, null, null> | null = null;
|
|
27
|
-
let pendingConnector: Promise<WorkerConnector<null, null, null>> | null =
|
|
28
|
-
null;
|
|
20
|
+
type Connection = {
|
|
21
|
+
connector: WorkerConnector<null, null, null>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
promise: Promise<ICompilerService>;
|
|
24
|
+
};
|
|
25
|
+
let current: Connection | null = null;
|
|
29
26
|
|
|
30
27
|
return {
|
|
31
28
|
connect(): Promise<ICompilerService> {
|
|
32
|
-
if (
|
|
29
|
+
if (current) return current.promise;
|
|
33
30
|
const connector: WorkerConnector<null, null, null> = new WorkerConnector(
|
|
34
31
|
null,
|
|
35
32
|
null,
|
|
36
33
|
);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
34
|
+
let closePromise: Promise<void> | null = null;
|
|
35
|
+
const connection = {} as Connection;
|
|
36
|
+
current = connection;
|
|
37
|
+
connection.connector = connector;
|
|
38
|
+
connection.close = () =>
|
|
39
|
+
(closePromise ??= Promise.resolve()
|
|
40
|
+
.then(() => connector.close())
|
|
41
|
+
.catch(() => {}));
|
|
42
|
+
connection.promise = Promise.resolve()
|
|
43
|
+
.then(() => connector.connect(options.workerUrl))
|
|
44
|
+
.then(() => connector.getDriver() as unknown as ICompilerService)
|
|
45
|
+
.catch(async (error: unknown) => {
|
|
46
|
+
if (current === connection) {
|
|
47
|
+
current = null;
|
|
48
|
+
await connection.close();
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
});
|
|
52
|
+
return connection.promise;
|
|
54
53
|
},
|
|
55
54
|
async reset(): Promise<void> {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// next connect attempt. Cover both code paths — the in-flight
|
|
64
|
-
// connector (await it first, then close) and the resolved active
|
|
65
|
-
// connector. Either may be null; both may be the same instance
|
|
66
|
-
// after a successful connect().
|
|
67
|
-
if (inflight) {
|
|
68
|
-
try {
|
|
69
|
-
const c = await inflight;
|
|
70
|
-
if (c !== active) await c.close().catch(() => {});
|
|
71
|
-
} catch {
|
|
72
|
-
// connect() rejected — nothing to close.
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (active) {
|
|
76
|
-
await active.close().catch(() => {});
|
|
55
|
+
const invalidated = current;
|
|
56
|
+
current = null;
|
|
57
|
+
if (!invalidated) return;
|
|
58
|
+
try {
|
|
59
|
+
await invalidated.promise;
|
|
60
|
+
} catch {
|
|
61
|
+
// A rejected connection never became usable.
|
|
77
62
|
}
|
|
63
|
+
await invalidated.close();
|
|
78
64
|
},
|
|
79
65
|
};
|
|
80
66
|
}
|
|
@@ -30,6 +30,12 @@ interface ISandboxRequireOptions {
|
|
|
30
30
|
| Record<string, (...args: unknown[]) => void>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// The sandbox evaluates CommonJS through a `require` wrapper in a browser. It
|
|
34
|
+
// therefore activates `require`; `default` is always available as the portable
|
|
35
|
+
// fallback. `node` and `import` stay inactive because the sandbox supplies
|
|
36
|
+
// neither Node's runtime nor an ESM evaluator.
|
|
37
|
+
const ACTIVE_EXPORT_CONDITIONS = new Set(["require", "default"]);
|
|
38
|
+
|
|
33
39
|
/**
|
|
34
40
|
* Build a sandboxed `require` function over a runtime pack. Resolves typia /
|
|
35
41
|
* `@typia/*` / randexp specifiers from the pack; throws on anything else so the
|
|
@@ -50,6 +56,35 @@ export function createSandboxRequire(
|
|
|
50
56
|
return null;
|
|
51
57
|
};
|
|
52
58
|
|
|
59
|
+
const packageDeclaresExports = (pkg: string): boolean => {
|
|
60
|
+
const key = `${pkg}/package.json`;
|
|
61
|
+
if (!has(key)) return false;
|
|
62
|
+
try {
|
|
63
|
+
return Object.prototype.hasOwnProperty.call(
|
|
64
|
+
JSON.parse(pack[key]!) as IPackJson,
|
|
65
|
+
"exports",
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const resolveExportTarget = (
|
|
73
|
+
pkg: string,
|
|
74
|
+
target: unknown,
|
|
75
|
+
replacement = "",
|
|
76
|
+
): string | null => {
|
|
77
|
+
const candidates = conditionalExportTargets(target);
|
|
78
|
+
if (!candidates) return null;
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
const resolved = `${pkg}/${stripDotSlash(
|
|
81
|
+
candidate.split("*").join(replacement),
|
|
82
|
+
)}`;
|
|
83
|
+
if (has(resolved)) return resolved;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
|
|
53
88
|
// Read package.json from pack and resolve via main/exports.
|
|
54
89
|
const resolvePackageEntry = (
|
|
55
90
|
pkg: string,
|
|
@@ -66,12 +101,8 @@ export function createSandboxRequire(
|
|
|
66
101
|
if (subpath === null) {
|
|
67
102
|
// bare "name": honor the root `exports` entry (string, subpath table
|
|
68
103
|
// "." key, or bare condition map) → CJS target, else main, else index.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (r) {
|
|
72
|
-
const resolved = `${pkg}/${stripDotSlash(r)}`;
|
|
73
|
-
return has(resolved) ? resolved : null;
|
|
74
|
-
}
|
|
104
|
+
if (pj.exports !== undefined)
|
|
105
|
+
return resolveExportTarget(pkg, rootExportTarget(pj.exports));
|
|
75
106
|
if (typeof pj.main === "string") {
|
|
76
107
|
return tryPaths(
|
|
77
108
|
`${pkg}/${stripDotSlash(pj.main)}`,
|
|
@@ -85,27 +116,36 @@ export function createSandboxRequire(
|
|
|
85
116
|
}
|
|
86
117
|
return tryPaths(`${pkg}/index.js`, `${pkg}/index.cjs`);
|
|
87
118
|
}
|
|
88
|
-
// Subpath: honor exports
|
|
119
|
+
// Subpath: honor exact exports and general single-star patterns.
|
|
89
120
|
const exportsAny = pj.exports;
|
|
90
|
-
if (
|
|
121
|
+
if (
|
|
122
|
+
exportsAny !== undefined &&
|
|
123
|
+
typeof exportsAny === "object" &&
|
|
124
|
+
exportsAny !== null
|
|
125
|
+
) {
|
|
91
126
|
const entries = exportsAny as Record<string, unknown>;
|
|
92
127
|
// Exact match first.
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
const exactKey = `./${subpath}`;
|
|
129
|
+
if (
|
|
130
|
+
Object.prototype.hasOwnProperty.call(entries, exactKey) &&
|
|
131
|
+
!exactKey.includes("*") &&
|
|
132
|
+
!exactKey.endsWith("/")
|
|
133
|
+
)
|
|
134
|
+
return resolveExportTarget(pkg, entries[exactKey]);
|
|
135
|
+
// Node resolves the most-specific wildcard, not insertion order.
|
|
136
|
+
const patterns = Object.entries(entries)
|
|
137
|
+
.map(([pattern, target]) => ({
|
|
138
|
+
pattern,
|
|
139
|
+
replacement: exportPatternReplacement(pattern, exactKey),
|
|
140
|
+
target,
|
|
141
|
+
}))
|
|
142
|
+
.filter(
|
|
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);
|
|
109
149
|
}
|
|
110
150
|
}
|
|
111
151
|
return null;
|
|
@@ -136,7 +176,10 @@ export function createSandboxRequire(
|
|
|
136
176
|
if (subpath === null) {
|
|
137
177
|
return resolvePackageEntry(pkg, null);
|
|
138
178
|
}
|
|
139
|
-
//
|
|
179
|
+
// A declared exports map is the public boundary. Only packages without one
|
|
180
|
+
// retain the historical packed-file fallback.
|
|
181
|
+
if (packageDeclaresExports(pkg)) return resolvePackageEntry(pkg, subpath);
|
|
182
|
+
// First try direct paths (covers packages that do not declare exports).
|
|
140
183
|
const direct = tryPaths(
|
|
141
184
|
`${pkg}/${subpath}`,
|
|
142
185
|
`${pkg}/${subpath}.js`,
|
|
@@ -158,22 +201,22 @@ export function createSandboxRequire(
|
|
|
158
201
|
const mod: ModuleObj = { exports: {} };
|
|
159
202
|
// Cache before evaluation so cyclic requires see the partial exports.
|
|
160
203
|
cache.set(key, mod);
|
|
161
|
-
if (key.endsWith(".json")) {
|
|
162
|
-
mod.exports = JSON.parse(code) as unknown;
|
|
163
|
-
return mod;
|
|
164
|
-
}
|
|
165
|
-
const localRequire = (specifier: string): unknown => {
|
|
166
|
-
const resolved = resolveSpecifier(specifier, key);
|
|
167
|
-
if (!resolved) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`require("${specifier}") is not available in the playground sandbox (from ${key})`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
return evaluate(resolved).exports;
|
|
173
|
-
};
|
|
174
|
-
const filename = "/sandbox/" + key;
|
|
175
|
-
const dir = "/sandbox/" + dirname(key);
|
|
176
204
|
try {
|
|
205
|
+
if (key.endsWith(".json")) {
|
|
206
|
+
mod.exports = JSON.parse(code) as unknown;
|
|
207
|
+
return mod;
|
|
208
|
+
}
|
|
209
|
+
const localRequire = (specifier: string): unknown => {
|
|
210
|
+
const resolved = resolveSpecifier(specifier, key);
|
|
211
|
+
if (!resolved) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`require("${specifier}") is not available in the playground sandbox (from ${key})`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return evaluate(resolved).exports;
|
|
217
|
+
};
|
|
218
|
+
const filename = "/sandbox/" + key;
|
|
219
|
+
const dir = "/sandbox/" + dirname(key);
|
|
177
220
|
const factory = new Function(
|
|
178
221
|
"require",
|
|
179
222
|
"module",
|
|
@@ -198,13 +241,18 @@ export function createSandboxRequire(
|
|
|
198
241
|
filename,
|
|
199
242
|
opts.console,
|
|
200
243
|
);
|
|
244
|
+
return mod;
|
|
201
245
|
} catch (err) {
|
|
246
|
+
// The entry is provisional until evaluation succeeds. Preserve a future
|
|
247
|
+
// replacement rather than evicting by key alone.
|
|
248
|
+
if (cache.get(key) === mod) {
|
|
249
|
+
cache.delete(key);
|
|
250
|
+
}
|
|
202
251
|
// Surface eval-time errors with context so debugging the sandbox is
|
|
203
252
|
// easier when typia ships a module that depends on something missing.
|
|
204
253
|
const message = err instanceof Error ? err.message : String(err);
|
|
205
254
|
throw new Error(`evaluating ${key}: ${message}`);
|
|
206
255
|
}
|
|
207
|
-
return mod;
|
|
208
256
|
};
|
|
209
257
|
|
|
210
258
|
return (specifier: string): unknown => {
|
|
@@ -218,10 +266,45 @@ export function createSandboxRequire(
|
|
|
218
266
|
};
|
|
219
267
|
}
|
|
220
268
|
|
|
269
|
+
/** Capture the middle of one valid single-star exports key. */
|
|
270
|
+
function exportPatternReplacement(
|
|
271
|
+
pattern: string,
|
|
272
|
+
subpath: string,
|
|
273
|
+
): string | undefined {
|
|
274
|
+
const star = pattern.indexOf("*");
|
|
275
|
+
if (
|
|
276
|
+
!pattern.startsWith("./") ||
|
|
277
|
+
star === -1 ||
|
|
278
|
+
pattern.indexOf("*", star + 1) !== -1
|
|
279
|
+
) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
const prefix = pattern.slice(0, star);
|
|
283
|
+
const suffix = pattern.slice(star + 1);
|
|
284
|
+
if (
|
|
285
|
+
subpath.length < pattern.length ||
|
|
286
|
+
!subpath.startsWith(prefix) ||
|
|
287
|
+
!subpath.endsWith(suffix)
|
|
288
|
+
) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
return subpath.slice(prefix.length, subpath.length - suffix.length);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Node exports patterns rank longer prefixes, then longer full keys, first. */
|
|
295
|
+
function compareExportPatternKeys(left: string, right: string): number {
|
|
296
|
+
const leftPrefix = left.indexOf("*");
|
|
297
|
+
const rightPrefix = right.indexOf("*");
|
|
298
|
+
if (leftPrefix !== rightPrefix) {
|
|
299
|
+
return rightPrefix - leftPrefix;
|
|
300
|
+
}
|
|
301
|
+
return right.length - left.length;
|
|
302
|
+
}
|
|
303
|
+
|
|
221
304
|
/**
|
|
222
305
|
* Reduce a `package.json` `exports` field to the value describing its ROOT
|
|
223
|
-
* (".") entry, ready for {@link
|
|
224
|
-
* root shapes and they must all resolve consistently:
|
|
306
|
+
* (".") entry, ready for {@link conditionalExportTargets}. Node accepts three
|
|
307
|
+
* valid root shapes and they must all resolve consistently:
|
|
225
308
|
*
|
|
226
309
|
* - A bare string target — `"exports": "./index.cjs"`;
|
|
227
310
|
* - A subpath table keyed by "." — `{ ".": <target>, "./sub": ... }`;
|
|
@@ -243,14 +326,22 @@ function rootExportTarget(exports: unknown): unknown {
|
|
|
243
326
|
return hasSubpathKey ? obj["."] : obj;
|
|
244
327
|
}
|
|
245
328
|
|
|
246
|
-
function
|
|
247
|
-
if (typeof value === "string") return value;
|
|
329
|
+
function conditionalExportTargets(value: unknown): string[] | null {
|
|
330
|
+
if (typeof value === "string") return [value];
|
|
331
|
+
if (Array.isArray(value)) {
|
|
332
|
+
const candidates: string[] = [];
|
|
333
|
+
for (const target of value) {
|
|
334
|
+
const resolved = conditionalExportTargets(target);
|
|
335
|
+
if (resolved) candidates.push(...resolved);
|
|
336
|
+
}
|
|
337
|
+
return candidates.length === 0 ? null : candidates;
|
|
338
|
+
}
|
|
248
339
|
if (value && typeof value === "object") {
|
|
249
340
|
const obj = value as Record<string, unknown>;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
341
|
+
for (const [condition, target] of Object.entries(obj)) {
|
|
342
|
+
if (!ACTIVE_EXPORT_CONDITIONS.has(condition)) continue;
|
|
343
|
+
return conditionalExportTargets(target);
|
|
344
|
+
}
|
|
254
345
|
}
|
|
255
346
|
return null;
|
|
256
347
|
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { IPlaygroundDependencyProgress } from "./IPlaygroundDependencyProgress";
|
|
2
|
+
import type { IPlaygroundInstalledDependency } from "./IPlaygroundInstalledDependency";
|
|
2
3
|
|
|
3
4
|
/** Options for {@link installPlaygroundDependencies}. */
|
|
4
5
|
export interface IPlaygroundDependencyInstallOptions {
|
|
5
6
|
/** Defaults to `globalThis.fetch`. Override for tests or for offline runs. */
|
|
6
7
|
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Exact package identities already mounted in this session.
|
|
10
|
+
*
|
|
11
|
+
* New edges are reconciled against their versions, registry targets, and
|
|
12
|
+
* active requests before a tarball is reused.
|
|
13
|
+
*/
|
|
14
|
+
installedDependencies?: Iterable<IPlaygroundInstalledDependency>;
|
|
15
|
+
/**
|
|
16
|
+
* Legacy name-only skip list.
|
|
17
|
+
*
|
|
18
|
+
* Prefer `installedDependencies`; names alone cannot validate later ranges.
|
|
19
|
+
*/
|
|
8
20
|
installedPackages?: Iterable<string>;
|
|
9
21
|
/** Package names to never install (preinstalled / built-in). */
|
|
10
22
|
ignoredPackages?: Iterable<string>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { IPlaygroundDependencyPackage } from "./IPlaygroundDependencyPackage";
|
|
2
|
+
import type { IPlaygroundInstalledDependency } from "./IPlaygroundInstalledDependency";
|
|
2
3
|
|
|
3
4
|
/** Aggregate result returned by {@link installPlaygroundDependencies}. */
|
|
4
5
|
export interface IPlaygroundDependencyInstallResult {
|
|
6
|
+
/** Complete exact state after merging installed packages with this call. */
|
|
7
|
+
resolvedDependencies: IPlaygroundInstalledDependency[];
|
|
8
|
+
/** Packages whose tarballs were downloaded and unpacked by this call. */
|
|
5
9
|
packages: IPlaygroundDependencyPackage[];
|
|
6
10
|
/**
|
|
7
11
|
* `node_modules/...` keyed map of files to mount inside the wasm-side
|