UNPKG

zura-stack-native

Version:

A comprehensive React Native CLI project generator with production-ready setup

720 lines (623 loc) 19.1 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); class FileGenerator { constructor(config) { this.config = config; } async generateAll() { try { await this.createDirectoryStructure(); await this.generateResponsiveUtils(); await this.generateStoreSetup(); await this.generateLoadingHook(); await this.generateSkeletonComponent(); await this.generateNavigationSetup(); await this.generateScreens(); await this.generateAPI(); await this.generateGluestackProvider(); if (this.config.testing) { await this.generateTestUtils(); } } catch (error) { throw new Error(`File generation failed: ${error.message}`); } } async createDirectoryStructure() { const directories = [ 'src/api', 'src/components/ui/gluestack-ui-provider', 'src/components/ui/skeleton', 'src/hooks', 'src/navigation', 'src/screens/home', 'src/screens/profile', 'src/screens/settings', 'src/screens/notifications', 'src/store', 'src/utils' ]; for (const dir of directories) { await fs.ensureDir(path.join(this.config.projectPath, dir)); } } async generateResponsiveUtils() { const content = `import { Dimensions } from 'react-native'; import { widthPercentageToDP as wp, heightPercentageToDP as hp, } from 'react-native-responsive-screen'; const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); // Base dimensions for design (iPhone 11 Pro - 375x812) const baseWidth = 375; const baseHeight = 812; // Responsive font size function export const responsiveFontSize = (size: number): number => { const scale = screenWidth / baseWidth; const newSize = size * scale; return Math.round(newSize); }; // Responsive pixel function export const responsivePixel = (size: number): number => { const scale = screenWidth / baseWidth; return Math.round(size * scale); }; // Responsive spacing export const responsiveSpacing = { xs: responsivePixel(4), sm: responsivePixel(8), md: responsivePixel(16), lg: responsivePixel(24), xl: responsivePixel(32), xxl: responsivePixel(48), }; // Responsive fonts export const responsiveFonts = { xs: responsiveFontSize(12), sm: responsiveFontSize(14), base: responsiveFontSize(16), lg: responsiveFontSize(18), xl: responsiveFontSize(20), '2xl': responsiveFontSize(24), '3xl': responsiveFontSize(30), '4xl': responsiveFontSize(36), }; // Responsive icons export const responsiveIcons = { xs: responsivePixel(12), sm: responsivePixel(16), md: responsivePixel(20), lg: responsivePixel(24), xl: responsivePixel(32), xxl: responsivePixel(48), }; // Responsive button sizes export const responsiveButton = { small: { height: responsivePixel(32), fontSize: responsiveFontSize(12), paddingHorizontal: responsivePixel(12), }, medium: { height: responsivePixel(40), fontSize: responsiveFontSize(14), paddingHorizontal: responsivePixel(16), }, large: { height: responsivePixel(48), fontSize: responsiveFontSize(16), paddingHorizontal: responsivePixel(20), }, }; // Screen dimensions export const screenDimensions = { width: screenWidth, height: screenHeight, isSmallDevice: screenWidth < 375, isMediumDevice: screenWidth >= 375 && screenWidth < 414, isLargeDevice: screenWidth >= 414, isTablet: screenWidth >= 768, }; // Platform-specific tab bar height and padding export const platformSpecific = { tabBarHeight: responsivePixel(83), // iOS with home indicator tabBarPaddingBottom: responsivePixel(34), // iOS home indicator height safeAreaTop: responsivePixel(44), // iOS status bar + navigation bar safeAreaBottom: responsivePixel(34), // iOS home indicator };`; await fs.writeFile(path.join(this.config.projectPath, 'src/utils/responsive.ts'), content); } async generateStoreSetup() { const content = `import { create } from 'zustand'; interface CounterState { count: number; incrementCounter: () => void; decrementCounter: () => void; resetCounter: () => void; } interface UserPreferencesState { theme: 'light' | 'dark'; notifications: boolean; language: 'en' | 'es' | 'fr'; setTheme: (theme: 'light' | 'dark') => void; updateUserPreferences: (preferences: Partial<Omit<UserPreferencesState, 'setTheme' | 'updateUserPreferences'>>) => void; } export const useCounter = create<CounterState>((set) => ({ count: 0, incrementCounter: () => set((state) => ({ count: state.count + 1 })), decrementCounter: () => set((state) => ({ count: state.count - 1 })), resetCounter: () => set({ count: 0 }), })); export const useUserPreferences = create<UserPreferencesState>((set) => ({ theme: 'light', notifications: true, language: 'en', setTheme: (theme) => set({ theme }), updateUserPreferences: (preferences) => set(preferences), })); // Combined store for backward compatibility export const useExampleStore = create<CounterState>((set) => ({ count: 0, incrementCounter: () => set((state) => ({ count: state.count + 1 })), decrementCounter: () => set((state) => ({ count: state.count - 1 })), resetCounter: () => set({ count: 0 }), }));`; await fs.writeFile(path.join(this.config.projectPath, 'src/store/index.ts'), content); } async generateLoadingHook() { const content = `import { useState, useCallback } from 'react'; interface UseLoadingReturn { isLoading: boolean; startLoading: () => void; stopLoading: () => void; withLoading: <T>(asyncFunction: () => Promise<T> | T) => Promise<T>; } export const useLoading = (initialState: boolean = false): UseLoadingReturn => { const [isLoading, setIsLoading] = useState(initialState); const startLoading = useCallback(() => { setIsLoading(true); }, []); const stopLoading = useCallback(() => { setIsLoading(false); }, []); const withLoading = useCallback(async <T>(asyncFunction: () => Promise<T> | T): Promise<T> => { startLoading(); try { const result = await asyncFunction(); return result; } finally { stopLoading(); } }, [startLoading, stopLoading]); return { isLoading, startLoading, stopLoading, withLoading, }; };`; await fs.writeFile(path.join(this.config.projectPath, 'src/hooks/useLoading.ts'), content); } async generateSkeletonComponent() { const content = `import React from 'react'; import { View, Animated, Easing } from 'react-native'; import { responsiveSpacing } from '@/utils/responsive'; interface SkeletonProps { width?: number | string; height?: number; borderRadius?: number; style?: any; } export const Skeleton: React.FC<SkeletonProps> = ({ width = '100%', height = responsiveSpacing.md, borderRadius = responsiveSpacing.xs, style, }) => { const animatedValue = React.useRef(new Animated.Value(0)).current; React.useEffect(() => { const animation = Animated.loop( Animated.sequence([ Animated.timing(animatedValue, { toValue: 1, duration: 1000, easing: Easing.inOut(Easing.ease), useNativeDriver: false, }), Animated.timing(animatedValue, { toValue: 0, duration: 1000, easing: Easing.inOut(Easing.ease), useNativeDriver: false, }), ]) ); animation.start(); return () => animation.stop(); }, [animatedValue]); const opacity = animatedValue.interpolate({ inputRange: [0, 1], outputRange: [0.3, 0.7], }); return ( <Animated.View style={[ { width, height, borderRadius, backgroundColor: '#E1E9EE', opacity, }, style, ]} /> ); }; // Skeleton components for different use cases export const SkeletonText = ({ lines = 1, ...props }: { lines?: number } & SkeletonProps) => ( <View> {Array.from({ length: lines }).map((_, index) => ( <Skeleton key={index} height={responsiveSpacing.sm} style={{ marginBottom: index < lines - 1 ? responsiveSpacing.xs : 0 }} {...props} /> ))} </View> ); export const SkeletonButton = (props: SkeletonProps) => ( <Skeleton height={responsiveSpacing.xl} borderRadius={responsiveSpacing.sm} {...props} /> ); export const SkeletonCard = (props: SkeletonProps) => ( <Skeleton height={responsiveSpacing.xxl * 2} borderRadius={responsiveSpacing.md} {...props} /> );`; await fs.writeFile(path.join(this.config.projectPath, 'src/components/ui/skeleton/index.tsx'), content); } async generateNavigationSetup() { const content = `import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Home, User, Settings, Bell } from 'lucide-react-native'; import { HomeScreen } from '@/screens/home/HomeScreen'; import { ProfileScreen } from '@/screens/profile/ProfileScreen'; import { SettingsScreen } from '@/screens/settings/SettingsScreen'; import { NotificationsScreen } from '@/screens/notifications/NotificationsScreen'; const Tab = createBottomTabNavigator(); export const RootNavigator = () => { return ( <NavigationContainer> <Tab.Navigator screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => { let iconName; if (route.name === 'Home') { iconName = Home; } else if (route.name === 'Profile') { iconName = User; } else if (route.name === 'Settings') { iconName = Settings; } else if (route.name === 'Notifications') { iconName = Bell; } return <iconName size={size} color={color} />; }, tabBarActiveTintColor: '#007AFF', tabBarInactiveTintColor: 'gray', })} > <Tab.Screen name="Home" component={HomeScreen} /> <Tab.Screen name="Profile" component={ProfileScreen} /> <Tab.Screen name="Settings" component={SettingsScreen} /> <Tab.Screen name="Notifications" component={NotificationsScreen} /> </Tab.Navigator> </NavigationContainer> ); };`; await fs.writeFile(path.join(this.config.projectPath, 'src/navigation/RootNavigator.tsx'), content); } async generateScreens() { // Home Screen const homeScreen = `import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { useCounter } from '@/store'; import { Button } from '@/components/ui/button'; import { Plus, Minus, RotateCcw } from 'lucide-react-native'; export const HomeScreen = () => { const { count, incrementCounter, decrementCounter, resetCounter } = useCounter(); return ( <View style={styles.container}> <Text style={styles.title}>Welcome to ZuraStackNative!</Text> <Text style={styles.subtitle}>Your React Native app is ready</Text> <View style={styles.counterContainer}> <Text style={styles.counterText}>Counter: {count}</Text> <View style={styles.buttonContainer}> <Button onPress={decrementCounter} style={styles.button}> <Minus size={20} color="white" /> <Text style={styles.buttonText}>Decrease</Text> </Button> <Button onPress={incrementCounter} style={styles.button}> <Plus size={20} color="white" /> <Text style={styles.buttonText}>Increase</Text> </Button> </View> <Button onPress={resetCounter} style={styles.resetButton}> <RotateCcw size={20} color="white" /> <Text style={styles.buttonText}>Reset</Text> </Button> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, backgroundColor: '#f5f5f5', }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10, textAlign: 'center', }, subtitle: { fontSize: 16, color: '#666', marginBottom: 40, textAlign: 'center', }, counterContainer: { alignItems: 'center', }, counterText: { fontSize: 18, marginBottom: 20, }, buttonContainer: { flexDirection: 'row', gap: 10, marginBottom: 20, }, button: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#007AFF', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8, gap: 5, }, resetButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FF3B30', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8, gap: 5, }, buttonText: { color: 'white', fontWeight: '600', }, });`; await fs.writeFile(path.join(this.config.projectPath, 'src/screens/home/HomeScreen.tsx'), homeScreen); // Profile Screen const profileScreen = `import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export const ProfileScreen = () => { return ( <View style={styles.container}> <Text style={styles.title}>Profile Screen</Text> <Text style={styles.subtitle}>This is your profile page</Text> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, backgroundColor: '#f5f5f5', }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10, }, subtitle: { fontSize: 16, color: '#666', }, });`; await fs.writeFile(path.join(this.config.projectPath, 'src/screens/profile/ProfileScreen.tsx'), profileScreen); // Settings Screen const settingsScreen = `import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export const SettingsScreen = () => { return ( <View style={styles.container}> <Text style={styles.title}>Settings Screen</Text> <Text style={styles.subtitle}>Configure your app settings here</Text> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, backgroundColor: '#f5f5f5', }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10, }, subtitle: { fontSize: 16, color: '#666', }, });`; await fs.writeFile(path.join(this.config.projectPath, 'src/screens/settings/SettingsScreen.tsx'), settingsScreen); // Notifications Screen const notificationsScreen = `import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export const NotificationsScreen = () => { return ( <View style={styles.container}> <Text style={styles.title}>Notifications Screen</Text> <Text style={styles.subtitle}>View your notifications here</Text> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, backgroundColor: '#f5f5f5', }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10, }, subtitle: { fontSize: 16, color: '#666', }, });`; await fs.writeFile(path.join(this.config.projectPath, 'src/screens/notifications/NotificationsScreen.tsx'), notificationsScreen); } async generateAPI() { const apiContent = `import axios from 'axios'; const API_BASE_URL = 'https://api.example.com'; export const api = axios.create({ baseURL: API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // Request interceptor api.interceptors.request.use( (config) => { // Add auth token if available // const token = await AsyncStorage.getItem('authToken'); // if (token) { // config.headers.Authorization = \`Bearer \${token}\`; // } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor api.interceptors.response.use( (response) => { return response; }, (error) => { // Handle common errors if (error.response?.status === 401) { // Handle unauthorized } return Promise.reject(error); } ); export default api;`; await fs.writeFile(path.join(this.config.projectPath, 'src/api/api.ts'), apiContent); const queryProviderContent = `import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 2, staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 10 * 60 * 1000, // 10 minutes }, }, }); interface QueryProviderProps { children: React.ReactNode; } export const QueryProvider: React.FC<QueryProviderProps> = ({ children }) => { return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); };`; await fs.writeFile(path.join(this.config.projectPath, 'src/api/QueryProvider.tsx'), queryProviderContent); } async generateGluestackProvider() { const configContent = `import { createConfig } from '@gluestack-ui/config'; import { config as defaultConfig } from '@gluestack-ui/config'; export const config = createConfig({ ...defaultConfig, tokens: { ...defaultConfig.tokens, colors: { ...defaultConfig.tokens.colors, primary0: '#ffffff', primary400: '#007AFF', primary500: '#007AFF', primary600: '#0056CC', }, }, });`; await fs.writeFile(path.join(this.config.projectPath, 'src/components/ui/gluestack-ui-provider/config.ts'), configContent); const indexContent = `import React from 'react'; import { GluestackUIProvider } from '@gluestack-ui/themed'; import { config } from './config'; interface GluestackProviderProps { children: React.ReactNode; } export const GluestackProvider: React.FC<GluestackProviderProps> = ({ children }) => { return ( <GluestackUIProvider config={config}> {children} </GluestackUIProvider> ); };`; await fs.writeFile(path.join(this.config.projectPath, 'src/components/ui/gluestack-ui-provider/index.tsx'), indexContent); } async generateTestUtils() { const content = `import React from 'react'; import { render } from '@testing-library/react-native'; import { QueryProvider } from '@/api/QueryProvider'; import { GluestackProvider } from '@/components/ui/gluestack-ui-provider'; const AllTheProviders = ({ children }: { children: React.ReactNode }) => { return ( <QueryProvider> <GluestackProvider> {children} </GluestackProvider> </QueryProvider> ); }; const customRender = (ui: React.ReactElement, options = {}) => render(ui, { wrapper: AllTheProviders, ...options }); export * from '@testing-library/react-native'; export { customRender as render };`; await fs.writeFile(path.join(this.config.projectPath, 'src/utils/test-utils.tsx'), content); } } module.exports = { FileGenerator };