#!/usr/bin/env node /** * Component Analyzer for Cursor AI * * Analyzes a component and generates a structured prompt for Cursor AI. * Copy the output and paste into Cursor Chat (Cmd+L) or Composer (Cmd+I). * * Usage: * node scripts/analyze-component.js * * Examples: * node scripts/analyze-component.js app/components/base/button/index.tsx * node scripts/analyze-component.js app/components/workflow/nodes/llm/panel.tsx */ const fs = require('fs'); const path = require('path'); // ============================================================================ // Simple Analyzer // ============================================================================ class ComponentAnalyzer { analyze(code, filePath) { const fileName = path.basename(filePath, path.extname(filePath)); return { name: fileName.charAt(0).toUpperCase() + fileName.slice(1), path: filePath, type: this.detectType(filePath, code), hasProps: code.includes('Props') || code.includes('interface'), hasState: code.includes('useState'), hasEffects: code.includes('useEffect'), hasCallbacks: code.includes('useCallback'), hasMemo: code.includes('useMemo'), hasEvents: /on[A-Z]\w+/.test(code), hasI18n: code.includes('useTranslation') || code.includes('react-i18next'), hasRouter: code.includes('useRouter') || code.includes('usePathname'), hasAPI: code.includes('service/') || code.includes('fetch('), isClientComponent: code.includes("'use client'") || code.includes('"use client"'), complexity: this.calculateComplexity(code), lineCount: code.split('\n').length, }; } detectType(filePath, code) { if (filePath.includes('/hooks/')) return 'hook'; if (filePath.includes('/utils/')) return 'util'; if (filePath.includes('/page.tsx')) return 'page'; if (code.includes('useState') || code.includes('useEffect')) return 'component'; return 'component'; } /** * Calculate component complexity score * Based on Cognitive Complexity + React-specific metrics * * Score Ranges: * 0-10: 🟢 Simple (5-10 min to test) * 11-30: 🟔 Medium (15-30 min to test) * 31-50: 🟠 Complex (30-60 min to test) * 51+: šŸ”“ Very Complex (60+ min, consider splitting) */ calculateComplexity(code) { let score = 0; // ===== React Hooks (State Management Complexity) ===== const stateHooks = (code.match(/useState/g) || []).length; const effectHooks = (code.match(/useEffect/g) || []).length; const callbackHooks = (code.match(/useCallback/g) || []).length; const memoHooks = (code.match(/useMemo/g) || []).length; const refHooks = (code.match(/useRef/g) || []).length; const customHooks = (code.match(/use[A-Z]\w+/g) || []).length - (stateHooks + effectHooks + callbackHooks + memoHooks + refHooks); score += stateHooks * 5; // Each state +5 (need to test state changes) score += effectHooks * 6; // Each effect +6 (need to test deps & cleanup) score += callbackHooks * 2; // Each callback +2 score += memoHooks * 2; // Each memo +2 score += refHooks * 1; // Each ref +1 score += customHooks * 3; // Each custom hook +3 // ===== Control Flow Complexity (Cyclomatic Complexity) ===== score += (code.match(/if\s*\(/g) || []).length * 2; // if statement score += (code.match(/else\s+if/g) || []).length * 2; // else if score += (code.match(/\?\s*[^:]+\s*:/g) || []).length * 1; // ternary operator score += (code.match(/switch\s*\(/g) || []).length * 3; // switch score += (code.match(/case\s+/g) || []).length * 1; // case branch score += (code.match(/&&/g) || []).length * 1; // logical AND score += (code.match(/\|\|/g) || []).length * 1; // logical OR score += (code.match(/\?\?/g) || []).length * 1; // nullish coalescing // ===== Loop Complexity ===== score += (code.match(/\.map\(/g) || []).length * 2; // map score += (code.match(/\.filter\(/g) || []).length * 1; // filter score += (code.match(/\.reduce\(/g) || []).length * 3; // reduce (complex) score += (code.match(/for\s*\(/g) || []).length * 2; // for loop score += (code.match(/while\s*\(/g) || []).length * 3; // while loop // ===== Props and Events Complexity ===== const propsMatches = code.match(/(\w+)\s*:\s*\w+/g) || []; const propsCount = Math.min(propsMatches.length, 20); // Max 20 props score += Math.floor(propsCount / 2); // Every 2 props +1 const eventHandlers = (code.match(/on[A-Z]\w+/g) || []).length; score += eventHandlers * 2; // Each event handler +2 // ===== API Call Complexity ===== score += (code.match(/fetch\(/g) || []).length * 4; // fetch score += (code.match(/axios\./g) || []).length * 4; // axios score += (code.match(/useSWR/g) || []).length * 4; // SWR score += (code.match(/useQuery/g) || []).length * 4; // React Query score += (code.match(/\.then\(/g) || []).length * 2; // Promise score += (code.match(/await\s+/g) || []).length * 2; // async/await // ===== Third-party Library Integration ===== const hasReactFlow = /reactflow|ReactFlow/.test(code); const hasMonaco = /@monaco-editor/.test(code); const hasEcharts = /echarts/.test(code); const hasLexical = /lexical/.test(code); if (hasReactFlow) score += 15; // ReactFlow is very complex if (hasMonaco) score += 12; // Monaco Editor if (hasEcharts) score += 8; // Echarts if (hasLexical) score += 10; // Lexical Editor // ===== Code Size Complexity ===== const lines = code.split('\n').length; if (lines > 500) score += 10; else if (lines > 300) score += 6; else if (lines > 150) score += 3; // ===== Nesting Depth (deep nesting reduces readability) ===== const maxNesting = this.calculateNestingDepth(code); score += Math.max(0, (maxNesting - 3)) * 2; // Over 3 levels, +2 per level // ===== Context and Global State ===== score += (code.match(/useContext/g) || []).length * 3; score += (code.match(/useStore|useAppStore/g) || []).length * 4; score += (code.match(/zustand|redux/g) || []).length * 3; return Math.min(score, 100); // Max 100 points } /** * Calculate maximum nesting depth */ calculateNestingDepth(code) { let maxDepth = 0; let currentDepth = 0; for (let i = 0; i < code.length; i++) { if (code[i] === '{') { currentDepth++; maxDepth = Math.max(maxDepth, currentDepth); } else if (code[i] === '}') { currentDepth--; } } return maxDepth; } } // ============================================================================ // Prompt Builder for Cursor // ============================================================================ class CursorPromptBuilder { build(analysis, sourceCode) { const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx'); return ` ╔════════════════════════════════════════════════════════════════════════════╗ ā•‘ šŸ“‹ GENERATE TEST FOR DIFY COMPONENT ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• šŸ“ Component: ${analysis.name} šŸ“‚ Path: ${analysis.path} šŸŽÆ Test File: ${testPath} šŸ“Š Component Analysis: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Type: ${analysis.type} Complexity: ${analysis.complexity} ${this.getComplexityLevel(analysis.complexity)} Lines: ${analysis.lineCount} Features Detected: ${analysis.hasProps ? 'āœ“' : 'āœ—'} Props/TypeScript interfaces ${analysis.hasState ? 'āœ“' : 'āœ—'} Local state (useState) ${analysis.hasEffects ? 'āœ“' : 'āœ—'} Side effects (useEffect) ${analysis.hasCallbacks ? 'āœ“' : 'āœ—'} Callbacks (useCallback) ${analysis.hasMemo ? 'āœ“' : 'āœ—'} Memoization (useMemo) ${analysis.hasEvents ? 'āœ“' : 'āœ—'} Event handlers ${analysis.hasI18n ? 'āœ“' : 'āœ—'} Internationalization ${analysis.hasRouter ? 'āœ“' : 'āœ—'} Next.js routing ${analysis.hasAPI ? 'āœ“' : 'āœ—'} API calls ${analysis.isClientComponent ? 'āœ“' : 'āœ—'} Client component ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ šŸ“ TASK FOR CURSOR AI: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Please generate a comprehensive test file for this component at: ${testPath} The component is located at: ${analysis.path} Follow the testing guidelines in .cursorrules file. ${this.getSpecificGuidelines(analysis)} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ šŸ“‹ COPY THIS TO CURSOR: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Generate a comprehensive test file for @${analysis.path} following the project's testing guidelines in .cursorrules. Focus on: ${this.buildFocusPoints(analysis)} Create the test file at: ${testPath} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `; } getComplexityLevel(score) { if (score < 10) return '🟢 Simple'; if (score < 30) return '🟔 Medium'; return 'šŸ”“ Complex'; } buildFocusPoints(analysis) { const points = []; if (analysis.hasProps) points.push('- Testing all prop variations'); if (analysis.hasState) points.push('- Testing state management and updates'); if (analysis.hasEffects) points.push('- Testing side effects and cleanup'); if (analysis.hasEvents) points.push('- Testing user interactions and event handlers'); if (analysis.hasI18n) points.push('- Mocking i18n (useTranslation)'); if (analysis.hasRouter) points.push('- Mocking Next.js router hooks'); if (analysis.hasAPI) points.push('- Mocking API calls'); points.push('- Testing accessibility (ARIA attributes)'); points.push('- Testing edge cases and error handling'); return points.join('\n'); } getSpecificGuidelines(analysis) { const guidelines = []; if (analysis.complexity > 30) { guidelines.push('āš ļø This is a COMPLEX component. Consider:'); guidelines.push(' - Breaking tests into multiple describe blocks'); guidelines.push(' - Testing integration scenarios'); guidelines.push(' - Adding performance tests'); } if (analysis.hasAPI) { guidelines.push('🌐 This component makes API calls:'); guidelines.push(' - Mock all API calls using jest.mock'); guidelines.push(' - Test loading, success, and error states'); guidelines.push(' - Test retry logic if applicable'); } if (analysis.hasRouter) { guidelines.push('šŸ”€ This component uses routing:'); guidelines.push(' - Mock useRouter, usePathname, useSearchParams'); guidelines.push(' - Test navigation behavior'); guidelines.push(' - Test URL parameter handling'); } if (analysis.path.includes('workflow')) { guidelines.push('āš™ļø This is a Workflow component:'); guidelines.push(' - Test node configuration'); guidelines.push(' - Test data validation'); guidelines.push(' - Test variable passing'); } return guidelines.length > 0 ? '\n' + guidelines.join('\n') + '\n' : ''; } } // ============================================================================ // Main Function // ============================================================================ function main() { const args = process.argv.slice(2); if (args.length === 0) { console.error(` āŒ Error: Component path is required Usage: node scripts/analyze-component.js Examples: node scripts/analyze-component.js app/components/base/button/index.tsx node scripts/analyze-component.js app/components/workflow/nodes/llm/panel.tsx This tool analyzes your component and generates a prompt for Cursor AI. Copy the output and use it in Cursor Chat (Cmd+L) or Composer (Cmd+I). `); process.exit(1); } const componentPath = args[0]; const absolutePath = path.resolve(process.cwd(), componentPath); // Check if file exists if (!fs.existsSync(absolutePath)) { console.error(`āŒ Error: File not found: ${componentPath}`); process.exit(1); } // Read source code const sourceCode = fs.readFileSync(absolutePath, 'utf-8'); // Analyze const analyzer = new ComponentAnalyzer(); const analysis = analyzer.analyze(sourceCode, componentPath); // Build prompt for Cursor const builder = new CursorPromptBuilder(); const prompt = builder.build(analysis, sourceCode); // Output console.log(prompt); // Also save to clipboard if pbcopy is available (macOS) try { const { execSync } = require('child_process'); execSync('which pbcopy', { stdio: 'ignore' }); // Save prompt to clipboard execSync('pbcopy', { input: prompt.split('šŸ“‹ COPY THIS TO CURSOR:')[1].split('━━━')[0].trim() }); console.log('\nšŸ“‹ Prompt copied to clipboard! Paste it in Cursor Chat (Cmd+L).\n'); } catch { // pbcopy not available, skip } } // ============================================================================ // Run // ============================================================================ main();