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
564 lines (493 loc) • 16.7 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
export class AppGenerator {
constructor(rnProjectPath, conversionResults, routeStructure) {
this.rnProjectPath = rnProjectPath;
this.conversionResults = conversionResults;
this.routeStructure = routeStructure;
this.screensDir = path.join(rnProjectPath, 'screens');
this.componentsDir = path.join(rnProjectPath, 'components');
}
async generateApp() {
console.log(chalk.cyan('🏗️ Generating main App.tsx with navigation...'));
const screens = await this.getAvailableScreens();
const hasScreens = screens.length > 0;
if (!hasScreens) {
console.log(chalk.yellow('⚠️ No screens found, creating basic app structure'));
await this.generateBasicApp();
return;
}
// Determine navigation pattern
const navPattern = this.determineNavigationPattern(screens);
console.log(chalk.cyan(`📱 Setting up ${navPattern} navigation with ${screens.length} screens`));
// Generate appropriate navigation structure
switch (navPattern) {
case 'tabs':
await this.generateTabApp(screens);
break;
case 'drawer':
await this.generateDrawerApp(screens);
break;
case 'stack':
default:
await this.generateStackApp(screens);
break;
}
// Generate navigation types
await this.generateNavigationTypes(screens);
console.log(chalk.green('✅ App.tsx generated successfully'));
}
async getAvailableScreens() {
const screens = [];
// Ensure screens directory exists
await fs.ensureDir(this.screensDir);
if (await fs.exists(this.screensDir)) {
const files = await fs.readdir(this.screensDir);
for (const file of files) {
if (file.endsWith('.tsx') || file.endsWith('.ts')) {
const screenName = path.basename(file, path.extname(file));
const filePath = path.join(this.screensDir, file);
// Check if file actually has content (not just an error placeholder)
try {
const content = await fs.readFile(filePath, 'utf-8');
const isValidScreen = !content.includes('CONVERSION FAILED') &&
!content.includes('Manual conversion required') &&
content.trim().length > 0;
if (isValidScreen) {
screens.push({
name: screenName,
file: file,
component: screenName,
route: this.getRouteForScreen(screenName)
});
}
} catch (error) {
console.warn(`⚠️ Could not read screen file: ${file}`);
}
}
}
}
// Ensure we have at least a Home screen
if (screens.length === 0) {
await this.createFallbackHomeScreen();
screens.push({
name: 'HomeScreen',
file: 'HomeScreen.tsx',
component: 'HomeScreen',
route: '/'
});
}
return screens;
}
getRouteForScreen(screenName) {
// Map screen names back to routes
const routeMapping = {
'HomeScreen': '/',
'AboutScreen': '/about',
'ContactScreen': '/contact',
'ProfileScreen': '/profile',
'SettingsScreen': '/settings',
'LoginScreen': '/login',
'SignupScreen': '/signup'
};
return routeMapping[screenName] || `/${screenName.toLowerCase().replace('screen', '')}`;
}
determineNavigationPattern(screens) {
const screenNames = screens.map(s => s.name.toLowerCase());
// Check for tab navigation patterns
const tabIndicators = ['home', 'profile', 'settings', 'search', 'notifications'];
const hasTabScreens = tabIndicators.some(indicator =>
screenNames.some(name => name.includes(indicator))
);
// Check for drawer navigation patterns
const drawerIndicators = ['menu', 'sidebar', 'drawer'];
const hasDrawerScreens = drawerIndicators.some(indicator =>
screenNames.some(name => name.includes(indicator))
);
if (screens.length >= 3 && hasTabScreens) {
return 'tabs';
} else if (screens.length >= 4 && hasDrawerScreens) {
return 'drawer';
} else {
return 'stack';
}
}
async generateStackApp(screens) {
const imports = screens.map(screen =>
`import ${screen.component} from './screens/${screen.file.replace('.tsx', '').replace('.ts', '')}';`
).join('\n');
const screenComponents = screens.map(screen =>
` <Stack.Screen
name="${screen.name}"
component={${screen.component}}
options={{ title: '${this.formatTitle(screen.name)}' }}
/>`
).join('\n');
const homeScreen = screens.find(s => s.name.includes('Home')) || screens[0];
const appContent = `
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
${imports}
const Stack = createNativeStackNavigator();
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName="${homeScreen.name}"
screenOptions={{
headerStyle: {
backgroundColor: '#007AFF',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
${screenComponents}
</Stack.Navigator>
<StatusBar style="light" />
</NavigationContainer>
</SafeAreaProvider>
);
}`.trim();
await fs.writeFile(path.join(this.rnProjectPath, 'App.tsx'), appContent);
}
async generateTabApp(screens) {
// Separate tab screens from modal screens
const tabScreens = screens.filter(s =>
['home', 'profile', 'settings', 'search'].some(tab =>
s.name.toLowerCase().includes(tab)
)
).slice(0, 5); // Limit to 5 tabs
const modalScreens = screens.filter(s => !tabScreens.includes(s));
const imports = [
...tabScreens.map(screen =>
`import ${screen.component} from './screens/${screen.file.replace('.tsx', '').replace('.ts', '')}';`
),
...modalScreens.map(screen =>
`import ${screen.component} from './screens/${screen.file.replace('.tsx', '').replace('.ts', '')}';`
)
].join('\n');
const tabScreenComponents = tabScreens.map(screen =>
` <Tab.Screen
name="${screen.name}"
component={${screen.component}}
options={{
title: '${this.formatTitle(screen.name)}',
tabBarIcon: ({ color, size }) => (
<Ionicons name="${this.getIconForScreen(screen.name)}" size={size} color={color} />
),
}}
/>`
).join('\n');
const modalScreenComponents = modalScreens.map(screen =>
` <Stack.Screen
name="${screen.name}"
component={${screen.component}}
options={{ title: '${this.formatTitle(screen.name)}' }}
/>`
).join('\n');
const appContent = `
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
${imports}
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
function MainTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: 'gray',
headerStyle: {
backgroundColor: '#007AFF',
},
headerTintColor: '#fff',
}}
>
${tabScreenComponents}
</Tab.Navigator>
);
}
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="MainTabs"
component={MainTabs}
options={{ headerShown: false }}
/>
${modalScreenComponents}
</Stack.Navigator>
<StatusBar style="auto" />
</NavigationContainer>
</SafeAreaProvider>
);
}`.trim();
await fs.writeFile(path.join(this.rnProjectPath, 'App.tsx'), appContent);
}
async generateDrawerApp(screens) {
const imports = screens.map(screen =>
`import ${screen.component} from './screens/${screen.file.replace('.tsx', '').replace('.ts', '')}';`
).join('\n');
const screenComponents = screens.map(screen =>
` <Drawer.Screen
name="${screen.name}"
component={${screen.component}}
options={{
title: '${this.formatTitle(screen.name)}',
drawerIcon: ({ color, size }) => (
<Ionicons name="${this.getIconForScreen(screen.name)}" size={size} color={color} />
),
}}
/>`
).join('\n');
const appContent = `
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
${imports}
const Drawer = createDrawerNavigator();
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Drawer.Navigator
screenOptions={{
drawerActiveTintColor: '#007AFF',
drawerInactiveTintColor: 'gray',
headerStyle: {
backgroundColor: '#007AFF',
},
headerTintColor: '#fff',
}}
>
${screenComponents}
</Drawer.Navigator>
<StatusBar style="auto" />
</NavigationContainer>
</SafeAreaProvider>
);
}`.trim();
await fs.writeFile(path.join(this.rnProjectPath, 'App.tsx'), appContent);
}
async generateBasicApp() {
const appContent = `
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, ScrollView } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.title}>🎉 Conversion Complete!</Text>
<Text style={styles.subtitle}>Your Next.js app has been converted to React Native</Text>
<View style={styles.section}>
<Text style={styles.sectionTitle}>📱 What's Next?</Text>
<Text style={styles.text}>
• Check the /screens folder for your converted pages{'\n'}
• Review the /components folder for your UI components{'\n'}
• Customize the navigation in App.tsx{'\n'}
• Test on your preferred platform
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>🛠️ Development</Text>
<Text style={styles.text}>
Run: npx expo start{'\n'}
Then scan the QR code with Expo Go app
</Text>
</View>
</ScrollView>
<StatusBar style="auto" />
</SafeAreaView>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 20,
alignItems: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
color: '#007AFF',
},
subtitle: {
fontSize: 16,
textAlign: 'center',
marginBottom: 30,
color: '#666',
},
section: {
backgroundColor: 'white',
padding: 20,
borderRadius: 12,
marginBottom: 20,
width: '100%',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
color: '#333',
},
text: {
fontSize: 14,
lineHeight: 20,
color: '#666',
},
});`.trim();
await fs.writeFile(path.join(this.rnProjectPath, 'App.tsx'), appContent);
}
async createFallbackHomeScreen() {
await fs.ensureDir(this.screensDir);
const homeScreenContent = `
import React from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function HomeScreen() {
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.title}>🏠 Welcome Home</Text>
<Text style={styles.subtitle}>Your Next.js app has been converted!</Text>
<View style={styles.card}>
<Text style={styles.cardTitle}>✅ Conversion Status</Text>
<Text style={styles.cardText}>
Your app has been successfully converted from Next.js to React Native.
Check the other screens and components that were generated.
</Text>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>📱 Next Steps</Text>
<Text style={styles.cardText}>
• Customize this screen with your content{'\n'}
• Add navigation between screens{'\n'}
• Test on different devices{'\n'}
• Deploy to app stores
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
content: {
padding: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 8,
color: '#1a1a1a',
},
subtitle: {
fontSize: 16,
textAlign: 'center',
marginBottom: 30,
color: '#666',
},
card: {
backgroundColor: 'white',
padding: 20,
borderRadius: 12,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
cardTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
color: '#007AFF',
},
cardText: {
fontSize: 14,
lineHeight: 20,
color: '#444',
},
});`.trim();
await fs.writeFile(path.join(this.screensDir, 'HomeScreen.tsx'), homeScreenContent);
}
formatTitle(screenName) {
return screenName
.replace('Screen', '')
.replace(/([A-Z])/g, ' $1')
.trim();
}
getIconForScreen(screenName) {
const iconMap = {
'HomeScreen': 'home',
'ProfileScreen': 'person',
'SettingsScreen': 'settings',
'SearchScreen': 'search',
'NotificationsScreen': 'notifications',
'ContactScreen': 'mail',
'AboutScreen': 'information-circle',
'LoginScreen': 'log-in',
'SignupScreen': 'person-add',
};
return iconMap[screenName] || 'document';
}
async generateNavigationTypes(screens) {
const screenParams = screens.map(screen =>
` ${screen.name}: undefined;`
).join('\n');
const typesContent = `
export type RootStackParamList = {
${screenParams}
};
export type RootTabParamList = {
${screenParams}
};
export type RootDrawerParamList = {
${screenParams}
};
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}`.trim();
await fs.ensureDir(path.join(this.rnProjectPath, 'types'));
await fs.writeFile(path.join(this.rnProjectPath, 'types', 'navigation.ts'), typesContent);
}
}