@ifecodes/backend-template 1.1.5 → 1.1.7

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,119 @@ 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:
375
+ mode === "docker"
376
+ ? "docker-compose up"
377
+ : "npx pm2 start pm2.config.js && npx pm2 logs",
378
+ stop: mode === "docker" ? "docker-compose down" : "npx pm2 kill",
379
+ restart:
380
+ mode === "docker"
381
+ ? "docker-compose restart"
382
+ : "npx pm2 restart all && npx pm2 logs",
383
+ lint: 'eslint "services/**/*.{js,ts,tsx}" "shared/**/*.{js,ts,tsx}"',
384
+ format:
385
+ 'prettier --write "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
386
+ "check-format":
387
+ 'prettier --check "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
240
388
  prepare: "husky install",
241
389
  },
242
390
  devDependencies: {
243
- husky: "^8.0.3",
391
+ husky: "^9.1.7",
392
+ prettier: "^3.7.4",
393
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
394
+ "@typescript-eslint/parser": "^8.50.1",
395
+ eslint: "^9.39.2",
396
+ "eslint-config-prettier": "^10.1.8",
244
397
  },
245
398
  };
399
+
400
+ // Add runtime dependencies for non-Docker (PM2) mode
401
+ if (mode !== "docker") {
402
+ rootPackageJson.dependencies = {
403
+ dotenv: "^17.2.3",
404
+ pm2: "^6.0.14",
405
+ "ts-node": "^10.9.2",
406
+ "tsconfig-paths": "^4.2.0",
407
+ };
408
+ }
246
409
  fs.writeFileSync(
247
410
  rootPackageJsonPath,
248
- JSON.stringify(rootPackageJson, null, 2) + "\n"
411
+ JSON.stringify(rootPackageJson, null, 2) + "\n",
249
412
  );
250
413
  }
251
414
 
415
+ // Ensure root lint/format config files exist (copy from template base if available), and remove any per-service copies
416
+ try {
417
+ const rootFiles = [".prettierrc", ".prettierignore", ".eslintrc.json"];
418
+ for (const f of rootFiles) {
419
+ const src = path.join(base, f);
420
+ const dest = path.join(target, f);
421
+ if (fs.existsSync(src)) {
422
+ fs.copyFileSync(src, dest);
423
+ } else if (!fs.existsSync(dest)) {
424
+ // create minimal defaults
425
+ if (f === ".prettierignore")
426
+ fs.writeFileSync(dest, "node_modules\n" + "dist\n");
427
+ else if (f === ".eslintrc.json")
428
+ fs.writeFileSync(dest, JSON.stringify({ root: true }, null, 2));
429
+ else fs.writeFileSync(dest, "{}");
430
+ }
431
+ }
432
+
433
+ // Write eslint.config.js with recommended workspace config (overwrite)
434
+ const eslintConfigPath = path.join(target, "eslint.config.js");
435
+
436
+ // Build dynamic project list for TypeScript projects based on the services present
437
+ const projectPaths = ["./tsconfig.json"];
438
+ try {
439
+ if (typeof allServices !== "undefined" && Array.isArray(allServices)) {
440
+ for (const svc of allServices) {
441
+ const svcTsPath = `./services/${svc}/tsconfig.json`;
442
+ if (fs.existsSync(path.join(target, svcTsPath))) {
443
+ projectPaths.push(svcTsPath);
444
+ }
445
+ }
446
+ }
447
+ } catch (e) {
448
+ // non-fatal; fall back to default projectPaths containing only root tsconfig
449
+ }
450
+
451
+ const projectEntries = projectPaths
452
+ .map((p) => ` "${p}",`)
453
+ .join("\n");
454
+
455
+ 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`;
456
+ fs.writeFileSync(eslintConfigPath, eslintConfigContent);
457
+
458
+ // Remove per-service copies if they exist (already removed in setupService, but double-check)
459
+ const servicesDirPath = path.join(target, "services");
460
+ if (fs.existsSync(servicesDirPath)) {
461
+ const svcs = fs
462
+ .readdirSync(servicesDirPath)
463
+ .filter((f) =>
464
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
465
+ );
466
+ for (const svc of svcs) {
467
+ for (const f of [...rootFiles, "eslint.config.js"]) {
468
+ const p = path.join(servicesDirPath, svc, f);
469
+ if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
470
+ }
471
+ }
472
+ }
473
+ } catch (err) {
474
+ // non-fatal
475
+ }
476
+
252
477
  // Step 3: Generate README and create root configuration files
253
478
  if (!isInMicroserviceProject) {
254
479
  console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
255
480
  const readmeContent = generateReadme(config);
256
481
  fs.writeFileSync(path.join(target, "README.md"), readmeContent);
257
-
482
+
258
483
  // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
259
484
  for (const service of allServices) {
260
485
  const gitignorePath = path.join(servicesDir, service, "gitignore");
@@ -263,60 +488,173 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
263
488
  fs.renameSync(gitignorePath, dotGitignorePath);
264
489
  }
265
490
  }
266
-
491
+
267
492
  // Create root .gitignore for microservices
268
493
  const rootGitignoreContent = `.env\nnode_modules\n`;
269
494
  fs.writeFileSync(path.join(target, ".gitignore"), rootGitignoreContent);
270
495
 
271
496
  // Create root .env and .env.example for microservices
272
497
  let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
273
-
498
+
274
499
  // Add port configuration for each service
275
500
  allServices.forEach((service, index) => {
276
501
  const isGateway = service === "gateway";
277
- const port = isGateway ? 4000 : 4001 + allServices.filter((s, i) => s !== "gateway" && i < index).length;
502
+ const port = isGateway
503
+ ? 4000
504
+ : 4001 +
505
+ allServices.filter((s, i) => s !== "gateway" && i < index).length;
278
506
  const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
279
- const serviceName = service.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
507
+ const serviceName = service
508
+ .split("-")
509
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
510
+ .join(" ");
280
511
  rootENVContent += `# ${serviceName}\n${envVarName}=${port}\n\n`;
281
512
  });
