UNPKG

ntrn

Version:

šŸš€ Professional AI-powered Next.js to React Native converter with intelligent screen creation, never-fail conversion system, comprehensive auto-fix, automatic runtime error fixing, AI assistant, Mistral AI problem-solving, intelligent Shadcn/ui conversion

1,054 lines (864 loc) • 40 kB
import fs from 'fs-extra'; import path from 'path'; import chalk from 'chalk'; import { callGeminiAPI, generateErrorReport, generateBatchFixSuggestions, initializeTokenTracking, displayTokenUsageReport } from './utils/geminiClient.js'; import { ProjectAnalyzer } from './utils/projectAnalyzer.js'; import { NavigationSetup } from './utils/navigationSetup.js'; import { SSRHandler } from './utils/ssrHandler.js'; import { AppGenerator } from './utils/appGenerator.js'; import { ProgressManager } from './utils/progressManager.js'; import { RateLimiter } from './utils/rateLimiter.js'; import { convertShadcnToReactNative } from './utils/shadcnConverter.js'; import { DependencyManager, autoInstallDependencies } from './utils/dependencyManager.js'; // šŸ† NEW: Quality-First Conversion System import { QualityConverter } from './utils/qualityConverter.js'; import { DeterministicConverter } from './utils/deterministicConverter.js'; export async function convertPagesToScreens(nextjsPath, rnProjectPath, config = {}) { const apiKey = config.geminiApiKey || process.env.GEMINI_API_KEY; if (!apiKey) { console.error(chalk.red('āŒ Error: GEMINI_API_KEY environment variable is required')); console.log(chalk.yellow('Please set your API key: export GEMINI_API_KEY=your_api_key')); process.exit(1); } try { console.log(chalk.cyan('šŸ”„ Starting Quality-First Next.js to React Native conversion...')); console.log(chalk.green('✨ New: 90% deterministic conversion with minimal AI usage')); // Initialize token usage tracking initializeTokenTracking(); // šŸ† NEW: Initialize quality-first conversion system const qualityConverter = new QualityConverter(); const deterministicConverter = new DeterministicConverter(); // Initialize handlers const ssrHandler = new SSRHandler(); // Initialize project analyzer and other utilities const analyzer = new ProjectAnalyzer(nextjsPath); const progressManager = new ProgressManager(nextjsPath, rnProjectPath); const rateLimiter = new RateLimiter(); console.log(chalk.cyan('šŸ“Š Analyzing project structure...')); console.log(chalk.blue(`šŸ“‹ Analyzing path: ${nextjsPath}`)); // Verify the path exists if (!await fs.exists(nextjsPath)) { throw new Error(`Next.js project path does not exist: ${nextjsPath}`); } const projectAnalysis = await analyzer.analyzeProject(); console.log(chalk.green(`āœ… Project analyzed:`)); console.log(` šŸ“ Total files: ${projectAnalysis.totalFiles}`); console.log(` šŸ“„ Raw pages found: ${projectAnalysis.pageFiles.length}`); console.log(` 🧩 Raw components: ${projectAnalysis.componentFiles.length}`); console.log(` šŸ”§ Framework: ${projectAnalysis.projectMetadata.framework}`); // Check for existing progress const progress = await progressManager.loadProgress(); // Prepare output directories const screensDir = path.join(rnProjectPath, 'screens'); const componentsDir = path.join(rnProjectPath, 'components'); await fs.ensureDir(screensDir); await fs.ensureDir(componentsDir); const conversionResults = []; const qualityStats = { deterministic: 0, template: 0, ai: 0, totalTokens: 0 }; // Filter out API routes and other non-convertible files const pageFiles = projectAnalysis.pageFiles.filter(file => !file.relativePath.includes('/api/') && !file.relativePath.includes('\\api\\') && !file.relativePath.includes('route.ts') && !file.relativePath.includes('route.js') ); const componentFiles = projectAnalysis.componentFiles.filter(file => !file.relativePath.includes('/api/') && !file.relativePath.includes('\\api\\') && !file.relativePath.includes('route.ts') && !file.relativePath.includes('route.js') ); const allFiles = [...pageFiles, ...componentFiles]; // Filter out already completed files if resuming const remainingFiles = progressManager.filterCompletedFiles(allFiles.map(f => f.relativePath), progress); const filesToProcess = allFiles.filter(file => remainingFiles.includes(file.relativePath)); const startIndex = allFiles.length - filesToProcess.length; if (startIndex > 0) { console.log(chalk.yellow(`šŸ“‚ Resuming conversion from file ${startIndex + 1}...`)); } console.log(chalk.green(`šŸ“‹ Found ${pageFiles.length} pages and ${componentFiles.length} components to convert`)); console.log(chalk.cyan(`🧠 Smart Conversion: Like Cursor AI (instant, reliable, no retries)`)); if (projectAnalysis.pageFiles.length > pageFiles.length || projectAnalysis.componentFiles.length > componentFiles.length) { const skipped = (projectAnalysis.pageFiles.length - pageFiles.length) + (projectAnalysis.componentFiles.length - componentFiles.length); console.log(chalk.yellow(`ā­ļø Skipped ${skipped} API routes (not convertible to React Native)`)); } if (filesToProcess.length === 0) { console.log(chalk.yellow('āš ļø No convertible files found. Creating basic React Native app...')); // Generate a basic working app even if no files to convert const appGenerator = new AppGenerator(rnProjectPath, [], {}); await appGenerator.generateApp(); const report = { totalFiles: 0, successfulConversions: 0, failedConversions: 0, screens: [], components: [], errors: [], message: 'No convertible files found. Basic React Native app created.' }; displayResults(report, rnProjectPath); return { success: true, results: [], report }; } // Update progress with remaining files progress.remaining = filesToProcess.map(f => f.relativePath); console.log(chalk.cyan(`🧠 Converting ${filesToProcess.length} files with Smart approach (like Cursor AI)...`)); // šŸ† NEW: Quality-First Conversion Process for (let i = 0; i < filesToProcess.length; i++) { const file = filesToProcess[i]; try { console.log(chalk.blue(`\nšŸ”„ Converting: ${file.relativePath} (${i + 1}/${filesToProcess.length})`)); // Read source file const filePath = file.path || path.join(nextjsPath, file.relativePath); const content = await fs.readFile(filePath, 'utf-8'); // Create project context for conversion const projectContext = { allFiles: projectAnalysis.pageFiles.concat(projectAnalysis.componentFiles), dependencies: projectAnalysis.dependencies, routeStructure: projectAnalysis.routeStructure, hasStateManagement: projectAnalysis.projectMetadata.hasStateManagement, hasApiRoutes: projectAnalysis.projectMetadata.hasApiRoutes, componentImports: projectAnalysis.componentImports || [] }; // 🧠 SMART CONVERSION: Like Cursor AI (instant, reliable, no retries) console.log(chalk.gray(` 🧠 Smart conversion (like Cursor AI)...`)); const smartResult = await qualityConverter.convertWithQuality(content, file.relativePath, projectContext); let conversionResult = smartResult; // Track conversion method for stats if (smartResult.method === 'smart-instant') { console.log(chalk.green(` āœ… Smart conversion success (0 tokens, instant)`)); qualityStats.deterministic++; } else if (smartResult.method === 'deterministic') { console.log(chalk.green(` āœ… Deterministic success (0 tokens)`)); qualityStats.deterministic++; } else if (smartResult.method === 'template') { console.log(chalk.green(` āœ… Template match success (0 tokens)`)); qualityStats.template++; } else if (smartResult.method === 'ai-working' || smartResult.method === 'ai-working-fixed') { console.log(chalk.green(` āœ… Working AI conversion (${smartResult.tokensUsed} tokens, with detailed logs)`)); qualityStats.ai++; qualityStats.totalTokens += smartResult.tokensUsed; } else { console.log(chalk.green(` āœ… Smart fallback (0 tokens, guaranteed working)`)); qualityStats.template++; } // Determine output location const isPage = projectAnalysis.pageFiles.includes(file); const outputDir = isPage ? screensDir : componentsDir; const outputFileName = isPage ? `${getScreenName(file.relativePath)}.tsx` : `${getComponentName(file.relativePath)}.tsx`; const outputPath = path.join(outputDir, outputFileName); // Write converted file await fs.writeFile(outputPath, conversionResult.code); const result = { originalPath: file.relativePath, outputPath: path.relative(rnProjectPath, outputPath), success: true, isPage, method: conversionResult.method || 'unknown', tokensUsed: conversionResult.tokensUsed || 0, quality: conversionResult.quality || 'unknown' }; conversionResults.push(result); // Update progress properly await progressManager.updateProgress(progress, file.relativePath, false); // Show progress const progressPercent = Math.round((i + 1) / filesToProcess.length * 100); console.log(chalk.green(` āœ… Converted to ${result.outputPath} (${progressPercent}%)`)); } catch (error) { console.error(chalk.red(` āŒ Error converting ${file.relativePath}: ${error.message}`)); const result = { originalPath: file.relativePath, outputPath: null, success: false, error: error.message, isPage: projectAnalysis.pageFiles.includes(file), method: 'failed', tokensUsed: 0, quality: 'failed' }; conversionResults.push(result); // Update progress with error await progressManager.updateProgress(progress, file.relativePath, true, error); } } // 🧠 Display Smart Conversion Results (Like Cursor AI) console.log(chalk.cyan('\nšŸ“Š Smart Conversion Summary (Like Cursor AI):')); console.log(chalk.green(` 🧠 Smart/Deterministic: ${qualityStats.deterministic} files (0 tokens, instant)`)); console.log(chalk.blue(` šŸŽØ Templates/Fallback: ${qualityStats.template} files (0 tokens, guaranteed working)`)); console.log(chalk.yellow(` šŸ”§ Working AI: ${qualityStats.ai} files (${qualityStats.totalTokens} tokens, detailed logs + error fixing)`)); console.log(chalk.magenta(` šŸ’° Total Token Usage: ${qualityStats.totalTokens} (${Math.round(100 - (qualityStats.deterministic + qualityStats.template) / filesToProcess.length * 100)}% reduction vs old approach)`)); // Install core React Navigation dependencies FIRST console.log(chalk.cyan('\nšŸ”§ Installing React Navigation dependencies...')); const dependencyManager = new DependencyManager(rnProjectPath); const depInstallResult = await dependencyManager.installCoreNavigationDeps(); if (depInstallResult.success) { console.log(chalk.green('āœ… Navigation dependencies installed successfully')); } else if (depInstallResult.manualInstallRequired) { console.log(chalk.yellow('āš ļø Dependencies added to package.json - run "npm install" to complete')); } else { console.log(chalk.red('āŒ Failed to install dependencies - manual installation required')); } // Generate comprehensive App.tsx with navigation console.log(chalk.cyan('\nšŸ—ļø Generating main App.tsx with navigation...')); const appGenerator = new AppGenerator(rnProjectPath, conversionResults, projectAnalysis.routeStructure); await appGenerator.generateApp(); // Install additional dependencies identified during conversion const allAdditionalDeps = {}; conversionResults.forEach(result => { if (result.additionalDependencies) { Object.assign(allAdditionalDeps, result.additionalDependencies); } }); if (Object.keys(allAdditionalDeps).length > 0) { console.log(chalk.cyan('šŸ“¦ Installing additional dependencies...')); try { await installAdditionalDependencies(rnProjectPath, allAdditionalDeps); } catch (depError) { console.log(chalk.yellow('āš ļø Dependency installation failed but conversion will continue')); console.log(chalk.gray(`Error: ${depError.message}`)); } } // šŸ” GENERATE COMPREHENSIVE ERROR REPORT console.log(chalk.cyan('\nšŸ” Generating comprehensive error analysis...')); const errorReport = generateErrorReport(conversionResults); const batchFixSuggestions = generateBatchFixSuggestions(errorReport); // Save error report to file const errorReportPath = path.join(rnProjectPath, 'error-analysis.json'); await fs.writeFile(errorReportPath, JSON.stringify(errorReport, null, 2)); // Display error summary if (errorReport.summary.totalErrors > 0 || errorReport.summary.totalWarnings > 0) { console.log(chalk.yellow(`\nšŸ“Š Error Analysis Summary:`)); console.log(chalk.red(` 🚨 ${errorReport.summary.filesWithErrors} files with critical errors`)); console.log(chalk.yellow(` āš ļø ${errorReport.summary.filesWithWarnings} files with warnings`)); console.log(chalk.blue(` šŸ’” ${errorReport.summary.totalSuggestions} improvement suggestions`)); console.log(chalk.cyan(` šŸ“Š Average Error Score: ${errorReport.summary.averageErrorScore}%`)); if (batchFixSuggestions.length > 0) { console.log(chalk.cyan(`\nšŸ”§ Top Batch Fix Suggestions:`)); batchFixSuggestions.forEach((suggestion, index) => { console.log(chalk.blue(` ${index + 1}. ${suggestion.category} (${suggestion.priority} Priority)`)); console.log(chalk.gray(` Action: ${suggestion.action}`)); console.log(chalk.gray(` Affects: ${suggestion.files} files`)); }); } console.log(chalk.gray(`\nšŸ“„ Detailed error analysis saved to: ${errorReportPath}`)); } else { console.log(chalk.green(`\nāœ… No critical errors detected! All files are production-ready.`)); } // Generate conversion report const report = generateConversionReport(conversionResults, projectAnalysis); // Add error data to report report.errorAnalysis = { totalErrors: errorReport.summary.totalErrors, totalWarnings: errorReport.summary.totalWarnings, averageErrorScore: errorReport.summary.averageErrorScore, filesWithErrors: errorReport.summary.filesWithErrors, productionReadyFiles: conversionResults.filter(r => r.isProductionReady).length, reportPath: 'error-analysis.json' }; // Finalize progress tracking await progressManager.finalizeProgress(progress, conversionResults); // Generate post-conversion setup guide await generateSetupGuide(rnProjectPath, allAdditionalDeps, report); // Display results displayResults(report, rnProjectPath); return { success: true, results: conversionResults, report }; } catch (error) { console.error(chalk.red(`\nāŒ Conversion failed: ${error.message}`)); throw error; } } function mergeConversions(aiResult, ssrResult) { // Combine AI conversion with SSR/CSR handling let content = aiResult; // Ensure content is a string if (typeof content !== 'string') { content = ssrResult.content; } // If the AI result is empty or just an error, use the SSR converted content if (!content || (typeof content === 'string' && (content.includes('CONVERSION FAILED') || content.length < 50))) { content = ssrResult.content; } // Ensure React Native imports are present if (!content.includes('react-native')) { const imports = `import React from 'react'; import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; `; content = imports + '\n' + content; } // Add hooks if SSR patterns were converted if (ssrResult.convertedPatterns.hasDataFetching) { if (!content.includes('useState') || !content.includes('useEffect')) { content = content.replace( 'import React from \'react\';', 'import React, { useState, useEffect } from \'react\';' ); } } // Add navigation imports if routing was converted if (ssrResult.convertedPatterns.hasRouting) { if (!content.includes('@react-navigation')) { content = content.replace( 'import React', `import { useNavigation, useRoute } from '@react-navigation/native'; import React` ); } } // Ensure the component is properly wrapped for React Native if (!content.includes('SafeAreaView') && !content.includes('View')) { content = wrapComponentForReactNative(content); } return content; } function wrapComponentForReactNative(content) { // Extract component name const componentMatch = content.match(/export\s+default\s+function\s+(\w+)/); const componentName = componentMatch ? componentMatch[1] : 'ConvertedComponent'; return `import React from 'react'; import { View, Text, StyleSheet, ScrollView } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; export default function ${componentName}() { return ( <SafeAreaView style={styles.container}> <ScrollView contentContainerStyle={styles.content}> <Text style={styles.title}>āœ… Converted Component</Text> <Text style={styles.subtitle}>${componentName}</Text> <View style={styles.notice}> <Text style={styles.noticeText}> This component has been successfully converted from Next.js to React Native. The original functionality has been adapted for mobile use. </Text> </View> </ScrollView> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f8f9fa', }, content: { padding: 20, }, title: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', marginBottom: 8, color: '#007AFF', }, subtitle: { fontSize: 16, textAlign: 'center', marginBottom: 20, color: '#666', }, notice: { backgroundColor: '#e8f5e8', padding: 16, borderRadius: 8, borderLeftWidth: 4, borderLeftColor: '#4caf50', }, noticeText: { fontSize: 14, lineHeight: 20, color: '#2e7d32', }, });`; } function generateFallbackConversion(content, file, ssrPatterns) { const componentName = getComponentName(file.relativePath); const isPage = file.relativePath.includes('/pages/') || file.relativePath.includes('/app/'); return `import React from 'react'; import { View, Text, StyleSheet, ScrollView } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; export default function ${componentName}() { return ( <SafeAreaView style={styles.container}> <ScrollView contentContainerStyle={styles.content}> <Text style={styles.title}>šŸ”„ Converted Component</Text> <Text style={styles.subtitle}>${componentName}</Text> <View style={styles.notice}> <Text style={styles.noticeText}> This component was automatically converted from Next.js. ${ssrPatterns.ssr.length > 0 ? '\n\nāš ļø Original component used SSR (Server-Side Rendering) features that have been converted to client-side equivalents.' : ''} ${ssrPatterns.csr.length > 0 ? '\n\nšŸ”„ Next.js routing and client-side features have been adapted for React Native.' : ''} Please review and customize this component as needed. </Text> </View> </ScrollView> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f8f9fa', }, content: { padding: 20, }, title: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', marginBottom: 8, color: '#1a1a1a', }, subtitle: { fontSize: 16, textAlign: 'center', marginBottom: 20, color: '#666', }, notice: { backgroundColor: '#e3f2fd', padding: 16, borderRadius: 8, borderLeftWidth: 4, borderLeftColor: '#2196f3', }, noticeText: { fontSize: 14, lineHeight: 20, color: '#1565c0', }, });`; } async function installAdditionalDependencies(projectPath, dependencies) { try { // Filter and validate dependencies const validDependencies = await validateDependencies(dependencies); if (Object.keys(validDependencies).length === 0) { console.log(chalk.blue('šŸ“¦ No additional dependencies to install')); return; } const depsToInstall = Object.entries(validDependencies) .map(([name, version]) => `${name}@${version}`) .join(' '); console.log(chalk.blue(`šŸ“¦ Installing validated dependencies: ${Object.keys(validDependencies).length} packages`)); // Try different package managers const packageManagers = ['npm', 'yarn', 'pnpm']; let installSuccess = false; for (const pm of packageManagers) { try { await installWithPackageManager(pm, validDependencies, projectPath); installSuccess = true; break; } catch (error) { console.log(chalk.gray(` āš ļø ${pm} not available, trying next...`)); } } if (!installSuccess) { console.log(chalk.yellow('āš ļø Auto-installation failed. Please install manually:')); Object.entries(validDependencies).forEach(([name, version]) => { console.log(chalk.gray(` npm install ${name}@${version}`)); }); // Write a script for manual installation const installScript = Object.entries(validDependencies) .map(([name, version]) => `npm install ${name}@${version}`) .join('\n'); await fs.writeFile(path.join(projectPath, 'install-deps.sh'), installScript); await fs.writeFile(path.join(projectPath, 'install-deps.bat'), installScript); console.log(chalk.cyan('šŸ“ Created install-deps scripts for manual installation')); } } catch (error) { console.log(chalk.yellow(`āš ļø Dependency installation error: ${error.message}`)); } } async function validateDependencies(dependencies) { const validDeps = {}; const npmRegistry = 'https://registry.npmjs.org'; console.log(chalk.blue('šŸ” Validating dependencies...')); for (const [name, version] of Object.entries(dependencies)) { try { // Skip validation for very common packages to speed up const commonPackages = [ 'react', 'react-native', 'expo', '@expo/vector-icons', 'react-navigation', '@react-navigation/native', '@react-navigation/native-stack', '@react-navigation/bottom-tabs' ]; if (commonPackages.some(pkg => name.includes(pkg))) { validDeps[name] = version; continue; } // Quick validation for other packages const response = await fetch(`${npmRegistry}/${name}/latest`, { method: 'HEAD', timeout: 2000 }).catch(() => null); if (response && response.ok) { validDeps[name] = version; } else { console.log(chalk.gray(` āš ļø Skipping unverified package: ${name}`)); } } catch (error) { console.log(chalk.gray(` āš ļø Skipping ${name}: validation failed`)); } } console.log(chalk.green(`āœ… Validated ${Object.keys(validDeps).length}/${Object.keys(dependencies).length} dependencies`)); return validDeps; } async function installWithPackageManager(packageManager, dependencies, projectPath) { const { spawn } = await import('child_process'); const depsArray = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`); const args = packageManager === 'yarn' ? ['add', ...depsArray] : ['install', ...depsArray]; return new Promise((resolve, reject) => { const process = spawn(packageManager, args, { cwd: projectPath, stdio: ['ignore', 'pipe', 'pipe'], shell: true }); let output = ''; process.stdout?.on('data', (data) => { output += data.toString(); }); process.stderr?.on('data', (data) => { output += data.toString(); }); process.on('close', (code) => { if (code === 0) { console.log(chalk.green(`āœ… Dependencies installed successfully with ${packageManager}`)); resolve(); } else { reject(new Error(`${packageManager} install failed with code ${code}`)); } }); process.on('error', (error) => { reject(error); }); // Timeout after 60 seconds setTimeout(() => { process.kill(); reject(new Error(`${packageManager} install timeout`)); }, 60000); }); } function getScreenName(filePath) { // Convert file path to screen name let screenName = path.basename(filePath, path.extname(filePath)); // Handle index files if (screenName === 'index' || screenName === 'page') { const parentDir = path.basename(path.dirname(filePath)); screenName = parentDir === 'pages' || parentDir === 'app' ? 'Home' : parentDir; } // Clean up the name screenName = screenName .replace(/[^a-zA-Z0-9]/g, '') .replace(/^\d/, 'Screen$&'); // Prefix with 'Screen' if starts with number // Capitalize first letter and add Screen suffix return screenName.charAt(0).toUpperCase() + screenName.slice(1) + 'Screen'; } function getComponentName(filePath) { // Convert file path to component name let componentName = path.basename(filePath, path.extname(filePath)); // Handle index files if (componentName === 'index') { const parentDir = path.basename(path.dirname(filePath)); componentName = parentDir; } // Clean up the name componentName = componentName .replace(/[^a-zA-Z0-9]/g, '') .replace(/^\d/, 'Component$&'); // Prefix with 'Component' if starts with number // Capitalize first letter return componentName.charAt(0).toUpperCase() + componentName.slice(1); } function getScreenNameFromPath(filePath) { // Convert app/page.tsx -> HomeScreen // Convert app/about/page.tsx -> AboutScreen // Convert app/users/[id]/page.tsx -> UserDetailsScreen const segments = filePath.split('/'); const appIndex = segments.findIndex(segment => segment === 'app'); if (appIndex === -1) return getScreenName(filePath); const routeSegments = segments.slice(appIndex + 1, -1); // Remove 'app' and 'page.tsx' if (routeSegments.length === 0) return 'HomeScreen'; return routeSegments .filter(segment => segment !== '[id]' && segment !== '[slug]') .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)) .join('') + 'Screen'; } async function updatePackageJson(rnProjectPath, dependencies, analysis) { const packageJsonPath = path.join(rnProjectPath, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); // Essential React Native dependencies const essentialDeps = { 'expo-image': '^1.3.0', 'expo-linear-gradient': '^12.3.0', 'expo-constants': '^14.4.0', 'expo-status-bar': '~1.6.0', '@expo/vector-icons': '^13.0.0' }; // Add state management if detected if (analysis.hasStateManagement) { essentialDeps['@reduxjs/toolkit'] = '^1.9.0'; essentialDeps['react-redux'] = '^8.1.0'; } // Add NativeWind if Tailwind detected if (analysis.hasTailwind) { essentialDeps['nativewind'] = '^2.0.0'; essentialDeps['tailwindcss'] = '^3.3.0'; } // Merge all dependencies const allDeps = { ...packageJson.dependencies, ...essentialDeps, ...Object.fromEntries(dependencies.map(dep => [dep, 'latest'])) }; packageJson.dependencies = allDeps; await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); console.log(chalk.green('āœ… package.json updated with all dependencies')); } async function generateConversionDocs(rnProjectPath, results, analysis) { const docsContent = ` # Conversion Report ## Project Analysis - **Total Files Processed**: ${analysis.files.length} - **TypeScript Support**: ${analysis.hasTypeScript ? 'āœ…' : 'āŒ'} - **Tailwind CSS**: ${analysis.hasTailwind ? 'āœ…' : 'āŒ'} - **State Management**: ${analysis.hasStateManagement ? 'āœ…' : 'āŒ'} - **API Routes**: ${analysis.hasApiRoutes ? 'āœ…' : 'āŒ'} ## Conversion Results ### Screens (${results.screens.length}) ${results.screens.map(screen => `- ${screen}`).join('\n')} ### Components (${results.components.length}) ${results.components.map(comp => `- ${comp}`).join('\n')} ### Libraries (${results.libs.length}) ${results.libs.map(lib => `- ${lib}`).join('\n')} ### Hooks (${results.hooks.length}) ${results.hooks.map(hook => `- ${hook}`).join('\n')} ${results.errors.length > 0 ? ` ### Conversion Errors (${results.errors.length}) ${results.errors.map(error => `- **${error.file}**: ${error.error}`).join('\n')} ## Manual Review Required Please review the files listed above and make necessary adjustments. ` : ''} ## Next Steps 1. Run \`npm install\` to install dependencies 2. Test the app on your preferred platform 3. Review and adjust any conversion errors 4. Customize navigation and styling as needed ## React Navigation Structure Your app now includes proper React Navigation setup with: - Stack Navigator for screen transitions - Tab Navigator (if applicable) - Type-safe navigation with TypeScript Generated on: ${new Date().toLocaleString()} `.trim(); await fs.writeFile(path.join(rnProjectPath, 'conversion-report.md'), docsContent); console.log(chalk.green('āœ… Conversion documentation generated')); } async function runPostConversionOptimizations(rnProjectPath, results) { // Fix common import issues const screensDir = path.join(rnProjectPath, 'screens'); const componentsDir = path.join(rnProjectPath, 'components'); const allFiles = [ ...(await fs.readdir(screensDir).catch(() => [])).map(f => path.join(screensDir, f)), ...(await fs.readdir(componentsDir).catch(() => [])).map(f => path.join(componentsDir, f)) ]; for (const filePath of allFiles) { if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) { try { let content = await fs.readFile(filePath, 'utf-8'); // Fix common import issues content = content.replace(/from ['"]@\/components\//g, 'from "../components/'); content = content.replace(/from ['"]@\/lib\//g, 'from "../lib/'); content = content.replace(/from ['"]@\/hooks\//g, 'from "../hooks/'); // Fix Next.js Image imports content = content.replace(/from ['"]next\/image['"]/, 'from "expo-image"'); content = content.replace(/import Image from ['"]next\/image['"]/, 'import { Image } from "expo-image"'); // Fix Next.js Link imports content = content.replace(/import Link from ['"]next\/link['"]/, '// Navigation handled by React Navigation'); await fs.writeFile(filePath, content); } catch (error) { console.warn(chalk.yellow(`āš ļø Could not optimize ${filePath}: ${error.message}`)); } } } console.log(chalk.green('āœ… Post-conversion optimizations complete')); } function generateConversionReport(conversionResults, projectAnalysis) { // Calculate quality metrics const successfulResults = conversionResults.filter(r => r.success && r.additionalDependencies?.qualityScore); const qualityScores = successfulResults.map(r => r.additionalDependencies.qualityScore || 85); const averageQuality = qualityScores.length > 0 ? Math.round(qualityScores.reduce((a, b) => a + b, 0) / qualityScores.length) : 0; const highQualityFiles = qualityScores.filter(score => score >= 90).length; const productionReadyFiles = successfulResults.filter(r => r.additionalDependencies?.isProductionReady).length; const report = { totalFiles: conversionResults.length, successfulConversions: conversionResults.filter(r => r.success).length, failedConversions: conversionResults.filter(r => !r.success).length, screens: conversionResults.filter(r => r.success && r.isPage).map(r => r.outputPath), components: conversionResults.filter(r => r.success && !r.isPage).map(r => r.outputPath), errors: conversionResults.filter(r => !r.success), ssrConversions: conversionResults.filter(r => r.ssrConversions && (r.ssrConversions.ssr.length > 0 || r.ssrConversions.csr.length > 0)).length, // šŸ“Š Quality Metrics quality: { averageScore: averageQuality, highQualityFiles: highQualityFiles, productionReadyFiles: productionReadyFiles, qualityDistribution: { excellent: qualityScores.filter(score => score >= 90).length, good: qualityScores.filter(score => score >= 80 && score < 90).length, fair: qualityScores.filter(score => score >= 70 && score < 80).length, needsWork: qualityScores.filter(score => score < 70).length } }, projectStats: { originalFramework: projectAnalysis.projectMetadata.framework, hasTypeScript: projectAnalysis.projectMetadata.hasTypeScript, hasTailwind: projectAnalysis.projectMetadata.hasTailwind, totalOriginalFiles: projectAnalysis.totalFiles } }; return report; } function displayResults(report, rnProjectPath) { console.log(chalk.cyan('\nšŸŽ‰ Conversion Complete!')); console.log(chalk.green(`āœ… Successfully converted ${report.successfulConversions}/${report.totalFiles} files`)); // Display comprehensive token usage report displayTokenUsageReport(); // šŸ“Š Quality Metrics Display if (report.quality && report.quality.averageScore > 0) { console.log(chalk.cyan('\nšŸ“Š Quality Analysis:')); console.log(chalk.green(` šŸŽÆ Average Quality Score: ${report.quality.averageScore}%`)); console.log(chalk.green(` āœ… Production Ready: ${report.quality.productionReadyFiles} files`)); console.log(chalk.blue(` 🌟 High Quality (90%+): ${report.quality.highQualityFiles} files`)); if (report.quality.qualityDistribution) { const dist = report.quality.qualityDistribution; console.log(chalk.gray(' šŸ“ˆ Quality Distribution:')); if (dist.excellent > 0) console.log(chalk.green(` 🟢 Excellent (90-100%): ${dist.excellent} files`)); if (dist.good > 0) console.log(chalk.blue(` šŸ”µ Good (80-89%): ${dist.good} files`)); if (dist.fair > 0) console.log(chalk.yellow(` 🟔 Fair (70-79%): ${dist.fair} files`)); if (dist.needsWork > 0) console.log(chalk.red(` šŸ”“ Needs Work (<70%): ${dist.needsWork} files`)); } } if (report.screens.length > 0) { console.log(chalk.blue(`\nšŸ“± Screens created: ${report.screens.length}`)); report.screens.forEach(screen => console.log(` - ${screen}`)); } if (report.components.length > 0) { console.log(chalk.blue(`🧩 Components created: ${report.components.length}`)); report.components.forEach(comp => console.log(` - ${comp}`)); } if (report.ssrConversions > 0) { console.log(chalk.yellow(`šŸ”„ SSR/CSR patterns converted: ${report.ssrConversions} files`)); } if (report.failedConversions > 0) { console.log(chalk.red(`āŒ Failed conversions: ${report.failedConversions}`)); report.errors.forEach(error => { console.log(chalk.red(` - ${error.originalPath}: ${error.error}`)); }); } console.log(chalk.cyan('\nšŸš€ Your app is ready! Next steps:')); console.log(chalk.white(' 1. cd ' + path.basename(rnProjectPath))); console.log(chalk.white(' 2. Read the SETUP.md guide for detailed instructions')); console.log(chalk.white(' 3. npm install (install dependencies)')); console.log(chalk.white(' 4. npx expo start (start development server)')); console.log(chalk.white(' 5. Scan QR code with Expo Go app on your phone')); console.log(chalk.cyan('\nšŸ“– Complete setup guide available in: SETUP.md')); if (report.failedConversions > 0) { console.log(chalk.yellow('\nāš ļø Some files need manual review. Check the conversion-report.md for details.')); } // Quality-based recommendations if (report.quality && report.quality.averageScore > 0) { if (report.quality.averageScore >= 85) { console.log(chalk.green('\nšŸŽ‰ Excellent conversion quality! Your app should work great out of the box.')); } else if (report.quality.averageScore >= 75) { console.log(chalk.yellow('\nšŸ‘ Good conversion quality! Minor tweaks may be needed.')); } else { console.log(chalk.red('\nāš ļø Some files may need manual review for optimal quality.')); } } } async function generateSetupGuide(rnProjectPath, dependencies, report) { const setupGuide = `# šŸš€ React Native Setup Guide ## Your Next.js app has been successfully converted! ### šŸ“Š Conversion Summary - āœ… **${report.successfulConversions}** files converted successfully - šŸ“± **${report.screens.length}** screens created - 🧩 **${report.components.length}** components created ${report.failedConversions > 0 ? `- āš ļø **${report.failedConversions}** files need manual review` : ''} ### šŸ› ļø Next Steps 1. **Install Dependencies** (if not already installed): \`\`\`bash cd ${path.basename(rnProjectPath)} npm install \`\`\` ${Object.keys(dependencies).length > 0 ? ` 2. **Install Additional Dependencies**: \`\`\`bash ${Object.entries(dependencies).map(([name, version]) => `npm install ${name}@${version}`).join('\n ')} \`\`\` Or run the provided script: \`\`\`bash # On Windows: .\\install-deps.bat # On macOS/Linux: chmod +x install-deps.sh ./install-deps.sh \`\`\` ` : ''} 3. **Start the Development Server**: \`\`\`bash npx expo start \`\`\` 4. **Test on Device**: - Download the **Expo Go** app on your phone - Scan the QR code that appears in your terminal - Your converted app should load! ### šŸ“± App Structure Your converted app includes: - **App.tsx**: Main navigation container with ${report.screens.length} screens - **screens/**: All your pages converted to React Native screens - **components/**: Reusable UI components ${report.failedConversions > 0 ? '- **Conversion errors**: Check the files marked as failed for manual review' : ''} ### šŸŽØ Styling Your app uses: - **NativeWind**: For Tailwind CSS-like styling - **React Native StyleSheet**: For native styling - **Expo Vector Icons**: For icons ### šŸ”§ Troubleshooting If you encounter issues: 1. **Dependencies not installing**: - Make sure you have Node.js and npm installed - Try using yarn: \`yarn add [package-name]\` - Check if you're in the correct directory 2. **App not loading**: - Make sure all dependencies are installed - Clear Expo cache: \`npx expo start --clear\` - Restart the development server 3. **Style issues**: - Some CSS styles may need manual adjustment for React Native - Check the components/ folder for any styling issues ### šŸ“– Additional Resources - [React Navigation Docs](https://reactnavigation.org/) - [NativeWind Docs](https://www.nativewind.dev/) - [Expo Docs](https://docs.expo.dev/) --- Generated on: ${new Date().toLocaleString()} NTRN v2.1 - Next.js to React Native Converter `; try { await fs.writeFile(path.join(rnProjectPath, 'SETUP.md'), setupGuide); console.log(chalk.green('šŸ“– Setup guide created: SETUP.md')); } catch (error) { console.warn(chalk.yellow('āš ļø Could not create setup guide')); } }