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.
- package/dist/browser/bundler.d.ts +8 -0
- package/dist/browser/bundler.d.ts.map +1 -1
- package/dist/browser/iframe-executor.d.ts +82 -0
- package/dist/browser/iframe-executor.d.ts.map +1 -0
- package/dist/browser/index.d.ts +4 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +205 -9
- package/dist/browser/main-thread-executor.d.ts +46 -0
- package/dist/browser/main-thread-executor.d.ts.map +1 -0
- package/dist/browser/preset.d.ts +7 -2
- package/dist/browser/preset.d.ts.map +1 -1
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/sandlot.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/node/bundler.d.ts +5 -0
- package/dist/node/bundler.d.ts.map +1 -1
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +174 -8
- package/dist/node/preset.d.ts +16 -1
- package/dist/node/preset.d.ts.map +1 -1
- package/dist/node/wasm-bundler.d.ts +86 -0
- package/dist/node/wasm-bundler.d.ts.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/browser/bundler.ts +17 -0
- package/src/browser/iframe-executor.ts +320 -0
- package/src/browser/index.ts +9 -2
- package/src/browser/preset.ts +30 -6
- package/src/core/executor.ts +8 -7
- package/src/core/sandlot.ts +7 -0
- package/src/node/bundler.ts +11 -0
- package/src/node/index.ts +10 -0
- package/src/node/preset.ts +59 -5
- package/src/node/wasm-bundler.ts +299 -0
- package/src/types.ts +27 -0
- /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
|
+
}
|
package/src/browser/index.ts
CHANGED
|
@@ -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
|
package/src/browser/preset.ts
CHANGED
|
@@ -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
|
-
:
|
|
125
|
-
?
|
|
126
|
-
:
|
|
127
|
-
executor
|
|
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
|
+
}
|
package/src/core/executor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/core/sandlot.ts
CHANGED
|
@@ -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
|
}
|
package/src/node/bundler.ts
CHANGED
|
@@ -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
|
// -----------------------------------------------------------------------------
|
package/src/node/preset.ts
CHANGED
|
@@ -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?:
|
|
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 =
|
|
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(
|