@arcpkg/initiator 0.0.1-beta.0.1
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/README.md +524 -0
- package/config.ts +153 -0
- package/index.ts +23 -0
- package/intl.ts +366 -0
- package/js/config.js +110 -0
- package/js/index.js +17 -0
- package/js/intl.js +287 -0
- package/js/native-routing.js +696 -0
- package/js/provider.js +303 -0
- package/js/routing.js +533 -0
- package/native-routing.ts +871 -0
- package/package.json +21 -0
- package/provider.ts +403 -0
- package/routing.ts +691 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { Pajo } from '@arcpkg/pajo';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export interface RouteConfig {
|
|
10
|
+
srcDir: string;
|
|
11
|
+
modulesDir: string;
|
|
12
|
+
pagesDir: string;
|
|
13
|
+
outputFile: string;
|
|
14
|
+
layoutFileName: string;
|
|
15
|
+
errorFileName: string;
|
|
16
|
+
notFoundFileName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RouteFile {
|
|
20
|
+
filePath: string;
|
|
21
|
+
relativePath: string;
|
|
22
|
+
routePath: string;
|
|
23
|
+
componentName: string;
|
|
24
|
+
layoutComponent?: string;
|
|
25
|
+
errorComponent?: string;
|
|
26
|
+
notFoundComponent?: string;
|
|
27
|
+
moduleName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class NativeRouteGenerator {
|
|
31
|
+
private config: RouteConfig;
|
|
32
|
+
private routeFiles: RouteFile[] = [];
|
|
33
|
+
private modules: Set<string> = new Set();
|
|
34
|
+
private modulePathOverrides: Map<string, string> = new Map();
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
config: Partial<RouteConfig> = {},
|
|
38
|
+
dirname: string | undefined = __dirname,
|
|
39
|
+
) {
|
|
40
|
+
this.config = {
|
|
41
|
+
srcDir: config.srcDir || Pajo.join(dirname, 'src') || '',
|
|
42
|
+
modulesDir: config.modulesDir || Pajo.join(dirname, 'src\\modules') || '',
|
|
43
|
+
pagesDir: config.pagesDir || Pajo.join(dirname, 'src\\pages') || '',
|
|
44
|
+
outputFile: config.outputFile || Pajo.join(dirname, 'src\\auto-routes.tsx') || '',
|
|
45
|
+
layoutFileName: config.layoutFileName || '_layout',
|
|
46
|
+
errorFileName: config.errorFileName || '_error',
|
|
47
|
+
notFoundFileName: config.notFoundFileName || '_404'
|
|
48
|
+
};
|
|
49
|
+
console.log(`--> NativeRouteGenerator config:: `, this.config);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async generate(): Promise<void> {
|
|
53
|
+
console.log('🔍 Scanning for native route files...');
|
|
54
|
+
|
|
55
|
+
await this.loadModulePathOverrides();
|
|
56
|
+
|
|
57
|
+
await this.findRouteFiles();
|
|
58
|
+
|
|
59
|
+
await this.generateNativeRoutesFile();
|
|
60
|
+
|
|
61
|
+
console.log(`✅ Generated ${this.config.outputFile} with ${this.routeFiles.length} native routes`);
|
|
62
|
+
console.log(`📦 Found modules with routes: ${Array.from(this.modules).join(', ')}`);
|
|
63
|
+
console.log(`🔄 Module path overrides: ${Array.from(this.modulePathOverrides.entries()).map(([k, v]) => `${k} -> ${v}`).join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async loadModulePathOverrides(): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(this.config.modulesDir)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const moduleDirs = fs.readdirSync(this.config.modulesDir, { withFileTypes: true })
|
|
74
|
+
.filter(dirent => dirent.isDirectory())
|
|
75
|
+
.map(dirent => dirent.name);
|
|
76
|
+
|
|
77
|
+
for (const moduleName of moduleDirs) {
|
|
78
|
+
const configPath = path.join(this.config.modulesDir, moduleName, 'config.json');
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(configPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
83
|
+
const config = JSON.parse(configContent);
|
|
84
|
+
|
|
85
|
+
if (config.path && typeof config.path === 'string') {
|
|
86
|
+
|
|
87
|
+
let modulePath = config.path.trim();
|
|
88
|
+
if (modulePath.startsWith('/')) {
|
|
89
|
+
modulePath = modulePath.substring(1);
|
|
90
|
+
}
|
|
91
|
+
if (modulePath.endsWith('/')) {
|
|
92
|
+
modulePath = modulePath.slice(0, -1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (modulePath) {
|
|
96
|
+
this.modulePathOverrides.set(moduleName, modulePath);
|
|
97
|
+
console.log(`🔄 Module ${moduleName} path override: ${modulePath}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`Error reading config.json for module ${moduleName}:`, error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error loading module path overrides:', error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async findRouteFiles(): Promise<void> {
|
|
111
|
+
this.routeFiles = [];
|
|
112
|
+
|
|
113
|
+
await this.scanDirectoryForRoutes(this.config.pagesDir);
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(this.config.modulesDir)) {
|
|
116
|
+
const moduleDirs = fs.readdirSync(this.config.modulesDir, { withFileTypes: true })
|
|
117
|
+
.filter(dirent => dirent.isDirectory())
|
|
118
|
+
.map(dirent => dirent.name);
|
|
119
|
+
|
|
120
|
+
for (const moduleName of moduleDirs) {
|
|
121
|
+
const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
|
|
122
|
+
if (fs.existsSync(modulePagesDir)) {
|
|
123
|
+
this.modules.add(moduleName);
|
|
124
|
+
await this.scanDirectoryForRoutes(modulePagesDir, moduleName);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async scanDirectoryForRoutes(dir: string, moduleName?: string): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const fullPath = path.join(dir, item.name);
|
|
136
|
+
|
|
137
|
+
if (item.name === 'node_modules' ||
|
|
138
|
+
item.name === 'dist' ||
|
|
139
|
+
item.name === 'build' ||
|
|
140
|
+
item.name.startsWith('.')) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (item.isDirectory()) {
|
|
145
|
+
|
|
146
|
+
await this.scanDirectoryForRoutes(fullPath, moduleName);
|
|
147
|
+
} else if (item.isFile()) {
|
|
148
|
+
|
|
149
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
150
|
+
if (['.tsx', '.jsx'].includes(ext)) {
|
|
151
|
+
const fileName = path.basename(item.name, ext);
|
|
152
|
+
|
|
153
|
+
if (fileName === this.config.layoutFileName ||
|
|
154
|
+
fileName === this.config.errorFileName ||
|
|
155
|
+
fileName === this.config.notFoundFileName) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await this.processRouteFile(fullPath, moduleName);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(`Error scanning directory ${dir}:`, error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async processRouteFile(filePath: string, moduleName?: string): Promise<void> {
|
|
169
|
+
try {
|
|
170
|
+
|
|
171
|
+
let routePath = this.convertFilePathToRoutePath(filePath, moduleName);
|
|
172
|
+
|
|
173
|
+
const componentName = this.extractComponentName(filePath, moduleName);
|
|
174
|
+
|
|
175
|
+
const layoutComponent = this.findLayoutComponent(filePath, moduleName);
|
|
176
|
+
const errorComponent = this.findErrorComponent(filePath, moduleName);
|
|
177
|
+
const notFoundComponent = this.findNotFoundComponent(filePath, moduleName);
|
|
178
|
+
|
|
179
|
+
const routeFile: RouteFile = {
|
|
180
|
+
filePath,
|
|
181
|
+
relativePath: path.relative(this.config.srcDir, filePath),
|
|
182
|
+
routePath,
|
|
183
|
+
componentName,
|
|
184
|
+
layoutComponent,
|
|
185
|
+
errorComponent,
|
|
186
|
+
notFoundComponent,
|
|
187
|
+
moduleName
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
this.routeFiles.push(routeFile);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(`Error processing route file ${filePath}:`, error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private convertFilePathToRoutePath(filePath: string, moduleName?: string): string {
|
|
197
|
+
let routePath = filePath;
|
|
198
|
+
|
|
199
|
+
routePath = routePath.replace(this.config.srcDir, '');
|
|
200
|
+
|
|
201
|
+
if (moduleName) {
|
|
202
|
+
|
|
203
|
+
const modulePagesPrefix = `modules${path.sep}${moduleName}${path.sep}pages`;
|
|
204
|
+
if (routePath.includes(modulePagesPrefix)) {
|
|
205
|
+
routePath = routePath.substring(routePath.indexOf(modulePagesPrefix) + modulePagesPrefix.length);
|
|
206
|
+
|
|
207
|
+
let modulePrefix = `/${moduleName}`;
|
|
208
|
+
if (this.modulePathOverrides.has(moduleName)) {
|
|
209
|
+
const overridePath = this.modulePathOverrides.get(moduleName);
|
|
210
|
+
if (overridePath) {
|
|
211
|
+
modulePrefix = `/${overridePath}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
routePath = modulePrefix + routePath;
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
|
|
218
|
+
const pagesPrefix = 'pages';
|
|
219
|
+
if (routePath.includes(pagesPrefix)) {
|
|
220
|
+
routePath = routePath.substring(routePath.indexOf(pagesPrefix) + pagesPrefix.length);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
routePath = routePath.replace(/\.(tsx|jsx)$/, '');
|
|
225
|
+
|
|
226
|
+
routePath = routePath.replace(/\\/g, '/');
|
|
227
|
+
|
|
228
|
+
if (routePath.endsWith('/index')) {
|
|
229
|
+
routePath = routePath.replace(/\/index$/, '');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
routePath = routePath.replace(/\{([^}]+)\}/g, ':$1');
|
|
233
|
+
|
|
234
|
+
if (!routePath.startsWith('/')) {
|
|
235
|
+
routePath = '/' + routePath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (routePath === '/' || routePath === '//') {
|
|
239
|
+
return '/';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return routePath;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private extractComponentName(filePath: string, moduleName?: string): string {
|
|
246
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
247
|
+
const dirName = path.dirname(filePath);
|
|
248
|
+
|
|
249
|
+
const parts = dirName.split(path.sep);
|
|
250
|
+
|
|
251
|
+
let startIndex = 0;
|
|
252
|
+
if (moduleName) {
|
|
253
|
+
|
|
254
|
+
const modulePagesPath = path.sep + 'modules' + path.sep + moduleName + path.sep + 'pages';
|
|
255
|
+
startIndex = parts.findIndex((part, index, arr) => {
|
|
256
|
+
const pathSoFar = arr.slice(0, index + 1).join(path.sep);
|
|
257
|
+
return pathSoFar.endsWith(modulePagesPath);
|
|
258
|
+
}) + 1;
|
|
259
|
+
} else {
|
|
260
|
+
|
|
261
|
+
const pagesPath = path.sep + 'pages';
|
|
262
|
+
startIndex = parts.findIndex((part, index, arr) => {
|
|
263
|
+
const pathSoFar = arr.slice(0, index + 1).join(path.sep);
|
|
264
|
+
return pathSoFar.endsWith(pagesPath);
|
|
265
|
+
}) + 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (startIndex < 0) startIndex = 0;
|
|
269
|
+
|
|
270
|
+
const relevantParts = parts.slice(startIndex);
|
|
271
|
+
|
|
272
|
+
let componentName = moduleName ? this.toPascalCase(moduleName) : '';
|
|
273
|
+
|
|
274
|
+
relevantParts.forEach(part => {
|
|
275
|
+
if (part && part !== 'pages') {
|
|
276
|
+
componentName += this.toPascalCase(part);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
let fileNamePart = fileName;
|
|
281
|
+
if (fileName.startsWith('{') && fileName.endsWith('}')) {
|
|
282
|
+
|
|
283
|
+
const paramName = fileName.substring(1, fileName.length - 1);
|
|
284
|
+
fileNamePart = this.toPascalCase(paramName) + 'Params';
|
|
285
|
+
} else {
|
|
286
|
+
fileNamePart = this.toPascalCase(fileName);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!fileNamePart.endsWith('Page')) {
|
|
290
|
+
fileNamePart += 'Page';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const reservedNames = ['Layout', 'ErrorBoundary', 'NotFound'];
|
|
294
|
+
if (reservedNames.includes(fileNamePart)) {
|
|
295
|
+
fileNamePart += 'Component';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return componentName + fileNamePart;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private toPascalCase(str: string): string {
|
|
302
|
+
if (!str) return '';
|
|
303
|
+
return str
|
|
304
|
+
.split(/[-_]/)
|
|
305
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
306
|
+
.join('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private findLayoutComponent(filePath: string, moduleName?: string): string | undefined {
|
|
310
|
+
const dir = path.dirname(filePath);
|
|
311
|
+
let currentDir = dir;
|
|
312
|
+
|
|
313
|
+
while (currentDir !== this.config.srcDir && currentDir !== path.dirname(this.config.srcDir)) {
|
|
314
|
+
const layoutPath = path.join(currentDir, `${this.config.layoutFileName}.tsx`);
|
|
315
|
+
|
|
316
|
+
if (fs.existsSync(layoutPath)) {
|
|
317
|
+
return this.generateComponentName(layoutPath, moduleName, 'Layout');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (moduleName) {
|
|
321
|
+
const modulePath = path.join(this.config.modulesDir, moduleName);
|
|
322
|
+
if (currentDir === modulePath) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
currentDir = path.dirname(currentDir);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const globalLayoutPath = path.join(this.config.pagesDir, `${this.config.layoutFileName}.tsx`);
|
|
331
|
+
if (fs.existsSync(globalLayoutPath)) {
|
|
332
|
+
return 'Layout';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private findErrorComponent(filePath: string, moduleName?: string): string | undefined {
|
|
339
|
+
const dir = path.dirname(filePath);
|
|
340
|
+
let currentDir = dir;
|
|
341
|
+
|
|
342
|
+
while (currentDir !== this.config.srcDir && currentDir !== path.dirname(this.config.srcDir)) {
|
|
343
|
+
const errorPath = path.join(currentDir, `${this.config.errorFileName}.tsx`);
|
|
344
|
+
|
|
345
|
+
if (fs.existsSync(errorPath)) {
|
|
346
|
+
return this.generateComponentName(errorPath, moduleName, 'ErrorBoundary');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (moduleName) {
|
|
350
|
+
const modulePath = path.join(this.config.modulesDir, moduleName);
|
|
351
|
+
if (currentDir === modulePath) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
currentDir = path.dirname(currentDir);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const globalErrorPath = path.join(this.config.pagesDir, `${this.config.errorFileName}.tsx`);
|
|
360
|
+
if (fs.existsSync(globalErrorPath)) {
|
|
361
|
+
return 'ErrorBoundary';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private findNotFoundComponent(filePath: string, moduleName?: string): string | undefined {
|
|
368
|
+
if (!moduleName) return undefined;
|
|
369
|
+
|
|
370
|
+
const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
|
|
371
|
+
const notFoundPath = path.join(modulePagesDir, `${this.config.notFoundFileName}.tsx`);
|
|
372
|
+
|
|
373
|
+
if (fs.existsSync(notFoundPath)) {
|
|
374
|
+
return this.generateComponentName(notFoundPath, moduleName, 'NotFound');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private generateComponentName(filePath: string, moduleName: string | undefined, suffix: string): string {
|
|
381
|
+
if (!moduleName) {
|
|
382
|
+
return suffix;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
386
|
+
const dirName = path.dirname(filePath);
|
|
387
|
+
|
|
388
|
+
const modulePagesPath = path.join(this.config.modulesDir, moduleName, 'pages');
|
|
389
|
+
const relativePath = path.relative(modulePagesPath, dirName);
|
|
390
|
+
|
|
391
|
+
let componentName = this.toPascalCase(moduleName);
|
|
392
|
+
|
|
393
|
+
if (relativePath && relativePath !== '.') {
|
|
394
|
+
const dirParts = relativePath.split(path.sep).filter(part => part && part !== '.');
|
|
395
|
+
dirParts.forEach(part => {
|
|
396
|
+
componentName += this.toPascalCase(part);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (fileName !== this.config.layoutFileName &&
|
|
401
|
+
fileName !== this.config.errorFileName &&
|
|
402
|
+
fileName !== this.config.notFoundFileName) {
|
|
403
|
+
componentName += this.toPascalCase(fileName);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return componentName + suffix;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async generateNativeRoutesFile(): Promise<void> {
|
|
410
|
+
const outputDir = path.dirname(this.config.outputFile);
|
|
411
|
+
|
|
412
|
+
if (!fs.existsSync(outputDir)) {
|
|
413
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const content = this.generateFileContent();
|
|
417
|
+
fs.writeFileSync(this.config.outputFile, content, 'utf-8');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private getRelativeImportPath(targetPath: string): string {
|
|
421
|
+
const outputDir = path.dirname(this.config.outputFile);
|
|
422
|
+
const relative = path.relative(outputDir, targetPath);
|
|
423
|
+
|
|
424
|
+
let importPath = relative.replace(/\\/g, '/');
|
|
425
|
+
|
|
426
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
427
|
+
importPath = './' + importPath;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
importPath = importPath.replace(/\.(tsx|jsx)$/, '');
|
|
431
|
+
|
|
432
|
+
return importPath;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private generateFileContent(): string {
|
|
436
|
+
|
|
437
|
+
const components = new Set<string>();
|
|
438
|
+
const imports: string[] = [
|
|
439
|
+
`import React from 'react';`,
|
|
440
|
+
`import { View, StyleSheet, Text } from 'react-native';`,
|
|
441
|
+
`import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';`,
|
|
442
|
+
``
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
const globalLayoutPath = path.join(this.config.pagesDir, `${this.config.layoutFileName}.tsx`);
|
|
446
|
+
const globalErrorPath = path.join(this.config.pagesDir, `${this.config.errorFileName}.tsx`);
|
|
447
|
+
const global404Path = path.join(this.config.pagesDir, `${this.config.notFoundFileName}.tsx`);
|
|
448
|
+
|
|
449
|
+
if (fs.existsSync(globalLayoutPath)) {
|
|
450
|
+
const importPath = this.getRelativeImportPath(globalLayoutPath);
|
|
451
|
+
imports.push(`import Layout from '${importPath}';`);
|
|
452
|
+
components.add('Layout');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (fs.existsSync(globalErrorPath)) {
|
|
456
|
+
const importPath = this.getRelativeImportPath(globalErrorPath);
|
|
457
|
+
imports.push(`import ErrorBoundary from '${importPath}';`);
|
|
458
|
+
components.add('ErrorBoundary');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (fs.existsSync(global404Path)) {
|
|
462
|
+
const importPath = this.getRelativeImportPath(global404Path);
|
|
463
|
+
imports.push(`import NotFound from '${importPath}';`);
|
|
464
|
+
components.add('NotFound');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
imports.push(``);
|
|
468
|
+
|
|
469
|
+
const specialComponents = new Map<string, string>();
|
|
470
|
+
|
|
471
|
+
const findSpecialFiles = (baseDir: string, isModule: boolean = false, moduleName?: string) => {
|
|
472
|
+
if (!fs.existsSync(baseDir)) return;
|
|
473
|
+
|
|
474
|
+
const walkDir = (dir: string) => {
|
|
475
|
+
try {
|
|
476
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
477
|
+
|
|
478
|
+
for (const item of items) {
|
|
479
|
+
const fullPath = path.join(dir, item.name);
|
|
480
|
+
|
|
481
|
+
if (item.isDirectory()) {
|
|
482
|
+
walkDir(fullPath);
|
|
483
|
+
} else if (item.isFile()) {
|
|
484
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
485
|
+
if (['.tsx', '.jsx'].includes(ext)) {
|
|
486
|
+
const fileName = path.basename(item.name, ext);
|
|
487
|
+
|
|
488
|
+
if (fileName === this.config.layoutFileName ||
|
|
489
|
+
fileName === this.config.errorFileName ||
|
|
490
|
+
fileName === this.config.notFoundFileName) {
|
|
491
|
+
|
|
492
|
+
const componentName = this.generateComponentName(fullPath, moduleName,
|
|
493
|
+
fileName === this.config.layoutFileName ? 'Layout' :
|
|
494
|
+
fileName === this.config.errorFileName ? 'ErrorBoundary' : 'NotFound');
|
|
495
|
+
|
|
496
|
+
if (!components.has(componentName)) {
|
|
497
|
+
specialComponents.set(fullPath, componentName);
|
|
498
|
+
components.add(componentName);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.error(`Error walking directory ${dir}:`, error);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
walkDir(baseDir);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
findSpecialFiles(this.config.pagesDir, false);
|
|
513
|
+
|
|
514
|
+
this.modules.forEach(moduleName => {
|
|
515
|
+
const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
|
|
516
|
+
if (fs.existsSync(modulePagesDir)) {
|
|
517
|
+
findSpecialFiles(modulePagesDir, true, moduleName);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
specialComponents.forEach((componentName, filePath) => {
|
|
522
|
+
const importPath = this.getRelativeImportPath(filePath);
|
|
523
|
+
imports.push(`import ${componentName} from '${importPath}';`);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
if (specialComponents.size > 0) {
|
|
527
|
+
imports.push(``);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.routeFiles.forEach(route => {
|
|
531
|
+
|
|
532
|
+
const importPath = this.getRelativeImportPath(route.filePath);
|
|
533
|
+
imports.push(`import ${route.componentName} from '${importPath}';`);
|
|
534
|
+
components.add(route.componentName);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
imports.push(``);
|
|
538
|
+
|
|
539
|
+
const content = [
|
|
540
|
+
...imports,
|
|
541
|
+
this.generateExpoRouteInterface(),
|
|
542
|
+
this.generateDefaultErrorPage(),
|
|
543
|
+
this.generateErrorBoundaryWrapper(),
|
|
544
|
+
this.generateCreateScreenFunction(),
|
|
545
|
+
this.generateRoutesArray(components, specialComponents)
|
|
546
|
+
].join('\n');
|
|
547
|
+
|
|
548
|
+
return content;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private generateExpoRouteInterface(): string {
|
|
552
|
+
return `
|
|
553
|
+
export interface ExpoRoute {
|
|
554
|
+
name: string;
|
|
555
|
+
component: React.ComponentType<any>;
|
|
556
|
+
path?: string;
|
|
557
|
+
options?: NativeStackNavigationOptions;
|
|
558
|
+
layout?: React.ComponentType<any>;
|
|
559
|
+
errorBoundary?: React.ComponentType<any>;
|
|
560
|
+
}
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private generateDefaultErrorPage(): string {
|
|
565
|
+
return `
|
|
566
|
+
const DefaultErrorPage: ({ error }: { error: any }) => React.JSX.Element = ({ error }) => {
|
|
567
|
+
return (
|
|
568
|
+
<View style={defaultErrorStyles.container}>
|
|
569
|
+
<Text style={defaultErrorStyles.title}>ERROR DETECTED</Text>
|
|
570
|
+
{!!error && (
|
|
571
|
+
<View style={defaultErrorStyles.error_container}>
|
|
572
|
+
<Text style={defaultErrorStyles.error_title}>{error.message}</Text>
|
|
573
|
+
</View>
|
|
574
|
+
)}
|
|
575
|
+
</View>
|
|
576
|
+
);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const defaultErrorStyles = StyleSheet.create({
|
|
580
|
+
container: {
|
|
581
|
+
flex: 1,
|
|
582
|
+
flexDirection: 'column',
|
|
583
|
+
justifyContent: 'center',
|
|
584
|
+
alignItems: 'center',
|
|
585
|
+
padding: 10,
|
|
586
|
+
gap: 5,
|
|
587
|
+
backgroundColor: '#f5f5f5',
|
|
588
|
+
},
|
|
589
|
+
title: {
|
|
590
|
+
fontSize: 24,
|
|
591
|
+
fontWeight: 'bold',
|
|
592
|
+
color: '#333',
|
|
593
|
+
},
|
|
594
|
+
error_container: {
|
|
595
|
+
display: 'flex',
|
|
596
|
+
flexDirection: 'column',
|
|
597
|
+
justifyContent: 'center',
|
|
598
|
+
alignItems: 'center',
|
|
599
|
+
gap: 3,
|
|
600
|
+
backgroundColor: '#d32f2f',
|
|
601
|
+
color: '#fff',
|
|
602
|
+
padding: 15,
|
|
603
|
+
borderRadius: 5,
|
|
604
|
+
},
|
|
605
|
+
error_title: {
|
|
606
|
+
fontWeight: 'medium',
|
|
607
|
+
fontSize: 18,
|
|
608
|
+
color: '#fff',
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private generateErrorBoundaryWrapper(): string {
|
|
615
|
+
return `
|
|
616
|
+
class ErrorBoundaryWrapper extends React.Component<{
|
|
617
|
+
children: React.ReactNode;
|
|
618
|
+
ErrorBoundaryComponent?: React.ComponentType<{ error: Error }>;
|
|
619
|
+
}> {
|
|
620
|
+
state = { hasError: false, error: null as Error | null };
|
|
621
|
+
|
|
622
|
+
static getDerivedStateFromError(error: Error) {
|
|
623
|
+
return { hasError: true, error };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
627
|
+
console.log(\`[src -> auto-route] ErrorBoundaryWrapper | error:: \`, error);
|
|
628
|
+
console.log(\`[src -> auto-route] ErrorBoundaryWrapper | errorInfo:: \`, errorInfo);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
render() {
|
|
632
|
+
if (this.state.hasError && this.props.ErrorBoundaryComponent) {
|
|
633
|
+
return React.createElement(this.props.ErrorBoundaryComponent, {
|
|
634
|
+
error: this.state.error as Error
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return this.props.children;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private generateCreateScreenFunction(): string {
|
|
645
|
+
return `
|
|
646
|
+
const createScreen = (
|
|
647
|
+
Component: React.ComponentType<any>,
|
|
648
|
+
layout?: React.ComponentType<any>,
|
|
649
|
+
errorBoundary?: React.ComponentType<any>
|
|
650
|
+
) => {
|
|
651
|
+
return function WrappedScreen(props: any) {
|
|
652
|
+
const Content = layout ?
|
|
653
|
+
React.createElement(layout, null, React.createElement(Component, props)) :
|
|
654
|
+
React.createElement(Component, props);
|
|
655
|
+
|
|
656
|
+
if (errorBoundary) {
|
|
657
|
+
return React.createElement(
|
|
658
|
+
ErrorBoundaryWrapper,
|
|
659
|
+
{
|
|
660
|
+
ErrorBoundaryComponent: errorBoundary,
|
|
661
|
+
children: Content
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
} else {
|
|
665
|
+
return React.createElement(
|
|
666
|
+
ErrorBoundaryWrapper,
|
|
667
|
+
{
|
|
668
|
+
ErrorBoundaryComponent: DefaultErrorPage,
|
|
669
|
+
children: Content
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private generateRoutesArray(components: Set<string>, specialComponents: Map<string, string>): string {
|
|
679
|
+
const routes: string[] = [];
|
|
680
|
+
routes.push(`export let routes: ExpoRoute[] = [`);
|
|
681
|
+
|
|
682
|
+
const sortedRoutes = this.routeFiles.sort((a, b) => {
|
|
683
|
+
|
|
684
|
+
if (a.routePath === '/') return -1;
|
|
685
|
+
if (b.routePath === '/') return 1;
|
|
686
|
+
|
|
687
|
+
if (!a.moduleName && b.moduleName) return -1;
|
|
688
|
+
if (a.moduleName && !b.moduleName) return 1;
|
|
689
|
+
|
|
690
|
+
if (a.moduleName === b.moduleName) {
|
|
691
|
+
return a.routePath.localeCompare(b.routePath);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return 0;
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
let lastModuleName: string | undefined = undefined;
|
|
698
|
+
|
|
699
|
+
sortedRoutes.forEach((route, index) => {
|
|
700
|
+
|
|
701
|
+
if (route.moduleName !== lastModuleName) {
|
|
702
|
+
if (lastModuleName !== undefined) {
|
|
703
|
+
routes.push(``);
|
|
704
|
+
}
|
|
705
|
+
if (route.moduleName) {
|
|
706
|
+
|
|
707
|
+
let displayName = route.moduleName;
|
|
708
|
+
if (this.modulePathOverrides.has(route.moduleName)) {
|
|
709
|
+
const overridePath = this.modulePathOverrides.get(route.moduleName);
|
|
710
|
+
displayName = `${route.moduleName} (path: ${overridePath})`;
|
|
711
|
+
}
|
|
712
|
+
routes.push(` // ${displayName} module routes`);
|
|
713
|
+
}
|
|
714
|
+
lastModuleName = route.moduleName;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let layoutComponent = route.layoutComponent;
|
|
718
|
+
if (!layoutComponent && route.moduleName) {
|
|
719
|
+
|
|
720
|
+
const moduleLayoutPath = path.join(this.config.modulesDir, route.moduleName, 'pages', `${this.config.layoutFileName}.tsx`);
|
|
721
|
+
if (fs.existsSync(moduleLayoutPath)) {
|
|
722
|
+
layoutComponent = this.generateComponentName(moduleLayoutPath, route.moduleName, 'Layout');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let errorComponent = route.errorComponent;
|
|
727
|
+
if (!errorComponent && route.moduleName) {
|
|
728
|
+
|
|
729
|
+
const moduleErrorPath = path.join(this.config.modulesDir, route.moduleName, 'pages', `${this.config.errorFileName}.tsx`);
|
|
730
|
+
if (fs.existsSync(moduleErrorPath)) {
|
|
731
|
+
errorComponent = this.generateComponentName(moduleErrorPath, route.moduleName, 'ErrorBoundary');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const screenName = this.generateScreenName(route);
|
|
736
|
+
|
|
737
|
+
const routeLines: string[] = [' {'];
|
|
738
|
+
routeLines.push(` name: '${screenName}',`);
|
|
739
|
+
routeLines.push(` component: createScreen(${route.componentName}, ${layoutComponent || 'undefined'}, ${errorComponent || 'undefined'}),`);
|
|
740
|
+
routeLines.push(` path: '${route.routePath}',`);
|
|
741
|
+
routeLines.push(` options: { headerShown: false }`);
|
|
742
|
+
routeLines.push(' }');
|
|
743
|
+
|
|
744
|
+
if (index < sortedRoutes.length - 1) {
|
|
745
|
+
routeLines[routeLines.length - 1] += ',';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
routes.push(routeLines.join('\n'));
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const modulesWith404 = new Set<string>();
|
|
752
|
+
|
|
753
|
+
this.modules.forEach(moduleName => {
|
|
754
|
+
const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
|
|
755
|
+
if (fs.existsSync(modulePagesDir)) {
|
|
756
|
+
const notFoundPath = path.join(modulePagesDir, `${this.config.notFoundFileName}.tsx`);
|
|
757
|
+
if (fs.existsSync(notFoundPath)) {
|
|
758
|
+
modulesWith404.add(moduleName);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
modulesWith404.forEach(moduleName => {
|
|
764
|
+
const componentName = this.generateComponentName(
|
|
765
|
+
path.join(this.config.modulesDir, moduleName, 'pages', `${this.config.notFoundFileName}.tsx`),
|
|
766
|
+
moduleName,
|
|
767
|
+
'NotFound'
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
if (routes[routes.length - 1].endsWith(',')) {
|
|
771
|
+
routes[routes.length - 1] = routes[routes.length - 1].slice(0, -1);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
let moduleRoutePath = `/${moduleName}`;
|
|
775
|
+
if (this.modulePathOverrides.has(moduleName)) {
|
|
776
|
+
const overridePath = this.modulePathOverrides.get(moduleName);
|
|
777
|
+
if (overridePath) {
|
|
778
|
+
moduleRoutePath = `/${overridePath}`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
routes.push(`,`);
|
|
783
|
+
routes.push(` {`);
|
|
784
|
+
routes.push(` name: '${moduleName}NotFound',`);
|
|
785
|
+
routes.push(` component: ${componentName},`);
|
|
786
|
+
routes.push(` path: '${moduleRoutePath}/*',`);
|
|
787
|
+
routes.push(` options: { headerShown: false }`);
|
|
788
|
+
routes.push(` }`);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
if (components.has('NotFound')) {
|
|
792
|
+
if (routes[routes.length - 1].endsWith(',')) {
|
|
793
|
+
routes[routes.length - 1] = routes[routes.length - 1].slice(0, -1);
|
|
794
|
+
}
|
|
795
|
+
routes.push(`,`);
|
|
796
|
+
routes.push(` {`);
|
|
797
|
+
routes.push(` name: 'NotFound',`);
|
|
798
|
+
routes.push(` component: NotFound,`);
|
|
799
|
+
routes.push(` path: '*',`);
|
|
800
|
+
routes.push(` options: { headerShown: false }`);
|
|
801
|
+
routes.push(` }`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
routes.push('];');
|
|
805
|
+
routes.push('');
|
|
806
|
+
|
|
807
|
+
routes.push(this.generateLinkingConfig());
|
|
808
|
+
routes.push('');
|
|
809
|
+
routes.push('export default routes;');
|
|
810
|
+
|
|
811
|
+
return routes.join('\n');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private generateScreenName(route: RouteFile): string {
|
|
815
|
+
let screenName = '';
|
|
816
|
+
|
|
817
|
+
if (route.moduleName) {
|
|
818
|
+
screenName += this.toPascalCase(route.moduleName);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const pathParts = route.routePath.split('/').filter(part => part && part !== '');
|
|
822
|
+
|
|
823
|
+
pathParts.forEach(part => {
|
|
824
|
+
if (part.startsWith(':')) {
|
|
825
|
+
|
|
826
|
+
screenName += this.toPascalCase(part.substring(1));
|
|
827
|
+
} else {
|
|
828
|
+
screenName += this.toPascalCase(part);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
if (!screenName) {
|
|
833
|
+
screenName = 'Index';
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return screenName;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private generateLinkingConfig(): string {
|
|
840
|
+
return `
|
|
841
|
+
export const linking = {
|
|
842
|
+
prefixes: ['/'],
|
|
843
|
+
config: {
|
|
844
|
+
screens: routes.reduce((config, route) => {
|
|
845
|
+
if (route.path) {
|
|
846
|
+
const path = route.path.replace(/:(\w+)/g, ':$1');
|
|
847
|
+
config[route.name] = path;
|
|
848
|
+
}
|
|
849
|
+
return config;
|
|
850
|
+
}, {} as Record<string, string>),
|
|
851
|
+
},
|
|
852
|
+
};`;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function NativeRouteInitiator(
|
|
857
|
+
dirname: string | undefined = __dirname
|
|
858
|
+
) {
|
|
859
|
+
const config: Partial<RouteConfig> = {};
|
|
860
|
+
|
|
861
|
+
const generator = new NativeRouteGenerator(config, dirname);
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
await generator.generate();
|
|
865
|
+
console.log('🎉 Native route generation completed successfully!');
|
|
866
|
+
} catch (err) {
|
|
867
|
+
console.error('❌ Failed to generate native routes: ', err);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export default NativeRouteInitiator;
|