create-backlist 10.0.9 → 10.1.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.
@@ -5,7 +5,14 @@ import path from "node:path";
5
5
  import ejs from "ejs";
6
6
 
7
7
  import { analyzeFrontend } from "../analyzer.js";
8
- import { renderAndWrite, getTemplatePath } from "./template.js";
8
+ import {
9
+ renderAndWrite,
10
+ renderAndWriteAll,
11
+ getTemplatePath,
12
+ preloadTemplates,
13
+ } from "./template.js";
14
+
15
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
9
16
 
10
17
  function stripQuery(p) {
11
18
  return String(p || "").split("?")[0];
@@ -15,7 +22,6 @@ function safePascalName(name) {
15
22
  const cleaned = String(name || "Default")
16
23
  .split("?")[0]
17
24
  .replace(/[^a-zA-Z0-9]/g, "");
18
-
19
25
  if (!cleaned) return "Default";
20
26
  return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
21
27
  }
@@ -25,7 +31,6 @@ function sanitizeEndpoints(endpoints) {
25
31
 
26
32
  return endpoints.map((ep) => {
27
33
  const rawPath = stripQuery(ep.path || ep.route || "/");
28
-
29
34
  const parts = rawPath
30
35
  .split("/")
31
36
  .filter(Boolean)
@@ -33,7 +38,6 @@ function sanitizeEndpoints(endpoints) {
33
38
 
34
39
  const resource = parts[0] || "Default";
35
40
  const controllerName = safePascalName(resource);
36
-
37
41
  let functionName = "";
38
42
 
39
43
  if (controllerName.toLowerCase() === "auth") {
@@ -43,15 +47,12 @@ function sanitizeEndpoints(endpoints) {
43
47
  } else {
44
48
  const singularName = resource.endsWith("s") ? resource.slice(0, -1) : resource;
45
49
  const pluralName = resource.endsWith("s") ? resource : `${resource}s`;
46
-
47
50
  const pascalSingular = safePascalName(singularName);
48
51
  const pascalPlural = safePascalName(pluralName);
49
-
50
52
  const method = String(ep.method || "GET").toUpperCase();
51
-
52
53
  const hasId =
53
- rawPath.includes(":") ||
54
- rawPath.includes("{") ||
54
+ rawPath.includes(":") ||
55
+ rawPath.includes("{") ||
55
56
  /\/\d+/.test(rawPath);
56
57
 
57
58
  if (method === "GET") {
@@ -71,6 +72,32 @@ function sanitizeEndpoints(endpoints) {
71
72
  });
72
73
  }
73
74
 
75
+ // ─── Performance timer util ────────────────────────────────────────────────────
76
+ function timer() {
77
+ const start = Date.now();
78
+ return () => `${((Date.now() - start) / 1000).toFixed(2)}s`;
79
+ }
80
+
81
+ // ─── Retry with exponential backoff ───────────────────────────────────────────
82
+ async function withRetry(fn, { attempts = 3, baseDelay = 300, label = "task" } = {}) {
83
+ let lastErr;
84
+ for (let i = 0; i < attempts; i++) {
85
+ try {
86
+ return await fn();
87
+ } catch (err) {
88
+ lastErr = err;
89
+ if (i < attempts - 1) {
90
+ const delay = baseDelay * 2 ** i;
91
+ console.log(chalk.yellow(` [RETRY] ${label} failed (attempt ${i + 1}/${attempts}), retrying in ${delay}ms...`));
92
+ await new Promise(r => setTimeout(r, delay));
93
+ }
94
+ }
95
+ }
96
+ throw lastErr;
97
+ }
98
+
99
+ // ─── Main Generator ───────────────────────────────────────────────────────────
100
+
74
101
  export async function generateNodeProject(options) {
75
102
  const {
76
103
  projectDir,
@@ -83,43 +110,63 @@ export async function generateNodeProject(options) {
83
110
  } = options;
84
111
 
85
112
  const port = 8000;
113
+ const totalTimer = timer();
86
114
 
87
115
  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);
116
+ // ── Step 1: Analyze + Preload templates in PARALLEL ────────────────────────
117
+ const step1 = timer();
118
+ console.log(chalk.blue(" -> [1/11] Analyzing frontend & preloading templates in parallel..."));
119
+
120
+ const templatePrefetch = [
121
+ "node-ts-express/partials/package.json.ejs",
122
+ "node-ts-express/partials/routes.ts.ejs",
123
+ ...(addAuth ? [
124
+ "node-ts-express/partials/Auth.controller.ts.ejs",
125
+ "node-ts-express/partials/Auth.routes.ts.ejs",
126
+ "node-ts-express/partials/Auth.middleware.ts.ejs",
127
+ ] : []),
128
+ ...(addSeeder ? ["node-ts-express/partials/Seeder.ts.ejs"] : []),
129
+ ...(extraFeatures.includes("swagger") ? ["node-ts-express/partials/ApiDocs.ts.ejs"] : []),
130
+ ...(extraFeatures.includes("testing") ? ["node-ts-express/partials/App.test.ts.ejs"] : []),
131
+ ...(extraFeatures.includes("docker") ? [
132
+ "node-ts-express/partials/Dockerfile.ejs",
133
+ "node-ts-express/partials/docker-compose.yml.ejs",
134
+ ] : []),
135
+ ];
136
+
137
+ const [endpointsRaw] = await Promise.all([
138
+ analyzeFrontend(frontendSrcDir),
139
+ preloadTemplates(templatePrefetch).catch(() => {}), // non-fatal prefetch
140
+ ]);
141
+
142
+ let endpoints = Array.isArray(endpointsRaw) && endpointsRaw.length > 0
143
+ ? sanitizeEndpoints(endpointsRaw)
144
+ : [];
145
+
146
+ if (endpoints.length > 0) {
147
+ console.log(chalk.green(` -> Found ${endpoints.length} endpoints. ${chalk.gray(step1())}`));
95
148
  } else {
96
- endpoints = [];
97
- console.log(chalk.yellow(" -> No API endpoints found. A basic project will be created."));
149
+ console.log(chalk.yellow(` -> No API endpoints found. Basic project will be created. ${chalk.gray(step1())}`));
98
150
  }
99
151
 
100
- // --- Step 2: Identify Models to Generate ---
152
+ // ── Step 2: Build model map ────────────────────────────────────────────────
101
153
  const modelsToGenerate = new Map();
102
-
103
154
  endpoints.forEach((ep) => {
104
155
  if (!ep) return;
105
156
  const ctrl = safePascalName(ep.controllerName);
106
157
  if (ctrl === "Default" || ctrl === "Auth") return;
107
-
108
158
  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
- }
159
+ const fields = ep.schemaFields
160
+ ? Object.entries(ep.schemaFields).map(([key, type]) => ({
161
+ name: key, type, isUnique: key === "email",
162
+ }))
163
+ : [];
117
164
  modelsToGenerate.set(ctrl, { name: ctrl, fields });
118
165
  }
119
166
  });
120
167
 
121
168
  if (addAuth && !modelsToGenerate.has("User")) {
122
- console.log(chalk.yellow(' -> Authentication requires a "User" model. Creating a default one.'));
169
+ console.log(chalk.yellow(' -> Auth requires "User" model. Creating default.'));
123
170
  modelsToGenerate.set("User", {
124
171
  name: "User",
125
172
  fields: [
@@ -130,15 +177,39 @@ export async function generateNodeProject(options) {
130
177
  });
131
178
  }
132
179
 
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);
180
+ // ── Step 3: Scaffold dirs + copy base files in PARALLEL ───────────────────
181
+ const step3 = timer();
182
+ console.log(chalk.blue(" -> [3/11] Scaffolding directory structure in parallel..."));
137
183
 
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"));
184
+ const destSrcDir = path.join(projectDir, "src");
140
185
 
141
- // --- Step 4: package.json ---
186
+ const dirsToCreate = [
187
+ destSrcDir,
188
+ ...(modelsToGenerate.size > 0 ? [
189
+ path.join(destSrcDir, "application", "ports", "controllers"),
190
+ path.join(destSrcDir, "domain", "services"),
191
+ path.join(destSrcDir, "infrastructure", "adapters", "repositories"),
192
+ path.join(destSrcDir, "domain", "models"),
193
+ path.join(destSrcDir, "routes"),
194
+ path.join(destSrcDir, "middleware"),
195
+ path.join(destSrcDir, "controllers"),
196
+ ] : []),
197
+ ...(addSeeder ? [path.join(projectDir, "scripts")] : []),
198
+ ...(extraFeatures.includes("swagger") ? [path.join(destSrcDir, "utils")] : []),
199
+ ...(extraFeatures.includes("testing") ? [path.join(projectDir, "src", "__tests__")] : []),
200
+ ...(extraFeatures.includes("docker") ? [] : []),
201
+ ...(dbType === "prisma" ? [path.join(projectDir, "prisma")] : []),
202
+ ];
203
+
204
+ await Promise.all([
205
+ ...dirsToCreate.map(d => fs.ensureDir(d)),
206
+ fs.copy(getTemplatePath("node-ts-express/base/server.ts"), path.join(destSrcDir, "server.ts")),
207
+ fs.copy(getTemplatePath("node-ts-express/base/tsconfig.json"), path.join(projectDir, "tsconfig.json")),
208
+ ]);
209
+
210
+ console.log(chalk.gray(` Scaffolding done. ${step3()}`));
211
+
212
+ // ── Step 4: package.json (build + write) ───────────────────────────────────
142
213
  const packageJsonContent = JSON.parse(
143
214
  await ejs.renderFile(getTemplatePath("node-ts-express/partials/package.json.ejs"), { projectName })
144
215
  );
@@ -149,30 +220,26 @@ export async function generateNodeProject(options) {
149
220
  packageJsonContent.devDependencies.prisma = "^5.6.0";
150
221
  packageJsonContent.prisma = { seed: `ts-node ${addSeeder ? "scripts/seeder.ts" : "prisma/seed.ts"}` };
151
222
  }
152
-
153
223
  if (addAuth) {
154
224
  packageJsonContent.dependencies.jsonwebtoken = "^9.0.2";
155
225
  packageJsonContent.dependencies.bcryptjs = "^2.4.3";
156
226
  packageJsonContent.devDependencies["@types/jsonwebtoken"] = "^9.0.5";
157
227
  packageJsonContent.devDependencies["@types/bcryptjs"] = "^2.4.6";
158
228
  }
159
-
160
229
  if (addSeeder) {
161
230
  packageJsonContent.devDependencies["@faker-js/faker"] = "^8.3.1";
162
231
  if (!packageJsonContent.dependencies.chalk) packageJsonContent.dependencies.chalk = "^4.1.2";
163
232
  packageJsonContent.scripts.seed = "ts-node scripts/seeder.ts";
164
233
  packageJsonContent.scripts.destroy = "ts-node scripts/seeder.ts -d";
165
234
  }
166
-
167
235
  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";
236
+ Object.assign(packageJsonContent.devDependencies, {
237
+ jest: "^29.7.0", supertest: "^6.3.3",
238
+ "@types/jest": "^29.5.10", "@types/supertest": "^2.0.16",
239
+ "ts-jest": "^29.1.1",
240
+ });
173
241
  packageJsonContent.scripts.test = "jest --detectOpenHandles --forceExit";
174
242
  }
175
-
176
243
  if (extraFeatures.includes("swagger")) {
177
244
  packageJsonContent.dependencies["swagger-ui-express"] = "^5.0.0";
178
245
  packageJsonContent.dependencies["swagger-jsdoc"] = "^6.2.8";
@@ -181,216 +248,167 @@ export async function generateNodeProject(options) {
181
248
 
182
249
  await fs.writeJson(path.join(projectDir, "package.json"), packageJsonContent, { spaces: 2 });
183
250
 
184
- // --- Step 5: DB + Hexagonal Architecture Scaffolding ---
251
+ // ── Step 5: Generate DB schema / models (parallel per model) ──────────────
185
252
  if (modelsToGenerate.size > 0) {
253
+ const step5 = timer();
254
+ console.log(chalk.blue(` -> [5/11] Generating ${modelsToGenerate.size} model(s) in parallel...`));
255
+
186
256
  const portDir = path.join(destSrcDir, "application", "ports", "controllers");
187
257
  const serviceDir = path.join(destSrcDir, "domain", "services");
188
258
  const repoDir = path.join(destSrcDir, "infrastructure", "adapters", "repositories");
189
259
  const modelDir = path.join(destSrcDir, "domain", "models");
190
-
191
- await fs.ensureDir(portDir);
192
- await fs.ensureDir(serviceDir);
193
- await fs.ensureDir(repoDir);
260
+
261
+ const hexTasks = [];
194
262
 
195
263
  if (dbType === "mongoose") {
196
- console.log(chalk.blue(" -> Generating Mongoose domain models..."));
197
264
  await fs.ensureDir(modelDir);
198
-
199
265
  for (const [modelName, modelData] of modelsToGenerate.entries()) {
200
- const schema = (modelData.fields || []).reduce((acc, field) => {
201
- acc[field.name] = field.type;
266
+ const schema = (modelData.fields || []).reduce((acc, f) => {
267
+ acc[f.name] = f.type;
202
268
  return acc;
203
269
  }, {});
204
- await renderAndWrite(
205
- getTemplatePath("node-ts-express/partials/Model.ts.ejs"),
206
- path.join(modelDir, `${modelName}.model.ts`),
207
- { modelName, schema, projectName }
208
- );
270
+ hexTasks.push({
271
+ templatePath: getTemplatePath("node-ts-express/partials/Model.ts.ejs"),
272
+ outPath: path.join(modelDir, `${modelName}.model.ts`),
273
+ data: { modelName, schema, projectName },
274
+ });
209
275
  }
210
276
  } 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);
277
+ const prismaOutPath = path.join(projectDir, "prisma", "schema.prisma");
278
+ if (options.aiBlocks?.prismaSchema) {
279
+ hexTasks.push({ _raw: true, outPath: prismaOutPath, content: options.aiBlocks.prismaSchema });
216
280
  } 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
- );
281
+ hexTasks.push({
282
+ templatePath: getTemplatePath("node-ts-express/partials/PrismaSchema.prisma.ejs"),
283
+ outPath: prismaOutPath,
284
+ data: { modelsToGenerate: Array.from(modelsToGenerate.values()) },
285
+ });
222
286
  }
223
287
  }
224
288
 
225
- console.log(chalk.blue(" -> Generating Hexagonal Architecture layers (Controllers, Services, Repositories)..."));
226
289
  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
- }
290
+ if (modelName === "Auth") continue;
291
+ const blockData = {
292
+ modelName, projectName, dbType,
293
+ aiSecurityConfig: options.aiBlocks?.aiSecurityConfig,
294
+ aiDbRelations: options.aiBlocks?.aiDbRelations,
295
+ aiValidationLogic: options.aiBlocks?.aiValidationLogic,
296
+ };
297
+ hexTasks.push(
298
+ { templatePath: getTemplatePath("node-ts-express/partials/HexController.ts.ejs"), outPath: path.join(portDir, `${modelName}.controller.ts`), data: blockData },
299
+ { templatePath: getTemplatePath("node-ts-express/partials/HexService.ts.ejs"), outPath: path.join(serviceDir, `${modelName}.service.ts`), data: blockData },
300
+ { templatePath: getTemplatePath("node-ts-express/partials/HexRepository.ts.ejs"), outPath: path.join(repoDir, `${modelName}.repository.ts`), data: blockData }
301
+ );
256
302
  }
303
+
304
+ // Execute all hex tasks in parallel batches
305
+ const rawTasks = hexTasks.filter(t => t._raw);
306
+ const renderTasks = hexTasks.filter(t => !t._raw);
307
+
308
+ await Promise.all([
309
+ renderAndWriteAll(renderTasks),
310
+ ...rawTasks.map(t => fs.outputFile(t.outPath, t.content)),
311
+ ]);
312
+
313
+ console.log(chalk.gray(` Models/Hex layers done. ${step5()}`));
257
314
  }
258
315
 
259
- // --- Step 6: Auth ---
316
+ // ── Step 6: Auth, Seeder, Swagger, Docker, Testing — ALL IN PARALLEL ───────
317
+ const step6 = timer();
318
+ const parallelExtras = [];
319
+
260
320
  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 }
321
+ console.log(chalk.blue(" -> [6a] Queuing auth boilerplate..."));
322
+ parallelExtras.push(
323
+ renderAndWrite(getTemplatePath("node-ts-express/partials/Auth.controller.ts.ejs"), path.join(destSrcDir, "controllers", "Auth.controller.ts"), { dbType, projectName }),
324
+ renderAndWrite(getTemplatePath("node-ts-express/partials/Auth.routes.ts.ejs"), path.join(destSrcDir, "routes", "Auth.routes.ts"), { projectName }),
325
+ renderAndWrite(getTemplatePath("node-ts-express/partials/Auth.middleware.ts.ejs"), path.join(destSrcDir, "middleware", "Auth.middleware.ts"), { projectName })
279
326
  );
280
327
  }
281
328
 
282
- // --- Step 7: Seeder ---
283
329
  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()) }
