create-backlist 10.1.0 → 10.1.2

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.
@@ -4,30 +4,45 @@ import fs from 'fs-extra';
4
4
  import path from 'node:path';
5
5
  import ejs from 'ejs';
6
6
  import { analyzeFrontend } from '../analyzer.js';
7
- import { renderAndWrite, getTemplatePath } from './template.js';
7
+ import { renderAndWrite, renderAndWriteAll, getTemplatePath, preloadTemplates } from './template.js';
8
+
9
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
10
+
11
+ function timer() {
12
+ const s = Date.now();
13
+ return () => `${((Date.now() - s) / 1000).toFixed(2)}s`;
14
+ }
15
+
16
+ async function withRetry(fn, { attempts = 3, baseDelay = 300, label = 'task' } = {}) {
17
+ let lastErr;
18
+ for (let i = 0; i < attempts; i++) {
19
+ try { return await fn(); } catch (err) {
20
+ lastErr = err;
21
+ if (i < attempts - 1) {
22
+ const delay = baseDelay * 2 ** i;
23
+ console.log(chalk.yellow(` [RETRY] ${label} (${i + 1}/${attempts}), retrying in ${delay}ms...`));
24
+ await new Promise(r => setTimeout(r, delay));
25
+ }
26
+ }
27
+ }
28
+ throw lastErr;
29
+ }
8
30
 
9
31
  function stripQuery(p) {
10
32
  return String(p || '').split('?')[0];
11
33
  }
12
34
 
13
35
  function safePascalName(name) {
14
- const cleaned = String(name || 'Default')
15
- .split('?')[0]
16
- .replace(/[^a-zA-Z0-9]/g, '');
36
+ const cleaned = String(name || 'Default').split('?')[0].replace(/[^a-zA-Z0-9]/g, '');
17
37
  if (!cleaned) return 'Default';
18
38
  return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
19
39
  }
20
40
 
21
41
  function sanitizeEndpoints(endpoints) {
22
42
  if (!Array.isArray(endpoints)) return [];
23
-
24
43
  return endpoints.map((ep) => {
25
44
  const rawPath = stripQuery(ep.path || ep.route || '/');
26
- const parts = rawPath
27
- .split('/')
28
- .filter(Boolean)
29
- .filter((p) => p !== 'api' && !/^v\d+$/i.test(p));
30
-
45
+ const parts = rawPath.split('/').filter(Boolean).filter((p) => p !== 'api' && !/^v\d+$/i.test(p));
31
46
  const resource = parts[0] || 'Default';
32
47
  const controllerName = safePascalName(resource);
33
48
  let functionName = '';
@@ -44,86 +59,108 @@ function sanitizeEndpoints(endpoints) {
44
59
  const method = String(ep.method || 'GET').toUpperCase();
45
60
  const hasId = rawPath.includes(':') || rawPath.includes('{') || /\/\d+/.test(rawPath);
46
61
 
47
- if (method === 'GET') {
48
- functionName = hasId ? `get${pascalSingular}ById` : `getAll${pascalPlural}`;
49
- } else if (method === 'POST') {
50
- functionName = `create${pascalSingular}`;
51
- } else if (method === 'PUT' || method === 'PATCH') {
52
- functionName = `update${pascalSingular}ById`;
53
- } else if (method === 'DELETE') {
54
- functionName = `delete${pascalSingular}ById`;
55
- } else {
56
- functionName = `${method.toLowerCase()}${pascalPlural}`;
57
- }
62
+ if (method === 'GET') functionName = hasId ? `get${pascalSingular}ById` : `getAll${pascalPlural}`;
63
+ else if (method === 'POST') functionName = `create${pascalSingular}`;
64
+ else if (method === 'PUT' || method === 'PATCH') functionName = `update${pascalSingular}ById`;
65
+ else if (method === 'DELETE') functionName = `delete${pascalSingular}ById`;
66
+ else functionName = `${method.toLowerCase()}${pascalPlural}`;
58
67
  }
59
68
 
60
69
  return { ...ep, path: rawPath, route: rawPath, controllerName, functionName };
61
70
  });
62
71
  }
