nestgen-cli 0.0.7 → 0.1.0

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,7 +1,9 @@
1
1
  #!/bin/bash
2
2
 
3
- RAW_NAME=$1
4
- ORM=$2
3
+ set -euo pipefail
4
+
5
+ RAW_NAME=${1:-}
6
+ ORM=${2:-}
5
7
 
6
8
  # ────── Charger les helpers ──────
7
9
  FEATURES_PATH="$(dirname "$0")"
@@ -9,33 +11,68 @@ source "$FEATURES_PATH/utils.sh"
9
11
  source "$FEATURES_PATH/logger.sh"
10
12
 
11
13
 
12
- if [ -z "$RAW_NAME" ]; then
13
- echo "❌ Tu dois passer un nom de module."
14
+ if [[ ! "$RAW_NAME" =~ ^[A-Za-z][A-Za-z0-9_-]*$ ]]; then
15
+ log_error "Le nom du module doit commencer par une lettre et ne contenir que des lettres, chiffres, tirets ou underscores."
16
+ exit 1
17
+ fi
18
+
19
+ if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
20
+ log_error "ORM non supporté : ${ORM:-vide}. Valeurs acceptées : typeorm, prisma."
14
21
  exit 1
15
22
  fi
16
23
 
17
- if [ -z "$ORM" ]; then
18
- echo "❌ ORM manquant (typeorm ou prisma)."
24
+ # ────── Formatage portable ──────
25
+ if ! RESOURCE_DATA=$(node "$FEATURES_PATH/resource_name.mjs" "$RAW_NAME"); then
26
+ log_error "Impossible de normaliser le nom de la ressource."
19
27
  exit 1
20
28
  fi
21
29
 
22
- # ────── Formatage ──────
23
- NAME=$(echo "$RAW_NAME" | tr '[:upper:]' '[:lower:]')
24
- PASCAL=$(echo "$NAME" | sed -E 's/(^|[-_])([a-z])/\u\2/g' | sed -E 's/[-_]//g')
30
+ while IFS='=' read -r key value; do
31
+ case "$key" in
32
+ NAME) NAME=$value ;;
33
+ PASCAL) PASCAL=$value ;;
34
+ CAMEL) CAMEL=$value ;;
35
+ PLURAL) PLURAL=$value ;;
36
+ ROUTE) ROUTE=$value ;;
37
+ TABLE) TABLE=$value ;;
38
+ esac
39
+ done <<< "$RESOURCE_DATA"
40
+
41
+ if [[ -z "${NAME:-}" || -z "${PASCAL:-}" || -z "${CAMEL:-}" || -z "${ROUTE:-}" || -z "${TABLE:-}" ]]; then
42
+ log_error "Impossible de normaliser le nom de la ressource."
43
+ exit 1
44
+ fi
25
45
 
26
- NAME=$(echo "$RAW_NAME" | tr '[:upper:]' '[:lower:]')
27
- PASCAL=$(echo "$NAME" | sed -E 's/(^|[-_])([a-z])/\U\2/g' | sed -E 's/[-_]//g')
46
+ REPOSITORY_TOKEN="${PASCAL}RepositoryToken"
28
47
 
29
- CAMEL=$(echo "$PASCAL" | sed -E 's/^([A-Z])/\L\1/')
48
+ PROJECT_ROOT="$(pwd -P)"
49
+ SOURCE_ROOT="$PROJECT_ROOT/src/app"
50
+
51
+ if ! node "$FEATURES_PATH/preflight.mjs" "$PROJECT_ROOT" "$ORM" >/dev/null; then
52
+ log_error "Le projet cible ne satisfait pas les préconditions de génération."
53
+ exit 1
54
+ fi
55
+
56
+ if [ -L "$PROJECT_ROOT/src" ] || [ -L "$SOURCE_ROOT" ]; then
57
+ log_error "Les liens symboliques ne sont pas pris en charge dans src/app."
58
+ exit 1
59
+ fi
60
+
61
+ mkdir -p "$SOURCE_ROOT"
62
+ MODULE_DIR="$SOURCE_ROOT/$NAME"
63
+
64
+ if [ -e "$MODULE_DIR" ] || [ -L "$MODULE_DIR" ]; then
65
+ log_error "Le module $NAME existe déjà. Aucune modification n'a été appliquée."
66
+ exit 1
67
+ fi
30
68
 