330
+ console.log(chalk.blue(" -> [6b] Queuing seeder..."));
331
+ parallelExtras.push(
332
+ renderAndWrite(getTemplatePath("node-ts-express/partials/Seeder.ts.ejs"), path.join(projectDir, "scripts", "seeder.ts"), { projectName, models: Array.from(modelsToGenerate.values()) })
290
333
  );
291
334
  }
292
335
 
293
- // --- Step 8: Extras (FIXED) ---
294
336
  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 }
337
+ console.log(chalk.blue(" -> [6c] Queuing Docker files..."));
338
+ parallelExtras.push(
339
+ renderAndWrite(getTemplatePath("node-ts-express/partials/Dockerfile.ejs"), path.join(projectDir, "Dockerfile"), { dbType, port }),
340
+ renderAndWrite(getTemplatePath("node-ts-express/partials/docker-compose.yml.ejs"), path.join(projectDir, "docker-compose.yml"), { projectName, dbType, port, addAuth, extraFeatures })
305
341
  );
306
342
  }
307
343
 
308
344
  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 }
345
+ console.log(chalk.blue(" -> [6d] Queuing Swagger setup..."));
346
+ parallelExtras.push(
347
+ renderAndWrite(getTemplatePath("node-ts-express/partials/ApiDocs.ts.ejs"), path.join(destSrcDir, "utils", "swagger.ts"), { projectName, port, addAuth, paths: endpoints })
316
348
  );
