#!/usr/bin/env bun /** * Build Standalone Server with Embedded Web UI and WASM * * 构建包含 Web UI 和 WASM 文件的独立服务器可执行文件 * * Usage: * bun run scripts/build-standalone.ts * bun run scripts/build-standalone.ts --output ./dist/ai-assistant */ import * as fs from 'fs'; import * as path from 'path'; import { $ } from 'bun'; const ROOT_DIR = path.join(import.meta.dir, '..'); const WEB_DIR = path.join(ROOT_DIR, 'packages/web'); const SERVER_DIR = path.join(ROOT_DIR, 'packages/server'); const NODE_MODULES = path.join(ROOT_DIR, 'node_modules'); const CORE_DIR = path.join(ROOT_DIR, 'packages/core'); // 解析命令行参数 function parseArgs(): { output: string } { const args = process.argv.slice(2); let output = path.join(ROOT_DIR, 'dist/ai-assistant'); for (let i = 0; i < args.length; i++) { if (args[i] === '--output' || args[i] === '-o') { output = args[i + 1]; i++; } else if (args[i] === '--help' || args[i] === '-h') { console.log(` Build Standalone Server with Embedded Web UI Usage: bun run scripts/build-standalone.ts [options] Options: -o, --output Output executable path (default: dist/ai-assistant) -h, --help Show this help message `); process.exit(0); } } return { output }; } async function main() { const { output } = parseArgs(); const outputDir = path.dirname(output); console.log('🚀 Building standalone AI Assistant...\n'); // 1. 构建 Core console.log('📦 Building @ai-assistant/core...'); await $`pnpm --filter @ai-assistant/core build`.quiet(); console.log(' ✅ Core built\n'); // 2. 构建 Web console.log('📦 Building @ai-assistant/web...'); await $`pnpm --filter @ai-assistant/web build`.quiet(); console.log(' ✅ Web built\n'); // 3. 生成嵌入式 Web 资源 console.log('📝 Embedding Web UI assets...'); const webDistDir = path.join(WEB_DIR, 'dist'); const embeddedAssetsFile = path.join(SERVER_DIR, 'src/embedded-assets.generated.ts'); await generateEmbeddedAssets(webDistDir, embeddedAssetsFile); console.log(' ✅ Assets embedded\n'); // 3.5. 生成嵌入式 WASM 资源 console.log('📝 Embedding WASM files...'); const embeddedWasmFile = path.join(SERVER_DIR, 'src/embedded-wasm.generated.ts'); await generateEmbeddedWasm(embeddedWasmFile); console.log(' ✅ WASM embedded\n'); // 4. 构建 Server(使用嵌入式入口) console.log('📦 Building standalone server...'); // 确保输出目录存在 if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } await $`bun build ${SERVER_DIR}/src/bin/standalone.ts --compile --outfile ${output}`.quiet(); console.log(' ✅ Server built\n'); // 5. 清理生成的文件 if (fs.existsSync(embeddedAssetsFile)) { fs.unlinkSync(embeddedAssetsFile); } if (fs.existsSync(embeddedWasmFile)) { fs.unlinkSync(embeddedWasmFile); } // 6. 显示结果 const stats = fs.statSync(output); const sizeMB = (stats.size / 1024 / 1024).toFixed(1); console.log('═══════════════════════════════════════════'); console.log('✅ Build complete!'); console.log('═══════════════════════════════════════════'); console.log(` Output: ${output}`); console.log(` Size: ${sizeMB} MB`); console.log(''); console.log('Usage:'); console.log(` ${output} --help`); console.log(` ${output} --port 3000`); console.log('═══════════════════════════════════════════'); } /** * 将 Web dist 目录的所有文件转换为嵌入式 TypeScript 模块 */ async function generateEmbeddedAssets(webDistDir: string, outputFile: string): Promise { const assets: Map = new Map(); // 递归收集所有文件 function collectFiles(dir: string, basePath: string = '') { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { collectFiles(fullPath, relativePath); } else { const content = fs.readFileSync(fullPath); const base64 = content.toString('base64'); const mimeType = getMimeType(entry.name); assets.set('/' + relativePath, { content: base64, mimeType }); } } } collectFiles(webDistDir); // 生成 TypeScript 代码 const lines: string[] = [ '// Auto-generated by scripts/build-standalone.ts', '// Do not edit manually', '', 'export interface EmbeddedAsset {', ' content: string; // base64 encoded', ' mimeType: string;', '}', '', 'export const EMBEDDED_ASSETS: Map = new Map([', ]; for (const [path, asset] of assets) { // 转义特殊字符 const escapedContent = asset.content; lines.push(` ['${path}', { content: '${escapedContent}', mimeType: '${asset.mimeType}' }],`); } lines.push(']);'); lines.push(''); lines.push('export function getAsset(path: string): EmbeddedAsset | undefined {'); lines.push(' return EMBEDDED_ASSETS.get(path);'); lines.push('}'); lines.push(''); lines.push('export function decodeAsset(asset: EmbeddedAsset): Uint8Array {'); lines.push(' const binaryString = atob(asset.content);'); lines.push(' const bytes = new Uint8Array(binaryString.length);'); lines.push(' for (let i = 0; i < binaryString.length; i++) {'); lines.push(' bytes[i] = binaryString.charCodeAt(i);'); lines.push(' }'); lines.push(' return bytes;'); lines.push('}'); lines.push(''); fs.writeFileSync(outputFile, lines.join('\n'), 'utf-8'); console.log(` Generated ${assets.size} embedded assets`); } /** * 根据文件扩展名获取 MIME 类型 */ function getMimeType(filename: string): string { const ext = path.extname(filename).toLowerCase(); const mimeTypes: Record = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', '.webmanifest': 'application/manifest+json', '.txt': 'text/plain', '.xml': 'application/xml', }; return mimeTypes[ext] || 'application/octet-stream'; } /** * 生成嵌入式 WASM 模块 * 包含 tree-sitter.wasm 和可用的语言 WASM 文件 */ async function generateEmbeddedWasm(outputFile: string): Promise { const wasmFiles: Map = new Map(); // 查找 web-tree-sitter 的 tree-sitter.wasm const treeSitterWasmPaths = [ path.join(NODE_MODULES, '.pnpm/web-tree-sitter@0.25.10/node_modules/web-tree-sitter/tree-sitter.wasm'), path.join(NODE_MODULES, 'web-tree-sitter/tree-sitter.wasm'), ]; for (const wasmPath of treeSitterWasmPaths) { if (fs.existsSync(wasmPath)) { const content = fs.readFileSync(wasmPath); wasmFiles.set('tree-sitter.wasm', content.toString('base64')); console.log(` Found tree-sitter.wasm (${(content.length / 1024).toFixed(1)} KB)`); break; } } // 查找语言特定的 WASM 文件 const languageWasmPatterns = [ // pnpm 格式 { dir: path.join(NODE_MODULES, '.pnpm'), pattern: /tree-sitter-(\w+)@.*\/tree-sitter-\1\.wasm$/ }, ]; // 扫描 node_modules/.pnpm 查找语言 WASM const pnpmDir = path.join(NODE_MODULES, '.pnpm'); if (fs.existsSync(pnpmDir)) { const entries = fs.readdirSync(pnpmDir); for (const entry of entries) { if (entry.startsWith('tree-sitter-') && !entry.startsWith('tree-sitter-wasms')) { // 提取语言名称 const match = entry.match(/^tree-sitter-(\w+)@/); if (match) { const lang = match[1]; const wasmPath = path.join(pnpmDir, entry, 'node_modules', `tree-sitter-${lang}`, `tree-sitter-${lang}.wasm`); if (fs.existsSync(wasmPath)) { const content = fs.readFileSync(wasmPath); wasmFiles.set(`tree-sitter-${lang}.wasm`, content.toString('base64')); console.log(` Found tree-sitter-${lang}.wasm (${(content.length / 1024).toFixed(1)} KB)`); } } } } } if (wasmFiles.size === 0) { console.warn(' ⚠️ No WASM files found'); } // 生成 TypeScript 代码 const lines: string[] = [ '// Auto-generated by scripts/build-standalone.ts', '// Do not edit manually', '', '/**', ' * Embedded WASM files for tree-sitter', ' * Keys: wasm filename, Values: base64 encoded content', ' */', 'export const EMBEDDED_WASM: Map = new Map([', ]; for (const [name, base64] of wasmFiles) { lines.push(` ['${name}', '${base64}'],`); } lines.push(']);'); lines.push(''); fs.writeFileSync(outputFile, lines.join('\n'), 'utf-8'); console.log(` Generated ${wasmFiles.size} embedded WASM files`); } main().catch((error) => { console.error('❌ Build failed:', error); process.exit(1); });