@lastbrain/app 0.1.25 → 0.1.26

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 (54) hide show
  1. package/dist/app-shell/(public)/page.d.ts.map +1 -1
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -0
  5. package/dist/layouts/AdminLayoutWithSidebar.d.ts +8 -0
  6. package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
  7. package/dist/layouts/AdminLayoutWithSidebar.js +9 -0
  8. package/dist/layouts/AuthLayoutWithSidebar.d.ts +8 -0
  9. package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
  10. package/dist/layouts/AuthLayoutWithSidebar.js +9 -0
  11. package/dist/scripts/db-init.js +1 -1
  12. package/dist/scripts/dev-sync.js +21 -10
  13. package/dist/scripts/init-app.d.ts.map +1 -1
  14. package/dist/scripts/init-app.js +114 -12
  15. package/dist/scripts/module-add.d.ts.map +1 -1
  16. package/dist/scripts/module-add.js +19 -6
  17. package/dist/scripts/module-build.d.ts.map +1 -1
  18. package/dist/scripts/module-build.js +285 -30
  19. package/dist/scripts/module-create.d.ts.map +1 -1
  20. package/dist/scripts/module-create.js +25 -15
  21. package/dist/scripts/module-remove.d.ts.map +1 -1
  22. package/dist/scripts/module-remove.js +24 -11
  23. package/dist/scripts/script-runner.js +1 -1
  24. package/dist/styles.css +1 -1
  25. package/dist/templates/DefaultDoc.js +1 -7
  26. package/dist/templates/components/AppAside.d.ts +6 -0
  27. package/dist/templates/components/AppAside.d.ts.map +1 -0
  28. package/dist/templates/components/AppAside.js +9 -0
  29. package/dist/templates/layouts/admin-layout.d.ts +4 -0
  30. package/dist/templates/layouts/admin-layout.d.ts.map +1 -0
  31. package/dist/templates/layouts/admin-layout.js +6 -0
  32. package/dist/templates/layouts/auth-layout.d.ts +4 -0
  33. package/dist/templates/layouts/auth-layout.d.ts.map +1 -0
  34. package/dist/templates/layouts/auth-layout.js +6 -0
  35. package/package.json +2 -1
  36. package/src/app-shell/(public)/page.tsx +6 -2
  37. package/src/auth/useAuthSession.ts +1 -1
  38. package/src/cli.ts +1 -1
  39. package/src/index.ts +6 -0
  40. package/src/layouts/AdminLayoutWithSidebar.tsx +35 -0
  41. package/src/layouts/AppProviders.tsx +1 -1
  42. package/src/layouts/AuthLayoutWithSidebar.tsx +35 -0
  43. package/src/scripts/db-init.ts +12 -7
  44. package/src/scripts/db-migrations-sync.ts +3 -3
  45. package/src/scripts/dev-sync.ts +49 -18
  46. package/src/scripts/init-app.ts +235 -65
  47. package/src/scripts/module-add.ts +50 -22
  48. package/src/scripts/module-build.ts +393 -88
  49. package/src/scripts/module-create.ts +85 -49
  50. package/src/scripts/module-remove.ts +116 -57
  51. package/src/scripts/readme-build.ts +2 -2
  52. package/src/scripts/script-runner.ts +3 -3
  53. package/src/templates/AuthGuidePage.tsx +1 -1
  54. package/src/templates/DefaultDoc.tsx +7 -7
@@ -1,12 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { createRequire } from "node:module";
5
4
 
