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
|
@@ -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,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
+
source "$(dirname "${BASH_SOURCE[0]}")/logger.sh"
|
|
4
|
+
|
|
3
5
|
set -euo pipefail
|
|
4
6
|
|
|
5
7
|
APP_NAME=${1:?Nom_application_requis}
|
|
@@ -8,14 +10,14 @@ PM=${2:-npm}
|
|
|
8
10
|
case "$PM" in
|
|
9
11
|
npm|pnpm|yarn) ;;
|
|
10
12
|
*)
|
|
11
|
-
|
|
13
|
+
log_error "Package manager non supporté pour Docker : $PM."
|
|
12
14
|
exit 1
|
|
13
15
|
;;
|
|
14
16
|
esac
|
|
15
17
|
|
|
16
18
|
for file in Dockerfile compose.yaml .dockerignore; do
|
|
17
19
|
if [ -e "$file" ]; then
|
|
18
|
-
|
|
20
|
+
log_error "$file existe déjà. Aucune configuration Docker n'a été remplacée."
|
|
19
21
|
exit 1
|
|
20
22
|
fi
|
|
21
23
|
done
|
|
@@ -186,7 +188,7 @@ POSTGRES_PORT=5432
|
|
|
186
188
|
PORT=3000
|
|
187
189
|
ENV
|
|
188
190
|
else
|
|
189
|
-
|
|
191
|
+
log_info ".env.example existe déjà et a été conservé."
|
|
190
192
|
fi
|
|
191
193
|
|
|
192
|
-
|
|
194
|
+
log_success "Docker de développement et image de production générés pour $APP_NAME ($PM)."
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
+
source "$(dirname "${BASH_SOURCE[0]}")/logger.sh"
|
|
4
|
+
|
|
3
5
|
set -euo pipefail
|
|
4
6
|
|
|
5
7
|
RAW_NAME=${1:-}
|
|
@@ -9,31 +11,31 @@ MODULE_FILE="src/app/${RAW_NAME}/${RAW_NAME}.module.ts"
|
|
|
9
11
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
12
|
|
|
11
13
|
if [[ -z "$RAW_NAME" ]]; then
|
|
12
|
-
|
|
14
|
+
log_error "Tu dois fournir un nom de module."
|
|
13
15
|
exit 1
|
|
14
16
|
fi
|
|
15
17
|
|
|
16
18
|
if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
|
|
17
|
-
|
|
19
|
+
log_error "ORM non supporté : ${ORM:-vide}."
|
|
18
20
|
exit 1
|
|
19
21
|
fi
|
|
20
22
|
|
|
21
23
|
if [[ ! -f "$MODULE_FILE" ]]; then
|
|
22
|
-
|
|
24
|
+
log_error "Le fichier $MODULE_FILE est introuvable."
|
|
23
25
|
exit 1
|
|
24
26
|
fi
|
|
25
27
|
|
|
26
28
|
MODULE_CLASS=$(grep -oE 'export class [A-Za-z0-9_]+' "$MODULE_FILE" | awk '{print $3}')
|
|
27
29
|
|
|
28
30
|
if [[ -z "$MODULE_CLASS" ]]; then
|
|
29
|
-
|
|
31
|
+
log_error "Impossible de détecter la classe du module dans $MODULE_FILE"
|
|
30
32
|
exit 1
|
|
31
33
|
fi
|
|
32
34
|
|
|
33
35
|
MODULE_PATH="./app/${RAW_NAME}/${RAW_NAME}.module"
|
|
34
36
|
|
|
35
37
|
if [[ ! -f "$APP_MODULE" ]]; then
|
|
36
|
-
|
|
38
|
+
log_info "Création de $APP_MODULE"
|
|
37
39
|
mkdir -p src
|
|
38
40
|
cat > "$APP_MODULE" <<'EOF'
|
|
39
41
|
import { Module } from '@nestjs/common';
|
|
@@ -49,4 +51,4 @@ fi
|
|
|
49
51
|
# avoir validé le décorateur @Module et son tableau imports.
|
|
50
52
|
node "$SCRIPT_DIR/update_app_module.mjs" "$APP_MODULE" "$MODULE_CLASS" "$MODULE_PATH" "$ORM"
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
log_success "Module $MODULE_CLASS injecté dans app.module.ts avec succès"
|
|
@@ -7,18 +7,22 @@ YELLOW='\033[1;33m'
|
|
|
7
7
|
BLUE='\033[1;34m'
|
|
8
8
|
NC='\033[0m' # No Color
|
|
9
9
|
|
|
10
|
+
if [ ! -t 1 ] || [ -n "${NO_COLOR+x}" ] || [ "${TERM:-}" = "dumb" ]; then
|
|
11
|
+
RED='' GREEN='' YELLOW='' BLUE='' NC=''
|
|
12
|
+
fi
|
|
13
|
+
|
|
10
14
|
log_info() {
|
|
11
|
-
|
|
15
|
+
printf ' %b›%b %s\n' "$BLUE" "$NC" "$1"
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
log_success() {
|
|
15
|
-
|
|
19
|
+
printf ' %b✓%b %s\n' "$GREEN" "$NC" "$1"
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
log_warn() {
|
|
19
|
-
|
|
23
|
+
printf ' %b⚠%b %s\n' "$YELLOW" "$NC" "$1"
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
log_error() {
|
|
23
|
-
|
|
27
|
+
printf ' %b✖%b %s\n' "$RED" "$NC" "$1" >&2
|
|
24
28
|
}
|
|
@@ -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,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
source "$(dirname "${BASH_SOURCE[0]}")/logger.sh"
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ORM=${1:?ORM requis}
|
|
8
|
+
FEATURES_PATH="$(dirname "$0")"
|
|
9
|
+
|
|
10
|
+
if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
|
|
11
|
+
log_error "ORM non supporté : $ORM" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
node "$FEATURES_PATH/operational_foundation.mjs" src "$ORM"
|
|
16
|
+
node "$FEATURES_PATH/update_app_module.mjs" src/app.module.ts OperationsModule './operations/operations.module' "$ORM"
|
|
17
|
+
|
|
18
|
+
log_success "Socle d’exploitation configuré : /health/live et /health/ready."
|
|
@@ -1,48 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'node:path';
|
|
1
|
+
import { inspectProject } from '../../dist/engine/project-preflight.js';
|
|
3
2
|
|
|
4
|
-
|
|
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 {
|
|
@@ -12,7 +12,7 @@ PM=${1:?Package manager requis}
|
|
|
12
12
|
APP_NAME=${2:?Nom_application_requis}
|
|
13
13
|
|
|
14
14
|
if [ -e prisma/schema.prisma ]; then
|
|
15
|
-
|
|
15
|
+
log_error "prisma/schema.prisma existe déjà. Aucune configuration Prisma n'a été remplacée."
|
|
16
16
|
exit 1
|
|
17
17
|
fi
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ cat > .env <<EOF
|
|
|
25
25
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/$APP_NAME"
|
|
26
26
|
EOF
|
|
27
27
|
else
|
|
28
|
-
|
|
28
|
+
log_info ".env existe déjà et a été conservé."
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
31
|
cat > prisma/schema.prisma <<EOF
|
|
@@ -70,4 +70,4 @@ EOF
|
|
|
70
70
|
|
|
71
71
|
pm_exec "$PM" prisma generate
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
log_success "Prisma initialisé avec succès !"
|
|
@@ -10,7 +10,7 @@ FEATURES_PATH="$(dirname "$0")"
|
|
|
10
10
|
source "$FEATURES_PATH/utils.sh"
|
|
11
11
|
source "$FEATURES_PATH/logger.sh"
|
|
12
12
|
|
|
13
|
-
log_info "
|
|
13
|
+
log_info "Installation de TypeORM et PostgreSQL..."
|
|
14
14
|
|
|
15
15
|
pm_add "$PM" @nestjs/typeorm@12 typeorm@0.3 pg@8 dotenv@16
|
|
16
16
|
pm_add_dev "$PM" ts-node@10
|
|
@@ -75,6 +75,6 @@ DATABASE_NAME=appdb
|
|
|
75
75
|
EOF
|
|
76
76
|
fi
|
|
77
77
|
|
|
78
|
-
log_success "
|
|
78
|
+
log_success "TypeORM installé avec succès"
|
|
79
79
|
|
|
80
|
-
log_info "
|
|
80
|
+
log_info "TypeORM utilise src/database/typeorm.config.ts et src/database/data-source.ts pour des migrations explicites."
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
+
source "$(dirname "${BASH_SOURCE[0]}")/logger.sh"
|
|
4
|
+
|
|
3
5
|
NEST_CLI_VERSION="12.0.0"
|
|
4
6
|
|
|
5
7
|
assert_package_manager() {
|
|
@@ -7,7 +9,7 @@ assert_package_manager() {
|
|
|
7
9
|
case "$package_manager" in
|
|
8
10
|
npm|pnpm|yarn) assert_command_exists "$package_manager" ;;
|
|
9
11
|
*)
|
|
10
|
-
|
|
12
|
+
log_error "Package manager non supporté : $package_manager. Valeurs acceptées : npm, pnpm, yarn."
|
|
11
13
|
return 1
|
|
12
14
|
;;
|
|
13
15
|
esac
|
|
@@ -57,8 +59,8 @@ nest_new() {
|
|
|
57
59
|
assert_command_exists() {
|
|
58
60
|
local CMD=$1
|
|
59
61
|
if ! command -v "$CMD" &> /dev/null; then
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
log_error "Commande introuvable : $CMD"
|
|
63
|
+
log_info "Installe-la ou vérifie ton PATH"
|
|
62
64
|
return 1
|
|
63
65
|
fi
|
|
64
66
|
}
|
|
@@ -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
|
-
|
|
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
|
|
@@ -103,11 +108,12 @@ done
|
|
|
103
108
|
|
|
104
109
|
# ────── Résumé final ──────
|
|
105
110
|
echo ""
|
|
106
|
-
log_success "
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if [ "$
|
|
111
|
-
if [ "$
|
|
112
|
-
if [ "$WITH_GIT" = "y" ]; then
|
|
113
|
-
if [ -n "$MODULES" ]; then
|
|
111
|
+
log_success "Projet NestJS \"$APP_NAME\" généré avec succès"
|
|
112
|
+
printf '\n %-24s %s\n' 'Localisation' "$FULL_PATH"
|
|
113
|
+
printf ' %-24s %s\n' 'Package manager' "$PM" 'ORM' "$ORM"
|
|
114
|
+
if [ "$WITH_DOCKER" = "y" ]; then printf ' %-24s %s\n' 'Docker' 'activé'; fi
|
|
115
|
+
if [ "$WITH_SWAGGER" = "y" ]; then printf ' %-24s %s\n' 'Swagger' 'activé'; fi
|
|
116
|
+
if [ "$WITH_OPERATIONS" = "y" ]; then printf ' %-24s %s\n' 'Socle d’exploitation' 'activé'; fi
|
|
117
|
+
if [ "$WITH_GIT" = "y" ]; then printf ' %-24s %s\n' 'Git' 'initialisé'; fi
|
|
118
|
+
if [ -n "$MODULES" ]; then printf ' %-24s %s\n' 'Modules générés' "$MODULES"; fi
|
|
119
|
+
printf '\n'
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestgen-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
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
|
}
|