@lastbrain/app 2.0.0 → 2.0.2

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 (132) hide show
  1. package/dist/config/version.d.ts +7 -0
  2. package/dist/config/version.d.ts.map +1 -0
  3. package/dist/config/version.js +25 -0
  4. package/dist/scripts/init-app.js +8 -16
  5. package/dist/src/__tests__/module-registry.test.d.ts +2 -0
  6. package/dist/src/__tests__/module-registry.test.d.ts.map +1 -0
  7. package/dist/src/__tests__/module-registry.test.js +53 -0
  8. package/dist/src/app-shell/(admin)/layout.d.ts +4 -0
  9. package/dist/src/app-shell/(admin)/layout.d.ts.map +1 -0
  10. package/dist/src/app-shell/(admin)/layout.js +5 -0
  11. package/dist/src/app-shell/(auth)/layout.d.ts +4 -0
  12. package/dist/src/app-shell/(auth)/layout.d.ts.map +1 -0
  13. package/dist/src/app-shell/(auth)/layout.js +5 -0
  14. package/dist/src/app-shell/(public)/page.d.ts +2 -0
  15. package/dist/src/app-shell/(public)/page.d.ts.map +1 -0
  16. package/dist/src/app-shell/(public)/page.js +5 -0
  17. package/dist/src/app-shell/layout.d.ts +3 -0
  18. package/dist/src/app-shell/layout.d.ts.map +1 -0
  19. package/dist/src/app-shell/layout.js +3 -0
  20. package/dist/src/app-shell/not-found.d.ts +2 -0
  21. package/dist/src/app-shell/not-found.d.ts.map +1 -0
  22. package/dist/src/app-shell/not-found.js +10 -0
  23. package/dist/src/auth/authHelpers.d.ts +7 -0
  24. package/dist/src/auth/authHelpers.d.ts.map +1 -0
  25. package/dist/src/auth/authHelpers.js +19 -0
  26. package/dist/src/auth/useAuthSession.d.ts +7 -0
  27. package/dist/src/auth/useAuthSession.d.ts.map +1 -0
  28. package/dist/src/auth/useAuthSession.js +49 -0
  29. package/dist/src/cli.d.ts +3 -0
  30. package/dist/src/cli.d.ts.map +1 -0
  31. package/dist/src/cli.js +143 -0
  32. package/dist/src/components/NotificationContainer.d.ts +2 -0
  33. package/dist/src/components/NotificationContainer.d.ts.map +1 -0
  34. package/dist/src/components/NotificationContainer.js +8 -0
  35. package/dist/src/hooks/useNotifications.d.ts +30 -0
  36. package/dist/src/hooks/useNotifications.d.ts.map +1 -0
  37. package/dist/src/hooks/useNotifications.js +165 -0
  38. package/dist/src/index.d.ts +22 -0
  39. package/dist/src/index.d.ts.map +1 -0
  40. package/dist/src/index.js +22 -0
  41. package/dist/src/layouts/AdminLayout.d.ts +4 -0
  42. package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
  43. package/dist/src/layouts/AdminLayout.js +4 -0
  44. package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +10 -0
  45. package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
  46. package/dist/src/layouts/AdminLayoutWithSidebar.js +62 -0
  47. package/dist/src/layouts/AppProviders.d.ts +27 -0
  48. package/dist/src/layouts/AppProviders.d.ts.map +1 -0
  49. package/dist/src/layouts/AppProviders.js +48 -0
  50. package/dist/src/layouts/AuthLayout.d.ts +4 -0
  51. package/dist/src/layouts/AuthLayout.d.ts.map +1 -0
  52. package/dist/src/layouts/AuthLayout.js +4 -0
  53. package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +12 -0
  54. package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
  55. package/dist/src/layouts/AuthLayoutWithSidebar.js +60 -0
  56. package/dist/src/layouts/PublicLayout.d.ts +8 -0
  57. package/dist/src/layouts/PublicLayout.d.ts.map +1 -0
  58. package/dist/src/layouts/PublicLayout.js +6 -0
  59. package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +9 -0
  60. package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
  61. package/dist/src/layouts/PublicLayoutWithSidebar.js +60 -0
  62. package/dist/src/layouts/RootLayout.d.ts +6 -0
  63. package/dist/src/layouts/RootLayout.d.ts.map +1 -0
  64. package/dist/src/layouts/RootLayout.js +9 -0
  65. package/dist/src/modules/module-loader.d.ts +5 -0
  66. package/dist/src/modules/module-loader.d.ts.map +1 -0
  67. package/dist/src/modules/module-loader.js +10 -0
  68. package/dist/src/scripts/db-init.d.ts +2 -0
  69. package/dist/src/scripts/db-init.d.ts.map +1 -0
  70. package/dist/src/scripts/db-init.js +300 -0
  71. package/dist/src/scripts/db-migrations-sync.d.ts +2 -0
  72. package/dist/src/scripts/db-migrations-sync.d.ts.map +1 -0
  73. package/dist/src/scripts/db-migrations-sync.js +84 -0
  74. package/dist/src/scripts/dev-sync.d.ts +2 -0
  75. package/dist/src/scripts/dev-sync.d.ts.map +1 -0
  76. package/dist/src/scripts/dev-sync.js +194 -0
  77. package/dist/src/scripts/init-app.d.ts +12 -0
  78. package/dist/src/scripts/init-app.d.ts.map +1 -0
  79. package/dist/src/scripts/init-app.js +2175 -0
  80. package/dist/src/scripts/module-add.d.ts +2 -0
  81. package/dist/src/scripts/module-add.d.ts.map +1 -0
  82. package/dist/src/scripts/module-add.js +232 -0
  83. package/dist/src/scripts/module-build.d.ts +2 -0
  84. package/dist/src/scripts/module-build.d.ts.map +1 -0
  85. package/dist/src/scripts/module-build.js +1280 -0
  86. package/dist/src/scripts/module-create.d.ts +28 -0
  87. package/dist/src/scripts/module-create.d.ts.map +1 -0
  88. package/dist/src/scripts/module-create.js +1429 -0
  89. package/dist/src/scripts/module-delete.d.ts +6 -0
  90. package/dist/src/scripts/module-delete.d.ts.map +1 -0
  91. package/dist/src/scripts/module-delete.js +147 -0
  92. package/dist/src/scripts/module-list.d.ts +2 -0
  93. package/dist/src/scripts/module-list.d.ts.map +1 -0
  94. package/dist/src/scripts/module-list.js +61 -0
  95. package/dist/src/scripts/module-remove.d.ts +2 -0
  96. package/dist/src/scripts/module-remove.d.ts.map +1 -0
  97. package/dist/src/scripts/module-remove.js +311 -0
  98. package/dist/src/scripts/readme-build.d.ts +2 -0
  99. package/dist/src/scripts/readme-build.d.ts.map +1 -0
  100. package/dist/src/scripts/readme-build.js +39 -0
  101. package/dist/src/scripts/script-runner.d.ts +5 -0
  102. package/dist/src/scripts/script-runner.d.ts.map +1 -0
  103. package/dist/src/scripts/script-runner.js +25 -0
  104. package/dist/src/templates/AuthGuidePage.d.ts +2 -0
  105. package/dist/src/templates/AuthGuidePage.d.ts.map +1 -0
  106. package/dist/src/templates/AuthGuidePage.js +9 -0
  107. package/dist/src/templates/DefaultDoc.d.ts +2 -0
  108. package/dist/src/templates/DefaultDoc.d.ts.map +1 -0
  109. package/dist/src/templates/DefaultDoc.js +240 -0
  110. package/dist/src/templates/DocPage.d.ts +17 -0
  111. package/dist/src/templates/DocPage.d.ts.map +1 -0
  112. package/dist/src/templates/DocPage.js +193 -0
  113. package/dist/src/templates/DocsPageWithModules.d.ts +2 -0
  114. package/dist/src/templates/DocsPageWithModules.d.ts.map +1 -0
  115. package/dist/src/templates/DocsPageWithModules.js +8 -0
  116. package/dist/src/templates/MigrationsGuidePage.d.ts +2 -0
  117. package/dist/src/templates/MigrationsGuidePage.d.ts.map +1 -0
  118. package/dist/src/templates/MigrationsGuidePage.js +11 -0
  119. package/dist/src/templates/ModuleGuidePage.d.ts +2 -0
  120. package/dist/src/templates/ModuleGuidePage.d.ts.map +1 -0
  121. package/dist/src/templates/ModuleGuidePage.js +14 -0
  122. package/dist/src/templates/SimpleDocPage.d.ts +2 -0
  123. package/dist/src/templates/SimpleDocPage.d.ts.map +1 -0
  124. package/dist/src/templates/SimpleDocPage.js +28 -0
  125. package/dist/src/templates/SimpleHomePage.d.ts +6 -0
  126. package/dist/src/templates/SimpleHomePage.d.ts.map +1 -0
  127. package/dist/src/templates/SimpleHomePage.js +7 -0
  128. package/dist/src/types/menu.d.ts +23 -0
  129. package/dist/src/types/menu.d.ts.map +1 -0
  130. package/dist/src/types/menu.js +1 -0
  131. package/package.json +3 -4
  132. package/src/scripts/init-app.ts +7 -81
