UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

410 lines (409 loc) 19.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SEOAnalyzer = void 0; const componentUtils_1 = require("./utils/componentUtils"); const routeUtils_1 = require("./utils/routeUtils"); const metaTagAnalyzer_1 = require("./analyzers/metaTagAnalyzer"); const structuredDataAnalyzer_1 = require("./analyzers/structuredDataAnalyzer"); const routingAnalyzer_1 = require("./analyzers/routingAnalyzer"); const accessibilityAnalyzer_1 = require("./analyzers/accessibilityAnalyzer"); const contentAnalyzer_1 = require("./analyzers/contentAnalyzer"); const headingAnalyzer_1 = require("./analyzers/headingAnalyzer"); const landmarkAnalyzer_1 = require("./analyzers/landmarkAnalyzer"); const appRouterAnalyzer_1 = require("./analyzers/appRouterAnalyzer"); const performanceAnalyzer_1 = require("./analyzers/performanceAnalyzer"); const imageOptimizationAnalyzer_1 = require("./analyzers/imageOptimizationAnalyzer"); const middlewareAnalyzer_1 = require("./analyzers/middlewareAnalyzer"); const bundleAnalyzer_1 = require("./analyzers/bundleAnalyzer"); const path_1 = __importDefault(require("path")); const analysisUtils_1 = require("../../utils/common/analysisUtils"); const uiPatterns_1 = require("../../constants/uiPatterns"); /** * Main analyzer for SEO-related aspects of React applications */ class SEOAnalyzer { /** * Create a new SEO analyzer * * @param components Array of component relations to analyze * @param options Optional analysis configuration */ constructor(components, options = {}) { this.components = components; this.pageComponents = new Map(); this.metaTags = new Map(); this.structuredData = new Map(); this.routes = new Map(); this.options = { analyzeRouting: true, analyzeStructuredData: true, analyzeAccessibility: true, analyzeTextContent: true, ...options, }; } /** * Run the comprehensive SEO analysis including new Next.js features */ async analyze() { // First pass: identify page components and build route map this.identifyPageComponents(); // Second pass: detailed analysis of each page component for (const [componentId, component] of this.pageComponents.entries()) { await this.analyzePageComponent(componentId, component); } // Create specialized analyzers const headingAnalyzer = new headingAnalyzer_1.HeadingAnalyzer(this.pageComponents); const landmarkAnalyzer = new landmarkAnalyzer_1.LandmarkAnalyzer(this.pageComponents); const accessibilityAnalyzer = new accessibilityAnalyzer_1.AccessibilityAnalyzer(this.pageComponents); const contentAnalyzer = new contentAnalyzer_1.ContentAnalyzer(this.pageComponents); const routingAnalyzer = new routingAnalyzer_1.RoutingAnalyzer(this.pageComponents, this.routes); // New analyzers for enhanced functionality const appRouterAnalyzer = new appRouterAnalyzer_1.AppRouterAnalyzer(this.pageComponents, this.components); const performanceAnalyzer = new performanceAnalyzer_1.PerformanceAnalyzer(this.pageComponents, this.components); const imageOptimizationAnalyzer = new imageOptimizationAnalyzer_1.ImageOptimizationAnalyzer(this.pageComponents, this.components); const middlewareAnalyzer = new middlewareAnalyzer_1.MiddlewareAnalyzer(this.components); const bundleAnalyzer = new bundleAnalyzer_1.BundleAnalyzer(this.pageComponents, this.components); // Build the comprehensive result return { metaTags: { pages: Object.fromEntries(this.metaTags), statistics: metaTagAnalyzer_1.MetaTagAnalyzer.calculateMetaTagStatistics(this.metaTags), }, semanticStructure: { headingHierarchy: headingAnalyzer.analyzeHeadingHierarchy(), landmarkElements: landmarkAnalyzer.analyzeLandmarkUsage(), accessibility: accessibilityAnalyzer.analyzeAccessibility(), }, contentStructure: { textContent: contentAnalyzer.analyzeTextContent(), structuredData: structuredDataAnalyzer_1.StructuredDataAnalyzer.generateStructuredDataAnalysis(this.structuredData), routing: routingAnalyzer.analyzeRouting(), }, appRouter: { specialFiles: appRouterAnalyzer.analyzeAppRouter().specialFiles, layoutHierarchy: appRouterAnalyzer.analyzeAppRouter().layoutHierarchy, routeGroups: appRouterAnalyzer.analyzeAppRouter().routeGroups, parallelRoutes: appRouterAnalyzer.analyzeAppRouter().parallelRoutes, }, performance: { coreWebVitals: performanceAnalyzer.analyzeCoreWebVitals(), bundleOptimization: bundleAnalyzer.analyzeBundleOptimization(), lazyLoading: performanceAnalyzer.analyzeLazyLoading(), }, imageOptimization: imageOptimizationAnalyzer.analyzeImageOptimization(), middleware: middlewareAnalyzer.analyzeMiddleware(), }; } /** * Identify page components from all components */ identifyPageComponents() { for (const component of this.components) { if (componentUtils_1.ComponentUtils.isPageComponent(component)) { const normalizedPath = component.fullPath; // Additional check to exclude UI components if (!this.isUIComponent(component)) { this.pageComponents.set((0, analysisUtils_1.generateComponentId)(component), component); // Extract route information const route = routeUtils_1.RouteUtils.extractRouteFromPath(normalizedPath); if (route) { this.routes.set((0, analysisUtils_1.generateComponentId)(component), [route]); } } } } } isUIComponent(component) { const normalizedPath = path_1.default.normalize(component.fullPath); // Check for components directory const isInComponentsDir = normalizedPath.includes("/components/") || normalizedPath.includes("\\components\\"); // If it's not in a components directory, check for UI component naming patterns if (!isInComponentsDir) { const fileName = path_1.default.basename(normalizedPath, path_1.default.extname(normalizedPath)); return uiPatterns_1.UI_COMPONENT_PATTERNS.some((pattern) => fileName === pattern || fileName.endsWith(pattern) || fileName.startsWith(pattern)); } // Special case: page components can sometimes live in component directories // Check for explicit page indicators if (isInComponentsDir) { return !(normalizedPath.endsWith("page.tsx") || normalizedPath.endsWith("page.jsx") || normalizedPath.endsWith("Page.tsx") || normalizedPath.endsWith("Page.jsx") || component.content?.includes("generateMetadata") || component.content?.includes("export const metadata")); } return isInComponentsDir; } /** * Analyze a single page component */ async analyzePageComponent(pagePath, component) { if (!component.content) return; try { // Analyze meta tags const metaAnalyzer = new metaTagAnalyzer_1.MetaTagAnalyzer(component); const metaAnalysis = metaAnalyzer.analyzeMetaTags(); this.metaTags.set(pagePath, metaAnalysis); // Analyze structured data if enabled if (this.options.analyzeStructuredData) { const structuredDataAnalyzer = new structuredDataAnalyzer_1.StructuredDataAnalyzer(component); const structuredDataAnalysis = structuredDataAnalyzer.analyzeStructuredData(); if (structuredDataAnalysis) { this.structuredData.set(pagePath, structuredDataAnalysis); } } } catch (error) { // Silently continue with other components if one fails } } /** * Get comprehensive SEO improvement suggestions including new features */ getSEOImprovementSuggestions() { // Create specialized analyzers const headingAnalyzer = new headingAnalyzer_1.HeadingAnalyzer(this.pageComponents); const landmarkAnalyzer = new landmarkAnalyzer_1.LandmarkAnalyzer(this.pageComponents); const accessibilityAnalyzer = new accessibilityAnalyzer_1.AccessibilityAnalyzer(this.pageComponents); const contentAnalyzer = new contentAnalyzer_1.ContentAnalyzer(this.pageComponents); const routingAnalyzer = new routingAnalyzer_1.RoutingAnalyzer(this.pageComponents, this.routes); // New analyzers const appRouterAnalyzer = new appRouterAnalyzer_1.AppRouterAnalyzer(this.pageComponents, this.components); const performanceAnalyzer = new performanceAnalyzer_1.PerformanceAnalyzer(this.pageComponents, this.components); const imageOptimizationAnalyzer = new imageOptimizationAnalyzer_1.ImageOptimizationAnalyzer(this.pageComponents, this.components); const middlewareAnalyzer = new middlewareAnalyzer_1.MiddlewareAnalyzer(this.components); const bundleAnalyzer = new bundleAnalyzer_1.BundleAnalyzer(this.pageComponents, this.components); // Generate meta tag suggestions const metaTags = []; const pages = Array.from(this.metaTags.values()); if (pages.filter((p) => p.title.present).length < this.pageComponents.size) { metaTags.push("Add title metadata to all pages"); } if (pages.filter((p) => p.description.present).length < this.pageComponents.size) { metaTags.push("Add description metadata to all pages"); } if (pages.filter((p) => p.openGraph.present).length < this.pageComponents.size * 0.5) { metaTags.push("Add Open Graph metadata for better social media sharing"); } // Get App Router suggestions const appRouterSuggestions = appRouterAnalyzer.getAppRouterImprovementSuggestions(); const appRouter = [ ...appRouterSuggestions.specialFiles, ...appRouterSuggestions.layouts, ...appRouterSuggestions.routeGroups, ...appRouterSuggestions.parallelRoutes, ...appRouterSuggestions.general, ]; return { metaTags, headings: headingAnalyzer.getHeadingImprovementSuggestions(), landmarks: landmarkAnalyzer.getLandmarkImprovementSuggestions(), accessibility: accessibilityAnalyzer.getAccessibilityImprovementSuggestions(), content: contentAnalyzer.getContentImprovementSuggestions(), routing: this.options.analyzeRouting ? routingAnalyzer.getRoutingImprovementSuggestions() : [], structuredData: this.options.analyzeStructuredData ? structuredDataAnalyzer_1.StructuredDataAnalyzer.getImprovementSuggestions(this.structuredData) : [], appRouter, performance: [ ...performanceAnalyzer.getPerformanceImprovementSuggestions(), ...bundleAnalyzer.getBundleOptimizationSuggestions(), ], imageOptimization: imageOptimizationAnalyzer.getImageOptimizationSuggestions(), middleware: middlewareAnalyzer.getMiddlewareImprovementSuggestions(), }; } /** * Get overall SEO health score (0-100) */ calculateSEOHealthScore() { const suggestions = this.getSEOImprovementSuggestions(); const criticalIssues = []; // Calculate category scores const metaTagsScore = Math.max(0, 100 - suggestions.metaTags.length * 15); const semanticScore = Math.max(0, 100 - (suggestions.headings.length + suggestions.landmarks.length) * 10); const performanceScore = Math.max(0, 100 - suggestions.performance.length * 12); const appRouterScore = Math.max(0, 100 - suggestions.appRouter.length * 8); const imageScore = Math.max(0, 100 - suggestions.imageOptimization.length * 10); const accessibilityScore = Math.max(0, 100 - suggestions.accessibility.length * 12); // Identify critical issues if (metaTagsScore < 50) { criticalIssues.push("Critical meta tag issues detected"); } if (performanceScore < 40) { criticalIssues.push("Severe performance issues affecting SEO"); } if (accessibilityScore < 30) { criticalIssues.push("Major accessibility issues impacting SEO"); } // Calculate weighted overall score const weights = { metaTags: 0.25, semantic: 0.15, performance: 0.2, appRouter: 0.15, image: 0.15, accessibility: 0.1, }; const overallScore = Math.round(metaTagsScore * weights.metaTags + semanticScore * weights.semantic + performanceScore * weights.performance + appRouterScore * weights.appRouter + imageScore * weights.image + accessibilityScore * weights.accessibility); return { overallScore, categoryScores: { metaTags: metaTagsScore, semanticStructure: semanticScore, performance: performanceScore, appRouter: appRouterScore, imageOptimization: imageScore, accessibility: accessibilityScore, }, criticalIssues, }; } /** * Get Next.js specific recommendations */ getNextJSRecommendations() { const appRouterAnalyzer = new appRouterAnalyzer_1.AppRouterAnalyzer(this.pageComponents, this.components); const performanceAnalyzer = new performanceAnalyzer_1.PerformanceAnalyzer(this.pageComponents, this.components); const imageOptimizationAnalyzer = new imageOptimizationAnalyzer_1.ImageOptimizationAnalyzer(this.pageComponents, this.components); const middlewareAnalyzer = new middlewareAnalyzer_1.MiddlewareAnalyzer(this.components); const bundleAnalyzer = new bundleAnalyzer_1.BundleAnalyzer(this.pageComponents, this.components); const appRouterSuggestions = appRouterAnalyzer.getAppRouterImprovementSuggestions(); return { appRouterMigration: appRouterSuggestions.general, imageOptimization: imageOptimizationAnalyzer.getImageOptimizationSuggestions(), performanceOptimization: [ ...performanceAnalyzer.getPerformanceImprovementSuggestions(), ...bundleAnalyzer.getBundleOptimizationSuggestions(), ], middlewareOptimization: middlewareAnalyzer.getMiddlewareImprovementSuggestions(), }; } /** * Get all page components detected during analysis */ getPageComponents() { return this.pageComponents; } /** * Get all routes detected during analysis */ getRoutes() { return this.routes; } /** * Get detailed meta tag analysis for a specific page */ getPageMetaTags(pagePath) { return this.metaTags.get(pagePath); } /** * Get structured data for a specific page */ getPageStructuredData(pagePath) { return this.structuredData.get(pagePath); } /** * Check if a component is a page component */ isPageComponent(component) { return componentUtils_1.ComponentUtils.isPageComponent(component); } /** * Get all meta tags analysis results */ getAllMetaTags() { return this.metaTags; } /** * Get all structured data analysis results */ getAllStructuredData() { return this.structuredData; } /** * Get performance insights for Core Web Vitals */ getCoreWebVitalsInsights() { const performanceAnalyzer = new performanceAnalyzer_1.PerformanceAnalyzer(this.pageComponents, this.components); const analysis = performanceAnalyzer.analyzeCoreWebVitals(); const lcpIssues = analysis.potentialIssues.filter((issue) => issue.type === "LCP").length; const clsIssues = analysis.potentialIssues.filter((issue) => issue.type === "CLS").length; const fidIssues = analysis.potentialIssues.filter((issue) => issue.type === "FID" || issue.type === "INP").length; const recommendations = [ ...performanceAnalyzer.getPerformanceImprovementSuggestions(), ...(lcpIssues > 0 ? ["Optimize Largest Contentful Paint (LCP) by improving image loading"] : []), ...(clsIssues > 0 ? ["Reduce Cumulative Layout Shift (CLS) by adding image dimensions"] : []), ...(fidIssues > 0 ? ["Improve First Input Delay (FID) by optimizing JavaScript execution"] : []), ]; return { lcpIssues, clsIssues, fidIssues, recommendations, }; } /** * Get bundle optimization insights */ getBundleOptimizationInsights() { const bundleAnalyzer = new bundleAnalyzer_1.BundleAnalyzer(this.pageComponents, this.components); const analysis = bundleAnalyzer.analyzeBundleOptimization(); return { heavyImports: analysis.statistics.totalHeavyImports, criticalResources: analysis.statistics.criticalResourcesCount, optimizationOpportunities: analysis.heavyImports .filter((hi) => hi.potentialImpact === "high") .map((hi) => `Consider dynamic import for ${hi.importPath}`), recommendations: bundleAnalyzer.getBundleOptimizationSuggestions(), }; } getAppRouterReadiness() { const appRouterAnalyzer = new appRouterAnalyzer_1.AppRouterAnalyzer(this.pageComponents, this.components); const seoReadiness = appRouterAnalyzer.analyzeSEOReadiness(); const suggestions = appRouterAnalyzer.getAppRouterImprovementSuggestions(); const hasAppRouter = this.components.some((component) => component.fullPath.includes("/app/") || component.fullPath.includes("\\app\\")); const missingFeatures = []; if (suggestions.specialFiles.length > 0) { missingFeatures.push("Missing special files (layout, error, loading)"); } if (suggestions.layouts.length > 0) { missingFeatures.push("Layout hierarchy issues"); } return { isUsingAppRouter: hasAppRouter, readinessScore: seoReadiness.score, missingFeatures, recommendations: [ ...suggestions.general, ...suggestions.specialFiles.slice(0, 3), // Top 3 suggestions ...suggestions.layouts.slice(0, 2), // Top 2 layout suggestions ], }; } } exports.SEOAnalyzer = SEOAnalyzer;