@lastbrain/app 0.1.36 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/__tests__/module-registry.test.js +5 -16
  2. package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
  3. package/dist/layouts/AdminLayoutWithSidebar.js +36 -1
  4. package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -1
  5. package/dist/layouts/AuthLayoutWithSidebar.js +36 -1
  6. package/dist/layouts/PublicLayout.js +1 -1
  7. package/dist/layouts/PublicLayoutWithSidebar.d.ts.map +1 -1
  8. package/dist/layouts/PublicLayoutWithSidebar.js +36 -1
  9. package/dist/scripts/init-app.d.ts.map +1 -1
  10. package/dist/scripts/init-app.js +2 -2
  11. package/dist/scripts/module-add.d.ts +0 -11
  12. package/dist/scripts/module-add.d.ts.map +1 -1
  13. package/dist/scripts/module-add.js +45 -22
  14. package/dist/scripts/module-build.d.ts.map +1 -1
  15. package/dist/scripts/module-build.js +90 -1
  16. package/dist/scripts/module-create.d.ts +23 -0
  17. package/dist/scripts/module-create.d.ts.map +1 -1
  18. package/dist/scripts/module-create.js +289 -56
  19. package/dist/scripts/module-delete.d.ts +6 -0
  20. package/dist/scripts/module-delete.d.ts.map +1 -0
  21. package/dist/scripts/module-delete.js +143 -0
  22. package/dist/scripts/module-list.d.ts.map +1 -1
  23. package/dist/scripts/module-list.js +2 -2
  24. package/dist/scripts/module-remove.d.ts.map +1 -1
  25. package/dist/scripts/module-remove.js +20 -4
  26. package/dist/styles.css +1 -1
  27. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  28. package/dist/templates/DefaultDoc.js +132 -9
  29. package/dist/templates/DocPage.d.ts.map +1 -1
  30. package/dist/templates/DocPage.js +24 -7
  31. package/package.json +4 -4
  32. package/src/__tests__/module-registry.test.ts +5 -17
  33. package/src/layouts/AdminLayoutWithSidebar.tsx +51 -1
  34. package/src/layouts/AuthLayoutWithSidebar.tsx +51 -1
  35. package/src/layouts/PublicLayout.tsx +1 -1
  36. package/src/layouts/PublicLayoutWithSidebar.tsx +51 -1
  37. package/src/scripts/init-app.ts +5 -2
  38. package/src/scripts/module-add.ts +55 -23
  39. package/src/scripts/module-build.ts +109 -1
  40. package/src/scripts/module-create.ts +392 -69
  41. package/src/scripts/module-delete.ts +202 -0
  42. package/src/scripts/module-list.ts +9 -2
  43. package/src/scripts/module-remove.ts +36 -4
  44. package/src/templates/DefaultDoc.tsx +1149 -424
  45. package/src/templates/DocPage.tsx +26 -10
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { AVAILABLE_MODULES } from "../scripts/module-add";
2
+ import { AVAILABLE_MODULES } from "@lastbrain/core/config/modules";
3
3
 
