diff --git a/package-lock.json b/package-lock.json index 26fb474..da9b9b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1197,6 +1197,18 @@ "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": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2851,7 +2863,8 @@ "license": "MIT", "dependencies": { "@mcp-ui/server": "^5.15.0", - "esbuild": "^0.25.0" + "esbuild": "^0.25.0", + "esbuild-wasm": "^0.25.0" }, "devDependencies": { "@types/node": "^20.10.0", diff --git a/packages/library/package.json b/packages/library/package.json index fcbd3fe..9f4a8bf 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -47,7 +47,8 @@ "license": "MIT", "dependencies": { "@mcp-ui/server": "^5.15.0", - "esbuild": "^0.25.0" + "esbuild": "^0.25.0", + "esbuild-wasm": "^0.25.0" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/library/server/bundle.ts b/packages/library/server/bundle.ts index 78ddd92..31b029d 100644 --- a/packages/library/server/bundle.ts +++ b/packages/library/server/bundle.ts @@ -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) const bundleCache = new Map(); @@ -6,12 +6,51 @@ const bundleCache = new Map(); // In development mode, skip cache to allow hot-reloading of component changes const isDev = process.env.NODE_ENV !== 'production'; -async function runBuild(entryPath: string): Promise> { - console.log('[mcp-ui-kit] runBuild called for:', entryPath); +// Esbuild instance - lazily loaded +let esbuild: typeof EsbuildType | null = null; +let usingWasm = false; + +async function getEsbuild(): Promise { + 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> { + const esbuild = await getEsbuild(); + console.log('[mcp-ui-kit] runBuild using', usingWasm ? 'wasm' : 'native', 'for:', entryPath); + return esbuild.build({ entryPoints: [entryPath], bundle: true, - write: false, // No disk I/O - keep everything in memory + write: false, format: 'iife', target: 'es2020', jsx: 'automatic', @@ -25,45 +64,33 @@ async function runBuild(entryPath: string): Promise { 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)) { console.log('[mcp-ui-kit] Returning cached bundle'); return bundleCache.get(entryPath)!; } - let result: esbuild.BuildResult<{ write: false }>; + let result: EsbuildType.BuildResult<{ write: false }>; try { - console.log('[mcp-ui-kit] Attempting first build...'); result = await runBuild(entryPath); - console.log('[mcp-ui-kit] First build succeeded'); + console.log('[mcp-ui-kit] Build succeeded'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - const errorStack = error instanceof Error ? error.stack : ''; - 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] ================================'); + console.log('[mcp-ui-kit] Build failed:', errorMessage); - // Handle esbuild service errors in serverless environments (Vercel, Lambda, etc.) - // This happens when the serverless runtime freezes/stops the esbuild subprocess. - const isServiceError = error instanceof Error && ( + // Handle service errors for native esbuild in serverless + const isServiceError = !usingWasm && error instanceof Error && ( errorMessage.includes('service was stopped') || errorMessage.includes('service is no longer running') || - errorMessage.includes('The service') || - errorMessage.includes('could not be found') + errorMessage.includes('The service') ); - console.log('[mcp-ui-kit] Is service error:', isServiceError); if (isServiceError) { - // Force stop the dead service, then retry - esbuild will start fresh - console.log('[mcp-ui-kit] Stopping esbuild service and retrying...'); + console.log('[mcp-ui-kit] Service error, stopping and retrying...'); + const esbuild = await getEsbuild(); await esbuild.stop(); result = await runBuild(entryPath); - console.log('[mcp-ui-kit] Retry succeeded'); } else { throw error; } @@ -72,10 +99,8 @@ export async function bundleComponent(entryPath: string): Promise { const bundledJS = result.outputFiles[0].text; console.log('[mcp-ui-kit] Bundle size:', bundledJS.length, 'bytes'); - // Only cache in production mode if (!isDev) { bundleCache.set(entryPath, bundledJS); - console.log('[mcp-ui-kit] Bundle cached'); } return bundledJS; diff --git a/yarn.lock b/yarn.lock index 4430447..83f7f81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -713,6 +713,11 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: 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: version "0.25.12" 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: "@mcp-ui/server" "^5.15.0" esbuild "^0.25.0" + esbuild-wasm "^0.25.0" media-typer@^1.1.0: version "1.1.0"