Skip to content

Commit 99f5d1e

Browse files
committed
Strengthen verification workflow
1 parent 4372e4f commit 99f5d1e

4 files changed

Lines changed: 169 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ jobs:
3434
- name: Install dependencies
3535
run: npm ci
3636

37-
- name: Run tests
38-
run: npm test
37+
- name: Run verification
38+
run: npm run check

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ Node.js 20.12.0 or newer is required.
2323
npx setup-node-api my-api
2424
```
2525

26+
Run it against the latest published version explicitly:
27+
28+
```bash
29+
npx setup-node-api@latest my-api
30+
```
31+
2632
Create a TypeScript project:
2733

2834
```bash
@@ -82,11 +88,34 @@ my-api/
8288
`-- app.ts
8389
```
8490

91+
The generated project name in `package.json` is automatically set to the selected folder name.
92+
93+
## Examples
94+
95+
Create a JavaScript project without installing dependencies:
96+
97+
```bash
98+
setup-node-api my-api --no-install
99+
```
100+
101+
Create a TypeScript project and set a custom port:
102+
103+
```bash
104+
setup-node-api my-api --typescript --port 4000
105+
```
106+
107+
Ask the CLI to guide you interactively:
108+
109+
```bash
110+
setup-node-api
111+
```
112+
85113
## Development
86114

87115
```bash
88116
npm install
89117
npm test
118+
npm run check
90119
```
91120

92121
## CI
@@ -97,7 +126,7 @@ GitHub Actions runs the test suite on Node.js 20 and 22 across Linux, Windows, a
97126

98127
- In a non-interactive environment, provide the project name as an argument.
99128
- If the target folder already exists, the CLI stops unless you explicitly confirm overwrite in an interactive terminal.
100-
- The generated project package name is automatically set to the selected folder name.
129+
- `prepublishOnly` runs `npm run check` before publish.
101130

102131
## License
103132

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
"bin": {
88
"setup-node-api": "bin/cli.js"
99
},
10+
"packageManager": "npm@10.9.4",
1011
"preferGlobal": true,
1112
"scripts": {
1213
"start": "node bin/cli.js",
1314
"dev": "node bin/cli.js",
14-
"test": "node test/run-tests.js"
15+
"test": "node test/run-tests.js",
16+
"check": "npm test",
17+
"prepublishOnly": "npm run check"
1518
},
1619
"keywords": [
1720
"cli",

test/run-tests.js

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const assert = require("node:assert/strict");
2+
const { execFileSync } = require("node:child_process");
23
const fs = require("node:fs");
34
const os = require("node:os");
45
const path = require("node:path");
@@ -8,6 +9,9 @@ const { createApp } = require("../src/core");
89
const { askProjectDetails } = require("../src/core/promptService");
910
const { validateProjectName } = require("../src/core/validators/projectValidator");
1011

12+
const cliPath = path.join(__dirname, "..", "bin", "cli.js");
13+
let cliSpawningUnavailable = false;
14+
1115
async function withTempDir(run) {
1216
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "setup-node-api-"));
1317
const previousCwd = process.cwd();
@@ -21,6 +25,55 @@ async function withTempDir(run) {
2125
}
2226
}
2327

