From 1d4b65d968d503ac427170e9552c941691736902 Mon Sep 17 00:00:00 2001 From: Raunit Thakur Date: Wed, 15 Oct 2025 18:55:10 +0530 Subject: [PATCH] #8 --- templates/production-bkend/README.md | 21 +++++ templates/production-bkend/package.json | 54 +++++++++++++ .../public/dashboard/css/style.css | 0 .../public/dashboard/index.html | 0 .../public/dashboard/js/app.js | 0 templates/production-bkend/src/app.ts | 60 ++++++++++++++ .../production-bkend/src/config/index.ts | 53 ++++++++++++ .../production-bkend/src/middleware/auth.ts | 45 +++++++++++ .../src/middleware/errorhandler.ts | 53 ++++++++++++ .../production-bkend/src/middleware/logger.ts | 0 .../src/middleware/ratelimit.ts | 37 +++++++++ .../src/middleware/requestlogger.ts | 27 +++++++ templates/production-bkend/src/routes/auth.ts | 72 +++++++++++++++++ .../production-bkend/src/routes/files.ts | 0 .../production-bkend/src/routes/index.ts | 12 +++ .../production-bkend/src/routes/monitoring.ts | 0 templates/production-bkend/src/server.ts | 31 +++++++ .../production-bkend/src/services/auth.ts | 0 .../src/services/monitoring.ts | 0 .../src/services/notification/discord.ts | 0 .../src/services/notification/email.ts | 0 .../src/services/notification/slack.ts | 0 .../src/services/notification/sms.ts | 0 .../src/services/notification/telegram.ts | 0 .../src/services/notification/whatsapp.ts | 0 templates/production-bkend/src/types/index.ts | 0 .../production-bkend/src/utils/filesystem.ts | 0 .../production-bkend/src/utils/logger.ts | 81 +++++++++++++++++++ .../src/utils/notification.ts | 0 templates/production-bkend/tsconfig.json | 18 +++++ 30 files changed, 564 insertions(+) create mode 100644 templates/production-bkend/README.md create mode 100644 templates/production-bkend/package.json create mode 100644 templates/production-bkend/public/dashboard/css/style.css create mode 100644 templates/production-bkend/public/dashboard/index.html create mode 100644 templates/production-bkend/public/dashboard/js/app.js create mode 100644 templates/production-bkend/src/app.ts create mode 100644 templates/production-bkend/src/config/index.ts create mode 100644 templates/production-bkend/src/middleware/auth.ts create mode 100644 templates/production-bkend/src/middleware/errorhandler.ts create mode 100644 templates/production-bkend/src/middleware/logger.ts create mode 100644 templates/production-bkend/src/middleware/ratelimit.ts create mode 100644 templates/production-bkend/src/middleware/requestlogger.ts create mode 100644 templates/production-bkend/src/routes/auth.ts create mode 100644 templates/production-bkend/src/routes/files.ts create mode 100644 templates/production-bkend/src/routes/index.ts create mode 100644 templates/production-bkend/src/routes/monitoring.ts create mode 100644 templates/production-bkend/src/server.ts create mode 100644 templates/production-bkend/src/services/auth.ts create mode 100644 templates/production-bkend/src/services/monitoring.ts create mode 100644 templates/production-bkend/src/services/notification/discord.ts create mode 100644 templates/production-bkend/src/services/notification/email.ts create mode 100644 templates/production-bkend/src/services/notification/slack.ts create mode 100644 templates/production-bkend/src/services/notification/sms.ts create mode 100644 templates/production-bkend/src/services/notification/telegram.ts create mode 100644 templates/production-bkend/src/services/notification/whatsapp.ts create mode 100644 templates/production-bkend/src/types/index.ts create mode 100644 templates/production-bkend/src/utils/filesystem.ts create mode 100644 templates/production-bkend/src/utils/logger.ts create mode 100644 templates/production-bkend/src/utils/notification.ts create mode 100644 templates/production-bkend/tsconfig.json diff --git a/templates/production-bkend/README.md b/templates/production-bkend/README.md new file mode 100644 index 0000000..bcb0c93 --- /dev/null +++ b/templates/production-bkend/README.md @@ -0,0 +1,21 @@ +# Production Backend Template + +A production-ready backend template with TypeScript, Express, and enterprise features. + +## Features + +- ✅ TypeScript +- ✅ Express.js with security middleware +- ✅ JWT Authentication & RBAC +- ✅ Rate Limiting +- ✅ Request Logging +- ✅ Error Handling +- ✅ File Upload (PDF, Excel support) +- ✅ Monitoring Dashboard +- ✅ Multi-channel Notifications + +## Quick Start + +1. Install dependencies: +```bash +npm install \ No newline at end of file diff --git a/templates/production-bkend/package.json b/templates/production-bkend/package.json new file mode 100644 index 0000000..6c2fa9d --- /dev/null +++ b/templates/production-bkend/package.json @@ -0,0 +1,54 @@ +{ + "name": "production-backend", + "version": "1.0.0", + "description": "Production-ready backend with TypeScript, Express, and enterprise features", + "type": "module", + "main": "dist/server.js", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "node dist/server.js", + "clean": "rimraf dist", + "test": "jest" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "dotenv": "^16.3.1", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "express-rate-limit": "^7.1.5", + "multer": "^1.4.5-lts.1", + "xlsx": "^0.18.5", + "pdf-parse": "^1.1.1", + "nodemailer": "^6.9.7", + "twilio": "^4.19.0", + "axios": "^1.5.0", + "socket.io": "^4.7.4", + "chalk": "^5.3.0", + "winston": "^3.11.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/cors": "^2.8.17", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.5", + "@types/multer": "^1.4.11", + "@types/node": "^20.8.0", + "@types/nodemailer": "^6.4.14", + "typescript": "^5.2.2", + "tsx": "^4.6.0", + "rimraf": "^5.0.5" + }, + "keywords": [ + "express", + "typescript", + "backend", + "production", + "authentication", + "logging", + "monitoring" + ] +} \ No newline at end of file diff --git a/templates/production-bkend/public/dashboard/css/style.css b/templates/production-bkend/public/dashboard/css/style.css new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/public/dashboard/index.html b/templates/production-bkend/public/dashboard/index.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/public/dashboard/js/app.js b/templates/production-bkend/public/dashboard/js/app.js new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/app.ts b/templates/production-bkend/src/app.ts new file mode 100644 index 0000000..c385127 --- /dev/null +++ b/templates/production-bkend/src/app.ts @@ -0,0 +1,60 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import { config } from './config'; +import { requestLogger } from './middleware/requestLogger'; +import { globalRateLimiter } from './middleware/rateLimit'; +import { errorHandler } from './middleware/errorHandler'; +import routes from './routes'; + +const app = express(); + +// Security middleware +app.use(helmet()); +app.use(cors({ + origin: config.cors.origins, + credentials: true +})); + +// Body parsing middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Logging middleware +if (config.nodeEnv === 'development') { + app.use(morgan('combined')); +} +app.use(requestLogger); + +// Rate limiting +app.use(globalRateLimiter); + +// Static files +app.use(express.static('public')); + +// API routes +app.use('/api/v1', routes); + +// Health check +app.get('/health', (req, res) => { + res.status(200).json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: config.nodeEnv + }); +}); + +// 404 handler +app.use('*', (req, res) => { + res.status(404).json({ + error: 'Route not found', + path: req.originalUrl + }); +}); + +// Error handling middleware (must be last) +app.use(errorHandler); + +export default app; \ No newline at end of file diff --git a/templates/production-bkend/src/config/index.ts b/templates/production-bkend/src/config/index.ts new file mode 100644 index 0000000..0f03d6d --- /dev/null +++ b/templates/production-bkend/src/config/index.ts @@ -0,0 +1,53 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +export const config = { + port: parseInt(process.env.PORT || '3000'), + nodeEnv: process.env.NODE_ENV || 'development', + + cors: { + origins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'] + }, + + database: { + url: process.env.DATABASE_URL || 'mongodb://localhost:27017/production-app' + }, + + jwt: { + secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production', + expiresIn: process.env.JWT_EXPIRES_IN || '7d' + }, + + rateLimit: { + windowMs: 15 * 60 * 1000, // 15 minutes + max: parseInt(process.env.RATE_LIMIT_MAX || '100'), + loginMax: parseInt(process.env.RATE_LIMIT_LOGIN_MAX || '5') + }, + + upload: { + maxFileSize: 10 * 1024 * 1024, // 10MB + allowedMimeTypes: [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/plain' + ] + }, + + email: { + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || '587'), + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + from: process.env.EMAIL_FROM || 'noreply@yourapp.com' + }, + + twilio: { + accountSid: process.env.TWILIO_ACCOUNT_SID, + authToken: process.env.TWILIO_AUTH_TOKEN, + phoneNumber: process.env.TWILIO_PHONE_NUMBER + } +}; \ No newline at end of file diff --git a/templates/production-bkend/src/middleware/auth.ts b/templates/production-bkend/src/middleware/auth.ts new file mode 100644 index 0000000..ef2e8cb --- /dev/null +++ b/templates/production-bkend/src/middleware/auth.ts @@ -0,0 +1,45 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import { config } from '../config'; +import { logger } from '../utils/logger'; + +export interface AuthRequest extends Request { + user?: any; +} + +export const authenticate = (req: AuthRequest, res: Response, next: NextFunction) => { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ error: 'Access denied. No token provided.' }); + } + + try { + const decoded = jwt.verify(token, config.jwt.secret); + req.user = decoded; + next(); + } catch (error) { + logger.error('Token verification failed', { error }); + res.status(401).json({ error: 'Invalid token.' }); + } +}; + +export const authorize = (...roles: string[]) => { + return (req: AuthRequest, res: Response, next: NextFunction) => { + if (!req.user) { + return res.status(401).json({ error: 'Authentication required.' }); + } + + const hasRole = roles.some(role => req.user.roles.includes(role)); + if (!hasRole) { + logger.warn('Unauthorized access attempt', { + userId: req.user.id, + roles: req.user.roles, + requiredRoles: roles + }); + return res.status(403).json({ error: 'Insufficient permissions.' }); + } + + next(); + }; +}; \ No newline at end of file diff --git a/templates/production-bkend/src/middleware/errorhandler.ts b/templates/production-bkend/src/middleware/errorhandler.ts new file mode 100644 index 0000000..e83864b --- /dev/null +++ b/templates/production-bkend/src/middleware/errorhandler.ts @@ -0,0 +1,53 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '../utils/logger'; + +export class AppError extends Error { + public statusCode: number; + public isOperational: boolean; + + constructor(message: string, statusCode: number = 500, isOperational: boolean = true) { + super(message); + this.statusCode = statusCode; + this.isOperational = isOperational; + + Error.captureStackTrace(this, this.constructor); + } +} + +export const errorHandler = ( + error: Error | AppError, + req: Request, + res: Response, + next: NextFunction +) => { + let statusCode = 500; + let message = 'Internal server error'; + + if (error instanceof AppError) { + statusCode = error.statusCode; + message = error.message; + } else if (error.name === 'ValidationError') { + statusCode = 400; + message = 'Validation error'; + } else if (error.name === 'JsonWebTokenError') { + statusCode = 401; + message = 'Invalid token'; + } + + logger.error('Error handled by middleware', { + error: error.message, + url: req.url, + method: req.method, + ip: req.ip, + statusCode + }); + + res.status(statusCode).json({ + error: message, + ...(process.env.NODE_ENV === 'development' && { stack: error.stack }) + }); +}; + +export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; \ No newline at end of file diff --git a/templates/production-bkend/src/middleware/logger.ts b/templates/production-bkend/src/middleware/logger.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/middleware/ratelimit.ts b/templates/production-bkend/src/middleware/ratelimit.ts new file mode 100644 index 0000000..9a088e8 --- /dev/null +++ b/templates/production-bkend/src/middleware/ratelimit.ts @@ -0,0 +1,37 @@ +import rateLimit from 'express-rate-limit'; +import { config } from '../config'; +import { logger } from '../utils/logger'; + +export const globalRateLimiter = rateLimit({ + windowMs: config.rateLimit.windowMs, + max: config.rateLimit.max, + message: { + error: 'Too many requests from this IP, please try again later.' + }, + handler: (req, res) => { + logger.warn('Rate limit exceeded', { + ip: req.ip, + url: req.url + }); + res.status(429).json({ + error: 'Too many requests from this IP, please try again later.' + }); + } +}); + +export const authRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: config.rateLimit.loginMax, + message: { + error: 'Too many login attempts, please try again later.' + }, + handler: (req, res) => { + logger.warn('Auth rate limit exceeded', { + ip: req.ip, + url: req.url + }); + res.status(429).json({ + error: 'Too many login attempts, please try again later.' + }); + } +}); \ No newline at end of file diff --git a/templates/production-bkend/src/middleware/requestlogger.ts b/templates/production-bkend/src/middleware/requestlogger.ts new file mode 100644 index 0000000..09e4ce4 --- /dev/null +++ b/templates/production-bkend/src/middleware/requestlogger.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '../utils/logger'; + +export const requestLogger = (req: Request, res: Response, next: NextFunction) => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + + const logData = { + method: req.method, + url: req.url, + statusCode: res.statusCode, + responseTime: `${duration}ms`, + ip: req.ip, + userAgent: req.get('User-Agent') + }; + + if (res.statusCode >= 400) { + logger.error('Request completed with error', logData); + } else { + logger.info('Request completed', logData); + } + }); + + next(); +}; \ No newline at end of file diff --git a/templates/production-bkend/src/routes/auth.ts b/templates/production-bkend/src/routes/auth.ts new file mode 100644 index 0000000..c42b38f --- /dev/null +++ b/templates/production-bkend/src/routes/auth.ts @@ -0,0 +1,72 @@ +import express from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { config } from '../config'; +import { authenticate, authorize, asyncHandler } from '../middleware/auth'; +import { authRateLimiter } from '../middleware/rateLimit'; +import { AuthRequest } from '../middleware/auth'; +import { logger } from '../utils/logger'; + +const router = express.Router(); + +// Mock user database +const users = [ + { + id: '1', + email: 'admin@example.com', + password: bcrypt.hashSync('admin123', 10), + roles: ['admin'], + permissions: ['read', 'write', 'delete'], + isActive: true + } +]; + +router.post('/login', authRateLimiter, asyncHandler(async (req: AuthRequest, res) => { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password are required' }); + } + + const user = users.find(u => u.email === email && u.isActive); + if (!user || !bcrypt.compareSync(password, user.password)) { + logger.warn('Failed login attempt', { email }); + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const token = jwt.sign( + { + id: user.id, + email: user.email, + roles: user.roles + }, + config.jwt.secret, + { expiresIn: config.jwt.expiresIn } + ); + + logger.info('User logged in successfully', { userId: user.id }); + + res.json({ + token, + user: { + id: user.id, + email: user.email, + roles: user.roles + } + }); +})); + +router.get('/profile', authenticate, asyncHandler(async (req: AuthRequest, res) => { + res.json({ + user: req.user + }); +})); + +router.get('/admin', authenticate, authorize('admin'), asyncHandler(async (req: AuthRequest, res) => { + res.json({ + message: 'Welcome admin!', + users: users.map(u => ({ id: u.id, email: u.email, roles: u.roles })) + }); +})); + +export default router; \ No newline at end of file diff --git a/templates/production-bkend/src/routes/files.ts b/templates/production-bkend/src/routes/files.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/routes/index.ts b/templates/production-bkend/src/routes/index.ts new file mode 100644 index 0000000..249cf4f --- /dev/null +++ b/templates/production-bkend/src/routes/index.ts @@ -0,0 +1,12 @@ +import express from 'express'; +import authRoutes from './auth'; +import fileRoutes from './files'; +import monitoringRoutes from './monitoring'; + +const router = express.Router(); + +router.use('/auth', authRoutes); +router.use('/files', fileRoutes); +router.use('/monitoring', monitoringRoutes); + +export default router; \ No newline at end of file diff --git a/templates/production-bkend/src/routes/monitoring.ts b/templates/production-bkend/src/routes/monitoring.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/server.ts b/templates/production-bkend/src/server.ts new file mode 100644 index 0000000..350ca7b --- /dev/null +++ b/templates/production-bkend/src/server.ts @@ -0,0 +1,31 @@ +import app from './app'; +import { config } from './config'; +import { logger } from './utils/logger'; + +// Handle uncaught exceptions +process.on('uncaughtException', (error: Error) => { + logger.error('UNCAUGHT EXCEPTION! 💥 Shutting down...', error); + process.exit(1); +}); + +// Handle unhandled promise rejections +process.on('unhandledRejection', (reason: any, promise: Promise) => { + logger.error('UNHANDLED REJECTION! 💥 Shutting down...', { reason, promise }); + process.exit(1); +}); + +// Start server +const server = app.listen(config.port, () => { + logger.info(`🚀 Server running on port ${config.port}`); + logger.info(`🌍 Environment: ${config.nodeEnv}`); + logger.info(`📊 Health check: http://localhost:${config.port}/health`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + logger.info('👋 SIGTERM received. Shutting down gracefully...'); + server.close(() => { + logger.info('💥 Process terminated!'); + process.exit(0); + }); +}); \ No newline at end of file diff --git a/templates/production-bkend/src/services/auth.ts b/templates/production-bkend/src/services/auth.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/monitoring.ts b/templates/production-bkend/src/services/monitoring.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/notification/discord.ts b/templates/production-bkend/src/services/notification/discord.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/notification/email.ts b/templates/production-bkend/src/services/notification/email.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/notification/slack.ts b/templates/production-bkend/src/services/notification/slack.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/notification/sms.ts b/templates/production-bkend/src/services/notification/sms.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/notification/telegram.ts b/templates/production-bkend/src/services/notification/telegram.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/services/notification/whatsapp.ts b/templates/production-bkend/src/services/notification/whatsapp.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/types/index.ts b/templates/production-bkend/src/types/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/utils/filesystem.ts b/templates/production-bkend/src/utils/filesystem.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/src/utils/logger.ts b/templates/production-bkend/src/utils/logger.ts new file mode 100644 index 0000000..2494d8e --- /dev/null +++ b/templates/production-bkend/src/utils/logger.ts @@ -0,0 +1,81 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +class Logger { + private logDir: string; + + constructor() { + this.logDir = path.join(process.cwd(), 'src', 'log'); + this.ensureLogDirectory(); + } + + private ensureLogDirectory() { + if (!fs.existsSync(this.logDir)) { + fs.mkdirSync(this.logDir, { recursive: true }); + } + } + + private getLogFileName(level: string): string { + const date = new Date().toISOString().split('T')[0]; + return path.join(this.logDir, `${level}-${date}.log`); + } + + private writeToFile(level: string, message: string, meta?: any) { + const logEntry = { + timestamp: new Date().toISOString(), + level, + message, + meta: meta || {} + }; + + const logString = JSON.stringify(logEntry) + '\n'; + + try { + fs.appendFileSync(this.getLogFileName(level), logString); + } catch (error) { + console.error('Failed to write log to file:', error); + } + } + + private formatConsoleMessage(level: string, message: string, meta?: any): string { + const timestamp = new Date().toISOString(); + let coloredMessage = message; + + switch (level) { + case 'error': + coloredMessage = chalk.red(`[ERROR] ${message}`); + break; + case 'warn': + coloredMessage = chalk.yellow(`[WARN] ${message}`); + break; + case 'info': + coloredMessage = chalk.blue(`[INFO] ${message}`); + break; + default: + coloredMessage = chalk.white(`[${level.toUpperCase()}] ${message}`); + } + + return `[${timestamp}] ${coloredMessage}${meta ? ' ' + JSON.stringify(meta) : ''}`; + } + + info(message: string, meta?: any) { + const consoleMessage = this.formatConsoleMessage('info', message, meta); + console.log(consoleMessage); + this.writeToFile('success', message, meta); + } + + warn(message: string, meta?: any) { + const consoleMessage = this.formatConsoleMessage('warn', message, meta); + console.warn(consoleMessage); + this.writeToFile('warning', message, meta); + } + + error(message: string, meta?: any) { + const consoleMessage = this.formatConsoleMessage('error', message, meta); + console.error(consoleMessage); + this.writeToFile('error', message, meta); + } +} + +export const logger = new Logger(); \ No newline at end of file diff --git a/templates/production-bkend/src/utils/notification.ts b/templates/production-bkend/src/utils/notification.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/production-bkend/tsconfig.json b/templates/production-bkend/tsconfig.json new file mode 100644 index 0000000..c241585 --- /dev/null +++ b/templates/production-bkend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "allowJs": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file