@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,25 +2,39 @@ 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";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
export interface PageConfig {
|
|
7
11
|
section: "public" | "auth" | "admin";
|
|
8
12
|
path: string;
|
|
9
13
|
name: string;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
interface TableConfig {
|
|
16
|
+
export interface TableConfig {
|
|
13
17
|
name: string;
|
|
14
18
|
sections: ("public" | "auth" | "admin")[];
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
interface ModuleConfig {
|
|
21
|
+
export interface ModuleConfig {
|
|
18
22
|
slug: string;
|
|
19
23
|
moduleName: string;
|
|
20
24
|
pages: PageConfig[];
|
|
21
25
|
tables: TableConfig[];
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
// Noms réservés pour éviter les collisions avec le routeur/app
|
|
29
|
+
const RESERVED_PAGE_NAMES = new Set([
|
|
30
|
+
"layout",
|
|
31
|
+
"page",
|
|
32
|
+
"api",
|
|
33
|
+
"admin",
|
|
34
|
+
"auth",
|
|
35
|
+
"public",
|
|
36
|
+
]);
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Parse une chaîne de pages séparées par des virgules
|
|
26
40
|
* Ex: "legal, privacy, terms" => ["legal", "privacy", "terms"]
|
|
@@ -33,6 +47,19 @@ function parsePagesList(input: string): string[] {
|
|
|
33
47
|
.filter((p) => p.length > 0);
|
|
34
48
|
}
|
|
35
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Slugifie un nom de page: minuscules, tirets, alphanum uniquement
|
|
52
|
+
*/
|
|
53
|
+
function slugifyPageName(name: string): string {
|
|
54
|
+
return name
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[\s_]+/g, "-")
|
|
58
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
59
|
+
.replace(/--+/g, "-")
|
|
60
|
+
.replace(/^-+|-+$/g, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
/**
|
|
37
64
|
* Parse une chaîne de tables séparées par des virgules
|
|
38
65
|
* Ex: "settings, users" => ["settings", "users"]
|
|
@@ -92,7 +119,8 @@ function generatePackageJson(
|
|
|
92
119
|
rootDir: string,
|
|
93
120
|
): string {
|
|
94
121
|
const versions = getLastBrainPackageVersions(rootDir);
|
|
95
|
-
const
|
|
122
|
+
const moduleNameOnly = slug.replace("module-", "");
|
|
123
|
+
const buildConfigExport = `./${moduleNameOnly}.build.config`;
|
|
96
124
|
return JSON.stringify(
|
|
97
125
|
{
|
|
98
126
|
name: moduleName,
|
|
@@ -126,8 +154,8 @@ function generatePackageJson(
|
|
|
126
154
|
default: "./dist/server.js",
|
|
127
155
|
},
|
|
128
156
|
[buildConfigExport]: {
|
|
129
|
-
types: `./dist/${
|
|
130
|
-
default: `./dist/${
|
|
157
|
+
types: `./dist/${moduleNameOnly}.build.config.d.ts`,
|
|
158
|
+
default: `./dist/${moduleNameOnly}.build.config.js`,
|
|
131
159
|
},
|
|
132
160
|
"./api/*": {
|
|
133
161
|
types: "./dist/api/*.d.ts",
|
|
@@ -165,6 +193,46 @@ function generateTsConfig(): string {
|
|
|
165
193
|
*/
|
|
166
194
|
function generateBuildConfig(config: ModuleConfig): string {
|
|
167
195
|
const { moduleName, pages, tables } = config;
|
|
196
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
197
|
+
|
|
198
|
+
// Helper pour normaliser les chemins (évite // et trim)
|
|
199
|
+
function normalizePath(...segments: string[]): string {
|
|
200
|
+
return (
|
|
201
|
+
"/" +
|
|
202
|
+
segments
|
|
203
|
+
.map((s) => s.replace(/^\/+/g, "").replace(/\/+$/g, ""))
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.join("/")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Construit un path de menu selon la section en respectant le pattern attendu
|
|
210
|
+
function buildMenuPath(
|
|
211
|
+
section: string,
|
|
212
|
+
pagePath: string,
|
|
213
|
+
pageName: string,
|
|
214
|
+
): string {
|
|
215
|
+
const cleanedPagePath = pagePath.replace(/^\/+/g, "");
|
|
216
|
+
switch (section) {
|
|
217
|
+
case "public": {
|
|
218
|
+
// Public: /<module>/<page> (si non déjà préfixé)
|
|
219
|
+
if (cleanedPagePath.startsWith(moduleNameOnly + "/")) {
|
|
220
|
+
return normalizePath(cleanedPagePath); // déjà préfixé
|
|
221
|
+
}
|
|
222
|
+
return normalizePath(moduleNameOnly, cleanedPagePath);
|
|
223
|
+
}
|
|
224
|
+
case "auth": {
|
|
225
|
+
// Auth: /auth/<module>/<page>
|
|
226
|
+
return normalizePath("auth", moduleNameOnly, cleanedPagePath);
|
|
227
|
+
}
|
|
228
|
+
case "admin": {
|
|
229
|
+
// Admin: /admin/<module>/<pageName> (utilise le slug brut)
|
|
230
|
+
return normalizePath("admin", moduleNameOnly, pageName);
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return normalizePath(cleanedPagePath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
168
236
|
|
|
169
237
|
// Générer la liste des pages
|
|
170
238
|
const pagesConfig = pages.map((page) => {
|
|
@@ -207,11 +275,12 @@ function generateBuildConfig(config: ModuleConfig): string {
|
|
|
207
275
|
.split("-")
|
|
208
276
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
209
277
|
.join(" ");
|
|
278
|
+
const menuPath = buildMenuPath("public", page.path, page.name);
|
|
210
279
|
return ` {
|
|
211
280
|
title: "${title}",
|
|
212
281
|
description: "Page ${title}",
|
|
213
282
|
icon: "FileText",
|
|
214
|
-
path: "${
|
|
283
|
+
path: "${menuPath}",
|
|
215
284
|
order: ${index + 1},
|
|
216
285
|
}`;
|
|
217
286
|
});
|
|
@@ -226,11 +295,12 @@ function generateBuildConfig(config: ModuleConfig): string {
|
|
|
226
295
|
.split("-")
|
|
227
296
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
228
297
|
.join(" ");
|
|
298
|
+
const menuPath = buildMenuPath("auth", page.path, page.name);
|
|
229
299
|
return ` {
|
|
230
300
|
title: "${title}",
|
|
231
301
|
description: "Page ${title}",
|
|
232
302
|
icon: "FileText",
|
|
233
|
-
path: "${
|
|
303
|
+
path: "${menuPath}",
|
|
234
304
|
order: ${index + 1},
|
|
235
305
|
}`;
|
|
236
306
|
});
|
|
@@ -245,11 +315,12 @@ function generateBuildConfig(config: ModuleConfig): string {
|
|
|
245
315
|
.split("-")
|
|
246
316
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
247
317
|
.join(" ");
|
|
318
|
+
const menuPath = buildMenuPath("admin", page.path, page.name);
|
|
248
319
|
return ` {
|
|
249
320
|
title: "${title}",
|
|
250
321
|
description: "Page ${title}",
|
|
251
322
|
icon: "Settings",
|
|
252
|
-
path: "${
|
|
323
|
+
path: "${menuPath}",
|
|
253
324
|
order: ${index + 1},
|
|
254
325
|
}`;
|
|
255
326
|
});
|
|
@@ -296,24 +367,32 @@ export default buildConfig;
|
|
|
296
367
|
/**
|
|
297
368
|
* Génère le contenu du fichier index.ts
|
|
298
369
|
*/
|
|
299
|
-
function
|
|
370
|
+
function toPascalCase(value: string): string {
|
|
371
|
+
return value
|
|
372
|
+
.split(/[-_]/g)
|
|
373
|
+
.filter(Boolean)
|
|
374
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
375
|
+
.join("");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function generateIndexTs(pages: PageConfig[], moduleNameOnly: string): string {
|
|
300
379
|
const exports = pages.map((page) => {
|
|
301
|
-
const componentName = page.name
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.join("");
|
|
305
|
-
const fileName = page.name
|
|
306
|
-
.split("-")
|
|
307
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
308
|
-
.join("");
|
|
309
|
-
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page.js";`;
|
|
380
|
+
const componentName = toPascalCase(page.name);
|
|
381
|
+
const fileName = toPascalCase(page.name);
|
|
382
|
+
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page";`;
|
|
310
383
|
});
|
|
311
384
|
|
|
385
|
+
const moduleAlias = toPascalCase(moduleNameOnly);
|
|
386
|
+
|
|
312
387
|
return `// Client Components
|
|
313
388
|
${exports.join("\n")}
|
|
314
389
|
|
|
390
|
+
// Documentation Component
|
|
391
|
+
export { Doc } from "./components/Doc";
|
|
392
|
+
export { Doc as ${moduleAlias}ModuleDoc } from "./components/Doc";
|
|
393
|
+
|
|
315
394
|
// Configuration de build
|
|
316
|
-
export { default as buildConfig } from "
|
|
395
|
+
export { default as buildConfig } from "./${moduleNameOnly}.build.config";
|
|
317
396
|
`;
|
|
318
397
|
}
|
|
319
398
|
|
|
@@ -373,8 +452,16 @@ export function ${componentName}Page() {
|
|
|
373
452
|
*/
|
|
374
453
|
function generateApiRoute(tableName: string, section: string): string {
|
|
375
454
|
const authRequired = section !== "public";
|
|
455
|
+
const clientImport =
|
|
456
|
+
section === "admin"
|
|
457
|
+
? 'import { getSupabaseServiceClient } from "@lastbrain/core/server"'
|
|
458
|
+
: 'import { getSupabaseServerClient } from "@lastbrain/core/server"';
|
|
459
|
+
const clientGetter =
|
|
460
|
+
section === "admin"
|
|
461
|
+
? "getSupabaseServiceClient"
|
|
462
|
+
: "getSupabaseServerClient";
|
|
376
463
|
|
|
377
|
-
return
|
|
464
|
+
return `${clientImport};
|
|
378
465
|
|
|
379
466
|
const jsonResponse = (payload: unknown, status = 200) => {
|
|
380
467
|
return new Response(JSON.stringify(payload), {
|
|
@@ -389,7 +476,7 @@ const jsonResponse = (payload: unknown, status = 200) => {
|
|
|
389
476
|
* GET - Liste tous les enregistrements de ${tableName}
|
|
390
477
|
*/
|
|
391
478
|
export async function GET(request: Request) {
|
|
392
|
-
const supabase = await
|
|
479
|
+
const supabase = await ${clientGetter}();
|
|
393
480
|
${
|
|
394
481
|
authRequired
|
|
395
482
|
? `
|
|
@@ -416,7 +503,7 @@ export async function GET(request: Request) {
|
|
|
416
503
|
* POST - Crée un nouvel enregistrement dans ${tableName}
|
|
417
504
|
*/
|
|
418
505
|
export async function POST(request: Request) {
|
|
419
|
-
const supabase = await
|
|
506
|
+
const supabase = await ${clientGetter}();
|
|
420
507
|
${
|
|
421
508
|
authRequired
|
|
422
509
|
? `
|
|
@@ -430,9 +517,12 @@ export async function POST(request: Request) {
|
|
|
430
517
|
|
|
431
518
|
const body = await request.json();
|
|
432
519
|
|
|
520
|
+
// Injection côté serveur de owner_id si l'utilisateur est authentifié (sécurité)
|
|
521
|
+
const insertPayload = ${authRequired ? `{ ...body, owner_id: user.id }` : "body"};
|
|
522
|
+
|
|
433
523
|
const { data, error } = await supabase
|
|
434
524
|
.from("${tableName}")
|
|
435
|
-
.insert(
|
|
525
|
+
.insert(insertPayload)
|
|
436
526
|
.select()
|
|
437
527
|
.single();
|
|
438
528
|
|
|
@@ -447,7 +537,7 @@ export async function POST(request: Request) {
|
|
|
447
537
|
* PUT - Met à jour un enregistrement dans ${tableName}
|
|
448
538
|
*/
|
|
449
539
|
export async function PUT(request: Request) {
|
|
450
|
-
const supabase = await
|
|
540
|
+
const supabase = await ${clientGetter}();
|
|
451
541
|
${
|
|
452
542
|
authRequired
|
|
453
543
|
? `
|
|
@@ -484,7 +574,7 @@ export async function PUT(request: Request) {
|
|
|
484
574
|
* DELETE - Supprime un enregistrement de ${tableName}
|
|
485
575
|
*/
|
|
486
576
|
export async function DELETE(request: Request) {
|
|
487
|
-
const supabase = await
|
|
577
|
+
const supabase = await ${clientGetter}();
|
|
488
578
|
${
|
|
489
579
|
authRequired
|
|
490
580
|
? `
|
|
@@ -605,10 +695,642 @@ ${tablesSQL}
|
|
|
605
695
|
`;
|
|
606
696
|
}
|
|
607
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Generate Doc.tsx component for the module
|
|
700
|
+
*/
|
|
701
|
+
function generateDocComponent(config: ModuleConfig): string {
|
|
702
|
+
const moduleNameClean = config.slug.replace("module-", "");
|
|
703
|
+
const moduleNameOnly = moduleNameClean;
|
|
704
|
+
|
|
705
|
+
// Generate pages sections
|
|
706
|
+
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
707
|
+
const authPages = config.pages.filter((p) => p.section === "auth");
|
|
708
|
+
const adminPages = config.pages.filter((p) => p.section === "admin");
|
|
709
|
+
|
|
710
|
+
let pagesSection = "";
|
|
711
|
+
if (config.pages.length > 0) {
|
|
712
|
+
pagesSection = `
|
|
713
|
+
<Card>
|
|
714
|
+
<CardHeader>
|
|
715
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
716
|
+
<FileText size={24} />
|
|
717
|
+
Pages Disponibles
|
|
718
|
+
</h2>
|
|
719
|
+
</CardHeader>
|
|
720
|
+
<CardBody className="space-y-4">`;
|
|
721
|
+
|
|
722
|
+
if (publicPages.length > 0) {
|
|
723
|
+
pagesSection += `
|
|
724
|
+
<div>
|
|
725
|
+
<h3 className="text-lg font-semibold mb-2">Pages Publiques</h3>
|
|
726
|
+
<div className="space-y-2">`;
|
|
727
|
+
for (const page of publicPages) {
|
|
728
|
+
const componentName = page.name
|
|
729
|
+
.split("-")
|
|
730
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
731
|
+
.join("");
|
|
732
|
+
pagesSection += `
|
|
733
|
+
<div className="flex items-start gap-2">
|
|
734
|
+
<Chip size="sm" color="success" variant="flat">GET</Chip>
|
|
735
|
+
<code className="text-sm">${page.path}</code>
|
|
736
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
737
|
+
</div>`;
|
|
738
|
+
}
|
|
739
|
+
pagesSection += `
|
|
740
|
+
</div>
|
|
741
|
+
</div>`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (authPages.length > 0) {
|
|
745
|
+
pagesSection += `
|
|
746
|
+
<div>
|
|
747
|
+
<h3 className="text-lg font-semibold mb-2">Pages Protégées (Auth)</h3>
|
|
748
|
+
<div className="space-y-2">`;
|
|
749
|
+
for (const page of authPages) {
|
|
750
|
+
const componentName = page.name
|
|
751
|
+
.split("-")
|
|
752
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
753
|
+
.join("");
|
|
754
|
+
pagesSection += `
|
|
755
|
+
<div className="flex items-start gap-2">
|
|
756
|
+
<Chip size="sm" color="primary" variant="flat">GET</Chip>
|
|
757
|
+
<code className="text-sm">${page.path}</code>
|
|
758
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
759
|
+
</div>`;
|
|
760
|
+
}
|
|
761
|
+
pagesSection += `
|
|
762
|
+
</div>
|
|
763
|
+
</div>`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (adminPages.length > 0) {
|
|
767
|
+
pagesSection += `
|
|
768
|
+
<div>
|
|
769
|
+
<h3 className="text-lg font-semibold mb-2">Pages Admin</h3>
|
|
770
|
+
<div className="space-y-2">`;
|
|
771
|
+
for (const page of adminPages) {
|
|
772
|
+
const componentName = page.name
|
|
773
|
+
.split("-")
|
|
774
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
775
|
+
.join("");
|
|
776
|
+
pagesSection += `
|
|
777
|
+
<div className="flex items-start gap-2">
|
|
778
|
+
<Chip size="sm" color="secondary" variant="flat">GET</Chip>
|
|
779
|
+
<code className="text-sm">/admin/${moduleNameOnly}/${page.name}</code>
|
|
780
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
781
|
+
</div>`;
|
|
782
|
+
}
|
|
783
|
+
pagesSection += `
|
|
784
|
+
</div>
|
|
785
|
+
</div>`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
pagesSection += `
|
|
789
|
+
</CardBody>
|
|
790
|
+
</Card>
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Generate APIs section
|
|
795
|
+
let apisSection = "";
|
|
796
|
+
if (config.tables.length > 0) {
|
|
797
|
+
apisSection = `
|
|
798
|
+
<Card>
|
|
799
|
+
<CardHeader>
|
|
800
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
801
|
+
<Zap size={24} />
|
|
802
|
+
API Routes
|
|
803
|
+
</h2>
|
|
804
|
+
</CardHeader>
|
|
805
|
+
<CardBody className="space-y-4">`;
|
|
806
|
+
|
|
807
|
+
for (const table of config.tables) {
|
|
808
|
+
for (const section of table.sections) {
|
|
809
|
+
apisSection += `
|
|
810
|
+
<div>
|
|
811
|
+
<h3 className="text-lg font-semibold mb-2">
|
|
812
|
+
<code>/api/${section}/${table.name}</code>
|
|
813
|
+
</h3>
|
|
814
|
+
<div className="flex gap-2">
|
|
815
|
+
<Chip size="sm" color="success" variant="flat">GET</Chip>
|
|
816
|
+
<Chip size="sm" color="primary" variant="flat">POST</Chip>
|
|
817
|
+
<Chip size="sm" color="warning" variant="flat">PUT</Chip>
|
|
818
|
+
<Chip size="sm" color="danger" variant="flat">DELETE</Chip>
|
|
819
|
+
</div>
|
|
820
|
+
</div>`;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
apisSection += `
|
|
825
|
+
</CardBody>
|
|
826
|
+
</Card>
|
|
827
|
+
`;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Generate tables section
|
|
831
|
+
let tablesSection = "";
|
|
832
|
+
if (config.tables.length > 0) {
|
|
833
|
+
tablesSection = `
|
|
834
|
+
<Card>
|
|
835
|
+
<CardHeader>
|
|
836
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
837
|
+
<Database size={24} />
|
|
838
|
+
Base de Données
|
|
839
|
+
</h2>
|
|
840
|
+
</CardHeader>
|
|
841
|
+
<CardBody className="space-y-6">`;
|
|
842
|
+
|
|
843
|
+
for (const table of config.tables) {
|
|
844
|
+
tablesSection += `
|
|
845
|
+
<TableStructure
|
|
846
|
+
tableName="${table.name}"
|
|
847
|
+
title="${table.name}"
|
|
848
|
+
description="Table ${table.name} du module ${moduleNameClean}"
|
|
849
|
+
/>`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
tablesSection += `
|
|
853
|
+
</CardBody>
|
|
854
|
+
</Card>
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Installation commands
|
|
859
|
+
const installSection = `
|
|
860
|
+
<Card>
|
|
861
|
+
<CardHeader>
|
|
862
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
863
|
+
<Package size={24} />
|
|
864
|
+
Installation
|
|
865
|
+
</h2>
|
|
866
|
+
</CardHeader>
|
|
867
|
+
<CardBody className="space-y-4">
|
|
868
|
+
<div>
|
|
869
|
+
<h3 className="text-lg font-semibold mb-2">Ajouter le module</h3>
|
|
870
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
871
|
+
pnpm lastbrain add-module ${moduleNameClean}
|
|
872
|
+
</Snippet>
|
|
873
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
874
|
+
pnpm build:modules
|
|
875
|
+
</Snippet>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
<div>
|
|
879
|
+
<h3 className="text-lg font-semibold mb-2">Appliquer les migrations</h3>
|
|
880
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
881
|
+
cd apps/votre-app
|
|
882
|
+
</Snippet>
|
|
883
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
884
|
+
supabase migration up
|
|
885
|
+
</Snippet>
|
|
886
|
+
</div>
|
|
887
|
+
</CardBody>
|
|
888
|
+
</Card>
|
|
889
|
+
`;
|
|
890
|
+
|
|
891
|
+
// Usage section with placeholder
|
|
892
|
+
const usageSection = `
|
|
893
|
+
<Card>
|
|
894
|
+
<CardHeader>
|
|
895
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
896
|
+
<BookOpen size={24} />
|
|
897
|
+
Utilisation
|
|
898
|
+
</h2>
|
|
899
|
+
</CardHeader>
|
|
900
|
+
<CardBody className="space-y-4">
|
|
901
|
+
<Alert color="default" className="mb-4">
|
|
902
|
+
<p className="text-sm">
|
|
903
|
+
📝 <strong>Section à compléter par l'auteur du module</strong>
|
|
904
|
+
</p>
|
|
905
|
+
<p className="text-sm text-slate-600 dark:text-slate-400 mt-2">
|
|
906
|
+
Ajoutez ici des exemples d'utilisation, des configurations spécifiques,
|
|
907
|
+
et toute information utile pour les développeurs utilisant ce module.
|
|
908
|
+
</p>
|
|
909
|
+
</Alert>
|
|
910
|
+
|
|
911
|
+
<div>
|
|
912
|
+
<h3 className="text-lg font-semibold mb-2">Exemple d'utilisation</h3>
|
|
913
|
+
<Alert color="primary" className="p-4 mb-4">
|
|
914
|
+
<pre className="whitespace-pre-wrap">{\`// Importez les composants depuis le module
|
|
915
|
+
import { ${
|
|
916
|
+
config.pages.length > 0
|
|
917
|
+
? config.pages[0].name
|
|
918
|
+
.split("-")
|
|
919
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
920
|
+
.join("") + "Page"
|
|
921
|
+
: "Component"
|
|
922
|
+
} } from "${config.moduleName}";
|
|
923
|
+
|
|
924
|
+
// Utilisez-les dans votre application
|
|
925
|
+
<${
|
|
926
|
+
config.pages.length > 0
|
|
927
|
+
? config.pages[0].name
|
|
928
|
+
.split("-")
|
|
929
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
930
|
+
.join("") + "Page"
|
|
931
|
+
: "Component"
|
|
932
|
+
} />\`}</pre>
|
|
933
|
+
</Alert>
|
|
934
|
+
</div>
|
|
935
|
+
</CardBody>
|
|
936
|
+
</Card>
|
|
937
|
+
`;
|
|
938
|
+
|
|
939
|
+
// Danger zone
|
|
940
|
+
const dangerSection = `
|
|
941
|
+
<Card>
|
|
942
|
+
<CardHeader>
|
|
943
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2 text-danger">
|
|
944
|
+
<AlertTriangle size={24} />
|
|
945
|
+
Danger Zone
|
|
946
|
+
</h2>
|
|
947
|
+
</CardHeader>
|
|
948
|
+
<CardBody className="space-y-4">
|
|
949
|
+
<Alert color="danger" className="mb-4">
|
|
950
|
+
<p className="text-sm font-semibold">
|
|
951
|
+
⚠️ Cette action est irréversible
|
|
952
|
+
</p>
|
|
953
|
+
<p className="text-sm mt-2">
|
|
954
|
+
La suppression du module supprimera toutes les pages, routes API et migrations associées.
|
|
955
|
+
</p>
|
|
956
|
+
</Alert>
|
|
957
|
+
|
|
958
|
+
<div>
|
|
959
|
+
<h3 className="text-lg font-semibold mb-2">Supprimer le module</h3>
|
|
960
|
+
<Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
|
|
961
|
+
pnpm lastbrain remove-module ${moduleNameClean}
|
|
962
|
+
</Snippet>
|
|
963
|
+
<Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
|
|
964
|
+
pnpm build:modules
|
|
965
|
+
</Snippet>
|
|
966
|
+
</div>
|
|
967
|
+
</CardBody>
|
|
968
|
+
</Card>
|
|
969
|
+
`;
|
|
970
|
+
|
|
971
|
+
return `"use client";
|
|
972
|
+
|
|
973
|
+
import { Card, CardBody, CardHeader } from "@lastbrain/ui";
|
|
974
|
+
import { Chip } from "@lastbrain/ui";
|
|
975
|
+
import { Snippet } from "@lastbrain/ui";
|
|
976
|
+
import { Alert } from "@lastbrain/ui";
|
|
977
|
+
import { TableStructure } from "@lastbrain/ui";
|
|
978
|
+
import {
|
|
979
|
+
FileText,
|
|
980
|
+
Zap,
|
|
981
|
+
Database,
|
|
982
|
+
Package,
|
|
983
|
+
BookOpen,
|
|
984
|
+
AlertTriangle
|
|
985
|
+
} from "lucide-react";
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Documentation component for ${config.moduleName}
|
|
989
|
+
* Auto-generated from ${config.slug}.build.config.ts
|
|
990
|
+
*
|
|
991
|
+
* To regenerate this file, run:
|
|
992
|
+
* pnpm generate:module-docs
|
|
993
|
+
*/
|
|
994
|
+
export function Doc() {
|
|
995
|
+
return (
|
|
996
|
+
<div className="container mx-auto p-6 space-y-6">
|
|
997
|
+
<Card>
|
|
998
|
+
<CardHeader>
|
|
999
|
+
<div>
|
|
1000
|
+
<h1 className="text-3xl font-bold mb-2">📦 Module ${moduleNameClean}</h1>
|
|
1001
|
+
<p className="text-slate-600 dark:text-slate-400">${config.moduleName}</p>
|
|
1002
|
+
</div>
|
|
1003
|
+
</CardHeader>
|
|
1004
|
+
<CardBody>
|
|
1005
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
1006
|
+
<div>
|
|
1007
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Package</p>
|
|
1008
|
+
<code className="text-sm font-semibold">${config.moduleName}</code>
|
|
1009
|
+
</div>
|
|
1010
|
+
<div>
|
|
1011
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Slug</p>
|
|
1012
|
+
<code className="text-sm font-semibold">${config.slug}</code>
|
|
1013
|
+
</div>
|
|
1014
|
+
<div>
|
|
1015
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Type</p>
|
|
1016
|
+
<code className="text-sm font-semibold">Module LastBrain</code>
|
|
1017
|
+
</div>
|
|
1018
|
+
</div>
|
|
1019
|
+
</CardBody>
|
|
1020
|
+
</Card>
|
|
1021
|
+
${pagesSection}${apisSection}${tablesSection}${installSection}${usageSection}${dangerSection}
|
|
1022
|
+
</div>
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Generate README.md for the module
|
|
1030
|
+
*/
|
|
1031
|
+
async function generateModuleReadme(config: ModuleConfig, moduleDir: string) {
|
|
1032
|
+
const moduleNameClean = config.slug.replace("module-", "");
|
|
1033
|
+
|
|
1034
|
+
let md = `# 📦 Module ${moduleNameClean}\n\n`;
|
|
1035
|
+
md += `> ${config.moduleName}\n\n`;
|
|
1036
|
+
|
|
1037
|
+
// Information section
|
|
1038
|
+
md += `## 📋 Informations\n\n`;
|
|
1039
|
+
md += `- **Nom du package**: \`${config.moduleName}\`\n`;
|
|
1040
|
+
md += `- **Slug**: \`${config.slug}\`\n`;
|
|
1041
|
+
md += `- **Type**: Module LastBrain\n\n`;
|
|
1042
|
+
|
|
1043
|
+
// Pages section
|
|
1044
|
+
if (config.pages.length > 0) {
|
|
1045
|
+
md += `## 📄 Pages Disponibles\n\n`;
|
|
1046
|
+
|
|
1047
|
+
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
1048
|
+
const authPages = config.pages.filter((p) => p.section === "auth");
|
|
1049
|
+
const adminPages = config.pages.filter((p) => p.section === "admin");
|
|
1050
|
+
|
|
1051
|
+
if (publicPages.length > 0) {
|
|
1052
|
+
md += `### Pages Publiques\n\n`;
|
|
1053
|
+
for (const page of publicPages) {
|
|
1054
|
+
const componentName = page.name
|
|
1055
|
+
.split("-")
|
|
1056
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1057
|
+
.join("");
|
|
1058
|
+
md += `- **GET** \`${page.path}\` - ${componentName}\n`;
|
|
1059
|
+
}
|
|
1060
|
+
md += `\n`;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (authPages.length > 0) {
|
|
1064
|
+
md += `### Pages Protégées (Auth)\n\n`;
|
|
1065
|
+
for (const page of authPages) {
|
|
1066
|
+
const componentName = page.name
|
|
1067
|
+
.split("-")
|
|
1068
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1069
|
+
.join("");
|
|
1070
|
+
md += `- **GET** \`${page.path}\` - ${componentName}\n`;
|
|
1071
|
+
}
|
|
1072
|
+
md += `\n`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (adminPages.length > 0) {
|
|
1076
|
+
md += `### Pages Admin\n\n`;
|
|
1077
|
+
for (const page of adminPages) {
|
|
1078
|
+
const componentName = page.name
|
|
1079
|
+
.split("-")
|
|
1080
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1081
|
+
.join("");
|
|
1082
|
+
md += `- **GET** \`/admin/${config.slug.replace("module-", "")}/${page.name}\` - ${componentName}\n`;
|
|
1083
|
+
}
|
|
1084
|
+
md += `\n`;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// APIs section
|
|
1089
|
+
if (config.tables.length > 0) {
|
|
1090
|
+
md += `## 🔌 API Routes\n\n`;
|
|
1091
|
+
|
|
1092
|
+
for (const table of config.tables) {
|
|
1093
|
+
for (const section of table.sections) {
|
|
1094
|
+
md += `### \`/api/${section}/${table.name}\`\n\n`;
|
|
1095
|
+
md += `**Méthodes supportées**: GET, POST, PUT, DELETE\n\n`;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Database section
|
|
1101
|
+
if (config.tables.length > 0) {
|
|
1102
|
+
md += `## 🗄️ Base de Données\n\n`;
|
|
1103
|
+
md += `### Tables\n\n`;
|
|
1104
|
+
|
|
1105
|
+
for (const table of config.tables) {
|
|
1106
|
+
md += `#### \`${table.name}\`\n\n`;
|
|
1107
|
+
md += `\`\`\`tsx\n`;
|
|
1108
|
+
md += `<TableStructure\n`;
|
|
1109
|
+
md += ` tableName="${table.name}"\n`;
|
|
1110
|
+
md += ` title="${table.name}"\n`;
|
|
1111
|
+
md += ` description="Table ${table.name} du module ${moduleNameClean}"\n`;
|
|
1112
|
+
md += `/>\n`;
|
|
1113
|
+
md += `\`\`\`\n\n`;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Get migration files
|
|
1117
|
+
const migrationsPath = path.join(moduleDir, "supabase", "migrations");
|
|
1118
|
+
if (fs.existsSync(migrationsPath)) {
|
|
1119
|
+
const migrationFiles = fs
|
|
1120
|
+
.readdirSync(migrationsPath)
|
|
1121
|
+
.filter((f) => f.endsWith(".sql"))
|
|
1122
|
+
.sort();
|
|
1123
|
+
|
|
1124
|
+
if (migrationFiles.length > 0) {
|
|
1125
|
+
md += `### Migrations\n\n`;
|
|
1126
|
+
for (const migration of migrationFiles) {
|
|
1127
|
+
md += `- \`${migration}\`\n`;
|
|
1128
|
+
}
|
|
1129
|
+
md += `\n`;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Installation section
|
|
1135
|
+
md += `## 📦 Installation\n\n`;
|
|
1136
|
+
md += `\`\`\`bash\n`;
|
|
1137
|
+
md += `pnpm lastbrain add-module ${moduleNameClean}\n`;
|
|
1138
|
+
md += `pnpm build:modules\n`;
|
|
1139
|
+
md += `\`\`\`\n\n`;
|
|
1140
|
+
|
|
1141
|
+
md += `### Appliquer les migrations\n\n`;
|
|
1142
|
+
md += `\`\`\`bash\n`;
|
|
1143
|
+
md += `cd apps/votre-app\n`;
|
|
1144
|
+
md += `supabase migration up\n`;
|
|
1145
|
+
md += `\`\`\`\n\n`;
|
|
1146
|
+
|
|
1147
|
+
// Usage section
|
|
1148
|
+
md += `## 💡 Utilisation\n\n`;
|
|
1149
|
+
md += `<!-- 📝 Section à compléter par l'auteur du module -->\n\n`;
|
|
1150
|
+
md += `### Exemple d'utilisation\n\n`;
|
|
1151
|
+
md += `\`\`\`tsx\n`;
|
|
1152
|
+
md += `// Importez les composants depuis le module\n`;
|
|
1153
|
+
if (config.pages.length > 0) {
|
|
1154
|
+
const firstPage = config.pages[0];
|
|
1155
|
+
const componentName = firstPage.name
|
|
1156
|
+
.split("-")
|
|
1157
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1158
|
+
.join("");
|
|
1159
|
+
md += `import { ${componentName}Page } from "${config.moduleName}";\n\n`;
|
|
1160
|
+
md += `// Utilisez-les dans votre application\n`;
|
|
1161
|
+
md += `<${componentName}Page />\n`;
|
|
1162
|
+
}
|
|
1163
|
+
md += `\`\`\`\n\n`;
|
|
1164
|
+
|
|
1165
|
+
md += `### Configuration\n\n`;
|
|
1166
|
+
md += `<!-- Ajoutez ici les détails de configuration spécifiques -->\n\n`;
|
|
1167
|
+
|
|
1168
|
+
// Danger zone
|
|
1169
|
+
md += `## ⚠️ Danger Zone\n\n`;
|
|
1170
|
+
md += `La suppression du module supprimera toutes les pages, routes API et migrations associées. **Cette action est irréversible.**\n\n`;
|
|
1171
|
+
md += `\`\`\`bash\n`;
|
|
1172
|
+
md += `pnpm lastbrain remove-module ${moduleNameClean}\n`;
|
|
1173
|
+
md += `pnpm build:modules\n`;
|
|
1174
|
+
md += `\`\`\`\n\n`;
|
|
1175
|
+
|
|
1176
|
+
// Write README.md
|
|
1177
|
+
const readmePath = path.join(moduleDir, "README.md");
|
|
1178
|
+
await fs.writeFile(readmePath, md);
|
|
1179
|
+
|
|
1180
|
+
console.log(chalk.yellow(" 📄 README.md"));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Met à jour le registre des modules dans core/src/config/modules.ts
|
|
1185
|
+
*/
|
|
1186
|
+
async function updateModuleRegistry(config: ModuleConfig, rootDir: string) {
|
|
1187
|
+
const moduleRegistryPath = path.join(
|
|
1188
|
+
rootDir,
|
|
1189
|
+
"packages",
|
|
1190
|
+
"core",
|
|
1191
|
+
"src",
|
|
1192
|
+
"config",
|
|
1193
|
+
"modules.ts",
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
if (!fs.existsSync(moduleRegistryPath)) {
|
|
1197
|
+
console.log(
|
|
1198
|
+
chalk.yellow(" ⚠️ Fichier de registre non trouvé, création..."),
|
|
1199
|
+
);
|
|
1200
|
+
// Si le fichier n'existe pas, on le crée avec le module actuel
|
|
1201
|
+
const content = `/**
|
|
1202
|
+
* Configuration centralisée des modules LastBrain
|
|
1203
|
+
* Ce fichier est auto-généré et maintenu par les scripts de gestion des modules
|
|
1204
|
+
*/
|
|
1205
|
+
|
|
1206
|
+
export interface ModuleMetadata {
|
|
1207
|
+
name: string;
|
|
1208
|
+
package: string;
|
|
1209
|
+
description: string;
|
|
1210
|
+
emoji: string;
|
|
1211
|
+
version?: string;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export const AVAILABLE_MODULES: ModuleMetadata[] = [
|
|
1215
|
+
{
|
|
1216
|
+
name: "${config.slug.replace("module-", "")}",
|
|
1217
|
+
package: "@lastbrain/${config.slug}",
|
|
1218
|
+
description: "Module ${config.moduleName}",
|
|
1219
|
+
emoji: "📦",
|
|
1220
|
+
},
|
|
1221
|
+
];
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Récupère les métadonnées d'un module par son nom
|
|
1225
|
+
*/
|
|
1226
|
+
export function getModuleMetadata(name: string): ModuleMetadata | undefined {
|
|
1227
|
+
return AVAILABLE_MODULES.find((m) => m.name === name);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Vérifie si un module existe
|
|
1232
|
+
*/
|
|
1233
|
+
export function moduleExists(name: string): boolean {
|
|
1234
|
+
return AVAILABLE_MODULES.some((m) => m.name === name);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Récupère la liste des noms de modules disponibles
|
|
1239
|
+
*/
|
|
1240
|
+
export function getAvailableModuleNames(): string[] {
|
|
1241
|
+
return AVAILABLE_MODULES.map((m) => m.name);
|
|
1242
|
+
}
|
|
1243
|
+
`;
|
|
1244
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1245
|
+
console.log(chalk.green(" ✓ Registre créé"));
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
try {
|
|
1250
|
+
let content = await fs.readFile(moduleRegistryPath, "utf-8");
|
|
1251
|
+
|
|
1252
|
+
const moduleName = config.slug.replace("module-", "");
|
|
1253
|
+
const moduleEntry = ` {
|
|
1254
|
+
name: "${moduleName}",
|
|
1255
|
+
package: "@lastbrain/${config.slug}",
|
|
1256
|
+
description: "Module ${config.moduleName}",
|
|
1257
|
+
emoji: "📦",
|
|
1258
|
+
},`;
|
|
1259
|
+
|
|
1260
|
+
// Vérifier si le module existe déjà
|
|
1261
|
+
if (content.includes(`name: "${moduleName}"`)) {
|
|
1262
|
+
console.log(
|
|
1263
|
+
chalk.yellow(
|
|
1264
|
+
` ⚠️ Module ${moduleName} déjà présent dans le registre`,
|
|
1265
|
+
),
|
|
1266
|
+
);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Trouver le tableau AVAILABLE_MODULES et ajouter le module
|
|
1271
|
+
const arrayMatch = content.match(
|
|
1272
|
+
/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/,
|
|
1273
|
+
);
|
|
1274
|
+
if (arrayMatch) {
|
|
1275
|
+
const arrayContent = arrayMatch[1];
|
|
1276
|
+
const lastItem = arrayContent.trim().split("\n").pop();
|
|
1277
|
+
|
|
1278
|
+
// Ajouter le nouveau module à la fin du tableau
|
|
1279
|
+
const newArrayContent =
|
|
1280
|
+
arrayContent.trimEnd() + "\n" + moduleEntry + "\n";
|
|
1281
|
+
content = content.replace(
|
|
1282
|
+
/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/,
|
|
1283
|
+
`export const AVAILABLE_MODULES: ModuleMetadata[] = [${newArrayContent}];`,
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1287
|
+
console.log(chalk.green(` ✓ Module ${moduleName} ajouté au registre`));
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log(
|
|
1290
|
+
chalk.yellow(
|
|
1291
|
+
" ⚠️ Format du registre non reconnu, ajout manuel requis",
|
|
1292
|
+
),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
console.log(
|
|
1297
|
+
chalk.yellow(` ⚠️ Erreur lors de la mise à jour du registre: ${error}`),
|
|
1298
|
+
);
|
|
1299
|
+
console.log(
|
|
1300
|
+
chalk.gray(" Vous devrez ajouter manuellement le module au registre"),
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Trouve le répertoire racine du workspace (avec pnpm-workspace.yaml)
|
|
1307
|
+
*/
|
|
1308
|
+
export function findWorkspaceRoot(): string {
|
|
1309
|
+
let rootDir = process.cwd();
|
|
1310
|
+
let attempts = 0;
|
|
1311
|
+
const maxAttempts = 5;
|
|
1312
|
+
|
|
1313
|
+
while (attempts < maxAttempts) {
|
|
1314
|
+
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
1315
|
+
if (fs.existsSync(workspaceFile)) {
|
|
1316
|
+
return rootDir;
|
|
1317
|
+
}
|
|
1318
|
+
rootDir = path.resolve(rootDir, "..");
|
|
1319
|
+
attempts++;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
"Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)",
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
608
1327
|
/**
|
|
609
1328
|
* Crée la structure du module
|
|
610
1329
|
*/
|
|
611
|
-
async function createModuleStructure(
|
|
1330
|
+
export async function createModuleStructure(
|
|
1331
|
+
config: ModuleConfig,
|
|
1332
|
+
rootDir: string,
|
|
1333
|
+
) {
|
|
612
1334
|
const moduleDir = path.join(rootDir, "packages", config.slug);
|
|
613
1335
|
|
|
614
1336
|
console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
|
|
@@ -618,7 +1340,9 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
618
1340
|
await fs.ensureDir(path.join(moduleDir, "src"));
|
|
619
1341
|
await fs.ensureDir(path.join(moduleDir, "src", "web"));
|
|
620
1342
|
await fs.ensureDir(path.join(moduleDir, "src", "api"));
|
|
1343
|
+
await fs.ensureDir(path.join(moduleDir, "src", "components"));
|
|
621
1344
|
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
|
|
1345
|
+
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations-down"));
|
|
622
1346
|
|
|
623
1347
|
// Créer package.json
|
|
624
1348
|
console.log(chalk.yellow(" 📄 package.json"));
|
|
@@ -631,8 +1355,9 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
631
1355
|
console.log(chalk.yellow(" 📄 tsconfig.json"));
|
|
632
1356
|
await fs.writeFile(path.join(moduleDir, "tsconfig.json"), generateTsConfig());
|
|
633
1357
|
|
|
634
|
-
// Créer {
|
|
635
|
-
const
|
|
1358
|
+
// Créer {name}.build.config.ts (sans le préfixe module-)
|
|
1359
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
1360
|
+
const buildConfigFileName = `${moduleNameOnly}.build.config.ts`;
|
|
636
1361
|
console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
|
|
637
1362
|
await fs.writeFile(
|
|
638
1363
|
path.join(moduleDir, "src", buildConfigFileName),
|
|
@@ -643,14 +1368,17 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
643
1368
|
console.log(chalk.yellow(" 📄 src/index.ts"));
|
|
644
1369
|
await fs.writeFile(
|
|
645
1370
|
path.join(moduleDir, "src", "index.ts"),
|
|
646
|
-
generateIndexTs(config.pages),
|
|
1371
|
+
generateIndexTs(config.pages, moduleNameOnly),
|
|
647
1372
|
);
|
|
648
1373
|
|
|
649
|
-
//
|
|
650
|
-
|
|
1374
|
+
// Note: server.ts n'est plus généré pour éviter les conflits d'exports
|
|
1375
|
+
// Les routes API sont exposées via le build.config et l'app les importe dynamiquement
|
|
1376
|
+
|
|
1377
|
+
// Créer Doc.tsx
|
|
1378
|
+
console.log(chalk.yellow(" 📄 src/components/Doc.tsx"));
|
|
651
1379
|
await fs.writeFile(
|
|
652
|
-
path.join(moduleDir, "src", "
|
|
653
|
-
|
|
1380
|
+
path.join(moduleDir, "src", "components", "Doc.tsx"),
|
|
1381
|
+
generateDocComponent(config),
|
|
654
1382
|
);
|
|
655
1383
|
|
|
656
1384
|
// Créer les pages
|
|
@@ -705,19 +1433,82 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
705
1433
|
path.join(moduleDir, "supabase", "migrations", migrationFileName),
|
|
706
1434
|
generateMigration(config.tables, config.slug),
|
|
707
1435
|
);
|
|
1436
|
+
|
|
1437
|
+
// Génération du fichier DOWN correspondant pour rollback
|
|
1438
|
+
const downFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
1439
|
+
const downContent = config.tables
|
|
1440
|
+
.map(
|
|
1441
|
+
(t) =>
|
|
1442
|
+
`-- 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`,
|
|
1443
|
+
)
|
|
1444
|
+
.join("\n");
|
|
1445
|
+
console.log(chalk.yellow(` 📄 supabase/migrations-down/${downFileName}`));
|
|
1446
|
+
await fs.writeFile(
|
|
1447
|
+
path.join(moduleDir, "supabase", "migrations-down", downFileName),
|
|
1448
|
+
`-- DOWN migration for ${config.slug}\n${downContent}`,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Générer la documentation du module
|
|
1453
|
+
console.log(chalk.blue("\n📝 Génération de la documentation..."));
|
|
1454
|
+
await generateModuleReadme(config, moduleDir);
|
|
1455
|
+
|
|
1456
|
+
// Installer les dépendances
|
|
1457
|
+
console.log(chalk.blue("\n📦 Installation des dépendances..."));
|
|
1458
|
+
const { execSync } = await import("child_process");
|
|
1459
|
+
const installCmd = process.env.CI
|
|
1460
|
+
? "pnpm install --no-frozen-lockfile"
|
|
1461
|
+
: "pnpm install";
|
|
1462
|
+
try {
|
|
1463
|
+
execSync(installCmd, {
|
|
1464
|
+
cwd: moduleDir,
|
|
1465
|
+
stdio: "inherit",
|
|
1466
|
+
});
|
|
1467
|
+
console.log(chalk.green("\n✅ Dépendances installées avec succès!"));
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
console.log(
|
|
1470
|
+
chalk.yellow(
|
|
1471
|
+
"\n⚠️ Erreur lors de l'installation, veuillez exécuter manuellement:",
|
|
1472
|
+
),
|
|
1473
|
+
);
|
|
1474
|
+
console.log(chalk.gray(` cd ${moduleDir} && pnpm install`));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Mettre à jour le registre des modules dans le core
|
|
1478
|
+
console.log(chalk.blue("\n📝 Mise à jour du registre des modules..."));
|
|
1479
|
+
await updateModuleRegistry(config, rootDir);
|
|
1480
|
+
|
|
1481
|
+
// Build auto du module pour générer dist immédiatement
|
|
1482
|
+
console.log(chalk.blue("\n🏗️ Build initial du module..."));
|
|
1483
|
+
try {
|
|
1484
|
+
execSync(`pnpm --filter ${config.slug} build`, {
|
|
1485
|
+
cwd: rootDir,
|
|
1486
|
+
stdio: "inherit",
|
|
1487
|
+
});
|
|
1488
|
+
console.log(chalk.green("✓ Module compilé"));
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
console.log(
|
|
1491
|
+
chalk.yellow("⚠️ Build automatique échoué, exécutez: cd"),
|
|
1492
|
+
config.slug,
|
|
1493
|
+
"&& pnpm build",
|
|
1494
|
+
);
|
|
708
1495
|
}
|
|
709
1496
|
|
|
710
1497
|
console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
|
|
711
1498
|
console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
|
|
712
1499
|
console.log(chalk.blue("Prochaines étapes:"));
|
|
713
|
-
console.log(chalk.gray(` 1. cd ${moduleDir}`));
|
|
714
|
-
console.log(chalk.gray(` 2. pnpm install`));
|
|
715
|
-
console.log(chalk.gray(` 3. pnpm build`));
|
|
716
1500
|
console.log(
|
|
717
1501
|
chalk.gray(
|
|
718
|
-
`
|
|
1502
|
+
` 1. Ajouter à une app: pnpm lastbrain add-module ${config.slug.replace("module-", "")}`,
|
|
1503
|
+
),
|
|
1504
|
+
);
|
|
1505
|
+
console.log(
|
|
1506
|
+
chalk.gray(
|
|
1507
|
+
" 2. (Optionnel) Modifier Doc.tsx pour documentation personnalisée",
|
|
719
1508
|
),
|
|
720
1509
|
);
|
|
1510
|
+
console.log(chalk.gray(" 3. Publier: pnpm publish:" + config.slug));
|
|
1511
|
+
console.log(chalk.gray(" 4. Générer docs globales: pnpm generate:all\n"));
|
|
721
1512
|
}
|
|
722
1513
|
|
|
723
1514
|
/**
|
|
@@ -779,31 +1570,76 @@ export async function createModule() {
|
|
|
779
1570
|
|
|
780
1571
|
// Pages publiques
|
|
781
1572
|
const publicPages = parsePagesList(answers.pagesPublic);
|
|
1573
|
+
const invalidPublic = publicPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1574
|
+
if (invalidPublic.length) {
|
|
1575
|
+
console.error(
|
|
1576
|
+
chalk.red(
|
|
1577
|
+
`❌ Noms de pages publiques réservés détectés: ${invalidPublic.join(", ")}`,
|
|
1578
|
+
),
|
|
1579
|
+
);
|
|
1580
|
+
console.error(
|
|
1581
|
+
chalk.yellow(
|
|
1582
|
+
"Noms interdits: layout, page, api, admin, auth, public. Choisissez des noms métier distincts.",
|
|
1583
|
+
),
|
|
1584
|
+
);
|
|
1585
|
+
process.exit(1);
|
|
1586
|
+
}
|
|
782
1587
|
for (const pageName of publicPages) {
|
|
1588
|
+
const slugName = slugifyPageName(pageName);
|
|
783
1589
|
pages.push({
|
|
784
1590
|
section: "public",
|
|
785
|
-
path: `/${
|
|
786
|
-
name:
|
|
1591
|
+
path: `/${slugName}`,
|
|
1592
|
+
name: slugName,
|
|
787
1593
|
});
|
|
788
1594
|
}
|
|
789
1595
|
|
|
790
1596
|
// Pages auth
|
|
791
1597
|
const authPages = parsePagesList(answers.pagesAuth);
|
|
1598
|
+
const invalidAuth = authPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1599
|
+
if (invalidAuth.length) {
|
|
1600
|
+
console.error(
|
|
1601
|
+
chalk.red(
|
|
1602
|
+
`❌ Noms de pages auth réservés détectés: ${invalidAuth.join(", ")}`,
|
|
1603
|
+
),
|
|
1604
|
+
);
|
|
1605
|
+
console.error(
|
|
1606
|
+
chalk.yellow(
|
|
1607
|
+
"Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: dashboard, profile).",
|
|
1608
|
+
),
|
|
1609
|
+
);
|
|
1610
|
+
process.exit(1);
|
|
1611
|
+
}
|
|
792
1612
|
for (const pageName of authPages) {
|
|
1613
|
+
const slugName = slugifyPageName(pageName);
|
|
793
1614
|
pages.push({
|
|
794
1615
|
section: "auth",
|
|
795
|
-
path: `/${
|
|
796
|
-
name:
|
|
1616
|
+
path: `/${slugName}`,
|
|
1617
|
+
name: slugName,
|
|
797
1618
|
});
|
|
798
1619
|
}
|
|
799
1620
|
|
|
800
1621
|
// Pages admin
|
|
801
1622
|
const adminPages = parsePagesList(answers.pagesAdmin);
|
|
1623
|
+
const invalidAdmin = adminPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1624
|
+
if (invalidAdmin.length) {
|
|
1625
|
+
console.error(
|
|
1626
|
+
chalk.red(
|
|
1627
|
+
`❌ Noms de pages admin réservés détectés: ${invalidAdmin.join(", ")}`,
|
|
1628
|
+
),
|
|
1629
|
+
);
|
|
1630
|
+
console.error(
|
|
1631
|
+
chalk.yellow(
|
|
1632
|
+
"Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: settings, users).",
|
|
1633
|
+
),
|
|
1634
|
+
);
|
|
1635
|
+
process.exit(1);
|
|
1636
|
+
}
|
|
802
1637
|
for (const pageName of adminPages) {
|
|
1638
|
+
const slugName = slugifyPageName(pageName);
|
|
803
1639
|
pages.push({
|
|
804
1640
|
section: "admin",
|
|
805
|
-
path: `/${
|
|
806
|
-
name:
|
|
1641
|
+
path: `/${slugName}`,
|
|
1642
|
+
name: slugName,
|
|
807
1643
|
});
|
|
808
1644
|
}
|
|
809
1645
|
|
|
@@ -836,25 +1672,11 @@ export async function createModule() {
|
|
|
836
1672
|
};
|
|
837
1673
|
|
|
838
1674
|
// Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
|
|
839
|
-
let rootDir
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
845
|
-
if (fs.existsSync(workspaceFile)) {
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
rootDir = path.resolve(rootDir, "..");
|
|
849
|
-
attempts++;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (attempts === maxAttempts) {
|
|
853
|
-
console.error(
|
|
854
|
-
chalk.red(
|
|
855
|
-
"❌ Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)",
|
|
856
|
-
),
|
|
857
|
-
);
|
|
1675
|
+
let rootDir: string;
|
|
1676
|
+
try {
|
|
1677
|
+
rootDir = findWorkspaceRoot();
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
console.error(chalk.red("❌ " + (error as Error).message));
|
|
858
1680
|
process.exit(1);
|
|
859
1681
|
}
|
|
860
1682
|
|