UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

740 lines (657 loc) 21.5 kB
/** * @fileoverview OrdoJS Dependency Analyzer - Tracks reactive variable usage and builds dependency graphs */ import { DirectiveType, ExpressionType, OptimizationError, OptimizationType, type AttributeNode, type ClientBlockNode, type ComponentAST, type ComponentNode, type ExpressionNode, type HTMLElementNode, type InterpolationNode, type MarkupBlockNode, type ReactiveVariableNode, type SourceRange } from '../types/index.js'; /** * Dependency relationship between variables */ export interface Dependency { from: string; to: string; type: DependencyType; location: SourceRange; } /** * Types of dependencies */ export enum DependencyType { READ = 'READ', // Variable is read WRITE = 'WRITE', // Variable is written to COMPUTED = 'COMPUTED', // Variable is used in computed expression EVENT = 'EVENT', // Variable is used in event handler INTERPOLATION = 'INTERPOLATION' // Variable is used in template interpolation } /** * Dependency graph node */ export interface DependencyNode { name: string; variable: ReactiveVariableNode; dependencies: Set<string>; dependents: Set<string>; updateOrder: number; isCircular: boolean; } /** * Dependency graph structure */ export interface DependencyGraph { nodes: Map<string, DependencyNode>; edges: Dependency[]; updateOrder: string[]; circularDependencies: string[][]; } /** * Update function metadata */ export interface UpdateFunction { variableName: string; targetElements: string[]; updateType: UpdateType; code: string; dependencies: string[]; } /** * Types of DOM updates */ export enum UpdateType { TEXT_CONTENT = 'TEXT_CONTENT', ATTRIBUTE = 'ATTRIBUTE', PROPERTY = 'PROPERTY', CLASS = 'CLASS', STYLE = 'STYLE', CONDITIONAL = 'CONDITIONAL', LIST = 'LIST' } /** * Dependency analyzer for reactive variables */ export class DependencyAnalyzer { private graph: DependencyGraph; private currentComponent: ComponentNode | null = null; private errors: OptimizationError[] = []; constructor() { this.graph = { nodes: new Map(), edges: [], updateOrder: [], circularDependencies: [] }; } /** * Analyze dependencies in a component AST */ analyze(ast: ComponentAST): DependencyGraph { 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: ComponentAST): UpdateFunction[] { const graph = this.analyze(ast); const updateFunctions: UpdateFunction[] = []; // 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(): OptimizationError[] { return this.errors; } /** * Reset analyzer state */ private reset(): void { this.graph = { nodes: new Map(), edges: [], updateOrder: [], circularDependencies: [] }; this.currentComponent = null; this.errors = []; } /** * Build initial dependency nodes from reactive variables */ private buildDependencyNodes(component: ComponentNode): void { if (!component.clientBlock) { return; } // First pass: Create all nodes without analyzing dependencies for (const variable of component.clientBlock.reactiveVariables) { const node: DependencyNode = { 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: DependencyNode = { 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 */ private analyzeVariableUsage(component: ComponentNode): void { // 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 */ private analyzeMarkupBlock(markupBlock: MarkupBlockNode): void { // 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 */ private analyzeHTMLElement(element: HTMLElementNode): void { // 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 as HTMLElementNode); } else if (child.type === 'Interpolation') { this.analyzeInterpolation(child as InterpolationNode); } } } /** * Analyze attribute for variable dependencies */ private analyzeAttribute(attr: AttributeNode): void { if (attr.isDirective && typeof attr.value !== 'string') { const expression = attr.value as ExpressionNode; 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 as ExpressionNode, '', DependencyType.READ); } } /** * Analyze interpolation for variable dependencies */ private analyzeInterpolation(interpolation: InterpolationNode): void { this.analyzeExpressionDependencies(interpolation.expression, '', DependencyType.INTERPOLATION); } /** * Analyze client block for variable dependencies */ private analyzeClientBlock(clientBlock: ClientBlockNode): void { // 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 as ExpressionNode, '', 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 */ private analyzeExpressionDependencies( expr: ExpressionNode, targetVariable: string, dependencyType: DependencyType ): void { 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 */ private addDependency( from: string, to: string, type: DependencyType, location: SourceRange ): void { const dependency: 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 */ private detectCircularDependencies(): void { const visited = new Set<string>(); const recursionStack = new Set<string>(); const currentPath: string[] = []; for (const [nodeName] of this.graph.nodes) { if (!visited.has(nodeName)) { this.dfsCircularDetection(nodeName, visited, recursionStack, currentPath); } } } /** * Depth-first search for circular dependency detection */ private dfsCircularDetection( nodeName: string, visited: Set<string>, recursionStack: Set<string>, currentPath: string[] ): void { 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 */ private calculateUpdateOrder(): void { const inDegree = new Map<string, number>(); const queue: string[] = []; // 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: string[] = []; 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 */ private generateUpdateFunction( varName: string, node: DependencyNode, component: ComponentNode ): UpdateFunction | null { const targetElements: string[] = []; const dependencies: string[] = 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 */ private determineUpdateType(varName: string, component: ComponentNode): UpdateType { // 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 */ private generateTextContentUpdate(varName: string): string { 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 */ private generateAttributeUpdate(varName: string): string { 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 */ private generatePropertyUpdate(varName: string): string { 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 */ private generateClassUpdate(varName: string): string { 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 */ private generateStyleUpdate(varName: string): string { 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 */ private generateGenericUpdate(varName: string): string { 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(); } }