282
-
513
+
283
514
  fs.writeFileSync(path.join(target, ".env"), rootENVContent);
284
515
  fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
285
516
 
286
517
  // Create root tsconfig.json for microservices workspace
287
518
  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
- }
519
+ compilerOptions: {
520
+ target: "ES2020",
521
+ module: "CommonJS",
522
+ lib: ["ES2020"],
523
+ moduleResolution: "node",
524
+ esModuleInterop: true,
525
+ skipLibCheck: true,
526
+ strict: true,
527
+ baseUrl: ".",
528
+ paths: {
529
+ "@/*": ["./*"],
530
+ },
300
531
  },
301
- "include": [],
302
- "references": allServices.map(service => ({
303
- "path": `./services/${service}`
304
- }))
532
+ include: [],
533
+ exclude: ["node_modules", "dist"],
534
+ references: allServices.map((service) => ({
535
+ path: `./services/${service}`,
536
+ })),
305
537
  };
306
538
  fs.writeFileSync(
307
539
  path.join(target, "tsconfig.json"),
308
- JSON.stringify(rootTsConfigContent, null, 2) + "\n"
540
+ JSON.stringify(rootTsConfigContent, null, 2) + "\n",
309
541
  );
310
542
  }
311
543
 
544
+ // If we're adding a service into an existing microservice project,
545
+ // ensure shared config and gateway are updated to reference the new service.
546
+ if (isInMicroserviceProject) {
547
+ try {
548
+ const sharedConfigDir = path.join(target, "shared", "config");
549
+ const languageExt = config.language === "javascript" ? "js" : "ts";
550
+ const sharedEnvPath = path.join(sharedConfigDir, `env.${languageExt}`);
551
+
552
+ if (fs.existsSync(sharedEnvPath)) {
553
+ let envContent = fs.readFileSync(sharedEnvPath, "utf8");
554
+
555
+ // Build port environment variables for all services
556
+ const portEnvVars = allServices
557
+ .map((service) => {
558
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
559
+ const assertion = config.language === "javascript" ? "" : "!";
560
+ return ` ${envVarName}: process.env.${envVarName}${assertion},`;
561
+ })
562
+ .join("\n");
563
+
564
+ // Remove any existing *_PORT lines to avoid duplication
565
+ envContent = envContent.replace(
566
+ /^[ \t]*[A-Z0-9_]+_PORT:\s*process\.env\.[A-Z0-9_]+!?\,?\s*$/gim,
567
+ "",
568
+ );
569
+ // Normalize multiple consecutive blank lines
570
+ envContent = envContent.replace(/\n{2,}/g, "\n\n");
571
+
572
+ // Attempt several fallback strategies to inject port variables:
573
+ // 1. Replace explicit placeholder if present in template
574
+ // 2. Insert right after the first object opening brace
575
+ // 3. Append to the end as a last resort
576
+ if (envContent.includes("/*__PORTS__*/")) {
577
+ envContent = envContent.replace("/*__PORTS__*/", portEnvVars);
578
+ } else {
579
+ // Fallback: find the opening brace of the exported ENV object and insert after it
580
+ const braceIndex = envContent.indexOf("{");
581
+ if (braceIndex !== -1) {
582
+ const insertPos =
583
+ envContent.indexOf("\n", braceIndex) + 1 || braceIndex + 1;
584
+ envContent =
585
+ envContent.slice(0, insertPos) +
586
+ portEnvVars +
587
+ "\n" +
588
+ envContent.slice(insertPos);
589
+ } else {
590
+ // Final fallback: append to the end
591
+ envContent = envContent + "\n" + portEnvVars;
592
+ }
593
+ }
594
+
595
+ fs.writeFileSync(sharedEnvPath, envContent);
596
+ }
597
+
598
+ // Re-generate gateway routes if gateway exists (so new service gets proxied)
599
+ const gatewayRoot = path.join(target, "services", "gateway");
600
+ if (fs.existsSync(gatewayRoot)) {
601
+ // Re-run setupService for gateway to rewrite app/server/env files
602
+ await setupService(
603
+ config,
604
+ "gateway",
605
+ gatewayRoot,
606
+ config.auth,
607
+ allServices,
608
+ true,
609
+ );
610
+ }
611
+
612
+ // Update root .env and .env.example so newly added services have port entries
613
+ try {
614
+ const servicesDirPath = path.join(target, "services");
615
+ const svcList = fs.existsSync(servicesDirPath)
616
+ ? fs
617
+ .readdirSync(servicesDirPath)
618
+ .filter((f) =>
619
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
620
+ )
621
+ : allServices;
622
+
623
+ let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
624
+ svcList.forEach((service, index) => {
625
+ const isGateway = service === "gateway";
626
+ const port = isGateway
627
+ ? 4000
628
+ : 4001 +
629
+ svcList.filter((s, i) => s !== "gateway" && i < index).length;
630
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
631
+ const serviceNamePretty = service
632
+ .split("-")
633
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
634
+ .join(" ");
635
+ rootENVContent += `# ${serviceNamePretty}\n${envVarName}=${port}\n\n`;
636
+ });
637
+
638
+ fs.writeFileSync(path.join(target, ".env"), rootENVContent);
639
+ fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
640
+ } catch (e) {
641
+ // non-fatal
642
+ }
643
+ } catch (e) {
644
+ // non-fatal; continue even if updating shared/gateway fails
645
+ }
646
+ }
647
+
312
648
  // Step 5: Install dependencies for all services
