nestcraftx 0.4.0 → 1.0.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.
Files changed (54) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/CHANGELOG.fr.md +74 -0
  3. package/CHANGELOG.md +74 -0
  4. package/PROGRESS.md +59 -57
  5. package/README.fr.md +60 -69
  6. package/bin/nestcraft.js +8 -0
  7. package/commands/demo.js +10 -0
  8. package/commands/generate.js +551 -2
  9. package/commands/generateConf.js +4 -4
  10. package/commands/help.js +11 -0
  11. package/commands/info.js +2 -3
  12. package/commands/list.js +93 -0
  13. package/commands/new.js +26 -9
  14. package/jest.config.js +9 -0
  15. package/package.json +7 -2
  16. package/readme.md +59 -68
  17. package/tests/e2e/generator.spec.js +71 -0
  18. package/tests/unit/appModuleUpdater.spec.js +111 -0
  19. package/tests/unit/cliParser.spec.js +74 -0
  20. package/tests/unit/controllerGenerator.spec.js +145 -0
  21. package/tests/unit/dtoGenerator.spec.js +87 -0
  22. package/tests/unit/entityGenerator.spec.js +65 -0
  23. package/tests/unit/generateCommand.spec.js +100 -0
  24. package/tests/unit/health.spec.js +69 -0
  25. package/tests/unit/listCommand.spec.js +91 -0
  26. package/tests/unit/middlewareGenerator.spec.js +64 -0
  27. package/tests/unit/newCommand.spec.js +88 -0
  28. package/tests/unit/relationCommand.spec.js +202 -0
  29. package/tests/unit/throttler.spec.js +117 -0
  30. package/utils/configs/setupCleanArchitecture.js +1 -1
  31. package/utils/configs/setupLightArchitecture.js +98 -26
  32. package/utils/envGenerator.js +3 -1
  33. package/utils/file-utils/saveProjectConfig.js +3 -0
  34. package/utils/fullModeInput.js +4 -0
  35. package/utils/generators/application/dtoGenerator.js +20 -3
  36. package/utils/generators/cleanModuleGenerator.js +40 -11
  37. package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
  38. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +78 -1
  39. package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
  40. package/utils/generators/lightModuleGenerator.js +14 -0
  41. package/utils/generators/presentation/controllerGenerator.js +70 -19
  42. package/utils/interactive/askRelationCommand.js +98 -0
  43. package/utils/interactive/entityBuilder.js +11 -0
  44. package/utils/lightModeInput.js +14 -0
  45. package/utils/setups/orms/typeOrmSetup.js +11 -4
  46. package/utils/setups/projectSetup.js +10 -2
  47. package/utils/setups/setupAuth.js +334 -18
  48. package/utils/setups/setupDatabase.js +4 -1
  49. package/utils/setups/setupHealth.js +74 -0
  50. package/utils/setups/setupMongoose.js +4 -1
  51. package/utils/setups/setupPrisma.js +110 -1
  52. package/utils/setups/setupSwagger.js +4 -1
  53. package/utils/setups/setupThrottler.js +92 -0
  54. package/utils/shell.js +21 -4
