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.
@@ -4,12 +4,32 @@ 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 safePascalName(name) {
10
- const cleaned = String(name || 'Default')
11
- .split('?')[0]
12
- .replace(/[^a-zA-Z0-9]/g, '');
32
+ const cleaned = String(name || 'Default').split('?')[0].replace(/[^a-zA-Z0-9]/g, '');
13
33
  if (!cleaned) return 'Default';
14
34
  return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
15
35
  }
@@ -25,69 +45,99 @@ function sanitizeEndpoints(endpoints) {
25
45
  });
26
46
  }
27
47
 
48
+ // ─── Main Generator ───────────────────────────────────────────────────────────
49
+
28
50
  export async function generateNestProject(options) {
29
- const {
30
- projectDir,
31
- projectName,
32
- frontendSrcDir,
33
- dbType,
34
- addAuth,
35
- addSeeder,
36
- extraFeatures = [],
37
- } = options;
51
+ const { projectDir, projectName, frontendSrcDir, dbType, addAuth, addSeeder, extraFeatures = [] } = options;
52
+ const totalTimer = timer();
38
53
 
39
54
  try {
40
- console.log(chalk.blue(' -> Analyzing frontend for NestJS backend...'));
41
- let endpoints = await analyzeFrontend(frontendSrcDir);
42
-
43
- if (Array.isArray(endpoints) && endpoints.length > 0) {
44
- console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
45
- endpoints = sanitizeEndpoints(endpoints);
46
- } else {
47
- endpoints = [];
48
- console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
49
- }
55
+ // ── Step 1: Analyze + preload templates in parallel ────────────────────────
56
+ const step1 = timer();
57
+ console.log(chalk.blue(' -> [1] Analyzing frontend & preloading templates in parallel...'));
58
+
59
+ const templatePrefetch = [
60
+ 'nestjs/partials/package.json.ejs',
61
+ 'nestjs/partials/module.ts.ejs',
62
+ 'nestjs/partials/controller.ts.ejs',
63
+ 'nestjs/partials/service.ts.ejs',
64
+ 'nestjs/partials/create-dto.ts.ejs',
65
+ 'nestjs/partials/update-dto.ts.ejs',
66
+ 'nestjs/partials/prisma.service.ts.ejs',
67
+ ...(dbType === 'mongoose' ? ['nestjs/partials/schema.ts.ejs'] : []),
68
+ ...(addAuth ? ['nestjs/partials/auth.module.ts.ejs', 'nestjs/partials/auth.controller.ts.ejs', 'nestjs/partials/auth.service.ts.ejs', 'nestjs/partials/jwt-guard.ts.ejs'] : []),
69
+ ...(addSeeder ? ['nestjs/partials/seeder.ts.ejs'] : []),
70
+ ];
71
+
72
+ const [endpointsRaw] = await Promise.all([
73
+ analyzeFrontend(frontendSrcDir),
74
+ preloadTemplates(templatePrefetch).catch(() => {}),
75
+ ]);
76
+
77
+ let endpoints = Array.isArray(endpointsRaw) && endpointsRaw.length > 0
78
+ ? sanitizeEndpoints(endpointsRaw)
79
+ : [];
80
+
81
+ console.log(
82
+ endpoints.length > 0
83
+ ? chalk.green(` -> Found ${endpoints.length} endpoints. ${chalk.gray(step1())}`)
84
+ : chalk.yellow(` -> No endpoints found. Basic project will be created. ${chalk.gray(step1())}`)
85
+ );
50
86
 
51
- // --- Identify Models ---
87
+ // ── Step 2: Build model map ────────────────────────────────────────────────
52
88
  const modelsToGenerate = new Map();
53
89
  endpoints.forEach((ep) => {
54
90
  if (!ep) return;
55
91
  const ctrl = safePascalName(ep.controllerName);
56
92
  if (ctrl === 'Default' || ctrl === 'Auth') return;
57
93
  if (!modelsToGenerate.has(ctrl)) {
58
- let fields = [];
59
- if (ep.schemaFields) {
60
- fields = Object.entries(ep.schemaFields).map(([key, type]) => ({
61
- name: key,
62
- type,
63
- isUnique: key === 'email',
64
- }));
65
- }
94
+ const fields = ep.schemaFields
95
+ ? Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type, isUnique: key === 'email' }))
96
+ : [];
66
97
  modelsToGenerate.set(ctrl, { name: ctrl, fields });
67
98
  }
68
99
  });
