nestgen-cli 0.0.7 → 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.
Files changed (35) hide show
  1. package/dist/engine/architecture-profile.js +10 -0
  2. package/dist/engine/file-transaction.js +166 -0
  3. package/dist/engine/generated-code.js +26 -0
  4. package/dist/engine/generation-manifest.js +118 -0
  5. package/dist/engine/migration-plan.js +78 -0
  6. package/dist/engine/module-generator.js +157 -0
  7. package/dist/engine/project-config.js +42 -0
  8. package/dist/engine/project-path.js +25 -0
  9. package/dist/engine/project-preflight.js +96 -0
  10. package/dist/engine/resource-generator.js +1153 -0
  11. package/dist/engine/resource-spec.js +287 -0
  12. package/examples/store-api/README.md +76 -0
  13. package/examples/store-api/definitions/customers.resource.json +14 -0
  14. package/examples/store-api/definitions/order-items.resource.json +26 -0
  15. package/examples/store-api/definitions/orders.resource.json +22 -0
  16. package/examples/store-api/definitions/products.resource.json +19 -0
  17. package/examples/store-api/nestgen.example.json +7 -0
  18. package/nestgen.js +700 -86
  19. package/nestjs-generator/features/add_module.sh +68 -24
  20. package/nestjs-generator/features/configure_bootstrap.mjs +42 -0
  21. package/nestjs-generator/features/docker.sh +169 -36
  22. package/nestjs-generator/features/git.sh +4 -2
  23. package/nestjs-generator/features/inject_module_to_app.sh +22 -69
  24. package/nestjs-generator/features/operational_foundation.mjs +210 -0
  25. package/nestjs-generator/features/operations.sh +16 -0
  26. package/nestjs-generator/features/preflight.mjs +13 -0
  27. package/nestjs-generator/features/prisma.sh +48 -8
  28. package/nestjs-generator/features/resource_name.mjs +57 -0
  29. package/nestjs-generator/features/swagger.sh +7 -20
  30. package/nestjs-generator/features/typeorm.sh +68 -7
  31. package/nestjs-generator/features/update_app_module.mjs +146 -0
  32. package/nestjs-generator/features/utils.sh +52 -14
  33. package/nestjs-generator/generate_project.sh +46 -70
  34. package/package.json +63 -31
  35. package/readme.md +298 -120
