@lastbrain/app 0.1.7 → 0.1.9
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/scripts/init-app.js +3 -3
- package/dist/styles.css +2 -0
- package/package.json +11 -3
- package/src/app-shell/(admin)/layout.tsx +13 -0
- package/src/app-shell/(auth)/layout.tsx +13 -0
- package/src/app-shell/(public)/page.tsx +11 -0
- package/src/app-shell/layout.tsx +5 -0
- package/src/app-shell/not-found.tsx +28 -0
- package/src/auth/authHelpers.ts +24 -0
- package/src/auth/useAuthSession.ts +54 -0
- package/src/cli.ts +96 -0
- package/src/index.ts +21 -0
- package/src/layouts/AdminLayout.tsx +7 -0
- package/src/layouts/AppProviders.tsx +61 -0
- package/src/layouts/AuthLayout.tsx +7 -0
- package/src/layouts/PublicLayout.tsx +7 -0
- package/src/layouts/RootLayout.tsx +27 -0
- package/src/modules/module-loader.ts +14 -0
- package/src/scripts/README.md +262 -0
- package/src/scripts/db-init.ts +338 -0
- package/src/scripts/db-migrations-sync.ts +86 -0
- package/src/scripts/dev-sync.ts +218 -0
- package/src/scripts/init-app.ts +1077 -0
- package/src/scripts/module-add.ts +242 -0
- package/src/scripts/module-build.ts +502 -0
- package/src/scripts/module-create.ts +809 -0
- package/src/scripts/module-list.ts +37 -0
- package/src/scripts/module-remove.ts +367 -0
- package/src/scripts/readme-build.ts +60 -0
- package/src/styles.css +3 -0
- package/src/templates/AuthGuidePage.tsx +68 -0
- package/src/templates/DefaultDoc.tsx +462 -0
- package/src/templates/DocPage.tsx +381 -0
- package/src/templates/DocsPageWithModules.tsx +22 -0
- package/src/templates/MigrationsGuidePage.tsx +61 -0
- package/src/templates/ModuleGuidePage.tsx +71 -0
- package/src/templates/SimpleDocPage.tsx +587 -0
- package/src/templates/SimpleHomePage.tsx +385 -0
- package/src/templates/env.example/.env.example +6 -0
- package/src/templates/migrations/20201010100000_app_base.sql +228 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ModuleApiConfig,
|
|
8
|
+
ModuleBuildConfig,
|
|
9
|
+
ModuleMenuConfig,
|
|
10
|
+
ModuleMenuItemConfig,
|
|
11
|
+
ModulePageConfig,
|
|
12
|
+
ModuleSection,
|
|
13
|
+
} from "@lastbrain/core";
|
|
14
|
+
|
|
15
|
+
// Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
|
|
16
|
+
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
17
|
+
const appDirectory = path.join(projectRoot, "app");
|
|
18
|
+
|
|
19
|
+
// Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
|
|
20
|
+
const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
21
|
+
|
|
22
|
+
// Charger les modules depuis modules.json
|
|
23
|
+
async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
|
|
24
|
+
const moduleConfigs: ModuleBuildConfig[] = [];
|
|
25
|
+
const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(modulesJsonPath)) {
|
|
28
|
+
return moduleConfigs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
|
|
33
|
+
const modules = modulesData.modules || [];
|
|
34
|
+
|
|
35
|
+
for (const module of modules) {
|
|
36
|
+
// Ne charger que les modules actifs
|
|
37
|
+
if (module.active === false) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const packageName = module.package;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const moduleSuffix = packageName.replace("@lastbrain/module-", "");
|
|
45
|
+
const possibleConfigNames = [
|
|
46
|
+
`${moduleSuffix}.build.config`,
|
|
47
|
+
"build.config",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
let loaded = false;
|
|
51
|
+
for (const configName of possibleConfigNames) {
|
|
52
|
+
try {
|
|
53
|
+
// Résoudre le chemin du module depuis l'application
|
|
54
|
+
const modulePath = projectRequire.resolve(`${packageName}/${configName}`);
|
|
55
|
+
// Convertir en URL file:// pour l'import dynamique
|
|
56
|
+
const moduleUrl = `file://${modulePath}`;
|
|
57
|
+
const moduleImport = await import(moduleUrl);
|
|
58
|
+
|
|
59
|
+
if (moduleImport.default) {
|
|
60
|
+
moduleConfigs.push(moduleImport.default);
|
|
61
|
+
loaded = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Essayer le nom suivant
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!loaded) {
|
|
70
|
+
console.warn(`⚠️ Could not load build config for ${packageName}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn(`⚠️ Failed to load module ${packageName}:`, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error("❌ Error loading modules.json:", error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return moduleConfigs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sectionDirectoryMap: Record<ModuleSection, string[]> = {
|
|
84
|
+
public: ["(public)"],
|
|
85
|
+
auth: ["auth"],
|
|
86
|
+
admin: ["admin"],
|
|
87
|
+
user: ["auth", "user"],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const sectionLayoutMap: Record<string, string> = {
|
|
91
|
+
auth: "AuthLayout",
|
|
92
|
+
admin: "AdminLayout",
|
|
93
|
+
"(public)": "PublicLayout",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const generatedSections = new Set<string>();
|
|
97
|
+
|
|
98
|
+
const navigation: Record<string, MenuEntry[]> = {
|
|
99
|
+
public: [],
|
|
100
|
+
auth: [],
|
|
101
|
+
admin: [],
|
|
102
|
+
};
|
|
103
|
+
const userMenu: MenuEntry[] = [];
|
|
104
|
+
|
|
105
|
+
interface MenuEntry {
|
|
106
|
+
label: string;
|
|
107
|
+
path: string;
|
|
108
|
+
module: string;
|
|
109
|
+
section: ModuleSection;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ensureDirectory(dir: string) {
|
|
113
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ensureSectionLayout(sectionPath: string[]) {
|
|
117
|
+
const sectionDir = path.join(appDirectory, ...sectionPath);
|
|
118
|
+
const sectionKey = sectionPath[0];
|
|
119
|
+
|
|
120
|
+
// Éviter de générer plusieurs fois le même layout
|
|
121
|
+
if (generatedSections.has(sectionKey)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ensureDirectory(sectionDir);
|
|
126
|
+
|
|
127
|
+
const layoutName = sectionLayoutMap[sectionKey];
|
|
128
|
+
if (layoutName) {
|
|
129
|
+
const layoutPath = path.join(sectionDir, "layout.tsx");
|
|
130
|
+
if (!fs.existsSync(layoutPath)) {
|
|
131
|
+
const layoutContent = `import { ${layoutName} } from "@lastbrain/app";
|
|
132
|
+
|
|
133
|
+
export default function SectionLayout({ children }: { children: React.ReactNode }) {
|
|
134
|
+
return <${layoutName}>{children}</${layoutName}>;
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
fs.writeFileSync(layoutPath, layoutContent);
|
|
138
|
+
console.log(`📐 Generated section layout: ${layoutPath}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
generatedSections.add(sectionKey);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toPascalCase(value: string) {
|
|
146
|
+
return value
|
|
147
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
|
|
150
|
+
.join("");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
154
|
+
const segments = page.path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
155
|
+
const sectionPath = sectionDirectoryMap[page.section] ?? ["(public)"];
|
|
156
|
+
|
|
157
|
+
// Générer le layout de section si nécessaire
|
|
158
|
+
ensureSectionLayout(sectionPath);
|
|
159
|
+
|
|
160
|
+
const routeDir = path.join(appDirectory, ...sectionPath, ...segments);
|
|
161
|
+
const filePath = path.join(routeDir, "page.tsx");
|
|
162
|
+
ensureDirectory(routeDir);
|
|
163
|
+
|
|
164
|
+
const wrapperSuffix = segments.length
|
|
165
|
+
? toPascalCase(segments.join("-"))
|
|
166
|
+
: "Root";
|
|
167
|
+
const wrapperName = `${page.componentExport}${wrapperSuffix}Route`;
|
|
168
|
+
|
|
169
|
+
// Vérifier si la route a des paramètres dynamiques (segments avec [])
|
|
170
|
+
const hasDynamicParams = segments.some(seg => seg.startsWith('[') && seg.endsWith(']'));
|
|
171
|
+
|
|
172
|
+
// Pour les pages publiques (signin, signup, etc.), utiliser dynamic import sans SSR
|
|
173
|
+
// pour éviter les erreurs d'hydratation avec les IDs HeroUI/React Aria
|
|
174
|
+
const isPublicAuthPage = page.section === "public" &&
|
|
175
|
+
(page.path.includes("signin") || page.path.includes("signup") || page.path.includes("reset-password"));
|
|
176
|
+
|
|
177
|
+
let content: string;
|
|
178
|
+
|
|
179
|
+
if (isPublicAuthPage) {
|
|
180
|
+
content = `"use client";
|
|
181
|
+
|
|
182
|
+
import dynamic from "next/dynamic";
|
|
183
|
+
|
|
184
|
+
const ${page.componentExport} = dynamic(
|
|
185
|
+
() => import("${moduleConfig.moduleName}").then((mod) => ({ default: mod.${page.componentExport} })),
|
|
186
|
+
{ ssr: false }
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'} {
|
|
190
|
+
return <${page.componentExport} ${hasDynamicParams ? '{...props}' : ''} />;
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
} else {
|
|
194
|
+
content = `import { ${page.componentExport} } from "${moduleConfig.moduleName}";
|
|
195
|
+
|
|
196
|
+
export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'} {
|
|
197
|
+
return <${page.componentExport} ${hasDynamicParams ? '{...props}' : ''} />;
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fs.writeFileSync(filePath, content);
|
|
203
|
+
console.log(`⭐ Generated page: ${filePath}`); const entry: MenuEntry = {
|
|
204
|
+
label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
|
|
205
|
+
path: page.path,
|
|
206
|
+
module: moduleConfig.moduleName,
|
|
207
|
+
section: page.section,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (page.section === "user") {
|
|
211
|
+
userMenu.push(entry);
|
|
212
|
+
} else if (page.section) {
|
|
213
|
+
navigation[page.section]?.push(entry);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
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
|
+
function dumpNavigation() {
|
|
234
|
+
const navPath = path.join(appDirectory, "navigation.generated.ts");
|
|
235
|
+
const content = `export type MenuEntry = { label: string; path: string; module: string; section: string };
|
|
236
|
+
|
|
237
|
+
export const navigation = ${JSON.stringify(navigation, null, 2)};
|
|
238
|
+
|
|
239
|
+
export const userMenu = ${JSON.stringify(userMenu, null, 2)};
|
|
240
|
+
`;
|
|
241
|
+
fs.writeFileSync(navPath, content);
|
|
242
|
+
console.log(`🧭 Generated navigation metadata: ${navPath}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Génère le fichier config/menu.ts à partir des configurations de menu des modules
|
|
247
|
+
*/
|
|
248
|
+
function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
249
|
+
const configDir = path.join(projectRoot, "config");
|
|
250
|
+
ensureDirectory(configDir);
|
|
251
|
+
|
|
252
|
+
const menuPath = path.join(configDir, "menu.ts");
|
|
253
|
+
|
|
254
|
+
// Collecter les menus de tous les modules
|
|
255
|
+
const publicMenus: ModuleMenuItemConfig[] = [];
|
|
256
|
+
const authMenus: ModuleMenuItemConfig[] = [];
|
|
257
|
+
const adminMenus: ModuleMenuItemConfig[] = [];
|
|
258
|
+
const accountMenus: ModuleMenuItemConfig[] = [];
|
|
259
|
+
|
|
260
|
+
moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
|
|
261
|
+
if (moduleConfig.menu) {
|
|
262
|
+
if (moduleConfig.menu.public) {
|
|
263
|
+
publicMenus.push(...moduleConfig.menu.public);
|
|
264
|
+
}
|
|
265
|
+
if (moduleConfig.menu.auth) {
|
|
266
|
+
authMenus.push(...moduleConfig.menu.auth);
|
|
267
|
+
}
|
|
268
|
+
if (moduleConfig.menu.admin) {
|
|
269
|
+
adminMenus.push(...moduleConfig.menu.admin);
|
|
270
|
+
}
|
|
271
|
+
if (moduleConfig.menu.account) {
|
|
272
|
+
accountMenus.push(...moduleConfig.menu.account);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Trier par ordre
|
|
278
|
+
const sortByOrder = (a: ModuleMenuItemConfig, b: ModuleMenuItemConfig) => {
|
|
279
|
+
return (a.order ?? 999) - (b.order ?? 999);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
publicMenus.sort(sortByOrder);
|
|
283
|
+
authMenus.sort(sortByOrder);
|
|
284
|
+
adminMenus.sort(sortByOrder);
|
|
285
|
+
accountMenus.sort(sortByOrder);
|
|
286
|
+
|
|
287
|
+
// Générer le contenu du fichier
|
|
288
|
+
const content = `// Auto-generated menu configuration
|
|
289
|
+
// Generated from module build configs
|
|
290
|
+
|
|
291
|
+
export interface MenuItem {
|
|
292
|
+
title: string;
|
|
293
|
+
description?: string;
|
|
294
|
+
icon?: string;
|
|
295
|
+
path: string;
|
|
296
|
+
order?: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface MenuConfig {
|
|
300
|
+
public: MenuItem[];
|
|
301
|
+
auth: MenuItem[];
|
|
302
|
+
admin: MenuItem[];
|
|
303
|
+
account: MenuItem[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const menuConfig: MenuConfig = {
|
|
307
|
+
public: ${JSON.stringify(publicMenus, null, 2)},
|
|
308
|
+
auth: ${JSON.stringify(authMenus, null, 2)},
|
|
309
|
+
admin: ${JSON.stringify(adminMenus, null, 2)},
|
|
310
|
+
account: ${JSON.stringify(accountMenus, null, 2)},
|
|
311
|
+
};
|
|
312
|
+
`;
|
|
313
|
+
|
|
314
|
+
fs.writeFileSync(menuPath, content);
|
|
315
|
+
console.log(`🍔 Generated menu configuration: ${menuPath}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
319
|
+
const supabaseMigrationsDir = path.join(projectRoot, "supabase", "migrations");
|
|
320
|
+
|
|
321
|
+
// S'assurer que le dossier migrations existe
|
|
322
|
+
if (!fs.existsSync(supabaseMigrationsDir)) {
|
|
323
|
+
ensureDirectory(supabaseMigrationsDir);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
|
|
327
|
+
|
|
328
|
+
// Charger modules.json
|
|
329
|
+
let modulesConfig: { modules: Array<{ package: string; migrations?: string[] }> } = { modules: [] };
|
|
330
|
+
if (fs.existsSync(modulesJsonPath)) {
|
|
331
|
+
modulesConfig = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
|
|
335
|
+
try {
|
|
336
|
+
const moduleName = moduleConfig.moduleName;
|
|
337
|
+
const copiedMigrations: string[] = [];
|
|
338
|
+
|
|
339
|
+
// Essayer plusieurs chemins possibles pour trouver le module
|
|
340
|
+
const possiblePaths = [
|
|
341
|
+
// Dans node_modules local de l'app
|
|
342
|
+
path.join(projectRoot, "node_modules", moduleName),
|
|
343
|
+
// Dans node_modules à la racine du workspace
|
|
344
|
+
path.join(projectRoot, "..", "..", "node_modules", moduleName),
|
|
345
|
+
// Directement dans packages/ (pour monorepo)
|
|
346
|
+
path.join(projectRoot, "..", "..", "packages", moduleName.replace("@lastbrain/", "")),
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
let moduleBasePath: string | null = null;
|
|
350
|
+
|
|
351
|
+
for (const possiblePath of possiblePaths) {
|
|
352
|
+
if (fs.existsSync(possiblePath)) {
|
|
353
|
+
moduleBasePath = possiblePath;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!moduleBasePath) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const moduleMigrationsDir = path.join(moduleBasePath, "supabase", "migrations");
|
|
363
|
+
|
|
364
|
+
if (fs.existsSync(moduleMigrationsDir)) {
|
|
365
|
+
const migrationFiles = fs.readdirSync(moduleMigrationsDir);
|
|
366
|
+
|
|
367
|
+
migrationFiles.forEach((file) => {
|
|
368
|
+
if (file.endsWith(".sql")) {
|
|
369
|
+
const sourcePath = path.join(moduleMigrationsDir, file);
|
|
370
|
+
const destPath = path.join(supabaseMigrationsDir, file);
|
|
371
|
+
|
|
372
|
+
// Copier seulement si le fichier n'existe pas déjà
|
|
373
|
+
if (!fs.existsSync(destPath)) {
|
|
374
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
375
|
+
console.log(`📦 Copied migration: ${file} from ${moduleName}`);
|
|
376
|
+
}
|
|
377
|
+
copiedMigrations.push(file);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Mettre à jour modules.json avec les migrations
|
|
383
|
+
if (copiedMigrations.length > 0) {
|
|
384
|
+
const moduleIndex = modulesConfig.modules.findIndex(
|
|
385
|
+
(m) => m.package === moduleName
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (moduleIndex >= 0) {
|
|
389
|
+
modulesConfig.modules[moduleIndex].migrations = copiedMigrations;
|
|
390
|
+
} else {
|
|
391
|
+
modulesConfig.modules.push({
|
|
392
|
+
package: moduleName,
|
|
393
|
+
migrations: copiedMigrations,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.warn(`⚠️ Could not copy migrations from ${moduleConfig.moduleName}:`, error);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Sauvegarder modules.json avec les migrations mises à jour
|
|
403
|
+
ensureDirectory(path.dirname(modulesJsonPath));
|
|
404
|
+
fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
|
|
408
|
+
const docsDir = path.join(appDirectory, "docs");
|
|
409
|
+
ensureDirectory(docsDir);
|
|
410
|
+
|
|
411
|
+
const docsPagePath = path.join(docsDir, "page.tsx");
|
|
412
|
+
|
|
413
|
+
// Charger tous les modules depuis modules.json (actifs ET inactifs)
|
|
414
|
+
const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
|
|
415
|
+
let allModules: Array<{ package: string; active: boolean; migrations?: string[] }> = [];
|
|
416
|
+
|
|
417
|
+
if (fs.existsSync(modulesJsonPath)) {
|
|
418
|
+
try {
|
|
419
|
+
const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
|
|
420
|
+
allModules = modulesData.modules || [];
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error("❌ Error reading modules.json:", error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Générer les imports des composants Doc de chaque module
|
|
427
|
+
const docImports: string[] = [];
|
|
428
|
+
const moduleConfigurations: string[] = [];
|
|
429
|
+
|
|
430
|
+
allModules.forEach((moduleEntry) => {
|
|
431
|
+
const moduleName = moduleEntry.package;
|
|
432
|
+
const moduleId = moduleName.replace("@lastbrain/module-", "");
|
|
433
|
+
const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
|
|
434
|
+
|
|
435
|
+
// Trouver la config du module pour obtenir la description
|
|
436
|
+
const moduleConfig = moduleConfigs.find(mc => mc.moduleName === moduleName);
|
|
437
|
+
const description = moduleConfig ? getModuleDescription(moduleConfig) : "Module non configuré";
|
|
438
|
+
|
|
439
|
+
// Importer le composant Doc seulement pour les modules actifs
|
|
440
|
+
if (moduleEntry.active) {
|
|
441
|
+
docImports.push(`import { ${docComponentName} } from "${moduleName}";`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const config = {
|
|
445
|
+
id: moduleId,
|
|
446
|
+
name: `Module ${moduleId.charAt(0).toUpperCase() + moduleId.slice(1)}`,
|
|
447
|
+
description: description,
|
|
448
|
+
component: docComponentName,
|
|
449
|
+
active: moduleEntry.active,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
moduleConfigurations.push(` {
|
|
453
|
+
id: "${config.id}",
|
|
454
|
+
name: "${config.name}",
|
|
455
|
+
description: "${config.description}",
|
|
456
|
+
${config.active ? `content: <${config.component} />,` : 'content: null,'}
|
|
457
|
+
available: ${config.active},
|
|
458
|
+
}`);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const docsContent = `// Auto-generated docs page with module documentation
|
|
462
|
+
import React from "react";
|
|
463
|
+
import { DocPage } from "@lastbrain/app";
|
|
464
|
+
${docImports.join('\n')}
|
|
465
|
+
|
|
466
|
+
export default function DocsPage() {
|
|
467
|
+
const modules = [
|
|
468
|
+
${moduleConfigurations.join(',\n')}
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
return <DocPage modules={modules} />;
|
|
472
|
+
}
|
|
473
|
+
`;
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(docsPagePath, docsContent);
|
|
476
|
+
console.log(`📚 Generated docs page: ${docsPagePath}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
|
|
480
|
+
// Essayer de déduire la description depuis les pages ou retourner une description par défaut
|
|
481
|
+
if (moduleConfig.pages.length > 0) {
|
|
482
|
+
return `${moduleConfig.pages.length} page(s), ${moduleConfig.apis.length} API(s)`;
|
|
483
|
+
}
|
|
484
|
+
return "Module documentation";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export async function runModuleBuild() {
|
|
488
|
+
ensureDirectory(appDirectory);
|
|
489
|
+
|
|
490
|
+
const moduleConfigs = await loadModuleConfigs();
|
|
491
|
+
moduleConfigs.forEach((moduleConfig) => {
|
|
492
|
+
moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
|
|
493
|
+
moduleConfig.apis.forEach((api) => buildApi(moduleConfig, api));
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
dumpNavigation();
|
|
497
|
+
generateMenuConfig(moduleConfigs);
|
|
498
|
+
generateDocsPage(moduleConfigs);
|
|
499
|
+
copyModuleMigrations(moduleConfigs);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
runModuleBuild();
|