@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 +477 -103
- package/bin/lib/microservice-config.js +12 -2
- package/bin/lib/prompts.js +15 -0
- package/bin/lib/readme-generator.js +67 -38
- package/bin/lib/service-setup.js +143 -20
- package/package.json +2 -2
- package/template/base/ts/src/utils/logger.ts +1 -1
- package/template/gateway/ts/app.ts +1 -0
- package/template/gateway/ts/inject.js +4 -3
package/bin/cli.js
CHANGED
|
@@ -27,9 +27,10 @@ const {
|
|
|
27
27
|
isInMicroserviceProject,
|
|
28
28
|
} = config;
|
|
29
29
|
|
|
30
|
-
const baseRoot =
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
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(
|
|
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
|
|
68
|
+
|
|
69
|
+
// Remove db file and remove connectDB export/import if auth is not enabled
|
|
65
70
|
if (!config.auth) {
|
|
66
|
-
const
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
124
|
+
// Remove db files and strip connectDB exports/imports when auth is not enabled
|
|
104
125
|
if (!config.auth) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
fs.rmSync(sharedDbPath);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
-
:
|
|
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: "^
|
|
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
|
|
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
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
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(
|
|
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("
|
|
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
|
+
}
|