Skip to main content

⚑ CLI Code Generators

Shipnative includes code generators that create fully-functional screens, components, stores, and API endpoints with a single command. No more copying and pasting boilerplate!
For AI Users: You don’t need these generators when using AI! Just ask your AI to β€œcreate a ProfileSettings screen” and it will generate the code. These generators are for manual development or when you want a quick starting point.

🎯 Why Use Generators?

Without generators:
# You have to:
1. Create a new file
2. Write all the imports
3. Set up the component structure
4. Add styling boilerplate
5. Remember the correct patterns
6. Export the component
With generators:
yarn generate screen ProfileSettings
# Done! βœ… Fully functional screen ready to customize
What you get:
  • βœ… Correct file structure
  • βœ… All imports included
  • βœ… Styling with Unistyles/design tokens
  • βœ… TypeScript types
  • βœ… Follows project patterns
  • βœ… Ready to customize

πŸš€ Quick Reference

# Generate a screen
yarn generate screen ProfileSettings

# Generate a component
yarn generate component UserCard

# Generate a Zustand store
yarn generate store notifications

# Generate an API endpoint
yarn generate api user-profile

πŸ“± Screen Generator

What It Does

Creates a complete screen component with:
  • Gradient background
  • Safe area handling
  • ScrollView for content
  • Proper styling with design tokens
  • Navigation ready

Usage

yarn generate screen <ScreenName>

Example

yarn generate screen ProfileSettings
Creates: apps/app/app/screens/ProfileSettingsScreen.tsx Generated code:
import { View, ScrollView } from 'react-native'
import { StyleSheet, useUnistyles } from 'react-native-unistyles'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LinearGradient } from 'expo-linear-gradient'
import { Text } from '@/components/Text'

export const ProfileSettingsScreen = () => {
  const { theme } = useUnistyles()
  const insets = useSafeAreaInsets()

  return (
    <View style={styles.container}>
      <LinearGradient
        colors={[
          theme.colors.gradientStart,
          theme.colors.gradientMiddle,
          theme.colors.gradientEnd,
        ]}
        style={styles.gradient}
      >
        <ScrollView
          contentContainerStyle={[
            styles.scrollContent,
            { paddingTop: insets.top + theme.spacing.lg }
          ]}
        >
          <Text preset="heading">Profile Settings</Text>
          {/* Add your content here */}
        </ScrollView>
      </LinearGradient>
    </View>
  )
}

const styles = StyleSheet.create((theme) => ({
  container: { flex: 1 },
  gradient: { flex: 1 },
  scrollContent: {
    paddingHorizontal: theme.spacing.lg,
    paddingBottom: 100,
  },
}))

Next Steps After Generation

  1. Add to navigation:
    // apps/app/app/navigators/AppNavigator.tsx
    import { ProfileSettingsScreen } from '@/screens/ProfileSettingsScreen'
    
    <Stack.Screen name="ProfileSettings" component={ProfileSettingsScreen} />
    
  2. Add TypeScript type:
    // apps/app/app/navigators/navigationTypes.ts
    export type AppStackParamList = {
      ProfileSettings: undefined  // or pass params: { userId: string }
    }
    
  3. Navigate to it:
    navigation.navigate('ProfileSettings')
    

🧩 Component Generator

What It Does

Creates a reusable component with:
  • TypeScript props interface
  • Unistyles styling
  • Design tokens
  • Export ready

Usage

yarn generate component <ComponentName>

Example

yarn generate component UserCard
Creates: apps/app/app/components/UserCard.tsx Generated code:
import { View, Pressable } from 'react-native'
import { StyleSheet, useUnistyles } from 'react-native-unistyles'
import { Text } from './Text'

export interface UserCardProps {
  name: string
  email?: string
  avatar?: string
  onPress?: () => void
}

export const UserCard = ({ name, email, onPress }: UserCardProps) => {
  const { theme } = useUnistyles()

  return (
    <Pressable
      style={styles.container}
      onPress={onPress}
    >
      <Text preset="subheading">{name}</Text>
      {email && <Text preset="body">{email}</Text>}
    </Pressable>
  )
}

const styles = StyleSheet.create((theme) => ({
  container: {
    backgroundColor: theme.colors.card,
    borderRadius: theme.radius.lg,
    padding: theme.spacing.md,
    marginBottom: theme.spacing.md,
    borderWidth: 1,
    borderColor: theme.colors.border,
  },
}))

