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
406 lines (346 loc) • 12.2 kB
JavaScript
// 🎯 Deterministic Next.js to React Native Converter
// Handles 90% of conversions without AI - like v0.dev for mobile
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
export class DeterministicConverter {
constructor() {
this.conversionRules = new Map();
this.initializeRules();
}
initializeRules() {
// 🎨 UI Component Mappings (like v0.dev templates)
this.conversionRules.set('SHADCN_COMPONENTS', {
Button: this.createButtonTemplate,
Input: this.createInputTemplate,
Card: this.createCardTemplate,
CardHeader: this.createCardHeaderTemplate,
CardTitle: this.createCardTitleTemplate,
CardContent: this.createCardContentTemplate,
Dialog: this.createModalTemplate,
Sheet: this.createModalTemplate,
Avatar: this.createAvatarTemplate,
Badge: this.createBadgeTemplate,
Checkbox: this.createCheckboxTemplate,
Switch: this.createSwitchTemplate,
Select: this.createPickerTemplate,
Textarea: this.createTextAreaTemplate,
Progress: this.createProgressTemplate,
Separator: this.createSeparatorTemplate,
Skeleton: this.createSkeletonTemplate,
Toast: this.createToastTemplate,
Tabs: this.createTabsTemplate,
Alert: this.createAlertTemplate
});
// 🌐 Web API Mappings
this.conversionRules.set('WEB_APIS', {
localStorage: this.createAsyncStorageTemplate,
sessionStorage: this.createAsyncStorageTemplate,
fetch: this.createFetchTemplate,
window: this.createWindowTemplate,
document: this.createDocumentTemplate,
navigator: this.createNavigatorTemplate,
location: this.createLocationTemplate
});
// 🧭 Navigation Mappings
this.conversionRules.set('NAVIGATION', {
'router.push': this.createNavigationTemplate,
'router.back': this.createGoBackTemplate,
'router.replace': this.createReplaceTemplate,
'useRouter': this.createUseNavigationTemplate,
'Link': this.createTouchableLinkTemplate
});
// 📱 React Native Element Mappings
this.conversionRules.set('ELEMENTS', {
div: 'View',
span: 'Text',
p: 'Text',
h1: 'Text',
h2: 'Text',
h3: 'Text',
h4: 'Text',
h5: 'Text',
h6: 'Text',
button: 'TouchableOpacity',
input: 'TextInput',
img: 'Image',
a: 'TouchableOpacity',
section: 'View',
article: 'View',
main: 'ScrollView',
header: 'View',
footer: 'View',
nav: 'View',
ul: 'View',
li: 'View',
form: 'View'
});
}
// 🚀 Main conversion method - deterministic like v0.dev
async convertFile(sourceCode, fileName) {
try {
console.log(chalk.gray(` 🎯 Attempting deterministic conversion...`));
// Phase 1: Simple pattern matching (no AST parsing for now)
let convertedCode = this.applySimpleTransformations(sourceCode, fileName);
// Phase 2: Generate React Native template
convertedCode = this.generateReactNativeTemplate(convertedCode, fileName);
// Phase 3: Add required imports
convertedCode = this.addRequiredImports(convertedCode);
// Phase 4: Generate styles
convertedCode = this.generateStyles(convertedCode);
// Phase 5: Final validation
const isValid = this.validateReactNativeCode(convertedCode);
if (!isValid.success) {
console.log(chalk.yellow(` ⚠️ Deterministic conversion validation failed: ${isValid.errors.join(', ')}`));
return {
success: false,
reason: `Validation failed: ${isValid.errors.join(', ')}`,
needsAI: true
};
}
console.log(chalk.green(` ✅ Deterministic conversion successful`));
return {
success: true,
code: convertedCode,
tokensUsed: 0, // Deterministic = 0 tokens!
patterns: 'deterministic'
};
} catch (error) {
console.log(chalk.yellow(` ⚠️ Deterministic conversion error: ${error.message}`));
return {
success: false,
reason: error.message,
needsAI: true
};
}
}
// 🔄 Simple pattern transformations (without AST for now)
applySimpleTransformations(sourceCode, fileName) {
let code = sourceCode;
// Transform HTML elements to React Native components
const elementMappings = this.conversionRules.get('ELEMENTS');
for (const [htmlElement, rnElement] of Object.entries(elementMappings)) {
// Transform opening tags
const openingTagRegex = new RegExp(`<${htmlElement}(\\s[^>]*)?(?<!/)>`, 'gi');
code = code.replace(openingTagRegex, `<${rnElement}$1>`);
// Transform closing tags
const closingTagRegex = new RegExp(`</${htmlElement}>`, 'gi');
code = code.replace(closingTagRegex, `</${rnElement}>`);
// Transform self-closing tags
const selfClosingRegex = new RegExp(`<${htmlElement}(\\s[^>]*)?/>`, 'gi');
code = code.replace(selfClosingRegex, `<${rnElement}$1/>`);
}
// Transform event handlers
code = code.replace(/onClick=/g, 'onPress=');
code = code.replace(/onChange=/g, 'onChangeText=');
// Transform props
code = code.replace(/className=/g, 'style=');
code = code.replace(/src=/g, 'source=');
// Transform common Shadcn components
code = this.transformShadcnComponents(code);
// Transform web APIs
code = this.transformWebAPIs(code);
return code;
}
transformShadcnComponents(code) {
// Simple Button transformation
code = code.replace(
/<Button([^>]*?)>([^<]+)<\/Button>/gi,
'<TouchableOpacity$1 activeOpacity={0.7}><Text>$2</Text></TouchableOpacity>'
);
// Simple Input transformation
code = code.replace(
/<Input([^>]*?)\/>/gi,
'<TextInput$1/>'
);
// Simple Card transformation
code = code.replace(/<Card([^>]*?)>/gi, '<View$1>');
code = code.replace(/<\/Card>/gi, '</View>');
return code;
}
transformWebAPIs(code) {
// localStorage transformations
code = code.replace(/localStorage\.setItem\(/g, 'AsyncStorage.setItem(');
code = code.replace(/localStorage\.getItem\(/g, 'AsyncStorage.getItem(');
code = code.replace(/localStorage\.removeItem\(/g, 'AsyncStorage.removeItem(');
// Navigation transformations
code = code.replace(/router\.push\(/g, 'navigation.navigate(');
code = code.replace(/router\.back\(\)/g, 'navigation.goBack()');
return code;
}
generateReactNativeTemplate(code, fileName) {
const isPage = fileName.includes('page.') || fileName.includes('/pages/');
const componentName = this.getComponentName(fileName);
if (isPage) {
return this.generateScreenTemplate(code, componentName);
} else {
return this.generateComponentTemplate(code, componentName);
}
}
generateScreenTemplate(code, screenName) {
// Extract the main JSX content
const jsxMatch = code.match(/return\s*\(([\s\S]*?)\);?/);
const jsxContent = jsxMatch ? jsxMatch[1] : '<View><Text>Screen Content</Text></View>';
return `import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
TextInput,
SafeAreaView
} from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
export default function ${screenName}() {
const navigation = useNavigation();
const route = useRoute();
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
${jsxContent}
</ScrollView>
</SafeAreaView>
);
}
${this.generateDefaultStyles('screen')}`;
}
generateComponentTemplate(code, componentName) {
// Extract the main JSX content
const jsxMatch = code.match(/return\s*\(([\s\S]*?)\);?/);
const jsxContent = jsxMatch ? jsxMatch[1] : '<View><Text>Component Content</Text></View>';
return `import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity
} from 'react-native';
export default function ${componentName}(props) {
return (
<View style={styles.container}>
${jsxContent}
</View>
);
}
${this.generateDefaultStyles('component')}`;
}
addRequiredImports(code) {
// Basic implementation - could be enhanced
return code;
}
generateStyles(code) {
// Check if styles already exist
if (code.includes('const styles = StyleSheet.create')) {
return code;
}
// Add basic styles
const additionalStyles = `
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
padding: 16,
},
content: {
flexGrow: 1,
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
marginVertical: 8,
},
buttonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
input: {
borderWidth: 1,
borderColor: '#E1E5E9',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 12,
fontSize: 16,
marginVertical: 8,
backgroundColor: '#ffffff',
},
});`;
return code + additionalStyles;
}
validateReactNativeCode(code) {
const errors = [];
// Check for common React Native issues
if (code.includes('className=')) {
errors.push('Found className usage - should be style prop');
}
if (code.includes('onClick=')) {
errors.push('Found onClick usage - should be onPress');
}
// Check for required React Native imports
if (!code.includes('from \'react-native\'')) {
errors.push('Missing React Native imports');
}
// Simple check for unwrapped text (basic implementation)
const suspiciousTextMatches = code.match(/>\s*[a-zA-Z0-9\s]+\s*</g);
if (suspiciousTextMatches) {
// This is a simplified check - could have false positives
const hasUnwrappedText = suspiciousTextMatches.some(match =>
!match.includes('<Text>') && !match.includes('{') && match.trim().length > 2
);
if (hasUnwrappedText) {
errors.push('Possible unwrapped text - all text must be in <Text> components');
}
}
return {
success: errors.length === 0,
errors
};
}
getComponentName(fileName) {
const baseName = path.basename(fileName, path.extname(fileName));
let componentName = baseName
.replace(/[^a-zA-Z0-9]/g, '')
.replace(/^./, str => str.toUpperCase());
// Handle common naming patterns
if (componentName.toLowerCase() === 'page') {
componentName = 'HomeScreen';
} else if (componentName.toLowerCase() === 'index') {
componentName = 'MainScreen';
}
return componentName;
}
generateDefaultStyles(type) {
return `
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
padding: 16,
},
});`;
}
// Placeholder template methods (simplified for now)
createButtonTemplate(props, children) {
return `<TouchableOpacity style={styles.button} onPress={${props.onClick}} activeOpacity={0.7}>
<Text style={styles.buttonText}>${children}</Text>
</TouchableOpacity>`;
}
createInputTemplate(props) {
return `<TextInput
style={styles.input}
placeholder="${props.placeholder || ''}"
value={${props.value}}
onChangeText={${props.onChange}}
${props.secureTextEntry ? 'secureTextEntry={true}' : ''}
/>`;
}
createCardTemplate(props, children) {
return `<View style={styles.card}>${children}</View>`;
}
}
export default DeterministicConverter;