create-backlist 6.2.3 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,374 +1,405 @@
1
- const chalk = require("chalk");
2
- const { execa } = require("execa");
3
- const fs = require("fs-extra");
4
- const path = require("path");
5
- const ejs = require("ejs");
6
-
7
- const { analyzeFrontend } = require("../analyzer");
8
- const { renderAndWrite, getTemplatePath } = require("./template");
9
-
10
- function stripQuery(p) {
11
- return String(p || "").split("?")[0];
12
- }
13
-
14
- function safePascalName(name) {
15
- const cleaned = String(name || "Default")
16
- .split("?")[0]
17
- .replace(/[^a-zA-Z0-9]/g, "");
18
-
19
- if (!cleaned) return "Default";
20
- return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
21
- }
22
-
23
- function sanitizeEndpoints(endpoints) {
24
- if (!Array.isArray(endpoints)) return [];
25
-
26
- return endpoints.map((ep) => {
27
- const rawPath = stripQuery(ep.path || ep.route || "/");
28
-
29
- const parts = rawPath
30
- .split("/")
31
- .filter(Boolean)
32
- .filter((p) => p !== "api" && !/^v\d+$/i.test(p));
33
-
34
- const resource = parts[0] || "Default";
35
- const controllerName = safePascalName(resource);
36
-
37
- let functionName = "";
38
-
39
- if (controllerName.toLowerCase() === "auth") {
40
- if (rawPath.includes("login")) functionName = "loginUser";
41
- else if (rawPath.includes("register")) functionName = "registerUser";
42
- else functionName = "authAction";
43
- } else {
44
- const singularName = resource.endsWith("s") ? resource.slice(0, -1) : resource;
45
- const pluralName = resource.endsWith("s") ? resource : `${resource}s`;
46
-
47
- const pascalSingular = safePascalName(singularName);
48
- const pascalPlural = safePascalName(pluralName);
49
-
50
- const method = String(ep.method || "GET").toUpperCase();
51
-
52
- const hasId =
53
- rawPath.includes(":") ||
54
- rawPath.includes("{") ||
55
- /\/\d+/.test(rawPath);
56
-
57
- if (method === "GET") {
58
- functionName = hasId ? `get${pascalSingular}ById` : `getAll${pascalPlural}`;
59
- } else if (method === "POST") {
60
- functionName = `create${pascalSingular}`;
61
- } else if (method === "PUT" || method === "PATCH") {
62
- functionName = `update${pascalSingular}ById`;
63
- } else if (method === "DELETE") {
64
- functionName = `delete${pascalSingular}ById`;
65
- } else {
66
- functionName = `${method.toLowerCase()}${pascalPlural}`;
67
- }
68
- }
69
-
70
- return { ...ep, path: rawPath, controllerName, functionName };
71
- });
72
- }
73
-
74
- async function generateNodeProject(options) {
75
- const {
76
- projectDir,
77
- projectName,
78
- frontendSrcDir,
79
- dbType,
80
- addAuth,
81
- addSeeder,
82
- extraFeatures = [],
83
- } = options;
84
-
85
- const port = 8000;
86
-
87
- try {
88
- // --- Step 1: Analyze Frontend ---
89
- console.log(chalk.blue(" -> Analyzing frontend for API endpoints..."));
90
- let endpoints = await analyzeFrontend(frontendSrcDir);
91
-
92
- if (Array.isArray(endpoints) && endpoints.length > 0) {
93
- console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
94
- endpoints = sanitizeEndpoints(endpoints);
95
- } else {
96
- endpoints = [];
97
- console.log(chalk.yellow(" -> No API endpoints found. A basic project will be created."));
98
- }
99
-
100
- // --- Step 2: Identify Models to Generate ---
101
- const modelsToGenerate = new Map();
102
-
103
- endpoints.forEach((ep) => {
104
- if (!ep) return;
105
- const ctrl = safePascalName(ep.controllerName);
106
- if (ctrl === "Default" || ctrl === "Auth") return;
107
-
108
- if (!modelsToGenerate.has(ctrl)) {
109
- let fields = [];
110
- if (ep.schemaFields) {
111
- fields = Object.entries(ep.schemaFields).map(([key, type]) => ({
112
- name: key,
113
- type,
114
- isUnique: key === "email",
115
- }));
116
- }
117
- modelsToGenerate.set(ctrl, { name: ctrl, fields });
118
- }
119
- });
120
-
121
- if (addAuth && !modelsToGenerate.has("User")) {
122
- console.log(chalk.yellow(' -> Authentication requires a "User" model. Creating a default one.'));
123
- modelsToGenerate.set("User", {
124
- name: "User",
125
- fields: [
126
- { name: "name", type: "String" },
127
- { name: "email", type: "String", isUnique: true },
128
- { name: "password", type: "String" },
129
- ],
130
- });
131
- }
132
-
133
- // --- Step 3: Base Scaffolding ---
134
- console.log(chalk.blue(" -> Scaffolding Node.js project..."));
135
- const destSrcDir = path.join(projectDir, "src");
136
- await fs.ensureDir(destSrcDir);
137
-
138
- await fs.copy(getTemplatePath("node-ts-express/base/server.ts"), path.join(destSrcDir, "server.ts"));
139
- await fs.copy(getTemplatePath("node-ts-express/base/tsconfig.json"), path.join(projectDir, "tsconfig.json"));
140
-
141
- // --- Step 4: package.json ---
142
- const packageJsonContent = JSON.parse(
143
- await ejs.renderFile(getTemplatePath("node-ts-express/partials/package.json.ejs"), { projectName })
144
- );
145
-
146
- if (dbType === "mongoose") packageJsonContent.dependencies.mongoose = "^7.6.3";
147
- if (dbType === "prisma") {
148
- packageJsonContent.dependencies["@prisma/client"] = "^5.6.0";
149
- packageJsonContent.devDependencies.prisma = "^5.6.0";
150
- packageJsonContent.prisma = { seed: `ts-node ${addSeeder ? "scripts/seeder.ts" : "prisma/seed.ts"}` };
151
- }
152
-
153
- if (addAuth) {
154
- packageJsonContent.dependencies.jsonwebtoken = "^9.0.2";
155
- packageJsonContent.dependencies.bcryptjs = "^2.4.3";
156
- packageJsonContent.devDependencies["@types/jsonwebtoken"] = "^9.0.5";
157
- packageJsonContent.devDependencies["@types/bcryptjs"] = "^2.4.6";
158
- }
159
-
160
- if (addSeeder) {
161
- packageJsonContent.devDependencies["@faker-js/faker"] = "^8.3.1";
162
- if (!packageJsonContent.dependencies.chalk) packageJsonContent.dependencies.chalk = "^4.1.2";
163
- packageJsonContent.scripts.seed = "ts-node scripts/seeder.ts";
164
- packageJsonContent.scripts.destroy = "ts-node scripts/seeder.ts -d";
165
- }
166
-
167
- if (extraFeatures.includes("testing")) {
168
- packageJsonContent.devDependencies.jest = "^29.7.0";
169
- packageJsonContent.devDependencies.supertest = "^6.3.3";
170
- packageJsonContent.devDependencies["@types/jest"] = "^29.5.10";
171
- packageJsonContent.devDependencies["@types/supertest"] = "^2.0.16";
172
- packageJsonContent.devDependencies["ts-jest"] = "^29.1.1";
173
- packageJsonContent.scripts.test = "jest --detectOpenHandles --forceExit";
174
- }
175
-
176
- if (extraFeatures.includes("swagger")) {
177
- packageJsonContent.dependencies["swagger-ui-express"] = "^5.0.0";
178
- packageJsonContent.dependencies["swagger-jsdoc"] = "^6.2.8";
179
- packageJsonContent.devDependencies["@types/swagger-ui-express"] = "^4.1.6";
180
- }
181
-
182
- await fs.writeJson(path.join(projectDir, "package.json"), packageJsonContent, { spaces: 2 });
183
-
184
- // --- Step 5: DB + Controllers ---
185
- if (modelsToGenerate.size > 0) {
186
- await fs.ensureDir(path.join(destSrcDir, "controllers"));
187
-
188
- if (dbType === "mongoose") {
189
- console.log(chalk.blue(" -> Generating Mongoose models and controllers..."));
190
- await fs.ensureDir(path.join(destSrcDir, "models"));
191
-
192
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
193
- const schema = (modelData.fields || []).reduce((acc, field) => {
194
- acc[field.name] = field.type;
195
- return acc;
196
- }, {});
197
- await renderAndWrite(
198
- getTemplatePath("node-ts-express/partials/Model.ts.ejs"),
199
- path.join(destSrcDir, "models", `${modelName}.model.ts`),
200
- { modelName, schema, projectName }
201
- );
202
- }
203
- } else if (dbType === "prisma") {
204
- console.log(chalk.blue(" -> Generating Prisma schema..."));
205
- await fs.ensureDir(path.join(projectDir, "prisma"));
206
- await renderAndWrite(
207
- getTemplatePath("node-ts-express/partials/PrismaSchema.prisma.ejs"),
208
- path.join(projectDir, "prisma", "schema.prisma"),
209
- { modelsToGenerate: Array.from(modelsToGenerate.values()) }
210
- );
211
- }
212
-
213
- console.log(chalk.blue(" -> Generating controllers..."));
214
- for (const [modelName] of modelsToGenerate.entries()) {
215
- const templateFile = dbType === "mongoose" ? "Controller.ts.ejs" : "PrismaController.ts.ejs";
216
- if (modelName !== "Auth") {
217
- await renderAndWrite(
218
- getTemplatePath(`node-ts-express/partials/${templateFile}`),
219
- path.join(destSrcDir, "controllers", `${modelName}.controller.ts`),
220
- { modelName, projectName }
221
- );
222
- }
223
- }
224
- }
225
-
226
- // --- Step 6: Auth ---
227
- if (addAuth) {
228
- console.log(chalk.blue(" -> Generating authentication boilerplate..."));
229
- await fs.ensureDir(path.join(destSrcDir, "routes"));
230
- await fs.ensureDir(path.join(destSrcDir, "middleware"));
231
-
232
- await renderAndWrite(
233
- getTemplatePath("node-ts-express/partials/Auth.controller.ts.ejs"),
234
- path.join(destSrcDir, "controllers", "Auth.controller.ts"),
235
- { dbType, projectName }
236
- );
237
- await renderAndWrite(
238
- getTemplatePath("node-ts-express/partials/Auth.routes.ts.ejs"),
239
- path.join(destSrcDir, "routes", "Auth.routes.ts"),
240
- { projectName }
241
- );
242
- await renderAndWrite(
243
- getTemplatePath("node-ts-express/partials/Auth.middleware.ts.ejs"),
244
- path.join(destSrcDir, "middleware", "Auth.middleware.ts"),
245
- { projectName }
246
- );
247
- }
248
-
249
- // --- Step 7: Seeder ---
250
- if (addSeeder) {
251
- console.log(chalk.blue(" -> Generating database seeder script..."));
252
- await fs.ensureDir(path.join(projectDir, "scripts"));
253
- await renderAndWrite(
254
- getTemplatePath("node-ts-express/partials/Seeder.ts.ejs"),
255
- path.join(projectDir, "scripts", "seeder.ts"),
256
- { projectName, models: Array.from(modelsToGenerate.values()) }
257
- );
258
- }
259
-
260
- // --- Step 8: Extras (FIXED) ---
261
- if (extraFeatures.includes("docker")) {
262
- console.log(chalk.blue(" -> Generating Docker files..."));
263
- await renderAndWrite(
264
- getTemplatePath("node-ts-express/partials/Dockerfile.ejs"),
265
- path.join(projectDir, "Dockerfile"),
266
- { dbType, port }
267
- );
268
- await renderAndWrite(
269
- getTemplatePath("node-ts-express/partials/docker-compose.yml.ejs"),
270
- path.join(projectDir, "docker-compose.yml"),
271
- { projectName, dbType, port, addAuth, extraFeatures }
272
- );
273
- }
274
-
275
- if (extraFeatures.includes("swagger")) {
276
- console.log(chalk.blue(" -> Generating API documentation setup..."));
277
- await fs.ensureDir(path.join(destSrcDir, "utils"));
278
- // FIX: Added 'paths' to the EJS data object
279
- await renderAndWrite(
280
- getTemplatePath("node-ts-express/partials/ApiDocs.ts.ejs"),
281
- path.join(destSrcDir, "utils", "swagger.ts"),
282
- { projectName, port, addAuth, paths: endpoints }
283
- );
284
- }
285
-
286
- if (extraFeatures.includes("testing")) {
287
- console.log(chalk.blue(" -> Generating testing boilerplate..."));
288
- const jestConfig =
289
- "/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n verbose: true,\n};";
290
-
291
- await fs.writeFile(path.join(projectDir, "jest.config.js"), jestConfig);
292
- await fs.ensureDir(path.join(projectDir, "src", "__tests__"));
293
-
294
- await renderAndWrite(
295
- getTemplatePath("node-ts-express/partials/App.test.ts.ejs"),
296
- path.join(projectDir, "src", "__tests__", "api.test.ts"),
297
- { addAuth, endpoints }
298
- );
299
- }
300
-
301
- // --- Step 9: routes.ts + server inject ---
302
- const nonAuthEndpoints = endpoints.filter((ep) => safePascalName(ep.controllerName) !== "Auth");
303
-
304
- await renderAndWrite(
305
- getTemplatePath("node-ts-express/partials/routes.ts.ejs"),
306
- path.join(destSrcDir, "routes.ts"),
307
- { endpoints: nonAuthEndpoints, addAuth, dbType }
308
- );
309
-
310
- let serverFileContent = await fs.readFile(path.join(destSrcDir, "server.ts"), "utf-8");
311
-
312
- let dbConnectionCode = "";
313
- let swaggerInjector = "";
314
- let authRoutesInjector = "";
315
-
316
- if (dbType === "mongoose") {
317
- dbConnectionCode = `
318
- import mongoose from 'mongoose';
319
- const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';
320
- mongoose.connect(MONGO_URI)
321
- .then(() => console.log('MongoDB Connected...'))
322
- .catch(err => console.error(err));
323
- `;
324
- } else if (dbType === "prisma") {
325
- dbConnectionCode = `
326
- import { PrismaClient } from '@prisma/client';
327
- export const prisma = new PrismaClient();
328
- `;
329
- }
330
-
331
- if (extraFeatures.includes("swagger")) {
332
- swaggerInjector = "\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n";
333
- }
334
-
335
- if (addAuth) {
336
- authRoutesInjector = "import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n";
337
- }
338
-
339
- serverFileContent = serverFileContent
340
- .replace("dotenv.config();", `dotenv.config();${dbConnectionCode}`)
341
- .replace(
342
- "// INJECT:ROUTES",
343
- `${authRoutesInjector}import apiRoutes from './routes';
344
- app.use('/api', apiRoutes);`
345
- );
346
-
347
- serverFileContent = serverFileContent.replace(/(app\.listen\()/, `${swaggerInjector}\n$1`);
348
-
349
- await fs.writeFile(path.join(destSrcDir, "server.ts"), serverFileContent);
350
-
351
- // --- Step 10: Install deps ---
352
- console.log(chalk.magenta(" -> Installing dependencies... This may take a moment."));
353
- await execa("npm", ["install"], { cwd: projectDir });
354
-
355
- if (dbType === "prisma") {
356
- console.log(chalk.blue(" -> Running `prisma generate`..."));
357
- await execa("npx", ["prisma", "generate"], { cwd: projectDir });
358
- }
359
-
360
- // --- Step 11: .env.example ---
361
- let envContent = `PORT=${port}\n`;
362
- if (dbType === "mongoose") envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
363
- if (dbType === "prisma") envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
364
- if (addAuth) envContent += "JWT_SECRET=your_super_secret_jwt_key_12345\nJWT_EXPIRES_IN=5h\n";
365
-
366
- await fs.writeFile(path.join(projectDir, ".env.example"), envContent);
367
-
368
- console.log(chalk.green(" -> Node backend generation complete."));
369
- } catch (error) {
370
- throw error;
371
- }
372
- }
373
-
374
- module.exports = { generateNodeProject };
1
+ import chalk from "chalk";
2
+ import { execa } from "execa";
3
+ import fs from "fs-extra";
4
+ import path from "node:path";
5
+ import ejs from "ejs";
6
+
7
+ import { analyzeFrontend } from "../analyzer.js";
8
+ import { renderAndWrite, getTemplatePath } from "./template.js";
9
+
10
+ function stripQuery(p) {
11
+ return String(p || "").split("?")[0];
12
+ }
13
+
14
+ function safePascalName(name) {
15
+ const cleaned = String(name || "Default")
16
+ .split("?")[0]
17
+ .replace(/[^a-zA-Z0-9]/g, "");
18
+
19
+ if (!cleaned) return "Default";
20
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
21
+ }
22
+
23
+ function sanitizeEndpoints(endpoints) {
24
+ if (!Array.isArray(endpoints)) return [];
25
+
26
+ return endpoints.map((ep) => {
27
+ const rawPath = stripQuery(ep.path || ep.route || "/");
28
+
29
+ const parts = rawPath
30
+ .split("/")
31
+ .filter(Boolean)
32
+ .filter((p) => p !== "api" && !/^v\d+$/i.test(p));
33
+
34
+ const resource = parts[0] || "Default";
35
+ const controllerName = safePascalName(resource);
36
+
37
+ let functionName = "";
38
+
39
+ if (controllerName.toLowerCase() === "auth") {
40
+ if (rawPath.includes("login")) functionName = "loginUser";
41
+ else if (rawPath.includes("register")) functionName = "registerUser";
42
+ else functionName = "authAction";
43
+ } else {
44
+ const singularName = resource.endsWith("s") ? resource.slice(0, -1) : resource;
45
+ const pluralName = resource.endsWith("s") ? resource : `${resource}s`;
46
+
47
+ const pascalSingular = safePascalName(singularName);
48
+ const pascalPlural = safePascalName(pluralName);
49
+
50
+ const method = String(ep.method || "GET").toUpperCase();
51
+
52
+ const hasId =
53
+ rawPath.includes(":") ||
54
+ rawPath.includes("{") ||
55
+ /\/\d+/.test(rawPath);
56
+
57
+ if (method === "GET") {
58
+ functionName = hasId ? `get${pascalSingular}ById` : `getAll${pascalPlural}`;
59
+ } else if (method === "POST") {
60
+ functionName = `create${pascalSingular}`;
61
+ } else if (method === "PUT" || method === "PATCH") {
62
+ functionName = `update${pascalSingular}ById`;
63
+ } else if (method === "DELETE") {
64
+ functionName = `delete${pascalSingular}ById`;
65
+ } else {
66
+ functionName = `${method.toLowerCase()}${pascalPlural}`;
67
+ }
68
+ }
69
+
70
+ return { ...ep, path: rawPath, controllerName, functionName };
71
+ });
72
+ }
73
+
74
+ export async function generateNodeProject(options) {
75
+ const {
76
+ projectDir,
77
+ projectName,
78
+ frontendSrcDir,
79
+ dbType,
80
+ addAuth,
81
+ addSeeder,
82
+ extraFeatures = [],
83
+ } = options;
84
+
85
+ const port = 8000;
86
+
87
+ try {
88
+ // --- Step 1: Analyze Frontend ---
89
+ console.log(chalk.blue(" -> Analyzing frontend for API endpoints..."));
90
+ let endpoints = await analyzeFrontend(frontendSrcDir);
91
+
92
+ if (Array.isArray(endpoints) && endpoints.length > 0) {
93
+ console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
94
+ endpoints = sanitizeEndpoints(endpoints);
95
+ } else {
96
+ endpoints = [];
97
+ console.log(chalk.yellow(" -> No API endpoints found. A basic project will be created."));
98
+ }
99
+
100
+ // --- Step 2: Identify Models to Generate ---
101
+ const modelsToGenerate = new Map();
102
+
103
+ endpoints.forEach((ep) => {
104
+ if (!ep) return;
105
+ const ctrl = safePascalName(ep.controllerName);
106
+ if (ctrl === "Default" || ctrl === "Auth") return;
107
+
108
+ if (!modelsToGenerate.has(ctrl)) {
109
+ let fields = [];
110
+ if (ep.schemaFields) {
111
+ fields = Object.entries(ep.schemaFields).map(([key, type]) => ({
112
+ name: key,
113
+ type,
114
+ isUnique: key === "email",
115
+ }));
116
+ }
117
+ modelsToGenerate.set(ctrl, { name: ctrl, fields });
118
+ }
119
+ });
120
+
121
+ if (addAuth && !modelsToGenerate.has("User")) {
122
+ console.log(chalk.yellow(' -> Authentication requires a "User" model. Creating a default one.'));
123
+ modelsToGenerate.set("User", {
124
+ name: "User",
125
+ fields: [
126
+ { name: "name", type: "String" },
127
+ { name: "email", type: "String", isUnique: true },
128
+ { name: "password", type: "String" },
129
+ ],
130
+ });
131
+ }
132
+
133
+ // --- Step 3: Base Scaffolding ---
134
+ console.log(chalk.blue(" -> Scaffolding Node.js project..."));
135
+ const destSrcDir = path.join(projectDir, "src");
136
+ await fs.ensureDir(destSrcDir);
137
+
138
+ await fs.copy(getTemplatePath("node-ts-express/base/server.ts"), path.join(destSrcDir, "server.ts"));
139
+ await fs.copy(getTemplatePath("node-ts-express/base/tsconfig.json"), path.join(projectDir, "tsconfig.json"));
140
+
141
+ // --- Step 4: package.json ---
142
+ const packageJsonContent = JSON.parse(
143
+ await ejs.renderFile(getTemplatePath("node-ts-express/partials/package.json.ejs"), { projectName })
144
+ );
145
+
146
+ if (dbType === "mongoose") packageJsonContent.dependencies.mongoose = "^7.6.3";
147
+ if (dbType === "prisma") {
148
+ packageJsonContent.dependencies["@prisma/client"] = "^5.6.0";
149
+ packageJsonContent.devDependencies.prisma = "^5.6.0";
150
+ packageJsonContent.prisma = { seed: `ts-node ${addSeeder ? "scripts/seeder.ts" : "prisma/seed.ts"}` };
151
+ }
152
+
153
+ if (addAuth) {
154
+ packageJsonContent.dependencies.jsonwebtoken = "^9.0.2";
155
+ packageJsonContent.dependencies.bcryptjs = "^2.4.3";
156
+ packageJsonContent.devDependencies["@types/jsonwebtoken"] = "^9.0.5";
157
+ packageJsonContent.devDependencies["@types/bcryptjs"] = "^2.4.6";
158
+ }
159
+
160
+ if (addSeeder) {
161
+ packageJsonContent.devDependencies["@faker-js/faker"] = "^8.3.1";
162
+ if (!packageJsonContent.dependencies.chalk) packageJsonContent.dependencies.chalk = "^4.1.2";
163
+ packageJsonContent.scripts.seed = "ts-node scripts/seeder.ts";
164
+ packageJsonContent.scripts.destroy = "ts-node scripts/seeder.ts -d";
165
+ }
166
+
167
+ if (extraFeatures.includes("testing")) {
168
+ packageJsonContent.devDependencies.jest = "^29.7.0";
169
+ packageJsonContent.devDependencies.supertest = "^6.3.3";
170
+ packageJsonContent.devDependencies["@types/jest"] = "^29.5.10";
171
+ packageJsonContent.devDependencies["@types/supertest"] = "^2.0.16";
172
+ packageJsonContent.devDependencies["ts-jest"] = "^29.1.1";
173
+ packageJsonContent.scripts.test = "jest --detectOpenHandles --forceExit";
174
+ }
175
+
176
+ if (extraFeatures.includes("swagger")) {
177
+ packageJsonContent.dependencies["swagger-ui-express"] = "^5.0.0";
178
+ packageJsonContent.dependencies["swagger-jsdoc"] = "^6.2.8";
179
+ packageJsonContent.devDependencies["@types/swagger-ui-express"] = "^4.1.6";
180
+ }
181
+
182
+ await fs.writeJson(path.join(projectDir, "package.json"), packageJsonContent, { spaces: 2 });
183
+
184
+ // --- Step 5: DB + Hexagonal Architecture Scaffolding ---
185
+ if (modelsToGenerate.size > 0) {
186
+ const portDir = path.join(destSrcDir, "application", "ports", "controllers");
187
+ const serviceDir = path.join(destSrcDir, "domain", "services");
188
+ const repoDir = path.join(destSrcDir, "infrastructure", "adapters", "repositories");
189
+ const modelDir = path.join(destSrcDir, "domain", "models");
190
+
191
+ await fs.ensureDir(portDir);
192
+ await fs.ensureDir(serviceDir);
193
+ await fs.ensureDir(repoDir);
194
+
195
+ if (dbType === "mongoose") {
196
+ console.log(chalk.blue(" -> Generating Mongoose domain models..."));
197
+ await fs.ensureDir(modelDir);
198
+
199
+ for (const [modelName, modelData] of modelsToGenerate.entries()) {
200
+ const schema = (modelData.fields || []).reduce((acc, field) => {
201
+ acc[field.name] = field.type;
202
+ return acc;
203
+ }, {});
204
+ await renderAndWrite(
205
+ getTemplatePath("node-ts-express/partials/Model.ts.ejs"),
206
+ path.join(modelDir, `${modelName}.model.ts`),
207
+ { modelName, schema, projectName }
208
+ );
209
+ }
210
+ } else if (dbType === "prisma") {
211
+ console.log(chalk.blue(" -> Generating Prisma schema..."));
212
+ await fs.ensureDir(path.join(projectDir, "prisma"));
213
+ // Check if we already have a generated schema from AI Pass 1 (options.aiBlocks.prismaSchema)
214
+ if (options.aiBlocks && options.aiBlocks.prismaSchema) {
215
+ await fs.writeFile(path.join(projectDir, "prisma", "schema.prisma"), options.aiBlocks.prismaSchema);
216
+ } else {
217
+ await renderAndWrite(
218
+ getTemplatePath("node-ts-express/partials/PrismaSchema.prisma.ejs"),
219
+ path.join(projectDir, "prisma", "schema.prisma"),
220
+ { modelsToGenerate: Array.from(modelsToGenerate.values()) }
221
+ );
222
+ }
223
+ }
224
+
225
+ console.log(chalk.blue(" -> Generating Hexagonal Architecture layers (Controllers, Services, Repositories)..."));
226
+ for (const [modelName] of modelsToGenerate.entries()) {
227
+ if (modelName !== "Auth") {
228
+ const blockData = {
229
+ modelName,
230
+ projectName,
231
+ dbType,
232
+ aiSecurityConfig: options.aiBlocks?.aiSecurityConfig,
233
+ aiDbRelations: options.aiBlocks?.aiDbRelations,
234
+ aiValidationLogic: options.aiBlocks?.aiValidationLogic
235
+ };
236
+
237
+ // Controller (Port)
238
+ await renderAndWrite(
239
+ getTemplatePath(`node-ts-express/partials/HexController.ts.ejs`),
240
+ path.join(portDir, `${modelName}.controller.ts`),
241
+ blockData
242
+ );
243
+ // Service (Domain)
244
+ await renderAndWrite(
245
+ getTemplatePath(`node-ts-express/partials/HexService.ts.ejs`),
246
+ path.join(serviceDir, `${modelName}.service.ts`),
247
+ blockData
248
+ );
249
+ // Repository (Adapter)
250
+ await renderAndWrite(
251
+ getTemplatePath(`node-ts-express/partials/HexRepository.ts.ejs`),
252
+ path.join(repoDir, `${modelName}.repository.ts`),
253
+ blockData
254
+ );
255
+ }
256
+ }
257
+ }
258
+
259
+ // --- Step 6: Auth ---
260
+ if (addAuth) {
261
+ console.log(chalk.blue(" -> Generating authentication boilerplate..."));
262
+ await fs.ensureDir(path.join(destSrcDir, "routes"));
263
+ await fs.ensureDir(path.join(destSrcDir, "middleware"));
264
+
265
+ await renderAndWrite(
266
+ getTemplatePath("node-ts-express/partials/Auth.controller.ts.ejs"),
267
+ path.join(destSrcDir, "controllers", "Auth.controller.ts"),
268
+ { dbType, projectName }
269
+ );
270
+ await renderAndWrite(
271
+ getTemplatePath("node-ts-express/partials/Auth.routes.ts.ejs"),
272
+ path.join(destSrcDir, "routes", "Auth.routes.ts"),
273
+ { projectName }
274
+ );
275
+ await renderAndWrite(
276
+ getTemplatePath("node-ts-express/partials/Auth.middleware.ts.ejs"),
277
+ path.join(destSrcDir, "middleware", "Auth.middleware.ts"),
278
+ { projectName }
279
+ );
280
+ }
281
+
282
+ // --- Step 7: Seeder ---
283
+ if (addSeeder) {
284
+ console.log(chalk.blue(" -> Generating database seeder script..."));
285
+ await fs.ensureDir(path.join(projectDir, "scripts"));
286
+ await renderAndWrite(
287
+ getTemplatePath("node-ts-express/partials/Seeder.ts.ejs"),
288
+ path.join(projectDir, "scripts", "seeder.ts"),
289
+ { projectName, models: Array.from(modelsToGenerate.values()) }
290
+ );
291
+ }
292
+
293
+ // --- Step 8: Extras (FIXED) ---
294
+ if (extraFeatures.includes("docker")) {
295
+ console.log(chalk.blue(" -> Generating Docker files..."));
296
+ await renderAndWrite(
297
+ getTemplatePath("node-ts-express/partials/Dockerfile.ejs"),
298
+ path.join(projectDir, "Dockerfile"),
299
+ { dbType, port }
300
+ );
301
+ await renderAndWrite(
302
+ getTemplatePath("node-ts-express/partials/docker-compose.yml.ejs"),
303
+ path.join(projectDir, "docker-compose.yml"),
304
+ { projectName, dbType, port, addAuth, extraFeatures }
305
+ );
306
+ }
307
+
308
+ if (extraFeatures.includes("swagger")) {
309
+ console.log(chalk.blue(" -> Generating API documentation setup..."));
310
+ await fs.ensureDir(path.join(destSrcDir, "utils"));
311
+ // FIX: Added 'paths' to the EJS data object
312
+ await renderAndWrite(
313
+ getTemplatePath("node-ts-express/partials/ApiDocs.ts.ejs"),
314
+ path.join(destSrcDir, "utils", "swagger.ts"),
315
+ { projectName, port, addAuth, paths: endpoints }
316
+ );
317
+ }
318
+
319
+ if (extraFeatures.includes("testing")) {
320
+ console.log(chalk.blue(" -> Generating testing boilerplate..."));
321
+ const jestConfig =
322
+ "/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n verbose: true,\n};";
323
+
324
+ await fs.writeFile(path.join(projectDir, "jest.config.js"), jestConfig);
325
+ await fs.ensureDir(path.join(projectDir, "src", "__tests__"));
326
+
327
+ await renderAndWrite(
328
+ getTemplatePath("node-ts-express/partials/App.test.ts.ejs"),
329
+ path.join(projectDir, "src", "__tests__", "api.test.ts"),
330
+ { addAuth, endpoints }
331
+ );
332
+ }
333
+
334
+ // --- Step 9: routes.ts + server inject ---
335
+ const nonAuthEndpoints = endpoints.filter((ep) => safePascalName(ep.controllerName) !== "Auth");
336
+
337
+ await renderAndWrite(
338
+ getTemplatePath("node-ts-express/partials/routes.ts.ejs"),
339
+ path.join(destSrcDir, "routes.ts"),
340
+ { endpoints: nonAuthEndpoints, addAuth, dbType }
341
+ );
342
+
343
+ let serverFileContent = await fs.readFile(path.join(destSrcDir, "server.ts"), "utf-8");
344
+
345
+ let dbConnectionCode = "";
346
+ let swaggerInjector = "";
347
+ let authRoutesInjector = "";
348
+
349
+ if (dbType === "mongoose") {
350
+ dbConnectionCode = `
351
+ import mongoose from 'mongoose';
352
+ const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';
353
+ mongoose.connect(MONGO_URI)
354
+ .then(() => console.log('MongoDB Connected...'))
355
+ .catch(err => console.error(err));
356
+ `;
357
+ } else if (dbType === "prisma") {
358
+ dbConnectionCode = `
359
+ import { PrismaClient } from '@prisma/client';
360
+ export const prisma = new PrismaClient();
361
+ `;
362
+ }
363
+
364
+ if (extraFeatures.includes("swagger")) {
365
+ swaggerInjector = "\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n";
366
+ }
367
+
368
+ if (addAuth) {
369
+ authRoutesInjector = "import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n";
370
+ }
371
+
372
+ serverFileContent = serverFileContent
373
+ .replace("dotenv.config();", `dotenv.config();${dbConnectionCode}`)
374
+ .replace(
375
+ "// INJECT:ROUTES",
376
+ `${authRoutesInjector}import apiRoutes from './routes';
377
+ app.use('/api', apiRoutes);`
378
+ );
379
+
380
+ serverFileContent = serverFileContent.replace(/(app\.listen\()/, `${swaggerInjector}\n$1`);
381
+
382
+ await fs.writeFile(path.join(destSrcDir, "server.ts"), serverFileContent);
383
+
384
+ // --- Step 10: Install deps ---
385
+ console.log(chalk.magenta(" -> Installing dependencies... This may take a moment."));
386
+ await execa("npm", ["install"], { cwd: projectDir });
387
+
388
+ if (dbType === "prisma") {
389
+ console.log(chalk.blue(" -> Running `prisma generate`..."));
390
+ await execa("npx", ["prisma", "generate"], { cwd: projectDir });
391
+ }
392
+
393
+ // --- Step 11: .env.example ---
394
+ let envContent = `PORT=${port}\n`;
395
+ if (dbType === "mongoose") envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
396
+ if (dbType === "prisma") envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
397
+ if (addAuth) envContent += "JWT_SECRET=your_super_secret_jwt_key_12345\nJWT_EXPIRES_IN=5h\n";
398
+
399
+ await fs.writeFile(path.join(projectDir, ".env.example"), envContent);
400
+
401
+ console.log(chalk.green(" -> Node backend generation complete."));
402
+ } catch (error) {
403
+ throw error;
404
+ }
405
+ }