nestgen-cli 0.1.0 → 0.2.2
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/dist/cli/doctor-output.js +45 -0
- package/dist/cli/output.js +65 -0
- package/dist/engine/file-transaction.js +166 -0
- package/dist/engine/generated-code.js +26 -0
- package/dist/engine/generation-manifest.js +118 -0
- package/dist/engine/migration-plan.js +78 -0
- package/dist/engine/module-generator.js +80 -65
- package/dist/engine/project-config.js +5 -2
- package/dist/engine/project-path.js +25 -0
- package/dist/engine/project-preflight.js +96 -0
- package/dist/engine/resource-generator.js +1104 -68
- package/dist/engine/resource-spec.js +254 -11
- package/examples/store-api/README.md +76 -0
- package/examples/store-api/definitions/customers.resource.json +14 -0
- package/examples/store-api/definitions/order-items.resource.json +26 -0
- package/examples/store-api/definitions/orders.resource.json +22 -0
- package/examples/store-api/definitions/products.resource.json +19 -0
- package/examples/store-api/nestgen.example.json +7 -0
- package/nestgen.js +488 -84
- package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
- package/nestjs-generator/features/docker.sh +6 -4
- package/nestjs-generator/features/git.sh +1 -1
- package/nestjs-generator/features/inject_module_to_app.sh +8 -6
- package/nestjs-generator/features/logger.sh +8 -4
- package/nestjs-generator/features/operational_foundation.mjs +210 -0
- package/nestjs-generator/features/operations.sh +18 -0
- package/nestjs-generator/features/preflight.mjs +2 -44
- package/nestjs-generator/features/prisma.sh +3 -3
- package/nestjs-generator/features/typeorm.sh +3 -3
- package/nestjs-generator/features/utils.sh +5 -3
- package/nestjs-generator/generate_project.sh +15 -9
- package/package.json +11 -4
- package/readme.md +234 -13
package/nestgen.js
CHANGED
|
@@ -7,11 +7,27 @@ import path from 'path';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { inspectProject } from './nestjs-generator/features/preflight.mjs';
|
|
10
|
-
import { generateModule } from './dist/engine/module-generator.js';
|
|
11
|
-
import { generateResource } from './dist/engine/resource-generator.js';
|
|
12
|
-
import {
|
|
10
|
+
import { generateModule, planModuleGeneration } from './dist/engine/module-generator.js';
|
|
11
|
+
import { generateResource, planResourceGeneration } from './dist/engine/resource-generator.js';
|
|
12
|
+
import {
|
|
13
|
+
parseResourceFields,
|
|
14
|
+
parseResourceIndexes,
|
|
15
|
+
parseResourceListOptions,
|
|
16
|
+
parseResourceRelations,
|
|
17
|
+
} from './dist/engine/resource-spec.js';
|
|
13
18
|
import { configFileName, defaultConfig, loadConfigIfPresent, writeConfig } from './dist/engine/project-config.js';
|
|
14
19
|
import { parseArchitectureProfile } from './dist/engine/architecture-profile.js';
|
|
20
|
+
import { planResourceMigration } from './dist/engine/migration-plan.js';
|
|
21
|
+
import { formatDoctorOutput } from './dist/cli/doctor-output.js';
|
|
22
|
+
import {
|
|
23
|
+
heading,
|
|
24
|
+
success,
|
|
25
|
+
formatError,
|
|
26
|
+
formatConfig,
|
|
27
|
+
formatPlan,
|
|
28
|
+
formatMigration,
|
|
29
|
+
formatHelp,
|
|
30
|
+
} from './dist/cli/output.js';
|
|
15
31
|
|
|
16
32
|
// ────── Resolve __dirname compatible ES Modules
|
|
17
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -26,17 +42,7 @@ const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
|
|
|
26
42
|
|
|
27
43
|
// ────── Logo CLI
|
|
28
44
|
function printLogo() {
|
|
29
|
-
console.log(
|
|
30
|
-
chalk.magentaBright(`
|
|
31
|
-
███╗ ██╗███████╗███████╗████████╗ ██████╗ ███████╗███╗ ██╗
|
|
32
|
-
████╗ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██╔════╝████╗ ██║
|
|
33
|
-
██╔██╗ ██║█████╗ ███████╗ ██║ ██║ ███╗█████╗ ██╔██╗ ██║
|
|
34
|
-
██║╚██╗██║██╔══╝ ╚════██║ ██║ ██║ ██║██╔══╝ ██║╚██╗██║
|
|
35
|
-
██║ ╚████║███████╗███████║ ██║ ╚██████╔╝███████╗██║ ╚████║
|
|
36
|
-
╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
|
|
37
|
-
`),
|
|
38
|
-
);
|
|
39
|
-
console.log(chalk.cyan.bold('✨ NestGen CLI — Générateur modulaire NestJS'));
|
|
45
|
+
console.log(heading('CLI', 'Générateur modulaire NestJS'));
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
// ────── Helpers
|
|
@@ -68,6 +74,11 @@ export function validateOrm(value) {
|
|
|
68
74
|
return orm;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
function validatePackageManager(value) {
|
|
78
|
+
if (!['npm', 'pnpm', 'yarn'].includes(value)) throw new Error('Package manager invalide.');
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
export function resolveProjectPath(value) {
|
|
72
83
|
const projectPath = String(value ?? '').trim();
|
|
73
84
|
|
|
@@ -90,6 +101,15 @@ export function parseCliArgs(args) {
|
|
|
90
101
|
orm: undefined,
|
|
91
102
|
profile: undefined,
|
|
92
103
|
packageManager: undefined,
|
|
104
|
+
projectPath: undefined,
|
|
105
|
+
docker: undefined,
|
|
106
|
+
swagger: undefined,
|
|
107
|
+
operations: undefined,
|
|
108
|
+
git: undefined,
|
|
109
|
+
modules: undefined,
|
|
110
|
+
application: undefined,
|
|
111
|
+
indexes: undefined,
|
|
112
|
+
definitionFile: undefined,
|
|
93
113
|
noInteractive: false,
|
|
94
114
|
quiet: false,
|
|
95
115
|
verbose: false,
|
|
@@ -97,6 +117,9 @@ export function parseCliArgs(args) {
|
|
|
97
117
|
help: false,
|
|
98
118
|
version: false,
|
|
99
119
|
dryRun: false,
|
|
120
|
+
update: false,
|
|
121
|
+
migrationName: undefined,
|
|
122
|
+
json: false,
|
|
100
123
|
};
|
|
101
124
|
const positionals = [];
|
|
102
125
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -110,8 +133,29 @@ export function parseCliArgs(args) {
|
|
|
110
133
|
else if (argument.startsWith('--profile=')) options.profile = argument.slice(10);
|
|
111
134
|
else if (argument === '--package-manager') options.packageManager = args[++index];
|
|
112
135
|
else if (argument.startsWith('--package-manager=')) options.packageManager = argument.slice(18);
|
|
136
|
+
else if (argument === '--project-path') options.projectPath = args[++index];
|
|
137
|
+
else if (argument.startsWith('--project-path=')) options.projectPath = argument.slice(15);
|
|
138
|
+
else if (argument === '--docker') options.docker = true;
|
|
139
|
+
else if (argument === '--no-docker') options.docker = false;
|
|
140
|
+
else if (argument === '--swagger') options.swagger = true;
|
|
141
|
+
else if (argument === '--no-swagger') options.swagger = false;
|
|
142
|
+
else if (argument === '--operations') options.operations = true;
|
|
143
|
+
else if (argument === '--no-operations') options.operations = false;
|
|
144
|
+
else if (argument === '--git') options.git = true;
|
|
145
|
+
else if (argument === '--no-git') options.git = false;
|
|
146
|
+
else if (argument === '--modules') options.modules = args[++index];
|
|
147
|
+
else if (argument.startsWith('--modules=')) options.modules = argument.slice(10);
|
|
148
|
+
else if (argument === '--application') {
|
|
149
|
+
const value = args[++index];
|
|
150
|
+
if (!value || value.startsWith('-')) throw new Error('--application requiert une valeur.');
|
|
151
|
+
options.application = value;
|
|
152
|
+
} else if (argument.startsWith('--application=')) options.application = argument.slice(14);
|
|
113
153
|
else if (argument === '--fields') options.fields = args[++index];
|
|
114
154
|
else if (argument.startsWith('--fields=')) options.fields = argument.slice(9);
|
|
155
|
+
else if (argument === '--indexes') options.indexes = args[++index];
|
|
156
|
+
else if (argument.startsWith('--indexes=')) options.indexes = argument.slice(10);
|
|
157
|
+
else if (argument === '--file') options.definitionFile = args[++index];
|
|
158
|
+
else if (argument.startsWith('--file=')) options.definitionFile = argument.slice(7);
|
|
115
159
|
else if (argument === '--route') options.route = args[++index];
|
|
116
160
|
else if (argument.startsWith('--route=')) options.route = argument.slice(8);
|
|
117
161
|
else if (argument === '--table') options.table = args[++index];
|
|
@@ -120,6 +164,10 @@ export function parseCliArgs(args) {
|
|
|
120
164
|
else if (argument === '--quiet') options.quiet = true;
|
|
121
165
|
else if (argument === '--verbose') options.verbose = true;
|
|
122
166
|
else if (argument === '--dry-run') options.dryRun = true;
|
|
167
|
+
else if (argument === '--update') options.update = true;
|
|
168
|
+
else if (argument === '--migration-name') options.migrationName = args[++index];
|
|
169
|
+
else if (argument.startsWith('--migration-name=')) options.migrationName = argument.slice(17);
|
|
170
|
+
else if (argument === '--json') options.json = true;
|
|
123
171
|
else if (argument === '--no-color') options.color = false;
|
|
124
172
|
else if (argument === '--help' || argument === '-h') options.help = true;
|
|
125
173
|
else if (argument === '--version' || argument === '-V') options.version = true;
|
|
@@ -129,10 +177,10 @@ export function parseCliArgs(args) {
|
|
|
129
177
|
return { command: positionals.shift(), positionals, options };
|
|
130
178
|
}
|
|
131
179
|
|
|
132
|
-
export function createModulePlan(projectRoot, moduleName, orm) {
|
|
180
|
+
export function createModulePlan(projectRoot, moduleName, orm, sourceRoot = 'src') {
|
|
133
181
|
const normalizedName = validateModuleName(moduleName);
|
|
134
182
|
const normalizedOrm = validateOrm(orm);
|
|
135
|
-
const resourceRoot =
|
|
183
|
+
const resourceRoot = `${sourceRoot}/app/${normalizedName}`;
|
|
136
184
|
return {
|
|
137
185
|
operation: 'module',
|
|
138
186
|
module: normalizedName,
|
|
@@ -148,8 +196,8 @@ export function createModulePlan(projectRoot, moduleName, orm) {
|
|
|
148
196
|
`${resourceRoot}/${normalizedName}.module.ts`,
|
|
149
197
|
],
|
|
150
198
|
mutations: [
|
|
151
|
-
|
|
152
|
-
|
|
199
|
+
`Ajoute le module dans @Module({ imports }) de ${sourceRoot}/app.module.ts.`,
|
|
200
|
+
`Ajoute CqrsModule dans ${sourceRoot}/app.module.ts si nécessaire.`,
|
|
153
201
|
...(normalizedOrm === 'typeorm'
|
|
154
202
|
? ['Ajoute la configuration TypeORM racine seulement si elle est absente.']
|
|
155
203
|
: []),
|
|
@@ -157,9 +205,28 @@ export function createModulePlan(projectRoot, moduleName, orm) {
|
|
|
157
205
|
};
|
|
158
206
|
}
|
|
159
207
|
|
|
208
|
+
function dryRunOutput(operation, plan) {
|
|
209
|
+
const conflicts = [
|
|
210
|
+
...(plan.featureConflict ? [{ path: '', reason: plan.featureConflict }] : []),
|
|
211
|
+
...plan.preview.filter((change) => change.status === 'conflict').map(({ path, reason }) => ({ path, reason })),
|
|
212
|
+
];
|
|
213
|
+
return {
|
|
214
|
+
version: 1,
|
|
215
|
+
operation,
|
|
216
|
+
changes: plan.preview.map(({ path, operation: fileOperation, status, diff, reason }) => ({
|
|
217
|
+
path,
|
|
218
|
+
operation: fileOperation,
|
|
219
|
+
status,
|
|
220
|
+
...(diff ? { diff } : {}),
|
|
221
|
+
...(reason ? { reason } : {}),
|
|
222
|
+
})),
|
|
223
|
+
conflicts,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
160
227
|
export function runBashScript(scriptPath, args = [], env = {}, quiet = false) {
|
|
161
228
|
const result = spawnSync('bash', [scriptPath, ...args], {
|
|
162
|
-
env: { ...process.env, ...env },
|
|
229
|
+
env: { ...process.env, ...(chalk.level === 0 ? { NO_COLOR: '1' } : {}), ...env },
|
|
163
230
|
stdio: quiet ? 'pipe' : 'inherit',
|
|
164
231
|
});
|
|
165
232
|
|
|
@@ -215,6 +282,12 @@ async function askInitQuestions() {
|
|
|
215
282
|
message: '🐳 Activer Docker ?',
|
|
216
283
|
default: false,
|
|
217
284
|
},
|
|
285
|
+
{
|
|
286
|
+
type: 'confirm',
|
|
287
|
+
name: 'withOperations',
|
|
288
|
+
message: '🩺 Activer le socle d’exploitation (santé, logs, arrêt propre) ?',
|
|
289
|
+
default: false,
|
|
290
|
+
},
|
|
218
291
|
{
|
|
219
292
|
type: 'confirm',
|
|
220
293
|
name: 'withGit',
|
|
@@ -249,42 +322,75 @@ function validatePromptValue(validator) {
|
|
|
249
322
|
|
|
250
323
|
async function runInteractiveInit(options) {
|
|
251
324
|
if (!options.quiet) printLogo();
|
|
252
|
-
if (options.noInteractive)
|
|
253
|
-
throw new Error(
|
|
254
|
-
'init --no-interactive requiert des options de projet qui ne sont pas encore prises en charge.',
|
|
255
|
-
);
|
|
256
|
-
|
|
257
325
|
if (!fs.existsSync(GENERATE_SCRIPT)) {
|
|
258
326
|
throw new Error(`Script introuvable : ${GENERATE_SCRIPT}`);
|
|
259
327
|
}
|
|
260
328
|
|
|
261
|
-
const answers =
|
|
262
|
-
|
|
329
|
+
const answers = options.noInteractive
|
|
330
|
+
? (() => {
|
|
331
|
+
const missing = [
|
|
332
|
+
['nom du projet', options.projectName],
|
|
333
|
+
['--project-path', options.projectPath],
|
|
334
|
+
['--package-manager', options.packageManager],
|
|
335
|
+
['--orm', options.orm],
|
|
336
|
+
['--docker ou --no-docker', options.docker],
|
|
337
|
+
['--swagger ou --no-swagger', options.swagger],
|
|
338
|
+
['--operations ou --no-operations', options.operations],
|
|
339
|
+
['--git ou --no-git', options.git],
|
|
340
|
+
['--modules', options.modules],
|
|
341
|
+
]
|
|
342
|
+
.filter(([, value]) => value === undefined)
|
|
343
|
+
.map(([name]) => name);
|
|
344
|
+
if (missing.length) throw new Error(`init --no-interactive requiert : ${missing.join(', ')}.`);
|
|
345
|
+
return {
|
|
346
|
+
projectName: options.projectName,
|
|
347
|
+
projectPath: options.projectPath,
|
|
348
|
+
packageManager: options.packageManager,
|
|
349
|
+
orm: options.orm,
|
|
350
|
+
withDocker: options.docker,
|
|
351
|
+
withSwagger: options.swagger,
|
|
352
|
+
withOperations: options.operations,
|
|
353
|
+
withGit: options.git,
|
|
354
|
+
modules: options.modules ? options.modules.split(',').filter(Boolean) : [],
|
|
355
|
+
};
|
|
356
|
+
})()
|
|
357
|
+
: await askInitQuestions();
|
|
358
|
+
const { projectName, projectPath, packageManager, orm, withSwagger, withDocker, withOperations, withGit, modules } =
|
|
359
|
+
answers;
|
|
263
360
|
|
|
264
361
|
const env = {
|
|
265
362
|
APP_NAME: validateModuleName(projectName),
|
|
266
363
|
PROJECT_PATH: resolveProjectPath(projectPath),
|
|
267
|
-
PM: packageManager,
|
|
364
|
+
PM: validatePackageManager(packageManager),
|
|
268
365
|
ORM: validateOrm(orm),
|
|
269
366
|
WITH_SWAGGER: withSwagger ? 'y' : 'n',
|
|
367
|
+
WITH_OPERATIONS: withOperations ? 'y' : 'n',
|
|
270
368
|
WITH_DOCKER: withDocker ? 'y' : 'n',
|
|
271
369
|
WITH_GIT: withGit ? 'y' : 'n',
|
|
272
370
|
MODULES: modules.map(validateModuleName).join(' '),
|
|
273
371
|
};
|
|
274
372
|
|
|
275
|
-
console.log('
|
|
276
|
-
runBashScript(GENERATE_SCRIPT, [], env);
|
|
373
|
+
if (!options.quiet) console.log(chalk.cyan(' › Génération du projet…'));
|
|
374
|
+
runBashScript(GENERATE_SCRIPT, [], env, options.quiet);
|
|
375
|
+
return {
|
|
376
|
+
projectName: env.APP_NAME,
|
|
377
|
+
projectPath: env.PROJECT_PATH,
|
|
378
|
+
packageManager: env.PM,
|
|
379
|
+
orm: env.ORM,
|
|
380
|
+
operations: env.WITH_OPERATIONS === 'y',
|
|
381
|
+
};
|
|
277
382
|
}
|
|
278
383
|
|
|
279
384
|
// ────── Commande : MODULE
|
|
280
385
|
async function runModuleGeneration(parsed) {
|
|
281
386
|
if (!parsed.options.quiet) printLogo();
|
|
387
|
+
const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
282
388
|
|
|
283
389
|
let moduleName, orm;
|
|
284
390
|
|
|
285
391
|
if (parsed.positionals[0]) {
|
|
286
392
|
moduleName = validateModuleName(parsed.positionals[0]);
|
|
287
|
-
orm = validateOrm(parsed.options.orm ??
|
|
393
|
+
orm = validateOrm(parsed.options.orm ?? config.orm);
|
|
288
394
|
} else {
|
|
289
395
|
if (parsed.options.noInteractive) throw new Error('module --no-interactive requiert un nom de module.');
|
|
290
396
|
const answers = await inquirer.prompt([
|
|
@@ -311,109 +417,385 @@ async function runModuleGeneration(parsed) {
|
|
|
311
417
|
}
|
|
312
418
|
|
|
313
419
|
if (parsed.options.dryRun) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
420
|
+
const plan = await planModuleGeneration(process.cwd(), moduleName, orm, parsed.options.application);
|
|
421
|
+
const output = dryRunOutput('module', plan);
|
|
422
|
+
if (!parsed.options.json) console.log(formatPlan(output, parsed.options.verbose));
|
|
423
|
+
return output;
|
|
317
424
|
}
|
|
318
425
|
|
|
319
|
-
if (!parsed.options.quiet) console.log(chalk.cyan(
|
|
426
|
+
if (!parsed.options.quiet) console.log(chalk.cyan(` › Génération du module ${moduleName}…`));
|
|
320
427
|
if (process.env.NESTGEN_ROOT) {
|
|
321
428
|
runBashScript(ADD_MODULE_SCRIPT, [moduleName, orm], {}, parsed.options.quiet);
|
|
322
|
-
return;
|
|
429
|
+
return { module: moduleName, orm, generated: true };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await generateModule(process.cwd(), moduleName, orm, parsed.options.application);
|
|
433
|
+
return { module: moduleName, orm, generated: true };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function loadResourceDefinition(file, projectRoot = process.cwd()) {
|
|
437
|
+
const candidate = path.resolve(projectRoot, String(file ?? ''));
|
|
438
|
+
if (!candidate.startsWith(`${path.resolve(projectRoot)}${path.sep}`))
|
|
439
|
+
throw new Error('--file doit désigner un fichier situé dans le projet courant.');
|
|
440
|
+
let value;
|
|
441
|
+
try {
|
|
442
|
+
value = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
447
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : un objet JSON est requis.`);
|
|
448
|
+
if (value.version !== 1) throw new Error(`Définition de ressource invalide (${candidate}) : version 1 requise.`);
|
|
449
|
+
if (!Array.isArray(value.fields) || value.fields.some((field) => typeof field !== 'string'))
|
|
450
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : fields doit être un tableau de chaînes.`);
|
|
451
|
+
for (const [index, field] of value.fields.entries()) {
|
|
452
|
+
try {
|
|
453
|
+
parseResourceFields([field]);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : fields[${index}] — ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const fields = parseResourceFields(value.fields);
|
|
459
|
+
if (value.relations !== undefined && !Array.isArray(value.relations))
|
|
460
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : relations doit être un tableau.`);
|
|
461
|
+
let relations;
|
|
462
|
+
try {
|
|
463
|
+
relations = parseResourceRelations(value.relations ?? []);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : relations — ${error.message}`);
|
|
466
|
+
}
|
|
467
|
+
const relationFields = relationIndexFields(relations);
|
|
468
|
+
try {
|
|
469
|
+
parseResourceListOptions(value.list, [...fields, ...relationFields]);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : list — ${error.message}`);
|
|
323
472
|
}
|
|
473
|
+
if (
|
|
474
|
+
value.indexes !== undefined &&
|
|
475
|
+
(!Array.isArray(value.indexes) || value.indexes.some((index) => typeof index !== 'string'))
|
|
476
|
+
)
|
|
477
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : indexes doit être un tableau de chaînes.`);
|
|
478
|
+
try {
|
|
479
|
+
if (
|
|
480
|
+
value.uniqueIndexes !== undefined &&
|
|
481
|
+
(!Array.isArray(value.uniqueIndexes) || value.uniqueIndexes.some((index) => typeof index !== 'string'))
|
|
482
|
+
)
|
|
483
|
+
throw new Error(
|
|
484
|
+
`Définition de ressource invalide (${candidate}) : uniqueIndexes doit être un tableau de chaînes.`,
|
|
485
|
+
);
|
|
486
|
+
parseResourceIndexes(value.indexes ?? [], [...fields, ...relationFields], value.uniqueIndexes ?? []);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : indexes — ${error.message}`);
|
|
489
|
+
}
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
324
492
|
|
|
325
|
-
|
|
493
|
+
function relationIndexFields(relations) {
|
|
494
|
+
return relations
|
|
495
|
+
.filter((relation) => relation.type === 'belongsTo')
|
|
496
|
+
.map((relation) => ({
|
|
497
|
+
name: `${relation.field ?? relation.target.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Id`,
|
|
498
|
+
}));
|
|
326
499
|
}
|
|
327
500
|
|
|
328
|
-
function runResourceGeneration(parsed) {
|
|
329
|
-
const
|
|
330
|
-
|
|
501
|
+
async function runResourceGeneration(parsed) {
|
|
502
|
+
const definition = parsed.options.definitionFile ? loadResourceDefinition(parsed.options.definitionFile) : {};
|
|
503
|
+
const name = validateModuleName(parsed.positionals[0] ?? definition.name ?? '');
|
|
504
|
+
const fieldValues = parsed.options.fields?.split(',').filter(Boolean) ?? definition.fields;
|
|
505
|
+
if (!fieldValues) throw new Error('resource requiert --fields ou --file avec fields.');
|
|
331
506
|
const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
332
|
-
const orm = validateOrm(parsed.options.orm ?? config.orm);
|
|
333
|
-
const profile = parseArchitectureProfile(parsed.options.profile ?? config.profile);
|
|
334
|
-
const fields = parseResourceFields(
|
|
335
|
-
const
|
|
336
|
-
const
|
|
507
|
+
const orm = validateOrm(parsed.options.orm ?? definition.orm ?? config.orm);
|
|
508
|
+
const profile = parseArchitectureProfile(parsed.options.profile ?? definition.profile ?? config.profile);
|
|
509
|
+
const fields = parseResourceFields(fieldValues);
|
|
510
|
+
const relations = parseResourceRelations(definition.relations ?? []);
|
|
511
|
+
const relationFields = relationIndexFields(relations);
|
|
512
|
+
const list = parseResourceListOptions(definition.list, [...fields, ...relationFields]);
|
|
513
|
+
const indexes = parseResourceIndexes(
|
|
514
|
+
parsed.options.indexes?.split(',').filter(Boolean) ?? definition.indexes ?? [],
|
|
515
|
+
[...fields, ...relationFields],
|
|
516
|
+
definition.uniqueIndexes ?? [],
|
|
517
|
+
);
|
|
518
|
+
const route = parsed.options.route ?? definition.route ?? `${name}s`;
|
|
519
|
+
const table = parsed.options.table ?? definition.table ?? `${name}s`;
|
|
337
520
|
if (!/^[a-z][a-z0-9/-]*$/.test(route) || !/^[a-z][a-z0-9_]*$/.test(table))
|
|
338
521
|
throw new Error('Route ou table invalide.');
|
|
522
|
+
if (parsed.options.migrationName && !parsed.options.update) throw new Error('--migration-name requiert --update.');
|
|
523
|
+
const resourceOptions = {
|
|
524
|
+
name,
|
|
525
|
+
route,
|
|
526
|
+
table,
|
|
527
|
+
fields,
|
|
528
|
+
indexes,
|
|
529
|
+
relations,
|
|
530
|
+
list,
|
|
531
|
+
orm,
|
|
532
|
+
profile,
|
|
533
|
+
application: parsed.options.application,
|
|
534
|
+
update: parsed.options.update,
|
|
535
|
+
};
|
|
536
|
+
const migration = parsed.options.migrationName
|
|
537
|
+
? planResourceMigration(
|
|
538
|
+
process.cwd(),
|
|
539
|
+
inspectProject(process.cwd(), orm, { application: parsed.options.application }).sourceRoot,
|
|
540
|
+
name,
|
|
541
|
+
orm,
|
|
542
|
+
parsed.options.migrationName,
|
|
543
|
+
fields,
|
|
544
|
+
indexes,
|
|
545
|
+
relations,
|
|
546
|
+
)
|
|
547
|
+
: undefined;
|
|
339
548
|
if (parsed.options.dryRun) {
|
|
340
|
-
|
|
341
|
-
|
|
549
|
+
const plan = await planResourceGeneration(process.cwd(), resourceOptions);
|
|
550
|
+
const output = { ...dryRunOutput('resource', plan), ...(migration ? { migration } : {}) };
|
|
551
|
+
if (!parsed.options.json) console.log(formatPlan(output, parsed.options.verbose));
|
|
552
|
+
return output;
|
|
342
553
|
}
|
|
343
|
-
generateResource(process.cwd(),
|
|
554
|
+
await generateResource(process.cwd(), resourceOptions);
|
|
555
|
+
return {
|
|
556
|
+
resource: name,
|
|
557
|
+
orm,
|
|
558
|
+
profile,
|
|
559
|
+
generated: true,
|
|
560
|
+
...(parsed.options.update ? { updated: true } : {}),
|
|
561
|
+
...(migration ? { migration } : {}),
|
|
562
|
+
};
|
|
344
563
|
}
|
|
345
564
|
|
|
346
565
|
function printUsage() {
|
|
347
566
|
console.log(
|
|
348
|
-
`Usage: nestgen <commande> [options]\n\nCommandes:\n init
|
|
567
|
+
formatHelp(`Usage: nestgen <commande> [options]\n\nCommandes:\n init [nom] Génère un projet NestJS\n module <nom> [--orm <orm>] Génère un module\n resource <nom> --fields ... Génère un CRUD TypeORM ou Prisma
|
|
349
568
|
config init|show Gère nestgen.config.json
|
|
350
569
|
doctor Vérifie l'installation\n\nOptions:\n -h, --help Affiche cette aide\n -V, --version Affiche la version\n --no-interactive Refuse les prompts\n --quiet Supprime les sorties non essentielles\n --verbose Active les diagnostics\n --no-color Désactive les couleurs\n --profile <simple|advanced> Choisit le profil d'architecture
|
|
351
|
-
--package-manager <pm> Définit
|
|
352
|
-
--
|
|
570
|
+
--package-manager <pm> Définit npm, pnpm ou yarn
|
|
571
|
+
--project-path <chemin> Définit le dossier parent du projet init
|
|
572
|
+
--docker, --no-docker Active ou désactive Docker pour init
|
|
573
|
+
--swagger, --no-swagger Active ou désactive Swagger pour init
|
|
574
|
+
--operations, --no-operations Active ou désactive le socle d’exploitation pour init
|
|
575
|
+
--git, --no-git Active ou désactive Git pour init
|
|
576
|
+
--modules <a,b> Définit les modules init, séparés par des virgules
|
|
577
|
+
--application <nom> Cible une application d’un workspace Nest
|
|
578
|
+
--update Met à jour une ressource déjà générée sans écraser les fichiers modifiés
|
|
579
|
+
--migration-name <nom> Prépare une commande de migration officielle avec --update, sans l’appliquer
|
|
580
|
+
--dry-run Affiche le plan sans écrire
|
|
581
|
+
--json Retourne un résultat JSON versionné
|
|
582
|
+
--indexes <a+b,c> Ajoute des index composites ou simples\n\nChamps resource : string, number, integer, decimal(precision;scale), enum(VALEUR|VALEUR), boolean, date, uuid.`),
|
|
353
583
|
);
|
|
354
584
|
}
|
|
355
585
|
|
|
356
|
-
function runDoctor() {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
586
|
+
function runDoctor(options = {}) {
|
|
587
|
+
const diagnostics = [];
|
|
588
|
+
const add = (id, severity, cause, action, detail) => diagnostics.push({ id, severity, cause, action, detail });
|
|
589
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
|
590
|
+
const supportedNode = (nodeMajor === 24 && nodeMinor >= 15) || nodeMajor === 25 || nodeMajor === 26;
|
|
591
|
+
add(
|
|
592
|
+
'NODE_VERSION',
|
|
593
|
+
supportedNode ? 'INFO' : 'ERROR',
|
|
594
|
+
supportedNode ? 'Version Node.js compatible.' : 'Node.js doit être >=24.15.0 et <27.',
|
|
595
|
+
'Installe une version Node.js supportée puis relance doctor.',
|
|
596
|
+
process.versions.node,
|
|
597
|
+
);
|
|
598
|
+
add(
|
|
599
|
+
'GENERATOR_SCRIPTS',
|
|
600
|
+
fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT) ? 'INFO' : 'ERROR',
|
|
601
|
+
fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT)
|
|
602
|
+
? 'Scripts du générateur disponibles.'
|
|
603
|
+
: 'Un script du générateur est introuvable.',
|
|
604
|
+
'Réinstalle NestGen.',
|
|
605
|
+
`${GENERATE_SCRIPT}, ${ADD_MODULE_SCRIPT}`,
|
|
606
|
+
);
|
|
607
|
+
const projectRoot = process.cwd();
|
|
608
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
609
|
+
let manifest;
|
|
610
|
+
if (!fs.existsSync(packagePath)) {
|
|
611
|
+
add(
|
|
612
|
+
'PROJECT_PACKAGE',
|
|
613
|
+
'WARNING',
|
|
614
|
+
'Aucun package.json de projet détecté.',
|
|
615
|
+
'Exécute doctor à la racine du projet Nest.',
|
|
616
|
+
packagePath,
|
|
617
|
+
);
|
|
618
|
+
} else {
|
|
619
|
+
try {
|
|
620
|
+
manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
621
|
+
const dependencies = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
|
|
622
|
+
for (const dependency of ['@nestjs/common', '@nestjs/core'])
|
|
623
|
+
add(
|
|
624
|
+
`DEPENDENCY_${dependency.replace(/^@/, '').replaceAll(/[/-]/g, '_').toUpperCase()}`,
|
|
625
|
+
dependencies[dependency] ? 'INFO' : 'ERROR',
|
|
626
|
+
dependencies[dependency] ? `${dependency} est déclaré.` : `${dependency} est absent.`,
|
|
627
|
+
`Installe ${dependency} avec le package manager du projet.`,
|
|
628
|
+
dependencies[dependency] ?? '',
|
|
629
|
+
);
|
|
630
|
+
const nestVersion = String(dependencies['@nestjs/common'] ?? '');
|
|
631
|
+
if (nestVersion)
|
|
632
|
+
add(
|
|
633
|
+
'NEST_VERSION',
|
|
634
|
+
/(?:\^|~)?12(?:\.|$)/.test(nestVersion) ? 'INFO' : 'WARNING',
|
|
635
|
+
/(?:\^|~)?12(?:\.|$)/.test(nestVersion)
|
|
636
|
+
? 'La version Nest déclarée est supportée.'
|
|
637
|
+
: 'La version Nest déclarée est hors de la matrice testée.',
|
|
638
|
+
'Utilise Nest 12 ou vérifie cette combinaison avec la matrice de compatibilité.',
|
|
639
|
+
nestVersion,
|
|
640
|
+
);
|
|
641
|
+
if (dependencies['@nestjs/typeorm'] || dependencies.typeorm)
|
|
642
|
+
add(
|
|
643
|
+
'TYPEORM_COHERENCE',
|
|
644
|
+
dependencies['@nestjs/typeorm'] && dependencies.typeorm ? 'INFO' : 'WARNING',
|
|
645
|
+
dependencies['@nestjs/typeorm'] && dependencies.typeorm
|
|
646
|
+
? 'Les dépendances TypeORM sont cohérentes.'
|
|
647
|
+
: 'Le support TypeORM est incomplet.',
|
|
648
|
+
'Installe @nestjs/typeorm et typeorm ensemble, ou utilise Prisma.',
|
|
649
|
+
'',
|
|
650
|
+
);
|
|
651
|
+
if (dependencies['@prisma/client'])
|
|
652
|
+
add(
|
|
653
|
+
'PRISMA_CLIENT',
|
|
654
|
+
'INFO',
|
|
655
|
+
'@prisma/client est déclaré.',
|
|
656
|
+
'Exécute npx prisma generate après une modification du schéma.',
|
|
657
|
+
dependencies['@prisma/client'],
|
|
658
|
+
);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
add(
|
|
661
|
+
'PROJECT_PACKAGE',
|
|
662
|
+
'ERROR',
|
|
663
|
+
'package.json est invalide.',
|
|
664
|
+
'Corrige le JSON du manifeste.',
|
|
665
|
+
error.message,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
373
669
|
try {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
670
|
+
const config = loadConfigIfPresent(projectRoot);
|
|
671
|
+
add(
|
|
672
|
+
'NESTGEN_CONFIG',
|
|
673
|
+
'INFO',
|
|
674
|
+
config
|
|
675
|
+
? 'Configuration NestGen valide.'
|
|
676
|
+
: 'Configuration NestGen absente ; les valeurs par défaut seront utilisées.',
|
|
677
|
+
config ? 'Aucune action requise.' : 'Exécute nestgen config init pour figer les choix du projet.',
|
|
678
|
+
config ? `orm=${config.orm}; profile=${config.profile}; packageManager=${config.packageManager}` : '',
|
|
377
679
|
);
|
|
680
|
+
if (config) {
|
|
681
|
+
const lockfile =
|
|
682
|
+
config.packageManager === 'npm'
|
|
683
|
+
? 'package-lock.json'
|
|
684
|
+
: config.packageManager === 'pnpm'
|
|
685
|
+
? 'pnpm-lock.yaml'
|
|
686
|
+
: 'yarn.lock';
|
|
687
|
+
add(
|
|
688
|
+
'PACKAGE_MANAGER_LOCKFILE',
|
|
689
|
+
fs.existsSync(path.join(projectRoot, lockfile)) ? 'INFO' : 'WARNING',
|
|
690
|
+
fs.existsSync(path.join(projectRoot, lockfile))
|
|
691
|
+
? `Le lockfile ${lockfile} correspond à la configuration.`
|
|
692
|
+
: `Le lockfile ${lockfile} est absent.`,
|
|
693
|
+
`Installe les dépendances avec ${config.packageManager} pour créer le lockfile attendu.`,
|
|
694
|
+
lockfile,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
378
697
|
} catch (error) {
|
|
379
|
-
|
|
380
|
-
|
|
698
|
+
add(
|
|
699
|
+
'NESTGEN_CONFIG',
|
|
700
|
+
'ERROR',
|
|
701
|
+
'Configuration NestGen invalide.',
|
|
702
|
+
'Corrige nestgen.config.json ou recrée-la.',
|
|
703
|
+
error.message,
|
|
381
704
|
);
|
|
382
705
|
}
|
|
383
|
-
|
|
706
|
+
let project;
|
|
707
|
+
try {
|
|
708
|
+
project = inspectProject(projectRoot, 'typeorm', { cqrs: false });
|
|
709
|
+
add(
|
|
710
|
+
'NEST_INTEGRATION',
|
|
711
|
+
'INFO',
|
|
712
|
+
'Structure Nest et intégration TypeORM valides.',
|
|
713
|
+
'Aucune action requise.',
|
|
714
|
+
project.sourceRoot,
|
|
715
|
+
);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
project = { error: error.message };
|
|
718
|
+
add(
|
|
719
|
+
'NEST_INTEGRATION',
|
|
720
|
+
'WARNING',
|
|
721
|
+
'Le projet ne remplit pas toutes les préconditions TypeORM.',
|
|
722
|
+
'Lis le diagnostic puis complète la structure ou les dépendances indiquées.',
|
|
723
|
+
error.message,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if (!options.json) {
|
|
727
|
+
const output = formatDoctorOutput(diagnostics, options);
|
|
728
|
+
if (output) console.log(output);
|
|
729
|
+
}
|
|
730
|
+
return { diagnostics, project };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function machineResult(command, result) {
|
|
734
|
+
return { version: 1, ok: true, command, result };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function errorCode(error) {
|
|
738
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
739
|
+
if (/^(Option inconnue|Commande inconnue|.* requiert |Route ou table invalide|Nom de )/.test(message))
|
|
740
|
+
return 'USAGE';
|
|
741
|
+
if (message.startsWith('Préflight échoué')) return 'PROJECT_INVALID';
|
|
742
|
+
if (/(existe déjà|Destination |Conflit |Transaction interrompue)/.test(message)) return 'CONFLICT';
|
|
743
|
+
return 'INTERNAL';
|
|
384
744
|
}
|
|
385
745
|
|
|
386
746
|
// ────── Entrée CLI
|
|
387
747
|
export async function main(args = process.argv.slice(2)) {
|
|
388
748
|
const parsed = parseCliArgs(args);
|
|
749
|
+
const machine = parsed.options.json;
|
|
750
|
+
if (machine) {
|
|
751
|
+
parsed.options.quiet = true;
|
|
752
|
+
parsed.options.color = false;
|
|
753
|
+
parsed.options.noInteractive = true;
|
|
754
|
+
}
|
|
389
755
|
if (!parsed.options.color) chalk.level = 0;
|
|
390
756
|
if (parsed.options.version) {
|
|
391
757
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
392
|
-
|
|
393
|
-
|
|
758
|
+
const result = { cliVersion: packageJson.version };
|
|
759
|
+
console.log(machine ? JSON.stringify(machineResult('version', result)) : packageJson.version);
|
|
760
|
+
return result;
|
|
394
761
|
}
|
|
395
762
|
if (parsed.options.help || !parsed.command) {
|
|
396
|
-
|
|
763
|
+
if (machine)
|
|
764
|
+
console.log(
|
|
765
|
+
JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
|
|
766
|
+
);
|
|
767
|
+
else printUsage();
|
|
397
768
|
return;
|
|
398
769
|
}
|
|
399
770
|
|
|
771
|
+
let result;
|
|
400
772
|
switch (parsed.command) {
|
|
401
773
|
case 'init':
|
|
402
|
-
|
|
774
|
+
if (parsed.positionals.length > 1) throw new Error('init accepte au plus un nom de projet.');
|
|
775
|
+
result = await runInteractiveInit({ ...parsed.options, projectName: parsed.positionals[0] });
|
|
403
776
|
break;
|
|
404
777
|
|
|
405
778
|
case 'module':
|
|
406
|
-
await runModuleGeneration(parsed);
|
|
779
|
+
result = await runModuleGeneration(parsed);
|
|
407
780
|
break;
|
|
408
781
|
|
|
409
782
|
case 'resource':
|
|
410
|
-
runResourceGeneration(parsed);
|
|
783
|
+
result = await runResourceGeneration(parsed);
|
|
411
784
|
break;
|
|
412
785
|
|
|
413
786
|
case 'config': {
|
|
414
787
|
const action = parsed.positionals[0];
|
|
415
788
|
if (action === 'show') {
|
|
416
|
-
|
|
789
|
+
result = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
790
|
+
if (!machine)
|
|
791
|
+
console.log(
|
|
792
|
+
formatConfig(
|
|
793
|
+
result,
|
|
794
|
+
fs.existsSync(path.join(process.cwd(), configFileName))
|
|
795
|
+
? configFileName
|
|
796
|
+
: 'Valeurs par défaut',
|
|
797
|
+
),
|
|
798
|
+
);
|
|
417
799
|
break;
|
|
418
800
|
}
|
|
419
801
|
if (action === 'init') {
|
|
@@ -426,28 +808,50 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
426
808
|
if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
|
|
427
809
|
throw new Error('Package manager invalide.');
|
|
428
810
|
if (parsed.options.dryRun) {
|
|
429
|
-
|
|
811
|
+
result = { operation: 'config-init', file: configFileName, config };
|
|
812
|
+
if (!parsed.options.quiet) console.log(formatConfig(config, 'Aperçu · aucun fichier écrit'));
|
|
430
813
|
break;
|
|
431
814
|
}
|
|
432
815
|
writeConfig(process.cwd(), config);
|
|
433
|
-
|
|
816
|
+
result = { file: configFileName, created: true, config };
|
|
817
|
+
if (!parsed.options.quiet) console.log(success(`Configuration créée : ${configFileName}`));
|
|
434
818
|
break;
|
|
435
819
|
}
|
|
436
820
|
throw new Error('config requiert init ou show.');
|
|
437
821
|
}
|
|
438
822
|
|
|
439
823
|
case 'doctor':
|
|
440
|
-
runDoctor();
|
|
824
|
+
result = runDoctor(parsed.options);
|
|
441
825
|
break;
|
|
442
826
|
|
|
443
827
|
default:
|
|
444
828
|
throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
|
|
445
829
|
}
|
|
830
|
+
if (machine) console.log(JSON.stringify(machineResult(parsed.command, result)));
|
|
831
|
+
else if (!parsed.options.quiet && result) {
|
|
832
|
+
if (parsed.command === 'module' && result.generated)
|
|
833
|
+
console.log(success(`Module ${result.module} généré (${result.orm}).`));
|
|
834
|
+
if (parsed.command === 'resource' && result.generated) {
|
|
835
|
+
console.log(
|
|
836
|
+
success(
|
|
837
|
+
`Ressource ${result.resource} ${result.updated ? 'mise à jour' : 'générée'} (${result.orm} · ${result.profile}).`,
|
|
838
|
+
),
|
|
839
|
+
);
|
|
840
|
+
if (result.migration) console.log(formatMigration(result.migration));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return result;
|
|
446
844
|
}
|
|
447
845
|
|
|
448
846
|
if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
|
|
449
847
|
main().catch((error) => {
|
|
450
|
-
|
|
848
|
+
if (process.argv.includes('--json')) {
|
|
849
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
850
|
+
console.error(JSON.stringify({ version: 1, ok: false, error: { code: errorCode(error), message } }));
|
|
851
|
+
} else {
|
|
852
|
+
if (process.argv.includes('--no-color')) chalk.level = 0;
|
|
853
|
+
console.error(formatError(error.message));
|
|
854
|
+
}
|
|
451
855
|
process.exitCode = 1;
|
|
452
856
|
});
|
|
453
857
|
}
|