63
72
 
64
- export async function generateJsProject(options) {
65
- const {
66
- projectDir,
67
- projectName,
68
- frontendSrcDir,
69
- dbType,
70
- addAuth,
71
- addSeeder,
72
- extraFeatures = [],
73
- } = options;
73
+ // ─── Main Generator ───────────────────────────────────────────────────────────
74
74
 
75
+ export async function generateJsProject(options) {
76
+ const { projectDir, projectName, frontendSrcDir, dbType, addAuth, addSeeder, extraFeatures = [] } = options;
75
77
  const port = 8000;
78
+ const totalTimer = timer();
76
79
 
77
80
  try {
78
- // --- Step 1: Analyze Frontend ---
79
- console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
80
- let endpoints = await analyzeFrontend(frontendSrcDir);
81
-
82
- if (Array.isArray(endpoints) && endpoints.length > 0) {
83
- console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
84
- endpoints = sanitizeEndpoints(endpoints);
85
- } else {
86
- endpoints = [];
87
- console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
88
- }
81
+ // ── Step 1: Analyze + preload templates in parallel ────────────────────────
82
+ const step1 = timer();
83
+ console.log(chalk.blue(' -> [1] Analyzing frontend & preloading templates in parallel...'));
84
+
85
+ const templatePrefetch = [
86
+ 'js-express/partials/package.json.ejs',
87
+ 'js-express/partials/db.js.ejs',
88
+ 'js-express/partials/routes.js.ejs',
89
+ 'js-express/partials/controller.js.ejs',
90
+ 'js-express/partials/service.js.ejs',
91
+ ...(dbType === 'mongoose' ? ['js-express/partials/model.js.ejs'] : []),
92
+ ...(dbType === 'prisma' ? ['js-express/partials/prisma.schema.ejs'] : []),
93
+ ...(addAuth ? ['js-express/partials/auth.controller.js.ejs', 'js-express/partials/auth.middleware.js.ejs', 'js-express/partials/auth.routes.js.ejs'] : []),
94
+ ...(addSeeder ? ['js-express/partials/seeder.js.ejs'] : []),
95
+ ...(extraFeatures.includes('docker') ? ['js-express/partials/Dockerfile.ejs', 'js-express/partials/docker-compose.yml.ejs'] : []),
96
+ ...(extraFeatures.includes('swagger') ? ['js-express/partials/swagger.js.ejs'] : []),
97
+ ...(extraFeatures.includes('testing') ? ['js-express/partials/test.js.ejs'] : []),
98
+ ];
99
+
100
+ const [endpointsRaw] = await Promise.all([
101
+ analyzeFrontend(frontendSrcDir),
102
+ preloadTemplates(templatePrefetch).catch(() => {}),
103
+ ]);
104
+
105
+ let endpoints = Array.isArray(endpointsRaw) && endpointsRaw.length > 0
106
+ ? sanitizeEndpoints(endpointsRaw)
107
+ : [];
108
+
109
+ console.log(
110
+ endpoints.length > 0
111
+ ? chalk.green(` -> Found ${endpoints.length} endpoints. ${chalk.gray(step1())}`)
112
+ : chalk.yellow(` -> No endpoints found. Basic project will be created. ${chalk.gray(step1())}`)
113
+ );
89
114
 
90
- // --- Step 2: Identify Models ---
115
+ // ── Step 2: Build model map ────────────────────────────────────────────────
91
116
  const modelsToGenerate = new Map();
92
117
  endpoints.forEach((ep) => {
93
118
  if (!ep) return;
94
119
  const ctrl = safePascalName(ep.controllerName);
95
120
  if (ctrl === 'Default' || ctrl === 'Auth') return;
96
121
  if (!modelsToGenerate.has(ctrl)) {
97
- let fields = [];
98
- if (ep.schemaFields) {
99
- fields = Object.entries(ep.schemaFields).map(([key, type]) => ({
100
- name: key,
101
- type,
102
- isUnique: key === 'email',
103
- }));
104
- }
122
+ const fields = ep.schemaFields
123
+ ? Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type, isUnique: key === 'email' }))
124
+ : [];
105
125
  modelsToGenerate.set(ctrl, { name: ctrl, fields });
106
126
  }
107
127
  });
