zura-stack-native
Version:
A comprehensive React Native CLI project generator with production-ready setup
720 lines (623 loc) • 19.1 kB
JavaScript
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 };