@ifecodes/backend-template 1.1.1 → 1.1.5

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 +205 -149
  3. package/bin/lib/microservice-config.js +57 -13
  4. package/bin/lib/prompts.js +42 -11
  5. package/bin/lib/readme-generator.js +75 -14
  6. package/bin/lib/service-setup.js +277 -123
  7. package/package.json +1 -1
  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 -47
  45. package/template/base/ts/src/utils/logger.ts +68 -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/{app.ts → ts/app.ts} +19 -3
  63. package/template/gateway/ts/inject.js +31 -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 -123
  69. package/template/base/src/app.ts +0 -8
  70. package/template/base/src/utils/logger.ts +0 -35
  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
@@ -8,14 +8,14 @@ import { fileURLToPath } from "url";
8
8
  // Helper function to get the correct file extension (.ts or .js)
9
9
  function getFileExtension(dir) {
10
10
  // Check if .ts files exist, otherwise use .js
11
- const sampleFiles = ['src/app.ts', 'src/server.ts', 'src/routes.ts'];
11
+ const sampleFiles = ["src/app.ts", "src/server.ts", "src/routes.ts"];
12
12
  for (const file of sampleFiles) {
13
13
  const tsPath = path.join(dir, file);
14
- if (fs.existsSync(tsPath)) return 'ts';
15
- const jsPath = path.join(dir, file.replace('.ts', '.js'));
16
- if (fs.existsSync(jsPath)) return 'js';
14
+ if (fs.existsSync(tsPath)) return "ts";
15
+ const jsPath = path.join(dir, file.replace(".ts", ".js"));
16
+ if (fs.existsSync(jsPath)) return "js";
17
17
  }
18
- return 'ts'; // default to ts
18
+ return "ts"; // default to ts
19
19
  }
20
20
 
