sicua
Version:
A tool for analyzing project structure and dependencies
368 lines (367 loc) • 16 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentUtils = void 0;
const typescript_1 = __importDefault(require("typescript"));
const path_browserify_1 = __importDefault(require("path-browserify"));
const uiPatterns_1 = require("../../../constants/uiPatterns");
/**
* Utility functions for identifying and processing components for SEO analysis
*/
class ComponentUtils {
/**
* Identifies if a component is a page component for SEO analysis
*/
// ComponentUtils.ts - updated isPageComponent function
static isPageComponent(component) {
if (!component.content)
return false;
const sourceFile = typescript_1.default.createSourceFile(component.fullPath, component.content, typescript_1.default.ScriptTarget.Latest, true);
let isPage = false;
let hasMetadata = false;
let hasDefaultExport = false;
let isUIComponent = false;
// Check normalized path for component patterns
const normalizedPath = path_browserify_1.default.normalize(component.fullPath);
// Check if this is likely a UI component based on path
isUIComponent = this.isUIComponentPath(normalizedPath);
const visit = (node) => {
// Check for metadata export
if (typescript_1.default.isVariableStatement(node)) {
const declaration = node.declarationList.declarations[0];
if (declaration && typescript_1.default.isIdentifier(declaration.name)) {
if (declaration.name.text === "metadata") {
hasMetadata = true;
}
}
}
// Check for generateMetadata function
if (typescript_1.default.isFunctionDeclaration(node) &&
node.name?.text === "generateMetadata") {
hasMetadata = true;
}
// Check for default export
if (typescript_1.default.isExportAssignment(node)) {
hasDefaultExport = true;
}
else if (typescript_1.default.isFunctionDeclaration(node) && node.modifiers) {
const isExport = node.modifiers.some((m) => m.kind === typescript_1.default.SyntaxKind.ExportKeyword);
const isDefault = node.modifiers.some((m) => m.kind === typescript_1.default.SyntaxKind.DefaultKeyword);
if (isExport && isDefault) {
hasDefaultExport = true;
}
}
typescript_1.default.forEachChild(node, visit);
};
visit(sourceFile);
// Check if the file matches page patterns
isPage = this.matchesPagePattern(normalizedPath);
// A component is a page if:
// 1. It's in a typical page location AND has a default export
// 2. OR It has metadata defined
// 3. AND it's not a UI component in a components directory
return ((isPage && hasDefaultExport) || hasMetadata) && !isUIComponent;
}
/**
* Identifies if a component is an App Router special file
*/
static isAppRouterSpecialFile(component) {
const normalizedPath = path_browserify_1.default.normalize(component.fullPath);
const fileName = path_browserify_1.default.basename(normalizedPath, path_browserify_1.default.extname(normalizedPath));
// Check if it's in app directory
if (!normalizedPath.includes("/app/") &&
!normalizedPath.includes("\\app\\")) {
return { isSpecialFile: false, fileType: null, routeSegment: "" };
}
// Determine file type
let fileType = null;
switch (fileName) {
case "layout":
fileType = "layout";
break;
case "loading":
fileType = "loading";
break;
case "error":
fileType = "error";
break;
case "not-found":
fileType = "not-found";
break;
case "global-error":
fileType = "global-error";
break;
case "template":
fileType = "template";
break;
default:
return { isSpecialFile: false, fileType: null, routeSegment: "" };
}
// Extract route segment
const appDir = normalizedPath.includes("/app/") ? "/app/" : "\\app\\";
const routeStart = normalizedPath.indexOf(appDir) + appDir.length;
const routeSegment = path_browserify_1.default
.dirname(normalizedPath.slice(routeStart))
.replace(/\\/g, "/");
return {
isSpecialFile: true,
fileType,
routeSegment: routeSegment === "." ? "/" : `/${routeSegment}`,
};
}
/**
* Identifies if a component is in a route group
*/
static getRouteGroupInfo(component) {
const normalizedPath = path_browserify_1.default.normalize(component.fullPath);
// Check if it's in app directory
if (!normalizedPath.includes("/app/") &&
!normalizedPath.includes("\\app\\")) {
return {
isInRouteGroup: false,
routeGroupName: null,
routeGroupPath: null,
};
}
// Look for route group pattern (parentheses)
const routeGroupMatch = normalizedPath.match(/[/\\](\([^)]+\))[/\\]/);
if (!routeGroupMatch) {
return {
isInRouteGroup: false,
routeGroupName: null,
routeGroupPath: null,
};
}
const routeGroupName = routeGroupMatch[1];
const routeGroupPath = normalizedPath.substring(0, normalizedPath.indexOf(routeGroupName) + routeGroupName.length);
return {
isInRouteGroup: true,
routeGroupName,
routeGroupPath,
};
}
/**
* Identifies if a component is a parallel route
*/
static getParallelRouteInfo(component) {
const normalizedPath = path_browserify_1.default.normalize(component.fullPath);
// Check if it's in app directory
if (!normalizedPath.includes("/app/") &&
!normalizedPath.includes("\\app\\")) {
return {
isParallelRoute: false,
slotName: null,
parentRoute: null,
isDefaultSlot: false,
};
}
// Look for parallel route pattern (@slot)
const parallelRouteMatch = normalizedPath.match(/[/\\](@[^/\\]+)[/\\]/);
if (!parallelRouteMatch) {
return {
isParallelRoute: false,
slotName: null,
parentRoute: null,
isDefaultSlot: false,
};
}
const slotName = parallelRouteMatch[1];
const slotIndex = normalizedPath.indexOf(slotName);
const parentRoute = normalizedPath.substring(0, slotIndex - 1);
// Check if this is a default slot file
const fileName = path_browserify_1.default.basename(normalizedPath, path_browserify_1.default.extname(normalizedPath));
const isDefaultSlot = fileName === "default";
return {
isParallelRoute: true,
slotName,
parentRoute,
isDefaultSlot,
};
}
/**
* Checks if a component has metadata that could conflict with parent layouts
*/
static hasMetadataConflicts(component) {
if (!component.content)
return { hasConflicts: false, conflictingFields: [] };
const sourceFile = typescript_1.default.createSourceFile(component.fullPath, component.content, typescript_1.default.ScriptTarget.Latest, true);
const metadataObject = this.extractMetadataObject(sourceFile);
if (!metadataObject)
return { hasConflicts: false, conflictingFields: [] };
// Fields that commonly cause conflicts between layouts
const potentialConflictFields = [
"title",
"description",
"openGraph",
"twitter",
"robots",
];
const conflictingFields = [];
metadataObject.properties.forEach((prop) => {
if (typescript_1.default.isPropertyAssignment(prop)) {
const propertyName = prop.name.getText();
if (potentialConflictFields.includes(propertyName)) {
conflictingFields.push(propertyName);
}
}
});
return {
hasConflicts: conflictingFields.length > 0,
conflictingFields,
};
}
/**
* Determines the nesting level of a layout in the App Router hierarchy
*/
static getLayoutNestingLevel(component) {
const normalizedPath = path_browserify_1.default.normalize(component.fullPath);
if (!normalizedPath.includes("/app/") &&
!normalizedPath.includes("\\app\\")) {
return 0;
}
const appDir = normalizedPath.includes("/app/") ? "/app/" : "\\app\\";
const routeStart = normalizedPath.indexOf(appDir) + appDir.length;
const routePath = normalizedPath.slice(routeStart);
// Count directory levels (excluding file itself)
const segments = path_browserify_1.default
.dirname(routePath)
.split(/[/\\]/)
.filter((segment) => segment && segment !== ".");
// Filter out route groups from nesting level calculation
const realSegments = segments.filter((segment) => !segment.startsWith("(") || !segment.endsWith(")"));
return realSegments.length;
}
/**
* Gets the parent layout path for a given component
*/
static getParentLayoutPath(component) {
const normalizedPath = path_browserify_1.default.normalize(component.fullPath);
if (!normalizedPath.includes("/app/") &&
!normalizedPath.includes("\\app\\")) {
return null;
}
const appDir = normalizedPath.includes("/app/") ? "/app/" : "\\app\\";
const appDirIndex = normalizedPath.indexOf(appDir);
const currentDir = path_browserify_1.default.dirname(normalizedPath);
// Walk up the directory tree looking for parent layout
let searchDir = path_browserify_1.default.dirname(currentDir);
while (searchDir.length > appDirIndex + appDir.length - 1) {
const possibleLayoutPath = path_browserify_1.default.join(searchDir, "layout.tsx");
const possibleLayoutPathJsx = path_browserify_1.default.join(searchDir, "layout.jsx");
// In a real implementation, you'd check if these files exist
// For now, we return the first potential parent layout path
if (searchDir !== currentDir) {
return possibleLayoutPath.replace(/\\/g, "/");
}
searchDir = path_browserify_1.default.dirname(searchDir);
}
return null;
}
// ComponentUtils.ts - new helper methods
static isUIComponentPath(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
// Check if it's in a components directory but not a page component
const isInComponentsDir = normalizedPath.includes("/components/") ||
normalizedPath.includes("\\components\\");
// Check if it's a typical UI component name
const fileName = path_browserify_1.default.basename(normalizedPath, path_browserify_1.default.extname(normalizedPath));
const matchesUIPattern = uiPatterns_1.UI_COMPONENT_PATTERNS.some((pattern) => fileName === pattern ||
fileName.endsWith(pattern) ||
fileName.startsWith(pattern));
return isInComponentsDir || matchesUIPattern;
}
static matchesPagePattern(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
// Check for various Next.js page patterns
return (normalizedPath.includes("/pages/") ||
normalizedPath.includes("\\pages\\") ||
normalizedPath.includes("/app/") ||
normalizedPath.includes("\\app\\") ||
normalizedPath.endsWith(".page.tsx") ||
normalizedPath.endsWith(".page.jsx") ||
normalizedPath.endsWith("Page.tsx") ||
normalizedPath.endsWith("Page.jsx") ||
// Exclude component files in these directories
(!normalizedPath.includes("/components/") &&
!normalizedPath.includes("\\components\\") &&
(normalizedPath.includes("/views/") ||
normalizedPath.includes("\\views\\") ||
normalizedPath.includes("/routes/") ||
normalizedPath.includes("\\routes\\"))));
}
/**
* Gets a source file from component content
*/
static getSourceFile(component) {
if (!component.content)
return null;
return typescript_1.default.createSourceFile(component.fullPath, component.content, typescript_1.default.ScriptTarget.Latest, true);
}
/**
* Checks if a filename matches page component patterns
*/
static isPageFilename(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
return (normalizedPath.includes("pages/") ||
normalizedPath.includes("app/") ||
normalizedPath.endsWith(".page.tsx") ||
normalizedPath.endsWith("Page.tsx") ||
normalizedPath.endsWith("Page.jsx") ||
normalizedPath.includes("views/") ||
normalizedPath.includes("routes/"));
}
/**
* Extracts metadata object from a page component's source file
*/
static extractMetadataObject(sourceFile) {
let metadataObject = null;
const visit = (node) => {
if (typescript_1.default.isVariableStatement(node)) {
const declaration = node.declarationList.declarations[0];
if (declaration &&
typescript_1.default.isIdentifier(declaration.name) &&
declaration.name.text === "metadata" &&
declaration.initializer &&
typescript_1.default.isObjectLiteralExpression(declaration.initializer)) {
metadataObject = declaration.initializer;
}
}
if (!metadataObject) {
typescript_1.default.forEachChild(node, visit);
}
};
visit(sourceFile);
return metadataObject;
}
/**
* Checks if a node is a generateMetadata function
*/
static isGenerateMetadataFunction(node) {
return (typescript_1.default.isFunctionDeclaration(node) &&
!!node.name &&
node.name.text === "generateMetadata");
}
/**
* Extracts page name from file path
*/
static extractPageName(filePath) {
const normalizedPath = path_browserify_1.default.normalize(filePath);
const fileName = path_browserify_1.default.basename(normalizedPath);
// Remove extension
let pageName = fileName.replace(/\.[^/.]+$/, "");
// Handle index pages
if (pageName === "index") {
const dirName = path_browserify_1.default.basename(path_browserify_1.default.dirname(normalizedPath));
pageName = dirName === "pages" || dirName === "app" ? "Home" : dirName;
}
// Handle page naming conventions
pageName = pageName
.replace(/^page$/, path_browserify_1.default.basename(path_browserify_1.default.dirname(normalizedPath)))
.replace(/\.page$/, "")
.replace(/Page$/, "");
return pageName;
}
}
exports.ComponentUtils = ComponentUtils;