create-xpressforge 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 +47 -0
- package/bin/cli.js +5 -0
- package/package.json +47 -0
- package/src/generators/appGenerator.js +65 -0
- package/src/generators/authGenerator.js +210 -0
- package/src/generators/dbGenerator.js +75 -0
- package/src/generators/envGenerator.js +59 -0
- package/src/generators/packageJsonGenerator.js +70 -0
- package/src/generators/projectGenerator.js +275 -0
- package/src/generators/readmeGenerator.js +77 -0
- package/src/generators/structureGenerator.js +265 -0
- package/src/index.js +38 -0
- package/src/prompts/questions.js +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# create-xpressforge
|
|
2
|
+
|
|
3
|
+
> Production-ready Node.js + Express project scaffolder
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.com/package/create-xpressforge)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx create-xpressforge my-app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or install globally:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g create-xpressforge
|
|
18
|
+
create-xpressforge my-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What you get
|
|
22
|
+
|
|
23
|
+
Interactive prompts let you choose:
|
|
24
|
+
|
|
25
|
+
- **Structure** — MVC, Modular (feature-based), or Layered (controller/service/repository)
|
|
26
|
+
- **Database** — MongoDB (Mongoose), PostgreSQL or MySQL (Prisma), or none
|
|
27
|
+
- **Auth** — JWT with refresh tokens, Session, or none
|
|
28
|
+
- **Extras** — Rate limiting, Helmet, CORS, Morgan, Validation, Multer, Socket.io, Swagger
|
|
29
|
+
- **Language** — JavaScript (ES Modules) or TypeScript
|
|
30
|
+
|
|
31
|
+
Every generated project includes:
|
|
32
|
+
|
|
33
|
+
- Global error handler with Mongoose/Prisma/JWT error detection
|
|
34
|
+
- Consistent `apiResponse` helper (`sendSuccess`, `sendError`, `sendPaginated`)
|
|
35
|
+
- Custom logger utility
|
|
36
|
+
- 404 not-found middleware
|
|
37
|
+
- Working User CRUD example
|
|
38
|
+
- `.env` + `.env.example` with all variables listed
|
|
39
|
+
- Auto-generated README with your stack details
|
|
40
|
+
|
|
41
|
+
## Author
|
|
42
|
+
|
|
43
|
+
Hammad Sadi <hammad.sadi@yahoo.com>
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-xpressforge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready Node.js + Express project scaffolder — MVC, Modular, Layered structures with DB, Auth & more",
|
|
5
|
+
"author": "Hammad Sadi <hammad.sadi@yahoo.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-xpressforge": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"express",
|
|
13
|
+
"nodejs",
|
|
14
|
+
"scaffold",
|
|
15
|
+
"cli",
|
|
16
|
+
"mvc",
|
|
17
|
+
"modular",
|
|
18
|
+
"boilerplate",
|
|
19
|
+
"generator",
|
|
20
|
+
"production",
|
|
21
|
+
"mongodb",
|
|
22
|
+
"postgresql",
|
|
23
|
+
"jwt"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"bin",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=16.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@inquirer/prompts": "^5.0.0",
|
|
34
|
+
"chalk": "^5.3.0",
|
|
35
|
+
"ora": "^8.0.1",
|
|
36
|
+
"fs-extra": "^11.2.0",
|
|
37
|
+
"gradient-string": "^2.0.2"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/hammadsadi/create-xpressforge"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/hammadsadi/create-xpressforge#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/hammadsadi/create-xpressforge/issues"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function generateAppJs(answers) {
|
|
2
|
+
const { extras, database, auth, structure } = answers;
|
|
3
|
+
|
|
4
|
+
const imports = [
|
|
5
|
+
`import express from 'express';`,
|
|
6
|
+
extras.includes('cors') ? `import cors from 'cors';` : '',
|
|
7
|
+
extras.includes('helmet') ? `import helmet from 'helmet';` : '',
|
|
8
|
+
extras.includes('morgan') ? `import morgan from 'morgan';` : '',
|
|
9
|
+
extras.includes('rateLimit') ? `import rateLimit from 'express-rate-limit';` : '',
|
|
10
|
+
extras.includes('swagger') ? `import swaggerUi from 'swagger-ui-express';\nimport { swaggerSpec } from './config/swagger.js';` : '',
|
|
11
|
+
database !== 'none' ? `import { connectDB } from './config/db.js';` : '',
|
|
12
|
+
`import { errorHandler } from './middlewares/errorHandler.js';`,
|
|
13
|
+
`import { notFound } from './middlewares/notFound.js';`,
|
|
14
|
+
structure === 'mvc' ? `import routes from './routes/index.js';` : '',
|
|
15
|
+
structure === 'modular' ? `import routes from './routes/index.js';` : '',
|
|
16
|
+
structure === 'layered' ? `import routes from './routes/index.js';` : '',
|
|
17
|
+
].filter(Boolean).join('\n');
|
|
18
|
+
|
|
19
|
+
const dbInit = database !== 'none' ? `\n// Connect to database\nconnectDB();\n` : '';
|
|
20
|
+
|
|
21
|
+
const rateLimitMiddleware = extras.includes('rateLimit') ? `
|
|
22
|
+
const limiter = rateLimit({
|
|
23
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
24
|
+
max: 100,
|
|
25
|
+
message: { success: false, message: 'Too many requests, please try again later' },
|
|
26
|
+
standardHeaders: true,
|
|
27
|
+
legacyHeaders: false,
|
|
28
|
+
});
|
|
29
|
+
app.use('/api', limiter);
|
|
30
|
+
` : '';
|
|
31
|
+
|
|
32
|
+
const middlewares = [
|
|
33
|
+
extras.includes('helmet') ? `app.use(helmet());` : '',
|
|
34
|
+
extras.includes('cors') ? `app.use(cors({ origin: process.env.CLIENT_URL || '*', credentials: true }));` : '',
|
|
35
|
+
`app.use(express.json({ limit: '10mb' }));`,
|
|
36
|
+
`app.use(express.urlencoded({ extended: true, limit: '10mb' }));`,
|
|
37
|
+
extras.includes('morgan') ? `app.use(morgan(process.env.NODE_ENV === 'development' ? 'dev' : 'combined'));` : '',
|
|
38
|
+
].filter(Boolean).join('\n');
|
|
39
|
+
|
|
40
|
+
const swaggerSetup = extras.includes('swagger') ? `\n// API Documentation\napp.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));\n` : '';
|
|
41
|
+
|
|
42
|
+
const healthCheck = `
|
|
43
|
+
// Health check
|
|
44
|
+
app.get('/health', (req, res) => {
|
|
45
|
+
res.json({ success: true, message: 'Server is healthy', timestamp: new Date().toISOString() });
|
|
46
|
+
});
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
return `${imports}
|
|
50
|
+
${dbInit}
|
|
51
|
+
const app = express();
|
|
52
|
+
${rateLimitMiddleware}
|
|
53
|
+
// Middlewares
|
|
54
|
+
${middlewares}
|
|
55
|
+
${swaggerSetup}${healthCheck}
|
|
56
|
+
// Routes
|
|
57
|
+
app.use('/api/v1', routes);
|
|
58
|
+
|
|
59
|
+
// Error handlers
|
|
60
|
+
app.use(notFound);
|
|
61
|
+
app.use(errorHandler);
|
|
62
|
+
|
|
63
|
+
export default app;
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function generateAuthFiles(answers, targetDir, ext) {
|
|
5
|
+
const { auth, database } = answers;
|
|
6
|
+
|
|
7
|
+
if (auth === 'jwt') {
|
|
8
|
+
// JWT middleware
|
|
9
|
+
await fs.writeFile(
|
|
10
|
+
path.join(targetDir, 'src', 'middlewares', `authenticate.${ext}`),
|
|
11
|
+
jwtMiddleware()
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Auth controller
|
|
15
|
+
await fs.ensureDir(path.join(targetDir, 'src', 'controllers'));
|
|
16
|
+
await fs.writeFile(
|
|
17
|
+
path.join(targetDir, 'src', 'controllers', `authController.${ext}`),
|
|
18
|
+
authController(database)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Auth routes
|
|
22
|
+
await fs.writeFile(
|
|
23
|
+
path.join(targetDir, 'src', 'routes', `authRoutes.${ext}`),
|
|
24
|
+
authRoutes()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// User model (if DB)
|
|
28
|
+
if (database === 'mongodb') {
|
|
29
|
+
await fs.writeFile(
|
|
30
|
+
path.join(targetDir, 'src', 'models', `User.${ext}`),
|
|
31
|
+
userModelMongo()
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (auth === 'session') {
|
|
37
|
+
await fs.writeFile(
|
|
38
|
+
path.join(targetDir, 'src', 'middlewares', `sessionAuth.${ext}`),
|
|
39
|
+
sessionMiddleware()
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function jwtMiddleware() {
|
|
45
|
+
return `import jwt from 'jsonwebtoken';
|
|
46
|
+
import { env } from '../config/env.js';
|
|
47
|
+
|
|
48
|
+
export const authenticate = (req, res, next) => {
|
|
49
|
+
const authHeader = req.headers.authorization;
|
|
50
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
51
|
+
return res.status(401).json({ success: false, message: 'No token provided' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const token = authHeader.split(' ')[1];
|
|
55
|
+
try {
|
|
56
|
+
const decoded = jwt.verify(token, env.JWT_SECRET);
|
|
57
|
+
req.user = decoded;
|
|
58
|
+
next();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return res.status(401).json({ success: false, message: err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token' });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const authorize = (...roles) => (req, res, next) => {
|
|
65
|
+
if (!roles.includes(req.user?.role)) {
|
|
66
|
+
return res.status(403).json({ success: false, message: 'Access denied' });
|
|
67
|
+
}
|
|
68
|
+
next();
|
|
69
|
+
};
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function authController(database) {
|
|
74
|
+
const userImport = database === 'mongodb'
|
|
75
|
+
? `import User from '../models/User.js';`
|
|
76
|
+
: `import { prisma } from '../config/db.js';`;
|
|
77
|
+
|
|
78
|
+
return `import jwt from 'jsonwebtoken';
|
|
79
|
+
import bcrypt from 'bcryptjs';
|
|
80
|
+
${userImport}
|
|
81
|
+
import { env } from '../config/env.js';
|
|
82
|
+
import { sendSuccess, sendError } from '../utils/apiResponse.js';
|
|
83
|
+
|
|
84
|
+
const signToken = (payload, secret, expiresIn) =>
|
|
85
|
+
jwt.sign(payload, secret, { expiresIn });
|
|
86
|
+
|
|
87
|
+
export const register = async (req, res, next) => {
|
|
88
|
+
try {
|
|
89
|
+
const { name, email, password } = req.body;
|
|
90
|
+
|
|
91
|
+
const existing = await ${database === 'mongodb' ? 'User.findOne({ email })' : 'prisma.user.findUnique({ where: { email } })'};
|
|
92
|
+
if (existing) return sendError(res, 'Email already registered', 409);
|
|
93
|
+
|
|
94
|
+
const hashed = await bcrypt.hash(password, 12);
|
|
95
|
+
const user = await ${database === 'mongodb'
|
|
96
|
+
? 'User.create({ name, email, password: hashed })'
|
|
97
|
+
: 'prisma.user.create({ data: { name, email, password: hashed } })'};
|
|
98
|
+
|
|
99
|
+
const accessToken = signToken({ id: user._id || user.id, role: user.role }, env.JWT_SECRET, env.JWT_EXPIRES_IN);
|
|
100
|
+
const refreshToken = signToken({ id: user._id || user.id }, env.JWT_REFRESH_SECRET, env.JWT_REFRESH_EXPIRES_IN);
|
|
101
|
+
|
|
102
|
+
sendSuccess(res, { accessToken, refreshToken, user: { id: user._id || user.id, name: user.name, email: user.email } }, 'Registered successfully', 201);
|
|
103
|
+
} catch (err) { next(err); }
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const login = async (req, res, next) => {
|
|
107
|
+
try {
|
|
108
|
+
const { email, password } = req.body;
|
|
109
|
+
|
|
110
|
+
const user = await ${database === 'mongodb'
|
|
111
|
+
? 'User.findOne({ email }).select(\'+password\')'
|
|
112
|
+
: 'prisma.user.findUnique({ where: { email } })'};
|
|
113
|
+
if (!user || !(await bcrypt.compare(password, user.password))) {
|
|
114
|
+
return sendError(res, 'Invalid email or password', 401);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const accessToken = signToken({ id: user._id || user.id, role: user.role }, env.JWT_SECRET, env.JWT_EXPIRES_IN);
|
|
118
|
+
const refreshToken = signToken({ id: user._id || user.id }, env.JWT_REFRESH_SECRET, env.JWT_REFRESH_EXPIRES_IN);
|
|
119
|
+
|
|
120
|
+
sendSuccess(res, { accessToken, refreshToken, user: { id: user._id || user.id, name: user.name, email: user.email } }, 'Login successful');
|
|
121
|
+
} catch (err) { next(err); }
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const refreshToken = async (req, res, next) => {
|
|
125
|
+
try {
|
|
126
|
+
const { token } = req.body;
|
|
127
|
+
if (!token) return sendError(res, 'Refresh token required', 401);
|
|
128
|
+
|
|
129
|
+
const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET);
|
|
130
|
+
const accessToken = signToken({ id: decoded.id }, env.JWT_SECRET, env.JWT_EXPIRES_IN);
|
|
131
|
+
sendSuccess(res, { accessToken }, 'Token refreshed');
|
|
132
|
+
} catch (err) { next(err); }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const getMe = async (req, res, next) => {
|
|
136
|
+
try {
|
|
137
|
+
const user = await ${database === 'mongodb'
|
|
138
|
+
? 'User.findById(req.user.id).select(\'-password\')'
|
|
139
|
+
: 'prisma.user.findUnique({ where: { id: req.user.id }, select: { id: true, name: true, email: true, role: true, createdAt: true } })'};
|
|
140
|
+
if (!user) return sendError(res, 'User not found', 404);
|
|
141
|
+
sendSuccess(res, user);
|
|
142
|
+
} catch (err) { next(err); }
|
|
143
|
+
};
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function authRoutes() {
|
|
148
|
+
return `import { Router } from 'express';
|
|
149
|
+
import { register, login, refreshToken, getMe } from '../controllers/authController.js';
|
|
150
|
+
import { authenticate } from '../middlewares/authenticate.js';
|
|
151
|
+
|
|
152
|
+
const router = Router();
|
|
153
|
+
|
|
154
|
+
router.post('/register', register);
|
|
155
|
+
router.post('/login', login);
|
|
156
|
+
router.post('/refresh', refreshToken);
|
|
157
|
+
router.get('/me', authenticate, getMe);
|
|
158
|
+
|
|
159
|
+
export default router;
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function userModelMongo() {
|
|
164
|
+
return `import mongoose from 'mongoose';
|
|
165
|
+
|
|
166
|
+
const userSchema = new mongoose.Schema({
|
|
167
|
+
name: {
|
|
168
|
+
type: String,
|
|
169
|
+
required: [true, 'Name is required'],
|
|
170
|
+
trim: true,
|
|
171
|
+
minlength: 2,
|
|
172
|
+
maxlength: 50,
|
|
173
|
+
},
|
|
174
|
+
email: {
|
|
175
|
+
type: String,
|
|
176
|
+
required: [true, 'Email is required'],
|
|
177
|
+
unique: true,
|
|
178
|
+
lowercase: true,
|
|
179
|
+
trim: true,
|
|
180
|
+
match: [/^\\S+@\\S+\\.\\S+$/, 'Invalid email format'],
|
|
181
|
+
},
|
|
182
|
+
password: {
|
|
183
|
+
type: String,
|
|
184
|
+
required: [true, 'Password is required'],
|
|
185
|
+
minlength: 6,
|
|
186
|
+
select: false,
|
|
187
|
+
},
|
|
188
|
+
role: {
|
|
189
|
+
type: String,
|
|
190
|
+
enum: ['user', 'admin'],
|
|
191
|
+
default: 'user',
|
|
192
|
+
},
|
|
193
|
+
}, { timestamps: true });
|
|
194
|
+
|
|
195
|
+
userSchema.index({ email: 1 });
|
|
196
|
+
|
|
197
|
+
const User = mongoose.model('User', userSchema);
|
|
198
|
+
export default User;
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sessionMiddleware() {
|
|
203
|
+
return `export const requireAuth = (req, res, next) => {
|
|
204
|
+
if (!req.session?.userId) {
|
|
205
|
+
return res.status(401).json({ success: false, message: 'Not authenticated' });
|
|
206
|
+
}
|
|
207
|
+
next();
|
|
208
|
+
};
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function generateDbConfig(answers) {
|
|
2
|
+
const { database } = answers;
|
|
3
|
+
|
|
4
|
+
if (database === 'mongodb') {
|
|
5
|
+
return `import mongoose from 'mongoose';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
const MAX_RETRIES = 5;
|
|
9
|
+
let retries = 0;
|
|
10
|
+
|
|
11
|
+
export const connectDB = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const conn = await mongoose.connect(process.env.MONGO_URI, {
|
|
14
|
+
serverSelectionTimeoutMS: 5000,
|
|
15
|
+
});
|
|
16
|
+
logger.info(\`MongoDB connected: \${conn.connection.host}\`);
|
|
17
|
+
retries = 0;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
retries++;
|
|
20
|
+
logger.error(\`MongoDB connection failed (attempt \${retries}/\${MAX_RETRIES}): \${err.message}\`);
|
|
21
|
+
if (retries < MAX_RETRIES) {
|
|
22
|
+
logger.info(\`Retrying in 5 seconds...\`);
|
|
23
|
+
setTimeout(connectDB, 5000);
|
|
24
|
+
} else {
|
|
25
|
+
logger.error('Max retries reached. Exiting...');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
mongoose.connection.on('disconnected', () => {
|
|
32
|
+
logger.warn('MongoDB disconnected. Attempting reconnect...');
|
|
33
|
+
connectDB();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
process.on('SIGINT', async () => {
|
|
37
|
+
await mongoose.connection.close();
|
|
38
|
+
logger.info('MongoDB connection closed due to app termination');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (database === 'postgresql' || database === 'mysql') {
|
|
45
|
+
return `import { PrismaClient } from '@prisma/client';
|
|
46
|
+
import { logger } from '../utils/logger.js';
|
|
47
|
+
|
|
48
|
+
const globalForPrisma = globalThis;
|
|
49
|
+
|
|
50
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
51
|
+
log: process.env.NODE_ENV === 'development'
|
|
52
|
+
? ['query', 'info', 'warn', 'error']
|
|
53
|
+
: ['error'],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
|
57
|
+
|
|
58
|
+
export const connectDB = async () => {
|
|
59
|
+
try {
|
|
60
|
+
await prisma.$connect();
|
|
61
|
+
logger.info('Database connected via Prisma');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger.error('Database connection failed: ' + err.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
process.on('beforeExit', async () => {
|
|
69
|
+
await prisma.$disconnect();
|
|
70
|
+
});
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function generateEnv(answers) {
|
|
2
|
+
const { database, auth, extras, projectName } = answers;
|
|
3
|
+
|
|
4
|
+
let content = `# App
|
|
5
|
+
NODE_ENV=development
|
|
6
|
+
PORT=3000
|
|
7
|
+
CLIENT_URL=http://localhost:3000
|
|
8
|
+
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
if (database === 'mongodb') {
|
|
12
|
+
content += `# MongoDB
|
|
13
|
+
MONGO_URI=mongodb://localhost:27017/${projectName}
|
|
14
|
+
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (database === 'postgresql') {
|
|
19
|
+
content += `# PostgreSQL (Prisma)
|
|
20
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/${projectName}?schema=public
|
|
21
|
+
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (database === 'mysql') {
|
|
26
|
+
content += `# MySQL (Prisma)
|
|
27
|
+
DATABASE_URL=mysql://user:password@localhost:3306/${projectName}
|
|
28
|
+
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (auth === 'jwt') {
|
|
33
|
+
content += `# JWT
|
|
34
|
+
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
|
|
35
|
+
JWT_EXPIRES_IN=7d
|
|
36
|
+
JWT_REFRESH_SECRET=your_super_secret_refresh_key_change_this_too
|
|
37
|
+
JWT_REFRESH_EXPIRES_IN=30d
|
|
38
|
+
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (auth === 'session') {
|
|
43
|
+
content += `# Session
|
|
44
|
+
SESSION_SECRET=your_session_secret_change_this_in_production
|
|
45
|
+
SESSION_MAX_AGE=86400000
|
|
46
|
+
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (extras.includes('multer')) {
|
|
51
|
+
content += `# File Upload
|
|
52
|
+
MAX_FILE_SIZE=5242880
|
|
53
|
+
UPLOAD_DIR=uploads
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return content.trimEnd() + '\n';
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function generatePackageJson(answers) {
|
|
2
|
+
const { projectName, database, auth, extras, language } = answers;
|
|
3
|
+
|
|
4
|
+
const deps = {
|
|
5
|
+
express: '^5.0.0',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const devDeps = {
|
|
9
|
+
nodemon: '^3.1.0',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
if (database === 'mongodb') deps['mongoose'] = '^8.0.0';
|
|
13
|
+
if (database === 'postgresql' || database === 'mysql') {
|
|
14
|
+
deps['@prisma/client'] = '^5.0.0';
|
|
15
|
+
devDeps['prisma'] = '^5.0.0';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (auth === 'jwt') { deps['jsonwebtoken'] = '^9.0.0'; deps['bcryptjs'] = '^2.4.3'; }
|
|
19
|
+
if (auth === 'session') { deps['express-session'] = '^1.18.0'; deps['connect-mongo'] = '^5.1.0'; }
|
|
20
|
+
|
|
21
|
+
if (extras.includes('cors')) deps['cors'] = '^2.8.5';
|
|
22
|
+
if (extras.includes('helmet')) deps['helmet'] = '^7.1.0';
|
|
23
|
+
if (extras.includes('morgan')) deps['morgan'] = '^1.10.0';
|
|
24
|
+
if (extras.includes('rateLimit')) deps['express-rate-limit'] = '^7.0.0';
|
|
25
|
+
if (extras.includes('validation')) deps['express-validator'] = '^7.0.0';
|
|
26
|
+
if (extras.includes('multer')) deps['multer'] = '^1.4.5';
|
|
27
|
+
if (extras.includes('socket')) deps['socket.io'] = '^4.7.0';
|
|
28
|
+
if (extras.includes('swagger')) {
|
|
29
|
+
deps['swagger-ui-express'] = '^5.0.0';
|
|
30
|
+
deps['swagger-jsdoc'] = '^6.2.8';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deps['dotenv'] = '^16.4.0';
|
|
34
|
+
|
|
35
|
+
if (language === 'ts') {
|
|
36
|
+
devDeps['typescript'] = '^5.4.0';
|
|
37
|
+
devDeps['tsx'] = '^4.7.0';
|
|
38
|
+
devDeps['@types/node'] = '^20.0.0';
|
|
39
|
+
devDeps['@types/express'] = '^5.0.0';
|
|
40
|
+
if (deps['cors']) devDeps['@types/cors'] = '^2.8.17';
|
|
41
|
+
if (deps['morgan']) devDeps['@types/morgan'] = '^1.9.9';
|
|
42
|
+
if (deps['bcryptjs']) devDeps['@types/bcryptjs'] = '^2.4.6';
|
|
43
|
+
if (deps['jsonwebtoken']) devDeps['@types/jsonwebtoken'] = '^9.0.6';
|
|
44
|
+
if (deps['multer']) devDeps['@types/multer'] = '^1.4.11';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const scripts = language === 'ts'
|
|
48
|
+
? {
|
|
49
|
+
dev: 'tsx watch server.ts',
|
|
50
|
+
build: 'tsc',
|
|
51
|
+
start: 'node dist/server.js',
|
|
52
|
+
lint: 'tsc --noEmit',
|
|
53
|
+
}
|
|
54
|
+
: {
|
|
55
|
+
dev: 'nodemon server.js',
|
|
56
|
+
start: 'node server.js',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
name: projectName,
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
description: '',
|
|
63
|
+
type: 'module',
|
|
64
|
+
main: language === 'ts' ? 'dist/server.js' : 'server.js',
|
|
65
|
+
scripts,
|
|
66
|
+
dependencies: deps,
|
|
67
|
+
devDependencies: devDeps,
|
|
68
|
+
engines: { node: '>=18.0.0' },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { generatePackageJson } from './packageJsonGenerator.js';
|
|
6
|
+
import { generateEnv } from './envGenerator.js';
|
|
7
|
+
import { generateAppJs } from './appGenerator.js';
|
|
8
|
+
import { generateDbConfig } from './dbGenerator.js';
|
|
9
|
+
import { generateAuthFiles } from './authGenerator.js';
|
|
10
|
+
import { generateStructure } from './structureGenerator.js';
|
|
11
|
+
import { generateReadme } from './readmeGenerator.js';
|
|
12
|
+
|
|
13
|
+
export async function generateProject(answers, targetDir) {
|
|
14
|
+
const spinner = ora({ text: 'Creating project...', color: 'cyan' }).start();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// 1. Check if directory exists
|
|
18
|
+
if (await fs.pathExists(targetDir)) {
|
|
19
|
+
spinner.fail(chalk.red(`Directory "${answers.projectName}" already exists!`));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await fs.ensureDir(targetDir);
|
|
24
|
+
spinner.text = 'Scaffolding folder structure...';
|
|
25
|
+
|
|
26
|
+
// 2. Generate folder structure based on chosen pattern
|
|
27
|
+
await generateStructure(answers, targetDir);
|
|
28
|
+
spinner.text = 'Writing configuration files...';
|
|
29
|
+
|
|
30
|
+
// 3. package.json
|
|
31
|
+
const pkg = generatePackageJson(answers);
|
|
32
|
+
await fs.writeJson(path.join(targetDir, 'package.json'), pkg, { spaces: 2 });
|
|
33
|
+
|
|
34
|
+
// 4. .env + .env.example
|
|
35
|
+
const envContent = generateEnv(answers);
|
|
36
|
+
await fs.writeFile(path.join(targetDir, '.env'), envContent);
|
|
37
|
+
await fs.writeFile(path.join(targetDir, '.env.example'), envContent.replace(/=.+/gm, '='));
|
|
38
|
+
|
|
39
|
+
// 5. .gitignore
|
|
40
|
+
await fs.writeFile(path.join(targetDir, '.gitignore'), gitignoreContent());
|
|
41
|
+
|
|
42
|
+
// 6. app.js / app.ts
|
|
43
|
+
const appContent = generateAppJs(answers);
|
|
44
|
+
const ext = answers.language === 'ts' ? 'ts' : 'js';
|
|
45
|
+
await fs.writeFile(path.join(targetDir, 'src', `app.${ext}`), appContent);
|
|
46
|
+
|
|
47
|
+
// 7. server.js entry
|
|
48
|
+
await fs.writeFile(path.join(targetDir, `server.${ext}`), serverContent(answers));
|
|
49
|
+
|
|
50
|
+
// 8. DB config
|
|
51
|
+
if (answers.database !== 'none') {
|
|
52
|
+
spinner.text = 'Setting up database connection...';
|
|
53
|
+
const dbContent = generateDbConfig(answers);
|
|
54
|
+
await fs.writeFile(path.join(targetDir, 'src', 'config', `db.${ext}`), dbContent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 9. env config
|
|
58
|
+
await fs.writeFile(
|
|
59
|
+
path.join(targetDir, 'src', 'config', `env.${ext}`),
|
|
60
|
+
envConfigContent(answers)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// 10. Auth files
|
|
64
|
+
if (answers.auth !== 'none') {
|
|
65
|
+
spinner.text = 'Generating auth files...';
|
|
66
|
+
await generateAuthFiles(answers, targetDir, ext);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 11. Utils
|
|
70
|
+
await fs.writeFile(
|
|
71
|
+
path.join(targetDir, 'src', 'utils', `apiResponse.${ext}`),
|
|
72
|
+
apiResponseContent()
|
|
73
|
+
);
|
|
74
|
+
await fs.writeFile(
|
|
75
|
+
path.join(targetDir, 'src', 'utils', `logger.${ext}`),
|
|
76
|
+
loggerContent()
|
|
77
|
+
);
|
|
78
|
+
await fs.writeFile(
|
|
79
|
+
path.join(targetDir, 'src', 'middlewares', `errorHandler.${ext}`),
|
|
80
|
+
errorHandlerContent()
|
|
81
|
+
);
|
|
82
|
+
await fs.writeFile(
|
|
83
|
+
path.join(targetDir, 'src', 'middlewares', `notFound.${ext}`),
|
|
84
|
+
notFoundContent()
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 12. TypeScript config
|
|
88
|
+
if (answers.language === 'ts') {
|
|
89
|
+
await fs.writeJson(path.join(targetDir, 'tsconfig.json'), tsConfig(), { spaces: 2 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 13. README
|
|
93
|
+
spinner.text = 'Generating README...';
|
|
94
|
+
const readme = generateReadme(answers);
|
|
95
|
+
await fs.writeFile(path.join(targetDir, 'README.md'), readme);
|
|
96
|
+
|
|
97
|
+
spinner.succeed(chalk.green('Project created successfully!'));
|
|
98
|
+
|
|
99
|
+
// Success message
|
|
100
|
+
console.log('\n' + chalk.bold(' Next steps:\n'));
|
|
101
|
+
console.log(chalk.cyan(` cd ${answers.projectName}`));
|
|
102
|
+
console.log(chalk.cyan(' npm install'));
|
|
103
|
+
if (answers.database === 'postgresql' || answers.database === 'mysql') {
|
|
104
|
+
console.log(chalk.cyan(' npx prisma migrate dev'));
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk.cyan(' cp .env.example .env # fill in your values'));
|
|
107
|
+
console.log(chalk.cyan(' npm run dev\n'));
|
|
108
|
+
console.log(chalk.dim(' Happy building! — create-xpressforge by Hammad Sadi\n'));
|
|
109
|
+
|
|
110
|
+
} catch (err) {
|
|
111
|
+
spinner.fail(chalk.red('Failed to create project'));
|
|
112
|
+
await fs.remove(targetDir).catch(() => {});
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function gitignoreContent() {
|
|
118
|
+
return `node_modules/
|
|
119
|
+
.env
|
|
120
|
+
dist/
|
|
121
|
+
build/
|
|
122
|
+
*.log
|
|
123
|
+
.DS_Store
|
|
124
|
+
coverage/
|
|
125
|
+
.nyc_output/
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function serverContent(answers) {
|
|
130
|
+
const ext = answers.language === 'ts' ? "import app from './src/app.js';" : "import app from './src/app.js';";
|
|
131
|
+
return `${ext}
|
|
132
|
+
import { env } from './src/config/env.js';
|
|
133
|
+
|
|
134
|
+
const PORT = env.PORT || 3000;
|
|
135
|
+
|
|
136
|
+
app.listen(PORT, () => {
|
|
137
|
+
console.log(\`Server running on http://localhost:\${PORT}\`);
|
|
138
|
+
console.log(\`Environment: \${env.NODE_ENV}\`);
|
|
139
|
+
});
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function envConfigContent(answers) {
|
|
144
|
+
return `export const env = {
|
|
145
|
+
PORT: process.env.PORT || 3000,
|
|
146
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
147
|
+
${answers.database === 'mongodb' ? ` MONGO_URI: process.env.MONGO_URI,\n` : ''}${answers.database === 'postgresql' || answers.database === 'mysql' ? ` DATABASE_URL: process.env.DATABASE_URL,\n` : ''}${answers.auth === 'jwt' ? ` JWT_SECRET: process.env.JWT_SECRET,\n JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '7d',\n JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET,\n JWT_REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || '30d',\n` : ''}};
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function apiResponseContent() {
|
|
152
|
+
return `/**
|
|
153
|
+
* Consistent API response helper
|
|
154
|
+
*/
|
|
155
|
+
export const sendSuccess = (res, data = null, message = 'Success', statusCode = 200) => {
|
|
156
|
+
return res.status(statusCode).json({
|
|
157
|
+
success: true,
|
|
158
|
+
message,
|
|
159
|
+
data,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const sendError = (res, message = 'Something went wrong', statusCode = 500, errors = null) => {
|
|
164
|
+
return res.status(statusCode).json({
|
|
165
|
+
success: false,
|
|
166
|
+
message,
|
|
167
|
+
...(errors && { errors }),
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const sendPaginated = (res, data, pagination) => {
|
|
172
|
+
return res.status(200).json({
|
|
173
|
+
success: true,
|
|
174
|
+
data,
|
|
175
|
+
pagination: {
|
|
176
|
+
total: pagination.total,
|
|
177
|
+
page: pagination.page,
|
|
178
|
+
limit: pagination.limit,
|
|
179
|
+
totalPages: Math.ceil(pagination.total / pagination.limit),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function loggerContent() {
|
|
187
|
+
return `import { env } from '../config/env.js';
|
|
188
|
+
|
|
189
|
+
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
190
|
+
const colors = { error: '\\x1b[31m', warn: '\\x1b[33m', info: '\\x1b[36m', debug: '\\x1b[35m', reset: '\\x1b[0m' };
|
|
191
|
+
|
|
192
|
+
const log = (level, message, meta = {}) => {
|
|
193
|
+
if (env.NODE_ENV === 'test') return;
|
|
194
|
+
const timestamp = new Date().toISOString();
|
|
195
|
+
const color = colors[level] || '';
|
|
196
|
+
const metaStr = Object.keys(meta).length ? ' ' + JSON.stringify(meta) : '';
|
|
197
|
+
console.log(\`\${color}[\${timestamp}] [\${level.toUpperCase()}] \${message}\${metaStr}\${colors.reset}\`);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const logger = {
|
|
201
|
+
error: (msg, meta) => log('error', msg, meta),
|
|
202
|
+
warn: (msg, meta) => log('warn', msg, meta),
|
|
203
|
+
info: (msg, meta) => log('info', msg, meta),
|
|
204
|
+
debug: (msg, meta) => log('debug', msg, meta),
|
|
205
|
+
};
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function errorHandlerContent() {
|
|
210
|
+
return `import { logger } from '../utils/logger.js';
|
|
211
|
+
import { env } from '../config/env.js';
|
|
212
|
+
|
|
213
|
+
export const errorHandler = (err, req, res, next) => {
|
|
214
|
+
let statusCode = err.statusCode || err.status || 500;
|
|
215
|
+
let message = err.message || 'Internal Server Error';
|
|
216
|
+
|
|
217
|
+
// Mongoose duplicate key
|
|
218
|
+
if (err.code === 11000) {
|
|
219
|
+
statusCode = 409;
|
|
220
|
+
const field = Object.keys(err.keyValue || {})[0];
|
|
221
|
+
message = \`\${field} already exists\`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Mongoose validation error
|
|
225
|
+
if (err.name === 'ValidationError') {
|
|
226
|
+
statusCode = 422;
|
|
227
|
+
message = Object.values(err.errors).map(e => e.message).join(', ');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// JWT errors
|
|
231
|
+
if (err.name === 'JsonWebTokenError') { statusCode = 401; message = 'Invalid token'; }
|
|
232
|
+
if (err.name === 'TokenExpiredError') { statusCode = 401; message = 'Token expired'; }
|
|
233
|
+
|
|
234
|
+
logger.error(message, { statusCode, path: req.path, method: req.method });
|
|
235
|
+
|
|
236
|
+
res.status(statusCode).json({
|
|
237
|
+
success: false,
|
|
238
|
+
message,
|
|
239
|
+
...(env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function notFoundContent() {
|
|
246
|
+
return `export const notFound = (req, res) => {
|
|
247
|
+
res.status(404).json({
|
|
248
|
+
success: false,
|
|
249
|
+
message: \`Route \${req.method} \${req.originalUrl} not found\`,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function tsConfig() {
|
|
256
|
+
return {
|
|
257
|
+
compilerOptions: {
|
|
258
|
+
target: 'ES2022',
|
|
259
|
+
module: 'ESNext',
|
|
260
|
+
moduleResolution: 'node',
|
|
261
|
+
outDir: './dist',
|
|
262
|
+
rootDir: './',
|
|
263
|
+
strict: true,
|
|
264
|
+
esModuleInterop: true,
|
|
265
|
+
skipLibCheck: true,
|
|
266
|
+
forceConsistentCasingInFileNames: true,
|
|
267
|
+
resolveJsonModule: true,
|
|
268
|
+
declaration: true,
|
|
269
|
+
declarationMap: true,
|
|
270
|
+
sourceMap: true,
|
|
271
|
+
},
|
|
272
|
+
include: ['src/**/*', 'server.ts'],
|
|
273
|
+
exclude: ['node_modules', 'dist'],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function generateReadme(answers) {
|
|
2
|
+
const { projectName, structure, database, auth, extras, language } = answers;
|
|
3
|
+
|
|
4
|
+
const dbSetup = database === 'mongodb'
|
|
5
|
+
? `Set your \`MONGO_URI\` in \`.env\``
|
|
6
|
+
: database !== 'none'
|
|
7
|
+
? `Set your \`DATABASE_URL\` in \`.env\`, then run:\n\`\`\`bash\nnpx prisma migrate dev\n\`\`\``
|
|
8
|
+
: '';
|
|
9
|
+
|
|
10
|
+
return `# ${projectName}
|
|
11
|
+
|
|
12
|
+
> Scaffolded with [create-xpressforge](https://github.com/hammad-sadi/create-xpressforge)
|
|
13
|
+
|
|
14
|
+
## Stack
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Runtime | Node.js (ES Modules) |
|
|
19
|
+
| Framework | Express.js v5 |
|
|
20
|
+
| Language | ${language === 'ts' ? 'TypeScript' : 'JavaScript'} |
|
|
21
|
+
| Structure | ${structure.toUpperCase()} |
|
|
22
|
+
| Database | ${database === 'none' ? 'None' : database.charAt(0).toUpperCase() + database.slice(1)} |
|
|
23
|
+
| Auth | ${auth === 'none' ? 'None' : auth.toUpperCase()} |
|
|
24
|
+
|
|
25
|
+
## Getting started
|
|
26
|
+
|
|
27
|
+
\`\`\`bash
|
|
28
|
+
npm install
|
|
29
|
+
cp .env.example .env # fill in your values
|
|
30
|
+
${dbSetup}
|
|
31
|
+
npm run dev
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
## Project structure
|
|
35
|
+
|
|
36
|
+
\`\`\`
|
|
37
|
+
src/
|
|
38
|
+
├── config/ # DB connection, env validation
|
|
39
|
+
├── controllers/ # Request handlers
|
|
40
|
+
├── middlewares/ # Auth, error handler, not-found
|
|
41
|
+
├── models/ # Data models / schemas
|
|
42
|
+
├── routes/ # Express routers
|
|
43
|
+
├── services/ # Business logic
|
|
44
|
+
└── utils/ # apiResponse, logger
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
## API endpoints
|
|
48
|
+
|
|
49
|
+
| Method | Endpoint | Description | Auth |
|
|
50
|
+
|--------|----------|-------------|------|
|
|
51
|
+
${auth !== 'none' ? `| POST | /api/v1/auth/register | Register | No |
|
|
52
|
+
| POST | /api/v1/auth/login | Login | No |
|
|
53
|
+
| POST | /api/v1/auth/refresh | Refresh token | No |
|
|
54
|
+
| GET | /api/v1/auth/me | Get current user | Yes |
|
|
55
|
+
` : ''}| GET | /api/v1/users | List users | ${auth !== 'none' ? 'Yes' : 'No'} |
|
|
56
|
+
| GET | /api/v1/users/:id | Get user | ${auth !== 'none' ? 'Yes' : 'No'} |
|
|
57
|
+
| PUT | /api/v1/users/:id | Update user | ${auth !== 'none' ? 'Yes' : 'No'} |
|
|
58
|
+
| DELETE | /api/v1/users/:id | Delete user | ${auth !== 'none' ? 'Admin' : 'No'} |
|
|
59
|
+
| GET | /health | Health check | No |
|
|
60
|
+
|
|
61
|
+
## Scripts
|
|
62
|
+
|
|
63
|
+
\`\`\`bash
|
|
64
|
+
npm run dev # development with hot reload
|
|
65
|
+
npm start # production
|
|
66
|
+
${language === 'ts' ? 'npm run build # compile TypeScript\nnpm run lint # type check' : ''}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
## Environment variables
|
|
70
|
+
|
|
71
|
+
See \`.env.example\` for all required variables.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
Built with ❤️ by Hammad Sadi — powered by [create-xpressforge](https://npmjs.com/package/create-xpressforge)
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function generateStructure(answers, targetDir) {
|
|
5
|
+
const { structure, database, auth } = answers;
|
|
6
|
+
const ext = answers.language === 'ts' ? 'ts' : 'js';
|
|
7
|
+
|
|
8
|
+
const base = (p) => path.join(targetDir, p);
|
|
9
|
+
|
|
10
|
+
// Common folders
|
|
11
|
+
const commonDirs = [
|
|
12
|
+
'src/config',
|
|
13
|
+
'src/middlewares',
|
|
14
|
+
'src/utils',
|
|
15
|
+
'src/routes',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
if (structure === 'mvc') {
|
|
19
|
+
const dirs = [
|
|
20
|
+
...commonDirs,
|
|
21
|
+
'src/controllers',
|
|
22
|
+
'src/models',
|
|
23
|
+
'src/services',
|
|
24
|
+
];
|
|
25
|
+
await Promise.all(dirs.map(d => fs.ensureDir(base(d))));
|
|
26
|
+
await writeMvcFiles(answers, targetDir, ext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (structure === 'modular') {
|
|
30
|
+
const dirs = [
|
|
31
|
+
...commonDirs,
|
|
32
|
+
'src/modules/user/controller',
|
|
33
|
+
'src/modules/user/service',
|
|
34
|
+
'src/modules/user/model',
|
|
35
|
+
'src/modules/user/routes',
|
|
36
|
+
'src/modules/user/dto',
|
|
37
|
+
];
|
|
38
|
+
await Promise.all(dirs.map(d => fs.ensureDir(base(d))));
|
|
39
|
+
await writeModularFiles(answers, targetDir, ext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (structure === 'layered') {
|
|
43
|
+
const dirs = [
|
|
44
|
+
...commonDirs,
|
|
45
|
+
'src/controllers',
|
|
46
|
+
'src/services',
|
|
47
|
+
'src/repositories',
|
|
48
|
+
'src/models',
|
|
49
|
+
];
|
|
50
|
+
await Promise.all(dirs.map(d => fs.ensureDir(base(d))));
|
|
51
|
+
await writeLayeredFiles(answers, targetDir, ext);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function writeMvcFiles(answers, targetDir, ext) {
|
|
56
|
+
const base = (p) => path.join(targetDir, p);
|
|
57
|
+
|
|
58
|
+
// Example User controller
|
|
59
|
+
await fs.writeFile(base(`src/controllers/userController.${ext}`), `import { sendSuccess, sendError, sendPaginated } from '../utils/apiResponse.js';
|
|
60
|
+
${answers.database === 'mongodb' ? "import User from '../models/User.js';" : "import { prisma } from '../config/db.js';"}
|
|
61
|
+
|
|
62
|
+
export const getAllUsers = async (req, res, next) => {
|
|
63
|
+
try {
|
|
64
|
+
const page = parseInt(req.query.page) || 1;
|
|
65
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
66
|
+
const skip = (page - 1) * limit;
|
|
67
|
+
|
|
68
|
+
${answers.database === 'mongodb'
|
|
69
|
+
? `const [users, total] = await Promise.all([\n User.find().select('-password').skip(skip).limit(limit).lean(),\n User.countDocuments(),\n ]);`
|
|
70
|
+
: `const [users, total] = await Promise.all([\n prisma.user.findMany({ skip, take: limit, select: { id: true, name: true, email: true, role: true, createdAt: true } }),\n prisma.user.count(),\n ]);`}
|
|
71
|
+
|
|
72
|
+
sendPaginated(res, users, { total, page, limit });
|
|
73
|
+
} catch (err) { next(err); }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const getUserById = async (req, res, next) => {
|
|
77
|
+
try {
|
|
78
|
+
const user = await ${answers.database === 'mongodb'
|
|
79
|
+
? "User.findById(req.params.id).select('-password')"
|
|
80
|
+
: "prisma.user.findUnique({ where: { id: req.params.id }, select: { id: true, name: true, email: true, role: true } })"};
|
|
81
|
+
if (!user) return sendError(res, 'User not found', 404);
|
|
82
|
+
sendSuccess(res, user);
|
|
83
|
+
} catch (err) { next(err); }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const updateUser = async (req, res, next) => {
|
|
87
|
+
try {
|
|
88
|
+
const { name } = req.body;
|
|
89
|
+
const user = await ${answers.database === 'mongodb'
|
|
90
|
+
? "User.findByIdAndUpdate(req.params.id, { name }, { new: true, runValidators: true }).select('-password')"
|
|
91
|
+
: "prisma.user.update({ where: { id: req.params.id }, data: { name }, select: { id: true, name: true, email: true } })"};
|
|
92
|
+
if (!user) return sendError(res, 'User not found', 404);
|
|
93
|
+
sendSuccess(res, user, 'User updated');
|
|
94
|
+
} catch (err) { next(err); }
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const deleteUser = async (req, res, next) => {
|
|
98
|
+
try {
|
|
99
|
+
const user = await ${answers.database === 'mongodb'
|
|
100
|
+
? "User.findByIdAndDelete(req.params.id)"
|
|
101
|
+
: "prisma.user.delete({ where: { id: req.params.id } })"};
|
|
102
|
+
if (!user) return sendError(res, 'User not found', 404);
|
|
103
|
+
sendSuccess(res, null, 'User deleted');
|
|
104
|
+
} catch (err) { next(err); }
|
|
105
|
+
};
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
// Routes index
|
|
109
|
+
await fs.writeFile(base(`src/routes/index.${ext}`), `import { Router } from 'express';
|
|
110
|
+
import userRoutes from './userRoutes.js';
|
|
111
|
+
${answers.auth !== 'none' ? "import authRoutes from './authRoutes.js';" : ''}
|
|
112
|
+
|
|
113
|
+
const router = Router();
|
|
114
|
+
|
|
115
|
+
${answers.auth !== 'none' ? "router.use('/auth', authRoutes);" : ''}
|
|
116
|
+
router.use('/users', userRoutes);
|
|
117
|
+
|
|
118
|
+
export default router;
|
|
119
|
+
`);
|
|
120
|
+
|
|
121
|
+
// User routes
|
|
122
|
+
await fs.writeFile(base(`src/routes/userRoutes.${ext}`), `import { Router } from 'express';
|
|
123
|
+
import { getAllUsers, getUserById, updateUser, deleteUser } from '../controllers/userController.js';
|
|
124
|
+
${answers.auth === 'jwt' ? "import { authenticate, authorize } from '../middlewares/authenticate.js';" : ''}
|
|
125
|
+
|
|
126
|
+
const router = Router();
|
|
127
|
+
|
|
128
|
+
${answers.auth === 'jwt' ? "router.use(authenticate);" : ''}
|
|
129
|
+
router.get('/', getAllUsers);
|
|
130
|
+
router.get('/:id', getUserById);
|
|
131
|
+
router.put('/:id', updateUser);
|
|
132
|
+
router.delete('/:id', ${answers.auth === 'jwt' ? "authorize('admin'), " : ''}deleteUser);
|
|
133
|
+
|
|
134
|
+
export default router;
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function writeModularFiles(answers, targetDir, ext) {
|
|
139
|
+
const base = (p) => path.join(targetDir, p);
|
|
140
|
+
|
|
141
|
+
// user module controller
|
|
142
|
+
await fs.writeFile(base(`src/modules/user/controller/user.controller.${ext}`), `import { UserService } from '../service/user.service.js';
|
|
143
|
+
import { sendSuccess, sendError } from '../../../utils/apiResponse.js';
|
|
144
|
+
|
|
145
|
+
const userService = new UserService();
|
|
146
|
+
|
|
147
|
+
export const getUsers = async (req, res, next) => {
|
|
148
|
+
try {
|
|
149
|
+
const users = await userService.findAll();
|
|
150
|
+
sendSuccess(res, users);
|
|
151
|
+
} catch (err) { next(err); }
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const getUser = async (req, res, next) => {
|
|
155
|
+
try {
|
|
156
|
+
const user = await userService.findById(req.params.id);
|
|
157
|
+
if (!user) return sendError(res, 'User not found', 404);
|
|
158
|
+
sendSuccess(res, user);
|
|
159
|
+
} catch (err) { next(err); }
|
|
160
|
+
};
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
// user service
|
|
164
|
+
await fs.writeFile(base(`src/modules/user/service/user.service.${ext}`), `${answers.database === 'mongodb' ? "import User from '../model/user.model.js';" : "import { prisma } from '../../../config/db.js';"}
|
|
165
|
+
|
|
166
|
+
export class UserService {
|
|
167
|
+
async findAll() {
|
|
168
|
+
return ${answers.database === 'mongodb' ? "User.find().select('-password').lean()" : "prisma.user.findMany({ select: { id: true, name: true, email: true } })"};
|
|
169
|
+
}
|
|
170
|
+
async findById(id) {
|
|
171
|
+
return ${answers.database === 'mongodb' ? "User.findById(id).select('-password')" : "prisma.user.findUnique({ where: { id } })"};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
// user routes
|
|
177
|
+
await fs.writeFile(base(`src/modules/user/routes/user.routes.${ext}`), `import { Router } from 'express';
|
|
178
|
+
import { getUsers, getUser } from '../controller/user.controller.js';
|
|
179
|
+
|
|
180
|
+
const router = Router();
|
|
181
|
+
router.get('/', getUsers);
|
|
182
|
+
router.get('/:id', getUser);
|
|
183
|
+
export default router;
|
|
184
|
+
`);
|
|
185
|
+
|
|
186
|
+
// main routes index
|
|
187
|
+
await fs.writeFile(base(`src/routes/index.${ext}`), `import { Router } from 'express';
|
|
188
|
+
import userRoutes from '../modules/user/routes/user.routes.js';
|
|
189
|
+
${answers.auth !== 'none' ? "import authRoutes from './authRoutes.js';" : ''}
|
|
190
|
+
|
|
191
|
+
const router = Router();
|
|
192
|
+
${answers.auth !== 'none' ? "router.use('/auth', authRoutes);" : ''}
|
|
193
|
+
router.use('/users', userRoutes);
|
|
194
|
+
export default router;
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function writeLayeredFiles(answers, targetDir, ext) {
|
|
199
|
+
const base = (p) => path.join(targetDir, p);
|
|
200
|
+
|
|
201
|
+
// Repository layer
|
|
202
|
+
await fs.writeFile(base(`src/repositories/userRepository.${ext}`), `${answers.database === 'mongodb' ? "import User from '../models/User.js';" : "import { prisma } from '../config/db.js';"}
|
|
203
|
+
|
|
204
|
+
export class UserRepository {
|
|
205
|
+
async findAll({ skip = 0, limit = 10 } = {}) {
|
|
206
|
+
return ${answers.database === 'mongodb' ? "User.find().select('-password').skip(skip).limit(limit).lean()" : "prisma.user.findMany({ skip, take: limit, select: { id: true, name: true, email: true } })"};
|
|
207
|
+
}
|
|
208
|
+
async findById(id) {
|
|
209
|
+
return ${answers.database === 'mongodb' ? "User.findById(id).select('-password')" : "prisma.user.findUnique({ where: { id } })"};
|
|
210
|
+
}
|
|
211
|
+
async findByEmail(email) {
|
|
212
|
+
return ${answers.database === 'mongodb' ? "User.findOne({ email })" : "prisma.user.findUnique({ where: { email } })"};
|
|
213
|
+
}
|
|
214
|
+
async update(id, data) {
|
|
215
|
+
return ${answers.database === 'mongodb' ? "User.findByIdAndUpdate(id, data, { new: true }).select('-password')" : "prisma.user.update({ where: { id }, data })"};
|
|
216
|
+
}
|
|
217
|
+
async delete(id) {
|
|
218
|
+
return ${answers.database === 'mongodb' ? "User.findByIdAndDelete(id)" : "prisma.user.delete({ where: { id } })"};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`);
|
|
222
|
+
|
|
223
|
+
// Service layer
|
|
224
|
+
await fs.writeFile(base(`src/services/userService.${ext}`), `import { UserRepository } from '../repositories/userRepository.js';
|
|
225
|
+
|
|
226
|
+
export class UserService {
|
|
227
|
+
constructor() { this.repo = new UserRepository(); }
|
|
228
|
+
|
|
229
|
+
async getAllUsers(query) { return this.repo.findAll(query); }
|
|
230
|
+
async getUserById(id) {
|
|
231
|
+
const user = await this.repo.findById(id);
|
|
232
|
+
if (!user) throw Object.assign(new Error('User not found'), { statusCode: 404 });
|
|
233
|
+
return user;
|
|
234
|
+
}
|
|
235
|
+
async updateUser(id, data) { return this.repo.update(id, data); }
|
|
236
|
+
async deleteUser(id) { return this.repo.delete(id); }
|
|
237
|
+
}
|
|
238
|
+
`);
|
|
239
|
+
|
|
240
|
+
// Controller
|
|
241
|
+
await fs.writeFile(base(`src/controllers/userController.${ext}`), `import { UserService } from '../services/userService.js';
|
|
242
|
+
import { sendSuccess } from '../utils/apiResponse.js';
|
|
243
|
+
|
|
244
|
+
const userService = new UserService();
|
|
245
|
+
|
|
246
|
+
export const getAllUsers = async (req, res, next) => { try { sendSuccess(res, await userService.getAllUsers(req.query)); } catch(e){next(e);} };
|
|
247
|
+
export const getUserById = async (req, res, next) => { try { sendSuccess(res, await userService.getUserById(req.params.id)); } catch(e){next(e);} };
|
|
248
|
+
export const updateUser = async (req, res, next) => { try { sendSuccess(res, await userService.updateUser(req.params.id, req.body), 'User updated'); } catch(e){next(e);} };
|
|
249
|
+
export const deleteUser = async (req, res, next) => { try { await userService.deleteUser(req.params.id); sendSuccess(res, null, 'User deleted'); } catch(e){next(e);} };
|
|
250
|
+
`);
|
|
251
|
+
|
|
252
|
+
// Routes
|
|
253
|
+
await fs.writeFile(base(`src/routes/index.${ext}`), `import { Router } from 'express';
|
|
254
|
+
import { getAllUsers, getUserById, updateUser, deleteUser } from '../controllers/userController.js';
|
|
255
|
+
${answers.auth !== 'none' ? "import authRoutes from './authRoutes.js';\nimport { authenticate } from '../middlewares/authenticate.js';" : ''}
|
|
256
|
+
|
|
257
|
+
const router = Router();
|
|
258
|
+
${answers.auth !== 'none' ? "router.use('/auth', authRoutes);" : ''}
|
|
259
|
+
router.get('/users', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}getAllUsers);
|
|
260
|
+
router.get('/users/:id', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}getUserById);
|
|
261
|
+
router.put('/users/:id', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}updateUser);
|
|
262
|
+
router.delete('/users/:id', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}deleteUser);
|
|
263
|
+
export default router;
|
|
264
|
+
`);
|
|
265
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import gradient from 'gradient-string';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { askQuestions } from './prompts/questions.js';
|
|
5
|
+
import { generateProject } from './generators/projectGenerator.js';
|
|
6
|
+
|
|
7
|
+
export async function run() {
|
|
8
|
+
console.clear();
|
|
9
|
+
|
|
10
|
+
const banner = gradient(['#6C63FF', '#48CAE4'])(
|
|
11
|
+
`
|
|
12
|
+
██╗ ██╗██████╗ ██████╗ ███████╗███████╗███████╗
|
|
13
|
+
╚██╗██╔╝██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝
|
|
14
|
+
╚███╔╝ ██████╔╝██████╔╝█████╗ ███████╗███████╗
|
|
15
|
+
██╔██╗ ██╔═══╝ ██╔══██╗██╔══╝ ╚════██║╚════██║
|
|
16
|
+
██╔╝ ██╗██║ ██║ ██║███████╗███████║███████║
|
|
17
|
+
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝
|
|
18
|
+
F O R G E
|
|
19
|
+
`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
console.log(banner);
|
|
23
|
+
console.log(chalk.dim(' Production-ready Node.js + Express scaffolder\n'));
|
|
24
|
+
console.log(chalk.dim(' by Hammad Sadi\n'));
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const answers = await askQuestions();
|
|
28
|
+
const targetDir = path.resolve(process.cwd(), answers.projectName);
|
|
29
|
+
await generateProject(answers, targetDir);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err.name === 'ExitPromptError') {
|
|
32
|
+
console.log(chalk.yellow('\n Cancelled. See you next time!\n'));
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
console.error(chalk.red('\n Error: ' + err.message));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { input, select, checkbox } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export async function askQuestions() {
|
|
5
|
+
console.log(chalk.bold(' Let\'s configure your project:\n'));
|
|
6
|
+
|
|
7
|
+
const projectName = await input({
|
|
8
|
+
message: 'Project name:',
|
|
9
|
+
default: 'my-express-app',
|
|
10
|
+
validate: (val) => {
|
|
11
|
+
if (!val.trim()) return 'Project name cannot be empty';
|
|
12
|
+
if (!/^[a-z0-9-_]+$/.test(val)) return 'Use lowercase letters, numbers, hyphens only';
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const structure = await select({
|
|
18
|
+
message: 'Project structure:',
|
|
19
|
+
choices: [
|
|
20
|
+
{ name: 'MVC — controllers, models, views, routes', value: 'mvc' },
|
|
21
|
+
{ name: 'Modular — feature-based modules (recommended for large apps)', value: 'modular' },
|
|
22
|
+
{ name: 'Layered — controllers, services, repositories, models', value: 'layered' },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const database = await select({
|
|
27
|
+
message: 'Database:',
|
|
28
|
+
choices: [
|
|
29
|
+
{ name: 'MongoDB — with Mongoose ODM', value: 'mongodb' },
|
|
30
|
+
{ name: 'PostgreSQL — with Prisma ORM', value: 'postgresql' },
|
|
31
|
+
{ name: 'MySQL — with Sequelize ORM', value: 'mysql' },
|
|
32
|
+
{ name: 'None — no database', value: 'none' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const auth = await select({
|
|
37
|
+
message: 'Authentication:',
|
|
38
|
+
choices: [
|
|
39
|
+
{ name: 'JWT — access + refresh token', value: 'jwt' },
|
|
40
|
+
{ name: 'Session — express-session + cookie', value: 'session' },
|
|
41
|
+
{ name: 'None — skip auth setup', value: 'none' },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const extras = await checkbox({
|
|
46
|
+
message: 'Extra features (space to select):',
|
|
47
|
+
choices: [
|
|
48
|
+
{ name: 'Rate limiting (express-rate-limit)', value: 'rateLimit', checked: true },
|
|
49
|
+
{ name: 'Security headers (helmet)', value: 'helmet', checked: true },
|
|
50
|
+
{ name: 'CORS (cors)', value: 'cors', checked: true },
|
|
51
|
+
{ name: 'Request logger (morgan)', value: 'morgan', checked: true },
|
|
52
|
+
{ name: 'Input validation (express-validator)', value: 'validation', checked: false },
|
|
53
|
+
{ name: 'File upload (multer)', value: 'multer', checked: false },
|
|
54
|
+
{ name: 'Socket.io (real-time)', value: 'socket', checked: false },
|
|
55
|
+
{ name: 'Swagger docs (swagger-ui-express)', value: 'swagger', checked: false },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const language = await select({
|
|
60
|
+
message: 'Language:',
|
|
61
|
+
choices: [
|
|
62
|
+
{ name: 'JavaScript — ES Modules (type: module)', value: 'js' },
|
|
63
|
+
{ name: 'TypeScript — fully typed', value: 'ts' },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { projectName, structure, database, auth, extras, language };
|
|
68
|
+
}
|