create-express-kickstart 1.3.2 → 1.3.4
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/.env.example +11 -9
- package/README.md +7 -7
- package/bin/cli.js +804 -393
- package/package.json +5 -5
- package/src/app.js +34 -78
- package/src/db/index.js +12 -14
- package/src/server.js +18 -20
- package/templates/.dockerignore +8 -0
- package/templates/Dockerfile +14 -20
- package/templates/auth/auth.controller.js +96 -31
- package/templates/auth/auth.middleware.js +30 -22
- package/templates/auth/user.model.js +31 -0
- package/templates/docker-compose.yml +21 -23
- package/tests/cli.test.js +573 -0
- package/src/models/example-model.js +0 -18
- package/src/utils/constants.js +0 -1
package/bin/cli.js
CHANGED
|
@@ -1,393 +1,804 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const ROOT_DIR = path.join(__dirname, "..");
|
|
13
|
+
const DEFAULT_PORT = 8000;
|
|
14
|
+
const DEFAULT_PACKAGE_MANAGER = "npm";
|
|
15
|
+
const AUTH_SECRET_PLACEHOLDER = "replace-me-with-a-long-random-secret";
|
|
16
|
+
const SUPPORTED_PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
|
|
17
|
+
const ENV_SKIP_INSTALL = "CREATE_EXPRESS_KICKSTART_SKIP_INSTALL";
|
|
18
|
+
const ENV_SKIP_GIT = "CREATE_EXPRESS_KICKSTART_SKIP_GIT";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_DEPENDENCIES = {
|
|
21
|
+
express: true,
|
|
22
|
+
mongoose: true,
|
|
23
|
+
cors: true,
|
|
24
|
+
helmet: true,
|
|
25
|
+
"cookie-parser": true,
|
|
26
|
+
"pino-http": true,
|
|
27
|
+
"express-rate-limit": true,
|
|
28
|
+
dotenv: true,
|
|
29
|
+
prettier: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const GITIGNORE_CONTENT = `node_modules
|
|
33
|
+
.env
|
|
34
|
+
.env.keys
|
|
35
|
+
.env.local
|
|
36
|
+
dist
|
|
37
|
+
build
|
|
38
|
+
coverage
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const HASH_UTIL_TEMPLATE = `import bcrypt from "bcryptjs";
|
|
42
|
+
|
|
43
|
+
export const hashData = async (data, saltRounds = process.env.BCRYPT_SALT_ROUNDS) => {
|
|
44
|
+
const salt = await bcrypt.genSalt(Number(saltRounds) || 10);
|
|
45
|
+
return bcrypt.hash(data, salt);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const compareData = async (data, hashedData) => {
|
|
49
|
+
return bcrypt.compare(data, hashedData);
|
|
50
|
+
};
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const JWT_UTIL_TEMPLATE = `import jwt from "jsonwebtoken";
|
|
54
|
+
|
|
55
|
+
const getJwtSecret = () => {
|
|
56
|
+
if (!process.env.JWT_SECRET) {
|
|
57
|
+
throw new Error("JWT_SECRET must be set before using JWT helpers.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return process.env.JWT_SECRET;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const generateToken = (payload, expiresIn = process.env.JWT_EXPIRES_IN || "1d") => {
|
|
64
|
+
return jwt.sign(payload, getJwtSecret(), { expiresIn });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const verifyToken = (token) => {
|
|
68
|
+
return jwt.verify(token, getJwtSecret());
|
|
69
|
+
};
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const DOCKER_TEMPLATE_MAP = {
|
|
73
|
+
npm: {
|
|
74
|
+
baseImage: "node:22-alpine",
|
|
75
|
+
packageManagerSetup: "",
|
|
76
|
+
installCommand: "npm install --omit=dev",
|
|
77
|
+
runtime: "node",
|
|
78
|
+
},
|
|
79
|
+
yarn: {
|
|
80
|
+
baseImage: "node:22-alpine",
|
|
81
|
+
packageManagerSetup: "RUN corepack enable",
|
|
82
|
+
installCommand: "yarn install --production=true",
|
|
83
|
+
runtime: "node",
|
|
84
|
+
},
|
|
85
|
+
pnpm: {
|
|
86
|
+
baseImage: "node:22-alpine",
|
|
87
|
+
packageManagerSetup: "RUN corepack enable",
|
|
88
|
+
installCommand: "pnpm install --prod",
|
|
89
|
+
runtime: "node",
|
|
90
|
+
},
|
|
91
|
+
bun: {
|
|
92
|
+
baseImage: "oven/bun:1-alpine",
|
|
93
|
+
packageManagerSetup: "",
|
|
94
|
+
installCommand: "bun install --production",
|
|
95
|
+
runtime: "bun",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const parseYesNo = (answer) => answer.trim().toLowerCase() !== "n";
|
|
100
|
+
|
|
101
|
+
export const normalizePackageManager = (value) => {
|
|
102
|
+
const normalized = value.trim().toLowerCase();
|
|
103
|
+
return SUPPORTED_PACKAGE_MANAGERS.has(normalized)
|
|
104
|
+
? normalized
|
|
105
|
+
: DEFAULT_PACKAGE_MANAGER;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const unique = (items) => [...new Set(items)];
|
|
109
|
+
|
|
110
|
+
const createSecret = () => crypto.randomBytes(32).toString("hex");
|
|
111
|
+
|
|
112
|
+
const copyRecursiveSync = (src, dest) => {
|
|
113
|
+
const stats = fs.statSync(src);
|
|
114
|
+
|
|
115
|
+
if (stats.isDirectory()) {
|
|
116
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
117
|
+
|
|
118
|
+
for (const child of fs.readdirSync(src)) {
|
|
119
|
+
copyRecursiveSync(path.join(src, child), path.join(dest, child));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fs.copyFileSync(src, dest);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const renderTemplate = (template, replacements) => {
|
|
129
|
+
let output = template;
|
|
130
|
+
|
|
131
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
132
|
+
output = output.replaceAll(token, value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return output
|
|
136
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
137
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
138
|
+
.trimEnd()
|
|
139
|
+
.concat("\n");
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const readTemplate = (...segments) =>
|
|
143
|
+
fs.readFileSync(path.join(ROOT_DIR, ...segments), "utf8");
|
|
144
|
+
|
|
145
|
+
const writeJson = (filePath, value) => {
|
|
146
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const appendBlock = (filePath, block) => {
|
|
150
|
+
const currentValue = fs.readFileSync(filePath, "utf8").trimEnd();
|
|
151
|
+
fs.writeFileSync(filePath, `${currentValue}\n\n${block.trim()}\n`);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const createPackageJsonTemplate = (config) => {
|
|
155
|
+
const packageJsonTemplate = {
|
|
156
|
+
name: config.packageJsonName.trim(),
|
|
157
|
+
version: "1.0.0",
|
|
158
|
+
description: config.description || "A configurable Node.js Express API starter",
|
|
159
|
+
main: "src/server.js",
|
|
160
|
+
type: "module",
|
|
161
|
+
scripts: {
|
|
162
|
+
start: config.deps.dotenv
|
|
163
|
+
? "dotenvx run -f .env.local -- node src/server.js"
|
|
164
|
+
: "node src/server.js",
|
|
165
|
+
dev: config.deps.dotenv
|
|
166
|
+
? "dotenvx run -f .env.local -- nodemon src/server.js"
|
|
167
|
+
: "nodemon src/server.js",
|
|
168
|
+
},
|
|
169
|
+
imports: {
|
|
170
|
+
"#*": "./src/*",
|
|
171
|
+
},
|
|
172
|
+
keywords: ["express", "node", "api"],
|
|
173
|
+
author: config.author || "",
|
|
174
|
+
license: "ISC",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (config.deps.prettier) {
|
|
178
|
+
packageJsonTemplate.scripts.format = 'prettier --write "src/**/*.{js,json}"';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (config.initTests) {
|
|
182
|
+
packageJsonTemplate.scripts.test =
|
|
183
|
+
"node --experimental-vm-modules node_modules/jest/bin/jest.js";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return packageJsonTemplate;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const resolveDependencyLists = (config) => {
|
|
190
|
+
const dependencyCandidates = Object.entries(config.deps)
|
|
191
|
+
.filter(([dependencyName, enabled]) => {
|
|
192
|
+
return enabled && dependencyName !== "dotenv" && dependencyName !== "prettier";
|
|
193
|
+
})
|
|
194
|
+
.map(([dependencyName]) => dependencyName);
|
|
195
|
+
|
|
196
|
+
const dependencies = unique([
|
|
197
|
+
...dependencyCandidates,
|
|
198
|
+
...(config.deps["pino-http"] ? ["pino"] : []),
|
|
199
|
+
...(config.initAuth ? ["jsonwebtoken", "bcryptjs"] : []),
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const devDependencies = unique([
|
|
203
|
+
"nodemon",
|
|
204
|
+
...(config.deps.dotenv ? ["@dotenvx/dotenvx"] : []),
|
|
205
|
+
...(config.deps.prettier ? ["prettier"] : []),
|
|
206
|
+
...(config.installPinoPretty && config.deps["pino-http"] ? ["pino-pretty"] : []),
|
|
207
|
+
...(config.initTests ? ["jest", "supertest"] : []),
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
return { dependencies, devDependencies };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const updatePackageJsonDependencies = (projectPath, dependencies, devDependencies) => {
|
|
214
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
215
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
216
|
+
|
|
217
|
+
packageJson.dependencies = Object.fromEntries(
|
|
218
|
+
dependencies.map((dependencyName) => [dependencyName, "latest"]),
|
|
219
|
+
);
|
|
220
|
+
packageJson.devDependencies = Object.fromEntries(
|
|
221
|
+
devDependencies.map((dependencyName) => [dependencyName, "latest"]),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
writeJson(packageJsonPath, packageJson);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const updatePackageJsonWithInstalledVersions = (projectPath, dependencies, devDependencies) => {
|
|
228
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
229
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
230
|
+
|
|
231
|
+
const getInstalledVersion = (dependencyName) => {
|
|
232
|
+
try {
|
|
233
|
+
const dependencyPackageJson = JSON.parse(
|
|
234
|
+
fs.readFileSync(
|
|
235
|
+
path.join(projectPath, "node_modules", dependencyName, "package.json"),
|
|
236
|
+
"utf8",
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return `^${dependencyPackageJson.version}`;
|
|
241
|
+
} catch {
|
|
242
|
+
return "latest";
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
for (const dependencyName of dependencies) {
|
|
247
|
+
packageJson.dependencies[dependencyName] = getInstalledVersion(dependencyName);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const dependencyName of devDependencies) {
|
|
251
|
+
packageJson.devDependencies[dependencyName] = getInstalledVersion(dependencyName);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
writeJson(packageJsonPath, packageJson);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const buildDockerfile = (packageManager) => {
|
|
258
|
+
const dockerTemplate = readTemplate("templates", "Dockerfile");
|
|
259
|
+
const dockerOptions = DOCKER_TEMPLATE_MAP[packageManager] || DOCKER_TEMPLATE_MAP.npm;
|
|
260
|
+
|
|
261
|
+
return renderTemplate(dockerTemplate, {
|
|
262
|
+
"__BASE_IMAGE__": dockerOptions.baseImage,
|
|
263
|
+
"__PACKAGE_MANAGER_SETUP__": dockerOptions.packageManagerSetup,
|
|
264
|
+
"__INSTALL_COMMAND__": dockerOptions.installCommand,
|
|
265
|
+
"__PORT__": String(DEFAULT_PORT),
|
|
266
|
+
"__RUNTIME__": dockerOptions.runtime,
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const buildDockerCompose = () => {
|
|
271
|
+
const dockerComposeTemplate = readTemplate("templates", "docker-compose.yml");
|
|
272
|
+
|
|
273
|
+
return renderTemplate(dockerComposeTemplate, {
|
|
274
|
+
"__PORT__": String(DEFAULT_PORT),
|
|
275
|
+
"__MONGODB_URI__": "mongodb://mongo:27017/my_app_db",
|
|
276
|
+
"__CORS_ORIGIN__": "http://localhost:3000",
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const buildAppCode = (config) => {
|
|
281
|
+
const appTemplate = readTemplate("src", "app.js");
|
|
282
|
+
|
|
283
|
+
return renderTemplate(appTemplate, {
|
|
284
|
+
"__CORS_IMPORT__": config.deps.cors ? 'import cors from "cors";' : "",
|
|
285
|
+
"__COOKIE_PARSER_IMPORT__": config.deps["cookie-parser"]
|
|
286
|
+
? 'import cookieParser from "cookie-parser";'
|
|
287
|
+
: "",
|
|
288
|
+
"__HELMET_IMPORT__": config.deps.helmet ? 'import helmet from "helmet";' : "",
|
|
289
|
+
"__LOGGER_IMPORT__": config.deps["pino-http"] ? 'import pinoHttp from "pino-http";' : "",
|
|
290
|
+
"__RATE_LIMIT_IMPORT__": config.deps["express-rate-limit"]
|
|
291
|
+
? 'import rateLimit from "express-rate-limit";'
|
|
292
|
+
: "",
|
|
293
|
+
"__AUTH_IMPORT__": config.initAuth ? 'import authRouter from "#routes/auth.routes.js";' : "",
|
|
294
|
+
"__HELMET_SETUP__": config.deps.helmet ? "app.use(helmet());" : "",
|
|
295
|
+
"__RATE_LIMIT_SETUP__": config.deps["express-rate-limit"]
|
|
296
|
+
? `const limiter = rateLimit({
|
|
297
|
+
windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
298
|
+
limit: Number(process.env.RATE_LIMIT_MAX) || 100,
|
|
299
|
+
standardHeaders: "draft-7",
|
|
300
|
+
legacyHeaders: false,
|
|
301
|
+
message: "Too many requests from this IP, please try again later",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
app.use("/api", limiter);`
|
|
305
|
+
: "",
|
|
306
|
+
"__LOGGER_SETUP__": config.deps["pino-http"]
|
|
307
|
+
? `const enablePrettyLogs =
|
|
308
|
+
process.env.NODE_ENV === "development" && process.env.PINO_PRETTY === "true";
|
|
309
|
+
|
|
310
|
+
app.use(
|
|
311
|
+
pinoHttp({
|
|
312
|
+
customLogLevel(req, res, err) {
|
|
313
|
+
if (res.statusCode >= 500 || err) {
|
|
314
|
+
return "error";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (res.statusCode >= 400) {
|
|
318
|
+
return "warn";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (res.statusCode >= 300) {
|
|
322
|
+
return "silent";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return "info";
|
|
326
|
+
},
|
|
327
|
+
transport: enablePrettyLogs
|
|
328
|
+
? {
|
|
329
|
+
target: "pino-pretty",
|
|
330
|
+
options: { colorize: true },
|
|
331
|
+
}
|
|
332
|
+
: undefined,
|
|
333
|
+
}),
|
|
334
|
+
);`
|
|
335
|
+
: "",
|
|
336
|
+
"__CORS_SETUP__": config.deps.cors
|
|
337
|
+
? `const allowedOrigins = (process.env.CORS_ORIGIN || "")
|
|
338
|
+
.split(",")
|
|
339
|
+
.map((origin) => origin.trim())
|
|
340
|
+
.filter(Boolean);
|
|
341
|
+
const allowAllOrigins = allowedOrigins.includes("*");
|
|
342
|
+
|
|
343
|
+
if (process.env.NODE_ENV === "production" && allowedOrigins.length === 0) {
|
|
344
|
+
throw new Error("CORS_ORIGIN must list one or more allowed origins in production.");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
app.use(
|
|
348
|
+
cors({
|
|
349
|
+
origin: allowAllOrigins
|
|
350
|
+
? true
|
|
351
|
+
: allowedOrigins.length > 0
|
|
352
|
+
? allowedOrigins
|
|
353
|
+
: true,
|
|
354
|
+
credentials: !allowAllOrigins && allowedOrigins.length > 0,
|
|
355
|
+
}),
|
|
356
|
+
);`
|
|
357
|
+
: "",
|
|
358
|
+
"__COOKIE_PARSER_SETUP__": config.deps["cookie-parser"] ? "app.use(cookieParser());" : "",
|
|
359
|
+
"__AUTH_ROUTE__": config.initAuth ? 'app.use("/api/v1/auth", authRouter);' : "",
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const buildServerCode = (config) => {
|
|
364
|
+
const serverTemplate = readTemplate("src", "server.js");
|
|
365
|
+
|
|
366
|
+
return renderTemplate(serverTemplate, {
|
|
367
|
+
"__DB_IMPORT__": config.deps.mongoose ? 'import connectDB from "#db/index.js";' : "",
|
|
368
|
+
"__SERVER_STARTUP__": config.deps.mongoose
|
|
369
|
+
? `const bootstrap = async () => {
|
|
370
|
+
await connectDB();
|
|
371
|
+
startServer();
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
bootstrap().catch((error) => {
|
|
375
|
+
console.error("Database connection failed", error);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
});`
|
|
378
|
+
: "startServer();",
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const ensureDir = (dirPath) => {
|
|
383
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const writeAuthUtilities = (projectPath) => {
|
|
387
|
+
const utilsPath = path.join(projectPath, "src", "utils");
|
|
388
|
+
ensureDir(utilsPath);
|
|
389
|
+
|
|
390
|
+
fs.writeFileSync(path.join(utilsPath, "hash.util.js"), HASH_UTIL_TEMPLATE);
|
|
391
|
+
fs.writeFileSync(path.join(utilsPath, "jwt.util.js"), JWT_UTIL_TEMPLATE);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const writeAuthFiles = (projectPath) => {
|
|
395
|
+
ensureDir(path.join(projectPath, "src", "controllers"));
|
|
396
|
+
ensureDir(path.join(projectPath, "src", "middlewares"));
|
|
397
|
+
ensureDir(path.join(projectPath, "src", "routes"));
|
|
398
|
+
ensureDir(path.join(projectPath, "src", "models"));
|
|
399
|
+
|
|
400
|
+
fs.copyFileSync(
|
|
401
|
+
path.join(ROOT_DIR, "templates", "auth", "auth.controller.js"),
|
|
402
|
+
path.join(projectPath, "src", "controllers", "auth.controller.js"),
|
|
403
|
+
);
|
|
404
|
+
fs.copyFileSync(
|
|
405
|
+
path.join(ROOT_DIR, "templates", "auth", "auth.middleware.js"),
|
|
406
|
+
path.join(projectPath, "src", "middlewares", "auth.middleware.js"),
|
|
407
|
+
);
|
|
408
|
+
fs.copyFileSync(
|
|
409
|
+
path.join(ROOT_DIR, "templates", "auth", "auth.routes.js"),
|
|
410
|
+
path.join(projectPath, "src", "routes", "auth.routes.js"),
|
|
411
|
+
);
|
|
412
|
+
fs.copyFileSync(
|
|
413
|
+
path.join(ROOT_DIR, "templates", "auth", "user.model.js"),
|
|
414
|
+
path.join(projectPath, "src", "models", "user.model.js"),
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const addAuthEnvironment = (projectPath, secretGenerator) => {
|
|
419
|
+
appendBlock(
|
|
420
|
+
path.join(projectPath, ".env.example"),
|
|
421
|
+
`# Bcrypt Configuration
|
|
422
|
+
BCRYPT_SALT_ROUNDS=10
|
|
423
|
+
|
|
424
|
+
# JWT Configuration
|
|
425
|
+
JWT_SECRET=${AUTH_SECRET_PLACEHOLDER}
|
|
426
|
+
JWT_EXPIRES_IN=1d`,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
appendBlock(
|
|
430
|
+
path.join(projectPath, ".env.local"),
|
|
431
|
+
`# Bcrypt Configuration
|
|
432
|
+
BCRYPT_SALT_ROUNDS=10
|
|
433
|
+
|
|
434
|
+
# JWT Configuration
|
|
435
|
+
JWT_SECRET=${secretGenerator()}
|
|
436
|
+
JWT_EXPIRES_IN=1d`,
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const addPinoPrettyEnvironment = (projectPath) => {
|
|
441
|
+
appendBlock(path.join(projectPath, ".env.example"), "PINO_PRETTY=true");
|
|
442
|
+
appendBlock(path.join(projectPath, ".env.local"), "PINO_PRETTY=true");
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const initializeGitRepository = ({ projectPath, runCommand, logger, skipGit }) => {
|
|
446
|
+
fs.writeFileSync(path.join(projectPath, ".gitignore"), GITIGNORE_CONTENT);
|
|
447
|
+
|
|
448
|
+
if (skipGit) {
|
|
449
|
+
logger.log(` Skipping git initialization because ${ENV_SKIP_GIT}=1.`);
|
|
450
|
+
return {
|
|
451
|
+
gitInitialized: false,
|
|
452
|
+
warnings: ["Git initialization was skipped by environment override."],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const warnings = [];
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
runCommand("git init", { cwd: projectPath, stdio: "inherit" });
|
|
460
|
+
runCommand("git add .", { cwd: projectPath, stdio: "inherit" });
|
|
461
|
+
runCommand('git commit -m "initial commit"', {
|
|
462
|
+
cwd: projectPath,
|
|
463
|
+
stdio: "inherit",
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return { gitInitialized: true, warnings };
|
|
467
|
+
} catch (error) {
|
|
468
|
+
warnings.push(
|
|
469
|
+
"Git initialization completed partially. Review git configuration before committing.",
|
|
470
|
+
);
|
|
471
|
+
logger.warn("\nGit setup could not finish cleanly. The project files are still ready to use.");
|
|
472
|
+
|
|
473
|
+
return { gitInitialized: false, warnings, error };
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const installDependencies = ({
|
|
478
|
+
projectPath,
|
|
479
|
+
packageManager,
|
|
480
|
+
dependencies,
|
|
481
|
+
devDependencies,
|
|
482
|
+
runCommand,
|
|
483
|
+
logger,
|
|
484
|
+
skipInstall,
|
|
485
|
+
}) => {
|
|
486
|
+
updatePackageJsonDependencies(projectPath, dependencies, devDependencies);
|
|
487
|
+
|
|
488
|
+
if (skipInstall) {
|
|
489
|
+
logger.log(`\n Skipping dependency installation because ${ENV_SKIP_INSTALL}=1.`);
|
|
490
|
+
return {
|
|
491
|
+
installSucceeded: false,
|
|
492
|
+
warnings: ["Dependency installation was skipped by environment override."],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
logger.log(`\n Configuring ${packageManager} and resolving dependency trees...`);
|
|
498
|
+
logger.log(`\n Running final installation via ${packageManager} (this might take a minute)...`);
|
|
499
|
+
|
|
500
|
+
const installCommand =
|
|
501
|
+
packageManager === "npm" ? "npm install" : `${packageManager} install`;
|
|
502
|
+
|
|
503
|
+
runCommand(installCommand, { cwd: projectPath, stdio: "inherit" });
|
|
504
|
+
updatePackageJsonWithInstalledVersions(projectPath, dependencies, devDependencies);
|
|
505
|
+
|
|
506
|
+
return { installSucceeded: true, warnings: [] };
|
|
507
|
+
} catch (error) {
|
|
508
|
+
logger.warn(
|
|
509
|
+
"\nDependency installation did not complete. You can still open the project and run the install manually.",
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
installSucceeded: false,
|
|
514
|
+
warnings: [
|
|
515
|
+
"Dependency installation failed. Run the package manager manually inside the project.",
|
|
516
|
+
],
|
|
517
|
+
error,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
export const createProject = (rawConfig, runtime = {}) => {
|
|
523
|
+
const logger = runtime.logger || console;
|
|
524
|
+
const cwd = runtime.cwd || process.cwd();
|
|
525
|
+
const runCommand = runtime.runCommand || execSync;
|
|
526
|
+
const secretGenerator = runtime.secretGenerator || createSecret;
|
|
527
|
+
const skipInstall = runtime.skipInstall ?? process.env[ENV_SKIP_INSTALL] === "1";
|
|
528
|
+
const skipGit = runtime.skipGit ?? process.env[ENV_SKIP_GIT] === "1";
|
|
529
|
+
|
|
530
|
+
const config = {
|
|
531
|
+
...rawConfig,
|
|
532
|
+
projectName: rawConfig.projectName?.trim(),
|
|
533
|
+
packageJsonName: rawConfig.packageJsonName?.trim() || rawConfig.projectName?.trim(),
|
|
534
|
+
packageManager: normalizePackageManager(
|
|
535
|
+
rawConfig.packageManager || DEFAULT_PACKAGE_MANAGER,
|
|
536
|
+
),
|
|
537
|
+
deps: {
|
|
538
|
+
...DEFAULT_DEPENDENCIES,
|
|
539
|
+
...rawConfig.deps,
|
|
540
|
+
express: true,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (!config.projectName) {
|
|
545
|
+
throw new Error("Project directory name is required.");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!config.packageJsonName) {
|
|
549
|
+
throw new Error("package.json name is required.");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const warnings = [];
|
|
553
|
+
const projectPath = path.join(cwd, config.projectName);
|
|
554
|
+
|
|
555
|
+
if (fs.existsSync(projectPath)) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`Folder ${config.projectName} already exists. Please choose a different directory name.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (config.initAuth && !config.deps.mongoose) {
|
|
562
|
+
config.deps.mongoose = true;
|
|
563
|
+
const authWarning =
|
|
564
|
+
"JWT auth boilerplate requires Mongoose in this starter, so MongoDB support was enabled automatically.";
|
|
565
|
+
warnings.push(authWarning);
|
|
566
|
+
logger.log(`\n ${authWarning}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
logger.log(`\n Creating a new Node.js Express API in ${projectPath}...`);
|
|
570
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
571
|
+
|
|
572
|
+
const sourceDir = path.join(ROOT_DIR, "src");
|
|
573
|
+
const targetSrcDir = path.join(projectPath, "src");
|
|
574
|
+
|
|
575
|
+
if (!fs.existsSync(sourceDir)) {
|
|
576
|
+
throw new Error('Could not find "src" directory in the template generator.');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
logger.log(" Bootstrapping application structure...");
|
|
580
|
+
copyRecursiveSync(sourceDir, targetSrcDir);
|
|
581
|
+
|
|
582
|
+
logger.log(" Generating environment files...");
|
|
583
|
+
const envExamplePath = path.join(ROOT_DIR, ".env.example");
|
|
584
|
+
if (fs.existsSync(envExamplePath)) {
|
|
585
|
+
fs.copyFileSync(envExamplePath, path.join(projectPath, ".env.example"));
|
|
586
|
+
fs.copyFileSync(envExamplePath, path.join(projectPath, ".env.local"));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
fs.writeFileSync(path.join(targetSrcDir, "app.js"), buildAppCode(config));
|
|
590
|
+
fs.writeFileSync(path.join(targetSrcDir, "server.js"), buildServerCode(config));
|
|
591
|
+
|
|
592
|
+
if (!config.deps.mongoose) {
|
|
593
|
+
const dbDir = path.join(targetSrcDir, "db");
|
|
594
|
+
if (fs.existsSync(dbDir)) {
|
|
595
|
+
fs.rmSync(dbDir, { recursive: true, force: true });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (config.initDocker) {
|
|
600
|
+
logger.log(" Adding Docker files...");
|
|
601
|
+
fs.writeFileSync(path.join(projectPath, "Dockerfile"), buildDockerfile(config.packageManager));
|
|
602
|
+
fs.copyFileSync(
|
|
603
|
+
path.join(ROOT_DIR, "templates", ".dockerignore"),
|
|
604
|
+
path.join(projectPath, ".dockerignore"),
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
if (config.deps.mongoose) {
|
|
608
|
+
fs.writeFileSync(path.join(projectPath, "docker-compose.yml"), buildDockerCompose());
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (config.initAuth) {
|
|
613
|
+
logger.log(" Adding auth templates...");
|
|
614
|
+
writeAuthFiles(projectPath);
|
|
615
|
+
writeAuthUtilities(projectPath);
|
|
616
|
+
addAuthEnvironment(projectPath, secretGenerator);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (config.installPinoPretty && config.deps["pino-http"]) {
|
|
620
|
+
addPinoPrettyEnvironment(projectPath);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (config.initTests) {
|
|
624
|
+
logger.log(" Adding Jest test templates...");
|
|
625
|
+
ensureDir(path.join(projectPath, "tests"));
|
|
626
|
+
fs.copyFileSync(
|
|
627
|
+
path.join(ROOT_DIR, "templates", "tests", "healthcheck.test.js"),
|
|
628
|
+
path.join(projectPath, "tests", "healthcheck.test.js"),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
logger.log(" Setting up package.json...");
|
|
633
|
+
writeJson(path.join(projectPath, "package.json"), createPackageJsonTemplate(config));
|
|
634
|
+
|
|
635
|
+
const { dependencies, devDependencies } = resolveDependencyLists(config);
|
|
636
|
+
const installResult = installDependencies({
|
|
637
|
+
projectPath,
|
|
638
|
+
packageManager: config.packageManager,
|
|
639
|
+
dependencies,
|
|
640
|
+
devDependencies,
|
|
641
|
+
runCommand,
|
|
642
|
+
logger,
|
|
643
|
+
skipInstall,
|
|
644
|
+
});
|
|
645
|
+
warnings.push(...installResult.warnings);
|
|
646
|
+
|
|
647
|
+
let gitResult = { gitInitialized: false, warnings: [] };
|
|
648
|
+
if (config.initGit) {
|
|
649
|
+
logger.log("\n Initializing Git repository...");
|
|
650
|
+
gitResult = initializeGitRepository({
|
|
651
|
+
projectPath,
|
|
652
|
+
runCommand,
|
|
653
|
+
logger,
|
|
654
|
+
skipGit,
|
|
655
|
+
});
|
|
656
|
+
warnings.push(...gitResult.warnings);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
projectPath,
|
|
661
|
+
config,
|
|
662
|
+
dependencies,
|
|
663
|
+
devDependencies,
|
|
664
|
+
installSucceeded: installResult.installSucceeded,
|
|
665
|
+
gitInitialized: gitResult.gitInitialized,
|
|
666
|
+
warnings,
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const createQuestioner = () => {
|
|
671
|
+
const rl = readline.createInterface({
|
|
672
|
+
input: process.stdin,
|
|
673
|
+
output: process.stdout,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
ask(prompt) {
|
|
678
|
+
return new Promise((resolve) => rl.question(prompt, resolve));
|
|
679
|
+
},
|
|
680
|
+
close() {
|
|
681
|
+
rl.close();
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
export const runCli = async ({
|
|
687
|
+
argv = process.argv,
|
|
688
|
+
cwd = process.cwd(),
|
|
689
|
+
logger = console,
|
|
690
|
+
questioner = createQuestioner(),
|
|
691
|
+
} = {}) => {
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
let projectName = argv[2];
|
|
695
|
+
if (!projectName) {
|
|
696
|
+
projectName = await questioner.ask("\n> Project Directory Name (e.g. my-awesome-api): ");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!projectName?.trim()) {
|
|
700
|
+
throw new Error("Project directory name is required.");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
let packageJsonName = await questioner.ask(`> package.json name (${projectName}): `);
|
|
704
|
+
if (!packageJsonName.trim()) {
|
|
705
|
+
packageJsonName = projectName;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const description = await questioner.ask("> Project description: ");
|
|
709
|
+
const author = await questioner.ask("> Author name: ");
|
|
710
|
+
|
|
711
|
+
logger.log("\n--- Select Dependencies ---");
|
|
712
|
+
logger.log('Press Enter for Yes (Y), type "n" for No.\n');
|
|
713
|
+
|
|
714
|
+
const deps = {
|
|
715
|
+
express: true,
|
|
716
|
+
mongoose: parseYesNo(await questioner.ask("Include Mongoose (MongoDB)? [Y/n] ")),
|
|
717
|
+
cors: parseYesNo(await questioner.ask("Include CORS? [Y/n] ")),
|
|
718
|
+
helmet: parseYesNo(await questioner.ask("Include Helmet (Security headers)? [Y/n] ")),
|
|
719
|
+
"cookie-parser": parseYesNo(await questioner.ask("Include cookie-parser? [Y/n] ")),
|
|
720
|
+
"pino-http": parseYesNo(await questioner.ask("Include Pino (HTTP Logger)? [Y/n] ")),
|
|
721
|
+
"express-rate-limit": parseYesNo(
|
|
722
|
+
await questioner.ask("Include Rate Limiting? [Y/n] "),
|
|
723
|
+
),
|
|
724
|
+
dotenv: parseYesNo(await questioner.ask("Include dotenvx (Environment variables)? [Y/n] ")),
|
|
725
|
+
prettier: parseYesNo(await questioner.ask("Include Prettier (Code formatter)? [Y/n] ")),
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
let installPinoPretty = false;
|
|
729
|
+
if (deps["pino-http"]) {
|
|
730
|
+
installPinoPretty = parseYesNo(
|
|
731
|
+
await questioner.ask("Include pino-pretty for clean development logs? [Y/n] "),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const packageManagerChoice = await questioner.ask(
|
|
736
|
+
"\n> Which package manager would you like to use? [npm/yarn/pnpm/bun] (default: npm): ",
|
|
737
|
+
);
|
|
738
|
+
const packageManager = normalizePackageManager(packageManagerChoice);
|
|
739
|
+
|
|
740
|
+
const initGit = parseYesNo(await questioner.ask("\n> Initialize a git repository? [Y/n] "));
|
|
741
|
+
const initDocker = parseYesNo(
|
|
742
|
+
await questioner.ask("> Include Dockerfile & docker-compose.yml? [Y/n] "),
|
|
743
|
+
);
|
|
744
|
+
const initAuth = parseYesNo(
|
|
745
|
+
await questioner.ask("> Include basic JWT Auth boilerplate? [Y/n] "),
|
|
746
|
+
);
|
|
747
|
+
const initTests = parseYesNo(
|
|
748
|
+
await questioner.ask("> Include Jest setup and boilerplate tests? [Y/n] "),
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const result = createProject(
|
|
752
|
+
{
|
|
753
|
+
projectName,
|
|
754
|
+
packageJsonName,
|
|
755
|
+
description,
|
|
756
|
+
author,
|
|
757
|
+
deps,
|
|
758
|
+
installPinoPretty,
|
|
759
|
+
packageManager,
|
|
760
|
+
initGit,
|
|
761
|
+
initDocker,
|
|
762
|
+
initAuth,
|
|
763
|
+
initTests,
|
|
764
|
+
},
|
|
765
|
+
{ cwd, logger },
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
logger.log(`\n Success! Created "${projectName}" at ${result.projectPath}`);
|
|
769
|
+
|
|
770
|
+
if (result.warnings.length > 0) {
|
|
771
|
+
logger.log("\nNotes:");
|
|
772
|
+
for (const warning of result.warnings) {
|
|
773
|
+
logger.log(`- ${warning}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const devCommand =
|
|
778
|
+
result.config.packageManager === "npm"
|
|
779
|
+
? "npm run dev"
|
|
780
|
+
: `${result.config.packageManager} dev`;
|
|
781
|
+
const startCommand =
|
|
782
|
+
result.config.packageManager === "npm"
|
|
783
|
+
? "npm start"
|
|
784
|
+
: `${result.config.packageManager} start`;
|
|
785
|
+
|
|
786
|
+
logger.log("\nInside that directory, you can run:");
|
|
787
|
+
logger.log(`\n ${devCommand}`);
|
|
788
|
+
logger.log(" Starts the development server.");
|
|
789
|
+
logger.log(`\n ${startCommand}`);
|
|
790
|
+
logger.log(" Starts the production server.");
|
|
791
|
+
logger.log("\nWe suggest that you begin with:");
|
|
792
|
+
logger.log(`\n cd ${projectName}`);
|
|
793
|
+
logger.log(` ${devCommand}\n`);
|
|
794
|
+
} finally {
|
|
795
|
+
questioner.close();
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
|
800
|
+
runCli().catch((error) => {
|
|
801
|
+
console.error(`\n${error.message}`);
|
|
802
|
+
process.exit(1);
|
|
803
|
+
});
|
|
804
|
+
}
|