sicua
Version:
A tool for analyzing project structure and dependencies
473 lines (472 loc) • 18.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentFlowAnalyzer = void 0;
const path = __importStar(require("path"));
const types_1 = require("./types");
const RouteScanner_1 = require("./scanners/RouteScanner");
const FlowTreeBuilder_1 = require("./builders/FlowTreeBuilder");
const RouteCoverageBuilder_1 = require("./builders/RouteCoverageBuilder");
const utils_1 = require("./utils");
/**
* Component flow analyzer using optimized services for enhanced performance
*/
class ComponentFlowAnalyzer {
constructor(projectRoot, srcDirectory, components, lookupService, pathResolver, scanResult, config = {}) {
this.projectRoot = projectRoot;
this.srcDirectory = srcDirectory;
this.components = components;
this.lookupService = lookupService;
this.pathResolver = pathResolver;
this.scanResult = scanResult;
this.config = this.mergeWithDefaultConfig(config);
this.errors = [];
// Find the app directory in common locations
const possibleAppDirs = [
path.join(projectRoot, "app"),
path.join(srcDirectory, "app"),
path.join(projectRoot, "src", "app"),
];
let appDirectory = "";
for (const dir of possibleAppDirs) {
if (require("fs").existsSync(dir)) {
appDirectory = dir;
break;
}
}
this.appDirectory = appDirectory || path.join(projectRoot, "app");
}
/**
* Performs complete component flow analysis using optimized services
*/
async analyze() {
try {
// Validate input and configuration
const validation = this.validateInput();
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors
.map((e) => e.message)
.join(", ")}`);
}
// Scan all routes in the app directory
const routeScanner = new RouteScanner_1.RouteScanner(this.appDirectory);
const routeStructures = routeScanner.scanAllRoutes();
if (routeStructures.length === 0) {
return this.createEmptyResult();
}
// Filter routes if specified in config
const filteredRoutes = this.filterRoutes(routeStructures);
// Build flow trees for all routes using optimized services
const flowTreeBuilder = new FlowTreeBuilder_1.FlowTreeBuilder(this.appDirectory, this.lookupService, this.pathResolver, this.scanResult, this.config);
const routeFlowTrees = [];
for (const routeStructure of filteredRoutes) {
try {
const flowTree = flowTreeBuilder.buildRouteFlowTree(routeStructure);
routeFlowTrees.push(flowTree);
}
catch (error) {
this.addError("parsing_error", `Failed to analyze route ${routeStructure.routePath}`, routeStructure.pageFilePath, error);
}
}
// Extract external dependencies using optimized detection
const externalDependencies = this.extractExternalDependencies(routeFlowTrees);
// Generate summary
const summary = this.generateSummary(routeFlowTrees, externalDependencies);
return {
routes: routeFlowTrees.map((tree) => ({
routePath: tree.routePath,
pageComponent: tree.pageComponent,
specialFiles: tree.specialFiles,
metadata: tree.metadata,
})),
summary,
externalDependencies,
};
}
catch (error) {
this.addError("parsing_error", "Analysis failed", "", error);
throw error;
}
}
/**
* Analyzes a single route using optimized services
*/
async analyzeSingleRoute(routePath) {
try {
const routeScanner = new RouteScanner_1.RouteScanner(this.appDirectory);
const routeStructure = routeScanner.scanRoute(routePath);
if (!routeStructure) {
return null;
}
const flowTreeBuilder = new FlowTreeBuilder_1.FlowTreeBuilder(this.appDirectory, this.lookupService, this.pathResolver, this.scanResult, this.config);
const enhancedTree = flowTreeBuilder.buildRouteFlowTree(routeStructure);
return {
routePath: enhancedTree.routePath,
pageComponent: enhancedTree.pageComponent,
specialFiles: enhancedTree.specialFiles,
metadata: enhancedTree.metadata,
};
}
catch (error) {
this.addError("parsing_error", `Failed to analyze route ${routePath}`, "", error);
return null;
}
}
/**
* Performs quick analysis with limited depth
*/
async quickAnalyze(maxDepth = 2) {
const originalMaxDepth = this.config.maxDepth;
this.config.maxDepth = maxDepth;
try {
return await this.analyze();
}
finally {
this.config.maxDepth = originalMaxDepth;
}
}
/**
* Gets analysis errors
*/
getErrors() {
return [...this.errors];
}
/**
* Gets coverage summary for all routes
*/
async getCoverageSummary() {
const routeScanner = new RouteScanner_1.RouteScanner(this.appDirectory);
const routeStructures = routeScanner.scanAllRoutes();
const coverageBuilder = new RouteCoverageBuilder_1.RouteCoverageBuilder(this.appDirectory);
const coverageAnalyses = coverageBuilder.buildMultipleRoutesCoverage(routeStructures);
const summary = coverageBuilder.buildOverallCoverageSummary(coverageAnalyses);
return {
totalRoutes: summary.totalRoutes,
routesWithMissingFiles: summary.criticalRiskRoutes.concat(summary.highRiskRoutes),
averageCoverage: summary.averageCoverage,
mostCommonMissingFiles: summary.mostCommonMissingFiles,
};
}
/**
* Validates input data and configuration
*/
validateInput() {
const errors = [];
const warnings = [];
// Check if app directory exists
try {
const fs = require("fs");
if (!fs.existsSync(this.appDirectory)) {
errors.push({
type: "file_not_found",
message: "App directory not found",
filePath: this.appDirectory,
});
}
}
catch (error) {
errors.push({
type: "file_not_found",
message: "Cannot access app directory",
filePath: this.appDirectory,
});
}
// Check if components array is not empty
if (this.components.length === 0) {
warnings.push("No components provided for analysis");
}
// Validate configuration
if (this.config.maxDepth < 1) {
errors.push({
type: "parsing_error",
message: "maxDepth must be at least 1",
filePath: "",
});
}
// Validate HTML element configuration
if (this.config.includeHtmlElements) {
const filter = this.config.htmlElementFilter;
if (!filter.includeAll && filter.includeTags.length === 0) {
warnings.push("HTML element tracking enabled but no tags specified to include");
}
if (filter.maxTextLength < 0) {
errors.push({
type: "parsing_error",
message: "maxTextLength cannot be negative",
filePath: "",
});
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Merges user config with default configuration
*/
mergeWithDefaultConfig(userConfig) {
const defaultConfig = {
maxDepth: 10,
includeExternalComponents: true,
excludePatterns: ["**/node_modules/**", "**/.next/**", "**/dist/**"],
onlyAnalyzeRoutes: [],
includeHtmlElements: false,
htmlElementFilter: types_1.DEFAULT_HTML_ELEMENT_FILTER,
};
return {
...defaultConfig,
...userConfig,
htmlElementFilter: {
...defaultConfig.htmlElementFilter,
...(userConfig.htmlElementFilter || {}),
},
};
}
/**
* Filters routes based on configuration
*/
filterRoutes(routeStructures) {
let filtered = routeStructures;
// Filter by onlyAnalyzeRoutes if specified
if (this.config.onlyAnalyzeRoutes.length > 0) {
filtered = filtered.filter((route) => this.config.onlyAnalyzeRoutes.some((pattern) => route.routePath.includes(pattern) ||
(0, utils_1.matchesPattern)(route.routePath, pattern)));
}
// Filter by exclude patterns
if (this.config.excludePatterns.length > 0) {
filtered = filtered.filter((route) => !this.config.excludePatterns.some((pattern) => (0, utils_1.matchesPattern)(route.pageFilePath, pattern)));
}
return filtered;
}
/**
* Extracts external dependencies from route flow trees using optimized detection
*/
extractExternalDependencies(routeFlowTrees) {
const dependencyMap = new Map();
const allImports = new Set();
for (const tree of routeFlowTrees) {
this.collectImportsFromComponent(tree.pageComponent, allImports);
}
// Build dependency usage map using PathResolver for external detection
for (const tree of routeFlowTrees) {
const routeImports = new Set();
this.collectImportsFromComponent(tree.pageComponent, routeImports);
for (const importPath of routeImports) {
// Use PathResolver to check if import is external
if (this.pathResolver.isExternalPackage(importPath)) {
const packageName = this.pathResolver.extractPackageName(importPath);
if (!dependencyMap.has(packageName)) {
dependencyMap.set(packageName, {
usedInRoutes: new Set(),
usageCount: 0,
});
}
const dependency = dependencyMap.get(packageName);
dependency.usedInRoutes.add(tree.routePath);
dependency.usageCount++;
}
}
}
return Array.from(dependencyMap.entries()).map(([name, data]) => ({
name,
usedInRoutes: Array.from(data.usedInRoutes),
usageCount: data.usageCount,
}));
}
/**
* Collects all imports from a component tree
*/
collectImportsFromComponent(component, imports, visited = new Set()) {
const componentKey = `${component.componentName}-${component.filePath}`;
if (visited.has(componentKey)) {
return;
}
visited.add(componentKey);
if (component.isExternal && component.componentName) {
imports.add(component.componentName);
}
// Process conditional renders
for (const conditionalRender of component.conditionalRenders) {
for (const child of conditionalRender.trueBranch) {
this.collectImportsFromComponent(child, imports, visited);
}
if (conditionalRender.falseBranch) {
for (const child of conditionalRender.falseBranch) {
this.collectImportsFromComponent(child, imports, visited);
}
}
}
// Process regular children
for (const child of component.children) {
this.collectImportsFromComponent(child, imports, visited);
}
}
/**
* Generates analysis summary
*/
generateSummary(routeFlowTrees, externalDependencies) {
const totalRoutes = routeFlowTrees.length;
const globalStats = this.calculateGlobalStats(routeFlowTrees);
const routesWithMissingFiles = [];
for (const tree of routeFlowTrees) {
if (tree.coverageAnalysis.coverageMetrics.missingFiles.length > 0) {
routesWithMissingFiles.push(tree.routePath);
}
}
// Find most complex route
let mostComplexRoute = "/";
let maxComplexity = 0;
for (const tree of routeFlowTrees) {
const routeComplexity = tree.componentStats.totalComponents +
tree.componentStats.conditionalRenderCount * 2;
if (routeComplexity > maxComplexity) {
maxComplexity = routeComplexity;
mostComplexRoute = tree.routePath;
}
}
const averageComponentDepth = totalRoutes > 0 ? globalStats.totalDepth / totalRoutes : 0;
return {
totalRoutes,
totalComponents: globalStats.uniqueComponents,
totalConditionalRenders: globalStats.totalConditionals,
routesWithMissingSpecialFiles: routesWithMissingFiles,
mostComplexRoute,
averageComponentDepth: Math.round(averageComponentDepth * 100) / 100,
};
}
/**
* Calculates global statistics across all routes
*/
calculateGlobalStats(routeFlowTrees) {
const visitedComponents = new Set();
let totalConditionals = 0;
let totalDepth = 0;
const traverseComponent = (component, depth = 0) => {
const componentKey = `${component.componentName}-${component.filePath}`;
if (visitedComponents.has(componentKey)) {
return;
}
visitedComponents.add(componentKey);
totalConditionals += component.conditionalRenders.length;
totalDepth += depth;
// Traverse children
for (const child of component.children) {
traverseComponent(child, depth + 1);
}
// Traverse conditional branches
for (const conditionalRender of component.conditionalRenders) {
for (const child of conditionalRender.trueBranch) {
traverseComponent(child, depth + 1);
}
if (conditionalRender.falseBranch) {
for (const child of conditionalRender.falseBranch) {
traverseComponent(child, depth + 1);
}
}
}
};
for (const tree of routeFlowTrees) {
traverseComponent(tree.pageComponent, 0);
}
return {
totalConditionals,
uniqueComponents: visitedComponents.size,
totalDepth,
};
}
/**
* Creates empty result for when no routes are found
*/
createEmptyResult() {
return {
routes: [],
summary: {
totalRoutes: 0,
totalComponents: 0,
totalConditionalRenders: 0,
routesWithMissingSpecialFiles: [],
mostComplexRoute: "",
averageComponentDepth: 0,
},
externalDependencies: [],
};
}
/**
* Adds an error to the error collection
*/
addError(type, message, filePath, originalError) {
this.errors.push({
type,
message: originalError ? `${message}: ${originalError.message}` : message,
filePath,
});
}
/**
* Clears all accumulated errors
*/
clearErrors() {
this.errors = [];
}
/**
* Gets configuration used for analysis
*/
getConfig() {
return { ...this.config };
}
/**
* Updates configuration
*/
updateConfig(newConfig) {
this.config = {
...this.config,
...newConfig,
htmlElementFilter: {
...this.config.htmlElementFilter,
...(newConfig.htmlElementFilter || {}),
},
};
}
/**
* Enables HTML element tracking with optional filter configuration
*/
enableHtmlElementTracking(filter) {
this.config.includeHtmlElements = true;
if (filter) {
this.config.htmlElementFilter = {
...this.config.htmlElementFilter,
...filter,
};
}
}
/**
* Disables HTML element tracking
*/
disableHtmlElementTracking() {
this.config.includeHtmlElements = false;
}
}
exports.ComponentFlowAnalyzer = ComponentFlowAnalyzer;