sandlot 0.2.1 → 0.2.3
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 +339 -76
- 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/bundler-utils.d.ts +43 -1
- package/dist/core/bundler-utils.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/index.js +63 -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 +243 -75
- 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 +23 -106
- 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/bundler-utils.ts +148 -0
- package/src/core/executor.ts +8 -7
- package/src/core/sandbox.ts +82 -0
- package/src/core/sandlot.ts +7 -0
- package/src/node/bundler.ts +17 -110
- package/src/node/index.ts +10 -0
- package/src/node/preset.ts +59 -5
- package/src/node/wasm-bundler.ts +195 -0
- package/src/types.ts +27 -0
- /package/src/browser/{executor.ts → main-thread-executor.ts} +0 -0
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
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
9
|
ISharedModuleRegistry,
|
|
10
|
+
BundleOptions,
|
|
11
|
+
BundleResult,
|
|
10
12
|
BundleWarning,
|
|
11
13
|
BundleError,
|
|
12
14
|
BundleLocation,
|
|
@@ -628,3 +630,149 @@ export function generateSharedModuleCode(
|
|
|
628
630
|
|
|
629
631
|
return code;
|
|
630
632
|
}
|
|
633
|
+
|
|
634
|
+
// =============================================================================
|
|
635
|
+
// Shared Bundle Execution
|
|
636
|
+
// =============================================================================
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Minimal esbuild interface needed for bundling.
|
|
640
|
+
* Compatible with both esbuild and esbuild-wasm.
|
|
641
|
+
*
|
|
642
|
+
* Uses a loose `Record<string, unknown>` for build options to avoid
|
|
643
|
+
* type conflicts between the various esbuild module signatures.
|
|
644
|
+
*/
|
|
645
|
+
export interface EsbuildInstance {
|
|
646
|
+
build(options: Record<string, unknown>): Promise<{
|
|
647
|
+
outputFiles?: Array<{ text: string }>;
|
|
648
|
+
warnings: EsbuildMessage[];
|
|
649
|
+
}>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Options for the shared bundle execution helper.
|
|
654
|
+
*/
|
|
655
|
+
export interface ExecuteBundleOptions {
|
|
656
|
+
/** The esbuild instance to use */
|
|
657
|
+
esbuild: EsbuildInstance;
|
|
658
|
+
/** Bundle options from the IBundler interface */
|
|
659
|
+
bundleOptions: BundleOptions;
|
|
660
|
+
/** Base URL for CDN imports */
|
|
661
|
+
cdnBaseUrl: string;
|
|
662
|
+
/**
|
|
663
|
+
* Whether to bundle CDN imports inline.
|
|
664
|
+
* - Browser: false (external) - browser can fetch at runtime
|
|
665
|
+
* - Node/Bun: true (bundle) - esbuild fetches during build
|
|
666
|
+
*/
|
|
667
|
+
bundleCdnImports: boolean;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Execute a bundle using esbuild with the VFS plugin.
|
|
672
|
+
*
|
|
673
|
+
* This is the shared implementation used by both browser and node WASM bundlers.
|
|
674
|
+
* It handles entry point normalization, VFS plugin creation, and error handling.
|
|
675
|
+
*
|
|
676
|
+
* @param options - Bundle execution options
|
|
677
|
+
* @returns Bundle result with code or errors
|
|
678
|
+
*/
|
|
679
|
+
export async function executeBundleWithEsbuild(
|
|
680
|
+
options: ExecuteBundleOptions
|
|
681
|
+
): Promise<BundleResult> {
|
|
682
|
+
const { esbuild, bundleOptions, cdnBaseUrl, bundleCdnImports } = options;
|
|
683
|
+
|
|
684
|
+
const {
|
|
685
|
+
fs,
|
|
686
|
+
entryPoint,
|
|
687
|
+
installedPackages = {},
|
|
688
|
+
sharedModules = [],
|
|
689
|
+
sharedModuleRegistry,
|
|
690
|
+
external = [],
|
|
691
|
+
format = "esm",
|
|
692
|
+
minify = false,
|
|
693
|
+
sourcemap = false,
|
|
694
|
+
target = ["es2020"],
|
|
695
|
+
} = bundleOptions;
|
|
696
|
+
|
|
697
|
+
// Normalize entry point to absolute path
|
|
698
|
+
const normalizedEntry = entryPoint.startsWith("/")
|
|
699
|
+
? entryPoint
|
|
700
|
+
: `/${entryPoint}`;
|
|
701
|
+
|
|
702
|
+
// Verify entry point exists
|
|
703
|
+
if (!fs.exists(normalizedEntry)) {
|
|
704
|
+
return {
|
|
705
|
+
success: false,
|
|
706
|
+
errors: [{ text: `Entry point not found: ${normalizedEntry}` }],
|
|
707
|
+
warnings: [],
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Track files included in the bundle
|
|
712
|
+
const includedFiles = new Set<string>();
|
|
713
|
+
|
|
714
|
+
// Create the VFS plugin
|
|
715
|
+
const plugin = createVfsPlugin({
|
|
716
|
+
fs,
|
|
717
|
+
entryPoint: normalizedEntry,
|
|
718
|
+
installedPackages,
|
|
719
|
+
sharedModules: new Set(sharedModules),
|
|
720
|
+
sharedModuleRegistry: sharedModuleRegistry ?? null,
|
|
721
|
+
cdnBaseUrl,
|
|
722
|
+
includedFiles,
|
|
723
|
+
bundleCdnImports,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
// Run esbuild
|
|
728
|
+
const result = await esbuild.build({
|
|
729
|
+
entryPoints: [normalizedEntry],
|
|
730
|
+
bundle: true,
|
|
731
|
+
write: false,
|
|
732
|
+
format,
|
|
733
|
+
minify,
|
|
734
|
+
sourcemap: sourcemap ? "inline" : false,
|
|
735
|
+
target,
|
|
736
|
+
external,
|
|
737
|
+
plugins: [plugin],
|
|
738
|
+
jsx: "automatic",
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const code = result.outputFiles?.[0]?.text ?? "";
|
|
742
|
+
|
|
743
|
+
// Convert esbuild warnings to our format
|
|
744
|
+
const warnings: BundleWarning[] = result.warnings.map((w) =>
|
|
745
|
+
convertEsbuildMessage(w)
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
code,
|
|
751
|
+
warnings,
|
|
752
|
+
includedFiles: Array.from(includedFiles),
|
|
753
|
+
};
|
|
754
|
+
} catch (err) {
|
|
755
|
+
// esbuild throws BuildFailure with .errors array
|
|
756
|
+
if (isEsbuildBuildFailure(err)) {
|
|
757
|
+
const errors: BundleError[] = err.errors.map((e) =>
|
|
758
|
+
convertEsbuildMessage(e)
|
|
759
|
+
);
|
|
760
|
+
const warnings: BundleWarning[] = err.warnings.map((w) =>
|
|
761
|
+
convertEsbuildMessage(w)
|
|
762
|
+
);
|
|
763
|
+
return {
|
|
764
|
+
success: false,
|
|
765
|
+
errors,
|
|
766
|
+
warnings,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Unknown error - wrap it
|
|
771
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
772
|
+
return {
|
|
773
|
+
success: false,
|
|
774
|
+
errors: [{ text: message }],
|
|
775
|
+
warnings: [],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
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
|
@@ -262,6 +262,88 @@ export async function createSandboxImpl(
|
|
|
262
262
|
fs.writeFile(TSCONFIG_PATH, JSON.stringify(DEFAULT_TSCONFIG, null, 2));
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Install types for shared modules (if both registry and resolver exist)
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
if (sharedModuleRegistry && typesResolver) {
|
|
270
|
+
const sharedModuleIds = sharedModuleRegistry.list();
|
|
271
|
+
|
|
272
|
+
// Resolve types for all shared modules in parallel
|
|
273
|
+
const typesFetches = sharedModuleIds.map(async (moduleId) => {
|
|
274
|
+
try {
|
|
275
|
+
const typeFiles = await typesResolver.resolveTypes(moduleId);
|
|
276
|
+
return { moduleId, typeFiles, error: null };
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// Log but don't fail - types are nice to have but not required
|
|
279
|
+
console.warn(`[sandlot] Failed to fetch types for shared module "${moduleId}":`, err);
|
|
280
|
+
return { moduleId, typeFiles: {}, error: err };
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const results = await Promise.all(typesFetches);
|
|
285
|
+
|
|
286
|
+
// Write type files to the filesystem
|
|
287
|
+
for (const { moduleId, typeFiles } of results) {
|
|
288
|
+
if (Object.keys(typeFiles).length === 0) continue;
|
|
289
|
+
|
|
290
|
+
// Determine the package name (strip subpath for @types resolution)
|
|
291
|
+
// e.g., "react-dom/client" -> "react-dom"
|
|
292
|
+
let packageName = moduleId;
|
|
293
|
+
let subpath: string | undefined;
|
|
294
|
+
|
|
295
|
+
if (moduleId.startsWith("@")) {
|
|
296
|
+
const parts = moduleId.split("/");
|
|
297
|
+
if (parts.length >= 2) {
|
|
298
|
+
packageName = `${parts[0]}/${parts[1]}`;
|
|
299
|
+
subpath = parts.length > 2 ? parts.slice(2).join("/") : undefined;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
const slashIndex = moduleId.indexOf("/");
|
|
303
|
+
if (slashIndex !== -1) {
|
|
304
|
+
packageName = moduleId.slice(0, slashIndex);
|
|
305
|
+
subpath = moduleId.slice(slashIndex + 1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const packageDir = `/node_modules/${packageName}`;
|
|
310
|
+
let typesEntry: string | null = null;
|
|
311
|
+
let fallbackEntry: string | null = null;
|
|
312
|
+
|
|
313
|
+
for (const [filePath, content] of Object.entries(typeFiles)) {
|
|
314
|
+
const fullPath = filePath.startsWith("/")
|
|
315
|
+
? filePath
|
|
316
|
+
: `${packageDir}/${filePath}`;
|
|
317
|
+
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
318
|
+
ensureDir(fs, dir);
|
|
319
|
+
fs.writeFile(fullPath, content);
|
|
320
|
+
|
|
321
|
+
// Track types entry: prefer index.d.ts, fallback to first top-level .d.ts
|
|
322
|
+
const relativePath = fullPath.replace(`${packageDir}/`, "");
|
|
323
|
+
if (relativePath === "index.d.ts") {
|
|
324
|
+
typesEntry = "index.d.ts";
|
|
325
|
+
} else if (!fallbackEntry && relativePath.endsWith(".d.ts") && !relativePath.includes("/")) {
|
|
326
|
+
fallbackEntry = relativePath;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Use index.d.ts if found, otherwise use fallback
|
|
331
|
+
const finalTypesEntry = typesEntry ?? fallbackEntry ?? "index.d.ts";
|
|
332
|
+
|
|
333
|
+
// Create package.json if it doesn't exist yet
|
|
334
|
+
const pkgJsonPath = `${packageDir}/package.json`;
|
|
335
|
+
if (!fs.exists(pkgJsonPath)) {
|
|
336
|
+
const pkgJson = {
|
|
337
|
+
name: packageName,
|
|
338
|
+
version: "shared",
|
|
339
|
+
types: finalTypesEntry,
|
|
340
|
+
main: finalTypesEntry.replace(/\.d\.ts$/, ".js"),
|
|
341
|
+
};
|
|
342
|
+
fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
265
347
|
// ---------------------------------------------------------------------------
|
|
266
348
|
// Core Methods
|
|
267
349
|
// ---------------------------------------------------------------------------
|
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
|
@@ -6,18 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type * as EsbuildTypes from "esbuild";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
BundleOptions,
|
|
12
|
-
BundleResult,
|
|
13
|
-
BundleWarning,
|
|
14
|
-
BundleError,
|
|
15
|
-
} from "../types";
|
|
16
|
-
import {
|
|
17
|
-
createVfsPlugin,
|
|
18
|
-
isEsbuildBuildFailure,
|
|
19
|
-
convertEsbuildMessage,
|
|
20
|
-
} from "../core/bundler-utils";
|
|
9
|
+
import type { IBundler, BundleOptions, BundleResult } from "../types";
|
|
10
|
+
import { executeBundleWithEsbuild } from "../core/bundler-utils";
|
|
21
11
|
|
|
22
12
|
export interface EsbuildNativeBundlerOptions {
|
|
23
13
|
/**
|
|
@@ -76,111 +66,28 @@ export class EsbuildNativeBundler implements IBundler {
|
|
|
76
66
|
return this.esbuild;
|
|
77
67
|
}
|
|
78
68
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
installedPackages = {},
|
|
88
|
-
sharedModules = [],
|
|
89
|
-
sharedModuleRegistry,
|
|
90
|
-
external = [],
|
|
91
|
-
format = "esm",
|
|
92
|
-
minify = false,
|
|
93
|
-
sourcemap = false,
|
|
94
|
-
target = ["es2020"],
|
|
95
|
-
} = options;
|
|
96
|
-
|
|
97
|
-
// Normalize entry point to absolute path
|
|
98
|
-
const normalizedEntry = entryPoint.startsWith("/")
|
|
99
|
-
? entryPoint
|
|
100
|
-
: `/${entryPoint}`;
|
|
101
|
-
|
|
102
|
-
// Verify entry point exists
|
|
103
|
-
if (!fs.exists(normalizedEntry)) {
|
|
104
|
-
return {
|
|
105
|
-
success: false,
|
|
106
|
-
errors: [{ text: `Entry point not found: ${normalizedEntry}` }],
|
|
107
|
-
warnings: [],
|
|
108
|
-
};
|
|
69
|
+
/**
|
|
70
|
+
* Dispose of the esbuild service.
|
|
71
|
+
* This stops the esbuild child process and allows the Node.js process to exit.
|
|
72
|
+
*/
|
|
73
|
+
async dispose(): Promise<void> {
|
|
74
|
+
if (this.esbuild) {
|
|
75
|
+
await this.esbuild.stop();
|
|
76
|
+
this.esbuild = null;
|
|
109
77
|
}
|
|
78
|
+
}
|
|
110
79
|
|
|
111
|
-
|
|
112
|
-
|
|
80
|
+
async bundle(options: BundleOptions): Promise<BundleResult> {
|
|
81
|
+
await this.initialize();
|
|
113
82
|
|
|
114
|
-
//
|
|
115
|
-
// Note: bundleCdnImports is true for Node/Bun because they cannot
|
|
83
|
+
// bundleCdnImports is true for Node/Bun because they cannot
|
|
116
84
|
// resolve HTTP imports at runtime - native esbuild will fetch and bundle them
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
installedPackages,
|
|
121
|
-
sharedModules: new Set(sharedModules),
|
|
122
|
-
sharedModuleRegistry: sharedModuleRegistry ?? null,
|
|
85
|
+
return executeBundleWithEsbuild({
|
|
86
|
+
esbuild: this.getEsbuild(),
|
|
87
|
+
bundleOptions: options,
|
|
123
88
|
cdnBaseUrl: this.options.cdnBaseUrl!,
|
|
124
|
-
includedFiles,
|
|
125
89
|
bundleCdnImports: true,
|
|
126
90
|
});
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
// Run esbuild
|
|
130
|
-
// Note: We do NOT mark http/https as external here because Node/Bun
|
|
131
|
-
// cannot resolve HTTP imports at runtime. Instead, bundleCdnImports: true
|
|
132
|
-
// tells the VFS plugin to let native esbuild fetch and bundle CDN imports.
|
|
133
|
-
const result = await esbuild.build({
|
|
134
|
-
entryPoints: [normalizedEntry],
|
|
135
|
-
bundle: true,
|
|
136
|
-
write: false,
|
|
137
|
-
format,
|
|
138
|
-
minify,
|
|
139
|
-
sourcemap: sourcemap ? "inline" : false,
|
|
140
|
-
target,
|
|
141
|
-
external,
|
|
142
|
-
// Cast to esbuild's Plugin type since our minimal interface is compatible
|
|
143
|
-
plugins: [plugin as EsbuildTypes.Plugin],
|
|
144
|
-
jsx: "automatic",
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const code = result.outputFiles?.[0]?.text ?? "";
|
|
148
|
-
|
|
149
|
-
// Convert esbuild warnings to our format
|
|
150
|
-
const warnings: BundleWarning[] = result.warnings.map((w) =>
|
|
151
|
-
convertEsbuildMessage(w)
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
success: true,
|
|
156
|
-
code,
|
|
157
|
-
warnings,
|
|
158
|
-
includedFiles: Array.from(includedFiles),
|
|
159
|
-
};
|
|
160
|
-
} catch (err) {
|
|
161
|
-
// esbuild throws BuildFailure with .errors array
|
|
162
|
-
if (isEsbuildBuildFailure(err)) {
|
|
163
|
-
const errors: BundleError[] = err.errors.map((e) =>
|
|
164
|
-
convertEsbuildMessage(e)
|
|
165
|
-
);
|
|
166
|
-
const warnings: BundleWarning[] = err.warnings.map((w) =>
|
|
167
|
-
convertEsbuildMessage(w)
|
|
168
|
-
);
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
errors,
|
|
172
|
-
warnings,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Unknown error - wrap it
|
|
177
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
-
return {
|
|
179
|
-
success: false,
|
|
180
|
-
errors: [{ text: message }],
|
|
181
|
-
warnings: [],
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
91
|
}
|
|
185
92
|
}
|
|
186
93
|
|
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(
|