c3db79c00d
- core: 工具描述从文件系统加载改为编译时内联生成 - core: 添加 WASM 加载器支持嵌入式 WASM 数据 - core: bash-parser 使用动态导入 web-tree-sitter - server: 添加静态文件托管支持 (--static 参数) - server: 新增 standalone 入口点 (嵌入 Web UI + WASM) - scripts: 添加 build-standalone.ts 构建脚本 - 更新 .gitignore 忽略生成文件
286 lines
9.3 KiB
TypeScript
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);
|
|
});
|