108
128
 
109
129
  if (addAuth && !modelsToGenerate.has('User')) {
110
- modelsToGenerate.set('User', {
111
- name: 'User',
112
- fields: [
113
- { name: 'name', type: 'String' },
114
- { name: 'email', type: 'String', isUnique: true },
115
- { name: 'password', type: 'String' },
116
- ],
117
- });
130
+ modelsToGenerate.set('User', { name: 'User', fields: [
131
+ { name: 'name', type: 'String' },
132
+ { name: 'email', type: 'String', isUnique: true },
133
+ { name: 'password', type: 'String' },
134
+ ]});
118
135
  }
119
136
 
120
- // --- Step 3: Base Scaffolding ---
121
- console.log(chalk.blue(' -> Scaffolding JavaScript (ESM) Express project...'));
122
- const srcDir = path.join(projectDir, 'src');
123
- await fs.ensureDir(srcDir);
124
- await fs.copy(getTemplatePath('js-express/base/server.js'), path.join(srcDir, 'server.js'));
137
+ // ── Step 3: Scaffold dirs + copy base server.js in parallel ───────────────
138
+ const step3 = timer();
139
+ console.log(chalk.blue(' -> [3] Scaffolding directory structure in parallel...'));
125
140
 
126
- // --- Step 4: package.json ---
141
+ const srcDir = path.join(projectDir, 'src');
142
+ const dirsToCreate = [
143
+ srcDir,
144
+ path.join(srcDir, 'routes'),
145
+ ...(modelsToGenerate.size > 0 ? [
146
+ path.join(srcDir, 'controllers'),
147
+ path.join(srcDir, 'services'),
148
+ ...(dbType === 'mongoose' ? [path.join(srcDir, 'models')] : []),
149
+ ...(dbType === 'prisma' ? [path.join(projectDir, 'prisma')] : []),
150
+ ] : []),
151
+ ...(addAuth ? [path.join(srcDir, 'middleware')] : []),
152
+ ...(addSeeder ? [path.join(projectDir, 'scripts')] : []),
153
+ ...(extraFeatures.includes('swagger') ? [path.join(srcDir, 'utils')] : []),
154
+ ...(extraFeatures.includes('testing') ? [path.join(projectDir, 'tests')] : []),
155
+ ];
156
+
157
+ await Promise.all([
158
+ ...dirsToCreate.map(d => fs.ensureDir(d)),
159
+ fs.copy(getTemplatePath('js-express/base/server.js'), path.join(srcDir, 'server.js')),
160
+ ]);
161
+ console.log(chalk.gray(` Scaffolding done. ${step3()}`));
162
+
163
+ // ── Step 4: package.json (build once) ─────────────────────────────────────
127
164
  const pkgTpl = await fs.readFile(getTemplatePath('js-express/partials/package.json.ejs'), 'utf-8');
128
165
  const packageJsonContent = JSON.parse(ejs.render(pkgTpl, { projectName }));
129
166
 
@@ -132,24 +169,20 @@ export async function generateJsProject(options) {
132
169
  packageJsonContent.dependencies['@prisma/client'] = '^5.6.0';
133
170
  packageJsonContent.devDependencies.prisma = '^5.6.0';
134
171
  }
135
-
136
172
  if (addAuth) {
137
173
  packageJsonContent.dependencies.jsonwebtoken = '^9.0.2';
138
174
  packageJsonContent.dependencies.bcryptjs = '^2.4.3';
139
175
  }
