bro-framework 2.4.4 → 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 +91 -129
- 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/README.md
CHANGED
|
@@ -84,6 +84,8 @@ export default defineConfig({
|
|
|
84
84
|
|
|
85
85
|
`cors: false` disables HTTP CORS middleware. Helmet is enabled by default and can be disabled with `helmet: false`. Redis is optional; when configured it powers distributed rate limiting, route caching, and Socket.IO scaling. In `NODE_ENV=test`, bro.js can inject `ioredis-mock`; install it in the consuming project's development dependencies.
|
|
86
86
|
|
|
87
|
+
> **Production Security Note**: When deploying behind a reverse proxy (Nginx, AWS ALB, Vercel, Render), ensure your load balancer properly sets `X-Forwarded-For`. Rate limiting and trusted IP functionality relies on this proxy configuration.
|
|
88
|
+
|
|
87
89
|
---
|
|
88
90
|
|
|
89
91
|
## The Core Experience
|
|
@@ -243,6 +245,40 @@ Handlers receive:
|
|
|
243
245
|
|
|
244
246
|
For Redis-backed integration tests without an external Redis server, install `ioredis-mock` in the consuming project and run with `NODE_ENV=test`. bro.js injects a mock Redis client and exercises cache, rate-limit, Socket.IO adapter, and shutdown paths.
|
|
245
247
|
|
|
248
|
+
### Supertest + Vitest Recipe
|
|
249
|
+
|
|
250
|
+
You can programmatically bootstrap `bro.js` using `createServer` for blazing fast integration tests. Here's a complete `vitest` recipe:
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
254
|
+
import request from 'supertest';
|
|
255
|
+
import path from 'path';
|
|
256
|
+
import { createServer } from 'bro-framework';
|
|
257
|
+
import config from '../bro.config.js';
|
|
258
|
+
|
|
259
|
+
describe('API Tests', () => {
|
|
260
|
+
let app, shutdown;
|
|
261
|
+
|
|
262
|
+
beforeAll(async () => {
|
|
263
|
+
// 1. Initialize the server programmatically
|
|
264
|
+
const instance = await createServer(config, path.resolve('./routes'), null);
|
|
265
|
+
app = instance.app;
|
|
266
|
+
shutdown = instance.shutdown;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
afterAll(async () => {
|
|
270
|
+
// 2. Cleanly teardown tasks, redis, and sockets
|
|
271
|
+
if (shutdown) await shutdown();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should return a 200 from the healthcheck', async () => {
|
|
275
|
+
const res = await request(app).get('/health/live');
|
|
276
|
+
expect(res.status).toBe(200);
|
|
277
|
+
expect(res.body.status).toBe('ok');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
246
282
|
---
|
|
247
283
|
|
|
248
284
|
## Architecture & Request Lifecycle
|
|
@@ -339,6 +375,14 @@ export const POST = defineRoute({
|
|
|
339
375
|
|
|
340
376
|
---
|
|
341
377
|
|
|
378
|
+
## Compatibility Table
|
|
379
|
+
|
|
380
|
+
| bro.js Version | Node.js | Next.js App Router | Express | Zod |
|
|
381
|
+
| :------------- | :-------- | :----------------- | :------ | :------ |
|
|
382
|
+
| `>= 2.0.0` | `>= 18.x` | `>= 13.4.x` | `4.x` | `3.x` |
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
342
386
|
## Author & License
|
|
343
387
|
|
|
344
388
|
- **Author**: Yass1n (@medyass1ne)
|
package/bin/bro.js
CHANGED
|
@@ -1,293 +1,195 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import { pathToFileURL } from 'url';
|
|
6
|
-
import { register } from 'tsx/esm/api';
|
|
7
|
-
|
|
8
|
-
register();
|
|
9
|
-
|
|
10
|
-
import { createServer } from '../src/server.js';
|
|
11
|
-
import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
|
|
12
|
-
import { generateSDK } from '../src/sdk.js';
|
|
13
|
-
import dotenv from 'dotenv';
|
|
14
|
-
import chokidar from 'chokidar';
|
|
15
|
-
|
|
16
|
-
dotenv.config();
|
|
17
|
-
|
|
18
|
-
const command = process.argv[2] || 'dev';
|
|
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
|
-
|
|
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
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
console.error("");
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
globalConfig.envData = envResult.data;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!fs.existsSync(routesDir)) {
|
|
204
|
-
console.error(`✗ Error: 'routes' directory not found in ${cwd}`);
|
|
205
|
-
console.error(` Please create a 'routes/' folder and add your first route.`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const localeDir = globalConfig.locale?.directory || path.join(cwd, 'locale');
|
|
210
|
-
const tasksDir = path.join(cwd, 'tasks');
|
|
211
|
-
const { app, server, routes: initialRoutes, reload, reloadLocale, reloadTasks, io, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
212
|
-
const port = globalConfig.port;
|
|
213
|
-
|
|
214
|
-
let currentRoutes = initialRoutes;
|
|
215
|
-
|
|
216
|
-
server.listen(port, async () => {
|
|
217
|
-
if (command === 'dev') {
|
|
218
|
-
console.clear();
|
|
219
|
-
printBanner(port, performance.now() - startTime);
|
|
220
|
-
} else if (command === 'start') {
|
|
221
|
-
console.log(`[bro.js] Server running in production on port ${port}`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (command === 'dev') {
|
|
225
|
-
const printCurrentRoutes = (routesToPrint) => {
|
|
226
|
-
if (routesToPrint.length > 0) {
|
|
227
|
-
routesToPrint.forEach((r, i) => {
|
|
228
|
-
printRoute(r.method, r.path, r.auth, i === routesToPrint.length - 1);
|
|
229
|
-
});
|
|
230
|
-
console.log("");
|
|
231
|
-
} else {
|
|
232
|
-
console.log(" No routes found.\n");
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
printCurrentRoutes(currentRoutes);
|
|
237
|
-
|
|
238
|
-
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts,json}';
|
|
239
|
-
const watcher = chokidar.watch([routesDir, localeGlob, tasksDir], { ignoreInitial: true });
|
|
240
|
-
|
|
241
|
-
watcher.on('all', async (event, filepath) => {
|
|
242
|
-
const isValidFile = filepath.match(/\.(js|ts|mjs|json)$/);
|
|
243
|
-
if (!isValidFile) return;
|
|
244
|
-
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
245
|
-
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
246
|
-
|
|
247
|
-
const relTask = path.relative(path.resolve(tasksDir), filepath);
|
|
248
|
-
const isTaskFile = !relTask.startsWith('..') && !path.isAbsolute(relTask);
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const reloadStartTime = performance.now();
|
|
252
|
-
if (isLocaleFile) {
|
|
253
|
-
await reloadLocale();
|
|
254
|
-
} else if (isTaskFile) {
|
|
255
|
-
await reloadTasks();
|
|
256
|
-
} else {
|
|
257
|
-
currentRoutes = await reload();
|
|
258
|
-
}
|
|
259
|
-
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
260
|
-
|
|
261
|
-
const fileType = isTaskFile ? 'Task' : (isLocaleFile ? 'Locale' : 'Route');
|
|
262
|
-
printHotReload(path.basename(filepath), event, reloadTimeMs, fileType);
|
|
263
|
-
printCurrentRoutes(currentRoutes);
|
|
264
|
-
} catch (err) {
|
|
265
|
-
const fileType = isTaskFile ? 'tasks' : (isLocaleFile ? 'locale' : 'routes');
|
|
266
|
-
console.error(`\n ✗ Error hot-reloading ${fileType}:`, err);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const handleShutdown = async (signal) => {
|
|
272
|
-
console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
|
|
273
|
-
await shutdown();
|
|
274
|
-
console.log('[bro.js] HTTP server closed.');
|
|
275
|
-
process.exit(0);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
279
|
-
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (command === 'dev' || command === 'start') {
|
|
284
|
-
bootstrap();
|
|
285
|
-
} else if (!['sdk', 'generate-client', 'client', 'init'].includes(command)) {
|
|
286
|
-
console.log(`\n ${colors.bold}${colors.green}bro.js CLI${colors.reset}\n`);
|
|
287
|
-
console.log(` ${colors.bold}Usage:${colors.reset} bro <command>\n`);
|
|
288
|
-
console.log(` ${colors.bold}Commands:${colors.reset}`);
|
|
289
|
-
console.log(` ${colors.cyan}dev${colors.reset} Start the development server with hot-reload`);
|
|
290
|
-
console.log(` ${colors.cyan}start${colors.reset} Start the production server gracefully`);
|
|
291
|
-
console.log(` ${colors.cyan}init${colors.reset} Scaffold a new bro.config.js workspace`);
|
|
292
|
-
console.log(` ${colors.cyan}sdk${colors.reset} Generate a typed frontend client\n`);
|
|
293
|
-
}
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { register } from 'tsx/esm/api';
|
|
7
|
+
|
|
8
|
+
register();
|
|
9
|
+
|
|
10
|
+
import { createServer } from '../src/server.js';
|
|
11
|
+
import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
|
|
12
|
+
import { generateSDK } from '../src/sdk.js';
|
|
13
|
+
import dotenv from 'dotenv';
|
|
14
|
+
import chokidar from 'chokidar';
|
|
15
|
+
|
|
16
|
+
dotenv.config();
|
|
17
|
+
|
|
18
|
+
const command = process.argv[2] || 'dev';
|
|
19
|
+
|
|
20
|
+
async function bootstrap() {
|
|
21
|
+
const startTime = performance.now();
|
|
22
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
23
|
+
let globalConfig = {};
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const configModule = await import(pathToFileURL(configPath).href);
|
|
28
|
+
globalConfig = configModule.default || configModule;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error('[bro.js] Error loading bro.config.js:', err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const routesDir = globalConfig.routesDir || path.resolve(process.cwd(), 'api');
|
|
36
|
+
const localeDir = globalConfig.locale?.directory || path.resolve(process.cwd(), 'locale');
|
|
37
|
+
const tasksDir = globalConfig.tasksDir || path.resolve(process.cwd(), 'tasks');
|
|
38
|
+
const port = globalConfig.server?.port || process.env.PORT || 3000;
|
|
39
|
+
|
|
40
|
+
let db = null;
|
|
41
|
+
// Initialize db from globalConfig if provided (mocked here for CLI boot)
|
|
42
|
+
if (globalConfig.db && typeof globalConfig.db === 'function') {
|
|
43
|
+
db = await globalConfig.db();
|
|
44
|
+
} else {
|
|
45
|
+
db = globalConfig.db;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { app, server, routes, reload, reloadLocale, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
49
|
+
let currentRoutes = routes;
|
|
50
|
+
|
|
51
|
+
server.listen(port, () => {
|
|
52
|
+
if (command === 'dev') {
|
|
53
|
+
console.clear();
|
|
54
|
+
printBanner(port, performance.now() - startTime);
|
|
55
|
+
|
|
56
|
+
if (process.argv.includes('--ui')) {
|
|
57
|
+
import('../src/dashboard.js').then(({ startDashboard }) => {
|
|
58
|
+
startDashboard(globalConfig, currentRoutes);
|
|
59
|
+
}).catch(err => console.error('[bro.js] Error starting dashboard:', err));
|
|
60
|
+
}
|
|
61
|
+
} else if (command === 'start') {
|
|
62
|
+
console.log(`[bro.js] Server running in production on port ${port}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (command === 'dev') {
|
|
66
|
+
const printCurrentRoutes = (routesToPrint) => {
|
|
67
|
+
if (routesToPrint.length > 0) {
|
|
68
|
+
routesToPrint.forEach((r, i) => {
|
|
69
|
+
printRoute(r.method, r.path, r.auth, i === routesToPrint.length - 1);
|
|
70
|
+
});
|
|
71
|
+
console.log("");
|
|
72
|
+
} else {
|
|
73
|
+
console.log(" No routes found.\n");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
printCurrentRoutes(currentRoutes);
|
|
78
|
+
|
|
79
|
+
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts,json}';
|
|
80
|
+
const watcher = chokidar.watch([routesDir, localeGlob, tasksDir], { ignoreInitial: true });
|
|
81
|
+
|
|
82
|
+
watcher.on('all', async (event, filepath) => {
|
|
83
|
+
const isValidFile = filepath.match(/\.(js|ts|mjs|json)$/);
|
|
84
|
+
if (!isValidFile) return;
|
|
85
|
+
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
86
|
+
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
87
|
+
|
|
88
|
+
const relTask = path.relative(path.resolve(tasksDir), filepath);
|
|
89
|
+
const isTaskFile = !relTask.startsWith('..') && !path.isAbsolute(relTask);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const reloadStartTime = performance.now();
|
|
93
|
+
if (isLocaleFile) {
|
|
94
|
+
await reloadLocale();
|
|
95
|
+
} else if (isTaskFile) {
|
|
96
|
+
// Tasks reload
|
|
97
|
+
} else {
|
|
98
|
+
currentRoutes = await reload();
|
|
99
|
+
}
|
|
100
|
+
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
101
|
+
|
|
102
|
+
const fileType = isTaskFile ? 'Task' : (isLocaleFile ? 'Locale' : 'Route');
|
|
103
|
+
printHotReload(path.basename(filepath), event, reloadTimeMs, fileType);
|
|
104
|
+
printCurrentRoutes(currentRoutes);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const fileType = isTaskFile ? 'tasks' : (isLocaleFile ? 'locale' : 'routes');
|
|
107
|
+
console.error(`\n ✗ Error hot-reloading ${fileType}:`, err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handleShutdown = async (signal) => {
|
|
113
|
+
console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
|
|
114
|
+
if (shutdown) await shutdown();
|
|
115
|
+
console.log('[bro.js] HTTP server closed.');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
120
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if (command === 'test') {
|
|
126
|
+
const hasVitest = fs.existsSync(path.resolve(process.cwd(), 'node_modules', 'vitest'));
|
|
127
|
+
if (!hasVitest) {
|
|
128
|
+
console.error(colors.red + 'Vitest is not installed. Please run: npm install -D vitest' + colors.reset);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
import('child_process').then(cp => {
|
|
133
|
+
console.log('\x1b[36m[bro.js]\x1b[0m Starting tests via vitest...');
|
|
134
|
+
cp.spawn('npx', ['vitest', ...process.argv.slice(3)], { stdio: 'inherit' });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
} else if (command === 'init') {
|
|
138
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
139
|
+
const envPath = path.resolve(process.cwd(), '.env.example');
|
|
140
|
+
if (!fs.existsSync(configPath)) {
|
|
141
|
+
fs.writeFileSync(configPath, `export default {
|
|
142
|
+
server: { port: 3000, cors: false },
|
|
143
|
+
auth: { jwtSecret: process.env.JWT_SECRET }
|
|
144
|
+
};
|
|
145
|
+
`);
|
|
146
|
+
console.log('[bro.js] Created bro.config.js');
|
|
147
|
+
}
|
|
148
|
+
if (!fs.existsSync(envPath)) {
|
|
149
|
+
fs.writeFileSync(envPath, 'JWT_SECRET=\n');
|
|
150
|
+
console.log('[bro.js] Created .env.example');
|
|
151
|
+
}
|
|
152
|
+
} else if (command === 'doctor') {
|
|
153
|
+
console.log('[bro.js] Running doctor...');
|
|
154
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
155
|
+
if (!fs.existsSync(configPath)) {
|
|
156
|
+
console.error('✗ No bro.config.js found.');
|
|
157
|
+
} else {
|
|
158
|
+
import(pathToFileURL(configPath).href).then(m => {
|
|
159
|
+
const config = m.default || m;
|
|
160
|
+
if (config.server?.cors === true) console.error('✗ Permissive CORS is enabled (cors: true). Use an array of allowed origins.');
|
|
161
|
+
else console.log('✓ CORS is strict.');
|
|
162
|
+
|
|
163
|
+
if (['dev_secret_please_change', 'bro_default_secret_key', 'your_jwt_secret_here'].includes(config.auth?.jwtSecret)) {
|
|
164
|
+
console.error('✗ Hardcoded insecure JWT secret detected.');
|
|
165
|
+
} else {
|
|
166
|
+
console.log('✓ Secrets look ok.');
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} else if (command === 'sdk' || command === 'client' || command === 'generate-client') {
|
|
171
|
+
const sdkOutPath = process.argv[3] || './client.ts';
|
|
172
|
+
const routesDir = path.resolve(process.cwd(), 'api');
|
|
173
|
+
generateSDK(routesDir, sdkOutPath).then(() => {
|
|
174
|
+
console.log(`\x1b[32m✓ SDK successfully generated at ${sdkOutPath}\x1b[0m`);
|
|
175
|
+
}).catch(err => {
|
|
176
|
+
console.error('\x1b[31m✗ Failed to generate SDK:\x1b[0m', err);
|
|
177
|
+
});
|
|
178
|
+
} else if (command === 'studio') {
|
|
179
|
+
import('../src/studio.js').then(({ startStudio }) => {
|
|
180
|
+
startStudio(process.cwd());
|
|
181
|
+
}).catch(err => console.error('[bro.js] Error starting studio:', err));
|
|
182
|
+
} else if (command === 'dev' || command === 'start') {
|
|
183
|
+
bootstrap();
|
|
184
|
+
} else if (!['init', 'doctor'].includes(command)) {
|
|
185
|
+
console.log(`\n ${colors.bold}${colors.green}bro.js CLI${colors.reset}\n`);
|
|
186
|
+
console.log(` ${colors.bold}Usage:${colors.reset} bro <command>\n`);
|
|
187
|
+
console.log(` ${colors.bold}Commands:${colors.reset}`);
|
|
188
|
+
console.log(` ${colors.cyan}dev${colors.reset} Start the development server with hot-reload`);
|
|
189
|
+
console.log(` ${colors.cyan}start${colors.reset} Start the production server gracefully`);
|
|
190
|
+
console.log(` ${colors.cyan}init${colors.reset} Scaffold a new bro.config.js workspace`);
|
|
191
|
+
console.log(` ${colors.cyan}sdk${colors.reset} Generate a typed frontend client`);
|
|
192
|
+
console.log(` ${colors.cyan}studio${colors.reset} Generate TS client, OpenAPI, and MSW mocks`);
|
|
193
|
+
console.log(` ${colors.cyan}doctor${colors.reset} Diagnose security and configuration issues`);
|
|
194
|
+
console.log(` ${colors.cyan}test${colors.reset} Run tests using vitest\n`);
|
|
195
|
+
}
|