69
100
 
70
101
  if (addAuth && !modelsToGenerate.has('User')) {
71
- modelsToGenerate.set('User', {
72
- name: 'User',
73
- fields: [
74
- { name: 'name', type: 'String' },
75
- { name: 'email', type: 'String', isUnique: true },
76
- { name: 'password', type: 'String' },
77
- ],
78
- });
102
+ modelsToGenerate.set('User', { name: 'User', fields: [
103
+ { name: 'name', type: 'String' },
104
+ { name: 'email', type: 'String', isUnique: true },
105
+ { name: 'password', type: 'String' },
106
+ ]});
79
107
  }
80
108
 
81
- // --- Base Scaffolding ---
82
- console.log(chalk.blue(' -> Scaffolding NestJS project...'));
83
- const srcDir = path.join(projectDir, 'src');
84
- await fs.ensureDir(srcDir);
109
+ // ── Step 3: Scaffold dirs + copy base files in parallel ───────────────────
110
+ const step3 = timer();
111
+ console.log(chalk.blue(' -> [3] Scaffolding NestJS project structure in parallel...'));
85
112
 
86
- await fs.copy(getTemplatePath('nestjs/base/main.ts'), path.join(srcDir, 'main.ts'));
87
- await fs.copy(getTemplatePath('nestjs/base/app.module.ts'), path.join(srcDir, 'app.module.ts'));
88
- await fs.copy(getTemplatePath('nestjs/base/tsconfig.json'), path.join(projectDir, 'tsconfig.json'));
113
+ const srcDir = path.join(projectDir, 'src');
89
114
 
90
- // --- package.json ---
115
+ // Pre-compute all module dirs
116
+ const moduleDirs = Array.from(modelsToGenerate.keys())
117
+ .filter(n => n !== 'Auth')
118
+ .flatMap(name => [
119
+ path.join(srcDir, name.toLowerCase()),
120
+ path.join(srcDir, name.toLowerCase(), 'dto'),
121
+ ]);
122
+
123
+ const extraDirs = [
124
+ ...(dbType === 'prisma' ? [path.join(srcDir, 'prisma'), path.join(projectDir, 'prisma')] : []),
125
+ ...(addAuth ? [path.join(srcDir, 'auth')] : []),
126
+ ...(addSeeder ? [path.join(projectDir, 'scripts')] : []),
127
+ ];
128
+
129
+ await Promise.all([
130
+ fs.ensureDir(srcDir),
131
+ ...moduleDirs.map(d => fs.ensureDir(d)),
132
+ ...extraDirs.map(d => fs.ensureDir(d)),
133
+ fs.copy(getTemplatePath('nestjs/base/main.ts'), path.join(srcDir, 'main.ts')),
134
+ fs.copy(getTemplatePath('nestjs/base/app.module.ts'), path.join(srcDir, 'app.module.ts')),
135
+ fs.copy(getTemplatePath('nestjs/base/tsconfig.json'), path.join(projectDir, 'tsconfig.json')),
136
+ ]);
137
+
138
+ console.log(chalk.gray(` Scaffolding done. ${step3()}`));
139
+
140
+ // ── Step 4: package.json + nest-cli.json ──────────────────────────────────
91
141
  const pkgTpl = await fs.readFile(getTemplatePath('nestjs/partials/package.json.ejs'), 'utf-8');
92
142
  const packageJsonContent = JSON.parse(ejs.render(pkgTpl, { projectName }));
93
143
 
@@ -109,64 +159,43 @@ export async function generateNestProject(options) {
109
159
  packageJsonContent.scripts.seed = 'ts-node scripts/seeder.ts';
110
160
  }
111
161
 
112
- await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
113
-
114
- // --- nest-cli.json ---
115
- await fs.writeJson(path.join(projectDir, 'nest-cli.json'), {
162
+ const nestCliJson = {
116
163
  "$schema": "https://json.schemastore.org/nest-cli",
117
164
  "collection": "@nestjs/schematics",
118
165
  "sourceRoot": "src",
119
- "compilerOptions": { "deleteOutDir": true }
120
- }, { spaces: 2 });
166
+ "compilerOptions": { "deleteOutDir": true },
167
+ };
121
168
 
