sandlot 0.2.1 → 0.2.2

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 (38) hide show
  1. package/dist/browser/bundler.d.ts +8 -0
  2. package/dist/browser/bundler.d.ts.map +1 -1
  3. package/dist/browser/iframe-executor.d.ts +82 -0
  4. package/dist/browser/iframe-executor.d.ts.map +1 -0
  5. package/dist/browser/index.d.ts +4 -2
  6. package/dist/browser/index.d.ts.map +1 -1
  7. package/dist/browser/index.js +205 -9
  8. package/dist/browser/main-thread-executor.d.ts +46 -0
  9. package/dist/browser/main-thread-executor.d.ts.map +1 -0
  10. package/dist/browser/preset.d.ts +7 -2
  11. package/dist/browser/preset.d.ts.map +1 -1
  12. package/dist/core/executor.d.ts.map +1 -1
  13. package/dist/core/sandlot.d.ts.map +1 -1
  14. package/dist/index.js +5 -0
  15. package/dist/node/bundler.d.ts +5 -0
  16. package/dist/node/bundler.d.ts.map +1 -1
  17. package/dist/node/index.d.ts +2 -0
  18. package/dist/node/index.d.ts.map +1 -1
  19. package/dist/node/index.js +174 -8
  20. package/dist/node/preset.d.ts +16 -1
  21. package/dist/node/preset.d.ts.map +1 -1
  22. package/dist/node/wasm-bundler.d.ts +86 -0
  23. package/dist/node/wasm-bundler.d.ts.map +1 -0
  24. package/dist/types.d.ts +25 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/browser/bundler.ts +17 -0
  28. package/src/browser/iframe-executor.ts +320 -0
  29. package/src/browser/index.ts +9 -2
  30. package/src/browser/preset.ts +30 -6
  31. package/src/core/executor.ts +8 -7
  32. package/src/core/sandlot.ts +7 -0
  33. package/src/node/bundler.ts +11 -0
  34. package/src/node/index.ts +10 -0
  35. package/src/node/preset.ts +59 -5
  36. package/src/node/wasm-bundler.ts +299 -0
  37. package/src/types.ts +27 -0
  38. /package/src/browser/{executor.ts → main-thread-executor.ts} +0 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Iframe executor for browser environments.
