smartui-migration-tool
Version:
Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI
799 lines (704 loc) ⢠26 kB
JavaScript
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;