@lastbrain/app 2.0.24 → 2.0.35

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 (81) hide show
  1. package/dist/analytics/registry.d.ts +7 -0
  2. package/dist/analytics/registry.d.ts.map +1 -0
  3. package/dist/analytics/registry.js +11 -0
  4. package/dist/auth/useAuthSession.d.ts.map +1 -1
  5. package/dist/auth/useAuthSession.js +85 -1
  6. package/dist/cli.js +19 -3
  7. package/dist/components/LanguageSwitcher.d.ts +3 -1
  8. package/dist/components/LanguageSwitcher.d.ts.map +1 -1
  9. package/dist/components/LanguageSwitcher.js +134 -21
  10. package/dist/config/version.d.ts.map +1 -1
  11. package/dist/config/version.js +30 -19
  12. package/dist/i18n/server-lang.d.ts +1 -1
  13. package/dist/i18n/server-lang.d.ts.map +1 -1
  14. package/dist/i18n/types.d.ts +1 -1
  15. package/dist/i18n/types.d.ts.map +1 -1
  16. package/dist/i18n/useLink.d.ts.map +1 -1
  17. package/dist/i18n/useLink.js +15 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -0
  21. package/dist/layouts/AdminLayoutWithSidebar.d.ts +3 -1
  22. package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
  23. package/dist/layouts/AdminLayoutWithSidebar.js +2 -2
  24. package/dist/layouts/AppProviders.d.ts +9 -1
  25. package/dist/layouts/AppProviders.d.ts.map +1 -1
  26. package/dist/layouts/AppProviders.js +24 -3
  27. package/dist/layouts/AuthLayout.js +1 -1
  28. package/dist/layouts/PublicLayout.js +1 -1
  29. package/dist/layouts/RootLayout.d.ts.map +1 -1
  30. package/dist/scripts/init-app.d.ts.map +1 -1
  31. package/dist/scripts/init-app.js +343 -138
  32. package/dist/scripts/module-build.d.ts.map +1 -1
  33. package/dist/scripts/module-build.js +784 -59
  34. package/dist/scripts/module-create.d.ts.map +1 -1
  35. package/dist/scripts/module-create.js +227 -10
  36. package/dist/scripts/sitemap-flat-generator.d.ts +39 -0
  37. package/dist/scripts/sitemap-flat-generator.d.ts.map +1 -0
  38. package/dist/scripts/sitemap-flat-generator.js +231 -0
  39. package/dist/scripts/sitemap-manifest-generator.d.ts +59 -0
  40. package/dist/scripts/sitemap-manifest-generator.d.ts.map +1 -0
  41. package/dist/scripts/sitemap-manifest-generator.js +290 -0
  42. package/dist/sitemap/manifest.d.ts +8 -0
  43. package/dist/sitemap/manifest.d.ts.map +1 -0
  44. package/dist/sitemap/manifest.js +6 -0
  45. package/dist/styles.css +2 -2
  46. package/dist/templates/AuthGuidePage.js +2 -0
  47. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  48. package/dist/templates/DefaultDoc.js +9 -5
  49. package/dist/templates/DocPage.d.ts.map +1 -1
  50. package/dist/templates/DocPage.js +40 -0
  51. package/dist/templates/MigrationsGuidePage.js +2 -0
  52. package/dist/templates/ModuleGuidePage.d.ts.map +1 -1
  53. package/dist/templates/ModuleGuidePage.js +4 -1
  54. package/dist/templates/SimpleHomePage.js +2 -0
  55. package/package.json +31 -26
  56. package/src/analytics/registry.ts +14 -0
  57. package/src/auth/useAuthSession.ts +91 -1
  58. package/src/cli.ts +19 -3
  59. package/src/components/LanguageSwitcher.tsx +183 -60
  60. package/src/config/version.ts +30 -19
  61. package/src/i18n/server-lang.ts +2 -1
  62. package/src/i18n/types.ts +2 -1
  63. package/src/i18n/useLink.ts +15 -0
  64. package/src/index.ts +17 -0
  65. package/src/layouts/AdminLayoutWithSidebar.tsx +4 -0
  66. package/src/layouts/AppProviders.tsx +74 -9
  67. package/src/layouts/AuthLayout.tsx +1 -1
  68. package/src/layouts/PublicLayout.tsx +1 -1
  69. package/src/layouts/RootLayout.tsx +0 -1
  70. package/src/scripts/init-app.ts +418 -149
  71. package/src/scripts/module-build.ts +923 -63
  72. package/src/scripts/module-create.ts +260 -10
  73. package/src/scripts/sitemap-flat-generator.ts +313 -0
  74. package/src/scripts/sitemap-manifest-generator.ts +476 -0
  75. package/src/sitemap/manifest.ts +17 -0
  76. package/src/templates/AuthGuidePage.tsx +1 -1
  77. package/src/templates/DefaultDoc.tsx +397 -6
  78. package/src/templates/DocPage.tsx +40 -0
  79. package/src/templates/MigrationsGuidePage.tsx +1 -1
  80. package/src/templates/ModuleGuidePage.tsx +3 -2
  81. package/src/templates/SimpleHomePage.tsx +1 -1
@@ -1,15 +1,23 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
+ import { generateManifestBasedSitemaps } from "./sitemap-manifest-generator.js";
5
+ import type { ModuleBuildConfig } from "../index.js";
4
6
 
5
7
  import type {
6
8
  ModuleApiConfig,
7
- ModuleBuildConfig,
8
9
  ModuleMenuItemConfig,
9
10
  ModulePageConfig,
10
11
  ModuleSection,
11
12
  ModuleRealtimeConfig,
12
- } from "@lastbrain/core";
13
+ ModuleSitemapConfig,
14
+ } from "../index.js";
15
+
16
+ const LOCALE_MAP: Record<string, string> = {
17
+ fr: "fr_FR",
18
+ en: "en_US",
19
+ es: "es_ES",
20
+ };
13
21
 
14
22
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
15
23
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
@@ -248,7 +256,7 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
248
256
  page.componentExport === "UserPage";
249
257
 
250
258
  // Détecter les pages légales qui utilisent cookies (privacy, terms, returns)
251
- const isLegalPage =
259
+ const _isLegalPage =
252
260
  page.section === "public" &&
253
261
  (page.path.includes("privacy") ||
254
262
  page.path.includes("terms") ||
@@ -261,6 +269,7 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
261
269
  // On importe directement depuis app/config au lieu de passer via props
262
270
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
263
271
  import { UserDetailPage } from "${moduleConfig.moduleName}";
272
+ import { logger } from "@lastbrain/core";
264
273
 
265
274
  interface UserPageProps { params: Promise<{ id: string }> }
266
275
 
@@ -270,7 +279,7 @@ async function getModuleUserTabs() {
270
279
  const { moduleUserTabs } = await import("../../../../../../config/user-tabs");
271
280
  return moduleUserTabs || [];
272
281
  } catch (e) {
273
- console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
282
+ logger.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
274
283
  return [];
275
284
  }
276
285
  }
@@ -292,16 +301,6 @@ const ${page.componentExport} = dynamic(
292
301
  { ssr: false }
293
302
  );
294
303
 
295
- export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
296
- return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
297
- }
298
- `;
299
- } else if (isLegalPage) {
300
- content = `// GENERATED BY LASTBRAIN MODULE BUILD
301
- import { ${page.componentExport} } from "${moduleConfig.moduleName}";
302
-
303
- export const dynamic = 'force-dynamic';
304
-
305
304
  export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
306
305
  return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
307
306
  }
@@ -320,7 +319,7 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
320
319
  .map((seg) => seg.slice(1, -1)); // Enlève les []
321
320
 
322
321
  // Détecter si le module path lui-même a des params dynamiques (pas seulement [lang])
