@ifecodes/backend-template 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -27,9 +27,10 @@ const {
27
27
  isInMicroserviceProject,
28
28
  } = config;
29
29
 
30
- const baseRoot = (config.language === "javascript")
31
- ? path.join(__dirname, "../template/base/js")
32
- : path.join(__dirname, "../template/base/ts");
30
+ const baseRoot =
31
+ config.language === "javascript"
32
+ ? path.join(__dirname, "../template/base/js")
33
+ : path.join(__dirname, "../template/base/ts");
33
34
  const base = baseRoot;
34
35
 
35
36
  // Determine which services to create
@@ -48,38 +49,56 @@ if (isInMicroserviceProject) {
48
49
  // Validate and prepare project
49
50
  if (!isInMicroserviceProject && config.projectType === "microservice") {
50
51
  if (isExistingProject) {
51
- 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
+ );
52
55
  process.exit(1);
53
56
  }
54
57
  console.log(
55
- `\n${pc.cyan("🏗️ Creating microservices:")} ${pc.bold(servicesToCreate.join(", "))}...\n`
58
+ `\n${pc.cyan("🏗️ Creating microservices:")} ${pc.bold(servicesToCreate.join(", "))}...\n`,
56
59
  );
57
60
  } else if (!isInMicroserviceProject && config.projectType === "monolith") {
58
61
  if (isExistingProject) {
59
- 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
+ );
60
65
  process.exit(1);
61
66
  }
62
67
  fs.cpSync(base, target, { recursive: true });
63
-
64
- // Remove db.ts from config if auth is not enabled
68
+
69
+ // Remove db file and remove connectDB export/import if auth is not enabled
65
70
  if (!config.auth) {
66
- const dbPath = path.join(target, "src/config/db.ts");
71
+ const ext = config.language === "javascript" ? "js" : "ts";
72
+ const dbPath = path.join(target, `src/config/db.${ext}`);
67
73
  if (fs.existsSync(dbPath)) {
68
74
  fs.rmSync(dbPath);
69
75
  }
70
-
71
- // Update index.ts to not export connectDB
72
- const indexPath = path.join(target, "src/config/index.ts");
76
+
77
+ // Update index.(js|ts) to not export or require connectDB
78
+ const indexPath = path.join(target, `src/config/index.${ext}`);
73
79
  if (fs.existsSync(indexPath)) {
74
80
  let indexContent = fs.readFileSync(indexPath, "utf8");
75
- indexContent = indexContent.replace('export { connectDB } from "./db";\n', '');
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
+ }
76
93
  fs.writeFileSync(indexPath, indexContent);
77
94
  }
78
95
  }
79
-
96
+
80
97
  // No TypeScript-to-JavaScript conversion — templates include language-specific variants
81
98
  } else if (isInMicroserviceProject) {
82
- 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
+ );
83
102
  }
84
103
 
85
104
  // Process services
@@ -88,7 +107,9 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
88
107
  if (!isInMicroserviceProject) {
89
108
  const sharedDir = path.join(target, "shared");
90
109
  if (!fs.existsSync(sharedDir)) {
91
- 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
+ );
92
113
  fs.mkdirSync(sharedDir, { recursive: true });
93
114
 
94
115
  // Copy config and utils from base template
@@ -100,48 +121,124 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
100
121
  fs.cpSync(baseConfigDir, sharedConfigDir, { recursive: true });
101
122
  fs.cpSync(baseUtilsDir, sharedUtilsDir, { recursive: true });
102
123
 
103
- // Remove db.ts from shared config if auth is not enabled
124
+ // Remove db files and strip connectDB exports/imports when auth is not enabled
104
125
  if (!config.auth) {
105
- const sharedDbPath = path.join(sharedConfigDir, "db.ts");
106
- if (fs.existsSync(sharedDbPath)) {
107
- fs.rmSync(sharedDbPath);
108
- }
109
-
110
- // Update index.ts to not export connectDB
111
- const sharedIndexPath = path.join(sharedConfigDir, "index.ts");
112
- if (fs.existsSync(sharedIndexPath)) {
113
- let indexContent = fs.readFileSync(sharedIndexPath, "utf8");
114
- indexContent = indexContent.replace('export { connectDB } from "./db";\n', '');
115
- fs.writeFileSync(sharedIndexPath, indexContent);
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
+ }
116
150
  }
117
151
  }