6
5
  import type {
7
6
  ModuleApiConfig,
8
7
  ModuleBuildConfig,
9
- ModuleMenuConfig,
10
8
  ModuleMenuItemConfig,
11
9
  ModulePageConfig,
12
10
  ModuleSection,
@@ -23,11 +21,11 @@ const projectRequire = createRequire(path.join(projectRoot, "package.json"));
23
21
  async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
24
22
  const moduleConfigs: ModuleBuildConfig[] = [];
25
23
  const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
26
-
24
+
27
25
  if (!fs.existsSync(modulesJsonPath)) {
28
26
  return moduleConfigs;
29
27
  }
30
-
28
+
31
29
  try {
32
30
  const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
33
31
  const modules = modulesData.modules || [];
@@ -37,9 +35,9 @@ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
37
35
  if (module.active === false) {
38
36
  continue;
39
37
  }
40
-
38
+
41
39
  const packageName = module.package;
42
-
40
+
43
41
  try {
44
42
  const moduleSuffix = packageName.replace("@lastbrain/module-", "");
45
43
  const possibleConfigNames = [
@@ -51,17 +49,19 @@ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
51
49
  for (const configName of possibleConfigNames) {
52
50
  try {
53
51
  // Résoudre le chemin du module depuis l'application
54
- const modulePath = projectRequire.resolve(`${packageName}/${configName}`);
52
+ const modulePath = projectRequire.resolve(
53
+ `${packageName}/${configName}`,
54
+ );
55
55
  // Convertir en URL file:// pour l'import dynamique
56
56
  const moduleUrl = `file://${modulePath}`;
57
57
  const moduleImport = await import(moduleUrl);
58
-
58
+
59
59
  if (moduleImport.default) {
60
60
  moduleConfigs.push(moduleImport.default);
61
61
  loaded = true;
62
62
  break;
63
63
  }
64
- } catch (err) {
64
+ } catch {
65
65
  // Essayer le nom suivant
66
66
  }
67
67
  }
@@ -76,7 +76,7 @@ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
76
76
  } catch (error) {
77
77
  console.error("❌ Error loading modules.json:", error);
78
78
  }
79
-
79
+
80
80
  return moduleConfigs;
81
81
  }
82
82
 
@@ -151,12 +151,34 @@ function toPascalCase(value: string) {
151
151
  }
152
152
 
153
153
  function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
154
- const segments = page.path.replace(/^\/+/, "").split("/").filter(Boolean);
155
- const sectionPath = sectionDirectoryMap[page.section] ?? ["(public)"];
154
+ // Extraire le préfixe du module (ex: @lastbrain/module-auth -> auth)
155
+ const modulePrefix = moduleConfig.moduleName
156
+ .replace(/^@lastbrain\/module-/, '')
157
+ .toLowerCase();
156
158
 
159
+ console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
160
+
161
+ // Ajouter le préfixe du module au path pour les sections admin et auth,
162
+ // MAIS seulement quand la section ne correspond PAS au module lui-même
163
+ let effectivePath = page.path;
164
+ if (page.section === 'admin' || (page.section === 'auth' && modulePrefix !== 'auth')) {
165
+ // Éviter les doublons si le préfixe est déjà présent
166
+ if (!page.path.startsWith(`/${modulePrefix}/`)) {
167
+ effectivePath = `/${modulePrefix}${page.path}`;
168
+ console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
169
+ } else {
170
+ console.log(`✅ Module prefix already present: ${page.path}`);
171
+ }
172
+ } else if (page.section === 'auth' && modulePrefix === 'auth') {
173
+ console.log(`🏠 Auth module in auth section, no prefix needed: ${page.path}`);
174
+ }
175
+
176
+ const segments = effectivePath.replace(/^\/+/, "").split("/").filter(Boolean);
177
+ const sectionPath = sectionDirectoryMap[page.section] ?? ["(public)"];
178
+
157
179
  // Générer le layout de section si nécessaire
158
180
  ensureSectionLayout(sectionPath);
159
-
181
+
160
182
  const routeDir = path.join(appDirectory, ...sectionPath, ...segments);
161
183
  const filePath = path.join(routeDir, "page.tsx");
162
184
  ensureDirectory(routeDir);
@@ -167,17 +189,23 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
167
189
  const wrapperName = `${page.componentExport}${wrapperSuffix}Route`;
168
190
 
169
191
  // Vérifier si la route a des paramètres dynamiques (segments avec [])
170
- const hasDynamicParams = segments.some(seg => seg.startsWith('[') && seg.endsWith(']'));
192
+ const hasDynamicParams = segments.some(
193
+ (seg) => seg.startsWith("[") && seg.endsWith("]"),
194
+ );
171
195
 
172
196
  // Pour les pages publiques (signin, signup, etc.), utiliser dynamic import sans SSR
173
197
  // pour éviter les erreurs d'hydratation avec les IDs HeroUI/React Aria
174
- const isPublicAuthPage = page.section === "public" &&
175
- (page.path.includes("signin") || page.path.includes("signup") || page.path.includes("reset-password"));
198
+ const isPublicAuthPage =
199
+ page.section === "public" &&
200
+ (page.path.includes("signin") ||
201
+ page.path.includes("signup") ||
202
+ page.path.includes("reset-password"));
176
203
 
177
204
  let content: string;
178
-
205
+
179
206
  if (isPublicAuthPage) {
180
- content = `"use client";
207
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
208
+ "use client";
181
209
 
182
210
  import dynamic from "next/dynamic";
183
211
 
@@ -186,23 +214,25 @@ const ${page.componentExport} = dynamic(
186
214
  { ssr: false }
187
215
  );
188
216
 
189
- export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'} {
190
- return <${page.componentExport} ${hasDynamicParams ? '{...props}' : ''} />;
217
+ export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"} {
218
+ return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
191
219
  }
192
220
  `;
193
221
  } else {
194
- content = `import { ${page.componentExport} } from "${moduleConfig.moduleName}";
222
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
223
+ import { ${page.componentExport} } from "${moduleConfig.moduleName}";
195
224
 
196
- export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'} {
197
- return <${page.componentExport} ${hasDynamicParams ? '{...props}' : ''} />;
225
+ export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"} {
226
+ return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
198
227
  }
199
228
  `;
200
229
  }
201
230
 
202
231
  fs.writeFileSync(filePath, content);
203
- console.log(`⭐ Generated page: ${filePath}`); const entry: MenuEntry = {
232
+ console.log(`⭐ Generated page: ${filePath}`);
233
+ const entry: MenuEntry = {
204
234
  label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
205
- path: page.path,
235
+ path: effectivePath,
206
236
  module: moduleConfig.moduleName,
207
237
  section: page.section,
208
238
  };
@@ -214,22 +244,6 @@ export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'
214
244
  }
215
245
  }
216
246
 
217
- function buildApi(moduleConfig: ModuleBuildConfig, api: ModuleApiConfig) {
218
- const segments = api.path.replace(/^\/+/, "").split("/").filter(Boolean);
219
- const sanitizedSegments =
220
- segments[0] === "api" ? segments.slice(1) : segments;
221
- const routeDir = path.join(appDirectory, "api", ...sanitizedSegments);
222
- const filePath = path.join(routeDir, "route.ts");
223
- ensureDirectory(routeDir);
224
-
225
- const handler = `${moduleConfig.moduleName}/${api.entryPoint}`;
226
- const content = `export { ${api.handlerExport} } from "${handler}";
227
- `;
228
-
229
- fs.writeFileSync(filePath, content);
230
- console.log(`🔌 Generated API route: ${filePath}`);
231
- }
232
-
233
247
  function dumpNavigation() {
234
248
  const navPath = path.join(appDirectory, "navigation.generated.ts");
235
249
  const content = `export type MenuEntry = { label: string; path: string; module: string; section: string };
@@ -248,15 +262,15 @@ export const userMenu = ${JSON.stringify(userMenu, null, 2)};
248
262
  function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
249
263
  const configDir = path.join(projectRoot, "config");
250
264
  ensureDirectory(configDir);
251
-
265
+
252
266
  const menuPath = path.join(configDir, "menu.ts");
253
-
267
+
254
268
  // Collecter les menus de tous les modules
255
269
  const publicMenus: ModuleMenuItemConfig[] = [];
256
270
  const authMenus: ModuleMenuItemConfig[] = [];
257
271
  const adminMenus: ModuleMenuItemConfig[] = [];
258
272
  const accountMenus: ModuleMenuItemConfig[] = [];
259
-
273
+
260
274
  moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
261
275
  if (moduleConfig.menu) {
262
276
  if (moduleConfig.menu.public) {
@@ -273,17 +287,17 @@ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
273
287
  }
274
288
  }
275
289
  });
276
-
290
+
277
291
  // Trier par ordre
278
292
  const sortByOrder = (a: ModuleMenuItemConfig, b: ModuleMenuItemConfig) => {
279
293
  return (a.order ?? 999) - (b.order ?? 999);
280
294
  };
281
-
295
+
282
296
  publicMenus.sort(sortByOrder);
283
297
  authMenus.sort(sortByOrder);
284
298
  adminMenus.sort(sortByOrder);
285
299
  accountMenus.sort(sortByOrder);
286
-
300
+
287
301
  // Générer le contenu du fichier
288
302
  const content = `// Auto-generated menu configuration
289
303
  // Generated from module build configs
@@ -310,32 +324,38 @@ export const menuConfig: MenuConfig = {
310
324
  account: ${JSON.stringify(accountMenus, null, 2)},
311
325
  };
312
326
  `;
313
-
327
+
314
328
  fs.writeFileSync(menuPath, content);
315
329
  console.log(`🍔 Generated menu configuration: ${menuPath}`);
316
330
  }
317
331
 
318
332
  function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
319
- const supabaseMigrationsDir = path.join(projectRoot, "supabase", "migrations");
320
-
333
+ const supabaseMigrationsDir = path.join(
334
+ projectRoot,
335
+ "supabase",
336
+ "migrations",
337
+ );
338
+
321
339
  // S'assurer que le dossier migrations existe
322
340
  if (!fs.existsSync(supabaseMigrationsDir)) {
323
341
  ensureDirectory(supabaseMigrationsDir);
324
342
  }
325
343
 
326
344
  const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
327
-
345
+
328
346
  // Charger modules.json
329
- let modulesConfig: { modules: Array<{ package: string; migrations?: string[] }> } = { modules: [] };
347
+ let modulesConfig: {
348
+ modules: Array<{ package: string; migrations?: string[] }>;
349
+ } = { modules: [] };
330
350
  if (fs.existsSync(modulesJsonPath)) {
331
351
  modulesConfig = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
332
352
  }
333
-
353
+
334
354
  moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
335
355
  try {
336
356
  const moduleName = moduleConfig.moduleName;
337
357
  const copiedMigrations: string[] = [];
338
-
358
+
339
359
  // Essayer plusieurs chemins possibles pour trouver le module
340
360
  const possiblePaths = [
341
361
  // Dans node_modules local de l'app
@@ -343,32 +363,42 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
343
363
  // Dans node_modules à la racine du workspace
344
364
  path.join(projectRoot, "..", "..", "node_modules", moduleName),
345
365
  // Directement dans packages/ (pour monorepo)
346
- path.join(projectRoot, "..", "..", "packages", moduleName.replace("@lastbrain/", "")),
366
+ path.join(
367
+ projectRoot,
368
+ "..",
369
+ "..",
370
+ "packages",
371
+ moduleName.replace("@lastbrain/", ""),
372
+ ),
347
373
  ];
348
-
374
+
349
375
  let moduleBasePath: string | null = null;
350
-
376
+
351
377
  for (const possiblePath of possiblePaths) {
352
378
  if (fs.existsSync(possiblePath)) {
353
379
  moduleBasePath = possiblePath;
354
380
  break;
355
381
  }
356
382
  }
357
-
383
+
358
384
  if (!moduleBasePath) {
359
385
  return;
360
386
  }
361
-
362
- const moduleMigrationsDir = path.join(moduleBasePath, "supabase", "migrations");
363
-
387
+
388
+ const moduleMigrationsDir = path.join(
389
+ moduleBasePath,
390
+ "supabase",
391
+ "migrations",
392
+ );
393
+
364
394
  if (fs.existsSync(moduleMigrationsDir)) {
365
395
  const migrationFiles = fs.readdirSync(moduleMigrationsDir);
366
-
396
+
367
397
  migrationFiles.forEach((file) => {
368
398
  if (file.endsWith(".sql")) {
369
399
  const sourcePath = path.join(moduleMigrationsDir, file);
370
400
  const destPath = path.join(supabaseMigrationsDir, file);
371
-
401
+
372
402
  // Copier seulement si le fichier n'existe pas déjà
373
403
  if (!fs.existsSync(destPath)) {
374
404
  fs.copyFileSync(sourcePath, destPath);
@@ -378,13 +408,13 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
378
408
  }
379
409
  });
380
410
  }
381
-
411
+
382
412
  // Mettre à jour modules.json avec les migrations
383
413
  if (copiedMigrations.length > 0) {
384
414
  const moduleIndex = modulesConfig.modules.findIndex(
385
- (m) => m.package === moduleName
415
+ (m) => m.package === moduleName,
386
416
  );
387
-
417
+
388
418
  if (moduleIndex >= 0) {
389
419
  modulesConfig.modules[moduleIndex].migrations = copiedMigrations;
390
420
  } else {
@@ -395,10 +425,13 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
395
425
  }
396
426
  }
397
427
  } catch (error) {
398
- console.warn(`⚠️ Could not copy migrations from ${moduleConfig.moduleName}:`, error);
428
+ console.warn(
429
+ `⚠️ Could not copy migrations from ${moduleConfig.moduleName}:`,
430
+ error,
431
+ );
399
432
  }
400
433
  });
401
-
434
+
402
435
  // Sauvegarder modules.json avec les migrations mises à jour
403
436
  ensureDirectory(path.dirname(modulesJsonPath));
404
437
  fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
@@ -407,13 +440,17 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
407
440
  function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
408
441
  const docsDir = path.join(appDirectory, "docs");
409
442
  ensureDirectory(docsDir);
410
-
443
+
411
444
  const docsPagePath = path.join(docsDir, "page.tsx");
412
-
445
+
413
446
  // Charger tous les modules depuis modules.json (actifs ET inactifs)
414
447
  const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
415
- let allModules: Array<{ package: string; active: boolean; migrations?: string[] }> = [];
416
-
448
+ let allModules: Array<{
449
+ package: string;
450
+ active: boolean;
451
+ migrations?: string[];
452
+ }> = [];
453
+
417
454
  if (fs.existsSync(modulesJsonPath)) {
418
455
  try {
419
456
  const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
@@ -422,25 +459,29 @@ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
422
459
  console.error("❌ Error reading modules.json:", error);
423
460
  }
424
461
  }
425
-
462
+
426
463
  // Générer les imports des composants Doc de chaque module
427
464
  const docImports: string[] = [];
428
465
  const moduleConfigurations: string[] = [];
429
-
466
+
430
467
  allModules.forEach((moduleEntry) => {
431
468
  const moduleName = moduleEntry.package;
432
469
  const moduleId = moduleName.replace("@lastbrain/module-", "");
433
470
  const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
434
-
471
+
435
472
  // Trouver la config du module pour obtenir la description
436
- const moduleConfig = moduleConfigs.find(mc => mc.moduleName === moduleName);
437
- const description = moduleConfig ? getModuleDescription(moduleConfig) : "Module non configuré";
438
-
473
+ const moduleConfig = moduleConfigs.find(
474
+ (mc) => mc.moduleName === moduleName,
475
+ );
476
+ const description = moduleConfig
477
+ ? getModuleDescription(moduleConfig)
478
+ : "Module non configuré";
479
+
439
480
  // Importer le composant Doc seulement pour les modules actifs
440
481
  if (moduleEntry.active) {
441
482
  docImports.push(`import { ${docComponentName} } from "${moduleName}";`);
442
483
  }
443
-
484
+
444
485
  const config = {
445
486
  id: moduleId,
446
487
  name: `Module ${moduleId.charAt(0).toUpperCase() + moduleId.slice(1)}`,
@@ -448,34 +489,68 @@ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
448
489
  component: docComponentName,
449
490
  active: moduleEntry.active,
450
491
  };
451
-
492
+
452
493
  moduleConfigurations.push(` {
453
494
  id: "${config.id}",
454
495
  name: "${config.name}",
455
496
  description: "${config.description}",
456
- ${config.active ? `content: <${config.component} />,` : 'content: null,'}
497
+ ${config.active ? `content: <${config.component} />,` : "content: null,"}
457
498
  available: ${config.active},
458
499
  }`);
459
500
  });
460
-
501
+
461
502
  const docsContent = `// Auto-generated docs page with module documentation
462
503
  import React from "react";
463
504
  import { DocPage } from "@lastbrain/app";
464
- ${docImports.join('\n')}
505
+ ${docImports.join("\n")}
465
506
 
466
507
  export default function DocsPage() {
467
508
  const modules = [
468
- ${moduleConfigurations.join(',\n')}
509
+ ${moduleConfigurations.join(",\n")}
469
510
  ];
470
511
 
471
512
  return <DocPage modules={modules} />;
472
513
  }
473
514
  `;
474
-
515
+
475
516
  fs.writeFileSync(docsPagePath, docsContent);
476
517
  console.log(`📚 Generated docs page: ${docsPagePath}`);
477
518
  }
478
519
 
520
+ function buildGroupedApi(apis: Array<{ moduleConfig: ModuleBuildConfig; api: ModuleApiConfig }>, routePath: string) {
521
+ const segments = routePath.replace(/^\/+/, "").split("/").filter(Boolean);
522
+ const sanitizedSegments = segments[0] === "api" ? segments.slice(1) : segments;
523
+ const routeDir = path.join(appDirectory, "api", ...sanitizedSegments);
524
+ const filePath = path.join(routeDir, "route.ts");
525
+ ensureDirectory(routeDir);
526
+
527
+ // Grouper par module/entryPoint pour créer les exports
528
+ const exportsBySource = new Map<string, string[]>();
529
+
530
+ apis.forEach(({ moduleConfig, api }) => {
531
+ const handler = `${moduleConfig.moduleName}/${api.entryPoint}`;
532
+ if (!exportsBySource.has(handler)) {
533
+ exportsBySource.set(handler, []);
534
+ }
535
+ exportsBySource.get(handler)!.push(api.handlerExport);
536
+ });
537
+
538
+ // Générer les exports - un export statement par source
539
+ const exportStatements: string[] = [];
540
+ exportsBySource.forEach((exports, source) => {
541
+ exportStatements.push(`export { ${exports.join(", ")} } from "${source}";`);
542
+ });
543
+
544
+ const content = exportStatements.join("\n") + "\n";
545
+
546
+ // Ajouter le marqueur de génération au début
547
+ const contentWithMarker = `// GENERATED BY LASTBRAIN MODULE BUILD
548
+ ${content}`;
549
+
550
+ fs.writeFileSync(filePath, contentWithMarker);
551
+ console.log(`🔌 Generated API route: ${filePath}`);
552
+ }
553
+
479
554
  function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
480
555
  // Essayer de déduire la description depuis les pages ou retourner une description par défaut
481
556
  if (moduleConfig.pages.length > 0) {
@@ -484,19 +559,249 @@ function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
484
559
  return "Module documentation";
485
560
  }
486
561
 
562
+ function cleanGeneratedFiles() {
563
+ const generatedComment = "// GENERATED BY LASTBRAIN MODULE BUILD";
564
+
565
+ // Fichiers de base à préserver (paths relatifs exacts)
566
+ const protectedFiles = new Set([
567
+ // API de base
568
+ "api/storage",
569
+ // Layouts de base
570
+ "layout.tsx",
571
+ "not-found.tsx",
572
+ "page.tsx", // Page racine seulement
573
+ "admin/page.tsx", // Page admin racine
574
+ "admin/layout.tsx", // Layout admin racine
575
+ "docs/page.tsx", // Page docs générée
576
+ // Middleware et autres fichiers core
577
+ "middleware.ts",
578
+ // Dossiers de lib et config
579
+ "lib",
580
+ "config"
581
+ ]);
582
+
583
+ // Fonction pour vérifier si un chemin est protégé
584
+ const isProtected = (filePath: string) => {
585
+ const relativePath = path.relative(appDirectory, filePath);
586
+
587
+ // Protection exacte pour certains fichiers
588
+ if (protectedFiles.has(relativePath)) {
589
+ return true;
590
+ }
591
+
592
+ // Protection par préfixe pour les dossiers
593
+ return Array.from(protectedFiles).some(protectedPath =>
594
+ (protectedPath.endsWith('/') || ['lib', 'config', 'api/storage'].includes(protectedPath)) &&
595
+ relativePath.startsWith(protectedPath)
596
+ );
597
+ };
598
+
599
+ // Fonction pour nettoyer récursivement un dossier
600
+ const cleanDirectory = (dirPath: string) => {
601
+ if (!fs.existsSync(dirPath)) return;
602
+
603
+ const items = fs.readdirSync(dirPath);
604
+
605
+ for (const item of items) {
606
+ const itemPath = path.join(dirPath, item);
607
+ const stat = fs.statSync(itemPath);
608
+
609
+ if (stat.isDirectory()) {
610
+ // Nettoyer récursivement le sous-dossier
611
+ cleanDirectory(itemPath);
612
+
613
+ // Supprimer le dossier s'il est vide et non protégé
614
+ try {
615
+ if (!isProtected(itemPath) && fs.readdirSync(itemPath).length === 0) {
616
+ fs.rmdirSync(itemPath);
617
+ console.log(`🗑️ Removed empty directory: ${itemPath}`);
618
+ }
619
+ } catch (e) {
620
+ // Ignorer les erreurs de suppression de dossiers
621
+ }
622
+ } else if (item.endsWith('.tsx') || item.endsWith('.ts')) {
623
+ // Vérifier si c'est un fichier généré
624
+ if (!isProtected(itemPath)) {
625
+ try {
626
+ const content = fs.readFileSync(itemPath, 'utf-8');
627
+ // Supprimer les fichiers générés ou les wrapper simples de modules
628
+ if (content.includes(generatedComment) ||
629
+ content.includes('from "@lastbrain/module-') ||
630
+ content.includes('export {') && content.includes('} from "@lastbrain/module-')) {
631
+ fs.unlinkSync(itemPath);
632
+ console.log(`🗑️ Cleaned generated file: ${itemPath}`);
633
+ }
634
+ } catch (e) {
635
+ // Ignorer les erreurs de lecture/suppression
636
+ }
637
+ } else {
638
+ console.log(`🔒 Protected file skipped: ${itemPath}`);
639
+ }
640
+ }
641
+ }
642
+ };
643
+
644
+ // Nettoyer les dossiers de sections
645
+ const sectionsToClean = ['(public)', 'auth', 'admin', 'api'];
646
+ sectionsToClean.forEach(section => {
647
+ const sectionPath = path.join(appDirectory, section);
648
+ if (fs.existsSync(sectionPath)) {
649
+ cleanDirectory(sectionPath);
650
+ }
651
+ });
652
+
653
+ // Nettoyer les fichiers générés à la racine
654
+ const rootFiles = ['navigation.generated.ts'];
655
+ rootFiles.forEach(file => {
656
+ const filePath = path.join(appDirectory, file);
657
+ if (fs.existsSync(filePath)) {
658
+ fs.unlinkSync(filePath);
659
+ console.log(`🗑️ Cleaned root file: ${filePath}`);
660
+ }
661
+ });
662
+
663
+ console.log("🧹 Cleanup completed");
664
+ }
665
+
666
+ function generateAppAside() {
667
+ const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
668
+
669
+ // Ne pas écraser si le fichier existe déjà
670
+ if (fs.existsSync(targetPath)) {
671
+ console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
672
+ return;
673
+ }
674
+
675
+ const templateContent = `"use client";
676
+
677
+ import { AppAside as UIAppAside } from "@lastbrain/app";
678
+ import { useAuthSession } from "@lastbrain/app";
679
+ import { menuConfig } from "../../config/menu";
680
+
681
+ interface AppAsideProps {
682
+ className?: string;
683
+ isVisible?: boolean;
684
+ }
685
+
686
+ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
687
+ const { isSuperAdmin } = useAuthSession();
688
+
689
+ return (
690
+ <UIAppAside
691
+ className={className}
692
+ menuConfig={menuConfig}
693
+ isSuperAdmin={isSuperAdmin}
694
+ isVisible={isVisible}
695
+ />
696
+ );
697
+ }`;
698
+
699
+ try {
700
+ ensureDirectory(path.dirname(targetPath));
701
+ fs.writeFileSync(targetPath, templateContent, "utf-8");
702
+ console.log(`✅ Generated AppAside component: ${targetPath}`);
703
+ } catch (error) {
704
+ console.error(`❌ Error generating AppAside component: ${error}`);
705
+ }
706
+ }
707
+
708
+ function generateLayouts() {
709
+ // Générer layout auth avec sidebar
710
+ const authLayoutPath = path.join(appDirectory, "auth", "layout.tsx");
711
+ if (!fs.existsSync(authLayoutPath)) {
712
+ const authLayoutContent = `import { AuthLayoutWithSidebar } from "@lastbrain/app";
713
+ import { menuConfig } from "../../config/menu";
714
+
715
+ export default function SectionLayout({
716
+ children,
717
+ }: {
718
+ children: React.ReactNode;
719
+ }) {
720
+ return (
721
+ <AuthLayoutWithSidebar menuConfig={menuConfig}>
722
+ {children}
723
+ </AuthLayoutWithSidebar>
724
+ );
725
+ }`;
726
+
727
+ try {
728
+ ensureDirectory(path.dirname(authLayoutPath));
729
+ fs.writeFileSync(authLayoutPath, authLayoutContent, "utf-8");
730
+ console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
731
+ } catch (error) {
732
+ console.error(`❌ Error generating auth layout: ${error}`);
733
+ }
734
+ } else {
735
+ console.log(`⏭️ Auth layout already exists, skipping: ${authLayoutPath}`);
736
+ }
737
+
738
+ // Générer layout admin avec sidebar
739
+ const adminLayoutPath = path.join(appDirectory, "admin", "layout.tsx");
740
+ if (!fs.existsSync(adminLayoutPath)) {
741
+ const adminLayoutContent = `import { AdminLayoutWithSidebar } from "@lastbrain/app";
742
+ import { menuConfig } from "../../config/menu";
743
+
744
+ export default function AdminLayout({
745
+ children,
746
+ }: {
747
+ children: React.ReactNode;
748
+ }) {
749
+ return (
750
+ <AdminLayoutWithSidebar menuConfig={menuConfig}>
751
+ {children}
752
+ </AdminLayoutWithSidebar>
753
+ );
754
+ }`;
755
+
756
+ try {
757
+ ensureDirectory(path.dirname(adminLayoutPath));
758
+ fs.writeFileSync(adminLayoutPath, adminLayoutContent, "utf-8");
759
+ console.log(`✅ Generated admin layout with sidebar: ${adminLayoutPath}`);
760
+ } catch (error) {
761
+ console.error(`❌ Error generating admin layout: ${error}`);
762
+ }
763
+ } else {
764
+ console.log(`⏭️ Admin layout already exists, skipping: ${adminLayoutPath}`);
765
+ }
766
+ }
767
+
487
768
  export async function runModuleBuild() {
488
769
  ensureDirectory(appDirectory);
489
770
 
771
+ // Nettoyer les fichiers générés précédemment
772
+ console.log("🧹 Cleaning previously generated files...");
773
+ cleanGeneratedFiles();
774
+
490
775
  const moduleConfigs = await loadModuleConfigs();
776
+ console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
777
+
778
+ // Générer les pages
491
779
  moduleConfigs.forEach((moduleConfig) => {
780
+ console.log(`📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`);
492
781
  moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
493
- moduleConfig.apis.forEach((api) => buildApi(moduleConfig, api));
782
+ });
783
+
784
+ // Grouper les APIs par chemin pour éviter les écrasements de fichier
785
+ const apisByPath = new Map<string, Array<{ moduleConfig: ModuleBuildConfig; api: ModuleApiConfig }>>();
786
+
787
+ moduleConfigs.forEach((moduleConfig) => {
788
+ moduleConfig.apis.forEach((api) => {
789
+ if (!apisByPath.has(api.path)) {
790
+ apisByPath.set(api.path, []);
791
+ }
792
+ apisByPath.get(api.path)!.push({ moduleConfig, api });
793
+ });
794
+ });
795
+
796
+ // Générer les fichiers de route groupés
797
+ apisByPath.forEach((apis, routePath) => {
798
+ buildGroupedApi(apis, routePath);
494
799
  });
495
800
 
496
801
  dumpNavigation();
497
802
  generateMenuConfig(moduleConfigs);
498
803
  generateDocsPage(moduleConfigs);
804
+ generateAppAside();
805
+ generateLayouts();
499
806
  copyModuleMigrations(moduleConfigs);
500
807
  }
501
-
502
- runModuleBuild();