@@ -0,0 +1,93 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { logError } = require("../utils/loggers/logError");
4
+ const { logInfo } = require("../utils/loggers/logInfo");
5
+ const { logWarning } = require("../utils/loggers/logWarning");
6
+ const { bold } = require("../utils/colors");
7
+
8
+ async function listCommand() {
9
+ const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
10
+
11
+ if (!fs.existsSync(configPath)) {
12
+ logError("Aucun fichier de configuration NestcraftX trouvé.");
13
+ logInfo("Assurez-vous d'être à la racine d'un projet NestcraftX.");
14
+ return;
15
+ }
16
+
17
+ let projectConfig;
18
+ try {
19
+ const rawData = fs.readFileSync(configPath, "utf8");
20
+ projectConfig = JSON.parse(rawData);
21
+ } catch (err) {
22
+ logError("Erreur lors de la lecture du fichier .nestcraftxrc. Le JSON est peut-être corrompu.");
23
+ return;
24
+ }
25
+
26
+ const { name, mode, orm, database, entities = [], relations = [] } = projectConfig;
27
+
28
+ console.log("\n" + "=".repeat(74));
29
+ console.log(`📋 Project: ${bold(name)} | Mode: ${bold(mode ? mode.toUpperCase() : "FULL")} | ORM: ${bold(orm ? orm.toUpperCase() : "")} | DB: ${bold(database || "")}`);
30
+ console.log("=".repeat(74));
31
+
32
+ if (entities.length === 0) {
33
+ logWarning("Aucune entité configurée pour le moment.");
34
+ return;
35
+ }
36
+
37
+ // 1. Draw Entities Table
38
+ console.log("\n" + bold("ENTITIES"));
39
+ console.log("┌" + "─".repeat(20) + "┬" + "─".repeat(50) + "┐");
40
+ console.log("│ " + "Entity Name".padEnd(18) + " │ " + "Fields (Type)".padEnd(48) + " │");
41
+ console.log("├" + "─".repeat(20) + "┼" + "─".repeat(50) + "┤");
42
+
43
+ entities.forEach(entity => {
44
+ const nameStr = entity.name || "";
45
+ const fieldsStr = (entity.fields || []).map(f => `${f.name} (${f.type})`).join(", ");
46
+
47
+ // Chunk fields if they are too long to prevent table layout breakage
48
+ if (fieldsStr.length <= 48) {
49
+ console.log("│ " + nameStr.padEnd(18) + " │ " + fieldsStr.padEnd(48) + " │");
50
+ } else {
51
+ // Print first line
52
+ let chunk = fieldsStr.substring(0, 48);
53
+ let lastSpace = chunk.lastIndexOf(", ");
54
+ if (lastSpace > 30) {
55
+ chunk = fieldsStr.substring(0, lastSpace + 1);
56
+ }
57
+ console.log("│ " + nameStr.padEnd(18) + " │ " + chunk.padEnd(48) + " │");
58
+
59
+ let remaining = fieldsStr.substring(chunk.length).trim();
60
+ while (remaining.length > 0) {
61
+ let nextChunk = remaining.substring(0, 48);
62
+ if (remaining.length > 48) {
63
+ let spaceIndex = nextChunk.lastIndexOf(", ");
64
+ if (spaceIndex > 30) {
65
+ nextChunk = remaining.substring(0, spaceIndex + 1);
66
+ }
67
+ }
68
+ console.log("│ " + "".padEnd(18) + " │ " + nextChunk.padEnd(48) + " │");
69
+ remaining = remaining.substring(nextChunk.length).trim();
70
+ }
71
+ }
72
+ });
73
+
74
+ console.log("└" + "─".repeat(20) + "┴" + "─".repeat(50) + "┘");
75
+
76
+ // 2. Draw Relations Table
77
+ if (relations && relations.length > 0) {
78
+ console.log("\n" + bold("RELATIONSHIPS"));
79
+ console.log("┌" + "─".repeat(40) + "┬" + "─".repeat(15) + "┐");
80
+ console.log("│ " + "Relation".padEnd(38) + " │ " + "Type".padEnd(13) + " │");
81
+ console.log("├" + "─".repeat(40) + "┼" + "─".repeat(15) + "┤");
82
+
83
+ relations.forEach(rel => {
84
+ const relationStr = `${rel.from} ──► ${rel.to}`;
85
+ const typeStr = rel.type || "";
86
+ console.log("│ " + relationStr.padEnd(38) + " │ " + typeStr.padEnd(13) + " │");
87
+ });
88
+
89
+ console.log("└" + "─".repeat(40) + "┴" + "─".repeat(15) + "┘\n");
90
+ }
91
+ }
92
+
93
+ module.exports = listCommand;
package/commands/new.js CHANGED
@@ -36,7 +36,11 @@ async function newCommand(projectName, flags = {}) {
36
36
  }
37
37
  }
38
38
 
39
- const inputs = await getFullModeInputs(currentProjectName, flags);
39
+ const mode = determineMode(flags);
40
+ const inputs =
41
+ mode === "light"
42
+ ? await buildLightModeInputs(currentProjectName, flags)
43
+ : await buildFullModeInputs(currentProjectName, flags);
40
44
  return executeProjectSetup(inputs);
41
45
  }
42
46
 