21
21
  export const setupService = async (
@@ -23,7 +23,8 @@ export const setupService = async (
23
23
  serviceName,
24
24
  serviceRoot,
25
25
  shouldIncludeAuth,
26
- allServices = []
26
+ allServices = [],
27
+ skipInstall = false,
27
28
  ) => {
28
29
  let imports = [];
29
30
  let middlewares = [];
@@ -31,13 +32,26 @@ export const setupService = async (
31
32
  let devDeps = [];
32
33
  let v1Imports = [];
33
34
  let v1Routes = [];
34
-
35
+
35
36
  // Detect file extension (ts or js)
36
37
  const ext = getFileExtension(serviceRoot);
37
38
 
39
+ // Ensure service-level gitignore is renamed immediately after template copy
40
+ try {
41
+ const serviceGitignore = path.join(serviceRoot, "gitignore");
42
+ const serviceDotGitignore = path.join(serviceRoot, ".gitignore");
43
+ if (fs.existsSync(serviceGitignore) && !fs.existsSync(serviceDotGitignore)) {
44
+ fs.renameSync(serviceGitignore, serviceDotGitignore);
45
+ }
46
+ } catch (err) {
47
+ // Non-fatal; continue setup
48
+ }
49
+
38
50
  // Special handling for gateway service
39
51
  if (serviceName === "gateway") {
40
- const gatewayModule = await import("../../template/gateway/inject.js");
52
+ const gatewayModule = await import(
53
+ `../../template/gateway/${res.language}/inject.js`
54
+ );
41
55
  deps.push(...gatewayModule.gatewayDeps);
42
56
 
43
57
  // Copy gateway-specific files
@@ -45,17 +59,24 @@ export const setupService = async (
45
59
  const gatewayServerPath = path.join(serviceRoot, `src/server.${ext}`);
46
60
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
47
61
 
62
+ // Read gateway template files according to selected language
63
+ const templateExt = res.language === "javascript" ? ".js" : ".ts";
64
+ const templateDir = path.join(
65
+ __dirname,
66
+ `../../template/gateway/${res.language}`,
67
+ );
48
68
  const gatewayAppContent = fs.readFileSync(
49
- path.join(__dirname, "../../template/gateway/app.ts"),
50
- "utf8"
69
+ path.join(templateDir, `app${templateExt}`),
70
+ "utf8",
51
71
  );
52
72
  const gatewayServerContent = fs.readFileSync(
53
- path.join(__dirname, "../../template/gateway/server.ts"),
54
- "utf8"
73
+ path.join(templateDir, `server${templateExt}`),
74
+ "utf8",
55
75
  );
56
76
 
57
- // Generate routes for all services
58
- const routes = gatewayModule.generateGatewayRoutes(allServices);
77
+ // Generate routes for all services with mode (docker or nodocker)
78
+ const mode = res.mode || "docker";
79
+ const routes = gatewayModule.generateGatewayRoutes(allServices, mode);
59
80
  const finalAppContent = gatewayAppContent.replace("/*__ROUTES__*/", routes);
60
81
 
61
82
  fs.writeFileSync(gatewayAppPath, finalAppContent);
@@ -76,29 +97,48 @@ export const setupService = async (
76
97
  if (res.projectType === "monolith" || serviceName === "health-service") {
77
98
  for (const f of res.features) {
78
99
  const feature = await import(`../../template/features/${f}/inject.js`);
79
- imports.push(feature.imports);
100
+ const featureImports = feature.getImports
101
+ ? feature.getImports(res.language)
102
+ : feature.imports;
103
+ imports.push(featureImports);
80
104
  middlewares.push(feature.middleware);
81
105
  deps.push(...feature.deps);
82
- if (feature.devDeps) {
106
+ if (feature.devDeps && res.language === "typescript") {
83
107
  devDeps.push(...feature.devDeps);
84
108
  }
109
+
110
+ // If the feature provides files for the selected language, write them
111
+ const featureFiles = feature.getFiles
112
+ ? feature.getFiles(res.language)
113
+ : feature.files;
114
+ if (featureFiles) {
115
+ for (const file in featureFiles) {
116
+ const filePath = file.replace(/\.ts$/, `.${ext}`);
117
+ const fullPath = path.join(serviceRoot, filePath);
118
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
119
+ fs.writeFileSync(fullPath, featureFiles[file]);
120
+ }
121
+ }
85
122
  }
86
123
  }
87
124
 
88
125
  // Add authentication (only for monolith or auth-service)
89
126
  if (shouldIncludeAuth && res.auth) {
90
- const baseAuth = await import(
91
- "../../template/features/auth/base/inject.js"
92
- );
127
+ const baseAuth =
128
+ await import("../../template/features/auth/base/inject.js");
93
129
  deps.push(...baseAuth.deps);
94
- if (baseAuth.devDeps) {
130
+ if (baseAuth.devDeps && res.language === "typescript") {
95
131
  devDeps.push(...baseAuth.devDeps);
96
132
  }
97
133
 
98
- for (const file in baseAuth.files) {
99
- const fullPath = path.join(serviceRoot, file);
134
+ const authFiles = baseAuth.getFiles
135
+ ? baseAuth.getFiles(res.language)
136
+ : baseAuth.files;
137
+ for (const file in authFiles) {
138
+ const filePath = file.replace(/\.ts$/, `.${ext}`);
139
+ const fullPath = path.join(serviceRoot, filePath);
100
140
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
101
- fs.writeFileSync(fullPath, baseAuth.files[file]);
141
+ fs.writeFileSync(fullPath, authFiles[file]);
102
142
  }
103
143
 
104
144
  const algo = await prompts({
@@ -108,17 +148,19 @@ export const setupService = async (
108
148
  serviceName ? ` for ${serviceName}` : ""
109
149
  }`,
110
150
  choices: [
111
- {
112
- title: process.platform === "win32"
113
- ? "bcrypt (recommended for Windows)"
114
- : "argon2 (recommended)",
115
- value: process.platform === "win32" ? "bcrypt" : "argon2"
151
+ {
152
+ title:
153
+ process.platform === "win32"
154
+ ? "bcrypt (recommended for Windows)"
155
+ : "argon2 (recommended)",
156
+ value: process.platform === "win32" ? "bcrypt" : "argon2",
116
157
  },
117
- {
118
- title: process.platform === "win32"
119
- ? "argon2 (requires build tools)"
120
- : "bcrypt",
121
- value: process.platform === "win32" ? "argon2" : "bcrypt"
158
+ {
159
+ title:
160
+ process.platform === "win32"
161
+ ? "argon2 (requires build tools)"
162
+ : "bcrypt",
163
+ value: process.platform === "win32" ? "argon2" : "bcrypt",
122
164
  },
123
165
  ],
124
166
  });
@@ -127,17 +169,26 @@ export const setupService = async (
127
169
  `../../template/features/auth/${algo.hasher}/inject.js`
128
170
  );
129
171
  deps.push(...hashFeature.deps);
130
- if (hashFeature.devDeps) {
172
+ if (hashFeature.devDeps && res.language === "typescript") {
131
173
  devDeps.push(...hashFeature.devDeps);
132
174
  }
133
175
 
134
- for (const file in hashFeature.files) {
135
- const fullPath = path.join(serviceRoot, file);
176
+ const hashFiles = hashFeature.getFiles
177
+ ? hashFeature.getFiles(res.language)
178
+ : hashFeature.files;
179
+ for (const file in hashFiles) {
180
+ const filePath = file.replace(/\.ts$/, `.${ext}`);
181
+ const fullPath = path.join(serviceRoot, filePath);
136
182
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
137
- fs.writeFileSync(fullPath, hashFeature.files[file]);
183
+ const content = hashFiles[file];
184
+ fs.writeFileSync(fullPath, content);
138
185
  }
139
186
 
140
- v1Imports.push(baseAuth.imports);
187
+ v1Imports.push(
188
+ baseAuth.getImports
189
+ ? baseAuth.getImports(res.language)
190
+ : baseAuth.imports,
191
+ );
141
192
  v1Routes.push(baseAuth.middleware);
142
193
  }
143
194
 
@@ -149,22 +200,31 @@ export const setupService = async (
149
200
  fs.writeFileSync(appPath, content);
150
201
 
151
202
  // Update root endpoint middleware with project info
152
- const rootMiddlewarePath = path.join(serviceRoot, `src/middlewares/root.middleware.${ext}`);
203
+ const rootMiddlewarePath = path.join(
204
+ serviceRoot,
205
+ `src/middlewares/root.middleware.${ext}`,
206
+ );
153
207
  if (fs.existsSync(rootMiddlewarePath)) {
154
208
  let rootContent = fs.readFileSync(rootMiddlewarePath, "utf8");
155
- rootContent = rootContent.replace("/*__PROJECT_NAME__*/", serviceName || res.sanitizedName);
156
- rootContent = rootContent.replace("/*__PROJECT_TYPE__*/", res.projectType);
157
-
209
+ rootContent = rootContent.replace(
210
+ "/*__PROJECT_NAME__*/",
211
+ serviceName || res.sanitizedName,
212
+ );
213
+ rootContent = rootContent.replace(
214
+ "/*__PROJECT_TYPE__*/",
215
+ res.projectType,
216
+ );
217
+
158
218
  // Add auth endpoint if auth is enabled
159
219
  if (shouldIncludeAuth && res.auth) {
160
220
  rootContent = rootContent.replace(
161
221
  "/*__AUTH_ENDPOINT__*/",
162
- 'auth: "/v1/auth",'
222
+ 'auth: "/api/v1/auth",',
163
223
  );
164
224
  } else {
165
225
  rootContent = rootContent.replace("/*__AUTH_ENDPOINT__*/", "");
166
226
  }
167
-
227
+
168
228
  fs.writeFileSync(rootMiddlewarePath, rootContent);
169
229
  }
170
230
 
@@ -190,106 +250,126 @@ export const setupService = async (
190
250
 
191
251
  fs.writeFileSync(v1IndexPath, v1Content);
192
252
  }
193
-
253
+
194
254
  // Update env file to conditionally include ALLOWED_ORIGIN and MONGO_URI
195
255
  const envPath = path.join(serviceRoot, `src/config/env.${ext}`);
196
256
  if (fs.existsSync(envPath)) {
197
257
  let envContent = fs.readFileSync(envPath, "utf8");
198
-
258
+
199
259
  // Import ENV in app.ts if CORS is selected
200
260
  if (res.features && res.features.includes("cors")) {
201
261
  let appContent = fs.readFileSync(appPath, "utf8");
202
262
  if (!appContent.includes("import { ENV } from")) {
203
263
  appContent = appContent.replace(
204
264
  "/*__IMPORTS__*/",
205
- "import { ENV } from '@/config';\n/*__IMPORTS__*/"
265
+ "import { ENV } from '@/config';\n/*__IMPORTS__*/",
206
266
  );
207
267
  fs.writeFileSync(appPath, appContent);
208
268
  }
209
269
  }
210
-
270
+
211
271
  // Add ALLOWED_ORIGIN if CORS is selected
212
272
  if (res.features && res.features.includes("cors")) {
273
+ const assertion = res.language === "javascript" ? "" : "!";
213
274
  envContent = envContent.replace(
214
275
  "/*__ALLOWED_ORIGIN__*/",
215
- 'ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN!,'
276
+ `ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN${assertion},`,
216
277
  );
217
278
  } else {
218
279
  envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
219
280
  }
220
-
281
+
221
282
  // Add MONGO_URI if auth is enabled
222
283
  if (shouldIncludeAuth && res.auth) {
284
+ const assertion = res.language === "javascript" ? "" : "!";
223
285
  envContent = envContent.replace(
224
286
  "/*__MONGO_URI__*/",
225
- 'MONGO_URI: process.env.MONGO_URI!,'
287
+ `MONGO_URI: process.env.MONGO_URI${assertion},`,
226
288
  );
227
289
  envContent = envContent.replace(
228
290
  "/*__JWT_SECRET__*/",
229
- 'JWT_SECRET: process.env.JWT_SECRET!,'
291
+ `JWT_SECRET: process.env.JWT_SECRET${assertion},`,
230
292
  );
231
293
  } else {
232
294
  envContent = envContent.replace("/*__MONGO_URI__*/", "");
233
295
  envContent = envContent.replace("/*__JWT_SECRET__*/", "");
234
296
  }
235
-
297
+
236
298
  fs.writeFileSync(envPath, envContent);
237
299
  }
238
-
300
+
239
301
  // Update server file to connect to DB if auth is enabled
240
302
  const serverPath = path.join(serviceRoot, `src/server.${ext}`);
241
303
  if (fs.existsSync(serverPath)) {
242
304
  let serverContent = fs.readFileSync(serverPath, "utf8");
243
-
305
+
244
306
  if (shouldIncludeAuth && res.auth) {
245
- serverContent = serverContent.replace(
246
- "/*__DB_IMPORT__*/",
247
- 'import { connectDB } from "./config";'
248
- );
307
+ const language = res.language;
308
+ if (language === "javascript") {
309
+ serverContent = serverContent.replace(
310
+ "/*__DB_IMPORT__*/",
311
+ ", connectDB",
312
+ );
313
+ } else {
314
+ serverContent = serverContent.replace(
315
+ "/*__DB_IMPORT__*/",
316
+ 'import { connectDB } from "./config";',
317
+ );
318
+ }
249
319
  serverContent = serverContent.replace(
250
320
  "/*__DB_CONNECT__*/",
251
- `// Connect to MongoDB
252
- await connectDB();`
321
+ `// Connect to MongoDB\nawait connectDB();`,
253
322
  );
254
323
  } else {
255
324
  serverContent = serverContent.replace("/*__DB_IMPORT__*/", "");
256
325
  serverContent = serverContent.replace("/*__DB_CONNECT__*/", "");
257
326
  }
258
-
327
+
259
328
  fs.writeFileSync(serverPath, serverContent);
260
329
  }
261
-
262
- // Update .env.example to conditionally include environment variables
263
- const envExamplePath = path.join(serviceRoot, ".env.example");
264
- if (fs.existsSync(envExamplePath)) {
265
- let envExampleContent = fs.readFileSync(envExamplePath, "utf8");
266
-
267
- // Add ALLOWED_ORIGIN if CORS is selected
268
- if (res.features && res.features.includes("cors")) {
269
- envExampleContent = envExampleContent.replace(
270
- "/*__ALLOWED_ORIGIN_ENV__*/",
271
- 'ALLOWED_ORIGIN=http://localhost:3000'
272
- );
273
- } else {
274
- envExampleContent = envExampleContent.replace("/*__ALLOWED_ORIGIN_ENV__*/", "");
275
- }
276
-
277
- // Add MONGO_URI and JWT_SECRET if auth is enabled
278
- if (shouldIncludeAuth && res.auth) {
279
- envExampleContent = envExampleContent.replace(
280
- "/*__MONGO_URI_ENV__*/",
281
- 'MONGO_URI=mongodb://localhost:27017/your-database-name'
282
- );
283
- envExampleContent = envExampleContent.replace(
284
- "/*__JWT_SECRET_ENV__*/",
285
- 'JWT_SECRET=your-super-secret-jwt-key-change-this-in-production'
286
- );
287
- } else {
288
- envExampleContent = envExampleContent.replace("/*__MONGO_URI_ENV__*/", "");
289
- envExampleContent = envExampleContent.replace("/*__JWT_SECRET_ENV__*/", "");
330
+
331
+ // Update .env.example to conditionally include environment variables (only for monolith)
332
+ if (res.projectType !== "microservice") {
333
+ const envExamplePath = path.join(serviceRoot, ".env.example");
334
+ if (fs.existsSync(envExamplePath)) {
335
+ let envExampleContent = fs.readFileSync(envExamplePath, "utf8");
336
+
337
+ // Add ALLOWED_ORIGIN if CORS is selected
338
+ if (res.features && res.features.includes("cors")) {
339
+ envExampleContent = envExampleContent.replace(
340
+ "/*__ALLOWED_ORIGIN_ENV__*/",
341
+ "ALLOWED_ORIGIN=http://localhost:3000",
342
+ );
343
+ } else {
344
+ envExampleContent = envExampleContent.replace(
345
+ "/*__ALLOWED_ORIGIN_ENV__*/",
346
+ "",
347
+ );
348
+ }
349
+
350
+ // Add MONGO_URI and JWT_SECRET if auth is enabled
351
+ if (shouldIncludeAuth && res.auth) {
352
+ envExampleContent = envExampleContent.replace(
353
+ "/*__MONGO_URI_ENV__*/",
354
+ "MONGO_URI=mongodb://localhost:27017/your-database-name",
355
+ );
356
+ envExampleContent = envExampleContent.replace(
357
+ "/*__JWT_SECRET_ENV__*/",
358
+ "JWT_SECRET=your-super-secret-jwt-key-change-this-in-production",
359
+ );
360
+ } else {
361
+ envExampleContent = envExampleContent.replace(
362
+ "/*__MONGO_URI_ENV__*/",
363
+ "",
364
+ );
365
+ envExampleContent = envExampleContent.replace(
366
+ "/*__JWT_SECRET_ENV__*/",
367
+ "",
368
+ );
369
+ }
370
+
371
+ fs.writeFileSync(envExamplePath, envExampleContent);
290
372
  }
291
-
292
- fs.writeFileSync(envExamplePath, envExampleContent);
293
373
  }
294
374
  } // End of else block for non-gateway services
295
375
 
@@ -297,28 +377,71 @@ await connectDB();`
297
377
  if (res.projectType === "microservice") {
298
378
  const tsconfigPath = path.join(serviceRoot, "tsconfig.json");
299
379
  let tsconfigContent = fs.readFileSync(tsconfigPath, "utf8");
300
-
380
+
301
381
  // Remove comments from JSON (strip-json-comments approach)
302
382
  tsconfigContent = tsconfigContent
303
- .replace(/\/\/.*$/gm, '') // Remove single-line comments
304
- .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments
305
-
383
+ .replace(/\/\/.*$/gm, "") // Remove single-line comments
384
+ .replace(/\/\*[\s\S]*?\*\//g, ""); // Remove multi-line comments
385
+
306
386
  const tsconfig = JSON.parse(tsconfigContent);
307
387
 
308
- // Update paths to include shared folder
388
+ // Update baseUrl to allow import from the shared folder
389
+ tsconfig.compilerOptions.baseUrl = ".";
390
+
391
+ // Update paths to include shared folder (works in both Docker and VS Code)
309
392
  tsconfig.compilerOptions.paths = {
310
- "@/*": ["*"],
311
- "@/config/*": ["../../shared/config/*"],
312
- "@/utils/*": ["../../shared/utils/*"],
393
+ "@/*": ["src/*"],
394
+ "@/shared/*": ["shared/*", "../../shared/*"],
313
395
  };
314
396
 
397
+ // Remove rootDir restriction to allow imports from outside src/
398
+ delete tsconfig.compilerOptions.rootDir;
399
+
315
400
  fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
401
+
402
+ // Update imports in service files to use @/shared/* instead of @/* for utils/config
403
+ // This applies to non-gateway services
404
+ if (serviceName !== "gateway") {
405
+ const healthControllerPath = path.join(
406
+ serviceRoot,
407
+ `src/modules/v1/health/health.controller.${ext}`,
408
+ );
409
+ if (fs.existsSync(healthControllerPath)) {
410
+ let healthControllerContent = fs.readFileSync(
411
+ healthControllerPath,
412
+ "utf8",
413
+ );
414
+ healthControllerContent = healthControllerContent.replace(
415
+ 'from "@/utils"',
416
+ 'from "@/shared/utils"',
417
+ );
418
+ fs.writeFileSync(healthControllerPath, healthControllerContent);
419
+ }
420
+
421
+ // Update server.ts to use shared imports
422
+ const serverPath = path.join(serviceRoot, `src/server.${ext}`);
423
+ if (fs.existsSync(serverPath)) {
424
+ let serverContent = fs.readFileSync(serverPath, "utf8");
425
+ serverContent = serverContent
426
+ .replace('from "@/utils"', 'from "@/shared/utils"')
427
+ .replace('from "@/config"', 'from "@/shared/config"');
428
+
429
+ // Update PORT to use service-specific environment variable
430
+ const portEnvVar = `${serviceName.toUpperCase().replace(/-/g, "_")}_PORT`;
431
+ serverContent = serverContent.replace(
432
+ /const PORT = ENV\.PORT \|\| (\d+);/,
433
+ `const PORT = ENV.${portEnvVar} || $1;`,
434
+ );
435
+
436
+ fs.writeFileSync(serverPath, serverContent);
437
+ }
438
+ }
316
439
  }
317
440
 
318
441
  // Update package.json
319
442
  const packageJsonPath = path.join(serviceRoot, "package.json");
320
443
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
321
-
444
+
322
445
  // Create new package.json with name at the top
323
446
  const orderedPackageJson = {
324
447
  name: serviceName || res.sanitizedName,
@@ -326,12 +449,12 @@ await connectDB();`
326
449
  description: res.description || packageJson.description,
327
450
  ...packageJson,
328
451
  };
329
-
452
+
330
453
  // Remove duplicate keys that were moved to the top
331
454
  delete orderedPackageJson.name;
332
455
  delete orderedPackageJson.version;
333
456
  delete orderedPackageJson.description;
334
-
457
+
335
458
  // Re-add them at the top in correct order
336
459
  const finalPackageJson = {
337
460
  name: serviceName || res.sanitizedName,
@@ -339,29 +462,57 @@ await connectDB();`
339
462
  description: res.description || packageJson.description,
340
463
  ...orderedPackageJson,
341
464
  };
342
-
465
+
343
466
  // Add author if provided
344
467
  if (res.author) {
345
468
  finalPackageJson.author = res.author;
346
469
  }
347
-
470
+
348
471
  // Add keywords if provided
349
472
  if (res.keywords && res.keywords.trim()) {
350
- finalPackageJson.keywords = res.keywords.split(',').map(k => k.trim()).filter(Boolean);
473
+ finalPackageJson.keywords = res.keywords
474
+ .split(",")
475
+ .map((k) => k.trim())
476
+ .filter(Boolean);
351
477
  }
352
-
478
+
479
+ // Add Node.js native path aliasing for JavaScript projects
480
+ // This replaces TypeScript's tsconfig paths with Node's native imports field
481
+ if (res.language === "javascript") {
482
+ finalPackageJson.imports = {
483
+ "#/*": "./src/*",
484
+ };
485
+ }
486
+
487
+ // Add --poll flag to dev script for Docker mode (fixes watch mode in Docker on Windows)
488
+ if (res.projectType === "microservice" && res.mode === "docker") {
489
+ if (finalPackageJson.scripts && finalPackageJson.scripts.dev) {
490
+ finalPackageJson.scripts.dev = finalPackageJson.scripts.dev.replace(
491
+ "ts-node-dev --respawn --transpile-only",
492
+ "ts-node-dev --respawn --transpile-only --poll",
493
+ );
494
+ }
495
+ }
496
+
353
497
  fs.writeFileSync(
354
498
  packageJsonPath,
355
- JSON.stringify(finalPackageJson, null, 2) + "\n"
499
+ JSON.stringify(finalPackageJson, null, 2) + "\n",
356
500
  );
357
501
 
502
+ // Skip installation if skipInstall is true (will be done later in batch)
503
+ if (skipInstall) {
504
+ return { deps, devDeps, installSucceeded: true };
505
+ }
506
+
358
507
  // Install dependencies
359
508
  console.log(
360
- pc.cyan(`\n📦 Installing dependencies for ${serviceName || "project"}...\n`)
509
+ pc.cyan(
510
+ `\n📦 Installing dependencies for ${serviceName || "project"}...\n`,
511
+ ),
361
512
  );
362
-
513
+
363
514
  let installSucceeded = false;
364
-
515
+
365
516
  try {
366
517
  if (deps.length) {
367
518
  execSync(`npm install ${deps.join(" ")}`, {
@@ -377,26 +528,29 @@ await connectDB();`
377
528
  }
378
529
  execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
379
530
  installSucceeded = true;
380
-
531
+
381
532
  // Run format after successful install
382
533
  console.log(pc.cyan("\n🎨 Formatting code...\n"));
383
534
  try {
384
535
  execSync("npm run format", { cwd: serviceRoot, stdio: "inherit" });
385
536
  } catch (formatError) {
386
- console.warn(pc.yellow("⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n"));
537
+ console.warn(
538
+ pc.yellow(
539
+ "⚠️ Warning: Code formatting failed. You can run it manually later with: npm run format\n",
540
+ ),
541
+ );
387
542
  }
388
543
  } catch (error) {
389
- console.error(pc.yellow("\n⚠️ Warning: Some dependencies failed to install."));
390
- console.error(pc.yellow("This is usually due to native modules (like argon2) requiring build tools.\n"));
391
- console.error(pc.cyan("💡 Solutions:"));
392
- console.error(pc.dim(" 1. Install build tools: npm install --global windows-build-tools"));
393
- console.error(pc.dim(" 2. Or switch to bcrypt (works better on Windows)"));
394
- console.error(pc.dim(" 3. Or manually install later: cd " + (serviceName || res.sanitizedName) + " && npm install"));
395
- console.error(pc.dim(" 4. Then run: npm run format\n"));
396
-
544
+ console.error(pc.red("\n Failed to install dependencies"));
545
+ console.error(pc.dim(`\nYou can install them later by running:`));
546
+ console.error(
547
+ pc.cyan(` cd ${serviceName || res.sanitizedName} && npm install`),
548
+ );
549
+ console.error(pc.dim(" Then run: npm run format\n"));
550
+
397
551
  // Don't exit - let the project be created anyway
398
552
  console.log(pc.cyan("⏭️ Continuing with project creation...\n"));
399
553
  }
400
554
 
401
- return { deps, installSucceeded };
555
+ return { deps, devDeps, installSucceeded };
402
556
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifecodes/backend-template",
3
- "version": "1.1.1",
3
+ "version": "1.1.5",
4
4
  "description": "Production-ready Express + TypeScript/JavaScript backend generator with optional features and microservice support",
5
5
  "bin": {
6
6
  "ifecodes-template": "bin/cli.js"
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": [
3
+ "eslint:recommended",
4
+ "prettier"
5
+ ],
6
+ "env": {
7
+ "node": true,
8
+ "es2021": true
9
+ },
10
+ "rules": {
11
+ "no-unused-vars": ["warn"]
12
+ }
13
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5",
5
+ "printWidth": 80,
6
+ "tabWidth": 2
7
+ }