31
- MODULE_DIR="src/app/$NAME"
32
69
  mkdir -p "$MODULE_DIR"/{core/{application/{commands,events,queries},domain/{entities,ports}},infrastructure/{adapters,persistences/repositories},interfaces/{controllers,dtos}}
33
70
 
34
71
  # ────── Entity ──────
35
72
  cat > "$MODULE_DIR/core/domain/entities/${NAME}.entity.ts" <<EOF
36
73
  export class $PASCAL {
37
74
  constructor(
38
- public readonly id: string,
75
+ public readonly id: string | undefined,
39
76
  public name: string,
40
77
  public email: string,
41
78
  ) {}
@@ -46,6 +83,8 @@ EOF
46
83
  cat > "$MODULE_DIR/core/domain/ports/${NAME}.repository.ts" <<EOF
47
84
  import { $PASCAL } from '../entities/${NAME}.entity';
48
85
 
86
+ export const ${REPOSITORY_TOKEN} = Symbol('${PASCAL}RepositoryPort');
87
+
49
88
  export interface ${PASCAL}RepositoryPort {
50
89
  save(${CAMEL}: $PASCAL): Promise<$PASCAL>;
51
90
  findById(id: string): Promise<$PASCAL | null>;
@@ -61,17 +100,20 @@ EOF
61
100
 
62
101
  # ────── Command Handler ──────
63
102
  cat > "$MODULE_DIR/core/application/commands/create-${NAME}.handler.ts" <<EOF
103
+ import { Inject } from '@nestjs/common';
64
104
  import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
65
105
  import { Create${PASCAL}Command } from './create-${NAME}.command';
66
106
  import { $PASCAL } from '../../domain/entities/${NAME}.entity';
67
- import { ${PASCAL}RepositoryPort } from '../../domain/ports/${NAME}.repository';
107
+ import { ${PASCAL}RepositoryPort, ${REPOSITORY_TOKEN} } from '../../domain/ports/${NAME}.repository';
68
108
 
69
109
  @CommandHandler(Create${PASCAL}Command)
70
110
  export class Create${PASCAL}Handler implements ICommandHandler<Create${PASCAL}Command> {
71
- constructor(private readonly repo: ${PASCAL}RepositoryPort) {}
111
+ constructor(
112
+ @Inject(${REPOSITORY_TOKEN}) private readonly repo: ${PASCAL}RepositoryPort,
113
+ ) {}
72
114
 
73
115
  async execute(command: Create${PASCAL}Command): Promise<string> {
74
- const $CAMEL = new $PASCAL(Date.now().toString(), command.name, command.email);
116
+ const $CAMEL = new $PASCAL(undefined, command.name, command.email);
75
117
  const saved = await this.repo.save($CAMEL);
76
118
  return saved.id;
77
119
  }
@@ -98,7 +140,7 @@ import { CommandBus } from '@nestjs/cqrs';
98
140
  import { Create${PASCAL}Command } from '../../core/application/commands/create-${NAME}.command';
99
141
  import { Create${PASCAL}Dto } from '../dtos/create-${NAME}.dto';
100
142
 
101
- @Controller('${NAME}s')
143
+ @Controller('${ROUTE}')
102
144
  export class ${PASCAL}Controller {
103
145
  constructor(private readonly commandBus: CommandBus) {}
104
146
 
@@ -121,7 +163,7 @@ if [ "$ORM" == "typeorm" ]; then
121
163
  cat > "$MODULE_DIR/infrastructure/persistences/repositories/${NAME}.orm.ts" <<EOF
122
164
  import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
123
165
 
124
- @Entity('${NAME}s')
166
+ @Entity('${TABLE}')
125
167
  export class ${PASCAL}Entity {
126
168
  @PrimaryGeneratedColumn('uuid')
127
169
  id: string;
@@ -147,7 +189,10 @@ export class ${PASCAL}TypeOrmRepository implements ${PASCAL}RepositoryPort {
147
189
  constructor(@InjectRepository(${PASCAL}Entity) private readonly repo: Repository<${PASCAL}Entity>) {}
148
190
 
149
191
  async save(${CAMEL}: $PASCAL): Promise<$PASCAL> {
150
- const entity = this.repo.create(${CAMEL});
192
+ const entity = this.repo.create({
193
+ name: ${CAMEL}.name,
194
+ email: ${CAMEL}.email,
195
+ });
151
196
  const saved = await this.repo.save(entity);
152
197
  return new $PASCAL(saved.id, saved.name, saved.email);
153
198
  }
@@ -162,7 +207,7 @@ EOF
162
207
  REPO_CLASS="${PASCAL}TypeOrmRepository"
163
208
  ENTITY_IMPORT="TypeOrmModule.forFeature([${PASCAL}Entity])"
164
209
  REPO_PROVIDER="{
165
- provide: '${PASCAL}RepositoryPort',
210
+ provide: ${REPOSITORY_TOKEN},
166
211
  useClass: ${PASCAL}TypeOrmRepository,
167
212
  }"
168
213
 
@@ -198,7 +243,7 @@ EOF
198
243
  REPO_CLASS="${PASCAL}PrismaRepository"
199
244
  ENTITY_IMPORT=""
200
245
  REPO_PROVIDER="{
201
- provide: '${PASCAL}RepositoryPort',
246
+ provide: ${REPOSITORY_TOKEN},
202
247
  useClass: ${PASCAL}PrismaRepository,
203
248
  }"
204
249
  fi
@@ -208,6 +253,7 @@ IMPORTS="import { Module } from '@nestjs/common';
208
253
  import { CqrsModule } from '@nestjs/cqrs';
209
254
  import { ${PASCAL}Controller } from './interfaces/controllers/${NAME}.controller';
210
255
  import { Create${PASCAL}Handler } from './core/application/commands/create-${NAME}.handler';
256
+ import { ${REPOSITORY_TOKEN} } from './core/domain/ports/${NAME}.repository';
211
257
  import { $REPO_CLASS } from './infrastructure/persistences/repositories/${NAME}.${ORM}.repository';"
212
258
 
213
259
  if [ "$ORM" = "typeorm" ]; then
@@ -223,7 +269,6 @@ echo "$IMPORTS
223
269
  controllers: [${PASCAL}Controller],
224
270
  providers: [
225
271
  Create${PASCAL}Handler,
226
- $REPO_CLASS,
227
272
  $REPO_PROVIDER,
228
273
  ],
229
274
  })
@@ -231,4 +276,3 @@ export class ${PASCAL}Module {}
231
276
  " > "$MODULE_DIR/${NAME}.module.ts"
232
277
 
233
278
  bash "$(dirname "$0")/inject_module_to_app.sh" "$NAME" "$ORM"
234
-
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+
3
+ const [mainPath, swaggerEnabled] = process.argv.slice(2);
4
+
5
+ if (!mainPath || !fs.existsSync(mainPath)) {
6
+ console.error(`❌ Bootstrap Nest introuvable : ${mainPath ?? 'src/main.ts'}`);
7
+ process.exitCode = 1;
8
+ } else {
9
+ const swagger = swaggerEnabled === 'y';
10
+ const source = `import { ValidationPipe } from '@nestjs/common';
11
+ import { NestFactory } from '@nestjs/core';
12
+ ${swagger ? "import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';\n" : ''}import { AppModule } from './app.module';
13
+
14
+ async function bootstrap() {
15
+ const app = await NestFactory.create(AppModule);
16
+ app.useGlobalPipes(new ValidationPipe({
17
+ whitelist: true,
18
+ forbidNonWhitelisted: true,
19
+ transform: true,
20
+ }));
21
+ ${
22
+ swagger
23
+ ? `
24
+ const config = new DocumentBuilder()
25
+ .setTitle('API')
26
+ .setDescription('Documentation de l’API')
27
+ .setVersion('1.0')
28
+ .build();
29
+ SwaggerModule.setup('api', app, SwaggerModule.createDocument(app, config));
30
+ `
31
+ : ''
32
+ }
33
+ await app.listen(process.env.PORT ?? 3000);
34
+ }
35
+
36
+ void bootstrap();
37
+ `;
38
+ fs.writeFileSync(mainPath, source);
39
+ }
@@ -1,59 +1,192 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
2
 
3
+ set -euo pipefail
3
4
 
4
- # ────── Charger les helpers ──────
5
- FEATURES_PATH="$(dirname "$0")/features"
6
- source "$FEATURES_PATH/utils.sh"
7
- source "$FEATURES_PATH/logger.sh"
5
+ APP_NAME=${1:?Nom_application_requis}
6
+ PM=${2:-npm}
8
7
 
8
+ case "$PM" in
9
+ npm|pnpm|yarn) ;;
10
+ *)
11
+ echo "❌ Package manager non supporté pour Docker : $PM."
12
+ exit 1
13
+ ;;
14
+ esac
9
15
 
