@ifecodes/backend-template 1.1.5 → 1.1.6
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/cli.js +477 -103
- package/bin/lib/microservice-config.js +12 -2
- package/bin/lib/prompts.js +15 -0
- package/bin/lib/readme-generator.js +67 -38
- package/bin/lib/service-setup.js +143 -20
- package/package.json +2 -2
- package/template/base/ts/src/utils/logger.ts +1 -1
- package/template/gateway/ts/app.ts +1 -0
- package/template/gateway/ts/inject.js +4 -3
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
|
|
7
|
-
export const generateDockerCompose = (target, allServices) => {
|
|
7
|
+
export const generateDockerCompose = (target, allServices, projectname) => {
|
|
8
8
|
const dockerCompose = {
|
|
9
9
|
services: {},
|
|
10
10
|
};
|
|
@@ -35,7 +35,7 @@ export const generateDockerCompose = (target, allServices) => {
|
|
|
35
35
|
dockerfile: "Dockerfile"
|
|
36
36
|
},
|
|
37
37
|
image: `${serviceName}:latest`,
|
|
38
|
-
container_name: serviceName,
|
|
38
|
+
container_name: projectname + "_" + serviceName,
|
|
39
39
|
ports: [
|
|
40
40
|
`\${${envVarName}:-${port}}:\${${envVarName}:-${port}}`,
|
|
41
41
|
],
|
|
@@ -67,6 +67,16 @@ export const generateDockerCompose = (target, allServices) => {
|
|
|
67
67
|
config.volumes.map((v) => ` - ${v}`).join("\n")
|
|
68
68
|
)
|
|
69
69
|
.join("\n")
|
|
70
|
+
+
|
|
71
|
+
"\n\n" +
|
|
72
|
+
// Add x-watch section to help file watchers map service src and shared folders
|
|
73
|
+
`x-watch:\n` +
|
|
74
|
+
allServices
|
|
75
|
+
.map(
|
|
76
|
+
(name) =>
|
|
77
|
+
` ${name}:\n - ./services/${name}/src\n - ./shared`
|
|
78
|
+
)
|
|
79
|
+
.join("\n")
|
|
70
80
|
);
|
|
71
81
|
};
|
|
72
82
|
|
package/bin/lib/prompts.js
CHANGED
|
@@ -215,6 +215,11 @@ export const getProjectConfig = async () => {
|
|
|
215
215
|
mode = res.mode;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// If adding to an existing microservice project, normalize the provided service name
|
|
219
|
+
if (isInMicroserviceProject && res.serviceName) {
|
|
220
|
+
res.serviceName = normalizeServiceName(res.serviceName);
|
|
221
|
+
}
|
|
222
|
+
|
|
218
223
|
return {
|
|
219
224
|
...res,
|
|
220
225
|
sanitizedName,
|
|
@@ -224,3 +229,13 @@ export const getProjectConfig = async () => {
|
|
|
224
229
|
isInMicroserviceProject,
|
|
225
230
|
};
|
|
226
231
|
};
|
|
232
|
+
|
|
233
|
+
// Normalize serviceName for existing microservice projects: ensure it ends with '-service'
|
|
234
|
+
// and is kebab-cased/lowercase for consistency.
|
|
235
|
+
function normalizeServiceName(name) {
|
|
236
|
+
if (!name) return name;
|
|
237
|
+
let svc = String(name).trim().toLowerCase();
|
|
238
|
+
svc = svc.replace(/\s+/g, "-").replace(/[^a-z0-9\-]/g, "");
|
|
239
|
+
if (!svc.endsWith("-service")) svc = `${svc}-service`;
|
|
240
|
+
return svc;
|
|
241
|
+
}
|
|
@@ -3,7 +3,6 @@ import path from "path";
|
|
|
3
3
|
|
|
4
4
|
export const generateReadme = (config, serviceName = null) => {
|
|
5
5
|
const { projectType, mode, features = [], auth, sanitizedName } = config;
|
|
6
|
-
const isMonolith = projectType === "monolith";
|
|
7
6
|
const isMicroservice = projectType === "microservice";
|
|
8
7
|
|
|
9
8
|
let readme = `# ${serviceName || sanitizedName}\n\n`;
|
|
@@ -24,9 +23,20 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
24
23
|
readme += `- **Deployment**: ${mode === "docker" ? "Docker" : "PM2"}\n`;
|
|
25
24
|
readme += `- **Gateway**: Port 4000 (main entry point)\n`;
|
|
26
25
|
readme += `- **Services**:\n`;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const servicesList = (config.allServices && config.allServices.length)
|
|
27
|
+
? config.allServices
|
|
28
|
+
: ["gateway", "health-service", ...(auth ? ["auth-service"] : [])];
|
|
29
|
+
servicesList.forEach((service, idx) => {
|
|
30
|
+
const isGateway = service === "gateway";
|
|
31
|
+
const port = isGateway
|
|
32
|
+
? 4000
|
|
33
|
+
: 4001 + servicesList.filter((s) => s !== "gateway" && servicesList.indexOf(s) < idx).length;
|
|
34
|
+
const pretty = service
|
|
35
|
+
.split("-")
|
|
36
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
37
|
+
.join(" ");
|
|
38
|
+
readme += ` - ${pretty} (port ${port})\n`;
|
|
39
|
+
});
|
|
30
40
|
readme += `\n`;
|
|
31
41
|
} else {
|
|
32
42
|
readme += `- **Type**: Monolith API\n`;
|
|
@@ -108,11 +118,11 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
108
118
|
readme += `### With Docker\n\n`;
|
|
109
119
|
readme += `\`\`\`bash\n`;
|
|
110
120
|
readme += `# Start all services\n`;
|
|
111
|
-
readme += `
|
|
121
|
+
readme += `npm run dev\n\n`;
|
|
112
122
|
readme += `# Start in detached mode\n`;
|
|
113
|
-
readme += `
|
|
123
|
+
readme += `npm run dev -d\n\n`;
|
|
114
124
|
readme += `# Stop all services\n`;
|
|
115
|
-
readme += `
|
|
125
|
+
readme += `npm stop\n`;
|
|
116
126
|
readme += `\`\`\`\n\n`;
|
|
117
127
|
} else if (isMicroservice && mode === "nodocker") {
|
|
118
128
|
readme += `### With PM2\n\n`;
|
|
@@ -159,9 +169,18 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
readme += `### Direct Service Access (Development Only)\n`;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
const directServices = (config.allServices && config.allServices.length)
|
|
173
|
+
? config.allServices
|
|
174
|
+
: ["gateway", "health-service", ...(auth ? ["auth-service"] : [])];
|
|
175
|
+
directServices.forEach((service) => {
|
|
176
|
+
const isGateway = service === "gateway";
|
|
177
|
+
const port = isGateway
|
|
178
|
+
? 4000
|
|
179
|
+
: 4001 + directServices.filter((s) => s !== "gateway" && directServices.indexOf(s) < directServices.indexOf(service)).length;
|
|
180
|
+
const basePath = isGateway ? `` : `/api/v1`;
|
|
181
|
+
readme += `- **${service}**: \`http://localhost:${port}${basePath}\`\n`;
|
|
182
|
+
});
|
|
183
|
+
readme += `\n`;
|
|
165
184
|
|
|
166
185
|
readme += "### Example Requests\n";
|
|
167
186
|
readme += "```bash\n";
|
|
@@ -169,28 +188,28 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
169
188
|
readme += "curl http://localhost:4000/\n\n";
|
|
170
189
|
readme += "# Gateway health\n";
|
|
171
190
|
readme += "curl http://localhost:4000/health\n\n";
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
readme +=
|
|
181
|
-
readme +=
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
readme +=
|
|
187
|
-
readme += ` -H "Content-Type: application/json" \\\n`;
|
|
188
|
-
readme += ` -d '{"username":"testuser","password":"password123"}'\n\n`;
|
|
189
|
-
readme +=
|
|
190
|
-
readme += `curl -X POST http://localhost
|
|
191
|
-
readme += ` -H "Content-Type: application/json" \\\n`;
|
|
192
|
-
readme += ` -d '{"username":"testuser","password":"password123"}'\n`;
|
|
191
|
+
|
|
192
|
+
// Direct access examples for non-gateway services
|
|
193
|
+
const exampleServices = (config.allServices && config.allServices.length)
|
|
194
|
+
? config.allServices
|
|
195
|
+
: ["gateway", "health-service", ...(auth ? ["auth-service"] : [])];
|
|
196
|
+
exampleServices.forEach((service) => {
|
|
197
|
+
if (service === "gateway") return; // gateway already covered
|
|
198
|
+
const port = service === "gateway" ? 4000 : 4001 + exampleServices.filter((s) => s !== "gateway" && exampleServices.indexOf(s) < exampleServices.indexOf(service)).length;
|
|
199
|
+
readme += `# ${service} (direct access)\n`;
|
|
200
|
+
readme += `curl http://localhost:${port}/api/v1/health\n\n`;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (auth && exampleServices.includes("auth-service")) {
|
|
204
|
+
const authPort = 4001 + exampleServices.filter((s) => s !== "gateway" && exampleServices.indexOf(s) < exampleServices.indexOf("auth-service")).length;
|
|
205
|
+
readme += "# Auth requests (through gateway)\n";
|
|
206
|
+
readme += `curl -X POST http://localhost:4000/api/v1/auth/register \\\n+ -H "Content-Type: application/json" \\\n+ -d '{"username":"testuser","password":"password123"}'\n\n`;
|
|
207
|
+
readme += `curl -X POST http://localhost:4000/api/v1/auth/login \\\n+ -H "Content-Type: application/json" \\\n+ -d '{"username":"testuser","password":"password123"}'\n\n`;
|
|
208
|
+
readme += `# Auth requests (direct access)\n`;
|
|
209
|
+
readme += `curl -X POST http://localhost:${authPort}/api/v1/auth/register \\\n+ -H "Content-Type: application/json" \\\n+ -d '{"username":"testuser","password":"password123"}'\n\n`;
|
|
210
|
+
readme += `curl -X POST http://localhost:${authPort}/api/v1/auth/login \\\n+ -H "Content-Type: application/json" \\\n+ -d '{"username":"testuser","password":"password123"}'\n\n`;
|
|
193
211
|
}
|
|
212
|
+
|
|
194
213
|
readme += "```\n\n";
|
|
195
214
|
|
|
196
215
|
} else {
|
|
@@ -234,10 +253,12 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
234
253
|
readme += `│ ├── config/ # Database, environment configs\n`;
|
|
235
254
|
readme += `│ └── utils/ # Logger, error handlers\n`;
|
|
236
255
|
readme += `├── services/\n`;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
256
|
+
const projectServices = (config.allServices && config.allServices.length)
|
|
257
|
+
? config.allServices
|
|
258
|
+
: ["gateway", "health-service", ...(auth ? ["auth-service"] : [])];
|
|
259
|
+
projectServices.forEach((service) => {
|
|
260
|
+
readme += `│ ├── ${service}/\n`;
|
|
261
|
+
});
|
|
241
262
|
readme += `├── ${mode === "docker" ? "docker-compose.yml" : "pm2.config.js"}\n`;
|
|
242
263
|
readme += `├── .husky/ # Git hooks\n`;
|
|
243
264
|
readme += `└── package.json # Root package.json\n`;
|
|
@@ -265,8 +286,8 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
265
286
|
readme += `## Available Scripts\n\n`;
|
|
266
287
|
if (isMicroservice) {
|
|
267
288
|
if (mode === "docker") {
|
|
268
|
-
readme += `- \`
|
|
269
|
-
readme += `- \`
|
|
289
|
+
readme += `- \`npm run dev\` - Start all services\n`;
|
|
290
|
+
readme += `- \`npm stop\` - Stop all services\n`;
|
|
270
291
|
readme += `- \`docker-compose logs -f [service-name]\` - View service logs\n`;
|
|
271
292
|
} else {
|
|
272
293
|
readme += `- \`pm2 start pm2.config.js\` - Start all services\n`;
|
|
@@ -298,9 +319,17 @@ export const generateReadme = (config, serviceName = null) => {
|
|
|
298
319
|
}
|
|
299
320
|
readme += `\n`;
|
|
300
321
|
|
|
322
|
+
// Scaffold attribution
|
|
323
|
+
readme += `\n`;
|
|
324
|
+
readme += `## About this Scaffold\n\n`;
|
|
325
|
+
readme += `This project was generated using the @ifecodes/backend-template scaffold. `;
|
|
326
|
+
readme += `You can recreate or customize this scaffold using the CLI: \n\n`;
|
|
327
|
+
readme += `- Run without installing (recommended): \`npx ifecodes-template\`\n`;
|
|
328
|
+
readme += `- Install globally: \`npm i -g @ifecodes/backend-template\` and run \`ifecodes-template\`\n\n`;
|
|
329
|
+
|
|
301
330
|
// License
|
|
302
331
|
readme += `## License\n\n`;
|
|
303
332
|
readme += `MIT\n`;
|
|
304
|
-
|
|
333
|
+
|
|
305
334
|
return readme;
|
|
306
335
|
};
|
package/bin/lib/service-setup.js
CHANGED
|
@@ -36,11 +36,36 @@ export const setupService = async (
|
|
|
36
36
|
// Detect file extension (ts or js)
|
|
37
37
|
const ext = getFileExtension(serviceRoot);
|
|
38
38
|
|
|
39
|
+
// Remove workspace-level config files from service (they should live at root)
|
|
40
|
+
try {
|
|
41
|
+
const serviceConfigFiles = [
|
|
42
|
+
".prettierrc",
|
|
43
|
+
".prettierignore",
|
|
44
|
+
".eslintrc.json",
|
|
45
|
+
"eslint.config.js",
|
|
46
|
+
"husky",
|
|
47
|
+
];
|
|
48
|
+
for (const f of serviceConfigFiles) {
|
|
49
|
+
const p = path.join(serviceRoot, f);
|
|
50
|
+
if (fs.existsSync(p)) {
|
|
51
|
+
// Remove file or directory
|
|
52
|
+
const stat = fs.statSync(p);
|
|
53
|
+
if (stat.isDirectory()) fs.rmSync(p, { recursive: true, force: true });
|
|
54
|
+
else fs.rmSync(p, { force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Non-fatal
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
// Ensure service-level gitignore is renamed immediately after template copy
|
|
40
62
|
try {
|
|
41
63
|
const serviceGitignore = path.join(serviceRoot, "gitignore");
|
|
42
64
|
const serviceDotGitignore = path.join(serviceRoot, ".gitignore");
|
|
43
|
-
if (
|
|
65
|
+
if (
|
|
66
|
+
fs.existsSync(serviceGitignore) &&
|
|
67
|
+
!fs.existsSync(serviceDotGitignore)
|
|
68
|
+
) {
|
|
44
69
|
fs.renameSync(serviceGitignore, serviceDotGitignore);
|
|
45
70
|
}
|
|
46
71
|
} catch (err) {
|
|
@@ -49,9 +74,11 @@ export const setupService = async (
|
|
|
49
74
|
|
|
50
75
|
// Special handling for gateway service
|
|
51
76
|
if (serviceName === "gateway") {
|
|
77
|
+
const tmplLang = res.language === "javascript" ? "js" : "ts";
|
|
52
78
|
const gatewayModule = await import(
|
|
53
|
-
`../../template/gateway/${
|
|
79
|
+
`../../template/gateway/${tmplLang}/inject.js`
|
|
54
80
|
);
|
|
81
|
+
|
|
55
82
|
deps.push(...gatewayModule.gatewayDeps);
|
|
56
83
|
|
|
57
84
|
// Copy gateway-specific files
|
|
@@ -63,7 +90,7 @@ export const setupService = async (
|
|
|
63
90
|
const templateExt = res.language === "javascript" ? ".js" : ".ts";
|
|
64
91
|
const templateDir = path.join(
|
|
65
92
|
__dirname,
|
|
66
|
-
`../../template/gateway/${
|
|
93
|
+
`../../template/gateway/${tmplLang}`,
|
|
67
94
|
);
|
|
68
95
|
const gatewayAppContent = fs.readFileSync(
|
|
69
96
|
path.join(templateDir, `app${templateExt}`),
|
|
@@ -86,9 +113,13 @@ export const setupService = async (
|
|
|
86
113
|
const routesPath = path.join(serviceRoot, `src/routes.${ext}`);
|
|
87
114
|
const modulesPath = path.join(serviceRoot, "src/modules");
|
|
88
115
|
const middlewaresPath = path.join(serviceRoot, "src/middlewares");
|
|
116
|
+
const configPath = path.join(serviceRoot, `src/config`);
|
|
117
|
+
const utilPath = path.join(serviceRoot, `src/utils`);
|
|
89
118
|
|
|
90
119
|
if (fs.existsSync(routesPath)) fs.rmSync(routesPath);
|
|
91
120
|
if (fs.existsSync(modulesPath)) fs.rmSync(modulesPath, { recursive: true });
|
|
121
|
+
if (fs.existsSync(configPath)) fs.rmSync(configPath, { recursive: true });
|
|
122
|
+
if (fs.existsSync(utilPath)) fs.rmSync(utilPath, { recursive: true });
|
|
92
123
|
if (fs.existsSync(middlewaresPath))
|
|
93
124
|
fs.rmSync(middlewaresPath, { recursive: true });
|
|
94
125
|
} else {
|
|
@@ -279,7 +310,7 @@ export const setupService = async (
|
|
|
279
310
|
envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
|
|
280
311
|
}
|
|
281
312
|
|
|
282
|
-
// Add MONGO_URI if auth is enabled
|
|
313
|
+
// Add MONGO_URI and JWT_SECRET if auth is enabled
|
|
283
314
|
if (shouldIncludeAuth && res.auth) {
|
|
284
315
|
const assertion = res.language === "javascript" ? "" : "!";
|
|
285
316
|
envContent = envContent.replace(
|
|
@@ -374,7 +405,8 @@ export const setupService = async (
|
|
|
374
405
|
} // End of else block for non-gateway services
|
|
375
406
|
|
|
376
407
|
// Update tsconfig.json for microservices to support @/ alias with shared folder
|
|
377
|
-
|
|
408
|
+
// Also run when adding a service into an existing microservice project
|
|
409
|
+
if (res.projectType === "microservice" || res.isInMicroserviceProject) {
|
|
378
410
|
const tsconfigPath = path.join(serviceRoot, "tsconfig.json");
|
|
379
411
|
let tsconfigContent = fs.readFileSync(tsconfigPath, "utf8");
|
|
380
412
|
|
|
@@ -421,17 +453,67 @@ export const setupService = async (
|
|
|
421
453
|
// Update server.ts to use shared imports
|
|
422
454
|
const serverPath = path.join(serviceRoot, `src/server.${ext}`);
|
|
423
455
|
if (fs.existsSync(serverPath)) {
|
|
456
|
+
// Determine a single port string for this specific service.
|
|
457
|
+
// Gateway should use 4000; other services use 4001, 4002, ...
|
|
458
|
+
let serverPort = "3000";
|
|
459
|
+
if (Array.isArray(allServices) && allServices.length) {
|
|
460
|
+
if (serviceName === "gateway") {
|
|
461
|
+
serverPort = "4000";
|
|
462
|
+
} else {
|
|
463
|
+
const idx = allServices.indexOf(serviceName);
|
|
464
|
+
if (idx !== -1) {
|
|
465
|
+
// Count non-gateway services before this one to compute offset
|
|
466
|
+
const nonGatewayBefore = allServices
|
|
467
|
+
.slice(0, idx)
|
|
468
|
+
.filter((s) => s !== "gateway").length;
|
|
469
|
+
serverPort = `${4001 + nonGatewayBefore}`;
|
|
470
|
+
} else {
|
|
471
|
+
// Fallback: assign next available port after 4000
|
|
472
|
+
serverPort = `${4001 + allServices.length - 1}`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
424
477
|
let serverContent = fs.readFileSync(serverPath, "utf8");
|
|
478
|
+
|
|
479
|
+
// Normalize imports: accept @/ or relative imports and rewrite to shared imports
|
|
425
480
|
serverContent = serverContent
|
|
426
|
-
.replace(
|
|
427
|
-
|
|
481
|
+
.replace(
|
|
482
|
+
/from\s+["'](?:@\/utils|\.\/utils|\.\.\/utils)["']/g,
|
|
483
|
+
'from "@/shared/utils"',
|
|
484
|
+
)
|
|
485
|
+
.replace(
|
|
486
|
+
/from\s+["'](?:@\/config|\.\/config|\.\.\/config)["']/g,
|
|
487
|
+
'from "@/shared/config"',
|
|
488
|
+
);
|
|
428
489
|
|
|
429
|
-
// Update PORT to use service-specific environment variable
|
|
490
|
+
// Update PORT to use service-specific environment variable and a correct default port.
|
|
430
491
|
const portEnvVar = `${serviceName.toUpperCase().replace(/-/g, "_")}_PORT`;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
492
|
+
const portRegex = /const\s+PORT\s*=\s*ENV\.PORT\s*\|\|\s*(\d+)\s*;/;
|
|
493
|
+
if (portRegex.test(serverContent)) {
|
|
494
|
+
serverContent = serverContent.replace(
|
|
495
|
+
portRegex,
|
|
496
|
+
`const PORT = ENV.${portEnvVar} || ${serverPort};`,
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
// Fallback: replace a simple numeric default or a bare PORT assignment
|
|
500
|
+
const simplePortRegex = /const\s+PORT\s*=\s*(\d+)\s*;/;
|
|
501
|
+
if (simplePortRegex.test(serverContent)) {
|
|
502
|
+
serverContent = serverContent.replace(
|
|
503
|
+
simplePortRegex,
|
|
504
|
+
`const PORT = ENV.${portEnvVar} || ${serverPort};`,
|
|
505
|
+
);
|
|
506
|
+
} else {
|
|
507
|
+
// Last resort: append a PORT assignment near the top after imports
|
|
508
|
+
const importEnd = serverContent.indexOf("\n\n");
|
|
509
|
+
const insertPos = importEnd === -1 ? 0 : importEnd + 2;
|
|
510
|
+
const portLine = `const PORT = ENV.${portEnvVar} || ${serverPort};\n\n`;
|
|
511
|
+
serverContent =
|
|
512
|
+
serverContent.slice(0, insertPos) +
|
|
513
|
+
portLine +
|
|
514
|
+
serverContent.slice(insertPos);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
435
517
|
|
|
436
518
|
fs.writeFileSync(serverPath, serverContent);
|
|
437
519
|
}
|
|
@@ -476,14 +558,6 @@ export const setupService = async (
|
|
|
476
558
|
.filter(Boolean);
|
|
477
559
|
}
|
|
478
560
|
|
|
479
|
-
// Add Node.js native path aliasing for JavaScript projects
|
|
480
|
-
// This replaces TypeScript's tsconfig paths with Node's native imports field
|
|
481
|
-
if (res.language === "javascript") {
|
|
482
|
-
finalPackageJson.imports = {
|
|
483
|
-
"#/*": "./src/*",
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
561
|
// Add --poll flag to dev script for Docker mode (fixes watch mode in Docker on Windows)
|
|
488
562
|
if (res.projectType === "microservice" && res.mode === "docker") {
|
|
489
563
|
if (finalPackageJson.scripts && finalPackageJson.scripts.dev) {
|
|
@@ -494,6 +568,55 @@ export const setupService = async (
|
|
|
494
568
|
}
|
|
495
569
|
}
|
|
496
570
|
|
|
571
|
+
// If creating microservices, do not install workspace-level devDependencies per service
|
|
572
|
+
if (res.projectType === "microservice") {
|
|
573
|
+
if (finalPackageJson.devDependencies) {
|
|
574
|
+
const toRemove = [
|
|
575
|
+
"prettier",
|
|
576
|
+
"eslint",
|
|
577
|
+
"eslint-config-prettier",
|
|
578
|
+
"@typescript-eslint/eslint-plugin",
|
|
579
|
+
"@typescript-eslint/parser",
|
|
580
|
+
"husky",
|
|
581
|
+
];
|
|
582
|
+
for (const dep of toRemove) {
|
|
583
|
+
if (finalPackageJson.devDependencies[dep]) {
|
|
584
|
+
delete finalPackageJson.devDependencies[dep];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Remove @types/* from JavaScript services; keep for TypeScript
|
|
589
|
+
if (res.language === "javascript") {
|
|
590
|
+
for (const key of Object.keys(finalPackageJson.devDependencies)) {
|
|
591
|
+
if (key.startsWith("@types/"))
|
|
592
|
+
delete finalPackageJson.devDependencies[key];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// If devDependencies becomes empty, remove the field
|
|
597
|
+
if (Object.keys(finalPackageJson.devDependencies).length === 0) {
|
|
598
|
+
delete finalPackageJson.devDependencies;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Remove per-service prepare script (which runs husky) for microservice workspaces
|
|
604
|
+
if (res.projectType === "microservice" || res.isInMicroserviceProject) {
|
|
605
|
+
if (finalPackageJson.scripts && finalPackageJson.scripts.prepare) {
|
|
606
|
+
delete finalPackageJson.scripts.prepare;
|
|
607
|
+
}
|
|
608
|
+
// Also remove per-service lint/format/check-format scripts (workspace-level tooling lives at root)
|
|
609
|
+
if (finalPackageJson.scripts) {
|
|
610
|
+
delete finalPackageJson.scripts.lint;
|
|
611
|
+
delete finalPackageJson.scripts.format;
|
|
612
|
+
delete finalPackageJson.scripts["check-format"];
|
|
613
|
+
// If scripts becomes empty, remove the field
|
|
614
|
+
if (Object.keys(finalPackageJson.scripts).length === 0) {
|
|
615
|
+
delete finalPackageJson.scripts;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
497
620
|
fs.writeFileSync(
|
|
498
621
|
packageJsonPath,
|
|
499
622
|
JSON.stringify(finalPackageJson, null, 2) + "\n",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifecodes/backend-template",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Production-ready Express + TypeScript/JavaScript backend generator with optional features and microservice support",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ifecodes-template": "bin/cli.js"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"bugs": {
|
|
21
21
|
"url": "https://github.com/ALADETAN-IFE/backend-template/issues"
|
|
22
22
|
},
|
|
23
|
-
"homepage": "https://
|
|
23
|
+
"homepage": "https://backend-template-generator.vercel.app",
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
@@ -28,7 +28,7 @@ console.log(
|
|
|
28
28
|
const requiredKeys = ENV && Object.keys(ENV).length ? Object.keys(ENV) : [];
|
|
29
29
|
|
|
30
30
|
const missing = requiredKeys.filter(
|
|
31
|
-
(k) => ENV == null || (ENV as Record<string, undefined>)[k] === undefined || (ENV as Record<string, undefined>)[k] === "",
|
|
31
|
+
(k) => ENV == null || (ENV as Record<string, string | undefined>)[k] === undefined || (ENV as Record<string, string | undefined>)[k] === "",
|
|
32
32
|
);
|
|
33
33
|
|
|
34
34
|
if (missing.length === requiredKeys.length) {
|
|
@@ -4,18 +4,19 @@ export const generateGatewayRoutes = (services, mode = "docker") => {
|
|
|
4
4
|
const routes = services
|
|
5
5
|
.filter((s) => s !== "gateway")
|
|
6
6
|
.map((service, index) => {
|
|
7
|
+
const servicePort = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
|
|
7
8
|
const port = 4001 + index; // Host port mapping: gateway=4000, services start at 4001
|
|
8
|
-
const routePath = service.replace("-service", "");
|
|
9
9
|
|
|
10
10
|
// Docker: use container name with internal port 4000
|
|
11
11
|
// Non-docker: use localhost with mapped host port
|
|
12
12
|
const host = mode === "docker" ? service : "localhost";
|
|
13
|
-
const
|
|
13
|
+
const servicePortEnv = `${servicePort}`
|
|
14
14
|
|
|
15
15
|
return `
|
|
16
16
|
// Proxy to ${service}
|
|
17
|
+
const ${servicePort} = ENV.${servicePort} || ${port}
|
|
17
18
|
app.use("/api", createProxyMiddleware({
|
|
18
|
-
target:
|
|
19
|
+
target: \`http://${host}:${servicePortEnv}/api\`,
|
|
19
20
|
changeOrigin: true,
|
|
20
21
|
on: {
|
|
21
22
|
error: (err, req, res) => {
|