323
- const moduleHasDynamicParams = segments.some(
322
+ const _moduleHasDynamicParams = segments.some(
324
323
  (seg) => seg.startsWith("[") && seg.endsWith("]")
325
324
  );
326
325
 
@@ -338,10 +337,10 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
338
337
  // Détecter si c'est un Server Component (entryPoint: "server")
339
338
  const isServerComponent = page.entryPoint === "server";
340
339
 
341
- // Les Server Components reçoivent params en props SEULEMENT si le module lui-même a des params
340
+ // Les Server Components reçoivent params en props si la page a des params dynamiques
342
341
  // Les Client Components utilisent useParams() en interne
343
342
  const componentProps =
344
- isServerComponent && moduleHasDynamicParams ? "params={params}" : "";
343
+ isServerComponent && hasDynamicParams ? "params={params}" : "";
345
344
 
346
345
  // Si params existe mais n'est pas passé au composant, on doit le unwrap quand même
347
346
  const awaitParams =
@@ -349,11 +348,49 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
349
348
  ? "await params; // Unwrap params for Next.js 15\n "
350
349
  : "";
351
350
 
351
+ // Calculer le chemin relatif correct pour locales.generated
352
+ // routeDir = appDirectory + sectionPath + segments
353
+ // on doit remonter de (sectionPath.length + segments.length) niveaux pour atteindre appDirectory
354
+ // puis accéder à config/locales.generated
355
+ const pathDepth = sectionPath.length + segments.length;
356
+ const localeImportPath =
357
+ Array(pathDepth + 1)
358
+ .fill("..")
359
+ .join("/") + "/config/locales.generated";
360
+
361
+ // Pour les pages publiques (section: "public"), importer et passer LOCALE_CONFIG
362
+ const localeImport =
363
+ page.section === "public"
364
+ ? `\nimport { LOCALE_CONFIG } from "${localeImportPath}";\n`
365
+ : "";
366
+
367
+ const localeProps =
368
+ page.section === "public" && isServerComponent
369
+ ? "localeConfig={LOCALE_CONFIG}"
370
+ : "";
371
+
372
+ const allComponentProps = [componentProps, localeProps]
373
+ .filter(Boolean)
374
+ .join(" ");
375
+
376
+ // Pour les pages publiques avec metadataExport, créer une fonction wrapper qui passe LOCALE_CONFIG
377
+ // On utilise un alias pour l'import afin d'éviter le conflit de nom avec la fonction exportée
378
+ const metadataImportAlias = page.metadataExport
379
+ ? `${page.metadataExport} as ${page.metadataExport}Original`
380
+ : "";
381
+ const metadataExport =
382
+ page.section === "public" && page.metadataExport && localeImport
383
+ ? `\nexport async function generateMetadata(props: any) {
384
+ return ${page.metadataExport}Original({ ...props, localeConfig: LOCALE_CONFIG });
385
+ }`
386
+ : page.metadataExport
387
+ ? `\nexport { ${page.metadataExport}Original as generateMetadata };`
388
+ : "";
389
+
352
390
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
353
- import { ${page.componentExport} } from "${importPath}";
354
- ${page.metadataExport ? `\nexport { ${page.metadataExport} as generateMetadata } from "${importPath}";\n` : ""}
391
+ import { ${page.componentExport}${page.metadataExport ? `, ${metadataImportAlias}` : ""} } from "${importPath}";${localeImport}${metadataExport}
355
392
  export default ${hasDynamicParams ? "async " : ""}function ${wrapperName}${propsSignature} {
356
- ${awaitParams}return <${page.componentExport} ${componentProps} />;
393
+ ${awaitParams}return <${page.componentExport} ${allComponentProps} />;
357
394
  }
358
395
  `;
359
396
  }
@@ -511,10 +548,12 @@ export interface MenuItem {
511
548
  shortcut?: string;
512
549
  shortcutDisplay?: string;
513
550
  type?: 'text' | 'icon' | 'textIcon';
551
+ keyboardOnly?: boolean;
514
552
  position?: 'center' | 'end';
515
553
  component?: React.ComponentType<any>;
516
554
  componentExport?: string;
517
555
  entryPoint?: string;
556
+ badge?: string;
518
557
  }
519
558
 
520
559
  export interface MenuConfig {
@@ -647,7 +686,7 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
647
686
  }
648
687
 
649
688
  function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
650
- const docsDir = path.join(appDirectory, "docs");
689
+ const docsDir = path.join(appDirectory, "[lang]", "(public)", "docs");
651
690
  ensureDirectory(docsDir);
652
691
 
653
692
  const docsPagePath = path.join(docsDir, "page.tsx");
@@ -675,13 +714,13 @@ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
675
714
 
676
715
  allModules.forEach((moduleEntry) => {
677
716
  const moduleName = moduleEntry.package;
678
- // Extraire le nom du module sans le scope et sans "module-"
717
+ // Extraire le nom du module sans le scope et sans "-pro"
679
718
  // Ex: @lastbrain/module-auth -> auth
680
- // Ex: @lastbrain-labs/module-recipes-pro -> recipes
719
+ // Ex: @lastbrain-labs/module-metrics-pro -> metrics
681
720
  const moduleId = moduleName
682
721
  .replace("@lastbrain-labs/module-", "")
683
722
  .replace("@lastbrain/module-", "")
684
- .replace(/-pro$/, ""); // Retirer le suffix -pro pour avoir le nom de base
723
+ .replace(/-pro$/, ""); // Retirer le suffixe -pro
685
724
 
686
725
  const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
687
726
 
@@ -733,6 +772,45 @@ ${moduleConfigurations.join(",\n")}
733
772
  if (isDebugMode) {
734
773
  console.log(`📚 Generated docs page: ${docsPagePath}`);
735
774
  }
775
+
776
+ // Generate docs layout with translation support
777
+ const docsLayoutPath = path.join(docsDir, "layout.tsx");
778
+ const docsLayoutContent = `// Auto-generated docs layout with translation support
779
+ import { loadTranslations } from "@lastbrain/app/i18n/server-lang";
780
+ import { ClientLayout } from "../../../../components/ClientLayout";
781
+ import { headers } from "next/headers";
782
+
783
+ // Extract language from request headers
784
+ async function getLanguageFromRequest(): Promise<string> {
785
+ const headersList = await headers();
786
+ const acceptLanguage = headersList.get("accept-language") || "";
787
+ // Detect if French is requested
788
+ if (acceptLanguage.toLowerCase().includes("fr")) {
789
+ return "fr";
790
+ }
791
+ return "en";
792
+ }
793
+
794
+ export default async function DocsLayout({
795
+ children,
796
+ }: {
797
+ children: React.ReactNode;
798
+ }) {
799
+ const lang = await getLanguageFromRequest();
800
+ const translations = await loadTranslations(lang);
801
+
802
+ return (
803
+ <ClientLayout lang={lang} translations={translations}>
804
+ <div className="min-h-screen pt-12">{children}</div>
805
+ </ClientLayout>
806
+ );
807
+ }
808
+ `;
809
+
810
+ fs.writeFileSync(docsLayoutPath, docsLayoutContent);
811
+ if (isDebugMode) {
812
+ console.log(`📚 Generated docs layout: ${docsLayoutPath}`);
813
+ }
736
814
  }
737
815
 
738
816
  function buildGroupedApi(
@@ -799,7 +877,8 @@ function cleanGeneratedFiles() {
799
877
  "page.tsx", // Page racine seulement
800
878
  "admin/page.tsx", // Page admin racine
801
879
  "admin/layout.tsx", // Layout admin racine
802
- "docs/page.tsx", // Page docs générée
880
+ "[lang]/(public)/docs/page.tsx", // Page docs générée
881
+ "[lang]/(public)/docs/layout.tsx", // Layout docs généré
803
882
  // Middleware et autres fichiers core
804
883
  "middleware.ts",
805
884
  // Dossiers de lib et config
@@ -878,7 +957,14 @@ function cleanGeneratedFiles() {
878
957
  };
879
958
 
880
959
  // Nettoyer les dossiers de sections
881
- const sectionsToClean = ["(public)", "auth", "admin", "api", "[lang]"];
960
+ const sectionsToClean = [
961
+ "(public)",
962
+ "auth",
963
+ "admin",
964
+ "api",
965
+ "[lang]",
966
+ "sitemap",
967
+ ];
882
968
  sectionsToClean.forEach((section) => {
883
969
  const sectionPath = path.join(appDirectory, section);
884
970
  if (fs.existsSync(sectionPath)) {
@@ -954,7 +1040,7 @@ function cleanOldRoutesWithoutLang() {
954
1040
  }
955
1041
  });
956
1042
  }
957
- function generateAppAside() {
1043
+ function _generateAppAside() {
958
1044
  const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
959
1045
  const templateContent = `"use client";
960
1046
 
@@ -987,10 +1073,15 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
987
1073
  }