10
- APP_NAME=$1
16
+ for file in Dockerfile compose.yaml .dockerignore; do
17
+ if [ -e "$file" ]; then
18
+ echo "❌ $file existe déjà. Aucune configuration Docker n'a été remplacée."
19
+ exit 1
20
+ fi
21
+ done
11
22
 
12
- cat > Dockerfile <<EOF
13
- FROM node:18-alpine
14
- WORKDIR /usr/src/app
15
- COPY package*.json ./
16
- RUN npm install
23
+ case "$PM" in
24
+ npm)
25
+ cat > Dockerfile <<'DOCKERFILE'
26
+ FROM node:24-alpine AS base
27
+ WORKDIR /app
28
+
29
+ FROM base AS dependencies
30
+ COPY package.json package-lock.json ./
31
+ RUN npm ci
32
+
33
+ FROM dependencies AS development
34
+ COPY . .
35
+ CMD ["npm", "run", "start:dev"]
36
+
37
+ FROM dependencies AS build
17
38
  COPY . .
18
39
  RUN npm run build
19
- CMD ["node", "dist/main"]
20
- EOF
21
40
 
22
- cat > docker-compose.yml <<EOF
23
- version: '3.9'
41
+ FROM base AS production-dependencies
42
+ COPY package.json package-lock.json ./
43
+ RUN npm ci --omit=dev && npm cache clean --force && mkdir -p node_modules
44
+
45
+ FROM node:24-alpine AS production
46
+ ENV NODE_ENV=production
47
+ WORKDIR /app
48
+ COPY --chown=node:node --from=production-dependencies /app/node_modules ./node_modules
49
+ COPY --chown=node:node --from=build /app/dist ./dist
50
+ USER node
51
+ EXPOSE 3000
52
+ CMD ["node", "dist/main.js"]
53
+ DOCKERFILE
54
+ ;;
55
+ pnpm)
56
+ cat > Dockerfile <<'DOCKERFILE'
57
+ FROM node:24-alpine AS base
58
+ WORKDIR /app
59
+ RUN corepack enable
60
+
61
+ FROM base AS dependencies
62
+ COPY package.json pnpm-lock.yaml ./
63
+ RUN pnpm install --frozen-lockfile
64
+
65
+ FROM dependencies AS development
66
+ COPY . .
67
+ CMD ["pnpm", "run", "start:dev"]
68
+
69
+ FROM dependencies AS build
70
+ COPY . .
71
+ RUN pnpm run build
72
+
73
+ FROM base AS production-dependencies
74
+ COPY package.json pnpm-lock.yaml ./
75
+ RUN pnpm install --prod --frozen-lockfile && mkdir -p node_modules
76
+
77
+ FROM node:24-alpine AS production
78
+ ENV NODE_ENV=production
79
+ WORKDIR /app
80
+ COPY --chown=node:node --from=production-dependencies /app/node_modules ./node_modules
81
+ COPY --chown=node:node --from=build /app/dist ./dist
82
+ USER node
83
+ EXPOSE 3000
84
+ CMD ["node", "dist/main.js"]
85
+ DOCKERFILE
86
+ ;;
87
+ yarn)
88
+ cat > Dockerfile <<'DOCKERFILE'
89
+ FROM node:24-alpine AS base
90
+ WORKDIR /app
91
+ RUN corepack enable
24
92
 
