UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

526 lines 21.4 kB
/** * @fileoverview OrdoJS DOM Optimizer - Generates efficient DOM update code */ import { DirectiveType, ExpressionType } from '../types/index.js'; import {} from './dependency-analyzer.js'; /** * Types of DOM update operations */ export var DOMUpdateType; (function (DOMUpdateType) { DOMUpdateType["TEXT_CONTENT"] = "TEXT_CONTENT"; DOMUpdateType["ATTRIBUTE"] = "ATTRIBUTE"; DOMUpdateType["PROPERTY"] = "PROPERTY"; DOMUpdateType["CLASS_TOGGLE"] = "CLASS_TOGGLE"; DOMUpdateType["STYLE"] = "STYLE"; DOMUpdateType["VISIBILITY"] = "VISIBILITY"; DOMUpdateType["LIST_UPDATE"] = "LIST_UPDATE"; DOMUpdateType["CONDITIONAL_RENDER"] = "CONDITIONAL_RENDER"; })(DOMUpdateType || (DOMUpdateType = {})); /** * DOM optimizer for efficient DOM manipulation code generation */ export class DOMOptimizer { componentId = ''; updateOperations = new Map(); updateBatches = []; twoWayBindings = []; elementCounter = 0; /** * Optimize DOM updates for a component */ optimize(ast, dependencyGraph) { this.reset(); this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`; // Analyze the component for DOM update opportunities this.analyzeComponent(ast.component, dependencyGraph); // Generate batched update operations this.generateUpdateBatches(); // Generate the optimized update code const updateCode = this.generateUpdateCode(); const setupCode = this.generateSetupCode(); const cleanupCode = this.generateCleanupCode(); return { updateCode, setupCode, cleanupCode }; } /** * Generate selective DOM updates (only changed elements) */ generateSelectiveUpdates(ast, dependencyGraph, changedVariables) { this.reset(); this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`; // Only analyze operations that depend on changed variables this.analyzeComponent(ast.component, dependencyGraph, changedVariables); this.generateUpdateBatches(); return this.generateUpdateCode(); } /** * Reset optimizer state */ reset() { this.componentId = ''; this.updateOperations.clear(); this.updateBatches = []; this.twoWayBindings = []; this.elementCounter = 0; } /** * Analyze component for DOM update opportunities */ analyzeComponent(component, dependencyGraph, changedVariables) { if (!component.markupBlock) return; // Analyze markup block for update operations this.analyzeMarkupBlock(component.markupBlock, dependencyGraph, changedVariables); // Analyze reactive variables for two-way bindings if (component.clientBlock) { this.analyzeTwoWayBindings(component.markupBlock, component.clientBlock.reactiveVariables); } } /** * Analyze markup block for DOM updates */ analyzeMarkupBlock(markupBlock, dependencyGraph, changedVariables) { // Analyze interpolations for (const interpolation of markupBlock.interpolations) { this.analyzeInterpolation(interpolation, dependencyGraph, changedVariables); } // Analyze HTML elements for (const element of markupBlock.elements) { this.analyzeHTMLElement(element, dependencyGraph, changedVariables); } } /** * Analyze HTML element for DOM updates */ analyzeHTMLElement(element, dependencyGraph, changedVariables, elementPath = '') { const elementId = this.generateElementId(); const currentPath = elementPath ? `${elementPath} > ${element.tagName}` : element.tagName; // Analyze attributes for reactive updates for (const attr of element.attributes) { this.analyzeAttribute(attr, elementId, dependencyGraph, changedVariables); } // Recursively analyze children for (let i = 0; i < element.children.length; i++) { const child = element.children[i]; if (child.type === 'HTMLElement') { this.analyzeHTMLElement(child, dependencyGraph, changedVariables, `${currentPath}:nth-child(${i + 1})`); } else if (child.type === 'Interpolation') { this.analyzeInterpolation(child, dependencyGraph, changedVariables, `${currentPath}:nth-child(${i + 1})`); } } } /** * Analyze attribute for reactive updates */ analyzeAttribute(attr, elementId, dependencyGraph, changedVariables) { if (!attr.isDirective || typeof attr.value === 'string') { return; } const expression = attr.value; const dependencies = this.extractDependencies(expression, dependencyGraph); // Filter by changed variables if specified if (changedVariables && !dependencies.some(dep => changedVariables.includes(dep))) { return; } const selector = `[data-ordojs-id="${elementId}"]`; switch (attr.directiveType) { case DirectiveType.BIND: this.createTwoWayBinding(attr, elementId, dependencies); break; case DirectiveType.ON: // Event handlers don't need DOM updates, just setup break; case DirectiveType.CLASS: this.createClassUpdate(attr, selector, expression, dependencies); break; case DirectiveType.STYLE: this.createStyleUpdate(attr, selector, expression, dependencies); break; default: this.createAttributeUpdate(attr, selector, expression, dependencies); break; } } /** * Analyze interpolation for reactive updates */ analyzeInterpolation(interpolation, dependencyGraph, changedVariables, elementPath) { const dependencies = this.extractDependencies(interpolation.expression, dependencyGraph); // Filter by changed variables if specified if (changedVariables && !dependencies.some(dep => changedVariables.includes(dep))) { return; } const elementId = this.generateElementId(); const selector = `[data-ordojs-interpolation="${elementId}"]`; const operation = { id: this.generateOperationId(), type: DOMUpdateType.TEXT_CONTENT, selector, property: 'textContent', expression: this.generateExpressionCode(interpolation.expression), dependencies }; this.addUpdateOperation(dependencies[0] || 'default', operation); } /** * Create two-way binding */ createTwoWayBinding(attr, elementId, dependencies) { const bindProperty = attr.name.substring(5); // Remove 'bind:' prefix const selector = `[data-ordojs-id="${elementId}"]`; // Determine event type based on property let eventType = 'input'; let propertyName = 'value'; switch (bindProperty) { case 'checked': eventType = 'change'; propertyName = 'checked'; break; case 'value': eventType = 'input'; propertyName = 'value'; break; default: eventType = 'input'; propertyName = bindProperty; } const binding = { elementSelector: selector, variableName: dependencies[0] || '', eventType, propertyName }; this.twoWayBindings.push(binding); // Also create a property update operation const operation = { id: this.generateOperationId(), type: DOMUpdateType.PROPERTY, selector, property: propertyName, expression: `component.state.${binding.variableName}`, dependencies }; this.addUpdateOperation(binding.variableName, operation); } /** * Create class update operation */ createClassUpdate(attr, selector, expression, dependencies) { const className = attr.name.substring(6); // Remove 'class:' prefix const operation = { id: this.generateOperationId(), type: DOMUpdateType.CLASS_TOGGLE, selector, property: className, expression: this.generateExpressionCode(expression), dependencies }; this.addUpdateOperation(dependencies[0] || 'default', operation); } /** * Create style update operation */ createStyleUpdate(attr, selector, expression, dependencies) { const styleProperty = attr.name.substring(6); // Remove 'style:' prefix const operation = { id: this.generateOperationId(), type: DOMUpdateType.STYLE, selector, property: styleProperty, expression: this.generateExpressionCode(expression), dependencies }; this.addUpdateOperation(dependencies[0] || 'default', operation); } /** * Create attribute update operation */ createAttributeUpdate(attr, selector, expression, dependencies) { const operation = { id: this.generateOperationId(), type: DOMUpdateType.ATTRIBUTE, selector, property: attr.name, expression: this.generateExpressionCode(expression), dependencies }; this.addUpdateOperation(dependencies[0] || 'default', operation); } /** * Analyze two-way bindings */ analyzeTwoWayBindings(markupBlock, reactiveVariables) { // This is handled in analyzeAttribute method // Additional analysis could be added here if needed } /** * Extract dependencies from expression */ extractDependencies(expression, dependencyGraph) { const dependencies = []; const extractFromExpr = (expr) => { switch (expr.expressionType) { case ExpressionType.IDENTIFIER: if (expr.identifier && dependencyGraph.nodes.has(expr.identifier)) { dependencies.push(expr.identifier); } break; case ExpressionType.BINARY: case ExpressionType.ASSIGNMENT: if (expr.left) extractFromExpr(expr.left); if (expr.right) extractFromExpr(expr.right); break; case ExpressionType.UNARY: if (expr.right) extractFromExpr(expr.right); break; case ExpressionType.CALL: if (expr.callee) extractFromExpr(expr.callee); if (expr.arguments) { expr.arguments.forEach(arg => extractFromExpr(arg)); } break; case ExpressionType.MEMBER: if (expr.object) extractFromExpr(expr.object); // For member expressions, we don't extract dependencies from the property // since it's usually a literal property name, but we still need to check // if the property itself is an identifier that references a reactive variable if (expr.property && expr.property.expressionType === ExpressionType.IDENTIFIER) { if (expr.property.identifier && dependencyGraph.nodes.has(expr.property.identifier)) { dependencies.push(expr.property.identifier); } } break; } }; extractFromExpr(expression); return [...new Set(dependencies)]; // Remove duplicates } /** * Generate expression code */ generateExpressionCode(expression) { switch (expression.expressionType) { case ExpressionType.LITERAL: return typeof expression.value === 'string' ? `"${expression.value}"` : String(expression.value); case ExpressionType.IDENTIFIER: return `component.state.${expression.identifier}`; case ExpressionType.BINARY: if (expression.left && expression.right && expression.operator) { const left = this.generateExpressionCode(expression.left); const right = this.generateExpressionCode(expression.right); return `(${left} ${expression.operator} ${right})`; } return ''; case ExpressionType.UNARY: if (expression.right && expression.operator) { const right = this.generateExpressionCode(expression.right); return `(${expression.operator}${right})`; } return ''; case ExpressionType.CALL: if (expression.callee && expression.arguments) { const callee = this.generateExpressionCode(expression.callee); const args = expression.arguments .map(arg => this.generateExpressionCode(arg)) .join(', '); return `${callee}(${args})`; } return ''; case ExpressionType.MEMBER: if (expression.object && expression.property) { const object = this.generateExpressionCode(expression.object); // For member expressions, the property should be treated as a literal property name const property = expression.property.expressionType === ExpressionType.IDENTIFIER ? expression.property.identifier : this.generateExpressionCode(expression.property); return `${object}.${property}`; } return ''; default: return ''; } } /** * Add update operation to the appropriate group */ addUpdateOperation(variableName, operation) { // Use the first dependency as the key, or 'default' if no dependencies const key = operation.dependencies.length > 0 ? operation.dependencies[0] : 'default'; if (!this.updateOperations.has(key)) { this.updateOperations.set(key, []); } this.updateOperations.get(key).push(operation); } /** * Generate batched update operations */ generateUpdateBatches() { let batchId = 0; for (const [variableName, operations] of this.updateOperations) { // Create a single batch per variable to reduce the number of batches const batch = { id: `batch_${batchId++}`, operations, dependencies: [variableName], priority: Math.min(...operations.map(op => this.getUpdatePriority(op.type))) }; this.updateBatches.push(batch); } // Sort batches by priority this.updateBatches.sort((a, b) => a.priority - b.priority); } /** * Get update priority for operation type */ getUpdatePriority(type) { switch (type) { case DOMUpdateType.VISIBILITY: return 1; case DOMUpdateType.CLASS_TOGGLE: return 2; case DOMUpdateType.STYLE: return 3; case DOMUpdateType.ATTRIBUTE: return 4; case DOMUpdateType.PROPERTY: return 5; case DOMUpdateType.TEXT_CONTENT: return 6; case DOMUpdateType.LIST_UPDATE: return 7; case DOMUpdateType.CONDITIONAL_RENDER: return 8; default: return 9; } } /** * Generate optimized update code */ generateUpdateCode() { const lines = []; lines.push('// Optimized DOM update functions'); lines.push('const updateFunctions = {'); for (const batch of this.updateBatches) { lines.push(` ${batch.id}: function() {`); lines.push(' // Batch DOM updates for better performance'); // Group operations by selector for efficiency const operationsBySelector = new Map(); for (const operation of batch.operations) { if (!operationsBySelector.has(operation.selector)) { operationsBySelector.set(operation.selector, []); } operationsBySelector.get(operation.selector).push(operation); } // Generate update code for each selector for (const [selector, operations] of operationsBySelector) { lines.push(` const elements = document.querySelectorAll('${selector}');`); lines.push(' elements.forEach(el => {'); for (const operation of operations) { lines.push(` // ${operation.type}: ${operation.property}`); switch (operation.type) { case DOMUpdateType.TEXT_CONTENT: lines.push(` el.textContent = ${operation.expression};`); break; case DOMUpdateType.ATTRIBUTE: lines.push(` el.setAttribute('${operation.property}', ${operation.expression});`); break; case DOMUpdateType.PROPERTY: lines.push(` el.${operation.property} = ${operation.expression};`); break; case DOMUpdateType.CLASS_TOGGLE: lines.push(` el.classList.toggle('${operation.property}', !!(${operation.expression}));`); break; case DOMUpdateType.STYLE: lines.push(` el.style.${operation.property} = ${operation.expression};`); break; } } lines.push(' });'); } lines.push(' },'); } lines.push('};'); lines.push(''); // Generate main update function lines.push('function updateDOM(component, changedVariables = []) {'); lines.push(' // Execute update batches based on changed variables'); for (const batch of this.updateBatches) { const condition = batch.dependencies .map(dep => `changedVariables.includes('${dep}')`) .join(' || '); lines.push(` if (changedVariables.length === 0 || ${condition}) {`); lines.push(` updateFunctions.${batch.id}();`); lines.push(' }'); } lines.push('}'); return lines.join('\n'); } /** * Generate setup code for two-way bindings */ generateSetupCode() { if (this.twoWayBindings.length === 0) { return ''; } const lines = []; lines.push('// Setup two-way data bindings'); lines.push('function setupTwoWayBindings(component) {'); for (const binding of this.twoWayBindings) { lines.push(` const ${binding.variableName}Elements = document.querySelectorAll('${binding.elementSelector}');`); lines.push(` ${binding.variableName}Elements.forEach(el => {`); lines.push(` el.addEventListener('${binding.eventType}', (event) => {`); if (binding.transformFunction) { lines.push(` const value = ${binding.transformFunction}(event.target.${binding.propertyName});`); } else { lines.push(` const value = event.target.${binding.propertyName};`); } lines.push(` component.state.${binding.variableName} = value;`); lines.push(' });'); lines.push(' });'); } lines.push('}'); return lines.join('\n'); } /** * Generate cleanup code */ generateCleanupCode() { if (this.twoWayBindings.length === 0) { return ''; } const lines = []; lines.push('// Cleanup event listeners'); lines.push('function cleanupBindings() {'); lines.push(' // Event listeners will be automatically cleaned up when elements are removed'); lines.push(' // Additional cleanup logic can be added here if needed'); lines.push('}'); return lines.join('\n'); } /** * Generate unique element ID */ generateElementId() { return `${this.componentId}_el_${this.elementCounter++}`; } /** * Generate unique operation ID */ generateOperationId() { return `op_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`; } } //# sourceMappingURL=dom-optimizer.js.map