create-wabck 1.0.0
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/README.md +79 -0
- package/package.json +48 -0
- package/src/generator.js +201 -0
- package/src/index.js +118 -0
- package/src/installer.js +62 -0
- package/src/prompts.js +113 -0
- package/src/templates/base.js +387 -0
- package/src/templates/config.js +271 -0
- package/src/templates/controllers.js +99 -0
- package/src/templates/docker.js +205 -0
- package/src/templates/graphql.js +157 -0
- package/src/templates/helpers.js +117 -0
- package/src/templates/index.js +78 -0
- package/src/templates/middlewares.js +294 -0
- package/src/templates/models.js +318 -0
- package/src/templates/routes.js +179 -0
- package/src/templates/services.js +252 -0
- package/src/templates/typescript.js +96 -0
- package/src/templates/utils.js +109 -0
- package/src/templates/validators.js +183 -0
- package/src/utils/fs.js +41 -0
- package/src/utils/logger.js +89 -0
- package/src/utils/pkg.js +158 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# wabck
|
|
2
|
+
|
|
3
|
+
> **Scaffold production-ready Express.js backends in seconds.**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx wabck my-api
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
or
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm create wabck@latest my-api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## β¨ Features
|
|
18
|
+
|
|
19
|
+
| Feature | Details |
|
|
20
|
+
|---------|---------|
|
|
21
|
+
| π Language | JavaScript (ESM) or TypeScript |
|
|
22
|
+
| π Database | MongoDB, PostgreSQL (Prisma), MySQL, SQLite, None |
|
|
23
|
+
| π Auth | JWT, Clerk, Firebase, None |
|
|
24
|
+
| β
Validation | Zod, Joi, Express Validator |
|
|
25
|
+
| π API Style | REST or GraphQL (Apollo Server 4) |
|
|
26
|
+
| π Docs | Swagger / OpenAPI 3 |
|
|
27
|
+
| π³ Docker | Multi-stage Dockerfile + docker-compose |
|
|
28
|
+
| β‘ Package managers | npm, pnpm, yarn, bun |
|
|
29
|
+
|
|
30
|
+
## π¨ CLI Experience
|
|
31
|
+
|
|
32
|
+
- **ASCII art banner** with gradient colors (figlet + gradient-string)
|
|
33
|
+
- **Animated spinners** for every step (ora)
|
|
34
|
+
- **Color-coded prompts** (chalk)
|
|
35
|
+
- **Boxed success summary** with numbered next steps
|
|
36
|
+
|
|
37
|
+
## π Generated Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
my-api/
|
|
41
|
+
βββ src/
|
|
42
|
+
β βββ config/ db.js + env.js [+ swagger.js]
|
|
43
|
+
β βββ controllers/ user.controller.js
|
|
44
|
+
β βββ middlewares/ error, notFound, rateLimiter, auth
|
|
45
|
+
β βββ models/ user.model.js
|
|
46
|
+
β βββ routes/ index, health, user [+ auth]
|
|
47
|
+
β βββ services/ user.service.js
|
|
48
|
+
β βββ utils/ asyncHandler, ApiError, ApiResponse
|
|
49
|
+
β βββ validators/ user.validator.js
|
|
50
|
+
β βββ constants/ HTTP_STATUS, ROLES, PAGINATION
|
|
51
|
+
β βββ helpers/ token.helper.js (JWT)
|
|
52
|
+
β βββ types/ (TypeScript only)
|
|
53
|
+
β βββ graphql/ (GraphQL only)
|
|
54
|
+
β βββ app.js
|
|
55
|
+
β βββ server.js
|
|
56
|
+
βββ .env.example
|
|
57
|
+
βββ .prettierrc
|
|
58
|
+
βββ eslint.config.js
|
|
59
|
+
βββ Dockerfile (optional)
|
|
60
|
+
βββ docker-compose.yml (optional)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## π Generated Endpoints
|
|
64
|
+
|
|
65
|
+
| Method | Path | Description |
|
|
66
|
+
|--------|------|-------------|
|
|
67
|
+
| GET | `/api/v1/health` | Health + uptime |
|
|
68
|
+
| GET | `/api/v1/users` | List users |
|
|
69
|
+
| POST | `/api/v1/users` | Create user |
|
|
70
|
+
| GET | `/api/v1/users/:id` | Get user |
|
|
71
|
+
| PUT | `/api/v1/users/:id` | Update user |
|
|
72
|
+
| DELETE | `/api/v1/users/:id` | Delete user |
|
|
73
|
+
| POST | `/api/v1/auth/register` | Register (JWT) |
|
|
74
|
+
| POST | `/api/v1/auth/login` | Login (JWT) |
|
|
75
|
+
| GET | `/api/v1/auth/me` | Profile (JWT) |
|
|
76
|
+
|
|
77
|
+
## π License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-wabck",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold production-ready Express.js backends in seconds",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wabck": "src/index.js",
|
|
8
|
+
"create-wabck": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node src/index.js",
|
|
15
|
+
"dev": "node src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@inquirer/prompts": "^7.1.0",
|
|
19
|
+
"boxen": "^8.0.1",
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"execa": "^9.3.0",
|
|
22
|
+
"figlet": "^1.7.0",
|
|
23
|
+
"fs-extra": "^11.2.0",
|
|
24
|
+
"gradient-string": "^3.0.0",
|
|
25
|
+
"ora": "^8.1.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"express",
|
|
32
|
+
"backend",
|
|
33
|
+
"scaffold",
|
|
34
|
+
"boilerplate",
|
|
35
|
+
"cli",
|
|
36
|
+
"create-app",
|
|
37
|
+
"nodejs",
|
|
38
|
+
"typescript",
|
|
39
|
+
"rest",
|
|
40
|
+
"graphql"
|
|
41
|
+
],
|
|
42
|
+
"author": "w4simz",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": ""
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { writeFile, createDir, pathExists, remove } from './utils/fs.js';
|
|
3
|
+
import { log, spinner } from './utils/logger.js';
|
|
4
|
+
import { buildPackageJson } from './utils/pkg.js';
|
|
5
|
+
import * as T from './templates/index.js';
|
|
6
|
+
|
|
7
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
8
|
+
// Project Generator
|
|
9
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Orchestrates full project scaffolding:
|
|
13
|
+
* 1. Validate that the target directory doesn't exist
|
|
14
|
+
* 2. Create directory structure
|
|
15
|
+
* 3. Generate all files from templates
|
|
16
|
+
*
|
|
17
|
+
* On any failure, rolls back all created directories/files.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options - Resolved user options from prompts
|
|
20
|
+
* @returns {Promise<string>} - Absolute path to the created project
|
|
21
|
+
*/
|
|
22
|
+
export async function generate(options) {
|
|
23
|
+
const { projectName, language, database, auth, validation, apiStyle, docker } = options;
|
|
24
|
+
const isTS = language === 'typescript';
|
|
25
|
+
const ext = isTS ? 'ts' : 'js';
|
|
26
|
+
const hasDB = database !== 'none';
|
|
27
|
+
const hasAuth = auth !== 'none';
|
|
28
|
+
const hasJwt = auth === 'jwt';
|
|
29
|
+
const isGql = apiStyle === 'graphql';
|
|
30
|
+
const hasValidation = validation && validation !== 'none';
|
|
31
|
+
|
|
32
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
33
|
+
const createdDirs = [];
|
|
34
|
+
|
|
35
|
+
// ββ Pre-flight: check directory doesn't already exist βββββββββββββββββββββ
|
|
36
|
+
if (await pathExists(projectDir)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Directory "${projectName}" already exists.\n` +
|
|
39
|
+
` Please choose a different name or remove the existing directory.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ββ Step 1: Create directory skeleton βββββββββββββββββββββββββββββββββββββ
|
|
44
|
+
const dirSpin = spinner('Creating project structureβ¦').start();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const dirs = [
|
|
48
|
+
'', // root
|
|
49
|
+
'src',
|
|
50
|
+
'src/config',
|
|
51
|
+
'src/controllers',
|
|
52
|
+
'src/routes',
|
|
53
|
+
'src/middlewares',
|
|
54
|
+
'src/services',
|
|
55
|
+
'src/utils',
|
|
56
|
+
'src/constants',
|
|
57
|
+
'src/helpers',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (hasDB) dirs.push('src/models');
|
|
61
|
+
if (isTS) dirs.push('src/types');
|
|
62
|
+
if (isGql) dirs.push('src/graphql');
|
|
63
|
+
if (hasValidation) dirs.push('src/validators');
|
|
64
|
+
if (database === 'postgresql') dirs.push('prisma');
|
|
65
|
+
|
|
66
|
+
for (const d of dirs) {
|
|
67
|
+
const full = d ? path.join(projectDir, d) : projectDir;
|
|
68
|
+
await createDir(full);
|
|
69
|
+
createdDirs.push(full);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dirSpin.succeed('Project structure created');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
dirSpin.fail('Failed to create directory structure');
|
|
75
|
+
await rollback(createdDirs);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ββ Step 2: Write all files ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
80
|
+
const fileSpin = spinner('Writing filesβ¦').start();
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const write = async (relPath, content) =>
|
|
84
|
+
writeFile(path.join(projectDir, relPath), content);
|
|
85
|
+
|
|
86
|
+
// ββ package.json ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
87
|
+
await write('package.json', JSON.stringify(buildPackageJson(options), null, 2));
|
|
88
|
+
|
|
89
|
+
// ββ Root config files βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
90
|
+
await write('.env', T.envFile(options));
|
|
91
|
+
await write('.env.example', T.envExample(options));
|
|
92
|
+
await write('.gitignore', T.gitignore());
|
|
93
|
+
await write('.prettierrc', T.prettierrc());
|
|
94
|
+
await write('eslint.config.js', T.eslintConfig(options));
|
|
95
|
+
await write('README.md', T.readme(options));
|
|
96
|
+
|
|
97
|
+
// ββ TypeScript extras βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
98
|
+
if (isTS) {
|
|
99
|
+
await write('tsconfig.json', T.tsconfig());
|
|
100
|
+
await write(`src/types/index.ts`, T.tsTypes(options));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ββ Core application files ββββββββββββββββββββββββββββββββββββββββββββ
|
|
104
|
+
await write(`src/app.${ext}`, T.app(options));
|
|
105
|
+
await write(`src/server.${ext}`, T.server(options));
|
|
106
|
+
|
|
107
|
+
// ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
108
|
+
await write(`src/config/db.${ext}`, T.db(options));
|
|
109
|
+
await write(`src/config/env.${ext}`, T.envConfig(options));
|
|
110
|
+
|
|
111
|
+
// ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
112
|
+
await write(`src/routes/index.${ext}`, T.routesIndex(options));
|
|
113
|
+
await write(`src/routes/health.route.${ext}`, T.healthRoute(options));
|
|
114
|
+
await write(`src/routes/user.route.${ext}`, T.userRoute(options));
|
|
115
|
+
if (hasJwt) {
|
|
116
|
+
await write(`src/routes/auth.route.${ext}`, T.authRoute(options));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ββ Controllers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
120
|
+
await write(`src/controllers/user.controller.${ext}`, T.userController(options));
|
|
121
|
+
|
|
122
|
+
// ββ Models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
123
|
+
if (hasDB) {
|
|
124
|
+
await write(`src/models/user.model.${ext}`, T.userModel(options));
|
|
125
|
+
}
|
|
126
|
+
if (database === 'postgresql') {
|
|
127
|
+
await write('prisma/schema.prisma', T.prismaSchema(options));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ββ Middlewares βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
131
|
+
await write(`src/middlewares/error.middleware.${ext}`, T.errorMiddleware(options));
|
|
132
|
+
await write(`src/middlewares/notFound.middleware.${ext}`, T.notFoundMiddleware(options));
|
|
133
|
+
await write(`src/middlewares/rateLimiter.middleware.${ext}`, T.rateLimiterMiddleware(options));
|
|
134
|
+
if (hasAuth) {
|
|
135
|
+
await write(`src/middlewares/auth.middleware.${ext}`, T.authMiddleware(options));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ββ Services ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
139
|
+
await write(`src/services/user.service.${ext}`, T.userService(options));
|
|
140
|
+
|
|
141
|
+
// ββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
142
|
+
await write(`src/utils/asyncHandler.${ext}`, T.asyncHandler(options));
|
|
143
|
+
await write(`src/utils/apiError.${ext}`, T.apiError(options));
|
|
144
|
+
await write(`src/utils/apiResponse.${ext}`, T.apiResponse(options));
|
|
145
|
+
|
|
146
|
+
// ββ Validators ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
147
|
+
if (hasValidation) {
|
|
148
|
+
await write(`src/validators/user.validator.${ext}`, T.userValidator(options));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
152
|
+
await write(`src/constants/index.${ext}`, T.constants(options));
|
|
153
|
+
|
|
154
|
+
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
155
|
+
if (hasJwt) {
|
|
156
|
+
await write(`src/helpers/token.helper.${ext}`, T.tokenHelper(options));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ββ GraphQL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
160
|
+
if (isGql) {
|
|
161
|
+
await write(`src/graphql/schema.${ext}`, T.graphqlSchema(options));
|
|
162
|
+
await write(`src/graphql/resolvers.${ext}`, T.graphqlResolvers(options));
|
|
163
|
+
await write(`src/graphql/server.${ext}`, T.graphqlServer(options));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ββ Docker ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
167
|
+
if (docker) {
|
|
168
|
+
await write('Dockerfile', T.dockerfile(options));
|
|
169
|
+
await write('docker-compose.yml', T.dockerCompose(options));
|
|
170
|
+
await write('.dockerignore', T.dockerignore());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fileSpin.succeed('Files written successfully');
|
|
174
|
+
return projectDir;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
fileSpin.fail('Failed to write project files');
|
|
177
|
+
await rollback(createdDirs);
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
183
|
+
// Rollback Helper
|
|
184
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Removes all created directories in reverse order (deepest first).
|
|
188
|
+
* Called when generation fails partway through.
|
|
189
|
+
* @param {string[]} dirs - Array of absolute directory paths to remove
|
|
190
|
+
*/
|
|
191
|
+
async function rollback(dirs) {
|
|
192
|
+
log.warn('Rolling back created filesβ¦');
|
|
193
|
+
for (const d of [...dirs].reverse()) {
|
|
194
|
+
try {
|
|
195
|
+
await remove(d);
|
|
196
|
+
} catch {
|
|
197
|
+
// Silently continue β best-effort cleanup
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
log.dim(' Cleanup complete.');
|
|
201
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { renderBanner, log, box } from './utils/logger.js';
|
|
7
|
+
import { runPrompts } from './prompts.js';
|
|
8
|
+
import { generate } from './generator.js';
|
|
9
|
+
import { installDependencies, runPrismaGenerate } from './installer.js';
|
|
10
|
+
|
|
11
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
12
|
+
// Entry Point
|
|
13
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
// ββ 1. Banner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
17
|
+
await renderBanner();
|
|
18
|
+
|
|
19
|
+
// ββ 2. Parse argv (optional pre-fill for project name) ββββββββββββββββββββ
|
|
20
|
+
const argName = process.argv[2];
|
|
21
|
+
|
|
22
|
+
// ββ 3. Interactive prompts βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
23
|
+
let options;
|
|
24
|
+
try {
|
|
25
|
+
options = await runPrompts(argName);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// User pressed Ctrl+C or similar
|
|
28
|
+
if (err?.name === 'ExitPromptError' || err?.message?.includes('User force closed')) {
|
|
29
|
+
log.blank();
|
|
30
|
+
log.warn('Scaffold cancelled.');
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
log.blank();
|
|
37
|
+
|
|
38
|
+
// ββ 4. Generate project files ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
39
|
+
let projectDir;
|
|
40
|
+
try {
|
|
41
|
+
projectDir = await generate(options);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
log.blank();
|
|
44
|
+
log.error(`Generation failed: ${err.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ββ 5. Install dependencies ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
49
|
+
if (options.installDeps) {
|
|
50
|
+
try {
|
|
51
|
+
await installDependencies(projectDir, options.packageManager);
|
|
52
|
+
|
|
53
|
+
// Run prisma generate after install for PostgreSQL projects
|
|
54
|
+
if (options.database === 'postgresql') {
|
|
55
|
+
await runPrismaGenerate(projectDir, options.packageManager);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Installation failure is non-fatal β project files are still valid
|
|
59
|
+
log.blank();
|
|
60
|
+
log.warn('Dependencies were not installed automatically. Install them manually:');
|
|
61
|
+
log.dim(` cd ${options.projectName}`);
|
|
62
|
+
log.dim(` ${options.packageManager} ${options.packageManager === 'yarn' ? '' : 'install'}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ββ 6. Success summary βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
67
|
+
log.blank();
|
|
68
|
+
printSuccessSummary(options, projectDir);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
72
|
+
// Success Summary
|
|
73
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
74
|
+
|
|
75
|
+
function printSuccessSummary(options, projectDir) {
|
|
76
|
+
const { projectName, language, database, auth, validation, docker, packageManager, installDeps } = options;
|
|
77
|
+
const pm = packageManager;
|
|
78
|
+
const relDir = path.relative(process.cwd(), projectDir) || projectName;
|
|
79
|
+
const devCmd = language === 'typescript' ? `${pm} run dev` : `${pm} run dev`;
|
|
80
|
+
|
|
81
|
+
// Progress recap
|
|
82
|
+
log.success('Project scaffolded successfully!');
|
|
83
|
+
log.blank();
|
|
84
|
+
log.label(' Language', language === 'typescript' ? 'TypeScript' : 'JavaScript');
|
|
85
|
+
log.label(' Database', database === 'none' ? 'None' : database);
|
|
86
|
+
log.label(' Auth', auth === 'none' ? 'None' : auth);
|
|
87
|
+
if (validation && validation !== 'none') log.label(' Validation', validation);
|
|
88
|
+
if (docker) log.label(' Docker', 'Dockerfile + docker-compose.yml');
|
|
89
|
+
log.blank();
|
|
90
|
+
|
|
91
|
+
// Next steps
|
|
92
|
+
const steps = [];
|
|
93
|
+
steps.push(`cd ${relDir}`);
|
|
94
|
+
if (!installDeps) steps.push(`${pm}${pm === 'yarn' ? '' : ' install'}`);
|
|
95
|
+
steps.push('cp .env.example .env');
|
|
96
|
+
if (database === 'postgresql') steps.push('npx prisma migrate dev --name init');
|
|
97
|
+
steps.push(devCmd);
|
|
98
|
+
|
|
99
|
+
const stepsStr = steps.map((s, i) => ` ${chalk.dim(`${i + 1}.`)} ${chalk.cyan(s)}`).join('\n');
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
box(
|
|
103
|
+
chalk.bold.white('π Next steps:\n\n') + stepsStr + '\n\n' +
|
|
104
|
+
chalk.dim(' API: ') + chalk.white('http://localhost:5000/api/v1/health'),
|
|
105
|
+
{ borderColor: 'green', title: chalk.green('Ready!'), titleAlignment: 'center' }
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
111
|
+
// Run
|
|
112
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
113
|
+
|
|
114
|
+
main().catch((err) => {
|
|
115
|
+
console.error(chalk.red('\n Fatal error:'), err.message || err);
|
|
116
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { spinner, log } from './utils/logger.js';
|
|
3
|
+
import { getInstallCmd } from './utils/pkg.js';
|
|
4
|
+
|
|
5
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
6
|
+
// Dependency Installer
|
|
7
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Installs project dependencies in the generated project directory.
|
|
11
|
+
* Uses the package manager chosen by the user.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} projectDir - Absolute path to the generated project
|
|
14
|
+
* @param {string} packageManager - 'npm' | 'pnpm' | 'yarn' | 'bun'
|
|
15
|
+
*/
|
|
16
|
+
export async function installDependencies(projectDir, packageManager) {
|
|
17
|
+
const { cmd, args } = getInstallCmd(packageManager);
|
|
18
|
+
const spin = spinner(`Installing dependencies with ${packageManager}β¦`).start();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await execa(cmd, args, {
|
|
22
|
+
cwd: projectDir,
|
|
23
|
+
// Pipe output so the spinner stays clean; errors are captured
|
|
24
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
spin.succeed(`Dependencies installed with ${packageManager}`);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
spin.fail(`Dependency installation failed`);
|
|
30
|
+
log.warn('You can install them manually:');
|
|
31
|
+
log.dim(` cd ${projectDir}`);
|
|
32
|
+
log.dim(` ${cmd} ${args.join(' ')}`);
|
|
33
|
+
|
|
34
|
+
// Re-throw so the caller can decide whether to abort or continue
|
|
35
|
+
throw new Error(`Installation failed: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
40
|
+
// Prisma Post-Install Steps
|
|
41
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs `prisma generate` after installation for PostgreSQL projects.
|
|
45
|
+
* @param {string} projectDir
|
|
46
|
+
* @param {string} packageManager
|
|
47
|
+
*/
|
|
48
|
+
export async function runPrismaGenerate(projectDir, packageManager) {
|
|
49
|
+
const spin = spinner('Running prisma generateβ¦').start();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const runner = packageManager === 'npm' ? 'npx' : packageManager;
|
|
53
|
+
await execa(runner, ['prisma', 'generate'], {
|
|
54
|
+
cwd: projectDir,
|
|
55
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
56
|
+
});
|
|
57
|
+
spin.succeed('Prisma client generated');
|
|
58
|
+
} catch {
|
|
59
|
+
spin.warn('Could not auto-run prisma generate β run it manually:');
|
|
60
|
+
log.dim(' npx prisma generate');
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
5
|
+
// Interactive CLI Prompts
|
|
6
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs the full interactive questionnaire and returns a resolved options object.
|
|
10
|
+
* @param {string} [initialName] - Pre-filled project name from CLI argv
|
|
11
|
+
* @returns {Promise<object>} options
|
|
12
|
+
*/
|
|
13
|
+
export async function runPrompts(initialName) {
|
|
14
|
+
console.log(chalk.dim(' Answer a few questions to scaffold your project.\n'));
|
|
15
|
+
|
|
16
|
+
// 1. Project Name
|
|
17
|
+
const projectName = await input({
|
|
18
|
+
message: 'Project name:',
|
|
19
|
+
default: initialName || 'my-api',
|
|
20
|
+
validate: (v) => {
|
|
21
|
+
if (!v.trim()) return 'Project name cannot be empty.';
|
|
22
|
+
if (!/^[a-z0-9-_]+$/i.test(v)) return 'Use only letters, numbers, hyphens or underscores.';
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 2. Language
|
|
28
|
+
const language = await select({
|
|
29
|
+
message: 'Language:',
|
|
30
|
+
choices: [
|
|
31
|
+
{ name: `${chalk.yellow('JavaScript')} (ES Modules)`, value: 'javascript' },
|
|
32
|
+
{ name: `${chalk.blue('TypeScript')} (strict mode)`, value: 'typescript' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 3. Package Manager
|
|
37
|
+
const packageManager = await select({
|
|
38
|
+
message: 'Package manager:',
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: `${chalk.red('npm')}`, value: 'npm' },
|
|
41
|
+
{ name: `${chalk.yellow('pnpm')} (faster, disk-efficient)`, value: 'pnpm' },
|
|
42
|
+
{ name: `${chalk.blue('yarn')}`, value: 'yarn' },
|
|
43
|
+
{ name: `${chalk.magenta('bun')} (fastest)`, value: 'bun' },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 4. Database
|
|
48
|
+
const database = await select({
|
|
49
|
+
message: 'Database:',
|
|
50
|
+
choices: [
|
|
51
|
+
{ name: `${chalk.green('MongoDB')} β Mongoose ODM`, value: 'mongodb' },
|
|
52
|
+
{ name: `${chalk.blue('PostgreSQL')} β Prisma ORM`, value: 'postgresql' },
|
|
53
|
+
{ name: `${chalk.cyan('MySQL')} β mysql2 driver`, value: 'mysql' },
|
|
54
|
+
{ name: `${chalk.magenta('SQLite')} β better-sqlite3`, value: 'sqlite' },
|
|
55
|
+
{ name: `${chalk.dim('None')} β no database`, value: 'none' },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 5. Authentication
|
|
60
|
+
const auth = await select({
|
|
61
|
+
message: 'Authentication:',
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: `${chalk.yellow('JWT')} β jsonwebtoken + bcryptjs`, value: 'jwt' },
|
|
64
|
+
{ name: `${chalk.blue('Clerk')} β @clerk/express`, value: 'clerk' },
|
|
65
|
+
{ name: `${chalk.orange?.('Firebase') || chalk.red('Firebase')} β firebase-admin`, value: 'firebase' },
|
|
66
|
+
{ name: `${chalk.dim('None')} β no auth`, value: 'none' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 6. Validation
|
|
71
|
+
const validation = await select({
|
|
72
|
+
message: 'Validation library:',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: `${chalk.blue('Zod')} β TypeScript-first schema validation`, value: 'zod' },
|
|
75
|
+
{ name: `${chalk.cyan('Joi')} β the classic schema validator`, value: 'joi' },
|
|
76
|
+
{ name: `${chalk.green('Express Validator')} β middleware-based validation`, value: 'express-validator' },
|
|
77
|
+
{ name: `${chalk.dim('None')} β no validation library`, value: 'none' },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 7. API Style
|
|
82
|
+
const apiStyle = await select({
|
|
83
|
+
message: 'API style:',
|
|
84
|
+
choices: [
|
|
85
|
+
{ name: `${chalk.green('REST')} β standard RESTful API`, value: 'rest' },
|
|
86
|
+
{ name: `${chalk.magenta('GraphQL')} β Apollo Server 4`, value: 'graphql' },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 8. Docker
|
|
91
|
+
const docker = await confirm({
|
|
92
|
+
message: 'Include Docker + docker-compose?',
|
|
93
|
+
default: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 9. Install Dependencies
|
|
97
|
+
const installDeps = await confirm({
|
|
98
|
+
message: `Install dependencies now with ${chalk.cyan(packageManager)}?`,
|
|
99
|
+
default: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
projectName: projectName.trim(),
|
|
104
|
+
language,
|
|
105
|
+
packageManager,
|
|
106
|
+
database,
|
|
107
|
+
auth,
|
|
108
|
+
validation,
|
|
109
|
+
apiStyle,
|
|
110
|
+
docker,
|
|
111
|
+
installDeps,
|
|
112
|
+
};
|
|
113
|
+
}
|