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 +21 -0
- package/README.md +64 -0
- package/bin/expo-forge.js +65 -0
- package/lib/config.js +50 -0
- package/lib/executor.js +62 -0
- package/lib/featureGenerator.js +68 -0
- package/lib/fileWriter.js +89 -0
- package/lib/helpers.js +30 -0
- package/lib/index.js +82 -0
- package/lib/initExpo.js +69 -0
- package/lib/logger.js +26 -0
- package/lib/templates.js +293 -0
- package/package.json +63 -0
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
|
+
};
|
package/lib/executor.js
ADDED
|
@@ -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
|
+
}
|
package/lib/initExpo.js
ADDED
|
@@ -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;
|
package/lib/templates.js
ADDED
|
@@ -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
|
+
}
|