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.
- package/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.fr.md +74 -0
- package/CHANGELOG.md +74 -0
- package/PROGRESS.md +59 -57
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +10 -0
- package/commands/generate.js +551 -2
- package/commands/generateConf.js +4 -4
- package/commands/help.js +11 -0
- package/commands/info.js +2 -3
- package/commands/list.js +93 -0
- package/commands/new.js +26 -9
- package/jest.config.js +9 -0
- package/package.json +7 -2
- package/readme.md +59 -68
- package/tests/e2e/generator.spec.js +71 -0
- package/tests/unit/appModuleUpdater.spec.js +111 -0
- package/tests/unit/cliParser.spec.js +74 -0
- package/tests/unit/controllerGenerator.spec.js +145 -0
- package/tests/unit/dtoGenerator.spec.js +87 -0
- package/tests/unit/entityGenerator.spec.js +65 -0
- package/tests/unit/generateCommand.spec.js +100 -0
- package/tests/unit/health.spec.js +69 -0
- package/tests/unit/listCommand.spec.js +91 -0
- package/tests/unit/middlewareGenerator.spec.js +64 -0
- package/tests/unit/newCommand.spec.js +88 -0
- package/tests/unit/relationCommand.spec.js +202 -0
- package/tests/unit/throttler.spec.js +117 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +3 -1
- package/utils/file-utils/saveProjectConfig.js +3 -0
- package/utils/fullModeInput.js +4 -0
- package/utils/generators/application/dtoGenerator.js +20 -3
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +78 -1
- package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
- package/utils/generators/lightModuleGenerator.js +14 -0
- package/utils/generators/presentation/controllerGenerator.js +70 -19
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +11 -0
- package/utils/lightModeInput.js +14 -0
- package/utils/setups/orms/typeOrmSetup.js +11 -4
- package/utils/setups/projectSetup.js +10 -2
- package/utils/setups/setupAuth.js +334 -18
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +4 -1
- package/utils/setups/setupPrisma.js +110 -1
- package/utils/setups/setupSwagger.js +4 -1
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +21 -4
package/commands/list.js
ADDED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
186
|
-
const useSwagger = flags.swagger
|
|
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:
|
|
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
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestcraftx",
|
|
3
|
-
"version": "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": "
|
|
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.
|
|
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
|
|
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
|
|
55
|
+
## What's New in v1.0.0 (Stable Release)
|
|
56
56
|
|
|
57
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
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
|
-
###
|
|
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.
|
|
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
|
-
|
|
554
|
-
- [x]
|
|
555
|
-
- [x]
|
|
556
|
-
- [
|
|
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.
|
|
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
|
-
|
|
561
|
-
- [
|
|
562
|
-
- [
|
|
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.
|
|
565
|
-
|
|
566
|
-
- [ ]
|
|
567
|
-
- [ ]
|
|
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
|
-
- [ ]
|
|
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.
|
|
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
|
+
});
|