UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

289 lines (288 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RouteCoverageBuilder = void 0; const SpecialFileCoverageScanner_1 = require("../scanners/SpecialFileCoverageScanner"); /** * Builds comprehensive coverage analysis for routes */ class RouteCoverageBuilder { constructor(appDirectory) { this.specialFileScanner = new SpecialFileCoverageScanner_1.SpecialFileCoverageScanner(appDirectory); } /** * Builds coverage analysis for a single route */ buildRouteCoverage(routeStructure) { const specialFilesCoverage = this.specialFileScanner.scanRouteSpecialFiles(routeStructure.routePath); const coverageMetrics = this.calculateCoverageMetrics(specialFilesCoverage, routeStructure); const recommendations = this.generateRecommendations(specialFilesCoverage, routeStructure); const riskAssessment = this.assessRisk(coverageMetrics, routeStructure); return { routePath: routeStructure.routePath, routeMetadata: routeStructure.metadata, specialFilesCoverage, coverageMetrics, recommendations, riskAssessment, }; } /** * Builds coverage analysis for multiple routes */ buildMultipleRoutesCoverage(routeStructures) { return routeStructures.map((route) => this.buildRouteCoverage(route)); } /** * Builds overall coverage summary across all routes */ buildOverallCoverageSummary(routeCoverages) { const totalRoutes = routeCoverages.length; const totalCoverage = routeCoverages.reduce((sum, route) => sum + route.coverageMetrics.coveragePercentage, 0); const averageCoverage = totalRoutes > 0 ? totalCoverage / totalRoutes : 0; const criticalRiskRoutes = routeCoverages .filter((route) => route.riskAssessment === "critical") .map((route) => route.routePath); const highRiskRoutes = routeCoverages .filter((route) => route.riskAssessment === "high") .map((route) => route.routePath); const missingFilesCount = new Map(); const allRecommendations = []; for (const route of routeCoverages) { // Count missing files for (const missingFile of route.coverageMetrics.missingFiles) { missingFilesCount.set(missingFile, (missingFilesCount.get(missingFile) || 0) + 1); } // Collect high-priority recommendations allRecommendations.push(...route.recommendations.filter((rec) => rec.priority === "high")); } const mostCommonMissingFiles = Array.from(missingFilesCount.entries()) .map(([fileName, count]) => ({ fileName, count })) .sort((a, b) => b.count - a.count) .slice(0, 5); return { totalRoutes, averageCoverage, criticalRiskRoutes, highRiskRoutes, mostCommonMissingFiles, recommendations: this.deduplicateRecommendations(allRecommendations), }; } /** * Calculates detailed coverage metrics */ calculateCoverageMetrics(coverage, routeStructure) { const missingFiles = []; let totalRequiredFiles = 0; let existingFiles = 0; // Analyze layout coverage const layoutTotal = coverage.layout.length; const layoutExisting = coverage.layout.filter((layout) => layout.exists).length; const layoutMissing = coverage.layout .filter((layout) => !layout.exists) .map((layout) => `layout (${layout.routeSegment || "root"})`); totalRequiredFiles += layoutTotal; existingFiles += layoutExisting; missingFiles.push(...layoutMissing); // Analyze other special files const specialFiles = [ { name: "template", info: coverage.template }, { name: "error", info: coverage.error }, { name: "loading", info: coverage.loading }, { name: "not-found", info: coverage.notFound }, ]; for (const file of specialFiles) { if (file.info) { totalRequiredFiles += 1; if (file.info.exists) { existingFiles += 1; } else { missingFiles.push(file.name); } } } const coveragePercentage = totalRequiredFiles > 0 ? (existingFiles / totalRequiredFiles) * 100 : 100; return { totalRequiredFiles, existingFiles, missingFiles, coveragePercentage, layoutCoverage: { total: layoutTotal, existing: layoutExisting, missing: layoutMissing, }, errorHandlingCoverage: { hasErrorBoundary: coverage.error?.exists || false, hasNotFound: coverage.notFound?.exists || false, hasLoading: coverage.loading?.exists || false, }, }; } /** * Generates recommendations based on coverage analysis */ generateRecommendations(coverage, routeStructure) { const recommendations = []; // Check for missing root layout const rootLayout = coverage.layout.find((layout) => layout.routeSegment === ""); if (!rootLayout?.exists) { recommendations.push({ type: "missing_file", priority: "high", message: "Missing root layout.tsx - required for all Next.js apps", filePath: "app/layout.tsx", action: "Create a root layout file with html and body tags", }); } // Check for missing error boundaries if (!coverage.error?.exists) { recommendations.push({ type: "missing_file", priority: routeStructure.metadata.isDynamic ? "high" : "medium", message: "Missing error.tsx - recommended for error handling", filePath: `app${routeStructure.routePath}/error.tsx`, action: "Create an error boundary to handle runtime errors gracefully", }); } // Check for missing loading UI if (!coverage.loading?.exists) { recommendations.push({ type: "missing_file", priority: "medium", message: "Missing loading.tsx - improves user experience during data fetching", filePath: `app${routeStructure.routePath}/loading.tsx`, action: "Create a loading component to show while the page is being rendered", }); } // Check for missing not-found page if (!coverage.notFound?.exists && routeStructure.metadata.isDynamic) { recommendations.push({ type: "missing_file", priority: "medium", message: "Missing not-found.tsx - recommended for dynamic routes", filePath: `app${routeStructure.routePath}/not-found.tsx`, action: "Create a not-found page for when dynamic routes don't match", }); } // Performance recommendations if (routeStructure.metadata.depth > 3 && !coverage.template?.exists) { recommendations.push({ type: "performance", priority: "low", message: "Consider using template.tsx for deeply nested routes to optimize re-renders", filePath: `app${routeStructure.routePath}/template.tsx`, action: "Create a template file if you need to re-render the layout on navigation", }); } // Best practice recommendations if (routeStructure.metadata.isCatchAll && !coverage.notFound?.exists) { recommendations.push({ type: "best_practice", priority: "high", message: "Catch-all routes should have proper not-found handling", filePath: `app${routeStructure.routePath}/not-found.tsx`, action: "Create a not-found page to handle unmatched catch-all routes", }); } return recommendations; } /** * Assesses risk level based on coverage metrics and route characteristics */ assessRisk(metrics, routeStructure) { let riskScore = 0; // Coverage percentage impact if (metrics.coveragePercentage < 50) { riskScore += 3; } else if (metrics.coveragePercentage < 75) { riskScore += 2; } else if (metrics.coveragePercentage < 90) { riskScore += 1; } // Missing critical files if (!metrics.layoutCoverage.existing && metrics.layoutCoverage.total > 0) { riskScore += 2; // Missing layouts are critical } if (!metrics.errorHandlingCoverage.hasErrorBoundary) { riskScore += routeStructure.metadata.isDynamic ? 2 : 1; } // Route complexity impact if (routeStructure.metadata.isDynamic) { riskScore += 1; } if (routeStructure.metadata.isCatchAll) { riskScore += 1; } if (routeStructure.metadata.depth > 3) { riskScore += 1; } // Determine risk level if (riskScore >= 6) { return "critical"; } else if (riskScore >= 4) { return "high"; } else if (riskScore >= 2) { return "medium"; } else { return "low"; } } /** * Removes duplicate recommendations */ deduplicateRecommendations(recommendations) { const seen = new Set(); const deduplicated = []; for (const rec of recommendations) { const key = `${rec.type}-${rec.message}`; if (!seen.has(key)) { seen.add(key); deduplicated.push(rec); } } return deduplicated.sort((a, b) => { const priorityOrder = { high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); } /** * Gets detailed analysis for a specific special file type across routes */ getSpecialFileAnalysis(routeCoverages, fileType) { const totalRoutes = routeCoverages.length; let routesWithFile = 0; const routesMissingFile = []; for (const route of routeCoverages) { const fileInfo = route.specialFilesCoverage[fileType]; let hasFile = false; if (Array.isArray(fileInfo)) { // Layout files hasFile = fileInfo.some((layout) => layout.exists); } else if (fileInfo) { // Other special files hasFile = fileInfo.exists; } if (hasFile) { routesWithFile++; } else { routesMissingFile.push(route.routePath); } } return { totalRoutes, routesWithFile, routesMissingFile, averageImplementation: totalRoutes > 0 ? (routesWithFile / totalRoutes) * 100 : 0, }; } } exports.RouteCoverageBuilder = RouteCoverageBuilder;