@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,6 +1,12 @@
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
+ const LOCALE_MAP = {
6
+ fr: "fr_FR",
7
+ en: "en_US",
8
+ es: "es_ES",
9
+ };
4
10
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
5
11
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
6
12
  // Si on est dans une app, monter jusqu'à la racine du monorepo
@@ -186,7 +192,7 @@ function buildPage(moduleConfig, page) {
186
192
  page.path.includes("users/[id]") &&
187
193
  page.componentExport === "UserPage";
188
194
  // Détecter les pages légales qui utilisent cookies (privacy, terms, returns)
189
- const isLegalPage = page.section === "public" &&
195
+ const _isLegalPage = page.section === "public" &&
190
196
  (page.path.includes("privacy") ||
191
197
  page.path.includes("terms") ||
192
198
  page.path.includes("returns"));
@@ -196,6 +202,7 @@ function buildPage(moduleConfig, page) {
196
202
  // On importe directement depuis app/config au lieu de passer via props
197
203
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
198
204
  import { UserDetailPage } from "${moduleConfig.moduleName}";
205
+ import { logger } from "@lastbrain/core";
199
206
 
200
207
  interface UserPageProps { params: Promise<{ id: string }> }
201
208
 
@@ -205,7 +212,7 @@ async function getModuleUserTabs() {
205
212
  const { moduleUserTabs } = await import("../../../../../../config/user-tabs");
206
213
  return moduleUserTabs || [];
207
214
  } catch (e) {
208
- console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
215
+ logger.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
209
216
  return [];
210
217
  }
211
218
  }
@@ -228,17 +235,6 @@ const ${page.componentExport} = dynamic(
228
235
  { ssr: false }
229
236
  );
230
237
 
231
- export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
232
- return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
233
- }
234
- `;
235
- }
236
- else if (isLegalPage) {
237
- content = `// GENERATED BY LASTBRAIN MODULE BUILD
238
- import { ${page.componentExport} } from "${moduleConfig.moduleName}";
239
-
240
- export const dynamic = 'force-dynamic';
241
-
242
238
  export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
243
239
  return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
244
240
  }
@@ -256,7 +252,7 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
256
252
  .filter((seg) => seg.startsWith("[") && seg.endsWith("]"))
257
253
  .map((seg) => seg.slice(1, -1)); // Enlève les []
258
254
  // Détecter si le module path lui-même a des params dynamiques (pas seulement [lang])
259
- const moduleHasDynamicParams = segments.some((seg) => seg.startsWith("[") && seg.endsWith("]"));
255
+ const _moduleHasDynamicParams = segments.some((seg) => seg.startsWith("[") && seg.endsWith("]"));
260
256
  // Générer le type des params si nécessaire
261
257
  const paramsType = dynamicParamNames.length > 0
262
258
  ? `{ ${dynamicParamNames.map((name) => `${name}: string`).join("; ")} }`
@@ -266,18 +262,47 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
266
262
  : "()";
267
263
  // Détecter si c'est un Server Component (entryPoint: "server")
268
264
  const isServerComponent = page.entryPoint === "server";
269
- // Les Server Components reçoivent params en props SEULEMENT si le module lui-même a des params
265
+ // Les Server Components reçoivent params en props si la page a des params dynamiques
270
266
  // Les Client Components utilisent useParams() en interne
271
- const componentProps = isServerComponent && moduleHasDynamicParams ? "params={params}" : "";
267
+ const componentProps = isServerComponent && hasDynamicParams ? "params={params}" : "";
272
268
  // Si params existe mais n'est pas passé au composant, on doit le unwrap quand même
273
269
  const awaitParams = hasDynamicParams && !componentProps
274
270
  ? "await params; // Unwrap params for Next.js 15\n "
275
271
  : "";
272
+ // Calculer le chemin relatif correct pour locales.generated
273
+ // routeDir = appDirectory + sectionPath + segments
274
+ // on doit remonter de (sectionPath.length + segments.length) niveaux pour atteindre appDirectory
275
+ // puis accéder à config/locales.generated
276
+ const pathDepth = sectionPath.length + segments.length;
277
+ const localeImportPath = Array(pathDepth + 1)
278
+ .fill("..")
279
+ .join("/") + "/config/locales.generated";
280
+ // Pour les pages publiques (section: "public"), importer et passer LOCALE_CONFIG
281
+ const localeImport = page.section === "public"
282
+ ? `\nimport { LOCALE_CONFIG } from "${localeImportPath}";\n`
283
+ : "";
284
+ const localeProps = page.section === "public" && isServerComponent
285
+ ? "localeConfig={LOCALE_CONFIG}"
286
+ : "";
287
+ const allComponentProps = [componentProps, localeProps]
288
+ .filter(Boolean)
289
+ .join(" ");
290
+ // Pour les pages publiques avec metadataExport, créer une fonction wrapper qui passe LOCALE_CONFIG
291
+ // On utilise un alias pour l'import afin d'éviter le conflit de nom avec la fonction exportée
292
+ const metadataImportAlias = page.metadataExport
293
+ ? `${page.metadataExport} as ${page.metadataExport}Original`
294
+ : "";
295
+ const metadataExport = page.section === "public" && page.metadataExport && localeImport
296
+ ? `\nexport async function generateMetadata(props: any) {
297
+ return ${page.metadataExport}Original({ ...props, localeConfig: LOCALE_CONFIG });
298
+ }`
299
+ : page.metadataExport
300
+ ? `\nexport { ${page.metadataExport}Original as generateMetadata };`
301
+ : "";
276
302
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
277
- import { ${page.componentExport} } from "${importPath}";
278
- ${page.metadataExport ? `\nexport { ${page.metadataExport} as generateMetadata } from "${importPath}";\n` : ""}
303
+ import { ${page.componentExport}${page.metadataExport ? `, ${metadataImportAlias}` : ""} } from "${importPath}";${localeImport}${metadataExport}
279
304
  export default ${hasDynamicParams ? "async " : ""}function ${wrapperName}${propsSignature} {
280
- ${awaitParams}return <${page.componentExport} ${componentProps} />;
305
+ ${awaitParams}return <${page.componentExport} ${allComponentProps} />;
281
306
  }
