UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

514 lines (513 loc) 19.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsxUtils = void 0; const typescript_1 = __importDefault(require("typescript")); const seoRelatedUtils_1 = require("../../../utils/common/seoRelatedUtils"); /** * Utility functions for working with JSX elements in SEO analysis */ class JsxUtils { /** * Gets the tag name of a JSX element */ static getTagName(node) { return seoRelatedUtils_1.SeoRelated.getJsxTagName(node); } /** * Gets the value of a JSX attribute */ static getAttribute(node, attributeName) { const attributes = typescript_1.default.isJsxElement(node) ? node.openingElement.attributes.properties : node.attributes.properties; for (const attr of attributes) { if (!typescript_1.default.isJsxAttribute(attr)) continue; if (attr.name.getText() === attributeName) { const initializer = attr.initializer; if (initializer) { if (typescript_1.default.isStringLiteral(initializer)) { return initializer.text; } else if (typescript_1.default.isJsxExpression(initializer) && initializer.expression) { return seoRelatedUtils_1.SeoRelated.extractStaticValue(initializer.expression); } } return ""; // attribute exists but has no value } } return null; // attribute doesn't exist } /** * Gets all attributes from a JSX element */ static getAllAttributes(node) { const attributes = typescript_1.default.isJsxElement(node) ? node.openingElement.attributes.properties : node.attributes.properties; const result = []; for (const attr of attributes) { if (!typescript_1.default.isJsxAttribute(attr)) continue; const name = attr.name.getText(); const initializer = attr.initializer; let value = null; if (initializer) { if (typescript_1.default.isStringLiteral(initializer)) { value = initializer.text; } else if (typescript_1.default.isJsxExpression(initializer) && initializer.expression) { value = seoRelatedUtils_1.SeoRelated.extractStaticValue(initializer.expression); } } else { value = ""; // attribute exists but has no value } result.push({ name, value }); } return result; } /** * Checks if a node is a Link component (Next.js, React Router, etc.) */ static isLinkComponent(node) { if (!typescript_1.default.isJsxElement(node) && !typescript_1.default.isJsxSelfClosingElement(node)) return false; const tagName = typescript_1.default.isJsxElement(node) ? node.openingElement.tagName.getText() : node.tagName.getText(); return (tagName === "Link" || tagName === "NavLink" || tagName === "RouterLink"); } /** * Checks if a node is an anchor tag */ static isAnchorTag(node) { if (!typescript_1.default.isJsxElement(node) && !typescript_1.default.isJsxSelfClosingElement(node)) return false; const tagName = typescript_1.default.isJsxElement(node) ? node.openingElement.tagName.getText() : node.tagName.getText(); return tagName.toLowerCase() === "a"; } /** * Extracts link target from Link component or anchor */ static extractLinkTarget(node) { if (!typescript_1.default.isJsxElement(node) && !typescript_1.default.isJsxSelfClosingElement(node)) return null; const attributes = typescript_1.default.isJsxElement(node) ? node.openingElement.attributes.properties : node.attributes.properties; for (const attr of attributes) { if (!typescript_1.default.isJsxAttribute(attr)) continue; if (attr.name.getText() === "to" || attr.name.getText() === "href") { const initializer = attr.initializer; if (initializer) { if (typescript_1.default.isStringLiteral(initializer)) { return initializer.text; } else if (typescript_1.default.isJsxExpression(initializer) && initializer.expression) { return seoRelatedUtils_1.SeoRelated.extractStaticValue(initializer.expression); } } } } return null; } /** * Checks if a node has accessible text (for SEO and a11y) */ static hasAccessibleText(node) { // Check for text content if (typescript_1.default.isJsxElement(node) && node.children) { const hasText = node.children.some((child) => typescript_1.default.isJsxText(child) && child.text.trim().length > 0); if (hasText) return true; } // Check for aria-label const ariaLabel = this.getAttribute(node, "aria-label"); if (ariaLabel) return true; // Check for aria-labelledby const ariaLabelledBy = this.getAttribute(node, "aria-labelledby"); if (ariaLabelledBy) return true; return false; } /** * Checks if a node is an icon-only button (accessibility issue for SEO) */ static isIconOnlyButton(node) { // Check if button only contains icon-like elements if (typescript_1.default.isJsxElement(node)) { const hasOnlyIconChildren = node.children.every((child) => { if (typescript_1.default.isJsxElement(child) || typescript_1.default.isJsxSelfClosingElement(child)) { const tagName = this.getTagName(child).toLowerCase(); return tagName === "svg" || tagName === "img" || tagName === "i"; } if (typescript_1.default.isJsxText(child)) { return child.text.trim().length === 0; } return true; }); return hasOnlyIconChildren; } return false; } /** * Extracts text content from JSX elements */ static extractTextContent(node) { let text = ""; const visit = (node) => { if (typescript_1.default.isJsxText(node)) { text += node.text.trim() + " "; } typescript_1.default.forEachChild(node, visit); }; visit(node); return text.trim(); } // ========== PERFORMANCE-RELATED DETECTION METHODS ========== /** * Detects Next.js Image component usage and optimization */ static isNextImageComponent(node) { if (!typescript_1.default.isJsxElement(node) && !typescript_1.default.isJsxSelfClosingElement(node)) { return false; } const tagName = this.getTagName(node); return tagName === "Image"; } /** * Checks if image has performance-critical attributes */ static hasPerformanceOptimizedImage(node) { const tagName = this.getTagName(node).toLowerCase(); const issues = []; let isOptimized = true; const loading = this.getAttribute(node, "loading"); const priority = this.getAttribute(node, "priority"); const width = this.getAttribute(node, "width"); const height = this.getAttribute(node, "height"); const sizes = this.getAttribute(node, "sizes"); const hasLazyLoading = loading === "lazy" || (tagName === "image" && !priority); const hasPriority = priority === "true" || priority === "{true}"; const hasDimensions = !!(width && height); if (tagName === "img") { issues.push("Use Next.js Image component for automatic optimization"); isOptimized = false; } if (!hasDimensions && tagName !== "image") { issues.push("Missing width/height attributes can cause CLS"); isOptimized = false; } if (tagName === "image" && !sizes && !width) { issues.push("Missing sizes prop for responsive images"); } return { isOptimized, issues, hasLazyLoading, hasPriority, hasDimensions, }; } /** * Detects lazy loading opportunities */ static getLazyLoadingInfo(node) { const tagName = this.getTagName(node).toLowerCase(); const loading = this.getAttribute(node, "loading"); const priority = this.getAttribute(node, "priority"); if (tagName === "img") { return { supportsLazyLoading: true, hasLazyLoading: loading === "lazy", method: loading === "lazy" ? "native" : "none", recommendation: loading !== "lazy" ? "Add loading='lazy' for below-fold images" : null, }; } if (tagName === "image") { return { supportsLazyLoading: true, hasLazyLoading: !priority, method: "next-image", recommendation: priority ? "Remove priority for below-fold images" : null, }; } return { supportsLazyLoading: false, hasLazyLoading: false, method: "none", recommendation: null, }; } /** * Detects components that might benefit from lazy loading */ static isHeavyComponent(node) { const tagName = this.getTagName(node); const attributes = this.getAllAttributes(node); const reasons = []; let complexity = "low"; // Check for heavy component patterns const heavyComponentPatterns = [ "Chart", "Graph", "Map", "Editor", "Calendar", "DataTable", "Video", "Canvas", "ThreeJS", "WebGL", "CodeEditor", ]; const isHeavyByName = heavyComponentPatterns.some((pattern) => tagName.includes(pattern)); if (isHeavyByName) { reasons.push(`Component name suggests heavy functionality: ${tagName}`); complexity = "high"; } // Check for many props (might indicate complexity) if (attributes.length > 10) { reasons.push("Component has many props indicating complexity"); complexity = complexity === "high" ? "high" : "medium"; } // Check for data-heavy attributes const dataAttributes = attributes.filter((attr) => attr.name.startsWith("data-") || attr.name.includes("config") || attr.name.includes("options")); if (dataAttributes.length > 3) { reasons.push("Component has many data attributes"); complexity = complexity === "high" ? "high" : "medium"; } return { isHeavy: reasons.length > 0, reasons, complexity, }; } /** * Detects performance-impacting event handlers */ static hasPerformanceIssues(node) { const attributes = this.getAllAttributes(node); const issues = []; // Count event handlers const eventHandlers = attributes.filter((attr) => attr.name.startsWith("on") && attr.name.length > 2); if (eventHandlers.length > 3) { issues.push({ type: "too-many-handlers", description: `Element has ${eventHandlers.length} event handlers`, severity: "medium", }); } // Check for inline functions (simplified detection) eventHandlers.forEach((handler) => { if (handler.value && (handler.value.includes("=>") || handler.value.includes("function"))) { issues.push({ type: "inline-functions", description: `Inline function in ${handler.name} handler`, severity: "low", }); } }); return { hasIssues: issues.length > 0, issues, }; } /** * Detects third-party scripts and their performance impact */ static isThirdPartyScript(node) { const tagName = this.getTagName(node).toLowerCase(); const src = this.getAttribute(node, "src"); const recommendations = []; if (tagName !== "script" || !src) { return { isThirdParty: false, service: null, performanceImpact: "low", recommendations: [], }; } // Common third-party services and their impact const thirdPartyServices = [ { pattern: "google-analytics", name: "Google Analytics", impact: "medium", }, { pattern: "googletagmanager", name: "Google Tag Manager", impact: "high", }, { pattern: "facebook.net", name: "Facebook Pixel", impact: "high", }, { pattern: "doubleclick", name: "Google Ads", impact: "high" }, { pattern: "hotjar", name: "Hotjar", impact: "medium" }, { pattern: "intercom", name: "Intercom", impact: "medium" }, { pattern: "zendesk", name: "Zendesk", impact: "medium" }, { pattern: "stripe", name: "Stripe", impact: "low" }, { pattern: "paypal", name: "PayPal", impact: "medium" }, { pattern: "recaptcha", name: "reCAPTCHA", impact: "medium" }, ]; const matchedService = thirdPartyServices.find((service) => src.toLowerCase().includes(service.pattern)); if (matchedService) { // Generate recommendations based on service type switch (matchedService.impact) { case "high": recommendations.push("Consider loading this script asynchronously"); recommendations.push("Use defer or async attributes to prevent blocking"); recommendations.push("Consider server-side implementation if possible"); break; case "medium": recommendations.push("Add async attribute to prevent blocking"); recommendations.push("Consider conditional loading based on user interaction"); break; case "low": recommendations.push("Ensure script has proper error handling"); break; } return { isThirdParty: true, service: matchedService.name, performanceImpact: matchedService.impact, recommendations, }; } // Check if it's any external script const isExternal = src.startsWith("http") && !src.includes("localhost"); if (isExternal) { recommendations.push("Consider hosting third-party scripts locally"); recommendations.push("Add async or defer attributes"); return { isThirdParty: true, service: "Unknown third-party service", performanceImpact: "medium", recommendations, }; } return { isThirdParty: false, service: null, performanceImpact: "low", recommendations: [], }; } /** * Detects preloading opportunities */ static shouldBePreloaded(node) { const tagName = this.getTagName(node).toLowerCase(); const src = this.getAttribute(node, "src"); const href = this.getAttribute(node, "href"); const rel = this.getAttribute(node, "rel"); // Critical images should be preloaded if ((tagName === "img" || tagName === "image") && src) { const priority = this.getAttribute(node, "priority"); if (priority === "true") { return { shouldPreload: true, resourceType: "image", priority: "high", recommendation: "Add <link rel='preload' as='image'> for this critical image", }; } } // Critical fonts should be preloaded if (tagName === "link" && rel === "stylesheet" && href?.includes("font")) { return { shouldPreload: true, resourceType: "font", priority: "high", recommendation: "Add <link rel='preload' as='font'> for critical fonts", }; } // Critical scripts if (tagName === "script" && src && !this.getAttribute(node, "async") && !this.getAttribute(node, "defer")) { return { shouldPreload: true, resourceType: "script", priority: "medium", recommendation: "Consider preloading or adding async/defer to prevent blocking", }; } return { shouldPreload: false, resourceType: null, priority: "low", recommendation: null, }; } /** * Analyzes Core Web Vitals impact of an element */ static analyzeCoreWebVitalsImpact(node) { const tagName = this.getTagName(node).toLowerCase(); const recommendations = []; let lcpImpact = "none"; let clsImpact = "none"; let fidImpact = "none"; // LCP Impact Analysis if (tagName === "img" || tagName === "image") { const priority = this.getAttribute(node, "priority"); const loading = this.getAttribute(node, "loading"); if (priority === "true") { lcpImpact = "high"; recommendations.push("Ensure this LCP candidate image is optimized"); } else if (loading !== "lazy") { lcpImpact = "medium"; } } // CLS Impact Analysis const width = this.getAttribute(node, "width"); const height = this.getAttribute(node, "height"); if ((tagName === "img" || tagName === "iframe" || tagName === "video") && (!width || !height)) { clsImpact = "high"; recommendations.push("Add explicit dimensions to prevent layout shift"); } // FID Impact Analysis const eventHandlers = this.getAllAttributes(node).filter((attr) => attr.name.startsWith("on")); if (eventHandlers.length > 2) { fidImpact = "medium"; recommendations.push("Optimize event handlers to reduce main thread blocking"); } if (tagName === "script" && !this.getAttribute(node, "async") && !this.getAttribute(node, "defer")) { fidImpact = "high"; recommendations.push("Add async or defer to scripts to improve FID"); } return { lcpImpact, clsImpact, fidImpact, recommendations, }; } } exports.JsxUtils = JsxUtils;