sicua
Version:
A tool for analyzing project structure and dependencies
260 lines (259 loc) • 10.6 kB
JavaScript
;
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);
}
});
}