run esbuild wasm

This commit is contained in:
Fredrik Jensen 2025-12-06 17:11:46 +01:00
parent b76b8c4c68
commit 2b64a038ce
4 changed files with 73 additions and 28 deletions

15
package-lock.json generated
View File

@ -1197,6 +1197,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/esbuild-wasm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.12.tgz",
"integrity": "sha512-rZqkjL3Y6FwLpSHzLnaEy8Ps6veCNo1kZa9EOfJvmWtBq5dJH4iVjfmOO6Mlkv9B0tt9WFPFmb/VxlgJOnueNg==",
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@ -2851,7 +2863,8 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mcp-ui/server": "^5.15.0", "@mcp-ui/server": "^5.15.0",
"esbuild": "^0.25.0" "esbuild": "^0.25.0",
"esbuild-wasm": "^0.25.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^20.10.0",

View File

@ -47,7 +47,8 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mcp-ui/server": "^5.15.0", "@mcp-ui/server": "^5.15.0",
"esbuild": "^0.25.0" "esbuild": "^0.25.0",
"esbuild-wasm": "^0.25.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=18.0.0", "react": ">=18.0.0",

View File

@ -1,4 +1,4 @@
import * as esbuild from 'esbuild'; import type * as EsbuildType from 'esbuild';
// Cache bundled JS per entry path (only used in production) // Cache bundled JS per entry path (only used in production)
const bundleCache = new Map<string, string>(); const bundleCache = new Map<string, string>();
@ -6,12 +6,51 @@ const bundleCache = new Map<string, string>();
// In development mode, skip cache to allow hot-reloading of component changes // In development mode, skip cache to allow hot-reloading of component changes
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
async function runBuild(entryPath: string): Promise<esbuild.BuildResult<{ write: false }>> { // Esbuild instance - lazily loaded
console.log('[mcp-ui-kit] runBuild called for:', entryPath); let esbuild: typeof EsbuildType | null = null;
let usingWasm = false;
async function getEsbuild(): Promise<typeof EsbuildType> {
if (esbuild) return esbuild;
// Try native esbuild first
try {
console.log('[mcp-ui-kit] Trying native esbuild...');
const native = await import('esbuild');
// Test if native works by running a simple transform
await native.transform('const x = 1', { loader: 'js' });
console.log('[mcp-ui-kit] Native esbuild works!');
esbuild = native;
return esbuild;
} catch (nativeError) {
console.log('[mcp-ui-kit] Native esbuild failed:', nativeError instanceof Error ? nativeError.message : String(nativeError));
// Fall back to wasm
try {
console.log('[mcp-ui-kit] Falling back to esbuild-wasm...');
const wasm = await import('esbuild-wasm');
await wasm.initialize({
wasmURL: `https://unpkg.com/esbuild-wasm@${wasm.version}/esbuild.wasm`,
});
console.log('[mcp-ui-kit] esbuild-wasm initialized!');
esbuild = wasm as typeof EsbuildType;
usingWasm = true;
return esbuild;
} catch (wasmError) {
console.log('[mcp-ui-kit] esbuild-wasm also failed:', wasmError instanceof Error ? wasmError.message : String(wasmError));
throw wasmError;
}
}
}
async function runBuild(entryPath: string): Promise<EsbuildType.BuildResult<{ write: false }>> {
const esbuild = await getEsbuild();
console.log('[mcp-ui-kit] runBuild using', usingWasm ? 'wasm' : 'native', 'for:', entryPath);
return esbuild.build({ return esbuild.build({
entryPoints: [entryPath], entryPoints: [entryPath],
bundle: true, bundle: true,
write: false, // No disk I/O - keep everything in memory write: false,
format: 'iife', format: 'iife',
target: 'es2020', target: 'es2020',
jsx: 'automatic', jsx: 'automatic',
@ -25,45 +64,33 @@ async function runBuild(entryPath: string): Promise<esbuild.BuildResult<{ write:
export async function bundleComponent(entryPath: string): Promise<string> { export async function bundleComponent(entryPath: string): Promise<string> {
console.log('[mcp-ui-kit] bundleComponent called for:', entryPath); console.log('[mcp-ui-kit] bundleComponent called for:', entryPath);
console.log('[mcp-ui-kit] isDev:', isDev, 'NODE_ENV:', process.env.NODE_ENV);
console.log('[mcp-ui-kit] Cache has entry:', bundleCache.has(entryPath));
if (!isDev && bundleCache.has(entryPath)) { if (!isDev && bundleCache.has(entryPath)) {
console.log('[mcp-ui-kit] Returning cached bundle'); console.log('[mcp-ui-kit] Returning cached bundle');
return bundleCache.get(entryPath)!; return bundleCache.get(entryPath)!;
} }
let result: esbuild.BuildResult<{ write: false }>; let result: EsbuildType.BuildResult<{ write: false }>;
try { try {
console.log('[mcp-ui-kit] Attempting first build...');
result = await runBuild(entryPath); result = await runBuild(entryPath);
console.log('[mcp-ui-kit] First build succeeded'); console.log('[mcp-ui-kit] Build succeeded');
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : ''; console.log('[mcp-ui-kit] Build failed:', errorMessage);
console.log('[mcp-ui-kit] ========== BUILD ERROR ==========');
console.log('[mcp-ui-kit] Error message:', errorMessage);
console.log('[mcp-ui-kit] Error stack:', errorStack);
console.log('[mcp-ui-kit] Full error:', JSON.stringify(error, Object.getOwnPropertyNames(error || {}), 2));
console.log('[mcp-ui-kit] ================================');
// Handle esbuild service errors in serverless environments (Vercel, Lambda, etc.) // Handle service errors for native esbuild in serverless
// This happens when the serverless runtime freezes/stops the esbuild subprocess. const isServiceError = !usingWasm && error instanceof Error && (
const isServiceError = error instanceof Error && (
errorMessage.includes('service was stopped') || errorMessage.includes('service was stopped') ||
errorMessage.includes('service is no longer running') || errorMessage.includes('service is no longer running') ||
errorMessage.includes('The service') || errorMessage.includes('The service')
errorMessage.includes('could not be found')
); );
console.log('[mcp-ui-kit] Is service error:', isServiceError);
if (isServiceError) { if (isServiceError) {
// Force stop the dead service, then retry - esbuild will start fresh console.log('[mcp-ui-kit] Service error, stopping and retrying...');
console.log('[mcp-ui-kit] Stopping esbuild service and retrying...'); const esbuild = await getEsbuild();
await esbuild.stop(); await esbuild.stop();
result = await runBuild(entryPath); result = await runBuild(entryPath);
console.log('[mcp-ui-kit] Retry succeeded');
} else { } else {
throw error; throw error;
} }
@ -72,10 +99,8 @@ export async function bundleComponent(entryPath: string): Promise<string> {
const bundledJS = result.outputFiles[0].text; const bundledJS = result.outputFiles[0].text;
console.log('[mcp-ui-kit] Bundle size:', bundledJS.length, 'bytes'); console.log('[mcp-ui-kit] Bundle size:', bundledJS.length, 'bytes');
// Only cache in production mode
if (!isDev) { if (!isDev) {
bundleCache.set(entryPath, bundledJS); bundleCache.set(entryPath, bundledJS);
console.log('[mcp-ui-kit] Bundle cached');
} }
return bundledJS; return bundledJS;

View File

@ -713,6 +713,11 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
dependencies: dependencies:
es-errors "^1.3.0" es-errors "^1.3.0"
esbuild-wasm@^0.25.0:
version "0.25.12"
resolved "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.12.tgz"
integrity sha512-rZqkjL3Y6FwLpSHzLnaEy8Ps6veCNo1kZa9EOfJvmWtBq5dJH4iVjfmOO6Mlkv9B0tt9WFPFmb/VxlgJOnueNg==
esbuild@^0.25.0: esbuild@^0.25.0:
version "0.25.12" version "0.25.12"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz"
@ -1042,6 +1047,7 @@ mcp-ui-kit@*, "mcp-ui-kit@file:/Users/fedjens/projects/mcp-ui-kit/packages/libra
dependencies: dependencies:
"@mcp-ui/server" "^5.15.0" "@mcp-ui/server" "^5.15.0"
esbuild "^0.25.0" esbuild "^0.25.0"
esbuild-wasm "^0.25.0"
media-typer@^1.1.0: media-typer@^1.1.0:
version "1.1.0" version "1.1.0"