@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
566 lines • 24.7 kB
JavaScript
/**
* @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