@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.
- package/dist/analytics/registry.d.ts +7 -0
- package/dist/analytics/registry.d.ts.map +1 -0
- package/dist/analytics/registry.js +11 -0
- package/dist/auth/useAuthSession.d.ts.map +1 -1
- package/dist/auth/useAuthSession.js +85 -1
- package/dist/cli.js +19 -3
- package/dist/components/LanguageSwitcher.d.ts +3 -1
- package/dist/components/LanguageSwitcher.d.ts.map +1 -1
- package/dist/components/LanguageSwitcher.js +134 -21
- package/dist/config/version.d.ts.map +1 -1
- package/dist/config/version.js +30 -19
- package/dist/i18n/server-lang.d.ts +1 -1
- package/dist/i18n/server-lang.d.ts.map +1 -1
- package/dist/i18n/types.d.ts +1 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/i18n/useLink.d.ts.map +1 -1
- package/dist/i18n/useLink.js +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/layouts/AdminLayoutWithSidebar.d.ts +3 -1
- package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AdminLayoutWithSidebar.js +2 -2
- package/dist/layouts/AppProviders.d.ts +9 -1
- package/dist/layouts/AppProviders.d.ts.map +1 -1
- package/dist/layouts/AppProviders.js +24 -3
- package/dist/layouts/AuthLayout.js +1 -1
- package/dist/layouts/PublicLayout.js +1 -1
- package/dist/layouts/RootLayout.d.ts.map +1 -1
- package/dist/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +343 -138
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +784 -59
- package/dist/scripts/module-create.d.ts.map +1 -1
- package/dist/scripts/module-create.js +227 -10
- package/dist/scripts/sitemap-flat-generator.d.ts +39 -0
- package/dist/scripts/sitemap-flat-generator.d.ts.map +1 -0
- package/dist/scripts/sitemap-flat-generator.js +231 -0
- package/dist/scripts/sitemap-manifest-generator.d.ts +59 -0
- package/dist/scripts/sitemap-manifest-generator.d.ts.map +1 -0
- package/dist/scripts/sitemap-manifest-generator.js +290 -0
- package/dist/sitemap/manifest.d.ts +8 -0
- package/dist/sitemap/manifest.d.ts.map +1 -0
- package/dist/sitemap/manifest.js +6 -0
- package/dist/styles.css +2 -2
- package/dist/templates/AuthGuidePage.js +2 -0
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +9 -5
- package/dist/templates/DocPage.d.ts.map +1 -1
- package/dist/templates/DocPage.js +40 -0
- package/dist/templates/MigrationsGuidePage.js +2 -0
- package/dist/templates/ModuleGuidePage.d.ts.map +1 -1
- package/dist/templates/ModuleGuidePage.js +4 -1
- package/dist/templates/SimpleHomePage.js +2 -0
- package/package.json +31 -26
- package/src/analytics/registry.ts +14 -0
- package/src/auth/useAuthSession.ts +91 -1
- package/src/cli.ts +19 -3
- package/src/components/LanguageSwitcher.tsx +183 -60
- package/src/config/version.ts +30 -19
- package/src/i18n/server-lang.ts +2 -1
- package/src/i18n/types.ts +2 -1
- package/src/i18n/useLink.ts +15 -0
- package/src/index.ts +17 -0
- package/src/layouts/AdminLayoutWithSidebar.tsx +4 -0
- package/src/layouts/AppProviders.tsx +74 -9
- package/src/layouts/AuthLayout.tsx +1 -1
- package/src/layouts/PublicLayout.tsx +1 -1
- package/src/layouts/RootLayout.tsx +0 -1
- package/src/scripts/init-app.ts +418 -149
- package/src/scripts/module-build.ts +923 -63
- package/src/scripts/module-create.ts +260 -10
- package/src/scripts/sitemap-flat-generator.ts +313 -0
- package/src/scripts/sitemap-manifest-generator.ts +476 -0
- package/src/sitemap/manifest.ts +17 -0
- package/src/templates/AuthGuidePage.tsx +1 -1
- package/src/templates/DefaultDoc.tsx +397 -6
- package/src/templates/DocPage.tsx +40 -0
- package/src/templates/MigrationsGuidePage.tsx +1 -1
- package/src/templates/ModuleGuidePage.tsx +3 -2
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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} ${
|
|
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 "
|
|
717
|
+
// Extraire le nom du module sans le scope et sans "-pro"
|
|
679
718
|
// Ex: @lastbrain/module-auth -> auth
|
|
680
|
-
// Ex: @lastbrain-labs/module-
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1338
|
-
|
|
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
|
-
|
|
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
|
|
1616
|
-
const pathUserId =
|
|
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,
|
|
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
|
-
//
|
|
1633
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
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!");
|