282
307
  `;
283
308
  }
@@ -408,10 +433,12 @@ export interface MenuItem {
408
433
  shortcut?: string;
409
434
  shortcutDisplay?: string;
410
435
  type?: 'text' | 'icon' | 'textIcon';
436
+ keyboardOnly?: boolean;
411
437
  position?: 'center' | 'end';
412
438
  component?: React.ComponentType<any>;
413
439
  componentExport?: string;
414
440
  entryPoint?: string;
441
+ badge?: string;
415
442
  }
416
443
 
417
444
  export interface MenuConfig {
@@ -507,7 +534,7 @@ function copyModuleMigrations(moduleConfigs) {
507
534
  fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
508
535
  }
509
536
  function generateDocsPage(moduleConfigs) {
510
- const docsDir = path.join(appDirectory, "docs");
537
+ const docsDir = path.join(appDirectory, "[lang]", "(public)", "docs");
511
538
  ensureDirectory(docsDir);
512
539
  const docsPagePath = path.join(docsDir, "page.tsx");
513
540
  // Charger tous les modules depuis modules.json (actifs ET inactifs)
@@ -527,13 +554,13 @@ function generateDocsPage(moduleConfigs) {
527
554
  const moduleConfigurations = [];
528
555
  allModules.forEach((moduleEntry) => {
529
556
  const moduleName = moduleEntry.package;
530
- // Extraire le nom du module sans le scope et sans "module-"
557
+ // Extraire le nom du module sans le scope et sans "-pro"
531
558
  // Ex: @lastbrain/module-auth -> auth
532
- // Ex: @lastbrain-labs/module-recipes-pro -> recipes
559
+ // Ex: @lastbrain-labs/module-metrics-pro -> metrics
533
560
  const moduleId = moduleName
534
561
  .replace("@lastbrain-labs/module-", "")
535
562
  .replace("@lastbrain/module-", "")
536
- .replace(/-pro$/, ""); // Retirer le suffix -pro pour avoir le nom de base
563
+ .replace(/-pro$/, ""); // Retirer le suffixe -pro
537
564
  const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
538
565
  // Trouver la config du module pour obtenir la description
539
566
  const moduleConfig = moduleConfigs.find((mc) => mc.moduleName === moduleName);
@@ -576,6 +603,43 @@ ${moduleConfigurations.join(",\n")}
576
603
  if (isDebugMode) {
577
604
  console.log(`📚 Generated docs page: ${docsPagePath}`);
578
605
  }
606
+ // Generate docs layout with translation support
607
+ const docsLayoutPath = path.join(docsDir, "layout.tsx");
608
+ const docsLayoutContent = `// Auto-generated docs layout with translation support
609
+ import { loadTranslations } from "@lastbrain/app/i18n/server-lang";
610
+ import { ClientLayout } from "../../../../components/ClientLayout";
611
+ import { headers } from "next/headers";
612
+
613
+ // Extract language from request headers
614
+ async function getLanguageFromRequest(): Promise<string> {
615
+ const headersList = await headers();
616
+ const acceptLanguage = headersList.get("accept-language") || "";
617
+ // Detect if French is requested
618
+ if (acceptLanguage.toLowerCase().includes("fr")) {
619
+ return "fr";
620
+ }
621
+ return "en";
622
+ }
623
+
624
+ export default async function DocsLayout({
625
+ children,
626
+ }: {
627
+ children: React.ReactNode;
628
+ }) {
629
+ const lang = await getLanguageFromRequest();
630
+ const translations = await loadTranslations(lang);
631
+
632
+ return (
633
+ <ClientLayout lang={lang} translations={translations}>
634
+ <div className="min-h-screen pt-12">{children}</div>
635
+ </ClientLayout>
636
+ );
637
+ }
638
+ `;
639
+ fs.writeFileSync(docsLayoutPath, docsLayoutContent);
640
+ if (isDebugMode) {
641
+ console.log(`📚 Generated docs layout: ${docsLayoutPath}`);
642
+ }
579
643
  }
580
644
  function buildGroupedApi(apis, routePath) {
581
645
  const segments = routePath.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -627,7 +691,8 @@ function cleanGeneratedFiles() {
627
691
  "page.tsx", // Page racine seulement
628
692
  "admin/page.tsx", // Page admin racine
629
693
  "admin/layout.tsx", // Layout admin racine
630
- "docs/page.tsx", // Page docs générée
694
+ "[lang]/(public)/docs/page.tsx", // Page docs générée
695
+ "[lang]/(public)/docs/layout.tsx", // Layout docs généré
631
696
  // Middleware et autres fichiers core
632
697
  "middleware.ts",
633
698
  // Dossiers de lib et config
@@ -697,7 +762,14 @@ function cleanGeneratedFiles() {
697
762
  }
698
763
  };
699
764
  // Nettoyer les dossiers de sections
700
- const sectionsToClean = ["(public)", "auth", "admin", "api", "[lang]"];
765
+ const sectionsToClean = [
766
+ "(public)",
767
+ "auth",
768
+ "admin",
769
+ "api",
770
+ "[lang]",
771
+ "sitemap",
772
+ ];
701
773
  sectionsToClean.forEach((section) => {
702
774
  const sectionPath = path.join(appDirectory, section);
703
775
  if (fs.existsSync(sectionPath)) {
@@ -766,7 +838,7 @@ function cleanOldRoutesWithoutLang() {
766
838
  }
767
839
  });
768
840
  }
769
- function generateAppAside() {
841
+ function _generateAppAside() {
770
842
  const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
771
843
  const templateContent = `"use client";
772
844
 
@@ -798,9 +870,11 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
798
870
  console.error(`❌ Error generating AppAside component: ${error}`);
799
871
  }
800
872
  }