152
+ const ext = config.language === "javascript" ? "js" : "ts";
118
153
 
119
154
  // Update shared env.ts to include all service port environment variables
120
- const sharedEnvPath = path.join(sharedConfigDir, "env.ts");
155
+ const sharedEnvPath = path.join(sharedConfigDir, `env.${ext}`);
121
156
  if (fs.existsSync(sharedEnvPath)) {
122
157
  let envContent = fs.readFileSync(sharedEnvPath, "utf8");
123
-
158
+ console.log(`\n${pc.cyan("🔧 Updating shared env configuration...")}`);
159
+
124
160
  // Build port environment variables for all services
125
161
  const allServices = ["gateway", "health-service"];
126
162
  if (config.auth) allServices.push("auth-service");
127
-
128
- const portEnvVars = allServices.map((service) => {
129
- const isGateway = service === "gateway";
130
- const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
131
- // Don't add ! for JavaScript projects - it will cause syntax errors
132
- const assertion = config.language === "javascript" ? "" : "!";
133
- return ` ${envVarName}: process.env.${envVarName}${assertion},`;
134
- }).join("\n");
135
-
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
+
136
173
  // Replace PORT with service-specific ports
137
174
  envContent = envContent.replace(
138
175
  " PORT: process.env.PORT!,",
139
- portEnvVars
176
+ portEnvVars,
140
177
  );
141
-
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
+
142
206
  fs.writeFileSync(sharedEnvPath, envContent);
143
207
  }
144
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
+
145
242
  // Create shared package.json
146
243
  const sharedPackageJson = {
147
244
  name: "@shared/common",
@@ -154,7 +251,7 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
154
251
  };
155
252
  fs.writeFileSync(
156
253
  path.join(sharedDir, "package.json"),
157
- JSON.stringify(sharedPackageJson, null, 2)
254
+ JSON.stringify(sharedPackageJson, null, 2),
158
255
  );
159
256
  }
160
257
  }
@@ -189,16 +286,21 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
189
286
 
190
287
  // Get all services first (needed for gateway routing)
191
288
  const servicesDir = path.join(target, "services");
192
- const allServices = fs.existsSync(servicesDir)
289
+ const existingServices = fs.existsSync(servicesDir)
193
290
  ? fs
194
291
  .readdirSync(servicesDir)
195
292
  .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
196
- : 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
+ );
197
299
 
198
300
  // Step 1: Setup all service files first (without installing dependencies)
199
301
  console.log(pc.cyan("\n⚙️ Setting up service files...\n"));
200
302
  const serviceConfigs = [];
201
-
303
+
202
304
  for (const serviceName of servicesToCreate) {
203
305
  const serviceRoot = path.join(target, "services", serviceName);
204
306
  const shouldIncludeAuth = isInMicroserviceProject
@@ -210,19 +312,50 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
210
312
  serviceRoot,
211
313
  shouldIncludeAuth,
212
314
  allServices,
213
- true // Skip install for now
315
+ true, // Skip install for now
214
316
  );
215
317
  serviceConfigs.push({
216
318
  serviceName,
217
319
  serviceRoot,
218
320
  deps: result.deps,
219
- devDeps: result.devDeps
321
+ devDeps: result.devDeps,
220
322
  });
221
323
  }
222
324
 
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
+ : [];
335
+
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
+ }
342
+ }
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
354
+ }
355
+
223
356
  // Step 2: Generate docker-compose/pm2 config and root files
224
357
  if (mode === "docker") {
225
- generateDockerCompose(target, allServices);
358
+ generateDockerCompose(target, allServices, config.sanitizedName);
226
359
  copyDockerfile(target, servicesToCreate);
227
360
  copyDockerignore(target, servicesToCreate);
228
361
  } else {
@@ -234,27 +367,103 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
234
367
  if (!fs.existsSync(rootPackageJsonPath)) {
235
368
  const rootPackageJson = {
236
369
  name: sanitizedName,
237
- version: "1.0.0",
370
+ version: config.version || "1.0.0",
371
+ description: config.description || "",
238
372
  private: true,
239
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}"',
240
382
  prepare: "husky install",
241
383
  },
242
384
  devDependencies: {
243
- 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",
244
391
  },
245
392
  };