4
4
  describe("Module Registry", () => {
5
5
  describe("AVAILABLE_MODULES", () => {
@@ -11,32 +11,20 @@ describe("Module Registry", () => {
11
11
  const authModule = AVAILABLE_MODULES.find((m) => m.name === "auth");
12
12
  expect(authModule).toBeDefined();
13
13
  expect(authModule?.package).toBe("@lastbrain/module-auth");
14
- expect(authModule?.hasMigrations).toBe(true);
15
14
  });
16
15
 
17
16
  it("should have ai module defined", () => {
18
17
  const aiModule = AVAILABLE_MODULES.find((m) => m.name === "ai");
19
18
  expect(aiModule).toBeDefined();
20
19
  expect(aiModule?.package).toBe("@lastbrain/module-ai");
21
- expect(aiModule?.hasMigrations).toBe(true);
22
20
  });
23
21
 
24
22
  it("should have all required properties for each module", () => {
25
23
  AVAILABLE_MODULES.forEach((module) => {
26
24
  expect(module.name).toBeDefined();
27
25
  expect(module.package).toBeDefined();
28
- expect(module.displayName).toBeDefined();
26
+ expect(module.emoji).toBeDefined();
29
27
  expect(module.description).toBeDefined();
30
- expect(typeof module.hasMigrations).toBe("boolean");
31
- });
32
- });
33
-
34
- it("should have migrations paths when hasMigrations is true", () => {
35
- AVAILABLE_MODULES.forEach((module) => {
36
- if (module.hasMigrations) {
37
- expect(module.migrationsPath).toBeDefined();
38
- expect(module.migrationsDownPath).toBeDefined();
39
- }
40
28
  });
41
29
  });
42
30
 
@@ -52,10 +40,10 @@ describe("Module Registry", () => {
52
40
  expect(packages.length).toBe(uniquePackages.size);
53
41
  });
54
42
 
55
- it("should have display names with emoji", () => {
43
+ it("should have emoji", () => {
56
44
  AVAILABLE_MODULES.forEach((module) => {
57
- // Check if displayName contains at least one emoji (basic check)
58
- expect(module.displayName).toMatch(/[\u{1F300}-\u{1F9FF}]/u);
45
+ // Check if emoji contains at least one emoji (basic check)
46
+ expect(module.emoji).toMatch(/[\u{1F300}-\u{1F9FF}]/u);
59
47
  });
60
48
  });
61
49
 
@@ -4,6 +4,7 @@ import { AdminLayout } from "./AdminLayout.js";
4
4
  import { AppAside, AppAsideSkeleton } from "@lastbrain/ui";
5
5
  import { useAuthSession } from "../auth/useAuthSession.js";
6
6
  import { usePathname } from "next/navigation";
7
+ import { useEffect, useState } from "react";
7
8
 
8
9
  interface AdminLayoutWithSidebarProps {
9
10
  children: React.ReactNode;
@@ -18,6 +19,53 @@ export function AdminLayoutWithSidebar({
18
19
  }: AdminLayoutWithSidebarProps) {
19
20
  const { isSuperAdmin, loading, user } = useAuthSession();
20
21
  const pathname = usePathname();
22
+ const [isCollapsed, setIsCollapsed] = useState(() => {
23
+ if (typeof window !== "undefined") {
24
+ const savedState = localStorage.getItem("aside-collapsed");
25
+ return savedState !== null ? JSON.parse(savedState) : false;
26
+ }
27
+ return false;
28
+ });
29
+ const [mounted, setMounted] = useState(false);
30
+
31
+ useEffect(() => {
32
+ // eslint-disable-next-line react-hooks/set-state-in-effect
33
+ setMounted(true);
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ if (typeof window !== "undefined") {
38
+ // Écouter les changements du localStorage depuis le même onglet
39
+ const handleStorageChange = (_e: StorageEvent | CustomEvent) => {
40
+ const savedState = localStorage.getItem("aside-collapsed");
41
+ if (savedState !== null) {
42
+ // Utiliser queueMicrotask pour déférer le setState
43
+ queueMicrotask(() => {
44
+ setIsCollapsed(JSON.parse(savedState));
45
+ });
46
+ }
47
+ };
48
+
49
+ // Écouter l'événement storage (changements depuis d'autres onglets)
50
+ window.addEventListener("storage", handleStorageChange as EventListener);
51
+ // Écouter l'événement custom pour les changements du même onglet
52
+ window.addEventListener(
53
+ "localStorage-changed",
54
+ handleStorageChange as EventListener,
55
+ );
56
+
57
+ return () => {
58
+ window.removeEventListener(
59
+ "storage",
60
+ handleStorageChange as EventListener,
61
+ );
62
+ window.removeEventListener(
63
+ "localStorage-changed",
64
+ handleStorageChange as EventListener,
65
+ );
66
+ };
67
+ }
68
+ }, []);
21
69
 
22
70
  // Détecter si on est dans la section admin pour le skeleton
23
71
  const isAdminSection = pathname.startsWith("/admin");
@@ -51,7 +99,9 @@ export function AdminLayoutWithSidebar({
51
99
  )}
52
100
 
53
101
  {/* Contenu principal avec marge pour la sidebar */}
54
- <div className="flex-1 lg:ml-72">
102
+ <div
103
+ className={`flex-1 transition-all duration-300 ${!mounted ? "lg:ml-72" : isCollapsed ? "lg:ml-20" : "lg:ml-72"}`}
104
+ >
55
105
  <AdminLayout>{children}</AdminLayout>
56
106
  </div>
57
107
  </div>
@@ -3,6 +3,7 @@
3
3
  import { AuthLayout } from "./AuthLayout.js";
4
4
  import { AppAside, AppAsideSkeleton } from "@lastbrain/ui";
5
5
  import { useAuthSession } from "../auth/useAuthSession.js";
6
+ import { useEffect, useState } from "react";
6
7
 
7
8
  interface AuthLayoutWithSidebarProps {
8
9
  children: React.ReactNode;
@@ -16,6 +17,53 @@ export function AuthLayoutWithSidebar({
16
17
  className = "",
17
18
  }: AuthLayoutWithSidebarProps) {
18
19
  const { isSuperAdmin, loading, user } = useAuthSession();
20
+ const [isCollapsed, setIsCollapsed] = useState(() => {
21
+ if (typeof window !== "undefined") {
22
+ const savedState = localStorage.getItem("aside-collapsed");
23
+ return savedState !== null ? JSON.parse(savedState) : false;
24
+ }
25
+ return false;
26
+ });
27
+ const [mounted, setMounted] = useState(false);
28
+
29
+ useEffect(() => {
30
+ // eslint-disable-next-line react-hooks/set-state-in-effect
31
+ setMounted(true);
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (typeof window !== "undefined") {
36
+ // Écouter les changements du localStorage depuis le même onglet
37
+ const handleStorageChange = (_e: StorageEvent | CustomEvent) => {
38
+ const savedState = localStorage.getItem("aside-collapsed");
39
+ if (savedState !== null) {
40
+ // Utiliser queueMicrotask pour déférer le setState
41
+ queueMicrotask(() => {
42
+ setIsCollapsed(JSON.parse(savedState));
43
+ });
44
+ }
45
+ };
46
+
47
+ // Écouter l'événement storage (changements depuis d'autres onglets)
48
+ window.addEventListener("storage", handleStorageChange as EventListener);
49
+ // Écouter l'événement custom pour les changements du même onglet
50
+ window.addEventListener(
51
+ "localStorage-changed",
52
+ handleStorageChange as EventListener,
53
+ );
54
+
55
+ return () => {
56
+ window.removeEventListener(
57
+ "storage",
58
+ handleStorageChange as EventListener,
59
+ );
60
+ window.removeEventListener(
61
+ "localStorage-changed",
62
+ handleStorageChange as EventListener,
63
+ );
64
+ };
65
+ }
66
+ }, []);
19
67
 
20
68
  // Pour la section auth, isAdminSection sera false
21
69
  const isAdminSection = false;
@@ -48,7 +96,9 @@ export function AuthLayoutWithSidebar({
48
96
  />
49
97
  )}
50
98
  {/* Contenu principal avec marge pour la sidebar */}
51
- <div className="flex-1 lg:ml-72">
99
+ <div
100
+ className={`flex-1 transition-all duration-300 ${!mounted ? "lg:ml-72" : isCollapsed ? "lg:ml-20" : "lg:ml-72"}`}
101
+ >
52
102
  <AuthLayout>{children}</AuthLayout>
53
103
  </div>
54
104
  </div>
@@ -1,5 +1,5 @@
1
1
  "use client";
2
2
 
3
3
  export function PublicLayout({ children }: { children: React.ReactNode }) {
4
- return <section className=" ">{children}</section>;
4
+ return <section className="pt-16 px-2 md:px-0 ">{children}</section>;
5
5
  }
@@ -3,6 +3,7 @@
3
3
  import { PublicLayout } from "./PublicLayout.js";
4
4
  import { AppAside, AppAsideSkeleton } from "@lastbrain/ui";
5
5
  import { useAuthSession } from "../auth/useAuthSession.js";
6
+ import { useEffect, useState } from "react";
6
7
 
7
8
  interface PublicLayoutWithSidebarProps {
8
9
  children: React.ReactNode;
@@ -16,6 +17,53 @@ export function PublicLayoutWithSidebar({
16
17
  className = "",
17
18
  }: PublicLayoutWithSidebarProps) {
18
19
  const { user, loading } = useAuthSession();
20
+ const [isCollapsed, setIsCollapsed] = useState(() => {
21
+ if (typeof window !== "undefined") {
22
+ const savedState = localStorage.getItem("aside-collapsed");
23
+ return savedState !== null ? JSON.parse(savedState) : false;
24
+ }
25
+ return false;
26
+ });
27
+ const [mounted, setMounted] = useState(false);
28
+
29
+ useEffect(() => {
30
+ // eslint-disable-next-line react-hooks/set-state-in-effect
31
+ setMounted(true);
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (typeof window !== "undefined") {
36
+ // Écouter les changements du localStorage depuis le même onglet
37
+ const handleStorageChange = (_e: StorageEvent | CustomEvent) => {
38
+ const savedState = localStorage.getItem("aside-collapsed");
39
+ if (savedState !== null) {
40
+ // Utiliser queueMicrotask pour déférer le setState
41
+ queueMicrotask(() => {
42
+ setIsCollapsed(JSON.parse(savedState));
43
+ });
44
+ }
45
+ };
46
+
47
+ // Écouter l'événement storage (changements depuis d'autres onglets)
48
+ window.addEventListener("storage", handleStorageChange as EventListener);
49
+ // Écouter l'événement custom pour les changements du même onglet
50
+ window.addEventListener(
51
+ "localStorage-changed",
52
+ handleStorageChange as EventListener,
53
+ );
54
+
55
+ return () => {
56
+ window.removeEventListener(
57
+ "storage",
58
+ handleStorageChange as EventListener,
59
+ );
60
+ window.removeEventListener(
61
+ "localStorage-changed",
62
+ handleStorageChange as EventListener,
63
+ );
64
+ };
65
+ }
66
+ }, []);
19
67
 
20
68
  // Pour la section public, isAdminSection sera false
21
69
  const isAdminSection = false;
@@ -48,7 +96,9 @@ export function PublicLayoutWithSidebar({
48
96
  />
49
97
  )}
50
98
  {/* Contenu principal avec marge pour la sidebar */}
51
- <div className="flex-1 lg:ml-72">
99
+ <div
100
+ className={`flex-1 transition-all duration-300 ${!mounted ? "lg:ml-72" : isCollapsed ? "lg:ml-20" : "lg:ml-72"}`}
101
+ >
52
102
  <PublicLayout>{children}</PublicLayout>
53
103
  </div>
54
104
  </div>
@@ -4,7 +4,10 @@ import { fileURLToPath } from "url";
4
4
  import chalk from "chalk";
5
5
  import inquirer from "inquirer";
6
6
  import { execSync } from "child_process";
7
- import { AVAILABLE_MODULES } from "./module-add.js";
7
+ import {
8
+ AVAILABLE_MODULES,
9
+ type ModuleMetadata,
10
+ } from "@lastbrain/core/config/modules";
8
11
 
9
12
  const __filename = fileURLToPath(import.meta.url);
10
13
  const __dirname = path.dirname(__filename);
@@ -47,7 +50,7 @@ export async function initApp(options: InitAppOptions) {
47
50
  name: "modules",
48
51
  message: "Quels modules voulez-vous installer ?",
49
52
  choices: AVAILABLE_MODULES.map((module) => ({
50
- name: `${module.displayName} - ${module.description}`,
53
+ name: `${module.emoji} ${module.name.charAt(0).toUpperCase() + module.name.slice(1)} - ${module.description}`,
51
54
  value: module.name,
52
55
  checked: false,
53
56
  })),
@@ -3,6 +3,10 @@ import path from "path";
3
3
  import chalk from "chalk";
4
4
  import { execSync } from "child_process";
5
5
  import inquirer from "inquirer";
6
+ import {
7
+ AVAILABLE_MODULES,
8
+ type ModuleMetadata,
9
+ } from "@lastbrain/core/config/modules";
6
10
 
7
11
  interface ModuleDefinition {
8
12
  name: string;
@@ -14,44 +18,35 @@ interface ModuleDefinition {
14
18
  migrationsDownPath?: string;
15
19
  }
16
20
 
17
- // Registre des modules disponibles
18
- export const AVAILABLE_MODULES: ModuleDefinition[] = [
19
- {
20
- name: "auth",
21
- package: "@lastbrain/module-auth",
22
- displayName: "🔐 Authentication",
23
- description:
24
- "Système d'authentification complet (signin, signup, sessions)",
21
+ // Convert core module metadata to local module definition
22
+ function toModuleDefinition(meta: ModuleMetadata): ModuleDefinition {
23
+ return {
24
+ name: meta.name,
25
+ package: meta.package,
26
+ displayName: `${meta.emoji} ${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`,
27
+ description: meta.description,
25
28
  hasMigrations: true,
26
29
  migrationsPath: "supabase/migrations",
27
30
  migrationsDownPath: "supabase/migrations-down",
28
- },
29
- {
30
- name: "ai",
31
- package: "@lastbrain/module-ai",
32
- displayName: "🤖 AI Generation",
33
- description: "Génération de texte et d'images avec gestion de tokens",
34
- hasMigrations: true,
35
- migrationsPath: "supabase/migrations",
36
- migrationsDownPath: "supabase/migrations-down",
37
- },
38
- // Ajouter d'autres modules ici au fur et à mesure
39
- ];
31
+ };
32
+ }
40
33
 
41
34
  export async function addModule(moduleName: string, targetDir: string) {
42
35
  console.log(chalk.blue(`\n🔧 Ajout du module: ${moduleName}\n`));
43
36
 
44
- const module = AVAILABLE_MODULES.find((m) => m.name === moduleName);
45
- if (!module) {
37
+ const moduleMeta = AVAILABLE_MODULES.find((m) => m.name === moduleName);
38
+ if (!moduleMeta) {
46
39
  console.error(
47
40
  chalk.red(`❌ Module "${moduleName}" non trouvé. Modules disponibles:`),
48
41
  );
49
42
  AVAILABLE_MODULES.forEach((m) => {
50
- console.log(chalk.gray(` - ${m.name}: ${m.description}`));
43
+ console.log(chalk.gray(` - ${m.emoji} ${m.name}: ${m.description}`));
51
44
  });
52
45
  process.exit(1);
53
46
  }
54
47
 
48
+ const module = toModuleDefinition(moduleMeta);
49
+
55
50
  // 1. Vérifier qu'on est dans un projet LastBrain
56
51
  const pkgPath = path.join(targetDir, "package.json");
57
52
  if (!fs.existsSync(pkgPath)) {
@@ -85,6 +80,43 @@ export async function addModule(moduleName: string, targetDir: string) {
85
80
  process.exit(1);
86
81
  }
87
82
 
83
+ // 3.1 Build du module ajouté (pour générer dist/ et éviter les erreurs d'import)
84
+ console.log(chalk.yellow(`\n🏗️ Build du module ${module.package}...`));
85
+ try {
86
+ // Trouver la racine du monorepo (chercher pnpm-workspace.yaml en remontant)
87
+ let workspaceRoot = targetDir;
88
+ let attempts = 0;
89
+ const maxAttempts = 6;
90
+ while (attempts < maxAttempts) {
91
+ if (fs.existsSync(path.join(workspaceRoot, "pnpm-workspace.yaml"))) {
92
+ break;
93
+ }
94
+ const parent = path.resolve(workspaceRoot, "..");
95
+ if (parent === workspaceRoot) break;
96
+ workspaceRoot = parent;
97
+ attempts++;
98
+ }
99
+ if (!fs.existsSync(path.join(workspaceRoot, "pnpm-workspace.yaml"))) {
100
+ console.log(
101
+ chalk.gray(
102
+ " ℹ️ Impossible de localiser la racine du monorepo, build ignoré",
103
+ ),
104
+ );
105
+ } else {
106
+ execSync(`pnpm --filter ${module.package} build`, {
107
+ cwd: workspaceRoot,
108
+ stdio: "inherit",
109
+ });
110
+ console.log(chalk.green(" ✓ Module compilé"));
111
+ }
112
+ } catch {
113
+ console.log(
114
+ chalk.yellow(" ⚠️ Build du module échoué, essayez: pnpm --filter"),
115
+ module.package,
116
+ "build",
117
+ );
118
+ }
119
+
88
120
  // 5. Copier les migrations du module
89
121
  const copiedMigrationFiles: string[] = [];
90
122
  if (module.hasMigrations) {
@@ -13,6 +13,10 @@ import type {
13
13
 
14
14
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
15
15
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
16
+ // Si on est dans une app, monter jusqu'à la racine du monorepo
17
+ const monorepoRoot = projectRoot.includes("/apps/")
18
+ ? path.resolve(projectRoot, "..", "..")
19
+ : projectRoot;
16
20
  const appDirectory = path.join(projectRoot, "app");
17
21
 
18
22
  // Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
@@ -207,9 +211,40 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
207
211
  page.path.includes("signup") ||
208
212
  page.path.includes("reset-password"));
209
213
 
214
+ // Détecter si c'est la page de détail utilisateur qui a besoin des user tabs
215
+ const isUserDetailPage =
216
+ page.section === "admin" &&
217
+ page.path.includes("users/[id]") &&
218
+ page.componentExport === "UserPage";
219
+
210
220
  let content: string;
211
221
 
212
- if (isPublicAuthPage) {
222
+ if (isUserDetailPage) {
223
+ // Page spéciale SSR avec injection des user tabs
224
+ // On importe directement depuis app/config au lieu de passer via props
225
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
226
+ import { UserDetailPage } from "${moduleConfig.moduleName}";
227
+
228
+ interface UserPageProps { params: Promise<{ id: string }> }
229
+
230
+ async function getModuleUserTabs() {
231
+ try {
232
+ // Depuis /app/admin/auth/users/[id]/ vers /apps/test-01/config/user-tabs
233
+ const { moduleUserTabs } = await import("../../../../../config/user-tabs");
234
+ return moduleUserTabs || [];
235
+ } catch (e) {
236
+ console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
237
+ return [];
238
+ }
239
+ }
240
+
241
+ export default async function ${wrapperName}(props: UserPageProps) {
242
+ const { id } = await props.params;
243
+ const moduleUserTabs = await getModuleUserTabs();
244
+ return <UserDetailPage userId={id} moduleUserTabs={moduleUserTabs} />;
245
+ }
246
+ `;
247
+ } else if (isPublicAuthPage) {
213
248
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
214
249
  "use client";
215
250
 
@@ -314,6 +349,8 @@ export interface MenuItem {
314
349
  icon?: string;
315
350
  path: string;
316
351
  order?: number;
352
+ shortcut?: string;
353
+ shortcutDisplay?: string;
317
354
  }
318
355
 
319
356
  export interface MenuConfig {
@@ -838,6 +875,73 @@ export default realtimeConfig;
838
875
  }
839
876
  }
840
877
 
878
+ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
879
+ try {
880
+ // Extraire les configurations user tabs des modules
881
+ const userTabsConfigs = moduleConfigs
882
+ .filter((config) => config.userTabs && config.userTabs.length > 0)
883
+ .flatMap((config) =>
884
+ config.userTabs!.map((tab) => ({
885
+ ...tab,
886
+ moduleName: config.moduleName,
887
+ })),
888
+ )
889
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
890
+
891
+ if (userTabsConfigs.length === 0) {
892
+ console.log("⏭️ No user tabs configuration found in modules");
893
+ return;
894
+ }
895
+
896
+ // Générer les imports statiques (Next/dynamic pour chaque composant)
897
+ const importsForApp = userTabsConfigs
898
+ .map(
899
+ (tab) =>
900
+ `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`,
901
+ )
902
+ .join("\n");
903
+
904
+ // Générer le tableau des tabs
905
+ const tabsArray = userTabsConfigs
906
+ .map(
907
+ (tab) => ` {
908
+ key: "${tab.key}",
909
+ title: "${tab.title}",
910
+ icon: "${tab.icon || ""}",
911
+ component: ${tab.componentExport},
912
+ }`,
913
+ )
914
+ .join(",\n");
915
+
916
+ const timestamp = new Date().toISOString();
917
+ const appContent = `// GENERATED FILE - DO NOT EDIT MANUALLY\n// User tabs configuration\n// Generated at: ${timestamp}\n\n"use client";\n\nimport dynamic from "next/dynamic";\nimport type React from "react";\n\n${importsForApp}\n\nexport interface ModuleUserTab {\n key: string;\n title: string;\n icon?: string;\n component: React.ComponentType<{ userId: string }>;\n}\n\nexport const moduleUserTabs: ModuleUserTab[] = [\n${tabsArray}\n];\n\nexport default moduleUserTabs;\n`;
918
+
919
+ // Créer le fichier de configuration (uniquement dans /config)
920
+ const outputPath = path.join(projectRoot, "config", "user-tabs.ts");
921
+ const configDir = path.dirname(outputPath);
922
+
923
+ // Créer le dossier config s'il n'existe pas
924
+ if (!fs.existsSync(configDir)) {
925
+ fs.mkdirSync(configDir, { recursive: true });
926
+ }
927
+
928
+ // Écrire le fichier TypeScript
929
+ fs.writeFileSync(outputPath, appContent);
930
+
931
+ console.log(`✅ Generated user tabs configuration: ${outputPath}`);
932
+ console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
933
+
934
+ // Afficher un résumé
935
+ userTabsConfigs.forEach((tab) => {
936
+ console.log(` - ${tab.title} (${tab.moduleName})`);
937
+ });
938
+
939
+ // Plus de copie vers app/config ni stub core
940
+ } catch (error) {
941
+ console.error("❌ Error generating user tabs configuration:", error);
942
+ }
943
+ }
944
+
841
945
  export async function runModuleBuild() {
842
946
  ensureDirectory(appDirectory);
843
947
 
@@ -886,4 +990,8 @@ export async function runModuleBuild() {
886
990
  // Générer la configuration realtime
887
991
  console.log("🔄 Generating realtime configuration...");
888
992
  await generateRealtimeConfig(moduleConfigs);
993
+
994
+ // Générer la configuration des user tabs
995
+ console.log("📑 Generating user tabs configuration...");
996
+ await generateUserTabsConfig(moduleConfigs);
889
997
  }