93
+ FROM base AS dependencies
94
+ COPY package.json yarn.lock ./
95
+ RUN yarn install --frozen-lockfile
96
+
97
+ FROM dependencies AS development
98
+ COPY . .
99
+ CMD ["yarn", "start:dev"]
100
+
101
+ FROM dependencies AS build
102
+ COPY . .
103
+ RUN yarn build
104
+
105
+ FROM base AS production-dependencies
106
+ COPY package.json yarn.lock ./
107
+ RUN yarn install --production=true --frozen-lockfile && mkdir -p node_modules
108
+
109
+ FROM node:24-alpine AS production
110
+ ENV NODE_ENV=production
111
+ WORKDIR /app
112
+ COPY --chown=node:node --from=production-dependencies /app/node_modules ./node_modules
113
+ COPY --chown=node:node --from=build /app/dist ./dist
114
+ USER node
115
+ EXPOSE 3000
116
+ CMD ["node", "dist/main.js"]
117
+ DOCKERFILE
118
+ ;;
119
+ esac
120
+
121
+ cat > compose.yaml <<COMPOSE
25
122
  services:
26
123
  postgres:
27
- image: postgres:15
28
- restart: always
124
+ image: postgres:16-alpine
29
125
  environment:
30
- POSTGRES_USER: postgres
31
- POSTGRES_PASSWORD: postgres
32
- POSTGRES_DB: appdb
126
+ POSTGRES_DB: \${POSTGRES_DB:-appdb}
127
+ POSTGRES_USER: \${POSTGRES_USER:-nestgen}
128
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-nestgen-local-only}
33
129
  ports:
