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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node/Bun/Deno bundler implementation using esbuild-wasm.
|
|
3
|
+
*
|
|
4
|
+
* This bundler uses the same WebAssembly-based esbuild as the browser bundler,
|
|
5
|
+
* but runs in Node.js/Bun/Deno environments. It's primarily useful for:
|
|
6
|
+
*
|
|
7
|
+
* 1. Testing consistency with the browser bundler
|
|
8
|
+
* 2. Ensuring identical import resolution behavior
|
|
9
|
+
* 3. Validating that bundled output matches between browser and server
|
|
10
|
+
*
|
|
11
|
+
* For production use, prefer EsbuildNativeBundler which is ~3-5x faster.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type * as EsbuildTypes from "esbuild-wasm";
|
|
15
|
+
import type {
|
|
16
|
+
IBundler,
|
|
17
|
+
BundleOptions,
|
|
18
|
+
BundleResult,
|
|
19
|
+
BundleWarning,
|
|
20
|
+
BundleError,
|
|
21
|
+
} from "../types";
|
|
22
|
+
import {
|
|
23
|
+
createVfsPlugin,
|
|
24
|
+
isEsbuildBuildFailure,
|
|
25
|
+
convertEsbuildMessage,
|
|
26
|
+
} from "../core/bundler-utils";
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Global Singleton for esbuild-wasm initialization
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// esbuild-wasm can only be initialized once per process. We track this globally
|
|
32
|
+
// so multiple EsbuildWasmNodeBundler instances can share the same initialization.
|
|
33
|
+
|
|
34
|
+
interface EsbuildGlobalState {
|
|
35
|
+
esbuild: typeof EsbuildTypes | null;
|
|
36
|
+
initialized: boolean;
|
|
37
|
+
initPromise: Promise<void> | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Use a different key than the browser bundler to avoid conflicts if both
|
|
41
|
+
// are somehow loaded in the same environment (e.g., during SSR)
|
|
42
|
+
const GLOBAL_KEY = "__sandlot_esbuild_wasm_node__";
|
|
43
|
+
|
|
44
|
+
function getGlobalState(): EsbuildGlobalState {
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
const g = globalThis as any;
|
|
47
|
+
if (!g[GLOBAL_KEY]) {
|
|
48
|
+
g[GLOBAL_KEY] = {
|
|
49
|
+
esbuild: null,
|
|
50
|
+
initialized: false,
|
|
51
|
+
initPromise: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return g[GLOBAL_KEY];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EsbuildWasmNodeBundlerOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Base URL for CDN imports.
|
|
60
|
+
* npm imports like "lodash" are rewritten to "{cdnBaseUrl}/lodash@{version}".
|
|
61
|
+
* @default "https://esm.sh"
|
|
62
|
+
*/
|
|
63
|
+
cdnBaseUrl?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bundler implementation using esbuild-wasm for Node.js/Bun/Deno.
|
|
68
|
+
*
|
|
69
|
+
* Uses the same WebAssembly-based esbuild as the browser bundler,
|
|
70
|
+
* making it ideal for testing consistency between browser and server builds.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const bundler = new EsbuildWasmNodeBundler();
|
|
75
|
+
* await bundler.initialize();
|
|
76
|
+
*
|
|
77
|
+
* const result = await bundler.bundle({
|
|
78
|
+
* fs: myFilesystem,
|
|
79
|
+
* entryPoint: "/src/index.ts",
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example Testing consistency with native bundler
|
|
84
|
+
* ```ts
|
|
85
|
+
* const native = new EsbuildNativeBundler();
|
|
86
|
+
* const wasm = new EsbuildWasmNodeBundler();
|
|
87
|
+
*
|
|
88
|
+
* await native.initialize();
|
|
89
|
+
* await wasm.initialize();
|
|
90
|
+
*
|
|
91
|
+
* const nativeResult = await native.bundle(options);
|
|
92
|
+
* const wasmResult = await wasm.bundle(options);
|
|
93
|
+
*
|
|
94
|
+
* // Results should be equivalent (modulo minor formatting differences)
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export class EsbuildWasmNodeBundler implements IBundler {
|
|
98
|
+
private options: EsbuildWasmNodeBundlerOptions;
|
|
99
|
+
|
|
100
|
+
constructor(options: EsbuildWasmNodeBundlerOptions = {}) {
|
|
101
|
+
this.options = {
|
|
102
|
+
cdnBaseUrl: "https://esm.sh",
|
|
103
|
+
...options,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Initialize the esbuild WASM module.
|
|
109
|
+
* Called automatically on first bundle() if not already initialized.
|
|
110
|
+
*
|
|
111
|
+
* Uses a global singleton pattern since esbuild-wasm can only be
|
|
112
|
+
* initialized once per process.
|
|
113
|
+
*/
|
|
114
|
+
async initialize(): Promise<void> {
|
|
115
|
+
const state = getGlobalState();
|
|
116
|
+
|
|
117
|
+
// Already initialized globally
|
|
118
|
+
if (state.initialized && state.esbuild) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Another instance is initializing - wait for it
|
|
123
|
+
if (state.initPromise) {
|
|
124
|
+
await state.initPromise;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// We're the first - do the initialization
|
|
129
|
+
state.initPromise = this.doInitialize(state);
|
|
130
|
+
await state.initPromise;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async doInitialize(state: EsbuildGlobalState): Promise<void> {
|
|
134
|
+
// Import esbuild-wasm from node_modules
|
|
135
|
+
const esbuild = await import("esbuild-wasm");
|
|
136
|
+
|
|
137
|
+
if (typeof esbuild?.initialize !== "function") {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"Failed to load esbuild-wasm: initialize function not found"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// In Node.js/Bun/Deno, esbuild-wasm automatically loads the WASM
|
|
144
|
+
// from node_modules without needing a wasmURL option.
|
|
145
|
+
// The wasmURL option is only for browsers.
|
|
146
|
+
await esbuild.initialize({});
|
|
147
|
+
|
|
148
|
+
// Store in global state
|
|
149
|
+
state.esbuild = esbuild;
|
|
150
|
+
state.initialized = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the initialized esbuild instance.
|
|
155
|
+
*/
|
|
156
|
+
private getEsbuild(): typeof EsbuildTypes {
|
|
157
|
+
const state = getGlobalState();
|
|
158
|
+
if (!state.esbuild) {
|
|
159
|
+
throw new Error("esbuild not initialized - call initialize() first");
|
|
160
|
+
}
|
|
161
|
+
return state.esbuild;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Dispose of the esbuild WASM service.
|
|
166
|
+
* This stops the esbuild service and allows the process to exit.
|
|
167
|
+
*
|
|
168
|
+
* Note: Since esbuild-wasm uses a global singleton, this affects all
|
|
169
|
+
* instances. After dispose(), you'll need to create a new bundler.
|
|
170
|
+
*/
|
|
171
|
+
async dispose(): Promise<void> {
|
|
172
|
+
const state = getGlobalState();
|
|
173
|
+
if (state.esbuild) {
|
|
174
|
+
await state.esbuild.stop();
|
|
175
|
+
state.esbuild = null;
|
|
176
|
+
state.initialized = false;
|
|
177
|
+
state.initPromise = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async bundle(options: BundleOptions): Promise<BundleResult> {
|
|
182
|
+
await this.initialize();
|
|
183
|
+
|
|
184
|
+
const esbuild = this.getEsbuild();
|
|
185
|
+
|
|
186
|
+
const {
|
|
187
|
+
fs,
|
|
188
|
+
entryPoint,
|
|
189
|
+
installedPackages = {},
|
|
190
|
+
sharedModules = [],
|
|
191
|
+
sharedModuleRegistry,
|
|
192
|
+
external = [],
|
|
193
|
+
format = "esm",
|
|
194
|
+
minify = false,
|
|
195
|
+
sourcemap = false,
|
|
196
|
+
target = ["es2020"],
|
|
197
|
+
} = options;
|
|
198
|
+
|
|
199
|
+
// Normalize entry point to absolute path
|
|
200
|
+
const normalizedEntry = entryPoint.startsWith("/")
|
|
201
|
+
? entryPoint
|
|
202
|
+
: `/${entryPoint}`;
|
|
203
|
+
|
|
204
|
+
// Verify entry point exists
|
|
205
|
+
if (!fs.exists(normalizedEntry)) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
errors: [{ text: `Entry point not found: ${normalizedEntry}` }],
|
|
209
|
+
warnings: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Track files included in the bundle
|
|
214
|
+
const includedFiles = new Set<string>();
|
|
215
|
+
|
|
216
|
+
// Create the VFS plugin
|
|
217
|
+
// Note: bundleCdnImports is true for Node/Bun because they cannot
|
|
218
|
+
// resolve HTTP imports at runtime - esbuild will fetch and bundle them
|
|
219
|
+
const plugin = createVfsPlugin({
|
|
220
|
+
fs,
|
|
221
|
+
entryPoint: normalizedEntry,
|
|
222
|
+
installedPackages,
|
|
223
|
+
sharedModules: new Set(sharedModules),
|
|
224
|
+
sharedModuleRegistry: sharedModuleRegistry ?? null,
|
|
225
|
+
cdnBaseUrl: this.options.cdnBaseUrl!,
|
|
226
|
+
includedFiles,
|
|
227
|
+
bundleCdnImports: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// Run esbuild
|
|
232
|
+
// Note: We do NOT mark http/https as external here because Node/Bun
|
|
233
|
+
// cannot resolve HTTP imports at runtime. Instead, bundleCdnImports: true
|
|
234
|
+
// tells the VFS plugin to let esbuild fetch and bundle CDN imports.
|
|
235
|
+
const result = await esbuild.build({
|
|
236
|
+
entryPoints: [normalizedEntry],
|
|
237
|
+
bundle: true,
|
|
238
|
+
write: false,
|
|
239
|
+
format,
|
|
240
|
+
minify,
|
|
241
|
+
sourcemap: sourcemap ? "inline" : false,
|
|
242
|
+
target,
|
|
243
|
+
external,
|
|
244
|
+
// Cast to esbuild's Plugin type since our minimal interface is compatible
|
|
245
|
+
plugins: [plugin as EsbuildTypes.Plugin],
|
|
246
|
+
jsx: "automatic",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const code = result.outputFiles?.[0]?.text ?? "";
|
|
250
|
+
|
|
251
|
+
// Convert esbuild warnings to our format
|
|
252
|
+
const warnings: BundleWarning[] = result.warnings.map((w) =>
|
|
253
|
+
convertEsbuildMessage(w)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
code,
|
|
259
|
+
warnings,
|
|
260
|
+
includedFiles: Array.from(includedFiles),
|
|
261
|
+
};
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// esbuild throws BuildFailure with .errors array
|
|
264
|
+
if (isEsbuildBuildFailure(err)) {
|
|
265
|
+
const errors: BundleError[] = err.errors.map((e) =>
|
|
266
|
+
convertEsbuildMessage(e)
|
|
267
|
+
);
|
|
268
|
+
const warnings: BundleWarning[] = err.warnings.map((w) =>
|
|
269
|
+
convertEsbuildMessage(w)
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
errors,
|
|
274
|
+
warnings,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Unknown error - wrap it
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
errors: [{ text: message }],
|
|
283
|
+
warnings: [],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create an esbuild-wasm bundler for Node.js/Bun/Deno.
|
|
291
|
+
*
|
|
292
|
+
* This is primarily useful for testing consistency with the browser bundler.
|
|
293
|
+
* For production use, prefer createEsbuildNativeBundler() which is ~3-5x faster.
|
|
294
|
+
*/
|
|
295
|
+
export function createEsbuildWasmNodeBundler(
|
|
296
|
+
options?: EsbuildWasmNodeBundlerOptions
|
|
297
|
+
): EsbuildWasmNodeBundler {
|
|
298
|
+
return new EsbuildWasmNodeBundler(options);
|
|
299
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface IBundler {
|
|
|
22
22
|
* Bundle source files from a filesystem into a single output
|
|
23
23
|
*/
|
|
24
24
|
bundle(options: BundleOptions): Promise<BundleResult>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Dispose of resources held by the bundler (optional).
|
|
28
|
+
* Called by Sandlot.dispose() to clean up background services.
|
|
29
|
+
*/
|
|
30
|
+
dispose?(): Promise<void>;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export interface BundleOptions {
|
|
@@ -669,4 +675,25 @@ export interface Sandlot {
|
|
|
669
675
|
* The shared module registry (if shared modules were provided)
|
|
670
676
|
*/
|
|
671
677
|
readonly sharedModules: ISharedModuleRegistry | null;
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Dispose of resources held by this Sandlot instance.
|
|
681
|
+
*
|
|
682
|
+
* This should be called when you're done using Sandlot to allow
|
|
683
|
+
* the process to exit cleanly. It stops any background services
|
|
684
|
+
* like the esbuild child process.
|
|
685
|
+
*
|
|
686
|
+
* After calling dispose(), this instance should not be used.
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* ```ts
|
|
690
|
+
* const sandlot = await createNodeSandlot();
|
|
691
|
+
* const sandbox = await sandlot.createSandbox();
|
|
692
|
+
*
|
|
693
|
+
* // ... do work ...
|
|
694
|
+
*
|
|
695
|
+
* await sandlot.dispose(); // Allow process to exit
|
|
696
|
+
* ```
|
|
697
|
+
*/
|
|
698
|
+
dispose(): Promise<void>;
|
|
672
699
|
}
|
|
File without changes
|