sicua
Version:
A tool for analyzing project structure and dependencies
365 lines (364 loc) • 14.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouteUtils = void 0;
const path_browserify_1 = __importDefault(require("path-browserify"));
const seoRelatedUtils_1 = require("../../../utils/common/seoRelatedUtils");
const uiPatterns_1 = require("../../../constants/uiPatterns");
/**
* Utility functions for route extraction and normalization
*/
class RouteUtils {
/**
* Extracts route from file path based on framework patterns
*/
// RouteUtils.ts - updated extractRouteFromPath function
static extractRouteFromPath(filePath) {
// Skip route extraction for UI components
if (this.isLikelyUIComponent(filePath)) {
return null;
}
const normalizedPath = path_browserify_1.default.normalize(filePath);
// Handle Next.js style routes
if (normalizedPath.includes("/pages/") ||
normalizedPath.includes("\\pages\\")) {
return this.extractNextJsRoute(normalizedPath);
}
// Handle React Router style routes
if (normalizedPath.includes("/routes/") ||
normalizedPath.includes("\\routes\\")) {
return this.extractReactRouterRoute(normalizedPath);
}
// Handle app directory routes (Next.js 13+)
if (normalizedPath.includes("/app/") ||
normalizedPath.includes("\\app\\")) {
return this.extractAppDirRoute(normalizedPath);
}
return null;
}
/**
* Enhanced App Router route extraction with full Next.js 13+ support
*/
static extractAppDirRoute(filePath) {
const appDir = "/app/";
const routeStart = filePath.indexOf(appDir) + appDir.length;
let route = filePath.slice(routeStart);
// Normalize path separators
route = route.replace(/\\/g, "/");
// Remove file extension and /page suffix
route = route.replace(/\.[^/.]+$/, "");
route = route.replace(/\/page$/, "");
// Handle App Router special files - they don't affect routing
const specialFiles = [
"layout",
"loading",
"error",
"not-found",
"global-error",
"template",
"default",
];
const fileName = path_browserify_1.default.basename(route);
if (specialFiles.includes(fileName)) {
// For special files, return the directory they're in as the route
route = path_browserify_1.default.dirname(route);
}
// Handle route groups (parentheses are ignored in routing)
route = route.replace(/\([^)]+\)\//g, "");
// Handle parallel routes (@folder becomes empty in actual routing)
route = route.replace(/@[^/]+\//g, "");
// Handle dynamic segments
route = route.replace(/\[([^\]]+)\]/g, ":$1");
// Handle catch-all segments
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
// Handle optional catch-all segments
route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
// Handle intercepting routes
route = route.replace(/\(\.\.\.\)/g, "");
route = route.replace(/\(\.\)\//g, "");
route = route.replace(/\(\.\.\)\//g, "");
// Clean up multiple slashes
route = route.replace(/\/+/g, "/");
// Handle index routes
if (route === "" || route === "/")
route = "/";
// Ensure route starts with /
if (!route.startsWith("/")) {
route = "/" + route;
}
// Remove trailing slash unless it's root
if (route !== "/" && route.endsWith("/")) {
route = route.slice(0, -1);
}
return route;
}
/**
* Extracts route groups from App Router path
*/
static extractRouteGroups(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
const routeGroups = [];
// Find all route group patterns (text in parentheses)
const routeGroupMatches = normalizedPath.match(/\([^)]+\)/g);
if (routeGroupMatches) {
routeGroups.push(...routeGroupMatches);
}
return routeGroups;
}
/**
* Extracts parallel route slots from App Router path
*/
static extractParallelRoutes(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
const parallelRoutes = [];
// Find all parallel route patterns (@slot)
const parallelRouteMatches = normalizedPath.match(/@[^/\\]+/g);
if (parallelRouteMatches) {
parallelRouteMatches.forEach((match) => {
const slotIndex = normalizedPath.indexOf(match);
const slotPath = normalizedPath.substring(0, slotIndex + match.length);
parallelRoutes.push({
slot: match,
path: slotPath,
});
});
}
return parallelRoutes;
}
/**
* Determines if a path represents an intercepting route
*/
static isInterceptingRoute(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
// Check for intercepting route patterns
const interceptPatterns = [
{ pattern: /\(\.\.\.\)/, type: "(...)" },
{ pattern: /\(\.\.\.\.\)/, type: "(....)" },
{ pattern: /\(\.\.\)/, type: "(..)" },
{ pattern: /\(\.\)/, type: "(.)" },
];
for (const { pattern, type } of interceptPatterns) {
if (pattern.test(normalizedPath)) {
// Extract the target route (everything after the intercept pattern)
const match = normalizedPath.match(pattern);
if (match) {
const interceptIndex = normalizedPath.indexOf(match[0]);
const targetRoute = normalizedPath.substring(interceptIndex + match[0].length);
return {
isIntercepting: true,
interceptType: type,
targetRoute: targetRoute || null,
};
}
}
}
return {
isIntercepting: false,
interceptType: null,
targetRoute: null,
};
}
/**
* Gets the layout hierarchy for a given route
*/
static getLayoutHierarchy(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
const layouts = [];
if (!normalizedPath.includes("/app/") &&
!normalizedPath.includes("\\app\\")) {
return layouts;
}
const appDir = normalizedPath.includes("/app/") ? "/app/" : "\\app\\";
const appDirIndex = normalizedPath.indexOf(appDir);
const routePath = normalizedPath.substring(appDirIndex + appDir.length);
// Get all directory segments
const segments = path_browserify_1.default
.dirname(routePath)
.split(/[/\\]/)
.filter((segment) => segment && segment !== ".");
// Build layout paths from root to current level
let currentPath = normalizedPath.substring(0, appDirIndex + appDir.length - 1);
for (const segment of segments) {
currentPath = path_browserify_1.default.join(currentPath, segment);
const layoutPath = path_browserify_1.default.join(currentPath, "layout.tsx");
layouts.push(layoutPath.replace(/\\/g, "/"));
}
return layouts;
}
/**
* Checks if a route has dynamic segments
*/
static hasDynamicSegments(route) {
const segments = [];
// Dynamic segments [param]
const dynamicMatches = route.match(/\[([^\]]+)\]/g);
if (dynamicMatches) {
dynamicMatches.forEach((match) => {
const paramName = match.slice(1, -1);
if (paramName.startsWith("...")) {
// Catch-all segment
segments.push({
type: "catch-all",
name: paramName.slice(3),
optional: false,
});
}
else {
// Regular dynamic segment
segments.push({
type: "dynamic",
name: paramName,
optional: false,
});
}
});
}
// Optional catch-all segments [[...param]]
const optionalCatchAllMatches = route.match(/\[\[\.\.\.([^\]]+)\]\]/g);
if (optionalCatchAllMatches) {
optionalCatchAllMatches.forEach((match) => {
const paramName = match.slice(5, -2); // Remove [[... and ]]
segments.push({
type: "optional-catch-all",
name: paramName,
optional: true,
});
});
}
return {
hasDynamic: segments.length > 0,
segments,
};
}
// RouteUtils.ts - new helper method
static isLikelyUIComponent(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
// Check if it's in a components directory
if (normalizedPath.includes("/components/") ||
normalizedPath.includes("\\components\\")) {
// Check if it's not a page component inside a components directory
return (!normalizedPath.endsWith("page.tsx") &&
!normalizedPath.endsWith("page.jsx") &&
!normalizedPath.endsWith("Page.tsx") &&
!normalizedPath.endsWith("Page.jsx"));
}
// Check for common UI component file names
const fileName = path_browserify_1.default.basename(normalizedPath, path_browserify_1.default.extname(normalizedPath));
return uiPatterns_1.UI_COMPONENT_PATTERNS.some((pattern) => fileName === pattern ||
fileName.endsWith(pattern) ||
fileName.startsWith(pattern));
}
/**
* Extracts route from Next.js pages directory
*/
static extractNextJsRoute(filePath) {
const pagesDir = "/pages/";
const routeStart = filePath.indexOf(pagesDir) + pagesDir.length;
let route = filePath.slice(routeStart);
// Normalize path separators
route = route.replace(/\\/g, "/");
// Remove file extension
route = route.replace(/\.[^/.]+$/, "");
// Handle dynamic routes
route = route.replace(/\[([^\]]+)\]/g, ":$1");
// Handle catch-all routes
route = route.replace(/\.\.\./g, "*");
// Handle index routes
route = route.replace(/\/index$/, "/");
if (route === "index")
route = "/";
// Ensure route starts with /
return route.startsWith("/") ? route : "/" + route;
}
/**
* Extracts route from React Router routes directory
*/
static extractReactRouterRoute(filePath) {
const routesDir = "/routes/";
const routeStart = filePath.indexOf(routesDir) + routesDir.length;
let route = filePath.slice(routeStart);
// Normalize path separators
route = route.replace(/\\/g, "/");
// Remove file extension
route = route.replace(/\.[^/.]+$/, "");
// Handle index routes
route = route.replace(/\/index$/, "/");
if (route === "index")
route = "/";
// Ensure route starts with /
return route.startsWith("/") ? route : "/" + route;
}
/**
* Normalizes a link path for consistent analysis
*/
static normalizeLinkPath(link, sourcePath) {
const isRelative = link.startsWith(".") || !link.startsWith("/");
let normalizedPath = link;
if (isRelative) {
// Convert relative path to absolute
const sourceDir = path_browserify_1.default.dirname(sourcePath);
normalizedPath = path_browserify_1.default.join(path_browserify_1.default.dirname(sourceDir), link);
normalizedPath = normalizedPath.replace(/\\/g, "/");
// Ensure it starts with /
if (!normalizedPath.startsWith("/")) {
normalizedPath = "/" + normalizedPath;
}
}
// Clean up the path
normalizedPath = path_browserify_1.default.normalize(normalizedPath).replace(/\\/g, "/");
// Remove file extensions
normalizedPath = normalizedPath.replace(/\.(jsx?|tsx?)$/, "");
// Handle index routes
normalizedPath = normalizedPath.replace(/\/index$/, "/");
return {
to: normalizedPath,
isRelative,
};
}
/**
* Extracts route params from a route string
*/
static extractRouteParams(route) {
return seoRelatedUtils_1.SeoRelated.extractRouteParams(route);
}
/**
* Checks if a route is a dynamic route
*/
static isDynamicRoute(route) {
return route.includes(":") || route.includes("*");
}
/**
* Gets matching routes between file system routes and route definitions
*/
static findMatchingRoutes(fileSystemRoutes, routeDefinitions) {
const matches = new Map();
// For each route definition, find matching file system route
routeDefinitions.forEach((routeDef) => {
const matchingRoutes = fileSystemRoutes.filter((fsRoute) => {
// Convert both routes to regex patterns for matching
const fsRoutePattern = this.routeToRegexPattern(fsRoute);
const routeDefPattern = this.routeToRegexPattern(routeDef);
return fsRoutePattern === routeDefPattern;
});
if (matchingRoutes.length > 0) {
matches.set(routeDef, matchingRoutes);
}
});
return matches;
}
/**
* Converts a route to a regex pattern for matching
*/
static routeToRegexPattern(route) {
// Remove trailing slashes for consistency
let pattern = route.replace(/\/+$/, "");
// Replace param patterns with generic pattern
pattern = pattern.replace(/:[^/]+/g, ":param");
// Replace catch-all patterns
pattern = pattern.replace(/\*[^/]*/g, "*");
return pattern;
}
}
exports.RouteUtils = RouteUtils;