@@ -86,7 +90,8 @@ function hasAllLightModeFlags(flags) {
86
90
  if (flags.interactive === true) {
87
91
  return false;
88
92
  }
89
- return flags.orm !== undefined;
93
+ const hasMode = flags.mode !== undefined || flags.light === true || flags.full === true;
94
+ return flags.orm !== undefined && hasMode;
90
95
  }
91
96
 
92
97
  function buildLightModeFromFlags(projectName, flags) {
@@ -97,13 +102,19 @@ function buildLightModeFromFlags(projectName, flags) {
97
102
  logError(`Unrecognized ORM: ${orm}. Using Prisma by default.`);
98
103
  }
99
104
 
105
+ let packageManager = "npm";
106
+ if (flags.pnpm) packageManager = "pnpm";
107
+ else if (flags.yarn) packageManager = "yarn";
108
+ else if (flags.packageManager) packageManager = flags.packageManager.toLowerCase();
109
+
100
110
  const inputs = {
101
111
  projectName,
102
112
  mode: "light",
103
- useYarn: false,
113
+ useYarn: packageManager === "yarn",
104
114
  useDocker: flags.docker !== false,
105
115
  useAuth: !!flags.auth,
106
116
  useSwagger: !!flags.swagger,
117
+ useThrottler: !!flags.throttler,
107
118
  swaggerInputs: flags.swagger
108
119
  ? {
109
120
  title: `${projectName} API`,
@@ -112,7 +123,7 @@ function buildLightModeFromFlags(projectName, flags) {
112
123
  endpoint: "api/docs",
113
124
  }
114
125
  : undefined,
115
- packageManager: "npm",
126
+ packageManager: packageManager,
116
127
  entitiesData: {
117
128
  entities: [],
118
129
  relations: [],
@@ -155,7 +166,8 @@ function hasAllFullModeFlags(flags) {
155
166
  if (flags.interactive === true) {
156
167
  return false;
157
168
  }
158
- return flags.orm !== undefined;
169
+ const hasMode = flags.mode !== undefined || flags.light === true || flags.full === true;
170
+ return flags.orm !== undefined && hasMode;
159
171
  }
160
172
 
161
173
  function buildFullModeFromFlags(projectName, flags) {
@@ -164,7 +176,10 @@ function buildFullModeFromFlags(projectName, flags) {
164
176
  const validOrms = ["prisma", "typeorm", "mongoose"];
165
177
  const finalOrm = validOrms.includes(orm) ? orm : "prisma";
166
178
 
167
- const packageManager = flags.yarn ? "yarn" : "npm";
179
+ let packageManager = "npm";
180
+ if (flags.pnpm) packageManager = "pnpm";
181
+ else if (flags.yarn) packageManager = "yarn";
182
+ else if (flags.packageManager) packageManager = flags.packageManager.toLowerCase();
168
183
 
169
184
  const defaultDBConfig =
170
185
  finalOrm === "mongoose"
@@ -182,18 +197,20 @@ function buildFullModeFromFlags(projectName, flags) {
182
197
  POSTGRES_PORT: flags.dbPort || "5432",
183
198
  };
184
199
 
185
- const useAuth = flags.auth !== false;
186
- const useSwagger = flags.swagger !== false;
200
+ const useAuth = !!flags.auth;
201
+ const useSwagger = !!flags.swagger;
187
202
  const useDocker = flags.docker !== false;
203
+ const useThrottler = !!flags.throttler;
188
204
 
189
205
  const inputs = {
190
206
  projectName,
191
207
  mode: "full",
192
- useYarn: flags.yarn === true,
208
+ useYarn: packageManager === "yarn",
193
209
  packageManager: packageManager,
194
210
  useDocker: useDocker,
195
211
  useAuth: useAuth,
196
212
  useSwagger: useSwagger,
213
+ useThrottler: useThrottler,
197
214
  swaggerInputs: useSwagger
198
215
  ? {
199
216
  title: flags.swaggerTitle || `${projectName} API`,
package/jest.config.js ADDED
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ testMatch: ['**/tests/**/*.spec.js'],
4
+ verbose: true,
5
+ forceExit: true,
6
+ clearMocks: true,
7
+ resetMocks: true,
8
+ restoreMocks: true
9
+ };
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "nestcraftx",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Modern CLI to generate scalable NestJS projects with Clean Architecture (FULL) and MVP mode (LIGHT) - Enhanced with auto-generated .env secrets, complete ORM support, and entity relations",
5
5
  "main": "bin/nestcraft.js",
6
6
  "bin": {
7
7
  "nestcraftx": "bin/nestcraft.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "nestcraftx test"
10
+ "test": "jest --runInBand tests/unit",
11
+ "test:unit": "jest --runInBand tests/unit",
12
+ "test:e2e": "jest --runInBand tests/e2e"
11
13
  },
12
14
  "repository": {
13
15
  "type": "git",
@@ -37,5 +39,8 @@
37
39
  "dependencies": {
38
40
  "inquirer": "^12.10.0",
39
41
  "readline-sync": "^1.4.10"
42
+ },
43
+ "devDependencies": {
44
+ "jest": "^30.4.2"
40
45
  }
41
46
  }
package/readme.md CHANGED
@@ -31,13 +31,13 @@ Key Features:
31
31
 
32
32
  - Smart Config: Automated Swagger decorators, auto-documented .env files, and pre-configured database connections.
33
33
 
34
- > **Version 0.2.5:** Major release Interactive generation via flags, Auth refactored with session management, professional templates (gitignore, README), and clean code standards maintained by the community!
34
+ > **Version 1.0.0 (Stable Release):** Production-grade NestJS scaffolding with Clean Architecture, JWT authentication, relations updater, conditional RBAC guards, global rate limiting, health checks, advanced pagination/filtering/sorting, Swagger UI, and Docker compose files!
35
35
 
36
36
  ---
37
37
 
38
38
  ## Table of Contents
39
39
 
40
- - [What's New in v0.2.5](#whats-new-in-v025)
40
+ - [What's New in v1.0.0](#whats-new-in-v100-stable-release)
41
41
  - [Project Objective](#project-objective)
42
42
  - [Prerequisites](#prerequisites)
43
43
  - [Installation](#installation)
@@ -52,59 +52,32 @@ Key Features:
52
52
 
53
53
  ---
54
54
 
55
- ## What's New in v0.2.5
55
+ ## What's New in v1.0.0 (Stable Release)
56
56
 
57
- ### Two Architecture Modes
57
+ ### 🔗 Standalone Relation Command (`nestcraftx g relation`)
58
+ - Create 1-n, n-1, 1-1, or n-n relations between **existing modules** at any time.
59
+ - Automatically updates domain entities (FK fields and getters), DTOs (Create and Update), and data mappers.
60
+ - Syncs database schemas (runs format, client generation, and migrations dev for Prisma; injects ObjectId references for Mongoose).
58
61
 
59
- **FULL Mode - Complete Architecture**
62
+ ### 🛡️ Conditional RBAC Guards & Swagger Integration
63
+ - Mutation endpoints (`POST`, `PATCH`, `DELETE`) are protected with `JwtAuthGuard` and `RolesGuard` (`@Roles('ADMIN')`) only if Auth is enabled.
64
+ - Read endpoints (`GET`, `GET :id`) are decorated with `@Public()`.
65
+ - Generates complete API documentation using `@ApiBearerAuth()` and standard error responses when Swagger is enabled.
60
66
 
61
- - Clean Architecture with use-cases, mappers, adapters
62
- - Strict separation: domain/application/infrastructure/presentation
63
- - Ideal for complex and scalable projects
67
+ ### 🚦 Global Rate Limiting (Throttler Module)
68
+ - Integrated `@nestjs/throttler` package with preconfigured global rate limiting policies (default: 10 requests per minute).
69
+ - Configurable via interactive prompts or CLI flag `--throttler`.
64
70
 
65
- **LIGHT Mode - MVP Architecture**
71
+ ### 🏥 Health Check Endpoint (`/health`)
72
+ - Exposes a native `/health` check route reporting global status (`ok`), timestamp, and application uptime (`process.uptime()`).
73
+ - Fully documented in Swagger UI, optimized with zero third-party packages for fast compilation and maximum safety.
66
74
 
67
- - Simplified structure: controllers services repositories
68
- - Quick start for prototypes
69
- - Perfect for small projects and MVPs
75
+ ### 🚀 Advanced Query Params (Pagination, Filtering, Sorting)
76
+ - Auto-injects paginated data retrieval (e.g. `?page=1&limit=10&search=...&sortBy=createdAt&sortOrder=desc`) into controllers and repository layers.
77
+ - Seamlessly maps incoming query params to Prisma, TypeORM, and Mongoose queries.
70
78
 
71
- ### Improved Demo Command
72
-
73
- - ✅ Flag options: `--light`, `--orm`, `--auth`, `--swagger`, `--docker`
74
- - ✅ Interactive mode: only asks questions for missing flags
75
- - ✅ Intelligent merging of flags and interactive responses
76
- - ✅ 3 pre-configured entities with relationships
77
- - ✅ Support for all ORMs (Prisma, TypeORM, Mongoose)
78
- - ✅ Separate instructions in [Demo Documentation](./DEMO.md)
79
-
80
- ### Modern CLI with Flags
81
-
82
- ```bash
83
- nestcraftx new <project-name> [options]
84
-
85
- Options:
86
- --light Simplified architecture mode
87
- --full Complete architecture mode (default)
88
- --db=<db> Database choice: postgresql|mongodb
89
- --orm=<orm> ORM choice: prisma|typeorm|mongoose
90
- --auth Enable JWT authentication
91
- --swagger Enable Swagger documentation
92
- --docker Enable Docker (default: true)
93
- ```
94
-
95
- ### Automatic Secret Generation
96
-
97
- - Auto-generated JWT secrets (64 secure characters)
98
- - Ready-to-use .env file
99
- - DATABASE_URL automatically configured
100
- - Sanitized .env.example file
101
-
102
- ### Improved UX
103
-
104
- - Colored messages (info, success, error)
105
- - Animated spinners for long operations
106
- - Detailed post-generation summary
107
- - Real-time validation of options
79
+ ### 📋 Interactive Scaffolding Report
80
+ - Out-of-the-box CLI report with ASCII borders after each module generation summarizing files generated, database schema updates, relations established, and clear next steps.
108
81
 
109
82
  ### Quick Examples
110
83
 
@@ -178,6 +151,21 @@ npm install
178
151
  npm link
179
152
  ```
180
153
 
154
+ ### Running Tests
155
+
156
+ We use Jest to run unit tests and E2E generator compiler tests:
157
+
158
+ ```bash
159
+ # Run unit tests (fast check)
160
+ npm run test:unit
161
+
162
+ # Run E2E integration tests (scaffolds project, runs npm install & tsc type checks)
163
+ npm run test:e2e
164
+
165
+ # Run all tests
166
+ npm test
167
+ ```
168
+
181
169
  ---
182
170
 
183
171
  ## Available Commands
@@ -548,31 +536,34 @@ open http://localhost:3000/api/docs
548
536
 
549
537
  ## Roadmap
550
538
 
551
- ### Version 0.2.x — Stabilization
539
+ ### Version 0.3.0 — Stabilization & UX (Completed)
540
+ - [x] CommonJS module system unification
541
+ - [x] Dynamic helper versioning and interactive flags checks
542
+ - [x] Dynamic package manager setup
552
543
 
553
- - [x] Light & Full architectures
554
- - [x] Interactive CLI
555
- - [x] Multi-ORM support (Prisma / TypeORM / Mongoose)
556
- - [ ] Comprehensive documentation & realistic examples
544
+ ### Version 0.4.0 Architecture Refactoring (Completed)
545
+ - [x] Split of monolithic files into dedicated generators
546
+ - [x] Interactive state-based `EntityBuilder` with revision menus
547
+ - [x] Dry-run simulation mode (`--dry-run`)
557
548
 
558
- ### Version 0.3.0 — Developer Experience
549
+ ### Version 0.5.0 — Quality & Security (Completed)
550
+ - [x] Persistent DB storage for OTPs & password reset tokens
551
+ - [x] Strict package version pinning
552
+ - [x] Contextual semantic Swagger DTO descriptions
559
553
 
560
- - [ ] `generate` command (scaffold modules/entities into existing projects)
561
- - [ ] SQLite support for zero-config demos & prototyping
562
- - [ ] Enhanced error handling & colored terminal logs
554
+ ### Version 0.6.0 Tests & CI/CD (Completed)
555
+ - [x] Unit test coverage using Jest
556
+ - [x] E2E compiler checks & resolution of upstream peer dependency conflicts
557
+ - [x] Multi-OS Node.js matrix GitHub Actions CI/CD pipeline
563
558
 
564
- ### Version 0.4.0 — Production Readiness
565
-
566
- - [ ] Pluggable session strategies (In-memory / Database / Redis)
567
- - [ ] Advanced data seeding templates
568
- - [ ] Project presets (API Only / Auth / Full CRUD)
559
+ ### Version 0.7.0 - 0.9.0 Missing Features (In Progress)
560
+ - [ ] Interactive `generate auth` and complete non-interactive execution
561
+ - [ ] API Pagination, filtering, rate limiting, and RBAC guards
562
+ - [ ] MySQL/SQLite support
569
563
 
570
564
  ### Version 1.0.0 — Stable Release
571
-
572
- - [ ] TypeScript-native CLI
573
- - [ ] Enforced conventions & stable API contracts
574
565
  - [ ] Official documentation website
575
- - [ ] Long-term support (LTS) guarantees
566
+ - [ ] Production audit and LTS guarantees
576
567
 
577
568
  ---
578
569
 
@@ -629,7 +620,7 @@ Thanks to all contributors and the NestJS community!
629
620
 
630
621
  ---
631
622
 
632
- **NestCraftX v0.2.5** - Clean Architecture Made Simple
623
+ **NestCraftX v0.6.0** - Clean Architecture Made Simple
633
624
 
634
625
  For more information:
635
626
 
@@ -0,0 +1,71 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const demoCommand = require('../../commands/demo');
5
+
6
+ describe('E2E Generator - Compilation Check', () => {
7
+ const e2eDir = path.join(__dirname, '..', 'e2e-output');
8
+ const projectDir = path.join(e2eDir, 'blog-demo');
9
+ const originalCwd = process.cwd();
10
+
11
+ beforeAll(() => {
12
+ // S'assurer que le dossier temporaire e2e-output existe et est vide
13
+ if (fs.existsSync(e2eDir)) {
14
+ fs.rmSync(e2eDir, { recursive: true, force: true });
15
+ }
16
+ fs.mkdirSync(e2eDir, { recursive: true });
17
+ });
18
+
19
+ afterAll(() => {
20
+ // Restaurer le CWD original et nettoyer les dossiers générés
21
+ process.chdir(originalCwd);
22
+ if (fs.existsSync(e2eDir)) {
23
+ try {
24
+ fs.rmSync(e2eDir, { recursive: true, force: true });
25
+ } catch (err) {
26
+ console.warn('Cleanup warning (some files might be locked):', err.message);
27
+ }
28
+ }
29
+ });
30
+
31
+ it('should generate a project with Prisma, Auth, Swagger and compile successfully', async () => {
32
+ // Se déplacer dans le dossier e2e-output avant la génération
33
+ process.chdir(e2eDir);
34
+
35
+ console.log('Generating E2E project...');
36
+ // Exécuter demoCommand en passant les options pour éviter les prompts inquirer
37
+ await demoCommand({
38
+ light: false, // Full Clean Architecture
39
+ orm: 'prisma',
40
+ docker: false,
41
+ auth: true,
42
+ swagger: true,
43
+ packageManager: 'npm'
44
+ });
45
+
46
+ // Vérifier l'existence physique du projet et de fichiers clés
47
+ expect(fs.existsSync(projectDir)).toBe(true);
48
+ expect(fs.existsSync(path.join(projectDir, 'package.json'))).toBe(true);
49
+ expect(fs.existsSync(path.join(projectDir, 'prisma', 'schema.prisma'))).toBe(true);
50
+ expect(fs.existsSync(path.join(projectDir, 'src', 'app.module.ts'))).toBe(true);
51
+
52
+ // Se déplacer dans le dossier du projet généré
53
+ process.chdir(projectDir);
54
+
55
+ console.log('Running npm install in the generated project...');
56
+ // Lancer npm install avec --legacy-peer-deps pour installer les dépendances figées
57
+ execSync('npm install --legacy-peer-deps --no-audit --no-fund', { stdio: 'inherit' });
58
+
59
+ console.log('Running tsc --noEmit in the generated project...');
60
+ // Lancer la vérification de type TypeScript
61
+ let compilePassed = false;
62
+ try {
63
+ execSync('npx tsc --noEmit', { stdio: 'inherit' });
64
+ compilePassed = true;
65
+ } catch (error) {
66
+ console.error('TypeScript compilation failed:', error.message);
67
+ }
68
+
69
+ expect(compilePassed).toBe(true);
70
+ }, 300000); // 5 minutes timeout pour npm install + compilation
71
+ });
@@ -0,0 +1,111 @@
1
+ const fs = require('fs');
2
+ const { safeUpdateAppModule } = require('../../utils/app-module.updater');
3
+ const { updateFile } = require('../../utils/file-system');
4
+
5
+ // On mock fs et file-system
6
+ jest.mock('fs');
7
+ jest.mock('../../utils/file-system', () => ({
8
+ updateFile: jest.fn()
9
+ }));
10
+
11
+ describe('appModuleUpdater - safeUpdateAppModule', () => {
12
+ let originalArgv;
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ originalArgv = [...process.argv];
17
+ });
18
+
19
+ afterEach(() => {
20
+ process.argv = originalArgv;
21
+ });
22
+
23
+ it('should do nothing and log simulation during dry run', async () => {
24
+ process.argv.push('--dry-run');
25
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
26
+
27
+ await safeUpdateAppModule('product');
28
+
29
+ expect(fs.readFileSync).not.toHaveBeenCalled();
30
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[DRY-RUN] Simulated: safe update app.module.ts with ProductModule'));
31
+
32
+ consoleSpy.mockRestore();
33
+ });
34
+
35
+ it('should insert import statement and inject module in @Module imports array', async () => {
36
+ // Retirer --dry-run
37
+ process.argv = process.argv.filter(arg => arg !== '--dry-run');
38
+
39
+ const initialAppModule = `
40
+ import { Module } from '@nestjs/common';
41
+ import { ConfigModule } from '@nestjs/config';
42
+
43
+ @Module({
44
+ imports: [
45
+ ConfigModule.forRoot({ isGlobal: true }),
46
+ ],
47
+ controllers: [],
48
+ providers: [],
49
+ })
50
+ export class AppModule {}
51
+ `;
52
+
53
+ // Mock fs.readFileSync pour retourner le contenu initial
54
+ fs.readFileSync.mockReturnValue(initialAppModule);
55
+
56
+ // Mock updateFile pour simuler la mise à jour de l'import et mettre à jour le mock de readFileSync
57
+ updateFile.mockImplementation(({ path, pattern, replacement }) => {
58
+ const updated = initialAppModule.replace(pattern, replacement);
59
+ fs.readFileSync.mockReturnValue(updated);
60
+ return Promise.resolve();
61
+ });
62
+
63
+ await safeUpdateAppModule('product');
64
+
65
+ // Vérifier l'insertion de l'import
66
+ expect(updateFile).toHaveBeenCalledWith({
67
+ path: 'src/app.module.ts',
68
+ pattern: "import { ConfigModule } from '@nestjs/config';",
69
+ replacement: "import { ConfigModule } from '@nestjs/config';\nimport { ProductModule } from 'src/product/product.module';"
70
+ });
71
+
72
+ // Vérifier la réécriture du fichier avec ProductModule injecté
73
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
74
+ 'src/app.module.ts',
75
+ expect.stringContaining('ProductModule'),
76
+ 'utf-8'
77
+ );
78
+
79
+ // Le contenu final écrit doit ressembler à :
80
+ const lastWriteCall = fs.writeFileSync.mock.calls[0][1];
81
+ expect(lastWriteCall).toContain('imports: [ConfigModule.forRoot({ isGlobal: true }), ProductModule,]');
82
+ });
83
+
84
+ it('should be idempotent and not duplicate existing imports', async () => {
85
+ process.argv = process.argv.filter(arg => arg !== '--dry-run');
86
+
87
+ const appModuleWithProduct = `
88
+ import { Module } from '@nestjs/common';
89
+ import { ConfigModule } from '@nestjs/config';
90
+ import { ProductModule } from 'src/product/product.module';
91
+
92
+ @Module({
93
+ imports: [
94
+ ConfigModule.forRoot({ isGlobal: true }), ProductModule,
95
+ ],
96
+ controllers: [],
97
+ providers: [],
98
+ })
99
+ export class AppModule {}
100
+ `;
101
+
102
+ fs.readFileSync.mockReturnValue(appModuleWithProduct);
103
+
104
+ await safeUpdateAppModule('product');
105
+
106
+ // updateFile ne doit pas être appelé car l'import est déjà présent
107
+ expect(updateFile).not.toHaveBeenCalled();
108
+ // writeFileSync ne doit pas être appelé car le module est déjà dans imports
109
+ expect(fs.writeFileSync).not.toHaveBeenCalled();
110
+ });
111
+ });
@@ -0,0 +1,74 @@
1
+ const { parseCliArgs } = require('../../utils/cliParser');
2
+
3
+ describe('cliParser - parseCliArgs', () => {
4
+ it('should parse command "new" with valid project name', () => {
5
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project'];
6
+ const result = parseCliArgs(args);
7
+ expect(result.command).toBe('new');
8
+ expect(result.projectName).toBe('my-project');
9
+ expect(result.errors).toHaveLength(0);
10
+ });
11
+
12
+ it('should register an error for invalid project name', () => {
13
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project@123'];
14
+ const result = parseCliArgs(args);
15
+ expect(result.projectName).toBe('my-project@123');
16
+ expect(result.errors).toContain('Nom de projet invalide: "my-project@123".');
17
+ });
18
+
19
+ it('should parse "generate" subcommand and target entity name', () => {
20
+ const args = ['node', 'bin/nestcraft.js', 'generate', 'module', 'Product'];
21
+ const result = parseCliArgs(args);
22
+ expect(result.command).toBe('generate');
23
+ expect(result.subCommand).toBe('module');
24
+ expect(result.targetName).toBe('Product');
25
+ });
26
+
27
+ it('should support shorthand alias "g"', () => {
28
+ const args = ['node', 'bin/nestcraft.js', 'g', 'module', 'User'];
29
+ const result = parseCliArgs(args);
30
+ expect(result.command).toBe('g');
31
+ expect(result.subCommand).toBe('module');
32
+ expect(result.targetName).toBe('User');
33
+ });
34
+
35
+ it('should parse flags with key=value syntax', () => {
36
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--orm=prisma', '--mode=light'];
37
+ const result = parseCliArgs(args);
38
+ expect(result.flags.orm).toBe('prisma');
39
+ expect(result.flags.mode).toBe('light');
40
+ expect(result.errors).toHaveLength(0);
41
+ });
42
+
43
+ it('should parse flags followed by value', () => {
44
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--orm', 'mongoose'];
45
+ const result = parseCliArgs(args);
46
+ expect(result.flags.orm).toBe('mongoose');
47
+ expect(result.errors).toHaveLength(0);
48
+ });
49
+
50
+ it('should default boolean flags to true when no value is provided', () => {
51
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--docker', '--auth'];
52
+ const result = parseCliArgs(args);
53
+ expect(result.flags.docker).toBe(true);
54
+ expect(result.flags.auth).toBe(true);
55
+ });
56
+
57
+ it('should register an error for invalid ORM', () => {
58
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--orm=redis'];
59
+ const result = parseCliArgs(args);
60
+ expect(result.errors).toContain('ORM invalide: "redis". Valeurs acceptées: prisma, typeorm, mongoose');
61
+ });
62
+
63
+ it('should register an error for invalid mode', () => {
64
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--mode=ultra'];
65
+ const result = parseCliArgs(args);
66
+ expect(result.errors).toContain('Mode invalide: "ultra". Valeurs acceptées: full, light');
67
+ });
68
+
69
+ it('should register an error if both --full and --light flags are passed', () => {
70
+ const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--full', '--light'];
71
+ const result = parseCliArgs(args);
72
+ expect(result.errors).toContain('Les flags --full et --light sont mutuellement exclusifs.');
73
+ });
74
+ });