3
+ *
4
+ * This executor runs code in a sandboxed iframe, providing DOM isolation
5
+ * and configurable security policies via the sandbox attribute.
6
+ *
7
+ * Key characteristics:
8
+ * - Per-execution lifecycle: fresh iframe for each execute() call
9
+ * - Configurable sandbox attributes (default: allow-scripts only)
10
+ * - No shared module support (use MainThreadExecutor for that)
11
+ * - Communication via postMessage
12
+ */
13
+
14
+ import type { IExecutor, ExecuteOptions, ExecuteResult } from "../types";
15
+
16
+ /**
17
+ * Options for creating an IframeExecutor.
18
+ */
19
+ export interface IframeExecutorOptions {
20
+ /**
21
+ * Sandbox attributes for the iframe.
22
+ * @default ["allow-scripts"]
23
+ *
24
+ * Common options:
25
+ * - "allow-scripts": Required for code execution
26
+ * - "allow-same-origin": Enables localStorage, cookies (reduces isolation)
27
+ * - "allow-modals": Enables alert/confirm/prompt
28
+ *
29
+ * Security note: "allow-scripts" + "allow-same-origin" together allows
30
+ * the iframe code to potentially remove the sandbox via script.
31
+ */
32
+ sandbox?: string[];
33
+
34
+ /**
35
+ * Default timeout in milliseconds.
36
+ * @default 30000
37
+ */
38
+ defaultTimeout?: number;
39
+
40
+ /**
41
+ * Container element for iframes.
42
+ * Iframes are created hidden (display: none).
43
+ * @default document.body
44
+ */
45
+ container?: HTMLElement;
46
+ }
47
+
48
+ /**
49
+ * Message types for parent -> iframe communication.
50
+ */
51
+ interface ExecuteMessage {
52
+ type: "execute";
53
+ code: string;
54
+ entryExport: "main" | "default";
55
+ context: Record<string, unknown>;
56
+ }
57
+
58
+ /**
59
+ * Message types for iframe -> parent communication.
60
+ */
61
+ interface LogMessage {
62
+ type: "log";
63
+ level: "log" | "warn" | "error" | "info" | "debug";
64
+ args: string;
65
+ }
66
+
67
+ interface ResultMessage {
68
+ type: "result";
69
+ success: boolean;
70
+ returnValue?: unknown;
71
+ error?: string;
72
+ }
73
+
74
+ interface ReadyMessage {
75
+ type: "ready";
76
+ }
77
+
78
+ type IframeMessage = LogMessage | ResultMessage | ReadyMessage;
79
+
80
+ /**
81
+ * Bootstrap HTML that runs inside the iframe.
82
+ * This is injected via srcdoc and handles:
83
+ * 1. Console capture and forwarding
84
+ * 2. Code execution via Blob URL import
85
+ * 3. Result reporting back to parent
86
+ */
87
+ const BOOTSTRAP_HTML = `<!DOCTYPE html>
88
+ <html>
89
+ <head>
90
+ <meta charset="utf-8">
91
+ </head>
92
+ <body>
93
+ <script type="module">
94
+ // Capture console methods and forward to parent
95
+ function formatArgs(...args) {
96
+ return args
97
+ .map(v => typeof v === "object" ? JSON.stringify(v) : String(v))
98
+ .join(" ");
99
+ }
100
+
101
+ function createLogger(level) {
102
+ return (...args) => {
103
+ parent.postMessage({ type: "log", level, args: formatArgs(...args) }, "*");
104
+ };
105
+ }
106
+
107
+ console.log = createLogger("log");
108
+ console.warn = createLogger("warn");
109
+ console.error = createLogger("error");
110
+ console.info = createLogger("info");
111
+ console.debug = createLogger("debug");
112
+
113
+ // Handle unhandled promise rejections
114
+ window.addEventListener("unhandledrejection", (event) => {
115
+ const message = event.reason instanceof Error
116
+ ? event.reason.message
117
+ : String(event.reason);
118
+ parent.postMessage({
119
+ type: "result",
120
+ success: false,
121
+ error: "Unhandled promise rejection: " + message
122
+ }, "*");
123
+ });
124
+
125
+ // Handle uncaught errors
126
+ window.addEventListener("error", (event) => {
127
+ parent.postMessage({
128
+ type: "result",
129
+ success: false,
130
+ error: event.message || "Unknown error"
131
+ }, "*");
132
+ });
133
+
134
+ // Listen for execute messages from parent
135
+ window.addEventListener("message", async (event) => {
136
+ if (event.data?.type !== "execute") return;
137
+
138
+ const { code, entryExport, context } = event.data;
139
+
140
+ try {
141
+ // Create Blob URL and import as ESM module
142
+ const blob = new Blob([code], { type: "application/javascript" });
143
+ const url = URL.createObjectURL(blob);
144
+
145
+ let module;
146
+ try {
147
+ module = await import(url);
148
+ } finally {
149
+ URL.revokeObjectURL(url);
150
+ }
151
+
152
+ // Execute the appropriate export
153
+ let returnValue;
154
+
155
+ if (entryExport === "main" && typeof module.main === "function") {
156
+ returnValue = await module.main(context);
157
+ } else if (entryExport === "default" && typeof module.default === "function") {
158
+ returnValue = await module.default();
159
+ } else if (entryExport === "default" && module.default !== undefined) {
160
+ returnValue = module.default;
161
+ }
162
+ // If neither export exists, top-level code already ran on import
163
+
164
+ parent.postMessage({ type: "result", success: true, returnValue }, "*");
165
+ } catch (err) {
166
+ const message = err instanceof Error ? err.message : String(err);
167
+ parent.postMessage({ type: "result", success: false, error: message }, "*");
168
+ }
169
+ });
170
+
171
+ // Signal that we're ready to receive code
172
+ parent.postMessage({ type: "ready" }, "*");
173
+ <\/script>
174
+ </body>
175
+ </html>`;
176
+
177
+ /**
178
+ * Executor that runs code in a sandboxed iframe.
179
+ *
180
+ * Each execute() call creates a fresh iframe, runs the code, and destroys
181
+ * the iframe. This provides clean isolation between executions.
182
+ *
183
+ * Note: This executor does NOT support shared modules. The iframe runs
184
+ * in complete isolation. Use MainThreadExecutor if you need shared modules.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * // Default: strict sandboxing (allow-scripts only)
189
+ * const executor = createIframeExecutor();
190
+ *
191
+ * // With additional permissions
192
+ * const executor = createIframeExecutor({
193
+ * sandbox: ["allow-scripts", "allow-same-origin"],
194
+ * });
195
+ *
196
+ * const result = await executor.execute(bundledCode, {
197
+ * entryExport: 'main',
198
+ * context: { args: ['--verbose'] },
199
+ * timeout: 5000,
200
+ * });
201
+ * console.log(result.logs);
202
+ * ```
203
+ */
204
+ export class IframeExecutor implements IExecutor {
205
+ private options: Required<Omit<IframeExecutorOptions, "container">> & {
206
+ container?: HTMLElement;
207
+ };
208
+
209
+ constructor(options: IframeExecutorOptions = {}) {
210
+ this.options = {
211
+ sandbox: options.sandbox ?? ["allow-scripts"],
212
+ defaultTimeout: options.defaultTimeout ?? 30000,
213
+ container: options.container,
214
+ };
215
+ }
216
+
217
+ async execute(code: string, options: ExecuteOptions = {}): Promise<ExecuteResult> {
218
+ const {
219
+ entryExport = "main",
220
+ context = {},
221
+ timeout = this.options.defaultTimeout,
222
+ } = options;
223
+
224
+ const startTime = performance.now();
225
+ const logs: string[] = [];
226
+
227
+ // Get container (default to document.body)
228
+ const container = this.options.container ?? document.body;
229
+
230
+ // Create iframe
231
+ const iframe = document.createElement("iframe");
232
+ iframe.style.display = "none";
233
+ iframe.sandbox.add(...this.options.sandbox);
234
+ iframe.srcdoc = BOOTSTRAP_HTML;
235
+
236
+ // Track whether we've received a result
237
+ let resolved = false;
238
+
239
+ return new Promise<ExecuteResult>((resolve) => {
240
+ const cleanup = () => {
241
+ if (iframe.parentNode) {
242
+ iframe.parentNode.removeChild(iframe);
243
+ }
244
+ window.removeEventListener("message", handleMessage);
245
+ if (timeoutId) clearTimeout(timeoutId);
246
+ };
247
+
248
+ const finish = (result: ExecuteResult) => {
249
+ if (resolved) return;
250
+ resolved = true;
251
+ cleanup();
252
+ resolve(result);
253
+ };
254
+
255
+ // Handle messages from iframe
256
+ const handleMessage = (event: MessageEvent) => {
257
+ // Verify message is from our iframe
258
+ if (event.source !== iframe.contentWindow) return;
259
+
260
+ const data = event.data as IframeMessage;
261
+
262
+ if (data.type === "log") {
263
+ const prefix =
264
+ data.level === "log" ? "" : `[${data.level}] `;
265
+ logs.push(prefix + data.args);
266
+ } else if (data.type === "result") {
267
+ const executionTimeMs = performance.now() - startTime;
268
+ finish({
269
+ success: data.success,
270
+ logs,
271
+ returnValue: data.returnValue,
272
+ error: data.error,
273
+ executionTimeMs,
274
+ });
275
+ } else if (data.type === "ready") {
276
+ // Iframe is ready, send the code to execute
277
+ const message: ExecuteMessage = {
278
+ type: "execute",
279
+ code,
280
+ entryExport,
281
+ context,
282
+ };
283
+ iframe.contentWindow?.postMessage(message, "*");
284
+ }
285
+ };
286
+
287
+ // Set up timeout
288
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
289
+ if (timeout > 0) {
290
+ timeoutId = setTimeout(() => {
291
+ const executionTimeMs = performance.now() - startTime;
292
+ finish({
293
+ success: false,
294
+ logs,
295
+ error: `Execution timed out after ${timeout}ms`,
296
+ executionTimeMs,
297
+ });
298
+ }, timeout);
299
+ }
300
+
301
+ // Listen for messages
302
+ window.addEventListener("message", handleMessage);
303
+
304
+ // Append iframe to DOM (this starts loading the srcdoc)
305
+ container.appendChild(iframe);
306
+ });
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Create an iframe executor.
312
+ *
313
+ * @param options - Executor options
314
+ * @returns A new IframeExecutor instance
315
+ */
316
+ export function createIframeExecutor(
317
+ options?: IframeExecutorOptions
318
+ ): IframeExecutor {
319
+ return new IframeExecutor(options);
320
+ }
@@ -46,8 +46,15 @@ export type { EsbuildWasmBundlerOptions } from "./bundler";
46
46
  // Executor (browser-specific: runs in main thread)
47
47
  // -----------------------------------------------------------------------------
48
48
 
49
- export { MainThreadExecutor, createMainThreadExecutor } from "./executor";
50
- export type { MainThreadExecutorOptions } from "./executor";
49
+ export { MainThreadExecutor, createMainThreadExecutor } from "./main-thread-executor";
50
+ export type { MainThreadExecutorOptions } from "./main-thread-executor";
51
+
52
+ // -----------------------------------------------------------------------------
53
+ // Iframe Executor (browser-specific: runs in sandboxed iframe)
54
+ // -----------------------------------------------------------------------------
55
+
56
+ export { IframeExecutor, createIframeExecutor } from "./iframe-executor";
57
+ export type { IframeExecutorOptions } from "./iframe-executor";
51
58
 
52
59
  // -----------------------------------------------------------------------------
53
60
  // Convenience Preset
@@ -12,7 +12,11 @@ import {
12
12
  import {
13
13
  MainThreadExecutor,
14
14
  type MainThreadExecutorOptions,
15
- } from "./executor";
15
+ } from "./main-thread-executor";
16
+ import {
17
+ IframeExecutor,
18
+ type IframeExecutorOptions,
19
+ } from "./iframe-executor";
16
20
 
17
21
  export interface CreateBrowserSandlotOptions
18
22
  extends Omit<SandlotOptions, "bundler" | "typechecker" | "typesResolver" | "executor"> {
@@ -42,11 +46,17 @@ export interface CreateBrowserSandlotOptions
42
46
  /**
43
47
  * Custom executor options, or a pre-configured executor instance.
44
48
  * Set to `false` to disable execution (sandbox.run() will throw).
49
+ * Set to `"iframe"` to use IframeExecutor with default options.
45
50
  * Defaults to MainThreadExecutor.
51
+ *
52
+ * Note: IframeExecutor does NOT support shared modules. Use MainThreadExecutor
53
+ * (the default) if you need shared modules like React.
46
54
  */
47
55
  executor?:
48
56
  | MainThreadExecutorOptions
57
+ | IframeExecutorOptions
49
58
  | SandlotOptions["executor"]
59
+ | "iframe"
50
60
  | false;
51
61
  }
52
62
 
@@ -121,11 +131,15 @@ export async function createBrowserSandlot(
121
131
  const executorInstance =
122
132
  executor === false
123
133
  ? undefined
124
- : isExecutor(executor)
125
- ? executor
126
- : new MainThreadExecutor(
127
- executor as MainThreadExecutorOptions | undefined
128
- );
134
+ : executor === "iframe"
135
+ ? new IframeExecutor()
136
+ : isExecutor(executor)
137
+ ? executor
138
+ : isIframeExecutorOptions(executor)
139
+ ? new IframeExecutor(executor)
140
+ : new MainThreadExecutor(
141
+ executor as MainThreadExecutorOptions | undefined
142
+ );
129
143
 
130
144
  return createSandlot({
131
145
  ...rest,
@@ -177,3 +191,13 @@ function isExecutor(value: unknown): value is SandlotOptions["executor"] {
177
191
  typeof (value as { execute: unknown }).execute === "function"
178
192
  );
179
193
  }
194
+
195
+ function isIframeExecutorOptions(value: unknown): value is IframeExecutorOptions {
196
+ // IframeExecutorOptions has "sandbox" or "container" properties
197
+ // MainThreadExecutorOptions only has "defaultTimeout"
198
+ return (
199
+ typeof value === "object" &&
200
+ value !== null &&
201
+ ("sandbox" in value || "container" in value)
202
+ );
203
+ }
@@ -68,23 +68,18 @@ export function createBasicExecutor(
68
68
 
69
69
  const captureLog = (...args: unknown[]) => {
70
70
  logs.push(formatArgs(...args));
71
- originalConsole.log.apply(console, args);
72
71
  };
73
72
  const captureWarn = (...args: unknown[]) => {
74
73
  logs.push(`[warn] ${formatArgs(...args)}`);
75
- originalConsole.warn.apply(console, args);
76
74
  };
77
75
  const captureError = (...args: unknown[]) => {
78
76
  logs.push(`[error] ${formatArgs(...args)}`);
79
- originalConsole.error.apply(console, args);
80
77
  };
81
78
  const captureInfo = (...args: unknown[]) => {
82
79
  logs.push(`[info] ${formatArgs(...args)}`);
83
- originalConsole.info.apply(console, args);
84
80
  };
85
81
  const captureDebug = (...args: unknown[]) => {
86
82
  logs.push(`[debug] ${formatArgs(...args)}`);
87
- originalConsole.debug.apply(console, args);
88
83
  };
89
84
 
90
85
  const restoreConsole = () => {
@@ -125,13 +120,19 @@ export function createBasicExecutor(
125
120
 
126
121
  // Execute with optional timeout
127
122
  if (timeout > 0) {
123
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
128
124
  const timeoutPromise = new Promise<never>((_, reject) => {
129
- setTimeout(
125
+ timeoutId = setTimeout(
130
126
  () => reject(new Error(`Execution timed out after ${timeout}ms`)),
131
127
  timeout
132
128
  );
133
129
  });
134
- await Promise.race([executeExport(), timeoutPromise]);
130
+ try {
131
+ await Promise.race([executeExport(), timeoutPromise]);
132
+ } finally {
133
+ // Clear the timeout to allow the process to exit
134
+ if (timeoutId) clearTimeout(timeoutId);
135
+ }
135
136
  } else {
136
137
  await executeExport();
137
138
  }
@@ -73,5 +73,12 @@ export function createSandlot(options: SandlotOptions): Sandlot {
73
73
  get sharedModules(): ISharedModuleRegistry | null {
74
74
  return sharedModuleRegistry;
75
75
  },
76
+
77
+ async dispose(): Promise<void> {
78
+ // Dispose of the bundler if it has a dispose method
79
+ if (bundler.dispose) {
80
+ await bundler.dispose();
81
+ }
82
+ },
76
83
  };
77
84
  }
@@ -76,6 +76,17 @@ export class EsbuildNativeBundler implements IBundler {
76
76
  return this.esbuild;
77
77
  }
78
78
 
79
+ /**
80
+ * Dispose of the esbuild service.
81
+ * This stops the esbuild child process and allows the Node.js process to exit.
82
+ */
83
+ async dispose(): Promise<void> {
84
+ if (this.esbuild) {
85
+ await this.esbuild.stop();
86
+ this.esbuild = null;
87
+ }
88
+ }
89
+
79
90
  async bundle(options: BundleOptions): Promise<BundleResult> {
80
91
  await this.initialize();
81
92
 
package/src/node/index.ts CHANGED
@@ -17,6 +17,16 @@
17
17
  export { EsbuildNativeBundler, createEsbuildNativeBundler } from "./bundler";
18
18
  export type { EsbuildNativeBundlerOptions } from "./bundler";
19
19
 
20
+ // -----------------------------------------------------------------------------
21
+ // WASM Bundler (for testing consistency with browser bundler)
22
+ // -----------------------------------------------------------------------------
23
+
24
+ export {
25
+ EsbuildWasmNodeBundler,
26
+ createEsbuildWasmNodeBundler,
27
+ } from "./wasm-bundler";
28
+ export type { EsbuildWasmNodeBundlerOptions } from "./wasm-bundler";
29
+
20
30
  // -----------------------------------------------------------------------------
21
31
  // Typechecker (platform-agnostic: re-exported for convenience)
22
32
  // -----------------------------------------------------------------------------
@@ -5,6 +5,10 @@ import {
5
5
  } from "../core/esm-types-resolver";
6
6
  import type { Sandlot, SandlotOptions } from "../types";
7
7
  import { EsbuildNativeBundler, type EsbuildNativeBundlerOptions } from "./bundler";
8
+ import {
9
+ EsbuildWasmNodeBundler,
10
+ type EsbuildWasmNodeBundlerOptions,
11
+ } from "./wasm-bundler";
8
12
  import {
9
13
  Typechecker,
10
14
  type TypecheckerOptions,
@@ -18,8 +22,17 @@ export interface CreateNodeSandlotOptions
18
22
  extends Omit<SandlotOptions, "bundler" | "typechecker" | "typesResolver" | "executor"> {
19
23
  /**
20
24
  * Custom bundler options, or a pre-configured bundler instance.
25
+ *
26
+ * Set to `"wasm"` to use the WASM bundler (for testing consistency with browser).
27
+ * You can also pass `{ wasm: true, ...options }` for WASM bundler with custom options.
28
+ *
29
+ * @default EsbuildNativeBundler (fastest, uses native esbuild binary)
21
30
  */
22
- bundler?: EsbuildNativeBundlerOptions | SandlotOptions["bundler"];
31
+ bundler?:
32
+ | EsbuildNativeBundlerOptions
33
+ | (EsbuildWasmNodeBundlerOptions & { wasm: true })
34
+ | SandlotOptions["bundler"]
35
+ | "wasm";
23
36
 
24
37
  /**
25
38
  * Custom typechecker options, or a pre-configured typechecker instance.
@@ -82,6 +95,13 @@ export interface CreateNodeSandlotOptions
82
95
  * typechecker: false,
83
96
  * });
84
97
  * ```
98
+ *
99
+ * @example Use WASM bundler for testing consistency with browser
100
+ * ```ts
101
+ * const sandlot = await createNodeSandlot({
102
+ * bundler: "wasm",
103
+ * });
104
+ * ```
85
105
  */
86
106
  export async function createNodeSandlot(
87
107
  options: CreateNodeSandlotOptions = {}
@@ -89,11 +109,9 @@ export async function createNodeSandlot(
89
109
  const { bundler, typechecker, typesResolver, executor, ...rest } = options;
90
110
 
91
111
  // Create or use provided bundler
92
- const bundlerInstance = isBundler(bundler)
93
- ? bundler
94
- : new EsbuildNativeBundler(bundler as EsbuildNativeBundlerOptions | undefined);
112
+ const bundlerInstance = createBundlerInstance(bundler);
95
113
 
96
- // Initialize bundler (loads native esbuild)
114
+ // Initialize bundler (loads native esbuild or WASM)
97
115
  await bundlerInstance.initialize();
98
116
 
99
117
  // Create or use provided typechecker
@@ -135,6 +153,42 @@ export async function createNodeSandlot(
135
153
  });
136
154
  }
137
155
 
156
+ // Helper to create bundler instance based on options
157
+
158
+ function createBundlerInstance(
159
+ bundler: CreateNodeSandlotOptions["bundler"]
160
+ ): (EsbuildNativeBundler | EsbuildWasmNodeBundler) & { initialize(): Promise<void> } {
161
+ // Already a bundler instance
162
+ if (isBundler(bundler)) {
163
+ return bundler as (EsbuildNativeBundler | EsbuildWasmNodeBundler) & { initialize(): Promise<void> };
164
+ }
165
+
166
+ // String shorthand for WASM bundler
167
+ if (bundler === "wasm") {
168
+ return new EsbuildWasmNodeBundler();
169
+ }
170
+
171
+ // Object with wasm: true flag
172
+ if (isWasmBundlerOptions(bundler)) {
173
+ const { wasm: _, ...wasmOptions } = bundler;
174
+ return new EsbuildWasmNodeBundler(wasmOptions);
175
+ }
176
+
177
+ // Default: native bundler (fastest)
178
+ return new EsbuildNativeBundler(bundler as EsbuildNativeBundlerOptions | undefined);
179
+ }
180
+
181
+ function isWasmBundlerOptions(
182
+ value: unknown
183
+ ): value is EsbuildWasmNodeBundlerOptions & { wasm: true } {
184
+ return (
185
+ typeof value === "object" &&
186
+ value !== null &&
187
+ "wasm" in value &&
188
+ (value as { wasm: unknown }).wasm === true
189
+ );
190
+ }
191
+
138
192
  // Type guards for detecting pre-configured instances
139
193
 
140
194
  function isBundler(