2026-02-06 23:01:30 -05:00

119 lines
4.4 KiB
TypeScript

/**
* MCPEngine Studio — Server Compiler
*
* Takes a ServerBundle of generated TypeScript files and produces a single
* bundled JavaScript string ready for Cloudflare Workers deployment.
*
* NOTE: This is Phase-1 concatenation. Real esbuild bundling will be added later.
*/
import type { ServerBundle, GeneratedFile } from '@mcpengine/ai-pipeline/types';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Strip TypeScript type annotations in a best-effort way (imports, type-only
* exports, interface/type blocks). Good enough for simple generated code. */
function stripTypes(src: string): string {
return src
// Remove `import type …` lines
.replace(/^import\s+type\s+.*$/gm, '')
// Remove `export type …` and `export interface …` blocks (single-line + multi-line)
.replace(/^export\s+(type|interface)\s+\w+[^{]*\{[^}]*\}/gm, '')
.replace(/^export\s+(type|interface)\s+.*$/gm, '')
// Remove inline type annotations `: Foo` after parameter names (rough)
.replace(/:\s*[A-Z]\w+(\[\])?\s*(,|\)|\s*=>)/g, (_m, _arr, tail) => tail)
// Remove `as Type` casts
.replace(/\s+as\s+\w+/g, '')
// Collapse blank lines
.replace(/\n{3,}/g, '\n\n');
}
/** Convert TS import paths to inline references (they all live in the same
* concatenated scope so we just drop the import statements). */
function stripImports(src: string): string {
return src.replace(/^import\s+.*$/gm, '');
}
/** Wrap file content in an IIFE-scoped module to avoid collisions. */
function wrapModule(name: string, code: string): string {
return [
`// ── ${name} ${'─'.repeat(Math.max(0, 60 - name.length))}`,
`const __mod_${safeId(name)} = (() => {`,
` const exports = {};`,
` const module = { exports };`,
code,
` return module.exports;`,
`})();`,
'',
].join('\n');
}
function safeId(path: string): string {
return path.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_');
}
// ---------------------------------------------------------------------------
// Main compiler
// ---------------------------------------------------------------------------
export function compileServer(bundle: ServerBundle): string {
const sections: string[] = [];
// 1. Banner
sections.push(
'// ═══════════════════════════════════════════════════════════',
'// MCPEngine Studio — Compiled MCP Server',
`// Generated at ${new Date().toISOString()}`,
`// Tools: ${bundle.toolCount}`,
'// ═══════════════════════════════════════════════════════════',
'',
);
// 2. Sort files: put the entry point last
const sorted = [...bundle.files].sort((a, b) => {
if (a.path === bundle.entryPoint) return 1;
if (b.path === bundle.entryPoint) return -1;
// Utility / shared modules first
if (a.path.includes('utils') || a.path.includes('shared')) return -1;
if (b.path.includes('utils') || b.path.includes('shared')) return 1;
return a.path.localeCompare(b.path);
});
// 3. Process each file
for (const file of sorted) {
if (file.language === 'json' || file.language === 'markdown') continue;
let code = file.content;
code = stripTypes(code);
code = stripImports(code);
sections.push(wrapModule(file.path, code));
}
// 4. Export a fetch handler that the Worker template will consume
sections.push(
'// ── Fetch handler export ──────────────────────────────────',
`const __entryModule = __mod_${safeId(bundle.entryPoint)};`,
'export const serverModule = __entryModule;',
'',
);
return sections.join('\n');
}
/** Compile and return metadata useful for deploy targets. */
export function compileWithMeta(bundle: ServerBundle) {
const compiledCode = compileServer(bundle);
const toolNames = bundle.files
.filter((f) => f.path.includes('tools/'))
.map((f) => f.path.replace(/^.*tools\//, '').replace(/\.ts$/, ''));
return {
code: compiledCode,
toolNames,
toolCount: bundle.toolCount,
fileCount: bundle.files.length,
sizeBytes: new TextEncoder().encode(compiledCode).byteLength,
};
}