Skip to content

Commit e1a8d2f

Browse files
committed
Release v1.2.0
1 parent 009866d commit e1a8d2f

18 files changed

Lines changed: 460 additions & 72 deletions

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ Node.js 20.12.0 or newer is required.
1313
- Scaffold a ready-to-run Express API
1414
- Choose JavaScript or TypeScript
1515
- Use prompts interactively or pass CLI flags
16+
- Accept defaults instantly with `--yes`
1617
- Validate project names before generation
1718
- Avoid overwriting existing folders without confirmation
1819
- Optionally skip dependency installation
20+
- Optionally enable CORS during generation
21+
- Include basic API defaults like JSON parsing, request logging, `/health`, `/echo`, and common starter files
1922

2023
## Quick start
2124

@@ -47,6 +50,18 @@ Set a custom port:
4750
npx setup-node-api my-api --port 8080
4851
```
4952

53+
Generate with defaults and skip prompts:
54+
55+
```bash
56+
npx setup-node-api my-api --yes
57+
```
58+
59+
Generate with CORS enabled:
60+
61+
```bash
62+
npx setup-node-api my-api --cors
63+
```
64+
5065
## CLI usage
5166

5267
```bash
@@ -60,6 +75,8 @@ setup-node-api [project-name] [options]
6075
| `--typescript` | Generate the TypeScript template |
6176
| `--no-install` | Skip dependency installation |
6277
| `--port <number>` | Write a custom `PORT` value to `.env` |
78+
| `--cors` | Add CORS support to the generated API |
79+
| `-y, --yes` | Use defaults for any missing options |
6380
| `-h, --help` | Show help |
6481
| `-V, --version` | Show the installed CLI version |
6582

@@ -71,8 +88,10 @@ JavaScript template:
7188

7289
```text
7390
my-api/
91+
|-- .gitignore
7492
|-- .env
7593
|-- package.json
94+
|-- README.md
7695
`-- src/
7796
`-- app.js
7897
```
@@ -81,14 +100,16 @@ TypeScript template:
81100

82101
```text
83102
my-api/
103+
|-- .gitignore
84104
|-- .env
85105
|-- package.json
106+
|-- README.md
86107
|-- tsconfig.json
87108
`-- src/
88109
`-- app.ts
89110
```
90111

91-
The generated project name in `package.json` is automatically set to the selected folder name.
112+
The generated project name in `package.json` is automatically set to the selected folder name, and the starter app includes request logging, JSON parsing, a `/health` endpoint, a `/echo` endpoint, a 404 handler, and a basic error handler.
92113

93114
## Examples
94115

@@ -104,6 +125,12 @@ Create a TypeScript project and set a custom port:
104125
setup-node-api my-api --typescript --port 4000
105126
```
106127

128+
Create a JavaScript API with CORS enabled and no prompts:
129+
130+
```bash
131+
setup-node-api my-api --cors --yes
132+
```
133+
107134
Ask the CLI to guide you interactively:
108135

109136
```bash
@@ -125,7 +152,9 @@ GitHub Actions runs the test suite on Node.js 20 and 22 across Linux, Windows, a
125152
## Notes
126153

127154
- In a non-interactive environment, provide the project name as an argument.
155+
- When using `--yes`, you still need to provide the project name as an argument.
128156
- If the target folder already exists, the CLI stops unless you explicitly confirm overwrite in an interactive terminal.
157+
- The JavaScript template includes a `dev` script using Node's built-in watch mode.
129158
- `prepublishOnly` runs `npm run check` before publish.
130159

131160
## License

bin/cli.js

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
#!/usr/bin/env node
22

3-
const path = require("path");
43
const chalk = require("chalk");
54
const { Command } = require("commander");
65
const packageJson = require("../package.json");
76
const { createApp } = require("../src/core");
8-
const {
9-
validateProjectName,
10-
} = require("../src/core/validators/projectValidator");
11-
const { handleExistingDir } = require("../src/core/services/fileService");
127