313
649
 
314
650
  console.log(pc.cyan("\n📦 Installing dependencies for all services...\n"));
315
651
  let allInstallsSucceeded = true;
316
652
 
317
653
  for (const { serviceName, serviceRoot, deps, devDeps } of serviceConfigs) {
318
- console.log(pc.cyan(`\n📦 Installing dependencies for ${serviceName}...\n`));
319
-
654
+ console.log(
655
+ pc.cyan(`\n📦 Installing dependencies for ${serviceName}...\n`),
656
+ );
657
+
320
658
  try {
321
659
  if (deps.length) {
322
660
  execSync(`npm install ${deps.join(" ")}`, {
@@ -331,18 +669,6 @@ if (isInMicroserviceProject || config.projectType === "microservice") {
331
669
  });
332
670
  }
333
671
  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
672
  } catch (error) {
347
673
  allInstallsSucceeded = false;
348
674
  console.error(
@@ -365,14 +691,14 @@ if (!isInMicroserviceProject && config.projectType === "monolith") {
365
691
  console.log(`\n${pc.cyan("📝 Generating README.md...")}\n`);
366
692
  const readmeContent = generateReadme(config);
367
693
  fs.writeFileSync(path.join(target, "README.md"), readmeContent);
368
-
694
+
369
695
  // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
370
696
  const gitignorePath = path.join(target, "gitignore");
371
697
  const dotGitignorePath = path.join(target, ".gitignore");
372
698
  if (fs.existsSync(gitignorePath)) {
373
699
  fs.renameSync(gitignorePath, dotGitignorePath);
374
700
  }
375
-
701
+
376
702
  // Generate .env from .env.example for monolith only
377
703
  console.log(`${pc.cyan("📄 Setting up environment files...")}\n`);
378
704
  try {
@@ -390,9 +716,9 @@ if (!isInMicroserviceProject && config.projectType === "monolith") {
390
716
  if (!isInMicroserviceProject) {
391
717
  execSync("git init", { cwd: target, stdio: "inherit" });
392
718
 
393
- // Install husky and setup at root level
719
+ // Install husky and other devDeps and setup at root level
394
720
  if (config.projectType === "microservice") {
395
- console.log("\n📦 Installing Husky at root level...\n");
721
+ console.log("\n📦 Installing dependencies at root level...\n");
396
722
  if (config.allInstallsSucceeded) {
397
723
  try {
398
724
  execSync("npm install", { cwd: target, stdio: "inherit" });
@@ -401,8 +727,21 @@ if (!isInMicroserviceProject) {
401
727
  } catch (error) {
402
728
  console.log("\n⚠️ Husky setup failed\n");
403
729
  }
730
+ // Run format after successful install
731
+ console.log(pc.cyan("\n🎨 Formatting code...\n"));
732
+ try {
733
+ execSync("npm run format", { cwd: target, stdio: "inherit" });
734
+ } catch (formatError) {
735
+ console.warn(
736
+ pc.yellow(
737
+ "⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n",
738
+ ),
739
+ );
740
+ }
404
741
  } else {
405
- console.log("\n⚠️ Husky setup skipped (run 'npm install && npm run prepare' after fixing service dependencies)\n");
742
+ console.log(
743
+ "\n⚠️ Husky setup skipped (run 'npm install && npm run prepare' after fixing service dependencies)\n",
744
+ );
406
745
  }
407
746
  } else if (config.projectType === "monolith") {
408
747
  // Only setup Husky if installation succeeded
@@ -411,10 +750,14 @@ if (!isInMicroserviceProject) {
411
750
  try {
412
751
  execSync("npm run prepare", { cwd: target, stdio: "inherit" });
413
752
  } catch (error) {
414
- console.log(`\n${pc.yellow("⚠️ Husky setup failed")} ${pc.dim("(run 'npm run prepare' manually after fixing dependencies)")}\n`);
753
+ console.log(
754
+ `\n${pc.yellow("⚠️ Husky setup failed")} ${pc.dim("(run 'npm run prepare' manually after fixing dependencies)")}\n`,
755
+ );
415
756
  }
416
757
  } else {
417
- console.log(`\n${pc.yellow("⚠️ Husky setup skipped")} ${pc.dim("(run 'npm install && npm run prepare' to set up git hooks)")}\n`);
758
+ console.log(
759
+ `\n${pc.yellow("⚠️ Husky setup skipped")} ${pc.dim("(run 'npm install && npm run prepare' to set up git hooks)")}\n`,
760
+ );
418
761
  }
419
762
  }
420
763
  }
@@ -426,29 +769,76 @@ const allServices = fs.existsSync(servicesDir)
426
769
  .readdirSync(servicesDir)
427
770
  .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
428
771
  : servicesToCreate;
772
+ // Update root README when adding services to an existing microservice project
773
+ if (isInMicroserviceProject) {
774
+ try {
775
+ const readmeContent = generateReadme({ ...config, allServices });
776
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
777
+ console.log(`\n${pc.cyan("📝 Updated README.md with new services")}`);
778
+ } catch (e) {
779
+ // non-fatal
780
+ }
781
+ }
429
782
 
430
783
  if (isInMicroserviceProject) {
431
- console.log(`\n${pc.green("✅ Service")} ${pc.bold(servicesToCreate[0])} ${pc.green("added successfully!")}`);
784
+ console.log(
785
+ `\n${pc.green("✅ Service")} ${pc.bold(servicesToCreate[0])} ${pc.green("added successfully!")}`,
786
+ );
432
787
  console.log(`\n${pc.cyan("📦 All services:")} ${allServices.join(", ")}`);
433
788
  console.log(`\n${pc.blue("💡 Next steps:")}`);
434
789
  console.log(
435
790
  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")}`
791
+ ? ` ${pc.dim("1.")} Start services: ${pc.bold("npm run dev")}`
792
+ : ` ${pc.dim("1.")} Start services: ${pc.bold("pm2 start pm2.config.js")}`,
438
793
  );
439
794
  } else if (config.projectType === "microservice") {
440
- console.log(`\n${pc.green("✅ Backend created successfully!")}`);
441
- console.log(`\n${pc.cyan("📦 Created services:")} ${servicesToCreate.join(", ")}`);
795
+ console.log(`\n${pc.green("✅ Microservice Backend created successfully!")}`);
796
+ console.log(
797
+ `\n${pc.cyan("📦 Created services:")} ${servicesToCreate.join(", ")}`,
798
+ );
442
799
  console.log(`\n${pc.blue("💡 Next steps:")}`);
443
800
  console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
444
801
  console.log(
445
802
  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")}`
803
+ ? ` ${pc.dim("2.")} Start services: ${pc.bold("npm run dev")}`
804
+ : ` ${pc.dim("2.")} Start services: ${pc.bold("pm2 start pm2.config.js")}`,
448
805
  );
449
806
  } else {
450
- console.log(`\n${pc.green("✅ Backend created successfully!")}`);
807
+ console.log(`\n${pc.green("✅ Monolith Backend created successfully!")}`);
451
808
  console.log(`\n${pc.blue("💡 Next steps:")}`);
452
809
  console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
453
810
  console.log(` ${pc.dim("2.")} npm run dev`);
454
811
  }
812
+ // Post-processing: ensure shared config does not export/connect to DB when auth is disabled
813
+ try {
814
+ if (!config.auth) {
815
+ const sharedConfigDir = path.join(target, "shared", "config");
816
+ if (fs.existsSync(sharedConfigDir)) {
817
+ for (const ext of ["ts", "js"]) {
818
+ const idxPath = path.join(sharedConfigDir, `index.${ext}`);
819
+ if (!fs.existsSync(idxPath)) continue;
820
+ let idxContent = fs.readFileSync(idxPath, "utf8");
821
+ // Remove various connectDB export/import patterns
822
+ idxContent = idxContent.replace(
823
+ /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
824
+ "",
825
+ );
826
+ idxContent = idxContent.replace(
827
+ /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
828
+ "",
829
+ );
830
+ idxContent = idxContent.replace(
831
+ /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
832
+ "",
833
+ );
834
+ // Remove any remaining connectDB identifiers (commas/newlines)
835
+ idxContent = idxContent.replace(/connectDB,?/g, "");
836
+ // Normalize multiple blank lines
837
+ idxContent = idxContent.replace(/\n{3,}/g, "\n\n");
838
+ fs.writeFileSync(idxPath, idxContent);
839
+ }
840
+ }
841
+ }
842
+ } catch (e) {
843
+ // non-fatal
844
+ }