Files
ai-terminal-assistant/scripts/build-standalone.ts
T
kurihada c3db79c00d feat: 支持 Bun standalone 单文件打包
- core: 工具描述从文件系统加载改为编译时内联生成
- core: 添加 WASM 加载器支持嵌入式 WASM 数据
- core: bash-parser 使用动态导入 web-tree-sitter
- server: 添加静态文件托管支持 (--static 参数)
- server: 新增 standalone 入口点 (嵌入 Web UI + WASM)
- scripts: 添加 build-standalone.ts 构建脚本
- 更新 .gitignore 忽略生成文件
2025-12-30 13:57:29 +08:00

286 lines
9.3 KiB
TypeScript

#!/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 <path> 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<void> {
const assets: Map<string, { content: string; mimeType: string }> = 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<string, EmbeddedAsset> = 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<string, string> = {
'.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<void> {
const wasmFiles: Map<string, string> = 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<string, string> = 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);
});