expo-forge 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 moasko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Expo Forge - Bulletproof Expo Architecture
2
+
3
+ Forge modern Expo apps with bulletproof architecture. Generate complete projects and features with TanStack Query, Zustand, and NativeWind.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g expo-forge
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Initialize a new Expo project
14
+
15
+ ```bash
16
+ expo-forge init my-app
17
+ ```
18
+
19
+ This creates:
20
+ - ✅ New Expo project with create-expo-app
21
+ - ✅ Modern dependencies installation
22
+ - ✅ Bulletproof structure (src/api, src/features, etc.)
23
+ - ✅ TanStack Query & Zustand configuration
24
+ - ✅ Tailwind CSS with NativeWind setup
25
+
26
+ ### Generate a new feature
27
+
28
+ ```bash
29
+ expo-forge generate feature booking
30
+ ```
31
+
32
+ This creates a complete structure:
33
+ ```
34
+ src/features/booking/
35
+ ├── api/ # TanStack Query hooks
36
+ ├── components/ # UI components
37
+ ├── hooks/ # Business logic
38
+ ├── services/ # API calls
39
+ ├── store/ # Zustand stores
40
+ ├── types/ # TypeScript types
41
+ ├── utils/ # Helpers
42
+ ├── BookingScreen.tsx # Main screen
43
+ └── index.ts # Exports
44
+ ```
45
+
46
+ ## Tech Stack
47
+
48
+ - **React Native** - Mobile framework
49
+ - **Expo** - React Native platform
50
+ - **TanStack Query** - Async state management
51
+ - **Zustand** - Local state management
52
+ - **Axios** - HTTP client
53
+ - **NativeWind** - Tailwind CSS for React Native
54
+ - **Lucide** - Icons
55
+
56
+ ## Links
57
+
58
+ - [GitHub Repository](https://github.com/moasko/expo-forge)
59
+ - [Documentation](https://github.com/moasko/expo-forge#readme)
60
+ - [Issues](https://github.com/moasko/expo-forge/issues)
61
+
62
+ ## License
63
+
64
+ MIT - see LICENSE file for details.
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const path = require('path');
5
+
6
+ // Import our modules
7
+ const initExpo = require('../lib/initExpo');
8
+ const featureGenerator = require('../lib/featureGenerator');
9
+ const logger = require('../lib/logger');
10
+
11
+ program
12
+ .name('expo-forge')
13
+ .description('Forge modern Expo apps with bulletproof architecture')
14
+ .version('1.0.0');
15
+
16
+ program
17
+ .command('init [projectName]')
18
+ .description('Initialize a new Expo project with bulletproof architecture')
19
+ .action(async (projectName) => {
20
+ try {
21
+ if (!projectName) {
22
+ logger.error('Please provide a project name: expo-forge init <projectName>');
23
+ process.exit(1);
24
+ }
25
+
26
+ logger.rocket(`Initializing Expo Forge project: ${projectName}`);
27
+ await initExpo.initializeProject(projectName);
28
+ logger.success(`Project ${projectName} created successfully!`);
29
+ logger.info(`Run: cd ${projectName} && npx expo start`);
30
+ } catch (error) {
31
+ logger.error(`Failed to initialize project: ${error.message}`);
32
+ process.exit(1);
33
+ }
34
+ });
35
+
36
+ program
37
+ .command('generate <type> <name>')
38
+ .description('Generate a new feature or component')
39
+ .action(async (type, name) => {
40
+ try {
41
+ if (type !== 'feature') {
42
+ logger.error('Currently only "feature" type is supported');
43
+ process.exit(1);
44
+ }
45
+
46
+ logger.build(`Generating ${type}: ${name}`);
47
+ await featureGenerator.generateFeature(name);
48
+ logger.success(`${type} ${name} generated successfully!`);
49
+ } catch (error) {
50
+ logger.error(`Failed to generate ${type}: ${error.message}`);
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ // Add help examples
56
+ program.addHelpText('after', `
57
+ Examples:
58
+ $ expo-forge init my-app
59
+ $ expo-forge generate feature booking
60
+
61
+ For more information, visit: https://github.com/moasko/expo-forge
62
+ `);
63
+
64
+ // Parse command line arguments
65
+ program.parse();
package/lib/config.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Config - Configuration centralisée du projet
3
+ */
4
+
5
+ const DEPENDENCIES = [
6
+ 'expo-router',
7
+ 'expo-constants',
8
+ 'expo-linking',
9
+ 'expo-status-bar',
10
+ 'react-native-safe-area-context',
11
+ 'react-native-screens',
12
+ '@tanstack/react-query',
13
+ 'zustand',
14
+ 'axios',
15
+ 'lucide-react-native',
16
+ 'nativewind',
17
+ 'tailwindcss',
18
+ 'typescript',
19
+ '@types/react',
20
+ '@types/react-native',
21
+ ];
22
+
23
+ const FOLDER_STRUCTURE = [
24
+ 'src/api', // Clients API & Query Providers
25
+ 'src/app', // Routes (Expo Router)
26
+ 'src/components/ui', // Composants atomiques réutilisables
27
+ 'src/features', // Logique par domaine
28
+ 'src/hooks', // Hooks globaux
29
+ 'src/store', // Zustand stores
30
+ 'src/types', // Interfaces TS globales
31
+ 'src/utils', // Fonctions helpers
32
+ 'src/constants', // Theme, API URLs
33
+ 'src/lib', // Utilitaires (API client, etc.)
34
+ ];
35
+
36
+ const FEATURE_STRUCTURE = [
37
+ 'api', // Hooks TanStack Query
38
+ 'components', // Composants UI
39
+ 'hooks', // Logique métier
40
+ 'services', // Appels API pure
41
+ 'store', // Zustand stores
42
+ 'types', // TypeScript types
43
+ 'utils', // Helpers et utilitaires
44
+ ];
45
+
46
+ module.exports = {
47
+ DEPENDENCIES,
48
+ FOLDER_STRUCTURE,
49
+ FEATURE_STRUCTURE,
50
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Executor - Gestion de l'exécution des commandes système
3
+ */
4
+
5
+ const { execSync } = require('child_process');
6
+ const logger = require('./logger');
7
+
8
+ /**
9
+ * Exécute une commande avec affichage en direct
10
+ */
11
+ const executeCommand = (command, options = {}) => {
12
+ const defaultOptions = {
13
+ stdio: 'inherit',
14
+ shell: true,
15
+ ...options,
16
+ };
17
+
18
+ try {
19
+ execSync(command, defaultOptions);
20
+ return true;
21
+ } catch (error) {
22
+ logger.error(`Erreur lors de l'exécution: ${command}`);
23
+ throw error;
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Exécute une commande silencieusement (récupère le résultat)
29
+ */
30
+ const executeCommandSilent = (command) => {
31
+ try {
32
+ const result = execSync(command, { encoding: 'utf-8' });
33
+ return result.trim();
34
+ } catch (error) {
35
+ throw error;
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Change le répertoire courant
41
+ */
42
+ const changeDirectory = (dirPath) => {
43
+ try {
44
+ process.chdir(dirPath);
45
+ return true;
46
+ } catch (error) {
47
+ logger.error(`Impossible de changer de répertoire: ${dirPath}`);
48
+ throw error;
49
+ }
50
+ };
51
+
52
+ /**
53
+ * Obtient le répertoire courant
54
+ */
55
+ const getCurrentDirectory = () => process.cwd();
56
+
57
+ module.exports = {
58
+ executeCommand,
59
+ executeCommandSilent,
60
+ changeDirectory,
61
+ getCurrentDirectory,
62
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * FeatureGenerator - Logique de génération de features modulaires
3
+ */
4
+
5
+ const path = require('path');
6
+ const { createDirectories, writeFiles } = require('./fileWriter');
7
+ const { featureTemplates } = require('./templates');
8
+ const { FEATURE_STRUCTURE } = require('./config');
9
+ const { pascalCase } = require('./helpers');
10
+ const logger = require('./logger');
11
+
12
+ const validateFeatureName = (name) => {
13
+ if (!name || typeof name !== 'string') {
14
+ logger.error('Nom de feature requis (ex: booking)');
15
+ process.exit(1);
16
+ }
17
+ return true;
18
+ };
19
+
20
+ const generateModernFeature = (featureName) => {
21
+ validateFeatureName(featureName);
22
+
23
+ const nameUpper = pascalCase(featureName);
24
+ const nameLower = featureName.toLowerCase();
25
+ const baseDir = path.join(process.cwd(), 'src', 'features', nameLower);
26
+
27
+ logger.build(`Construction de la feature: ${nameUpper}`);
28
+
29
+ try {
30
+ // 1. Créer la structure de dossiers
31
+ logger.section('ÉTAPE 1: Création de l\'arborescence');
32
+ createDirectories(baseDir, FEATURE_STRUCTURE);
33
+
34
+ // 2. Générer les fichiers
35
+ logger.section('ÉTAPE 2: Génération des fichiers');
36
+ const files = {
37
+ 'types/index.ts': featureTemplates.types(nameUpper),
38
+ [`services/${nameLower}.service.ts`]: featureTemplates.service(nameLower, nameUpper),
39
+ [`api/use${nameUpper}s.ts`]: featureTemplates.queries(nameUpper, nameLower),
40
+ [`store/use${nameUpper}Store.ts`]: featureTemplates.store(nameUpper),
41
+ [`components/${nameUpper}Card.tsx`]: featureTemplates.card(nameUpper),
42
+ [`${nameUpper}Screen.tsx`]: featureTemplates.screen(nameUpper, nameLower),
43
+ 'index.ts': featureTemplates.index(nameUpper),
44
+ };
45
+
46
+ writeFiles(baseDir, files);
47
+
48
+ // 3. Afficher le message de succès
49
+ logger.section('✅ SUCCÈS');
50
+ logger.sparkle(`Feature "${nameUpper}" créée avec succès!`);
51
+ console.log(`
52
+ 📚 Structure générée:
53
+ 📁 types/ - Définitions TypeScript
54
+ 📁 services/ - Logique API (Axios)
55
+ 📁 api/ - Hooks TanStack Query
56
+ 📁 store/ - Zustand stores
57
+ 📁 components/ - Composants UI
58
+
59
+ 💡 Prêt à utiliser:
60
+ import { ${nameUpper}Screen } from '@/features/${nameLower}';
61
+ `);
62
+ } catch (error) {
63
+ logger.error(`Génération échouée: ${error.message}`);
64
+ process.exit(1);
65
+ }
66
+ };
67
+
68
+ module.exports = { generateModernFeature, validateFeatureName };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * FileWriter - Gestion de la création de fichiers et répertoires
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const logger = require('./logger');
8
+
9
+ /**
10
+ * Crée les répertoires s'ils n'existent pas
11
+ */
12
+ const createDirectories = (baseDir, folders) => {
13
+ folders.forEach((dir) => {
14
+ const fullPath = path.join(baseDir, dir);
15
+ try {
16
+ fs.mkdirSync(fullPath, { recursive: true });
17
+ } catch (error) {
18
+ logger.error(`Impossible de créer le dossier: ${dir}`);
19
+ throw error;
20
+ }
21
+ });
22
+ };
23
+
24
+ /**
25
+ * Écrit un ensemble de fichiers
26
+ */
27
+ const writeFiles = (baseDir, filesObject) => {
28
+ Object.entries(filesObject).forEach(([filePath, content]) => {
29
+ const fullPath = path.join(baseDir, filePath);
30
+ const dir = path.dirname(fullPath);
31
+
32
+ // Créer le répertoire parent s'il n'existe pas
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+
37
+ try {
38
+ fs.writeFileSync(fullPath, content, 'utf-8');
39
+ logger.success(`${filePath}`);
40
+ } catch (error) {
41
+ logger.error(`Impossible d'écrire le fichier: ${filePath}`);
42
+ throw error;
43
+ }
44
+ });
45
+ };
46
+
47
+ /**
48
+ * Écrit un fichier unique
49
+ */
50
+ const writeFile = (filePath, content) => {
51
+ const dir = path.dirname(filePath);
52
+
53
+ if (!fs.existsSync(dir)) {
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ }
56
+
57
+ try {
58
+ fs.writeFileSync(filePath, content, 'utf-8');
59
+ return true;
60
+ } catch (error) {
61
+ logger.error(`Impossible d'écrire le fichier: ${filePath}`);
62
+ throw error;
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Vérifie si un répertoire existe
68
+ */
69
+ const directoryExists = (dirPath) => fs.existsSync(dirPath);
70
+
71
+ /**
72
+ * Lit le contenu d'un fichier
73
+ */
74
+ const readFile = (filePath) => {
75
+ try {
76
+ return fs.readFileSync(filePath, 'utf-8');
77
+ } catch (error) {
78
+ logger.error(`Impossible de lire le fichier: ${filePath}`);
79
+ throw error;
80
+ }
81
+ };
82
+
83
+ module.exports = {
84
+ createDirectories,
85
+ writeFiles,
86
+ writeFile,
87
+ directoryExists,
88
+ readFile,
89
+ };
package/lib/helpers.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Helpers - Fonctions utilitaires générales
3
+ */
4
+
5
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
6
+
7
+ const pascalCase = (str) => capitalize(str.toLowerCase());
8
+
9
+ const camelCase = (str) => {
10
+ const pascal = pascalCase(str);
11
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
12
+ };
13
+
14
+ const snakeCase = (str) => str.toLowerCase().replace(/\s/g, '_');
15
+
16
+ const kebabCase = (str) => str.toLowerCase().replace(/\s/g, '-');
17
+
18
+ const formatPath = (basePath, subPath) => {
19
+ const path = require('path');
20
+ return path.join(basePath, subPath);
21
+ };
22
+
23
+ module.exports = {
24
+ capitalize,
25
+ pascalCase,
26
+ camelCase,
27
+ snakeCase,
28
+ kebabCase,
29
+ formatPath,
30
+ };
package/lib/index.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Expo Forge - Main Entry Point
5
+ * Forge modern Expo apps with bulletproof architecture
6
+ */
7
+
8
+ const { initializeProject } = require('./initExpo');
9
+ const { generateModernFeature } = require('./featureGenerator');
10
+ const logger = require('./logger');
11
+
12
+ const args = process.argv.slice(2);
13
+ const command = args[0];
14
+ const subCommand = args[1];
15
+ const name = args[2];
16
+
17
+ console.log(`
18
+ 🔥 EXPO FORGE 🔥
19
+ Forge modern Expo apps with bulletproof architecture
20
+ `);
21
+
22
+ if (!command) {
23
+ showHelp();
24
+ process.exit(0);
25
+ }
26
+
27
+ switch (command) {
28
+ case 'init':
29
+ const projectName = name || 'my-expo-app';
30
+ logger.info(`Initializing new Expo project: ${projectName}`);
31
+ initializeProject(projectName);
32
+ break;
33
+
34
+ case 'generate':
35
+ if (subCommand === 'feature' && name) {
36
+ logger.info(`Generating feature: ${name}`);
37
+ generateModernFeature(name);
38
+ } else {
39
+ logger.error('Usage: expo-forge generate feature <name>');
40
+ process.exit(1);
41
+ }
42
+ break;
43
+
44
+ case '--help':
45
+ case '-h':
46
+ showHelp();
47
+ break;
48
+
49
+ case '--version':
50
+ case '-v':
51
+ showVersion();
52
+ break;
53
+
54
+ default:
55
+ logger.error(`Unknown command: ${command}`);
56
+ showHelp();
57
+ process.exit(1);
58
+ }
59
+
60
+ function showHelp() {
61
+ console.log(`
62
+ Usage:
63
+ expo-forge init [project-name] Initialize a new Expo project
64
+ expo-forge generate feature <name> Generate a new feature
65
+
66
+ Examples:
67
+ expo-forge init my-awesome-app
68
+ expo-forge generate feature booking
69
+ expo-forge generate feature payment
70
+
71
+ Options:
72
+ -h, --help Show this help message
73
+ -v, --version Show version information
74
+
75
+ For more information, visit: https://github.com/moasko/expo-forge
76
+ `);
77
+ }
78
+
79
+ function showVersion() {
80
+ const package = require('../package.json');
81
+ console.log(`${package.version}`);
82
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * InitExpo - Logique d'initialisation du projet Expo
3
+ */
4
+
5
+ const path = require('path');
6
+ const { executeCommand, changeDirectory } = require('./executor');
7
+ const { createDirectories, writeFiles } = require('./fileWriter');
8
+ const { initTemplates } = require('./templates');
9
+ const { DEPENDENCIES, FOLDER_STRUCTURE } = require('./config');
10
+ const logger = require('./logger');
11
+
12
+ const initializeProject = (projectName = 'my-modern-app') => {
13
+ const projectPath = path.join(process.cwd(), projectName);
14
+
15
+ logger.rocket(`Initialisation d'un projet Expo Ultra-Moderne: ${projectName}`);
16
+
17
+ try {
18
+ // 1. Créer le projet avec Expo
19
+ logger.section('ÉTAPE 1: Création du projet');
20
+ executeCommand(`npx create-expo-app@latest ${projectName} --template blank-typescript`);
21
+
22
+ // 2. Changer le répertoire
23
+ changeDirectory(projectPath);
24
+
25
+ // 3. Installer les dépendances
26
+ logger.section('ÉTAPE 2: Installation des dépendances');
27
+ logger.package('Installation des frameworks modernes...');
28
+ executeCommand(`npx expo install ${DEPENDENCIES.join(' ')}`);
29
+
30
+ // 4. Créer la structure de dossiers
31
+ logger.section('ÉTAPE 3: Création de l\'arborescence');
32
+ createDirectories(projectPath, FOLDER_STRUCTURE);
33
+ logger.success('Structure de dossiers créée');
34
+
35
+ // 5. Générer les fichiers de configuration
36
+ logger.section('ÉTAPE 4: Génération des fichiers');
37
+ const files = {
38
+ 'src/api/query-client.ts': initTemplates.queryClient(),
39
+ 'src/store/useAuthStore.ts': initTemplates.authStore(),
40
+ 'src/app/_layout.tsx': initTemplates.rootLayout(),
41
+ 'src/app/index.tsx': initTemplates.homeScreen(),
42
+ 'tailwind.config.js': initTemplates.tailwindConfig(),
43
+ 'tsconfig.json': initTemplates.tsconfig(),
44
+ '.env.example': initTemplates.envExample(),
45
+ };
46
+
47
+ writeFiles(projectPath, files);
48
+
49
+ // 6. Afficher le message de succès
50
+ logger.section('✅ SUCCÈS');
51
+ logger.sparkle('Projet initialisé avec succès!');
52
+ console.log(`
53
+ Pour démarrer:
54
+ 1. cd ${projectName}
55
+ 2. npx expo start
56
+
57
+ Structure:
58
+ 📁 src/features - Pour tes modules métiers
59
+ 📁 src/app - Pour tes pages (navigation auto)
60
+ 🚀 TanStack Query & Zustand configurés
61
+ 🎨 NativeWind & Tailwind CSS prêts
62
+ `);
63
+ } catch (error) {
64
+ logger.error(`Initialisation échouée: ${error.message}`);
65
+ process.exit(1);
66
+ }
67
+ };
68
+
69
+ module.exports = { initializeProject };
package/lib/logger.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Logger - Gestion des messages formatés
3
+ */
4
+
5
+ const colors = {
6
+ reset: '\x1b[0m',
7
+ green: '\x1b[32m',
8
+ red: '\x1b[31m',
9
+ yellow: '\x1b[33m',
10
+ blue: '\x1b[34m',
11
+ cyan: '\x1b[36m',
12
+ };
13
+
14
+ const logger = {
15
+ success: (msg) => console.log(`${colors.green}✅ ${msg}${colors.reset}`),
16
+ error: (msg) => console.error(`${colors.red}❌ ${msg}${colors.reset}`),
17
+ info: (msg) => console.log(`${colors.blue}ℹ️ ${msg}${colors.reset}`),
18
+ warning: (msg) => console.warn(`${colors.yellow}⚠️ ${msg}${colors.reset}`),
19
+ build: (msg) => console.log(`${colors.cyan}🏗️ ${msg}${colors.reset}`),
20
+ rocket: (msg) => console.log(`${colors.cyan}🚀 ${msg}${colors.reset}`),
21
+ package: (msg) => console.log(`${colors.blue}📦 ${msg}${colors.reset}`),
22
+ sparkle: (msg) => console.log(`${colors.green}✨ ${msg}${colors.reset}`),
23
+ section: (msg) => console.log(`\n${colors.cyan}──── ${msg} ────${colors.reset}\n`),
24
+ };
25
+
26
+ module.exports = logger;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Templates - Tous les fichiers templates par domaine
3
+ */
4
+
5
+ /**
6
+ * Templates pour l'initialisation d'un projet Expo
7
+ */
8
+ const initTemplates = {
9
+ queryClient: () => `import { QueryClient } from '@tanstack/react-query';
10
+
11
+ export const queryClient = new QueryClient({
12
+ defaultOptions: {
13
+ queries: {
14
+ retry: 2,
15
+ staleTime: 1000 * 60 * 5,
16
+ },
17
+ },
18
+ });`,
19
+
20
+ authStore: () => `import { create } from 'zustand';
21
+
22
+ interface AuthState {
23
+ user: any | null;
24
+ setUser: (user: any) => void;
25
+ logout: () => void;
26
+ }
27
+
28
+ export const useAuthStore = create<AuthState>((set) => ({
29
+ user: null,
30
+ setUser: (user) => set({ user }),
31
+ logout: () => set({ user: null }),
32
+ }));`,
33
+
34
+ rootLayout: () => `import { Stack } from 'expo-router';
35
+ import { QueryClientProvider } from '@tanstack/react-query';
36
+ import { queryClient } from '../api/query-client';
37
+
38
+ export default function RootLayout() {
39
+ return (
40
+ <QueryClientProvider client={queryClient}>
41
+ <Stack screenOptions={{ headerShown: false }}>
42
+ <Stack.Screen name="index" />
43
+ </Stack>
44
+ </QueryClientProvider>
45
+ );
46
+ }`,
47
+
48
+ homeScreen: () => `import { View, Text } from 'react-native';
49
+
50
+ export default function HomeScreen() {
51
+ return (
52
+ <View className="flex-1 items-center justify-center bg-white">
53
+ <Text className="text-2xl font-bold text-blue-600">Expo 2026 Stack</Text>
54
+ <Text className="text-gray-500 mt-2">TanStack Query • Zustand • NativeWind</Text>
55
+ </View>
56
+ );
57
+ }`,
58
+
59
+ tailwindConfig: () => `module.exports = {
60
+ content: [
61
+ './App.{js,jsx,ts,tsx}',
62
+ './src/**/*.{js,jsx,ts,tsx}',
63
+ ],
64
+ theme: {
65
+ extend: {},
66
+ },
67
+ plugins: [],
68
+ };`,
69
+
70
+ envExample: () => `EXPO_PUBLIC_API_URL=https://api.example.com
71
+ EXPO_PUBLIC_API_TIMEOUT=30000`,
72
+
73
+ tsconfig: () => `{
74
+ "extends": "expo/tsconfig.base",
75
+ "compilerOptions": {
76
+ "strict": true,
77
+ "baseUrl": ".",
78
+ "paths": {
79
+ "@/*": ["./src/*"]
80
+ }
81
+ }
82
+ }`,
83
+
84
+ packageJson: () => `{
85
+ "name": "my-modern-app",
86
+ "version": "1.0.0",
87
+ "main": "index.js",
88
+ "scripts": {
89
+ "start": "expo start",
90
+ "android": "expo start --android",
91
+ "ios": "expo start --ios",
92
+ "web": "expo start --web"
93
+ }
94
+ }`,
95
+ };
96
+
97
+ /**
98
+ * Templates pour la génération de features
99
+ * Utilise des fonctions pour interpoler les variables
100
+ */
101
+ const featureTemplates = {
102
+ types: (nameUpper) => `export type ${nameUpper} = {
103
+ id: string;
104
+ title: string;
105
+ status: 'pending' | 'completed';
106
+ createdAt: string;
107
+ };
108
+
109
+ export type Create${nameUpper}DTO = Pick<${nameUpper}, 'title'>;
110
+
111
+ export type Update${nameUpper}DTO = Partial<${nameUpper}>;`,
112
+
113
+ service: (nameLower, nameUpper) => `import { apiClient } from '@/lib/api-client';
114
+ import { ${nameUpper}, Create${nameUpper}DTO, Update${nameUpper}DTO } from '../types';
115
+
116
+ const BASE_URL = '/${nameLower}';
117
+
118
+ export const ${nameLower}Service = {
119
+ getAll: async (): Promise<${nameUpper}[]> => {
120
+ const { data } = await apiClient.get(BASE_URL);
121
+ return data;
122
+ },
123
+
124
+ getById: async (id: string): Promise<${nameUpper}> => {
125
+ const { data } = await apiClient.get(\`\${BASE_URL}/\${id}\`);
126
+ return data;
127
+ },
128
+
129
+ create: async (payload: Create${nameUpper}DTO): Promise<${nameUpper}> => {
130
+ const { data } = await apiClient.post(BASE_URL, payload);
131
+ return data;
132
+ },
133
+
134
+ update: async (id: string, payload: Update${nameUpper}DTO): Promise<${nameUpper}> => {
135
+ const { data } = await apiClient.patch(\`\${BASE_URL}/\${id}\`, payload);
136
+ return data;
137
+ },
138
+
139
+ delete: async (id: string): Promise<void> => {
140
+ await apiClient.delete(\`\${BASE_URL}/\${id}\`);
141
+ },
142
+ };`,
143
+
144
+ queries: (nameUpper, nameLower) => `import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
145
+ import { ${nameLower}Service } from '../services/${nameLower}.service';
146
+ import { Create${nameUpper}DTO, Update${nameUpper}DTO } from '../types';
147
+
148
+ const QUERY_KEY = ['${nameLower}'];
149
+
150
+ export const use${nameUpper}s = () => {
151
+ return useQuery({
152
+ queryKey: QUERY_KEY,
153
+ queryFn: ${nameLower}Service.getAll,
154
+ });
155
+ };
156
+
157
+ export const use${nameUpper} = (id: string) => {
158
+ return useQuery({
159
+ queryKey: [...QUERY_KEY, id],
160
+ queryFn: () => ${nameLower}Service.getById(id),
161
+ });
162
+ };
163
+
164
+ export const useCreate${nameUpper} = () => {
165
+ const queryClient = useQueryClient();
166
+ return useMutation({
167
+ mutationFn: ${nameLower}Service.create,
168
+ onSuccess: () => {
169
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
170
+ },
171
+ });
172
+ };
173
+
174
+ export const useUpdate${nameUpper} = () => {
175
+ const queryClient = useQueryClient();
176
+ return useMutation({
177
+ mutationFn: ({ id, data }: { id: string; data: Update${nameUpper}DTO }) =>
178
+ ${nameLower}Service.update(id, data),
179
+ onSuccess: () => {
180
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
181
+ },
182
+ });
183
+ };
184
+
185
+ export const useDelete${nameUpper} = () => {
186
+ const queryClient = useQueryClient();
187
+ return useMutation({
188
+ mutationFn: ${nameLower}Service.delete,
189
+ onSuccess: () => {
190
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
191
+ },
192
+ });
193
+ };`,
194
+
195
+ store: (nameUpper) => `import { create } from 'zustand';
196
+
197
+ interface ${nameUpper}State {
198
+ filter: string;
199
+ sortBy: 'date' | 'title';
200
+ setFilter: (filter: string) => void;
201
+ setSortBy: (sortBy: 'date' | 'title') => void;
202
+ }
203
+
204
+ export const use${nameUpper}Store = create<${nameUpper}State>((set) => ({
205
+ filter: '',
206
+ sortBy: 'date',
207
+ setFilter: (filter) => set({ filter }),
208
+ setSortBy: (sortBy) => set({ sortBy }),
209
+ }));`,
210
+
211
+ card: (nameUpper) => `import React from 'react';
212
+ import { View, Text, Pressable } from 'react-native';
213
+ import { ${nameUpper} } from '../types';
214
+
215
+ interface Props {
216
+ item: ${nameUpper};
217
+ onPress: (id: string) => void;
218
+ }
219
+
220
+ export const ${nameUpper}Card = ({ item, onPress }: Props) => (
221
+ <Pressable
222
+ onPress={() => onPress(item.id)}
223
+ className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-50"
224
+ >
225
+ <Text className="text-lg font-semibold text-gray-800">{item.title}</Text>
226
+ <View className="flex-row justify-between items-center mt-2">
227
+ <Text className="text-sm text-gray-500">{new Date(item.createdAt).toLocaleDateString()}</Text>
228
+ <Text className={\`text-xs font-medium \${item.status === 'completed' ? 'text-green-600' : 'text-yellow-600'}\`}>
229
+ {item.status}
230
+ </Text>
231
+ </View>
232
+ </Pressable>
233
+ );`,
234
+
235
+ screen: (nameUpper, nameLower) => `import React from 'react';
236
+ import { View, FlatList, Text, ActivityIndicator } from 'react-native';
237
+ import { use${nameUpper}s } from './api/use${nameUpper}s';
238
+ import { ${nameUpper}Card } from './components/${nameUpper}Card';
239
+ import { use${nameUpper}Store } from './store/use${nameUpper}Store';
240
+
241
+ export const ${nameUpper}Screen = () => {
242
+ const { data: items, isLoading, error } = use${nameUpper}s();
243
+ const { filter } = use${nameUpper}Store();
244
+
245
+ if (isLoading) {
246
+ return (
247
+ <View className="flex-1 items-center justify-center">
248
+ <ActivityIndicator size="large" />
249
+ </View>
250
+ );
251
+ }
252
+
253
+ if (error) {
254
+ return (
255
+ <View className="flex-1 items-center justify-center">
256
+ <Text className="text-red-500 font-semibold">Erreur lors du chargement</Text>
257
+ </View>
258
+ );
259
+ }
260
+
261
+ const filteredItems = items?.filter((i) =>
262
+ i.title.toLowerCase().includes(filter.toLowerCase())
263
+ ) || [];
264
+
265
+ return (
266
+ <View className="flex-1 bg-white">
267
+ <FlatList
268
+ data={filteredItems}
269
+ renderItem={({ item }) => (
270
+ <${nameUpper}Card
271
+ item={item}
272
+ onPress={(id) => console.log('Navigate to:', id)}
273
+ />
274
+ )}
275
+ keyExtractor={(item) => item.id}
276
+ contentContainerStyle={{ padding: 16 }}
277
+ ListEmptyComponent={
278
+ <Text className="text-center text-gray-500">Aucun ${nameLower} trouvé</Text>
279
+ }
280
+ />
281
+ </View>
282
+ );
283
+ };`,
284
+
285
+ index: (nameUpper) => `export * from './store/use${nameUpper}Store';
286
+ export { use${nameUpper}s, useCreate${nameUpper}, useUpdate${nameUpper}, useDelete${nameUpper} } from './api/use${nameUpper}s';
287
+ export { ${nameUpper}Screen } from './${nameUpper}Screen';`,
288
+ };
289
+
290
+ module.exports = {
291
+ initTemplates,
292
+ featureTemplates,
293
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "expo-forge",
3
+ "version": "2.0.0",
4
+ "description": "Forge modern Expo apps with bulletproof architecture - TanStack Query, Zustand, NativeWind",
5
+ "author": "moasko <moasko.dev@gmail.com>",
6
+ "license": "MIT",
7
+ "main": "lib/index.js",
8
+ "keywords": [
9
+ "expo",
10
+ "react-native",
11
+ "generator",
12
+ "scaffold",
13
+ "cli",
14
+ "typescript",
15
+ "tanstack-query",
16
+ "zustand",
17
+ "nativewind",
18
+ "bulletproof",
19
+ "architecture",
20
+ "mobile",
21
+ "cross-platform"
22
+ ],
23
+ "scripts": {
24
+ "init": "node init-expo.js",
25
+ "init:custom": "node init-expo.js $npm_config_name",
26
+ "gen": "node generate-feature.js generate feature",
27
+ "gen:feature": "node generate-feature.js generate feature $npm_config_name",
28
+ "help": "echo 'Usage: npm run init [projectName] or npm run gen:feature -- --name=featureName'",
29
+ "test": "node test.js",
30
+ "prepublishOnly": "npm run test",
31
+ "publish:beta": "npm publish --tag beta",
32
+ "publish:latest": "npm publish",
33
+ "pack": "npm pack",
34
+ "version:patch": "npm version patch",
35
+ "version:minor": "npm version minor",
36
+ "version:major": "npm version major"
37
+ },
38
+ "dependencies": {
39
+ "commander": "^11.1.0"
40
+ },
41
+ "bin": {
42
+ "expo-forge": "bin/expo-forge.js"
43
+ },
44
+ "engines": {
45
+ "node": ">=16.0.0",
46
+ "npm": ">=7.0.0"
47
+ },
48
+ "files": [
49
+ "bin/",
50
+ "lib/",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "preferGlobal": true,
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/moasko/expo-forge.git"
58
+ },
59
+ "bugs": {
60
+ "url": "https://github.com/moasko/expo-forge/issues"
61
+ },
62
+ "homepage": "https://github.com/moasko/expo-forge#readme"
63
+ }