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.
Files changed (51) 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 +249 -55
  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/commands/index.d.ts +1 -1
  13. package/dist/commands/index.d.ts.map +1 -1
  14. package/dist/commands/types.d.ts +9 -1
  15. package/dist/commands/types.d.ts.map +1 -1
  16. package/dist/core/executor.d.ts.map +1 -1
  17. package/dist/core/sandbox.d.ts.map +1 -1
  18. package/dist/core/sandlot.d.ts.map +1 -1
  19. package/dist/core/typechecker.d.ts.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +50 -46
  23. package/dist/node/bundler.d.ts +5 -0
  24. package/dist/node/bundler.d.ts.map +1 -1
  25. package/dist/node/index.d.ts +2 -0
  26. package/dist/node/index.d.ts.map +1 -1
  27. package/dist/node/index.js +218 -54
  28. package/dist/node/preset.d.ts +16 -1
  29. package/dist/node/preset.d.ts.map +1 -1
  30. package/dist/node/wasm-bundler.d.ts +86 -0
  31. package/dist/node/wasm-bundler.d.ts.map +1 -0
  32. package/dist/types.d.ts +35 -7
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/browser/bundler.ts +17 -0
  36. package/src/browser/iframe-executor.ts +320 -0
  37. package/src/browser/index.ts +9 -2
  38. package/src/browser/preset.ts +30 -6
  39. package/src/commands/index.ts +18 -40
  40. package/src/commands/types.ts +36 -0
  41. package/src/core/executor.ts +8 -7
  42. package/src/core/sandbox.ts +3 -0
  43. package/src/core/sandlot.ts +7 -0
  44. package/src/core/typechecker.ts +4 -2
  45. package/src/index.ts +2 -0
  46. package/src/node/bundler.ts +11 -0
  47. package/src/node/index.ts +10 -0
  48. package/src/node/preset.ts +59 -5
  49. package/src/node/wasm-bundler.ts +299 -0
  50. package/src/types.ts +38 -7
  51. /package/src/browser/{executor.ts → main-thread-executor.ts} +0 -0
@@ -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 { formatSize, formatDiagnostics, formatBundleErrors } from "./types";
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 { formatSize, formatDiagnostics, formatBundleErrors } from "./types";
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:\n${formatted}\n`,
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.\n`;
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 = `Run failed: Build error in ${result.buildFailure.phase} phase`;
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`;
@@ -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
+ }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -418,10 +418,12 @@ function parseTsConfig(
418
418
  configPath
419
419
  );
420
420
 
421
- if (parsed.errors.length > 0) {
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
- parsed.errors.map((e) => e.messageText)
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
@@ -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(
@@ -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
+ }