@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
740 lines (657 loc) • 21.5 kB
text/typescript
/**
* @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();
}
}