988
1074
  }
989
1075
 
990
- function generateLayouts() {
991
- // Générer ClientLayout wrapper client
1076
+ function generateLayouts(moduleConfigs: ModuleBuildConfig[]) {
1077
+ // Vérifier si le module billing-pro est actif
1078
+ const hasBillingModule = moduleConfigs.some(
1079
+ (config) => config.moduleName === "@lastbrain-labs/module-billing-pro"
1080
+ );
1081
+
1082
+ // Générer ClientLayout wrapper client (dans components/ racine)
992
1083
  const clientLayoutPath = path.join(
993
- appDirectory,
1084
+ projectRoot,
994
1085
  "components",
995
1086
  "ClientLayout.tsx"
996
1087
  );
@@ -1008,10 +1099,12 @@ export function ClientLayout({
1008
1099
  children,
1009
1100
  lang = "fr",
1010
1101
  translations = {},
1102
+ availableLanguages = ["fr", "en"],
1011
1103
  }: {
1012
1104
  children: ReactNode;
1013
1105
  lang?: Language;
1014
1106
  translations?: Record<string, string>;
1107
+ availableLanguages?: string[];
1015
1108
  }) {
1016
1109
  const router = useLocalizedRouter();
1017
1110
 
@@ -1023,7 +1116,7 @@ export function ClientLayout({
1023
1116
  enableSystem={false}
1024
1117
  storageKey="lastbrain-theme"
1025
1118
  >
1026
- <AppProviders lang={lang} translations={translations}>
1119
+ <AppProviders lang={lang} translations={translations} availableLanguages={availableLanguages}>
1027
1120
  <AppHeader />
1028
1121
  <div className="min-h-screen text-foreground bg-background">
1029
1122
  {children}
@@ -1044,33 +1137,49 @@ export function ClientLayout({
1044
1137
  console.error(`❌ Error generating ClientLayout component: ${error}`);
1045
1138
  }
1046
1139
 
1047
- // Générer AppProviders wrapper client
1140
+ // Générer AppProviders wrapper client (dans components/ racine)
1048
1141
  const appProvidersPath = path.join(
1049
- appDirectory,
1142
+ projectRoot,
1050
1143
  "components",
1051
1144
  "AppProviders.tsx"
1052
1145
  );
1146
+
1147
+ // Import conditionnel de EntitlementsProvider
1148
+ const entitlementsImport = hasBillingModule
1149
+ ? `import { EntitlementsProvider } from "@lastbrain-labs/module-billing-pro";`
1150
+ : "";
1151
+
1152
+ // Prop conditionnel pour EntitlementsProviderComponent
1153
+ const entitlementsProp = hasBillingModule
1154
+ ? `EntitlementsProviderComponent={EntitlementsProvider}`
1155
+ : "";
1156
+
1053
1157
  const appProvidersContent = `"use client";
1054
1158
 
1055
1159
  import { AppProviders as BaseAppProviders } from "@lastbrain/app";
1056
- import { realtimeConfig } from "../../config/realtime";
1160
+ ${entitlementsImport}
1161
+ import { realtimeConfig } from "../config/realtime";
1057
1162
  import type { ReactNode } from "react";
1058
1163
  import type { Language } from "@lastbrain/core";
1059
1164
 
1060
1165
  export function AppProviders({
1061
- children,
1166
+ children,
1062
1167
  lang = "fr",
1063
1168
  translations = {},
1169
+ availableLanguages = ["fr", "en"],
1064
1170
  }: {
1065
1171
  children: ReactNode;
1066
1172
  lang?: Language;
1067
1173
  translations?: Record<string, string>;
1174
+ availableLanguages?: string[];
1068
1175
  }) {
1069
1176
  return (
1070
1177
  <BaseAppProviders
1071
1178
  realtimeConfig={realtimeConfig}
1072
1179
  lang={lang}
1073
1180
  translations={translations}
1181
+ availableLanguages={availableLanguages}
1182
+ ${entitlementsProp}
1074
1183
  >
1075
1184
  {children}
1076
1185
  </BaseAppProviders>
@@ -1087,19 +1196,33 @@ export function AppProviders({
1087
1196
  console.error(`❌ Error generating AppProviders component: ${error}`);
1088
1197
  }
1089
1198
 
1090
- // Générer AppHeader component
1091
- const appHeaderPath = path.join(appDirectory, "components", "AppHeader.tsx");
1199
+ // Générer AppHeader component (dans components/ racine)
1200
+ const appHeaderPath = path.join(projectRoot, "components", "AppHeader.tsx");
1092
1201
  const appHeaderContent = `"use client";
1093
1202
 
1094
1203
  import { Header } from "@lastbrain/ui";
1095
- import { LanguageSwitcher } from "@lastbrain/app";
1096
- import { menuConfig } from "../../config/menu";
1204
+ import { LanguageSwitcher, useAuth } from "@lastbrain/app";
1205
+ import { menuConfig } from "../config/menu";
1206
+ import { menuIgnored } from "../config/menu-ignored";
1207
+ import { LOCALE_CONFIG } from "../config/locales.generated";
1097
1208
 
1098
1209
  export function AppHeader() {
1210
+ const { user, loading, isSuperAdmin } = useAuth();
1211
+
1099
1212
  return (
1100
1213
  <Header
1214
+ user={user}
1215
+ isSuperAdmin={isSuperAdmin}
1101
1216
  menuConfig={menuConfig}
1102
- languageSwitcher={<LanguageSwitcher variant="minimal" />}
1217
+ accountMenu={menuConfig.account}
1218
+ menuIgnored={menuIgnored}
1219
+ languageSwitcher={
1220
+ <LanguageSwitcher
1221
+ variant="minimal"
1222
+ availableLanguages={[...LOCALE_CONFIG.languages]}
1223
+ localeMap={LOCALE_CONFIG.localeMap}
1224
+ />
1225
+ }
1103
1226
  />
1104
1227
  );
1105
1228
  }`;
@@ -1116,6 +1239,27 @@ export function AppHeader() {
1116
1239
  const langLayoutContent = `import { loadTranslations } from "@lastbrain/app/i18n/server-lang";
1117
1240
  import { ClientLayout } from "../../components/ClientLayout";
1118
1241
 
1242
+ // Fallback default config in case locales.generated.ts is not available
1243
+ const DEFAULT_LOCALE_CONFIG = {
1244
+ languages: ["fr", "en"] as const,
1245
+ locales: ["fr_FR", "en_US"],
1246
+ localeMap: { fr: "fr_FR", en: "en_US" } as Record<string, string>,
1247
+ };
1248
+
1249
+ // Type for valid languages derived from config
1250
+ type ValidLanguage = (typeof DEFAULT_LOCALE_CONFIG.languages)[number];
1251
+
1252
+ // Helper function to safely load locale config
1253
+ async function getLocaleConfig() {
1254
+
1255
+ const importedConfig = await import("../../config/locales.generated");
1256
+ if (importedConfig?.LOCALE_CONFIG && importedConfig.LOCALE_CONFIG.languages) {
1257
+ return importedConfig.LOCALE_CONFIG;
1258
+ }
1259
+
1260
+ return DEFAULT_LOCALE_CONFIG;
1261
+ }
1262
+
1119
1263
  export default async function LangLayout({
1120
1264
  children,
1121
1265
  params,
@@ -1124,14 +1268,23 @@ export default async function LangLayout({
1124
1268
  params: Promise<{ lang: string }>;
1125
1269
  }) {
1126
1270
  const { lang } = await params;
1127
- const validLang = lang === "en" || lang === "fr" ? lang : "fr";
1271
+
1272
+ // Get the locale config (either from app or fallback)
1273
+ const LOCALE_CONFIG = await getLocaleConfig();
1274
+
1275
+ // Validate lang and cast to ValidLanguage type
1276
+ const isValidLang = (val: string): val is ValidLanguage => {
1277
+ return LOCALE_CONFIG.languages.includes(val as any);
1278
+ };
1279
+
1280
+ const validLang: ValidLanguage = isValidLang(lang) ? lang : "fr";
1128
1281
 
1129
1282
  // Charger les traductions pour cette langue
1130
1283
  const translations = await loadTranslations(validLang);
1131
1284
 
1132
1285
  return (
1133
- <ClientLayout lang={validLang} translations={translations}>
1134
- <div className="min-h-screen pt-16">
1286
+ <ClientLayout lang={validLang} translations={translations} availableLanguages={[...LOCALE_CONFIG.languages]}>
1287
+ <div className="min-h-screen ">
1135
1288
  {children}
1136
1289
  </div>
1137
1290
  </ClientLayout>
@@ -1162,6 +1315,7 @@ export default async function LangLayout({
1162
1315
 
1163
1316
  import { AuthLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
1164
1317
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1318
+ import { menuIgnored } from "../../../config/menu-ignored";
1165
1319
 
1166
1320
  export default function SectionLayout({
1167
1321
  children,
@@ -1182,7 +1336,7 @@ export default function SectionLayout({
1182
1336
  };
1183
1337
 
1184
1338
  return (
1185
- <AuthLayoutWithSidebar menuConfig={menuConfig}>
1339
+ <AuthLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1186
1340
  {children}
1187
1341
  </AuthLayoutWithSidebar>
1188
1342
  );
@@ -1198,6 +1352,39 @@ export default function SectionLayout({
1198
1352
  console.error(`❌ Error generating auth layout: ${error}`);
1199
1353
  }
1200
1354
 
1355
+ // Générer layout public avec footer
1356
+ const publicLayoutPath = path.join(
1357
+ appDirectory,
1358
+ "[lang]",
1359
+ "(public)",
1360
+ "layout.tsx"
1361
+ );
1362
+ const publicLayoutContent = `"use client";
1363
+
1364
+ import type React from "react";
1365
+ import { PublicLayout } from "@lastbrain/app";
1366
+ import { footerConfig } from "../../../config/footer";
1367
+
1368
+ export default function PublicSectionLayout({
1369
+ children,
1370
+ }: {
1371
+ children: React.ReactNode;
1372
+ }) {
1373
+ return <PublicLayout footerConfig={footerConfig}>
1374
+ {children}
1375
+ </PublicLayout>;
1376
+ }`;
1377
+
1378
+ try {
1379
+ writeScaffoldFile(
1380
+ publicLayoutPath,
1381
+ publicLayoutContent,
1382
+ "public layout with footer"
1383
+ );
1384
+ } catch (error) {
1385
+ console.error(`❌ Error generating public layout: ${error}`);
1386
+ }
1387
+
1201
1388
  // Générer layout admin avec sidebar
1202
1389
  const adminLayoutPath = path.join(
1203
1390
  appDirectory,
@@ -1209,6 +1396,7 @@ export default function SectionLayout({
1209
1396
 
1210
1397
  import { AdminLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
1211
1398
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1399
+ import { menuIgnored } from "../../../config/menu-ignored";
1212
1400
 
1213
1401
  export default function AdminLayout({
1214
1402
  children,
@@ -1229,7 +1417,7 @@ export default function AdminLayout({
1229
1417
  };
1230
1418
 
1231
1419
  return (
1232
- <AdminLayoutWithSidebar menuConfig={menuConfig}>
1420
+ <AdminLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1233
1421
  {children}
1234
1422
  </AdminLayoutWithSidebar>
1235
1423
  );
@@ -1333,10 +1521,12 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
1333
1521
  } else {
1334
1522
  // Générer les imports statiques (Next/dynamic pour chaque composant)
1335
1523
  const importsForApp = userTabsConfigs
1336
- .map(
1337
- (tab) =>
1338
- `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`
1339
- )
1524
+ .map((tab) => {
1525
+ // UserTokenTab est un Client Component, utiliser ssr: false
1526
+ const ssrMode =
1527
+ tab.componentExport === "UserTokenTab" ? "false" : "true";
1528
+ return `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: ${ssrMode} });`;
1529
+ })
1340
1530
  .join("\n");
1341
1531
 
1342
1532
  // Générer le tableau des tabs
@@ -1383,6 +1573,157 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
1383
1573
  }
1384
1574
  }
1385
1575
 
1576
+ async function generateAuthDashboard(moduleConfigs: ModuleBuildConfig[]) {
1577
+ try {
1578
+ const dashboards = moduleConfigs
1579
+ .flatMap((config) =>
1580
+ (config.authDashboard ?? []).map((dashboard) => ({
1581
+ ...dashboard,
1582
+ moduleName: config.moduleName,
1583
+ }))
1584
+ )
1585
+ .sort((a, b) => (a.order ?? 999) - (b.order ?? 999));
1586
+
1587
+ const timestamp = new Date().toISOString();
1588
+
1589
+ let configContent: string;
1590
+
1591
+ if (dashboards.length === 0) {
1592
+ configContent = `// GENERATED FILE - DO NOT EDIT MANUALLY
1593
+ // Auth dashboard configuration
1594
+ // Generated at: ${timestamp}
1595
+
1596
+ "use client";
1597
+
1598
+ import type React from "react";
1599
+
1600
+ export interface ModuleAuthDashboard {
1601
+ key: string;
1602
+ title: string;
1603
+ icon?: string;
1604
+ order?: number;
1605
+ component: React.ComponentType<Record<string, never>>;
1606
+ }
1607
+
1608
+ export const moduleAuthDashboards: ModuleAuthDashboard[] = [];
1609
+
1610
+ export default moduleAuthDashboards;
1611
+ `;
1612
+ } else {
1613
+ const dynamicImports = dashboards
1614
+ .map(
1615
+ (dashboard, index) =>
1616
+ `const AuthDashboardComponent${index} = dynamic(() => import("${dashboard.moduleName}").then(mod => ({ default: (mod as any)["${dashboard.componentExport}"] })), { ssr: true });`
1617
+ )
1618
+ .join("\n");
1619
+
1620
+ const dashboardsArray = dashboards
1621
+ .map(
1622
+ (dashboard, index) => ` {
1623
+ key: "${dashboard.key}",
1624
+ title: "${dashboard.title}",
1625
+ icon: "${dashboard.icon ?? ""}",
1626
+ order: ${dashboard.order ?? 999},
1627
+ component: AuthDashboardComponent${index},
1628
+ }`
1629
+ )
1630
+ .join(",\n");
1631
+
1632
+ configContent = `// GENERATED FILE - DO NOT EDIT MANUALLY
1633
+ // Auth dashboard configuration
1634
+ // Generated at: ${timestamp}
1635
+
1636
+ "use client";
1637
+
1638
+ import dynamic from "next/dynamic";
1639
+ import type React from "react";
1640
+
1641
+ ${dynamicImports}
1642
+
1643
+ export interface ModuleAuthDashboard {
1644
+ key: string;
1645
+ title: string;
1646
+ icon?: string;
1647
+ order?: number;
1648
+ component: React.ComponentType<Record<string, never>>;
1649
+ }
1650
+
1651
+ export const moduleAuthDashboards: ModuleAuthDashboard[] = [
1652
+ ${dashboardsArray}
1653
+ ];
1654
+
1655
+ export default moduleAuthDashboards;
1656
+ `;
1657
+ }
1658
+
1659
+ const configPath = path.join(projectRoot, "config", "auth-dashboard.ts");
1660
+ ensureDirectory(path.dirname(configPath));
1661
+ fs.writeFileSync(configPath, configContent);
1662
+
1663
+ if (isDebugMode) {
1664
+ console.log(
1665
+ `✅ Generated auth dashboard configuration: ${configPath} (${dashboards.length} item(s))`
1666
+ );
1667
+ }
1668
+
1669
+ const pagePath = path.join(
1670
+ appDirectory,
1671
+ "[lang]",
1672
+ "auth",
1673
+ "dashboard",
1674
+ "page.tsx"
1675
+ );
1676
+
1677
+ ensureDirectory(path.dirname(pagePath));
1678
+
1679
+ const pageContent = `// GENERATED BY LASTBRAIN MODULE BUILD
1680
+ "use client";
1681
+
1682
+ import { Suspense } from "react";
1683
+ import { moduleAuthDashboards } from "../../../../config/auth-dashboard";
1684
+
1685
+ export const dynamic = "force-dynamic";
1686
+
1687
+
1688
+ export default function AuthDashboardPage() {
1689
+ const dashboards = moduleAuthDashboards || [];
1690
+
1691
+ if (dashboards.length === 0) {
1692
+ return <div className="space-y-4">Aucun dashboard disponible.</div>;
1693
+ }
1694
+
1695
+ return (
1696
+ <div className="space-y-6">
1697
+ {dashboards.map((dashboard) => {
1698
+
1699
+ const Component = dashboard.component;
1700
+
1701
+ return (
1702
+ <section
1703
+ key={dashboard.key}
1704
+
1705
+ >
1706
+ <Suspense fallback={<div>Chargement...</div>}>
1707
+ <Component />
1708
+ </Suspense>
1709
+ </section>
1710
+ );
1711
+ })}
1712
+ </div>
1713
+ );
1714
+ }
1715
+ `;
1716
+
1717
+ fs.writeFileSync(pagePath, pageContent);
1718
+
1719
+ if (isDebugMode) {
1720
+ console.log(`🧭 Generated auth dashboard page: ${pagePath}`);
1721
+ }
1722
+ } catch (error) {
1723
+ console.error("❌ Error generating auth dashboard configuration:", error);
1724
+ }
1725
+ }
1726
+
1386
1727
  async function generateBucketsConfig(moduleConfigs: ModuleBuildConfig[]) {
1387
1728
  try {
1388
1729
  // Extraire les configurations storage des modules
@@ -1583,7 +1924,6 @@ export async function GET(
1583
1924
  .createSignedUrl(storagePath, 3600); // 1 heure
1584
1925
 
1585
1926
  if (error) {
1586
- console.error(\`[storage] Error creating signed URL for public image:\`, error);
1587
1927
  return new NextResponse("Not found", { status: 404 });
1588
1928
  }
1589
1929
 
@@ -1604,16 +1944,23 @@ export async function GET(
1604
1944
 
1605
1945
  // Cas spécial: si le chemin commence par /product/ ou /recipe/,
1606
1946
  // c'est une image avec format court qui nécessite le préfixe userId
1947
+ // Sinon le chemin contient déjà le userId
1607
1948
  let actualStoragePath = storagePath;
1608
-
1609
- if (storagePath.startsWith("product/") || storagePath.startsWith("recipe/")) {
1949
+ const pathParts = storagePath.split("/");
1950
+
1951
+ // Vérifier si le premier segment est un UUID (userId déjà présent)
1952
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1953
+ const firstSegmentIsUuid = uuidRegex.test(pathParts[0]);
1954
+
1955
+ if (!firstSegmentIsUuid) {
1956
+ // Le chemin ne contient pas le userId, on l'ajoute
1610
1957
  actualStoragePath = \`\${user.id}/\${storagePath}\`;
1611
1958
  }
1612
1959
 
1613
1960
  // Vérifier que l'utilisateur a accès à cette image
1614
1961
  // Format: {userId}/recipe/{recipeId}/{filename} ou {userId}/product/{productId}/{filename}
1615
- const pathParts = actualStoragePath.split("/");
1616
- const pathUserId = pathParts[0];
1962
+ const actualPathParts = actualStoragePath.split("/");
1963
+ const pathUserId = actualPathParts[0];
1617
1964
 
1618
1965
  if (pathUserId !== user.id) {
1619
1966
  return new NextResponse("Forbidden", { status: 403 });
@@ -1622,17 +1969,31 @@ export async function GET(
1622
1969
  // Créer une URL signée pour l'image privée
1623
1970
  const { data, error } = await supabase.storage
1624
1971
  .from(bucket)
1625
- .createSignedUrl(actualStoragePath, 3600); // 1 heure
1972
+ .createSignedUrl(actualStoragePath, 60); // 1 minute seulement
1626
1973
 
1627
1974
  if (error) {
1628
- console.error(\`[storage] Error creating signed URL:\`, error);
1629
1975
  return new NextResponse("Not found", { status: 404 });
1630
1976
  }
1631
1977
 
1632
- // Rediriger vers l'URL signée
1633
- return NextResponse.redirect(data.signedUrl);
1978
+ // Télécharger l'image et la retourner avec des headers no-cache
1979
+ const imageResponse = await fetch(data.signedUrl);
1980
+ if (!imageResponse.ok) {
1981
+ return new NextResponse("Not found", { status: 404 });
1982
+ }
1983
+
1984
+ const imageBuffer = await imageResponse.arrayBuffer();
1985
+ const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
1986
+
1987
+ return new NextResponse(imageBuffer, {
1988
+ status: 200,
1989
+ headers: {
1990
+ "Content-Type": contentType,
1991
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1992
+ "Pragma": "no-cache",
1993
+ "Expires": "0",
1994
+ },
1995
+ });
1634
1996
  } catch (error) {
1635
- console.error("[storage] Error:", error);
1636
1997
  return new NextResponse("Internal server error", { status: 500 });
1637
1998
  }
1638
1999
  }
@@ -1704,8 +2065,6 @@ async function generateFooterConfig(moduleConfigs: ModuleBuildConfig[]) {
1704
2065
  import type { FooterConfig } from "@lastbrain/ui";
1705
2066
 
1706
2067
  export const footerConfig: FooterConfig = {
1707
- companyName: "LastBrain",
1708
- companyDescription: "Plateforme de développement rapide d'applications",
1709
2068
  links: ${JSON.stringify(allFooterLinks, null, 2)},
1710
2069
  social: [],
1711
2070
  };
@@ -1726,11 +2085,364 @@ export const footerConfig: FooterConfig = {
1726
2085
  }
1727
2086
  }
1728
2087
 
2088
+ /**
2089
+ * Génère les routes sitemap à partir des configurations des modules
2090
+ */
2091
+ async function generateSitemapRoutes(moduleConfigs: ModuleBuildConfig[]) {
2092
+ try {
2093
+ // Collecter tous les sitemaps de tous les modules
2094
+ const allSitemaps: Array<{
2095
+ moduleConfig: ModuleBuildConfig;
2096
+ sitemap: ModuleSitemapConfig;
2097
+ }> = [];
2098
+
2099
+ moduleConfigs.forEach((config) => {
2100
+ if (config.sitemaps && config.sitemaps.length > 0) {
2101
+ config.sitemaps.forEach((sitemap) => {
2102
+ allSitemaps.push({ moduleConfig: config, sitemap });
2103
+ });
2104
+ }
2105
+ });
2106
+
2107
+ if (allSitemaps.length === 0) {
2108
+ if (isDebugMode) {
2109
+ console.log("⏭️ No sitemaps found, skipping sitemap generation");
2110
+ }
2111
+ return;
2112
+ }
2113
+
2114
+ // Détecter les sitemaps index des modules (sitemap.xml ou sitemap-index)
2115
+ const moduleSitemapIndexes: string[] = [];
2116
+
2117
+ // Renommer les /sitemap.xml en /sitemap-{moduleName}.xml pour éviter les conflits
2118
+ allSitemaps.forEach(({ moduleConfig, sitemap }) => {
2119
+ if (
2120
+ sitemap.path === "/sitemap.xml" ||
2121
+ sitemap.path.endsWith("-index.xml")
2122
+ ) {
2123
+ // Extraire le nom court du module (ex: module-blog -> blog)
2124
+ const moduleShortName = moduleConfig.moduleName
2125
+ .replace("@lastbrain/", "")
2126
+ .replace("@lastbrain-labs/", "")
2127
+ .replace("module-", "")
2128
+ .replace("module-core-", "");
2129
+
2130
+ // Renommer le path
2131
+ sitemap.path = `/sitemap-${moduleShortName}.xml`;
2132
+ moduleSitemapIndexes.push(sitemap.path);
2133
+
2134
+ if (isDebugMode) {
2135
+ console.log(
2136
+ `🔄 Renamed sitemap from /sitemap.xml to ${sitemap.path} for ${moduleConfig.moduleName}`
2137
+ );
2138
+ }
2139
+ }
2140
+ });
2141
+
2142
+ // Générer chaque route sitemap
2143
+ for (const { moduleConfig, sitemap } of allSitemaps) {
2144
+ const sitemapPath = sitemap.path;
2145
+
2146
+ // Déterminer le chemin du fichier route.ts
2147
+ let routeDir: string;
2148
+ if (sitemapPath === "/sitemap.xml") {
2149
+ // Route principale sitemap.xml à la racine de app
2150
+ routeDir = path.join(appDirectory, "sitemap.xml");
2151
+ } else {
2152
+ // Sous-sitemaps (ex: /sitemap/recipes.xml -> app/sitemap/recipes.xml)
2153
+ const segments = sitemapPath
2154
+ .replace(/^\//, "")
2155
+ .replace(/\.xml$/, ".xml")
2156
+ .split("/");
2157
+ routeDir = path.join(appDirectory, ...segments);
2158
+ }
2159
+
2160
+ const routeFilePath = path.join(routeDir, "route.ts");
2161
+ ensureDirectory(routeDir);
2162
+
2163
+ // Déterminer le type de sitemap et générer le code approprié
2164
+ const entryPoint = sitemap.entryPoint;
2165
+ let content: string;
2166
+
2167
+ if (entryPoint === "sitemap/index") {
2168
+ // Sitemap index principal
2169
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap Index Route
2170
+ // Module: ${moduleConfig.moduleName}
2171
+ // Path: ${sitemapPath}
2172
+ import { generateSitemapIndex } from "${moduleConfig.moduleName}/sitemap";
2173
+
2174
+ export async function GET(): Promise<Response> {
2175
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2176
+
2177
+ // Liste des sous-sitemaps
2178
+ const sitemapPaths = [
2179
+ "/sitemap/pages.xml",
2180
+ "/sitemap/recipes.xml",
2181
+ "/sitemap/chefs.xml",
2182
+ ];
2183
+
2184
+ const xml = generateSitemapIndex(baseUrl, sitemapPaths);
2185
+
2186
+ return new Response(xml, {
2187
+ headers: {
2188
+ "Content-Type": "application/xml",
2189
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2190
+ },
2191
+ });
2192
+ }
2193
+ `;
2194
+ } else if (entryPoint === "sitemap/pages") {
2195
+ // Sitemap des pages statiques
2196
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Static Pages Sitemap Route
2197
+ // Module: ${moduleConfig.moduleName}
2198
+ // Path: ${sitemapPath}
2199
+ import { generateStaticPagesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
2200
+
2201
+ export async function GET(): Promise<Response> {
2202
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2203
+
2204
+ const sitemap = await generateStaticPagesSitemap(baseUrl);
2205
+ const xml = generateSitemapXml(sitemap);
2206
+
2207
+ return new Response(xml, {
2208
+ headers: {
2209
+ "Content-Type": "application/xml",
2210
+ "Cache-Control": "public, max-age=86400, s-maxage=86400",
2211
+ },
2212
+ });
2213
+ }
2214
+ `;
2215
+ } else if (entryPoint === "sitemap/recipes") {
2216
+ // Sitemap des recettes
2217
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Recipes Sitemap Route
2218
+ // Module: ${moduleConfig.moduleName}
2219
+ // Path: ${sitemapPath}
2220
+ import { generateRecipesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
2221
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2222
+
2223
+ export async function GET(): Promise<Response> {
2224
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2225
+ const supabase = await getSupabaseServiceClient();
2226
+
2227
+ const sitemap = await generateRecipesSitemap(baseUrl, supabase);
2228
+ const xml = generateSitemapXml(sitemap);
2229
+
2230
+ return new Response(xml, {
2231
+ headers: {
2232
+ "Content-Type": "application/xml",
2233
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2234
+ },
2235
+ });
2236
+ }
2237
+ `;
2238
+ } else if (entryPoint === "sitemap/chefs") {
2239
+ // Sitemap des chefs
2240
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Chefs Sitemap Route
2241
+ // Module: ${moduleConfig.moduleName}
2242
+ // Path: ${sitemapPath}
2243
+ import { generateChefsSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
2244
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2245
+
2246
+ export async function GET(): Promise<Response> {
2247
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2248
+ const supabase = await getSupabaseServiceClient();
2249
+
2250
+ const sitemap = await generateChefsSitemap(baseUrl, supabase);
2251
+ const xml = generateSitemapXml(sitemap);
2252
+
2253
+ return new Response(xml, {
2254
+ headers: {
2255
+ "Content-Type": "application/xml",
2256
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2257
+ },
2258
+ });
2259
+ }
2260
+ `;
2261
+ } else {
2262
+ // Sitemap générique - export direct du handler
2263
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap route
2264
+ // Module: ${moduleConfig.moduleName}
2265
+ // Path: ${sitemapPath}
2266
+ export { ${sitemap.handlerExport} } from "${moduleConfig.moduleName}/${sitemap.entryPoint}";
2267
+ `;
2268
+ }
2269
+
2270
+ fs.writeFileSync(routeFilePath, content);
2271
+
2272
+ if (isDebugMode) {
2273
+ console.log(`🗺️ Generated sitemap route: ${routeFilePath}`);
2274
+ }
2275
+ }
2276
+
2277
+ // Générer le sitemap.xml global qui agrège tous les sitemaps des modules
2278
+ if (moduleSitemapIndexes.length > 0) {
2279
+ const globalSitemapDir = path.join(appDirectory, "sitemap.xml");
2280
+ const globalSitemapFile = path.join(globalSitemapDir, "route.ts");
2281
+ ensureDirectory(globalSitemapDir);
2282
+
2283
+ const sitemapUrls = moduleSitemapIndexes
2284
+ .map((path) => {
2285
+ return ` {
2286
+ url: \`\${baseUrl}${path}\`,
2287
+ lastModified: new Date(),
2288
+ }`;
2289
+ })
2290
+ .join(",\n");
2291
+
2292
+ const globalSitemapContent = `// GENERATED BY LASTBRAIN MODULE BUILD - Global Sitemap Index
2293
+ // This file aggregates all module sitemaps
2294
+ // Module sitemaps: ${moduleSitemapIndexes.join(", ")}
2295
+
2296
+ export async function GET(): Promise<Response> {
2297
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2298
+
2299
+ const sitemapIndexXml = \`<?xml version="1.0" encoding="UTF-8"?>
2300
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
2301
+ ${moduleSitemapIndexes
2302
+ .map(
2303
+ (path) => ` <sitemap>
2304
+ <loc>\${baseUrl}${path}</loc>
2305
+ <lastmod>\${new Date().toISOString()}</lastmod>
2306
+ </sitemap>`
2307
+ )
2308
+ .join("\n")}
2309
+ </sitemapindex>\`;
2310
+
2311
+ return new Response(sitemapIndexXml, {
2312
+ headers: {
2313
+ "Content-Type": "application/xml",
2314
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2315
+ },
2316
+ });
2317
+ }
2318
+ `;
2319
+
2320
+ fs.writeFileSync(globalSitemapFile, globalSitemapContent);
2321
+
2322
+ if (isDebugMode) {
2323
+ console.log(
2324
+ `🌍 Generated global sitemap.xml aggregating ${moduleSitemapIndexes.length} module sitemap(s)`
2325
+ );
2326
+ }
2327
+ }
2328
+
2329
+ if (isDebugMode) {
2330
+ console.log(`✅ Generated ${allSitemaps.length} sitemap route(s)`);
2331
+ }
2332
+ } catch (error) {
2333
+ console.error("❌ Error generating sitemap routes:", error);
2334
+ }
2335
+ }
2336
+
1729
2337
  /**
1730
2338
  * Génère les fichiers i18n en concaténant les traductions des modules
2339
+ * et en incluant les traductions personnalisées de i18n/default
1731
2340
  */
2341
+ async function generateLocaleConfig() {
2342
+ const appI18nDir = path.join(projectRoot, "i18n");
2343
+ const appConfigDir = path.join(projectRoot, "config");
2344
+
2345
+ try {
2346
+ // Scanner les fichiers i18n disponibles
2347
+ let languages: string[] = ["fr"]; // Fallback par défaut
2348
+
2349
+ if (fs.existsSync(appI18nDir)) {
2350
+ const files = fs.readdirSync(appI18nDir);
2351
+ languages = files
2352
+ .filter((f) => f.endsWith(".json"))
2353
+ .map((f) => f.replace(".json", ""))
2354
+ .filter((lang) => LOCALE_MAP[lang])
2355
+ .sort();
2356
+
2357
+ if (isDebugMode) {
2358
+ console.log(` 📋 Detected languages: ${languages.join(", ")}`);
2359
+ }
2360
+ }
2361
+
2362
+ const locales = languages.map((lang) => LOCALE_MAP[lang]);
2363
+ const localeMap = languages.reduce(
2364
+ (acc, lang) => {
2365
+ acc[lang] = LOCALE_MAP[lang];
2366
+ return acc;
2367
+ },
2368
+ {} as Record<string, string>
2369
+ );
2370
+
2371
+ // Générer le fichier TypeScript dans l'app (ex: apps/recipe/config/)
2372
+ ensureDirectory(appConfigDir);
2373
+
2374
+ // Générer le fichier dans config/
2375
+ const outputPath = path.join(appConfigDir, "locales.generated.ts");
2376
+ const content = `// This file is auto-generated by module-build.ts
2377
+ // Do not edit manually - regenerate with 'pnpm build:modules'
2378
+
2379
+ export interface LocaleConfig {
2380
+ languages: string[];
2381
+ locales: string[];
2382
+ localeMap: Record<string, string>;
2383
+ }
2384
+
2385
+ export const LOCALE_CONFIG: LocaleConfig = ${JSON.stringify(
2386
+ {
2387
+ languages,
2388
+ locales,
2389
+ localeMap,
2390
+ },
2391
+ null,
2392
+ 2
2393
+ )} as const;
2394
+
2395
+ /**
2396
+ * Convert language code to locale (e.g., "fr" -> "fr_FR")
2397
+ */
2398
+ export function langToLocale(lang: string): string {
2399
+ return LOCALE_CONFIG.localeMap[lang] || "fr_FR";
2400
+ }
2401
+
2402
+ /**
2403
+ * Get all locales except the given one
2404
+ */
2405
+ export function getAlternateLocales(lang: string): string[] {
2406
+ const currentLocale = LOCALE_CONFIG.localeMap[lang] || "fr_FR";
2407
+ return LOCALE_CONFIG.locales.filter((l) => l !== currentLocale);
2408
+ }
2409
+
2410
+ /**
2411
+ * Get locale map for metadata generation
2412
+ */
2413
+ export function getLocaleMap(): Record<string, string> {
2414
+ return LOCALE_CONFIG.localeMap;
2415
+ }
2416
+ `;
2417
+
2418
+ writeScaffoldFile(outputPath, content, "locale configuration");
2419
+
2420
+ // Générer aussi un fichier de ré-export dans le répertoire racine de l'app pour faciliter l'import
2421
+ const rootExportPath = path.join(projectRoot, "locale-helpers.ts");
2422
+ const rootExportContent = `// Re-export locale helpers from config for easy import in packages
2423
+ export { langToLocale, getAlternateLocales, getLocaleMap, LOCALE_CONFIG } from "./config/locales.generated";
2424
+ export type { LocaleConfig } from "./config/locales.generated";
2425
+ `;
2426
+
2427
+ writeScaffoldFile(
2428
+ rootExportPath,
2429
+ rootExportContent,
2430
+ "locale helpers re-export"
2431
+ );
2432
+
2433
+ if (isDebugMode) {
2434
+ console.log(
2435
+ ` ✅ Generated locale config with ${languages.length} language(s)`
2436
+ );
2437
+ }
2438
+ } catch (error) {
2439
+ console.error("❌ Error generating locale config:", error);
2440
+ }
2441
+ }
2442
+
1732
2443
  async function generateI18nFiles() {
1733
2444
  const appI18nDir = path.join(projectRoot, "i18n");
2445
+ const appI18nDefaultDir = path.join(projectRoot, "i18n", "default");
1734
2446
  const packagesDir = path.join(_monorepoRoot, "packages");
1735
2447
 
1736
2448
  interface TranslationMap {
@@ -1793,6 +2505,41 @@ async function generateI18nFiles() {
1793
2505
  }
1794
2506
  }
1795
2507
 
2508
+ // Charger les traductions personnalisées de i18n/default
2509
+ if (fs.existsSync(appI18nDefaultDir)) {
2510
+ // Découvrir dynamiquement les fichiers de langue disponibles
2511
+ const defaultFiles = fs
2512
+ .readdirSync(appI18nDefaultDir)
2513
+ .filter((file) => file.endsWith(".json"));
2514
+
2515
+ for (const defaultFile of defaultFiles) {
2516
+ const defaultFilePath = path.join(appI18nDefaultDir, defaultFile);
2517
+
2518
+ if (fs.existsSync(defaultFilePath)) {
2519
+ const lang = defaultFile.replace(".json", "");
2520
+
2521
+ if (!translations[lang]) {
2522
+ translations[lang] = {};
2523
+ }
2524
+
2525
+ try {
2526
+ const content = JSON.parse(
2527
+ fs.readFileSync(defaultFilePath, "utf-8")
2528
+ );
2529
+
2530
+ if (isDebugMode) {
2531
+ console.log(` ✓ app defaults (${lang})`);
2532
+ }
2533
+
2534
+ // Ajouter ou surcharger les traductions personnalisées
2535
+ Object.assign(translations[lang], content);
2536
+ } catch (error) {
2537
+ console.warn(` ⚠️ Error reading ${defaultFilePath}:`, error);
2538
+ }
2539
+ }
2540
+ }
2541
+ }
2542
+
1796
2543
  // Créer le dossier i18n s'il n'existe pas
1797
2544
  ensureDirectory(appI18nDir);
1798
2545
 
@@ -1824,6 +2571,81 @@ function extractModuleNameFromPath(filePath: string): string {
1824
2571
  return "unknown";
1825
2572
  }
1826
2573
 
2574
+ /**
2575
+ * Génère la page d'accueil app/[lang]/page.tsx si un module définit path: "/"
2576
+ */
2577
+ function generateHomePage(moduleConfigs: ModuleBuildConfig[]) {
2578
+ // Chercher une page avec section: "public" et path: "/"
2579
+ let homePageModule: ModuleBuildConfig | null = null;
2580
+ let homePageConfig: ModulePageConfig | null = null;
2581
+
2582
+ for (const moduleConfig of moduleConfigs) {
2583
+ const homePage = moduleConfig.pages.find(
2584
+ (page) => page.section === "public" && page.path === "/"
2585
+ );
2586
+ if (homePage) {
2587
+ homePageModule = moduleConfig;
2588
+ homePageConfig = homePage;
2589
+ break;
2590
+ }
2591
+ }
2592
+
2593
+ if (!homePageModule || !homePageConfig) {
2594
+ if (isDebugMode) {
2595
+ console.log(" ℹ️ No homepage (/) definition found in modules");
2596
+ }
2597
+ return;
2598
+ }
2599
+
2600
+ // Générer la home dans le segment public: app/[lang]/(public)/page.tsx
2601
+ const homePageDir = path.join(appDirectory, "[lang]", "(public)");
2602
+ const homePagePath = path.join(homePageDir, "page.tsx");
2603
+ ensureDirectory(homePageDir);
2604
+
2605
+ // Déterminer le chemin d'import
2606
+ const importPath = homePageConfig.entryPoint
2607
+ ? `${homePageModule.moduleName}/${homePageConfig.entryPoint}`
2608
+ : homePageModule.moduleName;
2609
+
2610
+ // Éviter les collisions de nom (HomePage importé et wrapper exporté)
2611
+ const importComponentName = homePageConfig.componentExport;
2612
+ const wrapperComponentName = `${importComponentName}AppWrapper`;
2613
+ const metadataImportAlias = homePageConfig.metadataExport
2614
+ ? `${homePageConfig.metadataExport} as ${homePageConfig.metadataExport}Original`
2615
+ : "";
2616
+
2617
+ const content = `// GENERATED BY LASTBRAIN MODULE BUILD
2618
+ // Homepage from ${homePageModule.moduleName}
2619
+ import { ${importComponentName} as ModuleHomePage${homePageConfig.metadataExport ? `, ${metadataImportAlias}` : ""} } from "${importPath}";
2620
+
2621
+ import { LOCALE_CONFIG } from "../../../config/locales.generated";
2622
+ ${
2623
+ homePageConfig.metadataExport
2624
+ ? `\nexport async function generateMetadata(props: any) {
2625
+ return ${homePageConfig.metadataExport}Original({ ...props, localeConfig: LOCALE_CONFIG });
2626
+ }`
2627
+ : ""
2628
+ }
2629
+ export default function ${wrapperComponentName}({
2630
+ params,
2631
+ }: {
2632
+ params: Promise<{ lang: string }>;
2633
+ }) {
2634
+ return <ModuleHomePage params={params} localeConfig={LOCALE_CONFIG} />;
2635
+ }
2636
+ `;
2637
+
2638
+ fs.writeFileSync(homePagePath, content, "utf-8");
2639
+
2640
+ if (isDebugMode) {
2641
+ console.log(
2642
+ ` ✅ Generated homepage: ${homePagePath} (from ${homePageModule.moduleName})`
2643
+ );
2644
+ } else {
2645
+ console.log(`🏠 Generated homepage from ${homePageModule.moduleName}`);
2646
+ }
2647
+ }
2648
+
1827
2649
  export async function runModuleBuild() {
1828
2650
  ensureDirectory(appDirectory);
1829
2651
 
@@ -1886,10 +2708,17 @@ export async function runModuleBuild() {
1886
2708
  dumpNavigation();
1887
2709
  generateMenuConfig(moduleConfigs);
1888
2710
  generateDocsPage(moduleConfigs);
1889
- generateAppAside();
1890
- generateLayouts();
2711
+ // Note: AppAside, AppHeader, etc. sont maintenant générés dans components/ à la racine
2712
+ // et non plus dupliqués dans app/components/
2713
+ generateLayouts(moduleConfigs);
1891
2714
  copyModuleMigrations(moduleConfigs);
1892
2715
 
2716
+ // Générer la page d'accueil "/" si un module la définit
2717
+ if (isDebugMode) {
2718
+ console.log("🏠 Checking for homepage (/) definition...");
2719
+ }
2720
+ generateHomePage(moduleConfigs);
2721
+
1893
2722
  // Générer la configuration realtime
1894
2723
  if (isDebugMode) {
1895
2724
  console.log("🔄 Generating realtime configuration...");
@@ -1902,6 +2731,12 @@ export async function runModuleBuild() {
1902
2731
  }
1903
2732
  await generateUserTabsConfig(moduleConfigs);
1904
2733
 
2734
+ // Générer la configuration et la page auth dashboard
2735
+ if (isDebugMode) {
2736
+ console.log("📊 Generating auth dashboard configuration...");
2737
+ }
2738
+ await generateAuthDashboard(moduleConfigs);
2739
+
1905
2740
  // Générer la configuration des buckets storage
1906
2741
  if (isDebugMode) {
1907
2742
  console.log("🗄️ Generating storage buckets configuration...");
@@ -1920,12 +2755,37 @@ export async function runModuleBuild() {
1920
2755
  }
1921
2756
  await generateFooterConfig(moduleConfigs);
1922
2757
 
2758
+ // Générer les routes sitemap (ancien système)
2759
+ if (isDebugMode) {
2760
+ console.log("🗺️ Generating sitemap routes (legacy)...");
2761
+ }
2762
+ //await generateSitemapRoutes(moduleConfigs);
2763
+
2764
+ // Générer les sitemaps basés sur manifests (nouveau système)
2765
+ if (isDebugMode) {
2766
+ console.log("🗺️ Generating manifest-based sitemaps...");
2767
+ }
2768
+ const availableLanguages = Object.keys(LOCALE_MAP);
2769
+ await generateManifestBasedSitemaps(
2770
+ appDirectory,
2771
+ moduleConfigs,
2772
+ availableLanguages,
2773
+ projectRequire,
2774
+ isDebugMode
2775
+ );
2776
+
1923
2777
  // Générer les fichiers i18n
1924
2778
  if (isDebugMode) {
1925
2779
  console.log("🌍 Building i18n files...");
1926
2780
  }
1927
2781
  await generateI18nFiles();
1928
2782
 
2783
+ // Générer la configuration des locales
2784
+ if (isDebugMode) {
2785
+ console.log("🌐 Generating locale configuration...");
2786
+ }
2787
+ await generateLocaleConfig();
2788
+
1929
2789
  // Message de succès final
1930
2790
  if (!isDebugMode) {
1931
2791
  console.log("\n✅ Module build completed successfully!");