sicua
Version:
A tool for analyzing project structure and dependencies
410 lines (409 loc) • 19.6 kB
JavaScript
"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;