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