create-stackflow 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.
@@ -0,0 +1,501 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import ora from "ora";
4
+ import { execa } from "execa";
5
+ import { ext, writeTemplate } from "../utils/template.js";
6
+
7
+ export async function createBackend(context) {
8
+ const spinner = ora("Generating backend API").start();
9
+ try {
10
+ await fs.ensureDir(context.backendDir);
11
+ await writeBackendFiles(context);
12
+ spinner.succeed("Generating backend API");
13
+ } catch (error) {
14
+ spinner.fail("Generating backend API");
15
+ throw error;
16
+ }
17
+
18
+ if (!context.skipInstall) {
19
+ const install = ora("Installing backend dependencies").start();
20
+ try {
21
+ await execa("npm", ["install"], { cwd: context.backendDir, stdio: "ignore" });
22
+ install.succeed("Installing backend dependencies");
23
+ } catch (error) {
24
+ install.fail("Installing backend dependencies");
25
+ throw error;
26
+ }
27
+ }
28
+ }
29
+
30
+ async function writeBackendFiles(context) {
31
+ const backendContext = { ...context, language: context.backendLanguage || context.language };
32
+ const e = ext(backendContext);
33
+ const isTs = backendContext.language === "typescript";
34
+ const clientUrl = context.frontend === "next" ? "http://localhost:3000" : "http://localhost:5173";
35
+ const deps = {
36
+ bcryptjs: "latest",
37
+ "cookie-parser": context.cookieParser === false ? undefined : "latest",
38
+ cors: context.cors === false ? undefined : "latest",
39
+ dotenv: "latest",
40
+ express: "latest",
41
+ "express-rate-limit": context.rateLimit === false ? undefined : "latest",
42
+ helmet: context.helmet === false ? undefined : "latest",
43
+ hpp: context.hpp ? "latest" : undefined,
44
+ jsonwebtoken: "latest",
45
+ mongoose: "latest",
46
+ multer: context.multer ? "latest" : undefined,
47
+ morgan: context.morgan ? "latest" : undefined,
48
+ winston: context.winston ? "latest" : undefined,
49
+ "swagger-jsdoc": context.swagger ? "latest" : undefined,
50
+ "swagger-ui-express": context.swagger ? "latest" : undefined,
51
+ "socket.io": context.socketio ? "latest" : undefined,
52
+ cloudinary: context.cloudinary ? "latest" : undefined
53
+ };
54
+ const devDeps = isTs
55
+ ? {
56
+ "@types/bcryptjs": "latest",
57
+ "@types/cookie-parser": "latest",
58
+ "@types/cors": "latest",
59
+ "@types/express": "latest",
60
+ "@types/jsonwebtoken": "latest",
61
+ "@types/morgan": context.morgan ? "latest" : undefined,
62
+ "@types/multer": context.multer ? "latest" : undefined,
63
+ "@types/node": "latest",
64
+ nodemon: "latest",
65
+ tsx: "latest",
66
+ typescript: "latest"
67
+ }
68
+ : { nodemon: "latest" };
69
+
70
+ await writeTemplate(path.join(context.backendDir, "package.json"), JSON.stringify({
71
+ name: path.basename(context.backendDir),
72
+ version: "1.0.0",
73
+ type: "module",
74
+ private: true,
75
+ scripts: {
76
+ dev: isTs ? "tsx watch src/server.ts" : "nodemon src/server.js",
77
+ start: isTs ? "node dist/server.js" : "node src/server.js",
78
+ build: isTs ? "tsc" : "echo \"No build step for JavaScript backend\""
79
+ },
80
+ dependencies: compact(deps),
81
+ devDependencies: compact(devDeps)
82
+ }, null, 2) + "\n");
83
+
84
+ if (isTs) {
85
+ await writeTemplate(path.join(context.backendDir, "tsconfig.json"), `{
86
+ "compilerOptions": {
87
+ "target": "ES2022",
88
+ "module": "NodeNext",
89
+ "moduleResolution": "NodeNext",
90
+ "strict": false,
91
+ "noImplicitAny": false,
92
+ "noCheck": true,
93
+ "esModuleInterop": true,
94
+ "skipLibCheck": true,
95
+ "outDir": "dist",
96
+ "rootDir": "src"
97
+ },
98
+ "include": ["src"]
99
+ }
100
+ `);
101
+ }
102
+
103
+ await writeTemplate(path.join(context.backendDir, ".env"), `PORT=5000
104
+ NODE_ENV=development
105
+ CLIENT_URL={{clientUrl}}
106
+ MONGODB_URI=mongodb://127.0.0.1:27017/{{databaseName}}
107
+ JWT_SECRET=change_this_super_secret_value
108
+ JWT_EXPIRES_IN=7d
109
+ COOKIE_NAME=stackflow_token
110
+ {{cloudinaryEnv}}
111
+ `, { ...context, clientUrl, cloudinaryEnv: context.cloudinary ? "CLOUDINARY_CLOUD_NAME=\nCLOUDINARY_API_KEY=\nCLOUDINARY_API_SECRET=" : "" });
112
+
113
+ const dirs = [
114
+ "src/config",
115
+ "src/controllers",
116
+ "src/middleware",
117
+ "src/models",
118
+ "src/routes",
119
+ "src/services",
120
+ "src/utils",
121
+ "src/validations"
122
+ ];
123
+ if (context.multer) dirs.push("uploads");
124
+ await Promise.all(dirs.map((dir) => fs.ensureDir(path.join(context.backendDir, dir))));
125
+ if (context.multer) await fs.outputFile(path.join(context.backendDir, "uploads", ".gitkeep"), "");
126
+
127
+ const files = backendTemplates({ ...context, language: backendContext.language, clientUrl }, e);
128
+ await Promise.all(Object.entries(files).map(([file, content]) =>
129
+ writeTemplate(path.join(context.backendDir, file), content, context)
130
+ ));
131
+ }
132
+
133
+ function compact(object) {
134
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value));
135
+ }
136
+
137
+ function backendTemplates(context, e) {
138
+ const isTs = context.language === "typescript";
139
+ const type = (name) => isTs ? `: ${name}` : "";
140
+ const reqRes = isTs ? "import type { Request, Response, NextFunction } from \"express\";\n" : "";
141
+ const imports = {
142
+ logger: context.winston ? "import { logger } from \"./utils/logger.js\";" : "",
143
+ morgan: context.morgan ? "import morgan from \"morgan\";" : "",
144
+ swagger: context.swagger ? "import { setupSwagger } from \"./config/swagger.js\";" : "",
145
+ socket: context.socketio ? "import { Server } from \"socket.io\";" : ""
146
+ };
147
+
148
+ return {
149
+ [`src/server.${e}`]: `import http from "node:http";
150
+ import app from "./app.js";
151
+ import { connectDB } from "./config/db.js";
152
+ ${imports.socket}
153
+ ${imports.logger}
154
+
155
+ const PORT = Number(process.env.PORT) || 5000;
156
+
157
+ async function bootstrap() {
158
+ await connectDB();
159
+ const server = http.createServer(app);
160
+ ${context.socketio ? `
161
+ const io = new Server(server, {
162
+ cors: {
163
+ origin: process.env.CLIENT_URL || "${context.clientUrl}",
164
+ credentials: true
165
+ }
166
+ });
167
+
168
+ io.on("connection", (socket) => {
169
+ socket.emit("connected", { message: "Socket.IO connected" });
170
+ });
171
+ ` : ""}
172
+ server.listen(PORT, () => {
173
+ ${context.winston ? "logger.info(`API running on http://localhost:${PORT}`);" : "console.log(`API running on http://localhost:${PORT}`);"}
174
+ });
175
+ }
176
+
177
+ bootstrap().catch((error) => {
178
+ ${context.winston ? "logger.error(error);" : "console.error(error);"}
179
+ process.exit(1);
180
+ });
181
+ `,
182
+ [`src/app.${e}`]: `import "dotenv/config";
183
+ import express from "express";
184
+ ${context.cookieParser === false ? "" : "import cookieParser from \"cookie-parser\";"}
185
+ ${context.cors === false ? "" : "import cors from \"cors\";"}
186
+ ${context.helmet === false ? "" : "import helmet from \"helmet\";"}
187
+ ${context.rateLimit === false ? "" : "import rateLimit from \"express-rate-limit\";"}
188
+ ${context.hpp ? "import hpp from \"hpp\";" : ""}
189
+ ${imports.morgan}
190
+ ${imports.swagger}
191
+ import apiRoutes from "./routes/index.routes.js";
192
+ import { errorHandler } from "./middleware/error.middleware.js";
193
+ import { notFound } from "./middleware/not-found.middleware.js";
194
+
195
+ const app = express();
196
+
197
+ ${context.helmet === false ? "" : "app.use(helmet());"}
198
+ ${context.hpp ? "app.use(hpp());" : ""}
199
+ ${context.cors === false ? "" : `app.use(cors({
200
+ origin: process.env.CLIENT_URL || "${context.clientUrl}",
201
+ credentials: true
202
+ }));`}
203
+ ${context.rateLimit === false ? "" : "app.use(rateLimit({ windowMs: 15 * 60 * 1000, limit: 300 }));"}
204
+ app.use("/uploads", express.static("uploads"));
205
+ app.use(express.json({ limit: "1mb" }));
206
+ app.use(express.urlencoded({ extended: true }));
207
+ ${context.cookieParser === false ? "" : "app.use(cookieParser());"}
208
+ ${context.morgan ? "app.use(morgan(\"dev\"));" : ""}
209
+ ${context.swagger ? "setupSwagger(app);" : ""}
210
+
211
+ app.get("/api/health", (_req, res) => {
212
+ res.json({ status: "ok", service: "stackflow-api" });
213
+ });
214
+
215
+ app.use("/api", apiRoutes);
216
+ app.use(notFound);
217
+ app.use(errorHandler);
218
+
219
+ export default app;
220
+ `,
221
+ [`src/config/db.${e}`]: `import mongoose from "mongoose";
222
+ ${context.winston ? "import { logger } from \"../utils/logger.js\";" : ""}
223
+
224
+ export async function connectDB() {
225
+ const uri = process.env.MONGODB_URI;
226
+ if (!uri) throw new Error("MONGODB_URI is required");
227
+ await mongoose.connect(uri);
228
+ ${context.winston ? "logger.info(\"MongoDB connected\");" : "console.log(\"MongoDB connected\");"}
229
+ }
230
+ `,
231
+ [`src/models/user.model.${e}`]: `import mongoose from "mongoose";
232
+ import bcrypt from "bcryptjs";
233
+
234
+ const userSchema = new mongoose.Schema({
235
+ name: { type: String, required: true, trim: true },
236
+ email: { type: String, required: true, unique: true, lowercase: true, trim: true },
237
+ password: { type: String, required: true, minlength: 6 }
238
+ }, { timestamps: true });
239
+
240
+ userSchema.pre("save", async function() {
241
+ if (!this.isModified("password")) return;
242
+ this.password = await bcrypt.hash(this.password, 12);
243
+ });
244
+
245
+ userSchema.methods.comparePassword = function(candidatePassword) {
246
+ return bcrypt.compare(candidatePassword, this.password);
247
+ };
248
+
249
+ export const User = mongoose.model("User", userSchema);
250
+ `,
251
+ [`src/models/task.model.${e}`]: `import mongoose from "mongoose";
252
+
253
+ const taskSchema = new mongoose.Schema({
254
+ title: { type: String, required: true, trim: true },
255
+ description: { type: String, default: "" },
256
+ imageUrl: { type: String, default: "" },
257
+ status: { type: String, enum: ["todo", "in-progress", "done"], default: "todo" },
258
+ owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }
259
+ }, { timestamps: true });
260
+
261
+ export const Task = mongoose.model("Task", taskSchema);
262
+ `,
263
+ [`src/utils/jwt.${e}`]: `import jwt from "jsonwebtoken";
264
+
265
+ export function signToken(userId) {
266
+ return jwt.sign({ userId }, process.env.JWT_SECRET || "dev_secret", {
267
+ expiresIn: process.env.JWT_EXPIRES_IN || "7d"
268
+ });
269
+ }
270
+
271
+ export function setAuthCookie(res, token) {
272
+ res.cookie(process.env.COOKIE_NAME || "stackflow_token", token, {
273
+ httpOnly: true,
274
+ sameSite: "lax",
275
+ secure: process.env.NODE_ENV === "production",
276
+ maxAge: 7 * 24 * 60 * 60 * 1000
277
+ });
278
+ }
279
+ `,
280
+ [`src/middleware/auth.middleware.${e}`]: `${reqRes}import jwt from "jsonwebtoken";
281
+ import { User } from "../models/user.model.js";
282
+
283
+ export async function protect(req${type("Request & { user?: unknown }")}, res${type("Response")}, next${type("NextFunction")}) {
284
+ try {
285
+ const cookieToken = req.cookies?.[process.env.COOKIE_NAME || "stackflow_token"];
286
+ const headerToken = req.headers.authorization?.startsWith("Bearer ")
287
+ ? req.headers.authorization.split(" ")[1]
288
+ : undefined;
289
+ const token = cookieToken || headerToken;
290
+ if (!token) return res.status(401).json({ message: "Authentication required" });
291
+
292
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || "dev_secret");
293
+ const userId = typeof decoded === "object" && "userId" in decoded ? decoded.userId : null;
294
+ const user = await User.findById(userId).select("-password");
295
+ if (!user) return res.status(401).json({ message: "Invalid authentication" });
296
+
297
+ req.user = user;
298
+ next();
299
+ } catch {
300
+ res.status(401).json({ message: "Invalid or expired token" });
301
+ }
302
+ }
303
+ `,
304
+ [`src/middleware/error.middleware.${e}`]: `${reqRes}export function errorHandler(error${type("Error & { statusCode?: number }")}, _req${type("Request")}, res${type("Response")}, _next${type("NextFunction")}) {
305
+ const status = error.statusCode || 500;
306
+ res.status(status).json({
307
+ message: error.message || "Server error",
308
+ stack: process.env.NODE_ENV === "production" ? undefined : error.stack
309
+ });
310
+ }
311
+ `,
312
+ [`src/middleware/not-found.middleware.${e}`]: `${reqRes}export function notFound(req${type("Request")}, res${type("Response")}) {
313
+ res.status(404).json({ message: \`Route not found: \${req.originalUrl}\` });
314
+ }
315
+ `,
316
+ [`src/controllers/auth.controller.${e}`]: `import { User } from "../models/user.model.js";
317
+ import { signToken, setAuthCookie } from "../utils/jwt.js";
318
+
319
+ function publicUser(user) {
320
+ return { id: user._id, name: user.name, email: user.email };
321
+ }
322
+
323
+ export async function register(req, res, next) {
324
+ try {
325
+ const { name, email, password } = req.body;
326
+ const existing = await User.findOne({ email });
327
+ if (existing) return res.status(409).json({ message: "Email already registered" });
328
+ const user = await User.create({ name, email, password });
329
+ const token = signToken(user._id);
330
+ setAuthCookie(res, token);
331
+ res.status(201).json({ user: publicUser(user), token });
332
+ } catch (error) {
333
+ next(error);
334
+ }
335
+ }
336
+
337
+ export async function login(req, res, next) {
338
+ try {
339
+ const { email, password } = req.body;
340
+ const user = await User.findOne({ email });
341
+ if (!user || !(await user.comparePassword(password))) {
342
+ return res.status(401).json({ message: "Invalid email or password" });
343
+ }
344
+ const token = signToken(user._id);
345
+ setAuthCookie(res, token);
346
+ res.json({ user: publicUser(user), token });
347
+ } catch (error) {
348
+ next(error);
349
+ }
350
+ }
351
+
352
+ export async function me(req, res) {
353
+ res.json({ user: publicUser(req.user) });
354
+ }
355
+
356
+ export async function logout(_req, res) {
357
+ res.clearCookie(process.env.COOKIE_NAME || "stackflow_token");
358
+ res.json({ message: "Logged out" });
359
+ }
360
+ `,
361
+ [`src/controllers/task.controller.${e}`]: `import { Task } from "../models/task.model.js";
362
+
363
+ export async function listTasks(req, res, next) {
364
+ try {
365
+ const tasks = await Task.find({ owner: req.user._id }).sort({ createdAt: -1 });
366
+ res.json({ tasks });
367
+ } catch (error) {
368
+ next(error);
369
+ }
370
+ }
371
+
372
+ export async function createTask(req, res, next) {
373
+ try {
374
+ const imageUrl = req.file ? \`/uploads/\${req.file.filename}\` : "";
375
+ const task = await Task.create({ ...req.body, imageUrl, owner: req.user._id });
376
+ res.status(201).json({ task });
377
+ } catch (error) {
378
+ next(error);
379
+ }
380
+ }
381
+
382
+ export async function updateTask(req, res, next) {
383
+ try {
384
+ const updates = { ...req.body };
385
+ if (req.file) updates.imageUrl = \`/uploads/\${req.file.filename}\`;
386
+ const task = await Task.findOneAndUpdate(
387
+ { _id: req.params.id, owner: req.user._id },
388
+ updates,
389
+ { new: true, runValidators: true }
390
+ );
391
+ if (!task) return res.status(404).json({ message: "Task not found" });
392
+ res.json({ task });
393
+ } catch (error) {
394
+ next(error);
395
+ }
396
+ }
397
+
398
+ export async function deleteTask(req, res, next) {
399
+ try {
400
+ const task = await Task.findOneAndDelete({ _id: req.params.id, owner: req.user._id });
401
+ if (!task) return res.status(404).json({ message: "Task not found" });
402
+ res.json({ message: "Task deleted" });
403
+ } catch (error) {
404
+ next(error);
405
+ }
406
+ }
407
+ `,
408
+ [`src/routes/auth.routes.${e}`]: `import { Router } from "express";
409
+ import { login, logout, me, register } from "../controllers/auth.controller.js";
410
+ import { protect } from "../middleware/auth.middleware.js";
411
+
412
+ const router = Router();
413
+
414
+ router.post("/register", register);
415
+ router.post("/login", login);
416
+ router.get("/me", protect, me);
417
+ router.post("/logout", logout);
418
+
419
+ export default router;
420
+ `,
421
+ [`src/routes/index.routes.${e}`]: `import { Router } from "express";
422
+ import authRoutes from "./auth.routes.js";
423
+ import taskRoutes from "./task.routes.js";
424
+
425
+ const router = Router();
426
+
427
+ router.use("/auth", authRoutes);
428
+ router.use("/tasks", taskRoutes);
429
+
430
+ export default router;
431
+ `,
432
+ [`src/routes/task.routes.${e}`]: `import { Router } from "express";
433
+ import { createTask, deleteTask, listTasks, updateTask } from "../controllers/task.controller.js";
434
+ import { protect } from "../middleware/auth.middleware.js";
435
+ ${context.multer ? "import { upload } from \"../services/upload.service.js\";" : ""}
436
+
437
+ const router = Router();
438
+
439
+ router.use(protect);
440
+ router.get("/", listTasks);
441
+ router.post("/", ${context.multer ? "upload.single(\"image\"), " : ""}createTask);
442
+ router.patch("/:id", ${context.multer ? "upload.single(\"image\"), " : ""}updateTask);
443
+ router.delete("/:id", deleteTask);
444
+
445
+ export default router;
446
+ `,
447
+ [`src/services/upload.service.${e}`]: context.multer ? `import multer from "multer";
448
+
449
+ const storage = multer.diskStorage({
450
+ destination: "uploads/",
451
+ filename: (_req, file, callback) => {
452
+ callback(null, \`\${Date.now()}-\${file.originalname}\`);
453
+ }
454
+ });
455
+
456
+ export const upload = multer({ storage });
457
+ ` : `export {};
458
+ `,
459
+ [`src/validations/auth.validation.${e}`]: `export const authValidation = {
460
+ register: ["name", "email", "password"],
461
+ login: ["email", "password"]
462
+ };
463
+ `,
464
+ [`src/utils/logger.${e}`]: context.winston ? `import winston from "winston";
465
+
466
+ export const logger = winston.createLogger({
467
+ level: "info",
468
+ format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
469
+ transports: [new winston.transports.Console()]
470
+ });
471
+ ` : `export const logger = console;
472
+ `,
473
+ [`src/config/swagger.${e}`]: context.swagger ? `import swaggerJSDoc from "swagger-jsdoc";
474
+ import swaggerUi from "swagger-ui-express";
475
+
476
+ export function setupSwagger(app) {
477
+ const spec = swaggerJSDoc({
478
+ definition: {
479
+ openapi: "3.0.0",
480
+ info: { title: "StackFlow API", version: "1.0.0" }
481
+ },
482
+ apis: ["./src/routes/*.js", "./src/routes/*.ts"]
483
+ });
484
+
485
+ app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(spec));
486
+ }
487
+ ` : `export function setupSwagger() {}
488
+ `,
489
+ [`src/config/cloudinary.${e}`]: context.cloudinary ? `import { v2 as cloudinary } from "cloudinary";
490
+
491
+ cloudinary.config({
492
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
493
+ api_key: process.env.CLOUDINARY_API_KEY,
494
+ api_secret: process.env.CLOUDINARY_API_SECRET
495
+ });
496
+
497
+ export { cloudinary };
498
+ ` : `export {};
499
+ `
500
+ };
501
+ }