28+
function runCli(args, cwd) {
29+
try {
30+
return execFileSync(process.execPath, [cliPath, ...args], {
31+
cwd,
32+
encoding: "utf8",
33+
stdio: ["ignore", "pipe", "pipe"],
34+
});
35+
} catch (error) {
36+
if (error.code === "EPERM" && !error.stdout && !error.stderr) {
37+
cliSpawningUnavailable = true;
38+
throw new Error("CLI spawning is unavailable in this environment.");
39+
}
40+
throw error;
41+
}
42+
}
43+
44+
function runCliExpectFailure(args, cwd) {
45+
try {
46+
runCli(args, cwd);
47+
throw new Error("Expected CLI command to fail");
48+
} catch (error) {
49+
if (!error.stdout && !error.stderr) {
50+
throw error;
51+
}
52+
53+
return {
54+
stdout: error.stdout ?? "",
55+
stderr: error.stderr ?? "",
56+
status: error.status,
57+
};
58+
}
59+
}
60+
61+
async function maybeRunCliTest(testFn) {
62+
if (cliSpawningUnavailable) {
63+
return "CLI spawning is unavailable in this environment.";
64+
}
65+
66+
try {
67+
await testFn();
68+
return null;
69+
} catch (error) {
70+
if (error.message === "CLI spawning is unavailable in this environment.") {
71+
return error.message;
72+
}
73+
throw error;
74+
}
75+
}
76+
2477
async function testJavaScriptTemplate() {
2578
await withTempDir(async (tempRoot) => {
2679
await createProject("sample-api", {
@@ -90,6 +143,64 @@ async function testCreateAppRejectsExistingDirectoryAfterPromptResolution() {
90143
});
91144
}
92145

146+
async function testCliHelpOutput() {
147+
const output = runCli(["--help"], process.cwd());
148+
149+
assert.match(output, /Scaffold a Node\.js \+ Express API/);
150+
assert.match(output, /--typescript/);
151+
assert.match(output, /--no-install/);
152+
}
153+
154+
async function testCliScaffoldsJavaScriptProject() {
155+
await withTempDir(async (tempRoot) => {
156+
const output = runCli(["cli-js-app", "--no-install", "--port", "5050"], tempRoot);
157+
158+
const packageJson = JSON.parse(
159+
fs.readFileSync(path.join(tempRoot, "cli-js-app", "package.json"), "utf8")
160+
);
161+
const appFile = fs.readFileSync(
162+
path.join(tempRoot, "cli-js-app", "src", "app.js"),
163+
"utf8"
164+
);
165+
const envFile = fs.readFileSync(path.join(tempRoot, "cli-js-app", ".env"), "utf8");
166+
167+
assert.equal(packageJson.name, "cli-js-app");
168+
assert.equal(packageJson.private, true);
169+
assert.match(appFile, /app\.get\("\/health"/);
170+
assert.equal(envFile, "PORT=5050\n");
171+
assert.match(output, /Project ready\./);
172+
});
173+
}
174+
175+
async function testCliScaffoldsTypeScriptProject() {
176+
await withTempDir(async (tempRoot) => {
177+
runCli(["cli-ts-app", "--typescript", "--no-install"], tempRoot);
178+
179+
const packageJson = JSON.parse(
180+
fs.readFileSync(path.join(tempRoot, "cli-ts-app", "package.json"), "utf8")
181+
);
182+
const tsconfig = fs.readFileSync(
183+
path.join(tempRoot, "cli-ts-app", "tsconfig.json"),
184+
"utf8"
185+
);
186+
187+
assert.equal(packageJson.name, "cli-ts-app");
188+
assert.equal(packageJson.private, true);
189+
assert.match(tsconfig, /"target": "ES2020"/);
190+
assert.match(tsconfig, /"forceConsistentCasingInFileNames": true/);
191+
});
192+
}
193+
194+
async function testCliRejectsInvalidPort() {
195+
const { stdout, status } = runCliExpectFailure(
196+
["demo-app", "--no-install", "--port", "99999"],
197+
process.cwd()
198+
);
199+
200+
assert.equal(status, 1);
201+
assert.match(stdout, /Port must be an integer between 1 and 65535\./);
202+
}
203+
93204
async function main() {
94205
const tests = [
95206
["JavaScript template customization", testJavaScriptTemplate],
@@ -100,14 +211,34 @@ async function main() {
100211
"Existing-directory protection after final project resolution",
101212
testCreateAppRejectsExistingDirectoryAfterPromptResolution,
102213
],
214+
["CLI help output", () => maybeRunCliTest(testCliHelpOutput)],
215+
[
216+
"CLI JavaScript scaffold smoke test",
217+
() => maybeRunCliTest(testCliScaffoldsJavaScriptProject),
218+
],
219+
[
220+
"CLI TypeScript scaffold smoke test",
221+
() => maybeRunCliTest(testCliScaffoldsTypeScriptProject),
222+
],
223+
["CLI invalid port handling", () => maybeRunCliTest(testCliRejectsInvalidPort)],
103224
];
225+
let passedCount = 0;
226+
let skippedCount = 0;
104227

105228
for (const [name, testFn] of tests) {
106-
await testFn();
229+
const note = await testFn();
230+
231+
if (note) {
232+
skippedCount += 1;
233+
console.log(`SKIP ${name}: ${note}`);
234+
continue;
235+
}
236+
237+
passedCount += 1;
107238
console.log(`PASS ${name}`);
108239
}
109240

110-
console.log(`\n${tests.length} tests passed`);
241+
console.log(`\n${passedCount} tests passed, ${skippedCount} skipped`);
111242
}
112243

113244
main().catch((error) => {

0 commit comments

Comments
 (0)