@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 +493 -103
- package/bin/lib/microservice-config.js +21 -4
- 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 +5 -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,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: "^
|
|
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
|
|
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
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
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(
|
|
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("
|
|
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
|
+
}
|