@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,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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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} ${
|
|
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 "
|
|
557
|
+
// Extraire le nom du module sans le scope et sans "-pro"
|
|
531
558
|
// Ex: @lastbrain/module-auth -> auth
|
|
532
|
-
// Ex: @lastbrain-labs/module-
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
-
//
|
|
803
|
-
const
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
1336
|
-
const pathUserId =
|
|
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,
|
|
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
|
-
//
|
|
1353
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
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!");
|