@apc-projects/elysia-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +45 -0
- package/src/commands/add.ts +362 -0
- package/src/commands/generate.ts +173 -0
- package/src/commands/init.ts +158 -0
- package/src/commands/macro.ts +68 -0
- package/src/index.ts +37 -0
- package/src/templates.ts +452 -0
- package/src/utils/db.ts +90 -0
- package/src/utils/env.ts +30 -0
- package/src/utils/gitignore.ts +20 -0
- package/src/utils/inject.ts +242 -0
- package/src/utils/logger.ts +21 -0
- package/src/utils/naming.ts +88 -0
- package/src/utils/package-json.ts +32 -0
- package/src/utils/pm.ts +39 -0
- package/src/utils/project.ts +62 -0
package/src/templates.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import type { NameSet } from "./utils/naming.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fonctions de génération de contenu de fichiers.
|
|
5
|
+
* On garde des template strings simples plutôt que Handlebars : moins de deps,
|
|
6
|
+
* typage complet, et facile à faire évoluer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Controller = une instance Elysia (routes), nommée d'après le module.
|
|
11
|
+
* Importe le Service (logique) et le Model (validation body/response).
|
|
12
|
+
*/
|
|
13
|
+
export function controllerTemplate(n: NameSet): string {
|
|
14
|
+
return `import { Elysia, t } from "elysia";
|
|
15
|
+
|
|
16
|
+
import { ${n.pascal} } from "./${n.kebab}.service";
|
|
17
|
+
import { ${n.pascal}Model } from "./${n.kebab}.model";
|
|
18
|
+
|
|
19
|
+
export const ${n.camel} = new Elysia({ prefix: "/${n.kebabPlural}" })
|
|
20
|
+
.get("/", () => ${n.pascal}.findAll())
|
|
21
|
+
.get(
|
|
22
|
+
"/:id",
|
|
23
|
+
({ params: { id } }) => ${n.pascal}.findOne(id),
|
|
24
|
+
{ params: t.Object({ id: t.String() }) },
|
|
25
|
+
)
|
|
26
|
+
.post(
|
|
27
|
+
"/",
|
|
28
|
+
({ body }) => ${n.pascal}.create(body),
|
|
29
|
+
{
|
|
30
|
+
body: ${n.pascal}Model.create,
|
|
31
|
+
// response optionnelle : contractualise le type de retour
|
|
32
|
+
response: { 200: ${n.pascal}Model.entity },
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
.put(
|
|
36
|
+
"/:id",
|
|
37
|
+
({ params: { id }, body }) => ${n.pascal}.update(id, body),
|
|
38
|
+
{ params: t.Object({ id: t.String() }), body: ${n.pascal}Model.update },
|
|
39
|
+
)
|
|
40
|
+
.delete(
|
|
41
|
+
"/:id",
|
|
42
|
+
({ params: { id } }) => ${n.pascal}.remove(id),
|
|
43
|
+
{ params: t.Object({ id: t.String() }) },
|
|
44
|
+
);
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Service = classe abstraite avec méthodes statiques (logique métier). */
|
|
49
|
+
export function serviceTemplate(n: NameSet): string {
|
|
50
|
+
return `import type { ${n.pascal}Model } from "./${n.kebab}.model";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Service ${n.pascal} — logique métier, indépendante d'une requête.
|
|
54
|
+
* Classe abstraite + méthodes statiques (bonne pratique Elysia pour un
|
|
55
|
+
* service sans état). Remplace le stub in-memory par ta couche data.
|
|
56
|
+
*/
|
|
57
|
+
export abstract class ${n.pascal} {
|
|
58
|
+
static async findAll(): Promise<${n.pascal}Model["entity"][]> {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async findOne(id: string) {
|
|
63
|
+
return { id, name: "" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static async create(body: ${n.pascal}Model["create"]) {
|
|
67
|
+
return { id: crypto.randomUUID(), ...body };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static async update(id: string, body: ${n.pascal}Model["update"]) {
|
|
71
|
+
return { id, ...body };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static async remove(id: string) {
|
|
75
|
+
return { id, deleted: true };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Model = objet de schémas t.* + type TS dérivé (source unique de vérité). */
|
|
82
|
+
export function modelTemplate(n: NameSet): string {
|
|
83
|
+
return `// Model define the data structure and validation for the request and response
|
|
84
|
+
import { t, type UnwrapSchema } from "elysia";
|
|
85
|
+
|
|
86
|
+
export const ${n.pascal}Model = {
|
|
87
|
+
entity: t.Object({
|
|
88
|
+
id: t.String(),
|
|
89
|
+
name: t.String(),
|
|
90
|
+
}),
|
|
91
|
+
create: t.Object({
|
|
92
|
+
name: t.String({ minLength: 1 }),
|
|
93
|
+
}),
|
|
94
|
+
update: t.Partial(
|
|
95
|
+
t.Object({
|
|
96
|
+
name: t.String({ minLength: 1 }),
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
// Optional, cast all model to TypeScript type
|
|
102
|
+
export type ${n.pascal}Model = {
|
|
103
|
+
[K in keyof typeof ${n.pascal}Model]: UnwrapSchema<(typeof ${n.pascal}Model)[K]>;
|
|
104
|
+
};
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Barrel export du module. */
|
|
109
|
+
export function barrelTemplate(n: NameSet): string {
|
|
110
|
+
return `export * from "./${n.kebab}.controller";
|
|
111
|
+
export * from "./${n.kebab}.service";
|
|
112
|
+
export * from "./${n.kebab}.model";
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Macro générique (`src/macros/<name>.macro.ts`). */
|
|
117
|
+
export function macroTemplate(n: NameSet): string {
|
|
118
|
+
return `import { Elysia } from "elysia";
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Macro ${n.camel} — activable par route via \`{ ${n.camel}: true }\`.
|
|
122
|
+
* \`resolve\` peut retourner des valeurs injectées dans le contexte des routes.
|
|
123
|
+
*/
|
|
124
|
+
export const ${n.camel}Macro = new Elysia({ name: "${n.kebab}.macro" }).macro({
|
|
125
|
+
${n.camel}: {
|
|
126
|
+
resolve() {
|
|
127
|
+
// TODO: logique de la macro (auth, permissions, contexte…)
|
|
128
|
+
return {};
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Macro `auth` liée à better-auth (`src/macros/auth.macro.ts`).
|
|
137
|
+
* Monte le handler et expose `user`/`session` aux routes protégées.
|
|
138
|
+
*/
|
|
139
|
+
export function authMacroTemplate(): string {
|
|
140
|
+
return `import { Elysia } from "elysia";
|
|
141
|
+
import { auth } from "../lib/auth";
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Plugin better-auth : monte le handler ET expose la macro \`auth\`.
|
|
145
|
+
* Usage : \`.get("/me", ({ user }) => user, { auth: true })\`
|
|
146
|
+
*/
|
|
147
|
+
export const authMacro = new Elysia({ name: "better-auth" })
|
|
148
|
+
.mount(auth.handler)
|
|
149
|
+
.macro({
|
|
150
|
+
auth: {
|
|
151
|
+
async resolve({ status, request: { headers } }) {
|
|
152
|
+
const session = await auth.api.getSession({ headers });
|
|
153
|
+
if (!session) return status(401);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
user: session.user,
|
|
157
|
+
session: session.session,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Module racine (`src/modules/index.ts`) — point d'agrégation des modules.
|
|
167
|
+
* `generate module` y branche chaque controller. Accueille éventuellement la
|
|
168
|
+
* route par défaut déplacée depuis l'app entry.
|
|
169
|
+
*/
|
|
170
|
+
export function modulesIndexTemplate(rootHandler?: string): string {
|
|
171
|
+
const route = rootHandler ? `\n .get("/", ${rootHandler})` : "";
|
|
172
|
+
return `import { Elysia } from "elysia";
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Agrégat des modules de l'application. Les controllers sont branchés ici via
|
|
176
|
+
* \`.use(...)\` par \`generate module\`.
|
|
177
|
+
*/
|
|
178
|
+
export const modules = new Elysia({ name: "modules" })${route};
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Hooks de logging des requêtes, injectés directement dans la chaîne de l'app
|
|
184
|
+
* entry (après `new Elysia()`). Nécessite l'import de `Logger`.
|
|
185
|
+
*/
|
|
186
|
+
export function loggerHooksSegment(): string {
|
|
187
|
+
return `.onBeforeHandle((ctx) => {
|
|
188
|
+
(ctx as any)._startTime = Date.now();
|
|
189
|
+
})
|
|
190
|
+
.onAfterResponse(({ request, set, server, ...ctx }) => {
|
|
191
|
+
const status = set.status;
|
|
192
|
+
const ip = server?.requestIP(request)?.address;
|
|
193
|
+
const duration = Date.now() - ((ctx as any)._startTime ?? Date.now());
|
|
194
|
+
Logger.app.request(request.method, request.url, status, ip, \`\${duration}ms\`);
|
|
195
|
+
})`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Logger applicatif (chalk) — écrit dans le projet cible par `add logger`.
|
|
200
|
+
* Nécessite \`chalk\` et \`elysia\` (déjà présent).
|
|
201
|
+
*/
|
|
202
|
+
export function loggerTemplate(): string {
|
|
203
|
+
return `import chalk from "chalk";
|
|
204
|
+
import { StatusMap } from "elysia";
|
|
205
|
+
|
|
206
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
|
207
|
+
|
|
208
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
209
|
+
debug: 0,
|
|
210
|
+
info: 1,
|
|
211
|
+
warn: 2,
|
|
212
|
+
error: 3,
|
|
213
|
+
silent: 4,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const currentLevel: LogLevel =
|
|
217
|
+
(process.env.LOG_LEVEL as LogLevel) ??
|
|
218
|
+
(process.env.NODE_ENV === "production" ? "info" : "debug");
|
|
219
|
+
|
|
220
|
+
function shouldLog(level: LogLevel): boolean {
|
|
221
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function messageParser(message: unknown): string {
|
|
225
|
+
if (typeof message !== "string") return JSON.stringify(message);
|
|
226
|
+
return message;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatMeta(meta?: object): string {
|
|
230
|
+
if (!meta) return "";
|
|
231
|
+
return " " + chalk.dim(JSON.stringify(meta));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatTime(): string {
|
|
235
|
+
const d = new Date();
|
|
236
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
237
|
+
return \`\${pad(d.getHours())}:\${pad(d.getMinutes())}:\${pad(d.getSeconds())}\`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const METHOD_COLORS: Record<string, (s: string) => string> = {
|
|
241
|
+
GET: chalk.green,
|
|
242
|
+
POST: chalk.blue,
|
|
243
|
+
PUT: chalk.yellow,
|
|
244
|
+
PATCH: chalk.magenta,
|
|
245
|
+
DELETE: chalk.red,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function getMethodColor(method: string): (s: string) => string {
|
|
249
|
+
return METHOD_COLORS[method] ?? chalk.white;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getStatusColor(status: number | keyof StatusMap): (s: string) => string {
|
|
253
|
+
const numericStatus = typeof status === "number" ? status : StatusMap[status];
|
|
254
|
+
if (numericStatus < 300) return chalk.green;
|
|
255
|
+
if (numericStatus < 400) return chalk.cyan;
|
|
256
|
+
if (numericStatus < 500) return chalk.yellow;
|
|
257
|
+
return chalk.red;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createAppLogger(serviceName: string) {
|
|
261
|
+
const time = () => chalk.dim.yellow.bold.underline(formatTime());
|
|
262
|
+
const service = (c: (s: string) => string) => c(\`[\${serviceName}]\`);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
debug: (message: unknown, meta?: object) => {
|
|
266
|
+
if (!shouldLog("debug")) return;
|
|
267
|
+
console.log(
|
|
268
|
+
\`[\${chalk.cyan("DBG")}] \${time()} \${service(chalk.cyan)} \${messageParser(message)}\${formatMeta(meta)}\`,
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
info: (message: unknown, meta?: object) => {
|
|
272
|
+
if (!shouldLog("info")) return;
|
|
273
|
+
console.log(
|
|
274
|
+
\`[\${chalk.green("INFO")}] \${time()} \${service(chalk.green)} \${messageParser(message)}\${formatMeta(meta)}\`,
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
warn: (message: unknown, meta?: object) => {
|
|
278
|
+
if (!shouldLog("warn")) return;
|
|
279
|
+
console.log(
|
|
280
|
+
\`[\${chalk.yellow("WARN")}] \${time()} \${service(chalk.yellow)} \${messageParser(message)}\${formatMeta(meta)}\`,
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
error: (message: unknown, meta?: object) => {
|
|
284
|
+
if (!shouldLog("error")) return;
|
|
285
|
+
console.log(
|
|
286
|
+
\`[\${chalk.red("ERR")}] \${time()} \${service(chalk.red)} \${messageParser(message)}\${formatMeta(meta)}\`,
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
request: (
|
|
290
|
+
method: string,
|
|
291
|
+
url: string,
|
|
292
|
+
status?: number | keyof StatusMap,
|
|
293
|
+
ip: string = "",
|
|
294
|
+
duration?: string,
|
|
295
|
+
meta?: object,
|
|
296
|
+
) => {
|
|
297
|
+
if (!shouldLog("info")) return;
|
|
298
|
+
const methodColor = getMethodColor(method);
|
|
299
|
+
const statusStr = status
|
|
300
|
+
? \` \${getStatusColor(status)(String(status))}\`
|
|
301
|
+
: "";
|
|
302
|
+
const durationStr = duration ? \` \${chalk.cyan(duration)}\` : "";
|
|
303
|
+
console.log(
|
|
304
|
+
\`[\${chalk.magenta("REQ")}] \${time()} \${service(chalk.magenta)} \${methodColor(method)} \${url}\${statusStr}\${durationStr} \${ip} \${formatMeta(meta)}\`,
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const Logger = {
|
|
311
|
+
app: createAppLogger("App"),
|
|
312
|
+
auth: createAppLogger("Auth"),
|
|
313
|
+
travel: createAppLogger("Travel"),
|
|
314
|
+
db: createAppLogger("Database"),
|
|
315
|
+
upload: createAppLogger("Upload"),
|
|
316
|
+
of: createAppLogger,
|
|
317
|
+
};
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Config better-auth (`src/lib/auth.ts`). Avec Prisma, l'adapter est branché ;
|
|
323
|
+
* sinon il reste en commentaire (à activer après `add prisma`).
|
|
324
|
+
*/
|
|
325
|
+
export function authLibTemplate(
|
|
326
|
+
withPrisma: boolean,
|
|
327
|
+
prismaProvider = "postgresql",
|
|
328
|
+
): string {
|
|
329
|
+
const header = withPrisma
|
|
330
|
+
? `import { betterAuth } from "better-auth";
|
|
331
|
+
import { openAPI } from "better-auth/plugins";
|
|
332
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
333
|
+
import prisma from "../db";`
|
|
334
|
+
: `import { betterAuth } from "better-auth";
|
|
335
|
+
import { openAPI } from "better-auth/plugins";
|
|
336
|
+
// import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
337
|
+
// import prisma from "../db";`;
|
|
338
|
+
|
|
339
|
+
const database = withPrisma
|
|
340
|
+
? ` database: prismaAdapter(prisma, { provider: "${prismaProvider}" }),`
|
|
341
|
+
: ` // database: prismaAdapter(prisma, { provider: "${prismaProvider}" }),`;
|
|
342
|
+
|
|
343
|
+
return `${header}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Instance better-auth. Handler monté via \`.mount(auth.handler)\` → routes sur
|
|
347
|
+
* /api/auth. Le plugin openAPI() expose le schéma consommé par @elysiajs/openapi.
|
|
348
|
+
*/
|
|
349
|
+
export const auth = betterAuth({
|
|
350
|
+
${database}
|
|
351
|
+
emailAndPassword: { enabled: true },
|
|
352
|
+
plugins: [openAPI()],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Extraction du schéma OpenAPI de better-auth pour @elysiajs/openapi.
|
|
356
|
+
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>;
|
|
357
|
+
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema());
|
|
358
|
+
|
|
359
|
+
export const OpenAPI = {
|
|
360
|
+
getPaths: (prefix = "/api/auth") =>
|
|
361
|
+
getSchema().then(({ paths }) => {
|
|
362
|
+
const reference: typeof paths = Object.create(null);
|
|
363
|
+
for (const path of Object.keys(paths)) {
|
|
364
|
+
const key = prefix + path;
|
|
365
|
+
reference[key] = paths[path];
|
|
366
|
+
for (const method of Object.keys(paths[path])) {
|
|
367
|
+
const operation = (reference[key] as any)[method];
|
|
368
|
+
operation.tags = ["Better Auth"];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return reference;
|
|
372
|
+
}) as Promise<any>,
|
|
373
|
+
components: getSchema().then(({ components }) => components) as Promise<any>,
|
|
374
|
+
} as const;
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Client Prisma singleton (`src/db/index.ts`).
|
|
380
|
+
* Prisma 7 requiert un driver adapter, dépendant du provider choisi.
|
|
381
|
+
*/
|
|
382
|
+
export function prismaClientTemplate(adapter: {
|
|
383
|
+
adapterImport: string;
|
|
384
|
+
adapterPkg: string;
|
|
385
|
+
adapterExpr: string;
|
|
386
|
+
}): string {
|
|
387
|
+
return `import { ${adapter.adapterImport} } from "${adapter.adapterPkg}";
|
|
388
|
+
// Client généré vers un output custom (Prisma 7) — import depuis ce dossier,
|
|
389
|
+
// pas depuis "@prisma/client".
|
|
390
|
+
import { PrismaClient } from "../generated/prisma/client";
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Singleton PrismaClient — évite d'ouvrir plusieurs pools de connexions
|
|
394
|
+
* (notamment en dev avec le hot-reload). Prisma 7 : la connexion passe par
|
|
395
|
+
* un adapter, l'URL est lue depuis DATABASE_URL (.env.local).
|
|
396
|
+
*/
|
|
397
|
+
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
|
398
|
+
|
|
399
|
+
const adapter = ${adapter.adapterExpr};
|
|
400
|
+
|
|
401
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
|
|
402
|
+
|
|
403
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
404
|
+
|
|
405
|
+
export default prisma;
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Schéma Prisma (`prisma/schema.prisma`). Prisma 7 : plus d'\`url\` dans le
|
|
411
|
+
* datasource (déplacée dans prisma.config.ts). Génère aussi les modèles de
|
|
412
|
+
* validation Elysia via prismabox.
|
|
413
|
+
*/
|
|
414
|
+
export function prismaSchemaTemplate(prismaProvider = "postgresql"): string {
|
|
415
|
+
return `generator client {
|
|
416
|
+
provider = "prisma-client"
|
|
417
|
+
output = "../src/generated/prisma"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
generator prismabox {
|
|
421
|
+
provider = "prismabox"
|
|
422
|
+
typeboxImportDependencyName = "elysia"
|
|
423
|
+
typeboxImportVariableName = "t"
|
|
424
|
+
inputModel = true
|
|
425
|
+
output = "../src/generated/prismabox"
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
datasource db {
|
|
429
|
+
provider = "${prismaProvider}"
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Config Prisma (`prisma.config.ts`). Prisma 7 : l'URL de connexion (utilisée
|
|
436
|
+
* par Migrate) vit ici, chargée depuis .env.local.
|
|
437
|
+
*/
|
|
438
|
+
export function prismaConfigTemplate(): string {
|
|
439
|
+
return `import { config } from "dotenv";
|
|
440
|
+
import { defineConfig, env } from "prisma/config";
|
|
441
|
+
|
|
442
|
+
// Prisma 7 charge .env par défaut ; on pointe explicitement sur .env.local.
|
|
443
|
+
config({ path: ".env.local" });
|
|
444
|
+
|
|
445
|
+
export default defineConfig({
|
|
446
|
+
schema: "./prisma/schema.prisma",
|
|
447
|
+
datasource: {
|
|
448
|
+
url: env("DATABASE_URL"),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
`;
|
|
452
|
+
}
|
package/src/utils/db.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface DbCredentials {
|
|
5
|
+
host: string;
|
|
6
|
+
port: string;
|
|
7
|
+
user: string;
|
|
8
|
+
password: string;
|
|
9
|
+
database: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Description d'un provider de base pour Prisma 7 (driver adapter requis). */
|
|
13
|
+
export interface DbProvider {
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
/** Valeur du datasource `provider` dans schema.prisma. */
|
|
17
|
+
prismaProvider: string;
|
|
18
|
+
/** Package de l'adapter Prisma. */
|
|
19
|
+
adapterPkg: string;
|
|
20
|
+
/** Nom de la classe adapter à importer. */
|
|
21
|
+
adapterImport: string;
|
|
22
|
+
/** Expression d'instanciation de l'adapter. */
|
|
23
|
+
adapterExpr: string;
|
|
24
|
+
/** Driver(s) natif(s) à installer. */
|
|
25
|
+
driverPkgs: string[];
|
|
26
|
+
/** Base fichier (SQLite) : pas de host/port/credentials. */
|
|
27
|
+
isFile: boolean;
|
|
28
|
+
/** Port par défaut (bases serveur). */
|
|
29
|
+
defaultPort?: string;
|
|
30
|
+
/** URL par défaut (SQLite : chemin du fichier). */
|
|
31
|
+
defaultUrl: string;
|
|
32
|
+
/** Construit l'URL de connexion à partir des composants (bases serveur). */
|
|
33
|
+
buildUrl?: (c: DbCredentials) => string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DB_PROVIDERS: Record<string, DbProvider> = {
|
|
37
|
+
postgresql: {
|
|
38
|
+
key: "postgresql",
|
|
39
|
+
label: "PostgreSQL",
|
|
40
|
+
prismaProvider: "postgresql",
|
|
41
|
+
adapterPkg: "@prisma/adapter-pg",
|
|
42
|
+
adapterImport: "PrismaPg",
|
|
43
|
+
adapterExpr: "new PrismaPg({ connectionString: process.env.DATABASE_URL! })",
|
|
44
|
+
driverPkgs: ["pg"],
|
|
45
|
+
isFile: false,
|
|
46
|
+
defaultPort: "5432",
|
|
47
|
+
defaultUrl: "postgresql://user:password@localhost:5432/mydb?schema=public",
|
|
48
|
+
buildUrl: (c) =>
|
|
49
|
+
`postgresql://${c.user}:${c.password}@${c.host}:${c.port}/${c.database}?schema=public`,
|
|
50
|
+
},
|
|
51
|
+
mysql: {
|
|
52
|
+
key: "mysql",
|
|
53
|
+
label: "MySQL / MariaDB",
|
|
54
|
+
prismaProvider: "mysql",
|
|
55
|
+
adapterPkg: "@prisma/adapter-mariadb",
|
|
56
|
+
adapterImport: "PrismaMariaDb",
|
|
57
|
+
// Le driver mariadb accepte une URI de connexion.
|
|
58
|
+
adapterExpr: "new PrismaMariaDb(process.env.DATABASE_URL!)",
|
|
59
|
+
driverPkgs: ["mariadb"],
|
|
60
|
+
isFile: false,
|
|
61
|
+
defaultPort: "3306",
|
|
62
|
+
defaultUrl: "mysql://user:password@localhost:3306/mydb",
|
|
63
|
+
buildUrl: (c) =>
|
|
64
|
+
`mysql://${c.user}:${c.password}@${c.host}:${c.port}/${c.database}`,
|
|
65
|
+
},
|
|
66
|
+
sqlite: {
|
|
67
|
+
key: "sqlite",
|
|
68
|
+
label: "SQLite",
|
|
69
|
+
prismaProvider: "sqlite",
|
|
70
|
+
adapterPkg: "@prisma/adapter-better-sqlite3",
|
|
71
|
+
adapterImport: "PrismaBetterSqlite3",
|
|
72
|
+
adapterExpr:
|
|
73
|
+
'new PrismaBetterSqlite3({ url: process.env.DATABASE_URL ?? "file:./dev.db" })',
|
|
74
|
+
driverPkgs: ["better-sqlite3"],
|
|
75
|
+
isFile: true,
|
|
76
|
+
defaultUrl: "file:./dev.db",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const DB_PROVIDER_KEYS = Object.keys(DB_PROVIDERS);
|
|
81
|
+
|
|
82
|
+
/** Lit le provider déclaré dans prisma/schema.prisma, si présent. */
|
|
83
|
+
export function detectPrismaProvider(): string | null {
|
|
84
|
+
const schema = join(process.cwd(), "prisma", "schema.prisma");
|
|
85
|
+
if (!existsSync(schema)) return null;
|
|
86
|
+
const match = readFileSync(schema, "utf8").match(
|
|
87
|
+
/datasource\s+\w+\s*\{[^}]*provider\s*=\s*"([^"]+)"/,
|
|
88
|
+
);
|
|
89
|
+
return match?.[1] ?? null;
|
|
90
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { log } from "./logger.js";
|
|
3
|
+
import { ensureGitignore } from "./gitignore.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Garantit la présence de variables dans un fichier d'env (`.env.local` par
|
|
7
|
+
* défaut) sans écraser les valeurs existantes. Crée le fichier au besoin et
|
|
8
|
+
* s'assure qu'il est git-ignoré (les secrets ne doivent pas être committés).
|
|
9
|
+
*/
|
|
10
|
+
export function ensureEnvVars(
|
|
11
|
+
vars: Record<string, string>,
|
|
12
|
+
file = ".env.local",
|
|
13
|
+
): void {
|
|
14
|
+
// Toujours ignorer le fichier d'env, même si aucune variable n'est ajoutée.
|
|
15
|
+
ensureGitignore([file]);
|
|
16
|
+
|
|
17
|
+
const content = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
18
|
+
const missing: string[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
21
|
+
if (!new RegExp(`^${key}=`, "m").test(content)) {
|
|
22
|
+
missing.push(`${key}=${value}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!missing.length) return;
|
|
26
|
+
|
|
27
|
+
const prefix = content && !content.endsWith("\n") ? "\n" : "";
|
|
28
|
+
appendFileSync(file, prefix + missing.join("\n") + "\n");
|
|
29
|
+
log.updated(`${file} (${missing.length} variable(s))`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync, readFileSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Garantit que des entrées figurent dans le .gitignore du projet courant.
|
|
7
|
+
* Évite notamment de committer les secrets (.env.local).
|
|
8
|
+
*/
|
|
9
|
+
export function ensureGitignore(entries: string[]): void {
|
|
10
|
+
const path = join(process.cwd(), ".gitignore");
|
|
11
|
+
const content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
12
|
+
const present = new Set(content.split(/\r?\n/).map((l) => l.trim()));
|
|
13
|
+
|
|
14
|
+
const missing = entries.filter((e) => !present.has(e));
|
|
15
|
+
if (!missing.length) return;
|
|
16
|
+
|
|
17
|
+
const prefix = content && !content.endsWith("\n") ? "\n" : "";
|
|
18
|
+
appendFileSync(path, prefix + missing.join("\n") + "\n");
|
|
19
|
+
log.updated(`.gitignore (${missing.join(", ")})`);
|
|
20
|
+
}
|