UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

368 lines (367 loc) 16 kB
"use strict"; 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;