317
349
  }
318
350
 
319
351
  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 }
352
+ console.log(chalk.blue(" -> [6e] Queuing test boilerplate..."));
353
+ const jestConfig = "/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n verbose: true,\n};";
354
+ parallelExtras.push(
355
+ fs.writeFile(path.join(projectDir, "jest.config.js"), jestConfig),
356
+ renderAndWrite(getTemplatePath("node-ts-express/partials/App.test.ts.ejs"), path.join(projectDir, "src", "__tests__", "api.test.ts"), { addAuth, endpoints })
331
357
  );
332
358
  }
333
359
 
334
- // --- Step 9: routes.ts + server inject ---
360
+ // Non-auth routes (always needed)
335
361
  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 }
362
+ parallelExtras.push(
363
+ renderAndWrite(getTemplatePath("node-ts-express/partials/routes.ts.ejs"), path.join(destSrcDir, "routes.ts"), { endpoints: nonAuthEndpoints, addAuth, dbType })
341
364
  );
342
365
 
366
+ await Promise.allSettled(parallelExtras);
367
+ console.log(chalk.gray(` Extras (auth/seeder/docker/swagger/tests/routes) done. ${step6()}`));
368
+
369
+ // ── Step 7: Patch server.ts ────────────────────────────────────────────────
343
370
  let serverFileContent = await fs.readFile(path.join(destSrcDir, "server.ts"), "utf-8");
