bro-framework 2.2.0 → 2.2.2
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 +190 -190
- package/bin/bro.js +254 -260
- package/package.json +88 -88
- package/src/auth.js +33 -33
- package/src/index.d.ts +66 -66
- package/src/logger.js +79 -66
- package/src/router.js +187 -172
- package/src/sdk.js +160 -156
- package/src/server.js +251 -243
- package/src/tasks.js +54 -49
package/src/router.js
CHANGED
|
@@ -1,172 +1,187 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { pathToFileURL } from 'url';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Recursively scans a directory for .js files.
|
|
9
|
-
* @param {string} dir - The base directory to scan.
|
|
10
|
-
* @param {string[]} [fileList] - Internal accumulator for recursion.
|
|
11
|
-
* @returns {string[]} Array of absolute file paths.
|
|
12
|
-
*/
|
|
13
|
-
export function scanDir(dir, fileList = []) {
|
|
14
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
15
|
-
|
|
16
|
-
const files = fs.readdirSync(dir);
|
|
17
|
-
|
|
18
|
-
for (const file of files) {
|
|
19
|
-
const filePath = path.join(dir, file);
|
|
20
|
-
if (fs.statSync(filePath).isDirectory()) {
|
|
21
|
-
scanDir(filePath, fileList);
|
|
22
|
-
} else if (filePath.endsWith('.js') || filePath.endsWith('.ts')) {
|
|
23
|
-
fileList.push(filePath);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return fileList;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Converts a file path to an Express route path.
|
|
32
|
-
* Example: routes/users/[id].get.js -> { routePath: '/users/:id', method: 'get' }
|
|
33
|
-
* @param {string} filePath - Absolute path to the route file.
|
|
34
|
-
* @param {string} routesDir - The root routes directory.
|
|
35
|
-
* @returns {{ routePath: string, method: string } | null}
|
|
36
|
-
*/
|
|
37
|
-
export function parseRouteFile(filePath, routesDir) {
|
|
38
|
-
const relativePath = path.relative(routesDir, filePath);
|
|
39
|
-
|
|
40
|
-
const parsed = path.parse(relativePath);
|
|
41
|
-
const parts = parsed.name.split('.');
|
|
42
|
-
|
|
43
|
-
if (parts.length < 2) return null;
|
|
44
|
-
|
|
45
|
-
const method = parts.pop().toLowerCase();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
routePath
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Recursively scans a directory for .js files.
|
|
9
|
+
* @param {string} dir - The base directory to scan.
|
|
10
|
+
* @param {string[]} [fileList] - Internal accumulator for recursion.
|
|
11
|
+
* @returns {string[]} Array of absolute file paths.
|
|
12
|
+
*/
|
|
13
|
+
export function scanDir(dir, fileList = []) {
|
|
14
|
+
if (!fs.existsSync(dir)) return fileList;
|
|
15
|
+
|
|
16
|
+
const files = fs.readdirSync(dir);
|
|
17
|
+
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const filePath = path.join(dir, file);
|
|
20
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
21
|
+
scanDir(filePath, fileList);
|
|
22
|
+
} else if (filePath.endsWith('.js') || filePath.endsWith('.ts')) {
|
|
23
|
+
fileList.push(filePath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return fileList;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Converts a file path to an Express route path.
|
|
32
|
+
* Example: routes/users/[id].get.js -> { routePath: '/users/:id', method: 'get' }
|
|
33
|
+
* @param {string} filePath - Absolute path to the route file.
|
|
34
|
+
* @param {string} routesDir - The root routes directory.
|
|
35
|
+
* @returns {{ routePath: string, method: string } | null}
|
|
36
|
+
*/
|
|
37
|
+
export function parseRouteFile(filePath, routesDir) {
|
|
38
|
+
const relativePath = path.relative(routesDir, filePath);
|
|
39
|
+
|
|
40
|
+
const parsed = path.parse(relativePath);
|
|
41
|
+
const parts = parsed.name.split('.');
|
|
42
|
+
|
|
43
|
+
if (parts.length < 2) return null;
|
|
44
|
+
|
|
45
|
+
const method = parts.pop().toLowerCase();
|
|
46
|
+
|
|
47
|
+
const allowedMethods = new Set(['get', 'post', 'put', 'delete', 'patch', 'options', 'head']);
|
|
48
|
+
if (!allowedMethods.has(method)) {
|
|
49
|
+
throw new Error(`Invalid HTTP method "${method}" in file: ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const namePart = parts.join('.');
|
|
53
|
+
|
|
54
|
+
let routePath = '/' + path.dirname(relativePath).replace(/\\/g, '/');
|
|
55
|
+
if (routePath === '/.') routePath = '';
|
|
56
|
+
|
|
57
|
+
if (namePart !== 'index') {
|
|
58
|
+
routePath += `/${namePart}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bracketRegex = /\[(.*?)\]/g;
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = bracketRegex.exec(routePath)) !== null) {
|
|
64
|
+
const paramName = match[1];
|
|
65
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
66
|
+
throw new Error(`Invalid dynamic parameter "[${paramName}]" in file: ${filePath}. Must be a valid JavaScript identifier.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
routePath = routePath.replace(/\[(.*?)\]/g, ':$1');
|
|
71
|
+
|
|
72
|
+
if (routePath === '') routePath = '/';
|
|
73
|
+
|
|
74
|
+
return { routePath, method };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Loads and maps all route files into the Express application.
|
|
79
|
+
* @param {import('express').Application} app - The Express app instance.
|
|
80
|
+
* @param {string} routesDir - Path to the user's routes folder.
|
|
81
|
+
* @param {Function} createHandler - Core wrapper function for route logic.
|
|
82
|
+
* @param {Object} [openApiSpec] - Optional OpenAPI Spec object to build.
|
|
83
|
+
* @returns {Promise<Array>} Array of loaded route objects.
|
|
84
|
+
*/
|
|
85
|
+
export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
86
|
+
const files = scanDir(routesDir);
|
|
87
|
+
const loadedRoutes = [];
|
|
88
|
+
const routeModules = [];
|
|
89
|
+
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const routeInfo = parseRouteFile(file, routesDir);
|
|
92
|
+
if (!routeInfo) continue;
|
|
93
|
+
|
|
94
|
+
const { routePath, method } = routeInfo;
|
|
95
|
+
|
|
96
|
+
if (typeof app[method] !== 'function') continue;
|
|
97
|
+
|
|
98
|
+
const moduleUrl = pathToFileURL(file).href + '?update=' + crypto.randomUUID();
|
|
99
|
+
const module = await import(moduleUrl);
|
|
100
|
+
const config = module.default;
|
|
101
|
+
|
|
102
|
+
if (!config) continue;
|
|
103
|
+
|
|
104
|
+
routeModules.push({ file, routePath, method, config });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const { file, routePath, method, config } of routeModules) {
|
|
108
|
+
const handler = createHandler(config);
|
|
109
|
+
app[method](routePath, handler);
|
|
110
|
+
|
|
111
|
+
if (openApiSpec) {
|
|
112
|
+
const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
|
|
113
|
+
if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
|
|
114
|
+
|
|
115
|
+
const operation = {
|
|
116
|
+
summary: config.summary || `${method.toUpperCase()} ${routePath}`,
|
|
117
|
+
responses: { '200': { description: 'Successful response' } }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const bodySchema = config.schema?.body || config.body;
|
|
121
|
+
const paramsSchema = config.schema?.params || config.params;
|
|
122
|
+
const querySchema = config.schema?.query || config.query;
|
|
123
|
+
|
|
124
|
+
if (bodySchema) {
|
|
125
|
+
operation.requestBody = {
|
|
126
|
+
content: { 'application/json': { schema: zodToJsonSchema(bodySchema) } }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (paramsSchema) {
|
|
131
|
+
operation.parameters = operation.parameters || [];
|
|
132
|
+
const pSchema = zodToJsonSchema(paramsSchema);
|
|
133
|
+
if (pSchema.properties) {
|
|
134
|
+
for (const [key, schema] of Object.entries(pSchema.properties)) {
|
|
135
|
+
operation.parameters.push({ name: key, in: 'path', required: true, schema });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (querySchema) {
|
|
141
|
+
operation.parameters = operation.parameters || [];
|
|
142
|
+
const qSchema = zodToJsonSchema(querySchema);
|
|
143
|
+
if (qSchema.properties) {
|
|
144
|
+
for (const [key, schema] of Object.entries(qSchema.properties)) {
|
|
145
|
+
operation.parameters.push({
|
|
146
|
+
name: key,
|
|
147
|
+
in: 'query',
|
|
148
|
+
required: qSchema.required?.includes(key),
|
|
149
|
+
schema
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Auto-inject security definition if auth is true
|
|
156
|
+
if (config.auth) {
|
|
157
|
+
operation.security = [{ bearerAuth: [] }];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (config.upload) {
|
|
161
|
+
operation.requestBody = operation.requestBody || { content: {} };
|
|
162
|
+
operation.requestBody.content['multipart/form-data'] = {
|
|
163
|
+
schema: { type: 'object' }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (bodySchema || paramsSchema || querySchema) {
|
|
168
|
+
operation.responses['400'] = { $ref: '#/components/responses/BadRequest' };
|
|
169
|
+
}
|
|
170
|
+
if (config.auth) {
|
|
171
|
+
operation.responses['401'] = { $ref: '#/components/responses/Unauthorized' };
|
|
172
|
+
}
|
|
173
|
+
operation.responses['404'] = { $ref: '#/components/responses/NotFound' };
|
|
174
|
+
operation.responses['500'] = { $ref: '#/components/responses/ServerError' };
|
|
175
|
+
|
|
176
|
+
openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
loadedRoutes.push({
|
|
180
|
+
method: method.toUpperCase(),
|
|
181
|
+
path: routePath,
|
|
182
|
+
auth: !!config.auth
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return loadedRoutes;
|
|
187
|
+
}
|