@@ -0,0 +1,210 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const [sourceDirectory, orm] = process.argv.slice(2);
5
+
6
+ if (!sourceDirectory || !['typeorm', 'prisma'].includes(orm)) {
7
+ console.error('❌ Usage : operational_foundation.mjs <source-directory> <typeorm|prisma>');
8
+ process.exitCode = 1;
9
+ } else {
10
+ const operationsDirectory = path.join(sourceDirectory, 'operations');
11
+ const appModule = path.join(sourceDirectory, 'app.module.ts');
12
+
13
+ if (!fs.existsSync(appModule)) {
14
+ console.error(`❌ AppModule introuvable : ${appModule}`);
15
+ process.exitCode = 1;
16
+ } else if (fs.existsSync(operationsDirectory)) {
17
+ console.error(`❌ ${operationsDirectory} existe déjà. Le socle d’exploitation n’a pas été remplacé.`);
18
+ process.exitCode = 1;
19
+ } else {
20
+ const databaseHealth =
21
+ orm === 'typeorm'
22
+ ? `import { Injectable } from '@nestjs/common';
23
+ import { InjectDataSource } from '@nestjs/typeorm';
24
+ import type { DataSource } from 'typeorm';
25
+
26
+ @Injectable()
27
+ export class DatabaseHealthService {
28
+ constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
29
+
30
+ async check(): Promise<{ status: 'up' | 'down' }> {
31
+ try {
32
+ if (!this.dataSource.isInitialized) return { status: 'down' };
33
+ await this.dataSource.query('SELECT 1');
34
+ return { status: 'up' };
35
+ } catch {
36
+ return { status: 'down' };
37
+ }
38
+ }
39
+ }
40
+ `
41
+ : `import { Injectable } from '@nestjs/common';
42
+ import { PrismaService } from '../prisma/prisma.service';
43
+
44
+ @Injectable()
45
+ export class DatabaseHealthService {
46
+ constructor(private readonly prisma: PrismaService) {}
47
+
48
+ async check(): Promise<{ status: 'up' | 'down' }> {
49
+ try {
50
+ await this.prisma.$queryRawUnsafe('SELECT 1');
51
+ return { status: 'up' };
52
+ } catch {
53
+ return { status: 'down' };
54
+ }
55
+ }
56
+ }
57
+ `;
58
+
59
+ const files = new Map([
60
+ [
61
+ 'request-context.ts',
62
+ `import { AsyncLocalStorage } from 'node:async_hooks';
63
+
64
+ export const requestContext = new AsyncLocalStorage<{ requestId: string }>();
65
+ `,
66
+ ],
67
+ [
68
+ 'request-id.middleware.ts',
69
+ `import { Injectable, type NestMiddleware } from '@nestjs/common';
70
+ import { randomUUID } from 'node:crypto';
71
+ import type { NextFunction, Request, Response } from 'express';
72
+ import { requestContext } from './request-context';
73
+
74
+ const REQUEST_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
75
+
76
+ @Injectable()
77
+ export class RequestIdMiddleware implements NestMiddleware {
78
+ use(request: Request, response: Response, next: NextFunction): void {
79
+ const candidate = request.header('x-request-id');
80
+ const requestId = candidate && REQUEST_ID.test(candidate) ? candidate : randomUUID();
81
+ response.setHeader('x-request-id', requestId);
82
+ requestContext.run({ requestId }, next);
83
+ }
84
+ }
85
+ `,
86
+ ],
87
+ [
88
+ 'structured-logger.service.ts',
89
+ `import { Injectable, type LoggerService } from '@nestjs/common';
90
+ import { requestContext } from './request-context';
91
+
92
+ const SENSITIVE_KEY = /password|secret|token|authorization|cookie|api[-_]?key/i;
93
+
94
+ export function redact(value: unknown): unknown {
95
+ if (Array.isArray(value)) return value.map(redact);
96
+ if (!value || typeof value !== 'object') return value;
97
+ if (value instanceof Error) return { name: value.name, message: value.message };
98
+ return Object.fromEntries(
99
+ Object.entries(value).map(([key, nested]) => [key, SENSITIVE_KEY.test(key) ? '[REDACTED]' : redact(nested)]),
100
+ );
101
+ }
102
+
103
+ @Injectable()
104
+ export class StructuredLogger implements LoggerService {
105
+ log(message: unknown, ...optional: unknown[]): void {
106
+ this.write('log', message, optional);
107
+ }
108
+
109
+ error(message: unknown, ...optional: unknown[]): void {
110
+ this.write('error', message, optional);
111
+ }
112
+
113
+ warn(message: unknown, ...optional: unknown[]): void {
114
+ this.write('warn', message, optional);
115
+ }
116
+
117
+ debug(message: unknown, ...optional: unknown[]): void {
118
+ this.write('debug', message, optional);
119
+ }
120
+
121
+ verbose(message: unknown, ...optional: unknown[]): void {
122
+ this.write('verbose', message, optional);
123
+ }
124
+
125
+ private write(level: string, message: unknown, optional: unknown[]): void {
126
+ const context = requestContext.getStore();
127
+ const record = {
128
+ timestamp: new Date().toISOString(),
129
+ level,
130
+ message: typeof message === 'string' ? message : redact(message),
131
+ ...(context ? { requestId: context.requestId } : {}),
132
+ ...(optional.length > 0 ? { details: redact(optional) } : {}),
133
+ };
134
+ process[level === 'error' ? 'stderr' : 'stdout'].write(JSON.stringify(record) + '\\n');
135
+ }
136
+ }
137
+ `,
138
+ ],
139
+ ['database-health.service.ts', databaseHealth],
140
+ [
141
+ 'health.controller.ts',
142
+ `import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
143
+ import { DatabaseHealthService } from './database-health.service';
144
+
145
+ @Controller('health')
146
+ export class HealthController {
147
+ constructor(private readonly databaseHealth: DatabaseHealthService) {}
148
+
149
+ @Get('live')
150
+ live(): { status: 'ok'; checks: { application: 'up' } } {
151
+ return { status: 'ok', checks: { application: 'up' } };
152
+ }
153
+
154
+ @Get('ready')
155
+ async ready(): Promise<{ status: 'ok'; checks: { database: 'up' } }> {
156
+ const database = await this.databaseHealth.check();
157
+ if (database.status === 'down') {
158
+ throw new ServiceUnavailableException({ status: 'error', checks: { database: 'down' } });
159
+ }
160
+ return { status: 'ok', checks: { database: 'up' } };
161
+ }
162
+ }
163
+ `,
164
+ ],
165
+ [
166
+ 'operations.module.ts',
167
+ `import { MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
168
+ ${orm === 'prisma' ? "import { PrismaModule } from '../prisma/prisma.module';\n" : ''}import { DatabaseHealthService } from './database-health.service';
169
+ import { HealthController } from './health.controller';
170
+ import { RequestIdMiddleware } from './request-id.middleware';
171
+ import { StructuredLogger } from './structured-logger.service';
172
+
173
+ @Module({
174
+ imports: [${orm === 'prisma' ? 'PrismaModule' : ''}],
175
+ controllers: [HealthController],
176
+ providers: [DatabaseHealthService, RequestIdMiddleware, StructuredLogger],
177
+ exports: [StructuredLogger],
178
+ })
179
+ export class OperationsModule implements NestModule {
180
+ configure(consumer: MiddlewareConsumer): void {
181
+ consumer.apply(RequestIdMiddleware).forRoutes('*');
182
+ }
183
+ }
184
+ `,
185
+ ],
186
+ [
187
+ 'README.md',
188
+ `# Socle d’exploitation
189
+
190
+ Cette option est locale au projet et ne requiert aucun service externe. Elle expose :
191
+
192
+ - \`GET /health/live\` : le processus Nest peut répondre ; aucun appel de dépendance n’est fait.
193
+ - \`GET /health/ready\` : le processus et la base PostgreSQL sont prêts. Une base indisponible retourne \`503\` avec \`{ status: 'error', checks: { database: 'down' } }\`.
194
+
195
+ Chaque réponse reçoit \`x-request-id\`. Une valeur cliente sûre est conservée ; sinon NestGen en crée une. Le logger Nest produit un JSON par ligne avec cet identifiant, et masque récursivement les clés contenant \`password\`, \`secret\`, \`token\`, \`authorization\`, \`cookie\` ou \`apiKey\`.
196
+
197
+ \`main.ts\` active \`enableShutdownHooks()\`. Ainsi, \`SIGTERM\` et \`SIGINT\` ferment le contexte Nest ; TypeORM ferme sa connexion et Prisma exécute son \`onModuleDestroy\` existant. Les migrations restent explicites et ne sont jamais appliquées par ces endpoints.
198
+ `,
199
+ ],
200
+ ]);
201
+
202
+ fs.mkdirSync(operationsDirectory, { recursive: true });
203
+ try {
204
+ for (const [name, content] of files) fs.writeFileSync(path.join(operationsDirectory, name), content);
205
+ } catch (error) {
206
+ fs.rmSync(operationsDirectory, { recursive: true, force: true });
207
+ throw error;
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ORM=${1:?ORM requis}
6
+ FEATURES_PATH="$(dirname "$0")"
7
+
8
+ if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
9
+ echo "❌ ORM non supporté : $ORM" >&2
10
+ exit 1
11
+ fi
12
+
13
+ node "$FEATURES_PATH/operational_foundation.mjs" src "$ORM"
14
+ node "$FEATURES_PATH/update_app_module.mjs" src/app.module.ts OperationsModule './operations/operations.module' "$ORM"
15
+
16
+ echo "✅ Socle d’exploitation configuré : /health/live et /health/ready."
@@ -0,0 +1,13 @@
1
+ import { inspectProject } from '../../dist/engine/project-preflight.js';
2
+
3
+ export { inspectProject };
4
+
5
+ if (import.meta.url === `file://${process.argv[1]}`) {
6
+ try {
7
+ const [projectRoot, orm] = process.argv.slice(2);
8
+ process.stdout.write(`${JSON.stringify(inspectProject(projectRoot, orm))}\n`);
9
+ } catch (cause) {
10
+ console.error(`❌ ${cause.message}`);
11
+ process.exitCode = 1;
12
+ }
13
+ }
@@ -1,21 +1,32 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
2
4
 
3
5
  # ────── Charger les helpers ──────
4
- FEATURES_PATH="$(dirname "$0")/features"
6
+ FEATURES_PATH="$(dirname "$0")"
5
7
  source "$FEATURES_PATH/utils.sh"
6
8
  source "$FEATURES_PATH/logger.sh"
7
9
 
8
10
 
9
- PM=$1
10
- APP_NAME=$2
11
+ PM=${1:?Package manager requis}
12
+ APP_NAME=${2:?Nom_application_requis}
13
+
14
+ if [ -e prisma/schema.prisma ]; then
15
+ echo "❌ prisma/schema.prisma existe déjà. Aucune configuration Prisma n'a été remplacée."
16
+ exit 1
17
+ fi
11
18
 
12
- $PM install prisma --save-dev
13
- $PM install @prisma/client
14
- npx prisma init
19
+ pm_add_dev "$PM" prisma@6
20
+ pm_add "$PM" @prisma/client@6
21
+ pm_exec "$PM" prisma init
15
22
 
23
+ if [ ! -e .env ]; then
16
24
  cat > .env <<EOF
17
25
  DATABASE_URL="postgresql://postgres:postgres@localhost:5432/$APP_NAME"
18
26
  EOF
27
+ else
28
+ echo "ℹ️ .env existe déjà et a été conservé."
29
+ fi
19
30
 
20
31
  cat > prisma/schema.prisma <<EOF
21
32
  generator client {
@@ -28,6 +39,35 @@ datasource db {
28
39
  }
29
40
  EOF
30
41
 
31
- npx prisma generate
42
+ mkdir -p src/prisma
43
+ cat > src/prisma/prisma.service.ts <<'EOF'
44
+ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
45
+ import { PrismaClient } from '@prisma/client';
46
+
47
+ @Injectable()
48
+ export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
49
+ async onModuleInit(): Promise<void> {
50
+ await this.$connect();
51
+ }
52
+
53
+ async onModuleDestroy(): Promise<void> {
54
+ await this.$disconnect();
55
+ }
56
+ }
57
+ EOF
58
+
59
+ cat > src/prisma/prisma.module.ts <<'EOF'
60
+ import { Global, Module } from '@nestjs/common';
61
+ import { PrismaService } from './prisma.service';
62
+
63
+ @Global()
64
+ @Module({
65
+ providers: [PrismaService],
66
+ exports: [PrismaService],
67
+ })
68
+ export class PrismaModule {}
69
+ EOF
70
+
71
+ pm_exec "$PM" prisma generate
32
72
 
33
73
  echo "✅ Prisma initialisé avec succès !"
@@ -0,0 +1,57 @@
1
+ const RESOURCE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
2
+ const ROUTE_PATTERN = /^[a-z][a-z0-9-]*$/;
3
+ const TABLE_PATTERN = /^[a-z][a-z0-9_]*$/;
4
+
5
+ function fail(message) {
6
+ throw new Error(`Nom de ressource invalide : ${message}`);
7
+ }
8
+
9
+ function pluralize(word) {
10
+ if (/(?:s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
11
+ if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
12
+ return `${word}s`;
13
+ }
14
+
15
+ export function describeResource(rawName, options = {}) {
16
+ const name = String(rawName ?? '').trim();
17
+ if (!RESOURCE_NAME_PATTERN.test(name)) {
18
+ fail('il doit commencer par une lettre et ne contenir que des lettres, chiffres, tirets ou underscores.');
19
+ }
20
+
21
+ const segments = name.toLowerCase().split(/[-_]/);
22
+ const singular = segments.join('-');
23
+ const plural = options.plural
24
+ ? String(options.plural).trim().toLowerCase()
25
+ : `${segments.slice(0, -1).join('-')}${segments.length > 1 ? '-' : ''}${pluralize(segments.at(-1))}`;
26
+ const route = options.route ? String(options.route).trim().toLowerCase() : plural;
27
+ const table = options.table ? String(options.table).trim().toLowerCase() : plural.replaceAll('-', '_');
28
+
29
+ if (!ROUTE_PATTERN.test(plural)) fail('le pluriel doit être une ressource kebab-case.');
30
+ if (!ROUTE_PATTERN.test(route)) fail('la route doit être une ressource kebab-case.');
31
+ if (!TABLE_PATTERN.test(table)) fail('la table doit être une ressource snake_case.');
32
+
33
+ const pascal = segments.map((segment) => `${segment[0].toUpperCase()}${segment.slice(1)}`).join('');
34
+ return {
35
+ name: singular,
36
+ pascal,
37
+ camel: `${pascal[0].toLowerCase()}${pascal.slice(1)}`,
38
+ plural,
39
+ route,
40
+ table,
41
+ };
42
+ }
43
+
44
+ if (import.meta.url === `file://${process.argv[1]}`) {
45
+ try {
46
+ const [rawName] = process.argv.slice(2);
47
+ const resource = describeResource(rawName, {
48
+ plural: process.env.RESOURCE_PLURAL,
49
+ route: process.env.RESOURCE_ROUTE,
50
+ table: process.env.RESOURCE_TABLE,
51
+ });
52
+ for (const [key, value] of Object.entries(resource)) console.log(`${key.toUpperCase()}=${value}`);
53
+ } catch (error) {
54
+ console.error(`❌ ${error.message}`);
55
+ process.exitCode = 1;
56
+ }
57
+ }
@@ -1,27 +1,14 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
2
4
 
3
5
  # ────── Charger les helpers ──────
4
- FEATURES_PATH="$(dirname "$0")/features"
6
+ FEATURES_PATH="$(dirname "$0")"
5
7
  source "$FEATURES_PATH/utils.sh"
6
8
  source "$FEATURES_PATH/logger.sh"
7
9
 
8
10
 
9
- PM=$1
10
-
11
- $PM install @nestjs/swagger swagger-ui-express
12
-
13
- # Ajout dans main.ts (à faire manuellement ou via automatisation)
14
- echo ""
15
- echo "📘 Pour activer Swagger, ajoute ceci dans main.ts :"
16
- echo ""
17
- cat <<'DOC'
18
- import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
11
+ PM=${1:?Package manager requis}
19
12
 
20
- const config = new DocumentBuilder()
21
- .setTitle('API Docs')
22
- .setDescription('The API description')
23
- .setVersion('1.0')
24
- .build();
25
- const document = SwaggerModule.createDocument(app, config);
26
- SwaggerModule.setup('api', app, document);
27
- DOC
13
+ pm_add "$PM" @nestjs/swagger@12 swagger-ui-express@5
14
+ log_success "Swagger sera activé sur /api dans src/main.ts."
@@ -1,19 +1,80 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
2
 
3
- PM=$1
4
- APP_NAME=$2
3
+ set -euo pipefail
4
+
5
+ PM=${1:?Package manager requis}
6
+ APP_NAME=${2:?Nom_application_requis}
5
7
 
6
8
  # ────── Charger les helpers ──────
7
9
  FEATURES_PATH="$(dirname "$0")"
8
10
  source "$FEATURES_PATH/utils.sh"
9
11
  source "$FEATURES_PATH/logger.sh"
10
12
 
11
- INSTALL_CMD=$(get_install_cmd "$PM")
12
-
13
13
  log_info "📦 Installation de TypeORM et PostgreSQL..."
14
14
 
15
- $PM $INSTALL_CMD @nestjs/typeorm typeorm pg
15
+ pm_add "$PM" @nestjs/typeorm@12 typeorm@0.3 pg@8 dotenv@16
16
+ pm_add_dev "$PM" ts-node@10
17
+
18
+ mkdir -p src/database/migrations
19
+
20
+ cat > src/database/typeorm.config.ts <<'EOF'
21
+ import type { TypeOrmModuleOptions } from '@nestjs/typeorm';
22
+
23
+ function required(name: string): string {
24
+ const value = process.env[name];
25
+ if (!value) throw new Error(`Variable d'environnement manquante : ${name}`);
26
+ return value;
27
+ }
28
+
29
+ export function typeOrmOptions(): TypeOrmModuleOptions {
30
+ return {
31
+ type: 'postgres',
32
+ host: required('DATABASE_HOST'),
33
+ port: Number(process.env.DATABASE_PORT ?? 5432),
34
+ username: required('DATABASE_USER'),
35
+ password: required('DATABASE_PASSWORD'),
36
+ database: required('DATABASE_NAME'),
37
+ autoLoadEntities: true,
38
+ synchronize: false,
39
+ migrationsRun: false,
40
+ migrations: ['dist/database/migrations/*.js'],
41
+ };
42
+ }
43
+ EOF
44
+
45
+ cat > src/database/data-source.ts <<'EOF'
46
+ import 'dotenv/config';
47
+ import { DataSource, type DataSourceOptions } from 'typeorm';
48
+ import { typeOrmOptions } from './typeorm.config';
49
+
50
+ const options = typeOrmOptions();
51
+
52
+ export default new DataSource({
53
+ ...(options as DataSourceOptions),
54
+ entities: ['src/**/*.entity{.ts,.js}'],
55
+ migrations: ['src/database/migrations/*{.ts,.js}'],
56
+ });
57
+ EOF
58
+
59
+ cat > src/database/migrations/0000000000000-InitialSchema.ts <<'EOF'
60
+ import type { MigrationInterface, QueryRunner } from 'typeorm';
61
+
62
+ export class InitialSchema0000000000000 implements MigrationInterface {
63
+ async up(_queryRunner: QueryRunner): Promise<void> {}
64
+ async down(_queryRunner: QueryRunner): Promise<void> {}
65
+ }
66
+ EOF
67
+
68
+ if [ ! -e .env.example ]; then
69
+ cat > .env.example <<'EOF'
70
+ DATABASE_HOST=localhost
71
+ DATABASE_PORT=5432
72
+ DATABASE_USER=postgres
73
+ DATABASE_PASSWORD=change-me
74
+ DATABASE_NAME=appdb
75
+ EOF
76
+ fi
16
77
 
17
78
  log_success "✅ TypeORM installé avec succès"
18
79
 
19
- log_info "💡 TypeORM sera automatiquement configuré dans app.module.ts"
80
+ log_info "💡 TypeORM utilise src/database/typeorm.config.ts et src/database/data-source.ts pour des migrations explicites."
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs';
2
+
3
+ const [appModulePath, moduleClass, modulePath, orm] = process.argv.slice(2);
4
+
5
+ function fail(message) {
6
+ throw new Error(`Impossible de mettre à jour AppModule : ${message}`);
7
+ }
8
+
9
+ function findMatching(source, start, open, close) {
10
+ let depth = 0;
11
+ let quote = null;
12
+ let escaped = false;
13
+ let lineComment = false;
14
+ let blockComment = false;
15
+
16
+ for (let index = start; index < source.length; index += 1) {
17
+ const character = source[index];
18
+ const next = source[index + 1];
19
+
20
+ if (lineComment) {
21
+ if (character === '\n') lineComment = false;
22
+ continue;
23
+ }
24
+ if (blockComment) {
25
+ if (character === '*' && next === '/') {
26
+ blockComment = false;
27
+ index += 1;
28
+ }
29
+ continue;
30
+ }
31
+ if (quote) {
32
+ if (!escaped && character === quote) quote = null;
33
+ escaped = !escaped && character === '\\';
34
+ if (character !== '\\') escaped = false;
35
+ continue;
36
+ }
37
+ if (character === '/' && next === '/') {
38
+ lineComment = true;
39
+ index += 1;
40
+ continue;
41
+ }
42
+ if (character === '/' && next === '*') {
43
+ blockComment = true;
44
+ index += 1;
45
+ continue;
46
+ }
47
+ if (character === '"' || character === "'" || character === '`') {
48
+ quote = character;
49
+ continue;
50
+ }
51
+ if (character === open) depth += 1;
52
+ if (character === close) {
53
+ depth -= 1;
54
+ if (depth === 0) return index;
55
+ }
56
+ }
57
+
58
+ fail(`délimiteur ${open}${close} non fermé.`);
59
+ }
60
+
61
+ function findDecoratorObject(source) {
62
+ const decoratorMatch = /@Module\s*\(/.exec(source);
63
+ if (!decoratorMatch) fail('décorateur @Module introuvable.');
64
+
65
+ const objectStart = source.indexOf('{', decoratorMatch.index + decoratorMatch[0].length);
66
+ if (objectStart === -1) fail('objet du décorateur @Module introuvable.');
67
+
68
+ return {
69
+ start: objectStart,
70
+ end: findMatching(source, objectStart, '{', '}'),
71
+ };
72
+ }
73
+
74
+ function findImportsArray(source, object) {
75
+ const propertyPattern = /\bimports\s*:\s*\[/g;
76
+ let match;
77
+
78
+ while ((match = propertyPattern.exec(source)) !== null) {
79
+ if (match.index > object.end) break;
80
+ const before = source.slice(object.start + 1, match.index);
81
+ let depth = 0;
82
+ for (const character of before) {
83
+ if (character === '{') depth += 1;
84
+ if (character === '}') depth -= 1;
85
+ }
86
+ if (depth !== 0) continue;
87
+
88
+ const arrayStart = match.index + match[0].lastIndexOf('[');
89
+ return { start: arrayStart, end: findMatching(source, arrayStart, '[', ']') };
90
+ }
91
+
92
+ fail('tableau imports dans @Module introuvable ou non supporté.');
93
+ }
94
+
95
+ function containsToken(items, token) {
96
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
+ return new RegExp(`\\b${escaped}\\b`).test(items);
98
+ }
99
+
100
+ function appendImports(source, array, additions) {
101
+ if (additions.length === 0) return source;
102
+
103
+ const current = source.slice(array.start + 1, array.end);
104
+ const separator = current.trim() === '' ? '' : current.trimEnd().endsWith(',') ? '\n' : ',\n';
105
+ const indentation = current.includes('\n') ? (current.match(/\n([ \t]*)[^\n]*$/)?.[1] ?? ' ') : '';
106
+ const inserted = additions.map((item) => `${indentation}${item}`).join(',\n');
107
+ const replacement =
108
+ current.trim() === ''
109
+ ? current.includes('\n')
110
+ ? `\n${inserted}\n `
111
+ : inserted
112
+ : `${current}${separator}${inserted}`;
113
+
114
+ return `${source.slice(0, array.start + 1)}${replacement}${source.slice(array.end)}`;
115
+ }
116
+
117
+ function addImport(source, line) {
118
+ return source.includes(line) ? source : `${line}\n${source}`;
119
+ }
120
+
121
+ try {
122
+ let source = fs.readFileSync(appModulePath, 'utf8');
123
+ const object = findDecoratorObject(source);
124
+ const array = findImportsArray(source, object);
125
+ const currentImports = source.slice(array.start + 1, array.end);
126
+ const additions = [];
127
+
128
+ if (!containsToken(currentImports, 'CqrsModule')) additions.push('CqrsModule');
129
+ if (!containsToken(currentImports, moduleClass)) additions.push(moduleClass);
130
+ if (orm === 'typeorm' && !/\bTypeOrmModule\.forRoot(?:Async)?\s*\(/.test(currentImports)) {
131
+ additions.unshift('TypeOrmModule.forRootAsync({ useFactory: typeOrmOptions })');
132
+ }
133
+
134
+ source = appendImports(source, array, additions);
135
+ source = addImport(source, `import { ${moduleClass} } from '${modulePath}';`);
136
+ source = addImport(source, "import { CqrsModule } from '@nestjs/cqrs';");
137
+ if (orm === 'typeorm') {
138
+ source = addImport(source, "import { typeOrmOptions } from './database/typeorm.config';");
139
+ source = addImport(source, "import { TypeOrmModule } from '@nestjs/typeorm';");
140
+ }
141
+
142
+ fs.writeFileSync(appModulePath, source);
143
+ } catch (error) {
144
+ console.error(`❌ ${error.message}`);
145
+ process.exitCode = 1;
146
+ }