UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

260 lines (259 loc) 10.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateGraphData = generateGraphData; const path = __importStar(require("path")); const graphFormatUtils_1 = require("./graphFormatUtils"); const analysisUtils_1 = require("../utils/common/analysisUtils"); const GENERATOR_VERSION = "2.1.0"; let graphCache = null; /** * Detects file type and metadata from file path and content */ function detectFileMetadata(filePath, component) { const fileType = path.extname(filePath).replace(".", ""); const isComponent = isReactComponent(component, fileType); const isNextRoute = filePath.includes("/app/") && (filePath.includes("/page.") || filePath.includes("/layout.") || filePath.includes("/loading.") || filePath.includes("/error.")); let routeType; if (isNextRoute) { if (filePath.includes("/page.")) routeType = "page"; else if (filePath.includes("/layout.")) routeType = "layout"; else if (filePath.includes("/loading.")) routeType = "loading"; else if (filePath.includes("/error.")) routeType = "error"; else if (filePath.includes("/not-found.")) routeType = "not-found"; } const contentStr = component.content || ""; const hasClientDirective = contentStr.includes('"use client"') || contentStr.includes("'use client'"); const hasServerDirective = contentStr.includes('"use server"') || contentStr.includes("'use server'"); return { fileType, isNextRoute, routeType, isComponent, hasClientDirective, hasServerDirective, }; } /** * Enhanced React component detection */ function isReactComponent(component, fileType) { if (fileType !== "tsx" && fileType !== "jsx") { return false; } const contentStr = component.content || ""; const reactPatterns = [ /export\s+default\s+function\s+\w+/, /export\s+default\s+\w+/, /function\s+\w+\s*\([^)]*\)\s*\{[\s\S]*return\s*\(/, /const\s+\w+\s*=\s*\([^)]*\)\s*=>\s*\{[\s\S]*return\s*\(/, /const\s+\w+\s*=\s*\([^)]*\)\s*=>\s*\(/, /export\s+const\s+\w+\s*=\s*\([^)]*\)\s*=>/, /export\s+function\s+\w+/, ]; const hasReactPattern = reactPatterns.some((pattern) => pattern.test(contentStr)); const hasJSX = /<[A-Z][A-Za-z0-9]*[\s\S]*?>/.test(contentStr) || /<[a-z]+[\s\S]*?>/.test(contentStr); return hasReactPattern && hasJSX; } /** * Generate graph data optimized for Sigma.js visualization of component relationships */ function generateGraphData(components, config) { if (graphCache && isValidCache(components, graphCache)) { return createReturnObject(graphCache); } // Create component map with unique IDs const componentMap = new Map(); const fileToComponentsMap = new Map(); // Build component maps for (const comp of components) { const componentId = (0, analysisUtils_1.generateComponentId)(comp); componentMap.set(componentId, comp); // Group components by file for import resolution const filePath = comp.fullPath; if (!fileToComponentsMap.has(filePath)) { fileToComponentsMap.set(filePath, []); } fileToComponentsMap.get(filePath).push(comp); } const rootComponentSet = new Set(config.rootComponentNames.map((name) => name.toLowerCase())); const componentNodes = []; const componentEdges = new Map(); // Generate component nodes with unique IDs for (const comp of components) { const componentId = (0, analysisUtils_1.generateComponentId)(comp); const metadata = detectFileMetadata(comp.fullPath, comp); const nodeProps = { isNextRoute: metadata.isNextRoute, isComponent: metadata.isComponent, routeType: metadata.routeType, }; const node = { id: componentId, label: comp.name, // Keep original component name as label fullPath: comp.fullPath, directory: comp.directory, fileType: metadata.fileType, isNextRoute: metadata.isNextRoute, routeType: metadata.routeType, isComponent: metadata.isComponent, hasClientDirective: metadata.hasClientDirective, hasServerDirective: metadata.hasServerDirective, x: Math.random() * 100, y: Math.random() * 100, size: (0, graphFormatUtils_1.getNodeSize)(nodeProps), color: (0, graphFormatUtils_1.getNodeColor)(nodeProps), }; componentNodes.push(node); } // Update cache graphCache = { componentNodes, componentEdges, componentMap, fileToComponentsMap, rootComponentSet, }; return createReturnObject(graphCache); } function createReturnObject(cache) { const loadComponentDetails = (componentName) => { // Handle both unique IDs and original component names const componentId = componentName.includes("#") ? componentName : Array.from(cache.componentMap.keys()).find((id) => id.endsWith(`#${componentName}`)); if (!componentId || cache.componentEdges.has(componentId)) { return; } const component = cache.componentMap.get(componentId); if (component) { const edges = []; createComponentEdges(component, componentId, cache.componentMap, cache.fileToComponentsMap, edges); if (cache.rootComponentSet.has(component.name.toLowerCase())) { createRootEdges(Array.from(cache.componentMap.entries()), component, componentId, edges); } cache.componentEdges.set(componentId, edges); } }; const getSigmaData = () => { // Ensure all components have their edges loaded cache.componentMap.forEach((_, componentId) => loadComponentDetails(componentId)); return { nodes: cache.componentNodes, edges: Array.from(cache.componentEdges.values()).flat(), version: GENERATOR_VERSION, }; }; return { getSigmaData, loadComponentDetails, }; } function isValidCache(components, cache) { return cache.componentNodes.length === components.length; } /** * Create edges between components based on imports - Enhanced for multiple components per file */ function createComponentEdges(component, componentId, componentMap, fileToComponentsMap, edges) { component.imports.forEach((importPath) => { const targetComponents = findComponentsByImportPath(importPath, componentMap, fileToComponentsMap); targetComponents.forEach((targetComponent) => { const targetComponentId = (0, analysisUtils_1.generateComponentId)(targetComponent); const edgeType = importPath.includes("lazy") ? "dynamic" : "import"; const edge = { id: `${componentId}-${targetComponentId}`, source: componentId, target: targetComponentId, type: edgeType, size: (0, graphFormatUtils_1.getEdgeSize)(edgeType), color: (0, graphFormatUtils_1.getEdgeColor)(edgeType), }; edges.push(edge); }); }); } /** * Enhanced component finding by import path - handles multiple components per file */ function findComponentsByImportPath(importPath, componentMap, fileToComponentsMap) { const results = []; const importName = path.basename(importPath, path.extname(importPath)); // Strategy 1: Direct component name match (for named imports) const directMatch = Array.from(componentMap.values()).find((c) => c.name === importName); if (directMatch) { results.push(directMatch); return results; } // Strategy 2: Match by file basename - return all components in that file const matchingFile = Array.from(fileToComponentsMap.entries()).find(([filePath, _]) => { const fileName = path.basename(filePath, path.extname(filePath)); return fileName === importName; }); if (matchingFile) { results.push(...matchingFile[1]); return results; } // Strategy 3: Match by partial path (for relative imports) if (importPath.startsWith(".")) { const matchingComponents = Array.from(componentMap.values()).filter((c) => c.fullPath.includes(importName) || c.name === importName); results.push(...matchingComponents); } // Strategy 4: Match by export name (for named imports) if (results.length === 0) { const exportMatches = Array.from(componentMap.values()).filter((c) => c.exports.some((exp) => exp === importName)); results.push(...exportMatches); } return results; } /** * Create edges for root components - Updated for unique IDs */ function createRootEdges(componentEntries, rootComponent, rootComponentId, edges) { componentEntries.forEach(([componentId, comp]) => { if (comp !== rootComponent && comp.directory === "") { const edgeType = "import"; const edge = { id: `${rootComponentId}-${componentId}`, source: rootComponentId, target: componentId, type: edgeType, size: (0, graphFormatUtils_1.getEdgeSize)(edgeType), color: (0, graphFormatUtils_1.getEdgeColor)(edgeType), }; edges.push(edge); } }); }