@lastbrain/app 0.1.34 → 0.1.37
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/README.md +23 -5
- package/dist/__tests__/module-registry.test.js +5 -16
- package/dist/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +2 -2
- package/dist/scripts/module-add.d.ts +0 -11
- package/dist/scripts/module-add.d.ts.map +1 -1
- package/dist/scripts/module-add.js +45 -22
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +90 -1
- package/dist/scripts/module-create.d.ts +23 -0
- package/dist/scripts/module-create.d.ts.map +1 -1
- package/dist/scripts/module-create.js +737 -52
- package/dist/scripts/module-delete.d.ts +6 -0
- package/dist/scripts/module-delete.d.ts.map +1 -0
- package/dist/scripts/module-delete.js +143 -0
- package/dist/scripts/module-list.d.ts.map +1 -1
- package/dist/scripts/module-list.js +2 -2
- package/dist/scripts/module-remove.d.ts.map +1 -1
- package/dist/scripts/module-remove.js +20 -4
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +170 -30
- package/dist/templates/DocPage.d.ts.map +1 -1
- package/dist/templates/DocPage.js +25 -8
- package/dist/templates/migrations/20201010100000_app_base.sql +23 -24
- package/package.json +4 -4
- package/src/__tests__/module-registry.test.ts +5 -17
- package/src/scripts/db-init.ts +2 -2
- package/src/scripts/init-app.ts +5 -2
- package/src/scripts/module-add.ts +55 -23
- package/src/scripts/module-build.ts +109 -1
- package/src/scripts/module-create.ts +885 -63
- package/src/scripts/module-delete.ts +202 -0
- package/src/scripts/module-list.ts +9 -2
- package/src/scripts/module-remove.ts +36 -4
- package/src/templates/DefaultDoc.tsx +1163 -753
- package/src/templates/DocPage.tsx +28 -11
- package/src/templates/migrations/20201010100000_app_base.sql +23 -24
|
@@ -2,6 +2,18 @@ import fs from "fs-extra";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
// Noms réservés pour éviter les collisions avec le routeur/app
|
|
9
|
+
const RESERVED_PAGE_NAMES = new Set([
|
|
10
|
+
"layout",
|
|
11
|
+
"page",
|
|
12
|
+
"api",
|
|
13
|
+
"admin",
|
|
14
|
+
"auth",
|
|
15
|
+
"public",
|
|
16
|
+
]);
|
|
5
17
|
/**
|
|
6
18
|
* Parse une chaîne de pages séparées par des virgules
|
|
7
19
|
* Ex: "legal, privacy, terms" => ["legal", "privacy", "terms"]
|
|
@@ -14,6 +26,18 @@ function parsePagesList(input) {
|
|
|
14
26
|
.map((p) => p.trim())
|
|
15
27
|
.filter((p) => p.length > 0);
|
|
16
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Slugifie un nom de page: minuscules, tirets, alphanum uniquement
|
|
31
|
+
*/
|
|
32
|
+
function slugifyPageName(name) {
|
|
33
|
+
return name
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[\s_]+/g, "-")
|
|
37
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
38
|
+
.replace(/--+/g, "-")
|
|
39
|
+
.replace(/^-+|-+$/g, "");
|
|
40
|
+
}
|
|
17
41
|
/**
|
|
18
42
|
* Parse une chaîne de tables séparées par des virgules
|
|
19
43
|
* Ex: "settings, users" => ["settings", "users"]
|
|
@@ -51,7 +75,8 @@ function getLastBrainPackageVersions(rootDir) {
|
|
|
51
75
|
*/
|
|
52
76
|
function generatePackageJson(moduleName, slug, rootDir) {
|
|
53
77
|
const versions = getLastBrainPackageVersions(rootDir);
|
|
54
|
-
const
|
|
78
|
+
const moduleNameOnly = slug.replace("module-", "");
|
|
79
|
+
const buildConfigExport = `./${moduleNameOnly}.build.config`;
|
|
55
80
|
return JSON.stringify({
|
|
56
81
|
name: moduleName,
|
|
57
82
|
version: "0.1.0",
|
|
@@ -84,8 +109,8 @@ function generatePackageJson(moduleName, slug, rootDir) {
|
|
|
84
109
|
default: "./dist/server.js",
|
|
85
110
|
},
|
|
86
111
|
[buildConfigExport]: {
|
|
87
|
-
types: `./dist/${
|
|
88
|
-
default: `./dist/${
|
|
112
|
+
types: `./dist/${moduleNameOnly}.build.config.d.ts`,
|
|
113
|
+
default: `./dist/${moduleNameOnly}.build.config.js`,
|
|
89
114
|
},
|
|
90
115
|
"./api/*": {
|
|
91
116
|
types: "./dist/api/*.d.ts",
|
|
@@ -114,6 +139,38 @@ function generateTsConfig() {
|
|
|
114
139
|
*/
|
|
115
140
|
function generateBuildConfig(config) {
|
|
116
141
|
const { moduleName, pages, tables } = config;
|
|
142
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
143
|
+
// Helper pour normaliser les chemins (évite // et trim)
|
|
144
|
+
function normalizePath(...segments) {
|
|
145
|
+
return ("/" +
|
|
146
|
+
segments
|
|
147
|
+
.map((s) => s.replace(/^\/+/g, "").replace(/\/+$/g, ""))
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join("/"));
|
|
150
|
+
}
|
|
151
|
+
// Construit un path de menu selon la section en respectant le pattern attendu
|
|
152
|
+
function buildMenuPath(section, pagePath, pageName) {
|
|
153
|
+
const cleanedPagePath = pagePath.replace(/^\/+/g, "");
|
|
154
|
+
switch (section) {
|
|
155
|
+
case "public": {
|
|
156
|
+
// Public: /<module>/<page> (si non déjà préfixé)
|
|
157
|
+
if (cleanedPagePath.startsWith(moduleNameOnly + "/")) {
|
|
158
|
+
return normalizePath(cleanedPagePath); // déjà préfixé
|
|
159
|
+
}
|
|
160
|
+
return normalizePath(moduleNameOnly, cleanedPagePath);
|
|
161
|
+
}
|
|
162
|
+
case "auth": {
|
|
163
|
+
// Auth: /auth/<module>/<page>
|
|
164
|
+
return normalizePath("auth", moduleNameOnly, cleanedPagePath);
|
|
165
|
+
}
|
|
166
|
+
case "admin": {
|
|
167
|
+
// Admin: /admin/<module>/<pageName> (utilise le slug brut)
|
|
168
|
+
return normalizePath("admin", moduleNameOnly, pageName);
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
return normalizePath(cleanedPagePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
117
174
|
// Générer la liste des pages
|
|
118
175
|
const pagesConfig = pages.map((page) => {
|
|
119
176
|
const componentName = page.name
|
|
@@ -152,11 +209,12 @@ function generateBuildConfig(config) {
|
|
|
152
209
|
.split("-")
|
|
153
210
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
154
211
|
.join(" ");
|
|
212
|
+
const menuPath = buildMenuPath("public", page.path, page.name);
|
|
155
213
|
return ` {
|
|
156
214
|
title: "${title}",
|
|
157
215
|
description: "Page ${title}",
|
|
158
216
|
icon: "FileText",
|
|
159
|
-
path: "${
|
|
217
|
+
path: "${menuPath}",
|
|
160
218
|
order: ${index + 1},
|
|
161
219
|
}`;
|
|
162
220
|
});
|
|
@@ -170,11 +228,12 @@ function generateBuildConfig(config) {
|
|
|
170
228
|
.split("-")
|
|
171
229
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
172
230
|
.join(" ");
|
|
231
|
+
const menuPath = buildMenuPath("auth", page.path, page.name);
|
|
173
232
|
return ` {
|
|
174
233
|
title: "${title}",
|
|
175
234
|
description: "Page ${title}",
|
|
176
235
|
icon: "FileText",
|
|
177
|
-
path: "${
|
|
236
|
+
path: "${menuPath}",
|
|
178
237
|
order: ${index + 1},
|
|
179
238
|
}`;
|
|
180
239
|
});
|
|
@@ -188,11 +247,12 @@ function generateBuildConfig(config) {
|
|
|
188
247
|
.split("-")
|
|
189
248
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
190
249
|
.join(" ");
|
|
250
|
+
const menuPath = buildMenuPath("admin", page.path, page.name);
|
|
191
251
|
return ` {
|
|
192
252
|
title: "${title}",
|
|
193
253
|
description: "Page ${title}",
|
|
194
254
|
icon: "Settings",
|
|
195
|
-
path: "${
|
|
255
|
+
path: "${menuPath}",
|
|
196
256
|
order: ${index + 1},
|
|
197
257
|
}`;
|
|
198
258
|
});
|
|
@@ -233,23 +293,29 @@ export default buildConfig;
|
|
|
233
293
|
/**
|
|
234
294
|
* Génère le contenu du fichier index.ts
|
|
235
295
|
*/
|
|
236
|
-
function
|
|
296
|
+
function toPascalCase(value) {
|
|
297
|
+
return value
|
|
298
|
+
.split(/[-_]/g)
|
|
299
|
+
.filter(Boolean)
|
|
300
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
301
|
+
.join("");
|
|
302
|
+
}
|
|
303
|
+
function generateIndexTs(pages, moduleNameOnly) {
|
|
237
304
|
const exports = pages.map((page) => {
|
|
238
|
-
const componentName = page.name
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.join("");
|
|
242
|
-
const fileName = page.name
|
|
243
|
-
.split("-")
|
|
244
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
245
|
-
.join("");
|
|
246
|
-
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page.js";`;
|
|
305
|
+
const componentName = toPascalCase(page.name);
|
|
306
|
+
const fileName = toPascalCase(page.name);
|
|
307
|
+
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page";`;
|
|
247
308
|
});
|
|
309
|
+
const moduleAlias = toPascalCase(moduleNameOnly);
|
|
248
310
|
return `// Client Components
|
|
249
311
|
${exports.join("\n")}
|
|
250
312
|
|
|
313
|
+
// Documentation Component
|
|
314
|
+
export { Doc } from "./components/Doc";
|
|
315
|
+
export { Doc as ${moduleAlias}ModuleDoc } from "./components/Doc";
|
|
316
|
+
|
|
251
317
|
// Configuration de build
|
|
252
|
-
export { default as buildConfig } from "
|
|
318
|
+
export { default as buildConfig } from "./${moduleNameOnly}.build.config";
|
|
253
319
|
`;
|
|
254
320
|
}
|
|
255
321
|
/**
|
|
@@ -301,7 +367,13 @@ export function ${componentName}Page() {
|
|
|
301
367
|
*/
|
|
302
368
|
function generateApiRoute(tableName, section) {
|
|
303
369
|
const authRequired = section !== "public";
|
|
304
|
-
|
|
370
|
+
const clientImport = section === "admin"
|
|
371
|
+
? 'import { getSupabaseServiceClient } from "@lastbrain/core/server"'
|
|
372
|
+
: 'import { getSupabaseServerClient } from "@lastbrain/core/server"';
|
|
373
|
+
const clientGetter = section === "admin"
|
|
374
|
+
? "getSupabaseServiceClient"
|
|
375
|
+
: "getSupabaseServerClient";
|
|
376
|
+
return `${clientImport};
|
|
305
377
|
|
|
306
378
|
const jsonResponse = (payload: unknown, status = 200) => {
|
|
307
379
|
return new Response(JSON.stringify(payload), {
|
|
@@ -316,7 +388,7 @@ const jsonResponse = (payload: unknown, status = 200) => {
|
|
|
316
388
|
* GET - Liste tous les enregistrements de ${tableName}
|
|
317
389
|
*/
|
|
318
390
|
export async function GET(request: Request) {
|
|
319
|
-
const supabase = await
|
|
391
|
+
const supabase = await ${clientGetter}();
|
|
320
392
|
${authRequired
|
|
321
393
|
? `
|
|
322
394
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
@@ -341,7 +413,7 @@ export async function GET(request: Request) {
|
|
|
341
413
|
* POST - Crée un nouvel enregistrement dans ${tableName}
|
|
342
414
|
*/
|
|
343
415
|
export async function POST(request: Request) {
|
|
344
|
-
const supabase = await
|
|
416
|
+
const supabase = await ${clientGetter}();
|
|
345
417
|
${authRequired
|
|
346
418
|
? `
|
|
347
419
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
@@ -353,9 +425,12 @@ export async function POST(request: Request) {
|
|
|
353
425
|
|
|
354
426
|
const body = await request.json();
|
|
355
427
|
|
|
428
|
+
// Injection côté serveur de owner_id si l'utilisateur est authentifié (sécurité)
|
|
429
|
+
const insertPayload = ${authRequired ? `{ ...body, owner_id: user.id }` : "body"};
|
|
430
|
+
|
|
356
431
|
const { data, error } = await supabase
|
|
357
432
|
.from("${tableName}")
|
|
358
|
-
.insert(
|
|
433
|
+
.insert(insertPayload)
|
|
359
434
|
.select()
|
|
360
435
|
.single();
|
|
361
436
|
|
|
@@ -370,7 +445,7 @@ export async function POST(request: Request) {
|
|
|
370
445
|
* PUT - Met à jour un enregistrement dans ${tableName}
|
|
371
446
|
*/
|
|
372
447
|
export async function PUT(request: Request) {
|
|
373
|
-
const supabase = await
|
|
448
|
+
const supabase = await ${clientGetter}();
|
|
374
449
|
${authRequired
|
|
375
450
|
? `
|
|
376
451
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
@@ -405,7 +480,7 @@ export async function PUT(request: Request) {
|
|
|
405
480
|
* DELETE - Supprime un enregistrement de ${tableName}
|
|
406
481
|
*/
|
|
407
482
|
export async function DELETE(request: Request) {
|
|
408
|
-
const supabase = await
|
|
483
|
+
const supabase = await ${clientGetter}();
|
|
409
484
|
${authRequired
|
|
410
485
|
? `
|
|
411
486
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
@@ -520,10 +595,559 @@ $$;
|
|
|
520
595
|
${tablesSQL}
|
|
521
596
|
`;
|
|
522
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Generate Doc.tsx component for the module
|
|
600
|
+
*/
|
|
601
|
+
function generateDocComponent(config) {
|
|
602
|
+
const moduleNameClean = config.slug.replace("module-", "");
|
|
603
|
+
const moduleNameOnly = moduleNameClean;
|
|
604
|
+
// Generate pages sections
|
|
605
|
+
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
606
|
+
const authPages = config.pages.filter((p) => p.section === "auth");
|
|
607
|
+
const adminPages = config.pages.filter((p) => p.section === "admin");
|
|
608
|
+
let pagesSection = "";
|
|
609
|
+
if (config.pages.length > 0) {
|
|
610
|
+
pagesSection = `
|
|
611
|
+
<Card>
|
|
612
|
+
<CardHeader>
|
|
613
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
614
|
+
<FileText size={24} />
|
|
615
|
+
Pages Disponibles
|
|
616
|
+
</h2>
|
|
617
|
+
</CardHeader>
|
|
618
|
+
<CardBody className="space-y-4">`;
|
|
619
|
+
if (publicPages.length > 0) {
|
|
620
|
+
pagesSection += `
|
|
621
|
+
<div>
|
|
622
|
+
<h3 className="text-lg font-semibold mb-2">Pages Publiques</h3>
|
|
623
|
+
<div className="space-y-2">`;
|
|
624
|
+
for (const page of publicPages) {
|
|
625
|
+
const componentName = page.name
|
|
626
|
+
.split("-")
|
|
627
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
628
|
+
.join("");
|
|
629
|
+
pagesSection += `
|
|
630
|
+
<div className="flex items-start gap-2">
|
|
631
|
+
<Chip size="sm" color="success" variant="flat">GET</Chip>
|
|
632
|
+
<code className="text-sm">${page.path}</code>
|
|
633
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
634
|
+
</div>`;
|
|
635
|
+
}
|
|
636
|
+
pagesSection += `
|
|
637
|
+
</div>
|
|
638
|
+
</div>`;
|
|
639
|
+
}
|
|
640
|
+
if (authPages.length > 0) {
|
|
641
|
+
pagesSection += `
|
|
642
|
+
<div>
|
|
643
|
+
<h3 className="text-lg font-semibold mb-2">Pages Protégées (Auth)</h3>
|
|
644
|
+
<div className="space-y-2">`;
|
|
645
|
+
for (const page of authPages) {
|
|
646
|
+
const componentName = page.name
|
|
647
|
+
.split("-")
|
|
648
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
649
|
+
.join("");
|
|
650
|
+
pagesSection += `
|
|
651
|
+
<div className="flex items-start gap-2">
|
|
652
|
+
<Chip size="sm" color="primary" variant="flat">GET</Chip>
|
|
653
|
+
<code className="text-sm">${page.path}</code>
|
|
654
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
655
|
+
</div>`;
|
|
656
|
+
}
|
|
657
|
+
pagesSection += `
|
|
658
|
+
</div>
|
|
659
|
+
</div>`;
|
|
660
|
+
}
|
|
661
|
+
if (adminPages.length > 0) {
|
|
662
|
+
pagesSection += `
|
|
663
|
+
<div>
|
|
664
|
+
<h3 className="text-lg font-semibold mb-2">Pages Admin</h3>
|
|
665
|
+
<div className="space-y-2">`;
|
|
666
|
+
for (const page of adminPages) {
|
|
667
|
+
const componentName = page.name
|
|
668
|
+
.split("-")
|
|
669
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
670
|
+
.join("");
|
|
671
|
+
pagesSection += `
|
|
672
|
+
<div className="flex items-start gap-2">
|
|
673
|
+
<Chip size="sm" color="secondary" variant="flat">GET</Chip>
|
|
674
|
+
<code className="text-sm">/admin/${moduleNameOnly}/${page.name}</code>
|
|
675
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
676
|
+
</div>`;
|
|
677
|
+
}
|
|
678
|
+
pagesSection += `
|
|
679
|
+
</div>
|
|
680
|
+
</div>`;
|
|
681
|
+
}
|
|
682
|
+
pagesSection += `
|
|
683
|
+
</CardBody>
|
|
684
|
+
</Card>
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
// Generate APIs section
|
|
688
|
+
let apisSection = "";
|
|
689
|
+
if (config.tables.length > 0) {
|
|
690
|
+
apisSection = `
|
|
691
|
+
<Card>
|
|
692
|
+
<CardHeader>
|
|
693
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
694
|
+
<Zap size={24} />
|
|
695
|
+
API Routes
|
|
696
|
+
</h2>
|
|
697
|
+
</CardHeader>
|
|
698
|
+
<CardBody className="space-y-4">`;
|
|
699
|
+
for (const table of config.tables) {
|
|
700
|
+
for (const section of table.sections) {
|
|
701
|
+
apisSection += `
|
|
702
|
+
<div>
|
|
703
|
+
<h3 className="text-lg font-semibold mb-2">
|
|
704
|
+
<code>/api/${section}/${table.name}</code>
|
|
705
|
+
</h3>
|
|
706
|
+
<div className="flex gap-2">
|
|
707
|
+
<Chip size="sm" color="success" variant="flat">GET</Chip>
|
|
708
|
+
<Chip size="sm" color="primary" variant="flat">POST</Chip>
|
|
709
|
+
<Chip size="sm" color="warning" variant="flat">PUT</Chip>
|
|
710
|
+
<Chip size="sm" color="danger" variant="flat">DELETE</Chip>
|
|
711
|
+
</div>
|
|
712
|
+
</div>`;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
apisSection += `
|
|
716
|
+
</CardBody>
|
|
717
|
+
</Card>
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
// Generate tables section
|
|
721
|
+
let tablesSection = "";
|
|
722
|
+
if (config.tables.length > 0) {
|
|
723
|
+
tablesSection = `
|
|
724
|
+
<Card>
|
|
725
|
+
<CardHeader>
|
|
726
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
727
|
+
<Database size={24} />
|
|
728
|
+
Base de Données
|
|
729
|
+
</h2>
|
|
730
|
+
</CardHeader>
|
|
731
|
+
<CardBody className="space-y-6">`;
|
|
732
|
+
for (const table of config.tables) {
|
|
733
|
+
tablesSection += `
|
|
734
|
+
<TableStructure
|
|
735
|
+
tableName="${table.name}"
|
|
736
|
+
title="${table.name}"
|
|
737
|
+
description="Table ${table.name} du module ${moduleNameClean}"
|
|
738
|
+
/>`;
|
|
739
|
+
}
|
|
740
|
+
tablesSection += `
|
|
741
|
+
</CardBody>
|
|
742
|
+
</Card>
|
|
743
|
+
`;
|
|
744
|
+
}
|
|
745
|
+
// Installation commands
|
|
746
|
+
const installSection = `
|
|
747
|
+
<Card>
|
|
748
|
+
<CardHeader>
|
|
749
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
750
|
+
<Package size={24} />
|
|
751
|
+
Installation
|
|
752
|
+
</h2>
|
|
753
|
+
</CardHeader>
|
|
754
|
+
<CardBody className="space-y-4">
|
|
755
|
+
<div>
|
|
756
|
+
<h3 className="text-lg font-semibold mb-2">Ajouter le module</h3>
|
|
757
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
758
|
+
pnpm lastbrain add-module ${moduleNameClean}
|
|
759
|
+
</Snippet>
|
|
760
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
761
|
+
pnpm build:modules
|
|
762
|
+
</Snippet>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<div>
|
|
766
|
+
<h3 className="text-lg font-semibold mb-2">Appliquer les migrations</h3>
|
|
767
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
768
|
+
cd apps/votre-app
|
|
769
|
+
</Snippet>
|
|
770
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
771
|
+
supabase migration up
|
|
772
|
+
</Snippet>
|
|
773
|
+
</div>
|
|
774
|
+
</CardBody>
|
|
775
|
+
</Card>
|
|
776
|
+
`;
|
|
777
|
+
// Usage section with placeholder
|
|
778
|
+
const usageSection = `
|
|
779
|
+
<Card>
|
|
780
|
+
<CardHeader>
|
|
781
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
782
|
+
<BookOpen size={24} />
|
|
783
|
+
Utilisation
|
|
784
|
+
</h2>
|
|
785
|
+
</CardHeader>
|
|
786
|
+
<CardBody className="space-y-4">
|
|
787
|
+
<Alert color="default" className="mb-4">
|
|
788
|
+
<p className="text-sm">
|
|
789
|
+
📝 <strong>Section à compléter par l'auteur du module</strong>
|
|
790
|
+
</p>
|
|
791
|
+
<p className="text-sm text-slate-600 dark:text-slate-400 mt-2">
|
|
792
|
+
Ajoutez ici des exemples d'utilisation, des configurations spécifiques,
|
|
793
|
+
et toute information utile pour les développeurs utilisant ce module.
|
|
794
|
+
</p>
|
|
795
|
+
</Alert>
|
|
796
|
+
|
|
797
|
+
<div>
|
|
798
|
+
<h3 className="text-lg font-semibold mb-2">Exemple d'utilisation</h3>
|
|
799
|
+
<Alert color="primary" className="p-4 mb-4">
|
|
800
|
+
<pre className="whitespace-pre-wrap">{\`// Importez les composants depuis le module
|
|
801
|
+
import { ${config.pages.length > 0
|
|
802
|
+
? config.pages[0].name
|
|
803
|
+
.split("-")
|
|
804
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
805
|
+
.join("") + "Page"
|
|
806
|
+
: "Component"} } from "${config.moduleName}";
|
|
807
|
+
|
|
808
|
+
// Utilisez-les dans votre application
|
|
809
|
+
<${config.pages.length > 0
|
|
810
|
+
? config.pages[0].name
|
|
811
|
+
.split("-")
|
|
812
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
813
|
+
.join("") + "Page"
|
|
814
|
+
: "Component"} />\`}</pre>
|
|
815
|
+
</Alert>
|
|
816
|
+
</div>
|
|
817
|
+
</CardBody>
|
|
818
|
+
</Card>
|
|
819
|
+
`;
|
|
820
|
+
// Danger zone
|
|
821
|
+
const dangerSection = `
|
|
822
|
+
<Card>
|
|
823
|
+
<CardHeader>
|
|
824
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2 text-danger">
|
|
825
|
+
<AlertTriangle size={24} />
|
|
826
|
+
Danger Zone
|
|
827
|
+
</h2>
|
|
828
|
+
</CardHeader>
|
|
829
|
+
<CardBody className="space-y-4">
|
|
830
|
+
<Alert color="danger" className="mb-4">
|
|
831
|
+
<p className="text-sm font-semibold">
|
|
832
|
+
⚠️ Cette action est irréversible
|
|
833
|
+
</p>
|
|
834
|
+
<p className="text-sm mt-2">
|
|
835
|
+
La suppression du module supprimera toutes les pages, routes API et migrations associées.
|
|
836
|
+
</p>
|
|
837
|
+
</Alert>
|
|
838
|
+
|
|
839
|
+
<div>
|
|
840
|
+
<h3 className="text-lg font-semibold mb-2">Supprimer le module</h3>
|
|
841
|
+
<Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
|
|
842
|
+
pnpm lastbrain remove-module ${moduleNameClean}
|
|
843
|
+
</Snippet>
|
|
844
|
+
<Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
|
|
845
|
+
pnpm build:modules
|
|
846
|
+
</Snippet>
|
|
847
|
+
</div>
|
|
848
|
+
</CardBody>
|
|
849
|
+
</Card>
|
|
850
|
+
`;
|
|
851
|
+
return `"use client";
|
|
852
|
+
|
|
853
|
+
import { Card, CardBody, CardHeader } from "@lastbrain/ui";
|
|
854
|
+
import { Chip } from "@lastbrain/ui";
|
|
855
|
+
import { Snippet } from "@lastbrain/ui";
|
|
856
|
+
import { Alert } from "@lastbrain/ui";
|
|
857
|
+
import { TableStructure } from "@lastbrain/ui";
|
|
858
|
+
import {
|
|
859
|
+
FileText,
|
|
860
|
+
Zap,
|
|
861
|
+
Database,
|
|
862
|
+
Package,
|
|
863
|
+
BookOpen,
|
|
864
|
+
AlertTriangle
|
|
865
|
+
} from "lucide-react";
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Documentation component for ${config.moduleName}
|
|
869
|
+
* Auto-generated from ${config.slug}.build.config.ts
|
|
870
|
+
*
|
|
871
|
+
* To regenerate this file, run:
|
|
872
|
+
* pnpm generate:module-docs
|
|
873
|
+
*/
|
|
874
|
+
export function Doc() {
|
|
875
|
+
return (
|
|
876
|
+
<div className="container mx-auto p-6 space-y-6">
|
|
877
|
+
<Card>
|
|
878
|
+
<CardHeader>
|
|
879
|
+
<div>
|
|
880
|
+
<h1 className="text-3xl font-bold mb-2">📦 Module ${moduleNameClean}</h1>
|
|
881
|
+
<p className="text-slate-600 dark:text-slate-400">${config.moduleName}</p>
|
|
882
|
+
</div>
|
|
883
|
+
</CardHeader>
|
|
884
|
+
<CardBody>
|
|
885
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
886
|
+
<div>
|
|
887
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Package</p>
|
|
888
|
+
<code className="text-sm font-semibold">${config.moduleName}</code>
|
|
889
|
+
</div>
|
|
890
|
+
<div>
|
|
891
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Slug</p>
|
|
892
|
+
<code className="text-sm font-semibold">${config.slug}</code>
|
|
893
|
+
</div>
|
|
894
|
+
<div>
|
|
895
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Type</p>
|
|
896
|
+
<code className="text-sm font-semibold">Module LastBrain</code>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
</CardBody>
|
|
900
|
+
</Card>
|
|
901
|
+
${pagesSection}${apisSection}${tablesSection}${installSection}${usageSection}${dangerSection}
|
|
902
|
+
</div>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
`;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Generate README.md for the module
|
|
909
|
+
*/
|
|
910
|
+
async function generateModuleReadme(config, moduleDir) {
|
|
911
|
+
const moduleNameClean = config.slug.replace("module-", "");
|
|
912
|
+
let md = `# 📦 Module ${moduleNameClean}\n\n`;
|
|
913
|
+
md += `> ${config.moduleName}\n\n`;
|
|
914
|
+
// Information section
|
|
915
|
+
md += `## 📋 Informations\n\n`;
|
|
916
|
+
md += `- **Nom du package**: \`${config.moduleName}\`\n`;
|
|
917
|
+
md += `- **Slug**: \`${config.slug}\`\n`;
|
|
918
|
+
md += `- **Type**: Module LastBrain\n\n`;
|
|
919
|
+
// Pages section
|
|
920
|
+
if (config.pages.length > 0) {
|
|
921
|
+
md += `## 📄 Pages Disponibles\n\n`;
|
|
922
|
+
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
923
|
+
const authPages = config.pages.filter((p) => p.section === "auth");
|
|
924
|
+
const adminPages = config.pages.filter((p) => p.section === "admin");
|
|
925
|
+
if (publicPages.length > 0) {
|
|
926
|
+
md += `### Pages Publiques\n\n`;
|
|
927
|
+
for (const page of publicPages) {
|
|
928
|
+
const componentName = page.name
|
|
929
|
+
.split("-")
|
|
930
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
931
|
+
.join("");
|
|
932
|
+
md += `- **GET** \`${page.path}\` - ${componentName}\n`;
|
|
933
|
+
}
|
|
934
|
+
md += `\n`;
|
|
935
|
+
}
|
|
936
|
+
if (authPages.length > 0) {
|
|
937
|
+
md += `### Pages Protégées (Auth)\n\n`;
|
|
938
|
+
for (const page of authPages) {
|
|
939
|
+
const componentName = page.name
|
|
940
|
+
.split("-")
|
|
941
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
942
|
+
.join("");
|
|
943
|
+
md += `- **GET** \`${page.path}\` - ${componentName}\n`;
|
|
944
|
+
}
|
|
945
|
+
md += `\n`;
|
|
946
|
+
}
|
|
947
|
+
if (adminPages.length > 0) {
|
|
948
|
+
md += `### Pages Admin\n\n`;
|
|
949
|
+
for (const page of adminPages) {
|
|
950
|
+
const componentName = page.name
|
|
951
|
+
.split("-")
|
|
952
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
953
|
+
.join("");
|
|
954
|
+
md += `- **GET** \`/admin/${config.slug.replace("module-", "")}/${page.name}\` - ${componentName}\n`;
|
|
955
|
+
}
|
|
956
|
+
md += `\n`;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// APIs section
|
|
960
|
+
if (config.tables.length > 0) {
|
|
961
|
+
md += `## 🔌 API Routes\n\n`;
|
|
962
|
+
for (const table of config.tables) {
|
|
963
|
+
for (const section of table.sections) {
|
|
964
|
+
md += `### \`/api/${section}/${table.name}\`\n\n`;
|
|
965
|
+
md += `**Méthodes supportées**: GET, POST, PUT, DELETE\n\n`;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// Database section
|
|
970
|
+
if (config.tables.length > 0) {
|
|
971
|
+
md += `## 🗄️ Base de Données\n\n`;
|
|
972
|
+
md += `### Tables\n\n`;
|
|
973
|
+
for (const table of config.tables) {
|
|
974
|
+
md += `#### \`${table.name}\`\n\n`;
|
|
975
|
+
md += `\`\`\`tsx\n`;
|
|
976
|
+
md += `<TableStructure\n`;
|
|
977
|
+
md += ` tableName="${table.name}"\n`;
|
|
978
|
+
md += ` title="${table.name}"\n`;
|
|
979
|
+
md += ` description="Table ${table.name} du module ${moduleNameClean}"\n`;
|
|
980
|
+
md += `/>\n`;
|
|
981
|
+
md += `\`\`\`\n\n`;
|
|
982
|
+
}
|
|
983
|
+
// Get migration files
|
|
984
|
+
const migrationsPath = path.join(moduleDir, "supabase", "migrations");
|
|
985
|
+
if (fs.existsSync(migrationsPath)) {
|
|
986
|
+
const migrationFiles = fs
|
|
987
|
+
.readdirSync(migrationsPath)
|
|
988
|
+
.filter((f) => f.endsWith(".sql"))
|
|
989
|
+
.sort();
|
|
990
|
+
if (migrationFiles.length > 0) {
|
|
991
|
+
md += `### Migrations\n\n`;
|
|
992
|
+
for (const migration of migrationFiles) {
|
|
993
|
+
md += `- \`${migration}\`\n`;
|
|
994
|
+
}
|
|
995
|
+
md += `\n`;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Installation section
|
|
1000
|
+
md += `## 📦 Installation\n\n`;
|
|
1001
|
+
md += `\`\`\`bash\n`;
|
|
1002
|
+
md += `pnpm lastbrain add-module ${moduleNameClean}\n`;
|
|
1003
|
+
md += `pnpm build:modules\n`;
|
|
1004
|
+
md += `\`\`\`\n\n`;
|
|
1005
|
+
md += `### Appliquer les migrations\n\n`;
|
|
1006
|
+
md += `\`\`\`bash\n`;
|
|
1007
|
+
md += `cd apps/votre-app\n`;
|
|
1008
|
+
md += `supabase migration up\n`;
|
|
1009
|
+
md += `\`\`\`\n\n`;
|
|
1010
|
+
// Usage section
|
|
1011
|
+
md += `## 💡 Utilisation\n\n`;
|
|
1012
|
+
md += `<!-- 📝 Section à compléter par l'auteur du module -->\n\n`;
|
|
1013
|
+
md += `### Exemple d'utilisation\n\n`;
|
|
1014
|
+
md += `\`\`\`tsx\n`;
|
|
1015
|
+
md += `// Importez les composants depuis le module\n`;
|
|
1016
|
+
if (config.pages.length > 0) {
|
|
1017
|
+
const firstPage = config.pages[0];
|
|
1018
|
+
const componentName = firstPage.name
|
|
1019
|
+
.split("-")
|
|
1020
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1021
|
+
.join("");
|
|
1022
|
+
md += `import { ${componentName}Page } from "${config.moduleName}";\n\n`;
|
|
1023
|
+
md += `// Utilisez-les dans votre application\n`;
|
|
1024
|
+
md += `<${componentName}Page />\n`;
|
|
1025
|
+
}
|
|
1026
|
+
md += `\`\`\`\n\n`;
|
|
1027
|
+
md += `### Configuration\n\n`;
|
|
1028
|
+
md += `<!-- Ajoutez ici les détails de configuration spécifiques -->\n\n`;
|
|
1029
|
+
// Danger zone
|
|
1030
|
+
md += `## ⚠️ Danger Zone\n\n`;
|
|
1031
|
+
md += `La suppression du module supprimera toutes les pages, routes API et migrations associées. **Cette action est irréversible.**\n\n`;
|
|
1032
|
+
md += `\`\`\`bash\n`;
|
|
1033
|
+
md += `pnpm lastbrain remove-module ${moduleNameClean}\n`;
|
|
1034
|
+
md += `pnpm build:modules\n`;
|
|
1035
|
+
md += `\`\`\`\n\n`;
|
|
1036
|
+
// Write README.md
|
|
1037
|
+
const readmePath = path.join(moduleDir, "README.md");
|
|
1038
|
+
await fs.writeFile(readmePath, md);
|
|
1039
|
+
console.log(chalk.yellow(" 📄 README.md"));
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Met à jour le registre des modules dans core/src/config/modules.ts
|
|
1043
|
+
*/
|
|
1044
|
+
async function updateModuleRegistry(config, rootDir) {
|
|
1045
|
+
const moduleRegistryPath = path.join(rootDir, "packages", "core", "src", "config", "modules.ts");
|
|
1046
|
+
if (!fs.existsSync(moduleRegistryPath)) {
|
|
1047
|
+
console.log(chalk.yellow(" ⚠️ Fichier de registre non trouvé, création..."));
|
|
1048
|
+
// Si le fichier n'existe pas, on le crée avec le module actuel
|
|
1049
|
+
const content = `/**
|
|
1050
|
+
* Configuration centralisée des modules LastBrain
|
|
1051
|
+
* Ce fichier est auto-généré et maintenu par les scripts de gestion des modules
|
|
1052
|
+
*/
|
|
1053
|
+
|
|
1054
|
+
export interface ModuleMetadata {
|
|
1055
|
+
name: string;
|
|
1056
|
+
package: string;
|
|
1057
|
+
description: string;
|
|
1058
|
+
emoji: string;
|
|
1059
|
+
version?: string;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export const AVAILABLE_MODULES: ModuleMetadata[] = [
|
|
1063
|
+
{
|
|
1064
|
+
name: "${config.slug.replace("module-", "")}",
|
|
1065
|
+
package: "@lastbrain/${config.slug}",
|
|
1066
|
+
description: "Module ${config.moduleName}",
|
|
1067
|
+
emoji: "📦",
|
|
1068
|
+
},
|
|
1069
|
+
];
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Récupère les métadonnées d'un module par son nom
|
|
1073
|
+
*/
|
|
1074
|
+
export function getModuleMetadata(name: string): ModuleMetadata | undefined {
|
|
1075
|
+
return AVAILABLE_MODULES.find((m) => m.name === name);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Vérifie si un module existe
|
|
1080
|
+
*/
|
|
1081
|
+
export function moduleExists(name: string): boolean {
|
|
1082
|
+
return AVAILABLE_MODULES.some((m) => m.name === name);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Récupère la liste des noms de modules disponibles
|
|
1087
|
+
*/
|
|
1088
|
+
export function getAvailableModuleNames(): string[] {
|
|
1089
|
+
return AVAILABLE_MODULES.map((m) => m.name);
|
|
1090
|
+
}
|
|
1091
|
+
`;
|
|
1092
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1093
|
+
console.log(chalk.green(" ✓ Registre créé"));
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
let content = await fs.readFile(moduleRegistryPath, "utf-8");
|
|
1098
|
+
const moduleName = config.slug.replace("module-", "");
|
|
1099
|
+
const moduleEntry = ` {
|
|
1100
|
+
name: "${moduleName}",
|
|
1101
|
+
package: "@lastbrain/${config.slug}",
|
|
1102
|
+
description: "Module ${config.moduleName}",
|
|
1103
|
+
emoji: "📦",
|
|
1104
|
+
},`;
|
|
1105
|
+
// Vérifier si le module existe déjà
|
|
1106
|
+
if (content.includes(`name: "${moduleName}"`)) {
|
|
1107
|
+
console.log(chalk.yellow(` ⚠️ Module ${moduleName} déjà présent dans le registre`));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
// Trouver le tableau AVAILABLE_MODULES et ajouter le module
|
|
1111
|
+
const arrayMatch = content.match(/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/);
|
|
1112
|
+
if (arrayMatch) {
|
|
1113
|
+
const arrayContent = arrayMatch[1];
|
|
1114
|
+
const lastItem = arrayContent.trim().split("\n").pop();
|
|
1115
|
+
// Ajouter le nouveau module à la fin du tableau
|
|
1116
|
+
const newArrayContent = arrayContent.trimEnd() + "\n" + moduleEntry + "\n";
|
|
1117
|
+
content = content.replace(/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/, `export const AVAILABLE_MODULES: ModuleMetadata[] = [${newArrayContent}];`);
|
|
1118
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1119
|
+
console.log(chalk.green(` ✓ Module ${moduleName} ajouté au registre`));
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
console.log(chalk.yellow(" ⚠️ Format du registre non reconnu, ajout manuel requis"));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
catch (error) {
|
|
1126
|
+
console.log(chalk.yellow(` ⚠️ Erreur lors de la mise à jour du registre: ${error}`));
|
|
1127
|
+
console.log(chalk.gray(" Vous devrez ajouter manuellement le module au registre"));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Trouve le répertoire racine du workspace (avec pnpm-workspace.yaml)
|
|
1132
|
+
*/
|
|
1133
|
+
export function findWorkspaceRoot() {
|
|
1134
|
+
let rootDir = process.cwd();
|
|
1135
|
+
let attempts = 0;
|
|
1136
|
+
const maxAttempts = 5;
|
|
1137
|
+
while (attempts < maxAttempts) {
|
|
1138
|
+
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
1139
|
+
if (fs.existsSync(workspaceFile)) {
|
|
1140
|
+
return rootDir;
|
|
1141
|
+
}
|
|
1142
|
+
rootDir = path.resolve(rootDir, "..");
|
|
1143
|
+
attempts++;
|
|
1144
|
+
}
|
|
1145
|
+
throw new Error("Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)");
|
|
1146
|
+
}
|
|
523
1147
|
/**
|
|
524
1148
|
* Crée la structure du module
|
|
525
1149
|
*/
|
|
526
|
-
async function createModuleStructure(config, rootDir) {
|
|
1150
|
+
export async function createModuleStructure(config, rootDir) {
|
|
527
1151
|
const moduleDir = path.join(rootDir, "packages", config.slug);
|
|
528
1152
|
console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
|
|
529
1153
|
// Créer la structure de base
|
|
@@ -531,23 +1155,28 @@ async function createModuleStructure(config, rootDir) {
|
|
|
531
1155
|
await fs.ensureDir(path.join(moduleDir, "src"));
|
|
532
1156
|
await fs.ensureDir(path.join(moduleDir, "src", "web"));
|
|
533
1157
|
await fs.ensureDir(path.join(moduleDir, "src", "api"));
|
|
1158
|
+
await fs.ensureDir(path.join(moduleDir, "src", "components"));
|
|
534
1159
|
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
|
|
1160
|
+
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations-down"));
|
|
535
1161
|
// Créer package.json
|
|
536
1162
|
console.log(chalk.yellow(" 📄 package.json"));
|
|
537
1163
|
await fs.writeFile(path.join(moduleDir, "package.json"), generatePackageJson(config.moduleName, config.slug, rootDir));
|
|
538
1164
|
// Créer tsconfig.json
|
|
539
1165
|
console.log(chalk.yellow(" 📄 tsconfig.json"));
|
|
540
1166
|
await fs.writeFile(path.join(moduleDir, "tsconfig.json"), generateTsConfig());
|
|
541
|
-
// Créer {
|
|
542
|
-
const
|
|
1167
|
+
// Créer {name}.build.config.ts (sans le préfixe module-)
|
|
1168
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
1169
|
+
const buildConfigFileName = `${moduleNameOnly}.build.config.ts`;
|
|
543
1170
|
console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
|
|
544
1171
|
await fs.writeFile(path.join(moduleDir, "src", buildConfigFileName), generateBuildConfig(config));
|
|
545
1172
|
// Créer index.ts
|
|
546
1173
|
console.log(chalk.yellow(" 📄 src/index.ts"));
|
|
547
|
-
await fs.writeFile(path.join(moduleDir, "src", "index.ts"), generateIndexTs(config.pages));
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
1174
|
+
await fs.writeFile(path.join(moduleDir, "src", "index.ts"), generateIndexTs(config.pages, moduleNameOnly));
|
|
1175
|
+
// Note: server.ts n'est plus généré pour éviter les conflits d'exports
|
|
1176
|
+
// Les routes API sont exposées via le build.config et l'app les importe dynamiquement
|
|
1177
|
+
// Créer Doc.tsx
|
|
1178
|
+
console.log(chalk.yellow(" 📄 src/components/Doc.tsx"));
|
|
1179
|
+
await fs.writeFile(path.join(moduleDir, "src", "components", "Doc.tsx"), generateDocComponent(config));
|
|
551
1180
|
// Créer les pages
|
|
552
1181
|
console.log(chalk.blue("\n📄 Création des pages..."));
|
|
553
1182
|
for (const page of config.pages) {
|
|
@@ -585,14 +1214,56 @@ async function createModuleStructure(config, rootDir) {
|
|
|
585
1214
|
const migrationFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
586
1215
|
console.log(chalk.yellow(` 📄 supabase/migrations/${migrationFileName}`));
|
|
587
1216
|
await fs.writeFile(path.join(moduleDir, "supabase", "migrations", migrationFileName), generateMigration(config.tables, config.slug));
|
|
1217
|
+
// Génération du fichier DOWN correspondant pour rollback
|
|
1218
|
+
const downFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
1219
|
+
const downContent = config.tables
|
|
1220
|
+
.map((t) => `-- Rollback for table ${t.name}\nDROP POLICY IF EXISTS ${t.name}_owner_delete ON public.${t.name};\nDROP POLICY IF EXISTS ${t.name}_owner_update ON public.${t.name};\nDROP POLICY IF EXISTS ${t.name}_owner_insert ON public.${t.name};\nDROP POLICY IF EXISTS ${t.name}_owner_select ON public.${t.name};\nDROP TRIGGER IF EXISTS set_${t.name}_updated_at ON public.${t.name};\nDROP INDEX IF EXISTS idx_${t.name}_owner_id;\nDROP TABLE IF EXISTS public.${t.name};\n`)
|
|
1221
|
+
.join("\n");
|
|
1222
|
+
console.log(chalk.yellow(` 📄 supabase/migrations-down/${downFileName}`));
|
|
1223
|
+
await fs.writeFile(path.join(moduleDir, "supabase", "migrations-down", downFileName), `-- DOWN migration for ${config.slug}\n${downContent}`);
|
|
1224
|
+
}
|
|
1225
|
+
// Générer la documentation du module
|
|
1226
|
+
console.log(chalk.blue("\n📝 Génération de la documentation..."));
|
|
1227
|
+
await generateModuleReadme(config, moduleDir);
|
|
1228
|
+
// Installer les dépendances
|
|
1229
|
+
console.log(chalk.blue("\n📦 Installation des dépendances..."));
|
|
1230
|
+
const { execSync } = await import("child_process");
|
|
1231
|
+
const installCmd = process.env.CI
|
|
1232
|
+
? "pnpm install --no-frozen-lockfile"
|
|
1233
|
+
: "pnpm install";
|
|
1234
|
+
try {
|
|
1235
|
+
execSync(installCmd, {
|
|
1236
|
+
cwd: moduleDir,
|
|
1237
|
+
stdio: "inherit",
|
|
1238
|
+
});
|
|
1239
|
+
console.log(chalk.green("\n✅ Dépendances installées avec succès!"));
|
|
1240
|
+
}
|
|
1241
|
+
catch (error) {
|
|
1242
|
+
console.log(chalk.yellow("\n⚠️ Erreur lors de l'installation, veuillez exécuter manuellement:"));
|
|
1243
|
+
console.log(chalk.gray(` cd ${moduleDir} && pnpm install`));
|
|
1244
|
+
}
|
|
1245
|
+
// Mettre à jour le registre des modules dans le core
|
|
1246
|
+
console.log(chalk.blue("\n📝 Mise à jour du registre des modules..."));
|
|
1247
|
+
await updateModuleRegistry(config, rootDir);
|
|
1248
|
+
// Build auto du module pour générer dist immédiatement
|
|
1249
|
+
console.log(chalk.blue("\n🏗️ Build initial du module..."));
|
|
1250
|
+
try {
|
|
1251
|
+
execSync(`pnpm --filter ${config.slug} build`, {
|
|
1252
|
+
cwd: rootDir,
|
|
1253
|
+
stdio: "inherit",
|
|
1254
|
+
});
|
|
1255
|
+
console.log(chalk.green("✓ Module compilé"));
|
|
1256
|
+
}
|
|
1257
|
+
catch (error) {
|
|
1258
|
+
console.log(chalk.yellow("⚠️ Build automatique échoué, exécutez: cd"), config.slug, "&& pnpm build");
|
|
588
1259
|
}
|
|
589
1260
|
console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
|
|
590
1261
|
console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
|
|
591
1262
|
console.log(chalk.blue("Prochaines étapes:"));
|
|
592
|
-
console.log(chalk.gray(` 1.
|
|
593
|
-
console.log(chalk.gray(
|
|
594
|
-
console.log(chalk.gray(
|
|
595
|
-
console.log(chalk.gray(
|
|
1263
|
+
console.log(chalk.gray(` 1. Ajouter à une app: pnpm lastbrain add-module ${config.slug.replace("module-", "")}`));
|
|
1264
|
+
console.log(chalk.gray(" 2. (Optionnel) Modifier Doc.tsx pour documentation personnalisée"));
|
|
1265
|
+
console.log(chalk.gray(" 3. Publier: pnpm publish:" + config.slug));
|
|
1266
|
+
console.log(chalk.gray(" 4. Générer docs globales: pnpm generate:all\n"));
|
|
596
1267
|
}
|
|
597
1268
|
/**
|
|
598
1269
|
* Point d'entrée du script
|
|
@@ -646,29 +1317,50 @@ export async function createModule() {
|
|
|
646
1317
|
const pages = [];
|
|
647
1318
|
// Pages publiques
|
|
648
1319
|
const publicPages = parsePagesList(answers.pagesPublic);
|
|
1320
|
+
const invalidPublic = publicPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1321
|
+
if (invalidPublic.length) {
|
|
1322
|
+
console.error(chalk.red(`❌ Noms de pages publiques réservés détectés: ${invalidPublic.join(", ")}`));
|
|
1323
|
+
console.error(chalk.yellow("Noms interdits: layout, page, api, admin, auth, public. Choisissez des noms métier distincts."));
|
|
1324
|
+
process.exit(1);
|
|
1325
|
+
}
|
|
649
1326
|
for (const pageName of publicPages) {
|
|
1327
|
+
const slugName = slugifyPageName(pageName);
|
|
650
1328
|
pages.push({
|
|
651
1329
|
section: "public",
|
|
652
|
-
path: `/${
|
|
653
|
-
name:
|
|
1330
|
+
path: `/${slugName}`,
|
|
1331
|
+
name: slugName,
|
|
654
1332
|
});
|
|
655
1333
|
}
|
|
656
1334
|
// Pages auth
|
|
657
1335
|
const authPages = parsePagesList(answers.pagesAuth);
|
|
1336
|
+
const invalidAuth = authPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1337
|
+
if (invalidAuth.length) {
|
|
1338
|
+
console.error(chalk.red(`❌ Noms de pages auth réservés détectés: ${invalidAuth.join(", ")}`));
|
|
1339
|
+
console.error(chalk.yellow("Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: dashboard, profile)."));
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
658
1342
|
for (const pageName of authPages) {
|
|
1343
|
+
const slugName = slugifyPageName(pageName);
|
|
659
1344
|
pages.push({
|
|
660
1345
|
section: "auth",
|
|
661
|
-
path: `/${
|
|
662
|
-
name:
|
|
1346
|
+
path: `/${slugName}`,
|
|
1347
|
+
name: slugName,
|
|
663
1348
|
});
|
|
664
1349
|
}
|
|
665
1350
|
// Pages admin
|
|
666
1351
|
const adminPages = parsePagesList(answers.pagesAdmin);
|
|
1352
|
+
const invalidAdmin = adminPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1353
|
+
if (invalidAdmin.length) {
|
|
1354
|
+
console.error(chalk.red(`❌ Noms de pages admin réservés détectés: ${invalidAdmin.join(", ")}`));
|
|
1355
|
+
console.error(chalk.yellow("Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: settings, users)."));
|
|
1356
|
+
process.exit(1);
|
|
1357
|
+
}
|
|
667
1358
|
for (const pageName of adminPages) {
|
|
1359
|
+
const slugName = slugifyPageName(pageName);
|
|
668
1360
|
pages.push({
|
|
669
1361
|
section: "admin",
|
|
670
|
-
path: `/${
|
|
671
|
-
name:
|
|
1362
|
+
path: `/${slugName}`,
|
|
1363
|
+
name: slugName,
|
|
672
1364
|
});
|
|
673
1365
|
}
|
|
674
1366
|
// Tables
|
|
@@ -698,19 +1390,12 @@ export async function createModule() {
|
|
|
698
1390
|
tables,
|
|
699
1391
|
};
|
|
700
1392
|
// Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
|
|
701
|
-
let rootDir
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
while (attempts < maxAttempts) {
|
|
705
|
-
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
706
|
-
if (fs.existsSync(workspaceFile)) {
|
|
707
|
-
break;
|
|
708
|
-
}
|
|
709
|
-
rootDir = path.resolve(rootDir, "..");
|
|
710
|
-
attempts++;
|
|
1393
|
+
let rootDir;
|
|
1394
|
+
try {
|
|
1395
|
+
rootDir = findWorkspaceRoot();
|
|
711
1396
|
}
|
|
712
|
-
|
|
713
|
-
console.error(chalk.red("❌
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
console.error(chalk.red("❌ " + error.message));
|
|
714
1399
|
process.exit(1);
|
|
715
1400
|
}
|
|
716
1401
|
// Créer le module
|