|
| 1 | +import { promises as fs } from 'node:fs' |
| 2 | +import path from 'node:path' |
| 3 | +import { build } from 'esbuild' |
| 4 | +import { globby } from 'globby' |
| 5 | + |
| 6 | +const ROUTE_PREFIX = '/mcp-use/widgets' |
| 7 | +const SRC_DIR = 'resources' |
| 8 | +const OUT_DIR = 'dist/resources' |
| 9 | + |
| 10 | +function toRoute(file: string) { |
| 11 | + const rel = file.replace(new RegExp(`^${SRC_DIR}/`), '').replace(/\.tsx?$/, '') |
| 12 | + return `${ROUTE_PREFIX}/${rel}` |
| 13 | +} |
| 14 | + |
| 15 | +function outDirForRoute(route: string) { |
| 16 | + return path.join(OUT_DIR, route.replace(/^\//, '')) |
| 17 | +} |
| 18 | + |
| 19 | +function htmlTemplate({ title, scriptPath }: { title: string, scriptPath: string }) { |
| 20 | + return `<!doctype html> |
| 21 | +<html lang="en"> |
| 22 | + <head> |
| 23 | + <meta charset="UTF-8" /> |
| 24 | + <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| 25 | + <title>${title} Widget</title> |
| 26 | + <style> |
| 27 | + body { |
| 28 | + margin: 0; |
| 29 | + padding: 20px; |
| 30 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; |
| 31 | + background: #f5f5f5; |
| 32 | + } |
| 33 | + #widget-root { |
| 34 | + max-width: 1200px; |
| 35 | + margin: 0 auto; |
| 36 | + } |
| 37 | + </style> |
| 38 | + </head> |
| 39 | + <body> |
| 40 | + <div id="widget-root"></div> |
| 41 | + <script type="module" src="${scriptPath}"></script> |
| 42 | + </body> |
| 43 | +</html>` |
| 44 | +} |
| 45 | + |
| 46 | +export async function buildWidgets(projectPath: string) { |
| 47 | + console.log('🔨 Building UI widgets with esbuild...') |
| 48 | + |
| 49 | + const srcDir = path.join(projectPath, SRC_DIR) |
| 50 | + const outDir = path.join(projectPath, OUT_DIR) |
| 51 | + |
| 52 | + // Clean dist |
| 53 | + await fs.rm(outDir, { recursive: true, force: true }) |
| 54 | + |
| 55 | + // Find all TSX entries |
| 56 | + const entries = await globby([`${srcDir}/**/*.tsx`]) |
| 57 | + console.log(`📦 Found ${entries.length} widget files`) |
| 58 | + |
| 59 | + // Build each entry as an isolated page with hashed output |
| 60 | + for (const entry of entries) { |
| 61 | + const relativePath = path.relative(projectPath, entry) |
| 62 | + const route = toRoute(relativePath) |
| 63 | + const pageOutDir = path.join(projectPath, outDirForRoute(route)) |
| 64 | + const baseName = path.parse(entry).name |
| 65 | + |
| 66 | + console.log(`🔨 Building ${baseName}...`) |
| 67 | + |
| 68 | + // Build JS/CSS chunks for this page |
| 69 | + await build({ |
| 70 | + entryPoints: [entry], |
| 71 | + bundle: true, |
| 72 | + splitting: true, |
| 73 | + format: 'esm', |
| 74 | + platform: 'browser', |
| 75 | + target: 'es2018', |
| 76 | + sourcemap: false, |
| 77 | + minify: true, |
| 78 | + outdir: path.join(pageOutDir, 'assets'), |
| 79 | + logLevel: 'silent', |
| 80 | + loader: { |
| 81 | + '.svg': 'file', |
| 82 | + '.png': 'file', |
| 83 | + '.jpg': 'file', |
| 84 | + '.jpeg': 'file', |
| 85 | + '.gif': 'file', |
| 86 | + '.css': 'css', |
| 87 | + }, |
| 88 | + entryNames: `[name]-[hash]`, |
| 89 | + chunkNames: `chunk-[hash]`, |
| 90 | + assetNames: `asset-[hash]`, |
| 91 | + define: { |
| 92 | + 'process.env.NODE_ENV': '"production"', |
| 93 | + }, |
| 94 | + }) |
| 95 | + |
| 96 | + // Find the main entry file name |
| 97 | + const files = await fs.readdir(path.join(pageOutDir, 'assets')) |
| 98 | + const mainJs = files.find(f => f.startsWith(`${baseName}-`) && f.endsWith('.js')) |
| 99 | + if (!mainJs) |
| 100 | + throw new Error(`Failed to locate entry JS for ${entry}`) |
| 101 | + |
| 102 | + // Write an index.html that points to the entry |
| 103 | + await fs.mkdir(pageOutDir, { recursive: true }) |
| 104 | + await fs.writeFile( |
| 105 | + path.join(pageOutDir, 'index.html'), |
| 106 | + htmlTemplate({ |
| 107 | + title: baseName, |
| 108 | + scriptPath: `./assets/${mainJs}`, |
| 109 | + }), |
| 110 | + 'utf8', |
| 111 | + ) |
| 112 | + |
| 113 | + console.log(`✅ Built ${baseName} -> ${route}`) |
| 114 | + } |
| 115 | + |
| 116 | + console.log('🎉 Build complete!') |
| 117 | +} |
| 118 | + |
0 commit comments