@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Segu27
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# @apc-projects/elysia-cli
|
|
2
|
+
|
|
3
|
+
CLI de scaffolding pour projets **ElysiaJS**. Génère des modules (controller + service + model + plugin) suivant les bonnes pratiques Elysia, et **branche automatiquement** le plugin dans ton application via manipulation d'AST (ts-morph).
|
|
4
|
+
|
|
5
|
+
## Prérequis
|
|
6
|
+
|
|
7
|
+
Ce CLI est **Bun-only** : le binaire s'exécute avec Bun (le `bin` pointe sur du TypeScript). Node/`npx` n'est pas supporté.
|
|
8
|
+
|
|
9
|
+
Installe Bun si ce n'est pas déjà fait :
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# macOS / Linux
|
|
13
|
+
curl -fsSL https://bun.sh/install | bash
|
|
14
|
+
|
|
15
|
+
# Windows (PowerShell)
|
|
16
|
+
powershell -c "irm bun.sh/install.ps1 | iex"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Vérifie l'installation :
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun --version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Stack
|
|
26
|
+
|
|
27
|
+
- **Bun** — runtime (exécute le TypeScript directement, pas de build en dev)
|
|
28
|
+
- **commander** — parsing des commandes
|
|
29
|
+
- **@clack/prompts** — prompts interactifs
|
|
30
|
+
- **chalk** — sortie colorée
|
|
31
|
+
- **ts-morph** — lecture/écriture de l'AST TypeScript (injection du `.use()`)
|
|
32
|
+
|
|
33
|
+
## Installation (dev)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bun install
|
|
37
|
+
bun run dev -- --help # exécute src/index.ts directement (aucun build)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Pour l'utiliser comme binaire global pendant le dev :
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bun link # dans ce package
|
|
44
|
+
cd ../mon-api-test
|
|
45
|
+
bun link @apc-projects/elysia-cli # dans le projet cible
|
|
46
|
+
elysia-cli --help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Le `bin` pointe sur `src/index.ts` (shebang `#!/usr/bin/env bun`) : Bun lit le TS, donc rien à compiler pour l'installer. Un `bun install` du package suffit.
|
|
50
|
+
|
|
51
|
+
## Build standalone (distribution)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bun run build # bun build --compile -> dist/elysia-cli
|
|
55
|
+
./dist/elysia-cli --help # exécutable autonome, sans Bun ni Node
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Utilisation sans cloner le dépôt
|
|
59
|
+
|
|
60
|
+
### Via npm (recommandé)
|
|
61
|
+
|
|
62
|
+
Après publication sur npm (`bun publish` / `npm publish` — le package est `@apc-projects/elysia-cli`), on peut l'exécuter sans installation :
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bunx @apc-projects/elysia-cli init mon-api
|
|
66
|
+
bunx @apc-projects/elysia-cli add prisma
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`bunx` résout le **nom du package** (pas le nom du binaire), d'où `@apc-projects/elysia-cli`. Les dépendances sont installées automatiquement au premier run puis mises en cache.
|
|
70
|
+
|
|
71
|
+
### Via GitHub (sans publier sur npm)
|
|
72
|
+
|
|
73
|
+
Bun sait installer depuis un dépôt Git. En dépendance de projet :
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun add github:<owner>/<repo> # ajoute @apc-projects/elysia-cli au projet
|
|
77
|
+
bunx elysia-cli init mon-api # le binaire local est alors résolu
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Ou en global :
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bun add -g github:<owner>/<repo>
|
|
84
|
+
elysia-cli init mon-api
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Comme le `bin` pointe sur `src/index.ts` (shebang `#!/usr/bin/env bun`), aucune étape de build n'est requise à l'installation — Bun exécute le TypeScript directement. Assure-toi que `files` inclut `src` (c'est le cas) pour que la source soit bien livrée.
|
|
88
|
+
|
|
89
|
+
> Note : `bunx github:<owner>/<repo>` en une commande n'est pas garanti — passe par `bun add` (projet ou global) pour la voie GitHub. Pour un `bunx <nom>` direct, publie sur npm.
|
|
90
|
+
|
|
91
|
+
## Commandes
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
elysia-cli generate module <name> # alias: g module
|
|
95
|
+
elysia-cli generate service <name>
|
|
96
|
+
elysia-cli generate model <name>
|
|
97
|
+
elysia-cli macro <name> # génère src/macros/<name>.macro.ts + .use()
|
|
98
|
+
elysia-cli add <plugin> # cors | openapi | jwt | logger | better-auth | prisma
|
|
99
|
+
elysia-cli init <project-name> # `bun create elysia` + couche elysia-cli
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`add` installe automatiquement les dépendances (`bun add`, désactivable avec `--no-install`) :
|
|
103
|
+
|
|
104
|
+
- `cors`, `jwt` — branchés par `.use(...)` dans l'app entry.
|
|
105
|
+
- `openapi` — branché **uniquement hors production** (`NODE_ENV !== "production"`). Si **better-auth** est présent, le schéma OpenAPI de better-auth est intégré à la doc (`documentation.components` / `documentation.paths`). L'intégration marche dans les deux sens : si tu ajoutes `openapi` puis `better-auth`, ce dernier **re-patche** l'appel `openapi()` existant pour y injecter la doc auth.
|
|
106
|
+
- `logger` — scaffolde le logger applicatif (chalk) dans `src/utils/logger.ts`, injecte les hooks `onBeforeHandle`/`onAfterResponse` **directement dans la chaîne** de `src/index.ts`, et remplace les `console.*` par le `Logger`.
|
|
107
|
+
- `prisma` — Prisma 7. **Demande le provider** (PostgreSQL / MySQL-MariaDB / SQLite) et l'**URL de connexion**, puis scaffolde le client singleton avec le bon driver adapter (`src/db/index.ts`), `prisma/schema.prisma` (provider choisi, sans `url`, + générateur **prismabox** pour les modèles de validation Elysia), `prisma.config.ts` (URL pour Migrate), et ajoute `DATABASE_URL` à `.env.local`. Le client est généré vers un **output custom** (`src/generated/prisma`, Prisma 7) et importé depuis `../generated/prisma/client` dans `src/db/index.ts` — jamais depuis `@prisma/client`. Le dossier `src/generated` est git-ignoré (régénéré via `prebuild`/post-install). `prisma generate` est lancé après install. Ajoute aussi au `package.json` (sans écraser l'existant) les scripts `build`/`start` s'ils manquent, plus `prebuild` (`prisma generate`) et `prestart` (`prisma migrate deploy`). Les paquets (`@prisma/adapter-*`, driver natif) dépendent du provider.
|
|
108
|
+
- `better-auth` — scaffolde `src/lib/auth.ts` **et une macro `auth`** (`src/macros/auth.macro.ts`) qui monte le handler (routes sur `/api/auth`) et expose `user`/`session` aux routes protégées via `{ auth: true }` ; branché par `.use(authMacro)`. **Demande `BETTER_AUTH_URL`** et ajoute `BETTER_AUTH_SECRET` (généré) + `BETTER_AUTH_URL` à `.env.local`. **Si Prisma est présent** (ou ajouté en même temps), l'adapter Prisma est câblé avec le provider détecté depuis le schéma, et les migrations auth sont générées puis appliquées (`@better-auth/cli generate` + `prisma migrate`).
|
|
109
|
+
|
|
110
|
+
`generate module` branche chaque controller dans l'agrégateur `src/modules/index.ts` (créé au besoin, avec `.use(modules)` dans l'app entry), et non plus directement dans `src/index.ts`.
|
|
111
|
+
|
|
112
|
+
`init` déplace la route racine par défaut dans `src/modules/index.ts` et propose de sélectionner des plugins à ajouter tout de suite (install auto).
|
|
113
|
+
|
|
114
|
+
Exemple :
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
elysia-cli g module user
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Génère :
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
src/modules/user/
|
|
124
|
+
user.controller.ts # instance Elysia `user` (routes, body/response = UserModel)
|
|
125
|
+
user.service.ts # classe abstraite `User` (logique métier)
|
|
126
|
+
user.model.ts # objet de schémas `UserModel` (t.*) + type TS dérivé
|
|
127
|
+
index.ts # barrel export
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Le controller s'exporte sous le nom du module (`export const user = …`), le service est une classe `User`, et le model est un objet de schémas (`UserModel.create`, `UserModel.entity`, `UserModel.update`) avec un type TypeScript dérivé — comme dans la doc Elysia. Le controller est ensuite branché dans l'agrégateur `src/modules/index.ts` via `.use(user)`.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
Optionnel — `elysia-cli.config.json` à la racine du projet cible :
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"srcDir": "src",
|
|
139
|
+
"modulesDir": "src/modules",
|
|
140
|
+
"appEntry": "src/index.ts",
|
|
141
|
+
"appVariable": "app"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Structure du package
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
src/
|
|
149
|
+
index.ts # entrée commander
|
|
150
|
+
commands/
|
|
151
|
+
generate.ts # generate module/service/model
|
|
152
|
+
add.ts # add <plugin>
|
|
153
|
+
init.ts # init <project>
|
|
154
|
+
utils/
|
|
155
|
+
logger.ts # wrappers chalk + clack
|
|
156
|
+
naming.ts # helpers de casse (pascal, camel, kebab)
|
|
157
|
+
project.ts # chargement du Project ts-morph + config
|
|
158
|
+
inject.ts # injection du .use() dans l'app
|
|
159
|
+
templates.ts # fonctions de génération de contenu
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
> Note : `tsup.config.ts` n'est plus utilisé (build géré par `bun build --compile`) et peut être supprimé.
|
|
163
|
+
|
|
164
|
+
## Étendre
|
|
165
|
+
|
|
166
|
+
Ajoute un générateur : nouvelle fonction dans `templates.ts` + branche-la dans `commands/generate.ts`. Pour un nouveau type d'injection, réutilise `utils/inject.ts` (basé sur ts-morph, donc robuste aux reformats).
|
|
167
|
+
|
|
168
|
+
## Licence
|
|
169
|
+
|
|
170
|
+
[MIT](./LICENSE) © Segu27
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apc-projects/elysia-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Générateur de scaffolding pour projets ElysiaJS (modules, services, models, plugins).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Segu27",
|
|
8
|
+
"homepage": "https://github.com/Segu27/elysia-cli#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Segu27/elysia-cli.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["elysia", "elysiajs", "cli", "scaffolding", "bun", "generator"],
|
|
14
|
+
"bin": {
|
|
15
|
+
"elysia-cli": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "bun src/index.ts",
|
|
27
|
+
"start": "bun src/index.ts",
|
|
28
|
+
"build": "bun build src/index.ts --compile --outfile dist/elysia-cli",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": ">=1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@clack/prompts": "^0.7.0",
|
|
36
|
+
"chalk": "^5.6.2",
|
|
37
|
+
"commander": "^12.1.0",
|
|
38
|
+
"ts-morph": "^23.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"@types/bun": "^1.1.0",
|
|
43
|
+
"@types/node": "^20.19.43"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname, relative } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { loadConfig, loadTsProject, type ElysiaCliConfig } from "../utils/project.js";
|
|
7
|
+
import {
|
|
8
|
+
registerUse,
|
|
9
|
+
appendChain,
|
|
10
|
+
moduleSpecifierFrom,
|
|
11
|
+
replaceConsoleWithLogger,
|
|
12
|
+
patchOpenapiWithAuth,
|
|
13
|
+
} from "../utils/inject.js";
|
|
14
|
+
import { installDeps, run } from "../utils/pm.js";
|
|
15
|
+
import { ensureEnvVars } from "../utils/env.js";
|
|
16
|
+
import { ensureGitignore } from "../utils/gitignore.js";
|
|
17
|
+
import { ensureScripts } from "../utils/package-json.js";
|
|
18
|
+
import {
|
|
19
|
+
DB_PROVIDERS,
|
|
20
|
+
DB_PROVIDER_KEYS,
|
|
21
|
+
detectPrismaProvider,
|
|
22
|
+
type DbProvider,
|
|
23
|
+
} from "../utils/db.js";
|
|
24
|
+
import { log, CliError } from "../utils/logger.js";
|
|
25
|
+
import {
|
|
26
|
+
loggerTemplate,
|
|
27
|
+
loggerHooksSegment,
|
|
28
|
+
authLibTemplate,
|
|
29
|
+
authMacroTemplate,
|
|
30
|
+
prismaClientTemplate,
|
|
31
|
+
prismaSchemaTemplate,
|
|
32
|
+
prismaConfigTemplate,
|
|
33
|
+
} from "../templates.js";
|
|
34
|
+
|
|
35
|
+
type PostInstall = () => void;
|
|
36
|
+
|
|
37
|
+
interface PresetOutcome {
|
|
38
|
+
pkgs?: string[];
|
|
39
|
+
devPkgs?: string[];
|
|
40
|
+
postInstall?: PostInstall;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Preset {
|
|
44
|
+
/** Dépendances de base (toujours installées). */
|
|
45
|
+
pkgs?: string[];
|
|
46
|
+
devPkgs?: string[];
|
|
47
|
+
/** Scaffolding + injection. Async pour permettre des prompts. */
|
|
48
|
+
apply: (config: ElysiaCliConfig) => Promise<PresetOutcome> | PresetOutcome | void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ApplyResult {
|
|
52
|
+
pkgs: string[];
|
|
53
|
+
devPkgs: string[];
|
|
54
|
+
postInstall?: PostInstall;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Preset middleware simple : `.use(<expr>)` + import + dépendance. */
|
|
58
|
+
function usePreset(spec: {
|
|
59
|
+
expr: string;
|
|
60
|
+
importName: string;
|
|
61
|
+
importFrom: string;
|
|
62
|
+
}): Preset {
|
|
63
|
+
return {
|
|
64
|
+
pkgs: [spec.importFrom],
|
|
65
|
+
apply: (config) => {
|
|
66
|
+
const project = loadTsProject();
|
|
67
|
+
registerUse(
|
|
68
|
+
project,
|
|
69
|
+
{ entry: config.appEntry, variable: config.appVariable },
|
|
70
|
+
{
|
|
71
|
+
useExpr: spec.expr,
|
|
72
|
+
ensureImport: { names: [spec.importName], moduleSpecifier: spec.importFrom },
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Prisma déjà présent dans le projet ? (schéma ou client généré). */
|
|
80
|
+
function hasPrisma(config: ElysiaCliConfig): boolean {
|
|
81
|
+
return (
|
|
82
|
+
existsSync(join(process.cwd(), "prisma", "schema.prisma")) ||
|
|
83
|
+
existsSync(join(process.cwd(), config.srcDir, "db", "index.ts"))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function promptProvider(): Promise<string> {
|
|
88
|
+
const answer = await p.select({
|
|
89
|
+
message: "Provider de base de données ?",
|
|
90
|
+
options: DB_PROVIDER_KEYS.map((k) => ({
|
|
91
|
+
value: k,
|
|
92
|
+
label: DB_PROVIDERS[k].label,
|
|
93
|
+
})),
|
|
94
|
+
initialValue: "postgresql",
|
|
95
|
+
});
|
|
96
|
+
if (p.isCancel(answer)) throw new CliError("Annulé.");
|
|
97
|
+
return answer as string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function promptText(message: string, initial: string): Promise<string> {
|
|
101
|
+
const answer = await p.text({ message, placeholder: initial, defaultValue: initial });
|
|
102
|
+
if (p.isCancel(answer)) throw new CliError("Annulé.");
|
|
103
|
+
return (answer as string) || initial;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function promptPassword(message: string): Promise<string> {
|
|
107
|
+
const answer = await p.password({ message });
|
|
108
|
+
if (p.isCancel(answer)) throw new CliError("Annulé.");
|
|
109
|
+
return (answer as string) ?? "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Demande les composants de connexion et construit l'URL selon le provider. */
|
|
113
|
+
async function promptDatabaseUrl(provider: DbProvider): Promise<string> {
|
|
114
|
+
if (provider.isFile || !provider.buildUrl) {
|
|
115
|
+
return promptText("Chemin du fichier SQLite", provider.defaultUrl);
|
|
116
|
+
}
|
|
117
|
+
const host = await promptText("Host", "localhost");
|
|
118
|
+
const port = await promptText("Port", provider.defaultPort ?? "");
|
|
119
|
+
const user = await promptText("Utilisateur", "user");
|
|
120
|
+
const password = await promptPassword("Mot de passe");
|
|
121
|
+
const database = await promptText("Base de données", "mydb");
|
|
122
|
+
return provider.buildUrl({ host, port, user, password, database });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** better-auth déjà présent dans le projet ? */
|
|
126
|
+
function hasBetterAuth(config: ElysiaCliConfig): boolean {
|
|
127
|
+
return existsSync(join(process.cwd(), config.srcDir, "lib", "auth.ts"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// L'ordre compte : prisma avant better-auth, et better-auth avant openapi
|
|
131
|
+
// (pour intégrer le schéma auth dans la doc).
|
|
132
|
+
const PLUGINS: Record<string, Preset> = {
|
|
133
|
+
cors: usePreset({ expr: "cors()", importName: "cors", importFrom: "@elysiajs/cors" }),
|
|
134
|
+
jwt: usePreset({
|
|
135
|
+
expr: 'jwt({ name: "jwt", secret: process.env.JWT_SECRET! })',
|
|
136
|
+
importName: "jwt",
|
|
137
|
+
importFrom: "@elysiajs/jwt",
|
|
138
|
+
}),
|
|
139
|
+
logger: {
|
|
140
|
+
pkgs: ["chalk"],
|
|
141
|
+
apply: (config) => {
|
|
142
|
+
scaffoldFile(join(config.srcDir, "utils", "logger.ts"), loggerTemplate());
|
|
143
|
+
const project = loadTsProject();
|
|
144
|
+
const app = { entry: config.appEntry, variable: config.appVariable };
|
|
145
|
+
const loggerPath = join(config.srcDir, "utils", "logger");
|
|
146
|
+
appendChain(project, app, loggerHooksSegment(), {
|
|
147
|
+
dedupe: "Logger.app.request",
|
|
148
|
+
ensureImports: [
|
|
149
|
+
{
|
|
150
|
+
names: ["Logger"],
|
|
151
|
+
moduleSpecifier: moduleSpecifierFrom(config.appEntry, loggerPath),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
replaceConsoleWithLogger(project, app, loggerPath);
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
prisma: {
|
|
159
|
+
pkgs: ["@prisma/client"],
|
|
160
|
+
devPkgs: ["prisma", "prismabox", "dotenv"],
|
|
161
|
+
apply: async (config) => {
|
|
162
|
+
const provider = DB_PROVIDERS[await promptProvider()];
|
|
163
|
+
const url = await promptDatabaseUrl(provider);
|
|
164
|
+
|
|
165
|
+
scaffoldFile("prisma.config.ts", prismaConfigTemplate());
|
|
166
|
+
scaffoldFile(
|
|
167
|
+
join("prisma", "schema.prisma"),
|
|
168
|
+
prismaSchemaTemplate(provider.prismaProvider),
|
|
169
|
+
);
|
|
170
|
+
scaffoldFile(join(config.srcDir, "db", "index.ts"), prismaClientTemplate(provider));
|
|
171
|
+
ensureEnvVars({ DATABASE_URL: `"${url}"` });
|
|
172
|
+
// Le client Prisma est généré (rebuild via prebuild/postInstall) : on l'ignore.
|
|
173
|
+
ensureGitignore(["src/generated"]);
|
|
174
|
+
// Ajoute build/start s'ils manquent, puis les hooks pre* correspondants :
|
|
175
|
+
// prisma generate avant le build, prisma migrate deploy avant le start.
|
|
176
|
+
ensureScripts({
|
|
177
|
+
build: `bun build ${config.appEntry} --target bun --outdir dist`,
|
|
178
|
+
start: `bun ${config.appEntry}`,
|
|
179
|
+
prebuild: "prisma generate",
|
|
180
|
+
prestart: "prisma migrate deploy",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
pkgs: [provider.adapterPkg, ...provider.driverPkgs],
|
|
185
|
+
postInstall: () => {
|
|
186
|
+
run("prisma generate", "bunx", ["prisma", "generate"]);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"better-auth": {
|
|
192
|
+
pkgs: ["better-auth"],
|
|
193
|
+
apply: async (config) => {
|
|
194
|
+
const withPrisma = hasPrisma(config);
|
|
195
|
+
const provider = detectPrismaProvider() ?? "postgresql";
|
|
196
|
+
|
|
197
|
+
scaffoldFile(
|
|
198
|
+
join(config.srcDir, "lib", "auth.ts"),
|
|
199
|
+
authLibTemplate(withPrisma, provider),
|
|
200
|
+
);
|
|
201
|
+
// Macro `auth` : monte le handler + expose user/session aux routes.
|
|
202
|
+
scaffoldFile(
|
|
203
|
+
join(config.srcDir, "macros", "auth.macro.ts"),
|
|
204
|
+
authMacroTemplate(),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const project = loadTsProject();
|
|
208
|
+
registerUse(
|
|
209
|
+
project,
|
|
210
|
+
{ entry: config.appEntry, variable: config.appVariable },
|
|
211
|
+
{
|
|
212
|
+
useExpr: "authMacro",
|
|
213
|
+
ensureImport: {
|
|
214
|
+
names: ["authMacro"],
|
|
215
|
+
moduleSpecifier: moduleSpecifierFrom(
|
|
216
|
+
config.appEntry,
|
|
217
|
+
join(config.srcDir, "macros", "auth.macro"),
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Si un openapi a déjà été injecté, on le re-patche avec la doc auth.
|
|
224
|
+
patchOpenapiWithAuth(
|
|
225
|
+
project,
|
|
226
|
+
config.appEntry,
|
|
227
|
+
join(config.srcDir, "lib", "auth"),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const baseUrl = await promptText(
|
|
231
|
+
"URL de l'app (BETTER_AUTH_URL)",
|
|
232
|
+
"http://localhost:3000",
|
|
233
|
+
);
|
|
234
|
+
ensureEnvVars({
|
|
235
|
+
BETTER_AUTH_SECRET: `"${randomBytes(32).toString("hex")}"`,
|
|
236
|
+
BETTER_AUTH_URL: `"${baseUrl}"`,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
log.info("better-auth monté sur /api/auth.");
|
|
240
|
+
|
|
241
|
+
if (!withPrisma) return {};
|
|
242
|
+
return {
|
|
243
|
+
postInstall: () => {
|
|
244
|
+
// Le client Prisma (output custom) doit exister avant que
|
|
245
|
+
// `better-auth generate` ne lise src/db.
|
|
246
|
+
run("prisma generate", "bunx", ["prisma", "generate"]);
|
|
247
|
+
const generated = run("better-auth generate", "bunx", [
|
|
248
|
+
"@better-auth/cli",
|
|
249
|
+
"generate",
|
|
250
|
+
"--y",
|
|
251
|
+
]);
|
|
252
|
+
if (generated) {
|
|
253
|
+
run("prisma migrate", "bunx", [
|
|
254
|
+
"prisma",
|
|
255
|
+
"migrate",
|
|
256
|
+
"dev",
|
|
257
|
+
"--name",
|
|
258
|
+
"add-auth",
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
openapi: {
|
|
266
|
+
pkgs: ["@elysiajs/openapi"],
|
|
267
|
+
apply: (config) => {
|
|
268
|
+
const project = loadTsProject();
|
|
269
|
+
const app = { entry: config.appEntry, variable: config.appVariable };
|
|
270
|
+
const withAuth = hasBetterAuth(config);
|
|
271
|
+
|
|
272
|
+
// Avec better-auth : intègre le schéma auth dans la doc OpenAPI.
|
|
273
|
+
const expr = withAuth
|
|
274
|
+
? "openapi({ enabled : process.env.NODE_ENV !== \"production\", documentation: { components: await OpenAPI.components, paths: await OpenAPI.getPaths() } })"
|
|
275
|
+
: "openapi({ enabled : process.env.NODE_ENV !== \"production\" })";
|
|
276
|
+
|
|
277
|
+
const ensureImports = [
|
|
278
|
+
{ names: ["openapi"], moduleSpecifier: "@elysiajs/openapi" },
|
|
279
|
+
];
|
|
280
|
+
if (withAuth) {
|
|
281
|
+
ensureImports.push({
|
|
282
|
+
names: ["OpenAPI"],
|
|
283
|
+
moduleSpecifier: moduleSpecifierFrom(
|
|
284
|
+
config.appEntry,
|
|
285
|
+
join(config.srcDir, "lib", "auth"),
|
|
286
|
+
),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
appendChain(project, app, `.use(${expr})`, {
|
|
291
|
+
dedupe: "openapi(",
|
|
292
|
+
ensureImports,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const PLUGIN_KEYS = Object.keys(PLUGINS);
|
|
299
|
+
|
|
300
|
+
/** Applique un preset. N'installe PAS (l'appelant regroupe deps + post-install). */
|
|
301
|
+
export async function applyPlugin(
|
|
302
|
+
key: string,
|
|
303
|
+
config: ElysiaCliConfig,
|
|
304
|
+
): Promise<ApplyResult> {
|
|
305
|
+
const preset = PLUGINS[key];
|
|
306
|
+
if (!preset) {
|
|
307
|
+
throw new CliError(
|
|
308
|
+
`Plugin inconnu "${key}". Disponibles : ${PLUGIN_KEYS.join(", ")}.`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const res: PresetOutcome = (await preset.apply(config)) ?? {};
|
|
312
|
+
return {
|
|
313
|
+
pkgs: [...(preset.pkgs ?? []), ...(res.pkgs ?? [])],
|
|
314
|
+
devPkgs: [...(preset.devPkgs ?? []), ...(res.devPkgs ?? [])],
|
|
315
|
+
postInstall: res.postInstall,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function scaffoldFile(relPath: string, content: string): void {
|
|
320
|
+
const target = join(process.cwd(), relPath);
|
|
321
|
+
if (existsSync(target)) {
|
|
322
|
+
log.skipped(relPath);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
326
|
+
writeFileSync(target, content, "utf8");
|
|
327
|
+
log.created(relative(process.cwd(), target));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function registerAddCommand(program: Command) {
|
|
331
|
+
program
|
|
332
|
+
.command("add")
|
|
333
|
+
.description(`Ajoute un plugin/preset (${PLUGIN_KEYS.join(", ")})`)
|
|
334
|
+
.argument("[plugin]", PLUGIN_KEYS.join(" | "))
|
|
335
|
+
.option("--no-install", "ne pas installer ni exécuter les post-install")
|
|
336
|
+
.action(async (plugin?: string, opts?: { install: boolean }) => {
|
|
337
|
+
let key = plugin;
|
|
338
|
+
if (!key) {
|
|
339
|
+
const answer = await p.select({
|
|
340
|
+
message: "Quel plugin ajouter ?",
|
|
341
|
+
options: PLUGIN_KEYS.map((v) => ({ value: v, label: v })),
|
|
342
|
+
});
|
|
343
|
+
if (p.isCancel(answer)) throw new CliError("Annulé.");
|
|
344
|
+
key = answer as string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const config = loadConfig();
|
|
348
|
+
const { pkgs, devPkgs, postInstall } = await applyPlugin(key, config);
|
|
349
|
+
|
|
350
|
+
if (opts?.install ?? true) {
|
|
351
|
+
installDeps(pkgs);
|
|
352
|
+
installDeps(devPkgs, { dev: true });
|
|
353
|
+
postInstall?.();
|
|
354
|
+
} else {
|
|
355
|
+
if (pkgs.length) log.info(`À installer : bun add ${pkgs.join(" ")}`);
|
|
356
|
+
if (devPkgs.length) log.info(`À installer : bun add -d ${devPkgs.join(" ")}`);
|
|
357
|
+
if (postInstall) log.info("Étapes post-install à lancer manuellement.");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
log.success(`Plugin ${key} ajouté.`);
|
|
361
|
+
});
|
|
362
|
+
}
|