UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

473 lines (472 loc) 18.1 kB
"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;