bro-framework 2.2.0 → 2.2.1
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 +1 -1
- package/bin/bro.js +3 -3
- package/package.json +1 -1
- package/src/logger.js +14 -1
- package/src/router.js +15 -0
- package/src/sdk.js +4 -0
- package/src/tasks.js +15 -10
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ Stop importing singleton database connections and socket instances into every fi
|
|
|
109
109
|
If you've ever hand-written OpenAPI YAML, you know the pain. `bro.js` parses your Zod schemas and automatically serves a stunning, interactive [Scalar](https://scalar.com/) API playground at `/docs`. It's highly secure: by default, these internal docs are disabled in production mode.
|
|
110
110
|
|
|
111
111
|
### The Frontend SDK Generator
|
|
112
|
-
Tired of writing frontend `fetch` wrappers? Run `bro sdk`. The CLI will parse your backend routes and compile a `bro-
|
|
112
|
+
Tired of writing frontend `fetch` wrappers? Run `bro sdk`. The CLI will parse your backend routes and compile a `bro-sdk.js` file for your frontend. It features built-in token management, request stringification, and type-safe deep tree traversal (e.g., `api.users.id("123").post(data)`).
|
|
113
113
|
|
|
114
114
|
### Background Task Scheduler
|
|
115
115
|
Don't spin up a separate worker server. Drop a JavaScript file anywhere in the `tasks/` folder, export a cron string (e.g., `"0 0 * * *"`), and an async handler. `bro.js` natively schedules it as a background worker with full access to your injected database and WebSocket contexts.
|
package/bin/bro.js
CHANGED
|
@@ -9,7 +9,7 @@ register();
|
|
|
9
9
|
|
|
10
10
|
import { createServer } from '../src/server.js';
|
|
11
11
|
import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
|
|
12
|
-
import { scanTasks
|
|
12
|
+
import { scanTasks } from '../src/tasks.js';
|
|
13
13
|
import { generateSDK } from '../src/sdk.js';
|
|
14
14
|
import dotenv from 'dotenv';
|
|
15
15
|
import chokidar from 'chokidar';
|
|
@@ -204,7 +204,7 @@ async function bootstrap() {
|
|
|
204
204
|
console.log(`[bro.js] Server running in production on port ${port}`);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
await scanTasks({ db, io });
|
|
207
|
+
let taskManager = await scanTasks({ db, io });
|
|
208
208
|
|
|
209
209
|
if (command === 'dev') {
|
|
210
210
|
const printCurrentRoutes = (routesToPrint) => {
|
|
@@ -240,7 +240,7 @@ async function bootstrap() {
|
|
|
240
240
|
|
|
241
241
|
const handleShutdown = async (signal) => {
|
|
242
242
|
console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
|
|
243
|
-
|
|
243
|
+
if (taskManager) taskManager.stopAll();
|
|
244
244
|
if (io) io.close();
|
|
245
245
|
server.close(() => {
|
|
246
246
|
console.log('[bro.js] HTTP server closed.');
|
package/package.json
CHANGED
package/src/logger.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
1
5
|
export const colors = {
|
|
2
6
|
reset: "\x1b[0m",
|
|
3
7
|
bold: "\x1b[1m",
|
|
@@ -25,7 +29,16 @@ export function formatMethod(method) {
|
|
|
25
29
|
|
|
26
30
|
export function printBanner(port, durationMs) {
|
|
27
31
|
const time = durationMs.toFixed(0);
|
|
28
|
-
|
|
32
|
+
|
|
33
|
+
let version = "2.2.0";
|
|
34
|
+
try {
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = path.dirname(__filename);
|
|
37
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
39
|
+
version = pkg.version;
|
|
40
|
+
} catch(e) {}
|
|
41
|
+
|
|
29
42
|
const innerWidth = 47;
|
|
30
43
|
|
|
31
44
|
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
package/src/router.js
CHANGED
|
@@ -43,6 +43,12 @@ export function parseRouteFile(filePath, routesDir) {
|
|
|
43
43
|
if (parts.length < 2) return null;
|
|
44
44
|
|
|
45
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
|
+
|
|
46
52
|
const namePart = parts.join('.');
|
|
47
53
|
|
|
48
54
|
let routePath = '/' + path.dirname(relativePath).replace(/\\/g, '/');
|
|
@@ -52,6 +58,15 @@ export function parseRouteFile(filePath, routesDir) {
|
|
|
52
58
|
routePath += `/${namePart}`;
|
|
53
59
|
}
|
|
54
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
|
+
|
|
55
70
|
routePath = routePath.replace(/\[(.*?)\]/g, ':$1');
|
|
56
71
|
|
|
57
72
|
if (routePath === '') routePath = '/';
|
package/src/sdk.js
CHANGED
|
@@ -98,6 +98,10 @@ function generateApiObject(endpoints) {
|
|
|
98
98
|
|
|
99
99
|
if (!current[name]) {
|
|
100
100
|
current[name] = { _isParam: isParam, _methods: {}, _children: {}, _path: pathAcc };
|
|
101
|
+
} else {
|
|
102
|
+
if (current[name]._isParam !== isParam) {
|
|
103
|
+
throw new Error(`SDK Collision: Route segment "${name}" conflicts between static and dynamic parameters at path "${pathAcc}"`);
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
if (i === parts.length - 1) {
|
package/src/tasks.js
CHANGED
|
@@ -5,21 +5,24 @@ import cron from 'node-cron';
|
|
|
5
5
|
import { colors } from './logger.js';
|
|
6
6
|
import { scanDir } from './router.js';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
export class TaskManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.taskHandles = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
stopAll() {
|
|
14
|
+
this.taskHandles.forEach(t => t.stop());
|
|
15
|
+
this.taskHandles = [];
|
|
16
|
+
}
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export async function scanTasks(ctx) {
|
|
20
|
+
const manager = new TaskManager();
|
|
16
21
|
const tasksDir = path.join(process.cwd(), 'tasks');
|
|
17
|
-
if (!fs.existsSync(tasksDir)) return;
|
|
22
|
+
if (!fs.existsSync(tasksDir)) return manager;
|
|
18
23
|
|
|
19
|
-
stopTasks();
|
|
20
|
-
|
|
21
24
|
const files = scanDir(tasksDir);
|
|
22
|
-
if (files.length === 0) return;
|
|
25
|
+
if (files.length === 0) return manager;
|
|
23
26
|
|
|
24
27
|
let count = 0;
|
|
25
28
|
for (const file of files) {
|
|
@@ -35,7 +38,7 @@ export async function scanTasks(ctx) {
|
|
|
35
38
|
console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
|
|
36
39
|
}
|
|
37
40
|
});
|
|
38
|
-
taskHandles.push(task);
|
|
41
|
+
manager.taskHandles.push(task);
|
|
39
42
|
count++;
|
|
40
43
|
}
|
|
41
44
|
} catch (err) {
|
|
@@ -46,4 +49,6 @@ export async function scanTasks(ctx) {
|
|
|
46
49
|
if (count > 0) {
|
|
47
50
|
console.log(` ${colors.dim}├──${colors.reset} ${colors.cyan}Scheduled ${count} background task(s)${colors.reset}`);
|
|
48
51
|
}
|
|
52
|
+
|
|
53
|
+
return manager;
|
|
49
54
|
}
|