@lastbrain/app 0.1.25 → 0.1.27
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/app-shell/(public)/page.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/layouts/AdminLayoutWithSidebar.d.ts +8 -0
- package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
- package/dist/layouts/AdminLayoutWithSidebar.js +9 -0
- package/dist/layouts/AuthLayoutWithSidebar.d.ts +8 -0
- package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
- package/dist/layouts/AuthLayoutWithSidebar.js +9 -0
- package/dist/scripts/db-init.js +2 -2
- package/dist/scripts/dev-sync.js +23 -12
- package/dist/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +114 -12
- package/dist/scripts/module-add.d.ts.map +1 -1
- package/dist/scripts/module-add.js +19 -6
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +288 -30
- package/dist/scripts/module-create.d.ts.map +1 -1
- package/dist/scripts/module-create.js +25 -15
- package/dist/scripts/module-remove.d.ts.map +1 -1
- package/dist/scripts/module-remove.js +28 -15
- package/dist/scripts/script-runner.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.js +1 -7
- package/dist/templates/components/AppAside.d.ts +6 -0
- package/dist/templates/components/AppAside.d.ts.map +1 -0
- package/dist/templates/components/AppAside.js +9 -0
- package/dist/templates/layouts/admin-layout.d.ts +4 -0
- package/dist/templates/layouts/admin-layout.d.ts.map +1 -0
- package/dist/templates/layouts/admin-layout.js +6 -0
- package/dist/templates/layouts/auth-layout.d.ts +4 -0
- package/dist/templates/layouts/auth-layout.d.ts.map +1 -0
- package/dist/templates/layouts/auth-layout.js +6 -0
- package/package.json +2 -1
- package/src/app-shell/(public)/page.tsx +6 -2
- package/src/auth/useAuthSession.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/index.ts +6 -0
- package/src/layouts/AdminLayoutWithSidebar.tsx +35 -0
- package/src/layouts/AppProviders.tsx +1 -1
- package/src/layouts/AuthLayoutWithSidebar.tsx +35 -0
- package/src/scripts/db-init.ts +13 -8
- package/src/scripts/db-migrations-sync.ts +4 -4
- package/src/scripts/dev-sync.ts +50 -19
- package/src/scripts/init-app.ts +243 -65
- package/src/scripts/module-add.ts +54 -22
- package/src/scripts/module-build.ts +412 -88
- package/src/scripts/module-create.ts +85 -49
- package/src/scripts/module-remove.ts +120 -61
- package/src/scripts/readme-build.ts +2 -2
- package/src/scripts/script-runner.ts +3 -3
- package/src/templates/AuthGuidePage.tsx +1 -1
- package/src/templates/DefaultDoc.tsx +7 -7
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { createRequire } from "node:module";
|
|
5
4
|
|
|
6
5
|
import type {
|
|
7
6
|
ModuleApiConfig,
|
|
8
7
|
ModuleBuildConfig,
|
|
9
|
-
ModuleMenuConfig,
|
|
10
8
|
ModuleMenuItemConfig,
|
|
11
9
|
ModulePageConfig,
|
|
12
10
|
ModuleSection,
|
|
@@ -23,11 +21,11 @@ const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
|
23
21
|
async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
|
|
24
22
|
const moduleConfigs: ModuleBuildConfig[] = [];
|
|
25
23
|
const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
|
|
26
|
-
|
|
24
|
+
|
|
27
25
|
if (!fs.existsSync(modulesJsonPath)) {
|
|
28
26
|
return moduleConfigs;
|
|
29
27
|
}
|
|
30
|
-
|
|
28
|
+
|
|
31
29
|
try {
|
|
32
30
|
const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
|
|
33
31
|
const modules = modulesData.modules || [];
|
|
@@ -37,9 +35,9 @@ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
|
|
|
37
35
|
if (module.active === false) {
|
|
38
36
|
continue;
|
|
39
37
|
}
|
|
40
|
-
|
|
38
|
+
|
|
41
39
|
const packageName = module.package;
|
|
42
|
-
|
|
40
|
+
|
|
43
41
|
try {
|
|
44
42
|
const moduleSuffix = packageName.replace("@lastbrain/module-", "");
|
|
45
43
|
const possibleConfigNames = [
|
|
@@ -51,17 +49,19 @@ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
|
|
|
51
49
|
for (const configName of possibleConfigNames) {
|
|
52
50
|
try {
|
|
53
51
|
// Résoudre le chemin du module depuis l'application
|
|
54
|
-
const modulePath = projectRequire.resolve(
|
|
52
|
+
const modulePath = projectRequire.resolve(
|
|
53
|
+
`${packageName}/${configName}`,
|
|
54
|
+
);
|
|
55
55
|
// Convertir en URL file:// pour l'import dynamique
|
|
56
56
|
const moduleUrl = `file://${modulePath}`;
|
|
57
57
|
const moduleImport = await import(moduleUrl);
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
if (moduleImport.default) {
|
|
60
60
|
moduleConfigs.push(moduleImport.default);
|
|
61
61
|
loaded = true;
|
|
62
62
|
break;
|
|
63
63
|
}
|
|
64
|
-
} catch
|
|
64
|
+
} catch {
|
|
65
65
|
// Essayer le nom suivant
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -76,7 +76,7 @@ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
|
|
|
76
76
|
} catch (error) {
|
|
77
77
|
console.error("❌ Error loading modules.json:", error);
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
return moduleConfigs;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -151,12 +151,39 @@ function toPascalCase(value: string) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
154
|
-
|
|
154
|
+
// Extraire le préfixe du module (ex: @lastbrain/module-auth -> auth)
|
|
155
|
+
const modulePrefix = moduleConfig.moduleName
|
|
156
|
+
.replace(/^@lastbrain\/module-/, "")
|
|
157
|
+
.toLowerCase();
|
|
158
|
+
|
|
159
|
+
console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
|
|
160
|
+
|
|
161
|
+
// Ajouter le préfixe du module au path pour les sections admin et auth,
|
|
162
|
+
// MAIS seulement quand la section ne correspond PAS au module lui-même
|
|
163
|
+
let effectivePath = page.path;
|
|
164
|
+
if (
|
|
165
|
+
page.section === "admin" ||
|
|
166
|
+
(page.section === "auth" && modulePrefix !== "auth")
|
|
167
|
+
) {
|
|
168
|
+
// Éviter les doublons si le préfixe est déjà présent
|
|
169
|
+
if (!page.path.startsWith(`/${modulePrefix}/`)) {
|
|
170
|
+
effectivePath = `/${modulePrefix}${page.path}`;
|
|
171
|
+
console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(`✅ Module prefix already present: ${page.path}`);
|
|
174
|
+
}
|
|
175
|
+
} else if (page.section === "auth" && modulePrefix === "auth") {
|
|
176
|
+
console.log(
|
|
177
|
+
`🏠 Auth module in auth section, no prefix needed: ${page.path}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const segments = effectivePath.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
155
182
|
const sectionPath = sectionDirectoryMap[page.section] ?? ["(public)"];
|
|
156
|
-
|
|
183
|
+
|
|
157
184
|
// Générer le layout de section si nécessaire
|
|
158
185
|
ensureSectionLayout(sectionPath);
|
|
159
|
-
|
|
186
|
+
|
|
160
187
|
const routeDir = path.join(appDirectory, ...sectionPath, ...segments);
|
|
161
188
|
const filePath = path.join(routeDir, "page.tsx");
|
|
162
189
|
ensureDirectory(routeDir);
|
|
@@ -167,17 +194,23 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
|
167
194
|
const wrapperName = `${page.componentExport}${wrapperSuffix}Route`;
|
|
168
195
|
|
|
169
196
|
// Vérifier si la route a des paramètres dynamiques (segments avec [])
|
|
170
|
-
const hasDynamicParams = segments.some(
|
|
197
|
+
const hasDynamicParams = segments.some(
|
|
198
|
+
(seg) => seg.startsWith("[") && seg.endsWith("]"),
|
|
199
|
+
);
|
|
171
200
|
|
|
172
201
|
// Pour les pages publiques (signin, signup, etc.), utiliser dynamic import sans SSR
|
|
173
202
|
// pour éviter les erreurs d'hydratation avec les IDs HeroUI/React Aria
|
|
174
|
-
const isPublicAuthPage =
|
|
175
|
-
|
|
203
|
+
const isPublicAuthPage =
|
|
204
|
+
page.section === "public" &&
|
|
205
|
+
(page.path.includes("signin") ||
|
|
206
|
+
page.path.includes("signup") ||
|
|
207
|
+
page.path.includes("reset-password"));
|
|
176
208
|
|
|
177
209
|
let content: string;
|
|
178
|
-
|
|
210
|
+
|
|
179
211
|
if (isPublicAuthPage) {
|
|
180
|
-
content =
|
|
212
|
+
content = `// GENERATED BY LASTBRAIN MODULE BUILD
|
|
213
|
+
"use client";
|
|
181
214
|
|
|
182
215
|
import dynamic from "next/dynamic";
|
|
183
216
|
|
|
@@ -186,23 +219,25 @@ const ${page.componentExport} = dynamic(
|
|
|
186
219
|
{ ssr: false }
|
|
187
220
|
);
|
|
188
221
|
|
|
189
|
-
export default function ${wrapperName}${hasDynamicParams ?
|
|
190
|
-
return <${page.componentExport} ${hasDynamicParams ?
|
|
222
|
+
export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"} {
|
|
223
|
+
return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
|
|
191
224
|
}
|
|
192
225
|
`;
|
|
193
226
|
} else {
|
|
194
|
-
content =
|
|
227
|
+
content = `// GENERATED BY LASTBRAIN MODULE BUILD
|
|
228
|
+
import { ${page.componentExport} } from "${moduleConfig.moduleName}";
|
|
195
229
|
|
|
196
|
-
export default function ${wrapperName}${hasDynamicParams ?
|
|
197
|
-
return <${page.componentExport} ${hasDynamicParams ?
|
|
230
|
+
export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"} {
|
|
231
|
+
return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
|
|
198
232
|
}
|
|
199
233
|
`;
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
fs.writeFileSync(filePath, content);
|
|
203
|
-
console.log(`⭐ Generated page: ${filePath}`);
|
|
237
|
+
console.log(`⭐ Generated page: ${filePath}`);
|
|
238
|
+
const entry: MenuEntry = {
|
|
204
239
|
label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
|
|
205
|
-
path:
|
|
240
|
+
path: effectivePath,
|
|
206
241
|
module: moduleConfig.moduleName,
|
|
207
242
|
section: page.section,
|
|
208
243
|
};
|
|
@@ -214,22 +249,6 @@ export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'
|
|
|
214
249
|
}
|
|
215
250
|
}
|
|
216
251
|
|
|
217
|
-
function buildApi(moduleConfig: ModuleBuildConfig, api: ModuleApiConfig) {
|
|
218
|
-
const segments = api.path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
219
|
-
const sanitizedSegments =
|
|
220
|
-
segments[0] === "api" ? segments.slice(1) : segments;
|
|
221
|
-
const routeDir = path.join(appDirectory, "api", ...sanitizedSegments);
|
|
222
|
-
const filePath = path.join(routeDir, "route.ts");
|
|
223
|
-
ensureDirectory(routeDir);
|
|
224
|
-
|
|
225
|
-
const handler = `${moduleConfig.moduleName}/${api.entryPoint}`;
|
|
226
|
-
const content = `export { ${api.handlerExport} } from "${handler}";
|
|
227
|
-
`;
|
|
228
|
-
|
|
229
|
-
fs.writeFileSync(filePath, content);
|
|
230
|
-
console.log(`🔌 Generated API route: ${filePath}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
252
|
function dumpNavigation() {
|
|
234
253
|
const navPath = path.join(appDirectory, "navigation.generated.ts");
|
|
235
254
|
const content = `export type MenuEntry = { label: string; path: string; module: string; section: string };
|
|
@@ -248,15 +267,15 @@ export const userMenu = ${JSON.stringify(userMenu, null, 2)};
|
|
|
248
267
|
function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
249
268
|
const configDir = path.join(projectRoot, "config");
|
|
250
269
|
ensureDirectory(configDir);
|
|
251
|
-
|
|
270
|
+
|
|
252
271
|
const menuPath = path.join(configDir, "menu.ts");
|
|
253
|
-
|
|
272
|
+
|
|
254
273
|
// Collecter les menus de tous les modules
|
|
255
274
|
const publicMenus: ModuleMenuItemConfig[] = [];
|
|
256
275
|
const authMenus: ModuleMenuItemConfig[] = [];
|
|
257
276
|
const adminMenus: ModuleMenuItemConfig[] = [];
|
|
258
277
|
const accountMenus: ModuleMenuItemConfig[] = [];
|
|
259
|
-
|
|
278
|
+
|
|
260
279
|
moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
|
|
261
280
|
if (moduleConfig.menu) {
|
|
262
281
|
if (moduleConfig.menu.public) {
|
|
@@ -273,17 +292,17 @@ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
273
292
|
}
|
|
274
293
|
}
|
|
275
294
|
});
|
|
276
|
-
|
|
295
|
+
|
|
277
296
|
// Trier par ordre
|
|
278
297
|
const sortByOrder = (a: ModuleMenuItemConfig, b: ModuleMenuItemConfig) => {
|
|
279
298
|
return (a.order ?? 999) - (b.order ?? 999);
|
|
280
299
|
};
|
|
281
|
-
|
|
300
|
+
|
|
282
301
|
publicMenus.sort(sortByOrder);
|
|
283
302
|
authMenus.sort(sortByOrder);
|
|
284
303
|
adminMenus.sort(sortByOrder);
|
|
285
304
|
accountMenus.sort(sortByOrder);
|
|
286
|
-
|
|
305
|
+
|
|
287
306
|
// Générer le contenu du fichier
|
|
288
307
|
const content = `// Auto-generated menu configuration
|
|
289
308
|
// Generated from module build configs
|
|
@@ -310,32 +329,38 @@ export const menuConfig: MenuConfig = {
|
|
|
310
329
|
account: ${JSON.stringify(accountMenus, null, 2)},
|
|
311
330
|
};
|
|
312
331
|
`;
|
|
313
|
-
|
|
332
|
+
|
|
314
333
|
fs.writeFileSync(menuPath, content);
|
|
315
334
|
console.log(`🍔 Generated menu configuration: ${menuPath}`);
|
|
316
335
|
}
|
|
317
336
|
|
|
318
337
|
function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
319
|
-
const supabaseMigrationsDir = path.join(
|
|
320
|
-
|
|
338
|
+
const supabaseMigrationsDir = path.join(
|
|
339
|
+
projectRoot,
|
|
340
|
+
"supabase",
|
|
341
|
+
"migrations",
|
|
342
|
+
);
|
|
343
|
+
|
|
321
344
|
// S'assurer que le dossier migrations existe
|
|
322
345
|
if (!fs.existsSync(supabaseMigrationsDir)) {
|
|
323
346
|
ensureDirectory(supabaseMigrationsDir);
|
|
324
347
|
}
|
|
325
348
|
|
|
326
349
|
const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
|
|
327
|
-
|
|
350
|
+
|
|
328
351
|
// Charger modules.json
|
|
329
|
-
let modulesConfig: {
|
|
352
|
+
let modulesConfig: {
|
|
353
|
+
modules: Array<{ package: string; migrations?: string[] }>;
|
|
354
|
+
} = { modules: [] };
|
|
330
355
|
if (fs.existsSync(modulesJsonPath)) {
|
|
331
356
|
modulesConfig = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
|
|
332
357
|
}
|
|
333
|
-
|
|
358
|
+
|
|
334
359
|
moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
|
|
335
360
|
try {
|
|
336
361
|
const moduleName = moduleConfig.moduleName;
|
|
337
362
|
const copiedMigrations: string[] = [];
|
|
338
|
-
|
|
363
|
+
|
|
339
364
|
// Essayer plusieurs chemins possibles pour trouver le module
|
|
340
365
|
const possiblePaths = [
|
|
341
366
|
// Dans node_modules local de l'app
|
|
@@ -343,32 +368,42 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
343
368
|
// Dans node_modules à la racine du workspace
|
|
344
369
|
path.join(projectRoot, "..", "..", "node_modules", moduleName),
|
|
345
370
|
// Directement dans packages/ (pour monorepo)
|
|
346
|
-
path.join(
|
|
371
|
+
path.join(
|
|
372
|
+
projectRoot,
|
|
373
|
+
"..",
|
|
374
|
+
"..",
|
|
375
|
+
"packages",
|
|
376
|
+
moduleName.replace("@lastbrain/", ""),
|
|
377
|
+
),
|
|
347
378
|
];
|
|
348
|
-
|
|
379
|
+
|
|
349
380
|
let moduleBasePath: string | null = null;
|
|
350
|
-
|
|
381
|
+
|
|
351
382
|
for (const possiblePath of possiblePaths) {
|
|
352
383
|
if (fs.existsSync(possiblePath)) {
|
|
353
384
|
moduleBasePath = possiblePath;
|
|
354
385
|
break;
|
|
355
386
|
}
|
|
356
387
|
}
|
|
357
|
-
|
|
388
|
+
|
|
358
389
|
if (!moduleBasePath) {
|
|
359
390
|
return;
|
|
360
391
|
}
|
|
361
|
-
|
|
362
|
-
const moduleMigrationsDir = path.join(
|
|
363
|
-
|
|
392
|
+
|
|
393
|
+
const moduleMigrationsDir = path.join(
|
|
394
|
+
moduleBasePath,
|
|
395
|
+
"supabase",
|
|
396
|
+
"migrations",
|
|
397
|
+
);
|
|
398
|
+
|
|
364
399
|
if (fs.existsSync(moduleMigrationsDir)) {
|
|
365
400
|
const migrationFiles = fs.readdirSync(moduleMigrationsDir);
|
|
366
|
-
|
|
401
|
+
|
|
367
402
|
migrationFiles.forEach((file) => {
|
|
368
403
|
if (file.endsWith(".sql")) {
|
|
369
404
|
const sourcePath = path.join(moduleMigrationsDir, file);
|
|
370
405
|
const destPath = path.join(supabaseMigrationsDir, file);
|
|
371
|
-
|
|
406
|
+
|
|
372
407
|
// Copier seulement si le fichier n'existe pas déjà
|
|
373
408
|
if (!fs.existsSync(destPath)) {
|
|
374
409
|
fs.copyFileSync(sourcePath, destPath);
|
|
@@ -378,13 +413,13 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
378
413
|
}
|
|
379
414
|
});
|
|
380
415
|
}
|
|
381
|
-
|
|
416
|
+
|
|
382
417
|
// Mettre à jour modules.json avec les migrations
|
|
383
418
|
if (copiedMigrations.length > 0) {
|
|
384
419
|
const moduleIndex = modulesConfig.modules.findIndex(
|
|
385
|
-
(m) => m.package === moduleName
|
|
420
|
+
(m) => m.package === moduleName,
|
|
386
421
|
);
|
|
387
|
-
|
|
422
|
+
|
|
388
423
|
if (moduleIndex >= 0) {
|
|
389
424
|
modulesConfig.modules[moduleIndex].migrations = copiedMigrations;
|
|
390
425
|
} else {
|
|
@@ -395,10 +430,13 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
395
430
|
}
|
|
396
431
|
}
|
|
397
432
|
} catch (error) {
|
|
398
|
-
console.warn(
|
|
433
|
+
console.warn(
|
|
434
|
+
`⚠️ Could not copy migrations from ${moduleConfig.moduleName}:`,
|
|
435
|
+
error,
|
|
436
|
+
);
|
|
399
437
|
}
|
|
400
438
|
});
|
|
401
|
-
|
|
439
|
+
|
|
402
440
|
// Sauvegarder modules.json avec les migrations mises à jour
|
|
403
441
|
ensureDirectory(path.dirname(modulesJsonPath));
|
|
404
442
|
fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
|
|
@@ -407,13 +445,17 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
407
445
|
function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
|
|
408
446
|
const docsDir = path.join(appDirectory, "docs");
|
|
409
447
|
ensureDirectory(docsDir);
|
|
410
|
-
|
|
448
|
+
|
|
411
449
|
const docsPagePath = path.join(docsDir, "page.tsx");
|
|
412
|
-
|
|
450
|
+
|
|
413
451
|
// Charger tous les modules depuis modules.json (actifs ET inactifs)
|
|
414
452
|
const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
|
|
415
|
-
let allModules: Array<{
|
|
416
|
-
|
|
453
|
+
let allModules: Array<{
|
|
454
|
+
package: string;
|
|
455
|
+
active: boolean;
|
|
456
|
+
migrations?: string[];
|
|
457
|
+
}> = [];
|
|
458
|
+
|
|
417
459
|
if (fs.existsSync(modulesJsonPath)) {
|
|
418
460
|
try {
|
|
419
461
|
const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
|
|
@@ -422,25 +464,29 @@ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
422
464
|
console.error("❌ Error reading modules.json:", error);
|
|
423
465
|
}
|
|
424
466
|
}
|
|
425
|
-
|
|
467
|
+
|
|
426
468
|
// Générer les imports des composants Doc de chaque module
|
|
427
469
|
const docImports: string[] = [];
|
|
428
470
|
const moduleConfigurations: string[] = [];
|
|
429
|
-
|
|
471
|
+
|
|
430
472
|
allModules.forEach((moduleEntry) => {
|
|
431
473
|
const moduleName = moduleEntry.package;
|
|
432
474
|
const moduleId = moduleName.replace("@lastbrain/module-", "");
|
|
433
475
|
const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
|
|
434
|
-
|
|
476
|
+
|
|
435
477
|
// Trouver la config du module pour obtenir la description
|
|
436
|
-
const moduleConfig = moduleConfigs.find(
|
|
437
|
-
|
|
438
|
-
|
|
478
|
+
const moduleConfig = moduleConfigs.find(
|
|
479
|
+
(mc) => mc.moduleName === moduleName,
|
|
480
|
+
);
|
|
481
|
+
const description = moduleConfig
|
|
482
|
+
? getModuleDescription(moduleConfig)
|
|
483
|
+
: "Module non configuré";
|
|
484
|
+
|
|
439
485
|
// Importer le composant Doc seulement pour les modules actifs
|
|
440
486
|
if (moduleEntry.active) {
|
|
441
487
|
docImports.push(`import { ${docComponentName} } from "${moduleName}";`);
|
|
442
488
|
}
|
|
443
|
-
|
|
489
|
+
|
|
444
490
|
const config = {
|
|
445
491
|
id: moduleId,
|
|
446
492
|
name: `Module ${moduleId.charAt(0).toUpperCase() + moduleId.slice(1)}`,
|
|
@@ -448,34 +494,72 @@ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
448
494
|
component: docComponentName,
|
|
449
495
|
active: moduleEntry.active,
|
|
450
496
|
};
|
|
451
|
-
|
|
497
|
+
|
|
452
498
|
moduleConfigurations.push(` {
|
|
453
499
|
id: "${config.id}",
|
|
454
500
|
name: "${config.name}",
|
|
455
501
|
description: "${config.description}",
|
|
456
|
-
${config.active ? `content: <${config.component} />,` :
|
|
502
|
+
${config.active ? `content: <${config.component} />,` : "content: null,"}
|
|
457
503
|
available: ${config.active},
|
|
458
504
|
}`);
|
|
459
505
|
});
|
|
460
|
-
|
|
506
|
+
|
|
461
507
|
const docsContent = `// Auto-generated docs page with module documentation
|
|
462
508
|
import React from "react";
|
|
463
509
|
import { DocPage } from "@lastbrain/app";
|
|
464
|
-
${docImports.join(
|
|
510
|
+
${docImports.join("\n")}
|
|
465
511
|
|
|
466
512
|
export default function DocsPage() {
|
|
467
513
|
const modules = [
|
|
468
|
-
${moduleConfigurations.join(
|
|
514
|
+
${moduleConfigurations.join(",\n")}
|
|
469
515
|
];
|
|
470
516
|
|
|
471
517
|
return <DocPage modules={modules} />;
|
|
472
518
|
}
|
|
473
519
|
`;
|
|
474
|
-
|
|
520
|
+
|
|
475
521
|
fs.writeFileSync(docsPagePath, docsContent);
|
|
476
522
|
console.log(`📚 Generated docs page: ${docsPagePath}`);
|
|
477
523
|
}
|
|
478
524
|
|
|
525
|
+
function buildGroupedApi(
|
|
526
|
+
apis: Array<{ moduleConfig: ModuleBuildConfig; api: ModuleApiConfig }>,
|
|
527
|
+
routePath: string,
|
|
528
|
+
) {
|
|
529
|
+
const segments = routePath.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
530
|
+
const sanitizedSegments =
|
|
531
|
+
segments[0] === "api" ? segments.slice(1) : segments;
|
|
532
|
+
const routeDir = path.join(appDirectory, "api", ...sanitizedSegments);
|
|
533
|
+
const filePath = path.join(routeDir, "route.ts");
|
|
534
|
+
ensureDirectory(routeDir);
|
|
535
|
+
|
|
536
|
+
// Grouper par module/entryPoint pour créer les exports
|
|
537
|
+
const exportsBySource = new Map<string, string[]>();
|
|
538
|
+
|
|
539
|
+
apis.forEach(({ moduleConfig, api }) => {
|
|
540
|
+
const handler = `${moduleConfig.moduleName}/${api.entryPoint}`;
|
|
541
|
+
if (!exportsBySource.has(handler)) {
|
|
542
|
+
exportsBySource.set(handler, []);
|
|
543
|
+
}
|
|
544
|
+
exportsBySource.get(handler)!.push(api.handlerExport);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Générer les exports - un export statement par source
|
|
548
|
+
const exportStatements: string[] = [];
|
|
549
|
+
exportsBySource.forEach((exports, source) => {
|
|
550
|
+
exportStatements.push(`export { ${exports.join(", ")} } from "${source}";`);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const content = exportStatements.join("\n") + "\n";
|
|
554
|
+
|
|
555
|
+
// Ajouter le marqueur de génération au début
|
|
556
|
+
const contentWithMarker = `// GENERATED BY LASTBRAIN MODULE BUILD
|
|
557
|
+
${content}`;
|
|
558
|
+
|
|
559
|
+
fs.writeFileSync(filePath, contentWithMarker);
|
|
560
|
+
console.log(`🔌 Generated API route: ${filePath}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
479
563
|
function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
|
|
480
564
|
// Essayer de déduire la description depuis les pages ou retourner une description par défaut
|
|
481
565
|
if (moduleConfig.pages.length > 0) {
|
|
@@ -484,19 +568,259 @@ function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
|
|
|
484
568
|
return "Module documentation";
|
|
485
569
|
}
|
|
486
570
|
|
|
571
|
+
function cleanGeneratedFiles() {
|
|
572
|
+
const generatedComment = "// GENERATED BY LASTBRAIN MODULE BUILD";
|
|
573
|
+
|
|
574
|
+
// Fichiers de base à préserver (paths relatifs exacts)
|
|
575
|
+
const protectedFiles = new Set([
|
|
576
|
+
// API de base
|
|
577
|
+
"api/storage",
|
|
578
|
+
// Layouts de base
|
|
579
|
+
"layout.tsx",
|
|
580
|
+
"not-found.tsx",
|
|
581
|
+
"page.tsx", // Page racine seulement
|
|
582
|
+
"admin/page.tsx", // Page admin racine
|
|
583
|
+
"admin/layout.tsx", // Layout admin racine
|
|
584
|
+
"docs/page.tsx", // Page docs générée
|
|
585
|
+
// Middleware et autres fichiers core
|
|
586
|
+
"middleware.ts",
|
|
587
|
+
// Dossiers de lib et config
|
|
588
|
+
"lib",
|
|
589
|
+
"config",
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
// Fonction pour vérifier si un chemin est protégé
|
|
593
|
+
const isProtected = (filePath: string) => {
|
|
594
|
+
const relativePath = path.relative(appDirectory, filePath);
|
|
595
|
+
|
|
596
|
+
// Protection exacte pour certains fichiers
|
|
597
|
+
if (protectedFiles.has(relativePath)) {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Protection par préfixe pour les dossiers
|
|
602
|
+
return Array.from(protectedFiles).some(
|
|
603
|
+
(protectedPath) =>
|
|
604
|
+
(protectedPath.endsWith("/") ||
|
|
605
|
+
["lib", "config", "api/storage"].includes(protectedPath)) &&
|
|
606
|
+
relativePath.startsWith(protectedPath),
|
|
607
|
+
);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// Fonction pour nettoyer récursivement un dossier
|
|
611
|
+
const cleanDirectory = (dirPath: string) => {
|
|
612
|
+
if (!fs.existsSync(dirPath)) return;
|
|
613
|
+
|
|
614
|
+
const items = fs.readdirSync(dirPath);
|
|
615
|
+
|
|
616
|
+
for (const item of items) {
|
|
617
|
+
const itemPath = path.join(dirPath, item);
|
|
618
|
+
const stat = fs.statSync(itemPath);
|
|
619
|
+
|
|
620
|
+
if (stat.isDirectory()) {
|
|
621
|
+
// Nettoyer récursivement le sous-dossier
|
|
622
|
+
cleanDirectory(itemPath);
|
|
623
|
+
|
|
624
|
+
// Supprimer le dossier s'il est vide et non protégé
|
|
625
|
+
try {
|
|
626
|
+
if (!isProtected(itemPath) && fs.readdirSync(itemPath).length === 0) {
|
|
627
|
+
fs.rmdirSync(itemPath);
|
|
628
|
+
console.log(`🗑️ Removed empty directory: ${itemPath}`);
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
// Ignorer les erreurs de suppression de fichiers
|
|
632
|
+
}
|
|
633
|
+
} else if (item.endsWith(".tsx") || item.endsWith(".ts")) {
|
|
634
|
+
// Vérifier si c'est un fichier généré
|
|
635
|
+
if (!isProtected(itemPath)) {
|
|
636
|
+
try {
|
|
637
|
+
const content = fs.readFileSync(itemPath, "utf-8");
|
|
638
|
+
// Supprimer les fichiers générés ou les wrapper simples de modules
|
|
639
|
+
if (
|
|
640
|
+
content.includes(generatedComment) ||
|
|
641
|
+
content.includes('from "@lastbrain/module-') ||
|
|
642
|
+
(content.includes("export {") &&
|
|
643
|
+
content.includes('} from "@lastbrain/module-'))
|
|
644
|
+
) {
|
|
645
|
+
fs.unlinkSync(itemPath);
|
|
646
|
+
console.log(`🗑️ Cleaned generated file: ${itemPath}`);
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
// Ignorer les erreurs de lecture/suppression
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
console.log(`🔒 Protected file skipped: ${itemPath}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Nettoyer les dossiers de sections
|
|
659
|
+
const sectionsToClean = ["(public)", "auth", "admin", "api"];
|
|
660
|
+
sectionsToClean.forEach((section) => {
|
|
661
|
+
const sectionPath = path.join(appDirectory, section);
|
|
662
|
+
if (fs.existsSync(sectionPath)) {
|
|
663
|
+
cleanDirectory(sectionPath);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Nettoyer les fichiers générés à la racine
|
|
668
|
+
const rootFiles = ["navigation.generated.ts"];
|
|
669
|
+
rootFiles.forEach((file) => {
|
|
670
|
+
const filePath = path.join(appDirectory, file);
|
|
671
|
+
if (fs.existsSync(filePath)) {
|
|
672
|
+
fs.unlinkSync(filePath);
|
|
673
|
+
console.log(`🗑️ Cleaned root file: ${filePath}`);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
console.log("🧹 Cleanup completed");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function generateAppAside() {
|
|
681
|
+
const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
|
|
682
|
+
|
|
683
|
+
// Ne pas écraser si le fichier existe déjà
|
|
684
|
+
if (fs.existsSync(targetPath)) {
|
|
685
|
+
console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const templateContent = `"use client";
|
|
690
|
+
|
|
691
|
+
import { AppAside as UIAppAside } from "@lastbrain/app";
|
|
692
|
+
import { useAuthSession } from "@lastbrain/app";
|
|
693
|
+
import { menuConfig } from "../../config/menu";
|
|
694
|
+
|
|
695
|
+
interface AppAsideProps {
|
|
696
|
+
className?: string;
|
|
697
|
+
isVisible?: boolean;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
|
|
701
|
+
const { isSuperAdmin } = useAuthSession();
|
|
702
|
+
|
|
703
|
+
return (
|
|
704
|
+
<UIAppAside
|
|
705
|
+
className={className}
|
|
706
|
+
menuConfig={menuConfig}
|
|
707
|
+
isSuperAdmin={isSuperAdmin}
|
|
708
|
+
isVisible={isVisible}
|
|
709
|
+
/>
|
|
710
|
+
);
|
|
711
|
+
}`;
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
ensureDirectory(path.dirname(targetPath));
|
|
715
|
+
fs.writeFileSync(targetPath, templateContent, "utf-8");
|
|
716
|
+
console.log(`✅ Generated AppAside component: ${targetPath}`);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error(`❌ Error generating AppAside component: ${error}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function generateLayouts() {
|
|
723
|
+
// Générer layout auth avec sidebar
|
|
724
|
+
const authLayoutPath = path.join(appDirectory, "auth", "layout.tsx");
|
|
725
|
+
if (!fs.existsSync(authLayoutPath)) {
|
|
726
|
+
const authLayoutContent = `import { AuthLayoutWithSidebar } from "@lastbrain/app";
|
|
727
|
+
import { menuConfig } from "../../config/menu";
|
|
728
|
+
|
|
729
|
+
export default function SectionLayout({
|
|
730
|
+
children,
|
|
731
|
+
}: {
|
|
732
|
+
children: React.ReactNode;
|
|
733
|
+
}) {
|
|
734
|
+
return (
|
|
735
|
+
<AuthLayoutWithSidebar menuConfig={menuConfig}>
|
|
736
|
+
{children}
|
|
737
|
+
</AuthLayoutWithSidebar>
|
|
738
|
+
);
|
|
739
|
+
}`;
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
ensureDirectory(path.dirname(authLayoutPath));
|
|
743
|
+
fs.writeFileSync(authLayoutPath, authLayoutContent, "utf-8");
|
|
744
|
+
console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error(`❌ Error generating auth layout: ${error}`);
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
console.log(`⏭️ Auth layout already exists, skipping: ${authLayoutPath}`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Générer layout admin avec sidebar
|
|
753
|
+
const adminLayoutPath = path.join(appDirectory, "admin", "layout.tsx");
|
|
754
|
+
if (!fs.existsSync(adminLayoutPath)) {
|
|
755
|
+
const adminLayoutContent = `import { AdminLayoutWithSidebar } from "@lastbrain/app";
|
|
756
|
+
import { menuConfig } from "../../config/menu";
|
|
757
|
+
|
|
758
|
+
export default function AdminLayout({
|
|
759
|
+
children,
|
|
760
|
+
}: {
|
|
761
|
+
children: React.ReactNode;
|
|
762
|
+
}) {
|
|
763
|
+
return (
|
|
764
|
+
<AdminLayoutWithSidebar menuConfig={menuConfig}>
|
|
765
|
+
{children}
|
|
766
|
+
</AdminLayoutWithSidebar>
|
|
767
|
+
);
|
|
768
|
+
}`;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
ensureDirectory(path.dirname(adminLayoutPath));
|
|
772
|
+
fs.writeFileSync(adminLayoutPath, adminLayoutContent, "utf-8");
|
|
773
|
+
console.log(`✅ Generated admin layout with sidebar: ${adminLayoutPath}`);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
console.error(`❌ Error generating admin layout: ${error}`);
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
console.log(`⏭️ Admin layout already exists, skipping: ${adminLayoutPath}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
487
782
|
export async function runModuleBuild() {
|
|
488
783
|
ensureDirectory(appDirectory);
|
|
489
784
|
|
|
785
|
+
// Nettoyer les fichiers générés précédemment
|
|
786
|
+
console.log("🧹 Cleaning previously generated files...");
|
|
787
|
+
cleanGeneratedFiles();
|
|
788
|
+
|
|
490
789
|
const moduleConfigs = await loadModuleConfigs();
|
|
790
|
+
console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
|
|
791
|
+
|
|
792
|
+
// Générer les pages
|
|
491
793
|
moduleConfigs.forEach((moduleConfig) => {
|
|
794
|
+
console.log(
|
|
795
|
+
`📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`,
|
|
796
|
+
);
|
|
492
797
|
moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
|
|
493
|
-
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Grouper les APIs par chemin pour éviter les écrasements de fichier
|
|
801
|
+
const apisByPath = new Map<
|
|
802
|
+
string,
|
|
803
|
+
Array<{ moduleConfig: ModuleBuildConfig; api: ModuleApiConfig }>
|
|
804
|
+
>();
|
|
805
|
+
|
|
806
|
+
moduleConfigs.forEach((moduleConfig) => {
|
|
807
|
+
moduleConfig.apis.forEach((api) => {
|
|
808
|
+
if (!apisByPath.has(api.path)) {
|
|
809
|
+
apisByPath.set(api.path, []);
|
|
810
|
+
}
|
|
811
|
+
apisByPath.get(api.path)!.push({ moduleConfig, api });
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Générer les fichiers de route groupés
|
|
816
|
+
apisByPath.forEach((apis, routePath) => {
|
|
817
|
+
buildGroupedApi(apis, routePath);
|
|
494
818
|
});
|
|
495
819
|
|
|
496
820
|
dumpNavigation();
|
|
497
821
|
generateMenuConfig(moduleConfigs);
|
|
498
822
|
generateDocsPage(moduleConfigs);
|
|
823
|
+
generateAppAside();
|
|
824
|
+
generateLayouts();
|
|
499
825
|
copyModuleMigrations(moduleConfigs);
|
|
500
826
|
}
|
|
501
|
-
|
|
502
|
-
runModuleBuild();
|