Using Your Component

import { UserCard } from '@/components/UserCard'

<UserCard
  name="John Doe"
  email="john@example.com"
  onPress={() => navigation.navigate('Profile', { userId: '123' })}
/>

Exporting Your Component

Add to apps/app/app/components/index.ts:
export * from './UserCard'
Now you can import it from anywhere:
import { UserCard } from '@/components'

πŸ—„οΈ Store Generator

What It Does

Creates a Zustand store with:
  • TypeScript types
  • Persistence (saves to device storage)
  • Common CRUD actions
  • Loading states

Usage

yarn generate store <storeName>

Example

yarn generate store notifications
Creates: apps/app/app/stores/notificationsStore.ts Generated code:
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { storage } from '@/utils/storage'

export interface NotificationsState {
  // State
  items: any[]
  loading: boolean

  // Actions
  fetchItems: () => Promise<void>
  addItem: (item: any) => void
  removeItem: (id: string) => void
  clearAll: () => void
}

export const useNotificationsStore = create<NotificationsState>()(
  persist(
    (set, get) => ({
      items: [],
      loading: false,

      fetchItems: async () => {
        set({ loading: true })
        try {
          // TODO: Fetch from your API
          const items = []
          set({ items, loading: false })
        } catch (error) {
          console.error('Error fetching notifications:', error)
          set({ loading: false })
        }
      },

      addItem: (item) => {
        set((state) => ({
          items: [...state.items, item],
        }))
      },

      removeItem: (id) => {
        set((state) => ({
          items: state.items.filter((item) => item.id !== id),
        }))
      },

      clearAll: () => {
        set({ items: [] })
      },
    }),
    {
      name: 'notifications-storage',  // Saved to device storage
      storage: createJSONStorage(() => ({
        getItem: (key) => storage.getString(key) ?? null,
        setItem: (key, value) => storage.set(key, value),
        removeItem: (key) => storage.delete(key),
      })),
    },
  ),
)

Using Your Store

import { useNotificationsStore } from '@/stores/notificationsStore'

function NotificationsScreen() {
  const { items, loading, fetchItems, addItem } = useNotificationsStore()

  useEffect(() => {
    fetchItems()
  }, [])

  if (loading) return <Spinner />

  return (
    <View>
      {items.map(item => (
        <Text key={item.id}>{item.message}</Text>
      ))}
    </View>
  )
}

What is Persistence?

Persistence means the store saves to device storage. When the user closes and reopens the app, the data is still there! Example:
// User adds a notification
addItem({ id: '1', message: 'Hello!' })

// User closes app
// User reopens app

// Data is still there!
console.log(items)  // [{ id: '1', message: 'Hello!' }]

🌐 API Generator

What It Does

Creates an API service with:
  • TypeScript types
  • CRUD operations (Create, Read, Update, Delete)
  • Error handling
  • Supabase integration

Usage

yarn generate api <endpointName>

Example

yarn generate api user-profile
Creates: apps/app/app/services/api/userProfile.ts Generated code:
import { supabase } from '@/services/supabase'

export interface UserProfile {
  id: string
  username: string
  full_name: string
  avatar_url?: string
  bio?: string
}

export const userProfileApi = {
  /**
   * Get user profile by ID
   */
  async getProfile(userId: string): Promise<UserProfile | null> {
    const { data, error } = await supabase
      .from('profiles')
      .select('*')
      .eq('id', userId)
      .single()

    if (error) {
      console.error('Error fetching profile:', error)
      return null
    }

    return data
  },

  /**
   * Update user profile
   */
  async updateProfile(
    userId: string,
    updates: Partial<UserProfile>
  ): Promise<boolean> {
    const { error } = await supabase
      .from('profiles')
      .update(updates)
      .eq('id', userId)

    if (error) {
      console.error('Error updating profile:', error)
      return false
    }

    return true
  },

  /**
   * Delete user profile
   */
  async deleteProfile(userId: string): Promise<boolean> {
    const { error } = await supabase
      .from('profiles')
      .delete()
      .eq('id', userId)

    if (error) {
      console.error('Error deleting profile:', error)
      return false
    }

    return true
  },
}

Using Your API

import { userProfileApi } from '@/services/api/userProfile'

// Get profile
const profile = await userProfileApi.getProfile('user-123')

// Update profile
const success = await userProfileApi.updateProfile('user-123', {
  full_name: 'Jane Doe',
  bio: 'Mobile app developer',
})

