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