UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

774 lines (658 loc) 22.2 kB
/** * @fileoverview OrdoJS Code Generator - Refactored modular implementation * @author OrdoJS Framework Team */ import type { ComponentAST, ComponentNode, ExpressionNode, GeneratedCode, MarkupBlockNode, Props, ServerBlockNode, SourceMap, StatementNode } from '../types/index.js'; /** * Code generation target environments */ export type CodeGenTarget = 'client' | 'server' | 'static' | 'universal'; /** * Code generation options */ export interface CodeGeneratorOptions { /** Target environment */ target?: 'development' | 'production'; /** Enable code minification */ minify?: boolean; /** Generate source maps */ sourceMaps?: boolean; /** Target ES version */ esTarget?: 'es2022' | 'es2020' | 'es2018' | 'es5'; /** Enable tree shaking */ treeShaking?: boolean; /** Custom transformers */ transformers?: CodeTransformer[]; /** Output format */ format?: 'esm' | 'cjs' | 'umd' | 'iife'; /** Enable hot module replacement */ hmr?: boolean; /** Bundle splitting strategy */ splitting?: 'none' | 'chunks' | 'modules'; } /** * Code transformer interface for plugin-based transformations */ export interface CodeTransformer { /** Transformer name */ name: string; /** Target phase */ phase: 'pre' | 'post' | 'optimize'; /** Target code type */ targets: CodeGenTarget[]; /** Transform function */ transform(code: string, context: TransformContext): TransformResult; } /** * Transform context for code transformers */ export interface TransformContext { /** Original AST */ ast: ComponentAST; /** Generation options */ options: Required<CodeGeneratorOptions>; /** Target environment */ target: CodeGenTarget; /** Component metadata */ metadata: ComponentMetadata; /** Source map builder */ sourceMapBuilder?: SourceMapBuilder; } /** * Transform result */ export interface TransformResult { /** Transformed code */ code: string; /** Source map changes */ sourceMap?: SourceMapSegment[]; /** Additional metadata */ metadata?: Record<string, any>; } /** * Component metadata for code generation */ export interface ComponentMetadata { name: string; hasClientCode: boolean; hasServerCode: boolean; hasStyles: boolean; dependencies: string[]; exports: string[]; props: string[]; state: string[]; functions: string[]; lifecycle: string[]; } /** * Source map segment */ export interface SourceMapSegment { generatedLine: number; generatedColumn: number; originalLine: number; originalColumn: number; source: string; name?: string; } /** * Source map builder utility */ export class SourceMapBuilder { private segments: SourceMapSegment[] = []; private sources: string[] = []; private names: string[] = []; addMapping( generatedLine: number, generatedColumn: number, originalLine: number, originalColumn: number, source: string, name?: string ): void { if (!this.sources.includes(source)) { this.sources.push(source); } if (name && !this.names.includes(name)) { this.names.push(name); } this.segments.push({ generatedLine, generatedColumn, originalLine, originalColumn, source, name }); } build(): SourceMap { return { version: 3, sources: this.sources, names: this.names, mappings: this.encodeMappings(), sourcesContent: [] }; } private encodeMappings(): string { // Simplified VLQ encoding - in production, use proper source map library return this.segments .map(seg => `${seg.generatedLine},${seg.generatedColumn},${seg.originalLine},${seg.originalColumn}`) .join(';'); } } /** * Code generation context for tracking state */ export interface CodeGenContext { /** Current indentation level */ indent: number; /** Generated code lines */ lines: string[]; /** Source map builder */ sourceMap?: SourceMapBuilder; /** Variable scope tracking */ scopes: Map<string, Set<string>>; /** Current scope depth */ scopeDepth: number; /** Import statements */ imports: Map<string, Set<string>>; /** Export statements */ exports: Set<string>; } /** * Enhanced OrdoJS Code Generator with modular architecture */ export class OrdoJSCodeGenerator { private options: Required<CodeGeneratorOptions>; private transformers: Map<string, CodeTransformer[]>; constructor(options: CodeGeneratorOptions = {}) { this.options = { target: 'development', minify: false, sourceMaps: true, esTarget: 'es2022', treeShaking: true, transformers: [], format: 'esm', hmr: false, splitting: 'none', ...options }; this.transformers = new Map([ ['pre', []], ['post', []], ['optimize', []] ]); this.registerDefaultTransformers(); this.registerCustomTransformers(); } /** * Generate code for all targets */ generate(ast: ComponentAST): GeneratedCode { const metadata = this.extractMetadata(ast); const context = this.createTransformContext(ast, metadata); const results: Partial<GeneratedCode> = {}; // Generate client code if (metadata.hasClientCode || ast.component.markupBlock) { results.client = this.generateClientCode(ast, context); } // Generate server code if (metadata.hasServerCode) { results.server = this.generateServerCode(ast, context); } // Generate HTML results.html = this.generateHTML(ast, context); // Generate CSS if (metadata.hasStyles) { results.css = this.generateCSS(ast, context); } // Generate source map if (this.options.sourceMaps) { results.sourceMap = context.sourceMapBuilder?.build() || this.createEmptySourceMap(); } return results as GeneratedCode; } /** * Generate client-side code */ generateClientCode(ast: ComponentAST, context: TransformContext): string { const genContext = this.createCodeGenContext(); this.generateClientImports(ast, genContext); this.generateClientComponent(ast.component, genContext); this.generateClientExports(ast, genContext); let code = genContext.lines.join('\n'); // Apply transformers code = this.applyTransformers(code, { ...context, target: 'client' }); return code; } /** * Generate server-side code */ generateServerCode(ast: ComponentAST, context: TransformContext): string { if (!ast.component.serverBlock) { return ''; } const genContext = this.createCodeGenContext(); this.generateServerImports(ast, genContext); this.generateServerFunctions(ast.component.serverBlock, genContext); this.generateServerExports(ast, genContext); let code = genContext.lines.join('\n'); // Apply transformers code = this.applyTransformers(code, { ...context, target: 'server' }); return code; } /** * Generate HTML template */ generateHTML(ast: ComponentAST, context: TransformContext, props: Props = {}): string { const genContext = this.createCodeGenContext(); this.generateHTMLStructure(ast.component.markupBlock, genContext, props); let code = genContext.lines.join('\n'); // Apply transformers code = this.applyTransformers(code, { ...context, target: 'static' }); return code; } /** * Generate CSS styles */ generateCSS(ast: ComponentAST, context: TransformContext): string { // CSS generation would be implemented here return ''; } /** * Add custom transformer */ addTransformer(transformer: CodeTransformer): void { const phaseTransformers = this.transformers.get(transformer.phase) || []; phaseTransformers.push(transformer); this.transformers.set(transformer.phase, phaseTransformers); } /** * Remove transformer by name */ removeTransformer(name: string): boolean { for (const [phase, transformers] of this.transformers) { const index = transformers.findIndex(t => t.name === name); if (index >= 0) { transformers.splice(index, 1); return true; } } return false; } private registerDefaultTransformers(): void { // Minification transformer this.addTransformer({ name: 'minifier', phase: 'optimize', targets: ['client', 'server'], transform: (code: string, context: TransformContext): TransformResult => { if (!context.options.minify) { return { code }; } // Simple minification - in production, use proper minifier const minified = code .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments .replace(/\/\/.*$/gm, '') // Remove single-line comments .replace(/\s+/g, ' ') // Collapse whitespace .replace(/;\s*}/g, '}') // Remove unnecessary semicolons .trim(); return { code: minified }; } }); // ES target transformer this.addTransformer({ name: 'es-target', phase: 'post', targets: ['client', 'server'], transform: (code: string, context: TransformContext): TransformResult => { if (context.options.esTarget === 'es2022') { return { code }; } // Transform modern JS to older versions let transformed = code; if (context.options.esTarget === 'es2018' || context.options.esTarget === 'es5') { // Transform arrow functions to regular functions transformed = transformed.replace( /(\w+)\s*=>\s*{([^}]*)}/g, 'function($1) { $2 }' ); // Transform const/let to var for ES5 if (context.options.esTarget === 'es5') { transformed = transformed.replace(/\b(const|let)\b/g, 'var'); } } return { code: transformed }; } }); // HMR transformer this.addTransformer({ name: 'hmr', phase: 'post', targets: ['client'], transform: (code: string, context: TransformContext): TransformResult => { if (!context.options.hmr || context.options.target === 'production') { return { code }; } const hmrCode = ` // Hot Module Replacement if (module.hot) { module.hot.accept(); module.hot.dispose(() => { // Cleanup code }); } `; return { code: code + '\n' + hmrCode }; } }); } private registerCustomTransformers(): void { // Register any custom transformers from options for (const transformer of this.options.transformers) { this.addTransformer(transformer); } } private extractMetadata(ast: ComponentAST): ComponentMetadata { const component = ast.component; return { name: component.name, hasClientCode: !!component.clientBlock, hasServerCode: !!component.serverBlock, hasStyles: false, // Would check for style blocks dependencies: ast.dependencies, exports: ast.exports, props: component.props.map(p => p.name), state: component.clientBlock?.reactiveVariables.map(v => v.name) || [], functions: [ ...(component.clientBlock?.functions.map(f => f.name) || []), ...(component.serverBlock?.functions.map(f => f.name) || []) ], lifecycle: component.clientBlock?.lifecycle.map(l => l.hookType.toString()) || [] }; } private createTransformContext(ast: ComponentAST, metadata: ComponentMetadata): TransformContext { return { ast, options: this.options, target: 'client', metadata, sourceMapBuilder: this.options.sourceMaps ? new SourceMapBuilder() : undefined }; } private createCodeGenContext(): CodeGenContext { return { indent: 0, lines: [], sourceMap: this.options.sourceMaps ? new SourceMapBuilder() : undefined, scopes: new Map(), scopeDepth: 0, imports: new Map(), exports: new Set() }; } private generateClientImports(ast: ComponentAST, context: CodeGenContext): void { // Generate import statements if (this.options.format === 'esm') { context.lines.push("import { createSignal, createEffect } from '@ordojs/runtime';"); } else { context.lines.push("const { createSignal, createEffect } = require('@ordojs/runtime');"); } context.lines.push(''); } private generateClientComponent(component: ComponentNode, context: CodeGenContext): void { const componentName = component.name; // Function declaration context.lines.push(`function ${componentName}(props = {}) {`); context.indent++; // Generate reactive variables if (component.clientBlock?.reactiveVariables) { for (const variable of component.clientBlock.reactiveVariables) { this.generateReactiveVariable(variable, context); } context.lines.push(''); } // Generate functions if (component.clientBlock?.functions) { for (const func of component.clientBlock.functions) { this.generateFunction(func, context); } context.lines.push(''); } // Generate lifecycle hooks if (component.clientBlock?.lifecycle) { for (const hook of component.clientBlock.lifecycle) { this.generateLifecycleHook(hook, context); } context.lines.push(''); } // Generate render function this.generateRenderFunction(component.markupBlock, context); // Return component API context.lines.push(' return {'); context.lines.push(' mount,'); context.lines.push(' unmount,'); context.lines.push(' render,'); context.lines.push(' getState: () => ({ ...state }),'); context.lines.push(' setState: (key, value) => { state[key] = value; }'); context.lines.push(' };'); context.indent--; context.lines.push('}'); context.lines.push(''); } private generateReactiveVariable(variable: any, context: CodeGenContext): void { const indent = ' '.repeat(context.indent); const initialValue = this.generateExpression(variable.initialValue); if (variable.isConst) { context.lines.push(`${indent}const ${variable.name} = ${initialValue};`); } else { context.lines.push(`${indent}const [${variable.name}, set${this.capitalize(variable.name)}] = createSignal(${initialValue});`); } } private generateFunction(func: any, context: CodeGenContext): void { const indent = ' '.repeat(context.indent); const params = func.parameters.map((p: any) => p.name).join(', '); context.lines.push(`${indent}function ${func.name}(${params}) {`); for (const statement of func.body) { this.generateStatement(statement, context); } context.lines.push(`${indent}}`); } private generateLifecycleHook(hook: any, context: CodeGenContext): void { const indent = ' '.repeat(context.indent); context.lines.push(`${indent}createEffect(() => {`); for (const statement of hook.handler) { this.generateStatement(statement, context); } context.lines.push(`${indent}});`); } private generateRenderFunction(markupBlock: MarkupBlockNode, context: CodeGenContext): void { const indent = ' '.repeat(context.indent); context.lines.push(`${indent}function render() {`); context.lines.push(`${indent} const element = document.createElement('div');`); context.lines.push(`${indent} element.className = 'ordojs-component';`); // Generate markup rendering logic for (const element of markupBlock.elements) { this.generateHTMLElement(element, context); } context.lines.push(`${indent} return element;`); context.lines.push(`${indent}}`); context.lines.push(''); context.lines.push(`${indent}function mount(target) {`); context.lines.push(`${indent} const element = render();`); context.lines.push(`${indent} target.appendChild(element);`); context.lines.push(`${indent}}`); context.lines.push(''); context.lines.push(`${indent}function unmount() {`); context.lines.push(`${indent} // Cleanup logic`); context.lines.push(`${indent}}`); } private generateHTMLElement(element: any, context: CodeGenContext): void { const indent = ' '.repeat(context.indent + 1); context.lines.push(`${indent}// HTML element: ${element.tagName || 'div'}`); } private generateStatement(statement: StatementNode, context: CodeGenContext): void { const indent = ' '.repeat(context.indent + 1); if (statement.expression) { const expr = this.generateExpression(statement.expression); context.lines.push(`${indent}${expr};`); } } private generateExpression(expression: ExpressionNode): string { switch (expression.expressionType) { case 'LITERAL': return JSON.stringify(expression.value); case 'IDENTIFIER': return expression.identifier || 'undefined'; case 'BINARY': const left = this.generateExpression(expression.left!); const right = this.generateExpression(expression.right!); return `${left} ${expression.operator} ${right}`; case 'CALL': const callee = this.generateExpression(expression.callee!); const args = expression.arguments?.map(arg => this.generateExpression(arg)).join(', ') || ''; return `${callee}(${args})`; default: return 'undefined'; } } private generateServerImports(ast: ComponentAST, context: CodeGenContext): void { context.lines.push("const { createServerFunction } = require('@ordojs/runtime');"); context.lines.push(''); } private generateServerFunctions(serverBlock: ServerBlockNode, context: CodeGenContext): void { for (const func of serverBlock.functions) { this.generateServerFunction(func, context); } } private generateServerFunction(func: any, context: CodeGenContext): void { const params = func.parameters.map((p: any) => p.name).join(', '); const visibility = func.isPublic ? 'public' : 'private'; context.lines.push(`// ${visibility} server function`); context.lines.push(`async function ${func.name}(${params}) {`); for (const statement of func.body) { this.generateStatement(statement, context); } context.lines.push('}'); context.lines.push(''); if (func.isPublic) { context.exports.add(func.name); } } private generateServerExports(ast: ComponentAST, context: CodeGenContext): void { if (context.exports.size > 0) { const exports = Array.from(context.exports).join(', '); if (this.options.format === 'esm') { context.lines.push(`export { ${exports} };`); } else { context.lines.push(`module.exports = { ${exports} };`); } } } private generateClientExports(ast: ComponentAST, context: CodeGenContext): void { const componentName = ast.component.name; if (this.options.format === 'esm') { context.lines.push(`export default ${componentName};`); context.lines.push(`export { ${componentName} };`); } else if (this.options.format === 'cjs') { context.lines.push(`module.exports = ${componentName};`); context.lines.push(`module.exports.${componentName} = ${componentName};`); } else if (this.options.format === 'umd') { context.lines.push(` (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.OrdoJS = {})); }(this, (function (exports) { exports.${componentName} = ${componentName}; })));`); } else if (this.options.format === 'iife') { context.lines.push(`window.${componentName} = ${componentName};`); } } private generateHTMLStructure(markupBlock: MarkupBlockNode, context: CodeGenContext, props: Props): void { context.lines.push('<div class="ordojs-component">'); // Generate HTML from markup block for (const element of markupBlock.elements) { context.lines.push(` <!-- ${element.tagName || 'element'} -->`); context.lines.push(' <div>Generated HTML content</div>'); } context.lines.push('</div>'); } private applyTransformers(code: string, context: TransformContext): string { const phases: Array<'pre' | 'post' | 'optimize'> = ['pre', 'post', 'optimize']; for (const phase of phases) { const transformers = this.transformers.get(phase) || []; for (const transformer of transformers) { if (transformer.targets.includes(context.target)) { const result = transformer.transform(code, context); code = result.code; } } } return code; } private createEmptySourceMap(): SourceMap { return { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] }; } private capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } } /** * Default code transformers */ export const defaultTransformers: CodeTransformer[] = [ { name: 'import-optimizer', phase: 'optimize', targets: ['client', 'server'], transform: (code: string, context: TransformContext): TransformResult => { // Remove unused imports const lines = code.split('\n'); const optimizedLines = lines.filter(line => { if (line.trim().startsWith('import') || line.trim().startsWith('const')) { // Simple check - in production, use proper AST analysis return true; } return true; }); return { code: optimizedLines.join('\n') }; } }, { name: 'dead-code-eliminator', phase: 'optimize', targets: ['client', 'server'], transform: (code: string, context: TransformContext): TransformResult => { if (!context.options.treeShaking) { return { code }; } // Simple dead code elimination - in production, use proper analysis const lines = code.split('\n'); const optimizedLines = lines.filter(line => { const trimmed = line.trim(); return trimmed !== '' && !trimmed.startsWith('//'); }); return { code: optimizedLines.join('\n') }; } } ];