bro-framework 2.0.0 → 2.1.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 +15 -0
- package/bin/bro.js +17 -10
- package/package.json +1 -1
- package/src/auth.js +4 -12
- package/src/logger.js +22 -10
- package/src/router.js +63 -52
- package/src/sdk.js +7 -4
- package/src/server.js +43 -11
- package/src/tasks.js +14 -4
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
## Table of Contents
|
|
27
27
|
|
|
28
|
+
- [Getting Started](#getting-started)
|
|
28
29
|
- [The Core Experience](#the-core-experience)
|
|
29
30
|
- [Deep-Dive Features](#deep-dive-features)
|
|
30
31
|
- [Architecture & Request Lifecycle](#architecture--request-lifecycle)
|
|
@@ -34,6 +35,20 @@
|
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
38
|
+
## Getting Started
|
|
39
|
+
|
|
40
|
+
Bootstrapping a new `bro.js` project is incredibly simple. We recommend using our official scaffolding tool to set everything up instantly (with your choice of JavaScript or TypeScript):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx create-bro-framework@latest my-api
|
|
44
|
+
cd my-api
|
|
45
|
+
npm run dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That's it! Your zero-boilerplate backend is now running with hot-reloading enabled.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
37
52
|
## The Core Experience
|
|
38
53
|
|
|
39
54
|
In `bro.js`, everything you need is handed to you instantly. No setup, no middleware wrangling, no manual `req/res` handling. You define your route, set your validation, and return an object.
|
package/bin/bro.js
CHANGED
|
@@ -48,6 +48,13 @@ export default defineConfig({
|
|
|
48
48
|
max: 100 // limit each IP to 100 requests per windowMs
|
|
49
49
|
},
|
|
50
50
|
|
|
51
|
+
// WebSockets Setup
|
|
52
|
+
sockets: async (io, db) => {
|
|
53
|
+
io.on('connection', (socket) => {
|
|
54
|
+
console.log('Client connected:', socket.id);
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
|
|
51
58
|
// Database Context Injection
|
|
52
59
|
// This instance will be injected into every route's ctx.db (if defined)
|
|
53
60
|
db: async () => {
|
|
@@ -67,13 +74,6 @@ export default defineConfig({
|
|
|
67
74
|
// return mongoose.connection;
|
|
68
75
|
// --------------------------
|
|
69
76
|
return null;
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
// WebSockets Setup
|
|
73
|
-
sockets: async (io, db) => {
|
|
74
|
-
io.on('connection', (socket) => {
|
|
75
|
-
console.log('Client connected:', socket.id);
|
|
76
|
-
});
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
`;
|
|
@@ -122,10 +122,10 @@ if (command === 'init') {
|
|
|
122
122
|
|
|
123
123
|
if (['sdk', 'generate-client', 'client'].includes(command)) {
|
|
124
124
|
generateSDK().then(() => {
|
|
125
|
-
console.log(`\n ${colors.green}
|
|
125
|
+
console.log(`\n ${colors.green} bro-client.js generated successfully!${colors.reset}\n`);
|
|
126
126
|
process.exit(0);
|
|
127
127
|
}).catch(err => {
|
|
128
|
-
console.error(`\n ${colors.red}
|
|
128
|
+
console.error(`\n ${colors.red} Error generating SDK:${colors.reset}`, err.message);
|
|
129
129
|
process.exit(1);
|
|
130
130
|
});
|
|
131
131
|
}
|
|
@@ -175,13 +175,20 @@ async function bootstrap() {
|
|
|
175
175
|
if (globalConfig.env) {
|
|
176
176
|
const envResult = globalConfig.env.safeParse(process.env);
|
|
177
177
|
if (!envResult.success) {
|
|
178
|
-
console.error(`\n ${colors.red}
|
|
178
|
+
console.error(`\n ${colors.red} Environment Validation Failed${colors.reset}`);
|
|
179
179
|
envResult.error.errors.forEach(err => {
|
|
180
180
|
console.error(` ${colors.dim}-${colors.reset} ${colors.bold}${err.path.join('.')}${colors.reset}: ${err.message}`);
|
|
181
181
|
});
|
|
182
182
|
console.error("");
|
|
183
183
|
process.exit(1);
|
|
184
184
|
}
|
|
185
|
+
globalConfig.envData = envResult.data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
|
|
189
|
+
console.error(`\n ✗ CRITICAL SECURITY ERROR: You are running in production with a default JWT secret!`);
|
|
190
|
+
console.error(` Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.`);
|
|
191
|
+
process.exit(1);
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
if (!fs.existsSync(routesDir)) {
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
|
|
3
|
-
let secret = 'bro_default_secret_key';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Update the secret used for signing and verifying JWTs.
|
|
7
|
-
* @param {string} newSecret
|
|
8
|
-
*/
|
|
9
|
-
export function setJwtSecret(newSecret) {
|
|
10
|
-
secret = newSecret;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
/**
|
|
14
4
|
* Signs a JWT payload.
|
|
15
5
|
* @param {Object} payload - The data to embed in the token.
|
|
6
|
+
* @param {string} secret - The JWT secret key.
|
|
16
7
|
* @param {jwt.SignOptions} [options] - jsonwebtoken sign options.
|
|
17
8
|
* @returns {string} The signed JWT token.
|
|
18
9
|
*/
|
|
19
|
-
export function signJwt(payload, options = { expiresIn: '1d' }) {
|
|
10
|
+
export function signJwt(payload, secret, options = { expiresIn: '1d' }) {
|
|
20
11
|
return jwt.sign(payload, secret, options);
|
|
21
12
|
}
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
15
|
* Verifies and decodes a JWT token.
|
|
25
16
|
* @param {string} token - The JWT token to verify.
|
|
17
|
+
* @param {string} secret - The JWT secret key.
|
|
26
18
|
* @returns {{ valid: boolean, payload?: any, error?: string }}
|
|
27
19
|
*/
|
|
28
|
-
export function verifyJwt(token) {
|
|
20
|
+
export function verifyJwt(token, secret) {
|
|
29
21
|
try {
|
|
30
22
|
const payload = jwt.verify(token, secret);
|
|
31
23
|
return { valid: true, payload };
|
package/src/logger.js
CHANGED
|
@@ -25,17 +25,29 @@ export function formatMethod(method) {
|
|
|
25
25
|
|
|
26
26
|
export function printBanner(port, durationMs) {
|
|
27
27
|
const time = durationMs.toFixed(0);
|
|
28
|
-
const version = "
|
|
29
|
-
|
|
28
|
+
const version = "2.0.1";
|
|
29
|
+
const innerWidth = 47;
|
|
30
|
+
|
|
31
|
+
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
32
|
+
|
|
33
|
+
const formatLine = (content = "") => {
|
|
34
|
+
const visibleLength = stripAnsi(content).length;
|
|
35
|
+
const padding = Math.max(0, innerWidth - visibleLength);
|
|
36
|
+
return `${colors.green}│${colors.reset}${content}${" ".repeat(padding)}${colors.green}│${colors.reset}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const borderTop = `${colors.green}╭${"─".repeat(innerWidth)}╮${colors.reset}`;
|
|
40
|
+
const borderBottom = `${colors.green}╰${"─".repeat(innerWidth)}╯${colors.reset}`;
|
|
41
|
+
|
|
30
42
|
console.log("");
|
|
31
|
-
console.log(
|
|
32
|
-
console.log(
|
|
33
|
-
console.log(
|
|
34
|
-
console.log(
|
|
35
|
-
console.log(
|
|
36
|
-
console.log(
|
|
37
|
-
console.log(
|
|
38
|
-
console.log(
|
|
43
|
+
console.log(borderTop);
|
|
44
|
+
console.log(formatLine());
|
|
45
|
+
console.log(formatLine(` ${colors.bold}bro.js${colors.reset} v${version}`));
|
|
46
|
+
console.log(formatLine());
|
|
47
|
+
console.log(formatLine(` ➜ ${colors.bold}Local:${colors.reset} ${colors.cyan}http://localhost:${port}${colors.reset}`));
|
|
48
|
+
console.log(formatLine(` ➜ ${colors.bold}Ready in:${colors.reset} ${colors.yellow}${time}ms${colors.reset}`));
|
|
49
|
+
console.log(formatLine());
|
|
50
|
+
console.log(borderBottom);
|
|
39
51
|
console.log("");
|
|
40
52
|
}
|
|
41
53
|
|
package/src/router.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
+
import crypto from 'crypto';
|
|
4
5
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -48,10 +49,11 @@ export function parseRouteFile(filePath, routesDir) {
|
|
|
48
49
|
if (routePath === '/.') routePath = '';
|
|
49
50
|
|
|
50
51
|
if (namePart !== 'index') {
|
|
51
|
-
|
|
52
|
-
routePath += `/${formattedName}`;
|
|
52
|
+
routePath += `/${namePart}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
routePath = routePath.replace(/\[(.*?)\]/g, ':$1');
|
|
56
|
+
|
|
55
57
|
if (routePath === '') routePath = '/';
|
|
56
58
|
|
|
57
59
|
return { routePath, method };
|
|
@@ -68,6 +70,7 @@ export function parseRouteFile(filePath, routesDir) {
|
|
|
68
70
|
export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
69
71
|
const files = scanDir(routesDir);
|
|
70
72
|
const loadedRoutes = [];
|
|
73
|
+
const routeModules = [];
|
|
71
74
|
|
|
72
75
|
for (const file of files) {
|
|
73
76
|
const routeInfo = parseRouteFile(file, routesDir);
|
|
@@ -77,68 +80,76 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
|
77
80
|
|
|
78
81
|
if (typeof app[method] !== 'function') continue;
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const moduleUrl = pathToFileURL(file).href + '?update=' + crypto.randomUUID();
|
|
84
|
+
const module = await import(moduleUrl);
|
|
85
|
+
const config = module.default;
|
|
86
|
+
|
|
87
|
+
if (!config) continue;
|
|
88
|
+
|
|
89
|
+
routeModules.push({ file, routePath, method, config });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const { file, routePath, method, config } of routeModules) {
|
|
93
|
+
const handler = createHandler(config);
|
|
94
|
+
app[method](routePath, handler);
|
|
95
|
+
|
|
96
|
+
if (openApiSpec) {
|
|
97
|
+
const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
|
|
98
|
+
if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
|
|
86
99
|
|
|
87
|
-
const
|
|
88
|
-
|
|
100
|
+
const operation = {
|
|
101
|
+
summary: config.summary || `${method.toUpperCase()} ${routePath}`,
|
|
102
|
+
responses: { '200': { description: 'Successful response' } }
|
|
103
|
+
};
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
const bodySchema = config.schema?.body || config.body;
|
|
106
|
+
const paramsSchema = config.schema?.params || config.params;
|
|
107
|
+
const querySchema = config.schema?.query || config.query;
|
|
108
|
+
|
|
109
|
+
if (bodySchema) {
|
|
110
|
+
operation.requestBody = {
|
|
111
|
+
content: { 'application/json': { schema: zodToJsonSchema(bodySchema) } }
|
|
97
112
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
operation.parameters = operation.parameters || [];
|
|
107
|
-
const pSchema = zodToJsonSchema(config.params);
|
|
108
|
-
if (pSchema.properties) {
|
|
109
|
-
for (const [key, schema] of Object.entries(pSchema.properties)) {
|
|
110
|
-
operation.parameters.push({ name: key, in: 'path', required: true, schema });
|
|
111
|
-
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (paramsSchema) {
|
|
116
|
+
operation.parameters = operation.parameters || [];
|
|
117
|
+
const pSchema = zodToJsonSchema(paramsSchema);
|
|
118
|
+
if (pSchema.properties) {
|
|
119
|
+
for (const [key, schema] of Object.entries(pSchema.properties)) {
|
|
120
|
+
operation.parameters.push({ name: key, in: 'path', required: true, schema });
|
|
112
121
|
}
|
|
113
122
|
}
|
|
123
|
+
}
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
125
|
+
if (querySchema) {
|
|
126
|
+
operation.parameters = operation.parameters || [];
|
|
127
|
+
const qSchema = zodToJsonSchema(querySchema);
|
|
128
|
+
if (qSchema.properties) {
|
|
129
|
+
for (const [key, schema] of Object.entries(qSchema.properties)) {
|
|
130
|
+
operation.parameters.push({
|
|
131
|
+
name: key,
|
|
132
|
+
in: 'query',
|
|
133
|
+
required: qSchema.required?.includes(key),
|
|
134
|
+
schema
|
|
135
|
+
});
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
|
-
|
|
130
|
-
openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
140
|
+
// Auto-inject security definition if auth is true
|
|
141
|
+
if (config.auth) {
|
|
142
|
+
operation.security = [{ bearerAuth: [] }];
|
|
143
|
+
}
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
console.error(`[bro.js] Failed to load route ${file}:`, err);
|
|
145
|
+
openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
|
|
141
146
|
}
|
|
147
|
+
|
|
148
|
+
loadedRoutes.push({
|
|
149
|
+
method: method.toUpperCase(),
|
|
150
|
+
path: routePath,
|
|
151
|
+
auth: !!config.auth
|
|
152
|
+
});
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
return loadedRoutes;
|
package/src/sdk.js
CHANGED
|
@@ -44,7 +44,10 @@ async function request(method, path, data) {
|
|
|
44
44
|
options.body = JSON.stringify(data);
|
|
45
45
|
} else if (data && ['GET', 'DELETE'].includes(method.toUpperCase())) {
|
|
46
46
|
const params = new URLSearchParams(data);
|
|
47
|
-
|
|
47
|
+
const qs = params.toString();
|
|
48
|
+
if (qs) {
|
|
49
|
+
path += '?' + qs;
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
const url = CONFIG.baseURL + path;
|
|
@@ -115,10 +118,10 @@ function generateApiObject(endpoints) {
|
|
|
115
118
|
|
|
116
119
|
for (const [key, val] of Object.entries(node)) {
|
|
117
120
|
if (val._isParam) {
|
|
118
|
-
result += `${indent}${key}: (${key}) => ({\n`;
|
|
121
|
+
result += `${indent}"${key}": (${key}) => ({\n`;
|
|
119
122
|
|
|
120
123
|
for (const [m, p] of Object.entries(val._methods)) {
|
|
121
|
-
const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${$1}');
|
|
124
|
+
const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${encodeURIComponent($1)}');
|
|
122
125
|
result += `${indent} ${m}: (data) => request('${m}', \`${templatedPath}\`, data),\n`;
|
|
123
126
|
}
|
|
124
127
|
|
|
@@ -129,7 +132,7 @@ function generateApiObject(endpoints) {
|
|
|
129
132
|
|
|
130
133
|
result += `${indent}}),\n`;
|
|
131
134
|
} else {
|
|
132
|
-
result += `${indent}${key}: {\n`;
|
|
135
|
+
result += `${indent}"${key}": {\n`;
|
|
133
136
|
for (const [m, p] of Object.entries(val._methods)) {
|
|
134
137
|
result += `${indent} ${m}: (data) => request('${m}', '${p}', data),\n`;
|
|
135
138
|
}
|
package/src/server.js
CHANGED
|
@@ -5,11 +5,9 @@ import { Server } from 'socket.io';
|
|
|
5
5
|
import rateLimit from 'express-rate-limit';
|
|
6
6
|
import multer from 'multer';
|
|
7
7
|
import { apiReference } from '@scalar/express-api-reference';
|
|
8
|
-
import { verifyJwt, signJwt
|
|
8
|
+
import { verifyJwt, signJwt } from './auth.js';
|
|
9
9
|
import { loadRoutes } from './router.js';
|
|
10
10
|
|
|
11
|
-
const upload = multer();
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
12
|
* Creates and configures the core Express server.
|
|
15
13
|
* @param {Object} globalConfig - User's bro.config.js configurations.
|
|
@@ -23,7 +21,10 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
23
21
|
|
|
24
22
|
const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
if (corsConfig !== false) {
|
|
25
|
+
app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
|
|
26
|
+
}
|
|
27
|
+
|
|
27
28
|
app.use(express.json());
|
|
28
29
|
|
|
29
30
|
if (globalConfig.rateLimit) {
|
|
@@ -36,10 +37,6 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
36
37
|
await globalConfig.sockets(io, db);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
if (globalConfig.jwtSecret) {
|
|
40
|
-
setJwtSecret(globalConfig.jwtSecret);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
40
|
const createHandler = (routeConfig) => {
|
|
44
41
|
const middlewares = [];
|
|
45
42
|
|
|
@@ -48,12 +45,23 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
if (routeConfig.upload) {
|
|
51
|
-
|
|
48
|
+
const routeMulterConfig = {
|
|
49
|
+
limits: globalConfig.upload?.limits || {
|
|
50
|
+
fileSize: 10 * 1024 * 1024,
|
|
51
|
+
files: 5,
|
|
52
|
+
fields: 20
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
if (typeof routeConfig.upload === 'object' && routeConfig.upload.limits) {
|
|
56
|
+
routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
|
|
57
|
+
}
|
|
58
|
+
middlewares.push(multer(routeMulterConfig).any());
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
middlewares.push(async (req, res) => {
|
|
55
62
|
try {
|
|
56
63
|
const ctx = {
|
|
64
|
+
env: globalConfig.envData || process.env,
|
|
57
65
|
db,
|
|
58
66
|
io,
|
|
59
67
|
body: req.body,
|
|
@@ -61,7 +69,7 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
61
69
|
query: req.query,
|
|
62
70
|
files: req.files || req.file,
|
|
63
71
|
user: null,
|
|
64
|
-
jwt: { sign: signJwt },
|
|
72
|
+
jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
|
|
65
73
|
error: (status, message) => {
|
|
66
74
|
const err = new Error(message);
|
|
67
75
|
err.status = status;
|
|
@@ -76,7 +84,7 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
const token = authHeader.split(' ')[1];
|
|
79
|
-
const authResult = verifyJwt(token);
|
|
87
|
+
const authResult = verifyJwt(token, globalConfig.jwtSecret);
|
|
80
88
|
|
|
81
89
|
if (!authResult.valid) {
|
|
82
90
|
return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
|
|
@@ -143,6 +151,21 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
143
151
|
let openApiSpec = {
|
|
144
152
|
openapi: '3.0.0',
|
|
145
153
|
info: { title: 'bro.js API', version: '1.0.0' },
|
|
154
|
+
components: {
|
|
155
|
+
securitySchemes: {
|
|
156
|
+
bearerAuth: {
|
|
157
|
+
type: 'http',
|
|
158
|
+
scheme: 'bearer',
|
|
159
|
+
bearerFormat: 'JWT'
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
responses: {
|
|
163
|
+
BadRequest: { description: 'Bad Request', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
|
|
164
|
+
Unauthorized: { description: 'Unauthorized', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
|
|
165
|
+
NotFound: { description: 'Not Found', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
|
|
166
|
+
ServerError: { description: 'Internal Server Error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
|
|
167
|
+
}
|
|
168
|
+
},
|
|
146
169
|
paths: {}
|
|
147
170
|
};
|
|
148
171
|
|
|
@@ -174,6 +197,15 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
174
197
|
routeStack(req, res, next);
|
|
175
198
|
});
|
|
176
199
|
|
|
200
|
+
app.use((req, res) => {
|
|
201
|
+
res.status(404).json({ error: 'Not Found' });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
app.use((err, req, res, next) => {
|
|
205
|
+
console.error(`[bro.js] Uncaught Error:`, err);
|
|
206
|
+
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
|
|
207
|
+
});
|
|
208
|
+
|
|
177
209
|
const reload = async () => {
|
|
178
210
|
const newRouter = express.Router();
|
|
179
211
|
openApiSpec.paths = {};
|
package/src/tasks.js
CHANGED
|
@@ -3,29 +3,39 @@ import path from 'path';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import cron from 'node-cron';
|
|
5
5
|
import { colors } from './logger.js';
|
|
6
|
+
import { scanDir } from './router.js';
|
|
7
|
+
|
|
8
|
+
let taskHandles = [];
|
|
9
|
+
|
|
10
|
+
export function stopTasks() {
|
|
11
|
+
taskHandles.forEach(t => t.stop());
|
|
12
|
+
taskHandles = [];
|
|
13
|
+
}
|
|
6
14
|
|
|
7
15
|
export async function scanTasks(ctx) {
|
|
8
16
|
const tasksDir = path.join(process.cwd(), 'tasks');
|
|
9
17
|
if (!fs.existsSync(tasksDir)) return;
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
stopTasks();
|
|
20
|
+
|
|
21
|
+
const files = scanDir(tasksDir);
|
|
12
22
|
if (files.length === 0) return;
|
|
13
23
|
|
|
14
24
|
let count = 0;
|
|
15
25
|
for (const file of files) {
|
|
16
|
-
const filePath = path.join(tasksDir, file);
|
|
17
26
|
try {
|
|
18
|
-
const moduleUrl = pathToFileURL(
|
|
27
|
+
const moduleUrl = pathToFileURL(file).href;
|
|
19
28
|
const taskModule = await import(moduleUrl);
|
|
20
29
|
|
|
21
30
|
if (taskModule.cron && typeof taskModule.handler === 'function') {
|
|
22
|
-
cron.schedule(taskModule.cron, async () => {
|
|
31
|
+
const task = cron.schedule(taskModule.cron, async () => {
|
|
23
32
|
try {
|
|
24
33
|
await taskModule.handler(ctx);
|
|
25
34
|
} catch (err) {
|
|
26
35
|
console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
|
|
27
36
|
}
|
|
28
37
|
});
|
|
38
|
+
taskHandles.push(task);
|
|
29
39
|
count++;
|
|
30
40
|
}
|
|
31
41
|
} catch (err) {
|