react-native-architecture-generator 1.1.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.
@@ -0,0 +1,400 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { Architecture, StateManagement, Routing } from '../models/config.js';
4
+ import { StringUtils } from './string-utils.js';
5
+ /**
6
+ * Generates feature modules in 4 different architecture patterns:
7
+ * 1. Clean Architecture (Domain → Data → Presentation)
8
+ * 2. Feature-Based (Lightweight, flat structure)
9
+ * 3. Atomic Design + Feature (Atoms → Molecules → Organisms)
10
+ * 4. MVVM with Hooks (Model → ViewModel → View)
11
+ */
12
+ export class FeatureHelper {
13
+ static async generateFeature(name, config) {
14
+ switch (config.architecture) {
15
+ case Architecture.cleanArchitecture:
16
+ await this.generateCleanArchFeature(name, config);
17
+ break;
18
+ case Architecture.featureBased:
19
+ await this.generateFeatureBasedFeature(name, config);
20
+ break;
21
+ case Architecture.atomicDesign:
22
+ await this.generateAtomicDesignFeature(name, config);
23
+ break;
24
+ case Architecture.mvvm:
25
+ await this.generateMvvmFeature(name, config);
26
+ break;
27
+ }
28
+ }
29
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30
+ // 1. CLEAN ARCHITECTURE
31
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32
+ static async generateCleanArchFeature(name, config) {
33
+ const root = process.cwd();
34
+ const snakeName = StringUtils.toSnakeCase(name);
35
+ const pascalName = StringUtils.toPascalCase(name);
36
+ const featurePath = path.join(root, 'src', 'features', snakeName);
37
+ // Directories
38
+ const layers = [
39
+ 'data/datasources',
40
+ 'data/models',
41
+ 'data/repositories',
42
+ 'domain/entities',
43
+ 'domain/repositories',
44
+ 'domain/usecases',
45
+ 'presentation/screens',
46
+ 'presentation/components',
47
+ 'presentation/hooks',
48
+ `presentation/${config.stateManagement}`,
49
+ ];
50
+ for (const layer of layers) {
51
+ await fs.ensureDir(path.join(featurePath, layer));
52
+ }
53
+ // Entity
54
+ await this.writeFile(path.join(featurePath, 'domain', 'entities', `${snakeName}Entity.ts`), `export interface ${pascalName}Entity {\n id: number;\n}\n`);
55
+ // Repository interface
56
+ await this.writeFile(path.join(featurePath, 'domain', 'repositories', `${pascalName}Repository.ts`), `import type { ${pascalName}Entity } from '../entities/${snakeName}Entity';\n\nexport interface I${pascalName}Repository {\n get${pascalName}Data(): Promise<${pascalName}Entity>;\n}\n`);
57
+ // Use Case
58
+ await this.writeFile(path.join(featurePath, 'domain', 'usecases', `Get${pascalName}UseCase.ts`), `import type { I${pascalName}Repository } from '../repositories/${pascalName}Repository';
59
+ import type { ${pascalName}Entity } from '../entities/${snakeName}Entity';
60
+
61
+ export class Get${pascalName}UseCase {
62
+ constructor(private repository: I${pascalName}Repository) {}
63
+
64
+ async execute(): Promise<${pascalName}Entity> {
65
+ return this.repository.get${pascalName}Data();
66
+ }
67
+ }
68
+ `);
69
+ // Model
70
+ await this.writeFile(path.join(featurePath, 'data', 'models', `${pascalName}Model.ts`), `import type { ${pascalName}Entity } from '../../domain/entities/${snakeName}Entity';
71
+
72
+ export class ${pascalName}Model implements ${pascalName}Entity {
73
+ id: number;
74
+
75
+ constructor(data: { id: number }) {
76
+ this.id = data.id;
77
+ }
78
+
79
+ static fromJson(json: Record<string, unknown>): ${pascalName}Model {
80
+ return new ${pascalName}Model({ id: json['id'] as number });
81
+ }
82
+
83
+ toJson(): Record<string, unknown> {
84
+ return { id: this.id };
85
+ }
86
+ }
87
+ `);
88
+ // Data Source
89
+ await this.writeFile(path.join(featurePath, 'data', 'datasources', `${pascalName}RemoteDataSource.ts`), `import apiClient from '../../../core/api/apiClient';
90
+ import { ${pascalName}Model } from '../models/${pascalName}Model';
91
+
92
+ export interface I${pascalName}RemoteDataSource {
93
+ get${pascalName}FromApi(): Promise<${pascalName}Model>;
94
+ }
95
+
96
+ export class ${pascalName}RemoteDataSourceImpl implements I${pascalName}RemoteDataSource {
97
+ async get${pascalName}FromApi(): Promise<${pascalName}Model> {
98
+ const response = await apiClient.get('/${snakeName}');
99
+ return ${pascalName}Model.fromJson(response.data);
100
+ }
101
+ }
102
+ `);
103
+ // Repository Impl
104
+ await this.writeFile(path.join(featurePath, 'data', 'repositories', `${pascalName}RepositoryImpl.ts`), `import type { I${pascalName}Repository } from '../../domain/repositories/${pascalName}Repository';
105
+ import type { ${pascalName}Entity } from '../../domain/entities/${snakeName}Entity';
106
+ import type { I${pascalName}RemoteDataSource } from '../datasources/${pascalName}RemoteDataSource';
107
+ import { ServerFailure } from '../../../core/errors/failures';
108
+
109
+ export class ${pascalName}RepositoryImpl implements I${pascalName}Repository {
110
+ constructor(private remoteDataSource: I${pascalName}RemoteDataSource) {}
111
+
112
+ async get${pascalName}Data(): Promise<${pascalName}Entity> {
113
+ try {
114
+ return await this.remoteDataSource.get${pascalName}FromApi();
115
+ } catch (error) {
116
+ throw new ServerFailure(String(error));
117
+ }
118
+ }
119
+ }
120
+ `);
121
+ // State Management
122
+ await this.generateStateManagement(featurePath, name, config, 'presentation');
123
+ // Screens
124
+ if (name === 'auth') {
125
+ await this.generateAuthScreens(featurePath, config, 'presentation/screens');
126
+ }
127
+ else {
128
+ await this.generateDefaultScreen(featurePath, name, 'presentation/screens');
129
+ }
130
+ // Navigation Registration
131
+ await this.registerInNavigation(name, config);
132
+ // Tests
133
+ if (config.tests) {
134
+ await this.generateCleanArchTest(name, config);
135
+ }
136
+ }
137
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
138
+ // 2. FEATURE-BASED (LIGHTWEIGHT)
139
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
140
+ static async generateFeatureBasedFeature(name, config) {
141
+ const root = process.cwd();
142
+ const snakeName = StringUtils.toSnakeCase(name);
143
+ const pascalName = StringUtils.toPascalCase(name);
144
+ const camelName = StringUtils.toCamelCase(name);
145
+ const featurePath = path.join(root, 'src', 'features', snakeName);
146
+ const dirs = [
147
+ 'components',
148
+ 'hooks',
149
+ 'screens',
150
+ 'services',
151
+ 'types',
152
+ 'utils',
153
+ ];
154
+ for (const dir of dirs) {
155
+ await fs.ensureDir(path.join(featurePath, dir));
156
+ }
157
+ // Types
158
+ await this.writeFile(path.join(featurePath, 'types', `${camelName}.types.ts`), `export interface ${pascalName} {\n id: number;\n}\n`);
159
+ // Service
160
+ await this.writeFile(path.join(featurePath, 'services', `${camelName}.service.ts`), `import apiClient from '../../core/api/apiClient';
161
+ import type { ${pascalName} } from '../types/${camelName}.types';
162
+
163
+ export const ${camelName}Service = {
164
+ async getAll(): Promise<${pascalName}[]> {
165
+ const response = await apiClient.get('/${snakeName}');
166
+ return response.data;
167
+ },
168
+ };
169
+ `);
170
+ // Hook
171
+ await this.writeFile(path.join(featurePath, 'hooks', `use${pascalName}.ts`), `import { useState, useCallback } from 'react';
172
+ import type { ${pascalName} } from '../types/${camelName}.types';
173
+ import { ${camelName}Service } from '../services/${camelName}.service';
174
+
175
+ export const use${pascalName} = () => {
176
+ const [data, setData] = useState<${pascalName} | null>(null);
177
+ const [isLoading, setIsLoading] = useState(false);
178
+
179
+ const fetch = useCallback(async () => {
180
+ setIsLoading(true);
181
+ try {
182
+ const result = await ${camelName}Service.getAll();
183
+ setData(result[0]);
184
+ } finally {
185
+ setIsLoading(false);
186
+ }
187
+ }, []);
188
+
189
+ return { data, isLoading, fetch };
190
+ };
191
+ `);
192
+ // Screens
193
+ if (name === 'auth') {
194
+ await this.generateAuthScreens(featurePath, config, 'screens');
195
+ }
196
+ else {
197
+ await this.generateDefaultScreen(featurePath, name, 'screens');
198
+ }
199
+ await this.registerInNavigation(name, config);
200
+ if (config.tests) {
201
+ await this.generateFeatureBasedTest(name, config);
202
+ }
203
+ }
204
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
205
+ // 3. ATOMIC DESIGN + FEATURE
206
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
207
+ static async generateAtomicDesignFeature(name, config) {
208
+ const root = process.cwd();
209
+ const snakeName = StringUtils.toSnakeCase(name);
210
+ const pascalName = StringUtils.toPascalCase(name);
211
+ const camelName = StringUtils.toCamelCase(name);
212
+ const featurePath = path.join(root, 'src', 'features', snakeName);
213
+ const dirs = ['atoms', 'molecules', 'organisms', 'templates', 'screens', 'hooks', 'services', 'types'];
214
+ for (const dir of dirs) {
215
+ await fs.ensureDir(path.join(featurePath, dir));
216
+ }
217
+ // Atom: Button (Theme-aware)
218
+ await this.writeFile(path.join(featurePath, 'atoms', `${pascalName}Button.tsx`), `import React from 'react';
219
+ import { TouchableOpacity, Text, StyleSheet } from 'react-native';
220
+ import { useTheme } from '../../../core/theme/ThemeContext';
221
+
222
+ interface Props { title: string; onPress: () => void; }
223
+
224
+ export const ${pascalName}Button: React.FC<Props> = ({ title, onPress }) => {
225
+ const { colors } = useTheme();
226
+ return (
227
+ <TouchableOpacity style={[styles.btn, { backgroundColor: colors.primary }]} onPress={onPress}>
228
+ <Text style={styles.text}>{title}</Text>
229
+ </TouchableOpacity>
230
+ );
231
+ };
232
+
233
+ const styles = StyleSheet.create({
234
+ btn: { padding: 16, borderRadius: 12, alignItems: 'center' },
235
+ text: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
236
+ });
237
+ `);
238
+ // Molecule: FormField
239
+ await this.writeFile(path.join(featurePath, 'molecules', `${pascalName}FormField.tsx`), `import React from 'react';
240
+ import { View, Text, TextInput, StyleSheet } from 'react-native';
241
+ import { useTheme } from '../../../core/theme/ThemeContext';
242
+
243
+ interface Props { label: string; value: string; onChangeText: (t: string) => void; }
244
+
245
+ export const ${pascalName}FormField: React.FC<Props> = ({ label, value, onChangeText }) => {
246
+ const { colors } = useTheme();
247
+ return (
248
+ <View style={styles.container}>
249
+ <Text style={[styles.label, { color: colors.textPrimary }]}>{label}</Text>
250
+ <TextInput style={[styles.input, { borderColor: colors.divider }]} value={value} onChangeText={onChangeText} />
251
+ </View>
252
+ );
253
+ };
254
+
255
+ const styles = StyleSheet.create({
256
+ container: { marginBottom: 16 },
257
+ label: { fontSize: 14, marginBottom: 8 },
258
+ input: { borderWidth: 1, borderRadius: 8, padding: 12 },
259
+ });
260
+ `);
261
+ // Screens
262
+ if (name === 'auth') {
263
+ await this.generateAuthScreens(featurePath, config, 'screens');
264
+ }
265
+ else {
266
+ await this.generateDefaultScreen(featurePath, name, 'screens');
267
+ }
268
+ await this.registerInNavigation(name, config);
269
+ if (config.tests)
270
+ await this.generateAtomicDesignTest(name, config);
271
+ }
272
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
273
+ // 4. MVVM WITH HOOKS
274
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275
+ static async generateMvvmFeature(name, config) {
276
+ const root = process.cwd();
277
+ const snakeName = StringUtils.toSnakeCase(name);
278
+ const pascalName = StringUtils.toPascalCase(name);
279
+ const camelName = StringUtils.toCamelCase(name);
280
+ const featurePath = path.join(root, 'src', 'features', snakeName);
281
+ const dirs = ['models', 'viewmodels', 'views/screens', 'views/components', 'services'];
282
+ for (const dir of dirs) {
283
+ await fs.ensureDir(path.join(featurePath, dir));
284
+ }
285
+ // ViewModel
286
+ await this.writeFile(path.join(featurePath, 'viewmodels', `use${pascalName}ViewModel.ts`), `import { useState, useCallback } from 'react';
287
+
288
+ export const use${pascalName}ViewModel = () => {
289
+ const [isLoading, setIsLoading] = useState(false);
290
+ const fetchData = useCallback(() => { /* ... */ }, []);
291
+ return { isLoading, fetchData };
292
+ };
293
+ `);
294
+ // Screen
295
+ if (name === 'auth') {
296
+ await this.generateAuthScreens(featurePath, config, 'views/screens');
297
+ }
298
+ else {
299
+ await this.generateDefaultScreen(featurePath, name, 'views/screens');
300
+ }
301
+ await this.registerInNavigation(name, config, 'views/screens');
302
+ if (config.tests)
303
+ await this.generateMvvmTest(name, config);
304
+ }
305
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
306
+ // SHARED: State Management
307
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
308
+ static async generateStateManagement(featurePath, name, config, baseFolder) {
309
+ const pascalName = StringUtils.toPascalCase(name);
310
+ const camelName = StringUtils.toCamelCase(name);
311
+ const smPath = path.join(featurePath, baseFolder, config.stateManagement);
312
+ switch (config.stateManagement) {
313
+ case StateManagement.redux:
314
+ await this.writeFile(path.join(smPath, `${camelName}Slice.ts`), `import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
315
+
316
+ export const ${camelName}Slice = createSlice({
317
+ name: '${camelName}',
318
+ initialState: { data: null, isLoading: false },
319
+ reducers: {
320
+ setData: (state, action: PayloadAction<any>) => { state.data = action.payload; },
321
+ },
322
+ });
323
+
324
+ export const { setData } = ${camelName}Slice.actions;
325
+ export default ${camelName}Slice.reducer;
326
+ `);
327
+ break;
328
+ case StateManagement.zustand:
329
+ await this.writeFile(path.join(smPath, `use${pascalName}Store.ts`), `import { create } from 'zustand';
330
+
331
+ export const use${pascalName}Store = create<any>((set) => ({
332
+ data: null,
333
+ setData: (data: any) => set({ data }),
334
+ }));
335
+ `);
336
+ break;
337
+ }
338
+ }
339
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
340
+ // SHARED: Auth Screens (Theme Integrated)
341
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
342
+ static async generateAuthScreens(featurePath, config, screenFolder) {
343
+ const loginContent = `import React, { useState } from 'react';
344
+ import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
345
+ import { useTheme } from '../../../core/theme/ThemeContext';
346
+
347
+ export const LoginScreen: React.FC = () => {
348
+ const { colors } = useTheme();
349
+ return (
350
+ <KeyboardAvoidingView style={[styles.container, { backgroundColor: colors.background }]} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
351
+ <Text style={[styles.title, { color: colors.textPrimary }]}>Welcome</Text>
352
+ <TextInput style={[styles.input, { borderColor: colors.divider, color: colors.textPrimary }]} placeholder="Email" placeholderTextColor={colors.textSecondary} />
353
+ <TouchableOpacity style={[styles.btn, { backgroundColor: colors.primary }]}>
354
+ <Text style={styles.btnText}>Login</Text>
355
+ </TouchableOpacity>
356
+ </KeyboardAvoidingView>
357
+ );
358
+ };
359
+
360
+ const styles = StyleSheet.create({
361
+ container: { flex: 1, padding: 24, justifyContent: 'center' },
362
+ title: { fontSize: 32, fontWeight: 'bold', marginBottom: 24 },
363
+ input: { borderWidth: 1, borderRadius: 12, padding: 16, marginBottom: 16 },
364
+ btn: { padding: 18, borderRadius: 12, alignItems: 'center' },
365
+ btnText: { color: '#fff', fontSize: 18, fontWeight: 'bold' },
366
+ });
367
+ `;
368
+ await this.writeFile(path.join(featurePath, screenFolder, 'LoginScreen.tsx'), loginContent);
369
+ await this.writeFile(path.join(featurePath, screenFolder, 'RegisterScreen.tsx'), loginContent.replace('Login', 'Register'));
370
+ }
371
+ static async generateDefaultScreen(featurePath, name, screenFolder) {
372
+ const pascalName = StringUtils.toPascalCase(name);
373
+ await this.writeFile(path.join(featurePath, screenFolder, `${pascalName}Screen.tsx`), `import React from 'react';\nimport { View, Text } from 'react-native';\n\nexport const ${pascalName}Screen = () => <View><Text>${pascalName} Screen</Text></View>;\n`);
374
+ }
375
+ static async registerInNavigation(name, config, screenSubPath) {
376
+ if (config.routing === Routing.expoRouter)
377
+ return;
378
+ const navFile = path.join(process.cwd(), 'src', 'navigation', 'AppNavigator.tsx');
379
+ if (!(await fs.pathExists(navFile)))
380
+ return;
381
+ const pascalName = StringUtils.toPascalCase(name);
382
+ const snakeName = StringUtils.toSnakeCase(name);
383
+ const screenDir = screenSubPath || (config.architecture === Architecture.cleanArchitecture ? 'presentation/screens' : config.architecture === Architecture.mvvm ? 'views/screens' : 'screens');
384
+ let contents = await fs.readFile(navFile, 'utf-8');
385
+ const importStr = name === 'auth'
386
+ ? `import { LoginScreen } from '../features/auth/${screenDir}/LoginScreen';\nimport { RegisterScreen } from '../features/auth/${screenDir}/RegisterScreen';`
387
+ : `import { ${pascalName}Screen } from '../features/${snakeName}/${screenDir}/${pascalName}Screen';`;
388
+ if (!contents.includes(importStr.split('\n')[0]))
389
+ contents = `${importStr}\n${contents}`;
390
+ await fs.writeFile(navFile, contents);
391
+ }
392
+ static async generateCleanArchTest(name, _config) { }
393
+ static async generateFeatureBasedTest(name, _config) { }
394
+ static async generateAtomicDesignTest(name, _config) { }
395
+ static async generateMvvmTest(name, _config) { }
396
+ static async writeFile(filePath, content) {
397
+ await fs.ensureDir(path.dirname(filePath));
398
+ await fs.writeFile(filePath, content);
399
+ }
400
+ }
@@ -0,0 +1,4 @@
1
+ import type { GeneratorConfig } from '../models/config.js';
2
+ export declare class FileHelper {
3
+ static generateBaseStructure(config: GeneratorConfig): Promise<void>;
4
+ }
@@ -0,0 +1,62 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { ConfigHelper } from './config-helper.js';
4
+ import { BaseTemplates } from '../templates/base-templates.js';
5
+ import { PackageJsonHelper } from './packagejson-helper.js';
6
+ export class FileHelper {
7
+ static async generateBaseStructure(config) {
8
+ const root = process.cwd();
9
+ // 1. Create Directories
10
+ const dirs = [
11
+ 'src/core/api',
12
+ 'src/core/errors',
13
+ 'src/core/theme',
14
+ 'src/core/utils',
15
+ 'src/core/components',
16
+ 'src/features',
17
+ 'src/navigation',
18
+ 'src/state',
19
+ 'assets/images',
20
+ 'assets/fonts',
21
+ ];
22
+ if (config.localization) {
23
+ dirs.push('src/i18n/locales');
24
+ }
25
+ if (config.tests) {
26
+ dirs.push('__tests__/unit', '__tests__/integration');
27
+ }
28
+ for (const dir of dirs) {
29
+ await fs.ensureDir(path.join(root, dir));
30
+ }
31
+ // 2. Create Base Files
32
+ const files = {
33
+ 'src/App.tsx': BaseTemplates.appEntryContent(config),
34
+ 'src/core/api/apiClient.ts': BaseTemplates.apiClientContent(),
35
+ 'src/core/errors/failures.ts': BaseTemplates.failuresContent(),
36
+ 'src/core/theme/AppTheme.ts': BaseTemplates.themeContent(),
37
+ 'src/core/theme/ThemeContext.tsx': BaseTemplates.themeContextContent(),
38
+ 'src/core/constants/AppConstants.ts': BaseTemplates.constantsContent(),
39
+ 'src/navigation/AppNavigator.tsx': BaseTemplates.navigationContent(config),
40
+ 'src/state/store.ts': BaseTemplates.storeContent(config),
41
+ '.env.development': 'API_BASE_URL=https://dev.api.example.com\n',
42
+ '.env.production': 'API_BASE_URL=https://api.example.com\n',
43
+ '.gitignore': BaseTemplates.gitignoreContent(),
44
+ };
45
+ if (config.localization) {
46
+ files['src/i18n/i18n.ts'] = BaseTemplates.i18nConfigContent();
47
+ files['src/i18n/locales/en.json'] = BaseTemplates.localeEnContent();
48
+ }
49
+ if (config.tests) {
50
+ files['__tests__/unit/sample.test.ts'] = BaseTemplates.sampleTestContent();
51
+ }
52
+ for (const [filePath, content] of Object.entries(files)) {
53
+ const fullPath = path.join(root, filePath);
54
+ await fs.ensureDir(path.dirname(fullPath));
55
+ await fs.writeFile(fullPath, content);
56
+ }
57
+ // 3. Update package.json with dependencies
58
+ await PackageJsonHelper.addDependencies(config);
59
+ // 4. Save config
60
+ await ConfigHelper.saveConfig(config);
61
+ }
62
+ }
@@ -0,0 +1,4 @@
1
+ import type { GeneratorConfig } from '../models/config.js';
2
+ export declare class PackageJsonHelper {
3
+ static addDependencies(config: GeneratorConfig): Promise<void>;
4
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { StateManagement, Routing } from '../models/config.js';
4
+ export class PackageJsonHelper {
5
+ static async addDependencies(config) {
6
+ const pkgPath = path.join(process.cwd(), 'package.json');
7
+ if (!(await fs.pathExists(pkgPath)))
8
+ return;
9
+ const pkg = await fs.readJson(pkgPath);
10
+ if (!pkg.dependencies)
11
+ pkg.dependencies = {};
12
+ if (!pkg.devDependencies)
13
+ pkg.devDependencies = {};
14
+ // ── Common Dependencies ──
15
+ const deps = {
16
+ 'axios': '^1.13.5',
17
+ 'react-native-config': '^1.6.1',
18
+ };
19
+ // ── State Management ──
20
+ switch (config.stateManagement) {
21
+ case StateManagement.redux:
22
+ deps['@reduxjs/toolkit'] = '^2.11.2';
23
+ deps['react-redux'] = '^9.2.0';
24
+ break;
25
+ case StateManagement.zustand:
26
+ deps['zustand'] = '^5.0.11';
27
+ break;
28
+ case StateManagement.context:
29
+ // React Context is built-in, no dep needed
30
+ break;
31
+ }
32
+ // ── Routing ──
33
+ switch (config.routing) {
34
+ case Routing.reactNavigation:
35
+ deps['@react-navigation/native'] = '^7.1.28';
36
+ deps['@react-navigation/native-stack'] = '^7.13.0';
37
+ deps['react-native-screens'] = '^4.23.0';
38
+ deps['react-native-safe-area-context'] = '^5.6.2';
39
+ break;
40
+ case Routing.expoRouter:
41
+ deps['expo-router'] = '^6.0.23';
42
+ break;
43
+ }
44
+ // ── Firebase ──
45
+ if (config.firebase) {
46
+ deps['@react-native-firebase/app'] = '^23.8.6';
47
+ }
48
+ // ── Localization ──
49
+ if (config.localization) {
50
+ deps['i18next'] = '^25.8.13';
51
+ deps['react-i18next'] = '^16.5.4';
52
+ }
53
+ // ── Dev Dependencies ──
54
+ const devDeps = {
55
+ '@types/react': '^19.2.14',
56
+ 'typescript': '^5.9.3',
57
+ };
58
+ if (config.tests) {
59
+ devDeps['jest'] = '^30.2.0';
60
+ devDeps['@testing-library/react-native'] = '^13.3.3';
61
+ }
62
+ // react-redux v9+ has built-in types, no need for @types/react-redux
63
+ // Merge without overwriting existing
64
+ for (const [name, version] of Object.entries(deps)) {
65
+ if (!pkg.dependencies[name]) {
66
+ pkg.dependencies[name] = version;
67
+ }
68
+ }
69
+ for (const [name, version] of Object.entries(devDeps)) {
70
+ if (!pkg.devDependencies[name]) {
71
+ pkg.devDependencies[name] = version;
72
+ }
73
+ }
74
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
75
+ }
76
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * String utility functions for naming convention conversions.
3
+ */
4
+ export declare class StringUtils {
5
+ static toSnakeCase(name: string): string;
6
+ static toPascalCase(name: string): string;
7
+ static toCamelCase(name: string): string;
8
+ static toKebabCase(name: string): string;
9
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * String utility functions for naming convention conversions.
3
+ */
4
+ export class StringUtils {
5
+ static toSnakeCase(name) {
6
+ return name
7
+ .replace(/([A-Z])/g, '_$1')
8
+ .toLowerCase()
9
+ .replace(/^_/, '')
10
+ .replace(/_+/g, '_');
11
+ }
12
+ static toPascalCase(name) {
13
+ const snake = StringUtils.toSnakeCase(name);
14
+ return snake
15
+ .split('_')
16
+ .map((s) => (s.length === 0 ? '' : s[0].toUpperCase() + s.slice(1)))
17
+ .join('');
18
+ }
19
+ static toCamelCase(name) {
20
+ const pascal = StringUtils.toPascalCase(name);
21
+ if (pascal.length === 0)
22
+ return '';
23
+ return pascal[0].toLowerCase() + pascal.slice(1);
24
+ }
25
+ static toKebabCase(name) {
26
+ return StringUtils.toSnakeCase(name).replace(/_/g, '-');
27
+ }
28
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "react-native-architecture-generator",
3
+ "version": "1.1.0",
4
+ "description": "A professional CLI tool to scaffold React Native apps with Clean Architecture, MVVM, Atomic Design, or Feature-Based patterns.",
5
+ "type": "module",
6
+ "main": "./dist/lib/index.js",
7
+ "module": "./dist/lib/index.js",
8
+ "types": "./dist/lib/index.d.ts",
9
+ "bin": {
10
+ "rn-arch-gen": "./dist/bin/rn_arch_gen.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/lib/index.d.ts",
15
+ "import": "./dist/lib/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist/",
20
+ "README.md",
21
+ "LICENSE",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "prepublishOnly": "npm run build",
30
+ "start": "node ./dist/bin/rn_arch_gen.js",
31
+ "test": "echo \"Error: no test specified\" && exit 1"
32
+ },
33
+ "keywords": [
34
+ "react-native",
35
+ "clean-architecture",
36
+ "feature-based",
37
+ "atomic-design",
38
+ "mvvm",
39
+ "cli",
40
+ "scaffolding",
41
+ "generator",
42
+ "code-generation",
43
+ "boilerplate",
44
+ "typescript",
45
+ "redux",
46
+ "zustand"
47
+ ],
48
+ "author": "Naimish Kumar <vnaimishkumar@gmail.com>",
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/Naimish-Kumar/react_native_architecture_generator.git"
53
+ },
54
+ "homepage": "https://github.com/Naimish-Kumar/react_native_architecture_generator#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/Naimish-Kumar/react_native_architecture_generator/issues"
57
+ },
58
+ "sideEffects": false,
59
+ "dependencies": {
60
+ "chalk": "^5.6.2",
61
+ "commander": "^14.0.3",
62
+ "fs-extra": "^11.3.3",
63
+ "inquirer": "^13.1.0",
64
+ "ora": "^9.3.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/fs-extra": "^11.0.4",
68
+ "@types/inquirer": "^9.0.9",
69
+ "@types/node": "^22.0.0",
70
+ "typescript": "^5.9.3"
71
+ }
72
+ }