sicua
Version:
A tool for analyzing project structure and dependencies
420 lines (419 loc) • 17.3 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.PerformanceUtils = void 0;
const typescript_1 = __importDefault(require("typescript"));
const jsxUtils_1 = require("./jsxUtils");
/**
* Utility functions for performance-related SEO analysis
*/
class PerformanceUtils {
/**
* Detects dynamic imports in a component
*/
static findDynamicImports(sourceFile) {
const dynamicImports = [];
const visitNode = (node) => {
// Check for React.lazy()
if (typescript_1.default.isCallExpression(node)) {
const expression = node.expression;
// React.lazy(() => import('...'))
if (typescript_1.default.isPropertyAccessExpression(expression) &&
typescript_1.default.isIdentifier(expression.expression) &&
expression.expression.text === "React" &&
expression.name.text === "lazy") {
const lazyArg = node.arguments[0];
if (typescript_1.default.isArrowFunction(lazyArg)) {
const importCall = this.findImportInExpression(lazyArg.body);
if (importCall) {
dynamicImports.push({
importPath: importCall,
isNextDynamic: false,
hasSSR: true, // React.lazy defaults to SSR
hasLoading: false,
});
}
}
}
// dynamic() from next/dynamic
if (typescript_1.default.isIdentifier(expression) && expression.text === "dynamic") {
const dynamicArg = node.arguments[0];
const optionsArg = node.arguments[1];
let importPath = "";
let hasSSR = true; // default
let hasLoading = false;
// Extract import path
if (typescript_1.default.isArrowFunction(dynamicArg)) {
const importCall = this.findImportInExpression(dynamicArg.body);
if (importCall) {
importPath = importCall;
}
}
// Check options
if (optionsArg && typescript_1.default.isObjectLiteralExpression(optionsArg)) {
optionsArg.properties.forEach((prop) => {
if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name)) {
if (prop.name.text === "ssr") {
hasSSR = prop.initializer.kind === typescript_1.default.SyntaxKind.TrueKeyword;
}
if (prop.name.text === "loading") {
hasLoading = true;
}
}
});
}
if (importPath) {
dynamicImports.push({
importPath,
isNextDynamic: true,
hasSSR,
hasLoading,
});
}
}
}
typescript_1.default.forEachChild(node, visitNode);
};
visitNode(sourceFile);
return dynamicImports;
}
/**
* Helper to find import() calls in expressions
*/
static findImportInExpression(expression) {
if (typescript_1.default.isCallExpression(expression) &&
expression.expression.kind === typescript_1.default.SyntaxKind.ImportKeyword) {
const importArg = expression.arguments[0];
if (typescript_1.default.isStringLiteral(importArg)) {
return importArg.text;
}
}
if (typescript_1.default.isBlock(expression)) {
for (const statement of expression.statements) {
if (typescript_1.default.isReturnStatement(statement) && statement.expression) {
return this.findImportInExpression(statement.expression);
}
}
}
return null;
}
/**
* Analyzes static imports that might impact performance
*/
static analyzeStaticImports(sourceFile) {
const imports = [];
// Known large libraries that should be dynamically imported
const largeLibraries = [
"lodash",
"moment",
"chart.js",
"three",
"@tensorflow/tfjs",
"monaco-editor",
"codemirror",
"highlight.js",
"prismjs",
"pdf-lib",
"fabric",
];
const visitNode = (node) => {
if (typescript_1.default.isImportDeclaration(node) &&
typescript_1.default.isStringLiteral(node.moduleSpecifier)) {
const importPath = node.moduleSpecifier.text;
const isLibrary = !importPath.startsWith(".") && !importPath.startsWith("/");
const isLargeLibrary = largeLibraries.some((lib) => importPath === lib || importPath.startsWith(`${lib}/`));
const isImageImport = /\.(jpg|jpeg|png|gif|svg|webp)$/.test(importPath);
let importType = "side-effect";
if (node.importClause) {
if (node.importClause.name) {
importType = "default";
}
else if (node.importClause.namedBindings) {
if (typescript_1.default.isNamespaceImport(node.importClause.namedBindings)) {
importType = "namespace";
}
else {
importType = "named";
}
}
}
imports.push({
importPath,
isLibrary,
isLargeLibrary,
isImageImport,
importType,
});
}
typescript_1.default.forEachChild(node, visitNode);
};
visitNode(sourceFile);
return imports;
}
/**
* Detects potential Core Web Vitals issues in JSX
*/
static detectCWVIssues(sourceFile) {
const issues = [];
const visitNode = (node) => {
if (typescript_1.default.isJsxElement(node) || typescript_1.default.isJsxSelfClosingElement(node)) {
const tagName = jsxUtils_1.JsxUtils.getTagName(node).toLowerCase();
const issues_found = this.analyzeElementForCWV(node, tagName);
issues.push(...issues_found);
}
typescript_1.default.forEachChild(node, visitNode);
};
visitNode(sourceFile);
return issues;
}
/**
* Analyze a JSX element for potential CWV issues
*/
static analyzeElementForCWV(node, tagName) {
const issues = [];
const location = `Line ${node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line +
1}`;
// LCP (Largest Contentful Paint) issues
if (tagName === "img") {
const src = jsxUtils_1.JsxUtils.getAttribute(node, "src");
const width = jsxUtils_1.JsxUtils.getAttribute(node, "width");
const height = jsxUtils_1.JsxUtils.getAttribute(node, "height");
const priority = jsxUtils_1.JsxUtils.getAttribute(node, "priority");
// Missing dimensions can cause CLS
if (!width || !height) {
issues.push({
type: "CLS",
severity: "medium",
description: "Image missing width or height attributes can cause layout shift",
location,
element: "img",
});
}
// Large images without priority
if (src && !priority && this.isLikelyAboveFold(node)) {
issues.push({
type: "LCP",
severity: "high",
description: "Above-the-fold image should have priority attribute for better LCP",
location,
element: "img",
});
}
}
// Next.js Image component analysis
if (tagName === "Image") {
const priority = jsxUtils_1.JsxUtils.getAttribute(node, "priority");
const placeholder = jsxUtils_1.JsxUtils.getAttribute(node, "placeholder");
const sizes = jsxUtils_1.JsxUtils.getAttribute(node, "sizes");
if (!priority && this.isLikelyAboveFold(node)) {
issues.push({
type: "LCP",
severity: "high",
description: "Above-the-fold Next.js Image should have priority prop",
location,
element: "Image",
});
}
if (!sizes) {
issues.push({
type: "LCP",
severity: "medium",
description: "Next.js Image missing sizes prop may cause suboptimal loading",
location,
element: "Image",
});
}
}
// FID/INP issues - large click handlers
const onClick = jsxUtils_1.JsxUtils.getAttribute(node, "onClick");
if (onClick && this.isComplexEventHandler(node)) {
issues.push({
type: "INP",
severity: "medium",
description: "Complex event handler may impact interaction responsiveness",
location,
element: tagName,
});
}
// CLS issues - elements without dimensions
if (["iframe", "embed", "object"].includes(tagName)) {
const width = jsxUtils_1.JsxUtils.getAttribute(node, "width");
const height = jsxUtils_1.JsxUtils.getAttribute(node, "height");
if (!width || !height) {
issues.push({
type: "CLS",
severity: "high",
description: `${tagName} without dimensions can cause significant layout shift`,
location,
element: tagName,
});
}
}
// Font loading issues
if (tagName === "link") {
const rel = jsxUtils_1.JsxUtils.getAttribute(node, "rel");
const href = jsxUtils_1.JsxUtils.getAttribute(node, "href");
if (rel === "stylesheet" &&
href &&
href.includes("fonts.googleapis.com")) {
const preconnect = this.hasPreconnectForGoogleFonts(node);
if (!preconnect) {
issues.push({
type: "LCP",
severity: "medium",
description: "Google Fonts loading without preconnect may impact LCP",
location,
element: "link",
});
}
}
}
return issues;
}
/**
* Determines if an element is likely above the fold
*/
static isLikelyAboveFold(node) {
// Simple heuristic: check if it's in the first few elements of the component
// In a real implementation, this could be more sophisticated
const sourceFile = node.getSourceFile();
const componentStart = this.findComponentStart(sourceFile);
const elementPosition = node.getStart();
// If element appears within first 20% of component, consider it above fold
if (componentStart) {
const componentLength = sourceFile.getEnd() - componentStart;
const elementOffset = elementPosition - componentStart;
return elementOffset / componentLength < 0.2;
}
return false;
}
/**
* Find the start of the main component in the source file
*/
static findComponentStart(sourceFile) {
let componentStart = null;
const visitNode = (node) => {
// Look for function components or class components
if ((typescript_1.default.isFunctionDeclaration(node) ||
typescript_1.default.isArrowFunction(node) ||
typescript_1.default.isFunctionExpression(node)) &&
!componentStart) {
// Check if this function returns JSX
const hasJSXReturn = this.functionReturnsJSX(node);
if (hasJSXReturn) {
componentStart = node.getStart();
}
}
if (!componentStart) {
typescript_1.default.forEachChild(node, visitNode);
}
};
visitNode(sourceFile);
return componentStart;
}
/**
* Check if a function returns JSX
*/
static functionReturnsJSX(node) {
const body = node.body;
if (!body) {
return false;
}
if (typescript_1.default.isBlock(body)) {
// Look for return statements with JSX
return body.statements.some((statement) => {
if (typescript_1.default.isReturnStatement(statement) && statement.expression) {
return this.isJSXExpression(statement.expression);
}
return false;
});
}
else {
// Arrow function with expression body
return this.isJSXExpression(body);
}
}
/**
* Check if an expression is JSX
*/
static isJSXExpression(expression) {
return (typescript_1.default.isJsxElement(expression) ||
typescript_1.default.isJsxSelfClosingElement(expression) ||
typescript_1.default.isJsxFragment(expression));
}
/**
* Check if an event handler is complex (potential performance issue)
*/
static isComplexEventHandler(node) {
// This is a simplified check - in practice, you'd analyze the handler function
// For now, just check if there are multiple event handlers or complex props
const attributes = typescript_1.default.isJsxElement(node)
? node.openingElement.attributes.properties
: node.attributes.properties;
const eventHandlers = attributes.filter((attr) => typescript_1.default.isJsxAttribute(attr) && attr.name.getText().startsWith("on"));
return eventHandlers.length > 2; // Arbitrary threshold
}
/**
* Check if there's a preconnect for Google Fonts
*/
static hasPreconnectForGoogleFonts(node) {
// This would need to check the document head for preconnect links
// For static analysis, we can't fully determine this, so we return false
// to encourage adding preconnect links
return false;
}
/**
* Analyze bundle impact of imports
*/
static analyzeBundleImpact(imports) {
const heavyImports = [];
const treeshakingIssues = [];
imports.forEach((imp) => {
if (imp.isLargeLibrary) {
heavyImports.push(imp.importPath);
}
// Check for imports that prevent tree shaking
if (imp.importType === "namespace" && imp.isLibrary) {
treeshakingIssues.push(imp.importPath);
}
});
// Calculate bundle score (0-100)
let bundleScore = 100;
bundleScore -= heavyImports.length * 15; // -15 per heavy import
bundleScore -= treeshakingIssues.length * 10; // -10 per tree shaking issue
return {
heavyImports,
treeshakingIssues,
bundleScore: Math.max(0, bundleScore),
};
}
/**
* Check if component is server or client component
*/
static isServerComponent(sourceFile) {
let hasUseClientDirective = false;
let hasUseServerDirective = false;
const visitNode = (node) => {
if (typescript_1.default.isStringLiteral(node)) {
if (node.text === "use client") {
hasUseClientDirective = true;
}
if (node.text === "use server") {
hasUseServerDirective = true;
}
}
typescript_1.default.forEachChild(node, visitNode);
};
visitNode(sourceFile);
// In App Router, components are server components by default unless marked with 'use client'
const isServerComponent = !hasUseClientDirective;
return {
isServerComponent,
hasUseClientDirective,
hasUseServerDirective,
};
}
}
exports.PerformanceUtils = PerformanceUtils;