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.
@@ -0,0 +1,573 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import vm from "node:vm";
6
+
7
+ import { createProject, runCli } from "../bin/cli.js";
8
+
9
+ const silentLogger = {
10
+ log() {},
11
+ warn() {},
12
+ error() {},
13
+ };
14
+
15
+ const tests = [];
16
+
17
+ const registerTest = (name, fn) => {
18
+ tests.push({ name, fn });
19
+ };
20
+
21
+ const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), "cek-"));
22
+
23
+ const readText = (...segments) => fs.readFileSync(path.join(...segments), "utf8");
24
+
25
+ const readJson = (...segments) => JSON.parse(readText(...segments));
26
+
27
+ const collectJsFiles = (dirPath) => {
28
+ const files = [];
29
+
30
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
31
+ const entryPath = path.join(dirPath, entry.name);
32
+
33
+ if (entry.isDirectory()) {
34
+ if (entry.name !== "node_modules") {
35
+ files.push(...collectJsFiles(entryPath));
36
+ }
37
+
38
+ continue;
39
+ }
40
+
41
+ if (entry.name.endsWith(".js")) {
42
+ files.push(entryPath);
43
+ }
44
+ }
45
+
46
+ return files;
47
+ };
48
+
49
+ const assertSyntaxValid = (projectPath) => {
50
+ for (const jsFile of collectJsFiles(projectPath)) {
51
+ const source = fs
52
+ .readFileSync(jsFile, "utf8")
53
+ .replace(/^import\s.+?;$/gm, "")
54
+ .replace(/^export\s+default\s+/gm, "")
55
+ .replace(/^export\s+\{[^}]+\};?$/gm, "")
56
+ .replace(/^export\s+/gm, "");
57
+
58
+ assert.doesNotThrow(() => {
59
+ new vm.Script(source, { filename: jsFile });
60
+ }, `Expected ${jsFile} to parse successfully.`);
61
+ }
62
+ };
63
+
64
+ const assertNoTemplateTokens = (projectPath) => {
65
+ for (const jsFile of collectJsFiles(projectPath)) {
66
+ const contents = fs.readFileSync(jsFile, "utf8");
67
+ assert.equal(
68
+ contents.includes("__"),
69
+ false,
70
+ `Template token leaked into generated file ${jsFile}.`,
71
+ );
72
+ }
73
+ };
74
+
75
+ const makeConfig = (overrides = {}) => {
76
+ const baseDeps = {
77
+ express: true,
78
+ mongoose: true,
79
+ cors: true,
80
+ helmet: true,
81
+ "cookie-parser": true,
82
+ "pino-http": true,
83
+ "express-rate-limit": true,
84
+ dotenv: true,
85
+ prettier: true,
86
+ };
87
+
88
+ return {
89
+ projectName: "sample-app",
90
+ packageJsonName: "sample-app",
91
+ description: "A generated project",
92
+ author: "Codex",
93
+ deps: {
94
+ ...baseDeps,
95
+ ...(overrides.deps || {}),
96
+ },
97
+ installPinoPretty: true,
98
+ packageManager: "npm",
99
+ initGit: false,
100
+ initDocker: true,
101
+ initAuth: true,
102
+ initTests: true,
103
+ ...overrides,
104
+ };
105
+ };
106
+
107
+ registerTest(
108
+ "scaffolds a full project with safe auth, CORS, Docker, and logger defaults",
109
+ () => {
110
+ const tempRoot = createTempRoot();
111
+
112
+ try {
113
+ const result = createProject(
114
+ makeConfig({
115
+ projectName: "full-app",
116
+ packageJsonName: "full-app",
117
+ }),
118
+ {
119
+ cwd: tempRoot,
120
+ skipInstall: true,
121
+ skipGit: true,
122
+ logger: silentLogger,
123
+ secretGenerator: () => "unit-test-secret",
124
+ },
125
+ );
126
+
127
+ const projectPath = result.projectPath;
128
+ const packageJson = readJson(projectPath, "package.json");
129
+ const envExample = readText(projectPath, ".env.example");
130
+ const envLocal = readText(projectPath, ".env.local");
131
+ const appCode = readText(projectPath, "src", "app.js");
132
+ const dbCode = readText(projectPath, "src", "db", "index.js");
133
+ const dockerfile = readText(projectPath, "Dockerfile");
134
+ const dockerCompose = readText(projectPath, "docker-compose.yml");
135
+
136
+ assert.equal(fs.existsSync(path.join(projectPath, "src", "models", "user.model.js")), true);
137
+ assert.equal(fs.existsSync(path.join(projectPath, "tests", "healthcheck.test.js")), true);
138
+
139
+ assert.deepEqual(
140
+ Object.keys(packageJson.dependencies).sort(),
141
+ [
142
+ "bcryptjs",
143
+ "cookie-parser",
144
+ "cors",
145
+ "express",
146
+ "express-rate-limit",
147
+ "helmet",
148
+ "jsonwebtoken",
149
+ "mongoose",
150
+ "pino",
151
+ "pino-http",
152
+ ],
153
+ );
154
+ assert.deepEqual(
155
+ Object.keys(packageJson.devDependencies).sort(),
156
+ ["@dotenvx/dotenvx", "jest", "nodemon", "pino-pretty", "prettier", "supertest"],
157
+ );
158
+
159
+ assert.match(envExample, /JWT_SECRET=replace-me-with-a-long-random-secret/);
160
+ assert.doesNotMatch(envExample, /JWT_SECRET=unit-test-secret/);
161
+ assert.match(envLocal, /JWT_SECRET=unit-test-secret/);
162
+ assert.match(envLocal, /PINO_PRETTY=true/);
163
+
164
+ assert.match(appCode, /const enablePrettyLogs/);
165
+ assert.match(appCode, /credentials: !allowAllOrigins && allowedOrigins.length > 0/);
166
+ assert.match(appCode, /app\.use\("\/api\/v1\/auth", authRouter\);/);
167
+ assert.doesNotMatch(appCode, /import\("pino-pretty"\)/);
168
+ assert.doesNotMatch(dbCode, /DB_NAME/);
169
+ assert.match(dbCode, /mongoose\.connect\(process\.env\.MONGODB_URI\)/);
170
+
171
+ assert.match(dockerfile, /FROM node:22-alpine/);
172
+ assert.match(dockerfile, /RUN npm install --omit=dev/);
173
+ assert.match(dockerfile, /EXPOSE 8000/);
174
+ assert.match(dockerCompose, /8000:8000/);
175
+ assert.match(dockerCompose, /mongodb:\/\/mongo:27017\/my_app_db/);
176
+
177
+ assertNoTemplateTokens(projectPath);
178
+ assertSyntaxValid(projectPath);
179
+ } finally {
180
+ fs.rmSync(tempRoot, { recursive: true, force: true });
181
+ }
182
+ },
183
+ );
184
+
185
+ registerTest(
186
+ "scaffolds a minimal project without optional middleware, auth, db, or tests",
187
+ () => {
188
+ const tempRoot = createTempRoot();
189
+
190
+ try {
191
+ const result = createProject(
192
+ makeConfig({
193
+ projectName: "minimal-app",
194
+ packageJsonName: "minimal-app",
195
+ deps: {
196
+ mongoose: false,
197
+ cors: false,
198
+ helmet: false,
199
+ "cookie-parser": false,
200
+ "pino-http": false,
201
+ "express-rate-limit": false,
202
+ dotenv: false,
203
+ prettier: false,
204
+ },
205
+ installPinoPretty: false,
206
+ initDocker: false,
207
+ initAuth: false,
208
+ initTests: false,
209
+ }),
210
+ {
211
+ cwd: tempRoot,
212
+ skipInstall: true,
213
+ skipGit: true,
214
+ logger: silentLogger,
215
+ },
216
+ );
217
+
218
+ const projectPath = result.projectPath;
219
+ const packageJson = readJson(projectPath, "package.json");
220
+ const appCode = readText(projectPath, "src", "app.js");
221
+ const serverCode = readText(projectPath, "src", "server.js");
222
+
223
+ assert.equal(fs.existsSync(path.join(projectPath, "src", "db")), false);
224
+ assert.equal(fs.existsSync(path.join(projectPath, "src", "routes", "auth.routes.js")), false);
225
+ assert.equal(fs.existsSync(path.join(projectPath, "tests")), false);
226
+ assert.equal(fs.existsSync(path.join(projectPath, "Dockerfile")), false);
227
+
228
+ assert.deepEqual(Object.keys(packageJson.dependencies), ["express"]);
229
+ assert.deepEqual(Object.keys(packageJson.devDependencies), ["nodemon"]);
230
+
231
+ assert.doesNotMatch(appCode, /cors/);
232
+ assert.doesNotMatch(appCode, /cookieParser/);
233
+ assert.doesNotMatch(appCode, /helmet/);
234
+ assert.doesNotMatch(appCode, /pinoHttp/);
235
+ assert.doesNotMatch(appCode, /rateLimit/);
236
+ assert.doesNotMatch(appCode, /authRouter/);
237
+ assert.doesNotMatch(serverCode, /connectDB/);
238
+ assert.match(serverCode, /startServer\(\);/);
239
+
240
+ assertNoTemplateTokens(projectPath);
241
+ assertSyntaxValid(projectPath);
242
+ } finally {
243
+ fs.rmSync(tempRoot, { recursive: true, force: true });
244
+ }
245
+ },
246
+ );
247
+
248
+ registerTest("auth scaffolding automatically enables mongoose when auth is selected", () => {
249
+ const tempRoot = createTempRoot();
250
+
251
+ try {
252
+ const result = createProject(
253
+ makeConfig({
254
+ projectName: "auth-app",
255
+ packageJsonName: "auth-app",
256
+ deps: {
257
+ mongoose: false,
258
+ },
259
+ initAuth: true,
260
+ initDocker: false,
261
+ initTests: false,
262
+ }),
263
+ {
264
+ cwd: tempRoot,
265
+ skipInstall: true,
266
+ skipGit: true,
267
+ logger: silentLogger,
268
+ secretGenerator: () => "another-secret",
269
+ },
270
+ );
271
+
272
+ assert.equal(result.config.deps.mongoose, true);
273
+ assert.equal(
274
+ result.warnings.some((warning) => warning.includes("enabled automatically")),
275
+ true,
276
+ );
277
+ assert.equal(fs.existsSync(path.join(result.projectPath, "src", "db", "index.js")), true);
278
+
279
+ const packageJson = readJson(result.projectPath, "package.json");
280
+ assert.equal(packageJson.dependencies.mongoose, "latest");
281
+ } finally {
282
+ fs.rmSync(tempRoot, { recursive: true, force: true });
283
+ }
284
+ });
285
+
286
+ registerTest("renders Dockerfiles for npm, yarn, pnpm, and bun", () => {
287
+ const packageManagers = {
288
+ npm: {
289
+ baseImage: "FROM node:22-alpine",
290
+ installCommand: "RUN npm install --omit=dev",
291
+ runtime: 'CMD [ "node", "src/server.js" ]',
292
+ },
293
+ yarn: {
294
+ baseImage: "FROM node:22-alpine",
295
+ installCommand: "RUN yarn install --production=true",
296
+ runtime: 'CMD [ "node", "src/server.js" ]',
297
+ },
298
+ pnpm: {
299
+ baseImage: "FROM node:22-alpine",
300
+ installCommand: "RUN pnpm install --prod",
301
+ runtime: 'CMD [ "node", "src/server.js" ]',
302
+ },
303
+ bun: {
304
+ baseImage: "FROM oven/bun:1-alpine",
305
+ installCommand: "RUN bun install --production",
306
+ runtime: 'CMD [ "bun", "src/server.js" ]',
307
+ },
308
+ };
309
+
310
+ for (const [packageManager, expectations] of Object.entries(packageManagers)) {
311
+ const tempRoot = createTempRoot();
312
+
313
+ try {
314
+ const result = createProject(
315
+ makeConfig({
316
+ projectName: `${packageManager}-app`,
317
+ packageJsonName: `${packageManager}-app`,
318
+ packageManager,
319
+ initDocker: true,
320
+ initAuth: false,
321
+ initTests: false,
322
+ }),
323
+ {
324
+ cwd: tempRoot,
325
+ skipInstall: true,
326
+ skipGit: true,
327
+ logger: silentLogger,
328
+ },
329
+ );
330
+
331
+ const dockerfile = readText(result.projectPath, "Dockerfile");
332
+
333
+ assert.match(dockerfile, new RegExp(expectations.baseImage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
334
+ assert.match(
335
+ dockerfile,
336
+ new RegExp(expectations.installCommand.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
337
+ );
338
+ assert.match(
339
+ dockerfile,
340
+ new RegExp(expectations.runtime.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
341
+ );
342
+ assert.equal(dockerfile.includes("__"), false);
343
+ } finally {
344
+ fs.rmSync(tempRoot, { recursive: true, force: true });
345
+ }
346
+ }
347
+ });
348
+
349
+ registerTest("pins installed dependency versions after a successful install", () => {
350
+ const tempRoot = createTempRoot();
351
+
352
+ try {
353
+ const versions = {
354
+ express: "4.21.2",
355
+ nodemon: "3.1.9",
356
+ };
357
+ const commands = [];
358
+
359
+ const runCommand = (command, options) => {
360
+ commands.push(command);
361
+
362
+ if (!command.includes("install")) {
363
+ return;
364
+ }
365
+
366
+ const packageJson = readJson(options.cwd, "package.json");
367
+ const allDependencies = {
368
+ ...(packageJson.dependencies || {}),
369
+ ...(packageJson.devDependencies || {}),
370
+ };
371
+
372
+ for (const dependencyName of Object.keys(allDependencies)) {
373
+ const dependencyDir = path.join(options.cwd, "node_modules", dependencyName);
374
+ fs.mkdirSync(dependencyDir, { recursive: true });
375
+ fs.writeFileSync(
376
+ path.join(dependencyDir, "package.json"),
377
+ JSON.stringify({
378
+ name: dependencyName,
379
+ version: versions[dependencyName] || "1.0.0",
380
+ }),
381
+ );
382
+ }
383
+ };
384
+
385
+ const result = createProject(
386
+ makeConfig({
387
+ projectName: "versioned-app",
388
+ packageJsonName: "versioned-app",
389
+ deps: {
390
+ mongoose: false,
391
+ cors: false,
392
+ helmet: false,
393
+ "cookie-parser": false,
394
+ "pino-http": false,
395
+ "express-rate-limit": false,
396
+ dotenv: false,
397
+ prettier: false,
398
+ },
399
+ installPinoPretty: false,
400
+ initDocker: false,
401
+ initAuth: false,
402
+ initTests: false,
403
+ }),
404
+ {
405
+ cwd: tempRoot,
406
+ logger: silentLogger,
407
+ runCommand,
408
+ skipGit: true,
409
+ },
410
+ );
411
+
412
+ const packageJson = readJson(result.projectPath, "package.json");
413
+
414
+ assert.equal(result.installSucceeded, true);
415
+ assert.deepEqual(commands, ["npm install"]);
416
+ assert.equal(packageJson.dependencies.express, "^4.21.2");
417
+ assert.equal(packageJson.devDependencies.nodemon, "^3.1.9");
418
+ } finally {
419
+ fs.rmSync(tempRoot, { recursive: true, force: true });
420
+ }
421
+ });
422
+
423
+ registerTest("continues scaffolding even if the initial git commit fails", () => {
424
+ const tempRoot = createTempRoot();
425
+
426
+ try {
427
+ const commands = [];
428
+ const runCommand = (command) => {
429
+ commands.push(command);
430
+
431
+ if (command.startsWith('git commit -m "initial commit"')) {
432
+ throw new Error("git identity missing");
433
+ }
434
+ };
435
+
436
+ const result = createProject(
437
+ makeConfig({
438
+ projectName: "git-app",
439
+ packageJsonName: "git-app",
440
+ deps: {
441
+ mongoose: false,
442
+ cors: false,
443
+ helmet: false,
444
+ "cookie-parser": false,
445
+ "pino-http": false,
446
+ "express-rate-limit": false,
447
+ dotenv: false,
448
+ prettier: false,
449
+ },
450
+ installPinoPretty: false,
451
+ initDocker: false,
452
+ initAuth: false,
453
+ initTests: false,
454
+ initGit: true,
455
+ }),
456
+ {
457
+ cwd: tempRoot,
458
+ logger: silentLogger,
459
+ runCommand,
460
+ skipInstall: true,
461
+ },
462
+ );
463
+
464
+ assert.equal(result.gitInitialized, false);
465
+ assert.equal(
466
+ result.warnings.some((warning) => warning.includes("Git initialization completed partially")),
467
+ true,
468
+ );
469
+ assert.deepEqual(commands, ["git init", "git add .", 'git commit -m "initial commit"']);
470
+ assert.equal(fs.existsSync(path.join(result.projectPath, ".gitignore")), true);
471
+ } finally {
472
+ fs.rmSync(tempRoot, { recursive: true, force: true });
473
+ }
474
+ });
475
+
476
+ registerTest("supports the interactive CLI path end to end without spawning a subprocess", async () => {
477
+ const tempRoot = createTempRoot();
478
+ const logs = [];
479
+ const answers = [
480
+ "",
481
+ "Smoke test app",
482
+ "Tester",
483
+ "n",
484
+ "n",
485
+ "n",
486
+ "n",
487
+ "n",
488
+ "n",
489
+ "n",
490
+ "n",
491
+ "",
492
+ "n",
493
+ "n",
494
+ "n",
495
+ "n",
496
+ ];
497
+ const questioner = {
498
+ ask() {
499
+ const answer = answers.shift();
500
+ if (answer === undefined) {
501
+ throw new Error("Questioner ran out of canned answers.");
502
+ }
503
+
504
+ return Promise.resolve(answer);
505
+ },
506
+ close() {},
507
+ };
508
+
509
+ const logger = {
510
+ log(message) {
511
+ logs.push(message);
512
+ },
513
+ warn(message) {
514
+ logs.push(message);
515
+ },
516
+ error(message) {
517
+ logs.push(message);
518
+ },
519
+ };
520
+
521
+ const previousSkipInstall = process.env.CREATE_EXPRESS_KICKSTART_SKIP_INSTALL;
522
+ const previousSkipGit = process.env.CREATE_EXPRESS_KICKSTART_SKIP_GIT;
523
+
524
+ process.env.CREATE_EXPRESS_KICKSTART_SKIP_INSTALL = "1";
525
+ process.env.CREATE_EXPRESS_KICKSTART_SKIP_GIT = "1";
526
+
527
+ try {
528
+ await runCli({
529
+ argv: ["node", "bin/cli.js", "smoke-app"],
530
+ cwd: tempRoot,
531
+ logger,
532
+ questioner,
533
+ });
534
+
535
+ assert.equal(fs.existsSync(path.join(tempRoot, "smoke-app", "package.json")), true);
536
+ assert.equal(logs.join("\n").includes('Success! Created "smoke-app"'), true);
537
+ } finally {
538
+ if (previousSkipInstall === undefined) {
539
+ delete process.env.CREATE_EXPRESS_KICKSTART_SKIP_INSTALL;
540
+ } else {
541
+ process.env.CREATE_EXPRESS_KICKSTART_SKIP_INSTALL = previousSkipInstall;
542
+ }
543
+
544
+ if (previousSkipGit === undefined) {
545
+ delete process.env.CREATE_EXPRESS_KICKSTART_SKIP_GIT;
546
+ } else {
547
+ process.env.CREATE_EXPRESS_KICKSTART_SKIP_GIT = previousSkipGit;
548
+ }
549
+
550
+ fs.rmSync(tempRoot, { recursive: true, force: true });
551
+ }
552
+ });
553
+
554
+ let passed = 0;
555
+
556
+ for (const { name, fn } of tests) {
557
+ try {
558
+ await fn();
559
+ passed += 1;
560
+ console.log(`ok - ${name}`);
561
+ } catch (error) {
562
+ console.error(`not ok - ${name}`);
563
+ console.error(error.stack || error);
564
+ process.exitCode = 1;
565
+ }
566
+ }
567
+
568
+ console.log(`\n${passed}/${tests.length} tests passed`);
569
+
570
+ if (process.exitCode) {
571
+ process.exit(process.exitCode);
572
+ }
573
+
@@ -1,18 +0,0 @@
1
- //example-model.js
2
- import mongoose from "mongoose";
3
-
4
- const exampleSchema = new mongoose.Schema({
5
- name: {
6
- type: String,
7
- required: [true, "Name is required"],
8
- },
9
- age: {
10
- type: Number,
11
- required: [true, "Age is required"],
12
- },
13
- email: {
14
- type: String,
15
- required: [true, "Email is required"],
16
- unique: true,
17
- },
18
- });
@@ -1 +0,0 @@
1
- export const DB_NAME = "my_app_db";