34
- - "5432:5432"
130
+ - "\${POSTGRES_PORT:-5432}:5432"
35
131
  volumes:
36
- - pgdata:/var/lib/postgresql/data
132
+ - postgres-data:/var/lib/postgresql/data
133
+ healthcheck:
134
+ test: ["CMD-SHELL", "pg_isready -U \$\$POSTGRES_USER -d \$\$POSTGRES_DB"]
135
+ interval: 5s
136
+ timeout: 3s
137
+ retries: 20
138
+ start_period: 5s
37
139
 
38
140
  app:
39
- build: .
141
+ build:
142
+ context: .
143
+ target: development
144
+ command: \${NESTGEN_DEV_COMMAND:-$PM run start:dev}
145
+ environment:
146
+ DATABASE_HOST: postgres
147
+ DATABASE_PORT: 5432
148
+ DATABASE_NAME: \${POSTGRES_DB:-appdb}
149
+ DATABASE_USER: \${POSTGRES_USER:-nestgen}
150
+ DATABASE_PASSWORD: \${POSTGRES_PASSWORD:-nestgen-local-only}
151
+ PORT: \${PORT:-3000}
40
152
  ports:
41
- - "3000:3000"
42
- depends_on:
43
- - postgres
44
- env_file:
45
- - .env
153
+ - "\${PORT:-3000}:3000"
46
154
  volumes:
