@ifecodes/backend-template 1.1.9 → 1.4.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.
Files changed (94) hide show
  1. package/README.md +423 -383
  2. package/bin/cli.js +1294 -964
  3. package/bin/lib/microservice-config.js +155 -150
  4. package/bin/lib/prompts.js +277 -241
  5. package/bin/lib/readme-generator.js +364 -329
  6. package/bin/lib/service-setup.js +901 -684
  7. package/package.json +64 -55
  8. package/template/base/js/.eslintrc.json +10 -13
  9. package/template/base/js/.prettierrc +7 -7
  10. package/template/base/js/eslint.config.js +33 -31
  11. package/template/base/js/package.json +29 -28
  12. package/template/base/js/src/app.js +20 -18
  13. package/template/base/js/src/config/db.js +8 -8
  14. package/template/base/js/src/config/env.js +44 -14
  15. package/template/base/js/src/config/index.js +7 -7
  16. package/template/base/js/src/docs/index.js +5 -0
  17. package/template/base/js/src/docs/route-registry.js +63 -0
  18. package/template/base/js/src/middlewares/error-handler.middleware.js +22 -19
  19. package/template/base/js/src/middlewares/index.js +15 -11
  20. package/template/base/js/src/middlewares/method-not-allowed.middleware.js +19 -13
  21. package/template/base/js/src/middlewares/not-found.middleware.js +13 -10
  22. package/template/base/js/src/middlewares/observability.middleware.js +24 -0
  23. package/template/base/js/src/middlewares/root.middleware.js +18 -16
  24. package/template/base/js/src/middlewares/validation.middleware.js +39 -0
  25. package/template/base/js/src/modules/index.js +8 -8
  26. package/template/base/js/src/modules/v1/health/health.controller.auth.js +29 -0
  27. package/template/base/js/src/modules/v1/health/health.controller.js +21 -21
  28. package/template/base/js/src/modules/v1/health/health.route.js +74 -9
  29. package/template/base/js/src/modules/v1/health/index.js +5 -5
  30. package/template/base/js/src/modules/v1/index.js +8 -8
  31. package/template/base/js/src/routes.js +23 -16
  32. package/template/base/js/src/server.js +18 -18
  33. package/template/base/js/src/utils/http-error.js +74 -74
  34. package/template/base/js/src/utils/index.js +28 -28
  35. package/template/base/js/src/utils/logger.js +57 -67
  36. package/template/base/ts/.eslintrc.json +13 -17
  37. package/template/base/ts/.prettierrc +7 -7
  38. package/template/base/ts/eslint.config.js +33 -33
  39. package/template/base/ts/package.json +41 -39
  40. package/template/base/ts/src/app.ts +20 -18
  41. package/template/base/ts/src/config/db.ts +4 -4
  42. package/template/base/ts/src/config/env.ts +40 -10
  43. package/template/base/ts/src/config/index.ts +2 -2
  44. package/template/base/ts/src/docs/index.ts +3 -0
  45. package/template/base/ts/src/docs/route-registry.ts +98 -0
  46. package/template/base/ts/src/middlewares/error-handler.middleware.ts +4 -1
  47. package/template/base/ts/src/middlewares/index.ts +6 -4
  48. package/template/base/ts/src/middlewares/method-not-allowed.middleware.ts +23 -18
  49. package/template/base/ts/src/middlewares/not-found.middleware.ts +10 -8
  50. package/template/base/ts/src/middlewares/observability.middleware.ts +25 -0
  51. package/template/base/ts/src/middlewares/root.middleware.ts +16 -14
  52. package/template/base/ts/src/middlewares/validation.middleware.ts +46 -0
  53. package/template/base/ts/src/modules/index.ts +8 -8
  54. package/template/base/ts/src/modules/v1/health/health.controller.auth.ts +26 -0
  55. package/template/base/ts/src/modules/v1/health/health.controller.ts +18 -18
  56. package/template/base/ts/src/modules/v1/health/health.route.ts +68 -9
  57. package/template/base/ts/src/modules/v1/health/index.ts +1 -1
  58. package/template/base/ts/src/modules/v1/index.ts +8 -8
  59. package/template/base/ts/src/routes.ts +23 -15
  60. package/template/base/ts/src/server.ts +19 -19
  61. package/template/base/ts/src/utils/http-error.ts +63 -63
  62. package/template/base/ts/src/utils/index.ts +14 -14
  63. package/template/base/ts/src/utils/logger.ts +58 -68
  64. package/template/base/ts/tsconfig.json +21 -21
  65. package/template/features/auth/argon2/inject.js +50 -50
  66. package/template/features/auth/base/health-openapi.ts +62 -0
  67. package/template/features/auth/base/inject.js +174 -172
  68. package/template/features/auth/bcrypt/inject.js +40 -40
  69. package/template/features/auth/models/index.ts +1 -1
  70. package/template/features/auth/models/user.model.js +24 -24
  71. package/template/features/auth/models/user.model.ts +28 -28
  72. package/template/features/auth/modules/auth.controller.js +21 -21
  73. package/template/features/auth/modules/auth.controller.ts +28 -20
  74. package/template/features/auth/modules/auth.routes.js +89 -10
  75. package/template/features/auth/modules/auth.routes.ts +86 -11
  76. package/template/features/auth/modules/auth.service.js +29 -29
  77. package/template/features/auth/modules/auth.service.ts +38 -38
  78. package/template/features/auth/modules/index.js +1 -1
  79. package/template/features/auth/modules/index.ts +1 -1
  80. package/template/features/auth/utils/hash.ts +20 -20
  81. package/template/features/auth/utils/jwt.js +12 -12
  82. package/template/features/auth/utils/jwt.ts +15 -15
  83. package/template/features/cors/inject.js +14 -13
  84. package/template/features/helmet/inject.js +7 -6
  85. package/template/features/morgan/inject.js +8 -7
  86. package/template/features/rate-limit/inject.js +7 -6
  87. package/template/gateway/js/app.js +42 -42
  88. package/template/gateway/js/inject.js +33 -33
  89. package/template/gateway/js/server.js +19 -19
  90. package/template/gateway/ts/app.ts +43 -43
  91. package/template/gateway/ts/inject.js +33 -33
  92. package/template/gateway/ts/server.ts +19 -19
  93. package/template/microservice/docker/docker-compose.yml +5 -5
  94. package/template/microservice/nodocker/pm2.config.js +3 -3
