nestgen-cli 0.1.0 → 0.2.1
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/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 +450 -71
- package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
- package/nestjs-generator/features/operational_foundation.mjs +210 -0
- package/nestjs-generator/features/operations.sh +16 -0
- package/nestjs-generator/features/preflight.mjs +2 -44
- package/nestjs-generator/generate_project.sh +7 -1
- package/package.json +11 -4
- package/readme.md +234 -13
package/nestgen.js
CHANGED
|
@@ -7,11 +7,17 @@ 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';
|
|
15
21
|
|
|
16
22
|
// ────── Resolve __dirname compatible ES Modules
|
|
17
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -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,6 +205,25 @@ 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
229
|
env: { ...process.env, ...env },
|
|
@@ -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('\n🚀 Lancement de la génération du projet...\n');
|
|
276
|
-
runBashScript(GENERATE_SCRIPT, [], env);
|
|
373
|
+
if (!options.quiet) console.log('\n🚀 Lancement de la génération du projet...\n');
|
|
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,375 @@ 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(JSON.stringify(output, null, 2));
|
|
423
|
+
return output;
|
|
317
424
|
}
|
|
318
425
|
|
|
319
426
|
if (!parsed.options.quiet) console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
|
|
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}`);
|
|
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}`);
|
|
323
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(JSON.stringify(output, null, 2));
|
|
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
|
+
`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(machine = false) {
|
|
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
|
+
add(
|
|
591
|
+
'NODE_VERSION',
|
|
592
|
+
nodeMajor === 24 && nodeMinor >= 15 ? 'INFO' : 'ERROR',
|
|
593
|
+
nodeMajor === 24 && nodeMinor >= 15 ? 'Version Node.js compatible.' : 'Node.js doit être >=24.15.0 et <27.',
|
|
594
|
+
'Installe une version Node.js supportée puis relance doctor.',
|
|
595
|
+
process.versions.node,
|
|
596
|
+
);
|
|
597
|
+
add(
|
|
598
|
+
'GENERATOR_SCRIPTS',
|
|
599
|
+
fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT) ? 'INFO' : 'ERROR',
|
|
600
|
+
fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT)
|
|
601
|
+
? 'Scripts du générateur disponibles.'
|
|
602
|
+
: 'Un script du générateur est introuvable.',
|
|
603
|
+
'Réinstalle NestGen.',
|
|
604
|
+
`${GENERATE_SCRIPT}, ${ADD_MODULE_SCRIPT}`,
|
|
605
|
+
);
|
|
606
|
+
const projectRoot = process.cwd();
|
|
607
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
608
|
+
let manifest;
|
|
609
|
+
if (!fs.existsSync(packagePath)) {
|
|
610
|
+
add(
|
|
611
|
+
'PROJECT_PACKAGE',
|
|
612
|
+
'WARNING',
|
|
613
|
+
'Aucun package.json de projet détecté.',
|
|
614
|
+
'Exécute doctor à la racine du projet Nest.',
|
|
615
|
+
packagePath,
|
|
616
|
+
);
|
|
617
|
+
} else {
|
|
618
|
+
try {
|
|
619
|
+
manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
620
|
+
const dependencies = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
|
|
621
|
+
for (const dependency of ['@nestjs/common', '@nestjs/core'])
|
|
622
|
+
add(
|
|
623
|
+
`DEPENDENCY_${dependency.replace(/^@/, '').replaceAll(/[/-]/g, '_').toUpperCase()}`,
|
|
624
|
+
dependencies[dependency] ? 'INFO' : 'ERROR',
|
|
625
|
+
dependencies[dependency] ? `${dependency} est déclaré.` : `${dependency} est absent.`,
|
|
626
|
+
`Installe ${dependency} avec le package manager du projet.`,
|
|
627
|
+
dependencies[dependency] ?? '',
|
|
628
|
+
);
|
|
629
|
+
const nestVersion = String(dependencies['@nestjs/common'] ?? '');
|
|
630
|
+
if (nestVersion)
|
|
631
|
+
add(
|
|
632
|
+
'NEST_VERSION',
|
|
633
|
+
/(?:\^|~)?12(?:\.|$)/.test(nestVersion) ? 'INFO' : 'WARNING',
|
|
634
|
+
/(?:\^|~)?12(?:\.|$)/.test(nestVersion)
|
|
635
|
+
? 'La version Nest déclarée est supportée.'
|
|
636
|
+
: 'La version Nest déclarée est hors de la matrice testée.',
|
|
637
|
+
'Utilise Nest 12 ou vérifie cette combinaison avec la matrice de compatibilité.',
|
|
638
|
+
nestVersion,
|
|
639
|
+
);
|
|
640
|
+
if (dependencies['@nestjs/typeorm'] || dependencies.typeorm)
|
|
641
|
+
add(
|
|
642
|
+
'TYPEORM_COHERENCE',
|
|
643
|
+
dependencies['@nestjs/typeorm'] && dependencies.typeorm ? 'INFO' : 'WARNING',
|
|
644
|
+
dependencies['@nestjs/typeorm'] && dependencies.typeorm
|
|
645
|
+
? 'Les dépendances TypeORM sont cohérentes.'
|
|
646
|
+
: 'Le support TypeORM est incomplet.',
|
|
647
|
+
'Installe @nestjs/typeorm et typeorm ensemble, ou utilise Prisma.',
|
|
648
|
+
'',
|
|
649
|
+
);
|
|
650
|
+
if (dependencies['@prisma/client'])
|
|
651
|
+
add(
|
|
652
|
+
'PRISMA_CLIENT',
|
|
653
|
+
'INFO',
|
|
654
|
+
'@prisma/client est déclaré.',
|
|
655
|
+
'Exécute npx prisma generate après une modification du schéma.',
|
|
656
|
+
dependencies['@prisma/client'],
|
|
657
|
+
);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
add(
|
|
660
|
+
'PROJECT_PACKAGE',
|
|
661
|
+
'ERROR',
|
|
662
|
+
'package.json est invalide.',
|
|
663
|
+
'Corrige le JSON du manifeste.',
|
|
664
|
+
error.message,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
373
668
|
try {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
669
|
+
const config = loadConfigIfPresent(projectRoot);
|
|
670
|
+
add(
|
|
671
|
+
'NESTGEN_CONFIG',
|
|
672
|
+
'INFO',
|
|
673
|
+
config
|
|
674
|
+
? 'Configuration NestGen valide.'
|
|
675
|
+
: 'Configuration NestGen absente ; les valeurs par défaut seront utilisées.',
|
|
676
|
+
config ? 'Aucune action requise.' : 'Exécute nestgen config init pour figer les choix du projet.',
|
|
677
|
+
config ? `orm=${config.orm}; profile=${config.profile}; packageManager=${config.packageManager}` : '',
|
|
377
678
|
);
|
|
679
|
+
if (config) {
|
|
680
|
+
const lockfile =
|
|
681
|
+
config.packageManager === 'npm'
|
|
682
|
+
? 'package-lock.json'
|
|
683
|
+
: config.packageManager === 'pnpm'
|
|
684
|
+
? 'pnpm-lock.yaml'
|
|
685
|
+
: 'yarn.lock';
|
|
686
|
+
add(
|
|
687
|
+
'PACKAGE_MANAGER_LOCKFILE',
|
|
688
|
+
fs.existsSync(path.join(projectRoot, lockfile)) ? 'INFO' : 'WARNING',
|
|
689
|
+
fs.existsSync(path.join(projectRoot, lockfile))
|
|
690
|
+
? `Le lockfile ${lockfile} correspond à la configuration.`
|
|
691
|
+
: `Le lockfile ${lockfile} est absent.`,
|
|
692
|
+
`Installe les dépendances avec ${config.packageManager} pour créer le lockfile attendu.`,
|
|
693
|
+
lockfile,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
378
696
|
} catch (error) {
|
|
379
|
-
|
|
380
|
-
|
|
697
|
+
add(
|
|
698
|
+
'NESTGEN_CONFIG',
|
|
699
|
+
'ERROR',
|
|
700
|
+
'Configuration NestGen invalide.',
|
|
701
|
+
'Corrige nestgen.config.json ou recrée-la.',
|
|
702
|
+
error.message,
|
|
381
703
|
);
|
|
382
704
|
}
|
|
383
|
-
|
|
705
|
+
let project;
|
|
706
|
+
try {
|
|
707
|
+
project = inspectProject(projectRoot, 'typeorm', { cqrs: false });
|
|
708
|
+
add(
|
|
709
|
+
'NEST_INTEGRATION',
|
|
710
|
+
'INFO',
|
|
711
|
+
'Structure Nest et intégration TypeORM valides.',
|
|
712
|
+
'Aucune action requise.',
|
|
713
|
+
project.sourceRoot,
|
|
714
|
+
);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
project = { error: error.message };
|
|
717
|
+
add(
|
|
718
|
+
'NEST_INTEGRATION',
|
|
719
|
+
'WARNING',
|
|
720
|
+
'Le projet ne remplit pas toutes les préconditions TypeORM.',
|
|
721
|
+
'Lis le diagnostic puis complète la structure ou les dépendances indiquées.',
|
|
722
|
+
error.message,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (!machine)
|
|
726
|
+
for (const diagnostic of diagnostics)
|
|
727
|
+
console.log(`${diagnostic.severity} ${diagnostic.id}: ${diagnostic.cause} — ${diagnostic.action}`);
|
|
728
|
+
return { diagnostics, project };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function machineResult(command, result) {
|
|
732
|
+
return { version: 1, ok: true, command, result };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function errorCode(error) {
|
|
736
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
737
|
+
if (/^(Option inconnue|Commande inconnue|.* requiert |Route ou table invalide|Nom de )/.test(message))
|
|
738
|
+
return 'USAGE';
|
|
739
|
+
if (message.startsWith('Préflight échoué')) return 'PROJECT_INVALID';
|
|
740
|
+
if (/(existe déjà|Destination |Conflit |Transaction interrompue)/.test(message)) return 'CONFLICT';
|
|
741
|
+
return 'INTERNAL';
|
|
384
742
|
}
|
|
385
743
|
|
|
386
744
|
// ────── Entrée CLI
|
|
387
745
|
export async function main(args = process.argv.slice(2)) {
|
|
388
746
|
const parsed = parseCliArgs(args);
|
|
747
|
+
const machine = parsed.options.json;
|
|
748
|
+
if (machine) {
|
|
749
|
+
parsed.options.quiet = true;
|
|
750
|
+
parsed.options.color = false;
|
|
751
|
+
parsed.options.noInteractive = true;
|
|
752
|
+
}
|
|
389
753
|
if (!parsed.options.color) chalk.level = 0;
|
|
390
754
|
if (parsed.options.version) {
|
|
391
755
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
392
|
-
|
|
393
|
-
|
|
756
|
+
const result = { cliVersion: packageJson.version };
|
|
757
|
+
console.log(machine ? JSON.stringify(machineResult('version', result)) : packageJson.version);
|
|
758
|
+
return result;
|
|
394
759
|
}
|
|
395
760
|
if (parsed.options.help || !parsed.command) {
|
|
396
|
-
|
|
761
|
+
if (machine)
|
|
762
|
+
console.log(
|
|
763
|
+
JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
|
|
764
|
+
);
|
|
765
|
+
else printUsage();
|
|
397
766
|
return;
|
|
398
767
|
}
|
|
399
768
|
|
|
769
|
+
let result;
|
|
400
770
|
switch (parsed.command) {
|
|
401
771
|
case 'init':
|
|
402
|
-
|
|
772
|
+
if (parsed.positionals.length > 1) throw new Error('init accepte au plus un nom de projet.');
|
|
773
|
+
result = await runInteractiveInit({ ...parsed.options, projectName: parsed.positionals[0] });
|
|
403
774
|
break;
|
|
404
775
|
|
|
405
776
|
case 'module':
|
|
406
|
-
await runModuleGeneration(parsed);
|
|
777
|
+
result = await runModuleGeneration(parsed);
|
|
407
778
|
break;
|
|
408
779
|
|
|
409
780
|
case 'resource':
|
|
410
|
-
runResourceGeneration(parsed);
|
|
781
|
+
result = await runResourceGeneration(parsed);
|
|
411
782
|
break;
|
|
412
783
|
|
|
413
784
|
case 'config': {
|
|
414
785
|
const action = parsed.positionals[0];
|
|
415
786
|
if (action === 'show') {
|
|
416
|
-
|
|
787
|
+
result = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
788
|
+
if (!machine) console.log(JSON.stringify(result, null, 2));
|
|
417
789
|
break;
|
|
418
790
|
}
|
|
419
791
|
if (action === 'init') {
|
|
@@ -426,28 +798,35 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
426
798
|
if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
|
|
427
799
|
throw new Error('Package manager invalide.');
|
|
428
800
|
if (parsed.options.dryRun) {
|
|
429
|
-
|
|
801
|
+
result = { operation: 'config-init', file: configFileName, config };
|
|
802
|
+
if (!machine) console.log(JSON.stringify(result, null, 2));
|
|
430
803
|
break;
|
|
431
804
|
}
|
|
432
805
|
writeConfig(process.cwd(), config);
|
|
433
|
-
|
|
806
|
+
result = { file: configFileName, created: true, config };
|
|
807
|
+
if (!machine) console.log(configFileName);
|
|
434
808
|
break;
|
|
435
809
|
}
|
|
436
810
|
throw new Error('config requiert init ou show.');
|
|
437
811
|
}
|
|
438
812
|
|
|
439
813
|
case 'doctor':
|
|
440
|
-
runDoctor();
|
|
814
|
+
result = runDoctor(machine);
|
|
441
815
|
break;
|
|
442
816
|
|
|
443
817
|
default:
|
|
444
818
|
throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
|
|
445
819
|
}
|
|
820
|
+
if (machine) console.log(JSON.stringify(machineResult(parsed.command, result)));
|
|
821
|
+
return result;
|
|
446
822
|
}
|
|
447
823
|
|
|
448
824
|
if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
|
|
449
825
|
main().catch((error) => {
|
|
450
|
-
|
|
826
|
+
if (process.argv.includes('--json')) {
|
|
827
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
828
|
+
console.error(JSON.stringify({ version: 1, ok: false, error: { code: errorCode(error), message } }));
|
|
829
|
+
} else console.error(chalk.red(`❌ ${error.message}`));
|
|
451
830
|
process.exitCode = 1;
|
|
452
831
|
});
|
|
453
832
|
}
|