140
-
141
176
  if (addSeeder) {
142
177
  packageJsonContent.devDependencies['@faker-js/faker'] = '^8.3.1';
143
178
  packageJsonContent.scripts.seed = 'node scripts/seeder.js';
144
179
  packageJsonContent.scripts.destroy = 'node scripts/seeder.js -d';
145
180
  }
146
-
147
181
  if (extraFeatures.includes('testing')) {
148
182
  packageJsonContent.devDependencies.vitest = '^1.1.0';
149
183
  packageJsonContent.devDependencies.supertest = '^6.3.3';
150
184
  packageJsonContent.scripts.test = 'vitest run';
151
185
  }
152
-
153
186
  if (extraFeatures.includes('swagger')) {
154
187
  packageJsonContent.dependencies['swagger-ui-express'] = '^5.0.0';
155
188
  packageJsonContent.dependencies['swagger-jsdoc'] = '^6.2.8';
@@ -157,181 +190,140 @@ export async function generateJsProject(options) {
157
190
 
158
191
  await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
159
192
 
160
- // --- Step 5: Database connection file ---
161
- await renderAndWrite(
162
- getTemplatePath('js-express/partials/db.js.ejs'),
163
- path.join(srcDir, 'db.js'),
164
- { dbType, projectName }
165
- );
193
+ // ── Step 5: All file generation tasks in PARALLEL batches ─────────────────
194
+ const step5 = timer();
195
+ console.log(chalk.blue(' -> [5] Generating all project files in parallel...'));
166
196
 
167
- // --- Step 6: Models + Controllers + Services ---
168
- if (modelsToGenerate.size > 0) {
169
- const controllersDir = path.join(srcDir, 'controllers');
170
- const servicesDir = path.join(srcDir, 'services');
171
- const modelsDir = path.join(srcDir, 'models');
172
-
173
- await fs.ensureDir(controllersDir);
174
- await fs.ensureDir(servicesDir);
175
-
176
- if (dbType === 'mongoose') {
177
- console.log(chalk.blue(' -> Generating Mongoose models...'));
178
- await fs.ensureDir(modelsDir);
179
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
180
- await renderAndWrite(
181
- getTemplatePath('js-express/partials/model.js.ejs'),
182
- path.join(modelsDir, `${modelName}.model.js`),
183
- { modelName, fields: modelData.fields || [] }
184
- );
185
- }
186
- } else if (dbType === 'prisma') {
187
- console.log(chalk.blue(' -> Generating Prisma schema...'));
188
- await fs.ensureDir(path.join(projectDir, 'prisma'));
189
- await renderAndWrite(
190
- getTemplatePath('js-express/partials/prisma.schema.ejs'),
191
- path.join(projectDir, 'prisma', 'schema.prisma'),
192
- { models: Array.from(modelsToGenerate.values()) }
193
- );
194
- }
197
+ const renderTasks = [];
195
198
 
196
- console.log(chalk.blue(' -> Generating Controllers & Services...'));
197
- for (const [modelName] of modelsToGenerate.entries()) {
198
- if (modelName === 'Auth') continue;
199
- await renderAndWrite(
200
- getTemplatePath('js-express/partials/controller.js.ejs'),
201
- path.join(controllersDir, `${modelName}.controller.js`),
202
- { modelName, dbType }
203
- );
204
- await renderAndWrite(
205
- getTemplatePath('js-express/partials/service.js.ejs'),
206
- path.join(servicesDir, `${modelName}.service.js`),
207
- { modelName, dbType }
208
- );
199
+ // DB connection file
200
+ renderTasks.push({
201
+ templatePath: getTemplatePath('js-express/partials/db.js.ejs'),
202
+ outPath: path.join(srcDir, 'db.js'),
203
+ data: { dbType, projectName },
204
+ });
205
+
206
+ // Models
207
+ if (dbType === 'mongoose') {
208
+ for (const [modelName, modelData] of modelsToGenerate.entries()) {
209
+ renderTasks.push({
210
+ templatePath: getTemplatePath('js-express/partials/model.js.ejs'),
211
+ outPath: path.join(srcDir, 'models', `${modelName}.model.js`),
212
+ data: { modelName, fields: modelData.fields || [] },
213
+ });
209
214
  }
215
+ } else if (dbType === 'prisma') {
216
+ renderTasks.push({
217
+ templatePath: getTemplatePath('js-express/partials/prisma.schema.ejs'),
218
+ outPath: path.join(projectDir, 'prisma', 'schema.prisma'),
219
+ data: { models: Array.from(modelsToGenerate.values()) },
220
+ });
210
221
  }
211
222
 
212
- // --- Step 7: Routes ---
213
- const nonAuthEndpoints = endpoints.filter((ep) => safePascalName(ep.controllerName) !== 'Auth');
214
- await fs.ensureDir(path.join(srcDir, 'routes'));
223
+ // Controllers + Services
224
+ for (const [modelName] of modelsToGenerate.entries()) {
225
+ if (modelName === 'Auth') continue;
226
+ renderTasks.push(
227
+ { templatePath: getTemplatePath('js-express/partials/controller.js.ejs'), outPath: path.join(srcDir, 'controllers', `${modelName}.controller.js`), data: { modelName, dbType } },
228
+ { templatePath: getTemplatePath('js-express/partials/service.js.ejs'), outPath: path.join(srcDir, 'services', `${modelName}.service.js`), data: { modelName, dbType } }
229
+ );
230
+ }
215
231
 
216
- await renderAndWrite(
217
- getTemplatePath('js-express/partials/routes.js.ejs'),
218
- path.join(srcDir, 'routes', 'index.js'),
219
- { endpoints: nonAuthEndpoints, addAuth, dbType }
220
- );
232
+ // Routes
233
+ const nonAuthEndpoints = endpoints.filter((ep) => safePascalName(ep.controllerName) !== 'Auth');
234
+ renderTasks.push({
235
+ templatePath: getTemplatePath('js-express/partials/routes.js.ejs'),
236
+ outPath: path.join(srcDir, 'routes', 'index.js'),
237
+ data: { endpoints: nonAuthEndpoints, addAuth, dbType },
238
+ });
221
239
 
222
- // --- Step 8: Auth ---
240
+ // Auth
223
241
  if (addAuth) {
224
- console.log(chalk.blue(' -> Generating authentication boilerplate...'));
225
- await fs.ensureDir(path.join(srcDir, 'controllers'));
226
- await fs.ensureDir(path.join(srcDir, 'middleware'));
227
- await fs.ensureDir(path.join(srcDir, 'routes'));
228
-
229
- await renderAndWrite(
230
- getTemplatePath('js-express/partials/auth.controller.js.ejs'),
231
- path.join(srcDir, 'controllers', 'Auth.controller.js'),
232
- { dbType, projectName }
233
- );
234
- await renderAndWrite(
235
- getTemplatePath('js-express/partials/auth.middleware.js.ejs'),
236
- path.join(srcDir, 'middleware', 'auth.js'),
237
- { projectName }
238
- );
239
- await renderAndWrite(
240
- getTemplatePath('js-express/partials/auth.routes.js.ejs'),
241
- path.join(srcDir, 'routes', 'auth.js'),
242
- { projectName }
242
+ renderTasks.push(
243
+ { templatePath: getTemplatePath('js-express/partials/auth.controller.js.ejs'), outPath: path.join(srcDir, 'controllers', 'Auth.controller.js'), data: { dbType, projectName } },
244
+ { templatePath: getTemplatePath('js-express/partials/auth.middleware.js.ejs'), outPath: path.join(srcDir, 'middleware', 'auth.js'), data: { projectName } },
245
+ { templatePath: getTemplatePath('js-express/partials/auth.routes.js.ejs'), outPath: path.join(srcDir, 'routes', 'auth.js'), data: { projectName } }
243
246
  );
244
247
  }
245
248
 
246
- // --- Step 9: Seeder ---
249
+ // Seeder
247
250
  if (addSeeder) {
248
- console.log(chalk.blue(' -> Generating database seeder script...'));
249
- await fs.ensureDir(path.join(projectDir, 'scripts'));
250
- await renderAndWrite(
251
- getTemplatePath('js-express/partials/seeder.js.ejs'),
252
- path.join(projectDir, 'scripts', 'seeder.js'),
253
- { projectName, dbType, models: Array.from(modelsToGenerate.values()) }
254
- );
251
+ renderTasks.push({
252
+ templatePath: getTemplatePath('js-express/partials/seeder.js.ejs'),
253
+ outPath: path.join(projectDir, 'scripts', 'seeder.js'),
254
+ data: { projectName, dbType, models: Array.from(modelsToGenerate.values()) },
255
+ });
255
256
  }
256
257
 
257
- // --- Step 10: Docker ---
258
+ // Docker
258
259
  if (extraFeatures.includes('docker')) {
259
- console.log(chalk.blue(' -> Generating Docker files...'));
260
- await renderAndWrite(
261
- getTemplatePath('js-express/partials/Dockerfile.ejs'),
262
- path.join(projectDir, 'Dockerfile'),
263
- { port }
264
- );
265
- await renderAndWrite(
266
- getTemplatePath('js-express/partials/docker-compose.yml.ejs'),
267
- path.join(projectDir, 'docker-compose.yml'),
268
- { projectName, dbType, port }
260
+ renderTasks.push(
261
+ { templatePath: getTemplatePath('js-express/partials/Dockerfile.ejs'), outPath: path.join(projectDir, 'Dockerfile'), data: { port } },
262
+ { templatePath: getTemplatePath('js-express/partials/docker-compose.yml.ejs'), outPath: path.join(projectDir, 'docker-compose.yml'), data: { projectName, dbType, port } }
269
263
  );
270
264
  }
271
265
 
272
- // --- Step 11: Swagger ---
266
+ // Swagger
273
267
  if (extraFeatures.includes('swagger')) {
274
- console.log(chalk.blue(' -> Generating API documentation setup...'));
275
- await fs.ensureDir(path.join(srcDir, 'utils'));
276
- await renderAndWrite(
277
- getTemplatePath('js-express/partials/swagger.js.ejs'),
278
- path.join(srcDir, 'utils', 'swagger.js'),
279
- { projectName, port, addAuth }
280
- );
268
+ renderTasks.push({
269
+ templatePath: getTemplatePath('js-express/partials/swagger.js.ejs'),
270
+ outPath: path.join(srcDir, 'utils', 'swagger.js'),
271
+ data: { projectName, port, addAuth },
272
+ });
281
273
  }
282
274
 
283
- // --- Step 12: Testing ---
275
+ // Testing
284
276
  if (extraFeatures.includes('testing')) {
285
- console.log(chalk.blue(' -> Generating test boilerplate...'));
286
- await fs.ensureDir(path.join(projectDir, 'tests'));
287
- await renderAndWrite(
288
- getTemplatePath('js-express/partials/test.js.ejs'),
289
- path.join(projectDir, 'tests', 'api.test.js'),
290
- { addAuth, endpoints }
291
- );
277
+ renderTasks.push({
278
+ templatePath: getTemplatePath('js-express/partials/test.js.ejs'),
279
+ outPath: path.join(projectDir, 'tests', 'api.test.js'),
280
+ data: { addAuth, endpoints },
281
+ });
292
282
  }
293
283
 
294
- // --- Step 13: Inject into server.js ---
284
+ await renderAndWriteAll(renderTasks, 12);
285
+ console.log(chalk.gray(` All files generated. ${step5()}`));
286
+
287
+ // ── Step 6: Patch server.js ────────────────────────────────────────────────
295
288
  let serverContent = await fs.readFile(path.join(srcDir, 'server.js'), 'utf-8');
296
289
 
297
- let dbImport = '';
298
- if (dbType === 'mongoose') {
299
- dbImport = "\nimport { connectDB } from './db.js';\nconnectDB();\n";
300
- } else if (dbType === 'prisma') {
301
- dbImport = "\nimport { prisma } from './db.js';\n";
302
- }
290
+ const dbImport =
291
+ dbType === 'mongoose' ? "\nimport { connectDB } from './db.js';\nconnectDB();\n"
292
+ : dbType === 'prisma' ? "\nimport { prisma } from './db.js';\n"
293
+ : '';
303
294
 
304
- let swaggerInject = '';
305
- if (extraFeatures.includes('swagger')) {
306
- swaggerInject = "\nimport { setupSwagger } from './utils/swagger.js';\nsetupSwagger(app);\n";
307
- }
295
+ const swaggerInject = extraFeatures.includes('swagger')
296
+ ? "\nimport { setupSwagger } from './utils/swagger.js';\nsetupSwagger(app);\n"
297
+ : '';
308
298
 
309
- let authInject = '';
310
- if (addAuth) {
311
- authInject = "import authRoutes from './routes/auth.js';\napp.use('/api/auth', authRoutes);\n\n";
312
- }
299
+ const authInject = addAuth
300
+ ? "import authRoutes from './routes/auth.js';\napp.use('/api/auth', authRoutes);\n\n"
301
+ : '';
313
302
 
314
303
  serverContent = serverContent
315
304
  .replace('dotenv.config();', `dotenv.config();${dbImport}`)
316
- .replace(
317
- '// INJECT:ROUTES',
318
- `${authInject}import apiRoutes from './routes/index.js';\napp.use('/api', apiRoutes);`
319
- );
320
-
321
- serverContent = serverContent.replace(/(const server = app\.listen\()/, `${swaggerInject}\n$1`);
305
+ .replace('// INJECT:ROUTES', `${authInject}import apiRoutes from './routes/index.js';\napp.use('/api', apiRoutes);`)
306
+ .replace(/(const server = app\.listen\()/, `${swaggerInject}\n$1`);
322
307
 
323
308
  await fs.writeFile(path.join(srcDir, 'server.js'), serverContent);
324
309
 
325
- // --- Step 14: Install deps ---
326
- console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
327
- await execa('npm', ['install'], { cwd: projectDir });
310
+ // ── Step 7: Install deps (with retry) ─────────────────────────────────────
311
+ console.log(chalk.magenta(' -> [7] Installing dependencies...'));
312
+ const step7 = timer();
313
+
314
+ await withRetry(() => execa('npm', ['install'], { cwd: projectDir }), {
315
+ attempts: 2, baseDelay: 1000, label: 'npm install',
316
+ });
317
+
318
+ console.log(chalk.gray(` npm install done. ${step7()}`));
328
319
 
329
320
  if (dbType === 'prisma') {
330
- console.log(chalk.blue(' -> Running `prisma generate`...'));
331
- await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
321
+ await withRetry(() => execa('npx', ['prisma', 'generate'], { cwd: projectDir }), {
322
+ attempts: 2, baseDelay: 500, label: 'prisma generate',
323
+ });
332
324
  }
333
325
 
334
- // --- Step 15: .env.example ---
326
+ // ── Step 8: .env.example ───────────────────────────────────────────────────
335
327
  let envContent = `PORT=${port}\n`;
336
328
  if (dbType === 'mongoose') envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
337
329
  if (dbType === 'prisma') envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
@@ -339,7 +331,7 @@ export async function generateJsProject(options) {
339
331
 
340
332
  await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
341
333
 
342
- console.log(chalk.green(' -> JavaScript Express backend generation complete.'));
334
+ console.log(chalk.green(` -> JavaScript Express backend generation complete. Total: ${chalk.bold(totalTimer())}`));
343
335
  } catch (error) {
344
336
  throw error;
345
337
  }