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.
- package/bin/index.js +1411 -1060
- package/package.json +1 -1
- package/src/generators/dotnet.js +137 -81
- package/src/generators/java.js +118 -130
- package/src/generators/js.js +199 -207
- package/src/generators/nestjs.js +168 -155
- package/src/generators/node.js +212 -194
- package/src/generators/python.js +130 -45
- package/src/generators/template.js +47 -2
- package/src/qa/qa-engine.js +2320 -414
- package/src/templates/dotnet/partials/Controller.cs.ejs +264 -16
- package/src/templates/dotnet/partials/DbContext.cs.ejs +93 -3
- package/src/templates/dotnet/partials/Model.cs.ejs +192 -31
package/src/generators/js.js
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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: '
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
//
|
|
213
|
-
const
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
//
|
|
240
|
+
// Auth
|
|
223
241
|
if (addAuth) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
//
|
|
249
|
+
// Seeder
|
|
247
250
|
if (addSeeder) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
258
|
+
// Docker
|
|
258
259
|
if (extraFeatures.includes('docker')) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
getTemplatePath('js-express/partials/
|
|
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
|
-
//
|
|
266
|
+
// Swagger
|
|
273
267
|
if (extraFeatures.includes('swagger')) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
//
|
|
275
|
+
// Testing
|
|
284
276
|
if (extraFeatures.includes('testing')) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
295
|
+
const swaggerInject = extraFeatures.includes('swagger')
|
|
296
|
+
? "\nimport { setupSwagger } from './utils/swagger.js';\nsetupSwagger(app);\n"
|
|
297
|
+
: '';
|
|
308
298
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
326
|
-
console.log(chalk.magenta(' -> Installing dependencies...
|
|
327
|
-
|
|
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
|
-
|
|
331
|
-
|
|
321
|
+
await withRetry(() => execa('npx', ['prisma', 'generate'], { cwd: projectDir }), {
|
|
322
|
+
attempts: 2, baseDelay: 500, label: 'prisma generate',
|
|
323
|
+
});
|
|
332
324
|
}
|
|
333
325
|
|
|
334
|
-
//
|
|
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(
|
|
334
|
+
console.log(chalk.green(` -> ✓ JavaScript Express backend generation complete. Total: ${chalk.bold(totalTimer())}`));
|
|
343
335
|
} catch (error) {
|
|
344
336
|
throw error;
|
|
345
337
|
}
|