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.
@@ -1,18 +1,21 @@
1
1
  import fs from 'node:fs';
2
2
 
3
- const [mainPath, swaggerEnabled] = process.argv.slice(2);
3
+ const [mainPath, swaggerEnabled, operationsEnabled] = process.argv.slice(2);
4
4
 
5
5
  if (!mainPath || !fs.existsSync(mainPath)) {
6
6
  console.error(`❌ Bootstrap Nest introuvable : ${mainPath ?? 'src/main.ts'}`);
7
7
  process.exitCode = 1;
8
8
  } else {
9
9
  const swagger = swaggerEnabled === 'y';
10
+ const operations = operationsEnabled === 'y';
10
11
  const source = `import { ValidationPipe } from '@nestjs/common';
11
12
  import { NestFactory } from '@nestjs/core';
12
13
  ${swagger ? "import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';\n" : ''}import { AppModule } from './app.module';
14
+ ${operations ? "import { StructuredLogger } from './operations/structured-logger.service';\n" : ''}
13
15
 
14
16
  async function bootstrap() {
15
- const app = await NestFactory.create(AppModule);
17
+ const app = await NestFactory.create(AppModule${operations ? ', { bufferLogs: true }' : ''});
18
+ ${operations ? ' app.useLogger(app.get(StructuredLogger));\n app.enableShutdownHooks();\n' : ''}
16
19
  app.useGlobalPipes(new ValidationPipe({
17
20
  whitelist: true,
18
21
  forbidNonWhitelisted: true,
@@ -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."
@@ -1,48 +1,6 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
1
+ import { inspectProject } from '../../dist/engine/project-preflight.js';
3
2
 
4
- function error(message) {
5
- throw new Error(`Préflight échoué : ${message}`);
6
- }
7
-
8
- export function inspectProject(projectRoot, orm) {
9
- const packagePath = path.join(projectRoot, 'package.json');
10
- const appModulePath = path.join(projectRoot, 'src', 'app.module.ts');
11
- const nestCliPath = path.join(projectRoot, 'nest-cli.json');
12
-
13
- if (!fs.existsSync(packagePath))
14
- error('package.json introuvable. Exécute la commande à la racine d’un projet NestJS.');
15
- if (!fs.existsSync(appModulePath))
16
- error(
17
- 'src/app.module.ts introuvable. Les workspaces et structures personnalisées ne sont pas encore supportés.',
18
- );
19
-
20
- const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
21
- const nestCli = fs.existsSync(nestCliPath) ? JSON.parse(fs.readFileSync(nestCliPath, 'utf8')) : null;
22
- if (nestCli?.monorepo) error('les workspaces Nest ne sont pas encore supportés. Cible une application autonome.');
23
-
24
- const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
25
- const missing = ['@nestjs/common', '@nestjs/core', '@nestjs/cqrs'];
26
- if (orm === 'typeorm') missing.push('@nestjs/typeorm', 'typeorm');
27
- if (orm === 'prisma') missing.push('@prisma/client');
28
- const unavailable = missing.filter((dependency) => !dependencies[dependency]);
29
- if (unavailable.length) error(`dépendances manquantes : ${unavailable.join(', ')}.`);
30
-
31
- const appModule = fs.readFileSync(appModulePath, 'utf8');
32
- if (!/@Module\s*\(/.test(appModule)) error('le décorateur @Module est introuvable dans src/app.module.ts.');
33
-
34
- return {
35
- appModulePath,
36
- packageManager: fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))
37
- ? 'pnpm'
38
- : fs.existsSync(path.join(projectRoot, 'yarn.lock'))
39
- ? 'yarn'
40
- : fs.existsSync(path.join(projectRoot, 'package-lock.json'))
41
- ? 'npm'
42
- : null,
43
- hasRootTypeOrmConnection: /\bTypeOrmModule\.forRoot\s*\(/.test(appModule),
44
- };
45
- }
3
+ export { inspectProject };
46
4
 
47
5
  if (import.meta.url === `file://${process.argv[1]}`) {
48
6
  try {
@@ -87,7 +87,12 @@ if [ "$WITH_SWAGGER" = "y" ]; then
87
87
  run_step "Configuration Swagger" bash "$FEATURES_PATH/swagger.sh" "$PM"
88
88
  fi
89
89
 
90
- run_step "Configuration du bootstrap Nest" node "$FEATURES_PATH/configure_bootstrap.mjs" src/main.ts "$WITH_SWAGGER"
90
+ WITH_OPERATIONS=${WITH_OPERATIONS:-$(read -p "🩺 Activer le socle d’exploitation ? (y/n) : " tmp && echo "$tmp")}
91
+ if [ "$WITH_OPERATIONS" = "y" ]; then
92
+ run_step "Configuration du socle d’exploitation" bash "$FEATURES_PATH/operations.sh" "$ORM"
93
+ fi
94
+
95
+ run_step "Configuration du bootstrap Nest" node "$FEATURES_PATH/configure_bootstrap.mjs" src/main.ts "$WITH_SWAGGER" "$WITH_OPERATIONS"
91
96
 
92
97
  WITH_GIT=${WITH_GIT:-$(read -p "🔃 Initialiser Git ? (y/n) : " tmp && echo "$tmp")}
93
98
  if [ "$WITH_GIT" = "y" ]; then
@@ -109,5 +114,6 @@ echo "📦 Package manager : $PM"
109
114
  echo "🧠 ORM : $ORM"
110
115
  if [ "$WITH_DOCKER" = "y" ]; then echo "🐳 Docker activé"; fi
111
116
  if [ "$WITH_SWAGGER" = "y" ]; then echo "📚 Swagger activé"; fi
117
+ if [ "$WITH_OPERATIONS" = "y" ]; then echo "🩺 Socle d’exploitation activé"; fi
112
118
  if [ "$WITH_GIT" = "y" ]; then echo "🔃 Git initialisé"; fi
113
119
  if [ -n "$MODULES" ]; then echo "📦 Modules générés : $MODULES"; fi
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "nestgen-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "NestGen CLI — Générateur modulaire NestJS (DDD, CQRS, Hexagonal)",
5
5
  "files": [
6
6
  "dist",
7
+ "examples",
7
8
  "nestgen.js",
8
9
  "nestjs-generator"
9
10
  ],
@@ -35,24 +36,30 @@
35
36
  "check": "npm run typecheck && eslint .",
36
37
  "format": "prettier --write .",
37
38
  "format:check": "prettier --check .",
38
- "prepack": "npm run build && npm run check && npm run format:check && npm test",
39
+ "prepack": "npm run build && npm run check && npm run docs:check && npm run landing:check && npm run format:check && npm test && npm run test:example",
39
40
  "test": "node --test",
40
41
  "test:package": "node scripts/package-smoke.mjs",
42
+ "test:package-manager": "node scripts/package-manager-lockfile-integration.mjs",
41
43
  "test:prisma": "node scripts/prisma-integration.mjs",
42
44
  "test:docker": "node scripts/docker-config-integration.mjs",
45
+ "test:example": "node scripts/example-store-api-integration.mjs",
43
46
  "test:typeorm": "node scripts/typeorm-http-integration.mjs",
47
+ "release:verify": "node scripts/release-verify.mjs",
48
+ "release:verify-remote": "node scripts/release-verify-remote.mjs",
49
+ "docs:check": "node scripts/docs-contract.mjs",
50
+ "landing:check": "node scripts/landing-contract.mjs",
44
51
  "typecheck": "tsc -p tsconfig.json --noEmit"
45
52
  },
46
53
  "licenses": "MIT",
47
54
  "dependencies": {
48
55
  "chalk": "^5.3.0",
49
56
  "inquirer": "^9.0.0",
57
+ "prettier": "^3.9.9",
50
58
  "typescript": "^5.9.3"
51
59
  },
52
60
  "devDependencies": {
53
61
  "@eslint/js": "^9.39.5",
54
62
  "@types/node": "^24.19.0",
55
- "eslint": "^9.39.5",
56
- "prettier": "^3.9.9"
63
+ "eslint": "^9.39.5"
57
64
  }
58
65
  }