344
371
 
345
372
  let dbConnectionCode = "";
346
- let swaggerInjector = "";
347
- let authRoutesInjector = "";
348
-
349
373
  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
- `;
374
+ dbConnectionCode = `\nimport mongoose from 'mongoose';\nconst MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';\nmongoose.connect(MONGO_URI).then(() => console.log('MongoDB Connected...')).catch(err => console.error(err));\n`;
357
375
  } else if (dbType === "prisma") {
358
- dbConnectionCode = `
359
- import { PrismaClient } from '@prisma/client';
360
- export const prisma = new PrismaClient();
361
- `;
376
+ dbConnectionCode = `\nimport { PrismaClient } from '@prisma/client';\nexport const prisma = new PrismaClient();\n`;
362
377
  }
363
378
 
364
- if (extraFeatures.includes("swagger")) {
365
- swaggerInjector = "\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n";
366
- }
379
+ const swaggerInjector = extraFeatures.includes("swagger")
380
+ ? "\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n"
381
+ : "";
367
382
 
368
- if (addAuth) {
369
- authRoutesInjector = "import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n";
370
- }
383
+ const authRoutesInjector = addAuth
384
+ ? "import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n"
385
+ : "";
371
386
 
372
387
  serverFileContent = serverFileContent
373
388
  .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`);
389
+ .replace("// INJECT:ROUTES", `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes);`)
390
+ .replace(/(app\.listen\()/, `${swaggerInjector}\n$1`);
381
391
 
382
392
  await fs.writeFile(path.join(destSrcDir, "server.ts"), serverFileContent);
383
393
 
384
- // --- Step 10: Install deps ---
385
- console.log(chalk.magenta(" -> Installing dependencies... This may take a moment."));
386
- await execa("npm", ["install"], { cwd: projectDir });
394
+ // ── Step 8: Install deps + prisma generate in sequence ────────────────────
395
+ console.log(chalk.magenta(" -> [8/11] Installing dependencies..."));
396
+ const step8 = timer();
397
+
398
+ await withRetry(() => execa("npm", ["install"], { cwd: projectDir }), {
399
+ attempts: 2, baseDelay: 1000, label: "npm install",
400
+ });
401
+
402
+ console.log(chalk.gray(` npm install done. ${step8()}`));
387
403
 
388
404
  if (dbType === "prisma") {
389
405
  console.log(chalk.blue(" -> Running `prisma generate`..."));
390
- await execa("npx", ["prisma", "generate"], { cwd: projectDir });
406
+ await withRetry(() => execa("npx", ["prisma", "generate"], { cwd: projectDir }), {
407
+ attempts: 2, baseDelay: 500, label: "prisma generate",
408
+ });
391
409
  }
392
410
 
393
- // --- Step 11: .env.example ---
411
+ // ── Step 9: .env.example ───────────────────────────────────────────────────
394
412
  let envContent = `PORT=${port}\n`;
395
413
  if (dbType === "mongoose") envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
396
414
  if (dbType === "prisma") envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
@@ -398,7 +416,7 @@ app.use('/api', apiRoutes);`
398
416
 
399
417
  await fs.writeFile(path.join(projectDir, ".env.example"), envContent);
400
418
 
401
- console.log(chalk.green(" -> Node backend generation complete."));
419
+ console.log(chalk.green(` -> Node backend generation complete. Total: ${chalk.bold(totalTimer())}`));
402
420
  } catch (error) {
403
421
  throw error;
404
422
  }