bro-framework 2.4.5 → 3.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 +44 -0
- package/bin/bro.js +194 -292
- package/package.json +138 -103
- package/src/dashboard.js +80 -0
- package/src/database.js +105 -0
- package/src/edge.js +244 -0
- package/src/engine.js +245 -0
- package/src/index.d.ts +31 -11
- package/src/index.js +10 -0
- package/src/logger.js +47 -0
- package/src/next.d.ts +9 -9
- package/src/next.js +82 -156
- package/src/observability.js +81 -0
- package/src/plugins.js +135 -0
- package/src/policy-auth.js +88 -0
- package/src/router.js +1 -1
- package/src/sdk.js +229 -169
- package/src/server.js +181 -170
- package/src/studio.js +162 -0
- package/src/task-engine.js +88 -0
- package/src/testing.js +142 -0
- package/src/uploads.js +129 -0
package/src/sdk.js
CHANGED
|
@@ -1,169 +1,229 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { parseRouteFile, scanDir } from './router.js';
|
|
6
|
+
|
|
7
|
+
export async function generateSDK(routesDir = path.join(process.cwd(), 'routes'), outputPath = path.join(process.cwd(), 'bro-sdk.js')) {
|
|
8
|
+
if (!fs.existsSync(routesDir)) {
|
|
9
|
+
throw new Error(`Routes directory not found at ${routesDir}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const files = scanDir(routesDir);
|
|
13
|
+
const endpoints = [];
|
|
14
|
+
|
|
15
|
+
let routeCounter = 0;
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const routeInfo = parseRouteFile(file, routesDir);
|
|
18
|
+
if (routeInfo) {
|
|
19
|
+
const moduleUrl = pathToFileURL(file).href + '?update=' + crypto.randomUUID();
|
|
20
|
+
const mod = await import(moduleUrl);
|
|
21
|
+
const config = mod.default;
|
|
22
|
+
|
|
23
|
+
if (!config.params && !config.query && !config.body) {
|
|
24
|
+
console.warn(`\x1b[33m[bro.js] ⚠️ SDK Warning: Route ${routeInfo.method.toUpperCase()} ${routeInfo.routePath} has no schema validation (params/query/body). It will be untyped.\x1b[0m`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
endpoints.push({
|
|
28
|
+
...routeInfo,
|
|
29
|
+
file,
|
|
30
|
+
moduleName: `Route${routeCounter++}`
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const jsCode = `// Auto-generated by bro.js
|
|
36
|
+
const CONFIG = {
|
|
37
|
+
baseURL: 'http://localhost:5000',
|
|
38
|
+
tokenKey: 'bro_token',
|
|
39
|
+
locale: undefined
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
async function request(method, path, data) {
|
|
43
|
+
const headers = {};
|
|
44
|
+
|
|
45
|
+
if (CONFIG.locale) {
|
|
46
|
+
headers['Accept-Language'] = CONFIG.locale;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof localStorage !== 'undefined') {
|
|
50
|
+
const token = localStorage.getItem(CONFIG.tokenKey);
|
|
51
|
+
if (token) {
|
|
52
|
+
headers['Authorization'] = \`Bearer \${token}\`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const options = {
|
|
57
|
+
method: method.toUpperCase(),
|
|
58
|
+
headers
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (data && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
|
62
|
+
headers['Content-Type'] = 'application/json';
|
|
63
|
+
options.body = JSON.stringify(data);
|
|
64
|
+
} else if (data && ['GET', 'DELETE'].includes(method.toUpperCase())) {
|
|
65
|
+
const params = new URLSearchParams(data);
|
|
66
|
+
const qs = params.toString();
|
|
67
|
+
if (qs) {
|
|
68
|
+
path += '?' + qs;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const url = CONFIG.baseURL + path;
|
|
73
|
+
const response = await fetch(url, options);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
let errMessage = response.statusText;
|
|
77
|
+
try {
|
|
78
|
+
const errData = await response.json();
|
|
79
|
+
errMessage = errData.error || errData.message || errMessage;
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
throw new Error(\`HTTP \${response.status}: \${errMessage}\`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return response.json();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const api = {
|
|
88
|
+
${generateApiObject(endpoints, false)}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function setBaseURL(url) {
|
|
92
|
+
CONFIG.baseURL = url;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function setTokenKey(key) {
|
|
96
|
+
CONFIG.tokenKey = key;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function setLocale(locale) {
|
|
100
|
+
CONFIG.locale = locale;
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(path.join(process.cwd(), 'bro-sdk.js'), jsCode, 'utf-8');
|
|
105
|
+
|
|
106
|
+
let typeImports = `// Auto-generated by bro.js
|
|
107
|
+
import type { z } from 'zod';
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
for (const ep of endpoints) {
|
|
111
|
+
let relativePath = './' + path.relative(process.cwd(), ep.file).replace(/\\/g, '/');
|
|
112
|
+
if (relativePath.endsWith('.js')) {
|
|
113
|
+
// TS allows importing .js directly if allowJs is true, or you can omit the extension
|
|
114
|
+
}
|
|
115
|
+
typeImports += `import type ${ep.moduleName} from '${relativePath}';\n`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const typeDefs = `
|
|
119
|
+
type Infer<T> = T extends import('zod').ZodType<infer O, any, infer I> ? I : any;
|
|
120
|
+
type RouteData<C> = (C extends { body: any } ? Infer<C['body']> : {}) & (C extends { query: any } ? Infer<C['query']> : {}) & (C extends { params: any } ? Infer<C['params']> : {});
|
|
121
|
+
type SafeData<C> = keyof RouteData<C> extends never ? any : RouteData<C>;
|
|
122
|
+
|
|
123
|
+
export declare const api: {
|
|
124
|
+
${generateApiObject(endpoints, true)}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export declare function setBaseURL(url: string): void;
|
|
128
|
+
export declare function setTokenKey(key: string): void;
|
|
129
|
+
export declare function setLocale(locale: string): void;
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(path.join(process.cwd(), 'bro-sdk.d.ts'), typeImports + typeDefs, 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function generateApiObject(endpoints, isTypes) {
|
|
136
|
+
const tree = Object.create(null);
|
|
137
|
+
|
|
138
|
+
for (const ep of endpoints) {
|
|
139
|
+
const { routePath, method, moduleName } = ep;
|
|
140
|
+
const parts = routePath.split('/').filter(Boolean);
|
|
141
|
+
|
|
142
|
+
let current = tree;
|
|
143
|
+
let pathAcc = '';
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < parts.length; i++) {
|
|
146
|
+
const part = parts[i];
|
|
147
|
+
const isParam = part.startsWith(':');
|
|
148
|
+
const name = isParam ? part.slice(1) : part;
|
|
149
|
+
pathAcc += '/' + part;
|
|
150
|
+
|
|
151
|
+
if (!current[name]) {
|
|
152
|
+
current[name] = Object.assign(Object.create(null), { _isParam: isParam, _methods: Object.create(null), _children: Object.create(null), _path: pathAcc });
|
|
153
|
+
} else {
|
|
154
|
+
if (current[name]._isParam !== isParam) {
|
|
155
|
+
throw new Error(`SDK Collision: Route segment "${name}" conflicts between static and dynamic parameters at path "${pathAcc}"`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (i === parts.length - 1) {
|
|
160
|
+
current[name]._methods[method.toLowerCase()] = { path: pathAcc, moduleName };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
current = current[name]._children;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (parts.length === 0) {
|
|
167
|
+
if (!tree['root']) tree['root'] = Object.assign(Object.create(null), { _isParam: false, _methods: Object.create(null), _children: Object.create(null), _path: '/' });
|
|
168
|
+
tree['root']._methods[method.toLowerCase()] = { path: '/', moduleName };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderTree(node, indent = ' ') {
|
|
173
|
+
let result = '';
|
|
174
|
+
|
|
175
|
+
const isValidIdentifier = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
|
|
176
|
+
|
|
177
|
+
for (const [key, val] of Object.entries(node)) {
|
|
178
|
+
const formattedKey = isValidIdentifier(key) ? key : `"${key}"`;
|
|
179
|
+
|
|
180
|
+
if (val._isParam) {
|
|
181
|
+
if (isTypes) {
|
|
182
|
+
result += `${indent}${formattedKey}: (${key}: string | number) => {\n`;
|
|
183
|
+
} else {
|
|
184
|
+
result += `${indent}${formattedKey}: (${key}) => ({\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const [m, info] of Object.entries(val._methods)) {
|
|
188
|
+
if (isTypes) {
|
|
189
|
+
result += `${indent} ${m}: (data?: SafeData<typeof ${info.moduleName}.default>) => Promise<any>;\n`;
|
|
190
|
+
} else {
|
|
191
|
+
const templatedPath = info.path.replace(/:([a-zA-Z0-9_$]+)/g, '\${encodeURIComponent($1)}');
|
|
192
|
+
result += `${indent} ${m}: (data) => request('${m}', \`${templatedPath}\`, data),\n`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const childrenStr = renderTree(val._children, indent + ' ');
|
|
197
|
+
if (childrenStr) {
|
|
198
|
+
result += childrenStr;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isTypes) {
|
|
202
|
+
result += `${indent}};\n`;
|
|
203
|
+
} else {
|
|
204
|
+
result += `${indent}}),\n`;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
result += `${indent}${formattedKey}: {\n`;
|
|
208
|
+
for (const [m, info] of Object.entries(val._methods)) {
|
|
209
|
+
if (isTypes) {
|
|
210
|
+
result += `${indent} ${m}: (data?: SafeData<typeof ${info.moduleName}.default>) => Promise<any>;\n`;
|
|
211
|
+
} else {
|
|
212
|
+
result += `${indent} ${m}: (data) => request('${m}', '${info.path}', data),\n`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const childrenStr = renderTree(val._children, indent + ' ');
|
|
217
|
+
if (childrenStr) {
|
|
218
|
+
result += childrenStr;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
result += `${indent}},
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return renderTree(tree);
|
|
229
|
+
}
|