138
const program = new Command();
149

@@ -40,16 +35,10 @@ program
4035
.argument("[project-name]", "Name of the project")
4136
.option("--no-install", "Skip installing dependencies")
4237
.option("--port <number>", "Set server port")
38+
.option("--cors", "Enable CORS in the generated API")
4339
.option("--typescript", "Use TypeScript template")
40+
.option("-y, --yes", "Use defaults for any missing options")
4441
.action(async (projectName, options) => {
45-
try {
46-
if (projectName !== undefined) {
47-
validateProjectName(projectName);
48-
}
49-
} catch (err) {
50-
exitWithError(err.message);
51-
}
52-
5342
let parsedPort;
5443

5544
try {
@@ -58,24 +47,10 @@ program
5847
exitWithError(err.message);
5948
}
6049

61-
const projectPath = projectName
62-
? path.join(process.cwd(), projectName)
63-
: null;
64-
65-
if (projectPath && projectPath === process.cwd()) {
66-
exitWithError("Refusing to overwrite the current directory.");
67-
}
68-
69-
if (projectPath) {
70-
try {
71-
await handleExistingDir(projectPath);
72-
} catch (err) {
73-
exitWithError(err.message);
74-
}
75-
}
76-
7750
try {
7851
await createApp(projectName, {
52+
yes: options.yes,
53+
cors: options.cors,
7954
typescript: options.typescript,
8055
install: options.install === false ? false : undefined,
8156
port: parsedPort,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "setup-node-api",
3-
"version": "1.1.8",
3+
"version": "1.2.0",
44
"description": "CLI tool to scaffold Node.js + Express APIs instantly",
55
"type": "commonjs",
66
"main": "bin/cli.js",

src/core/createProject.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,26 @@ function warn(message) {
2121

2222
async function createProject(projectName, options = {}) {
2323
const projectPath = path.join(process.cwd(), projectName);
24+
const templateName = options.typescript ? "node-api-ts" : "node-api";
25+
const templateRoot = options.templateRoot ?? path.join(__dirname, "../../templates");
26+
const templatePath = path.join(templateRoot, templateName);
2427

2528
info(`\nCreating Node API: ${projectName}\n`);
2629

27-
createProjectFolder(projectPath);
28-
success("Project folder created");
29-
30-
const templateName = options.typescript ? "node-api-ts" : "node-api";
31-
const templatePath = path.join(__dirname, "../../templates", templateName);
32-
3330
if (!fs.existsSync(templatePath)) {
3431
throw new Error("Template not found");
3532
}
3633

34+
createProjectFolder(projectPath);
35+
success("Project folder created");
36+
3737
copyTemplate(templatePath, projectPath);
3838
success("Template copied");
39-
updatePackageMetadata(projectPath, projectName);
40-
success("Package metadata updated");
39+
updatePackageMetadata(projectPath, projectName, {
40+
cors: options.cors,
41+
typescript: options.typescript,
42+
});
43+
success("Project files customized");
4144

4245
if (options.port) {
4346
fs.writeFileSync(

src/core/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@ const { handleExistingDir } = require("./services/fileService");
77
const path = require("path");
88

99
async function createApp(projectName, options = {}) {
10+
if (projectName !== undefined) {
11+
validateProjectName(projectName);
12+
}
13+
1014
const finalOptions = await askProjectDetails({
1115
projectName,
16+
yes: options.yes,
17+
cors: options.cors,
1218
typescript: options.typescript,
1319
install: options.install,
1420
});
1521

1622
validateProjectName(finalOptions.projectName);
17-
await handleExistingDir(path.join(process.cwd(), finalOptions.projectName));
23+
const projectPath = path.join(process.cwd(), finalOptions.projectName);
24+
25+
await handleExistingDir(projectPath);
1826

1927
await createProject(finalOptions.projectName, {
28+
cors: finalOptions.cors,
2029
typescript: finalOptions.typescript,
2130
install: finalOptions.install,
2231
port: options.port,

src/core/promptService.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ async function getInquirer() {
1313
}
1414

1515
async function askProjectDetails(options = {}) {
16+
if (options.yes) {
17+
if (!options.projectName) {
18+
throw new Error(
19+
"Project name is required when using --yes. Pass it as an argument."
20+
);
21+
}
22+
23+
return {
24+
projectName: options.projectName,
25+
typescript: options.typescript ?? false,
26+
install: options.install ?? true,
27+
cors: options.cors ?? false,
28+
};
29+
}
30+
1631
const questions = [];
1732

1833
if (!options.projectName) {
@@ -49,11 +64,21 @@ async function askProjectDetails(options = {}) {
4964
});
5065
}
5166

67+
if (options.cors === undefined) {
68+
questions.push({
69+
type: "confirm",
70+
name: "cors",
71+
message: "Enable CORS?",
72+
default: false,
73+
});
74+
}
75+
5276
if (!questions.length) {
5377
return {
5478
projectName: options.projectName,
5579
typescript: options.typescript,
5680
install: options.install,
81+
cors: options.cors,
5782
};
5883
}
5984

@@ -68,6 +93,7 @@ async function askProjectDetails(options = {}) {
6893
projectName: options.projectName,
6994
typescript: options.typescript ?? false,
7095
install: options.install ?? true,
96+
cors: options.cors ?? false,
7197
};
7298
}
7399

@@ -87,6 +113,7 @@ async function askProjectDetails(options = {}) {
87113
projectName: options.projectName || answers.projectName,
88114
typescript: options.typescript ?? answers.typescript,
89115
install: options.install ?? answers.install,
116+
cors: options.cors ?? answers.cors,
90117
};
91118
}
92119

src/core/services/fileService.js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,36 @@ function isInteractive() {
44
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
55
}
66

7-
async function handleExistingDir(projectPath) {
8-
if (!fs.existsSync(projectPath)) return;
9-
10-
if (!isInteractive()) {
7+
async function handleExistingDir(projectPath, dependencies = {}) {
8+
const existsSync = dependencies.existsSync ?? fs.existsSync;
9+
const removeDirSync = dependencies.removeDirSync ?? fs.rmSync;
10+
const promptOverwrite =
11+
dependencies.promptOverwrite ??
12+
(async () => {
13+
const { default: inquirer } = await import("inquirer");
14+
return inquirer.prompt([
15+
{
16+
type: "confirm",
17+
name: "overwrite",
18+
message: "Folder already exists. Overwrite?",
19+
default: false,
20+
},
21+
]);
22+
});
23+
const isInteractiveFn = dependencies.isInteractive ?? isInteractive;
24+
25+
if (!existsSync(projectPath)) return;
26+
27+
if (!isInteractiveFn()) {
1128
throw new Error(
1229
"Target folder already exists. Remove it first or rerun this command in an interactive terminal."
1330
);
1431
}
1532

16-
const { default: inquirer } = await import("inquirer");
17-
1833
let answer;
1934

2035
try {
21-
answer = await inquirer.prompt([
22-
{
23-
type: "confirm",
24-
name: "overwrite",
25-
message: "Folder already exists. Overwrite?",
26-
default: false,
27-
},
28-
]);
36+
answer = await promptOverwrite();
2937
} catch (err) {
3038
if (err.name === "ExitPromptError") {
3139
throw new Error("Operation cancelled");
@@ -37,7 +45,7 @@ async function handleExistingDir(projectPath) {
3745
throw new Error("Operation cancelled");
3846
}
3947

40-
fs.rmSync(projectPath, { recursive: true, force: true });
48+
removeDirSync(projectPath, { recursive: true, force: true });
4149
console.log("Existing folder removed\n");
4250
}
4351

0 commit comments

Comments
 (0)