UNPKG

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

567 lines (506 loc) 16 kB
/** * ConversionContext.js * Maintains the state and context during React to Flutter conversion * * This context tracks all mappings, errors, warnings, and metadata * accumulated during the conversion process. */ const { ComponentModel } = require('./ComponentModel'); const { WidgetModel } = require('./WidgetModel'); /** * Conversion status * @enum {string} */ const ConversionStatus = { NOT_STARTED: 'not_started', IN_PROGRESS: 'in_progress', COMPLETED: 'completed', FAILED: 'failed', PARTIAL: 'partial', }; /** * Conversion phase * @enum {string} */ const ConversionPhase = { PARSING: 'parsing', MAPPING: 'mapping', GENERATION: 'generation', VALIDATION: 'validation', OUTPUT: 'output', }; /** * Represents a conversion mapping between React and Flutter */ class ConversionMapping { /** * @param {Object} options * @param {string} options.reactElement - React element/component name * @param {string} options.flutterWidget - Flutter widget name * @param {Object} [options.propMapping={}] - Property name mappings * @param {string} [options.notes] - Additional notes */ constructor({ reactElement, flutterWidget, propMapping = {}, notes = null }) { this._validate({ reactElement, flutterWidget }); this.reactElement = reactElement; this.flutterWidget = flutterWidget; this.propMapping = propMapping; this.notes = notes; } _validate({ reactElement, flutterWidget }) { if (!reactElement || typeof reactElement !== 'string') { throw new Error('ConversionMapping: reactElement is required and must be a string'); } if (!flutterWidget || typeof flutterWidget !== 'string') { throw new Error('ConversionMapping: flutterWidget is required and must be a string'); } } toJSON() { return { reactElement: this.reactElement, flutterWidget: this.flutterWidget, propMapping: this.propMapping, notes: this.notes, }; } static fromJSON(json) { return new ConversionMapping(json); } } /** * Represents a conversion error or warning */ class ConversionIssue { /** * @param {Object} options * @param {string} options.type - 'error' or 'warning' * @param {string} options.message - Issue message * @param {string} [options.phase] - Conversion phase where issue occurred * @param {string} [options.component] - Component/widget name * @param {string} [options.filePath] - File path where issue occurred * @param {number} [options.line] - Line number * @param {Object} [options.context={}] - Additional context */ constructor({ type, message, phase = null, component = null, filePath = null, line = null, context = {}, }) { this._validate({ type, message }); this.type = type; this.message = message; this.phase = phase; this.component = component; this.filePath = filePath; this.line = line; this.context = context; this.timestamp = new Date().toISOString(); } _validate({ type, message }) { if (!type || !['error', 'warning'].includes(type)) { throw new Error('ConversionIssue: type must be "error" or "warning"'); } if (!message || typeof message !== 'string') { throw new Error('ConversionIssue: message is required and must be a string'); } } toString() { let str = `[${this.type.toUpperCase()}]`; if (this.phase) str += ` [${this.phase}]`; if (this.component) str += ` ${this.component}`; if (this.filePath) str += ` (${this.filePath}${this.line ? `:${this.line}` : ''})`; str += `: ${this.message}`; return str; } toJSON() { return { type: this.type, message: this.message, phase: this.phase, component: this.component, filePath: this.filePath, line: this.line, context: this.context, timestamp: this.timestamp, }; } static fromJSON(json) { const issue = new ConversionIssue(json); issue.timestamp = json.timestamp; return issue; } } /** * Main conversion context that tracks the entire conversion process */ class ConversionContext { /** * @param {Object} options * @param {string} [options.projectName] - Project name * @param {string} [options.sourceRoot] - React source root directory * @param {string} [options.targetRoot] - Flutter target root directory */ constructor({ projectName = null, sourceRoot = null, targetRoot = null } = {}) { this.projectName = projectName; this.sourceRoot = sourceRoot; this.targetRoot = targetRoot; // Conversion state this.status = ConversionStatus.NOT_STARTED; this.currentPhase = null; this.startTime = null; this.endTime = null; // Components and widgets this.components = new Map(); // Map<string, ComponentModel> this.widgets = new Map(); // Map<string, WidgetModel> // Mappings this.elementMappings = new Map(); // Map<string, ConversionMapping> this.typeMappings = new Map(); // Map<string, string> - Type conversions this.packageMappings = new Map(); // Map<string, string> - Package/import conversions // Issues this.errors = []; this.warnings = []; // Metadata this.metadata = { reactVersion: null, flutterVersion: null, dependencies: {}, statistics: { totalComponents: 0, convertedComponents: 0, failedComponents: 0, totalFiles: 0, generatedFiles: 0, }, }; // Options this.options = { preserveComments: true, generateTests: true, useNullSafety: true, stateManagementDefault: 'bloc', }; } /** * Start conversion process * @param {string} phase - Starting phase */ start(phase = ConversionPhase.PARSING) { this.status = ConversionStatus.IN_PROGRESS; this.currentPhase = phase; this.startTime = new Date().toISOString(); } /** * Complete conversion process * @param {boolean} [success=true] - Whether conversion was successful */ complete(success = true) { this.status = success ? ConversionStatus.COMPLETED : ConversionStatus.FAILED; this.currentPhase = null; this.endTime = new Date().toISOString(); } /** * Set current conversion phase * @param {string} phase */ setPhase(phase) { if (!Object.values(ConversionPhase).includes(phase)) { throw new Error(`Invalid phase: ${phase}`); } this.currentPhase = phase; } /** * Add a React component * @param {ComponentModel} component */ addComponent(component) { if (!(component instanceof ComponentModel)) { throw new Error('ConversionContext: component must be an instance of ComponentModel'); } this.components.set(component.name, component); this.metadata.statistics.totalComponents++; } /** * Get a component by name * @param {string} name * @returns {ComponentModel|undefined} */ getComponent(name) { return this.components.get(name); } /** * Add a Flutter widget * @param {WidgetModel} widget */ addWidget(widget) { if (!(widget instanceof WidgetModel)) { throw new Error('ConversionContext: widget must be an instance of WidgetModel'); } this.widgets.set(widget.name, widget); this.metadata.statistics.convertedComponents++; } /** * Get a widget by name * @param {string} name * @returns {WidgetModel|undefined} */ getWidget(name) { return this.widgets.get(name); } /** * Add an element mapping * @param {ConversionMapping|Object} mapping */ addMapping(mapping) { const conversionMapping = mapping instanceof ConversionMapping ? mapping : new ConversionMapping(mapping); this.elementMappings.set(conversionMapping.reactElement, conversionMapping); } /** * Get Flutter widget for React element * @param {string} reactElement * @returns {string|null} */ getFlutterWidget(reactElement) { const mapping = this.elementMappings.get(reactElement); return mapping ? mapping.flutterWidget : null; } /** * Add a type mapping * @param {string} reactType - React/TypeScript type * @param {string} dartType - Dart type */ addTypeMapping(reactType, dartType) { this.typeMappings.set(reactType, dartType); } /** * Get Dart type for React type * @param {string} reactType * @returns {string|null} */ getDartType(reactType) { return this.typeMappings.get(reactType) || null; } /** * Add a package mapping * @param {string} reactPackage - React package name * @param {string} dartPackage - Dart package name */ addPackageMapping(reactPackage, dartPackage) { this.packageMappings.set(reactPackage, dartPackage); } /** * Get Dart package for React package * @param {string} reactPackage * @returns {string|null} */ getDartPackage(reactPackage) { return this.packageMappings.get(reactPackage) || null; } /** * Add an error * @param {ConversionIssue|Object} error */ addError(error) { const issue = error instanceof ConversionIssue ? error : new ConversionIssue({ type: 'error', ...error }); this.errors.push(issue); } /** * Add a warning * @param {ConversionIssue|Object} warning */ addWarning(warning) { const issue = warning instanceof ConversionIssue ? warning : new ConversionIssue({ type: 'warning', ...warning }); this.warnings.push(issue); } /** * Check if conversion has errors * @returns {boolean} */ hasErrors() { return this.errors.length > 0; } /** * Check if conversion has warnings * @returns {boolean} */ hasWarnings() { return this.warnings.length > 0; } /** * Get all issues (errors + warnings) * @returns {ConversionIssue[]} */ getAllIssues() { return [...this.errors, ...this.warnings]; } /** * Get issues by phase * @param {string} phase * @returns {ConversionIssue[]} */ getIssuesByPhase(phase) { return this.getAllIssues().filter(issue => issue.phase === phase); } /** * Get issues by component * @param {string} component * @returns {ConversionIssue[]} */ getIssuesByComponent(component) { return this.getAllIssues().filter(issue => issue.component === component); } /** * Calculate conversion duration in seconds * @returns {number|null} */ getDuration() { if (!this.startTime) return null; const end = this.endTime ? new Date(this.endTime) : new Date(); const start = new Date(this.startTime); return (end - start) / 1000; } /** * Get conversion statistics * @returns {Object} */ getStatistics() { return { ...this.metadata.statistics, duration: this.getDuration(), successRate: this.metadata.statistics.totalComponents > 0 ? (this.metadata.statistics.convertedComponents / this.metadata.statistics.totalComponents * 100).toFixed(2) + '%' : 'N/A', errorCount: this.errors.length, warningCount: this.warnings.length, }; } /** * Generate conversion summary * @returns {string} */ getSummary() { const stats = this.getStatistics(); return `Conversion Summary: Status: ${this.status} Duration: ${stats.duration ? stats.duration.toFixed(2) + 's' : 'N/A'} Components: ${stats.convertedComponents}/${stats.totalComponents} (${stats.successRate}) Errors: ${stats.errorCount} Warnings: ${stats.warningCount}`; } /** * Initialize default mappings */ initializeDefaultMappings() { // Common React to Flutter widget mappings const defaultMappings = [ { reactElement: 'View', flutterWidget: 'Container' }, { reactElement: 'Text', flutterWidget: 'Text' }, { reactElement: 'Image', flutterWidget: 'Image' }, { reactElement: 'TouchableOpacity', flutterWidget: 'InkWell' }, { reactElement: 'ScrollView', flutterWidget: 'SingleChildScrollView' }, { reactElement: 'FlatList', flutterWidget: 'ListView' }, { reactElement: 'TextInput', flutterWidget: 'TextField' }, { reactElement: 'Button', flutterWidget: 'ElevatedButton' }, { reactElement: 'Modal', flutterWidget: 'Dialog' }, { reactElement: 'SafeAreaView', flutterWidget: 'SafeArea' }, ]; defaultMappings.forEach(mapping => this.addMapping(mapping)); // Common type mappings this.addTypeMapping('string', 'String'); this.addTypeMapping('number', 'num'); this.addTypeMapping('boolean', 'bool'); this.addTypeMapping('any', 'dynamic'); this.addTypeMapping('void', 'void'); this.addTypeMapping('null', 'Null'); this.addTypeMapping('undefined', 'Null'); } /** * Convert to JSON * @returns {Object} */ toJSON() { return { projectName: this.projectName, sourceRoot: this.sourceRoot, targetRoot: this.targetRoot, status: this.status, currentPhase: this.currentPhase, startTime: this.startTime, endTime: this.endTime, components: Array.from(this.components.entries()).map(([name, component]) => ({ name, component: component.toJSON(), })), widgets: Array.from(this.widgets.entries()).map(([name, widget]) => ({ name, widget: widget.toJSON(), })), elementMappings: Array.from(this.elementMappings.entries()).map(([name, mapping]) => ({ name, mapping: mapping.toJSON(), })), typeMappings: Array.from(this.typeMappings.entries()), packageMappings: Array.from(this.packageMappings.entries()), errors: this.errors.map(e => e.toJSON()), warnings: this.warnings.map(w => w.toJSON()), metadata: this.metadata, options: this.options, }; } /** * Create from JSON * @param {Object} json * @returns {ConversionContext} */ static fromJSON(json) { const context = new ConversionContext({ projectName: json.projectName, sourceRoot: json.sourceRoot, targetRoot: json.targetRoot, }); context.status = json.status; context.currentPhase = json.currentPhase; context.startTime = json.startTime; context.endTime = json.endTime; // Restore components json.components?.forEach(({ name, component }) => { context.components.set(name, ComponentModel.fromJSON(component)); }); // Restore widgets json.widgets?.forEach(({ name, widget }) => { context.widgets.set(name, WidgetModel.fromJSON(widget)); }); // Restore mappings json.elementMappings?.forEach(({ name, mapping }) => { context.elementMappings.set(name, ConversionMapping.fromJSON(mapping)); }); json.typeMappings?.forEach(([reactType, dartType]) => { context.typeMappings.set(reactType, dartType); }); json.packageMappings?.forEach(([reactPackage, dartPackage]) => { context.packageMappings.set(reactPackage, dartPackage); }); // Restore issues json.errors?.forEach(error => { context.errors.push(ConversionIssue.fromJSON(error)); }); json.warnings?.forEach(warning => { context.warnings.push(ConversionIssue.fromJSON(warning)); }); context.metadata = json.metadata || context.metadata; context.options = json.options || context.options; return context; } } module.exports = { ConversionContext, ConversionMapping, ConversionIssue, ConversionStatus, ConversionPhase, };