sandlot 0.1.4 → 0.2.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 (115) hide show
  1. package/dist/browser/bundler.d.ts +68 -0
  2. package/dist/browser/bundler.d.ts.map +1 -0
  3. package/dist/browser/executor.d.ts +46 -0
  4. package/dist/browser/executor.d.ts.map +1 -0
  5. package/dist/browser/index.d.ts +9 -0
  6. package/dist/browser/index.d.ts.map +1 -0
  7. package/dist/browser/index.js +2690 -0
  8. package/dist/browser/preset.d.ts +63 -0
  9. package/dist/browser/preset.d.ts.map +1 -0
  10. package/dist/commands/index.d.ts +20 -11
  11. package/dist/commands/index.d.ts.map +1 -1
  12. package/dist/commands/types.d.ts +37 -130
  13. package/dist/commands/types.d.ts.map +1 -1
  14. package/dist/core/bundler-utils.d.ts +142 -0
  15. package/dist/core/bundler-utils.d.ts.map +1 -0
  16. package/dist/core/esm-types-resolver.d.ts +125 -0
  17. package/dist/core/esm-types-resolver.d.ts.map +1 -0
  18. package/dist/core/executor.d.ts +35 -0
  19. package/dist/core/executor.d.ts.map +1 -0
  20. package/dist/{fs.d.ts → core/fs.d.ts} +27 -29
  21. package/dist/core/fs.d.ts.map +1 -0
  22. package/dist/core/sandbox.d.ts +30 -0
  23. package/dist/core/sandbox.d.ts.map +1 -0
  24. package/dist/core/sandlot.d.ts +30 -0
  25. package/dist/core/sandlot.d.ts.map +1 -0
  26. package/dist/core/shared-module-registry.d.ts +46 -0
  27. package/dist/core/shared-module-registry.d.ts.map +1 -0
  28. package/dist/core/typechecker.d.ts +60 -0
  29. package/dist/core/typechecker.d.ts.map +1 -0
  30. package/dist/index.d.ts +11 -16
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1398 -2010
  33. package/dist/node/bundler.d.ts +48 -0
  34. package/dist/node/bundler.d.ts.map +1 -0
  35. package/dist/node/executor.d.ts +48 -0
  36. package/dist/node/executor.d.ts.map +1 -0
  37. package/dist/node/index.d.ts +9 -0
  38. package/dist/node/index.d.ts.map +1 -0
  39. package/dist/node/index.js +2644 -0
  40. package/dist/node/preset.d.ts +62 -0
  41. package/dist/node/preset.d.ts.map +1 -0
  42. package/dist/types.d.ts +528 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/package.json +16 -6
  45. package/src/browser/bundler.ts +294 -0
  46. package/src/browser/executor.ts +71 -0
  47. package/src/browser/index.ts +57 -0
  48. package/src/browser/preset.ts +179 -0
  49. package/src/commands/index.ts +498 -37
  50. package/src/commands/types.ts +117 -145
  51. package/src/core/bundler-utils.ts +630 -0
  52. package/src/core/esm-types-resolver.ts +432 -0
  53. package/src/core/executor.ts +161 -0
  54. package/src/{fs.ts → core/fs.ts} +59 -37
  55. package/src/core/sandbox.ts +624 -0
  56. package/src/core/sandlot.ts +77 -0
  57. package/src/core/shared-module-registry.ts +138 -0
  58. package/src/core/typechecker.ts +609 -0
  59. package/src/index.ts +106 -139
  60. package/src/node/bundler.ts +194 -0
  61. package/src/node/executor.ts +87 -0
  62. package/src/node/index.ts +39 -0
  63. package/src/node/preset.ts +178 -0
  64. package/src/types.ts +672 -0
  65. package/README.md +0 -243
  66. package/dist/build-emitter.d.ts +0 -47
  67. package/dist/build-emitter.d.ts.map +0 -1
  68. package/dist/builder.d.ts +0 -370
  69. package/dist/builder.d.ts.map +0 -1
  70. package/dist/bundler.d.ts +0 -152
  71. package/dist/bundler.d.ts.map +0 -1
  72. package/dist/commands/compile.d.ts +0 -13
  73. package/dist/commands/compile.d.ts.map +0 -1
  74. package/dist/commands/packages.d.ts +0 -17
  75. package/dist/commands/packages.d.ts.map +0 -1
  76. package/dist/commands/run.d.ts +0 -40
  77. package/dist/commands/run.d.ts.map +0 -1
  78. package/dist/commands.d.ts +0 -179
  79. package/dist/commands.d.ts.map +0 -1
  80. package/dist/fs.d.ts.map +0 -1
  81. package/dist/internal.d.ts +0 -79
  82. package/dist/internal.d.ts.map +0 -1
  83. package/dist/internal.js +0 -1942
  84. package/dist/loader.d.ts +0 -164
  85. package/dist/loader.d.ts.map +0 -1
  86. package/dist/packages.d.ts +0 -199
  87. package/dist/packages.d.ts.map +0 -1
  88. package/dist/runner.d.ts +0 -314
  89. package/dist/runner.d.ts.map +0 -1
  90. package/dist/sandbox-manager.d.ts +0 -261
  91. package/dist/sandbox-manager.d.ts.map +0 -1
  92. package/dist/sandbox.d.ts +0 -267
  93. package/dist/sandbox.d.ts.map +0 -1
  94. package/dist/shared-modules.d.ts +0 -148
  95. package/dist/shared-modules.d.ts.map +0 -1
  96. package/dist/shared-resources.d.ts +0 -102
  97. package/dist/shared-resources.d.ts.map +0 -1
  98. package/dist/ts-libs.d.ts +0 -85
  99. package/dist/ts-libs.d.ts.map +0 -1
  100. package/dist/typechecker.d.ts +0 -127
  101. package/dist/typechecker.d.ts.map +0 -1
  102. package/src/build-emitter.ts +0 -64
  103. package/src/builder.ts +0 -498
  104. package/src/bundler.ts +0 -575
  105. package/src/commands/compile.ts +0 -236
  106. package/src/commands/packages.ts +0 -154
  107. package/src/commands/run.ts +0 -245
  108. package/src/internal.ts +0 -119
  109. package/src/loader.ts +0 -229
  110. package/src/packages.ts +0 -936
  111. package/src/sandbox.ts +0 -398
  112. package/src/shared-modules.ts +0 -280
  113. package/src/shared-resources.ts +0 -166
  114. package/src/ts-libs.ts +0 -218
  115. package/src/typechecker.ts +0 -635
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Types Resolver - Fetches TypeScript type definitions for npm packages.
3
+ *
4
+ * This is a platform-independent implementation that works anywhere with fetch().
5
+ * (Browser, Node 18+, Bun, Deno, Cloudflare Workers)
6
+ *
7
+ * Design principles:
8
+ * 1. Single responsibility: resolve package → types. No VFS writing.
9
+ * 2. CDN-agnostic interface with esm.sh implementation
10
+ * 3. Transparent @types fallback (caller doesn't need to know)
11
+ * 4. Subpaths resolved on-demand, not pre-fetched
12
+ * 5. Caching is external/injectable (platform-specific cache implementations)
13
+ */
14
+
15
+ import type { ITypesResolver } from "../types";
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Resolved type definitions for a package.
23
+ */
24
+ export interface ResolvedTypes {
25
+ /**
26
+ * The package name (may differ from request if @types fallback was used)
27
+ */
28
+ packageName: string;
29
+
30
+ /**
31
+ * The resolved version
32
+ */
33
+ version: string;
34
+
35
+ /**
36
+ * Map of relative file paths to content.
37
+ * Paths are relative to the package root (e.g., "index.d.ts", "utils.d.ts")
38
+ */
39
+ files: Record<string, string>;
40
+
41
+ /**
42
+ * Whether types came from @types/* package
43
+ */
44
+ fromTypesPackage: boolean;
45
+ }
46
+
47
+ /**
48
+ * Cache interface for type definitions.
49
+ * Implementations can be in-memory, IndexedDB, KV store, filesystem, etc.
50
+ */
51
+ export interface ITypesCache {
52
+ get(key: string): Promise<ResolvedTypes | null>;
53
+ set(key: string, value: ResolvedTypes): Promise<void>;
54
+ has(key: string): Promise<boolean>;
55
+ }
56
+
57
+ /**
58
+ * Simple in-memory cache implementation.
59
+ * Works on all platforms.
60
+ */
61
+ export class InMemoryTypesCache implements ITypesCache {
62
+ private cache = new Map<string, ResolvedTypes>();
63
+
64
+ async get(key: string): Promise<ResolvedTypes | null> {
65
+ return this.cache.get(key) ?? null;
66
+ }
67
+
68
+ async set(key: string, value: ResolvedTypes): Promise<void> {
69
+ this.cache.set(key, value);
70
+ }
71
+
72
+ async has(key: string): Promise<boolean> {
73
+ return this.cache.has(key);
74
+ }
75
+
76
+ clear(): void {
77
+ this.cache.clear();
78
+ }
79
+ }
80
+
81
+ // =============================================================================
82
+ // EsmTypesResolver
83
+ // =============================================================================
84
+
85
+ export interface EsmTypesResolverOptions {
86
+ /**
87
+ * Base URL for esm.sh (default: "https://esm.sh")
88
+ */
89
+ baseUrl?: string;
90
+
91
+ /**
92
+ * External cache. If not provided, no caching is done.
93
+ * This allows sharing cache across resolver instances.
94
+ */
95
+ cache?: ITypesCache;
96
+
97
+ /**
98
+ * Whether to try @types/* packages as fallback when main package
99
+ * doesn't have bundled types. Default: true
100
+ */
101
+ tryTypesPackages?: boolean;
102
+ }
103
+
104
+ /**
105
+ * Types resolver using esm.sh CDN.
106
+ *
107
+ * Platform-independent - works anywhere with fetch().
108
+ *
109
+ * Resolution strategy:
110
+ * 1. Fetch package from esm.sh, check X-TypeScript-Types header
111
+ * 2. If no types, try @types/{package} as fallback
112
+ * 3. Fetch .d.ts files and any /// <reference> dependencies
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * const resolver = new EsmTypesResolver();
117
+ *
118
+ * // Resolve types for a package
119
+ * const types = await resolver.resolve("lodash", "4.17.21");
120
+ * // types.files: { "index.d.ts": "...", "common.d.ts": "..." }
121
+ *
122
+ * // Resolve subpath types
123
+ * const clientTypes = await resolver.resolve("react-dom/client", "18.2.0");
124
+ * ```
125
+ */
126
+ export class EsmTypesResolver implements ITypesResolver {
127
+ private baseUrl: string;
128
+ private cache: ITypesCache | null;
129
+ private tryTypesPackages: boolean;
130
+
131
+ constructor(options: EsmTypesResolverOptions = {}) {
132
+ this.baseUrl = options.baseUrl ?? "https://esm.sh";
133
+ this.cache = options.cache ?? null;
134
+ this.tryTypesPackages = options.tryTypesPackages ?? true;
135
+ }
136
+
137
+ /**
138
+ * Resolve type definitions for a package.
139
+ *
140
+ * @param specifier - Package specifier with optional subpath (e.g., "react", "react-dom/client")
141
+ * @param version - Optional version constraint
142
+ * @returns Map of file paths to content, or empty object if no types found
143
+ */
144
+ async resolveTypes(
145
+ specifier: string,
146
+ version?: string
147
+ ): Promise<Record<string, string>> {
148
+ const resolved = await this.resolve(specifier, version);
149
+ if (!resolved) {
150
+ return {};
151
+ }
152
+
153
+ // Prefix paths with package location for VFS
154
+ const result: Record<string, string> = {};
155
+ const pkgPath = `/node_modules/${resolved.packageName}`;
156
+
157
+ for (const [relativePath, content] of Object.entries(resolved.files)) {
158
+ result[`${pkgPath}/${relativePath}`] = content;
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Resolve with full metadata (useful for advanced use cases).
166
+ */
167
+ async resolve(
168
+ specifier: string,
169
+ version?: string
170
+ ): Promise<ResolvedTypes | null> {
171
+ const { packageName, subpath } = parseSpecifier(specifier);
172
+ const cacheKey = makeCacheKey(packageName, subpath, version);
173
+
174
+ // Check cache first
175
+ if (this.cache) {
176
+ const cached = await this.cache.get(cacheKey);
177
+ if (cached) {
178
+ return cached;
179
+ }
180
+ }
181
+
182
+ // Try to resolve types
183
+ let result = await this.tryResolve(packageName, subpath, version);
184
+
185
+ // Fallback to @types package if enabled and no types found
186
+ if (!result && this.tryTypesPackages && !packageName.startsWith("@types/")) {
187
+ const typesPackageName = toTypesPackageName(packageName);
188
+ result = await this.tryResolve(typesPackageName, subpath, version);
189
+ if (result) {
190
+ result.fromTypesPackage = true;
191
+ // Keep original package name for the result so caller knows what they asked for
192
+ result.packageName = packageName;
193
+ }
194
+ }
195
+
196
+ // Cache the result
197
+ if (result && this.cache) {
198
+ await this.cache.set(cacheKey, result);
199
+ }
200
+
201
+ return result;
202
+ }
203
+
204
+ /**
205
+ * Attempt to resolve types for a specific package.
206
+ */
207
+ private async tryResolve(
208
+ packageName: string,
209
+ subpath: string | undefined,
210
+ version: string | undefined
211
+ ): Promise<ResolvedTypes | null> {
212
+ try {
213
+ // Build URL
214
+ const versionSuffix = version ? `@${version}` : "";
215
+ const pathSuffix = subpath ? `/${subpath}` : "";
216
+ const url = `${this.baseUrl}/${packageName}${versionSuffix}${pathSuffix}`;
217
+
218
+ // Fetch to get headers (types URL, resolved version)
219
+ const response = await fetch(url, { method: "HEAD" });
220
+ if (!response.ok) {
221
+ return null;
222
+ }
223
+
224
+ // Extract resolved version from URL or headers
225
+ const resolvedVersion = this.extractVersion(response, packageName, version);
226
+
227
+ // Get types URL from header
228
+ const typesHeader = response.headers.get("X-TypeScript-Types");
229
+ if (!typesHeader) {
230
+ return null;
231
+ }
232
+
233
+ const typesUrl = new URL(typesHeader, response.url).href;
234
+
235
+ // Fetch the types
236
+ const files = await this.fetchTypesRecursively(typesUrl, subpath);
237
+
238
+ if (Object.keys(files).length === 0) {
239
+ return null;
240
+ }
241
+
242
+ return {
243
+ packageName,
244
+ version: resolvedVersion,
245
+ files,
246
+ fromTypesPackage: packageName.startsWith("@types/"),
247
+ };
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Fetch a .d.ts file and any files it references.
255
+ */
256
+ private async fetchTypesRecursively(
257
+ entryUrl: string,
258
+ subpath: string | undefined,
259
+ visited = new Set<string>()
260
+ ): Promise<Record<string, string>> {
261
+ if (visited.has(entryUrl)) {
262
+ return {};
263
+ }
264
+ visited.add(entryUrl);
265
+
266
+ const response = await fetch(entryUrl);
267
+ if (!response.ok) {
268
+ return {};
269
+ }
270
+
271
+ const content = await response.text();
272
+ const files: Record<string, string> = {};
273
+
274
+ // Determine the file path
275
+ // For main entry: "index.d.ts"
276
+ // For subpath "client": "client.d.ts" (or "client/index.d.ts")
277
+ const fileName = subpath ? `${subpath}.d.ts` : "index.d.ts";
278
+ files[fileName] = content;
279
+
280
+ // If this is a subpath, also create the directory form
281
+ // e.g., react-dom/client can be imported, needs client/index.d.ts too
282
+ if (subpath) {
283
+ files[`${subpath}/index.d.ts`] = content;
284
+ }
285
+
286
+ // Parse and fetch referenced files
287
+ const refs = parseReferences(content);
288
+
289
+ for (const ref of refs.paths) {
290
+ const refUrl = new URL(ref, entryUrl).href;
291
+ const refFiles = await this.fetchTypesRecursively(refUrl, undefined, visited);
292
+
293
+ // Add referenced files with their relative paths
294
+ for (const [refPath, refContent] of Object.entries(refFiles)) {
295
+ // Compute relative path from the reference
296
+ const normalizedRef = ref.replace(/^\.\//, "");
297
+ if (refPath === "index.d.ts") {
298
+ files[normalizedRef] = refContent;
299
+ } else {
300
+ const dir = normalizedRef.replace(/\.d\.ts$/, "");
301
+ files[`${dir}/${refPath}`] = refContent;
302
+ }
303
+ }
304
+ }
305
+
306
+ return files;
307
+ }
308
+
309
+ /**
310
+ * Extract the resolved version from the response.
311
+ */
312
+ private extractVersion(
313
+ response: Response,
314
+ packageName: string,
315
+ requestedVersion: string | undefined
316
+ ): string {
317
+ // Try x-esm-id header first (most reliable)
318
+ const esmId = response.headers.get("x-esm-id");
319
+ if (esmId) {
320
+ const match = esmId.match(new RegExp(`${escapeRegex(packageName)}@([^/]+)`));
321
+ if (match?.[1]) {
322
+ return match[1];
323
+ }
324
+ }
325
+
326
+ // Try extracting from final URL
327
+ const urlMatch = response.url.match(new RegExp(`${escapeRegex(packageName)}@([^/]+)`));
328
+ if (urlMatch?.[1]) {
329
+ return urlMatch[1];
330
+ }
331
+
332
+ // Fall back to requested version or "latest"
333
+ return requestedVersion ?? "latest";
334
+ }
335
+ }
336
+
337
+ // =============================================================================
338
+ // Utilities
339
+ // =============================================================================
340
+
341
+ /**
342
+ * Parse a package specifier into package name and optional subpath.
343
+ *
344
+ * @example
345
+ * parseSpecifier("react") // { packageName: "react", subpath: undefined }
346
+ * parseSpecifier("react-dom/client") // { packageName: "react-dom", subpath: "client" }
347
+ * parseSpecifier("@tanstack/react-query") // { packageName: "@tanstack/react-query", subpath: undefined }
348
+ * parseSpecifier("@tanstack/react-query/devtools") // { packageName: "@tanstack/react-query", subpath: "devtools" }
349
+ */
350
+ function parseSpecifier(specifier: string): {
351
+ packageName: string;
352
+ subpath: string | undefined;
353
+ } {
354
+ if (specifier.startsWith("@")) {
355
+ // Scoped package: @scope/name or @scope/name/subpath
356
+ const parts = specifier.split("/");
357
+ if (parts.length >= 2) {
358
+ const packageName = `${parts[0]}/${parts[1]}`;
359
+ const subpath = parts.length > 2 ? parts.slice(2).join("/") : undefined;
360
+ return { packageName, subpath };
361
+ }
362
+ return { packageName: specifier, subpath: undefined };
363
+ }
364
+
365
+ // Regular package: name or name/subpath
366
+ const slashIndex = specifier.indexOf("/");
367
+ if (slashIndex === -1) {
368
+ return { packageName: specifier, subpath: undefined };
369
+ }
370
+
371
+ return {
372
+ packageName: specifier.slice(0, slashIndex),
373
+ subpath: specifier.slice(slashIndex + 1),
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Convert a package name to its @types/* equivalent.
379
+ *
380
+ * @example
381
+ * toTypesPackageName("lodash") // "@types/lodash"
382
+ * toTypesPackageName("@tanstack/react-query") // "@types/tanstack__react-query"
383
+ */
384
+ function toTypesPackageName(packageName: string): string {
385
+ if (packageName.startsWith("@")) {
386
+ // Scoped: @scope/name -> @types/scope__name
387
+ return "@types/" + packageName.slice(1).replace("/", "__");
388
+ }
389
+ return `@types/${packageName}`;
390
+ }
391
+
392
+ /**
393
+ * Parse /// <reference> directives from a .d.ts file.
394
+ */
395
+ function parseReferences(content: string): { paths: string[]; types: string[] } {
396
+ const paths: string[] = [];
397
+ const types: string[] = [];
398
+
399
+ // /// <reference path="..." />
400
+ const pathRegex = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
401
+ let match;
402
+ while ((match = pathRegex.exec(content)) !== null) {
403
+ if (match[1]) paths.push(match[1]);
404
+ }
405
+
406
+ // /// <reference types="..." />
407
+ const typesRegex = /\/\/\/\s*<reference\s+types="([^"]+)"\s*\/>/g;
408
+ while ((match = typesRegex.exec(content)) !== null) {
409
+ if (match[1]) types.push(match[1]);
410
+ }
411
+
412
+ return { paths, types };
413
+ }
414
+
415
+ /**
416
+ * Create a cache key for a package resolution.
417
+ */
418
+ function makeCacheKey(
419
+ packageName: string,
420
+ subpath: string | undefined,
421
+ version: string | undefined
422
+ ): string {
423
+ const base = version ? `${packageName}@${version}` : packageName;
424
+ return subpath ? `${base}/${subpath}` : base;
425
+ }
426
+
427
+ /**
428
+ * Escape special regex characters.
429
+ */
430
+ function escapeRegex(str: string): string {
431
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
432
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Base executor implementation shared by browser and node executors.
3
+ *
4
+ * This module provides the core execution logic with console capture,
5
+ * timeout handling, and export invocation. Platform-specific executors
6
+ * only need to provide a function to load code as a module.
7
+ */
8
+
9
+ import type { IExecutor, ExecuteOptions, ExecuteResult } from "../types";
10
+
11
+ /**
12
+ * Function that loads JavaScript code as a module.
13
+ * Platform-specific implementations convert code to an importable URL.
14
+ */
15
+ export type ModuleLoader = (code: string) => Promise<Record<string, unknown>>;
16
+
17
+ /**
18
+ * Options for creating a basic executor.
19
+ */
20
+ export interface BasicExecutorOptions {
21
+ /**
22
+ * Default timeout in milliseconds.
23
+ * @default 30000
24
+ */
25
+ defaultTimeout?: number;
26
+ }
27
+
28
+ /**
29
+ * Create an executor using the provided module loader.
30
+ *
31
+ * This is the shared implementation used by both browser and node executors.
32
+ * The only platform-specific part is how code is loaded as a module.
33
+ *
34
+ * @param loadModule - Function that loads code as a module
35
+ * @param options - Executor options
36
+ * @returns An executor instance
37
+ */
38
+ export function createBasicExecutor(
39
+ loadModule: ModuleLoader,
40
+ options: BasicExecutorOptions = {}
41
+ ): IExecutor {
42
+ const defaultTimeout = options.defaultTimeout ?? 30000;
43
+
44
+ return {
45
+ async execute(code: string, execOptions: ExecuteOptions = {}): Promise<ExecuteResult> {
46
+ const {
47
+ entryExport = "main",
48
+ context = {},
49
+ timeout = defaultTimeout,
50
+ } = execOptions;
51
+
52
+ const startTime = performance.now();
53
+ const logs: string[] = [];
54
+
55
+ // Capture console output
56
+ const originalConsole = {
57
+ log: console.log,
58
+ warn: console.warn,
59
+ error: console.error,
60
+ info: console.info,
61
+ debug: console.debug,
62
+ };
63
+
64
+ const formatArgs = (...args: unknown[]) =>
65
+ args
66
+ .map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v)))
67
+ .join(" ");
68
+
69
+ const captureLog = (...args: unknown[]) => {
70
+ logs.push(formatArgs(...args));
71
+ originalConsole.log.apply(console, args);
72
+ };
73
+ const captureWarn = (...args: unknown[]) => {
74
+ logs.push(`[warn] ${formatArgs(...args)}`);
75
+ originalConsole.warn.apply(console, args);
76
+ };
77
+ const captureError = (...args: unknown[]) => {
78
+ logs.push(`[error] ${formatArgs(...args)}`);
79
+ originalConsole.error.apply(console, args);
80
+ };
81
+ const captureInfo = (...args: unknown[]) => {
82
+ logs.push(`[info] ${formatArgs(...args)}`);
83
+ originalConsole.info.apply(console, args);
84
+ };
85
+ const captureDebug = (...args: unknown[]) => {
86
+ logs.push(`[debug] ${formatArgs(...args)}`);
87
+ originalConsole.debug.apply(console, args);
88
+ };
89
+
90
+ const restoreConsole = () => {
91
+ console.log = originalConsole.log;
92
+ console.warn = originalConsole.warn;
93
+ console.error = originalConsole.error;
94
+ console.info = originalConsole.info;
95
+ console.debug = originalConsole.debug;
96
+ };
97
+
98
+ // Install console interceptors
99
+ console.log = captureLog;
100
+ console.warn = captureWarn;
101
+ console.error = captureError;
102
+ console.info = captureInfo;
103
+ console.debug = captureDebug;
104
+
105
+ try {
106
+ // Load the module using the platform-specific loader
107
+ const module = await loadModule(code);
108
+
109
+ // Execute the appropriate export
110
+ let returnValue: unknown;
111
+
112
+ const executeExport = async () => {
113
+ if (entryExport === "main" && typeof module.main === "function") {
114
+ // Call main(context)
115
+ returnValue = await module.main(context);
116
+ } else if (entryExport === "default" && typeof module.default === "function") {
117
+ // Call default export (no args)
118
+ returnValue = await module.default();
119
+ } else if (entryExport === "default" && module.default !== undefined) {
120
+ // Default export is a value, not a function
121
+ returnValue = module.default;
122
+ }
123
+ // If neither export exists, top-level code already ran on import
124
+ };
125
+
126
+ // Execute with optional timeout
127
+ if (timeout > 0) {
128
+ const timeoutPromise = new Promise<never>((_, reject) => {
129
+ setTimeout(
130
+ () => reject(new Error(`Execution timed out after ${timeout}ms`)),
131
+ timeout
132
+ );
133
+ });
134
+ await Promise.race([executeExport(), timeoutPromise]);
135
+ } else {
136
+ await executeExport();
137
+ }
138
+
139
+ const executionTimeMs = performance.now() - startTime;
140
+ restoreConsole();
141
+
142
+ return {
143
+ success: true,
144
+ logs,
145
+ returnValue,
146
+ executionTimeMs,
147
+ };
148
+ } catch (err) {
149
+ const executionTimeMs = performance.now() - startTime;
150
+ restoreConsole();
151
+
152
+ return {
153
+ success: false,
154
+ logs,
155
+ error: err instanceof Error ? err.message : String(err),
156
+ executionTimeMs,
157
+ };
158
+ }
159
+ },
160
+ };
161
+ }