UNPKG

smartui-migration-tool

Version:

Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI

799 lines (704 loc) • 26 kB
const { Command, Flags } = require('@oclif/core'); const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); const glob = require('glob'); class FrameworkAnalyzer extends Command { static description = 'Framework-specific analyzer for React, Angular, Vue, Cypress, and Playwright'; static flags = { path: Flags.string({ char: 'p', description: 'Path to analyze (default: current directory)', default: process.cwd() }), framework: Flags.string({ char: 'f', description: 'Specific framework to analyze', options: ['react', 'angular', 'vue', 'cypress', 'playwright', 'all'] }), include: Flags.string({ char: 'i', description: 'File patterns to include (comma-separated)', default: '**/*.{js,ts,jsx,tsx,py,java,cs}' }), exclude: Flags.string({ char: 'e', description: 'File patterns to exclude (comma-separated)', default: 'node_modules/**,dist/**,build/**,*.min.js' }), output: Flags.string({ char: 'o', description: 'Output file for framework analysis', default: 'framework-analysis.json' }), verbose: Flags.boolean({ char: 'v', description: 'Enable verbose output', default: false }) }; async run() { const { flags } = await this.parse(FrameworkAnalyzer); console.log(chalk.blue.bold('\nšŸ”§ Framework Analyzer')); console.log(chalk.gray('Analyzing framework-specific patterns and conventions...\n')); try { // Create framework analyzer const analyzer = this.createFrameworkAnalyzer(); // Perform analysis const results = await this.performAnalysis(flags, analyzer); // Display results this.displayResults(results, flags.verbose); // Save results if (flags.output) { await fs.writeJson(flags.output, results, { spaces: 2 }); console.log(chalk.green(`\nāœ… Framework analysis saved to: ${flags.output}`)); } } catch (error) { console.error(chalk.red(`\nāŒ Error during framework analysis: ${error.message}`)); this.exit(1); } } createFrameworkAnalyzer() { return { // React Analyzer analyzeReact: (files) => { const analysis = { framework: 'react', components: [], hooks: [], patterns: [], conventions: [], issues: [], recommendations: [] }; files.forEach(file => { if (file.content) { // Analyze React components const componentRegex = /(?:function|const)\s+([A-Z][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g; let match; while ((match = componentRegex.exec(file.content)) !== null) { analysis.components.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze React hooks const hookRegex = /(?:useState|useEffect|useContext|useReducer|useCallback|useMemo|useRef|useImperativeHandle|useLayoutEffect|useDebugValue)\s*\(/g; while ((match = hookRegex.exec(file.content)) !== null) { const hookName = match[0].replace(/\s*\(/, ''); analysis.hooks.push({ name: hookName, file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze JSX patterns if (file.content.includes('jsx') || file.content.includes('JSX')) { analysis.patterns.push({ type: 'jsx', description: 'JSX syntax detected', file: file.path }); } // Analyze React Router if (file.content.includes('react-router') || file.content.includes('BrowserRouter')) { analysis.patterns.push({ type: 'routing', description: 'React Router detected', file: file.path }); } // Analyze state management if (file.content.includes('redux') || file.content.includes('useReducer')) { analysis.patterns.push({ type: 'state_management', description: 'State management detected', file: file.path }); } } }); // Check conventions this.checkReactConventions(analysis); this.generateReactRecommendations(analysis); return analysis; }, // Angular Analyzer analyzeAngular: (files) => { const analysis = { framework: 'angular', components: [], services: [], modules: [], patterns: [], conventions: [], issues: [], recommendations: [] }; files.forEach(file => { if (file.content) { // Analyze Angular components const componentRegex = /@Component\s*\(\s*\{[^}]*\}\s*\)\s*export\s+class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g; let match; while ((match = componentRegex.exec(file.content)) !== null) { analysis.components.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Angular services const serviceRegex = /@Injectable\s*\(\s*\{[^}]*\}\s*\)\s*export\s+class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g; while ((match = serviceRegex.exec(file.content)) !== null) { analysis.services.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Angular modules const moduleRegex = /@NgModule\s*\(\s*\{[^}]*\}\s*\)\s*export\s+class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g; while ((match = moduleRegex.exec(file.content)) !== null) { analysis.modules.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Angular patterns if (file.content.includes('@angular/')) { analysis.patterns.push({ type: 'angular_core', description: 'Angular core modules detected', file: file.path }); } if (file.content.includes('ngOnInit') || file.content.includes('ngOnDestroy')) { analysis.patterns.push({ type: 'lifecycle_hooks', description: 'Angular lifecycle hooks detected', file: file.path }); } } }); this.checkAngularConventions(analysis); this.generateAngularRecommendations(analysis); return analysis; }, // Vue Analyzer analyzeVue: (files) => { const analysis = { framework: 'vue', components: [], composables: [], patterns: [], conventions: [], issues: [], recommendations: [] }; files.forEach(file => { if (file.content) { // Analyze Vue components const componentRegex = /export\s+default\s*\{[^}]*name:\s*['"]([^'"]+)['"]/g; let match; while ((match = componentRegex.exec(file.content)) !== null) { analysis.components.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Vue composables const composableRegex = /export\s+(?:function|const)\s+use([A-Z][a-zA-Z0-9_]*)/g; while ((match = composableRegex.exec(file.content)) !== null) { analysis.composables.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Vue patterns if (file.content.includes('<template>') || file.content.includes('<script>')) { analysis.patterns.push({ type: 'single_file_component', description: 'Vue SFC detected', file: file.path }); } if (file.content.includes('ref(') || file.content.includes('reactive(')) { analysis.patterns.push({ type: 'composition_api', description: 'Vue Composition API detected', file: file.path }); } } }); this.checkVueConventions(analysis); this.generateVueRecommendations(analysis); return analysis; }, // Cypress Analyzer analyzeCypress: (files) => { const analysis = { framework: 'cypress', tests: [], commands: [], patterns: [], conventions: [], issues: [], recommendations: [] }; files.forEach(file => { if (file.content) { // Analyze Cypress tests const testRegex = /(?:describe|it)\s*\(\s*['"]([^'"]+)['"]/g; let match; while ((match = testRegex.exec(file.content)) !== null) { analysis.tests.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Cypress commands const commandRegex = /cy\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g; while ((match = commandRegex.exec(file.content)) !== null) { analysis.commands.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Cypress patterns if (file.content.includes('cy.visit(')) { analysis.patterns.push({ type: 'navigation', description: 'Cypress navigation detected', file: file.path }); } if (file.content.includes('cy.get(')) { analysis.patterns.push({ type: 'element_selection', description: 'Cypress element selection detected', file: file.path }); } if (file.content.includes('cy.intercept(')) { analysis.patterns.push({ type: 'api_mocking', description: 'Cypress API mocking detected', file: file.path }); } } }); this.checkCypressConventions(analysis); this.generateCypressRecommendations(analysis); return analysis; }, // Playwright Analyzer analyzePlaywright: (files) => { const analysis = { framework: 'playwright', tests: [], pages: [], patterns: [], conventions: [], issues: [], recommendations: [] }; files.forEach(file => { if (file.content) { // Analyze Playwright tests const testRegex = /(?:test|test\.describe)\s*\(\s*['"]([^'"]+)['"]/g; let match; while ((match = testRegex.exec(file.content)) !== null) { analysis.tests.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Playwright page objects const pageRegex = /class\s+([A-Z][a-zA-Z0-9_]*)\s*\{/g; while ((match = pageRegex.exec(file.content)) !== null) { analysis.pages.push({ name: match[1], file: file.path, line: this.getLineNumber(file.content, match.index) }); } // Analyze Playwright patterns if (file.content.includes('page.goto(')) { analysis.patterns.push({ type: 'navigation', description: 'Playwright navigation detected', file: file.path }); } if (file.content.includes('page.locator(')) { analysis.patterns.push({ type: 'element_selection', description: 'Playwright element selection detected', file: file.path }); } if (file.content.includes('page.screenshot(')) { analysis.patterns.push({ type: 'screenshot', description: 'Playwright screenshot detected', file: file.path }); } } }); this.checkPlaywrightConventions(analysis); this.generatePlaywrightRecommendations(analysis); return analysis; } }; } async performAnalysis(flags, analyzer) { const results = { timestamp: new Date().toISOString(), path: flags.path, framework: flags.framework || 'all', files: [], analyses: [], summary: {} }; // Find files to analyze const files = await this.findFiles(flags); results.files = files; // Perform framework-specific analysis const frameworks = flags.framework === 'all' ? ['react', 'angular', 'vue', 'cypress', 'playwright'] : [flags.framework]; for (const framework of frameworks) { let analysis = null; switch (framework) { case 'react': analysis = analyzer.analyzeReact(files); break; case 'angular': analysis = analyzer.analyzeAngular(files); break; case 'vue': analysis = analyzer.analyzeVue(files); break; case 'cypress': analysis = analyzer.analyzeCypress(files); break; case 'playwright': analysis = analyzer.analyzePlaywright(files); break; } if (analysis) { results.analyses.push(analysis); } } // Generate summary results.summary = this.generateSummary(results); return results; } async findFiles(flags) { const includePatterns = flags.include.split(','); const excludePatterns = flags.exclude.split(','); const files = []; for (const pattern of includePatterns) { const matches = glob.sync(pattern, { cwd: flags.path, absolute: true, ignore: excludePatterns }); files.push(...matches.map(file => ({ path: file }))); } // Read file contents for (const file of files) { try { const content = await fs.readFile(file.path, 'utf8'); file.content = content; file.size = content.length; file.lines = content.split('\n').length; } catch (error) { if (flags.verbose) { console.warn(chalk.yellow(`āš ļø Could not read file: ${file.path}`)); } } } return files; } getLineNumber(content, position) { return content.substring(0, position).split('\n').length; } checkReactConventions(analysis) { // Check for proper component naming analysis.components.forEach(component => { if (!component.name.match(/^[A-Z]/)) { analysis.conventions.push({ type: 'naming', severity: 'warning', message: `Component ${component.name} should start with uppercase letter`, file: component.file, line: component.line }); } }); // Check for proper hook usage const hookUsage = analysis.hooks.reduce((acc, hook) => { acc[hook.name] = (acc[hook.name] || 0) + 1; return acc; }, {}); if (hookUsage.useState > 10) { analysis.conventions.push({ type: 'performance', severity: 'warning', message: 'Too many useState hooks, consider using useReducer', file: 'multiple files' }); } } checkAngularConventions(analysis) { // Check for proper service naming analysis.services.forEach(service => { if (!service.name.endsWith('Service')) { analysis.conventions.push({ type: 'naming', severity: 'warning', message: `Service ${service.name} should end with 'Service'`, file: service.file, line: service.line }); } }); } checkVueConventions(analysis) { // Check for proper component naming analysis.components.forEach(component => { if (!component.name.match(/^[A-Z]/)) { analysis.conventions.push({ type: 'naming', severity: 'warning', message: `Component ${component.name} should start with uppercase letter`, file: component.file, line: component.line }); } }); } checkCypressConventions(analysis) { // Check for proper test naming analysis.tests.forEach(test => { if (!test.name.match(/^[a-z]/)) { analysis.conventions.push({ type: 'naming', severity: 'warning', message: `Test ${test.name} should start with lowercase letter`, file: test.file, line: test.line }); } }); } checkPlaywrightConventions(analysis) { // Check for proper test naming analysis.tests.forEach(test => { if (!test.name.match(/^[a-z]/)) { analysis.conventions.push({ type: 'naming', severity: 'warning', message: `Test ${test.name} should start with lowercase letter`, file: test.file, line: test.line }); } }); } generateReactRecommendations(analysis) { if (analysis.components.length === 0) { analysis.recommendations.push({ type: 'structure', priority: 'medium', title: 'No React Components Found', description: 'Consider creating React components for better organization', action: 'Create functional or class components for UI elements' }); } if (analysis.hooks.length === 0 && analysis.components.length > 0) { analysis.recommendations.push({ type: 'modern_patterns', priority: 'low', title: 'Consider Using React Hooks', description: 'No React hooks detected in components', action: 'Consider using hooks for state management and side effects' }); } } generateAngularRecommendations(analysis) { if (analysis.components.length === 0) { analysis.recommendations.push({ type: 'structure', priority: 'medium', title: 'No Angular Components Found', description: 'Consider creating Angular components for better organization', action: 'Create Angular components using @Component decorator' }); } if (analysis.services.length === 0 && analysis.components.length > 0) { analysis.recommendations.push({ type: 'architecture', priority: 'low', title: 'Consider Using Angular Services', description: 'No Angular services detected', action: 'Consider creating services for business logic and data management' }); } } generateVueRecommendations(analysis) { if (analysis.components.length === 0) { analysis.recommendations.push({ type: 'structure', priority: 'medium', title: 'No Vue Components Found', description: 'Consider creating Vue components for better organization', action: 'Create Vue components using Single File Components (SFC)' }); } if (analysis.composables.length === 0 && analysis.components.length > 0) { analysis.recommendations.push({ type: 'modern_patterns', priority: 'low', title: 'Consider Using Vue Composables', description: 'No Vue composables detected', action: 'Consider creating composables for reusable logic' }); } } generateCypressRecommendations(analysis) { if (analysis.tests.length === 0) { analysis.recommendations.push({ type: 'testing', priority: 'high', title: 'No Cypress Tests Found', description: 'Consider creating Cypress tests for better test coverage', action: 'Create Cypress tests using describe() and it() functions' }); } if (analysis.commands.length === 0 && analysis.tests.length > 0) { analysis.recommendations.push({ type: 'testing', priority: 'medium', title: 'No Cypress Commands Found', description: 'No Cypress commands detected in tests', action: 'Consider using Cypress commands for element interaction' }); } } generatePlaywrightRecommendations(analysis) { if (analysis.tests.length === 0) { analysis.recommendations.push({ type: 'testing', priority: 'high', title: 'No Playwright Tests Found', description: 'Consider creating Playwright tests for better test coverage', action: 'Create Playwright tests using test() and test.describe() functions' }); } if (analysis.pages.length === 0 && analysis.tests.length > 0) { analysis.recommendations.push({ type: 'architecture', priority: 'medium', title: 'Consider Using Page Object Model', description: 'No Playwright page objects detected', action: 'Consider creating page objects for better test organization' }); } } generateSummary(results) { const summary = { totalFiles: results.files.length, frameworks: results.analyses.map(a => a.framework), totalComponents: 0, totalTests: 0, totalServices: 0, totalPatterns: 0, totalConventions: 0, totalRecommendations: 0 }; results.analyses.forEach(analysis => { summary.totalComponents += analysis.components?.length || 0; summary.totalTests += analysis.tests?.length || 0; summary.totalServices += analysis.services?.length || 0; summary.totalPatterns += analysis.patterns?.length || 0; summary.totalConventions += analysis.conventions?.length || 0; summary.totalRecommendations += analysis.recommendations?.length || 0; }); return summary; } displayResults(results, verbose) { console.log(chalk.green.bold('\nšŸ”§ Framework Analysis Results')); console.log(chalk.gray('=' * 50)); // Summary console.log(chalk.blue.bold('\nšŸ“Š Summary:')); console.log(` Total files: ${results.summary.totalFiles}`); console.log(` Frameworks: ${results.summary.frameworks.join(', ')}`); console.log(` Total components: ${results.summary.totalComponents}`); console.log(` Total tests: ${results.summary.totalTests}`); console.log(` Total services: ${results.summary.totalServices}`); console.log(` Total patterns: ${results.summary.totalPatterns}`); console.log(` Total conventions: ${results.summary.totalConventions}`); console.log(` Total recommendations: ${results.summary.totalRecommendations}`); // Framework-specific results results.analyses.forEach(analysis => { console.log(chalk.blue.bold(`\nšŸ”§ ${analysis.framework.toUpperCase()} Analysis:`)); if (analysis.components?.length > 0) { console.log(` Components: ${analysis.components.length}`); analysis.components.slice(0, 5).forEach(component => { console.log(` - ${component.name} (${path.basename(component.file)})`); }); } if (analysis.tests?.length > 0) { console.log(` Tests: ${analysis.tests.length}`); analysis.tests.slice(0, 5).forEach(test => { console.log(` - ${test.name} (${path.basename(test.file)})`); }); } if (analysis.services?.length > 0) { console.log(` Services: ${analysis.services.length}`); analysis.services.slice(0, 5).forEach(service => { console.log(` - ${service.name} (${path.basename(service.file)})`); }); } if (analysis.patterns?.length > 0) { console.log(` Patterns: ${analysis.patterns.length}`); analysis.patterns.slice(0, 3).forEach(pattern => { console.log(` - ${pattern.type}: ${pattern.description}`); }); } if (analysis.conventions?.length > 0) { console.log(` Conventions: ${analysis.conventions.length}`); analysis.conventions.slice(0, 3).forEach(convention => { const severityColor = convention.severity === 'error' ? chalk.red : convention.severity === 'warning' ? chalk.yellow : chalk.green; console.log(` - ${severityColor(convention.type)}: ${convention.message}`); }); } if (analysis.recommendations?.length > 0) { console.log(` Recommendations: ${analysis.recommendations.length}`); analysis.recommendations.slice(0, 3).forEach(rec => { const priorityColor = rec.priority === 'high' ? chalk.red : rec.priority === 'medium' ? chalk.yellow : chalk.green; console.log(` - ${priorityColor(rec.title)} (${rec.priority})`); console.log(` ${rec.description}`); }); } }); if (verbose) { console.log(chalk.blue.bold('\nšŸ” Detailed Analysis:')); console.log(JSON.stringify(results, null, 2)); } } } module.exports.default = FrameworkAnalyzer;