UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

407 lines (406 loc) 16.3 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentReferenceResolver = void 0; const path = __importStar(require("path")); const fs = __importStar(require("fs")); const parser_1 = require("@babel/parser"); const traverse_1 = __importDefault(require("@babel/traverse")); const t = __importStar(require("@babel/types")); const NodeModuleDetector_1 = require("../utils/NodeModuleDetector"); /** * Resolves component references to actual component files and handles external dependencies */ class ComponentReferenceResolver { constructor(projectRoot, srcDirectory, components) { this.nodeModuleDetector = new NodeModuleDetector_1.NodeModuleDetector(projectRoot, srcDirectory); this.componentMap = new Map(); this.importMap = new Map(); this.resolveCache = new Map(); this.buildComponentMap(components); this.buildImportMap(components); } /** * Resolves a component reference to a ComponentFlowNode - FIXED VERSION */ resolveComponentReference(reference, currentFilePath) { const cacheKey = `${currentFilePath}:${reference.name}`; if (this.resolveCache.has(cacheKey)) { return this.resolveCache.get(cacheKey) || null; } const resolved = this.performResolution(reference, currentFilePath); this.resolveCache.set(cacheKey, resolved); return resolved; } /** * Resolves multiple component references */ resolveMultipleReferences(references, currentFilePath) { const resolved = []; for (const reference of references) { const node = this.resolveComponentReference(reference, currentFilePath); if (node) { resolved.push(node); } } return resolved; } /** * Gets all imports for a specific file by parsing the actual file content */ getFileImports(filePath) { // Check cache first if (this.importMap.has(filePath)) { return this.importMap.get(filePath); } // Parse file directly if not in cache try { const content = fs.readFileSync(filePath, "utf-8"); const imports = this.parseImportsFromContent(content); this.importMap.set(filePath, imports); return imports; } catch (error) { console.warn(`Failed to read file ${filePath}:`, error); return []; } } /** * Parses imports directly from file content using AST */ parseImportsFromContent(content) { const imports = []; try { const ast = (0, parser_1.parse)(content, { sourceType: "module", plugins: [ "jsx", "typescript", "decorators", "classProperties", "exportDefaultFrom", "exportNamespaceFrom", "dynamicImport", ], errorRecovery: true, }); (0, traverse_1.default)(ast, { ImportDeclaration: (path) => { const source = path.node.source.value; for (const specifier of path.node.specifiers) { if (t.isImportDefaultSpecifier(specifier)) { imports.push({ name: specifier.local.name, source, isDefault: true, isNamespace: false, localName: specifier.local.name, }); } else if (t.isImportNamespaceSpecifier(specifier)) { imports.push({ name: "*", source, isDefault: false, isNamespace: true, localName: specifier.local.name, }); } else if (t.isImportSpecifier(specifier)) { const importedName = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value; imports.push({ name: importedName, source, isDefault: false, isNamespace: false, localName: specifier.local.name, }); } } }, }); } catch (error) { console.warn("Failed to parse imports from content:", error); } return imports; } /** * Checks if a component name is externally imported in a file */ isExternallyImported(componentName, filePath) { const imports = this.getFileImports(filePath); for (const importRef of imports) { if ((importRef.localName === componentName || importRef.name === componentName) && this.nodeModuleDetector.isExternalComponent(importRef.source, filePath)) { return true; } } return false; } /** * Resolves the actual file path for a component */ resolveComponentFilePath(componentName, currentFilePath) { // Check imports to resolve the component const imports = this.getFileImports(currentFilePath); const importRef = this.findComponentInImports(componentName, imports); if (!importRef) { // If no import found, check if it's directly mapped in our component map const directMatch = this.findDirectComponentMatch(componentName); return directMatch ? directMatch.fullPath : null; } // Skip external imports if (this.nodeModuleDetector.isExternalComponent(importRef.source, currentFilePath)) { return null; } // Resolve the import path for internal components const resolvedPath = this.nodeModuleDetector.resolveInternalComponentPath(importRef.source, currentFilePath); if (resolvedPath) { return resolvedPath; } // Try to find by import source in component map return this.findComponentByImportSource(importRef.source); } /** * Extracts the actual component name from a file path - NEW METHOD */ extractComponentNameFromFile(filePath) { try { // Try to extract from file content first const content = fs.readFileSync(filePath, "utf-8"); const componentName = this.extractComponentNameFromContent(content); if (componentName) { return componentName; } } catch (error) { // Fall back to filename-based extraction if file read fails } // Fallback: extract from filename const fileName = path.basename(filePath, path.extname(filePath)); // Handle common patterns if (fileName === "index") { // For index files, use the directory name const dirName = path.basename(path.dirname(filePath)); return this.toPascalCase(dirName); } return this.toPascalCase(fileName); } /** * Extracts component name from file content using AST - NEW METHOD */ extractComponentNameFromContent(content) { try { const ast = (0, parser_1.parse)(content, { sourceType: "module", plugins: [ "jsx", "typescript", "decorators", "classProperties", "exportDefaultFrom", "exportNamespaceFrom", "dynamicImport", ], errorRecovery: true, }); let componentName = null; (0, traverse_1.default)(ast, { // Check for default export function declarations ExportDefaultDeclaration: (path) => { const declaration = path.node.declaration; if (t.isFunctionDeclaration(declaration) && declaration.id) { componentName = declaration.id.name; } else if (t.isIdentifier(declaration)) { componentName = declaration.name; } }, // Check for function declarations that look like components FunctionDeclaration: (path) => { const func = path.node; if (func.id && this.isLikelyReactComponent(func.id.name)) { componentName = func.id.name; } }, // Check for variable declarations with arrow functions VariableDeclarator: (path) => { if (t.isIdentifier(path.node.id) && (t.isArrowFunctionExpression(path.node.init) || t.isFunctionExpression(path.node.init)) && this.isLikelyReactComponent(path.node.id.name)) { componentName = path.node.id.name; } }, }); return componentName; } catch (error) { return null; } } /** * Checks if a name looks like a React component - NEW METHOD */ isLikelyReactComponent(name) { // React components should start with uppercase and not be common non-component names const nonComponentNames = ["App", "Document", "Error", "Layout"]; return (name.charAt(0) === name.charAt(0).toUpperCase() && !nonComponentNames.includes(name)); } /** * Converts string to PascalCase - NEW METHOD */ toPascalCase(str) { return str .split(/[-_\s]+/) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(""); } /** * Performs the actual resolution logic - FIXED VERSION */ performResolution(reference, currentFilePath) { const componentName = reference.name; // Skip native HTML elements if (this.nodeModuleDetector.isNativeHTMLElement(componentName)) { return null; } // Skip React built-ins if (this.nodeModuleDetector.isReactBuiltIn(componentName)) { return null; } // Check if it's an external component if (this.isExternallyImported(componentName, currentFilePath)) { return { componentName, // Use the actual component name, not file path filePath: "", // External components don't have local file paths isExternal: true, conditionalRenders: [], children: [], }; } // Try to resolve internal component const filePath = this.resolveComponentFilePath(componentName, currentFilePath); if (!filePath) { // Check if it might be an external component that we missed const imports = this.getFileImports(currentFilePath); const importRef = this.findComponentInImports(componentName, imports); if (importRef) { // If we found an import but couldn't resolve the path, it's likely external return { componentName, // Use the actual component name filePath: "", isExternal: true, conditionalRenders: [], children: [], }; } // Component not found anywhere return null; } // Use the original component name from the reference, not from file extraction // The reference name (e.g., "Card") is what matters for the flow tree return { componentName: componentName, // Use the original reference name (e.g., "Card") filePath, isExternal: false, conditionalRenders: [], children: [], }; } /** * Builds a map of component names to ComponentRelation objects */ buildComponentMap(components) { for (const component of components) { this.componentMap.set(component.name, component); // Also map by file basename without extension const basename = path.basename(component.fullPath, path.extname(component.fullPath)); if (basename !== component.name) { this.componentMap.set(basename, component); } // Also try to extract actual component name from file try { const actualName = this.extractComponentNameFromFile(component.fullPath); if (actualName && actualName !== component.name) { this.componentMap.set(actualName, component); } } catch (error) { // Ignore errors in component name extraction during mapping } } } /** * Builds a map of file paths to their import references */ buildImportMap(components) { for (const component of components) { if (component.content) { const imports = this.parseImportsFromContent(component.content); this.importMap.set(component.fullPath, imports); } } } /** * Finds a direct component match in the component map */ findDirectComponentMatch(componentName) { return this.componentMap.get(componentName) || null; } /** * Finds a component in the imports list */ findComponentInImports(componentName, imports) { return (imports.find((imp) => imp.localName === componentName || imp.name === componentName || (imp.isNamespace && componentName.startsWith(imp.localName + "."))) || null); } /** * Finds a component by its import source */ findComponentByImportSource(importSource) { for (const [, component] of this.componentMap) { if (component.imports && component.imports.includes(importSource)) { return component.fullPath; } // Check if the import source matches the component's directory structure const relativePath = path.relative(path.dirname(component.fullPath), importSource); if (relativePath === "." || relativePath === "./index") { return component.fullPath; } } return null; } } exports.ComponentReferenceResolver = ComponentReferenceResolver;