create-backlist 7.3.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/bin/index.js +483 -470
  2. package/bin/qa.js +103 -0
  3. package/package.json +7 -3
  4. package/src/env-resolver.js +70 -70
  5. package/src/generators/dotnet.js +134 -134
  6. package/src/generators/java.js +248 -248
  7. package/src/generators/js.js +345 -345
  8. package/src/generators/nestjs.js +277 -277
  9. package/src/generators/python.js +86 -86
  10. package/src/project-detector.js +131 -131
  11. package/src/qa/qa-engine.js +909 -0
  12. package/src/templates/dotnet/partials/Dockerfile.ejs +27 -27
  13. package/src/templates/dotnet/partials/docker-compose.yml.ejs +33 -33
  14. package/src/templates/js-express/base/server.js +59 -59
  15. package/src/templates/js-express/partials/Dockerfile.ejs +12 -12
  16. package/src/templates/js-express/partials/auth.controller.js.ejs +66 -66
  17. package/src/templates/js-express/partials/auth.middleware.js.ejs +19 -19
  18. package/src/templates/js-express/partials/auth.routes.js.ejs +9 -9
  19. package/src/templates/js-express/partials/controller.js.ejs +53 -53
  20. package/src/templates/js-express/partials/db.js.ejs +19 -19
  21. package/src/templates/js-express/partials/docker-compose.yml.ejs +46 -46
  22. package/src/templates/js-express/partials/model.js.ejs +18 -18
  23. package/src/templates/js-express/partials/package.json.ejs +17 -17
  24. package/src/templates/js-express/partials/prisma.schema.ejs +21 -21
  25. package/src/templates/js-express/partials/routes.js.ejs +19 -19
  26. package/src/templates/js-express/partials/seeder.js.ejs +103 -103
  27. package/src/templates/js-express/partials/service.js.ejs +51 -51
  28. package/src/templates/js-express/partials/swagger.js.ejs +30 -30
  29. package/src/templates/js-express/partials/test.js.ejs +46 -46
  30. package/src/templates/nestjs/base/app.module.ts +9 -9
  31. package/src/templates/nestjs/base/main.ts +23 -23
  32. package/src/templates/nestjs/base/tsconfig.json +21 -21
  33. package/src/templates/nestjs/partials/auth.controller.ts.ejs +17 -17
  34. package/src/templates/nestjs/partials/auth.module.ts.ejs +17 -17
  35. package/src/templates/nestjs/partials/auth.service.ts.ejs +70 -70
  36. package/src/templates/nestjs/partials/controller.ts.ejs +34 -34
  37. package/src/templates/nestjs/partials/create-dto.ts.ejs +22 -22
  38. package/src/templates/nestjs/partials/jwt-guard.ts.ejs +24 -24
  39. package/src/templates/nestjs/partials/module.ts.ejs +10 -10
  40. package/src/templates/nestjs/partials/package.json.ejs +27 -27
  41. package/src/templates/nestjs/partials/prisma.service.ts.ejs +13 -13
  42. package/src/templates/nestjs/partials/schema.ts.ejs +19 -19
  43. package/src/templates/nestjs/partials/service.ts.ejs +67 -67
  44. package/src/templates/nestjs/partials/update-dto.ts.ejs +4 -4
