```javascript title="analyze-components.js"
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
// Parse .gitignore patterns
function parseGitignore(gitignorePath) {
const patterns = [];
if (fs.existsSync(gitignorePath)) {
const content = fs.readFileSync(gitignorePath, "utf8");
const lines = content
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
patterns.push(...lines);
}
return patterns;
}
// Check if a path should be ignored based on .gitignore patterns
function shouldIgnore(filePath, gitignorePatterns, rootDir) {
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/"); // Use forward slashes
for (const pattern of gitignorePatterns) {
// Handle negation patterns (starting with !)
if (pattern.startsWith("!")) {
continue; // Skip negation patterns for simplicity
}
// Convert gitignore pattern to regex
let regexPattern = pattern
.replace(/\./g, "\\.") // Escape dots
.replace(/\*\*/g, "§§§") // Temporarily replace **
.replace(/\*/g, "[^/]*") // * matches any characters except /
.replace(/§§§/g, ".*") // ** matches any characters including /
.replace(/\?/g, "[^/]"); // ? matches single character except /
// If pattern ends with /, it only matches directories
if (pattern.endsWith("/")) {
regexPattern = "^" + regexPattern.slice(0, -1) + "($|/)";
} else {
// Pattern can match files or directories
regexPattern = "^" + regexPattern + "($|/)";
}
const regex = new RegExp(regexPattern);
if (regex.test(relativePath) || regex.test(relativePath + "/")) {
return true;
}
// Also check if any parent directory matches the pattern
const pathParts = relativePath.split("/");
for (let i = 1; i <= pathParts.length; i++) {
const partialPath = pathParts.slice(0, i).join("/");
if (regex.test(partialPath) || regex.test(partialPath + "/")) {
return true;
}
}
}
return false;
}
// Function to recursively find all .tsx files
function findTsxFiles(
dir,
fileList = [],
gitignorePatterns = [],
rootDir = dir,
) {
try {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
// Skip if this path should be ignored
if (shouldIgnore(filePath, gitignorePatterns, rootDir)) {
continue;
}
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
findTsxFiles(filePath, fileList, gitignorePatterns, rootDir);
} else if (file.endsWith(".tsx")) {
fileList.push(filePath);
}
} catch (error) {
// Skip files/directories we can't access
console.warn(`Warning: Could not access ${filePath}`);
continue;
}
}
} catch (error) {
console.warn(`Warning: Could not read directory ${dir}`);
}
return fileList;
}
// Function to check if file contains React components
function isReactComponent(filePath) {
try {
const content = fs.readFileSync(filePath, "utf8");
// Check for common React component patterns
const reactPatterns = [
/export\s+(default\s+)?function\s+[A-Z]\w*\s*\(/, // export function Component() or export default function Component()
/const\s+[A-Z]\w*\s*=\s*\([^)]*\)\s*=>/, // const Component = () =>
/function\s+[A-Z]\w*\s*\([^)]*\)\s*{/, // function Component() {
/export\s+(default\s+)?[A-Z]\w*\s*(?:=|:)/, // export default Component or export Component
/<[A-Z]\w*\s*[^>]*>/, // JSX with capitalized tags
/return\s*\(/, // return statement (common in components)
/React\./, // Direct React usage
/import.*React/, // React import
];
return reactPatterns.some((pattern) => pattern.test(content));
} catch (error) {
console.warn(`Error reading file ${filePath}:`, error.message);
return false;
}
}
// Function to count lines of code (excluding empty lines and comments)
function countLinesOfCode(filePath) {
try {
const content = fs.readFileSync(filePath, "utf8");
const lines = content.split("\n");
let totalLines = lines.length;
let codeLines = 0;
let inBlockComment = false;
for (let line of lines) {
line = line.trim();
// Skip empty lines
if (line === "") continue;
// Handle block comments
if (line.includes("/*")) {
inBlockComment = true;
}
if (line.includes("*/")) {
inBlockComment = false;
continue;
}
if (inBlockComment) continue;
// Skip single line comments (but not URLs)
if (line.startsWith("//") && !line.includes("http")) continue;
codeLines++;
}
return { totalLines, codeLines };
} catch (error) {
console.warn(`Error counting lines in ${filePath}:`, error.message);
return { totalLines: 0, codeLines: 0 };
}
}
// Function to extract component name from file
function extractComponentName(filePath) {
const content = fs.readFileSync(filePath, "utf8");
// Try to find the main component name
const patterns = [
/export\s+default\s+function\s+([A-Z]\w*)/, // export default function ComponentName
/function\s+([A-Z]\w*)\s*\(/, // function ComponentName(
/const\s+([A-Z]\w*)\s*=\s*\([^)]*\)\s*=>/, // const ComponentName = () =>
/export\s+function\s+([A-Z]\w*)/, // export function ComponentName
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
return match[1];
}
}
// Fallback to filename without extension
return path.basename(filePath, ".tsx");
}
function outputTable(components) {
console.log("rank\tcomponent_name\tlines_of_code\ttotal_lines\tfile_path");
components.forEach((component, index) => {
const rank = index + 1;
const name = component.name;
const codeLines = component.codeLines;
const totalLines = component.totalLines;
const path = component.path;
console.log(`${rank}\t${name}\t${codeLines}\t${totalLines}\t${path}`);
});
}
// Main analysis function
function analyzeComponents() {
const rootDir = process.cwd();
const gitignorePath = path.join(rootDir, ".gitignore");
const gitignorePatterns = parseGitignore(gitignorePath);
const tsxFiles = findTsxFiles(rootDir, [], gitignorePatterns, rootDir);
const components = [];
for (const filePath of tsxFiles) {
if (isReactComponent(filePath)) {
const { totalLines, codeLines } = countLinesOfCode(filePath);
const componentName = extractComponentName(filePath);
const relativePath = path.relative(rootDir, filePath);
components.push({
name: componentName,
path: relativePath,
totalLines,
codeLines,
filePath,
});
}
}
// Sort by lines of code (descending)
components.sort((a, b) => b.codeLines - a.codeLines);
outputTable(components);
return components;
}
// Run the analysis
if (require.main === module) {
try {
// Get format from command line argument
const format = process.argv[2] || "csv";
const validFormats = ["csv", "tsv", "json", "table"];
if (!validFormats.includes(format)) {
console.error(`Invalid format: ${format}`);
console.error(`Valid formats: ${validFormats.join(", ")}`);
console.error("Usage: node analyze-components.js [csv|tsv|json|table]");
process.exit(1);
}
analyzeComponents(format);
} catch (error) {
console.error("Error analyzing components:", error);
process.exit(1);
}
}
module.exports = { analyzeComponents };
```
Run `node analyze-components.js | head -n 10` to find the React component in our codebase with the greatest number of lines, and make it more modular. If all of the components are less than 600 lines you can just do nothing.