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

727 lines (628 loc) 19.7 kB
/** * IRStorage - Intermediate Representation Storage System * * Handles serialization, deserialization, and management of IR (Intermediate Representation) * for React-to-Flutter conversion pipeline. * * Features: * - Serialize ComponentModel to JSON files * - Organize by feature/component hierarchy * - Support incremental conversion * - Generate feature manifests * - Cache management * - Versioning support */ const fs = require('fs-extra'); const path = require('path'); const crypto = require('crypto'); const { ComponentModel } = require('../models/ComponentModel'); class IRStorage { constructor(config = {}) { this.config = { basePath: config.basePath || 'tmp/ir', prettyPrint: config.prettyPrint !== false, compress: config.compress || false, version: config.version || '1.0.0', enableCache: config.enableCache !== false, cacheExpiry: config.cacheExpiry || 3600000, // 1 hour in milliseconds generateManifest: config.generateManifest !== false, verbose: config.verbose || false, }; this.cache = new Map(); this.manifest = { version: this.config.version, features: {}, components: {}, metadata: {}, }; } /** * Initialize storage directories * @returns {Promise<void>} */ async initialize() { await fs.ensureDir(this.config.basePath); await fs.ensureDir(path.join(this.config.basePath, '.cache')); await fs.ensureDir(path.join(this.config.basePath, 'features')); await fs.ensureDir(path.join(this.config.basePath, 'components')); // Load existing manifest if available const manifestPath = path.join(this.config.basePath, 'manifest.json'); if (await fs.pathExists(manifestPath)) { this.manifest = await fs.readJson(manifestPath); } } /** * Save ComponentModel to IR storage * @param {ComponentModel} component - Component model to save * @param {Object} options - Save options * @returns {Promise<Object>} Save result */ async saveComponent(component, options = {}) { const feature = options.feature || this.detectFeature(component); const componentName = component.name || 'UnnamedComponent'; const componentPath = this.getComponentPath(feature, componentName); // Create IR representation const ir = await this.createIR(component); // Add metadata ir.metadata = { ...ir.metadata, savedAt: new Date().toISOString(), version: this.config.version, hash: this.generateHash(ir), feature, path: componentPath, }; // Ensure directory exists await fs.ensureDir(path.dirname(componentPath)); // Save to file const jsonContent = this.config.prettyPrint ? JSON.stringify(ir, null, 2) : JSON.stringify(ir); if (this.config.compress) { const zlib = require('zlib'); const compressed = zlib.gzipSync(Buffer.from(jsonContent)); await fs.writeFile(componentPath + '.gz', compressed); } else { await fs.writeFile(componentPath, jsonContent, 'utf8'); } // Update manifest await this.updateManifest(feature, componentName, ir); // Update cache if (this.config.enableCache) { this.cache.set(componentPath, { ir, timestamp: Date.now(), }); } if (this.config.verbose) { console.log(`Saved IR: ${componentPath}`); } return { success: true, path: componentPath, feature, component: componentName, hash: ir.metadata.hash, size: jsonContent.length, }; } /** * Load ComponentModel from IR storage * @param {string} feature - Feature name * @param {string} componentName - Component name * @returns {Promise<Object>} Loaded IR */ async loadComponent(feature, componentName) { const componentPath = this.getComponentPath(feature, componentName); // Check cache first if (this.config.enableCache && this.cache.has(componentPath)) { const cached = this.cache.get(componentPath); if (Date.now() - cached.timestamp < this.config.cacheExpiry) { if (this.config.verbose) { console.log(`Loaded from cache: ${componentPath}`); } return cached.ir; } } // Check if file exists let filePath = componentPath; if (this.config.compress) { filePath = componentPath + '.gz'; } if (!await fs.pathExists(filePath)) { throw new Error(`IR file not found: ${filePath}`); } // Read from file let jsonContent; if (this.config.compress) { const zlib = require('zlib'); const compressed = await fs.readFile(filePath); jsonContent = zlib.gunzipSync(compressed).toString('utf8'); } else { jsonContent = await fs.readFile(filePath, 'utf8'); } const ir = JSON.parse(jsonContent); // Update cache if (this.config.enableCache) { this.cache.set(componentPath, { ir, timestamp: Date.now(), }); } if (this.config.verbose) { console.log(`Loaded IR: ${componentPath}`); } return ir; } /** * Save multiple components in batch * @param {Array<ComponentModel>} components - Components to save * @param {Object} options - Save options * @returns {Promise<Array>} Save results */ async saveComponents(components, options = {}) { const results = []; for (const component of components) { try { const result = await this.saveComponent(component, options); results.push(result); } catch (error) { results.push({ success: false, component: component.name, error: error.message, }); } } return results; } /** * Load all components for a feature * @param {string} feature - Feature name * @returns {Promise<Array>} Loaded components */ async loadFeatureComponents(feature) { const featurePath = path.join(this.config.basePath, 'features', feature); if (!await fs.pathExists(featurePath)) { return []; } const files = await fs.readdir(featurePath); const components = []; for (const file of files) { if (file.endsWith('.json') || file.endsWith('.json.gz')) { const componentName = path.basename(file, path.extname(file)); try { const ir = await this.loadComponent(feature, componentName.replace('.json', '')); components.push(ir); } catch (error) { console.warn(`Failed to load component ${componentName}: ${error.message}`); } } } return components; } /** * Create IR representation from ComponentModel * @param {ComponentModel} component - Component model * @returns {Object} IR representation */ async createIR(component) { return { name: component.name, type: component.type, path: component.path, props: this.serializeProps(component.props), state: this.serializeState(component.state), hooks: this.serializeHooks(component.hooks), methods: this.serializeMethods(component.methods), jsx: this.serializeJSX(component.jsx), styles: component.styles, imports: component.imports, exports: component.exports, dependencies: this.extractDependencies(component), metadata: { lines: component.lines, complexity: this.calculateComplexity(component), hasTests: component.hasTests, isAsync: this.hasAsyncOperations(component), stateManagement: component.stateManagement, }, }; } /** * Serialize props for IR * @param {Array} props - Component props * @returns {Array} Serialized props */ serializeProps(props) { if (!props) return []; return props.map(prop => ({ name: prop.name, type: prop.type || 'any', required: prop.required || false, defaultValue: prop.defaultValue, description: prop.description, })); } /** * Serialize state for IR * @param {Array} state - Component state * @returns {Array} Serialized state */ serializeState(state) { if (!state) return []; return state.map(s => ({ name: s.name, type: s.type || 'dynamic', initialValue: s.initialValue, setter: s.setter, usage: s.usage || [], })); } /** * Serialize hooks for IR * @param {Array} hooks - Component hooks * @returns {Array} Serialized hooks */ serializeHooks(hooks) { if (!hooks) return []; return hooks.map(hook => ({ type: hook.type, dependencies: hook.dependencies || [], body: hook.body, async: hook.async || false, })); } /** * Serialize methods for IR * @param {Array} methods - Component methods * @returns {Array} Serialized methods */ serializeMethods(methods) { if (!methods) return []; return methods.map(method => ({ name: method.name, params: method.params || [], body: method.body, async: method.async || false, isEventHandler: method.isEventHandler || false, })); } /** * Serialize JSX for IR * @param {Object} jsx - JSX tree * @returns {Object} Serialized JSX */ serializeJSX(jsx) { if (!jsx) return null; return this.traverseJSXTree(jsx); } /** * Traverse and serialize JSX tree * @param {Object} node - JSX node * @returns {Object} Serialized node */ traverseJSXTree(node) { if (!node) return null; const serialized = { type: node.type, tag: node.tag, props: node.props || {}, children: [], }; if (node.children && Array.isArray(node.children)) { serialized.children = node.children.map(child => { if (typeof child === 'string') { return { type: 'text', value: child }; } return this.traverseJSXTree(child); }); } return serialized; } /** * Extract dependencies from component * @param {ComponentModel} component - Component model * @returns {Object} Dependencies */ extractDependencies(component) { const deps = { internal: [], external: [], assets: [], }; // Extract from imports if (component.imports) { for (const imp of component.imports) { if (imp.startsWith('.') || imp.startsWith('/')) { deps.internal.push(imp); } else { deps.external.push(imp); } // Check for asset imports if (imp.match(/\.(png|jpg|svg|gif|json)$/)) { deps.assets.push(imp); } } } return deps; } /** * Calculate component complexity * @param {ComponentModel} component - Component model * @returns {number} Complexity score */ calculateComplexity(component) { let complexity = 1; // Add complexity for state complexity += (component.state?.length || 0) * 2; // Add complexity for hooks complexity += (component.hooks?.length || 0) * 3; // Add complexity for methods complexity += (component.methods?.length || 0); // Add complexity for conditional rendering if (component.jsx) { complexity += this.countConditionals(component.jsx); } return complexity; } /** * Count conditional expressions in JSX * @param {Object} jsx - JSX tree * @returns {number} Conditional count */ countConditionals(jsx) { let count = 0; const traverse = (node) => { if (!node) return; if (node.type === 'conditional' || node.type === 'ternary') { count++; } if (node.children && Array.isArray(node.children)) { node.children.forEach(traverse); } }; traverse(jsx); return count; } /** * Check if component has async operations * @param {ComponentModel} component - Component model * @returns {boolean} Has async */ hasAsyncOperations(component) { // Check hooks for async if (component.hooks?.some(h => h.async)) { return true; } // Check methods for async if (component.methods?.some(m => m.async)) { return true; } return false; } /** * Detect feature from component * @param {ComponentModel} component - Component model * @returns {string} Feature name */ detectFeature(component) { // Try to detect from path if (component.path) { const parts = component.path.split(/[/\\]/); // Look for common feature indicators const featureIndicators = ['features', 'modules', 'pages', 'screens', 'views']; for (let i = 0; i < parts.length; i++) { if (featureIndicators.includes(parts[i].toLowerCase()) && i + 1 < parts.length) { return parts[i + 1]; } } // Use parent directory as feature if (parts.length >= 2) { return parts[parts.length - 2]; } } return 'common'; } /** * Get component path in storage * @param {string} feature - Feature name * @param {string} componentName - Component name * @returns {string} Component path */ getComponentPath(feature, componentName) { const safeName = componentName.replace(/[^a-zA-Z0-9-_]/g, '_'); return path.join(this.config.basePath, 'features', feature, `${safeName}.json`); } /** * Update manifest with component info * @param {string} feature - Feature name * @param {string} componentName - Component name * @param {Object} ir - IR representation * @returns {Promise<void>} */ async updateManifest(feature, componentName, ir) { // Update feature entry if (!this.manifest.features[feature]) { this.manifest.features[feature] = { components: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; } else { this.manifest.features[feature].updatedAt = new Date().toISOString(); } // Add component if not exists if (!this.manifest.features[feature].components.includes(componentName)) { this.manifest.features[feature].components.push(componentName); } // Update component entry this.manifest.components[`${feature}/${componentName}`] = { feature, name: componentName, type: ir.type, hash: ir.metadata.hash, complexity: ir.metadata.complexity, dependencies: ir.dependencies, savedAt: ir.metadata.savedAt, }; // Update metadata this.manifest.metadata = { totalFeatures: Object.keys(this.manifest.features).length, totalComponents: Object.keys(this.manifest.components).length, lastUpdated: new Date().toISOString(), }; // Save manifest if (this.config.generateManifest) { const manifestPath = path.join(this.config.basePath, 'manifest.json'); await fs.writeJson(manifestPath, this.manifest, { spaces: 2 }); } } /** * Generate hash for IR content * @param {Object} ir - IR representation * @returns {string} Hash */ generateHash(ir) { const content = JSON.stringify({ name: ir.name, type: ir.type, props: ir.props, state: ir.state, jsx: ir.jsx, }); return crypto.createHash('md5').update(content).digest('hex'); } /** * Get manifest * @returns {Promise<Object>} Manifest */ async getManifest() { const manifestPath = path.join(this.config.basePath, 'manifest.json'); if (await fs.pathExists(manifestPath)) { return await fs.readJson(manifestPath); } return this.manifest; } /** * Clear cache */ clearCache() { this.cache.clear(); if (this.config.verbose) { console.log('IR cache cleared'); } } /** * Clear all IR storage * @returns {Promise<void>} */ async clearStorage() { await fs.emptyDir(this.config.basePath); this.cache.clear(); this.manifest = { version: this.config.version, features: {}, components: {}, metadata: {}, }; if (this.config.verbose) { console.log('IR storage cleared'); } } /** * Generate conversion report * @returns {Promise<Object>} Conversion report */ async generateReport() { const manifest = await this.getManifest(); const report = { summary: { features: manifest.metadata?.totalFeatures || 0, components: manifest.metadata?.totalComponents || 0, lastUpdated: manifest.metadata?.lastUpdated, }, features: {}, complexity: { simple: 0, // complexity < 5 moderate: 0, // complexity 5-15 complex: 0, // complexity > 15 }, }; // Analyze features for (const [featureName, feature] of Object.entries(manifest.features || {})) { report.features[featureName] = { componentCount: feature.components.length, components: [], }; // Analyze components for (const componentName of feature.components) { const key = `${featureName}/${componentName}`; const component = manifest.components[key]; if (component) { report.features[featureName].components.push({ name: componentName, type: component.type, complexity: component.complexity, }); // Update complexity distribution if (component.complexity < 5) { report.complexity.simple++; } else if (component.complexity <= 15) { report.complexity.moderate++; } else { report.complexity.complex++; } } } } return report; } /** * Check if component needs update * @param {string} feature - Feature name * @param {string} componentName - Component name * @param {string} hash - Current hash * @returns {Promise<boolean>} Needs update */ async needsUpdate(feature, componentName, hash) { try { const ir = await this.loadComponent(feature, componentName); return ir.metadata.hash !== hash; } catch (error) { // Component doesn't exist, needs creation return true; } } /** * Get storage statistics * @returns {Promise<Object>} Storage statistics */ async getStatistics() { const stats = { totalSize: 0, fileCount: 0, features: {}, cacheSize: this.cache.size, }; const basePath = this.config.basePath; if (await fs.pathExists(basePath)) { const walkDir = async (dir) => { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory()) { await walkDir(fullPath); } else if (item.isFile()) { const stat = await fs.stat(fullPath); stats.totalSize += stat.size; stats.fileCount++; } } }; await walkDir(basePath); } return stats; } } module.exports = IRStorage;