@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
593 lines (592 loc) • 22.1 kB
JavaScript
/**
* @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