@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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve, relative, dirname } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
SyntaxKind,
|
|
5
|
+
type Project,
|
|
6
|
+
type SourceFile,
|
|
7
|
+
type NewExpression,
|
|
8
|
+
type PropertyAccessExpression,
|
|
9
|
+
} from "ts-morph";
|
|
10
|
+
import { log } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
/** Cible d'injection : un fichier + le nom de la variable de l'instance Elysia. */
|
|
13
|
+
export interface InjectTarget {
|
|
14
|
+
entry: string;
|
|
15
|
+
variable: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ImportSpec {
|
|
19
|
+
names: string[];
|
|
20
|
+
moduleSpecifier: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Greffe un maillon de chaîne (`segment`, commençant par `.`) juste après
|
|
25
|
+
* `new Elysia(...)`. AST-based (ts-morph), robuste au formatage et idempotent
|
|
26
|
+
* via `dedupe` (sous-chaîne dont la présence signale un no-op).
|
|
27
|
+
*/
|
|
28
|
+
export function appendChain(
|
|
29
|
+
project: Project,
|
|
30
|
+
target: InjectTarget,
|
|
31
|
+
segment: string,
|
|
32
|
+
opts: { dedupe?: string; ensureImports?: ImportSpec[] } = {},
|
|
33
|
+
): boolean {
|
|
34
|
+
const source = openEntry(project, target.entry);
|
|
35
|
+
if (!source) return false;
|
|
36
|
+
|
|
37
|
+
const elysiaNew = findElysiaInstance(source, target.variable);
|
|
38
|
+
if (!elysiaNew) {
|
|
39
|
+
log.warn(
|
|
40
|
+
`Aucune instance \`new Elysia()\` (variable \`${target.variable}\`) dans ` +
|
|
41
|
+
`${target.entry}. Segment non injecté.`,
|
|
42
|
+
);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const scope =
|
|
47
|
+
elysiaNew.getFirstAncestorByKind(SyntaxKind.VariableStatement) ??
|
|
48
|
+
elysiaNew.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
49
|
+
const marker = opts.dedupe ?? segment;
|
|
50
|
+
if ((scope ?? source).getText().includes(marker)) {
|
|
51
|
+
log.skipped(`${target.entry} (déjà présent)`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
elysiaNew.replaceWithText(`${elysiaNew.getText()}\n ${segment}`);
|
|
56
|
+
for (const spec of opts.ensureImports ?? []) ensureImport(source, spec);
|
|
57
|
+
|
|
58
|
+
source.saveSync();
|
|
59
|
+
log.updated(target.entry);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Raccourci : injecte `.use(<useExpr>)`. */
|
|
64
|
+
export function registerUse(
|
|
65
|
+
project: Project,
|
|
66
|
+
target: InjectTarget,
|
|
67
|
+
opts: { useExpr: string; ensureImport?: ImportSpec },
|
|
68
|
+
): boolean {
|
|
69
|
+
return appendChain(project, target, `.use(${opts.useExpr})`, {
|
|
70
|
+
dedupe: `.use(${opts.useExpr})`,
|
|
71
|
+
ensureImports: opts.ensureImport ? [opts.ensureImport] : undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ré-écrit un appel `openapi()` déjà présent dans l'app entry pour y injecter
|
|
77
|
+
* la doc better-auth, et garantit l'import de `OpenAPI`. No-op si aucun appel
|
|
78
|
+
* `openapi(...)` n'est trouvé, ou s'il est déjà intégré.
|
|
79
|
+
*/
|
|
80
|
+
export function patchOpenapiWithAuth(
|
|
81
|
+
project: Project,
|
|
82
|
+
appEntry: string,
|
|
83
|
+
authImportPath: string,
|
|
84
|
+
): boolean {
|
|
85
|
+
const source = openEntry(project, appEntry);
|
|
86
|
+
if (!source) return false;
|
|
87
|
+
|
|
88
|
+
// Déjà intégré.
|
|
89
|
+
if (source.getText().includes("OpenAPI.getPaths")) return false;
|
|
90
|
+
|
|
91
|
+
const call = source
|
|
92
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
93
|
+
.find((c) => c.getExpression().getText() === "openapi");
|
|
94
|
+
if (!call) return false;
|
|
95
|
+
|
|
96
|
+
call.replaceWithText(
|
|
97
|
+
"openapi({ enabled : process.env.NODE_ENV !== \"production\", documentation: { components: await OpenAPI.components, " +
|
|
98
|
+
"paths: await OpenAPI.getPaths() } })",
|
|
99
|
+
);
|
|
100
|
+
ensureImport(source, {
|
|
101
|
+
names: ["OpenAPI"],
|
|
102
|
+
moduleSpecifier: moduleSpecifierFrom(appEntry, authImportPath),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
source.saveSync();
|
|
106
|
+
log.updated(`${appEntry} (openapi ↔ better-auth intégré)`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Retire la route racine par défaut (`.get("/", ...)`) de la cible et renvoie
|
|
112
|
+
* le texte du handler pour le replacer ailleurs. `null` si absente.
|
|
113
|
+
*/
|
|
114
|
+
export function removeRootRoute(
|
|
115
|
+
project: Project,
|
|
116
|
+
target: InjectTarget,
|
|
117
|
+
): string | null {
|
|
118
|
+
const source = openEntry(project, target.entry);
|
|
119
|
+
if (!source) return null;
|
|
120
|
+
|
|
121
|
+
const getCall = source
|
|
122
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
123
|
+
.find((call) => {
|
|
124
|
+
const expr = call.getExpression();
|
|
125
|
+
if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return false;
|
|
126
|
+
if ((expr as PropertyAccessExpression).getName() !== "get") return false;
|
|
127
|
+
const first = call.getArguments()[0];
|
|
128
|
+
if (!first || first.getKind() !== SyntaxKind.StringLiteral) return false;
|
|
129
|
+
return first.getText().replace(/['"]/g, "") === "/";
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!getCall) return null;
|
|
133
|
+
|
|
134
|
+
const handler = getCall.getArguments()[1]?.getText() ?? '() => "Hello Elysia"';
|
|
135
|
+
const objText = (getCall.getExpression() as PropertyAccessExpression)
|
|
136
|
+
.getExpression()
|
|
137
|
+
.getText();
|
|
138
|
+
getCall.replaceWithText(objText);
|
|
139
|
+
|
|
140
|
+
source.saveSync();
|
|
141
|
+
log.updated(`${target.entry} (route racine déplacée)`);
|
|
142
|
+
return handler;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const CONSOLE_MAP: Record<string, string> = {
|
|
146
|
+
log: "Logger.app.info",
|
|
147
|
+
info: "Logger.app.info",
|
|
148
|
+
debug: "Logger.app.debug",
|
|
149
|
+
warn: "Logger.app.warn",
|
|
150
|
+
error: "Logger.app.error",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Remplace les `console.*` de la cible par le Logger, et garantit son import. */
|
|
154
|
+
export function replaceConsoleWithLogger(
|
|
155
|
+
project: Project,
|
|
156
|
+
target: InjectTarget,
|
|
157
|
+
loggerPath: string,
|
|
158
|
+
): number {
|
|
159
|
+
const source = openEntry(project, target.entry);
|
|
160
|
+
if (!source) return 0;
|
|
161
|
+
|
|
162
|
+
let changed = 0;
|
|
163
|
+
for (;;) {
|
|
164
|
+
const pae = source
|
|
165
|
+
.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
|
|
166
|
+
.find(
|
|
167
|
+
(pa) =>
|
|
168
|
+
pa.getExpression().getText() === "console" && pa.getName() in CONSOLE_MAP,
|
|
169
|
+
);
|
|
170
|
+
if (!pae) break;
|
|
171
|
+
pae.replaceWithText(CONSOLE_MAP[pae.getName()]);
|
|
172
|
+
changed++;
|
|
173
|
+
if (changed > 200) break; // garde-fou
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (changed > 0) {
|
|
177
|
+
ensureImport(source, {
|
|
178
|
+
names: ["Logger"],
|
|
179
|
+
moduleSpecifier: moduleSpecifierFrom(target.entry, loggerPath),
|
|
180
|
+
});
|
|
181
|
+
source.saveSync();
|
|
182
|
+
log.updated(`${target.entry} (console.* → Logger ×${changed})`);
|
|
183
|
+
}
|
|
184
|
+
return changed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Specifier d'import relatif depuis `fromEntry` vers un fichier cible. */
|
|
188
|
+
export function moduleSpecifierFrom(fromEntry: string, targetPath: string): string {
|
|
189
|
+
const fromAbs = resolve(process.cwd(), fromEntry);
|
|
190
|
+
const rel = relative(dirname(fromAbs), resolve(process.cwd(), targetPath))
|
|
191
|
+
.replace(/\\/g, "/")
|
|
192
|
+
.replace(/\.ts$/, "");
|
|
193
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
function openEntry(project: Project, entry: string): SourceFile | null {
|
|
199
|
+
const abs = resolve(process.cwd(), entry);
|
|
200
|
+
if (!existsSync(abs)) {
|
|
201
|
+
log.warn(`Fichier cible introuvable (${entry}) — injection ignorée.`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return (
|
|
205
|
+
project.addSourceFileAtPathIfExists(abs) ?? project.addSourceFileAtPath(abs)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ensureImport(source: SourceFile, spec: ImportSpec): void {
|
|
210
|
+
const existing = source
|
|
211
|
+
.getImportDeclarations()
|
|
212
|
+
.find((d) => d.getModuleSpecifierValue() === spec.moduleSpecifier);
|
|
213
|
+
if (!existing) {
|
|
214
|
+
source.addImportDeclaration({
|
|
215
|
+
moduleSpecifier: spec.moduleSpecifier,
|
|
216
|
+
namedImports: spec.names,
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const present = new Set(existing.getNamedImports().map((n) => n.getName()));
|
|
221
|
+
for (const name of spec.names) {
|
|
222
|
+
if (!present.has(name)) existing.addNamedImport(name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function findElysiaInstance(
|
|
227
|
+
source: SourceFile,
|
|
228
|
+
variable: string,
|
|
229
|
+
): NewExpression | undefined {
|
|
230
|
+
const decl = source.getVariableDeclaration(variable);
|
|
231
|
+
if (decl) {
|
|
232
|
+
const fromDecl = decl
|
|
233
|
+
.getDescendantsOfKind(SyntaxKind.NewExpression)
|
|
234
|
+
.find(isElysia);
|
|
235
|
+
if (fromDecl) return fromDecl;
|
|
236
|
+
}
|
|
237
|
+
return source.getDescendantsOfKind(SyntaxKind.NewExpression).find(isElysia);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isElysia(node: NewExpression): boolean {
|
|
241
|
+
return node.getExpression().getText() === "Elysia";
|
|
242
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/** Petits wrappers de sortie colorée, centralisés pour rester cohérents. */
|
|
4
|
+
export const log = {
|
|
5
|
+
info: (msg: string) => console.log(chalk.cyan("ℹ"), msg),
|
|
6
|
+
success: (msg: string) => console.log(chalk.green("✔"), msg),
|
|
7
|
+
warn: (msg: string) => console.log(chalk.yellow("⚠"), msg),
|
|
8
|
+
error: (msg: string) => console.error(chalk.red("✖"), msg),
|
|
9
|
+
/** Affiche un fichier créé. */
|
|
10
|
+
created: (path: string) =>
|
|
11
|
+
console.log(` ${chalk.green("+")} ${chalk.dim(path)}`),
|
|
12
|
+
/** Affiche un fichier modifié (injection). */
|
|
13
|
+
updated: (path: string) =>
|
|
14
|
+
console.log(` ${chalk.yellow("~")} ${chalk.dim(path)}`),
|
|
15
|
+
/** Affiche un fichier ignoré (déjà présent). */
|
|
16
|
+
skipped: (path: string) =>
|
|
17
|
+
console.log(` ${chalk.gray("=")} ${chalk.dim(path + " (existe déjà)")}`),
|
|
18
|
+
title: (msg: string) => console.log("\n" + chalk.bold(msg)),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class CliError extends Error {}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers de casse pour dériver les différentes formes d'un nom de module.
|
|
3
|
+
* Ex: "user-profile" ->
|
|
4
|
+
* kebab: user-profile
|
|
5
|
+
* pascal: UserProfile
|
|
6
|
+
* camel: userProfile
|
|
7
|
+
* plural: userProfiles
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CliError } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Noms autorisés : commence par une lettre, puis lettres/chiffres/. _ -.
|
|
14
|
+
* Bloque les caractères qui permettraient une injection shell (le nom passe
|
|
15
|
+
* dans `bun create elysia <name>`) ou un path traversal (dossiers de modules).
|
|
16
|
+
*/
|
|
17
|
+
const NAME_RE = /^[a-zA-Z][a-zA-Z0-9._-]*$/;
|
|
18
|
+
|
|
19
|
+
export function nameError(input: string): string | undefined {
|
|
20
|
+
if (!input.trim()) return "Nom requis";
|
|
21
|
+
if (input.length > 100) return "Nom trop long (max 100).";
|
|
22
|
+
if (!NAME_RE.test(input)) {
|
|
23
|
+
return "Autorisé : lettres, chiffres, . _ - (doit commencer par une lettre).";
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Lève si le nom est invalide (à utiliser sur les noms passés en argument). */
|
|
29
|
+
export function assertValidName(input: string): void {
|
|
30
|
+
const err = nameError(input);
|
|
31
|
+
if (err) throw new CliError(`Nom invalide "${input}" : ${err}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitWords(input: string): string[] {
|
|
35
|
+
return input
|
|
36
|
+
// sépare camelCase / PascalCase
|
|
37
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
38
|
+
// remplace séparateurs par des espaces
|
|
39
|
+
.replace(/[-_/\s]+/g, " ")
|
|
40
|
+
.trim()
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.split(" ")
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toKebab(input: string): string {
|
|
47
|
+
return splitWords(input).join("-");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function toPascal(input: string): string {
|
|
51
|
+
return splitWords(input)
|
|
52
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
53
|
+
.join("");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toCamel(input: string): string {
|
|
57
|
+
const pascal = toPascal(input);
|
|
58
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Pluriel naïf en anglais — suffisant pour les chemins de route. */
|
|
62
|
+
export function toPlural(word: string): string {
|
|
63
|
+
if (/[^aeiou]y$/i.test(word)) return word.replace(/y$/i, "ies");
|
|
64
|
+
if (/(s|sh|ch|x|z)$/i.test(word)) return word + "es";
|
|
65
|
+
return word + "s";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface NameSet {
|
|
69
|
+
raw: string;
|
|
70
|
+
kebab: string;
|
|
71
|
+
pascal: string;
|
|
72
|
+
camel: string;
|
|
73
|
+
camelPlural: string;
|
|
74
|
+
kebabPlural: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildNames(input: string): NameSet {
|
|
78
|
+
const kebab = toKebab(input);
|
|
79
|
+
const camel = toCamel(input);
|
|
80
|
+
return {
|
|
81
|
+
raw: input,
|
|
82
|
+
kebab,
|
|
83
|
+
pascal: toPascal(input),
|
|
84
|
+
camel,
|
|
85
|
+
camelPlural: toPlural(camel),
|
|
86
|
+
kebabPlural: toPlural(kebab),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ajoute des scripts au package.json du projet courant sans écraser ceux qui
|
|
7
|
+
* existent déjà. Conserve l'indentation à 2 espaces.
|
|
8
|
+
*/
|
|
9
|
+
export function ensureScripts(scripts: Record<string, string>): void {
|
|
10
|
+
const path = join(process.cwd(), "package.json");
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
log.warn("package.json introuvable — scripts non ajoutés.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pkg = JSON.parse(readFileSync(path, "utf8")) as {
|
|
17
|
+
scripts?: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
pkg.scripts ??= {};
|
|
20
|
+
|
|
21
|
+
const added: string[] = [];
|
|
22
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
23
|
+
if (pkg.scripts[name] === undefined) {
|
|
24
|
+
pkg.scripts[name] = cmd;
|
|
25
|
+
added.push(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!added.length) return;
|
|
29
|
+
|
|
30
|
+
writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
31
|
+
log.updated(`package.json (scripts : ${added.join(", ")})`);
|
|
32
|
+
}
|
package/src/utils/pm.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { log, CliError } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Installe des dépendances dans le projet courant via `bun add`.
|
|
6
|
+
* Dédoublonne, ignore la liste vide, et remonte une erreur si l'install échoue.
|
|
7
|
+
*/
|
|
8
|
+
export function installDeps(pkgs: string[], opts: { dev?: boolean } = {}): void {
|
|
9
|
+
const unique = [...new Set(pkgs)].filter(Boolean);
|
|
10
|
+
if (!unique.length) return;
|
|
11
|
+
|
|
12
|
+
const devFlag = opts.dev ? ["-d"] : [];
|
|
13
|
+
log.info(`Installation : bun add ${devFlag.join(" ")} ${unique.join(" ")}`.trim());
|
|
14
|
+
const res = spawnSync("bun", ["add", ...devFlag, ...unique], {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
shell: true,
|
|
17
|
+
});
|
|
18
|
+
if (res.error) {
|
|
19
|
+
throw new CliError("Impossible de lancer `bun add` (Bun est-il installé ?).");
|
|
20
|
+
}
|
|
21
|
+
if (res.status !== 0) {
|
|
22
|
+
throw new CliError("`bun add` a échoué.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Exécute une commande best-effort : logue, ne jette pas si elle échoue
|
|
28
|
+
* (utile pour les migrations qui nécessitent une base de données joignable).
|
|
29
|
+
* Retourne `true` si la commande a réussi.
|
|
30
|
+
*/
|
|
31
|
+
export function run(label: string, cmd: string, args: string[]): boolean {
|
|
32
|
+
log.info(`${label} : ${cmd} ${args.join(" ")}`);
|
|
33
|
+
const res = spawnSync(cmd, args, { stdio: "inherit", shell: true });
|
|
34
|
+
if (res.error || res.status !== 0) {
|
|
35
|
+
log.warn(`Échec de "${label}" — à relancer manuellement.`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { Project } from "ts-morph";
|
|
4
|
+
import { CliError } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export interface ElysiaCliConfig {
|
|
7
|
+
/** Racine du code source. */
|
|
8
|
+
srcDir: string;
|
|
9
|
+
/** Dossier où sont générés les modules. */
|
|
10
|
+
modulesDir: string;
|
|
11
|
+
/** Fichier d'entrée qui instancie l'app Elysia. */
|
|
12
|
+
appEntry: string;
|
|
13
|
+
/** Nom de la variable Elysia dans l'app entry (pour l'injection du .use()). */
|
|
14
|
+
appVariable: string;
|
|
15
|
+
/** Fichier agrégateur des modules (où `generate module` branche les controllers). */
|
|
16
|
+
modulesEntry: string;
|
|
17
|
+
/** Nom de la variable Elysia dans l'agrégateur des modules. */
|
|
18
|
+
modulesVariable: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CONFIG: ElysiaCliConfig = {
|
|
22
|
+
srcDir: "src",
|
|
23
|
+
modulesDir: "src/modules",
|
|
24
|
+
appEntry: "src/index.ts",
|
|
25
|
+
appVariable: "app",
|
|
26
|
+
modulesEntry: "src/modules/index.ts",
|
|
27
|
+
modulesVariable: "modules",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CONFIG_FILE = "elysia-cli.config.json";
|
|
31
|
+
|
|
32
|
+
/** Résout la config du projet cible (fichier optionnel + defaults). */
|
|
33
|
+
export function loadConfig(cwd = process.cwd()): ElysiaCliConfig {
|
|
34
|
+
const configPath = join(cwd, CONFIG_FILE);
|
|
35
|
+
if (!existsSync(configPath)) return DEFAULT_CONFIG;
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
38
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new CliError(
|
|
41
|
+
`Impossible de lire ${CONFIG_FILE}: ${(err as Error).message}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Charge un Project ts-morph pointant sur le tsconfig du projet cible.
|
|
48
|
+
* Utilisé pour les opérations d'injection (add / register de plugin).
|
|
49
|
+
*/
|
|
50
|
+
export function loadTsProject(cwd = process.cwd()): Project {
|
|
51
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
52
|
+
if (existsSync(tsconfigPath)) {
|
|
53
|
+
return new Project({ tsConfigFilePath: tsconfigPath });
|
|
54
|
+
}
|
|
55
|
+
// Fallback sans tsconfig : projet en mémoire, on ajoute les fichiers à la demande.
|
|
56
|
+
return new Project({ skipAddingFilesFromTsConfig: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Chemin absolu vers le dossier d'un module. */
|
|
60
|
+
export function moduleDir(config: ElysiaCliConfig, kebabName: string): string {
|
|
61
|
+
return resolve(process.cwd(), config.modulesDir, kebabName);
|
|
62
|
+
}
|