47
- - .:/usr/src/app
48
- command: npm run start:dev
155
+ - .:/app
156
+ - app-node-modules:/app/node_modules
157
+ depends_on:
158
+ postgres:
159
+ condition: service_healthy
49
160
 
50
161
  volumes:
51
- pgdata:
52
- EOF
162
+ postgres-data:
163
+ app-node-modules:
164
+ COMPOSE
165
+
166
+ cat > .dockerignore <<'IGNORE'
167
+ node_modules
168
+ dist
169
+ coverage
170
+ .git
171
+ .env
172
+ .env.*
173
+ !.env.example
174
+ npm-debug.log*
175
+ yarn-debug.log*
176
+ pnpm-debug.log*
177
+ IGNORE
53
178
 
54
- cat > .env <<EOF
55
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/appdb
179
+ if [ ! -e .env.example ]; then
180
+ cat > .env.example <<'ENV'
181
+ # Variables utilisées par compose.yaml en développement local.
182
+ POSTGRES_DB=appdb
183
+ POSTGRES_USER=nestgen
184
+ POSTGRES_PASSWORD=change-me-before-sharing
185
+ POSTGRES_PORT=5432
56
186
  PORT=3000
57
- EOF
187
+ ENV
188
+ else
189
+ echo "ℹ️ .env.example existe déjà et a été conservé."
190
+ fi
58
191
 
59
- echo "✅ Dockerfile, docker-compose et .env générés !"
192
+ echo "✅ Docker de développement et image de production générés pour $APP_NAME ($PM)."
@@ -1,7 +1,9 @@
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
 
@@ -1,35 +1,41 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
2
 
3
- RAW_NAME=$1
4
- ORM=$2
3
+ set -euo pipefail
4
+
5
+ RAW_NAME=${1:-}
6
+ ORM=${2:-}
5
7
  APP_MODULE="src/app.module.ts"
6
8
  MODULE_FILE="src/app/${RAW_NAME}/${RAW_NAME}.module.ts"
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+
11
+ if [[ -z "$RAW_NAME" ]]; then
12
+ echo "❌ Tu dois fournir un nom de module." >&2
13
+ exit 1
14
+ fi
7
15
 
8
- if [ -z "$RAW_NAME" ]; then
9
- echo "❌ Tu dois fournir un nom de module."
16
+ if [[ "$ORM" != "typeorm" && "$ORM" != "prisma" ]]; then
17
+ echo "❌ ORM non supporté : ${ORM:-vide}." >&2
10
18
  exit 1
11
19
  fi
12
20
 
13
- if [ ! -f "$MODULE_FILE" ]; then
14
- echo "❌ Le fichier $MODULE_FILE est introuvable."
21
+ if [[ ! -f "$MODULE_FILE" ]]; then
22
+ echo "❌ Le fichier $MODULE_FILE est introuvable." >&2
15
23
  exit 1
16
24
  fi
17
25
 
18
- # 🧠 Extraire le vrai nom de la classe exportée
19
26
  MODULE_CLASS=$(grep -oE 'export class [A-Za-z0-9_]+' "$MODULE_FILE" | awk '{print $3}')
20
27
 
21
- if [ -z "$MODULE_CLASS" ]; then
22
- echo "❌ Impossible de détecter la classe du module dans $MODULE_FILE"
28
+ if [[ -z "$MODULE_CLASS" ]]; then
29
+ echo "❌ Impossible de détecter la classe du module dans $MODULE_FILE" >&2
23
30
  exit 1
24
31
  fi
25
32
 
26
33
  MODULE_PATH="./app/${RAW_NAME}/${RAW_NAME}.module"
27
34
 
28
- # 🔧 Créer app.module.ts s'il n'existe pas
29
- if [ ! -f "$APP_MODULE" ]; then
35
+ if [[ ! -f "$APP_MODULE" ]]; then
30
36
  echo "📄 Création de $APP_MODULE"
31
37
  mkdir -p src
32
- cat > "$APP_MODULE" <<EOF
38
+ cat > "$APP_MODULE" <<'EOF'
33
39
  import { Module } from '@nestjs/common';
