feat: add component analysis script and test generation command in package.json

This commit is contained in:
CodingOnStar 2025-10-24 18:13:39 +08:00
parent f45c18ee35
commit 0831fc8aba
2 changed files with 354 additions and 0 deletions

View File

@ -37,6 +37,7 @@
"check:i18n-types": "node ./i18n-config/check-i18n-sync.js",
"test": "jest",
"test:watch": "jest --watch",
"test:gen": "node scripts/analyze-component.js",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"preinstall": "npx only-allow pnpm",

353
web/scripts/analyze-component.js Executable file
View File

@ -0,0 +1,353 @@
#!/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 <component-path>
*
* 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 <component-path>
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();