branch:
bundler.ts
6594 bytesRaw
/**
* esbuild-wasm bundling functionality.
*/
// Use the browser entry directly — the default "main" entry rejects
// wasmModule in Workers with nodejs_compat (it thinks it's Node.js).
import * as esbuild from "esbuild-wasm/lib/browser.js";
// @ts-expect-error - WASM module import
import esbuildWasm from "./esbuild.wasm";
import { resolveModule } from "./resolver";
import type { CreateWorkerResult, Files, Modules } from "./types";
/**
* Bundle files using esbuild-wasm
*/
export async function bundleWithEsbuild(
files: Files,
entryPoint: string,
externals: string[],
target: string,
minify: boolean,
sourcemap: boolean,
nodejsCompat: boolean
): Promise<CreateWorkerResult> {
// Ensure esbuild is initialized (happens lazily on first use)
await initializeEsbuild();
// Create a virtual file system plugin for esbuild
const virtualFsPlugin: esbuild.Plugin = {
name: "virtual-fs",
setup(build) {
// Resolve all paths to our virtual file system
build.onResolve({ filter: /.*/ }, (args) => {
// Handle entry point - it's passed directly without ./ prefix
if (args.kind === "entry-point") {
return { path: args.path, namespace: "virtual" };
}
// Handle relative imports
if (args.path.startsWith(".")) {
const resolved = resolveRelativePath(
args.resolveDir,
args.path,
files
);
if (resolved) {
return { path: resolved, namespace: "virtual" };
}
}
// Handle bare imports (npm packages)
if (!args.path.startsWith("/") && !args.path.startsWith(".")) {
// Check if it's in externals
if (
externals.includes(args.path) ||
externals.some(
(e) => args.path.startsWith(`${e}/`) || args.path.startsWith(e)
)
) {
return { path: args.path, external: true };
}
// Try to resolve from node_modules in virtual fs
try {
const result = resolveModule(args.path, { files });
if (!result.external) {
return { path: result.path, namespace: "virtual" };
}
} catch {
// Resolution failed
}
// Mark as external (package not found in node_modules)
return { path: args.path, external: true };
}
// Absolute paths in virtual fs
const normalizedPath = args.path.startsWith("/")
? args.path.slice(1)
: args.path;
if (normalizedPath in files) {
return { path: normalizedPath, namespace: "virtual" };
}
return { path: args.path, external: true };
});
// Load files from virtual file system
build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
const content = files[args.path];
if (content === undefined) {
return { errors: [{ text: `File not found: ${args.path}` }] };
}
const loader = getLoader(args.path);
// Set resolveDir so relative imports within this file resolve correctly
const lastSlash = args.path.lastIndexOf("/");
const resolveDir = lastSlash >= 0 ? args.path.slice(0, lastSlash) : "";
return { contents: content, loader, resolveDir };
});
}
};
const result = await esbuild.build({
entryPoints: [entryPoint],
bundle: true,
write: false,
format: "esm",
platform: nodejsCompat ? "node" : "browser",
target,
minify,
sourcemap: sourcemap ? "inline" : false,
plugins: [virtualFsPlugin],
outfile: "bundle.js"
});
const output = result.outputFiles?.[0];
if (!output) {
throw new Error("No output generated from esbuild");
}
const modules: Modules = {
"bundle.js": output.text
};
const warnings = result.warnings.map((w) => w.text);
if (warnings.length > 0) {
return { mainModule: "bundle.js", modules, warnings };
}
return { mainModule: "bundle.js", modules };
}
/**
* Resolve a relative path against a directory within the virtual filesystem.
*/
function resolveRelativePath(
resolveDir: string,
relativePath: string,
files: Files
): string | undefined {
// Normalize the resolve directory
const dir = resolveDir.replace(/^\//, "");
// Resolve the relative path
const parts = dir ? dir.split("/") : [];
const relParts = relativePath.split("/");
for (const part of relParts) {
if (part === "..") {
parts.pop();
} else if (part !== ".") {
parts.push(part);
}
}
const resolved = parts.join("/");
// Try exact match
if (resolved in files) {
return resolved;
}
// Try adding extensions
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"];
for (const ext of extensions) {
if (resolved + ext in files) {
return resolved + ext;
}
}
// Try index files
for (const ext of extensions) {
const indexPath = `${resolved}/index${ext}`;
if (indexPath in files) {
return indexPath;
}
}
return undefined;
}
function getLoader(path: string): esbuild.Loader {
if (path.endsWith(".ts") || path.endsWith(".mts")) return "ts";
if (path.endsWith(".tsx")) return "tsx";
if (path.endsWith(".jsx")) return "jsx";
if (path.endsWith(".json")) return "json";
if (path.endsWith(".css")) return "css";
return "js";
}
// Track esbuild initialization state
let esbuildInitialized = false;
let esbuildInitializePromise: Promise<void> | null = null;
/**
* Initialize the esbuild bundler.
* This is called automatically when needed.
*/
async function initializeEsbuild(): Promise<void> {
// If already initialized, return immediately
if (esbuildInitialized) return;
// If initialization is in progress, wait for it
if (esbuildInitializePromise) {
return esbuildInitializePromise;
}
// Start initialization
esbuildInitializePromise = (async () => {
try {
await esbuild.initialize({
wasmModule: esbuildWasm,
worker: false
});
esbuildInitialized = true;
} catch (error) {
// If initialization fails, esbuild may already be initialized
if (
error instanceof Error &&
error.message.includes('Cannot call "initialize" more than once')
) {
esbuildInitialized = true;
return;
}
throw error;
}
})();
try {
await esbuildInitializePromise;
} catch (error) {
// Reset promise so caller can try again
esbuildInitializePromise = null;
throw error;
}
}