From b9b0524bbd107919f9cea4c97e4f6ff524d0b109 Mon Sep 17 00:00:00 2001 From: Rui Martins Date: Thu, 30 Jan 2025 16:13:23 +0000 Subject: [PATCH 1/5] feat: implement new list output format --- README.md | 1 + main.js | 124 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ccbb8f2..002785c 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ react-component-usage-finder -c ComponentName - `-c, --component`: Name of the component to analyze (required) - `-d, --directory`: Root directory to scan (defaults to current directory) +- `-m, --markdown`: Output as markdown list - `-h, --help`: Show help ## Output Example diff --git a/main.js b/main.js index 38886ea..08da2f7 100755 --- a/main.js +++ b/main.js @@ -177,6 +177,69 @@ class TreeFormatter { 0 ); } + + getMarkdownList(hierarchy, parentPath = "") { + let result = ""; + const currentPath = parentPath + ? `${parentPath} -> ${hierarchy.name}` + : hierarchy.name; + + // If this is a leaf node (no further usages), add a list item + if (!hierarchy.usedIn || hierarchy.usedIn.length === 0) { + result += `- ${currentPath}\n`; + } + + // Recursively process children + if (hierarchy.usedIn && hierarchy.usedIn.length > 0) { + hierarchy.usedIn.forEach((child) => { + result += this.getMarkdownList(child, currentPath); + }); + } + + return result; + } + + getStatistics(hierarchy) { + const stats = { + totalComponents: new Set(), + maxDepth: 0, + leafNodes: 0, + uniqueFiles: new Set(), + }; + + this.calculateStats(hierarchy, 1, stats); + + return { + totalComponents: stats.totalComponents.size, + maxDepth: stats.maxDepth, + leafNodes: stats.leafNodes, + uniqueFiles: stats.uniqueFiles.size, + }; + } + + calculateStats(node, depth, stats) { + stats.totalComponents.add(node.name); + stats.maxDepth = Math.max(stats.maxDepth, depth); + if (node.definedIn) { + stats.uniqueFiles.add(node.definedIn); + } + + if (!node.usedIn || node.usedIn.length === 0) { + stats.leafNodes++; + } else { + node.usedIn.forEach((child) => { + this.calculateStats(child, depth + 1, stats); + }); + } + } + + formatSummary(stats) { + return `Summary: +• Total unique components: ${stats.totalComponents} +• Maximum depth: ${stats.maxDepth} +• Leaf usages: ${stats.leafNodes} +• Files involved: ${stats.uniqueFiles}\n`; + } } const argv = yargs(hideBin(process.argv)) @@ -193,6 +256,12 @@ const argv = yargs(hideBin(process.argv)) demandOption: true, type: "string", }) + .option("m", { + alias: "markdown", + describe: "Output as markdown list", + type: "boolean", + default: false, + }) .help("h") .alias("h", "help").argv; @@ -200,26 +269,61 @@ async function main() { try { const analyzer = new ComponentAnalyzer(argv.directory); const hierarchy = await analyzer.analyze(argv.component); - - console.log("\nComponent Usage Tree:"); const formatter = new TreeFormatter(); - const treeOutput = formatter.formatHierarchy(hierarchy); - console.log(treeOutput); - const totalUsages = formatter.getLeafUsages(hierarchy); - console.log(`\nTotal leaf usages found: ${totalUsages}`); + // Calculate and display statistics + const stats = formatter.getStatistics(hierarchy); + console.log("\n" + formatter.formatSummary(stats)); + + // Prompt for viewing the list + const viewResponse = await prompts([ + { + type: "confirm", + name: "viewList", + message: "Would you like to see the detailed usage list?", + initial: true, + }, + { + type: (prev) => (prev ? "select" : null), + name: "format", + message: "Select output format:", + choices: [ + { title: "Tree view", value: "tree" }, + { title: "Markdown list", value: "markdown" }, + ], + initial: 0, + }, + ]); + + let output = ""; + if (viewResponse.viewList) { + if (viewResponse.format === "markdown") { + output = formatter.getMarkdownList(hierarchy); + console.log("\nComponent Usage List:"); + } else { + output = formatter.formatHierarchy(hierarchy); + console.log("\nComponent Usage Tree:"); + } + console.log(output); + } - const response = await prompts({ + const clipboardResponse = await prompts({ type: "confirm", name: "copyToClipboard", message: "Copy output to clipboard?", initial: false, }); - if (response.copyToClipboard) { - const markdownOutput = `\`\`\`text\n${treeOutput}\`\`\``; + if (clipboardResponse.copyToClipboard) { + const markdownOutput = + viewResponse.format === "markdown" + ? `${formatter.formatSummary(stats)}\n${output}` + : `\`\`\`text\n${formatter.formatSummary(stats)}\n${output}\`\`\``; await clipboardy.write(markdownOutput); - console.log("\n✓ Copied to clipboard in markdown format"); + console.log( + "\n✓ Copied to clipboard" + + (viewResponse.format === "markdown" ? "" : " in markdown format") + ); } } catch (error) { console.error("Error:", error); From 8028bac12d8ab577485a941427c725edfd31a488 Mon Sep 17 00:00:00 2001 From: Rui Martins Date: Tue, 11 Feb 2025 18:39:56 +0000 Subject: [PATCH 2/5] feat: implement file usage --- main.js | 497 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 297 insertions(+), 200 deletions(-) diff --git a/main.js b/main.js index 08da2f7..5cecb32 100755 --- a/main.js +++ b/main.js @@ -12,234 +12,333 @@ import clipboardy from "clipboardy"; const traverse = _traverse.default; -class ComponentAnalyzer { - constructor(rootDir) { - this.rootDir = rootDir; - this.componentUsages = new Map(); - this.componentDefinitions = new Map(); - } - - async analyze(targetComponent) { - await this.scanDirectory(this.rootDir); - return this.getUsageHierarchy(targetComponent); - } +/** Tree characters used for formatting the component hierarchy */ +const treeChars = { + corner: "└── ", + pipe: "│ ", + tee: "├── ", + blank: " ", +}; + +/** + * Checks if a string follows PascalCase naming convention + * @param {string} str - The string to check + * @returns {boolean} True if string is PascalCase + */ +function isPascalCase(str) { + return /^[A-Z][A-Za-z0-9]*$/.test(str); +} - async scanDirectory(dir) { - const files = await fs.readdir(dir); +/** + * Determines if an AST node represents a React component + * @param {Object} node - The AST node to check + * @returns {boolean} True if node is a React component declaration + */ +function isReactComponent(node) { + return ( + node.type === "FunctionDeclaration" && + isPascalCase(node.id.name) && + node.params.length <= 1 + ); +} - for (const file of files) { - const fullPath = path.join(dir, file); - const stat = await fs.stat(fullPath); +/** + * Checks if a directory path should be ignored during scanning + * @param {string} dirPath - The directory path to check + * @returns {boolean} True if directory should be ignored + */ +function isNodeModulesOrBuild(dirPath) { + const ignoredDirs = ["node_modules", "build", "dist", ".git"]; + return ignoredDirs.some((dir) => dirPath.includes(dir)); +} - if (stat.isDirectory() && !this.isNodeModulesOrBuild(fullPath)) { - await this.scanDirectory(fullPath); - } else if (this.isReactFile(file)) { - await this.analyzeFile(fullPath); - } - } - } +/** + * Determines if a file is a React component file + * @param {string} file - The filename to check + * @returns {boolean} True if file is a React component file + */ +function isReactFile(file) { + const testPattern = /\.(test|spec|stories)\.(js|jsx|tsx|ts)$/; + return ( + file.match(/\.(js|jsx|tsx|ts)$/) && + !file.endsWith(".d.ts") && + !testPattern.test(file) + ); +} - isNodeModulesOrBuild(dirPath) { - const ignoredDirs = ["node_modules", "build", "dist", ".git"]; - return ignoredDirs.some((dir) => dirPath.includes(dir)); - } +/** + * Analyzes a single file for React component definitions and usages + * @param {string} filePath - Path to the file to analyze + * @param {Map} componentUsages - Map to store component usage information + * @param {Map} componentDefinitions - Map to store component definition locations + */ +async function analyzeFile(filePath, componentUsages, componentDefinitions) { + const content = await fs.readFile(filePath, "utf-8"); - isReactFile(file) { - return file.match(/\.(js|jsx|tsx|ts)$/) && !file.endsWith(".d.ts"); - } + try { + const ast = parser.parse(content, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators-legacy"], + }); - async analyzeFile(filePath) { - const content = await fs.readFile(filePath, "utf-8"); + let currentComponent = null; - try { - const ast = parser.parse(content, { - sourceType: "module", - plugins: ["jsx", "typescript", "decorators-legacy"], - }); + traverse(ast, { + FunctionDeclaration: (path) => { + if (isReactComponent(path.node)) { + currentComponent = path.node.id.name; + componentDefinitions.set(currentComponent, filePath); + } + }, - let currentComponent = null; + VariableDeclarator: (path) => { + if ( + path.node.init && + (path.node.init.type === "ArrowFunctionExpression" || + path.node.init.type === "FunctionExpression") && + path.node.id.type === "Identifier" && + isPascalCase(path.node.id.name) + ) { + currentComponent = path.node.id.name; + componentDefinitions.set(currentComponent, filePath); + } + }, - traverse(ast, { - FunctionDeclaration: (path) => { - if (this.isReactComponent(path.node)) { - currentComponent = path.node.id.name; - this.componentDefinitions.set(currentComponent, filePath); - } - }, - - VariableDeclarator: (path) => { - if ( - path.node.init && - (path.node.init.type === "ArrowFunctionExpression" || - path.node.init.type === "FunctionExpression") && - path.node.id.type === "Identifier" && - this.isPascalCase(path.node.id.name) - ) { - currentComponent = path.node.id.name; - this.componentDefinitions.set(currentComponent, filePath); + JSXElement: (path) => { + const elementName = path.node.openingElement.name.name; + if (isPascalCase(elementName)) { + if (!componentUsages.has(elementName)) { + componentUsages.set(elementName, new Set()); } - }, - - JSXElement: (path) => { - const elementName = path.node.openingElement.name.name; - if (this.isPascalCase(elementName)) { - if (!this.componentUsages.has(elementName)) { - this.componentUsages.set(elementName, new Set()); - } - if (currentComponent) { - this.componentUsages.get(elementName).add(currentComponent); - } + if (currentComponent) { + componentUsages.get(elementName).add({ + component: currentComponent, + file: filePath, + line: path.node.loc.start.line, + }); } - }, - }); - } catch (error) { - console.error(`Error analyzing ${filePath}:`, error); - } + } + }, + }); + } catch (error) { + console.error(`Error analyzing ${filePath}:`, error); } +} - getUsageHierarchy(componentName, visited = new Set()) { - if (visited.has(componentName)) { - return { name: componentName, usedIn: ["Circular Reference"] }; +/** + * Recursively scans a directory for React component files + * @param {string} dir - Directory to scan + * @param {Map} componentUsages - Map to store component usage information + * @param {Map} componentDefinitions - Map to store component definition locations + */ +async function scanDirectory(dir, componentUsages, componentDefinitions) { + const files = await fs.readdir(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = await fs.stat(fullPath); + + if (stat.isDirectory() && !isNodeModulesOrBuild(fullPath)) { + await scanDirectory(fullPath, componentUsages, componentDefinitions); + } else if (isReactFile(file)) { + await analyzeFile(fullPath, componentUsages, componentDefinitions); } - - visited.add(componentName); - - const usages = this.componentUsages.get(componentName) || new Set(); - const hierarchy = { - name: componentName, - definedIn: this.componentDefinitions.get(componentName), - usedIn: Array.from(usages).map((parent) => - this.getUsageHierarchy(parent, new Set(visited)) - ), - }; - - return hierarchy; } +} - isPascalCase(str) { - return /^[A-Z][A-Za-z0-9]*$/.test(str); +/** + * Builds a hierarchy of component usages + * @param {string} componentName - Name of the component to analyze + * @param {Map} componentUsages - Map of component usage information + * @param {Map} componentDefinitions - Map of component definition locations + * @param {Set} visited - Set of visited components (for circular reference detection) + * @returns {Object} Hierarchy object representing component usage + */ +function getUsageHierarchy( + componentName, + componentUsages, + componentDefinitions, + visited = new Set() +) { + if (visited.has(componentName)) { + return { name: componentName, usedIn: ["Circular Reference"] }; } - isReactComponent(node) { - return ( - node.type === "FunctionDeclaration" && - this.isPascalCase(node.id.name) && - node.params.length <= 1 - ); - } + visited.add(componentName); + + const usages = componentUsages.get(componentName) || new Set(); + const hierarchy = { + name: componentName, + definedIn: componentDefinitions.get(componentName), + usedIn: Array.from(usages).map((usage) => + getUsageHierarchy( + usage.component, + componentUsages, + componentDefinitions, + new Set(visited) + ) + ), + locations: Array.from(usages).map((usage) => ({ + file: usage.file, + line: usage.line, + })), + }; + + return hierarchy; } -class TreeFormatter { - constructor() { - this.treeChars = { - corner: "└── ", - pipe: "│ ", - tee: "├── ", - blank: " ", - }; +/** + * Formats a component hierarchy as a tree string + * @param {Object} hierarchy - Component hierarchy object + * @param {string} prefix - Current line prefix for tree formatting + * @param {boolean} isLast - Whether this is the last item in its branch + * @returns {string} Formatted tree string + */ +function formatHierarchy(hierarchy, prefix = "", isLast = true) { + let result = ""; + + const connector = isLast ? treeChars.corner : treeChars.tee; + const componentName = hierarchy.name; + const definedIn = hierarchy.definedIn + ? ` (${path.relative(process.cwd(), hierarchy.definedIn)})` + : ""; + + result += `${prefix}${connector}${componentName}${definedIn}\n`; + + if (hierarchy.locations && hierarchy.locations.length > 0) { + const locationPrefix = prefix + (isLast ? treeChars.blank : treeChars.pipe); + hierarchy.locations.forEach((loc) => { + result += `${locationPrefix}${treeChars.pipe}Used at: ${path.relative( + process.cwd(), + loc.file + )}:${loc.line}\n`; + }); } - formatHierarchy(hierarchy, prefix = "", isLast = true) { - let result = ""; + const childPrefix = prefix + (isLast ? treeChars.blank : treeChars.pipe); - // Format current node - const connector = isLast ? this.treeChars.corner : this.treeChars.tee; - const componentName = hierarchy.name; - const definedIn = hierarchy.definedIn - ? ` (${path.relative(process.cwd(), hierarchy.definedIn)})` - : ""; - - result += `${prefix}${connector}${componentName}${definedIn}\n`; + if (hierarchy.usedIn && hierarchy.usedIn.length > 0) { + hierarchy.usedIn.forEach((child, index) => { + const isLastChild = index === hierarchy.usedIn.length - 1; + result += formatHierarchy(child, childPrefix, isLastChild); + }); + } - // Format children - const childPrefix = - prefix + (isLast ? this.treeChars.blank : this.treeChars.pipe); + return result; +} - if (hierarchy.usedIn && hierarchy.usedIn.length > 0) { - hierarchy.usedIn.forEach((child, index) => { - const isLastChild = index === hierarchy.usedIn.length - 1; - result += this.formatHierarchy(child, childPrefix, isLastChild); +/** + * Formats a component hierarchy as a markdown list + * @param {Object} hierarchy - Component hierarchy object + * @param {string} parentPath - Current path in the component tree + * @returns {string} Formatted markdown list + */ +function getMarkdownList(hierarchy, parentPath = "") { + let result = ""; + const currentPath = parentPath + ? `${parentPath} -> ${hierarchy.name}` + : hierarchy.name; + + const definedIn = hierarchy.definedIn + ? ` (defined in ${path.relative(process.cwd(), hierarchy.definedIn)})` + : ""; + + if (!hierarchy.usedIn || hierarchy.usedIn.length === 0) { + result += `- ${currentPath}${definedIn}\n`; + if (hierarchy.locations) { + hierarchy.locations.forEach((loc) => { + result += ` - Used at: ${path.relative(process.cwd(), loc.file)}:${ + loc.line + }\n`; }); } - - return result; + } else { + result += `- ${currentPath}${definedIn}\n`; } - getLeafUsages(hierarchy) { - // If there are no children, this is a leaf node - if (!hierarchy.usedIn || hierarchy.usedIn.length === 0) { - return 1; - } - - // If there are children, recursively count their leaves - return hierarchy.usedIn.reduce( - (sum, child) => sum + this.getLeafUsages(child), - 0 - ); + if (hierarchy.usedIn && hierarchy.usedIn.length > 0) { + hierarchy.usedIn.forEach((child) => { + result += getMarkdownList(child, currentPath); + }); } - getMarkdownList(hierarchy, parentPath = "") { - let result = ""; - const currentPath = parentPath - ? `${parentPath} -> ${hierarchy.name}` - : hierarchy.name; - - // If this is a leaf node (no further usages), add a list item - if (!hierarchy.usedIn || hierarchy.usedIn.length === 0) { - result += `- ${currentPath}\n`; - } - - // Recursively process children - if (hierarchy.usedIn && hierarchy.usedIn.length > 0) { - hierarchy.usedIn.forEach((child) => { - result += this.getMarkdownList(child, currentPath); - }); - } + return result; +} - return result; +/** + * Calculates statistics for a component hierarchy + * @param {Object} node - Current node in the hierarchy + * @param {number} depth - Current depth in the tree + * @param {Object} stats - Statistics object to update + */ +function calculateStats(node, depth, stats) { + stats.totalComponents.add(node.name); + stats.maxDepth = Math.max(stats.maxDepth, depth); + if (node.definedIn) { + stats.uniqueFiles.add(node.definedIn); } - getStatistics(hierarchy) { - const stats = { - totalComponents: new Set(), - maxDepth: 0, - leafNodes: 0, - uniqueFiles: new Set(), - }; - - this.calculateStats(hierarchy, 1, stats); - - return { - totalComponents: stats.totalComponents.size, - maxDepth: stats.maxDepth, - leafNodes: stats.leafNodes, - uniqueFiles: stats.uniqueFiles.size, - }; + if (!node.usedIn || node.usedIn.length === 0) { + stats.leafNodes++; + } else { + node.usedIn.forEach((child) => { + calculateStats(child, depth + 1, stats); + }); } +} - calculateStats(node, depth, stats) { - stats.totalComponents.add(node.name); - stats.maxDepth = Math.max(stats.maxDepth, depth); - if (node.definedIn) { - stats.uniqueFiles.add(node.definedIn); - } - - if (!node.usedIn || node.usedIn.length === 0) { - stats.leafNodes++; - } else { - node.usedIn.forEach((child) => { - this.calculateStats(child, depth + 1, stats); - }); - } - } +/** + * Gets statistics for a component hierarchy + * @param {Object} hierarchy - Component hierarchy object + * @returns {Object} Statistics about the component hierarchy + */ +function getStatistics(hierarchy) { + const stats = { + totalComponents: new Set(), + maxDepth: 0, + leafNodes: 0, + uniqueFiles: new Set(), + }; + + calculateStats(hierarchy, 1, stats); + + return { + totalComponents: stats.totalComponents.size, + maxDepth: stats.maxDepth, + leafNodes: stats.leafNodes, + uniqueFiles: stats.uniqueFiles.size, + }; +} - formatSummary(stats) { - return `Summary: +/** + * Formats statistics as a summary string + * @param {Object} stats - Statistics object + * @returns {string} Formatted summary + */ +function formatSummary(stats) { + return `Summary: • Total unique components: ${stats.totalComponents} • Maximum depth: ${stats.maxDepth} • Leaf usages: ${stats.leafNodes} • Files involved: ${stats.uniqueFiles}\n`; - } +} + +/** + * Main analysis function that scans a directory and builds component hierarchy + * @param {string} rootDir - Root directory to scan + * @param {string} targetComponent - Name of the component to analyze + * @returns {Promise} Component hierarchy object + */ +async function analyze(rootDir, targetComponent) { + const componentUsages = new Map(); + const componentDefinitions = new Map(); + + await scanDirectory(rootDir, componentUsages, componentDefinitions); + return getUsageHierarchy( + targetComponent, + componentUsages, + componentDefinitions + ); } const argv = yargs(hideBin(process.argv)) @@ -265,17 +364,15 @@ const argv = yargs(hideBin(process.argv)) .help("h") .alias("h", "help").argv; +/** + * Main CLI function + */ async function main() { try { - const analyzer = new ComponentAnalyzer(argv.directory); - const hierarchy = await analyzer.analyze(argv.component); - const formatter = new TreeFormatter(); - - // Calculate and display statistics - const stats = formatter.getStatistics(hierarchy); - console.log("\n" + formatter.formatSummary(stats)); + const hierarchy = await analyze(argv.directory, argv.component); + const stats = getStatistics(hierarchy); + console.log("\n" + formatSummary(stats)); - // Prompt for viewing the list const viewResponse = await prompts([ { type: "confirm", @@ -298,10 +395,10 @@ async function main() { let output = ""; if (viewResponse.viewList) { if (viewResponse.format === "markdown") { - output = formatter.getMarkdownList(hierarchy); + output = getMarkdownList(hierarchy); console.log("\nComponent Usage List:"); } else { - output = formatter.formatHierarchy(hierarchy); + output = formatHierarchy(hierarchy); console.log("\nComponent Usage Tree:"); } console.log(output); @@ -317,8 +414,8 @@ async function main() { if (clipboardResponse.copyToClipboard) { const markdownOutput = viewResponse.format === "markdown" - ? `${formatter.formatSummary(stats)}\n${output}` - : `\`\`\`text\n${formatter.formatSummary(stats)}\n${output}\`\`\``; + ? `${formatSummary(stats)}\n${output}` + : `\`\`\`text\n${formatSummary(stats)}\n${output}\`\`\``; await clipboardy.write(markdownOutput); console.log( "\n✓ Copied to clipboard" + @@ -335,4 +432,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { main(); } -export { ComponentAnalyzer, TreeFormatter }; +export { analyze, formatHierarchy, getMarkdownList }; From 76917e3ad11b236c4955a5fa74f5f9f5d5df4601 Mon Sep 17 00:00:00 2001 From: Rui Martins Date: Tue, 11 Feb 2025 19:37:31 +0000 Subject: [PATCH 3/5] chore: add line number --- main.js | 96 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/main.js b/main.js index 5cecb32..4989b5e 100755 --- a/main.js +++ b/main.js @@ -199,21 +199,16 @@ function formatHierarchy(hierarchy, prefix = "", isLast = true) { const connector = isLast ? treeChars.corner : treeChars.tee; const componentName = hierarchy.name; - const definedIn = hierarchy.definedIn - ? ` (${path.relative(process.cwd(), hierarchy.definedIn)})` - : ""; - - result += `${prefix}${connector}${componentName}${definedIn}\n`; - - if (hierarchy.locations && hierarchy.locations.length > 0) { - const locationPrefix = prefix + (isLast ? treeChars.blank : treeChars.pipe); - hierarchy.locations.forEach((loc) => { - result += `${locationPrefix}${treeChars.pipe}Used at: ${path.relative( - process.cwd(), - loc.file - )}:${loc.line}\n`; - }); - } + + // Show the location where this component is used + const usageLocation = + hierarchy.locations && hierarchy.locations[0] + ? ` (${path.relative(process.cwd(), hierarchy.locations[0].file)}:${ + hierarchy.locations[0].line + })` + : ""; + + result += `${prefix}${connector}${componentName}${usageLocation}\n`; const childPrefix = prefix + (isLast ? treeChars.blank : treeChars.pipe); @@ -228,37 +223,42 @@ function formatHierarchy(hierarchy, prefix = "", isLast = true) { } /** - * Formats a component hierarchy as a markdown list + * Formats a component hierarchy as a markdown list, showing only complete paths * @param {Object} hierarchy - Component hierarchy object * @param {string} parentPath - Current path in the component tree + * @param {Object|null} firstUsageLocation - Location where the first child component uses the target * @returns {string} Formatted markdown list */ -function getMarkdownList(hierarchy, parentPath = "") { +function getMarkdownList( + hierarchy, + parentPath = "", + firstUsageLocation = null +) { let result = ""; const currentPath = parentPath ? `${parentPath} -> ${hierarchy.name}` : hierarchy.name; - const definedIn = hierarchy.definedIn - ? ` (defined in ${path.relative(process.cwd(), hierarchy.definedIn)})` - : ""; - + // Only output leaf nodes (components that aren't used by other components) if (!hierarchy.usedIn || hierarchy.usedIn.length === 0) { - result += `- ${currentPath}${definedIn}\n`; - if (hierarchy.locations) { - hierarchy.locations.forEach((loc) => { - result += ` - Used at: ${path.relative(process.cwd(), loc.file)}:${ - loc.line - }\n`; - }); - } + const usageLocation = firstUsageLocation + ? ` (${path.relative(process.cwd(), firstUsageLocation.file)}:${ + firstUsageLocation.line + })` + : ""; + result += `- ${currentPath}${usageLocation}\n`; } else { - result += `- ${currentPath}${definedIn}\n`; - } - - if (hierarchy.usedIn && hierarchy.usedIn.length > 0) { + // Continue traversing the tree hierarchy.usedIn.forEach((child) => { - result += getMarkdownList(child, currentPath); + // If this is the root component (Link), look for where it's used in its child + const nextLocation = + !parentPath && + hierarchy.locations?.find((loc) => loc.file === child.definedIn); + result += getMarkdownList( + child, + currentPath, + nextLocation || firstUsageLocation + ); }); } @@ -270,19 +270,26 @@ function getMarkdownList(hierarchy, parentPath = "") { * @param {Object} node - Current node in the hierarchy * @param {number} depth - Current depth in the tree * @param {Object} stats - Statistics object to update + * @param {Array} currentPath - Array of components in current path */ -function calculateStats(node, depth, stats) { - stats.totalComponents.add(node.name); - stats.maxDepth = Math.max(stats.maxDepth, depth); - if (node.definedIn) { - stats.uniqueFiles.add(node.definedIn); - } +function calculateStats(node, depth, stats, currentPath = []) { + currentPath.push(node.name); if (!node.usedIn || node.usedIn.length === 0) { + // Store the complete path as a string, excluding the target component itself + const pathString = currentPath.slice(1).join(" -> "); + if (pathString) { + stats.uniquePaths.add(pathString); + } + + stats.maxDepth = Math.max(stats.maxDepth, depth); + if (node.definedIn) { + stats.uniqueFiles.add(node.definedIn); + } stats.leafNodes++; } else { node.usedIn.forEach((child) => { - calculateStats(child, depth + 1, stats); + calculateStats(child, depth + 1, stats, [...currentPath]); }); } } @@ -294,7 +301,7 @@ function calculateStats(node, depth, stats) { */ function getStatistics(hierarchy) { const stats = { - totalComponents: new Set(), + uniquePaths: new Set(), maxDepth: 0, leafNodes: 0, uniqueFiles: new Set(), @@ -303,7 +310,7 @@ function getStatistics(hierarchy) { calculateStats(hierarchy, 1, stats); return { - totalComponents: stats.totalComponents.size, + uniquePaths: stats.uniquePaths.size, maxDepth: stats.maxDepth, leafNodes: stats.leafNodes, uniqueFiles: stats.uniqueFiles.size, @@ -317,9 +324,8 @@ function getStatistics(hierarchy) { */ function formatSummary(stats) { return `Summary: -• Total unique components: ${stats.totalComponents} +• Unique usage paths: ${stats.uniquePaths} • Maximum depth: ${stats.maxDepth} -• Leaf usages: ${stats.leafNodes} • Files involved: ${stats.uniqueFiles}\n`; } From 16680e6217be429ccfce48425b23f5ec4d63f98e Mon Sep 17 00:00:00 2001 From: Rui Martins Date: Tue, 11 Feb 2025 19:50:16 +0000 Subject: [PATCH 4/5] feat: group same usage in list view --- main.js | 65 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/main.js b/main.js index 4989b5e..af2c5a8 100755 --- a/main.js +++ b/main.js @@ -235,33 +235,50 @@ function getMarkdownList( firstUsageLocation = null ) { let result = ""; - const currentPath = parentPath - ? `${parentPath} -> ${hierarchy.name}` - : hierarchy.name; - - // Only output leaf nodes (components that aren't used by other components) - if (!hierarchy.usedIn || hierarchy.usedIn.length === 0) { - const usageLocation = firstUsageLocation - ? ` (${path.relative(process.cwd(), firstUsageLocation.file)}:${ - firstUsageLocation.line - })` - : ""; - result += `- ${currentPath}${usageLocation}\n`; - } else { - // Continue traversing the tree - hierarchy.usedIn.forEach((child) => { - // If this is the root component (Link), look for where it's used in its child - const nextLocation = - !parentPath && - hierarchy.locations?.find((loc) => loc.file === child.definedIn); - result += getMarkdownList( - child, - currentPath, - nextLocation || firstUsageLocation - ); + const pathsByLocation = new Map(); + let totalPaths = 0; // Add counter for verification + + function addPath(path, location) { + const locationKey = location + ? `${location.file}:${location.line}` + : "unknown"; + if (!pathsByLocation.has(locationKey)) { + pathsByLocation.set(locationKey, []); + } + pathsByLocation.get(locationKey).push(path); + totalPaths++; // Increment counter + } + + function processHierarchy(node, currentPath, usageLocation) { + const newPath = currentPath ? `${currentPath} -> ${node.name}` : node.name; + + if (!node.usedIn || node.usedIn.length === 0) { + addPath(newPath, usageLocation); + } else { + node.usedIn.forEach((child) => { + const nextLocation = + !currentPath && + node.locations?.find((loc) => loc.file === child.definedIn); + processHierarchy(child, newPath, nextLocation || usageLocation); + }); + } + } + + processHierarchy(hierarchy, parentPath, firstUsageLocation); + + // Format the output with grouped paths + for (const [location, paths] of pathsByLocation) { + result += `- Paths to ${location}:\n`; + paths.forEach((path) => { + result += ` • ${path}\n`; }); } + // Add verification info + let groupedTotal = 0; + pathsByLocation.forEach((paths) => (groupedTotal += paths.length)); + result += `\nVerification: Found ${totalPaths} total paths, grouped into ${pathsByLocation.size} locations (${groupedTotal} total paths in groups)\n`; + return result; } From a7e527f62c29553355c005c2d530a45de25dda4c Mon Sep 17 00:00:00 2001 From: Rui Martins Date: Tue, 11 Feb 2025 19:58:27 +0000 Subject: [PATCH 5/5] refactor: simplify list output and improve formatting --- main.js | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/main.js b/main.js index af2c5a8..45341a2 100755 --- a/main.js +++ b/main.js @@ -223,17 +223,13 @@ function formatHierarchy(hierarchy, prefix = "", isLast = true) { } /** - * Formats a component hierarchy as a markdown list, showing only complete paths + * Formats a component hierarchy as a list, showing complete paths grouped by location * @param {Object} hierarchy - Component hierarchy object * @param {string} parentPath - Current path in the component tree * @param {Object|null} firstUsageLocation - Location where the first child component uses the target - * @returns {string} Formatted markdown list + * @returns {string} Formatted list */ -function getMarkdownList( - hierarchy, - parentPath = "", - firstUsageLocation = null -) { +function getList(hierarchy, parentPath = "", firstUsageLocation = null) { let result = ""; const pathsByLocation = new Map(); let totalPaths = 0; // Add counter for verification @@ -268,17 +264,13 @@ function getMarkdownList( // Format the output with grouped paths for (const [location, paths] of pathsByLocation) { - result += `- Paths to ${location}:\n`; + result += `### Paths to ${location}\n\n`; paths.forEach((path) => { - result += ` • ${path}\n`; + result += `* ${path}\n`; }); + result += "\n"; } - // Add verification info - let groupedTotal = 0; - pathsByLocation.forEach((paths) => (groupedTotal += paths.length)); - result += `\nVerification: Found ${totalPaths} total paths, grouped into ${pathsByLocation.size} locations (${groupedTotal} total paths in groups)\n`; - return result; } @@ -409,7 +401,7 @@ async function main() { message: "Select output format:", choices: [ { title: "Tree view", value: "tree" }, - { title: "Markdown list", value: "markdown" }, + { title: "List", value: "markdown" }, ], initial: 0, }, @@ -418,7 +410,7 @@ async function main() { let output = ""; if (viewResponse.viewList) { if (viewResponse.format === "markdown") { - output = getMarkdownList(hierarchy); + output = getList(hierarchy); console.log("\nComponent Usage List:"); } else { output = formatHierarchy(hierarchy); @@ -455,4 +447,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { main(); } -export { analyze, formatHierarchy, getMarkdownList }; +export { analyze, formatHierarchy, getList };