prprompts-flutter-generator
Version:
AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp
532 lines (439 loc) ⢠16.7 kB
JavaScript
/**
* RefactorCommand - Main React-to-Flutter conversion orchestrator
* Integrates parsing, generation, AI enhancement, and validation
*/
const fs = require('fs').promises;
const path = require('path');
const { ReactParser } = require('../parsers/ReactParser');
const { CodeGenerator } = require('../generators/CodeGenerator');
const { CleanArchitectureGenerator } = require('../generators/CleanArchitectureGenerator');
const { CodeEnhancer, WidgetOptimizer, TestGenerator } = require('../ai');
const { ValidationOrchestrator, ReportGenerator } = require('../validation');
const ProgressBar = require('./ProgressBar');
const Reporter = require('./Reporter');
const { createModuleLogger } = require('../utils/logger');
const logger = createModuleLogger('RefactorCommand');
class RefactorCommand {
constructor(options = {}) {
this.options = {
reactSource: options.reactSource,
flutterTarget: options.flutterTarget,
ai: options.ai || 'none',
validate: options.validate !== false,
stateManagement: options.stateManagement || 'auto',
architecture: options.architecture || 'clean',
config: options.config,
dryRun: options.dryRun || false,
verbose: options.verbose || false
};
this.stats = {
filesProcessed: 0,
filesGenerated: 0,
components: 0,
widgets: 0,
linesOfCode: {
react: 0,
flutter: 0
},
stateManagement: {},
errors: [],
warnings: []
};
}
/**
* Main conversion workflow
*/
async execute() {
try {
const startTime = Date.now();
logger.info('š Starting React-to-Flutter conversion...\n');
// Step 1: Validate inputs
await this.validateInputs();
// Step 2: Parse React files
const components = await this.parseReactFiles();
// Step 3: Generate Flutter code
const generatedFiles = await this.generateFlutterCode(components);
// Step 4: Enhance with AI (if enabled)
if (this.options.ai !== 'none') {
await this.enhanceWithAI(generatedFiles);
}
// Step 5: Validate output (if enabled)
let validationResults = null;
if (this.options.validate) {
validationResults = await this.validateOutput();
}
// Step 6: Write files (unless dry run)
if (!this.options.dryRun) {
await this.writeFiles(generatedFiles);
}
// Step 7: Generate report
const duration = Date.now() - startTime;
await this.generateReport(validationResults, duration);
logger.success('\nā
Conversion complete!');
return {
success: true,
stats: this.stats,
duration,
validationResults
};
} catch (error) {
logger.error(`\nā Conversion failed: ${error.message}`);
if (this.options.verbose) {
logger.error(error.stack);
}
return {
success: false,
error: error.message,
stats: this.stats
};
}
}
/**
* Validate inputs
*/
async validateInputs() {
logger.info('š Validating inputs...');
// Check React source directory
try {
const stats = await fs.stat(this.options.reactSource);
if (!stats.isDirectory()) {
throw new Error(`React source path is not a directory: ${this.options.reactSource}`);
}
} catch (error) {
// Re-throw with original message if it mentions "not a directory"
if (error.message && error.message.includes('not a directory')) {
throw error;
}
throw new Error(`React source directory not found: ${this.options.reactSource}`);
}
// Check Flutter target (create if doesn't exist)
try {
await fs.access(this.options.flutterTarget);
} catch {
if (!this.options.dryRun) {
logger.info(`Creating target directory: ${this.options.flutterTarget}`);
await fs.mkdir(this.options.flutterTarget, { recursive: true });
}
}
// Validate AI provider
if (this.options.ai !== 'none') {
const validProviders = ['claude', 'qwen', 'gemini', 'none'];
if (!validProviders.includes(this.options.ai)) {
throw new Error(`Invalid AI provider: ${this.options.ai}. Valid options: ${validProviders.join(', ')}`);
}
}
// Validate state management
const validStateManagement = ['bloc', 'cubit', 'provider', 'auto'];
if (!validStateManagement.includes(this.options.stateManagement)) {
throw new Error(`Invalid state management: ${this.options.stateManagement}. Valid options: ${validStateManagement.join(', ')}`);
}
// Validate architecture
const validArchitectures = ['clean', 'mvc', 'mvvm'];
if (!validArchitectures.includes(this.options.architecture)) {
throw new Error(`Invalid architecture: ${this.options.architecture}. Valid options: ${validArchitectures.join(', ')}`);
}
logger.success('ā
Inputs validated\n');
}
/**
* Parse React files
*/
async parseReactFiles() {
logger.info('š Parsing React files...');
const parser = new ReactParser();
const reactFiles = await this.findReactFiles(this.options.reactSource);
const progressBar = new ProgressBar({
total: reactFiles.length,
format: 'Parsing React files... {bar} {percentage}% ({value}/{total} files) [{eta}s]'
});
const components = [];
for (const file of reactFiles) {
try {
const content = await fs.readFile(file, 'utf8');
this.stats.linesOfCode.react += content.split('\n').length;
const component = parser.parse(content, file);
this.stats.filesProcessed++;
if (component) {
components.push(component);
this.stats.components++;
// Track state management patterns
if (component.stateManagement) {
this.stats.stateManagement[component.stateManagement] =
(this.stats.stateManagement[component.stateManagement] || 0) + 1;
}
}
progressBar.increment();
if (this.options.verbose) {
logger.info(` ā Parsed: ${path.basename(file)}`);
}
} catch (error) {
this.stats.errors.push({
file,
error: error.message
});
logger.warn(` ā Error parsing ${path.basename(file)}: ${error.message}`);
progressBar.increment();
}
}
progressBar.stop();
logger.success(`ā
Parsed ${components.length} components\n`);
return components;
}
/**
* Generate Flutter code
*/
async generateFlutterCode(components) {
logger.info('šļø Generating Flutter code...');
const generator = this.options.architecture === 'clean'
? new CleanArchitectureGenerator({
outputPath: this.options.flutterTarget,
config: {
stateManagement: this.options.stateManagement
}
})
: new CodeGenerator({
architecture: this.options.architecture,
stateManagement: this.options.stateManagement
});
const progressBar = new ProgressBar({
total: components.length,
format: 'Generating Flutter code... {bar} {percentage}% ({value}/{total} components) [ETA: {eta}s]'
});
const generatedFiles = {};
for (const component of components) {
try {
const files = await generator.generate(component);
// Merge files
Object.assign(generatedFiles, files);
// Update stats
this.stats.filesGenerated += Object.keys(files).length;
this.stats.widgets += component.children?.length || 0;
// Count lines
for (const content of Object.values(files)) {
if (typeof content === 'string') {
this.stats.linesOfCode.flutter += content.split('\n').length;
}
}
progressBar.increment();
if (this.options.verbose) {
logger.info(` ā Generated: ${component.name} ā ${Object.keys(files).length} files`);
}
} catch (error) {
this.stats.errors.push({
component: component.name,
error: error.message
});
logger.warn(` ā Error generating ${component.name}: ${error.message}`);
progressBar.increment();
}
}
progressBar.stop();
logger.success(`ā
Generated ${this.stats.filesGenerated} Flutter files\n`);
return generatedFiles;
}
/**
* Enhance with AI
*/
async enhanceWithAI(generatedFiles) {
logger.info(`š¤ Enhancing with ${this.options.ai.toUpperCase()}...`);
const enhancer = new CodeEnhancer({ provider: this.options.ai });
const optimizer = new WidgetOptimizer({ provider: this.options.ai });
const testGenerator = new TestGenerator({ provider: this.options.ai });
const progressBar = new ProgressBar({
total: Object.keys(generatedFiles).length,
format: 'AI Enhancement... {bar} {percentage}% ({value}/{total} files) [ETA: {eta}s]'
});
for (const [filePath, content] of Object.entries(generatedFiles)) {
try {
// Enhance code quality
const enhanced = await enhancer.enhance(content, { filePath });
// Optimize widgets
if (filePath.includes('/widgets/') || filePath.includes('/pages/')) {
const optimized = await optimizer.optimize(null, enhanced);
generatedFiles[filePath] = optimized.code || optimized;
} else {
generatedFiles[filePath] = enhanced.enhancedCode || enhanced;
}
// Generate tests
if (!filePath.includes('_test.dart')) {
const testPath = filePath.replace(/\.dart$/, '_test.dart').replace('/lib/', '/test/');
const testCode = await testGenerator.generateTests(null, generatedFiles[filePath]);
if (testCode && testCode.code) {
generatedFiles[testPath] = testCode.code;
this.stats.filesGenerated++;
}
}
progressBar.increment();
} catch (error) {
this.stats.warnings.push({
file: filePath,
warning: `AI enhancement failed: ${error.message}`
});
progressBar.increment();
}
}
progressBar.stop();
logger.success('ā
AI enhancement complete\n');
}
/**
* Validate output
*/
async validateOutput() {
logger.info('š Validating Flutter code...');
const orchestrator = new ValidationOrchestrator({
projectPath: this.options.flutterTarget,
config: 'standard'
});
const results = await orchestrator.validate();
// Display results summary
const score = results.summary.overallScore;
const scoreColor = score >= 85 ? 'green' : score >= 70 ? 'yellow' : 'red';
logger.log(`\n Overall Score: ${this.colorize(score + '/100', scoreColor)} (${this.getGrade(score)})`);
logger.log(` ā Code Quality: ${results.summary.codeScore}/100`);
logger.log(` ā Security: ${results.summary.securityScore}/100`);
logger.log(` ā Performance: ${results.summary.performanceScore}/100`);
logger.log(` ā Accessibility: ${results.summary.accessibilityScore}/100\n`);
// Show issues summary
if (results.issues.length > 0) {
const errors = results.issues.filter(i => i.severity === 'error');
const warnings = results.issues.filter(i => i.severity === 'warning');
logger.warn(` Issues: ${errors.length} errors, ${warnings.length} warnings`);
}
return results;
}
/**
* Write files to disk
*/
async writeFiles(generatedFiles) {
logger.info('š¾ Writing files...');
const progressBar = new ProgressBar({
total: Object.keys(generatedFiles).length,
format: 'Writing files... {bar} {percentage}% ({value}/{total} files)'
});
for (const [filePath, content] of Object.entries(generatedFiles)) {
try {
const fullPath = path.join(this.options.flutterTarget, filePath);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content, 'utf8');
progressBar.increment();
} catch (error) {
this.stats.errors.push({
file: filePath,
error: `Write failed: ${error.message}`
});
progressBar.increment();
}
}
progressBar.stop();
logger.success(`ā
Wrote ${Object.keys(generatedFiles).length} files\n`);
}
/**
* Generate report
*/
async generateReport(validationResults, duration) {
logger.info('š Generating report...\n');
const reporter = new Reporter();
// Conversion summary
reporter.section('Conversion Summary');
reporter.table([
['Files converted', this.stats.filesProcessed],
['Components', this.stats.components],
['Widgets', this.stats.widgets],
['Files generated', this.stats.filesGenerated],
['React LOC', this.stats.linesOfCode.react],
['Flutter LOC', this.stats.linesOfCode.flutter],
['Duration', `${(duration / 1000).toFixed(1)}s`]
]);
// State management breakdown
if (Object.keys(this.stats.stateManagement).length > 0) {
reporter.section('State Management');
const stateTable = Object.entries(this.stats.stateManagement).map(([type, count]) => [
type.toUpperCase(), `${count} components`
]);
reporter.table(stateTable);
}
// Validation results
if (validationResults) {
reporter.section('Validation Results');
reporter.validationSummary(validationResults);
}
// Errors and warnings
if (this.stats.errors.length > 0) {
reporter.section(`Errors (${this.stats.errors.length})`);
this.stats.errors.slice(0, 5).forEach(err => {
logger.error(` ā ${err.file || err.component}: ${err.error}`);
});
if (this.stats.errors.length > 5) {
logger.warn(` ... and ${this.stats.errors.length - 5} more errors`);
}
}
if (this.stats.warnings.length > 0) {
reporter.section(`Warnings (${this.stats.warnings.length})`);
this.stats.warnings.slice(0, 5).forEach(warn => {
logger.warn(` ā ${warn.file}: ${warn.warning}`);
});
if (this.stats.warnings.length > 5) {
logger.warn(` ... and ${this.stats.warnings.length - 5} more warnings`);
}
}
// Recommendations
reporter.section('Next Steps');
logger.info(' 1. Review generated code in:', this.options.flutterTarget);
logger.info(' 2. Run: cd', this.options.flutterTarget, '&& flutter pub get');
logger.info(' 3. Run: flutter analyze');
logger.info(' 4. Run: flutter test');
if (validationResults) {
const reportPath = path.join(this.options.flutterTarget, '.prprompts', 'validation-report.html');
logger.info(` 5. View validation report: ${reportPath}`);
}
console.log('');
}
/**
* Find React files recursively
*/
async findReactFiles(dir) {
const files = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules, build, dist, etc.
if (!['node_modules', 'build', 'dist', '.git'].includes(entry.name)) {
files.push(...await this.findReactFiles(fullPath));
}
} else if (entry.isFile()) {
// Include .jsx, .tsx, .js, .ts files
if (/\.(jsx|tsx|js|ts)$/.test(entry.name)) {
files.push(fullPath);
}
}
}
return files;
}
/**
* Get grade from score
*/
getGrade(score) {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
/**
* Colorize text (simple version)
*/
colorize(text, color) {
const colors = {
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
reset: '\x1b[0m'
};
// Only add reset code if a valid color was used
if (colors[color]) {
return `${colors[color]}${text}${colors.reset}`;
}
return text;
}
}
module.exports = RefactorCommand;