UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

593 lines (592 loc) 22.1 kB
/** * @fileoverview OrdoJS Dependency Analyzer - Tracks reactive variable usage and builds dependency graphs */ import { DirectiveType, ExpressionType, OptimizationError, OptimizationType } from '../types/index.js'; /** * Types of dependencies */ export var DependencyType; (function (DependencyType) { DependencyType["READ"] = "READ"; DependencyType["WRITE"] = "WRITE"; DependencyType["COMPUTED"] = "COMPUTED"; DependencyType["EVENT"] = "EVENT"; DependencyType["INTERPOLATION"] = "INTERPOLATION"; // Variable is used in template interpolation })(DependencyType || (DependencyType = {})); /** * Types of DOM updates */ export var UpdateType; (function (UpdateType) { UpdateType["TEXT_CONTENT"] = "TEXT_CONTENT"; UpdateType["ATTRIBUTE"] = "ATTRIBUTE"; UpdateType["PROPERTY"] = "PROPERTY"; UpdateType["CLASS"] = "CLASS"; UpdateType["STYLE"] = "STYLE"; UpdateType["CONDITIONAL"] = "CONDITIONAL"; UpdateType["LIST"] = "LIST"; })(UpdateType || (UpdateType = {})); /** * Dependency analyzer for reactive variables */ export class DependencyAnalyzer { graph; currentComponent = null; errors = []; constructor() { this.graph = { nodes: new Map(), edges: [], updateOrder: [], circularDependencies: [] }; } /** * Analyze dependencies in a component AST */ analyze(ast) { this.reset(); this.currentComponent = ast.component; try { // Step 1: Build initial dependency nodes from reactive variables this.buildDependencyNodes(ast.component); // Step 2: Analyze expressions and markup for variable usage this.analyzeVariableUsage(ast.component); // Step 3: Detect circular dependencies this.detectCircularDependencies(); // Step 4: Calculate update order this.calculateUpdateOrder(); return this.graph; } catch (error) { if (error instanceof OptimizationError) { this.errors.push(error); } throw error; } } /** * Generate efficient update functions for reactive changes */ generateUpdateFunctions(ast) { const graph = this.analyze(ast); const updateFunctions = []; // Generate update functions for each reactive variable for (const [varName, node] of graph.nodes) { const updateFunction = this.generateUpdateFunction(varName, node, ast.component); if (updateFunction) { updateFunctions.push(updateFunction); } } return updateFunctions; } /** * Get analysis errors */ getErrors() { return this.errors; } /** * Reset analyzer state */ reset() { this.graph = { nodes: new Map(), edges: [], updateOrder: [], circularDependencies: [] }; this.currentComponent = null; this.errors = []; } /** * Build initial dependency nodes from reactive variables */ buildDependencyNodes(component) { if (!component.clientBlock) { return; } // First pass: Create all nodes without analyzing dependencies for (const variable of component.clientBlock.reactiveVariables) { const node = { name: variable.name, variable, dependencies: new Set(), dependents: new Set(), updateOrder: 0, isCircular: false }; this.graph.nodes.set(variable.name, node); } // Create nodes for computed values for (const computed of component.clientBlock.computedValues) { const node = { name: computed.name, variable: { type: 'ReactiveVariable', name: computed.name, initialValue: computed.expression, dataType: computed.dataType, isConst: true, range: computed.range }, dependencies: new Set(), dependents: new Set(), updateOrder: 0, isCircular: false }; this.graph.nodes.set(computed.name, node); } // Second pass: Analyze dependencies now that all nodes exist for (const variable of component.clientBlock.reactiveVariables) { this.analyzeExpressionDependencies(variable.initialValue, variable.name, DependencyType.COMPUTED); } for (const computed of component.clientBlock.computedValues) { this.analyzeExpressionDependencies(computed.expression, computed.name, DependencyType.COMPUTED); } } /** * Analyze variable usage throughout the component */ analyzeVariableUsage(component) { // Analyze markup block for interpolations and directives if (component.markupBlock) { this.analyzeMarkupBlock(component.markupBlock); } // Analyze client block for event handlers and functions if (component.clientBlock) { this.analyzeClientBlock(component.clientBlock); } } /** * Analyze markup block for variable dependencies */ analyzeMarkupBlock(markupBlock) { // Analyze interpolations for (const interpolation of markupBlock.interpolations) { this.analyzeInterpolation(interpolation); } // Analyze HTML elements for (const element of markupBlock.elements) { this.analyzeHTMLElement(element); } } /** * Analyze HTML element for variable dependencies */ analyzeHTMLElement(element) { // Analyze attributes for directives and expressions for (const attr of element.attributes) { this.analyzeAttribute(attr); } // Recursively analyze children for (const child of element.children) { if (child.type === 'HTMLElement') { this.analyzeHTMLElement(child); } else if (child.type === 'Interpolation') { this.analyzeInterpolation(child); } } } /** * Analyze attribute for variable dependencies */ analyzeAttribute(attr) { if (attr.isDirective && typeof attr.value !== 'string') { const expression = attr.value; if (attr.directiveType === DirectiveType.BIND) { // Two-way binding creates both read and write dependencies this.analyzeExpressionDependencies(expression, '', DependencyType.READ); this.analyzeExpressionDependencies(expression, '', DependencyType.WRITE); } else if (attr.directiveType === DirectiveType.ON) { // Event handlers create write dependencies this.analyzeExpressionDependencies(expression, '', DependencyType.EVENT); } else { // Other directives create read dependencies this.analyzeExpressionDependencies(expression, '', DependencyType.READ); } } else if (typeof attr.value !== 'string') { // Regular attribute with expression this.analyzeExpressionDependencies(attr.value, '', DependencyType.READ); } } /** * Analyze interpolation for variable dependencies */ analyzeInterpolation(interpolation) { this.analyzeExpressionDependencies(interpolation.expression, '', DependencyType.INTERPOLATION); } /** * Analyze client block for variable dependencies */ analyzeClientBlock(clientBlock) { // Analyze event handlers for (const handler of clientBlock.eventHandlers) { if (handler.handler && typeof handler.handler !== 'string') { if (Array.isArray(handler.handler)) { // Statement array - would need statement analysis // For now, skip complex statement analysis } else { this.analyzeExpressionDependencies(handler.handler, '', DependencyType.EVENT); } } } // Analyze functions for (const func of clientBlock.functions) { // Analyze function body statements for (const statement of func.body) { if (statement.expression) { this.analyzeExpressionDependencies(statement.expression, '', DependencyType.READ); } } } } /** * Analyze expression for variable dependencies */ analyzeExpressionDependencies(expr, targetVariable, dependencyType) { switch (expr.expressionType) { case ExpressionType.IDENTIFIER: if (expr.identifier && this.graph.nodes.has(expr.identifier)) { // For computed dependencies, the target depends on the identifier // So we add dependency from identifier to target this.addDependency(expr.identifier, targetVariable, dependencyType, expr.range); } break; case ExpressionType.BINARY: if (expr.left) { this.analyzeExpressionDependencies(expr.left, targetVariable, dependencyType); } if (expr.right) { this.analyzeExpressionDependencies(expr.right, targetVariable, dependencyType); } break; case ExpressionType.UNARY: if (expr.right) { this.analyzeExpressionDependencies(expr.right, targetVariable, dependencyType); } break; case ExpressionType.CALL: if (expr.callee) { this.analyzeExpressionDependencies(expr.callee, targetVariable, dependencyType); } if (expr.arguments) { for (const arg of expr.arguments) { this.analyzeExpressionDependencies(arg, targetVariable, dependencyType); } } break; case ExpressionType.MEMBER: if (expr.object) { this.analyzeExpressionDependencies(expr.object, targetVariable, dependencyType); } if (expr.property) { this.analyzeExpressionDependencies(expr.property, targetVariable, dependencyType); } break; case ExpressionType.ASSIGNMENT: if (expr.left) { // Left side is being written to this.analyzeExpressionDependencies(expr.left, targetVariable, DependencyType.WRITE); } if (expr.right) { // Right side is being read from this.analyzeExpressionDependencies(expr.right, targetVariable, DependencyType.READ); } break; case ExpressionType.CONDITIONAL: // Analyze all parts of conditional expression if (expr.left) { // condition this.analyzeExpressionDependencies(expr.left, targetVariable, dependencyType); } if (expr.right) { // true/false branches would be in a different structure this.analyzeExpressionDependencies(expr.right, targetVariable, dependencyType); } break; case ExpressionType.ARRAY: if (expr.arguments) { for (const element of expr.arguments) { this.analyzeExpressionDependencies(element, targetVariable, dependencyType); } } break; case ExpressionType.OBJECT: // Object expressions would need property analysis // For now, skip complex object analysis break; case ExpressionType.LITERAL: // Literals don't create dependencies break; } } /** * Add a dependency relationship */ addDependency(from, to, type, location) { const dependency = { from, to, type, location }; this.graph.edges.push(dependency); // Update dependency graph nodes if (to && this.graph.nodes.has(to)) { const toNode = this.graph.nodes.get(to); toNode.dependencies.add(from); } if (this.graph.nodes.has(from)) { const fromNode = this.graph.nodes.get(from); if (to) { fromNode.dependents.add(to); } } } /** * Detect circular dependencies using depth-first search */ detectCircularDependencies() { const visited = new Set(); const recursionStack = new Set(); const currentPath = []; for (const [nodeName] of this.graph.nodes) { if (!visited.has(nodeName)) { this.dfsCircularDetection(nodeName, visited, recursionStack, currentPath); } } } /** * Depth-first search for circular dependency detection */ dfsCircularDetection(nodeName, visited, recursionStack, currentPath) { visited.add(nodeName); recursionStack.add(nodeName); currentPath.push(nodeName); const node = this.graph.nodes.get(nodeName); if (!node) return; // Follow the dependency chain - if A depends on B, we go from A to B for (const dependency of node.dependencies) { if (!visited.has(dependency)) { this.dfsCircularDetection(dependency, visited, recursionStack, currentPath); } else if (recursionStack.has(dependency)) { // Found circular dependency const cycleStart = currentPath.indexOf(dependency); const cycle = currentPath.slice(cycleStart).concat([dependency]); this.graph.circularDependencies.push(cycle); // Mark all nodes in cycle as circular for (const cycleName of cycle) { const cycleNode = this.graph.nodes.get(cycleName); if (cycleNode) { cycleNode.isCircular = true; } } // Create warning for circular dependency const currentNode = this.graph.nodes.get(nodeName); if (currentNode) { this.errors.push(new OptimizationError(`Circular dependency detected: ${cycle.join(' -> ')}`, currentNode.variable.range, OptimizationType.DEPENDENCY_OPTIMIZATION)); } } } recursionStack.delete(nodeName); currentPath.pop(); } /** * Calculate optimal update order using topological sort */ calculateUpdateOrder() { const inDegree = new Map(); const queue = []; // Initialize in-degree count for (const [nodeName] of this.graph.nodes) { inDegree.set(nodeName, 0); } // Calculate in-degrees - count how many dependencies each node has for (const [nodeName, node] of this.graph.nodes) { inDegree.set(nodeName, node.dependencies.size); } // Find nodes with no dependencies (in-degree = 0) for (const [nodeName, degree] of inDegree) { if (degree === 0) { queue.push(nodeName); } } const updateOrder = []; let orderIndex = 0; // Process nodes in topological order while (queue.length > 0) { const nodeName = queue.shift(); updateOrder.push(nodeName); const node = this.graph.nodes.get(nodeName); if (node) { node.updateOrder = orderIndex++; // Reduce in-degree of dependent nodes for (const dependent of node.dependents) { const currentInDegree = inDegree.get(dependent) || 0; const newInDegree = currentInDegree - 1; inDegree.set(dependent, newInDegree); if (newInDegree === 0) { queue.push(dependent); } } } } // Handle circular dependencies by adding them at the end for (const [nodeName, node] of this.graph.nodes) { if (!updateOrder.includes(nodeName)) { updateOrder.push(nodeName); node.updateOrder = orderIndex++; } } this.graph.updateOrder = updateOrder; } /** * Generate update function for a specific variable */ generateUpdateFunction(varName, node, component) { const targetElements = []; const dependencies = Array.from(node.dependencies); // Find all elements that depend on this variable const updateType = this.determineUpdateType(varName, component); // Generate update code based on update type let code = ''; switch (updateType) { case UpdateType.TEXT_CONTENT: code = this.generateTextContentUpdate(varName); break; case UpdateType.ATTRIBUTE: code = this.generateAttributeUpdate(varName); break; case UpdateType.PROPERTY: code = this.generatePropertyUpdate(varName); break; case UpdateType.CLASS: code = this.generateClassUpdate(varName); break; case UpdateType.STYLE: code = this.generateStyleUpdate(varName); break; default: code = this.generateGenericUpdate(varName); } return { variableName: varName, targetElements, updateType, code, dependencies }; } /** * Determine the type of update needed for a variable */ determineUpdateType(varName, component) { // Analyze how the variable is used in the markup // For now, default to text content updates return UpdateType.TEXT_CONTENT; } /** * Generate text content update code */ generateTextContentUpdate(varName) { return ` const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]'); ${varName}Elements.forEach(el => { if (el.getAttribute('data-bind-type-${varName}') === 'text') { el.textContent = component.state.${varName}; } }); `.trim(); } /** * Generate attribute update code */ generateAttributeUpdate(varName) { return ` const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]'); ${varName}Elements.forEach(el => { const attrName = el.getAttribute('data-bind-attr-${varName}'); if (attrName) { el.setAttribute(attrName, component.state.${varName}); } }); `.trim(); } /** * Generate property update code */ generatePropertyUpdate(varName) { return ` const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]'); ${varName}Elements.forEach(el => { const propName = el.getAttribute('data-bind-prop-${varName}'); if (propName && propName in el) { el[propName] = component.state.${varName}; } }); `.trim(); } /** * Generate class update code */ generateClassUpdate(varName) { return ` const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]'); ${varName}Elements.forEach(el => { const className = el.getAttribute('data-bind-class-${varName}'); if (className) { el.classList.toggle(className, !!component.state.${varName}); } }); `.trim(); } /** * Generate style update code */ generateStyleUpdate(varName) { return ` const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]'); ${varName}Elements.forEach(el => { const styleProp = el.getAttribute('data-bind-style-${varName}'); if (styleProp) { el.style[styleProp] = component.state.${varName}; } }); `.trim(); } /** * Generate generic update code */ generateGenericUpdate(varName) { return ` const ${varName}Elements = document.querySelectorAll('[data-bind-${varName}]'); ${varName}Elements.forEach(el => { const bindType = el.getAttribute('data-bind-type-${varName}'); const value = component.state.${varName}; switch (bindType) { case 'text': el.textContent = value; break; case 'attr': const attrName = el.getAttribute('data-bind-attr-${varName}'); if (attrName) el.setAttribute(attrName, value); break; case 'prop': const propName = el.getAttribute('data-bind-prop-${varName}'); if (propName && propName in el) el[propName] = value; break; case 'class': const className = el.getAttribute('data-bind-class-${varName}'); if (className) el.classList.toggle(className, !!value); break; case 'style': const styleProp = el.getAttribute('data-bind-style-${varName}'); if (styleProp) el.style[styleProp] = value; break; } }); `.trim(); } } //# sourceMappingURL=dependency-analyzer.js.map