246
393
  fs.writeFileSync(
247
394
  rootPackageJsonPath,
248
- JSON.stringify(rootPackageJson, null, 2) + "\n"
395
+ JSON.stringify(rootPackageJson, null, 2) + "\n",
249
396
  );
250
397
  }
251
398
 
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
+
252
461
  // Step 3: Generate README and create root configuration files
253
462
  if (!isInMicroserviceProject) {
254
463
  console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
255
464
  const readmeContent = generateReadme(config);
256
465
  fs.writeFileSync(path.join(target, "README.md"), readmeContent);
257
-
466
+
258
467
  // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
259
468
  for (const service of allServices) {
260
469
  const gitignorePath = path.join(servicesDir, service, "gitignore");
@@ -263,60 +472,173 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
263
472
  fs.renameSync(gitignorePath, dotGitignorePath);
264
473
  }
265
474
  }
266
-
475
+
267
476
  // Create root .gitignore for microservices
268
477
  const rootGitignoreContent = `.env\nnode_modules\n`;
269
478
  fs.writeFileSync(path.join(target, ".gitignore"), rootGitignoreContent);
270
479
 
271
480
  // Create root .env and .env.example for microservices
272
481
  let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
273
-
482
+
274
483
  // Add port configuration for each service
275
484
  allServices.forEach((service, index) => {
276
485
  const isGateway = service === "gateway";
277
- const port = isGateway ? 4000 : 4001 + allServices.filter((s, i) => s !== "gateway" && i < index).length;
486
+ const port = isGateway
487
+ ? 4000
488
+ : 4001 +
489
+ allServices.filter((s, i) => s !== "gateway" && i < index).length;
278
490
  const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
279
- const serviceName = service.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
491
+ const serviceName = service
492
+ .split("-")
493
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
494
+ .join(" ");
280
495
  rootENVContent += `# ${serviceName}\n${envVarName}=${port}\n\n`;
281
496
  });
282
-
497
+
283
498
  fs.writeFileSync(path.join(target, ".env"), rootENVContent);
284
499
  fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
285
500
 
286
501
  // Create root tsconfig.json for microservices workspace
287
502
  const rootTsConfigContent = {
288
- "compilerOptions": {
289
- "target": "ES2020",
290
- "module": "CommonJS",
291
- "lib": ["ES2020"],
292
- "moduleResolution": "node",
293
- "esModuleInterop": true,
294
- "skipLibCheck": true,
295
- "strict": true,
296
- "baseUrl": ".",
297
- "paths": {
298
- "@/*": ["./*"]
299
- }
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
+ },
300
515
  },
301
- "include": [],
302
- "references": allServices.map(service => ({
303
- "path": `./services/${service}`
304
- }))
516
+ include: [],
517
+ exclude: ["node_modules", "dist"],
518
+ references: allServices.map((service) => ({
519
+ path: `./services/${service}`,
520
+ })),
305
521
  };
306
522
  fs.writeFileSync(
307
523
  path.join(target, "tsconfig.json"),
308
- JSON.stringify(rootTsConfigContent, null, 2) + "\n"
524
+ JSON.stringify(rootTsConfigContent, null, 2) + "\n",
309
525
  );
310
526
  }
311
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
629
+ }
630
+ }
631
+
312
632
  // Step 5: Install dependencies for all services
313
633
 
314
634
  console.log(pc.cyan("\n📦 Installing dependencies for all services...\n"));
315
635
  let allInstallsSucceeded = true;
316
636
 
