sandlot 0.2.0 → 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 +249 -55
- 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/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/types.d.ts +9 -1
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/sandbox.d.ts.map +1 -1
- package/dist/core/sandlot.d.ts.map +1 -1
- package/dist/core/typechecker.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -46
- 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 +218 -54
- 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 +35 -7
- 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/commands/index.ts +18 -40
- package/src/commands/types.ts +36 -0
- package/src/core/executor.ts +8 -7
- package/src/core/sandbox.ts +3 -0
- package/src/core/sandlot.ts +7 -0
- package/src/core/typechecker.ts +4 -2
- package/src/index.ts +2 -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 +38 -7
- /package/src/browser/{executor.ts → main-thread-executor.ts} +0 -0
package/src/commands/index.ts
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
import { defineCommand, type CommandContext } from "just-bash/browser";
|
|
13
13
|
import type { SandboxRef } from "./types";
|
|
14
14
|
export type { SandboxRef } from "./types";
|
|
15
|
-
export {
|
|
15
|
+
export {
|
|
16
|
+
formatSize,
|
|
17
|
+
formatDiagnostics,
|
|
18
|
+
formatBundleErrors,
|
|
19
|
+
formatBuildFailure,
|
|
20
|
+
} from "./types";
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Create the main `sandlot` command with all subcommands.
|
|
@@ -96,7 +101,12 @@ Examples:
|
|
|
96
101
|
// Build
|
|
97
102
|
// =============================================================================
|
|
98
103
|
|
|
99
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
formatSize,
|
|
106
|
+
formatDiagnostics,
|
|
107
|
+
formatBundleErrors,
|
|
108
|
+
formatBuildFailure,
|
|
109
|
+
} from "./types";
|
|
100
110
|
|
|
101
111
|
async function handleBuild(sandboxRef: SandboxRef, args: (string | undefined)[]) {
|
|
102
112
|
let entryPoint: string | undefined;
|
|
@@ -150,37 +160,9 @@ Examples:
|
|
|
150
160
|
|
|
151
161
|
// Handle build failure
|
|
152
162
|
if (!result.success) {
|
|
153
|
-
let stderr = `Build failed`;
|
|
154
|
-
|
|
155
|
-
switch (result.phase) {
|
|
156
|
-
case "entry":
|
|
157
|
-
stderr = `Build failed: ${result.message}\n`;
|
|
158
|
-
break;
|
|
159
|
-
|
|
160
|
-
case "typecheck":
|
|
161
|
-
if (result.diagnostics) {
|
|
162
|
-
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
163
|
-
stderr = `Build failed: Type check errors\n\n${formatDiagnostics(errors)}\n`;
|
|
164
|
-
} else {
|
|
165
|
-
stderr = `Build failed: Type check errors\n`;
|
|
166
|
-
}
|
|
167
|
-
break;
|
|
168
|
-
|
|
169
|
-
case "bundle":
|
|
170
|
-
if (result.bundleErrors && result.bundleErrors.length > 0) {
|
|
171
|
-
stderr = `Build failed: Bundle errors\n\n${formatBundleErrors(result.bundleErrors)}\n`;
|
|
172
|
-
} else {
|
|
173
|
-
stderr = `Build failed: Bundle error\n`;
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
|
|
177
|
-
default:
|
|
178
|
-
stderr = `Build failed: Unknown error\n`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
163
|
return {
|
|
182
164
|
stdout: "",
|
|
183
|
-
stderr,
|
|
165
|
+
stderr: formatBuildFailure(result),
|
|
184
166
|
exitCode: 1,
|
|
185
167
|
};
|
|
186
168
|
}
|
|
@@ -249,16 +231,16 @@ Examples:
|
|
|
249
231
|
const formatted = formatDiagnostics(errors);
|
|
250
232
|
return {
|
|
251
233
|
stdout: "",
|
|
252
|
-
stderr: `Type check failed
|
|
234
|
+
stderr: `Type check failed\n\n${formatted}\n`,
|
|
253
235
|
exitCode: 1,
|
|
254
236
|
};
|
|
255
237
|
}
|
|
256
238
|
|
|
257
239
|
const warnings = result.diagnostics.filter((d) => d.severity === "warning");
|
|
258
|
-
let output = `Type check passed
|
|
240
|
+
let output = `Type check passed\n`;
|
|
259
241
|
|
|
260
242
|
if (warnings.length > 0) {
|
|
261
|
-
output += `\nWarnings:\n${formatDiagnostics(warnings)}\n`;
|
|
243
|
+
output += `\nWarnings:\n\n${formatDiagnostics(warnings)}\n`;
|
|
262
244
|
}
|
|
263
245
|
|
|
264
246
|
return {
|
|
@@ -458,13 +440,9 @@ Examples:
|
|
|
458
440
|
if (!result.success) {
|
|
459
441
|
let stderr = "";
|
|
460
442
|
|
|
461
|
-
// Build failure
|
|
443
|
+
// Build failure - use the shared formatter
|
|
462
444
|
if (result.buildFailure) {
|
|
463
|
-
stderr =
|
|
464
|
-
if (result.buildFailure.message) {
|
|
465
|
-
stderr += `\n${result.buildFailure.message}`;
|
|
466
|
-
}
|
|
467
|
-
stderr += "\n";
|
|
445
|
+
stderr = formatBuildFailure(result.buildFailure, "Run failed");
|
|
468
446
|
} else {
|
|
469
447
|
// Execution failure
|
|
470
448
|
stderr = `Run failed: ${result.error ?? "Unknown error"}\n`;
|
package/src/commands/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
InstallResult,
|
|
11
11
|
UninstallResult,
|
|
12
12
|
BuildResult,
|
|
13
|
+
BuildFailureDetails,
|
|
13
14
|
TypecheckResult,
|
|
14
15
|
SandboxBuildOptions,
|
|
15
16
|
SandboxTypecheckOptions,
|
|
@@ -106,3 +107,38 @@ export function formatBundleErrors(errors: BundleError[]): string {
|
|
|
106
107
|
})
|
|
107
108
|
.join("\n\n");
|
|
108
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format a build failure for shell output.
|
|
113
|
+
* Used by both build and run commands for consistent error formatting.
|
|
114
|
+
*
|
|
115
|
+
* @param failure - The build failure details
|
|
116
|
+
* @param prefix - Optional prefix for the error message (default: "Build failed")
|
|
117
|
+
*/
|
|
118
|
+
export function formatBuildFailure(
|
|
119
|
+
failure: BuildFailureDetails,
|
|
120
|
+
prefix = "Build failed"
|
|
121
|
+
): string {
|
|
122
|
+
switch (failure.phase) {
|
|
123
|
+
case "entry":
|
|
124
|
+
return `${prefix}: ${failure.message}\n`;
|
|
125
|
+
|
|
126
|
+
case "typecheck":
|
|
127
|
+
if (failure.diagnostics && failure.diagnostics.length > 0) {
|
|
128
|
+
const errors = failure.diagnostics.filter((d) => d.severity === "error");
|
|
129
|
+
if (errors.length > 0) {
|
|
130
|
+
return `${prefix}: Type check errors\n\n${formatDiagnostics(errors)}\n`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return `${prefix}: Type check errors\n`;
|
|
134
|
+
|
|
135
|
+
case "bundle":
|
|
136
|
+
if (failure.bundleErrors && failure.bundleErrors.length > 0) {
|
|
137
|
+
return `${prefix}: Bundle errors\n\n${formatBundleErrors(failure.bundleErrors)}\n`;
|
|
138
|
+
}
|
|
139
|
+
return `${prefix}: Bundle error\n`;
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
return `${prefix}: Unknown error\n`;
|
|
143
|
+
}
|
|
144
|
+
}
|
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/sandbox.ts
CHANGED
|
@@ -513,6 +513,9 @@ export async function createSandboxImpl(
|
|
|
513
513
|
buildFailure: {
|
|
514
514
|
phase: buildResult.phase,
|
|
515
515
|
message: buildResult.message,
|
|
516
|
+
diagnostics: buildResult.diagnostics,
|
|
517
|
+
bundleErrors: buildResult.bundleErrors,
|
|
518
|
+
bundleWarnings: buildResult.bundleWarnings,
|
|
516
519
|
},
|
|
517
520
|
};
|
|
518
521
|
}
|
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/core/typechecker.ts
CHANGED
|
@@ -418,10 +418,12 @@ function parseTsConfig(
|
|
|
418
418
|
configPath
|
|
419
419
|
);
|
|
420
420
|
|
|
421
|
-
|
|
421
|
+
// Filter out "no inputs found" error (TS18003) - we pass entry points explicitly
|
|
422
|
+
const relevantErrors = parsed.errors.filter((e) => e.code !== 18003);
|
|
423
|
+
if (relevantErrors.length > 0) {
|
|
422
424
|
console.warn(
|
|
423
425
|
"[typechecker] tsconfig parse errors:",
|
|
424
|
-
|
|
426
|
+
relevantErrors.map((e) => e.messageText)
|
|
425
427
|
);
|
|
426
428
|
}
|
|
427
429
|
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
formatSize,
|
|
48
48
|
formatDiagnostics,
|
|
49
49
|
formatBundleErrors,
|
|
50
|
+
formatBuildFailure,
|
|
50
51
|
} from "./commands";
|
|
51
52
|
export type { SandboxRef } from "./commands";
|
|
52
53
|
|
|
@@ -95,6 +96,7 @@ export type {
|
|
|
95
96
|
BuildResult,
|
|
96
97
|
BuildSuccess,
|
|
97
98
|
BuildFailure,
|
|
99
|
+
BuildFailureDetails,
|
|
98
100
|
SandboxBuildOptions,
|
|
99
101
|
|
|
100
102
|
// Install/Uninstall types
|
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(
|
|
@@ -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
|
+
}
|