@ttsc/playground 0.19.3 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -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
- // Mirror `preinstalledPackages` into a ref so the worker-teardown
90
- // effect can read the current value without taking the prop as a
91
- // dep listing the array prop in deps would tear down the worker on
92
- // every parent re-render that produces a fresh array reference.
93
- const preinstalledPackagesRef =
94
- useRef<readonly string[]>(preinstalledPackages);
95
- // The ref tracks names the wasm MemFS already has preinstalled at boot
96
- // (via `preinstalledPackages`) plus everything `installPlaygroundDependencies`
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
- const firstPassMissing = firstPassPackageNames.filter(
203
- (name) => !installedDependencyNames.current.has(name),
204
- );
205
- if (firstPassMissing.length === 0) return null;
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 missing = packageNames.filter(
215
- (name) => !installedDependencyNames.current.has(name),
199
+ const delta = dependencyRootDelta(
200
+ packageNames,
201
+ dependencyRoots.current,
216
202
  );
217
- if (missing.length === 0) return null;
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(missing);
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(missing, {
228
- installedPackages: installedDependencyNames.current,
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
- for (const pkg of installed.packages) {
245
- installedDependencyNames.current.add(pkg.name);
246
- }
247
- if (Object.keys(installed.editorLibs).length > 0) {
248
- setEditorExtraLibs((prev) => ({
249
- ...prev,
250
- ...installed.editorLibs,
251
- }));
252
- }
253
- // Accumulate runtime files so the in-page Execute sandbox can
254
- // resolve every package the user installed across this session.
255
- runtimeDependencyFiles.current = {
256
- ...runtimeDependencyFiles.current,
257
- ...installed.runtimeFiles,
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: missing[0],
283
+ packageName: requested[0],
274
284
  completed: 0,
275
- total: missing.length,
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-tracking refs too. They named packages that
416
- // existed in the previous worker's MemFS; the fresh worker boots with
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
- // The effect depends on `[client]` ONLY — listing the array prop
423
- // `preinstalledPackages` in deps would tear down the worker on every
424
- // parent re-render that produces a fresh array reference. We read the
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
- installedDependencyNames.current = new Set<string>(
431
- preinstalledPackagesRef.current,
432
- );
432
+ installedDependencies.current = new Map();
433
+ dependencyRoots.current = new Set();
433
434
  runtimeDependencyFiles.current = {};
434
- },
435
- [client],
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
- * The promise is cached; the first call boots, every subsequent call shares the
13
- * same connection. If the boot rejects the cache is cleared so the next call
14
- * retries otherwise every retry would resolve to the same rejection.
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
- let connectionPromise: Promise<ICompilerService> | null = null;
21
- // Track the active connector AND the in-flight connector promise so
22
- // reset() can dispose either one — disposing only the resolved
23
- // `activeConnector` leaks the connector when reset() is called during
24
- // an in-flight `await connector.connect(...)` (the connector reference
25
- // exists but isn't yet assigned to activeConnector).
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 (connectionPromise) return connectionPromise;
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
- const connectPromise = connector
38
- .connect(options.workerUrl)
39
- .then(() => connector);
40
- pendingConnector = connectPromise;
41
- connectionPromise = (async () => {
42
- try {
43
- await connectPromise;
44
- } catch (err) {
45
- connectionPromise = null;
46
- if (pendingConnector === connectPromise) pendingConnector = null;
47
- throw err;
48
- }
49
- if (pendingConnector === connectPromise) pendingConnector = null;
50
- activeConnector = connector;
51
- return connector.getDriver() as unknown as ICompilerService;
52
- })();
53
- return connectionPromise;
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 inflight = pendingConnector;
57
- const active = activeConnector;
58
- connectionPromise = null;
59
- pendingConnector = null;
60
- activeConnector = null;
61
- // Swallow close errors: the worker is already being torn down,
62
- // and a Retry that fails to close cleanly should not block the
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
- const root = rootExportTarget(pj.exports);
70
- const r = pickConditionalExport(root);
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["./subpath"] or exports["./subpath/*"] patterns.
119
+ // Subpath: honor exact exports and general single-star patterns.
89
120
  const exportsAny = pj.exports;
90
- if (typeof exportsAny === "object" && exportsAny !== null) {
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 exact = entries[`./${subpath}`];
94
- const ex = pickConditionalExport(exact);
95
- if (ex) {
96
- const resolved = `${pkg}/${stripDotSlash(ex)}`;
97
- if (has(resolved)) return resolved;
98
- }
99
- // Wildcard match.
100
- for (const [pattern, target] of Object.entries(entries)) {
101
- if (!pattern.endsWith("/*")) continue;
102
- const prefix = pattern.slice(2, -1); // strip "./" and trailing "*"
103
- if (!subpath.startsWith(prefix)) continue;
104
- const rest = subpath.slice(prefix.length);
105
- const targetStr = pickConditionalExport(target);
106
- if (!targetStr) continue;
107
- const resolved = `${pkg}/${stripDotSlash(targetStr.replace("*", rest))}`;
108
- if (has(resolved)) return resolved;
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
- // First try direct paths (covers the common typia/lib/internal/X case).
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 pickConditionalExport}. Node accepts three valid
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 pickConditionalExport(value: unknown): string | null {
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
- // Prefer require (CJS) > default > node.
251
- const pick = obj.require ?? obj.default ?? obj.node;
252
- if (typeof pick === "string") return pick;
253
- if (pick && typeof pick === "object") return pickConditionalExport(pick);
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
- /** Package names the wasm already has — skip re-installing them. */
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