UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

566 lines 24.7 kB
/** * @fileoverview OrdoJS Code Generator - Transforms AST into JavaScript and HTML */ import {} from '../types/index.js'; import { RPCGenerator } from './rpc-generator.js'; /** * Default code generator options */ const DEFAULT_OPTIONS = { minify: false, sourceMaps: true, target: 'development', compilationTarget: 'client' }; /** * OrdoJS Code Generator * Transforms AST into JavaScript and HTML output */ export class OrdoJSCodeGenerator { options; componentId; reactiveVars; eventHandlers; indentLevel = 0; sourceMap; rpcGenerator; constructor(options = {}) { this.options = { ...DEFAULT_OPTIONS, ...options }; this.componentId = ''; this.reactiveVars = new Map(); this.eventHandlers = new Map(); this.sourceMap = { version: 3, sources: [], names: [], mappings: '', sourcesContent: [], }; this.rpcGenerator = new RPCGenerator(options.rpcOptions); } /** * Generate client-side JavaScript code from AST */ generateClientCode(ast) { this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`; this.reactiveVars.clear(); this.eventHandlers.clear(); this.indentLevel = 0; // Extract reactive variables from client block if (ast.component.clientBlock) { for (const variable of ast.component.clientBlock.reactiveVariables) { this.reactiveVars.set(variable.name, variable); } } const lines = []; // Generate component wrapper function lines.push(`// OrdoJS component: ${ast.component.name}`); lines.push(`function ${ast.component.name}(props = {}) {`); this.indentLevel++; // Generate component initialization lines.push(this.indent(`const component = {};`)); lines.push(this.indent(`component.id = "${this.componentId}";`)); lines.push(this.indent(`component.props = props;`)); lines.push(this.indent(`component.state = {};`)); lines.push(this.indent(`component.refs = {};`)); lines.push(this.indent(`component.update = function() { updateDOM(component); };`)); lines.push(''); // Generate reactive state initialization if (ast.component.clientBlock) { lines.push(this.indent(`// Initialize reactive state`)); for (const variable of ast.component.clientBlock.reactiveVariables) { const initialValueCode = this.generateExpression(variable.initialValue); lines.push(this.indent(`let ${variable.name} = ${initialValueCode};`)); lines.push(this.indent(`Object.defineProperty(component.state, "${variable.name}", {`)); this.indentLevel++; lines.push(this.indent(`get: () => ${variable.name},`)); lines.push(this.indent(`set: (value) => {`)); this.indentLevel++; lines.push(this.indent(`${variable.name} = value;`)); lines.push(this.indent(`component.update();`)); this.indentLevel--; lines.push(this.indent(`},`)); lines.push(this.indent(`enumerable: true`)); this.indentLevel--; lines.push(this.indent(`});`)); } lines.push(''); } // Generate client-side functions if (ast.component.clientBlock && ast.component.clientBlock.functions.length > 0) { lines.push(this.indent(`// Client-side functions`)); for (const func of ast.component.clientBlock.functions) { lines.push(this.indent(`function ${func.name}(${this.generateFunctionParameters(func.parameters)}) {`)); this.indentLevel++; // Generate function body for (const statement of func.body) { lines.push(this.indent(this.generateStatement(statement))); } this.indentLevel--; lines.push(this.indent(`}`)); lines.push(''); } } // Generate server function stubs for RPC calls if (ast.component.serverBlock) { const rpcStubs = this.rpcGenerator.generateRPCStubs(ast); if (rpcStubs.length > 0) { lines.push(this.indent(`// Server function stubs for RPC calls`)); lines.push(this.indent(`component.server = component.server || {};`)); for (const stub of rpcStubs) { // Extract the function body from the stub code const stubCode = stub.clientCode; const functionBodyStart = stubCode.indexOf('{') + 1; const functionBodyEnd = stubCode.lastIndexOf('}'); const functionBody = stubCode.substring(functionBodyStart, functionBodyEnd).trim(); // Add the function to the component.server object lines.push(this.indent(`component.server.${stub.functionName} = async function(${this.generateFunctionParameters(stub.metadata.parameters)}) {`)); this.indentLevel++; // Split the function body into lines and add proper indentation const bodyLines = functionBody.split('\n'); for (const line of bodyLines) { if (line.trim()) { lines.push(this.indent(line.trim())); } } this.indentLevel--; lines.push(this.indent(`};`)); lines.push(''); } } } // Generate DOM creation code lines.push(this.indent(`// Create DOM elements`)); lines.push(this.indent(`function createDOM() {`)); this.indentLevel++; if (ast.component.markupBlock) { const domCreationCode = this.generateMarkupBlockCode(ast.component.markupBlock); lines.push(this.indent(`const rootElement = ${domCreationCode};`)); lines.push(this.indent(`return rootElement;`)); } else { lines.push(this.indent(`return document.createElement("div");`)); } this.indentLevel--; lines.push(this.indent(`}`)); lines.push(''); // Generate DOM update function lines.push(this.indent(`// Update DOM elements`)); lines.push(this.indent(`function updateDOM(component) {`)); this.indentLevel++; lines.push(this.indent(`// Update text nodes and attributes based on state`)); // Generate update code for each reactive variable for (const [name, variable] of this.reactiveVars.entries()) { lines.push(this.indent(`// Update elements that depend on ${name}`)); lines.push(this.indent(`const ${name}Value = component.state.${name};`)); lines.push(this.indent(`const ${name}Elements = document.querySelectorAll('[data-bind-${name}]');`)); lines.push(this.indent(`${name}Elements.forEach(el => {`)); this.indentLevel++; lines.push(this.indent(`const bindType = el.getAttribute('data-bind-type-${name}');`)); lines.push(this.indent(`if (bindType === 'text') {`)); this.indentLevel++; lines.push(this.indent(`el.textContent = ${name}Value;`)); this.indentLevel--; lines.push(this.indent(`} else if (bindType === 'attr') {`)); this.indentLevel++; lines.push(this.indent(`const attrName = el.getAttribute('data-bind-attr-${name}');`)); lines.push(this.indent(`if (attrName) el.setAttribute(attrName, ${name}Value);`)); this.indentLevel--; lines.push(this.indent(`}`)); this.indentLevel--; lines.push(this.indent(`});`)); } this.indentLevel--; lines.push(this.indent(`}`)); lines.push(''); // Generate event handler setup if (this.eventHandlers.size > 0) { lines.push(this.indent(`// Set up event handlers`)); lines.push(this.indent(`function setupEventHandlers(rootElement) {`)); this.indentLevel++; for (const [selector, handlerCode] of this.eventHandlers.entries()) { const [elementId, eventName] = selector.split(':'); lines.push(this.indent(`const ${elementId}El = rootElement.querySelector('#${elementId}');`)); lines.push(this.indent(`if (${elementId}El) {`)); this.indentLevel++; lines.push(this.indent(`${elementId}El.addEventListener('${eventName}', (event) => {`)); this.indentLevel++; lines.push(this.indent(`${handlerCode}`)); lines.push(this.indent(`component.update();`)); this.indentLevel--; lines.push(this.indent(`});`)); this.indentLevel--; lines.push(this.indent(`}`)); } this.indentLevel--; lines.push(this.indent(`}`)); lines.push(''); } // Generate mount and unmount methods lines.push(this.indent(`// Component lifecycle methods`)); lines.push(this.indent(`component.mount = function(container) {`)); this.indentLevel++; lines.push(this.indent(`const rootElement = createDOM();`)); if (this.eventHandlers.size > 0) { lines.push(this.indent(`setupEventHandlers(rootElement);`)); } lines.push(this.indent(`container.appendChild(rootElement);`)); lines.push(this.indent(`component.rootElement = rootElement;`)); lines.push(this.indent(`return component;`)); this.indentLevel--; lines.push(this.indent(`};`)); lines.push(''); lines.push(this.indent(`component.unmount = function() {`)); this.indentLevel++; lines.push(this.indent(`if (component.rootElement && component.rootElement.parentNode) {`)); this.indentLevel++; lines.push(this.indent(`component.rootElement.parentNode.removeChild(component.rootElement);`)); this.indentLevel--; lines.push(this.indent(`}`)); this.indentLevel--; lines.push(this.indent(`};`)); lines.push(''); // Return the component object lines.push(this.indent(`return component;`)); this.indentLevel--; lines.push(`}`); return lines.join('\n'); } /** * Generate server-side JavaScript code from AST */ generateServerCode(ast) { if (!ast.component.serverBlock) { return '// No server block defined for this component'; } this.indentLevel = 0; const lines = []; // Generate server module wrapper lines.push(`// OrdoJS server component: ${ast.component.name}`); lines.push(`const ${ast.component.name}Server = (function() {`); this.indentLevel++; // Generate server module object lines.push(this.indent(`const serverModule = {};`)); lines.push(''); // Generate server functions if (ast.component.serverBlock.functions.length > 0) { lines.push(this.indent(`// Server functions`)); for (const func of ast.component.serverBlock.functions) { const isPublic = func.isPublic ? 'public ' : ''; lines.push(this.indent(`${isPublic}async function ${func.name}(${this.generateFunctionParameters(func.parameters)}) {`)); this.indentLevel++; // Generate function body for (const statement of func.body) { lines.push(this.indent(this.generateStatement(statement))); } this.indentLevel--; lines.push(this.indent(`}`)); lines.push(''); // Export public functions if (func.isPublic) { lines.push(this.indent(`serverModule.${func.name} = ${func.name};`)); } } } // Return the server module lines.push(this.indent(`return serverModule;`)); this.indentLevel--; lines.push(`})();`); lines.push(''); lines.push(`module.exports = ${ast.component.name}Server;`); return lines.join('\n'); } /** * Generate HTML template for server-side rendering */ generateHTML(ast, initialProps = {}) { this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`; let html = ''; if (ast.component.markupBlock) { html = this.generateMarkupBlockHTML(ast.component.markupBlock); } // Add component props as data attributes for hydration let propsAttributes = ''; if (ast.component.props.length > 0 || Object.keys(initialProps).length > 0) { // Create a props object with default values from prop definitions const defaultProps = {}; // Add default values from prop definitions for (const prop of ast.component.props) { if (prop.defaultValue && prop.defaultValue.expressionType === 'LITERAL') { defaultProps[prop.name] = prop.defaultValue.value; } } // Merge with initial props const mergedProps = { ...defaultProps, ...initialProps }; // Add as data attribute for hydration propsAttributes = ` data-props="${this.escapeString(JSON.stringify(mergedProps))}"`; } // Add hydration markers and component metadata html = `<div data-ordojs-component="${ast.component.name}" data-component-id="${this.componentId}"${propsAttributes} data-ordojs-version="1.0" >${html}</div>`; return html; } /** * Generate complete code bundle */ generate(ast) { const clientCode = this.generateClientCode(ast); const serverCode = this.generateServerCode(ast); const html = this.generateHTML(ast); return { client: clientCode, server: serverCode, html, sourceMap: this.sourceMap }; } /** * Generate code for markup block */ generateMarkupBlockCode(markupBlock) { // For simplicity, we'll assume there's a single root element if (markupBlock.elements.length === 0) { return 'document.createElement("div")'; } // Generate code for the first element return this.generateElementCode(markupBlock.elements[0]); } /** * Generate code for HTML element */ generateElementCode(element) { const elementId = `el_${this.generateUniqueId()}`; const lines = []; // Create element lines.push(`(() => {`); lines.push(` const ${elementId} = document.createElement("${element.tagName}");`); // Add attributes for (const attr of element.attributes) { if (attr.isDirective) { if (attr.directiveType === 'BIND') { // Handle bind directive const bindName = attr.name.substring(5); // Remove 'bind:' prefix const varName = typeof attr.value === 'string' ? attr.value : this.generateExpression(attr.value); lines.push(` ${elementId}.setAttribute("data-bind-${bindName}", "");`); lines.push(` ${elementId}.setAttribute("data-bind-type-${bindName}", "attr");`); lines.push(` ${elementId}.setAttribute("data-bind-attr-${bindName}", "${attr.name}");`); lines.push(` ${elementId}.setAttribute("${attr.name}", ${varName});`); } else if (attr.directiveType === 'ON') { // Handle event directive const eventName = attr.name.substring(3); // Remove 'on:' prefix const handlerCode = typeof attr.value === 'string' ? attr.value : this.generateExpression(attr.value); // Store event handler for later binding const handlerId = `handler_${this.generateUniqueId()}`; this.eventHandlers.set(`${elementId}:${eventName}`, handlerCode); // Add ID to element for event binding lines.push(` ${elementId}.id = "${elementId}";`); } } else { // Regular attribute const attrValue = typeof attr.value === 'string' ? `"${attr.value}"` : this.generateExpression(attr.value); lines.push(` ${elementId}.setAttribute("${attr.name}", ${attrValue});`); } } // Add children for (const child of element.children) { if (child.type === 'HTMLElement') { const childCode = this.generateElementCode(child); lines.push(` ${elementId}.appendChild(${childCode});`); } else if (child.type === 'Text') { const textNode = child; lines.push(` ${elementId}.appendChild(document.createTextNode("${this.escapeString(textNode.content)}"));`); } else if (child.type === 'Interpolation') { const interpolation = child; const varName = this.generateExpression(interpolation.expression); const textNodeId = `text_${this.generateUniqueId()}`; // Create text node for interpolation lines.push(` const ${textNodeId} = document.createTextNode(${varName});`); lines.push(` ${textNodeId}.setAttribute("data-bind-${varName}", "");`); lines.push(` ${textNodeId}.setAttribute("data-bind-type-${varName}", "text");`); lines.push(` ${elementId}.appendChild(${textNodeId});`); } } lines.push(` return ${elementId};`); lines.push(`})()`); return lines.join('\n'); } /** * Generate HTML for markup block */ generateMarkupBlockHTML(markupBlock) { // For simplicity, we'll assume there's a single root element if (markupBlock.elements.length === 0) { return '<div></div>'; } // Generate HTML for the first element return this.generateElementHTML(markupBlock.elements[0]); } /** * Generate HTML for element */ generateElementHTML(element) { let html = `<${element.tagName}`; // Add attributes for (const attr of element.attributes) { if (!attr.isDirective) { const attrValue = typeof attr.value === 'string' ? attr.value : ''; // For server rendering, we'll use empty string for expressions html += ` ${attr.name}="${this.escapeString(attrValue)}"`; } else { // Add data attributes for directives to help with hydration if (attr.directiveType === 'BIND') { const bindName = attr.name.substring(5); // Remove 'bind:' prefix html += ` data-bind="${bindName}"`; } else if (attr.directiveType === 'ON') { const eventName = attr.name.substring(3); // Remove 'on:' prefix html += ` data-event="${eventName}"`; } } } // Add hydration markers html += ` data-ordojs-hydrate="true"`; if (element.isSelfClosing || element.isVoidElement) { html += ' />'; return html; } html += '>'; // Add children for (const child of element.children) { if (child.type === 'HTMLElement') { html += this.generateElementHTML(child); } else if (child.type === 'Text') { const textNode = child; html += this.escapeString(textNode.content); } else if (child.type === 'Interpolation') { // For server rendering, we'll use a placeholder with data attributes for hydration const interpolation = child; const varName = interpolation.expression.expressionType === 'IDENTIFIER' ? interpolation.expression.identifier : ''; if (varName) { html += `<span data-interpolation="${varName}"><!-- ${varName} --></span>`; } else { html += `<span data-interpolation="expression"><!-- expression --></span>`; } } } html += `</${element.tagName}>`; return html; } /** * Generate code for expression */ generateExpression(expr) { switch (expr.expressionType) { case 'LITERAL': if (typeof expr.value === 'string') { return `"${this.escapeString(expr.value)}"`; } return String(expr.value); case 'IDENTIFIER': return expr.identifier || ''; case 'BINARY': if (expr.left && expr.right && expr.operator) { const left = this.generateExpression(expr.left); const right = this.generateExpression(expr.right); return `(${left} ${expr.operator} ${right})`; } return ''; case 'UNARY': if (expr.right && expr.operator) { const right = this.generateExpression(expr.right); return `(${expr.operator}${right})`; } return ''; case 'CALL': if (expr.callee && expr.arguments) { const callee = this.generateExpression(expr.callee); const args = expr.arguments.map(arg => this.generateExpression(arg)).join(', '); return `${callee}(${args})`; } return ''; case 'MEMBER': if (expr.object && expr.property) { const object = this.generateExpression(expr.object); const property = this.generateExpression(expr.property); return `${object}.${property}`; } return ''; case 'ASSIGNMENT': if (expr.left && expr.right && expr.operator) { const left = this.generateExpression(expr.left); const right = this.generateExpression(expr.right); return `${left} ${expr.operator} ${right}`; } return ''; default: return ''; } } /** * Generate code for a statement */ generateStatement(statement) { if (statement.statementType === 'EXPRESSION' && statement.expression) { return `${this.generateExpression(statement.expression)};`; } else if (statement.statementType === 'RETURN' && statement.expression) { return `return ${this.generateExpression(statement.expression)};`; } else { // For other statement types, we'd need more complex handling return '/* Statement not implemented */'; } } /** * Generate function parameters string */ generateFunctionParameters(parameters) { return parameters.map(param => { let result = param.name; if (param.defaultValue) { result += ` = ${this.generateExpression(param.defaultValue)}`; } return result; }).join(', '); } /** * Helper to generate indentation */ indent(text) { return ' '.repeat(this.indentLevel) + text; } /** * Helper to escape string literals */ escapeString(str) { return str .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); } /** * Generate a unique ID for elements */ generateUniqueId() { return Math.random().toString(36).substring(2, 10); } } //# sourceMappingURL=code-generator.js.map