@@ -1,684 +1,901 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import prompts from "prompts";
4
- import pc from "picocolors";
5
- import { execSync } from "child_process";
6
- import { fileURLToPath } from "url";
7
-
8
- // Helper function to get the correct file extension (.ts or .js)
9
- function getFileExtension(dir) {
10
- // Check if .ts files exist, otherwise use .js
11
- const sampleFiles = ["src/app.ts", "src/server.ts", "src/routes.ts"];
12
- for (const file of sampleFiles) {
13
- const tsPath = path.join(dir, file);
14
- if (fs.existsSync(tsPath)) return "ts";
15
- const jsPath = path.join(dir, file.replace(".ts", ".js"));
16
- if (fs.existsSync(jsPath)) return "js";
17
- }
18
- return "ts"; // default to ts
19
- }
20
-
21
- export const setupService = async (
22
- res,
23
- serviceName,
24
- serviceRoot,
25
- shouldIncludeAuth,
26
- allServices = [],
27
- skipInstall = false,
28
- ) => {
29
- let imports = [];
30
- let middlewares = [];
31
- let deps = [];
32
- let devDeps = [];
33
- let v1Imports = [];
34
- let v1Routes = [];
35
-
36
- // Detect file extension (ts or js)
37
- const ext = getFileExtension(serviceRoot);
38
-
39
- // Remove workspace-level config files from service only in microservice mode (they should live at root)
40
- // (monolith projects keep these files at project root)
41
- if (res.projectType === "microservice" || res.isInMicroserviceProject) {
42
- try {
43
- const serviceConfigFiles = [
44
- ".prettierrc",
45
- ".prettierignore",
46
- ".eslintrc.json",
47
- "eslint.config.js",
48
- ".husky",
49
- "husky",
50
- ];
51
- for (const f of serviceConfigFiles) {
52
- const p = path.join(serviceRoot, f);
53
- if (fs.existsSync(p)) {
54
- // Remove file or directory
55
- const stat = fs.statSync(p);
56
- if (stat.isDirectory())
57
- fs.rmSync(p, { recursive: true, force: true });
58
- else fs.rmSync(p, { force: true });
59
- }
60
- }
61
- } catch (err) {
62
- // Non-fatal
63
- }
64
- }
65
-
66
- // Ensure service-level gitignore is renamed immediately after template copy
67
- try {
68
- const serviceGitignore = path.join(serviceRoot, "gitignore");
69
- const serviceDotGitignore = path.join(serviceRoot, ".gitignore");
70
- if (
71
- fs.existsSync(serviceGitignore) &&
72
- !fs.existsSync(serviceDotGitignore)
73
- ) {
74
- fs.renameSync(serviceGitignore, serviceDotGitignore);
75
- }
76
- } catch (err) {
77
- // Non-fatal; continue setup
78
- }
79
-
80
- // Special handling for gateway service
81
- if (serviceName === "gateway") {
82
- const tmplLang = res.language === "javascript" ? "js" : "ts";
83
- const gatewayModule = await import(
84
- `../../template/gateway/${tmplLang}/inject.js`
85
- );
86
-
87
- deps.push(...gatewayModule.gatewayDeps);
88
-
89
- // Copy gateway-specific files
90
- const gatewayAppPath = path.join(serviceRoot, `src/app.${ext}`);
91
- const gatewayServerPath = path.join(serviceRoot, `src/server.${ext}`);
92
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
93
-
94
- // Read gateway template files according to selected language
95
- const templateExt = res.language === "javascript" ? ".js" : ".ts";
96
- const templateDir = path.join(
97
- __dirname,
98
- `../../template/gateway/${tmplLang}`,
99
- );
100
- const gatewayAppContent = fs.readFileSync(
101
- path.join(templateDir, `app${templateExt}`),
102
- "utf8",
103
- );
104
- const gatewayServerContent = fs.readFileSync(
105
- path.join(templateDir, `server${templateExt}`),
106
- "utf8",
107
- );
108
-
109
- // Generate routes for all services with mode (docker or nodocker)
110
- const mode = res.mode || "docker";
111
- const routes = gatewayModule.generateGatewayRoutes(allServices, mode);
112
- const finalAppContent = gatewayAppContent.replace("/*__ROUTES__*/", routes);
113
-
114
- fs.writeFileSync(gatewayAppPath, finalAppContent);
115
- fs.writeFileSync(gatewayServerPath, gatewayServerContent);
116
-
117
- // Remove unnecessary files for gateway
118
- const routesPath = path.join(serviceRoot, `src/routes.${ext}`);
119
- const modulesPath = path.join(serviceRoot, "src/modules");
120
- const middlewaresPath = path.join(serviceRoot, "src/middlewares");
121
- const configPath = path.join(serviceRoot, `src/config`);
122
- const utilPath = path.join(serviceRoot, `src/utils`);
123
-
124
- if (fs.existsSync(routesPath)) fs.rmSync(routesPath);
125
- if (fs.existsSync(modulesPath)) fs.rmSync(modulesPath, { recursive: true });
126
- if (fs.existsSync(configPath)) fs.rmSync(configPath, { recursive: true });
127
- if (fs.existsSync(utilPath)) fs.rmSync(utilPath, { recursive: true });
128
- if (fs.existsSync(middlewaresPath))
129
- fs.rmSync(middlewaresPath, { recursive: true });
130
- } else {
131
- // Regular service setup (existing code)
132
- // Add features (only for monolith or health-service)
133
- if (res.projectType === "monolith" || serviceName === "health-service") {
134
- for (const f of res.features) {
135
- const feature = await import(`../../template/features/${f}/inject.js`);
136
- const featureImports = feature.getImports
137
- ? feature.getImports(res.language)
138
- : feature.imports;
139
- imports.push(featureImports);
140
- middlewares.push(feature.middleware);
141
- deps.push(...feature.deps);
142
- if (feature.devDeps && res.language === "typescript") {
143
- devDeps.push(...feature.devDeps);
144
- }
145
-
146
- // If the feature provides files for the selected language, write them
147
- const featureFiles = feature.getFiles
148
- ? feature.getFiles(res.language)
149
- : feature.files;
150
- if (featureFiles) {
151
- for (const file in featureFiles) {
152
- const filePath = file.replace(/\.ts$/, `.${ext}`);
153
- const fullPath = path.join(serviceRoot, filePath);
154
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
155
- fs.writeFileSync(fullPath, featureFiles[file]);
156
- }
157
- }
158
- }
159
- }
160
-
161
- // Add authentication (only for monolith or auth-service)
162
- if (shouldIncludeAuth && res.auth) {
163
- const baseAuth =
164
- await import("../../template/features/auth/base/inject.js");
165
- deps.push(...baseAuth.deps);
166
- if (baseAuth.devDeps && res.language === "typescript") {
167
- devDeps.push(...baseAuth.devDeps);
168
- }
169
-
170
- const authFiles = baseAuth.getFiles
171
- ? baseAuth.getFiles(res.language)
172
- : baseAuth.files;
173
- for (const file in authFiles) {
174
- const filePath = file.replace(/\.ts$/, `.${ext}`);
175
- const fullPath = path.join(serviceRoot, filePath);
176
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
177
- fs.writeFileSync(fullPath, authFiles[file]);
178
- }
179
-
180
- const algo = await prompts({
181
- type: "select",
182
- name: "hasher",
183
- message: `Password hashing method${
184
- serviceName ? ` for ${serviceName}` : ""
185
- }`,
186
- choices: [
187
- {
188
- title:
189
- process.platform === "win32"
190
- ? "bcrypt (recommended for Windows)"
191
- : "argon2 (recommended)",
192
- value: process.platform === "win32" ? "bcrypt" : "argon2",
193
- },
194
- {
195
- title:
196
- process.platform === "win32"
197
- ? "argon2 (requires build tools)"
198
- : "bcrypt",
199
- value: process.platform === "win32" ? "argon2" : "bcrypt",
200
- },
201
- ],
202
- });
203
-
204
- const hashFeature = await import(
205
- `../../template/features/auth/${algo.hasher}/inject.js`
206
- );
207
- deps.push(...hashFeature.deps);
208
- if (hashFeature.devDeps && res.language === "typescript") {
209
- devDeps.push(...hashFeature.devDeps);
210
- }
211
-
212
- const hashFiles = hashFeature.getFiles
213
- ? hashFeature.getFiles(res.language)
214
- : hashFeature.files;
215
- for (const file in hashFiles) {
216
- const filePath = file.replace(/\.ts$/, `.${ext}`);
217
- const fullPath = path.join(serviceRoot, filePath);
218
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
219
- const content = hashFiles[file];
220
- fs.writeFileSync(fullPath, content);
221
- }
222
-
223
- v1Imports.push(
224
- baseAuth.getImports
225
- ? baseAuth.getImports(res.language)
226
- : baseAuth.imports,
227
- );
228
- v1Routes.push(baseAuth.middleware);
229
- }
230
-
231
- // Update app file
232
- const appPath = path.join(serviceRoot, `src/app.${ext}`);
233
- let content = fs.readFileSync(appPath, "utf8");
234
- content = content.replace("/*__IMPORTS__*/", imports.join("\n"));
235
- content = content.replace("/*__MIDDLEWARE__*/", middlewares.join("\n"));
236
- fs.writeFileSync(appPath, content);
237
-
238
- // Update root endpoint middleware with project info
239
- const rootMiddlewarePath = path.join(
240
- serviceRoot,
241
- `src/middlewares/root.middleware.${ext}`,
242
- );
243
- if (fs.existsSync(rootMiddlewarePath)) {
244
- let rootContent = fs.readFileSync(rootMiddlewarePath, "utf8");
245
- rootContent = rootContent.replace(
246
- "/*__PROJECT_NAME__*/",
247
- serviceName || res.sanitizedName,
248
- );
249
- rootContent = rootContent.replace(
250
- "/*__PROJECT_TYPE__*/",
251
- res.projectType,
252
- );
253
-
254
- // Add auth endpoint if auth is enabled
255
- if (shouldIncludeAuth && res.auth) {
256
- rootContent = rootContent.replace(
257
- "/*__AUTH_ENDPOINT__*/",
258
- 'auth: "/api/v1/auth",',
259
- );
260
- } else {
261
- rootContent = rootContent.replace("/*__AUTH_ENDPOINT__*/", "");
262
- }
263
-
264
- fs.writeFileSync(rootMiddlewarePath, rootContent);
265
- }
266
-
267
- // Update v1 index file if needed
268
- if (v1Imports.length || v1Routes.length) {
269
- const v1IndexPath = path.join(serviceRoot, `src/modules/v1/index.${ext}`);
270
- let v1Content = fs.readFileSync(v1IndexPath, "utf8");
271
-
272
- const lastImportIndex = v1Content.lastIndexOf("import");
273
- const importEndIndex = v1Content.indexOf("\n", lastImportIndex) + 1;
274
- v1Content =
275
- v1Content.slice(0, importEndIndex) +
276
- v1Imports.join("\n") +
277
- "\n" +
278
- v1Content.slice(importEndIndex);
279
-
280
- const exportIndex = v1Content.lastIndexOf("export default");
281
- v1Content =
282
- v1Content.slice(0, exportIndex) +
283
- v1Routes.join("\n") +
284
- "\n\n" +
285
- v1Content.slice(exportIndex);
286
-
287
- fs.writeFileSync(v1IndexPath, v1Content);
288
- }
289
-
290
- // Update env file to conditionally include ALLOWED_ORIGIN and MONGO_URI
291
- const envPath = path.join(serviceRoot, `src/config/env.${ext}`);
292
- if (fs.existsSync(envPath)) {
293
- let envContent = fs.readFileSync(envPath, "utf8");
294
-
295
- // Import ENV in app.ts if CORS is selected
296
- if (res.features && res.features.includes("cors")) {
297
- let appContent = fs.readFileSync(appPath, "utf8");
298
- if (!appContent.includes("import { ENV } from")) {
299
- appContent = appContent.replace(
300
- "/*__IMPORTS__*/",
301
- "import { ENV } from '@/config';\n/*__IMPORTS__*/",
302
- );
303
- fs.writeFileSync(appPath, appContent);
304
- }
305
- }
306
-
307
- // Add ALLOWED_ORIGIN if CORS is selected
308
- if (res.features && res.features.includes("cors")) {
309
- const assertion = res.language === "javascript" ? "" : "!";
310
- envContent = envContent.replace(
311
- "/*__ALLOWED_ORIGIN__*/",
312
- `ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN${assertion},`,
313
- );
314
- } else {
315
- envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
316
- }
317
-
318
- // Add MONGO_URI and JWT_SECRET if auth is enabled
319
- if (shouldIncludeAuth && res.auth) {
320
- const assertion = res.language === "javascript" ? "" : "!";
321
- envContent = envContent.replace(
322
- "/*__MONGO_URI__*/",
323
- `MONGO_URI: process.env.MONGO_URI${assertion},`,
324
- );
325
- envContent = envContent.replace(
326
- "/*__JWT_SECRET__*/",
327
- `JWT_SECRET: process.env.JWT_SECRET${assertion},`,
328
- );
329
- } else {
330
- envContent = envContent.replace("/*__MONGO_URI__*/", "");
331
- envContent = envContent.replace("/*__JWT_SECRET__*/", "");
332
- }
333
-
334
- fs.writeFileSync(envPath, envContent);
335
- }
336
-
337
- // Update server file to connect to DB if auth is enabled
338
- const serverPath = path.join(serviceRoot, `src/server.${ext}`);
339
- if (fs.existsSync(serverPath)) {
340
- let serverContent = fs.readFileSync(serverPath, "utf8");
341
-
342
- if (shouldIncludeAuth && res.auth) {
343
- const language = res.language;
344
- if (language === "javascript") {
345
- serverContent = serverContent.replace(
346
- "/*__DB_IMPORT__*/",
347
- ", connectDB",
348
- );
349
- } else {
350
- serverContent = serverContent.replace(
351
- "/*__DB_IMPORT__*/",
352
- 'import { connectDB } from "./config";',
353
- );
354
- }
355
- serverContent = serverContent.replace(
356
- "/*__DB_CONNECT__*/",
357
- `// Connect to MongoDB\nawait connectDB();`,
358
- );
359
- } else {
360
- serverContent = serverContent.replace("/*__DB_IMPORT__*/", "");
361
- serverContent = serverContent.replace("/*__DB_CONNECT__*/", "");
362
- }
363
-
364
- fs.writeFileSync(serverPath, serverContent);
365
- }
366
-
367
- // Update .env.example to conditionally include environment variables (only for monolith)
368
- if (res.projectType !== "microservice") {
369
- const envExamplePath = path.join(serviceRoot, ".env.example");
370
- if (fs.existsSync(envExamplePath)) {
371
- let envExampleContent = fs.readFileSync(envExamplePath, "utf8");
372
-
373
- // Add ALLOWED_ORIGIN if CORS is selected
374
- if (res.features && res.features.includes("cors")) {
375
- envExampleContent = envExampleContent.replace(
376
- "/*__ALLOWED_ORIGIN_ENV__*/",
377
- "ALLOWED_ORIGIN=http://localhost:3000",
378
- );
379
- } else {
380
- envExampleContent = envExampleContent.replace(
381
- "/*__ALLOWED_ORIGIN_ENV__*/",
382
- "",
383
- );
384
- }
385
-
386
- // Add MONGO_URI and JWT_SECRET if auth is enabled
387
- if (shouldIncludeAuth && res.auth) {
388
- envExampleContent = envExampleContent.replace(
389
- "/*__MONGO_URI_ENV__*/",
390
- "MONGO_URI=mongodb://localhost:27017/your-database-name",
391
- );
392
- envExampleContent = envExampleContent.replace(
393
- "/*__JWT_SECRET_ENV__*/",
394
- "JWT_SECRET=your-super-secret-jwt-key-change-this-in-production",
395
- );
396
- } else {
397
- envExampleContent = envExampleContent.replace(
398
- "/*__MONGO_URI_ENV__*/",
399
- "",
400
- );
401
- envExampleContent = envExampleContent.replace(
402
- "/*__JWT_SECRET_ENV__*/",
403
- "",
404
- );
405
- }
406
-
407
- fs.writeFileSync(envExamplePath, envExampleContent);
408
- }
409
- }
410
- } // End of else block for non-gateway services
411
-
412
- // Update tsconfig.json for microservices to support @/ alias with shared folder
413
- // Also run when adding a service into an existing microservice project
414
- if (res.projectType === "microservice" || res.isInMicroserviceProject) {
415
- const tsconfigPath = path.join(serviceRoot, "tsconfig.json");
416
- let tsconfigContent = fs.readFileSync(tsconfigPath, "utf8");
417
-
418
- // Remove comments from JSON (strip-json-comments approach)
419
- tsconfigContent = tsconfigContent
420
- .replace(/\/\/.*$/gm, "") // Remove single-line comments
421
- .replace(/\/\*[\s\S]*?\*\//g, ""); // Remove multi-line comments
422
-
423
- const tsconfig = JSON.parse(tsconfigContent);
424
-
425
- // Update baseUrl to allow import from the shared folder
426
- tsconfig.compilerOptions.baseUrl = ".";
427
-
428
- // Update paths to include shared folder (works in both Docker and VS Code)
429
- tsconfig.compilerOptions.paths = {
430
- "@/*": ["src/*"],
431
- "@/shared/*": ["shared/*", "../../shared/*"],
432
- };
433
-
434
- // Remove rootDir restriction to allow imports from outside src/
435
- delete tsconfig.compilerOptions.rootDir;
436
-
437
- fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
438
-
439
- // Update imports in service files to use @/shared/* instead of @/* for utils/config
440
- // This applies to non-gateway services
441
- if (serviceName !== "gateway") {
442
- const healthControllerPath = path.join(
443
- serviceRoot,
444
- `src/modules/v1/health/health.controller.${ext}`,
445
- );
446
- if (fs.existsSync(healthControllerPath)) {
447
- let healthControllerContent = fs.readFileSync(
448
- healthControllerPath,
449
- "utf8",
450
- );
451
- healthControllerContent = healthControllerContent.replace(
452
- 'from "@/utils"',
453
- 'from "@/shared/utils"',
454
- );
455
- fs.writeFileSync(healthControllerPath, healthControllerContent);
456
- }
457
-
458
- // Update server.ts to use shared imports
459
- const serverPath = path.join(serviceRoot, `src/server.${ext}`);
460
- if (fs.existsSync(serverPath)) {
461
- // Determine a single port string for this specific service.
462
- // Gateway should use 4000; other services use 4001, 4002, ...
463
- let serverPort = "3000";
464
- if (Array.isArray(allServices) && allServices.length) {
465
- if (serviceName === "gateway") {
466
- serverPort = "4000";
467
- } else {
468
- const idx = allServices.indexOf(serviceName);
469
- if (idx !== -1) {
470
- // Count non-gateway services before this one to compute offset
471
- const nonGatewayBefore = allServices
472
- .slice(0, idx)
473
- .filter((s) => s !== "gateway").length;
474
- serverPort = `${4001 + nonGatewayBefore}`;
475
- } else {
476
- // Fallback: assign next available port after 4000
477
- serverPort = `${4001 + allServices.length - 1}`;
478
- }
479
- }
480
- }
481
-
482
- let serverContent = fs.readFileSync(serverPath, "utf8");
483
-
484
- // Normalize imports: accept @/ or relative imports and rewrite to shared imports
485
- serverContent = serverContent
486
- .replace(
487
- /from\s+["'](?:@\/utils|\.\/utils|\.\.\/utils)["']/g,
488
- 'from "@/shared/utils"',
489
- )
490
- .replace(
491
- /from\s+["'](?:@\/config|\.\/config|\.\.\/config)["']/g,
492
- 'from "@/shared/config"',
493
- );
494
-
495
- // Update PORT to use service-specific environment variable and a correct default port.
496
- const portEnvVar = `${serviceName.toUpperCase().replace(/-/g, "_")}_PORT`;
497
- const portRegex = /const\s+PORT\s*=\s*ENV\.PORT\s*\|\|\s*(\d+)\s*;/;
498
- if (portRegex.test(serverContent)) {
499
- serverContent = serverContent.replace(
500
- portRegex,
501
- `const PORT = ENV.${portEnvVar} || ${serverPort};`,
502
- );
503
- } else {
504
- // Fallback: replace a simple numeric default or a bare PORT assignment
505
- const simplePortRegex = /const\s+PORT\s*=\s*(\d+)\s*;/;
506
- if (simplePortRegex.test(serverContent)) {
507
- serverContent = serverContent.replace(
508
- simplePortRegex,
509
- `const PORT = ENV.${portEnvVar} || ${serverPort};`,
510
- );
511
- } else {
512
- // Last resort: append a PORT assignment near the top after imports
513
- const importEnd = serverContent.indexOf("\n\n");
514
- const insertPos = importEnd === -1 ? 0 : importEnd + 2;
515
- const portLine = `const PORT = ENV.${portEnvVar} || ${serverPort};\n\n`;
516
- serverContent =
517
- serverContent.slice(0, insertPos) +
518
- portLine +
519
- serverContent.slice(insertPos);
520
- }
521
- }
522
-
523
- fs.writeFileSync(serverPath, serverContent);
524
- }
525
- }
526
- }
527
-
528
- // Update package.json
529
- const packageJsonPath = path.join(serviceRoot, "package.json");
530
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
531
-
532
- // Create new package.json with name at the top
533
- const orderedPackageJson = {
534
- name: serviceName || res.sanitizedName,
535
- version: packageJson.version,
536
- description: res.description || packageJson.description,
537
- ...packageJson,
538
- };
539
-
540
- // Remove duplicate keys that were moved to the top
541
- delete orderedPackageJson.name;
542
- delete orderedPackageJson.version;
543
- delete orderedPackageJson.description;
544
-
545
- // Re-add them at the top in correct order
546
- const finalPackageJson = {
547
- name: serviceName || res.sanitizedName,
548
- version: packageJson.version,
549
- description: res.description || packageJson.description,
550
- ...orderedPackageJson,
551
- };
552
-
553
- // Add author if provided
554
- if (res.author) {
555
- finalPackageJson.author = res.author;
556
- }
557
-
558
- // Add keywords if provided
559
- if (res.keywords && res.keywords.trim()) {
560
- finalPackageJson.keywords = res.keywords
561
- .split(",")
562
- .map((k) => k.trim())
563
- .filter(Boolean);
564
- }
565
-
566
- // Add --poll flag to dev script for Docker mode (fixes watch mode in Docker on Windows)
567
- if (res.projectType === "microservice" && res.mode === "docker") {
568
- if (finalPackageJson.scripts && finalPackageJson.scripts.dev) {
569
- finalPackageJson.scripts.dev = finalPackageJson.scripts.dev.replace(
570
- "ts-node-dev --respawn --transpile-only",
571
- "ts-node-dev --respawn --transpile-only --poll",
572
- );
573
- }
574
- }
575
-
576
- // If creating microservices, do not install workspace-level devDependencies per service
577
- if (res.projectType === "microservice") {
578
- if (finalPackageJson.devDependencies) {
579
- const toRemove = [
580
- "prettier",
581
- "eslint",
582
- "eslint-config-prettier",
583
- "@typescript-eslint/eslint-plugin",
584
- "@typescript-eslint/parser",
585
- "husky",
586
- ];
587
- for (const dep of toRemove) {
588
- if (finalPackageJson.devDependencies[dep]) {
589
- delete finalPackageJson.devDependencies[dep];
590
- }
591
- }
592
-
593
- // Remove @types/* from JavaScript services; keep for TypeScript
594
- if (res.language === "javascript") {
595
- for (const key of Object.keys(finalPackageJson.devDependencies)) {
596
- if (key.startsWith("@types/"))
597
- delete finalPackageJson.devDependencies[key];
598
- }
599
- }
600
-
601
- // If devDependencies becomes empty, remove the field
602
- if (Object.keys(finalPackageJson.devDependencies).length === 0) {
603
- delete finalPackageJson.devDependencies;
604
- }
605
- }
606
- }
607
-
608
- // Remove per-service prepare script (which runs husky) for microservice workspaces
609
- if (res.projectType === "microservice" || res.isInMicroserviceProject) {
610
- if (finalPackageJson.scripts && finalPackageJson.scripts.prepare) {
611
- delete finalPackageJson.scripts.prepare;
612
- }
613
- // Also remove per-service lint/format/check-format scripts (workspace-level tooling lives at root)
614
- if (finalPackageJson.scripts) {
615
- delete finalPackageJson.scripts.lint;
616
- delete finalPackageJson.scripts.format;
617
- delete finalPackageJson.scripts["check-format"];
618
- // If scripts becomes empty, remove the field
619
- if (Object.keys(finalPackageJson.scripts).length === 0) {
620
- delete finalPackageJson.scripts;
621
- }
622
- }
623
- }
624
-
625
- fs.writeFileSync(
626
- packageJsonPath,
627
- JSON.stringify(finalPackageJson, null, 2) + "\n",
628
- );
629
-
630
- // Skip installation if skipInstall is true (will be done later in batch)
631
- if (skipInstall) {
632
- return { deps, devDeps, installSucceeded: true };
633
- }
634
-
635
- // Install dependencies
636
- console.log(
637
- pc.cyan(
638
- `\n📦 Installing dependencies for ${serviceName || "project"}...\n`,
639
- ),
640
- );
641
-
642
- let installSucceeded = false;
643
-
644
- try {
645
- if (deps.length) {
646
- execSync(`npm install ${deps.join(" ")}`, {
647
- cwd: serviceRoot,
648
- stdio: "inherit",
649
- });
650
- }
651
- if (devDeps.length) {
652
- execSync(`npm install -D ${devDeps.join(" ")}`, {
653
- cwd: serviceRoot,
654
- stdio: "inherit",
655
- });
656
- }
657
- execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
658
- installSucceeded = true;
659
-
660
- // Run format after successful install
661
- console.log(pc.cyan("\n🎨 Formatting code...\n"));
662
- try {
663
- execSync("npm run format", { cwd: serviceRoot, stdio: "inherit" });
664
- } catch (formatError) {
665
- console.warn(
666
- pc.yellow(
667
- "⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n",
668
- ),
669
- );
670
- }
671
- } catch (error) {
672
- console.error(pc.red("\n❌ Failed to install dependencies"));
673
- console.error(pc.dim(`\nYou can install them later by running:`));
674
- console.error(
675
- pc.cyan(` cd ${serviceName || res.sanitizedName} && npm install`),
676
- );
677
- console.error(pc.dim(" Then run: npm run format\n"));
678
-
679
- // Don't exit - let the project be created anyway
680
- console.log(pc.cyan("⏭️ Continuing with project creation...\n"));
681
- }
682
-
683
- return { deps, devDeps, installSucceeded };
684
- };
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import prompts from "prompts";
4
+ import pc from "picocolors";
5
+ import { execSync } from "child_process";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // Helper function to get the correct file extension (.ts or .js)
9
+ function getFileExtension(dir) {
10
+ // Check if .ts files exist, otherwise use .js
11
+ const sampleFiles = ["src/app.ts", "src/server.ts", "src/routes.ts"];
12
+ for (const file of sampleFiles) {
13
+ const tsPath = path.join(dir, file);
14
+ if (fs.existsSync(tsPath)) return "ts";
15
+ const jsPath = path.join(dir, file.replace(".ts", ".js"));
16
+ if (fs.existsSync(jsPath)) return "js";
17
+ }
18
+ return "ts"; // default to ts
19
+ }
20
+
21
+ export const setupService = async (
22
+ res,
23
+ serviceName,
24
+ serviceRoot,
25
+ shouldIncludeAuth,
26
+ allServices = [],
27
+ skipInstall = false,
28
+ ) => {
29
+ let imports = [];
30
+ let middlewares = [];
31
+ let deps = [];
32
+ let devDeps = [];
33
+ let v1Imports = [];
34
+ let v1Routes = [];
35
+
36
+ // Detect file extension (ts or js)
37
+ const ext = getFileExtension(serviceRoot);
38
+
39
+ // Remove workspace-level config files from service only in microservice mode (they should live at root)
40
+ // (monolith projects keep these files at project root)
41
+ if (res.projectType === "microservice" || res.isInMicroserviceProject) {
42
+ try {
43
+ const serviceConfigFiles = [
44
+ ".prettierrc",
45
+ ".prettierignore",
46
+ ".eslintrc.json",
47
+ "eslint.config.js",
48
+ ".husky",
49
+ "husky",
50
+ ];
51
+ for (const f of serviceConfigFiles) {
52
+ const p = path.join(serviceRoot, f);
53
+ if (fs.existsSync(p)) {
54
+ // Remove file or directory
55
+ const stat = fs.statSync(p);
56
+ if (stat.isDirectory())
57
+ fs.rmSync(p, { recursive: true, force: true });
58
+ else fs.rmSync(p, { force: true });
59
+ }
60
+ }
61
+ } catch (err) {
62
+ // Non-fatal
63
+ }
64
+ }
65
+
66
+ // Ensure service-level gitignore is renamed immediately after template copy
67
+ try {
68
+ const serviceGitignore = path.join(serviceRoot, "gitignore");
69
+ const serviceDotGitignore = path.join(serviceRoot, ".gitignore");
70
+ if (
71
+ fs.existsSync(serviceGitignore) &&
72
+ !fs.existsSync(serviceDotGitignore)
73
+ ) {
74
+ fs.renameSync(serviceGitignore, serviceDotGitignore);
75
+ }
76
+ } catch (err) {
77
+ // Non-fatal; continue setup
78
+ }
79
+
80
+ // Special handling for gateway service
81
+ if (serviceName === "gateway") {
82
+ const tmplLang = res.language === "javascript" ? "js" : "ts";
83
+ const gatewayModule = await import(
84
+ `../../template/gateway/${tmplLang}/inject.js`
85
+ );
86
+
87
+ deps.push(...gatewayModule.gatewayDeps);
88
+
89
+ // Copy gateway-specific files
90
+ const gatewayAppPath = path.join(serviceRoot, `src/app.${ext}`);
91
+ const gatewayServerPath = path.join(serviceRoot, `src/server.${ext}`);
92
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
93
+
94
+ // Read gateway template files according to selected language
95
+ const templateExt = res.language === "javascript" ? ".js" : ".ts";
96
+ const templateDir = path.join(
97
+ __dirname,
98
+ `../../template/gateway/${tmplLang}`,
99
+ );
100
+ const gatewayAppContent = fs.readFileSync(
101
+ path.join(templateDir, `app${templateExt}`),
102
+ "utf8",
103
+ );
104
+ const gatewayServerContent = fs.readFileSync(
105
+ path.join(templateDir, `server${templateExt}`),
106
+ "utf8",
107
+ );
108
+
109
+ // Generate routes for all services with mode (docker or nodocker)
110
+ const mode = res.mode || "docker";
111
+ const routes = gatewayModule.generateGatewayRoutes(allServices, mode);
112
+ const finalAppContent = gatewayAppContent.replace("/*__ROUTES__*/", routes);
113
+
114
+ fs.writeFileSync(gatewayAppPath, finalAppContent);
115
+ fs.writeFileSync(gatewayServerPath, gatewayServerContent);
116
+
117
+ // Remove unnecessary files for gateway
118
+ const routesPath = path.join(serviceRoot, `src/routes.${ext}`);
119
+ const modulesPath = path.join(serviceRoot, "src/modules");
120
+ const middlewaresPath = path.join(serviceRoot, "src/middlewares");
121
+ const configPath = path.join(serviceRoot, `src/config`);
122
+ const utilPath = path.join(serviceRoot, `src/utils`);
123
+
124
+ if (fs.existsSync(routesPath)) fs.rmSync(routesPath);
125
+ if (fs.existsSync(modulesPath)) fs.rmSync(modulesPath, { recursive: true });
126
+ if (fs.existsSync(configPath)) fs.rmSync(configPath, { recursive: true });
127
+ if (fs.existsSync(utilPath)) fs.rmSync(utilPath, { recursive: true });
128
+ if (fs.existsSync(middlewaresPath))
129
+ fs.rmSync(middlewaresPath, { recursive: true });
130
+ } else {
131
+ // Regular service setup (existing code)
132
+ // Add Swagger UI as runtime dependency (used by routes for /api-docs)
133
+ deps.push("swagger-ui-express");
134
+ if (res.language === "typescript") {
135
+ devDeps.push("@types/swagger-ui-express");
136
+ }
137
+
138
+ // Add Zod only when validation is enabled
139
+ if (res.validation) {
140
+ deps.push("zod");
141
+ }
142
+
143
+ // Add features (only for monolith or health-service)
144
+ if (res.projectType === "monolith" || serviceName === "health-service") {
145
+ for (const f of res.features) {
146
+ const feature = await import(`../../template/features/${f}/inject.js`);
147
+ const featureImports = feature.getImports
148
+ ? feature.getImports(res.language)
149
+ : feature.imports;
150
+ imports.push(featureImports);
151
+ middlewares.push(feature.middleware);
152
+ deps.push(...feature.deps);
153
+ if (feature.devDeps && res.language === "typescript") {
154
+ devDeps.push(...feature.devDeps);
155
+ }
156
+
157
+ // If the feature provides files for the selected language, write them
158
+ const featureFiles = feature.getFiles
159
+ ? feature.getFiles(res.language)
160
+ : feature.files;
161
+ if (featureFiles) {
162
+ for (const file in featureFiles) {
163
+ const filePath = file.replace(/\.ts$/, `.${ext}`);
164
+ const fullPath = path.join(serviceRoot, filePath);
165
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
166
+ fs.writeFileSync(fullPath, featureFiles[file]);
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // Add authentication (only for monolith or auth-service)
173
+ if (shouldIncludeAuth && res.auth) {
174
+ const baseAuth = await import(
175
+ "../../template/features/auth/base/inject.js"
176
+ );
177
+ deps.push(...baseAuth.deps);
178
+ if (baseAuth.devDeps && res.language === "typescript") {
179
+ devDeps.push(...baseAuth.devDeps);
180
+ }
181
+
182
+ const authFiles = baseAuth.getFiles
183
+ ? baseAuth.getFiles(res.language)
184
+ : baseAuth.files;
185
+ for (const file in authFiles) {
186
+ const filePath = file.replace(/\.ts$/, `.${ext}`);
187
+ const fullPath = path.join(serviceRoot, filePath);
188
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
189
+ fs.writeFileSync(fullPath, authFiles[file]);
190
+ }
191
+
192
+ const algo = await prompts({
193
+ type: "select",
194
+ name: "hasher",
195
+ message: `Password hashing method${
196
+ serviceName ? ` for ${serviceName}` : ""
197
+ }`,
198
+ choices: [
199
+ {
200
+ title:
201
+ process.platform === "win32"
202
+ ? "bcrypt (recommended for Windows)"
203
+ : "argon2 (recommended)",
204
+ value: process.platform === "win32" ? "bcrypt" : "argon2",
205
+ },
206
+ {
207
+ title:
208
+ process.platform === "win32"
209
+ ? "argon2 (requires build tools)"
210
+ : "bcrypt",
211
+ value: process.platform === "win32" ? "argon2" : "bcrypt",
212
+ },
213
+ ],
214
+ });
215
+
216
+ const hashFeature = await import(
217
+ `../../template/features/auth/${algo.hasher}/inject.js`
218
+ );
219
+ deps.push(...hashFeature.deps);
220
+ if (hashFeature.devDeps && res.language === "typescript") {
221
+ devDeps.push(...hashFeature.devDeps);
222
+ }
223
+
224
+ const hashFiles = hashFeature.getFiles
225
+ ? hashFeature.getFiles(res.language)
226
+ : hashFeature.files;
227
+ for (const file in hashFiles) {
228
+ const filePath = file.replace(/\.ts$/, `.${ext}`);
229
+ const fullPath = path.join(serviceRoot, filePath);
230
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
231
+ const content = hashFiles[file];
232
+ fs.writeFileSync(fullPath, content);
233
+ }
234
+
235
+ v1Imports.push(
236
+ baseAuth.getImports
237
+ ? baseAuth.getImports(res.language)
238
+ : baseAuth.imports,
239
+ );
240
+ v1Routes.push(baseAuth.middleware);
241
+ }
242
+
243
+ // Update health route to remove validation if not enabled
244
+ if (!res.validation) {
245
+ const healthRoutePath = path.join(
246
+ serviceRoot,
247
+ `src/modules/v1/health/health.route.${ext}`,
248
+ );
249
+ if (fs.existsSync(healthRoutePath)) {
250
+ let healthContent = fs.readFileSync(healthRoutePath, "utf8");
251
+ // Remove validation-related imports, schema definitions and usages more robustly
252
+ // Remove z import, validateRequest import, any const <name> = z... schema blocks,
253
+ // and usages like ", validateRequest({ query: <name> })".
254
+ if (ext === "ts") {
255
+ // Remove validateRequest from middlewares import, keep methodNotAllowedHandler
256
+ healthContent = healthContent.replace(
257
+ /import\s+\{([\s\S]*?)\}\s+from\s+["']@\/middlewares["'];?/m,
258
+ (m, p1) => {
259
+ const parts = p1
260
+ .split(",")
261
+ .map((s) => s.trim())
262
+ .filter(Boolean)
263
+ .filter((name) => name !== "validateRequest");
264
+ return `import { ${parts.join(", ")} } from "@/middlewares";\n`;
265
+ },
266
+ );
267
+
268
+ // Remove any import of z from zod
269
+ healthContent = healthContent.replace(
270
+ /import\s+\{\s*z\s*\}\s+from\s+["']zod["'];?\s*/g,
271
+ "",
272
+ );
273
+
274
+ // Remove any const <identifier> = z...; schema blocks (non-greedy)
275
+ // Be tolerant of CRLF (Windows) and LF line endings
276
+ healthContent = healthContent.replace(
277
+ /const\s+\w+\s*=\s*z[\s\S]*?;\s*(?:\r?\n){2,}/gm,
278
+ "",
279
+ );
280
+
281
+ // Remove validateRequest usage patterns
282
+ healthContent = healthContent.replace(
283
+ /,\s*validateRequest\s*\(\{[\s\S]*?\}\)/g,
284
+ "",
285
+ );
286
+ } else {
287
+ // JS: remove require('zod') and adjust middleware require
288
+ healthContent = healthContent.replace(
289
+ /const\s+\{\s*z\s*\}\s*=\s*require\(['\"]zod['\"]\);?\s*/g,
290
+ "",
291
+ );
292
+
293
+ healthContent = healthContent.replace(
294
+ /const\s+\{([\s\S]*?)\}\s*=\s*require\(['\"][\s\S]*?middlewares['\"]\);?/m,
295
+ (m, p1) => {
296
+ const parts = p1
297
+ .split(",")
298
+ .map((s) => s.trim())
299
+ .filter(Boolean)
300
+ .filter((name) => name !== "validateRequest");
301
+ return `const { ${parts.join(
302
+ ", ",
303
+ )} } = require('../../../middlewares');`;
304
+ },
305
+ );
306
+
307
+ // Remove schema const blocks and validateRequest usages
308
+ healthContent = healthContent.replace(
309
+ /const\s+\w+\s*=\s*z[\s\S]*?;\s*\n\n/gm,
310
+ "",
311
+ );
312
+ healthContent = healthContent.replace(
313
+ /,\s*validateRequest\s*\(\{[\s\S]*?\}\)/g,
314
+ "",
315
+ );
316
+ }
317
+ fs.writeFileSync(healthRoutePath, healthContent);
318
+ }
319
+ }
320
+
321
+ const baseDir = path.dirname(fileURLToPath(import.meta.url));
322
+
323
+ // Replace health controller with auth version if auth is enabled
324
+ if (shouldIncludeAuth && res.auth) {
325
+ const baseDir = path.dirname(fileURLToPath(import.meta.url));
326
+ const healthControllerSourcePath = path.join(
327
+ baseDir,
328
+ `../../template/base/${ext}/src/modules/v1/health/health.controller.auth.${ext}`,
329
+ );
330
+ const healthControllerTargetPath = path.join(
331
+ serviceRoot,
332
+ `src/modules/v1/health/health.controller.${ext}`,
333
+ );
334
+
335
+ if (fs.existsSync(healthControllerSourcePath)) {
336
+ const healthControllerContent = fs.readFileSync(
337
+ healthControllerSourcePath,
338
+ "utf8",
339
+ );
340
+ fs.writeFileSync(healthControllerTargetPath, healthControllerContent);
341
+ }
342
+ } else {
343
+ const healthControllerSourcePath = path.join(
344
+ baseDir,
345
+ `../../template/base/${ext}/src/modules/v1/health/health.controller.${ext}`,
346
+ );
347
+ const healthControllerTargetPath = path.join(
348
+ serviceRoot,
349
+ `src/modules/v1/health/health.controller.${ext}`,
350
+ );
351
+
352
+ if (fs.existsSync(healthControllerSourcePath)) {
353
+ const healthControllerContent = fs.readFileSync(
354
+ healthControllerSourcePath,
355
+ "utf8",
356
+ );
357
+ fs.writeFileSync(healthControllerTargetPath, healthControllerContent);
358
+ }
359
+ }
360
+
361
+ // Remove the auth-aware health controller when auth is disabled so TypeScript
362
+ // does not try to compile a mongoose-dependent file in non-auth projects.
363
+ if (!res.auth) {
364
+ const authHealthControllerPath = path.join(
365
+ serviceRoot,
366
+ `src/modules/v1/health/health.controller.auth.${ext}`,
367
+ );
368
+ if (fs.existsSync(authHealthControllerPath)) {
369
+ fs.rmSync(authHealthControllerPath, { force: true });
370
+ }
371
+ }
372
+
373
+ // Update auth routes to remove validation if not enabled
374
+ if (!res.validation && shouldIncludeAuth && res.auth) {
375
+ const authRoutePath = path.join(
376
+ serviceRoot,
377
+ `src/modules/v1/auth/auth.routes.${ext}`,
378
+ );
379
+ if (fs.existsSync(authRoutePath)) {
380
+ let authContent = fs.readFileSync(authRoutePath, "utf8");
381
+ // Remove validation-related imports, zod schema blocks and usages robustly
382
+ if (ext === "ts") {
383
+ authContent = authContent.replace(
384
+ /import\s+\{([\s\S]*?)\}\s+from\s+["']@\/middlewares["'];?/m,
385
+ (m, p1) => {
386
+ const parts = p1
387
+ .split(",")
388
+ .map((s) => s.trim())
389
+ .filter(Boolean)
390
+ .filter((name) => name !== "validateRequest");
391
+ return parts.length
392
+ ? `import { ${parts.join(", ")} } from "@/middlewares";\n`
393
+ : "";
394
+ },
395
+ );
396
+
397
+ authContent = authContent.replace(
398
+ /import\s+\{\s*z\s*\}\s+from\s+["']zod["'];?\s*/g,
399
+ "",
400
+ );
401
+ authContent = authContent.replace(
402
+ /const\s+\w+Schema\s*=\s*z[\s\S]*?;\s*(?:\r?\n){2,}/gm,
403
+ "",
404
+ );
405
+ authContent = authContent.replace(
406
+ /,\s*validateRequest\s*\(\{[\s\S]*?\}\)/g,
407
+ "",
408
+ );
409
+ } else {
410
+ authContent = authContent.replace(
411
+ /const\s+\{\s*z\s*\}\s*=\s*require\(['\"]zod['\"]\);?\s*/g,
412
+ "",
413
+ );
414
+
415
+ authContent = authContent.replace(
416
+ /const\s+\{([\s\S]*?)\}\s*=\s*require\(['\"](?:\.\.\/)*middlewares['\"]\);?/m,
417
+ (m, p1) => {
418
+ const parts = p1
419
+ .split(",")
420
+ .map((s) => s.trim())
421
+ .filter(Boolean)
422
+ .filter((name) => name !== "validateRequest");
423
+ return parts.length
424
+ ? `const { ${parts.join(
425
+ ", ",
426
+ )} } = require('../../../middlewares');`
427
+ : "const { methodNotAllowedHandler } = require('../../../middlewares');";
428
+ },
429
+ );
430
+
431
+ authContent = authContent.replace(
432
+ /const\s+\w+Schema\s*=\s*z[\s\S]*?;\s*(?:\r?\n){2,}/gm,
433
+ "",
434
+ );
435
+ authContent = authContent.replace(
436
+ /,\s*validateRequest\s*\(\{[\s\S]*?\}\)/g,
437
+ "",
438
+ );
439
+ }
440
+ fs.writeFileSync(authRoutePath, authContent);
441
+ }
442
+ }
443
+
444
+ // OpenAPI is generated at runtime from routeRegistry; static spec patching removed
445
+
446
+ // Update app file
447
+ const appPath = path.join(serviceRoot, `src/app.${ext}`);
448
+ let content = fs.readFileSync(appPath, "utf8");
449
+ content = content.replace("/*__IMPORTS__*/", imports.join("\n"));
450
+ content = content.replace("/*__MIDDLEWARE__*/", middlewares.join("\n"));
451
+ fs.writeFileSync(appPath, content);
452
+
453
+ // Update root endpoint middleware with project info
454
+ const rootMiddlewarePath = path.join(
455
+ serviceRoot,
456
+ `src/middlewares/root.middleware.${ext}`,
457
+ );
458
+ if (fs.existsSync(rootMiddlewarePath)) {
459
+ let rootContent = fs.readFileSync(rootMiddlewarePath, "utf8");
460
+ rootContent = rootContent.replace(
461
+ "/*__PROJECT_NAME__*/",
462
+ serviceName || res.sanitizedName,
463
+ );
464
+ rootContent = rootContent.replace(
465
+ "/*__PROJECT_TYPE__*/",
466
+ res.projectType,
467
+ );
468
+
469
+ // Add auth endpoint if auth is enabled
470
+ if (shouldIncludeAuth && res.auth) {
471
+ rootContent = rootContent.replace(
472
+ "/*__AUTH_ENDPOINT__*/",
473
+ 'auth: "/api/v1/auth",',
474
+ );
475
+ } else {
476
+ rootContent = rootContent.replace("/*__AUTH_ENDPOINT__*/", "");
477
+ }
478
+
479
+ fs.writeFileSync(rootMiddlewarePath, rootContent);
480
+ }
481
+
482
+ // Update v1 index file if needed
483
+ if (v1Imports.length || v1Routes.length) {
484
+ const v1IndexPath = path.join(serviceRoot, `src/modules/v1/index.${ext}`);
485
+ let v1Content = fs.readFileSync(v1IndexPath, "utf8");
486
+
487
+ const lastImportIndex = v1Content.lastIndexOf("import");
488
+ const importEndIndex = v1Content.indexOf("\n", lastImportIndex) + 1;
489
+ v1Content =
490
+ v1Content.slice(0, importEndIndex) +
491
+ v1Imports.join("\n") +
492
+ "\n" +
493
+ v1Content.slice(importEndIndex);
494
+
495
+ const exportIndex = v1Content.lastIndexOf("export default");
496
+ v1Content =
497
+ v1Content.slice(0, exportIndex) +
498
+ v1Routes.join("\n") +
499
+ "\n\n" +
500
+ v1Content.slice(exportIndex);
501
+
502
+ fs.writeFileSync(v1IndexPath, v1Content);
503
+ }
504
+
505
+ // Update env file to conditionally include ALLOWED_ORIGIN and MONGO_URI
506
+ const envPath = path.join(serviceRoot, `src/config/env.${ext}`);
507
+ if (fs.existsSync(envPath)) {
508
+ let envContent = fs.readFileSync(envPath, "utf8");
509
+
510
+ // Import ENV in app.ts if CORS is selected
511
+ if (res.features && res.features.includes("cors")) {
512
+ let appContent = fs.readFileSync(appPath, "utf8");
513
+ if (!appContent.includes("import { ENV } from")) {
514
+ appContent = appContent.replace(
515
+ "/*__IMPORTS__*/",
516
+ "import { ENV } from '@/config';\n/*__IMPORTS__*/",
517
+ );
518
+ fs.writeFileSync(appPath, appContent);
519
+ }
520
+ }
521
+
522
+ // Add ALLOWED_ORIGIN if CORS is selected
523
+ if (res.features && res.features.includes("cors")) {
524
+ const assertion = res.language === "javascript" ? "" : "!";
525
+ envContent = envContent.replace(
526
+ "/*__ALLOWED_ORIGIN__*/",
527
+ `ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN${assertion},`,
528
+ );
529
+ } else {
530
+ envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
531
+ }
532
+
533
+ // Add MONGO_URI and JWT_SECRET if auth is enabled
534
+ if (shouldIncludeAuth && res.auth) {
535
+ const assertion = res.language === "javascript" ? "" : "!";
536
+ envContent = envContent.replace(
537
+ "/*__MONGO_URI__*/",
538
+ `MONGO_URI: process.env.MONGO_URI${assertion},`,
539
+ );
540
+ envContent = envContent.replace(
541
+ "/*__JWT_SECRET__*/",
542
+ `JWT_SECRET: process.env.JWT_SECRET${assertion},`,
543
+ );
544
+ } else {
545
+ envContent = envContent.replace("/*__MONGO_URI__*/", "");
546
+ envContent = envContent.replace("/*__JWT_SECRET__*/", "");
547
+ }
548
+
549
+ fs.writeFileSync(envPath, envContent);
550
+ }
551
+
552
+ // Update server file to connect to DB if auth is enabled
553
+ const serverPath = path.join(serviceRoot, `src/server.${ext}`);
554
+ if (fs.existsSync(serverPath)) {
555
+ let serverContent = fs.readFileSync(serverPath, "utf8");
556
+
557
+ if (shouldIncludeAuth && res.auth) {
558
+ const language = res.language;
559
+ if (language === "javascript") {
560
+ serverContent = serverContent.replace(
561
+ "/*__DB_IMPORT__*/",
562
+ ", connectDB",
563
+ );
564
+ } else {
565
+ serverContent = serverContent.replace(
566
+ "/*__DB_IMPORT__*/",
567
+ 'import { connectDB } from "./config";',
568
+ );
569
+ }
570
+ serverContent = serverContent.replace(
571
+ "/*__DB_CONNECT__*/",
572
+ `// Connect to MongoDB\nawait connectDB();`,
573
+ );
574
+ } else {
575
+ serverContent = serverContent.replace("/*__DB_IMPORT__*/", "");
576
+ serverContent = serverContent.replace("/*__DB_CONNECT__*/", "");
577
+ }
578
+
579
+ fs.writeFileSync(serverPath, serverContent);
580
+ }
581
+
582
+ // Update .env.example to conditionally include environment variables (only for monolith)
583
+ if (res.projectType !== "microservice") {
584
+ const envExamplePath = path.join(serviceRoot, ".env.example");
585
+ if (fs.existsSync(envExamplePath)) {
586
+ let envExampleContent = fs.readFileSync(envExamplePath, "utf8");
587
+
588
+ // Add ALLOWED_ORIGIN if CORS is selected
589
+ if (res.features && res.features.includes("cors")) {
590
+ envExampleContent = envExampleContent.replace(
591
+ "/*__ALLOWED_ORIGIN_ENV__*/",
592
+ "ALLOWED_ORIGIN=http://localhost:3000",
593
+ );
594
+ } else {
595
+ envExampleContent = envExampleContent.replace(
596
+ "/*__ALLOWED_ORIGIN_ENV__*/",
597
+ "",
598
+ );
599
+ }
600
+
601
+ // Add MONGO_URI and JWT_SECRET if auth is enabled
602
+ if (shouldIncludeAuth && res.auth) {
603
+ envExampleContent = envExampleContent.replace(
604
+ "/*__MONGO_URI_ENV__*/",
605
+ "MONGO_URI=mongodb://localhost:27017/your-database-name",
606
+ );
607
+ envExampleContent = envExampleContent.replace(
608
+ "/*__JWT_SECRET_ENV__*/",
609
+ "JWT_SECRET=your-super-secret-jwt-key-change-this-in-production",
610
+ );
611
+ } else {
612
+ envExampleContent = envExampleContent.replace(
613
+ "/*__MONGO_URI_ENV__*/",
614
+ "",
615
+ );
616
+ envExampleContent = envExampleContent.replace(
617
+ "/*__JWT_SECRET_ENV__*/",
618
+ "",
619
+ );
620
+ }
621
+
622
+ fs.writeFileSync(envExamplePath, envExampleContent);
623
+ }
624
+ }
625
+ } // End of else block for non-gateway services
626
+
627
+ // Update tsconfig.json for microservices to support @/ alias with shared folder
628
+ // Also run when adding a service into an existing microservice project
629
+ if (res.projectType === "microservice" || res.isInMicroserviceProject) {
630
+ const tsconfigPath = path.join(serviceRoot, "tsconfig.json");
631
+ let tsconfigContent = fs.readFileSync(tsconfigPath, "utf8");
632
+
633
+ // Remove comments from JSON (strip-json-comments approach)
634
+ tsconfigContent = tsconfigContent
635
+ .replace(/\/\/.*$/gm, "") // Remove single-line comments
636
+ .replace(/\/\*[\s\S]*?\*\//g, ""); // Remove multi-line comments
637
+
638
+ const tsconfig = JSON.parse(tsconfigContent);
639
+
640
+ // Update baseUrl to allow import from the shared folder
641
+ tsconfig.compilerOptions.baseUrl = ".";
642
+
643
+ // Update paths to include shared folder (works in both Docker and VS Code)
644
+ tsconfig.compilerOptions.paths = {
645
+ "@/*": ["src/*"],
646
+ "@/shared/*": ["shared/*", "../../shared/*"],
647
+ };
648
+
649
+ // Remove rootDir restriction to allow imports from outside src/
650
+ delete tsconfig.compilerOptions.rootDir;
651
+
652
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
653
+
654
+ // Update imports in service files to use @/shared/* instead of @/* for utils/config
655
+ // This applies to non-gateway services
656
+ if (serviceName !== "gateway") {
657
+ const healthControllerPath = path.join(
658
+ serviceRoot,
659
+ `src/modules/v1/health/health.controller.${ext}`,
660
+ );
661
+ if (fs.existsSync(healthControllerPath)) {
662
+ let healthControllerContent = fs.readFileSync(
663
+ healthControllerPath,
664
+ "utf8",
665
+ );
666
+ healthControllerContent = healthControllerContent.replace(
667
+ 'from "@/utils"',
668
+ 'from "@/shared/utils"',
669
+ );
670
+ fs.writeFileSync(healthControllerPath, healthControllerContent);
671
+ }
672
+
673
+ // Update server.ts to use shared imports
674
+ const serverPath = path.join(serviceRoot, `src/server.${ext}`);
675
+ if (fs.existsSync(serverPath)) {
676
+ // Determine a single port string for this specific service.
677
+ // Gateway should use 4000; other services use 4001, 4002, ...
678
+ let serverPort = "3000";
679
+ if (Array.isArray(allServices) && allServices.length) {
680
+ if (serviceName === "gateway") {
681
+ serverPort = "4000";
682
+ } else {
683
+ const idx = allServices.indexOf(serviceName);
684
+ if (idx !== -1) {
685
+ // Count non-gateway services before this one to compute offset
686
+ const nonGatewayBefore = allServices
687
+ .slice(0, idx)
688
+ .filter((s) => s !== "gateway").length;
689
+ serverPort = `${4001 + nonGatewayBefore}`;
690
+ } else {
691
+ // Fallback: assign next available port after 4000
692
+ serverPort = `${4001 + allServices.length - 1}`;
693
+ }
694
+ }
695
+ }
696
+
697
+ let serverContent = fs.readFileSync(serverPath, "utf8");
698
+
699
+ // Normalize imports: accept @/ or relative imports and rewrite to shared imports
700
+ serverContent = serverContent
701
+ .replace(
702
+ /from\s+["'](?:@\/utils|\.\/utils|\.\.\/utils)["']/g,
703
+ 'from "@/shared/utils"',
704
+ )
705
+ .replace(
706
+ /from\s+["'](?:@\/config|\.\/config|\.\.\/config)["']/g,
707
+ 'from "@/shared/config"',
708
+ );
709
+
710
+ // Update PORT to use service-specific environment variable and a correct default port.
711
+ const portEnvVar = `${serviceName
712
+ .toUpperCase()
713
+ .replace(/-/g, "_")}_PORT`;
714
+ const portRegex = /const\s+PORT\s*=\s*ENV\.PORT\s*\|\|\s*(\d+)\s*;/;
715
+ if (portRegex.test(serverContent)) {
716
+ serverContent = serverContent.replace(
717
+ portRegex,
718
+ `const PORT = ENV.${portEnvVar} || ${serverPort};`,
719
+ );
720
+ } else {
721
+ // Fallback: replace a simple numeric default or a bare PORT assignment
722
+ const simplePortRegex = /const\s+PORT\s*=\s*(\d+)\s*;/;
723
+ if (simplePortRegex.test(serverContent)) {
724
+ serverContent = serverContent.replace(
725
+ simplePortRegex,
726
+ `const PORT = ENV.${portEnvVar} || ${serverPort};`,
727
+ );
728
+ } else {
729
+ // Last resort: append a PORT assignment near the top after imports
730
+ const importEnd = serverContent.indexOf("\n\n");
731
+ const insertPos = importEnd === -1 ? 0 : importEnd + 2;
732
+ const portLine = `const PORT = ENV.${portEnvVar} || ${serverPort};\n\n`;
733
+ serverContent =
734
+ serverContent.slice(0, insertPos) +
735
+ portLine +
736
+ serverContent.slice(insertPos);
737
+ }
738
+ }
739
+
740
+ fs.writeFileSync(serverPath, serverContent);
741
+ }
742
+ }
743
+ }
744
+
745
+ // Update package.json
746
+ const packageJsonPath = path.join(serviceRoot, "package.json");
747
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
748
+
749
+ // Create new package.json with name at the top
750
+ const orderedPackageJson = {
751
+ name: serviceName || res.sanitizedName,
752
+ version: packageJson.version,
753
+ description: res.description || packageJson.description,
754
+ ...packageJson,
755
+ };
756
+
757
+ // Remove duplicate keys that were moved to the top
758
+ delete orderedPackageJson.name;
759
+ delete orderedPackageJson.version;
760
+ delete orderedPackageJson.description;
761
+
762
+ // Re-add them at the top in correct order
763
+ const finalPackageJson = {
764
+ name: serviceName || res.sanitizedName,
765
+ version: packageJson.version,
766
+ description: res.description || packageJson.description,
767
+ ...orderedPackageJson,
768
+ };
769
+
770
+ // Add author if provided
771
+ if (res.author) {
772
+ finalPackageJson.author = res.author;
773
+ }
774
+
775
+ // Add keywords if provided
776
+ if (res.keywords && res.keywords.trim()) {
777
+ finalPackageJson.keywords = res.keywords
778
+ .split(",")
779
+ .map((k) => k.trim())
780
+ .filter(Boolean);
781
+ }
782
+
783
+ // Add --poll flag to dev script for Docker mode (fixes watch mode in Docker on Windows)
784
+ if (res.projectType === "microservice" && res.mode === "docker") {
785
+ if (finalPackageJson.scripts && finalPackageJson.scripts.dev) {
786
+ finalPackageJson.scripts.dev = finalPackageJson.scripts.dev.replace(
787
+ "ts-node-dev --respawn --transpile-only",
788
+ "ts-node-dev --respawn --transpile-only --poll",
789
+ );
790
+ }
791
+ }
792
+
793
+ // If creating microservices, do not install workspace-level devDependencies per service
794
+ if (res.projectType === "microservice") {
795
+ if (finalPackageJson.devDependencies) {
796
+ const toRemove = [
797
+ "prettier",
798
+ "eslint",
799
+ "eslint-config-prettier",
800
+ "@typescript-eslint/eslint-plugin",
801
+ "@typescript-eslint/parser",
802
+ "husky",
803
+ ];
804
+ for (const dep of toRemove) {
805
+ if (finalPackageJson.devDependencies[dep]) {
806
+ delete finalPackageJson.devDependencies[dep];
807
+ }
808
+ }
809
+
810
+ // Remove @types/* from JavaScript services; keep for TypeScript
811
+ if (res.language === "javascript") {
812
+ for (const key of Object.keys(finalPackageJson.devDependencies)) {
813
+ if (key.startsWith("@types/"))
814
+ delete finalPackageJson.devDependencies[key];
815
+ }
816
+ }
817
+
818
+ // If devDependencies becomes empty, remove the field
819
+ if (Object.keys(finalPackageJson.devDependencies).length === 0) {
820
+ delete finalPackageJson.devDependencies;
821
+ }
822
+ }
823
+ }
824
+
825
+ // Remove per-service prepare script (which runs husky) for microservice workspaces
826
+ if (res.projectType === "microservice" || res.isInMicroserviceProject) {
827
+ if (finalPackageJson.scripts && finalPackageJson.scripts.prepare) {
828
+ delete finalPackageJson.scripts.prepare;
829
+ }
830
+ // Also remove per-service lint/format/check-format scripts (workspace-level tooling lives at root)
831
+ if (finalPackageJson.scripts) {
832
+ delete finalPackageJson.scripts.lint;
833
+ delete finalPackageJson.scripts.format;
834
+ delete finalPackageJson.scripts["check-format"];
835
+ // If scripts becomes empty, remove the field
836
+ if (Object.keys(finalPackageJson.scripts).length === 0) {
837
+ delete finalPackageJson.scripts;
838
+ }
839
+ }
840
+ }
841
+
842
+ fs.writeFileSync(
843
+ packageJsonPath,
844
+ JSON.stringify(finalPackageJson, null, 2) + "\n",
845
+ );
846
+
847
+ // Skip installation if skipInstall is true (will be done later in batch)
848
+ if (skipInstall) {
849
+ return { deps, devDeps, installSucceeded: true };
850
+ }
851
+
852
+ // Install dependencies
853
+ console.log(
854
+ pc.cyan(
855
+ `\n📦 Installing dependencies for ${serviceName || "project"}...\n`,
856
+ ),
857
+ );
858
+
859
+ let installSucceeded = false;
860
+
861
+ try {
862
+ if (deps.length) {
863
+ execSync(`npm install ${deps.join(" ")}`, {
864
+ cwd: serviceRoot,
865
+ stdio: "inherit",
866
+ });
867
+ }
868
+ if (devDeps.length) {
869
+ execSync(`npm install -D ${devDeps.join(" ")}`, {
870
+ cwd: serviceRoot,
871
+ stdio: "inherit",
872
+ });
873
+ }
874
+ execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
875
+ installSucceeded = true;
876
+
877
+ // Run format after successful install
878
+ console.log(pc.cyan("\n🎨 Formatting code...\n"));
879
+ try {
880
+ execSync("npm run format", { cwd: serviceRoot, stdio: "inherit" });
881
+ } catch (formatError) {
882
+ console.warn(
883
+ pc.yellow(
884
+ "⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n",
885
+ ),
886
+ );
887
+ }
888
+ } catch (error) {
889
+ console.error(pc.red("\n❌ Failed to install dependencies"));
890
+ console.error(pc.dim(`\nYou can install them later by running:`));
891
+ console.error(
892
+ pc.cyan(` cd ${serviceName || res.sanitizedName} && npm install`),
893
+ );
894
+ console.error(pc.dim(" Then run: npm run format\n"));
895
+
896
+ // Don't exit - let the project be created anyway
897
+ console.log(pc.cyan("⏭️ Continuing with project creation...\n"));
898
+ }
899
+
900
+ return { deps, devDeps, installSucceeded };
901
+ };