122
- // --- Generate Modules ---
169
+ await Promise.all([
170
+ fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 }),
171
+ fs.writeJson(path.join(projectDir, 'nest-cli.json'), nestCliJson, { spaces: 2 }),
172
+ ]);
173
+
174
+ // ── Step 5: Generate all NestJS modules in PARALLEL ───────────────────────
175
+ const step5 = timer();
123
176
  const moduleImports = [];
124
177
  const moduleNames = [];
178
+ const renderTasks = [];
125
179
 
126
180
  if (modelsToGenerate.size > 0) {
127
- console.log(chalk.blue(' -> Generating NestJS modules (Controller, Service, DTO)...'));
181
+ console.log(chalk.blue(` -> [5] Generating ${modelsToGenerate.size} NestJS module(s) in parallel...`));
128
182
 
129
183
  for (const [modelName, modelData] of modelsToGenerate.entries()) {
130
184
  if (modelName === 'Auth') continue;
131
-
132
185
  const moduleDir = path.join(srcDir, modelName.toLowerCase());
133
186
  const dtoDir = path.join(moduleDir, 'dto');
134
- await fs.ensureDir(dtoDir);
135
-
136
187
  const tplData = { modelName, dbType, fields: modelData.fields || [] };
137
188
 
138
- await renderAndWrite(
139
- getTemplatePath('nestjs/partials/module.ts.ejs'),
140
- path.join(moduleDir, `${modelName.toLowerCase()}.module.ts`),
141
- tplData
142
- );
143
- await renderAndWrite(
144
- getTemplatePath('nestjs/partials/controller.ts.ejs'),
145
- path.join(moduleDir, `${modelName.toLowerCase()}.controller.ts`),
146
- tplData
147
- );
148
- await renderAndWrite(
149
- getTemplatePath('nestjs/partials/service.ts.ejs'),
150
- path.join(moduleDir, `${modelName.toLowerCase()}.service.ts`),
151
- tplData
152
- );
153
- await renderAndWrite(
154
- getTemplatePath('nestjs/partials/create-dto.ts.ejs'),
155
- path.join(dtoDir, `create-${modelName.toLowerCase()}.dto.ts`),
156
- tplData
157
- );
158
- await renderAndWrite(
159
- getTemplatePath('nestjs/partials/update-dto.ts.ejs'),
160
- path.join(dtoDir, `update-${modelName.toLowerCase()}.dto.ts`),
161
- tplData
189
+ renderTasks.push(
190
+ { templatePath: getTemplatePath('nestjs/partials/module.ts.ejs'), outPath: path.join(moduleDir, `${modelName.toLowerCase()}.module.ts`), data: tplData },
191
+ { templatePath: getTemplatePath('nestjs/partials/controller.ts.ejs'), outPath: path.join(moduleDir, `${modelName.toLowerCase()}.controller.ts`), data: tplData },
192
+ { templatePath: getTemplatePath('nestjs/partials/service.ts.ejs'), outPath: path.join(moduleDir, `${modelName.toLowerCase()}.service.ts`), data: tplData },
193
+ { templatePath: getTemplatePath('nestjs/partials/create-dto.ts.ejs'), outPath: path.join(dtoDir, `create-${modelName.toLowerCase()}.dto.ts`), data: tplData },
194
+ { templatePath: getTemplatePath('nestjs/partials/update-dto.ts.ejs'), outPath: path.join(dtoDir, `update-${modelName.toLowerCase()}.dto.ts`), data: tplData }
162
195
  );
163
196
 
164
197
  if (dbType === 'mongoose') {
165
- await renderAndWrite(
166
- getTemplatePath('nestjs/partials/schema.ts.ejs'),
167
- path.join(moduleDir, `${modelName.toLowerCase()}.schema.ts`),
168
- tplData
169
- );
198
+ renderTasks.push({ templatePath: getTemplatePath('nestjs/partials/schema.ts.ejs'), outPath: path.join(moduleDir, `${modelName.toLowerCase()}.schema.ts`), data: tplData });
170
199
  }
171
200
 
172
201
  moduleImports.push(`import { ${modelName}Module } from './${modelName.toLowerCase()}/${modelName.toLowerCase()}.module';`);
@@ -174,96 +203,80 @@ export async function generateNestProject(options) {
174
203
  }
175
204
  }
176
205
 
177
- // --- Prisma module ---
206
+ // Prisma module tasks
178
207
  if (dbType === 'prisma') {
179
- const prismaDir = path.join(srcDir, 'prisma');
180
- await fs.ensureDir(prismaDir);
181
- await renderAndWrite(
182
- getTemplatePath('nestjs/partials/prisma.service.ts.ejs'),
183
- path.join(prismaDir, 'prisma.service.ts'),
184
- {}
208
+ const prismaModuleContent = `import { Global, Module } from '@nestjs/common';\nimport { PrismaService } from './prisma.service';\n\n@Global()\n@Module({\n providers: [PrismaService],\n exports: [PrismaService],\n})\nexport class PrismaModule {}\n`;
209
+ renderTasks.push(
210
+ { templatePath: getTemplatePath('nestjs/partials/prisma.service.ts.ejs'), outPath: path.join(srcDir, 'prisma', 'prisma.service.ts'), data: {} },
211
+ { templatePath: getTemplatePath('js-express/partials/prisma.schema.ejs'), outPath: path.join(projectDir, 'prisma', 'schema.prisma'), data: { models: Array.from(modelsToGenerate.values()) } }
185
212
  );
186
- const prismaModuleContent = `import { Global, Module } from '@nestjs/common';
187
- import { PrismaService } from './prisma.service';
188
-
189
- @Global()
190
- @Module({
191
- providers: [PrismaService],
192
- exports: [PrismaService],
193
- })
194
- export class PrismaModule {}
195
- `;
196
- await fs.writeFile(path.join(prismaDir, 'prisma.module.ts'), prismaModuleContent);
213
+ // Write the prisma module content directly (not via template)
214
+ renderTasks.push({ _raw: true, outPath: path.join(srcDir, 'prisma', 'prisma.module.ts'), content: prismaModuleContent });
197
215
  moduleImports.unshift("import { PrismaModule } from './prisma/prisma.module';");
198
216
  moduleNames.unshift('PrismaModule');
199
-
200
- await fs.ensureDir(path.join(projectDir, 'prisma'));
201
- await renderAndWrite(
202
- getTemplatePath('js-express/partials/prisma.schema.ejs'),
203
- path.join(projectDir, 'prisma', 'schema.prisma'),
204
- { models: Array.from(modelsToGenerate.values()) }
205
- );
206
217
  }
207
218
 
208
- // --- Mongoose config in AppModule ---
209
219
  if (dbType === 'mongoose') {
210
220
  moduleImports.unshift("import { MongooseModule } from '@nestjs/mongoose';");
211
221
  moduleNames.unshift(`MongooseModule.forRoot(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}')`);
212
222
  }
213
223
 
214
- // --- Auth Module ---
224
+ // Auth tasks
215
225
  if (addAuth) {
216
- console.log(chalk.blue(' -> Generating Auth module...'));
217
- const authDir = path.join(srcDir, 'auth');
218
- await fs.ensureDir(authDir);
219
-
220
- await renderAndWrite(
221
- getTemplatePath('nestjs/partials/auth.module.ts.ejs'),
222
- path.join(authDir, 'auth.module.ts'),
223
- { dbType }
224
- );
225
- await renderAndWrite(
226
- getTemplatePath('nestjs/partials/auth.controller.ts.ejs'),
227
- path.join(authDir, 'auth.controller.ts'),
228
- { dbType }
229
- );
230
- await renderAndWrite(
231
- getTemplatePath('nestjs/partials/auth.service.ts.ejs'),
232
- path.join(authDir, 'auth.service.ts'),
233
- { dbType }
226
+ renderTasks.push(
227
+ { templatePath: getTemplatePath('nestjs/partials/auth.module.ts.ejs'), outPath: path.join(srcDir, 'auth', 'auth.module.ts'), data: { dbType } },
228
+ { templatePath: getTemplatePath('nestjs/partials/auth.controller.ts.ejs'), outPath: path.join(srcDir, 'auth', 'auth.controller.ts'), data: { dbType } },
229
+ { templatePath: getTemplatePath('nestjs/partials/auth.service.ts.ejs'), outPath: path.join(srcDir, 'auth', 'auth.service.ts'), data: { dbType } },
230
+ { templatePath: getTemplatePath('nestjs/partials/jwt-guard.ts.ejs'), outPath: path.join(srcDir, 'auth', 'jwt-auth.guard.ts'), data: {} }
234
231
  );
235
- await renderAndWrite(
236
- getTemplatePath('nestjs/partials/jwt-guard.ts.ejs'),
237
- path.join(authDir, 'jwt-auth.guard.ts'),
238
- {}
239
- );
240
-
241
232
  moduleImports.push("import { AuthModule } from './auth/auth.module';");
242
233
  moduleNames.push('AuthModule');
243
234
  }
244
235
 
245
- // --- Inject into app.module.ts ---
236
+ // Seeder
237
+ if (addSeeder) {
238
+ renderTasks.push({
239
+ templatePath: getTemplatePath('nestjs/partials/seeder.ts.ejs'),
240
+ outPath: path.join(projectDir, 'scripts', 'seeder.ts'),
241
+ data: { projectName, models: Array.from(modelsToGenerate.values()) },
242
+ });
243
+ }
244
+
245
+ // Fire all render tasks in parallel
246
+ const rawTasks = renderTasks.filter(t => t._raw);
247
+ const normalTasks = renderTasks.filter(t => !t._raw);
248
+
249
+ await Promise.all([
250
+ renderAndWriteAll(normalTasks, 12),
251
+ ...rawTasks.map(t => fs.outputFile(t.outPath, t.content)),
252
+ ]);
253
+
254
+ console.log(chalk.gray(` Modules generated. ${step5()}`));
255
+
256
+ // ── Step 6: Inject into app.module.ts ─────────────────────────────────────
246
257
  let appModuleContent = await fs.readFile(path.join(srcDir, 'app.module.ts'), 'utf-8');
247
- appModuleContent = appModuleContent.replace(
248
- '// INJECT:IMPORTS',
249
- moduleImports.join('\n')
250
- );
251
- appModuleContent = appModuleContent.replace(
252
- '// INJECT:MODULES',
253
- moduleNames.join(',\n ')
254
- );
258
+ appModuleContent = appModuleContent
259
+ .replace('// INJECT:IMPORTS', moduleImports.join('\n'))
260
+ .replace('// INJECT:MODULES', moduleNames.join(',\n '));
255
261
  await fs.writeFile(path.join(srcDir, 'app.module.ts'), appModuleContent);
256
262
 
257
- // --- Install deps ---
258
- console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
259
- await execa('npm', ['install'], { cwd: projectDir });
263
+ // ── Step 7: Install deps (with retry) ─────────────────────────────────────
264
+ console.log(chalk.magenta(' -> [7] Installing dependencies...'));
265
+ const step7 = timer();
266
+
267
+ await withRetry(() => execa('npm', ['install'], { cwd: projectDir }), {
268
+ attempts: 2, baseDelay: 1000, label: 'npm install',
269
+ });
270
+
271
+ console.log(chalk.gray(` npm install done. ${step7()}`));
260
272
 
261
273
  if (dbType === 'prisma') {
262
- console.log(chalk.blue(' -> Running `prisma generate`...'));
263
- await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
274
+ await withRetry(() => execa('npx', ['prisma', 'generate'], { cwd: projectDir }), {
275
+ attempts: 2, baseDelay: 500, label: 'prisma generate',
276
+ });
264
277
  }
265
278
 
266
- // --- .env.example ---
279
+ // ── Step 8: .env.example ───────────────────────────────────────────────────
267
280
  let envContent = `PORT=8000\n`;
268
281
  if (dbType === 'mongoose') envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
269
282
  if (dbType === 'prisma') envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
@@ -271,7 +284,7 @@ export class PrismaModule {}
271
284
 
272
285
  await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
273
286
 
274
- console.log(chalk.green(' -> NestJS backend generation complete.'));
287
+ console.log(chalk.green(` -> NestJS backend generation complete. Total: ${chalk.bold(totalTimer())}`));
275
288
  } catch (error) {
276
289
  throw error;
277
290
  }