@@ -1,346 +1,346 @@
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
- import { analyzeFrontend } from '../analyzer.js';
7
- import { renderAndWrite, getTemplatePath } from './template.js';
8
-
9
- function stripQuery(p) {
10
- return String(p || '').split('?')[0];
11
- }
12
-
13
- function safePascalName(name) {
14
- const cleaned = String(name || 'Default')
15
- .split('?')[0]
16
- .replace(/[^a-zA-Z0-9]/g, '');
17
- if (!cleaned) return 'Default';
18
- return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
19
- }
20
-
21
- function sanitizeEndpoints(endpoints) {
22
- if (!Array.isArray(endpoints)) return [];
23
-
24
- return endpoints.map((ep) => {
25
- 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
-
31
- const resource = parts[0] || 'Default';
32
- const controllerName = safePascalName(resource);
33
- let functionName = '';
34
-
35
- if (controllerName.toLowerCase() === 'auth') {
36
- if (rawPath.includes('login')) functionName = 'loginUser';
37
- else if (rawPath.includes('register')) functionName = 'registerUser';
38
- else functionName = 'authAction';
39
- } else {
40
- const singularName = resource.endsWith('s') ? resource.slice(0, -1) : resource;
41
- const pluralName = resource.endsWith('s') ? resource : `${resource}s`;
42
- const pascalSingular = safePascalName(singularName);
43
- const pascalPlural = safePascalName(pluralName);
44
- const method = String(ep.method || 'GET').toUpperCase();
45
- const hasId = rawPath.includes(':') || rawPath.includes('{') || /\/\d+/.test(rawPath);
46
-
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
- }
58
- }
59
-
60
- return { ...ep, path: rawPath, route: rawPath, controllerName, functionName };
61
- });
62
- }
63
-
64
- export async function generateJsProject(options) {
65
- const {
66
- projectDir,
67
- projectName,
68
- frontendSrcDir,
69
- dbType,
70
- addAuth,
71
- addSeeder,
72
- extraFeatures = [],
73
- } = options;
74
-
75
- const port = 8000;
76
-
77
- 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
- }
89
-
90
- // --- Step 2: Identify Models ---
91
- const modelsToGenerate = new Map();
92
- endpoints.forEach((ep) => {
93
- if (!ep) return;
94
- const ctrl = safePascalName(ep.controllerName);
95
- if (ctrl === 'Default' || ctrl === 'Auth') return;
96
- 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
- }
105
- modelsToGenerate.set(ctrl, { name: ctrl, fields });
106
- }
107
- });
108
-
109
- 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
- });
118
- }
119
-
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'));
125
-
126
- // --- Step 4: package.json ---
127
- const pkgTpl = await fs.readFile(getTemplatePath('js-express/partials/package.json.ejs'), 'utf-8');
128
- const packageJsonContent = JSON.parse(ejs.render(pkgTpl, { projectName }));
129
-
130
- if (dbType === 'mongoose') packageJsonContent.dependencies.mongoose = '^7.6.3';
131
- if (dbType === 'prisma') {
132
- packageJsonContent.dependencies['@prisma/client'] = '^5.6.0';
133
- packageJsonContent.devDependencies.prisma = '^5.6.0';
134
- }
135
-
136
- if (addAuth) {
137
- packageJsonContent.dependencies.jsonwebtoken = '^9.0.2';
138
- packageJsonContent.dependencies.bcryptjs = '^2.4.3';
139
- }
140
-
141
- if (addSeeder) {
142
- packageJsonContent.devDependencies['@faker-js/faker'] = '^8.3.1';
143
- packageJsonContent.scripts.seed = 'node scripts/seeder.js';
144
- packageJsonContent.scripts.destroy = 'node scripts/seeder.js -d';
145
- }
146
-
147
- if (extraFeatures.includes('testing')) {
148
- packageJsonContent.devDependencies.vitest = '^1.1.0';
149
- packageJsonContent.devDependencies.supertest = '^6.3.3';
150
- packageJsonContent.scripts.test = 'vitest run';
151
- }
152
-
153
- if (extraFeatures.includes('swagger')) {
154
- packageJsonContent.dependencies['swagger-ui-express'] = '^5.0.0';
155
- packageJsonContent.dependencies['swagger-jsdoc'] = '^6.2.8';
156
- }
157
-
158
- await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
159
-
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
- );
166
-
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
- }
195
-
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
- );
209
- }
210
- }
211
-
212
- // --- Step 7: Routes ---
213
- const nonAuthEndpoints = endpoints.filter((ep) => safePascalName(ep.controllerName) !== 'Auth');
214
- await fs.ensureDir(path.join(srcDir, 'routes'));
215
-
216
- await renderAndWrite(
217
- getTemplatePath('js-express/partials/routes.js.ejs'),
218
- path.join(srcDir, 'routes', 'index.js'),
219
- { endpoints: nonAuthEndpoints, addAuth, dbType }
220
- );
221
-
222
- // --- Step 8: Auth ---
223
- 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 }
243
- );
244
- }
245
-
246
- // --- Step 9: Seeder ---
247
- 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
- );
255
- }
256
-
257
- // --- Step 10: Docker ---
258
- 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 }
269
- );
270
- }
271
-
272
- // --- Step 11: Swagger ---
273
- 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
- );
281
- }
282
-
283
- // --- Step 12: Testing ---
284
- 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
- );
292
- }
293
-
294
- // --- Step 13: Inject into server.js ---
295
- let serverContent = await fs.readFile(path.join(srcDir, 'server.js'), 'utf-8');
296
-
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
- }
303
-
304
- let swaggerInject = '';
305
- if (extraFeatures.includes('swagger')) {
306
- swaggerInject = "\nimport { setupSwagger } from './utils/swagger.js';\nsetupSwagger(app);\n";
307
- }
308
-
309
- let authInject = '';
310
- if (addAuth) {
311
- authInject = "import authRoutes from './routes/auth.js';\napp.use('/api/auth', authRoutes);\n\n";
312
- }
313
-
314
- serverContent = serverContent
315
- .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`);
322
-
323
- await fs.writeFile(path.join(srcDir, 'server.js'), serverContent);
324
-
325
- // --- Step 14: Install deps ---
326
- console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
327
- await execa('npm', ['install'], { cwd: projectDir });
328
-
329
- if (dbType === 'prisma') {
330
- console.log(chalk.blue(' -> Running `prisma generate`...'));
331
- await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
332
- }
333
-
334
- // --- Step 15: .env.example ---
335
- let envContent = `PORT=${port}\n`;
336
- if (dbType === 'mongoose') envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
337
- if (dbType === 'prisma') envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
338
- if (addAuth) envContent += 'JWT_SECRET=your_super_secret_jwt_key_12345\nJWT_EXPIRES_IN=5h\n';
339
-
340
- await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
341
-
342
- console.log(chalk.green(' -> JavaScript Express backend generation complete.'));
343
- } catch (error) {
344
- throw error;
345
- }
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
+ import { analyzeFrontend } from '../analyzer.js';
7
+ import { renderAndWrite, getTemplatePath } from './template.js';
8
+
9
+ function stripQuery(p) {
10
+ return String(p || '').split('?')[0];
11
+ }
12
+
13
+ function safePascalName(name) {
14
+ const cleaned = String(name || 'Default')
15
+ .split('?')[0]
16
+ .replace(/[^a-zA-Z0-9]/g, '');
17
+ if (!cleaned) return 'Default';
18
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
19
+ }
20
+
21
+ function sanitizeEndpoints(endpoints) {
22
+ if (!Array.isArray(endpoints)) return [];
23
+
24
+ return endpoints.map((ep) => {
25
+ 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
+
31
+ const resource = parts[0] || 'Default';
32
+ const controllerName = safePascalName(resource);
33
+ let functionName = '';
34
+
35
+ if (controllerName.toLowerCase() === 'auth') {
36
+ if (rawPath.includes('login')) functionName = 'loginUser';
37
+ else if (rawPath.includes('register')) functionName = 'registerUser';
38
+ else functionName = 'authAction';
39
+ } else {
40
+ const singularName = resource.endsWith('s') ? resource.slice(0, -1) : resource;
41
+ const pluralName = resource.endsWith('s') ? resource : `${resource}s`;
42
+ const pascalSingular = safePascalName(singularName);
43
+ const pascalPlural = safePascalName(pluralName);
44
+ const method = String(ep.method || 'GET').toUpperCase();
45
+ const hasId = rawPath.includes(':') || rawPath.includes('{') || /\/\d+/.test(rawPath);
46
+
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
+ }
58
+ }
59
+
60
+ return { ...ep, path: rawPath, route: rawPath, controllerName, functionName };
61
+ });
62
+ }
63
+
64
+ export async function generateJsProject(options) {
65
+ const {
66
+ projectDir,
67
+ projectName,
68
+ frontendSrcDir,
69
+ dbType,
70
+ addAuth,
71
+ addSeeder,
72
+ extraFeatures = [],
73
+ } = options;
74
+
75
+ const port = 8000;
76
+
77
+ 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
+ }
89
+
90
+ // --- Step 2: Identify Models ---
91
+ const modelsToGenerate = new Map();
92
+ endpoints.forEach((ep) => {
93
+ if (!ep) return;
94
+ const ctrl = safePascalName(ep.controllerName);
95
+ if (ctrl === 'Default' || ctrl === 'Auth') return;
96
+ 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
+ }
105
+ modelsToGenerate.set(ctrl, { name: ctrl, fields });
106
+ }
107
+ });
108
+
109
+ 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
+ });
118
+ }
119
+
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'));
125
+
126
+ // --- Step 4: package.json ---
127
+ const pkgTpl = await fs.readFile(getTemplatePath('js-express/partials/package.json.ejs'), 'utf-8');
128
+ const packageJsonContent = JSON.parse(ejs.render(pkgTpl, { projectName }));
129
+
130
+ if (dbType === 'mongoose') packageJsonContent.dependencies.mongoose = '^7.6.3';
131
+ if (dbType === 'prisma') {
132
+ packageJsonContent.dependencies['@prisma/client'] = '^5.6.0';
133
+ packageJsonContent.devDependencies.prisma = '^5.6.0';
134
+ }
135
+
136
+ if (addAuth) {
137
+ packageJsonContent.dependencies.jsonwebtoken = '^9.0.2';
138
+ packageJsonContent.dependencies.bcryptjs = '^2.4.3';
139
+ }
140
+
141
+ if (addSeeder) {
142
+ packageJsonContent.devDependencies['@faker-js/faker'] = '^8.3.1';
143
+ packageJsonContent.scripts.seed = 'node scripts/seeder.js';
144
+ packageJsonContent.scripts.destroy = 'node scripts/seeder.js -d';
145
+ }
146
+
147
+ if (extraFeatures.includes('testing')) {
148
+ packageJsonContent.devDependencies.vitest = '^1.1.0';
149
+ packageJsonContent.devDependencies.supertest = '^6.3.3';
150
+ packageJsonContent.scripts.test = 'vitest run';
151
+ }
152
+
153
+ if (extraFeatures.includes('swagger')) {
154
+ packageJsonContent.dependencies['swagger-ui-express'] = '^5.0.0';
155
+ packageJsonContent.dependencies['swagger-jsdoc'] = '^6.2.8';
156
+ }
157
+
158
+ await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
159
+
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
+ );
166
+
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
+ }
195
+
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
+ );
209
+ }
210
+ }
211
+
212
+ // --- Step 7: Routes ---
213
+ const nonAuthEndpoints = endpoints.filter((ep) => safePascalName(ep.controllerName) !== 'Auth');
214
+ await fs.ensureDir(path.join(srcDir, 'routes'));
215
+
216
+ await renderAndWrite(
217
+ getTemplatePath('js-express/partials/routes.js.ejs'),
218
+ path.join(srcDir, 'routes', 'index.js'),
219
+ { endpoints: nonAuthEndpoints, addAuth, dbType }
220
+ );
221
+
222
+ // --- Step 8: Auth ---
223
+ 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 }
243
+ );
244
+ }
245
+
246
+ // --- Step 9: Seeder ---
247
+ 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
+ );
255
+ }
256
+
257
+ // --- Step 10: Docker ---
258
+ 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 }
269
+ );
270
+ }
271
+
272
+ // --- Step 11: Swagger ---
273
+ 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
+ );
281
+ }
282
+
283
+ // --- Step 12: Testing ---
284
+ 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
+ );
292
+ }
293
+
294
+ // --- Step 13: Inject into server.js ---
295
+ let serverContent = await fs.readFile(path.join(srcDir, 'server.js'), 'utf-8');
296
+
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
+ }
303
+
304
+ let swaggerInject = '';
305
+ if (extraFeatures.includes('swagger')) {
306
+ swaggerInject = "\nimport { setupSwagger } from './utils/swagger.js';\nsetupSwagger(app);\n";
307
+ }
308
+
309
+ let authInject = '';
310
+ if (addAuth) {
311
+ authInject = "import authRoutes from './routes/auth.js';\napp.use('/api/auth', authRoutes);\n\n";
312
+ }
313
+
314
+ serverContent = serverContent
315
+ .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`);
322
+
323
+ await fs.writeFile(path.join(srcDir, 'server.js'), serverContent);
324
+
325
+ // --- Step 14: Install deps ---
326
+ console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
327
+ await execa('npm', ['install'], { cwd: projectDir });
328
+
329
+ if (dbType === 'prisma') {
330
+ console.log(chalk.blue(' -> Running `prisma generate`...'));
331
+ await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
332
+ }
333
+
334
+ // --- Step 15: .env.example ---
335
+ let envContent = `PORT=${port}\n`;
336
+ if (dbType === 'mongoose') envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
337
+ if (dbType === 'prisma') envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
338
+ if (addAuth) envContent += 'JWT_SECRET=your_super_secret_jwt_key_12345\nJWT_EXPIRES_IN=5h\n';
339
+
340
+ await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
341
+
342
+ console.log(chalk.green(' -> JavaScript Express backend generation complete.'));
343
+ } catch (error) {
344
+ throw error;
345
+ }
346
346
  }