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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/dist/bin/rn_arch_gen.d.ts +2 -0
- package/dist/bin/rn_arch_gen.js +41 -0
- package/dist/lib/commands/feature.d.ts +3 -0
- package/dist/lib/commands/feature.js +52 -0
- package/dist/lib/commands/init.d.ts +3 -0
- package/dist/lib/commands/init.js +75 -0
- package/dist/lib/commands/model.d.ts +3 -0
- package/dist/lib/commands/model.js +94 -0
- package/dist/lib/commands/screen.d.ts +4 -0
- package/dist/lib/commands/screen.js +82 -0
- package/dist/lib/index.d.ts +12 -0
- package/dist/lib/index.js +12 -0
- package/dist/lib/models/config.d.ts +27 -0
- package/dist/lib/models/config.js +27 -0
- package/dist/lib/templates/base-templates.d.ts +18 -0
- package/dist/lib/templates/base-templates.js +453 -0
- package/dist/lib/utils/config-helper.d.ts +6 -0
- package/dist/lib/utils/config-helper.js +27 -0
- package/dist/lib/utils/feature-helper.d.ts +24 -0
- package/dist/lib/utils/feature-helper.js +400 -0
- package/dist/lib/utils/file-helper.d.ts +4 -0
- package/dist/lib/utils/file-helper.js +62 -0
- package/dist/lib/utils/packagejson-helper.d.ts +4 -0
- package/dist/lib/utils/packagejson-helper.js +76 -0
- package/dist/lib/utils/string-utils.d.ts +9 -0
- package/dist/lib/utils/string-utils.js +28 -0
- package/package.json +72 -0
|
@@ -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,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,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
|
+
}
|