@ifecodes/backend-template 1.1.3 → 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.
Files changed (86) hide show
  1. package/README.md +39 -18
  2. package/bin/cli.js +602 -101
  3. package/bin/lib/microservice-config.js +68 -14
  4. package/bin/lib/prompts.js +25 -6
  5. package/bin/lib/readme-generator.js +119 -29
  6. package/bin/lib/service-setup.js +401 -132
  7. package/package.json +2 -2
  8. package/template/base/js/.eslintrc.json +13 -0
  9. package/template/base/js/.prettierrc +7 -0
  10. package/template/base/js/eslint.config.js +31 -0
  11. package/template/base/js/package.json +28 -0
  12. package/template/base/js/src/app.js +15 -0
  13. package/template/base/js/src/config/db.js +8 -0
  14. package/template/base/js/src/config/env.js +14 -0
  15. package/template/base/js/src/config/index.js +7 -0
  16. package/template/base/js/src/middlewares/index.js +9 -0
  17. package/template/base/js/src/middlewares/method-not-allowed.middleware.js +13 -0
  18. package/template/base/js/src/middlewares/not-found.middleware.js +10 -0
  19. package/template/base/js/src/middlewares/root.middleware.js +16 -0
  20. package/template/base/js/src/modules/index.js +8 -0
  21. package/template/base/js/src/modules/v1/health/health.controller.js +21 -0
  22. package/template/base/js/src/modules/v1/health/health.route.js +9 -0
  23. package/template/base/js/src/modules/v1/health/index.js +5 -0
  24. package/template/base/js/src/modules/v1/index.js +8 -0
  25. package/template/base/js/src/routes.js +16 -0
  26. package/template/base/js/src/server.js +18 -0
  27. package/template/base/js/src/utils/http-error.js +53 -0
  28. package/template/base/js/src/utils/index.js +22 -0
  29. package/template/base/js/src/utils/logger.js +67 -0
  30. package/template/base/ts/.env.example +5 -0
  31. package/template/base/ts/.husky/pre-commit +13 -0
  32. package/template/base/ts/.prettierignore +47 -0
  33. package/template/base/ts/gitignore +31 -0
  34. package/template/base/ts/src/app.ts +15 -0
  35. package/template/base/{src → ts/src}/config/env.ts +10 -10
  36. package/template/base/{src → ts/src}/middlewares/index.ts +3 -3
  37. package/template/base/{src → ts/src}/middlewares/method-not-allowed.middleware.ts +16 -17
  38. package/template/base/{src → ts/src}/middlewares/root.middleware.ts +2 -2
  39. package/template/base/{src → ts/src}/modules/v1/health/health.controller.ts +18 -18
  40. package/template/base/{src → ts/src}/modules/v1/health/health.route.ts +9 -9
  41. package/template/base/{src → ts/src}/modules/v1/health/index.ts +1 -1
  42. package/template/base/{src → ts/src}/modules/v1/index.ts +8 -8
  43. package/template/base/{src → ts/src}/routes.ts +15 -15
  44. package/template/base/{src → ts/src}/utils/http-error.ts +45 -45
  45. package/template/base/{src → ts/src}/utils/logger.ts +23 -0
  46. package/template/features/auth/argon2/inject.js +29 -4
  47. package/template/features/auth/base/inject.js +108 -26
  48. package/template/features/auth/bcrypt/inject.js +25 -5
  49. package/template/features/auth/models/user.model.js +24 -0
  50. package/template/features/auth/modules/auth.controller.js +21 -0
  51. package/template/features/auth/modules/auth.routes.js +10 -0
  52. package/template/features/auth/modules/auth.service.js +29 -0
  53. package/template/features/auth/modules/index.js +1 -0
  54. package/template/features/auth/utils/jwt.js +12 -0
  55. package/template/features/cors/inject.js +4 -1
  56. package/template/features/helmet/inject.js +4 -1
  57. package/template/features/morgan/inject.js +4 -1
  58. package/template/features/rate-limit/inject.js +4 -1
  59. package/template/gateway/js/app.js +42 -0
  60. package/template/gateway/js/inject.js +31 -0
  61. package/template/gateway/js/server.js +19 -0
  62. package/template/gateway/ts/app.ts +43 -0
  63. package/template/gateway/ts/inject.js +32 -0
  64. package/template/gateway/{server.ts → ts/server.ts} +3 -3
  65. package/template/microservice/docker/.dockerignore +10 -0
  66. package/template/microservice/docker/Dockerfile +2 -1
  67. package/template/microservice/docker/docker-compose.yml +0 -1
  68. package/bin/lib/ts-to-js.js +0 -342
  69. package/template/base/src/app.ts +0 -8
  70. package/template/gateway/app.ts +0 -26
  71. package/template/gateway/inject.js +0 -27
  72. /package/template/base/{.env.example → js/.env.example} +0 -0
  73. /package/template/base/{.husky → js/.husky}/pre-commit +0 -0
  74. /package/template/base/{.prettierignore → js/.prettierignore} +0 -0
  75. /package/template/base/{gitignore → js/gitignore} +0 -0
  76. /package/template/base/{.eslintrc.json → ts/.eslintrc.json} +0 -0
  77. /package/template/base/{.prettierrc → ts/.prettierrc} +0 -0
  78. /package/template/base/{eslint.config.js → ts/eslint.config.js} +0 -0
  79. /package/template/base/{package.json → ts/package.json} +0 -0
  80. /package/template/base/{src → ts/src}/config/db.ts +0 -0
  81. /package/template/base/{src → ts/src}/config/index.ts +0 -0
  82. /package/template/base/{src → ts/src}/middlewares/not-found.middleware.ts +0 -0
  83. /package/template/base/{src → ts/src}/modules/index.ts +0 -0
  84. /package/template/base/{src → ts/src}/server.ts +0 -0
  85. /package/template/base/{src → ts/src}/utils/index.ts +0 -0
  86. /package/template/base/{tsconfig.json → ts/tsconfig.json} +0 -0
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { execSync } from "child_process";
@@ -7,11 +7,12 @@ import pc from "picocolors";
7
7
  import { getProjectConfig } from "./lib/prompts.js";
