@lastbrain/app 2.0.1 → 2.0.3
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/config/version.d.ts +7 -0
- package/dist/config/version.d.ts.map +1 -0
- package/dist/config/version.js +25 -0
- package/dist/src/__tests__/module-registry.test.d.ts +2 -0
- package/dist/src/__tests__/module-registry.test.d.ts.map +1 -0
- package/dist/src/__tests__/module-registry.test.js +53 -0
- package/dist/src/app-shell/(admin)/layout.d.ts +4 -0
- package/dist/src/app-shell/(admin)/layout.d.ts.map +1 -0
- package/dist/src/app-shell/(admin)/layout.js +5 -0
- package/dist/src/app-shell/(auth)/layout.d.ts +4 -0
- package/dist/src/app-shell/(auth)/layout.d.ts.map +1 -0
- package/dist/src/app-shell/(auth)/layout.js +5 -0
- package/dist/src/app-shell/(public)/page.d.ts +2 -0
- package/dist/src/app-shell/(public)/page.d.ts.map +1 -0
- package/dist/src/app-shell/(public)/page.js +5 -0
- package/dist/src/app-shell/layout.d.ts +3 -0
- package/dist/src/app-shell/layout.d.ts.map +1 -0
- package/dist/src/app-shell/layout.js +3 -0
- package/dist/src/app-shell/not-found.d.ts +2 -0
- package/dist/src/app-shell/not-found.d.ts.map +1 -0
- package/dist/src/app-shell/not-found.js +10 -0
- package/dist/src/auth/authHelpers.d.ts +7 -0
- package/dist/src/auth/authHelpers.d.ts.map +1 -0
- package/dist/src/auth/authHelpers.js +19 -0
- package/dist/src/auth/useAuthSession.d.ts +7 -0
- package/dist/src/auth/useAuthSession.d.ts.map +1 -0
- package/dist/src/auth/useAuthSession.js +49 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +143 -0
- package/dist/src/components/NotificationContainer.d.ts +2 -0
- package/dist/src/components/NotificationContainer.d.ts.map +1 -0
- package/dist/src/components/NotificationContainer.js +8 -0
- package/dist/src/hooks/useNotifications.d.ts +30 -0
- package/dist/src/hooks/useNotifications.d.ts.map +1 -0
- package/dist/src/hooks/useNotifications.js +165 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +22 -0
- package/dist/src/layouts/AdminLayout.d.ts +4 -0
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayout.js +4 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +10 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.js +62 -0
- package/dist/src/layouts/AppProviders.d.ts +27 -0
- package/dist/src/layouts/AppProviders.d.ts.map +1 -0
- package/dist/src/layouts/AppProviders.js +48 -0
- package/dist/src/layouts/AuthLayout.d.ts +4 -0
- package/dist/src/layouts/AuthLayout.d.ts.map +1 -0
- package/dist/src/layouts/AuthLayout.js +4 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +12 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.js +60 -0
- package/dist/src/layouts/PublicLayout.d.ts +8 -0
- package/dist/src/layouts/PublicLayout.d.ts.map +1 -0
- package/dist/src/layouts/PublicLayout.js +6 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +9 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.js +60 -0
- package/dist/src/layouts/RootLayout.d.ts +6 -0
- package/dist/src/layouts/RootLayout.d.ts.map +1 -0
- package/dist/src/layouts/RootLayout.js +9 -0
- package/dist/src/modules/module-loader.d.ts +5 -0
- package/dist/src/modules/module-loader.d.ts.map +1 -0
- package/dist/src/modules/module-loader.js +10 -0
- package/dist/src/scripts/db-init.d.ts +2 -0
- package/dist/src/scripts/db-init.d.ts.map +1 -0
- package/dist/src/scripts/db-init.js +300 -0
- package/dist/src/scripts/db-migrations-sync.d.ts +2 -0
- package/dist/src/scripts/db-migrations-sync.d.ts.map +1 -0
- package/dist/src/scripts/db-migrations-sync.js +84 -0
- package/dist/src/scripts/dev-sync.d.ts +2 -0
- package/dist/src/scripts/dev-sync.d.ts.map +1 -0
- package/dist/src/scripts/dev-sync.js +194 -0
- package/dist/src/scripts/init-app.d.ts +12 -0
- package/dist/src/scripts/init-app.d.ts.map +1 -0
- package/dist/src/scripts/init-app.js +2175 -0
- package/dist/src/scripts/module-add.d.ts +2 -0
- package/dist/src/scripts/module-add.d.ts.map +1 -0
- package/dist/src/scripts/module-add.js +232 -0
- package/dist/src/scripts/module-build.d.ts +2 -0
- package/dist/src/scripts/module-build.d.ts.map +1 -0
- package/dist/src/scripts/module-build.js +1280 -0
- package/dist/src/scripts/module-create.d.ts +28 -0
- package/dist/src/scripts/module-create.d.ts.map +1 -0
- package/dist/src/scripts/module-create.js +1429 -0
- package/dist/src/scripts/module-delete.d.ts +6 -0
- package/dist/src/scripts/module-delete.d.ts.map +1 -0
- package/dist/src/scripts/module-delete.js +147 -0
- package/dist/src/scripts/module-list.d.ts +2 -0
- package/dist/src/scripts/module-list.d.ts.map +1 -0
- package/dist/src/scripts/module-list.js +61 -0
- package/dist/src/scripts/module-remove.d.ts +2 -0
- package/dist/src/scripts/module-remove.d.ts.map +1 -0
- package/dist/src/scripts/module-remove.js +311 -0
- package/dist/src/scripts/readme-build.d.ts +2 -0
- package/dist/src/scripts/readme-build.d.ts.map +1 -0
- package/dist/src/scripts/readme-build.js +39 -0
- package/dist/src/scripts/script-runner.d.ts +5 -0
- package/dist/src/scripts/script-runner.d.ts.map +1 -0
- package/dist/src/scripts/script-runner.js +25 -0
- package/dist/src/templates/AuthGuidePage.d.ts +2 -0
- package/dist/src/templates/AuthGuidePage.d.ts.map +1 -0
- package/dist/src/templates/AuthGuidePage.js +9 -0
- package/dist/src/templates/DefaultDoc.d.ts +2 -0
- package/dist/src/templates/DefaultDoc.d.ts.map +1 -0
- package/dist/src/templates/DefaultDoc.js +240 -0
- package/dist/src/templates/DocPage.d.ts +17 -0
- package/dist/src/templates/DocPage.d.ts.map +1 -0
- package/dist/src/templates/DocPage.js +193 -0
- package/dist/src/templates/DocsPageWithModules.d.ts +2 -0
- package/dist/src/templates/DocsPageWithModules.d.ts.map +1 -0
- package/dist/src/templates/DocsPageWithModules.js +8 -0
- package/dist/src/templates/MigrationsGuidePage.d.ts +2 -0
- package/dist/src/templates/MigrationsGuidePage.d.ts.map +1 -0
- package/dist/src/templates/MigrationsGuidePage.js +11 -0
- package/dist/src/templates/ModuleGuidePage.d.ts +2 -0
- package/dist/src/templates/ModuleGuidePage.d.ts.map +1 -0
- package/dist/src/templates/ModuleGuidePage.js +14 -0
- package/dist/src/templates/SimpleDocPage.d.ts +2 -0
- package/dist/src/templates/SimpleDocPage.d.ts.map +1 -0
- package/dist/src/templates/SimpleDocPage.js +28 -0
- package/dist/src/templates/SimpleHomePage.d.ts +6 -0
- package/dist/src/templates/SimpleHomePage.d.ts.map +1 -0
- package/dist/src/templates/SimpleHomePage.js +7 -0
- package/dist/src/types/menu.d.ts +23 -0
- package/dist/src/types/menu.d.ts.map +1 -0
- package/dist/src/types/menu.js +1 -0
- package/package.json +4 -5
- package/src/scripts/init-app.ts +7 -76
|
@@ -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
|
+
}
|