// Delete profile
await userProfileApi.deleteProfile('user-123')
For better caching and loading states, use React Query:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { userProfileApi } from '@/services/api/userProfile'

export function useUserProfile(userId: string) {
  return useQuery({
    queryKey: ['user-profile', userId],
    queryFn: () => userProfileApi.getProfile(userId),
    enabled: !!userId,  // Only run if userId exists
  })
}

export function useUpdateUserProfile() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({ userId, updates }) =>
      userProfileApi.updateProfile(userId, updates),
    onSuccess: (_, { userId }) => {
      // Refresh the profile data after update
      queryClient.invalidateQueries({ queryKey: ['user-profile', userId] })
    },
  })
}
Usage in component:
function ProfileScreen({ userId }) {
  const { data: profile, isLoading } = useUserProfile(userId)
  const updateProfile = useUpdateUserProfile()

  const handleSave = () => {
    updateProfile.mutate({
      userId,
      updates: { bio: 'New bio!' }
    })
  }

  if (isLoading) return <Spinner />

  return (
    <View>
      <Text>{profile.full_name}</Text>
      <Button onPress={handleSave} title="Save" />
    </View>
  )
}

πŸ’‘ Tips & Best Practices

When to Use Generators vs AI

Use GeneratorsUse AI
Quick file creationComplex features with business logic
Starting point for manual codingWhen you want AI to handle patterns
Learning the codebase structureBuilding complete flows end-to-end
Simple CRUD operationsCustom implementations

Naming Conventions

Screens:
  • Use PascalCase: ProfileSettings, UserDashboard
  • Generator adds β€œScreen” suffix automatically
  • File: ProfileSettingsScreen.tsx
  • Component: ProfileSettingsScreen
Components:
  • Use PascalCase: UserCard, PostList
  • File: UserCard.tsx
  • Component: UserCard
Stores:
  • Use camelCase: notifications, userPreferences
  • Generator adds β€œStore” suffix automatically
  • File: notificationsStore.ts
  • Hook: useNotificationsStore
APIs:
  • Use kebab-case: user-profile, post-comments
  • File: userProfile.ts (camelCase)
  • Object: userProfileApi

Customizing Templates

Want to change what the generators create? Edit the generator script! Location: scripts/generate.js (create this file if it doesn’t exist) Example: Add default error handling to all API generators:
// In generateApi function
const template = `
export const ${name}Api = {
  async get(id: string) {
    try {
      const { data, error } = await supabase
        .from('${name}')
        .select('*')
        .eq('id', id)
        .single()

      if (error) throw error
      return data
    } catch (error) {
      console.error('[${name}Api] Get error:', error)
      Sentry.captureException(error)  // Add Sentry tracking
      return null
    }
  },
}
`

πŸ†˜ Troubleshooting

Cause: The generator script isn’t set up in your project.Solution:
  1. Check if scripts/generate.js exists
  2. Verify package.json has the script:
    {
      "scripts": {
        "generate": "node scripts/generate.js"
      }
    }
    
  3. Run yarn install to refresh scripts
Cause: Path aliases may not be configured.Solution:
  1. Check tsconfig.json has:
    {
      "compilerOptions": {
        "paths": {
          "@/*": ["./app/*"]
        }
      }
    }
    
  2. Update the generator template to use correct paths
Cause: You need to manually add it to the navigator.Solution:
  1. Import the screen in AppNavigator.tsx
  2. Add <Stack.Screen> entry
  3. Add TypeScript type to navigationTypes.ts
  4. See Screen Generator for details
Cause: Storage permissions or middleware configuration.Solution:
  1. Check that MMKV storage is initialized in utils/storage.ts
  2. Verify the store has persist() middleware
  3. Check device storage permissions
  4. Try clearing app data and reinstalling
Cause: Mock Supabase doesn’t have the table you’re querying.Solution:
  1. Use mock helpers to seed data:
    import { mockSupabaseHelpers } from '@/services/mocks/supabase'
    
    mockSupabaseHelpers.seedTable('profiles', [
      { id: '1', username: 'johndoe', full_name: 'John Doe' }
    ])
    
  2. Or add real Supabase credentials to test with real data

πŸŽ“ Next Steps

Pro Tip: Generate files as a starting point, then customize them for your needs. Don’t be afraid to modify the generated code - it’s yours to own!