34
40
 
35
41
  @Module({
@@ -39,61 +45,8 @@ export class AppModule {}
39
45
  EOF
40
46
  fi
41
47
 
42
- # 🔌 Ajouter l'import du module s'il est absent
43
- IMPORT_LINE="import { $MODULE_CLASS } from '${MODULE_PATH}';"
44
- if ! grep -q "$IMPORT_LINE" "$APP_MODULE"; then
45
- echo "🔧 Insertion de $MODULE_CLASS dans les imports du haut"
46
- sed -i '' "1s|^|$IMPORT_LINE\n|" "$APP_MODULE"
47
- else
48
- echo "ℹ️ Import $MODULE_CLASS déjà présent"
49
- fi
50
-
51
- # ➕ Ajouter dans imports: []
52
- if grep -q "imports: \[" "$APP_MODULE"; then
53
- if ! grep -q "$MODULE_CLASS" "$APP_MODULE"; then
54
- sed -i '' "s|imports: \[|imports: [$MODULE_CLASS, |" "$APP_MODULE"
55
- echo "✅ $MODULE_CLASS injecté dans imports"
56
- else
57
- echo "ℹ️ $MODULE_CLASS déjà dans imports"
58
- fi
59
- else
60
- echo "❌ Aucun bloc imports: [] trouvé dans $APP_MODULE"
61
- fi
62
-
63
- # ➕ Ajouter CqrsModule si absent
64
- if ! grep -q "CqrsModule" "$APP_MODULE"; then
65
- sed -i '' "1i\\
66
- import { CqrsModule } from '@nestjs/cqrs';
67
- " "$APP_MODULE"
68
- sed -i '' "s|imports: \[|imports: [CqrsModule, |" "$APP_MODULE"
69
- echo "✅ CqrsModule injecté"
70
- fi
71
-
72
- # ➕ Ajouter TypeOrmModule si ORM = typeorm
73
- if [ "$ORM" = "typeorm" ]; then
74
- if ! grep -q "TypeOrmModule" "$APP_MODULE"; then
75
- sed -i '' "1i\\
76
- import { TypeOrmModule } from '@nestjs/typeorm';
77
- " "$APP_MODULE"
78
- echo "✅ Import de TypeOrmModule ajouté"
79
- fi
80
-
81
- if ! grep -q "TypeOrmModule.forRoot" "$APP_MODULE"; then
82
- sed -i '' -E "s|(imports: \[)|\1\
83
- TypeOrmModule.forRoot({\
84
- type: 'postgres',\
85
- host: 'localhost',\
86
- port: 5432,\
87
- username: 'postgres',\
88
- password: 'postgres',\
89
- database: 'appdb',\
90
- synchronize: true,\
91
- autoLoadEntities: true,\
92
- }), |" "$APP_MODULE"
93
- echo "✅ TypeOrmModule.forRoot injecté"
94
- else
95
- echo "ℹ️ TypeOrmModule.forRoot déjà présent"
96
- fi
97
- fi
48
+ # Le transformateur prépare le nouveau contenu en mémoire et n'écrit qu'après
49
+ # avoir validé le décorateur @Module et son tableau imports.
50
+ node "$SCRIPT_DIR/update_app_module.mjs" "$APP_MODULE" "$MODULE_CLASS" "$MODULE_PATH" "$ORM"
98
51
 
99
52
  echo "🎯 Module $MODULE_CLASS injecté dans app.module.ts avec succès 🧩"
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
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
+ }
46
+
47
+ if (import.meta.url === `file://${process.argv[1]}`) {
48
+ try {
49
+ const [projectRoot, orm] = process.argv.slice(2);
50
+ process.stdout.write(`${JSON.stringify(inspectProject(projectRoot, orm))}\n`);
51
+ } catch (cause) {
52
+ console.error(`❌ ${cause.message}`);
53
+ process.exitCode = 1;
54
+ }
55
+ }