sicua
Version:
A tool for analyzing project structure and dependencies
305 lines (304 loc) • 11.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.RouteScanner = void 0;
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const SpecialFileCoverageScanner_1 = require("./SpecialFileCoverageScanner");
const utils_1 = require("../utils");
/**
* Scans Next.js app directory for routes and integrates special file coverage
*/
class RouteScanner {
constructor(appDirectory) {
this.appDirectory = appDirectory;
this.specialFileScanner = new SpecialFileCoverageScanner_1.SpecialFileCoverageScanner(appDirectory);
this.validExtensions = [".js", ".jsx", ".ts", ".tsx"];
}
/**
* Scans the entire app directory and returns all route structures
*/
scanAllRoutes() {
const routes = [];
try {
const pageFiles = this.findAllPageFiles(this.appDirectory);
for (const pageFile of pageFiles) {
const route = this.buildRouteStructure(pageFile);
if (route) {
routes.push(route);
}
}
}
catch (error) {
console.error("Error scanning routes:", error);
}
return routes;
}
/**
* Scans a specific route path
*/
scanRoute(routePath) {
const pageFilePath = this.findPageFile(routePath);
if (!pageFilePath) {
return null;
}
return this.buildRouteStructure(pageFilePath);
}
/**
* Gets all route paths in the app directory
*/
getAllRoutePaths() {
const pageFiles = this.findAllPageFiles(this.appDirectory);
return pageFiles.map((pageFile) => this.extractRoutePathFromFile(pageFile));
}
/**
* Checks if a route exists
*/
routeExists(routePath) {
return this.findPageFile(routePath) !== null;
}
/**
* Gets route metadata for a specific path
*/
getRouteMetadata(routePath) {
const segments = (0, utils_1.parseRoutePath)(routePath);
return this.buildRouteMetadata(segments, routePath);
}
/**
* Finds all page.js/page.tsx files in the app directory
*/
findAllPageFiles(directory, basePath = "") {
const pageFiles = [];
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
const relativePath = path.join(basePath, entry.name);
if (entry.isDirectory()) {
// Skip certain directories
if (this.shouldSkipDirectory(entry.name)) {
continue;
}
// Recursively scan subdirectories
pageFiles.push(...this.findAllPageFiles(fullPath, relativePath));
}
else if (entry.isFile()) {
// Check if it's a page file
if ((0, utils_1.isPageFile)(entry.name)) {
pageFiles.push(fullPath);
}
}
}
}
catch (error) {
console.warn(`Error reading directory ${directory}:`, error);
}
return pageFiles;
}
/**
* Finds page file for a specific route path
*/
findPageFile(routePath) {
const segments = (0, utils_1.parseRoutePath)(routePath);
let currentPath = this.appDirectory;
// Navigate through route segments
for (const segment of segments) {
currentPath = path.join(currentPath, segment);
if (!fs.existsSync(currentPath)) {
// Try to find dynamic route that matches
const parentDir = path.dirname(currentPath);
const dynamicMatch = this.findDynamicRouteMatch(parentDir, segment);
if (dynamicMatch) {
currentPath = path.join(parentDir, dynamicMatch);
}
else {
return null;
}
}
}
// Look for page file in the final directory
return this.findPageFileInDirectory(currentPath);
}
/**
* Builds complete route structure from a page file
*/
buildRouteStructure(pageFilePath) {
const routePath = this.extractRoutePathFromFile(pageFilePath);
const segments = (0, utils_1.parseRoutePath)(routePath);
const metadata = this.buildRouteMetadata(segments, routePath);
if (!metadata) {
return null;
}
const routeSegments = this.buildRouteSegments(segments);
return {
routePath,
pageFilePath,
segments: routeSegments,
metadata,
};
}
/**
* Builds route segments with special file coverage
*/
buildRouteSegments(segments) {
const routeSegments = [];
// Add root segment
const rootSpecialFiles = this.specialFileScanner.getSegmentSpecialFiles("");
routeSegments.push({
name: "",
isDynamic: false,
isCatchAll: false,
isRouteGroup: false,
specialFiles: rootSpecialFiles,
depth: 0,
});
// Add each route segment
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const segmentPath = segments.slice(0, i + 1).join("/");
const specialFiles = this.specialFileScanner.getSegmentSpecialFiles(segmentPath);
routeSegments.push({
name: segment,
isDynamic: (0, utils_1.isDynamicSegment)(segment),
isCatchAll: (0, utils_1.isCatchAllSegment)(segment),
isRouteGroup: (0, utils_1.isRouteGroup)(segment),
specialFiles,
depth: i + 1,
});
}
return routeSegments;
}
/**
* Builds route metadata from segments
*/
buildRouteMetadata(segments, routePath) {
const isDynamic = segments.some((segment) => (0, utils_1.isDynamicSegment)(segment));
const isCatchAll = segments.some((segment) => (0, utils_1.isCatchAllSegment)(segment));
const routeGroups = segments.filter((segment) => (0, utils_1.isRouteGroup)(segment));
return {
isDynamic,
isCatchAll,
routeGroup: routeGroups.length > 0 ? routeGroups[0] : undefined,
depth: segments.length,
segments: segments.filter((segment) => !(0, utils_1.isRouteGroup)(segment)), // Exclude route groups from segments
};
}
/**
* Extracts route path from page file path
*/
extractRoutePathFromFile(pageFilePath) {
const relativePath = path.relative(this.appDirectory, path.dirname(pageFilePath));
if (!relativePath || relativePath === ".") {
return "/";
}
// Convert file path to route path
const segments = relativePath.split(path.sep);
const routeSegments = segments.filter((segment) => !(0, utils_1.isRouteGroup)(segment));
return "/" + routeSegments.join("/");
}
/**
* Finds page file in a specific directory
*/
findPageFileInDirectory(directory) {
try {
for (const ext of this.validExtensions) {
const pageFile = path.join(directory, `page${ext}`);
if (fs.existsSync(pageFile)) {
return pageFile;
}
}
}
catch (error) {
console.warn(`Error searching for page file in ${directory}:`, error);
}
return null;
}
/**
* Finds dynamic route match in a directory
*/
findDynamicRouteMatch(directory, segment) {
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && (0, utils_1.isDynamicSegment)(entry.name)) {
// Check if this dynamic route has a page file
const pageFile = this.findPageFileInDirectory(path.join(directory, entry.name));
if (pageFile) {
return entry.name;
}
}
}
}
catch (error) {
console.warn(`Error finding dynamic route match in ${directory}:`, error);
}
return null;
}
/**
* Checks if a directory should be skipped during scanning
*/
shouldSkipDirectory(dirName) {
// Skip common non-route directories
const skipDirs = [
"node_modules",
".next",
".git",
"public",
"styles",
"components",
"lib",
"utils",
];
return skipDirs.includes(dirName) || dirName.startsWith(".");
}
/**
* Gets special file coverage for a specific route
*/
getRouteSpecialFileCoverage(routePath) {
return this.specialFileScanner.scanRouteSpecialFiles(routePath);
}
/**
* Gets coverage summary for all routes
*/
getAllRoutesCoverageSummary() {
const routes = this.scanAllRoutes();
const routesWithMissingFiles = [];
let totalCoverage = 0;
for (const route of routes) {
const coverage = this.specialFileScanner.scanRouteSpecialFiles(route.routePath);
const summary = this.specialFileScanner.getCoverageSummary(coverage);
totalCoverage += summary.coveragePercentage;
if (summary.missingFiles.length > 0) {
routesWithMissingFiles.push(route.routePath);
}
}
return {
totalRoutes: routes.length,
routesWithMissingFiles,
averageCoverage: routes.length > 0 ? totalCoverage / routes.length : 0,
};
}
}
exports.RouteScanner = RouteScanner;