@jsarc/initiator 0.0.0 → 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.
Files changed (3) hide show
  1. package/README.md +99 -15
  2. package/package.json +1 -1
  3. package/routing.ts +621 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ![React](https://img.shields.io/badge/React-18+-61DAFB)
6
6
  ![React Router](https://img.shields.io/badge/React%20Router-6+-CA4245)
7
7
 
8
- **@jsarc/initiator** est un plugin d'initialisation intelligent pour les applications React avec TypeScript/Javascript. Il génère automatiquement les fichiers de configuration, de routage et d'internationalisation basés sur la structure de votre projet.
8
+ **@jsarc/initiator** est un plugin d'initialisation intelligent pour les applications React avec TypeScript/Javascript. Il génère automatiquement les fichiers de configuration, de routage, d'internationalisation et de providers basés sur la structure de votre projet.
9
9
 
10
10
  ## ✨ Fonctionnalités Principales
11
11
 
@@ -13,19 +13,22 @@
13
13
  - **Génération automatique des routes** à partir de la structure du système de fichiers
14
14
  - **Configuration modulaire** avec détection automatique des modules
15
15
  - **Internationalisation automatisée** avec extraction des clés de traduction
16
+ - **Providers React organisés** par module et priorité
16
17
  - **Fichiers de configuration** générés dynamiquement
17
18
 
18
19
  ### ⚙️ Initialisation Intelligente
19
- - **Détection automatique** des fichiers de pages et modules
20
+ - **Détection automatique** des fichiers de pages, modules et providers
20
21
  - **Génération de fichiers TypeScript** typesafe
21
22
  - **Support des layouts hiérarchiques** avec héritage automatique
23
+ - **Organisation automatique des providers** (Context, Redux, Router, etc.)
22
24
  - **Configuration minimale** requise
23
25
 
24
26
  ### 📁 Structure de Projet
25
27
  - **Organisation modulaire** naturelle
26
28
  - **Support des pages spéciales** (layout, error, 404)
27
29
  - **Routes dynamiques** avec paramètres
28
- - **Modules indépendants** avec leur propre configuration
30
+ - **Modules indépendants** avec leur propre configuration et providers
31
+ - **Providers globaux et par module** hiérarchisés
29
32
 
30
33
  ## 📦 Installation
31
34
 
@@ -78,10 +81,16 @@ src/
78
81
  ├── auto-config.ts # Configuration générée
79
82
  ├── auto-intl.ts # Internationalisation générée
80
83
  ├── auto-routes.tsx # Routes générées
84
+ ├── auto-provider.tsx # Providers organisés
81
85
  ├── config.json # Configuration racine
82
86
  ├── locales/
83
87
  │ ├── en.json # Traductions anglais
84
88
  │ └── fr.json # Traductions français
89
+ ├── providers/ # Providers globaux
90
+ │ ├── RouterProvider.tsx
91
+ │ ├── ReduxProvider.tsx
92
+ │ ├── ThemeProvider.tsx
93
+ │ └── AuthProvider.tsx
85
94
  ├── pages/
86
95
  │ ├── _layout.tsx # Layout racine
87
96
  │ ├── _error.tsx # Page d'erreur
@@ -93,6 +102,9 @@ src/
93
102
  ├── locales/
94
103
  │ ├── en.json # Traductions module
95
104
  │ └── fr.json # Traductions module
105
+ ├── providers/ # Providers du module
106
+ │ ├── ReduxProvider.tsx
107
+ │ └── AuthProvider.tsx
96
108
  └── pages/
97
109
  └── index.tsx # Page du module
98
110
  ```
@@ -119,6 +131,14 @@ Génération centralisée de configuration :
119
131
  - **Configuration des modules** depuis `modules/*/config.json`
120
132
  - **Fichier TypeScript** avec imports dynamiques
121
133
 
134
+ ### 4. Génération de Providers
135
+ Organisation automatique des providers React :
136
+ - **Détection automatique** des fichiers provider (`Provider.tsx`)
137
+ - **Hiérarchisation intelligente** par priorité
138
+ - **Organisation par module** avec encapsulation automatique
139
+ - **Chargement lazy** avec Suspense intégré
140
+ - **Typage TypeScript** complet
141
+
122
142
  ## 📚 API du Plugin
123
143
 
124
144
  ### Fonction principale
@@ -137,13 +157,15 @@ init('/chemin/vers/mon/projet');
137
157
  import {
138
158
  TranslationGenerator,
139
159
  RouteGenerator,
140
- ConfigGenerator
160
+ ConfigGenerator,
161
+ ProviderGenerator
141
162
  } from '@jsarc/initiator';
142
163
 
143
164
  // Utilisation avancée
144
165
  const translationGen = new TranslationGenerator(config);
145
166
  const routeGen = new RouteGenerator(config);
146
167
  const configGen = new ConfigGenerator(config);
168
+ const providerGen = new ProviderGenerator(config);
147
169
  ```
148
170
 
149
171
  ### Interfaces TypeScript
@@ -152,8 +174,10 @@ import type {
152
174
  TranslationConfig,
153
175
  RouteConfig,
154
176
  ConfigGeneratorOptions,
177
+ ProviderConfig,
155
178
  TranslationKey,
156
- RouteFile
179
+ RouteFile,
180
+ ProviderInfo
157
181
  } from '@jsarc/initiator';
158
182
  ```
159
183
 
@@ -173,19 +197,17 @@ console.log('✅ Initialisation terminée !');
173
197
  ### Exemple 2 : Personnalisation avancée
174
198
  ```typescript
175
199
  // scripts/custom-init.ts
176
- import { RouteGenerator } from '@jsarc/initiator';
200
+ import { ProviderGenerator } from '@jsarc/initiator';
177
201
 
178
202
  const customConfig = {
179
203
  srcDir: './src',
180
204
  modulesDir: './src/modules',
181
- pagesDir: './src/views', // Dossier personnalisé
182
- outputFile: './src/generated/routes.tsx',
183
- layoutFileName: 'layout',
184
- errorFileName: 'error-page',
185
- notFoundFileName: 'not-found-page'
205
+ providersDir: './src/providers',
206
+ globalProvidersDir: './src/global-providers', // Dossier personnalisé
207
+ outputFile: './src/generated/provider.tsx'
186
208
  };
187
209
 
188
- const generator = new RouteGenerator(customConfig);
210
+ const generator = new ProviderGenerator(customConfig);
189
211
  await generator.generate();
190
212
  ```
191
213
 
@@ -198,7 +220,8 @@ await generator.generate();
198
220
  "generate": "node scripts/generate-all.js",
199
221
  "generate:routes": "node scripts/generate-routes.js",
200
222
  "generate:intl": "node scripts/generate-intl.js",
201
- "generate:config": "node scripts/generate-config.js"
223
+ "generate:config": "node scripts/generate-config.js",
224
+ "generate:providers": "node scripts/generate-providers.js"
202
225
  }
203
226
  }
204
227
  ```
@@ -229,6 +252,17 @@ const routeConfig = {
229
252
  };
230
253
  ```
231
254
 
255
+ ### Configuration des providers
256
+ ```typescript
257
+ const providerConfig = {
258
+ srcDir: './src',
259
+ modulesDir: './src/modules',
260
+ providersDir: './src/providers',
261
+ outputFile: './src/auto-provider.tsx',
262
+ globalProvidersDir: './src/providers' // Dossier des providers globaux
263
+ };
264
+ ```
265
+
232
266
  ### Fichier config.json racine
233
267
  ```json
234
268
  {
@@ -273,6 +307,16 @@ const routeConfig = {
273
307
  | `[...slug].tsx` | Route catch-all | `/*` |
274
308
  | `index.tsx` | Page d'index | `/` ou `/dossier/` |
275
309
 
310
+ ### Providers détectés automatiquement
311
+ | Pattern | Type détecté | Priorité |
312
+ |---------|-------------|----------|
313
+ | `*Router*.tsx` | Provider Router | Haute |
314
+ | `*Redux*.tsx` | Provider Redux | Haute |
315
+ | `*Theme*.tsx` | Provider Theme | Moyenne |
316
+ | `*Context*.tsx` | Provider Context | Moyenne |
317
+ | `*Provider.tsx` | Provider générique | Basse |
318
+ | `*Error*.tsx` | Error Boundary | Très basse |
319
+
276
320
  ### Structure de module
277
321
  ```
278
322
  modules/
@@ -281,6 +325,10 @@ modules/
281
325
  ├── locales/ # Traductions du module
282
326
  │ ├── en.json
283
327
  │ └── fr.json
328
+ ├── providers/ # Providers du module
329
+ │ ├── ReduxProvider.tsx
330
+ │ ├── AuthProvider.tsx
331
+ │ └── ThemeProvider.tsx
284
332
  ├── pages/ # Pages du module
285
333
  │ ├── _layout.tsx # Layout du module
286
334
  │ ├── _error.tsx # Erreur du module
@@ -307,18 +355,29 @@ npx @jsarc/initiator
307
355
  ### 2. Ajout d'un nouveau module
308
356
  ```bash
309
357
  # Créer la structure du module
310
- mkdir -p src/modules/admin/{locales,pages,components}
358
+ mkdir -p src/modules/admin/{locales,providers,pages,components}
311
359
 
312
360
  # Ajouter les fichiers de base
313
361
  touch src/modules/admin/config.json
314
362
  touch src/modules/admin/pages/index.tsx
315
363
  touch src/modules/admin/locales/fr.json
364
+ touch src/modules/admin/providers/ReduxProvider.tsx
316
365
 
317
366
  # Régénérer les fichiers
318
367
  npx @jsarc/initiator
319
368
  ```
320
369
 
321
- ### 3. Développement avec hot-reload
370
+ ### 3. Ajout d'un provider global
371
+ ```bash
372
+ # Créer un provider global
373
+ mkdir -p src/providers
374
+ touch src/providers/ThemeProvider.tsx
375
+
376
+ # Régénérer le fichier auto-provider.tsx
377
+ npx @jsarc/initiator
378
+ ```
379
+
380
+ ### 4. Développement avec hot-reload
322
381
  ```bash
323
382
  # Démarrer le serveur de développement
324
383
  npm run dev
@@ -345,6 +404,24 @@ export default defineConfig({
345
404
  });
346
405
  ```
347
406
 
407
+ ### Utilisation du AppProvider généré
408
+ ```tsx
409
+ // main.tsx
410
+ import React from 'react';
411
+ import ReactDOM from 'react-dom/client';
412
+ import App from './App';
413
+ import { AppProvider } from './auto-provider';
414
+ import './index.css';
415
+
416
+ ReactDOM.createRoot(document.getElementById('root')!).render(
417
+ <React.StrictMode>
418
+ <AppProvider>
419
+ <App />
420
+ </AppProvider>
421
+ </React.StrictMode>
422
+ );
423
+ ```
424
+
348
425
  ### Avec Next.js (Adaptation)
349
426
  ```javascript
350
427
  // next.config.js
@@ -403,6 +480,13 @@ module.exports = {
403
480
  npx @jsarc/initiator
404
481
  ```
405
482
 
483
+ 4. **Providers non détectés**
484
+ ```bash
485
+ # Vérifier que le fichier contient "Provider" dans le nom
486
+ # Ou utilise createContext / Context.Provider
487
+ mv src/my-context.tsx src/MyContextProvider.tsx
488
+ ```
489
+
406
490
  ### Logs de débogage
407
491
  ```bash
408
492
  # Activer les logs détaillés
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.0.0",
6
+ "version": "0.0.1-beta.0.1",
7
7
  "description": "INITIATOR est un plugin d'initialisation intelligent pour les applications React avec TypeScript/Javascript. Il génère automatiquement les fichiers de configuration, de routage et d'internationalisation basés sur la structure de votre projet.",
8
8
  "main": "index.ts",
9
9
  "keywords": [],
package/routing.ts ADDED
@@ -0,0 +1,621 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { Pajo } from '@jsarc/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 RouteGenerator {
31
+ private config: RouteConfig;
32
+ private routeFiles: RouteFile[] = [];
33
+ private modules: Set<string> = new Set();
34
+
35
+ constructor(
36
+ config: Partial<RouteConfig> = {},
37
+ dirname: string | undefined = __dirname,
38
+ ) {
39
+ this.config = {
40
+ srcDir: config.srcDir || Pajo.join(dirname, 'src') || '',
41
+ modulesDir: config.modulesDir || Pajo.join(dirname, 'src\\modules') || '',
42
+ pagesDir: config.pagesDir || Pajo.join(dirname, 'src\\pages') || '',
43
+ outputFile: config.outputFile || Pajo.join(dirname, 'src\\auto-routes.tsx') || '',
44
+ layoutFileName: config.layoutFileName || '_layout',
45
+ errorFileName: config.errorFileName || '_error',
46
+ notFoundFileName: config.notFoundFileName || '_404'
47
+ };
48
+ console.log(`--> RouteGenerator config:: `, this.config);
49
+ }
50
+
51
+ public async generate(): Promise<void> {
52
+ console.log('🔍 Scanning for route files...');
53
+
54
+ await this.findRouteFiles();
55
+
56
+ await this.generateAutoRoutesFile();
57
+
58
+ console.log(`✅ Generated ${this.config.outputFile} with ${this.routeFiles.length} routes`);
59
+ console.log(`📦 Found modules with routes: ${Array.from(this.modules).join(', ')}`);
60
+ }
61
+
62
+ private async findRouteFiles(): Promise<void> {
63
+ this.routeFiles = [];
64
+
65
+ await this.scanDirectoryForRoutes(this.config.pagesDir);
66
+
67
+ if (fs.existsSync(this.config.modulesDir)) {
68
+ const moduleDirs = fs.readdirSync(this.config.modulesDir, { withFileTypes: true })
69
+ .filter(dirent => dirent.isDirectory())
70
+ .map(dirent => dirent.name);
71
+
72
+ for (const moduleName of moduleDirs) {
73
+ const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
74
+ if (fs.existsSync(modulePagesDir)) {
75
+ this.modules.add(moduleName);
76
+ await this.scanDirectoryForRoutes(modulePagesDir, moduleName);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ private async scanDirectoryForRoutes(dir: string, moduleName?: string): Promise<void> {
83
+ try {
84
+ const items = fs.readdirSync(dir, { withFileTypes: true });
85
+
86
+ for (const item of items) {
87
+ const fullPath = path.join(dir, item.name);
88
+
89
+ if (item.name === 'node_modules' ||
90
+ item.name === 'dist' ||
91
+ item.name === 'build' ||
92
+ item.name.startsWith('.')) {
93
+ continue;
94
+ }
95
+
96
+ if (item.isDirectory()) {
97
+
98
+ await this.scanDirectoryForRoutes(fullPath, moduleName);
99
+ } else if (item.isFile()) {
100
+
101
+ const ext = path.extname(item.name).toLowerCase();
102
+ if (['.tsx', '.jsx'].includes(ext)) {
103
+ const fileName = path.basename(item.name, ext);
104
+
105
+ if (fileName === this.config.layoutFileName ||
106
+ fileName === this.config.errorFileName ||
107
+ fileName === this.config.notFoundFileName) {
108
+ continue;
109
+ }
110
+
111
+ await this.processRouteFile(fullPath, moduleName);
112
+ }
113
+ }
114
+ }
115
+ } catch (error) {
116
+ console.error(`Error scanning directory ${dir}:`, error);
117
+ }
118
+ }
119
+
120
+ private async processRouteFile(filePath: string, moduleName?: string): Promise<void> {
121
+ try {
122
+
123
+ let routePath = this.convertFilePathToRoutePath(filePath, moduleName);
124
+
125
+ const componentName = this.extractComponentName(filePath, moduleName);
126
+
127
+ const layoutComponent = this.findLayoutComponent(filePath, moduleName);
128
+ const errorComponent = this.findErrorComponent(filePath, moduleName);
129
+ const notFoundComponent = this.findNotFoundComponent(filePath, moduleName);
130
+
131
+ const routeFile: RouteFile = {
132
+ filePath,
133
+ relativePath: path.relative(this.config.srcDir, filePath),
134
+ routePath,
135
+ componentName,
136
+ layoutComponent,
137
+ errorComponent,
138
+ notFoundComponent,
139
+ moduleName
140
+ };
141
+
142
+ this.routeFiles.push(routeFile);
143
+ } catch (error) {
144
+ console.error(`Error processing route file ${filePath}:`, error);
145
+ }
146
+ }
147
+
148
+ private convertFilePathToRoutePath(filePath: string, moduleName?: string): string {
149
+ let routePath = filePath;
150
+
151
+ routePath = routePath.replace(this.config.srcDir, '');
152
+
153
+ if (moduleName) {
154
+
155
+ const modulePagesPrefix = `modules${path.sep}${moduleName}${path.sep}pages`;
156
+ if (routePath.includes(modulePagesPrefix)) {
157
+ routePath = routePath.substring(routePath.indexOf(modulePagesPrefix) + modulePagesPrefix.length);
158
+
159
+ routePath = `/${moduleName}${routePath}`;
160
+ }
161
+ } else {
162
+
163
+ const pagesPrefix = 'pages';
164
+ if (routePath.includes(pagesPrefix)) {
165
+ routePath = routePath.substring(routePath.indexOf(pagesPrefix) + pagesPrefix.length);
166
+ }
167
+ }
168
+
169
+ routePath = routePath.replace(/\.(tsx|jsx)$/, '');
170
+
171
+ routePath = routePath.replace(/\\/g, '/');
172
+
173
+ if (routePath.endsWith('/index')) {
174
+ routePath = routePath.replace(/\/index$/, '');
175
+ }
176
+
177
+ routePath = routePath.replace(/\[([^\]]+)\]/g, '{$1}');
178
+
179
+ if (!routePath.startsWith('/')) {
180
+ routePath = '/' + routePath;
181
+ }
182
+
183
+ if (routePath === '/' || routePath === '//') {
184
+ return '/';
185
+ }
186
+
187
+ return routePath;
188
+ }
189
+
190
+ private extractComponentName(filePath: string, moduleName?: string): string {
191
+ const fileName = path.basename(filePath, path.extname(filePath));
192
+ const dirName = path.dirname(filePath);
193
+
194
+ const parts = dirName.split(path.sep);
195
+
196
+ let startIndex = 0;
197
+ if (moduleName) {
198
+
199
+ const modulePagesPath = path.sep + 'modules' + path.sep + moduleName + path.sep + 'pages';
200
+ startIndex = parts.findIndex((part, index, arr) => {
201
+ const pathSoFar = arr.slice(0, index + 1).join(path.sep);
202
+ return pathSoFar.endsWith(modulePagesPath);
203
+ }) + 1;
204
+ } else {
205
+
206
+ const pagesPath = path.sep + 'pages';
207
+ startIndex = parts.findIndex((part, index, arr) => {
208
+ const pathSoFar = arr.slice(0, index + 1).join(path.sep);
209
+ return pathSoFar.endsWith(pagesPath);
210
+ }) + 1;
211
+ }
212
+
213
+ if (startIndex < 0) startIndex = 0;
214
+
215
+ const relevantParts = parts.slice(startIndex);
216
+
217
+ let componentName = moduleName ? this.toPascalCase(moduleName) : '';
218
+
219
+ relevantParts.forEach(part => {
220
+ if (part && part !== 'pages') {
221
+ componentName += this.toPascalCase(part);
222
+ }
223
+ });
224
+
225
+ let fileNamePart = fileName;
226
+ if (fileName.startsWith('{') && fileName.endsWith('}')) {
227
+
228
+ const paramName = fileName.substring(1, fileName.length - 1);
229
+ fileNamePart = this.toPascalCase(paramName) + 'Params';
230
+ } else {
231
+ fileNamePart = this.toPascalCase(fileName);
232
+ }
233
+
234
+ if (!fileNamePart.endsWith('Page')) {
235
+ fileNamePart += 'Page';
236
+ }
237
+
238
+ const reservedNames = ['Layout', 'ErrorBoundary', 'NotFound'];
239
+ if (reservedNames.includes(fileNamePart)) {
240
+ fileNamePart += 'Component';
241
+ }
242
+
243
+ return componentName + fileNamePart;
244
+ }
245
+
246
+ private toPascalCase(str: string): string {
247
+ if (!str) return '';
248
+ return str
249
+ .split(/[-_]/)
250
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
251
+ .join('');
252
+ }
253
+
254
+ private findLayoutComponent(filePath: string, moduleName?: string): string | undefined {
255
+ const dir = path.dirname(filePath);
256
+ let currentDir = dir;
257
+
258
+ while (currentDir !== this.config.srcDir && currentDir !== path.dirname(this.config.srcDir)) {
259
+ const layoutPath = path.join(currentDir, `${this.config.layoutFileName}.tsx`);
260
+
261
+ if (fs.existsSync(layoutPath)) {
262
+ return this.generateComponentName(layoutPath, moduleName, 'Layout');
263
+ }
264
+
265
+ if (moduleName) {
266
+ const modulePath = path.join(this.config.modulesDir, moduleName);
267
+ if (currentDir === modulePath) {
268
+ break;
269
+ }
270
+ }
271
+
272
+ currentDir = path.dirname(currentDir);
273
+ }
274
+
275
+ const globalLayoutPath = path.join(this.config.pagesDir, `${this.config.layoutFileName}.tsx`);
276
+ if (fs.existsSync(globalLayoutPath)) {
277
+ return 'Layout';
278
+ }
279
+
280
+ return undefined;
281
+ }
282
+
283
+ private findErrorComponent(filePath: string, moduleName?: string): string | undefined {
284
+ const dir = path.dirname(filePath);
285
+ let currentDir = dir;
286
+
287
+ while (currentDir !== this.config.srcDir && currentDir !== path.dirname(this.config.srcDir)) {
288
+ const errorPath = path.join(currentDir, `${this.config.errorFileName}.tsx`);
289
+
290
+ if (fs.existsSync(errorPath)) {
291
+ return this.generateComponentName(errorPath, moduleName, 'ErrorBoundary');
292
+ }
293
+
294
+ if (moduleName) {
295
+ const modulePath = path.join(this.config.modulesDir, moduleName);
296
+ if (currentDir === modulePath) {
297
+ break;
298
+ }
299
+ }
300
+
301
+ currentDir = path.dirname(currentDir);
302
+ }
303
+
304
+ const globalErrorPath = path.join(this.config.pagesDir, `${this.config.errorFileName}.tsx`);
305
+ if (fs.existsSync(globalErrorPath)) {
306
+ return 'ErrorBoundary';
307
+ }
308
+
309
+ return undefined;
310
+ }
311
+
312
+ private findNotFoundComponent(filePath: string, moduleName?: string): string | undefined {
313
+ if (!moduleName) return undefined;
314
+
315
+ const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
316
+ const notFoundPath = path.join(modulePagesDir, `${this.config.notFoundFileName}.tsx`);
317
+
318
+ if (fs.existsSync(notFoundPath)) {
319
+ return this.generateComponentName(notFoundPath, moduleName, 'NotFound');
320
+ }
321
+
322
+ return undefined;
323
+ }
324
+
325
+ private generateComponentName(filePath: string, moduleName: string | undefined, suffix: string): string {
326
+ if (!moduleName) {
327
+ return suffix;
328
+ }
329
+
330
+ const fileName = path.basename(filePath, path.extname(filePath));
331
+ const dirName = path.dirname(filePath);
332
+
333
+ const modulePagesPath = path.join(this.config.modulesDir, moduleName, 'pages');
334
+ const relativePath = path.relative(modulePagesPath, dirName);
335
+
336
+ let componentName = this.toPascalCase(moduleName);
337
+
338
+ if (relativePath && relativePath !== '.') {
339
+ const dirParts = relativePath.split(path.sep).filter(part => part && part !== '.');
340
+ dirParts.forEach(part => {
341
+ componentName += this.toPascalCase(part);
342
+ });
343
+ }
344
+
345
+ if (fileName !== this.config.layoutFileName &&
346
+ fileName !== this.config.errorFileName &&
347
+ fileName !== this.config.notFoundFileName) {
348
+ componentName += this.toPascalCase(fileName);
349
+ }
350
+
351
+ return componentName + suffix;
352
+ }
353
+
354
+ private async generateAutoRoutesFile(): Promise<void> {
355
+ const outputDir = path.dirname(this.config.outputFile);
356
+
357
+ if (!fs.existsSync(outputDir)) {
358
+ fs.mkdirSync(outputDir, { recursive: true });
359
+ }
360
+
361
+ const content = this.generateFileContent();
362
+ fs.writeFileSync(this.config.outputFile, content, 'utf-8');
363
+ }
364
+
365
+ private getRelativeImportPath(targetPath: string): string {
366
+ const outputDir = path.dirname(this.config.outputFile);
367
+ const relative = path.relative(outputDir, targetPath);
368
+
369
+ let importPath = relative.replace(/\\/g, '/');
370
+
371
+ if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
372
+ importPath = './' + importPath;
373
+ }
374
+
375
+ importPath = importPath.replace(/\.(tsx|jsx)$/, '');
376
+
377
+ return importPath;
378
+ }
379
+
380
+ private generateFileContent(): string {
381
+
382
+ const components = new Set<string>();
383
+ const imports: string[] = [
384
+ `import { lazy } from 'react';`,
385
+ `import type { RouteObject } from 'react-router-dom';`,
386
+ ``
387
+ ];
388
+
389
+ const globalLayoutPath = path.join(this.config.pagesDir, `${this.config.layoutFileName}.tsx`);
390
+ const globalErrorPath = path.join(this.config.pagesDir, `${this.config.errorFileName}.tsx`);
391
+ const global404Path = path.join(this.config.pagesDir, `${this.config.notFoundFileName}.tsx`);
392
+
393
+ if (fs.existsSync(globalLayoutPath)) {
394
+ const importPath = this.getRelativeImportPath(globalLayoutPath);
395
+ imports.push(`const Layout = lazy(() => import('${importPath}'));`);
396
+ components.add('Layout');
397
+ }
398
+
399
+ if (fs.existsSync(globalErrorPath)) {
400
+ const importPath = this.getRelativeImportPath(globalErrorPath);
401
+ imports.push(`const ErrorBoundary = lazy(() => import('${importPath}'));`);
402
+ components.add('ErrorBoundary');
403
+ }
404
+
405
+ if (fs.existsSync(global404Path)) {
406
+ const importPath = this.getRelativeImportPath(global404Path);
407
+ imports.push(`const NotFound = lazy(() => import('${importPath}'));`);
408
+ components.add('NotFound');
409
+ }
410
+
411
+ imports.push(``);
412
+
413
+ const specialComponents = new Map<string, string>();
414
+
415
+ const findSpecialFiles = (baseDir: string, isModule: boolean = false, moduleName?: string) => {
416
+ if (!fs.existsSync(baseDir)) return;
417
+
418
+ const walkDir = (dir: string) => {
419
+ try {
420
+ const items = fs.readdirSync(dir, { withFileTypes: true });
421
+
422
+ for (const item of items) {
423
+ const fullPath = path.join(dir, item.name);
424
+
425
+ if (item.isDirectory()) {
426
+ walkDir(fullPath);
427
+ } else if (item.isFile()) {
428
+ const ext = path.extname(item.name).toLowerCase();
429
+ if (['.tsx', '.jsx'].includes(ext)) {
430
+ const fileName = path.basename(item.name, ext);
431
+
432
+ if (fileName === this.config.layoutFileName ||
433
+ fileName === this.config.errorFileName ||
434
+ fileName === this.config.notFoundFileName) {
435
+
436
+ const componentName = this.generateComponentName(fullPath, moduleName,
437
+ fileName === this.config.layoutFileName ? 'Layout' :
438
+ fileName === this.config.errorFileName ? 'ErrorBoundary' : 'NotFound');
439
+
440
+ if (!components.has(componentName)) {
441
+ specialComponents.set(fullPath, componentName);
442
+ components.add(componentName);
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ } catch (error) {
449
+ console.error(`Error walking directory ${dir}:`, error);
450
+ }
451
+ };
452
+
453
+ walkDir(baseDir);
454
+ };
455
+
456
+ findSpecialFiles(this.config.pagesDir, false);
457
+
458
+ this.modules.forEach(moduleName => {
459
+ const modulePagesDir = path.join(this.config.modulesDir, moduleName, 'pages');
460
+ if (fs.existsSync(modulePagesDir)) {
461
+ findSpecialFiles(modulePagesDir, true, moduleName);
462
+ }
463
+ });
464
+
465
+ specialComponents.forEach((componentName, filePath) => {
466
+ const importPath = this.getRelativeImportPath(filePath);
467
+ imports.push(`const ${componentName} = lazy(() => import('${importPath}'));`);
468
+ });
469
+
470
+ if (specialComponents.size > 0) {
471
+ imports.push(``);
472
+ }
473
+
474
+ this.routeFiles.forEach(route => {
475
+
476
+ const importPath = this.getRelativeImportPath(route.filePath);
477
+ imports.push(`const ${route.componentName} = lazy(() => import('${importPath}'));`);
478
+ components.add(route.componentName);
479
+ });
480
+
481
+ imports.push(``);
482
+
483
+ const routes: string[] = ['export const routes: RouteObject[] = ['];
484
+
485
+ const sortedRoutes = this.routeFiles.sort((a, b) => {
486
+
487
+ if (a.routePath === '/') return -1;
488
+ if (b.routePath === '/') return 1;
489
+
490
+ if (!a.moduleName && b.moduleName) return -1;
491
+ if (a.moduleName && !b.moduleName) return 1;
492
+
493
+ if (a.moduleName === b.moduleName) {
494
+ return a.routePath.localeCompare(b.routePath);
495
+ }
496
+
497
+ return 0;
498
+ });
499
+
500
+ let lastModuleName: string | undefined = undefined;
501
+
502
+ sortedRoutes.forEach((route, index) => {
503
+
504
+ if (route.moduleName !== lastModuleName) {
505
+ if (lastModuleName !== undefined) {
506
+ routes.push(``);
507
+ }
508
+ if (route.moduleName) {
509
+ routes.push(` // ${route.moduleName} module routes`);
510
+ }
511
+ lastModuleName = route.moduleName;
512
+ }
513
+
514
+ const routeLines: string[] = [' {'];
515
+ routeLines.push(` path: '${route.routePath.replace(/\{([^}]+)\}/g, ':$1')}',`);
516
+
517
+ let layoutComponent = route.layoutComponent;
518
+ if (!layoutComponent && route.moduleName) {
519
+
520
+ const moduleLayoutPath = path.join(this.config.modulesDir, route.moduleName, 'pages', `${this.config.layoutFileName}.tsx`);
521
+ if (fs.existsSync(moduleLayoutPath)) {
522
+ layoutComponent = this.generateComponentName(moduleLayoutPath, route.moduleName, 'Layout');
523
+ }
524
+ }
525
+
526
+ if (layoutComponent) {
527
+ routeLines.push(` element: <${layoutComponent}><${route.componentName} /></${layoutComponent}>,`);
528
+ } else {
529
+ routeLines.push(` element: <${route.componentName} />,`);
530
+ }
531
+
532
+ let errorComponent = route.errorComponent;
533
+ if (!errorComponent && route.moduleName) {
534
+
535
+ const moduleErrorPath = path.join(this.config.modulesDir, route.moduleName, 'pages', `${this.config.errorFileName}.tsx`);
536
+ if (fs.existsSync(moduleErrorPath)) {
537
+ errorComponent = this.generateComponentName(moduleErrorPath, route.moduleName, 'ErrorBoundary');
538
+ }
539
+ }
540
+
541
+ if (errorComponent) {
542
+ routeLines.push(` errorElement: <${errorComponent} />`);
543
+ }
544
+
545
+ if (routeLines[routeLines.length - 1].endsWith(',')) {
546
+ routeLines[routeLines.length - 1] = routeLines[routeLines.length - 1].slice(0, -1);
547
+ }
548
+
549
+ routeLines.push(' }');
550
+
551
+ if (index < sortedRoutes.length - 1) {
552
+ routeLines[routeLines.length - 1] += ',';
553
+ }
554
+
555
+ routes.push(routeLines.join('\n'));
556
+ });
557
+
558
+ const modulesWith404 = new Set<string>();
559
+ specialComponents.forEach((componentName, filePath) => {
560
+ const fileName = path.basename(filePath, path.extname(filePath));
561
+ if (fileName === this.config.notFoundFileName) {
562
+
563
+ const pathParts = filePath.split(path.sep);
564
+ const modulesIndex = pathParts.indexOf('modules');
565
+ if (modulesIndex !== -1 && modulesIndex + 1 < pathParts.length) {
566
+ const moduleName = pathParts[modulesIndex + 1];
567
+ if (!modulesWith404.has(moduleName)) {
568
+ modulesWith404.add(moduleName);
569
+
570
+ if (routes[routes.length - 1].endsWith(',')) {
571
+ routes[routes.length - 1] = routes[routes.length - 1].slice(0, -1);
572
+ }
573
+ routes.push(`,`);
574
+ routes.push(` {`);
575
+ routes.push(` path: '/${moduleName}/*',`.replace(/\{([^}]+)\}/g, ':$1'));
576
+ routes.push(` element: <${componentName} />`);
577
+ routes.push(` }`);
578
+ }
579
+ }
580
+ }
581
+ });
582
+
583
+ if (components.has('NotFound')) {
584
+ if (routes[routes.length - 1].endsWith(',')) {
585
+ routes[routes.length - 1] = routes[routes.length - 1].slice(0, -1);
586
+ }
587
+ routes.push(`,`);
588
+ routes.push(` {`);
589
+ routes.push(` path: '*',`.replace(/\{([^}]+)\}/g, ':$1'));
590
+ routes.push(` element: <NotFound />`);
591
+ routes.push(` }`);
592
+ }
593
+
594
+ routes.push('];');
595
+ routes.push('');
596
+ routes.push('export default routes;');
597
+
598
+ return [
599
+ ...imports,
600
+ '',
601
+ ...routes
602
+ ].join('\n');
603
+ }
604
+ }
605
+
606
+ async function RouteInitiator(
607
+ dirname: string | undefined = __dirname
608
+ ) {
609
+ const config: Partial<RouteConfig> = {};
610
+
611
+ const generator = new RouteGenerator(config, dirname);
612
+
613
+ try {
614
+ await generator.generate();
615
+ console.log('🎉 Route generation completed successfully!');
616
+ } catch (err) {
617
+ console.error('❌ Failed to generate routes: ', err);
618
+ }
619
+ }
620
+
621
+ export default RouteInitiator;