801
- function generateLayouts() {
802
- // Générer ClientLayout wrapper client
803
- const clientLayoutPath = path.join(appDirectory, "components", "ClientLayout.tsx");
873
+ function generateLayouts(moduleConfigs) {
874
+ // Vérifier si le module billing-pro est actif
875
+ const hasBillingModule = moduleConfigs.some((config) => config.moduleName === "@lastbrain-labs/module-billing-pro");
876
+ // Générer ClientLayout wrapper client (dans components/ racine)
877
+ const clientLayoutPath = path.join(projectRoot, "components", "ClientLayout.tsx");
804
878
  const clientLayoutContent = `"use client";
805
879
 
806
880
  import { HeroUIProvider } from "@heroui/system";
@@ -815,10 +889,12 @@ export function ClientLayout({
815
889
  children,
816
890
  lang = "fr",
817
891
  translations = {},
892
+ availableLanguages = ["fr", "en"],
818
893
  }: {
819
894
  children: ReactNode;
820
895
  lang?: Language;
821
896
  translations?: Record<string, string>;
897
+ availableLanguages?: string[];
822
898
  }) {
823
899
  const router = useLocalizedRouter();
824
900
 
@@ -830,7 +906,7 @@ export function ClientLayout({
830
906
  enableSystem={false}
831
907
  storageKey="lastbrain-theme"
832
908
  >
833
- <AppProviders lang={lang} translations={translations}>
909
+ <AppProviders lang={lang} translations={translations} availableLanguages={availableLanguages}>
834
910
  <AppHeader />
835
911
  <div className="min-h-screen text-foreground bg-background">
836
912
  {children}
@@ -846,29 +922,42 @@ export function ClientLayout({
846
922
  catch (error) {
847
923
  console.error(`❌ Error generating ClientLayout component: ${error}`);
848
924
  }
849
- // Générer AppProviders wrapper client
850
- const appProvidersPath = path.join(appDirectory, "components", "AppProviders.tsx");
925
+ // Générer AppProviders wrapper client (dans components/ racine)
926
+ const appProvidersPath = path.join(projectRoot, "components", "AppProviders.tsx");
927
+ // Import conditionnel de EntitlementsProvider
928
+ const entitlementsImport = hasBillingModule
929
+ ? `import { EntitlementsProvider } from "@lastbrain-labs/module-billing-pro";`
930
+ : "";
931
+ // Prop conditionnel pour EntitlementsProviderComponent
932
+ const entitlementsProp = hasBillingModule
933
+ ? `EntitlementsProviderComponent={EntitlementsProvider}`
934
+ : "";
851
935
  const appProvidersContent = `"use client";
852
936
 
853
937
  import { AppProviders as BaseAppProviders } from "@lastbrain/app";
854
- import { realtimeConfig } from "../../config/realtime";
938
+ ${entitlementsImport}
939
+ import { realtimeConfig } from "../config/realtime";
855
940
  import type { ReactNode } from "react";
856
941
  import type { Language } from "@lastbrain/core";
857
942
 
858
943
  export function AppProviders({
859
- children,
944
+ children,
860
945
  lang = "fr",
861
946
  translations = {},
947
+ availableLanguages = ["fr", "en"],
862
948
  }: {
863
949
  children: ReactNode;
864
950
  lang?: Language;
865
951
  translations?: Record<string, string>;
952
+ availableLanguages?: string[];
866
953
  }) {
867
954
  return (
868
955
  <BaseAppProviders
869
956
  realtimeConfig={realtimeConfig}
870
957
  lang={lang}
871
958
  translations={translations}
959
+ availableLanguages={availableLanguages}
960
+ ${entitlementsProp}
872
961
  >
873
962
  {children}
874
963
  </BaseAppProviders>
@@ -880,19 +969,33 @@ export function AppProviders({
880
969
  catch (error) {
881
970
  console.error(`❌ Error generating AppProviders component: ${error}`);
882
971
  }
883
- // Générer AppHeader component
884
- const appHeaderPath = path.join(appDirectory, "components", "AppHeader.tsx");
972
+ // Générer AppHeader component (dans components/ racine)
973
+ const appHeaderPath = path.join(projectRoot, "components", "AppHeader.tsx");
885
974
  const appHeaderContent = `"use client";
886
975
 
887
976
  import { Header } from "@lastbrain/ui";
888
- import { LanguageSwitcher } from "@lastbrain/app";
889
- import { menuConfig } from "../../config/menu";
977
+ import { LanguageSwitcher, useAuth } from "@lastbrain/app";
978
+ import { menuConfig } from "../config/menu";
979
+ import { menuIgnored } from "../config/menu-ignored";
980
+ import { LOCALE_CONFIG } from "../config/locales.generated";
890
981
 
891
982
  export function AppHeader() {
983
+ const { user, loading, isSuperAdmin } = useAuth();
984
+
892
985
  return (
893
986
  <Header
987
+ user={user}
988
+ isSuperAdmin={isSuperAdmin}
894
989
  menuConfig={menuConfig}
895
- languageSwitcher={<LanguageSwitcher variant="minimal" />}
990
+ accountMenu={menuConfig.account}
991
+ menuIgnored={menuIgnored}
992
+ languageSwitcher={
993
+ <LanguageSwitcher
994
+ variant="minimal"
995
+ availableLanguages={[...LOCALE_CONFIG.languages]}
996
+ localeMap={LOCALE_CONFIG.localeMap}
997
+ />
998
+ }
896
999
  />
897
1000
  );
898
1001
  }`;
@@ -908,6 +1011,27 @@ export function AppHeader() {
908
1011
  const langLayoutContent = `import { loadTranslations } from "@lastbrain/app/i18n/server-lang";
909
1012
  import { ClientLayout } from "../../components/ClientLayout";
910
1013
 
1014
+ // Fallback default config in case locales.generated.ts is not available
1015
+ const DEFAULT_LOCALE_CONFIG = {
1016
+ languages: ["fr", "en"] as const,
1017
+ locales: ["fr_FR", "en_US"],
1018
+ localeMap: { fr: "fr_FR", en: "en_US" } as Record<string, string>,
1019
+ };
1020
+
1021
+ // Type for valid languages derived from config
1022
+ type ValidLanguage = (typeof DEFAULT_LOCALE_CONFIG.languages)[number];
1023
+
1024
+ // Helper function to safely load locale config
1025
+ async function getLocaleConfig() {
1026
+
1027
+ const importedConfig = await import("../../config/locales.generated");
1028
+ if (importedConfig?.LOCALE_CONFIG && importedConfig.LOCALE_CONFIG.languages) {
1029
+ return importedConfig.LOCALE_CONFIG;
1030
+ }
1031
+
1032
+ return DEFAULT_LOCALE_CONFIG;
1033
+ }
1034
+
911
1035
  export default async function LangLayout({
912
1036
  children,
913
1037
  params,
@@ -916,14 +1040,23 @@ export default async function LangLayout({
916
1040
  params: Promise<{ lang: string }>;
917
1041
  }) {
918
1042
  const { lang } = await params;
919
- const validLang = lang === "en" || lang === "fr" ? lang : "fr";
1043
+
1044
+ // Get the locale config (either from app or fallback)
1045
+ const LOCALE_CONFIG = await getLocaleConfig();
1046
+
1047
+ // Validate lang and cast to ValidLanguage type
1048
+ const isValidLang = (val: string): val is ValidLanguage => {
1049
+ return LOCALE_CONFIG.languages.includes(val as any);
1050
+ };
1051
+
1052
+ const validLang: ValidLanguage = isValidLang(lang) ? lang : "fr";
920
1053
 
921
1054
  // Charger les traductions pour cette langue
922
1055
  const translations = await loadTranslations(validLang);
923
1056
 
924
1057
  return (
925
- <ClientLayout lang={validLang} translations={translations}>
926
- <div className="min-h-screen pt-16">
1058
+ <ClientLayout lang={validLang} translations={translations} availableLanguages={[...LOCALE_CONFIG.languages]}>
1059
+ <div className="min-h-screen ">
927
1060
  {children}
928
1061
  </div>
929
1062
  </ClientLayout>
@@ -949,6 +1082,7 @@ export default async function LangLayout({
949
1082
 
950
1083
  import { AuthLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
951
1084
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1085
+ import { menuIgnored } from "../../../config/menu-ignored";
952
1086
 
953
1087
  export default function SectionLayout({
954
1088
  children,
@@ -969,7 +1103,7 @@ export default function SectionLayout({
969
1103
  };
970
1104
 
971
1105
  return (
972
- <AuthLayoutWithSidebar menuConfig={menuConfig}>
1106
+ <AuthLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
973
1107
  {children}
974
1108
  </AuthLayoutWithSidebar>
975
1109
  );
@@ -980,12 +1114,36 @@ export default function SectionLayout({
980
1114
  catch (error) {
981
1115
  console.error(`❌ Error generating auth layout: ${error}`);
982
1116
  }
1117
+ // Générer layout public avec footer
1118
+ const publicLayoutPath = path.join(appDirectory, "[lang]", "(public)", "layout.tsx");
1119
+ const publicLayoutContent = `"use client";
1120
+
1121
+ import type React from "react";
1122
+ import { PublicLayout } from "@lastbrain/app";
1123
+ import { footerConfig } from "../../../config/footer";
1124
+
1125
+ export default function PublicSectionLayout({
1126
+ children,
1127
+ }: {
1128
+ children: React.ReactNode;
1129
+ }) {
1130
+ return <PublicLayout footerConfig={footerConfig}>
1131
+ {children}
1132
+ </PublicLayout>;
1133
+ }`;
1134
+ try {
1135
+ writeScaffoldFile(publicLayoutPath, publicLayoutContent, "public layout with footer");
1136
+ }
1137
+ catch (error) {
1138
+ console.error(`❌ Error generating public layout: ${error}`);
1139
+ }
983
1140
  // Générer layout admin avec sidebar
984
1141
  const adminLayoutPath = path.join(appDirectory, "[lang]", "admin", "layout.tsx");
985
1142
  const adminLayoutContent = `"use client";
986
1143
 
987
1144
  import { AdminLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
988
1145
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1146
+ import { menuIgnored } from "../../../config/menu-ignored";
989
1147
 
990
1148
  export default function AdminLayout({
991
1149
  children,
@@ -1006,7 +1164,7 @@ export default function AdminLayout({
1006
1164
  };
1007
1165
 
1008
1166
  return (
1009
- <AdminLayoutWithSidebar menuConfig={menuConfig}>
1167
+ <AdminLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1010
1168
  {children}
1011
1169
  </AdminLayoutWithSidebar>
1012
1170
  );
@@ -1086,7 +1244,11 @@ async function generateUserTabsConfig(moduleConfigs) {
1086
1244
  else {
1087
1245
  // Générer les imports statiques (Next/dynamic pour chaque composant)
1088
1246
  const importsForApp = userTabsConfigs
1089
- .map((tab) => `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`)
1247
+ .map((tab) => {
1248
+ // UserTokenTab est un Client Component, utiliser ssr: false
1249
+ const ssrMode = tab.componentExport === "UserTokenTab" ? "false" : "true";
1250
+ return `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: ${ssrMode} });`;
1251
+ })
1090
1252
  .join("\n");
1091
1253
  // Générer le tableau des tabs
1092
1254
  const tabsArray = userTabsConfigs
@@ -1123,6 +1285,131 @@ async function generateUserTabsConfig(moduleConfigs) {
1123
1285
  console.error("❌ Error generating user tabs configuration:", error);
1124
1286
  }
1125
1287
  }
1288
+ async function generateAuthDashboard(moduleConfigs) {
1289
+ try {
1290
+ const dashboards = moduleConfigs
1291
+ .flatMap((config) => (config.authDashboard ?? []).map((dashboard) => ({
1292
+ ...dashboard,
1293
+ moduleName: config.moduleName,
1294
+ })))
1295
+ .sort((a, b) => (a.order ?? 999) - (b.order ?? 999));
1296
+ const timestamp = new Date().toISOString();
1297
+ let configContent;
1298
+ if (dashboards.length === 0) {
1299
+ configContent = `// GENERATED FILE - DO NOT EDIT MANUALLY
1300
+ // Auth dashboard configuration
1301
+ // Generated at: ${timestamp}
1302
+
1303
+ "use client";
1304
+
1305
+ import type React from "react";
1306
+
1307
+ export interface ModuleAuthDashboard {
1308
+ key: string;
1309
+ title: string;
1310
+ icon?: string;
1311
+ order?: number;
1312
+ component: React.ComponentType<Record<string, never>>;
1313
+ }
1314
+
1315
+ export const moduleAuthDashboards: ModuleAuthDashboard[] = [];
1316
+
1317
+ export default moduleAuthDashboards;
1318
+ `;
1319
+ }
1320
+ else {
1321
+ const dynamicImports = dashboards
1322
+ .map((dashboard, index) => `const AuthDashboardComponent${index} = dynamic(() => import("${dashboard.moduleName}").then(mod => ({ default: (mod as any)["${dashboard.componentExport}"] })), { ssr: true });`)
1323
+ .join("\n");
1324
+ const dashboardsArray = dashboards
1325
+ .map((dashboard, index) => ` {
1326
+ key: "${dashboard.key}",
1327
+ title: "${dashboard.title}",
1328
+ icon: "${dashboard.icon ?? ""}",
1329
+ order: ${dashboard.order ?? 999},
1330
+ component: AuthDashboardComponent${index},
1331
+ }`)
1332
+ .join(",\n");
1333
+ configContent = `// GENERATED FILE - DO NOT EDIT MANUALLY
1334
+ // Auth dashboard configuration
1335
+ // Generated at: ${timestamp}
1336
+
1337
+ "use client";
1338
+
1339
+ import dynamic from "next/dynamic";
1340
+ import type React from "react";
1341
+
1342
+ ${dynamicImports}
1343
+
1344
+ export interface ModuleAuthDashboard {
1345
+ key: string;
1346
+ title: string;
1347
+ icon?: string;
1348
+ order?: number;
1349
+ component: React.ComponentType<Record<string, never>>;
1350
+ }
1351
+
1352
+ export const moduleAuthDashboards: ModuleAuthDashboard[] = [
1353
+ ${dashboardsArray}
1354
+ ];
1355
+
1356
+ export default moduleAuthDashboards;
1357
+ `;
1358
+ }
1359
+ const configPath = path.join(projectRoot, "config", "auth-dashboard.ts");
1360
+ ensureDirectory(path.dirname(configPath));
1361
+ fs.writeFileSync(configPath, configContent);
1362
+ if (isDebugMode) {
1363
+ console.log(`✅ Generated auth dashboard configuration: ${configPath} (${dashboards.length} item(s))`);
1364
+ }
1365
+ const pagePath = path.join(appDirectory, "[lang]", "auth", "dashboard", "page.tsx");
1366
+ ensureDirectory(path.dirname(pagePath));
1367
+ const pageContent = `// GENERATED BY LASTBRAIN MODULE BUILD
1368
+ "use client";
1369
+
1370
+ import { Suspense } from "react";
1371
+ import { moduleAuthDashboards } from "../../../../config/auth-dashboard";
1372
+
1373
+ export const dynamic = "force-dynamic";
1374
+
1375
+
1376
+ export default function AuthDashboardPage() {
1377
+ const dashboards = moduleAuthDashboards || [];
1378
+
1379
+ if (dashboards.length === 0) {
1380
+ return <div className="space-y-4">Aucun dashboard disponible.</div>;
1381
+ }
1382
+
1383
+ return (
1384
+ <div className="space-y-6">
1385
+ {dashboards.map((dashboard) => {
1386
+
1387
+ const Component = dashboard.component;
1388
+
1389
+ return (
1390
+ <section
1391
+ key={dashboard.key}
1392
+
1393
+ >
1394
+ <Suspense fallback={<div>Chargement...</div>}>
1395
+ <Component />
1396
+ </Suspense>
1397
+ </section>
1398
+ );
1399
+ })}
1400
+ </div>
1401
+ );
1402
+ }
1403
+ `;
1404
+ fs.writeFileSync(pagePath, pageContent);
1405
+ if (isDebugMode) {
1406
+ console.log(`🧭 Generated auth dashboard page: ${pagePath}`);
1407
+ }
1408
+ }
1409
+ catch (error) {
1410
+ console.error("❌ Error generating auth dashboard configuration:", error);
1411
+ }
1412
+ }
1126
1413
  async function generateBucketsConfig(moduleConfigs) {
1127
1414
  try {
1128
1415
  // Extraire les configurations storage des modules
@@ -1303,7 +1590,6 @@ export async function GET(
1303
1590
  .createSignedUrl(storagePath, 3600); // 1 heure
1304
1591
 
1305
1592
  if (error) {
1306
- console.error(\`[storage] Error creating signed URL for public image:\`, error);
1307
1593
  return new NextResponse("Not found", { status: 404 });
1308
1594
  }
1309
1595
 
@@ -1324,16 +1610,23 @@ export async function GET(
1324
1610
 
1325
1611
  // Cas spécial: si le chemin commence par /product/ ou /recipe/,
1326
1612
  // c'est une image avec format court qui nécessite le préfixe userId
1613
+ // Sinon le chemin contient déjà le userId
1327
1614
  let actualStoragePath = storagePath;
1328
-
1329
- if (storagePath.startsWith("product/") || storagePath.startsWith("recipe/")) {
1615
+ const pathParts = storagePath.split("/");
1616
+
1617
+ // Vérifier si le premier segment est un UUID (userId déjà présent)
1618
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1619
+ const firstSegmentIsUuid = uuidRegex.test(pathParts[0]);
1620
+
1621
+ if (!firstSegmentIsUuid) {
1622
+ // Le chemin ne contient pas le userId, on l'ajoute
1330
1623
  actualStoragePath = \`\${user.id}/\${storagePath}\`;
1331
1624
  }
1332
1625
 
1333
1626
  // Vérifier que l'utilisateur a accès à cette image
1334
1627
  // Format: {userId}/recipe/{recipeId}/{filename} ou {userId}/product/{productId}/{filename}
1335
- const pathParts = actualStoragePath.split("/");
1336
- const pathUserId = pathParts[0];
1628
+ const actualPathParts = actualStoragePath.split("/");
1629
+ const pathUserId = actualPathParts[0];
1337
1630
 
1338
1631
  if (pathUserId !== user.id) {
1339
1632
  return new NextResponse("Forbidden", { status: 403 });
@@ -1342,17 +1635,31 @@ export async function GET(
1342
1635
  // Créer une URL signée pour l'image privée
1343
1636
  const { data, error } = await supabase.storage
1344
1637
  .from(bucket)
1345
- .createSignedUrl(actualStoragePath, 3600); // 1 heure
1638
+ .createSignedUrl(actualStoragePath, 60); // 1 minute seulement
1346
1639
 
1347
1640
  if (error) {
1348
- console.error(\`[storage] Error creating signed URL:\`, error);
1349
1641
  return new NextResponse("Not found", { status: 404 });
1350
1642
  }
1351
1643
 
1352
- // Rediriger vers l'URL signée
1353
- return NextResponse.redirect(data.signedUrl);
1644
+ // Télécharger l'image et la retourner avec des headers no-cache
1645
+ const imageResponse = await fetch(data.signedUrl);
1646
+ if (!imageResponse.ok) {
1647
+ return new NextResponse("Not found", { status: 404 });
1648
+ }
1649
+
1650
+ const imageBuffer = await imageResponse.arrayBuffer();
1651
+ const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
1652
+
1653
+ return new NextResponse(imageBuffer, {
1654
+ status: 200,
1655
+ headers: {
1656
+ "Content-Type": contentType,
1657
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1658
+ "Pragma": "no-cache",
1659
+ "Expires": "0",
1660
+ },
1661
+ });
1354
1662
  } catch (error) {
1355
- console.error("[storage] Error:", error);
1356
1663
  return new NextResponse("Internal server error", { status: 500 });
1357
1664
  }
1358
1665
  }
@@ -1402,8 +1709,6 @@ async function generateFooterConfig(moduleConfigs) {
1402
1709
  import type { FooterConfig } from "@lastbrain/ui";
1403
1710
 
1404
1711
  export const footerConfig: FooterConfig = {
1405
- companyName: "LastBrain",
1406
- companyDescription: "Plateforme de développement rapide d'applications",
1407
1712
  links: ${JSON.stringify(allFooterLinks, null, 2)},
1408
1713
  social: [],
1409
1714
  };
@@ -1421,11 +1726,316 @@ export const footerConfig: FooterConfig = {
1421
1726
  console.error("❌ Error generating footer config:", error);
1422
1727
  }
1423
1728
  }
1729
+ /**
1730
+ * Génère les routes sitemap à partir des configurations des modules
1731
+ */
1732
+ async function generateSitemapRoutes(moduleConfigs) {
1733
+ try {
1734
+ // Collecter tous les sitemaps de tous les modules
1735
+ const allSitemaps = [];
1736
+ moduleConfigs.forEach((config) => {
1737
+ if (config.sitemaps && config.sitemaps.length > 0) {
1738
+ config.sitemaps.forEach((sitemap) => {
1739
+ allSitemaps.push({ moduleConfig: config, sitemap });
1740
+ });
1741
+ }
1742
+ });
1743
+ if (allSitemaps.length === 0) {
1744
+ if (isDebugMode) {
1745
+ console.log("⏭️ No sitemaps found, skipping sitemap generation");
1746
+ }
1747
+ return;
1748
+ }
1749
+ // Détecter les sitemaps index des modules (sitemap.xml ou sitemap-index)
1750
+ const moduleSitemapIndexes = [];
1751
+ // Renommer les /sitemap.xml en /sitemap-{moduleName}.xml pour éviter les conflits
1752
+ allSitemaps.forEach(({ moduleConfig, sitemap }) => {
1753
+ if (sitemap.path === "/sitemap.xml" ||
1754
+ sitemap.path.endsWith("-index.xml")) {
1755
+ // Extraire le nom court du module (ex: module-blog -> blog)
1756
+ const moduleShortName = moduleConfig.moduleName
1757
+ .replace("@lastbrain/", "")
1758
+ .replace("@lastbrain-labs/", "")
1759
+ .replace("module-", "")
1760
+ .replace("module-core-", "");
1761
+ // Renommer le path
1762
+ sitemap.path = `/sitemap-${moduleShortName}.xml`;
1763
+ moduleSitemapIndexes.push(sitemap.path);
1764
+ if (isDebugMode) {
1765
+ console.log(`🔄 Renamed sitemap from /sitemap.xml to ${sitemap.path} for ${moduleConfig.moduleName}`);
1766
+ }
1767
+ }
1768
+ });
1769
+ // Générer chaque route sitemap
1770
+ for (const { moduleConfig, sitemap } of allSitemaps) {
1771
+ const sitemapPath = sitemap.path;
1772
+ // Déterminer le chemin du fichier route.ts
1773
+ let routeDir;
1774
+ if (sitemapPath === "/sitemap.xml") {
1775
+ // Route principale sitemap.xml à la racine de app
1776
+ routeDir = path.join(appDirectory, "sitemap.xml");
1777
+ }
1778
+ else {
1779
+ // Sous-sitemaps (ex: /sitemap/recipes.xml -> app/sitemap/recipes.xml)
1780
+ const segments = sitemapPath
1781
+ .replace(/^\//, "")
1782
+ .replace(/\.xml$/, ".xml")
1783
+ .split("/");
1784
+ routeDir = path.join(appDirectory, ...segments);
1785
+ }
1786
+ const routeFilePath = path.join(routeDir, "route.ts");
1787
+ ensureDirectory(routeDir);
1788
+ // Déterminer le type de sitemap et générer le code approprié
1789
+ const entryPoint = sitemap.entryPoint;
1790
+ let content;
1791
+ if (entryPoint === "sitemap/index") {
1792
+ // Sitemap index principal
1793
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap Index Route
1794
+ // Module: ${moduleConfig.moduleName}
1795
+ // Path: ${sitemapPath}
1796
+ import { generateSitemapIndex } from "${moduleConfig.moduleName}/sitemap";
1797
+
1798
+ export async function GET(): Promise<Response> {
1799
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1800
+
1801
+ // Liste des sous-sitemaps
1802
+ const sitemapPaths = [
1803
+ "/sitemap/pages.xml",
1804
+ "/sitemap/recipes.xml",
1805
+ "/sitemap/chefs.xml",
1806
+ ];
1807
+
1808
+ const xml = generateSitemapIndex(baseUrl, sitemapPaths);
1809
+
1810
+ return new Response(xml, {
1811
+ headers: {
1812
+ "Content-Type": "application/xml",
1813
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1814
+ },
1815
+ });
1816
+ }
1817
+ `;
1818
+ }
1819
+ else if (entryPoint === "sitemap/pages") {
1820
+ // Sitemap des pages statiques
1821
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Static Pages Sitemap Route
1822
+ // Module: ${moduleConfig.moduleName}
1823
+ // Path: ${sitemapPath}
1824
+ import { generateStaticPagesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
1825
+
1826
+ export async function GET(): Promise<Response> {
1827
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1828
+
1829
+ const sitemap = await generateStaticPagesSitemap(baseUrl);
1830
+ const xml = generateSitemapXml(sitemap);
1831
+
1832
+ return new Response(xml, {
1833
+ headers: {
1834
+ "Content-Type": "application/xml",
1835
+ "Cache-Control": "public, max-age=86400, s-maxage=86400",
1836
+ },
1837
+ });
1838
+ }
1839
+ `;
1840
+ }
1841
+ else if (entryPoint === "sitemap/recipes") {
1842
+ // Sitemap des recettes
1843
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Recipes Sitemap Route
1844
+ // Module: ${moduleConfig.moduleName}
1845
+ // Path: ${sitemapPath}
1846
+ import { generateRecipesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
1847
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
1848
+
1849
+ export async function GET(): Promise<Response> {
1850
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1851
+ const supabase = await getSupabaseServiceClient();
1852
+
1853
+ const sitemap = await generateRecipesSitemap(baseUrl, supabase);
1854
+ const xml = generateSitemapXml(sitemap);
1855
+
1856
+ return new Response(xml, {
1857
+ headers: {
1858
+ "Content-Type": "application/xml",
1859
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1860
+ },
1861
+ });
1862
+ }
1863
+ `;
1864
+ }
1865
+ else if (entryPoint === "sitemap/chefs") {
1866
+ // Sitemap des chefs
1867
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Chefs Sitemap Route
1868
+ // Module: ${moduleConfig.moduleName}
1869
+ // Path: ${sitemapPath}
1870
+ import { generateChefsSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
1871
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
1872
+
1873
+ export async function GET(): Promise<Response> {
1874
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1875
+ const supabase = await getSupabaseServiceClient();
1876
+
1877
+ const sitemap = await generateChefsSitemap(baseUrl, supabase);
1878
+ const xml = generateSitemapXml(sitemap);
1879
+
1880
+ return new Response(xml, {
1881
+ headers: {
1882
+ "Content-Type": "application/xml",
1883
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1884
+ },
1885
+ });
1886
+ }
1887
+ `;
1888
+ }
1889
+ else {
1890
+ // Sitemap générique - export direct du handler
1891
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap route
1892
+ // Module: ${moduleConfig.moduleName}
1893
+ // Path: ${sitemapPath}
1894
+ export { ${sitemap.handlerExport} } from "${moduleConfig.moduleName}/${sitemap.entryPoint}";
1895
+ `;
1896
+ }
1897
+ fs.writeFileSync(routeFilePath, content);
1898
+ if (isDebugMode) {
1899
+ console.log(`🗺️ Generated sitemap route: ${routeFilePath}`);
1900
+ }
1901
+ }
1902
+ // Générer le sitemap.xml global qui agrège tous les sitemaps des modules
1903
+ if (moduleSitemapIndexes.length > 0) {
1904
+ const globalSitemapDir = path.join(appDirectory, "sitemap.xml");
1905
+ const globalSitemapFile = path.join(globalSitemapDir, "route.ts");
1906
+ ensureDirectory(globalSitemapDir);
1907
+ const sitemapUrls = moduleSitemapIndexes
1908
+ .map((path) => {
1909
+ return ` {
1910
+ url: \`\${baseUrl}${path}\`,
1911
+ lastModified: new Date(),
1912
+ }`;
1913
+ })
1914
+ .join(",\n");
1915
+ const globalSitemapContent = `// GENERATED BY LASTBRAIN MODULE BUILD - Global Sitemap Index
1916
+ // This file aggregates all module sitemaps
1917
+ // Module sitemaps: ${moduleSitemapIndexes.join(", ")}
1918
+
1919
+ export async function GET(): Promise<Response> {
1920
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1921
+
1922
+ const sitemapIndexXml = \`<?xml version="1.0" encoding="UTF-8"?>
1923
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1924
+ ${moduleSitemapIndexes
1925
+ .map((path) => ` <sitemap>
1926
+ <loc>\${baseUrl}${path}</loc>
1927
+ <lastmod>\${new Date().toISOString()}</lastmod>
1928
+ </sitemap>`)
1929
+ .join("\n")}
1930
+ </sitemapindex>\`;
1931
+
1932
+ return new Response(sitemapIndexXml, {
1933
+ headers: {
1934
+ "Content-Type": "application/xml",
1935
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1936
+ },
1937
+ });
1938
+ }
1939
+ `;
1940
+ fs.writeFileSync(globalSitemapFile, globalSitemapContent);
1941
+ if (isDebugMode) {
1942
+ console.log(`🌍 Generated global sitemap.xml aggregating ${moduleSitemapIndexes.length} module sitemap(s)`);
1943
+ }
1944
+ }
1945
+ if (isDebugMode) {
1946
+ console.log(`✅ Generated ${allSitemaps.length} sitemap route(s)`);
1947
+ }
1948
+ }
1949
+ catch (error) {
1950
+ console.error("❌ Error generating sitemap routes:", error);
1951
+ }
1952
+ }
1424
1953
  /**
1425
1954
  * Génère les fichiers i18n en concaténant les traductions des modules
1955
+ * et en incluant les traductions personnalisées de i18n/default
1956
+ */
1957
+ async function generateLocaleConfig() {
1958
+ const appI18nDir = path.join(projectRoot, "i18n");
1959
+ const appConfigDir = path.join(projectRoot, "config");
1960
+ try {
1961
+ // Scanner les fichiers i18n disponibles
1962
+ let languages = ["fr"]; // Fallback par défaut
1963
+ if (fs.existsSync(appI18nDir)) {
1964
+ const files = fs.readdirSync(appI18nDir);
1965
+ languages = files
1966
+ .filter((f) => f.endsWith(".json"))
1967
+ .map((f) => f.replace(".json", ""))
1968
+ .filter((lang) => LOCALE_MAP[lang])
1969
+ .sort();
1970
+ if (isDebugMode) {
1971
+ console.log(` 📋 Detected languages: ${languages.join(", ")}`);
1972
+ }
1973
+ }
1974
+ const locales = languages.map((lang) => LOCALE_MAP[lang]);
1975
+ const localeMap = languages.reduce((acc, lang) => {
1976
+ acc[lang] = LOCALE_MAP[lang];
1977
+ return acc;
1978
+ }, {});
1979
+ // Générer le fichier TypeScript dans l'app (ex: apps/recipe/config/)
1980
+ ensureDirectory(appConfigDir);
1981
+ // Générer le fichier dans config/
1982
+ const outputPath = path.join(appConfigDir, "locales.generated.ts");
1983
+ const content = `// This file is auto-generated by module-build.ts
1984
+ // Do not edit manually - regenerate with 'pnpm build:modules'
1985
+
1986
+ export interface LocaleConfig {
1987
+ languages: string[];
1988
+ locales: string[];
1989
+ localeMap: Record<string, string>;
1990
+ }
1991
+
1992
+ export const LOCALE_CONFIG: LocaleConfig = ${JSON.stringify({
1993
+ languages,
1994
+ locales,
1995
+ localeMap,
1996
+ }, null, 2)} as const;
1997
+
1998
+ /**
1999
+ * Convert language code to locale (e.g., "fr" -> "fr_FR")
2000
+ */
2001
+ export function langToLocale(lang: string): string {
2002
+ return LOCALE_CONFIG.localeMap[lang] || "fr_FR";
2003
+ }
2004
+
2005
+ /**
2006
+ * Get all locales except the given one
1426
2007
  */
2008
+ export function getAlternateLocales(lang: string): string[] {
2009
+ const currentLocale = LOCALE_CONFIG.localeMap[lang] || "fr_FR";
2010
+ return LOCALE_CONFIG.locales.filter((l) => l !== currentLocale);
2011
+ }
2012
+
2013
+ /**
2014
+ * Get locale map for metadata generation
2015
+ */
2016
+ export function getLocaleMap(): Record<string, string> {
2017
+ return LOCALE_CONFIG.localeMap;
2018
+ }
2019
+ `;
2020
+ writeScaffoldFile(outputPath, content, "locale configuration");
2021
+ // Générer aussi un fichier de ré-export dans le répertoire racine de l'app pour faciliter l'import
2022
+ const rootExportPath = path.join(projectRoot, "locale-helpers.ts");
2023
+ const rootExportContent = `// Re-export locale helpers from config for easy import in packages
2024
+ export { langToLocale, getAlternateLocales, getLocaleMap, LOCALE_CONFIG } from "./config/locales.generated";
2025
+ export type { LocaleConfig } from "./config/locales.generated";
2026
+ `;
2027
+ writeScaffoldFile(rootExportPath, rootExportContent, "locale helpers re-export");
2028
+ if (isDebugMode) {
2029
+ console.log(` ✅ Generated locale config with ${languages.length} language(s)`);
2030
+ }
2031
+ }
2032
+ catch (error) {
2033
+ console.error("❌ Error generating locale config:", error);
2034
+ }
2035
+ }
1427
2036
  async function generateI18nFiles() {
1428
2037
  const appI18nDir = path.join(projectRoot, "i18n");
2038
+ const appI18nDefaultDir = path.join(projectRoot, "i18n", "default");
1429
2039
  const packagesDir = path.join(_monorepoRoot, "packages");
1430
2040
  const translations = {
1431
2041
  fr: {},
@@ -1472,6 +2082,33 @@ async function generateI18nFiles() {
1472
2082
  console.warn(` ⚠️ Error reading ${file}:`, error);
1473
2083
  }
1474
2084
  }
2085
+ // Charger les traductions personnalisées de i18n/default
2086
+ if (fs.existsSync(appI18nDefaultDir)) {
2087
+ // Découvrir dynamiquement les fichiers de langue disponibles
2088
+ const defaultFiles = fs
2089
+ .readdirSync(appI18nDefaultDir)
2090
+ .filter((file) => file.endsWith(".json"));
2091
+ for (const defaultFile of defaultFiles) {
2092
+ const defaultFilePath = path.join(appI18nDefaultDir, defaultFile);
2093
+ if (fs.existsSync(defaultFilePath)) {
2094
+ const lang = defaultFile.replace(".json", "");
2095
+ if (!translations[lang]) {
2096
+ translations[lang] = {};
2097
+ }
2098
+ try {
2099
+ const content = JSON.parse(fs.readFileSync(defaultFilePath, "utf-8"));
2100
+ if (isDebugMode) {
2101
+ console.log(` ✓ app defaults (${lang})`);
2102
+ }
2103
+ // Ajouter ou surcharger les traductions personnalisées
2104
+ Object.assign(translations[lang], content);
2105
+ }
2106
+ catch (error) {
2107
+ console.warn(` ⚠️ Error reading ${defaultFilePath}:`, error);
2108
+ }
2109
+ }
2110
+ }
2111
+ }
1475
2112
  // Créer le dossier i18n s'il n'existe pas
1476
2113
  ensureDirectory(appI18nDir);
1477
2114
  // Écrire les fichiers de traduction
@@ -1500,6 +2137,67 @@ function extractModuleNameFromPath(filePath) {
1500
2137
  }
1501
2138
  return "unknown";
1502
2139
  }
2140
+ /**
2141
+ * Génère la page d'accueil app/[lang]/page.tsx si un module définit path: "/"
2142
+ */
2143
+ function generateHomePage(moduleConfigs) {
2144
+ // Chercher une page avec section: "public" et path: "/"
2145
+ let homePageModule = null;
2146
+ let homePageConfig = null;
2147
+ for (const moduleConfig of moduleConfigs) {
2148
+ const homePage = moduleConfig.pages.find((page) => page.section === "public" && page.path === "/");
2149
+ if (homePage) {
2150
+ homePageModule = moduleConfig;
2151
+ homePageConfig = homePage;
2152
+ break;
2153
+ }
2154
+ }
2155
+ if (!homePageModule || !homePageConfig) {
2156
+ if (isDebugMode) {
2157
+ console.log(" ℹ️ No homepage (/) definition found in modules");
2158
+ }
2159
+ return;
2160
+ }
2161
+ // Générer la home dans le segment public: app/[lang]/(public)/page.tsx
2162
+ const homePageDir = path.join(appDirectory, "[lang]", "(public)");
2163
+ const homePagePath = path.join(homePageDir, "page.tsx");
2164
+ ensureDirectory(homePageDir);
2165
+ // Déterminer le chemin d'import
2166
+ const importPath = homePageConfig.entryPoint
2167
+ ? `${homePageModule.moduleName}/${homePageConfig.entryPoint}`
2168
+ : homePageModule.moduleName;
2169
+ // Éviter les collisions de nom (HomePage importé et wrapper exporté)
2170
+ const importComponentName = homePageConfig.componentExport;
2171
+ const wrapperComponentName = `${importComponentName}AppWrapper`;
2172
+ const metadataImportAlias = homePageConfig.metadataExport
2173
+ ? `${homePageConfig.metadataExport} as ${homePageConfig.metadataExport}Original`
2174
+ : "";
2175
+ const content = `// GENERATED BY LASTBRAIN MODULE BUILD
2176
+ // Homepage from ${homePageModule.moduleName}
2177
+ import { ${importComponentName} as ModuleHomePage${homePageConfig.metadataExport ? `, ${metadataImportAlias}` : ""} } from "${importPath}";
2178
+
2179
+ import { LOCALE_CONFIG } from "../../../config/locales.generated";
2180
+ ${homePageConfig.metadataExport
2181
+ ? `\nexport async function generateMetadata(props: any) {
2182
+ return ${homePageConfig.metadataExport}Original({ ...props, localeConfig: LOCALE_CONFIG });
2183
+ }`
2184
+ : ""}
2185
+ export default function ${wrapperComponentName}({
2186
+ params,
2187
+ }: {
2188
+ params: Promise<{ lang: string }>;
2189
+ }) {
2190
+ return <ModuleHomePage params={params} localeConfig={LOCALE_CONFIG} />;
2191
+ }
2192
+ `;
2193
+ fs.writeFileSync(homePagePath, content, "utf-8");
2194
+ if (isDebugMode) {
2195
+ console.log(` ✅ Generated homepage: ${homePagePath} (from ${homePageModule.moduleName})`);
2196
+ }
2197
+ else {
2198
+ console.log(`🏠 Generated homepage from ${homePageModule.moduleName}`);
2199
+ }
2200
+ }
1503
2201
  export async function runModuleBuild() {
1504
2202
  ensureDirectory(appDirectory);
1505
2203
  // Nettoyer les fichiers générés précédemment
@@ -1548,9 +2246,15 @@ export async function runModuleBuild() {
1548
2246
  dumpNavigation();
1549
2247
  generateMenuConfig(moduleConfigs);
1550
2248
  generateDocsPage(moduleConfigs);
1551
- generateAppAside();
1552
- generateLayouts();
2249
+ // Note: AppAside, AppHeader, etc. sont maintenant générés dans components/ à la racine
2250
+ // et non plus dupliqués dans app/components/
2251
+ generateLayouts(moduleConfigs);
1553
2252
  copyModuleMigrations(moduleConfigs);
2253
+ // Générer la page d'accueil "/" si un module la définit
2254
+ if (isDebugMode) {
2255
+ console.log("🏠 Checking for homepage (/) definition...");
2256
+ }
2257
+ generateHomePage(moduleConfigs);
1554
2258
  // Générer la configuration realtime
1555
2259
  if (isDebugMode) {
1556
2260
  console.log("🔄 Generating realtime configuration...");
@@ -1561,6 +2265,11 @@ export async function runModuleBuild() {
1561
2265
  console.log("📑 Generating user tabs configuration...");
1562
2266
  }
1563
2267
  await generateUserTabsConfig(moduleConfigs);
2268
+ // Générer la configuration et la page auth dashboard
2269
+ if (isDebugMode) {
2270
+ console.log("📊 Generating auth dashboard configuration...");
2271
+ }
2272
+ await generateAuthDashboard(moduleConfigs);
1564
2273
  // Générer la configuration des buckets storage
1565
2274
  if (isDebugMode) {
1566
2275
  console.log("🗄️ Generating storage buckets configuration...");
@@ -1576,11 +2285,27 @@ export async function runModuleBuild() {
1576
2285
  console.log("🦶 Generating footer configuration...");
1577
2286
  }
1578
2287
  await generateFooterConfig(moduleConfigs);
2288
+ // Générer les routes sitemap (ancien système)
2289
+ if (isDebugMode) {
2290
+ console.log("🗺️ Generating sitemap routes (legacy)...");
2291
+ }
2292
+ //await generateSitemapRoutes(moduleConfigs);
2293
+ // Générer les sitemaps basés sur manifests (nouveau système)
2294
+ if (isDebugMode) {
2295
+ console.log("🗺️ Generating manifest-based sitemaps...");
2296
+ }
2297
+ const availableLanguages = Object.keys(LOCALE_MAP);
2298
+ await generateManifestBasedSitemaps(appDirectory, moduleConfigs, availableLanguages, projectRequire, isDebugMode);
1579
2299
  // Générer les fichiers i18n
1580
2300
  if (isDebugMode) {
1581
2301
  console.log("🌍 Building i18n files...");
1582
2302
  }
1583
2303
  await generateI18nFiles();
2304
+ // Générer la configuration des locales
2305
+ if (isDebugMode) {
2306
+ console.log("🌐 Generating locale configuration...");
2307
+ }
2308
+ await generateLocaleConfig();
1584
2309
  // Message de succès final
1585
2310
  if (!isDebugMode) {
1586
2311
  console.log("\n✅ Module build completed successfully!");