317
637
  for (const { serviceName, serviceRoot, deps, devDeps } of serviceConfigs) {
318
- console.log(pc.cyan(`\n📦 Installing dependencies for ${serviceName}...\n`));
319
-
638
+ console.log(
639
+ pc.cyan(`\n📦 Installing dependencies for ${serviceName}...\n`),
640
+ );
641
+
320
642
  try {
321
643
  if (deps.length) {
322
644
  execSync(`npm install ${deps.join(" ")}`, {
@@ -331,18 +653,6 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
331
653
  });
332
654
  }
333
655
  execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
334
-
335
- // Run format after successful install
336
- console.log(pc.cyan("\n🎨 Formatting code...\n"));
337
- try {
338
- execSync("npm run format", { cwd: serviceRoot, stdio: "inherit" });
339
- } catch (formatError) {
340
- console.warn(
341
- pc.yellow(
342
- "⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n",
343
- ),
344
- );
345
- }
346
656
  } catch (error) {
347
657
  allInstallsSucceeded = false;
348
658
  console.error(
@@ -365,14 +675,14 @@ if (!isInMicroserviceProject && config.projectType === "monolith") {
365
675
  console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
366
676
  const readmeContent = generateReadme(config);
367
677
  fs.writeFileSync(path.join(target, "README.md"), readmeContent);
368
-
678
+
369
679
  // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
370
680
  const gitignorePath = path.join(target, "gitignore");
371
681
  const dotGitignorePath = path.join(target, ".gitignore");
372
682
  if (fs.existsSync(gitignorePath)) {
373
683
  fs.renameSync(gitignorePath, dotGitignorePath);
374
684
  }
375
-
685
+
376
686
  // Generate .env from .env.example for monolith only
377
687
  console.log(`${pc.cyan("📄 Setting up environment files...")}\n`);
378
688
  try {
@@ -390,9 +700,9 @@ if (!isInMicroserviceProject && config.projectType === "monolith") {
390
700
  if (!isInMicroserviceProject) {
391
701
  execSync("git init", { cwd: target, stdio: "inherit" });
392
702
 
393
- // Install husky and setup at root level
703
+ // Install husky and other devDeps and setup at root level
394
704
  if (config.projectType === "microservice") {
395
- console.log("\n📦 Installing Husky at root level...\n");
705
+ console.log("\n📦 Installing dependencies at root level...\n");
396
706
  if (config.allInstallsSucceeded) {
397
707
  try {
398
708
  execSync("npm install", { cwd: target, stdio: "inherit" });
@@ -401,8 +711,21 @@ if (!isInMicroserviceProject) {
401
711
  } catch (error) {
402
712
  console.log("\n⚠️ Husky setup failed\n");
403
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
+ }
404
725
  } else {
405
- 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
+ );
406
729
  }
407
730
  } else if (config.projectType === "monolith") {
408
731
  // Only setup Husky if installation succeeded
@@ -411,10 +734,14 @@ if (!isInMicroserviceProject) {
411
734
  try {
412
735
  execSync("npm run prepare", { cwd: target, stdio: "inherit" });
413
736
  } catch (error) {
414
- 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
+ );
415
740
  }
416
741
  } else {
417
- 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
+ );
418
745
  }
419
746
  }
420
747
  }
@@ -426,29 +753,76 @@ const allServices = fs.existsSync(servicesDir)
426
753
  .readdirSync(servicesDir)
427
754
  .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
428
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
+ }
429
766
 
430
767
  if (isInMicroserviceProject) {
431
- 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
+ );
432
771
  console.log(`\n${pc.cyan("📦 All services:")} ${allServices.join(", ")}`);
433
772
  console.log(`\n${pc.blue("💡 Next steps:")}`);
434
773
  console.log(
435
774
  mode === "docker"
436
- ? ` ${pc.dim("1.")} Start services: ${pc.bold("docker-compose up")}`
437
- : ` ${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")}`,
438
777
  );
439
778
  } else if (config.projectType === "microservice") {
440
- console.log(`\n${pc.green("✅ Backend created successfully!")}`);
441
- 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
+ );
442
783
  console.log(`\n${pc.blue("💡 Next steps:")}`);
443
784
  console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
444
785
  console.log(
445
786
  mode === "docker"
446
- ? ` ${pc.dim("2.")} Start services: ${pc.bold("docker-compose up")}`
447
- : ` ${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")}`,
448
789
  );
449
790
  } else {
450
- console.log(`\n${pc.green("✅ Backend created successfully!")}`);
791
+ console.log(`\n${pc.green("✅ Monolith Backend created successfully!")}`);
451
792
  console.log(`\n${pc.blue("💡 Next steps:")}`);
452
793
  console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
453
794
  console.log(` ${pc.dim("2.")} npm run dev`);
454
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
+ }