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

448 lines (380 loc) 11.4 kB
/** * importResolver.js * Resolves import paths and tracks dependencies * * Handles relative imports, node_modules, aliases, and asset imports */ const path = require('path'); const { createModuleLogger } = require('../../utils/logger'); const logger = createModuleLogger('ImportResolver'); /** * Import types */ const ImportType = { REACT: 'react', REACT_NATIVE: 'react_native', NODE_MODULE: 'node_module', RELATIVE: 'relative', ABSOLUTE: 'absolute', ALIAS: 'alias', ASSET: 'asset', STYLE: 'style', }; /** * Import resolver class */ class ImportResolver { /** * @param {Object} options * @param {string} options.basePath - Base path of the project * @param {Object} options.aliases - Path aliases (e.g., { '@': './src' }) * @param {string[]} options.extensions - File extensions to resolve */ constructor(options = {}) { this.basePath = options.basePath || process.cwd(); this.aliases = options.aliases || {}; this.extensions = options.extensions || ['.js', '.jsx', '.ts', '.tsx']; this.resolvedImports = new Map(); } /** * Resolve an import path * @param {string} importPath - Import path from source * @param {string} currentFile - Current file path * @returns {Object} - Resolved import information */ resolve(importPath, currentFile) { const importType = this._determineImportType(importPath); let resolvedPath = importPath; let isExternal = false; let packageName = null; switch (importType) { case ImportType.REACT: case ImportType.REACT_NATIVE: case ImportType.NODE_MODULE: isExternal = true; packageName = this._extractPackageName(importPath); break; case ImportType.RELATIVE: resolvedPath = this._resolveRelativePath(importPath, currentFile); break; case ImportType.ABSOLUTE: resolvedPath = path.join(this.basePath, importPath); break; case ImportType.ALIAS: resolvedPath = this._resolveAliasPath(importPath); break; case ImportType.ASSET: case ImportType.STYLE: resolvedPath = this._resolveAssetPath(importPath, currentFile); break; } const importInfo = { original: importPath, resolved: resolvedPath, type: importType, isExternal, packageName, needsFlutterEquivalent: this._needsFlutterEquivalent(importType, packageName), flutterEquivalent: this._getFlutterEquivalent(importType, packageName), }; // Cache resolution this.resolvedImports.set(importPath, importInfo); return importInfo; } /** * Determine the type of import * @private */ _determineImportType(importPath) { // React imports if (importPath === 'react' || importPath.startsWith('react/')) { return ImportType.REACT; } // React Native imports if (importPath === 'react-native' || importPath.startsWith('react-native/')) { return ImportType.REACT_NATIVE; } // Asset imports (images, fonts, etc.) if (this._isAssetPath(importPath)) { return ImportType.ASSET; } // Style imports (CSS, SCSS, etc.) if (this._isStylePath(importPath)) { return ImportType.STYLE; } // Alias imports (e.g., @/components/Button) if (this._isAliasPath(importPath)) { return ImportType.ALIAS; } // Relative imports if (importPath.startsWith('.')) { return ImportType.RELATIVE; } // Absolute imports from src if (importPath.startsWith('/')) { return ImportType.ABSOLUTE; } // Node modules return ImportType.NODE_MODULE; } /** * Check if path is an asset * @private */ _isAssetPath(importPath) { const assetExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.otf', '.eot', '.mp4', '.webm', '.ogg', '.mp3', '.wav', '.pdf', '.json', '.xml']; return assetExtensions.some(ext => importPath.endsWith(ext)); } /** * Check if path is a style file * @private */ _isStylePath(importPath) { const styleExtensions = ['.css', '.scss', '.sass', '.less', '.styl']; return styleExtensions.some(ext => importPath.endsWith(ext)); } /** * Check if path uses alias * @private */ _isAliasPath(importPath) { return Object.keys(this.aliases).some(alias => importPath.startsWith(alias)); } /** * Resolve relative import path * @private */ _resolveRelativePath(importPath, currentFile) { const currentDir = path.dirname(currentFile); const resolved = path.resolve(currentDir, importPath); // Try to find file with extensions return this._tryExtensions(resolved); } /** * Resolve aliased import path * @private */ _resolveAliasPath(importPath) { for (const [alias, aliasPath] of Object.entries(this.aliases)) { if (importPath.startsWith(alias)) { const relativePath = importPath.substring(alias.length); const resolved = path.join(this.basePath, aliasPath, relativePath); return this._tryExtensions(resolved); } } return importPath; } /** * Resolve asset path * @private */ _resolveAssetPath(importPath, currentFile) { if (importPath.startsWith('.')) { return this._resolveRelativePath(importPath, currentFile); } return path.join(this.basePath, importPath); } /** * Try to find file with different extensions * @private */ _tryExtensions(basePath) { // If path already has extension, return it if (path.extname(basePath)) { return basePath; } // Try each extension for (const ext of this.extensions) { const withExt = basePath + ext; // In real implementation, would check if file exists // For now, just return first extension return withExt; } return basePath; } /** * Extract package name from import path * @private */ _extractPackageName(importPath) { // Handle scoped packages (@org/package) if (importPath.startsWith('@')) { const parts = importPath.split('/'); return parts.slice(0, 2).join('/'); } // Regular packages return importPath.split('/')[0]; } /** * Check if import needs Flutter equivalent * @private */ _needsFlutterEquivalent(importType, packageName) { if (importType === ImportType.REACT || importType === ImportType.REACT_NATIVE) { return true; } if (importType === ImportType.NODE_MODULE) { // Common packages that need Flutter equivalents const needsEquivalent = [ 'axios', 'lodash', 'moment', 'date-fns', 'uuid', 'redux', 'zustand', 'mobx', 'recoil', 'react-router', 'react-navigation', 'formik', 'react-hook-form', 'styled-components', 'emotion', ]; return needsEquivalent.some(pkg => packageName && packageName.includes(pkg)); } return false; } /** * Get Flutter package equivalent * @private */ _getFlutterEquivalent(importType, packageName) { const equivalents = { // React core 'react': 'flutter (built-in)', 'react-native': 'flutter (built-in)', // HTTP clients 'axios': 'dio', 'fetch': 'http or dio', // State management 'redux': 'flutter_bloc or redux', 'zustand': 'flutter_bloc or provider', 'mobx': 'mobx or flutter_bloc', 'recoil': 'flutter_bloc or riverpod', // Navigation 'react-router': 'go_router or flutter_navigation', 'react-navigation': 'go_router', '@react-navigation/native': 'go_router', // Forms 'formik': 'flutter_form_builder or reactive_forms', 'react-hook-form': 'flutter_form_builder', // Utilities 'lodash': 'dart built-in or collection package', 'moment': 'intl or jiffy', 'date-fns': 'intl or jiffy', 'uuid': 'uuid', // Styling 'styled-components': 'flutter theming', 'emotion': 'flutter theming', // Storage 'async-storage': 'shared_preferences or hive', 'localStorage': 'shared_preferences', // Animations 'react-spring': 'flutter animations', 'framer-motion': 'flutter animations', // Icons 'react-icons': 'flutter_icons or material_icons', '@fortawesome/react-fontawesome': 'font_awesome_flutter', }; if (packageName && equivalents[packageName]) { return equivalents[packageName]; } if (importType === ImportType.REACT || importType === ImportType.REACT_NATIVE) { return 'flutter (built-in)'; } return null; } /** * Get all resolved imports * @returns {Map} */ getResolvedImports() { return new Map(this.resolvedImports); } /** * Get external dependencies * @returns {Object[]} */ getExternalDependencies() { const deps = []; for (const [importPath, info] of this.resolvedImports) { if (info.isExternal && info.packageName) { deps.push({ package: info.packageName, importPath, flutterEquivalent: info.flutterEquivalent, }); } } return deps; } /** * Get Flutter package suggestions * @returns {Object} */ getFlutterPackageSuggestions() { const suggestions = {}; for (const [importPath, info] of this.resolvedImports) { if (info.needsFlutterEquivalent && info.flutterEquivalent) { suggestions[info.packageName] = info.flutterEquivalent; } } return suggestions; } /** * Get asset imports * @returns {Object[]} */ getAssetImports() { const assets = []; for (const [importPath, info] of this.resolvedImports) { if (info.type === ImportType.ASSET) { assets.push({ original: importPath, resolved: info.resolved, type: this._getAssetType(importPath), }); } } return assets; } /** * Get asset type (image, font, video, etc.) * @private */ _getAssetType(importPath) { const ext = path.extname(importPath).toLowerCase(); const typeMap = { '.png': 'image', '.jpg': 'image', '.jpeg': 'image', '.gif': 'image', '.svg': 'image', '.webp': 'image', '.ico': 'image', '.woff': 'font', '.woff2': 'font', '.ttf': 'font', '.otf': 'font', '.mp4': 'video', '.webm': 'video', '.ogg': 'audio', '.mp3': 'audio', '.wav': 'audio', }; return typeMap[ext] || 'unknown'; } /** * Add path alias * @param {string} alias - Alias prefix * @param {string} aliasPath - Path to resolve to */ addAlias(alias, aliasPath) { this.aliases[alias] = aliasPath; } /** * Clear resolved imports cache */ clearCache() { this.resolvedImports.clear(); } } module.exports = { ImportResolver, ImportType, };