8
8
  import { setupService } from "./lib/service-setup.js";
9
9
  import { generateReadme } from "./lib/readme-generator.js";
10
- import { stripTypeScript, getJavaScriptScripts, getJavaScriptDependencies, transformToJavaScript, transformDirectory } from "./lib/ts-to-js.js";
10
+ // No TS->JS transform: templates contain language-specific folders (base/js, base/ts)
11
11
  import {
12
12
  generateDockerCompose,
13
13
  generatePm2Config,
14
14
  copyDockerfile,
15
+ copyDockerignore,
15
16
  } from "./lib/microservice-config.js";
16
17
 
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -26,7 +27,11 @@ const {
26
27
  isInMicroserviceProject,
27
28
  } = config;
28
29
 
29
- const base = path.join(__dirname, "../template/base");
30
+ const baseRoot =
31
+ config.language === "javascript"
32
+ ? path.join(__dirname, "../template/base/js")
33
+ : path.join(__dirname, "../template/base/ts");
34
+ const base = baseRoot;
30
35
 
31
36
  // Determine which services to create
32
37
  const servicesToCreate = [];
@@ -44,27 +49,56 @@ if (isInMicroserviceProject) {
44
49
  // Validate and prepare project
45
50
  if (!isInMicroserviceProject && config.projectType === "microservice") {
46
51
  if (isExistingProject) {
47
- console.error(`\n${pc.red("❌ Error:")} Project ${pc.bold(sanitizedName)} already exists!`);
52
+ console.error(
53
+ `\n${pc.red("❌ Error:")} Project ${pc.bold(sanitizedName)} already exists!`,
54
+ );
48
55
  process.exit(1);
49
56
  }
50
57
  console.log(
51
- `\n${pc.cyan("🏗️ Creating microservices:")} ${pc.bold(servicesToCreate.join(", "))}...\n`
58
+ `\n${pc.cyan("🏗️ Creating microservices:")} ${pc.bold(servicesToCreate.join(", "))}...\n`,
52
59
  );
53
60
  } else if (!isInMicroserviceProject && config.projectType === "monolith") {
54
61
  if (isExistingProject) {
55
- console.error(`\n${pc.red("❌ Error:")} Project ${pc.bold(sanitizedName)} already exists!`);
62
+ console.error(
63
+ `\n${pc.red("❌ Error:")} Project ${pc.bold(sanitizedName)} already exists!`,
64
+ );
56
65
  process.exit(1);
57
66
  }
58
67
  fs.cpSync(base, target, { recursive: true });
59
-
60
- // Transform to JavaScript if selected
61
- if (config.language === "javascript") {
62
- // console.log("\n🔄 Converting TypeScript to JavaScript...\n");
63
- console.log(`\n${pc.cyan("⚙️ Setting up JavaScript project...")}\n`);
64
- transformToJavaScript(target);
68
+
69
+ // Remove db file and remove connectDB export/import if auth is not enabled
70
+ if (!config.auth) {
71
+ const ext = config.language === "javascript" ? "js" : "ts";
72
+ const dbPath = path.join(target, `src/config/db.${ext}`);
73
+ if (fs.existsSync(dbPath)) {
74
+ fs.rmSync(dbPath);
75
+ }
76
+
77
+ // Update index.(js|ts) to not export or require connectDB
78
+ const indexPath = path.join(target, `src/config/index.${ext}`);
79
+ if (fs.existsSync(indexPath)) {
80
+ let indexContent = fs.readFileSync(indexPath, "utf8");
81
+ if (ext === "ts") {
82
+ indexContent = indexContent.replace(
83
+ 'export { connectDB } from "./db";\n',
84
+ "",
85
+ );
86
+ // also remove any trailing references like `connectDB,` in exported objects
87
+ indexContent = indexContent.replace(/connectDB,?/g, "");
88
+ } else {
89
+ indexContent = indexContent
90
+ .replace('const { connectDB } = require("./db");', "")
91
+ .replace(/connectDB,?/g, "");
92
+ }
93
+ fs.writeFileSync(indexPath, indexContent);
94
+ }
65
95
  }
96
+
97
+ // No TypeScript-to-JavaScript conversion — templates include language-specific variants
66
98
  } else if (isInMicroserviceProject) {
67
- console.log(`\n${pc.cyan("🏗️ Adding service:")} ${pc.bold(servicesToCreate[0])}...\n`);
99
+ console.log(
100
+ `\n${pc.cyan("🏗️ Adding service:")} ${pc.bold(servicesToCreate[0])}...\n`,
101
+ );
68
102
  }
69
103
 
70
104
  // Process services
@@ -73,7 +107,9 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
73
107
  if (!isInMicroserviceProject) {
74
108
  const sharedDir = path.join(target, "shared");
75
109
  if (!fs.existsSync(sharedDir)) {
76
- console.log(`\n${pc.cyan("📦 Creating shared folder for config and utils...")}`);
110
+ console.log(
111
+ `\n${pc.cyan("📦 Creating shared folder for config and utils...")}`,
112
+ );
77
113
  fs.mkdirSync(sharedDir, { recursive: true });
78
114
 
79
115
  // Copy config and utils from base template
@@ -85,11 +121,129 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
85
121
  fs.cpSync(baseConfigDir, sharedConfigDir, { recursive: true });
86
122
  fs.cpSync(baseUtilsDir, sharedUtilsDir, { recursive: true });
87
123
 
124
+ // Remove db files and strip connectDB exports/imports when auth is not enabled
125
+ if (!config.auth) {
126
+ for (const ext of ["ts", "js"]) {
127
+ const sharedDbPath = path.join(sharedConfigDir, `db.${ext}`);
128
+ if (fs.existsSync(sharedDbPath)) fs.rmSync(sharedDbPath);
129
+
130
+ const sharedIndexPath = path.join(sharedConfigDir, `index.${ext}`);
131
+ if (fs.existsSync(sharedIndexPath)) {
132
+ let idx = fs.readFileSync(sharedIndexPath, "utf8");
133
+ // Remove various export/import patterns referencing connectDB
134
+ idx = idx.replace(
135
+ /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
136
+ "",
137
+ );
138
+ idx = idx.replace(
139
+ /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
140
+ "",
141
+ );
142
+ idx = idx.replace(
143
+ /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
144
+ "",
145
+ );
146
+ idx = idx.replace(/\bconnectDB,?\b/g, "");
147
+ idx = idx.replace(/\n{3,}/g, "\n\n");
148
+ fs.writeFileSync(sharedIndexPath, idx);
149
+ }
150
+ }
151
+ }
152
+ const ext = config.language === "javascript" ? "js" : "ts";
153
+
154
+ // Update shared env.ts to include all service port environment variables
155
+ const sharedEnvPath = path.join(sharedConfigDir, `env.${ext}`);
156
+ if (fs.existsSync(sharedEnvPath)) {
157
+ let envContent = fs.readFileSync(sharedEnvPath, "utf8");
158
+ console.log(`\n${pc.cyan("🔧 Updating shared env configuration...")}`);
159
+
160
+ // Build port environment variables for all services
161
+ const allServices = ["gateway", "health-service"];
162
+ if (config.auth) allServices.push("auth-service");
163
+
164
+ const portEnvVars = allServices
165
+ .map((service) => {
166
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
167
+ // Don't add ! for JavaScript projects - it will cause syntax errors
168
+ const assertion = config.language === "javascript" ? "" : "!";
169
+ return ` ${envVarName}: process.env.${envVarName}${assertion},`;
170
+ })
171
+ .join("\n");
172
+
173
+ // Replace PORT with service-specific ports
174
+ envContent = envContent.replace(
175
+ " PORT: process.env.PORT!,",
176
+ portEnvVars,
177
+ );
178
+
179
+ // Add ALLOWED_ORIGIN if CORS is selected
180
+ if (config.features && config.features.includes("cors")) {
181
+ const assertion = config.language === "javascript" ? "" : "!";
182
+ envContent = envContent.replace(
183
+ "/*__ALLOWED_ORIGIN__*/",
184
+ `ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN${assertion},`,
185
+ );
186
+ } else {
187
+ envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
188
+ }
189
+
190
+ // Add MONGO_URI and JWT_SECRET if auth is enabled
191
+ if (config.auth) {
192
+ const assertion = config.language === "javascript" ? "" : "!";
193
+ envContent = envContent.replace(
194
+ "/*__MONGO_URI__*/",
195
+ `MONGO_URI: process.env.MONGO_URI${assertion},`,
196
+ );
197
+ envContent = envContent.replace(
198
+ "/*__JWT_SECRET__*/",
199
+ `JWT_SECRET: process.env.JWT_SECRET${assertion},`,
200
+ );
201
+ } else {
202
+ envContent = envContent.replace("/*__MONGO_URI__*/", "");
203
+ envContent = envContent.replace("/*__JWT_SECRET__*/", "");
204
+ }
205
+
206
+ fs.writeFileSync(sharedEnvPath, envContent);
207
+ }
208
+
209
+ // Update shared config/index to conditionally export connectDB
210
+ const sharedConfigIndexPath = path.join(sharedConfigDir, `index.${ext}`);
211
+ if (fs.existsSync(sharedConfigIndexPath)) {
212
+ let indexContent = fs.readFileSync(sharedConfigIndexPath, "utf8");
213
+ if (!config.auth) {
214
+ if (ext === "ts") {
215
+ indexContent = indexContent.replace(
216
+ 'export { connectDB } from "./db";\n',
217
+ "",
218
+ );
219
+ }
220
+ indexContent = indexContent
221
+ .replace('const { connectDB } = require("./db");', "")
222
+ .replace("connectDB,", "");
223
+ fs.writeFileSync(sharedConfigIndexPath, indexContent);
224
+ }
225
+ }
226
+
227
+ // Update shared utils/logger to use shared config
228
+ const sharedLoggerPath = path.join(sharedUtilsDir, `logger.${ext}`);
229
+ if (fs.existsSync(sharedLoggerPath)) {
230
+ console.log(
231
+ `\n${pc.cyan("🔧 Updating shared logger to use shared config...")}`,
232
+ );
233
+ let loggerContent = fs.readFileSync(sharedLoggerPath, "utf8");
234
+ // Replace imports like: from '@/config'; or from "@/config" with relative import to shared config
235
+ loggerContent = loggerContent.replace(
236
+ /from\s+["']@\/config["'];?/g,
237
+ "from '../config';",
238
+ );
239
+ fs.writeFileSync(sharedLoggerPath, loggerContent);
240
+ }
241
+
88
242
  // Create shared package.json
89
243
  const sharedPackageJson = {
90
244
  name: "@shared/common",
91
245
  version: "1.0.0",
92
- type: "module",
246
+ type: "commonjs",
93
247
  exports: {
94
248
  "./config/*": "./config/*",
95
249
  "./utils/*": "./utils/*",
@@ -97,7 +251,7 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
97
251
  };
98
252
  fs.writeFileSync(
99
253
  path.join(sharedDir, "package.json"),
100
- JSON.stringify(sharedPackageJson, null, 2)
254
+ JSON.stringify(sharedPackageJson, null, 2),
101
255
  );
102
256
  }
103
257
  }
@@ -113,6 +267,12 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
113
267
  console.log(`\n🔨 Setting up ${serviceName}...`);
114
268
  fs.cpSync(base, serviceRoot, { recursive: true });
115
269
 
270
+ // Remove .env and .env.example from microservices (environment variables come from docker-compose/pm2)
271
+ const envPath = path.join(serviceRoot, ".env");
272
+ const envExamplePath = path.join(serviceRoot, ".env.example");
273
+ if (fs.existsSync(envPath)) fs.rmSync(envPath);
274
+ if (fs.existsSync(envExamplePath)) fs.rmSync(envExamplePath);
275
+
116
276
  // Remove config and utils from service (they'll use shared) - except gateway handles it differently
117
277
  if (serviceName !== "gateway") {
118
278
  const serviceConfigDir = path.join(serviceRoot, "src", "config");
@@ -126,16 +286,21 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
126
286
 
127
287
  // Get all services first (needed for gateway routing)
128
288
  const servicesDir = path.join(target, "services");
129
- const allServices = fs.existsSync(servicesDir)
289
+ const existingServices = fs.existsSync(servicesDir)
130
290
  ? fs
131
291
  .readdirSync(servicesDir)
132
292
  .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
133
- : servicesToCreate;
293
+ : [];
294
+ // Include services we're about to create so port computation and gateway routing
295
+ // are aware of newly added services when setting up files.
296
+ const allServices = Array.from(
297
+ new Set([...existingServices, ...servicesToCreate]),
298
+ );
134
299
 
135
- // Track if all installs succeeded for Husky setup
136
- let allInstallsSucceeded = true;
300
+ // Step 1: Setup all service files first (without installing dependencies)
301
+ console.log(pc.cyan("\n⚙️ Setting up service files...\n"));
302
+ const serviceConfigs = [];
137
303
 
138
- // Now setup each service with knowledge of all services
139
304
  for (const serviceName of servicesToCreate) {
140
305
  const serviceRoot = path.join(target, "services", serviceName);
141
306
  const shouldIncludeAuth = isInMicroserviceProject
@@ -146,39 +311,53 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
146
311
  serviceName,
147
312
  serviceRoot,
148
313
  shouldIncludeAuth,
149
- allServices
314
+ allServices,
315
+ true, // Skip install for now
150
316
  );
151
- if (!result.installSucceeded) {
152
- allInstallsSucceeded = false;
153
- }
317
+ serviceConfigs.push({
318
+ serviceName,
319
+ serviceRoot,
320
+ deps: result.deps,
321
+ devDeps: result.devDeps,
322
+ });
154
323
  }
155
324
 
156
- // Store for later use
157
- config.allInstallsSucceeded = allInstallsSucceeded;
325
+ // Remove per-service husky hooks and ensure a single root pre-commit hook
326
+ try {
327
+ const servicesDirPath = path.join(target, "services");
328
+ const allServicesList = fs.existsSync(servicesDirPath)
329
+ ? fs
330
+ .readdirSync(servicesDirPath)
331
+ .filter((f) =>
332
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
333
+ )
334
+ : [];
158
335
 
159
- // Transform to JavaScript if selected (for microservices)
160
- if (config.language === "javascript") {
161
- console.log(`\n${pc.cyan("⚙️ Converting microservices to JavaScript...")}\n`);
162
-
163
- // Transform shared folder
164
- const sharedDir = path.join(target, "shared");
165
- if (fs.existsSync(sharedDir)) {
166
- transformDirectory(sharedDir);
167
- }
168
-
169
- // Transform each service
170
- for (const serviceName of allServices) {
171
- const serviceRoot = path.join(target, "services", serviceName);
172
- console.log(pc.dim(` Transforming ${serviceName}...`));
173
- transformToJavaScript(serviceRoot);
336
+ // Remove `.husky` folders from each service
337
+ for (const svc of allServicesList) {
338
+ const svcHusky = path.join(servicesDirPath, svc, ".husky");
339
+ if (fs.existsSync(svcHusky)) {
340
+ fs.rmSync(svcHusky, { recursive: true, force: true });
341
+ }
174
342
  }
175
-
176
- console.log(pc.green("✓ JavaScript transformation complete\n"));
343
+
344
+ // Ensure root .husky/pre-commit exists at target
345
+ const rootHuskyDir = path.join(target, ".husky");
346
+ if (!fs.existsSync(rootHuskyDir))
347
+ fs.mkdirSync(rootHuskyDir, { recursive: true });
348
+ const preCommitPath = path.join(rootHuskyDir, "pre-commit");
349
+ const preCommitContent =
350
+ 'set -e\n\necho "Checking format (prettier)..."\nnpm run check-format\n\necho "Running TypeScript type-check..."\nnpx tsc --noEmit\n\necho "Checking lint..."\nnpm run lint -- --max-warnings=0\n';
351
+ fs.writeFileSync(preCommitPath, preCommitContent);
352
+ } catch (err) {
353
+ // Non-fatal; continue setup even if husky files couldn't be created/removed
177
354
  }
178
355
 
356
+ // Step 2: Generate docker-compose/pm2 config and root files
179
357
  if (mode === "docker") {
180
- generateDockerCompose(target, allServices);
358
+ generateDockerCompose(target, allServices, config.sanitizedName);
181
359
  copyDockerfile(target, servicesToCreate);
360
+ copyDockerignore(target, servicesToCreate);
182
361
  } else {
183
362
  generatePm2Config(target, allServices);
184
363
  }
@@ -188,38 +367,104 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
188
367
  if (!fs.existsSync(rootPackageJsonPath)) {
189
368
  const rootPackageJson = {
190
369
  name: sanitizedName,
191
- version: "1.0.0",
370
+ version: config.version || "1.0.0",
371
+ description: config.description || "",
192
372
  private: true,
193
373
  scripts: {
374
+ dev: "docker-compose up",
375
+ stop: "docker-compose down",
376
+ restart: "docker-compose restart",
377
+ lint: 'eslint "services/**/*.{js,ts,tsx}" "shared/**/*.{js,ts,tsx}"',
378
+ format:
379
+ 'prettier --write "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
380
+ "check-format":
381
+ 'prettier --check "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
194
382
  prepare: "husky install",
195
383
  },
196
384
  devDependencies: {
197
- husky: "^8.0.3",
385
+ husky: "^9.1.7",
386
+ prettier: "^3.7.4",
387
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
388
+ "@typescript-eslint/parser": "^8.50.1",
389
+ eslint: "^9.39.2",
390
+ "eslint-config-prettier": "^10.1.8",
198
391
  },
199
392
  };
200
393
  fs.writeFileSync(
201
394
  rootPackageJsonPath,
202
- JSON.stringify(rootPackageJson, null, 2) + "\n"
395
+ JSON.stringify(rootPackageJson, null, 2) + "\n",
203
396
  );
204
397
  }
205
- } else {
206
- const result = await setupService(config, null, target, true);
207
- config.installSucceeded = result.installSucceeded;
208
- }
209
398
 
210
- // Generate README.md
211
- if (!isInMicroserviceProject) {
212
- console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
213
- const readmeContent = generateReadme(config);
214
- fs.writeFileSync(path.join(target, "README.md"), readmeContent);
215
-
216
- // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
217
- if (config.projectType === "microservice") {
218
- const servicesDir = path.join(target, "services");
219
- const allServices = fs.readdirSync(servicesDir).filter((f) =>
220
- fs.statSync(path.join(servicesDir, f)).isDirectory()
221
- );
222
-
399
+ // Ensure root lint/format config files exist (copy from template base if available), and remove any per-service copies
400
+ try {
401
+ const rootFiles = [".prettierrc", ".prettierignore", ".eslintrc.json"];
402
+ for (const f of rootFiles) {
403
+ const src = path.join(base, f);
404
+ const dest = path.join(target, f);
405
+ if (fs.existsSync(src)) {
406
+ fs.copyFileSync(src, dest);
407
+ } else if (!fs.existsSync(dest)) {
408
+ // create minimal defaults
409
+ if (f === ".prettierignore")
410
+ fs.writeFileSync(dest, "node_modules\n" + "dist\n");
411
+ else if (f === ".eslintrc.json")
412
+ fs.writeFileSync(dest, JSON.stringify({ root: true }, null, 2));
413
+ else fs.writeFileSync(dest, "{}");
414
+ }
415
+ }
416
+
417
+ // Write eslint.config.js with recommended workspace config (overwrite)
418
+ const eslintConfigPath = path.join(target, "eslint.config.js");
419
+
420
+ // Build dynamic project list for TypeScript projects based on the services present
421
+ const projectPaths = ["./tsconfig.json"];
422
+ try {
423
+ if (typeof allServices !== "undefined" && Array.isArray(allServices)) {
424
+ for (const svc of allServices) {
425
+ const svcTsPath = `./services/${svc}/tsconfig.json`;
426
+ if (fs.existsSync(path.join(target, svcTsPath))) {
427
+ projectPaths.push(svcTsPath);
428
+ }
429
+ }
430
+ }
431
+ } catch (e) {
432
+ // non-fatal; fall back to default projectPaths containing only root tsconfig
433
+ }
434
+
435
+ const projectEntries = projectPaths
436
+ .map((p) => ` "${p}",`)
437
+ .join("\n");
438
+
439
+ const eslintConfigContent = `const tsParser = require("@typescript-eslint/parser");\nconst tsPlugin = require("@typescript-eslint/eslint-plugin");\n\nmodule.exports = [\n // Files/paths to ignore (replaces .eslintignore usage in flat config)\n {\n ignores: ["node_modules/**", "dist/**"],\n },\n\n // TypeScript rules for source files\n {\n files: ["services/**/*.{js,ts,tsx}", "shared/**/*.{js,ts,tsx}"],\n languageOptions: {\n parser: tsParser,\n parserOptions: {\n project: [\n${projectEntries}\n ],\n tsconfigRootDir: __dirname,\n ecmaVersion: 2020,\n sourceType: "module",\n },\n },\n plugins: {\n "@typescript-eslint": tsPlugin,\n },\n rules: {\n // Disallow explicit 'any'\n "@typescript-eslint/no-explicit-any": "error",\n\n // You can add or tune more TypeScript rules here\n "@typescript-eslint/explicit-module-boundary-types": "off",\n },\n },\n];\n`;
440
+ fs.writeFileSync(eslintConfigPath, eslintConfigContent);
441
+
442
+ // Remove per-service copies if they exist (already removed in setupService, but double-check)
443
+ const servicesDirPath = path.join(target, "services");
444
+ if (fs.existsSync(servicesDirPath)) {
445
+ const svcs = fs
446
+ .readdirSync(servicesDirPath)
447
+ .filter((f) =>
448
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
449
+ );
450
+ for (const svc of svcs) {
451
+ for (const f of [...rootFiles, "eslint.config.js"]) {
452
+ const p = path.join(servicesDirPath, svc, f);
453
+ if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
454
+ }
455
+ }
456
+ }
457
+ } catch (err) {
458
+ // non-fatal
459
+ }
460
+
461
+ // Step 3: Generate README and create root configuration files
462
+ if (!isInMicroserviceProject) {
463
+ console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
464
+ const readmeContent = generateReadme(config);
465
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
466
+
467
+ // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
223
468
  for (const service of allServices) {
224
469
  const gitignorePath = path.join(servicesDir, service, "gitignore");
225
470
  const dotGitignorePath = path.join(servicesDir, service, ".gitignore");
@@ -227,35 +472,227 @@ if (!isInMicroserviceProject) {
227
472
  fs.renameSync(gitignorePath, dotGitignorePath);
228
473
  }
229
474
  }
230
- } else {
231
- const gitignorePath = path.join(target, "gitignore");
232
- const dotGitignorePath = path.join(target, ".gitignore");
233
- if (fs.existsSync(gitignorePath)) {
234
- fs.renameSync(gitignorePath, dotGitignorePath);
475
+
476
+ // Create root .gitignore for microservices
477
+ const rootGitignoreContent = `.env\nnode_modules\n`;
478
+ fs.writeFileSync(path.join(target, ".gitignore"), rootGitignoreContent);
479
+
480
+ // Create root .env and .env.example for microservices
481
+ let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
482
+
483
+ // Add port configuration for each service
484
+ allServices.forEach((service, index) => {
485
+ const isGateway = service === "gateway";
486
+ const port = isGateway
487
+ ? 4000
488
+ : 4001 +
489
+ allServices.filter((s, i) => s !== "gateway" && i < index).length;
490
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
491
+ const serviceName = service
492
+ .split("-")
493
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
494
+ .join(" ");
495
+ rootENVContent += `# ${serviceName}\n${envVarName}=${port}\n\n`;
496
+ });
497
+
498
+ fs.writeFileSync(path.join(target, ".env"), rootENVContent);
499
+ fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
500
+
501
+ // Create root tsconfig.json for microservices workspace
502
+ const rootTsConfigContent = {
503
+ compilerOptions: {
504
+ target: "ES2020",
505
+ module: "CommonJS",
506
+ lib: ["ES2020"],
507
+ moduleResolution: "node",
508
+ esModuleInterop: true,
509
+ skipLibCheck: true,
510
+ strict: true,
511
+ baseUrl: ".",
512
+ paths: {
513
+ "@/*": ["./*"],
514
+ },
515
+ },
516
+ include: [],
517
+ exclude: ["node_modules", "dist"],
518
+ references: allServices.map((service) => ({
519
+ path: `./services/${service}`,
520
+ })),
521
+ };
522
+ fs.writeFileSync(
523
+ path.join(target, "tsconfig.json"),
524
+ JSON.stringify(rootTsConfigContent, null, 2) + "\n",
525
+ );
526
+ }
527
+
528
+ // If we're adding a service into an existing microservice project,
529
+ // ensure shared config and gateway are updated to reference the new service.
530
+ if (isInMicroserviceProject) {
531
+ try {
532
+ const sharedConfigDir = path.join(target, "shared", "config");
533
+ const languageExt = config.language === "javascript" ? "js" : "ts";
534
+ const sharedEnvPath = path.join(sharedConfigDir, `env.${languageExt}`);
535
+
536
+ if (fs.existsSync(sharedEnvPath)) {
537
+ let envContent = fs.readFileSync(sharedEnvPath, "utf8");
538
+
539
+ // Build port environment variables for all services
540
+ const portEnvVars = allServices
541
+ .map((service) => {
542
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
543
+ const assertion = config.language === "javascript" ? "" : "!";
544
+ return ` ${envVarName}: process.env.${envVarName}${assertion},`;
545
+ })
546
+ .join("\n");
547
+
548
+ // Remove any existing *_PORT lines to avoid duplication
549
+ envContent = envContent.replace(
550
+ /^[ \t]*[A-Z0-9_]+_PORT:\s*process\.env\.[A-Z0-9_]+!?\,?\s*$/gim,
551
+ "",
552
+ );
553
+ // Normalize multiple consecutive blank lines
554
+ envContent = envContent.replace(/\n{2,}/g, "\n\n");
555
+
556
+ // Attempt several fallback strategies to inject port variables:
557
+ // 1. Replace explicit placeholder if present in template
558
+ // 2. Insert right after the first object opening brace
559
+ // 3. Append to the end as a last resort
560
+ if (envContent.includes("/*__PORTS__*/")) {
561
+ envContent = envContent.replace("/*__PORTS__*/", portEnvVars);
562
+ } else {
563
+ // Fallback: find the opening brace of the exported ENV object and insert after it
564
+ const braceIndex = envContent.indexOf("{");
565
+ if (braceIndex !== -1) {
566
+ const insertPos =
567
+ envContent.indexOf("\n", braceIndex) + 1 || braceIndex + 1;
568
+ envContent =
569
+ envContent.slice(0, insertPos) +
570
+ portEnvVars +
571
+ "\n" +
572
+ envContent.slice(insertPos);
573
+ } else {
574
+ // Final fallback: append to the end
575
+ envContent = envContent + "\n" + portEnvVars;
576
+ }
577
+ }
578
+
579
+ fs.writeFileSync(sharedEnvPath, envContent);
580
+ }
581
+
582
+ // Re-generate gateway routes if gateway exists (so new service gets proxied)
583
+ const gatewayRoot = path.join(target, "services", "gateway");
584
+ if (fs.existsSync(gatewayRoot)) {
585
+ // Re-run setupService for gateway to rewrite app/server/env files
586
+ await setupService(
587
+ config,
588
+ "gateway",
589
+ gatewayRoot,
590
+ config.auth,
591
+ allServices,
592
+ true,
593
+ );
594
+ }
595
+
596
+ // Update root .env and .env.example so newly added services have port entries
597
+ try {
598
+ const servicesDirPath = path.join(target, "services");
599
+ const svcList = fs.existsSync(servicesDirPath)
600
+ ? fs
601
+ .readdirSync(servicesDirPath)
602
+ .filter((f) =>
603
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
604
+ )
605
+ : allServices;
606
+
607
+ let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
608
+ svcList.forEach((service, index) => {
609
+ const isGateway = service === "gateway";
610
+ const port = isGateway
611
+ ? 4000
612
+ : 4001 +
613
+ svcList.filter((s, i) => s !== "gateway" && i < index).length;
614
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
615
+ const serviceNamePretty = service
616
+ .split("-")
617
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
618
+ .join(" ");
619
+ rootENVContent += `# ${serviceNamePretty}\n${envVarName}=${port}\n\n`;
620
+ });
621
+
622
+ fs.writeFileSync(path.join(target, ".env"), rootENVContent);
623
+ fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
624
+ } catch (e) {
625
+ // non-fatal
626
+ }
627
+ } catch (e) {
628
+ // non-fatal; continue even if updating shared/gateway fails
235
629
  }
236
630
  }
237
-
238
- // Generate .env from .env.example for each service or root
239
- console.log(`${pc.cyan("📄 Setting up environment files...")}\n`);
240
- if (config.projectType === "microservice") {
241
- const servicesDir = path.join(target, "services");
242
- const allServices = fs.readdirSync(servicesDir).filter((f) =>
243
- fs.statSync(path.join(servicesDir, f)).isDirectory()
631
+
632
+ // Step 5: Install dependencies for all services
633
+
634
+ console.log(pc.cyan("\n📦 Installing dependencies for all services...\n"));
635
+ let allInstallsSucceeded = true;
636
+
637
+ for (const { serviceName, serviceRoot, deps, devDeps } of serviceConfigs) {
638
+ console.log(
639
+ pc.cyan(`\n📦 Installing dependencies for ${serviceName}...\n`),
244
640
  );
245
-
246
- for (const service of allServices) {
247
- const envExamplePath = path.join(servicesDir, service, ".env.example");
248
- const envPath = path.join(servicesDir, service, ".env");
249
- if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
250
- fs.copyFileSync(envExamplePath, envPath);
641
+
642
+ try {
643
+ if (deps.length) {
644
+ execSync(`npm install ${deps.join(" ")}`, {
645
+ cwd: serviceRoot,
646
+ stdio: "inherit",
647
+ });
648
+ }
649
+ if (devDeps.length) {
650
+ execSync(`npm install -D ${devDeps.join(" ")}`, {
651
+ cwd: serviceRoot,
652
+ stdio: "inherit",
653
+ });
251
654
  }
655
+ execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
656
+ } catch (error) {
657
+ allInstallsSucceeded = false;
658
+ console.error(
659
+ pc.red(`\n❌ Failed to install dependencies for ${serviceName}`),
660
+ );
661
+ console.error(pc.dim(`\nYou can install them later by running:`));
662
+ console.error(pc.cyan(` cd services/${serviceName} && npm install\n`));
252
663
  }
253
- } else {
254
- const envExamplePath = path.join(target, ".env.example");
255
- const envPath = path.join(target, ".env");
256
- if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
257
- fs.copyFileSync(envExamplePath, envPath);
664
+ }
665
+
666
+ // Store for later use
667
+ config.allInstallsSucceeded = allInstallsSucceeded;
668
+ } else {
669
+ const result = await setupService(config, null, target, true);
670
+ config.installSucceeded = result.installSucceeded;
671
+ }
672
+
673
+ // Generate README.md for monolith (microservices already done above)
674
+ if (!isInMicroserviceProject && config.projectType === "monolith") {
675
+ console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
676
+ const readmeContent = generateReadme(config);
677
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
678
+
679
+ // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
680
+ const gitignorePath = path.join(target, "gitignore");
681
+ const dotGitignorePath = path.join(target, ".gitignore");
682
+ if (fs.existsSync(gitignorePath)) {
683
+ fs.renameSync(gitignorePath, dotGitignorePath);
684
+ }
685
+
686
+ // Generate .env from .env.example for monolith only
687
+ console.log(`${pc.cyan("📄 Setting up environment files...")}\n`);
688
+ try {
689
+ const rootEnvExamplePath = path.join(target, ".env.example");
690
+ const rootEnvPath = path.join(target, ".env");
691
+ if (fs.existsSync(rootEnvExamplePath) && !fs.existsSync(rootEnvPath)) {
692
+ fs.copyFileSync(rootEnvExamplePath, rootEnvPath);
258
693
  }
694
+ } catch (err) {
695
+ // Non-fatal; proceed even if we fail to write env files
259
696
  }
260
697
  }
261
698
 
@@ -263,9 +700,9 @@ if (!isInMicroserviceProject) {
263
700
  if (!isInMicroserviceProject) {
264
701
  execSync("git init", { cwd: target, stdio: "inherit" });
265
702
 
266
- // Install husky and setup at root level
703
+ // Install husky and other devDeps and setup at root level
267
704
  if (config.projectType === "microservice") {
268
- console.log("\n📦 Installing Husky at root level...\n");
705
+ console.log("\n📦 Installing dependencies at root level...\n");
269
706
  if (config.allInstallsSucceeded) {
270
707
  try {
271
708
  execSync("npm install", { cwd: target, stdio: "inherit" });
@@ -274,8 +711,21 @@ if (!isInMicroserviceProject) {
274
711
  } catch (error) {
275
712
  console.log("\n⚠️ Husky setup failed\n");
276
713
  }
714
+ // Run format after successful install
715
+ console.log(pc.cyan("\n🎨 Formatting code...\n"));
716
+ try {
717
+ execSync("npm run format", { cwd: target, stdio: "inherit" });
718
+ } catch (formatError) {
719
+ console.warn(
720
+ pc.yellow(
721
+ "⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n",
722
+ ),
723
+ );
724
+ }
277
725
  } else {
278
- console.log("\n⚠️ Husky setup skipped (run 'npm install && npm run prepare' after fixing service dependencies)\n");
726
+ console.log(
727
+ "\n⚠️ Husky setup skipped (run 'npm install && npm run prepare' after fixing service dependencies)\n",
728
+ );
279
729
  }
280
730
  } else if (config.projectType === "monolith") {
281
731
  // Only setup Husky if installation succeeded
@@ -284,10 +734,14 @@ if (!isInMicroserviceProject) {
284
734
  try {
285
735
  execSync("npm run prepare", { cwd: target, stdio: "inherit" });
286
736
  } catch (error) {
287
- console.log(`\n${pc.yellow("⚠️ Husky setup failed")} ${pc.dim("(run 'npm run prepare' manually after fixing dependencies)")}\n`);
737
+ console.log(
738
+ `\n${pc.yellow("⚠️ Husky setup failed")} ${pc.dim("(run 'npm run prepare' manually after fixing dependencies)")}\n`,
739
+ );
288
740
  }
289
741
  } else {
290
- console.log(`\n${pc.yellow("⚠️ Husky setup skipped")} ${pc.dim("(run 'npm install && npm run prepare' to set up git hooks)")}\n`);
742
+ console.log(
743
+ `\n${pc.yellow("⚠️ Husky setup skipped")} ${pc.dim("(run 'npm install && npm run prepare' to set up git hooks)")}\n`,
744
+ );
291
745
  }
292
746
  }
293
747
  }
@@ -299,29 +753,76 @@ const allServices = fs.existsSync(servicesDir)
299
753
  .readdirSync(servicesDir)
300
754
  .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
301
755
  : servicesToCreate;
756
+ // Update root README when adding services to an existing microservice project
757
+ if (isInMicroserviceProject) {
758
+ try {
759
+ const readmeContent = generateReadme({ ...config, allServices });
760
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
761
+ console.log(`\n${pc.cyan("📝 Updated README.md with new services")}`);
762
+ } catch (e) {
763
+ // non-fatal
764
+ }
765
+ }
302
766
 
303
767
  if (isInMicroserviceProject) {
304
- console.log(`\n${pc.green("✅ Service")} ${pc.bold(servicesToCreate[0])} ${pc.green("added successfully!")}`);
768
+ console.log(
769
+ `\n${pc.green("✅ Service")} ${pc.bold(servicesToCreate[0])} ${pc.green("added successfully!")}`,
770
+ );
305
771
  console.log(`\n${pc.cyan("📦 All services:")} ${allServices.join(", ")}`);
306
772
  console.log(`\n${pc.blue("💡 Next steps:")}`);
307
773
  console.log(
308
774
  mode === "docker"
309
- ? ` ${pc.dim("1.")} Start services: ${pc.bold("docker-compose up")}`
310
- : ` ${pc.dim("1.")} Start services: ${pc.bold("pm2 start pm2.config.js")}`
775
+ ? ` ${pc.dim("1.")} Start services: ${pc.bold("npm run dev")}`
776
+ : ` ${pc.dim("1.")} Start services: ${pc.bold("pm2 start pm2.config.js")}`,
311
777
  );
312
778
  } else if (config.projectType === "microservice") {
313
- console.log(`\n${pc.green("✅ Backend created successfully!")}`);
314
- console.log(`\n${pc.cyan("📦 Created services:")} ${servicesToCreate.join(", ")}`);
779
+ console.log(`\n${pc.green("✅ Microservice Backend created successfully!")}`);
780
+ console.log(
781
+ `\n${pc.cyan("📦 Created services:")} ${servicesToCreate.join(", ")}`,
782
+ );
315
783
  console.log(`\n${pc.blue("💡 Next steps:")}`);
316
784
  console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
317
785
  console.log(
318
786
  mode === "docker"
319
- ? ` ${pc.dim("2.")} Start services: ${pc.bold("docker-compose up")}`
320
- : ` ${pc.dim("2.")} Start services: ${pc.bold("pm2 start pm2.config.js")}`
787
+ ? ` ${pc.dim("2.")} Start services: ${pc.bold("npm run dev")}`
788
+ : ` ${pc.dim("2.")} Start services: ${pc.bold("pm2 start pm2.config.js")}`,
321
789
  );
322
790
  } else {
323
- console.log(`\n${pc.green("✅ Backend created successfully!")}`);
791
+ console.log(`\n${pc.green("✅ Monolith Backend created successfully!")}`);
324
792
  console.log(`\n${pc.blue("💡 Next steps:")}`);
325
793
  console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
326
794
  console.log(` ${pc.dim("2.")} npm run dev`);
327
795
  }
796
+ // Post-processing: ensure shared config does not export/connect to DB when auth is disabled
797
+ try {
798
+ if (!config.auth) {
799
+ const sharedConfigDir = path.join(target, "shared", "config");
800
+ if (fs.existsSync(sharedConfigDir)) {
801
+ for (const ext of ["ts", "js"]) {
802
+ const idxPath = path.join(sharedConfigDir, `index.${ext}`);
803
+ if (!fs.existsSync(idxPath)) continue;
804
+ let idxContent = fs.readFileSync(idxPath, "utf8");
805
+ // Remove various connectDB export/import patterns
806
+ idxContent = idxContent.replace(
807
+ /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
808
+ "",
809
+ );
810
+ idxContent = idxContent.replace(
811
+ /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
812
+ "",
813
+ );
814
+ idxContent = idxContent.replace(
815
+ /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
816
+ "",
817
+ );
818
+ // Remove any remaining connectDB identifiers (commas/newlines)
819
+ idxContent = idxContent.replace(/connectDB,?/g, "");
820
+ // Normalize multiple blank lines
821
+ idxContent = idxContent.replace(/\n{3,}/g, "\n\n");
822
+ fs.writeFileSync(idxPath, idxContent);
823
+ }
824
+ }
825
+ }
826
+ } catch (e) {
827
+ // non-fatal
828
+ }