@lastbrain/app 0.1.25 → 0.1.27

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