@@ -0,0 +1,1280 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
5
+ const projectRoot = process.env.PROJECT_ROOT || process.cwd();
6
+ // Si on est dans une app, monter jusqu'à la racine du monorepo
7
+ const _monorepoRoot = projectRoot.includes("/apps/")
8
+ ? path.resolve(projectRoot, "..", "..")
9
+ : projectRoot;
10
+ const appDirectory = path.join(projectRoot, "app");
11
+ // Mode debug activé via --debug
12
+ const isDebugMode = process.argv.includes("--debug");
13
+ // Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
14
+ const projectRequire = createRequire(path.join(projectRoot, "package.json"));
15
+ // Charger les modules depuis modules.json
16
+ async function loadModuleConfigs() {
17
+ const moduleConfigs = [];
18
+ const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
19
+ if (!fs.existsSync(modulesJsonPath)) {
20
+ return moduleConfigs;
21
+ }
22
+ try {
23
+ const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
24
+ const modules = modulesData.modules || [];
25
+ for (const module of modules) {
26
+ // Ne charger que les modules actifs
27
+ if (module.active === false) {
28
+ continue;
29
+ }
30
+ const packageName = module.package;
31
+ try {
32
+ // Extraire le suffix du module (la partie après @lastbrain/module- ou @lastbrain-labs/module-)
33
+ const moduleSuffix = packageName
34
+ .replace("@lastbrain/module-", "")
35
+ .replace("@lastbrain-labs/module-", "");
36
+ const possibleConfigNames = [
37
+ `${moduleSuffix}.build.config`,
38
+ "build.config",
39
+ ];
40
+ let loaded = false;
41
+ for (const configName of possibleConfigNames) {
42
+ try {
43
+ // Résoudre le chemin du module depuis l'application
44
+ const modulePath = projectRequire.resolve(`${packageName}/${configName}`);
45
+ // Convertir en URL file:// pour l'import dynamique
46
+ const moduleUrl = `file://${modulePath}`;
47
+ const moduleImport = await import(moduleUrl);
48
+ if (moduleImport.default) {
49
+ moduleConfigs.push(moduleImport.default);
50
+ loaded = true;
51
+ break;
52
+ }
53
+ }
54
+ catch {
55
+ // Essayer le nom suivant
56
+ }
57
+ }
58
+ if (!loaded) {
59
+ console.warn(`⚠️ Could not load build config for ${packageName}`);
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.warn(`⚠️ Failed to load module ${packageName}:`, error);
64
+ }
65
+ }
66
+ }
67
+ catch (error) {
68
+ console.error("❌ Error loading modules.json:", error);
69
+ }
70
+ return moduleConfigs;
71
+ }
72
+ const sectionDirectoryMap = {
73
+ public: ["(public)"],
74
+ auth: ["auth"],
75
+ admin: ["admin"],
76
+ user: ["auth", "user"],
77
+ };
78
+ const sectionLayoutMap = {
79
+ auth: "AuthLayout",
80
+ admin: "AdminLayout",
81
+ "(public)": "PublicLayout",
82
+ };
83
+ const generatedSections = new Set();
84
+ const navigation = {
85
+ public: [],
86
+ auth: [],
87
+ admin: [],
88
+ };
89
+ const userMenu = [];
90
+ function ensureDirectory(dir) {
91
+ fs.mkdirSync(dir, { recursive: true });
92
+ }
93
+ function ensureSectionLayout(sectionPath) {
94
+ const sectionDir = path.join(appDirectory, ...sectionPath);
95
+ const sectionKey = sectionPath[0];
96
+ // Éviter de générer plusieurs fois le même layout
97
+ if (generatedSections.has(sectionKey)) {
98
+ return;
99
+ }
100
+ ensureDirectory(sectionDir);
101
+ const layoutName = sectionLayoutMap[sectionKey];
102
+ if (layoutName) {
103
+ const layoutPath = path.join(sectionDir, "layout.tsx");
104
+ if (!fs.existsSync(layoutPath)) {
105
+ const layoutContent = `import { ${layoutName} } from "@lastbrain/app";
106
+
107
+ export default function SectionLayout({ children }: { children: React.ReactNode }) {
108
+ return <${layoutName}>{children}</${layoutName}>;
109
+ }
110
+ `;
111
+ fs.writeFileSync(layoutPath, layoutContent);
112
+ console.log(`📐 Generated section layout: ${layoutPath}`);
113
+ }
114
+ }
115
+ generatedSections.add(sectionKey);
116
+ }
117
+ function toPascalCase(value) {
118
+ return value
119
+ .split(/[^a-zA-Z0-9]+/)
120
+ .filter(Boolean)
121
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
122
+ .join("");
123
+ }
124
+ function buildPage(moduleConfig, page) {
125
+ // Extraire le préfixe du module (ex: @lastbrain/module-auth -> auth, @lastbrain-labs/module-recipes-pro -> recipes)
126
+ // Enlever -pro pour les routes auth et admin afin d'avoir des chemins propres
127
+ const modulePrefix = moduleConfig.moduleName
128
+ .replace(/^@lastbrain\/module-/, "")
129
+ .replace(/^@lastbrain-labs\/module-/, "")
130
+ .replace(/-pro$/, "") // Enlever le suffixe -pro
131
+ .toLowerCase();
132
+ if (isDebugMode) {
133
+ console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
134
+ }
135
+ // Ajouter le préfixe du module au path pour les sections admin et auth,
136
+ // MAIS seulement quand la section ne correspond PAS au module lui-même
137
+ let effectivePath = page.path;
138
+ if (page.section === "admin" ||
139
+ (page.section === "auth" && modulePrefix !== "auth")) {
140
+ // Éviter les doublons si le préfixe est déjà présent
141
+ if (!page.path.startsWith(`/${modulePrefix}/`)) {
142
+ effectivePath = `/${modulePrefix}${page.path}`;
143
+ if (isDebugMode) {
144
+ console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
145
+ }
146
+ }
147
+ else if (isDebugMode) {
148
+ console.log(`✅ Module prefix already present: ${page.path}`);
149
+ }
150
+ }
151
+ else if (page.section === "auth" &&
152
+ modulePrefix === "auth" &&
153
+ isDebugMode) {
154
+ console.log(`🏠 Auth module in auth section, no prefix needed: ${page.path}`);
155
+ }
156
+ const segments = effectivePath.replace(/^\/+/, "").split("/").filter(Boolean);
157
+ const sectionPath = sectionDirectoryMap[page.section] ?? ["(public)"];
158
+ // Générer le layout de section si nécessaire
159
+ ensureSectionLayout(sectionPath);
160
+ const routeDir = path.join(appDirectory, ...sectionPath, ...segments);
161
+ const filePath = path.join(routeDir, "page.tsx");
162
+ ensureDirectory(routeDir);
163
+ const wrapperSuffix = segments.length
164
+ ? toPascalCase(segments.join("-"))
165
+ : "Root";
166
+ const wrapperName = `${page.componentExport}${wrapperSuffix}Route`;
167
+ // Vérifier si la route a des paramètres dynamiques (segments avec [])
168
+ const hasDynamicParams = segments.some((seg) => seg.startsWith("[") && seg.endsWith("]"));
169
+ // Pour les pages publiques (signin, signup, etc.), utiliser dynamic import sans SSR
170
+ // pour éviter les erreurs d'hydratation avec les IDs HeroUI/React Aria
171
+ const isPublicAuthPage = page.section === "public" &&
172
+ (page.path.includes("signin") ||
173
+ page.path.includes("signup") ||
174
+ page.path.includes("reset-password"));
175
+ // Détecter si c'est la page de détail utilisateur qui a besoin des user tabs
176
+ const isUserDetailPage = page.section === "admin" &&
177
+ page.path.includes("users/[id]") &&
178
+ page.componentExport === "UserPage";
179
+ // Détecter les pages légales qui utilisent cookies (privacy, terms, returns)
180
+ const isLegalPage = page.section === "public" &&
181
+ (page.path.includes("privacy") ||
182
+ page.path.includes("terms") ||
183
+ page.path.includes("returns"));
184
+ let content;
185
+ if (isUserDetailPage) {
186
+ // Page spéciale SSR avec injection des user tabs
187
+ // On importe directement depuis app/config au lieu de passer via props
188
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
189
+ import { UserDetailPage } from "${moduleConfig.moduleName}";
190
+
191
+ interface UserPageProps { params: Promise<{ id: string }> }
192
+
193
+ async function getModuleUserTabs() {
194
+ try {
195
+ // Depuis /app/admin/auth/users/[id]/ vers /apps/test-01/config/user-tabs
196
+ const { moduleUserTabs } = await import("../../../../../config/user-tabs");
197
+ return moduleUserTabs || [];
198
+ } catch (e) {
199
+ console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
200
+ return [];
201
+ }
202
+ }
203
+
204
+ export default async function ${wrapperName}(props: UserPageProps) {
205
+ const { id } = await props.params;
206
+ const moduleUserTabs = await getModuleUserTabs();
207
+ return <UserDetailPage userId={id} moduleUserTabs={moduleUserTabs} />;
208
+ }
209
+ `;
210
+ }
211
+ else if (isPublicAuthPage) {
212
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
213
+ "use client";
214
+
215
+ import dynamic from "next/dynamic";
216
+
217
+ const ${page.componentExport} = dynamic(
218
+ () => import("${moduleConfig.moduleName}").then((mod) => ({ default: mod.${page.componentExport} })),
219
+ { ssr: false }
220
+ );
221
+
222
+ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
223
+ return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
224
+ }
225
+ `;
226
+ }
227
+ else if (isLegalPage) {
228
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
229
+ import { ${page.componentExport} } from "${moduleConfig.moduleName}";
230
+
231
+ export const dynamic = 'force-dynamic';
232
+
233
+ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
234
+ return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
235
+ }
236
+ `;
237
+ }
238
+ else {
239
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
240
+ import { ${page.componentExport} } from "${moduleConfig.moduleName}";
241
+
242
+ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
243
+ return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
244
+ }
245
+ `;
246
+ }
247
+ fs.writeFileSync(filePath, content);
248
+ if (isDebugMode) {
249
+ console.log(`⭐ Generated page: ${filePath}`);
250
+ }
251
+ const entry = {
252
+ label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
253
+ path: effectivePath,
254
+ module: moduleConfig.moduleName,
255
+ section: page.section,
256
+ };
257
+ if (page.section === "user") {
258
+ userMenu.push(entry);
259
+ }
260
+ else if (page.section) {
261
+ navigation[page.section]?.push(entry);
262
+ }
263
+ }
264
+ function dumpNavigation() {
265
+ const navPath = path.join(appDirectory, "navigation.generated.ts");
266
+ const content = `export type MenuEntry = { label: string; path: string; module: string; section: string };
267
+
268
+ export const navigation = ${JSON.stringify(navigation, null, 2)};
269
+
270
+ export const userMenu = ${JSON.stringify(userMenu, null, 2)};
271
+ `;
272
+ fs.writeFileSync(navPath, content);
273
+ if (isDebugMode) {
274
+ console.log(`🧭 Generated navigation metadata: ${navPath}`);
275
+ }
276
+ }
277
+ /**
278
+ * Génère le fichier config/menu.ts à partir des configurations de menu des modules
279
+ */
280
+ function generateMenuConfig(moduleConfigs) {
281
+ const configDir = path.join(projectRoot, "config");
282
+ ensureDirectory(configDir);
283
+ const menuPath = path.join(configDir, "menu.ts");
284
+ // Collecter les menus de tous les modules avec leurs modules sources
285
+ const publicMenus = [];
286
+ const authMenus = [];
287
+ const adminMenus = [];
288
+ const accountMenus = [];
289
+ moduleConfigs.forEach((moduleConfig) => {
290
+ if (moduleConfig.menu) {
291
+ if (moduleConfig.menu.public) {
292
+ publicMenus.push(...moduleConfig.menu.public.map((item) => ({
293
+ ...item,
294
+ moduleName: moduleConfig.moduleName,
295
+ })));
296
+ }
297
+ if (moduleConfig.menu.auth) {
298
+ authMenus.push(...moduleConfig.menu.auth.map((item) => ({
299
+ ...item,
300
+ moduleName: moduleConfig.moduleName,
301
+ })));
302
+ }
303
+ if (moduleConfig.menu.admin) {
304
+ adminMenus.push(...moduleConfig.menu.admin.map((item) => ({
305
+ ...item,
306
+ moduleName: moduleConfig.moduleName,
307
+ })));
308
+ }
309
+ if (moduleConfig.menu.account) {
310
+ accountMenus.push(...moduleConfig.menu.account.map((item) => ({
311
+ ...item,
312
+ moduleName: moduleConfig.moduleName,
313
+ })));
314
+ }
315
+ }
316
+ });
317
+ // Trier par ordre
318
+ const sortByOrder = (a, b) => {
319
+ return (a.order ?? 999) - (b.order ?? 999);
320
+ };
321
+ publicMenus.sort(sortByOrder);
322
+ authMenus.sort(sortByOrder);
323
+ adminMenus.sort(sortByOrder);
324
+ accountMenus.sort(sortByOrder);
325
+ // Collecter les composants à importer dynamiquement
326
+ const componentImports = [];
327
+ const allMenus = [
328
+ ...publicMenus,
329
+ ...authMenus,
330
+ ...adminMenus,
331
+ ...accountMenus,
332
+ ];
333
+ allMenus.forEach((menu, index) => {
334
+ if (menu.componentExport && menu.moduleName) {
335
+ const componentName = `MenuComponent${index}`;
336
+ componentImports.push(`const ${componentName} = dynamic(() => import("${menu.moduleName}").then(mod => ({ default: mod.${menu.componentExport} })), { ssr: false });`);
337
+ // Ajouter une référence au composant
338
+ menu.__componentRef = componentName;
339
+ }
340
+ });
341
+ // Fonction pour préparer les menus avec les composants
342
+ const prepareMenusForExport = (menus) => {
343
+ return menus.map((menu) => {
344
+ const { moduleName, __componentRef, ...cleanMenu } = menu;
345
+ if (__componentRef) {
346
+ // Retourner une référence au composant au lieu de la sérialiser
347
+ return `{ ...${JSON.stringify(cleanMenu)}, component: ${__componentRef} }`;
348
+ }
349
+ return JSON.stringify(cleanMenu);
350
+ });
351
+ };
352
+ const publicMenusExport = prepareMenusForExport(publicMenus);
353
+ const authMenusExport = prepareMenusForExport(authMenus);
354
+ const adminMenusExport = prepareMenusForExport(adminMenus);
355
+ const accountMenusExport = prepareMenusForExport(accountMenus);
356
+ // Générer le contenu du fichier
357
+ const content = `// Auto-generated menu configuration
358
+ // Generated from module build configs
359
+ "use client";
360
+
361
+ import dynamic from "next/dynamic";
362
+
363
+ ${componentImports.join("\n")}
364
+
365
+ export interface MenuItem {
366
+ title: string;
367
+ description?: string;
368
+ icon?: string;
369
+ path: string;
370
+ order?: number;
371
+ shortcut?: string;
372
+ shortcutDisplay?: string;
373
+ type?: 'text' | 'icon' | 'textIcon';
374
+ position?: 'center' | 'end';
375
+ component?: React.ComponentType<any>;
376
+ componentExport?: string;
377
+ entryPoint?: string;
378
+ }
379
+
380
+ export interface MenuConfig {
381
+ public: MenuItem[];
382
+ auth: MenuItem[];
383
+ admin: MenuItem[];
384
+ account: MenuItem[];
385
+ }
386
+
387
+ export const menuConfig: MenuConfig = {
388
+ public: [${publicMenusExport.join(",\n ")}],
389
+ auth: [${authMenusExport.join(",\n ")}],
390
+ admin: [${adminMenusExport.join(",\n ")}],
391
+ account: [${accountMenusExport.join(",\n ")}],
392
+ };
393
+ `;
394
+ fs.writeFileSync(menuPath, content);
395
+ if (isDebugMode) {
396
+ console.log(`🍔 Generated menu configuration: ${menuPath}`);
397
+ }
398
+ }
399
+ function copyModuleMigrations(moduleConfigs) {
400
+ const supabaseMigrationsDir = path.join(projectRoot, "supabase", "migrations");
401
+ // S'assurer que le dossier migrations existe
402
+ if (!fs.existsSync(supabaseMigrationsDir)) {
403
+ ensureDirectory(supabaseMigrationsDir);
404
+ }
405
+ const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
406
+ // Charger modules.json
407
+ let modulesConfig = { modules: [] };
408
+ if (fs.existsSync(modulesJsonPath)) {
409
+ modulesConfig = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
410
+ }
411
+ moduleConfigs.forEach((moduleConfig) => {
412
+ try {
413
+ const moduleName = moduleConfig.moduleName;
414
+ const copiedMigrations = [];
415
+ // Essayer plusieurs chemins possibles pour trouver le module
416
+ const possiblePaths = [
417
+ // Dans node_modules local de l'app
418
+ path.join(projectRoot, "node_modules", moduleName),
419
+ // Dans node_modules à la racine du workspace
420
+ path.join(projectRoot, "..", "..", "node_modules", moduleName),
421
+ // Directement dans packages/ (pour monorepo)
422
+ path.join(projectRoot, "..", "..", "packages", moduleName.replace("@lastbrain/", "")),
423
+ ];
424
+ let moduleBasePath = null;
425
+ for (const possiblePath of possiblePaths) {
426
+ if (fs.existsSync(possiblePath)) {
427
+ moduleBasePath = possiblePath;
428
+ break;
429
+ }
430
+ }
431
+ if (!moduleBasePath) {
432
+ return;
433
+ }
434
+ const moduleMigrationsDir = path.join(moduleBasePath, "supabase", "migrations");
435
+ if (fs.existsSync(moduleMigrationsDir)) {
436
+ const migrationFiles = fs.readdirSync(moduleMigrationsDir);
437
+ migrationFiles.forEach((file) => {
438
+ if (file.endsWith(".sql")) {
439
+ const sourcePath = path.join(moduleMigrationsDir, file);
440
+ const destPath = path.join(supabaseMigrationsDir, file);
441
+ // Copier seulement si le fichier n'existe pas déjà
442
+ if (!fs.existsSync(destPath)) {
443
+ fs.copyFileSync(sourcePath, destPath);
444
+ console.log(`📦 Copied migration: ${file} from ${moduleName}`);
445
+ }
446
+ copiedMigrations.push(file);
447
+ }
448
+ });
449
+ }
450
+ // Mettre à jour modules.json avec les migrations
451
+ if (copiedMigrations.length > 0) {
452
+ const moduleIndex = modulesConfig.modules.findIndex((m) => m.package === moduleName);
453
+ if (moduleIndex >= 0) {
454
+ modulesConfig.modules[moduleIndex].migrations = copiedMigrations;
455
+ }
456
+ else {
457
+ modulesConfig.modules.push({
458
+ package: moduleName,
459
+ migrations: copiedMigrations,
460
+ });
461
+ }
462
+ }
463
+ }
464
+ catch (error) {
465
+ console.warn(`⚠️ Could not copy migrations from ${moduleConfig.moduleName}:`, error);
466
+ }
467
+ });
468
+ // Sauvegarder modules.json avec les migrations mises à jour
469
+ ensureDirectory(path.dirname(modulesJsonPath));
470
+ fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
471
+ }
472
+ function generateDocsPage(moduleConfigs) {
473
+ const docsDir = path.join(appDirectory, "docs");
474
+ ensureDirectory(docsDir);
475
+ const docsPagePath = path.join(docsDir, "page.tsx");
476
+ // Charger tous les modules depuis modules.json (actifs ET inactifs)
477
+ const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
478
+ let allModules = [];
479
+ if (fs.existsSync(modulesJsonPath)) {
480
+ try {
481
+ const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
482
+ allModules = modulesData.modules || [];
483
+ }
484
+ catch (error) {
485
+ console.error("❌ Error reading modules.json:", error);
486
+ }
487
+ }
488
+ // Générer les imports des composants Doc de chaque module
489
+ const docImports = [];
490
+ const moduleConfigurations = [];
491
+ allModules.forEach((moduleEntry) => {
492
+ const moduleName = moduleEntry.package;
493
+ // Extraire le nom du module sans le scope et sans "module-"
494
+ // Ex: @lastbrain/module-auth -> auth
495
+ // Ex: @lastbrain-labs/module-recipes-pro -> recipes
496
+ const moduleId = moduleName
497
+ .replace("@lastbrain-labs/module-", "")
498
+ .replace("@lastbrain/module-", "")
499
+ .replace(/-pro$/, ""); // Retirer le suffix -pro pour avoir le nom de base
500
+ const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
501
+ // Trouver la config du module pour obtenir la description
502
+ const moduleConfig = moduleConfigs.find((mc) => mc.moduleName === moduleName);
503
+ const description = moduleConfig
504
+ ? getModuleDescription(moduleConfig)
505
+ : "Module non configuré";
506
+ // Importer le composant Doc seulement pour les modules actifs
507
+ if (moduleEntry.active) {
508
+ docImports.push(`import { ${docComponentName} } from "${moduleName}";`);
509
+ }
510
+ const config = {
511
+ id: moduleName, // Utiliser le nom complet du package comme ID
512
+ name: `Module ${toPascalCase(moduleId)}`,
513
+ description: description,
514
+ component: docComponentName,
515
+ active: moduleEntry.active,
516
+ };
517
+ moduleConfigurations.push(` {
518
+ id: "${config.id}",
519
+ name: "${config.name}",
520
+ description: "${config.description}",
521
+ ${config.active ? `content: <${config.component} />,` : "content: null,"}
522
+ available: ${config.active},
523
+ }`);
524
+ });
525
+ const docsContent = `// Auto-generated docs page with module documentation
526
+ import React from "react";
527
+ import { DocPage } from "@lastbrain/app";
528
+ ${docImports.join("\n")}
529
+
530
+ export default function DocsPage() {
531
+ const modules = [
532
+ ${moduleConfigurations.join(",\n")}
533
+ ];
534
+
535
+ return <DocPage modules={modules} />;
536
+ }
537
+ `;
538
+ fs.writeFileSync(docsPagePath, docsContent);
539
+ if (isDebugMode) {
540
+ console.log(`📚 Generated docs page: ${docsPagePath}`);
541
+ }
542
+ }
543
+ function buildGroupedApi(apis, routePath) {
544
+ const segments = routePath.replace(/^\/+/, "").split("/").filter(Boolean);
545
+ const sanitizedSegments = segments[0] === "api" ? segments.slice(1) : segments;
546
+ const routeDir = path.join(appDirectory, "api", ...sanitizedSegments);
547
+ const filePath = path.join(routeDir, "route.ts");
548
+ ensureDirectory(routeDir);
549
+ // Grouper par module/entryPoint pour créer les exports
550
+ const exportsBySource = new Map();
551
+ apis.forEach(({ moduleConfig, api }) => {
552
+ const handler = `${moduleConfig.moduleName}/${api.entryPoint}`;
553
+ if (!exportsBySource.has(handler)) {
554
+ exportsBySource.set(handler, []);
555
+ }
556
+ exportsBySource.get(handler).push(api.handlerExport);
557
+ });
558
+ // Générer les exports - un export statement par source
559
+ const exportStatements = [];
560
+ exportsBySource.forEach((exports, source) => {
561
+ exportStatements.push(`export { ${exports.join(", ")} } from "${source}";`);
562
+ });
563
+ const content = exportStatements.join("\n") + "\n";
564
+ // Ajouter le marqueur de génération au début
565
+ const contentWithMarker = `// GENERATED BY LASTBRAIN MODULE BUILD
566
+ ${content}`;
567
+ fs.writeFileSync(filePath, contentWithMarker);
568
+ if (isDebugMode) {
569
+ console.log(`🔌 Generated API route: ${filePath}`);
570
+ }
571
+ }
572
+ function getModuleDescription(moduleConfig) {
573
+ // Essayer de déduire la description depuis les pages ou retourner une description par défaut
574
+ if (moduleConfig.pages.length > 0) {
575
+ return `${moduleConfig.pages.length} page(s), ${moduleConfig.apis.length} API(s)`;
576
+ }
577
+ return "Module documentation";
578
+ }
579
+ function cleanGeneratedFiles() {
580
+ const generatedComment = "// GENERATED BY LASTBRAIN MODULE BUILD";
581
+ // Fichiers de base à préserver (paths relatifs exacts)
582
+ const protectedFiles = new Set([
583
+ // API de base
584
+ "api/storage",
585
+ // Layouts de base
586
+ "layout.tsx",
587
+ "not-found.tsx",
588
+ "page.tsx", // Page racine seulement
589
+ "admin/page.tsx", // Page admin racine
590
+ "admin/layout.tsx", // Layout admin racine
591
+ "docs/page.tsx", // Page docs générée
592
+ // Middleware et autres fichiers core
593
+ "middleware.ts",
594
+ // Dossiers de lib et config
595
+ "lib",
596
+ "config",
597
+ ]);
598
+ // Fonction pour vérifier si un chemin est protégé
599
+ const isProtected = (filePath) => {
600
+ const relativePath = path.relative(appDirectory, filePath);
601
+ // Protection exacte pour certains fichiers
602
+ if (protectedFiles.has(relativePath)) {
603
+ return true;
604
+ }
605
+ // Protection par préfixe pour les dossiers
606
+ return Array.from(protectedFiles).some((protectedPath) => (protectedPath.endsWith("/") ||
607
+ ["lib", "config", "api/storage"].includes(protectedPath)) &&
608
+ relativePath.startsWith(protectedPath));
609
+ };
610
+ // Fonction pour nettoyer récursivement un dossier
611
+ const cleanDirectory = (dirPath) => {
612
+ if (!fs.existsSync(dirPath))
613
+ return;
614
+ const items = fs.readdirSync(dirPath);
615
+ for (const item of items) {
616
+ const itemPath = path.join(dirPath, item);
617
+ const stat = fs.statSync(itemPath);
618
+ if (stat.isDirectory()) {
619
+ // Nettoyer récursivement le sous-dossier
620
+ cleanDirectory(itemPath);
621
+ // Supprimer le dossier s'il est vide et non protégé
622
+ try {
623
+ if (!isProtected(itemPath) && fs.readdirSync(itemPath).length === 0) {
624
+ fs.rmdirSync(itemPath);
625
+ if (isDebugMode) {
626
+ console.log(`📁 Removed empty directory: ${itemPath}`);
627
+ }
628
+ }
629
+ }
630
+ catch {
631
+ // Ignorer les erreurs de suppression de fichiers
632
+ }
633
+ }
634
+ else if (item.endsWith(".tsx") || item.endsWith(".ts")) {
635
+ // Vérifier si c'est un fichier généré
636
+ if (!isProtected(itemPath)) {
637
+ try {
638
+ const content = fs.readFileSync(itemPath, "utf-8");
639
+ // Supprimer les fichiers générés ou les wrapper simples de modules
640
+ if (content.includes(generatedComment) ||
641
+ content.includes('from "@lastbrain/module-') ||
642
+ (content.includes("export {") &&
643
+ content.includes('} from "@lastbrain/module-'))) {
644
+ fs.unlinkSync(itemPath);
645
+ if (isDebugMode) {
646
+ console.log(`🗑️ Removed: ${itemPath}`);
647
+ }
648
+ }
649
+ }
650
+ catch {
651
+ // Ignorer les erreurs de lecture/suppression
652
+ }
653
+ }
654
+ else if (isDebugMode) {
655
+ console.log(`🔒 Protected file skipped: ${itemPath}`);
656
+ }
657
+ }
658
+ }
659
+ };
660
+ // Nettoyer les dossiers de sections
661
+ const sectionsToClean = ["(public)", "auth", "admin", "api"];
662
+ sectionsToClean.forEach((section) => {
663
+ const sectionPath = path.join(appDirectory, section);
664
+ if (fs.existsSync(sectionPath)) {
665
+ cleanDirectory(sectionPath);
666
+ }
667
+ });
668
+ // Nettoyer les fichiers générés à la racine
669
+ const rootFiles = ["navigation.generated.ts"];
670
+ rootFiles.forEach((file) => {
671
+ const filePath = path.join(appDirectory, file);
672
+ if (fs.existsSync(filePath)) {
673
+ fs.unlinkSync(filePath);
674
+ if (isDebugMode) {
675
+ console.log(`🗑️ Removed: ${filePath}`);
676
+ }
677
+ }
678
+ });
679
+ if (isDebugMode) {
680
+ console.log("🧹 Cleanup completed");
681
+ }
682
+ }
683
+ function generateAppAside() {
684
+ const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
685
+ // Ne pas écraser si le fichier existe déjà
686
+ if (fs.existsSync(targetPath)) {
687
+ if (isDebugMode) {
688
+ console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
689
+ }
690
+ return;
691
+ }
692
+ const templateContent = `"use client";
693
+
694
+ import { AppAside as UIAppAside } from "@lastbrain/app";
695
+ import { useAuthSession } from "@lastbrain/app";
696
+ import { menuConfig } from "../../config/menu";
697
+
698
+ interface AppAsideProps {
699
+ className?: string;
700
+ isVisible?: boolean;
701
+ }
702
+
703
+ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
704
+ const { isSuperAdmin } = useAuthSession();
705
+
706
+ return (
707
+ <UIAppAside
708
+ className={className}
709
+ menuConfig={menuConfig}
710
+ isSuperAdmin={isSuperAdmin}
711
+ isVisible={isVisible}
712
+ />
713
+ );
714
+ }`;
715
+ try {
716
+ ensureDirectory(path.dirname(targetPath));
717
+ fs.writeFileSync(targetPath, templateContent, "utf-8");
718
+ if (isDebugMode) {
719
+ console.log(`✅ Generated AppAside component: ${targetPath}`);
720
+ }
721
+ }
722
+ catch (error) {
723
+ console.error(`❌ Error generating AppAside component: ${error}`);
724
+ }
725
+ }
726
+ function generateLayouts() {
727
+ // Générer layout auth avec sidebar
728
+ const authLayoutPath = path.join(appDirectory, "auth", "layout.tsx");
729
+ if (!fs.existsSync(authLayoutPath)) {
730
+ const authLayoutContent = `import { AuthLayoutWithSidebar } from "@lastbrain/app";
731
+ import { menuConfig } from "../../config/menu";
732
+
733
+ export default function SectionLayout({
734
+ children,
735
+ }: {
736
+ children: React.ReactNode;
737
+ }) {
738
+ return (
739
+ <AuthLayoutWithSidebar menuConfig={menuConfig}>
740
+ {children}
741
+ </AuthLayoutWithSidebar>
742
+ );
743
+ }`;
744
+ try {
745
+ ensureDirectory(path.dirname(authLayoutPath));
746
+ fs.writeFileSync(authLayoutPath, authLayoutContent, "utf-8");
747
+ if (isDebugMode) {
748
+ console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
749
+ }
750
+ }
751
+ catch (error) {
752
+ console.error(`❌ Error generating auth layout: ${error}`);
753
+ }
754
+ }
755
+ else if (isDebugMode) {
756
+ console.log(`⏭️ Auth layout already exists, skipping: ${authLayoutPath}`);
757
+ }
758
+ // Générer layout admin avec sidebar
759
+ const adminLayoutPath = path.join(appDirectory, "admin", "layout.tsx");
760
+ if (!fs.existsSync(adminLayoutPath)) {
761
+ const adminLayoutContent = `import { AdminLayoutWithSidebar } from "@lastbrain/app";
762
+ import { menuConfig } from "../../config/menu";
763
+
764
+ export default function AdminLayout({
765
+ children,
766
+ }: {
767
+ children: React.ReactNode;
768
+ }) {
769
+ return (
770
+ <AdminLayoutWithSidebar menuConfig={menuConfig}>
771
+ {children}
772
+ </AdminLayoutWithSidebar>
773
+ );
774
+ }`;
775
+ try {
776
+ ensureDirectory(path.dirname(adminLayoutPath));
777
+ fs.writeFileSync(adminLayoutPath, adminLayoutContent, "utf-8");
778
+ if (isDebugMode) {
779
+ console.log(`✅ Generated admin layout with sidebar: ${adminLayoutPath}`);
780
+ }
781
+ }
782
+ catch (error) {
783
+ console.error(`❌ Error generating admin layout: ${error}`);
784
+ }
785
+ }
786
+ else if (isDebugMode) {
787
+ console.log(`⏭️ Admin layout already exists, skipping: ${adminLayoutPath}`);
788
+ }
789
+ }
790
+ async function generateRealtimeConfig(moduleConfigs) {
791
+ try {
792
+ // Extraire les configurations realtime des modules
793
+ const realtimeConfigs = moduleConfigs
794
+ .filter((config) => config.realtime)
795
+ .map((config) => config.realtime);
796
+ if (realtimeConfigs.length === 0) {
797
+ console.log("⏭️ No realtime configuration found in modules");
798
+ return;
799
+ }
800
+ // Générer le contenu TypeScript
801
+ const imports = `import type { ModuleRealtimeConfig } from "@lastbrain/core";`;
802
+ const configData = {
803
+ modules: realtimeConfigs,
804
+ generatedAt: new Date().toISOString(),
805
+ version: "1.0.0",
806
+ };
807
+ const content = `${imports}
808
+
809
+ // GENERATED FILE - DO NOT EDIT MANUALLY
810
+ // Generated at: ${configData.generatedAt}
811
+
812
+ export const realtimeConfig: ModuleRealtimeConfig[] = ${JSON.stringify(configData.modules, null, 2)};
813
+
814
+ export default realtimeConfig;
815
+ `;
816
+ // Créer le fichier de configuration
817
+ const outputPath = path.join(projectRoot, "config", "realtime.ts");
818
+ const configDir = path.dirname(outputPath);
819
+ // Créer le dossier config s'il n'existe pas
820
+ if (!fs.existsSync(configDir)) {
821
+ fs.mkdirSync(configDir, { recursive: true });
822
+ }
823
+ // Écrire le fichier TypeScript
824
+ fs.writeFileSync(outputPath, content);
825
+ if (isDebugMode) {
826
+ console.log(`✅ Generated realtime configuration: ${outputPath}`);
827
+ console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
828
+ // Afficher un résumé
829
+ realtimeConfigs.forEach((module) => {
830
+ console.log(` - ${module.moduleId}: ${module.tables.length} table(s)`);
831
+ module.tables.forEach((table) => {
832
+ console.log(` • ${table.schema}.${table.table} (${table.event})`);
833
+ });
834
+ });
835
+ }
836
+ }
837
+ catch (error) {
838
+ console.error("❌ Error generating realtime configuration:", error);
839
+ }
840
+ }
841
+ async function generateUserTabsConfig(moduleConfigs) {
842
+ try {
843
+ // Extraire les configurations user tabs des modules
844
+ const userTabsConfigs = moduleConfigs
845
+ .filter((config) => config.userTabs && config.userTabs.length > 0)
846
+ .flatMap((config) => config.userTabs.map((tab) => ({
847
+ ...tab,
848
+ moduleName: config.moduleName,
849
+ })))
850
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
851
+ let appContent;
852
+ if (userTabsConfigs.length === 0) {
853
+ console.log("⏭️ No user tabs configuration found in modules, creating empty config");
854
+ // Créer un fichier vide avec l'interface correcte
855
+ const timestamp = new Date().toISOString();
856
+ appContent = `// GENERATED FILE - DO NOT EDIT MANUALLY\n// User tabs configuration\n// Generated at: ${timestamp}\n\n"use client";\n\nimport type React from "react";\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\nexport default moduleUserTabs;\n`;
857
+ }
858
+ else {
859
+ // Générer les imports statiques (Next/dynamic pour chaque composant)
860
+ const importsForApp = userTabsConfigs
861
+ .map((tab) => `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`)
862
+ .join("\n");
863
+ // Générer le tableau des tabs
864
+ const tabsArray = userTabsConfigs
865
+ .map((tab) => ` {
866
+ key: "${tab.key}",
867
+ title: "${tab.title}",
868
+ icon: "${tab.icon || ""}",
869
+ component: ${tab.componentExport},
870
+ }`)
871
+ .join(",\n");
872
+ const timestamp = new Date().toISOString();
873
+ 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`;
874
+ }
875
+ // Créer le fichier de configuration (uniquement dans /config)
876
+ const outputPath = path.join(projectRoot, "config", "user-tabs.ts");
877
+ const configDir = path.dirname(outputPath);
878
+ // Créer le dossier config s'il n'existe pas
879
+ if (!fs.existsSync(configDir)) {
880
+ fs.mkdirSync(configDir, { recursive: true });
881
+ }
882
+ // Écrire le fichier TypeScript
883
+ fs.writeFileSync(outputPath, appContent);
884
+ if (isDebugMode) {
885
+ console.log(`✅ Generated user tabs configuration: ${outputPath}`);
886
+ console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
887
+ // Afficher un résumé
888
+ userTabsConfigs.forEach((tab) => {
889
+ console.log(` - ${tab.title} (${tab.moduleName})`);
890
+ });
891
+ }
892
+ // Plus de copie vers app/config ni stub core
893
+ }
894
+ catch (error) {
895
+ console.error("❌ Error generating user tabs configuration:", error);
896
+ }
897
+ }
898
+ async function generateBucketsConfig(moduleConfigs) {
899
+ try {
900
+ // Extraire les configurations storage des modules
901
+ const allBuckets = moduleConfigs
902
+ .filter((config) => config.storage?.buckets && config.storage.buckets.length > 0)
903
+ .flatMap((config) => config.storage.buckets);
904
+ if (allBuckets.length === 0) {
905
+ console.log("⏭️ No storage buckets configuration found in modules");
906
+ return;
907
+ }
908
+ // Générer le contenu du fichier
909
+ const timestamp = new Date().toISOString();
910
+ const bucketsEntries = allBuckets
911
+ .map((bucket) => {
912
+ // Sérialiser customAccessControl si elle existe
913
+ const customAccessControl = bucket.customAccessControl
914
+ ? `\n customAccessControl: ${bucket.customAccessControl.toString()},`
915
+ : "";
916
+ return ` ${bucket.name}: {
917
+ name: "${bucket.name}",
918
+ isPublic: ${bucket.public},
919
+ description: "${bucket.description || `Storage bucket for ${bucket.name}`}",${bucket.allowedMimeTypes ? `\n allowedFileTypes: ${JSON.stringify(bucket.allowedMimeTypes)},` : ""}${bucket.maxFileSize ? `\n maxFileSize: ${bucket.maxFileSize}, // ${bucket.fileSizeLimit || `${Math.round(bucket.maxFileSize / 1024 / 1024)}MB`}` : ""}${customAccessControl}
920
+ }`;
921
+ })
922
+ .join(",\n");
923
+ const content = `/**
924
+ * Storage configuration for buckets and access control
925
+ *
926
+ * GENERATED FILE - DO NOT EDIT MANUALLY
927
+ * Generated at: ${timestamp}
928
+ * Generated from module build configs
929
+ */
930
+
931
+ export interface BucketConfig {
932
+ name: string;
933
+ isPublic: boolean;
934
+ description: string;
935
+ allowedFileTypes?: string[];
936
+ maxFileSize?: number; // in bytes
937
+ customAccessControl?: (userId: string, filePath: string) => boolean;
938
+ }
939
+
940
+ export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
941
+ ${bucketsEntries}
942
+ };
943
+
944
+ /**
945
+ * Get bucket configuration
946
+ */
947
+ export function getBucketConfig(bucketName: string): BucketConfig | null {
948
+ return BUCKET_CONFIGS[bucketName] || null;
949
+ }
950
+
951
+ /**
952
+ * Check if bucket is public
953
+ */
954
+ export function isPublicBucket(bucketName: string): boolean {
955
+ const config = getBucketConfig(bucketName);
956
+ return config?.isPublic ?? false;
957
+ }
958
+
959
+ /**
960
+ * Check if user has access to a specific file
961
+ */
962
+ export function hasFileAccess(bucketName: string, userId: string, filePath: string): boolean {
963
+ const config = getBucketConfig(bucketName);
964
+ if (!config) return false;
965
+
966
+ // Public buckets are accessible to everyone
967
+ if (config.isPublic) return true;
968
+
969
+ // Private buckets require authentication
970
+ if (!userId) return false;
971
+
972
+ // Apply custom access control if defined
973
+ if (config.customAccessControl) {
974
+ return config.customAccessControl(userId, filePath);
975
+ }
976
+
977
+ return true;
978
+ }
979
+
980
+ /**
981
+ * Validate file type for bucket
982
+ */
983
+ export function isValidFileType(bucketName: string, contentType: string): boolean {
984
+ const config = getBucketConfig(bucketName);
985
+ if (!config || !config.allowedFileTypes) return true;
986
+
987
+ return config.allowedFileTypes.includes(contentType);
988
+ }
989
+
990
+ /**
991
+ * Check if file size is within bucket limits
992
+ */
993
+ export function isValidFileSize(bucketName: string, fileSize: number): boolean {
994
+ const config = getBucketConfig(bucketName);
995
+ if (!config || !config.maxFileSize) return true;
996
+
997
+ return fileSize <= config.maxFileSize;
998
+ }
999
+ `;
1000
+ // Créer le fichier dans lib/
1001
+ const outputPath = path.join(projectRoot, "lib", "bucket-config.ts");
1002
+ const libDir = path.dirname(outputPath);
1003
+ // Créer le dossier lib s'il n'existe pas
1004
+ if (!fs.existsSync(libDir)) {
1005
+ fs.mkdirSync(libDir, { recursive: true });
1006
+ }
1007
+ // Écrire le fichier TypeScript
1008
+ fs.writeFileSync(outputPath, content);
1009
+ if (isDebugMode) {
1010
+ console.log(`✅ Generated storage buckets configuration: ${outputPath}`);
1011
+ console.log(`📊 Storage buckets count: ${allBuckets.length}`);
1012
+ // Afficher un résumé
1013
+ allBuckets.forEach((bucket) => {
1014
+ const access = bucket.public ? "public" : "private";
1015
+ console.log(` - ${bucket.name} (${access})`);
1016
+ });
1017
+ }
1018
+ }
1019
+ catch (error) {
1020
+ console.error("❌ Error generating buckets configuration:", error);
1021
+ }
1022
+ }
1023
+ async function generateStorageProxyApi(moduleConfigs) {
1024
+ try {
1025
+ // Extraire les configurations storage des modules
1026
+ const allBuckets = moduleConfigs
1027
+ .filter((config) => config.storage?.buckets && config.storage.buckets.length > 0)
1028
+ .flatMap((config) => config.storage.buckets);
1029
+ if (allBuckets.length === 0) {
1030
+ console.log("⏭️ No storage buckets found, skipping proxy API generation");
1031
+ return;
1032
+ }
1033
+ // Identifier les buckets publics et privés
1034
+ const publicBuckets = allBuckets.filter((b) => b.public);
1035
+ const privateBuckets = allBuckets.filter((b) => !b.public);
1036
+ // Générer les conditions pour les buckets publics
1037
+ const publicBucketConditions = publicBuckets
1038
+ .map((bucket) => `bucket === "${bucket.name}"`)
1039
+ .filter(Boolean);
1040
+ // Ajouter les conditions pour les chemins publics dans les buckets privés
1041
+ const publicPathConditions = [];
1042
+ if (publicBuckets.some((b) => b.name === "recipes")) {
1043
+ publicPathConditions.push('storagePath.startsWith("recipes/")');
1044
+ }
1045
+ const allPublicConditions = [
1046
+ ...publicBucketConditions,
1047
+ ...publicPathConditions,
1048
+ ];
1049
+ const publicCondition = allPublicConditions.length > 0
1050
+ ? allPublicConditions.join(" || ")
1051
+ : "false";
1052
+ const timestamp = new Date().toISOString();
1053
+ const content = `import { getSupabaseServerClient } from "@lastbrain/core/server";
1054
+ import { NextRequest, NextResponse } from "next/server";
1055
+
1056
+ /**
1057
+ * GET /api/storage/[bucket]/[...path]
1058
+ * Proxy pour servir les images avec authentication
1059
+ *
1060
+ * GENERATED FILE - DO NOT EDIT MANUALLY
1061
+ * Generated at: ${timestamp}
1062
+ * Generated from module storage configurations
1063
+ * Buckets configurés:
1064
+ ${allBuckets.map((b) => ` * - ${b.name} (${b.public ? "public" : "private"})`).join("\n")}
1065
+ */
1066
+ export async function GET(
1067
+ request: NextRequest,
1068
+ context: { params: Promise<{ bucket: string; path: string[] }> },
1069
+ ) {
1070
+ try {
1071
+ const { bucket, path } = await context.params;
1072
+ const storagePath = path.join("/");
1073
+
1074
+ // Les images publiques sont accessibles sans auth
1075
+ if (${publicCondition}) {
1076
+ const supabase = await getSupabaseServerClient();
1077
+ const { data, error } = await supabase.storage
1078
+ .from(bucket)
1079
+ .createSignedUrl(storagePath, 3600); // 1 heure
1080
+
1081
+ if (error) {
1082
+ console.error(\`[storage] Error creating signed URL for public image:\`, error);
1083
+ return new NextResponse("Not found", { status: 404 });
1084
+ }
1085
+
1086
+ // Rediriger vers l'URL signée
1087
+ return NextResponse.redirect(data.signedUrl);
1088
+ }
1089
+
1090
+ // Les images privées nécessitent une authentification
1091
+ const supabase = await getSupabaseServerClient();
1092
+ const {
1093
+ data: { user },
1094
+ error: authError,
1095
+ } = await supabase.auth.getUser();
1096
+
1097
+ if (authError || !user) {
1098
+ return new NextResponse("Unauthorized", { status: 401 });
1099
+ }
1100
+
1101
+ // Cas spécial: si le chemin commence par /product/ ou /recipe/,
1102
+ // c'est une image avec format court qui nécessite le préfixe userId
1103
+ let actualStoragePath = storagePath;
1104
+
1105
+ if (storagePath.startsWith("product/") || storagePath.startsWith("recipe/")) {
1106
+ actualStoragePath = \`\${user.id}/\${storagePath}\`;
1107
+ }
1108
+
1109
+ // Vérifier que l'utilisateur a accès à cette image
1110
+ // Format: {userId}/recipe/{recipeId}/{filename} ou {userId}/product/{productId}/{filename}
1111
+ const pathParts = actualStoragePath.split("/");
1112
+ const pathUserId = pathParts[0];
1113
+
1114
+ if (pathUserId !== user.id) {
1115
+ return new NextResponse("Forbidden", { status: 403 });
1116
+ }
1117
+
1118
+ // Créer une URL signée pour l'image privée
1119
+ const { data, error } = await supabase.storage
1120
+ .from(bucket)
1121
+ .createSignedUrl(actualStoragePath, 3600); // 1 heure
1122
+
1123
+ if (error) {
1124
+ console.error(\`[storage] Error creating signed URL:\`, error);
1125
+ return new NextResponse("Not found", { status: 404 });
1126
+ }
1127
+
1128
+ // Rediriger vers l'URL signée
1129
+ return NextResponse.redirect(data.signedUrl);
1130
+ } catch (error) {
1131
+ console.error("[storage] Error:", error);
1132
+ return new NextResponse("Internal server error", { status: 500 });
1133
+ }
1134
+ }
1135
+ `;
1136
+ // Créer le fichier dans app/api/storage/[bucket]/[...path]/
1137
+ const outputPath = path.join(projectRoot, "app", "api", "storage", "[bucket]", "[...path]", "route.ts");
1138
+ const outputDir = path.dirname(outputPath);
1139
+ // Créer le dossier s'il n'existe pas
1140
+ if (!fs.existsSync(outputDir)) {
1141
+ fs.mkdirSync(outputDir, { recursive: true });
1142
+ }
1143
+ // Écrire le fichier TypeScript
1144
+ fs.writeFileSync(outputPath, content);
1145
+ if (isDebugMode) {
1146
+ console.log(`✅ Generated storage proxy API: ${outputPath}`);
1147
+ console.log(`📊 Public buckets: ${publicBuckets.map((b) => b.name).join(", ") || "none"}`);
1148
+ console.log(`📊 Private buckets: ${privateBuckets.map((b) => b.name).join(", ") || "none"}`);
1149
+ }
1150
+ }
1151
+ catch (error) {
1152
+ console.error("❌ Error generating storage proxy API:", error);
1153
+ }
1154
+ }
1155
+ async function generateFooterConfig(moduleConfigs) {
1156
+ try {
1157
+ // Extraire tous les liens footer des modules
1158
+ const allFooterLinks = moduleConfigs
1159
+ .filter((config) => config.footer && config.footer.length > 0)
1160
+ .flatMap((config) => config.footer);
1161
+ if (allFooterLinks.length === 0) {
1162
+ console.log("⏭️ No footer links found, skipping footer config generation");
1163
+ return;
1164
+ }
1165
+ // Trier les liens par position et order
1166
+ allFooterLinks.sort((a, b) => {
1167
+ const posOrder = { left: 0, center: 1, right: 2 };
1168
+ const posA = posOrder[a.position || "left"] || 0;
1169
+ const posB = posOrder[b.position || "left"] || 0;
1170
+ if (posA !== posB)
1171
+ return posA - posB;
1172
+ return (a.order || 0) - (b.order || 0);
1173
+ });
1174
+ const timestamp = new Date().toISOString();
1175
+ const content = `// Auto-generated footer configuration
1176
+ // Generated from module build configs
1177
+ // Generated at: ${timestamp}
1178
+ "use client";
1179
+
1180
+ import type { FooterConfig } from "@lastbrain/ui";
1181
+
1182
+ export const footerConfig: FooterConfig = {
1183
+ companyName: "LastBrain",
1184
+ companyDescription: "Plateforme de développement rapide d'applications",
1185
+ links: ${JSON.stringify(allFooterLinks, null, 2)},
1186
+ social: [],
1187
+ };
1188
+ `;
1189
+ const configDir = path.join(projectRoot, "config");
1190
+ ensureDirectory(configDir);
1191
+ const footerPath = path.join(configDir, "footer.ts");
1192
+ fs.writeFileSync(footerPath, content);
1193
+ if (isDebugMode) {
1194
+ console.log(`✅ Generated footer config: ${footerPath}`);
1195
+ console.log(` - ${allFooterLinks.length} footer link(s)`);
1196
+ }
1197
+ }
1198
+ catch (error) {
1199
+ console.error("❌ Error generating footer config:", error);
1200
+ }
1201
+ }
1202
+ export async function runModuleBuild() {
1203
+ ensureDirectory(appDirectory);
1204
+ // Nettoyer les fichiers générés précédemment
1205
+ if (isDebugMode) {
1206
+ console.log("🧹 Cleaning previously generated files...");
1207
+ }
1208
+ cleanGeneratedFiles();
1209
+ const moduleConfigs = await loadModuleConfigs();
1210
+ if (isDebugMode) {
1211
+ console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
1212
+ }
1213
+ // Générer les pages
1214
+ if (isDebugMode) {
1215
+ console.log("\n📝 Generating pages...");
1216
+ }
1217
+ moduleConfigs.forEach((moduleConfig) => {
1218
+ if (isDebugMode) {
1219
+ console.log(`📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`);
1220
+ }
1221
+ moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
1222
+ });
1223
+ // Grouper les APIs par chemin pour éviter les écrasements de fichier
1224
+ if (isDebugMode) {
1225
+ console.log("\n🔌 Generating API routes...");
1226
+ }
1227
+ const apisByPath = new Map();
1228
+ moduleConfigs.forEach((moduleConfig) => {
1229
+ moduleConfig.apis.forEach((api) => {
1230
+ if (!apisByPath.has(api.path)) {
1231
+ apisByPath.set(api.path, []);
1232
+ }
1233
+ apisByPath.get(api.path).push({ moduleConfig, api });
1234
+ });
1235
+ });
1236
+ // Générer les fichiers de route groupés
1237
+ apisByPath.forEach((apis, routePath) => {
1238
+ buildGroupedApi(apis, routePath);
1239
+ });
1240
+ dumpNavigation();
1241
+ generateMenuConfig(moduleConfigs);
1242
+ generateDocsPage(moduleConfigs);
1243
+ generateAppAside();
1244
+ generateLayouts();
1245
+ copyModuleMigrations(moduleConfigs);
1246
+ // Générer la configuration realtime
1247
+ if (isDebugMode) {
1248
+ console.log("🔄 Generating realtime configuration...");
1249
+ }
1250
+ await generateRealtimeConfig(moduleConfigs);
1251
+ // Générer la configuration des user tabs
1252
+ if (isDebugMode) {
1253
+ console.log("📑 Generating user tabs configuration...");
1254
+ }
1255
+ await generateUserTabsConfig(moduleConfigs);
1256
+ // Générer la configuration des buckets storage
1257
+ if (isDebugMode) {
1258
+ console.log("🗄️ Generating storage buckets configuration...");
1259
+ }
1260
+ await generateBucketsConfig(moduleConfigs);
1261
+ // Générer le proxy storage API
1262
+ if (isDebugMode) {
1263
+ console.log("🔌 Generating storage proxy API...");
1264
+ }
1265
+ await generateStorageProxyApi(moduleConfigs);
1266
+ // Générer la configuration footer
1267
+ if (isDebugMode) {
1268
+ console.log("🦶 Generating footer configuration...");
1269
+ }
1270
+ await generateFooterConfig(moduleConfigs);
1271
+ // Message de succès final
1272
+ if (!isDebugMode) {
1273
+ console.log("\n✅ Module build completed successfully!");
1274
+ console.log(`📦 ${moduleConfigs.length} module(s) processed`);
1275
+ console.log("💡 Use --debug flag for detailed logs\n");
1276
+ }
1277
+ else {
1278
+ console.log("\n✅ Module build completed (debug mode)\n");
1279
+ }
1280
+ }