continuedev/modularize-react-components icon
public
Published on 10/29/2025
Modularize React Components

Modularize the largest React component

Agents
Modularize the largest React component

Model

Continue Default (Claude Sonnet 4.5)

Tools

All built-in tools (default)

Prompt

Prompt to kick off agents. Additional user input is appended to this.

```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.

How to use this agent

Use this agent from Mission Control or Continue CLI. The agent prompt will be combined with your input to create the agent task.