bro-framework 2.2.1 → 2.3.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 +205 -190
- package/bin/bro.js +268 -259
- package/package.json +88 -88
- package/src/auth.js +33 -33
- package/src/index.d.ts +80 -66
- package/src/index.js +1 -0
- package/src/locale.js +118 -0
- package/src/logger.js +79 -79
- package/src/router.js +192 -187
- package/src/sdk.js +169 -160
- package/src/server.js +277 -243
- package/src/tasks.js +54 -54
package/bin/bro.js
CHANGED
|
@@ -1,260 +1,269 @@
|
|
|
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 {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
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
|
-
|
|
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
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (command === '
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
if (command === 'dev') {
|
|
21
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
|
22
|
+
} else if (command === 'start') {
|
|
23
|
+
process.env.NODE_ENV = 'production';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CONFIG_TEMPLATE = `import { defineConfig } from 'bro-framework';
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
// Server Settings
|
|
30
|
+
server: {
|
|
31
|
+
port: 5000,
|
|
32
|
+
cors: true // Set to true to allow all, or pass a CORS options object
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Authentication Settings
|
|
36
|
+
auth: {
|
|
37
|
+
jwtSecret: 'dev_secret_please_change',
|
|
38
|
+
expiresIn: '7d'
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Optional file-based API translations
|
|
42
|
+
// Add locale/en.js, locale/fr.js, etc.
|
|
43
|
+
locale: {
|
|
44
|
+
defaultLocale: 'en'
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// API Documentation (Scalar UI)
|
|
48
|
+
docs: process.env.NODE_ENV !== 'production', // Set to false to disable completely, or true to force in prod
|
|
49
|
+
|
|
50
|
+
// Rate Limiting
|
|
51
|
+
rateLimit: {
|
|
52
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
53
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// WebSockets Setup
|
|
57
|
+
sockets: async (io, db) => {
|
|
58
|
+
io.on('connection', (socket) => {
|
|
59
|
+
console.log('Client connected:', socket.id);
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Database Context Injection
|
|
64
|
+
// This instance will be injected into every route's ctx.db (if defined)
|
|
65
|
+
db: async () => {
|
|
66
|
+
// If you use a database, set up your connection here
|
|
67
|
+
// and return the connection instance or an object of your models.
|
|
68
|
+
// Could be MongoDB, MySQL, etc. (your choice)
|
|
69
|
+
// --- MONGOOSE EXAMPLE ---
|
|
70
|
+
// import mongoose from 'mongoose';
|
|
71
|
+
|
|
72
|
+
// await mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost:27017/bro_database');
|
|
73
|
+
// console.log("Connected to MongoDB");
|
|
74
|
+
|
|
75
|
+
// You can return mongoose itself, or an object of your models
|
|
76
|
+
// to access them instantly in your routes without importing them!
|
|
77
|
+
// Example: return { User, Post };
|
|
78
|
+
|
|
79
|
+
// return mongoose.connection;
|
|
80
|
+
// --------------------------
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
function scaffoldConfig() {
|
|
87
|
+
const configPath = path.join(process.cwd(), 'bro.config.js');
|
|
88
|
+
if (!fs.existsSync(configPath)) {
|
|
89
|
+
fs.writeFileSync(configPath, CONFIG_TEMPLATE, 'utf-8');
|
|
90
|
+
console.log(`\n ${colors.green} Created default bro.config.js${colors.reset}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureTypeModule() {
|
|
95
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(pkgPath)) {
|
|
98
|
+
try {
|
|
99
|
+
const pkgRaw = fs.readFileSync(pkgPath, 'utf-8');
|
|
100
|
+
const pkg = JSON.parse(pkgRaw);
|
|
101
|
+
|
|
102
|
+
if (pkg.type !== 'module') {
|
|
103
|
+
pkg.type = 'module';
|
|
104
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
|
|
105
|
+
console.log(`\n ${colors.green} Auto-configured package.json for ES Modules${colors.reset}`);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(`\n ${colors.red} Failed to parse package.json for ES Modules setup${colors.reset}`, err);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
const defaultPkg = {
|
|
112
|
+
name: "bro-app",
|
|
113
|
+
version: "1.0.0",
|
|
114
|
+
type: "module",
|
|
115
|
+
private: true
|
|
116
|
+
};
|
|
117
|
+
fs.writeFileSync(pkgPath, JSON.stringify(defaultPkg, null, 2), 'utf-8');
|
|
118
|
+
console.log(`\n ${colors.green} Created package.json with ES Modules enabled${colors.reset}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (command === 'init') {
|
|
123
|
+
ensureTypeModule();
|
|
124
|
+
scaffoldConfig();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (['sdk', 'generate-client', 'client'].includes(command)) {
|
|
129
|
+
generateSDK().then(() => {
|
|
130
|
+
console.log(`\n ${colors.green} bro-sdk.js generated successfully!${colors.reset}\n`);
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}).catch(err => {
|
|
133
|
+
console.error(`\n ${colors.red} Error generating SDK:${colors.reset}`, err.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function bootstrap() {
|
|
139
|
+
if (command === 'dev') {
|
|
140
|
+
ensureTypeModule();
|
|
141
|
+
scaffoldConfig();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const startTime = performance.now();
|
|
145
|
+
|
|
146
|
+
const cwd = process.cwd();
|
|
147
|
+
const configPath = path.join(cwd, 'bro.config.js');
|
|
148
|
+
const routesDir = path.join(cwd, 'routes');
|
|
149
|
+
|
|
150
|
+
let globalConfig = {
|
|
151
|
+
port: process.env.PORT || 5000,
|
|
152
|
+
jwtSecret: process.env.JWT_SECRET || 'dev_secret_please_change'
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let db = null;
|
|
156
|
+
|
|
157
|
+
if (fs.existsSync(configPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const configModule = await import(pathToFileURL(configPath).href);
|
|
160
|
+
const userConfig = configModule.default || configModule.config || {};
|
|
161
|
+
|
|
162
|
+
if (userConfig.server?.port) globalConfig.port = userConfig.server.port;
|
|
163
|
+
if (userConfig.auth?.jwtSecret) globalConfig.jwtSecret = userConfig.auth.jwtSecret;
|
|
164
|
+
|
|
165
|
+
globalConfig = { ...globalConfig, ...userConfig };
|
|
166
|
+
|
|
167
|
+
if (typeof globalConfig.db === 'function') {
|
|
168
|
+
db = await globalConfig.db();
|
|
169
|
+
} else if (globalConfig.db && typeof globalConfig.db.init === 'function') {
|
|
170
|
+
db = await globalConfig.db.init();
|
|
171
|
+
} else if (globalConfig.db) {
|
|
172
|
+
db = globalConfig.db;
|
|
173
|
+
if (db instanceof Promise) db = await db;
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('✗ Failed to load bro.config.js:', err);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (globalConfig.env) {
|
|
181
|
+
const envResult = globalConfig.env.safeParse(process.env);
|
|
182
|
+
if (!envResult.success) {
|
|
183
|
+
console.error(`\n ${colors.red} Environment Validation Failed${colors.reset}`);
|
|
184
|
+
envResult.error.errors.forEach(err => {
|
|
185
|
+
console.error(` ${colors.dim}-${colors.reset} ${colors.bold}${err.path.join('.')}${colors.reset}: ${err.message}`);
|
|
186
|
+
});
|
|
187
|
+
console.error("");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
globalConfig.envData = envResult.data;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(routesDir)) {
|
|
194
|
+
console.error(`✗ Error: 'routes' directory not found in ${cwd}`);
|
|
195
|
+
console.error(` Please create a 'routes/' folder and add your first route.`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const localeDir = globalConfig.locale?.directory || path.join(cwd, 'locale');
|
|
200
|
+
const { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
201
|
+
const port = globalConfig.port;
|
|
202
|
+
|
|
203
|
+
let currentRoutes = initialRoutes;
|
|
204
|
+
|
|
205
|
+
server.listen(port, async () => {
|
|
206
|
+
if (command === 'dev') {
|
|
207
|
+
console.clear();
|
|
208
|
+
printBanner(port, performance.now() - startTime);
|
|
209
|
+
} else if (command === 'start') {
|
|
210
|
+
console.log(`[bro.js] Server running in production on port ${port}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (command === 'dev') {
|
|
214
|
+
const printCurrentRoutes = (routesToPrint) => {
|
|
215
|
+
if (routesToPrint.length > 0) {
|
|
216
|
+
routesToPrint.forEach((r, i) => {
|
|
217
|
+
printRoute(r.method, r.path, r.auth, i === routesToPrint.length - 1);
|
|
218
|
+
});
|
|
219
|
+
console.log("");
|
|
220
|
+
} else {
|
|
221
|
+
console.log(" No routes found.\n");
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
printCurrentRoutes(currentRoutes);
|
|
226
|
+
|
|
227
|
+
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts}';
|
|
228
|
+
const watcher = chokidar.watch([routesDir, localeGlob], { ignoreInitial: true });
|
|
229
|
+
|
|
230
|
+
watcher.on('all', async (event, filepath) => {
|
|
231
|
+
const isJavaScriptFile = filepath.endsWith('.js') || filepath.endsWith('.ts') || filepath.endsWith('.mjs');
|
|
232
|
+
if (!isJavaScriptFile) return;
|
|
233
|
+
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
234
|
+
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const reloadStartTime = performance.now();
|
|
238
|
+
if (isLocaleFile) {
|
|
239
|
+
await reloadLocale();
|
|
240
|
+
} else {
|
|
241
|
+
currentRoutes = await reload();
|
|
242
|
+
}
|
|
243
|
+
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
244
|
+
|
|
245
|
+
printHotReload(path.basename(filepath), event, reloadTimeMs, isLocaleFile ? 'Locale' : 'Route');
|
|
246
|
+
printCurrentRoutes(currentRoutes);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error(`\n ✗ Error hot-reloading ${isLocaleFile ? 'locale' : 'routes'}:`, err);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const handleShutdown = async (signal) => {
|
|
254
|
+
console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
|
|
255
|
+
await shutdown();
|
|
256
|
+
console.log('[bro.js] HTTP server closed.');
|
|
257
|
+
process.exit(0);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
261
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === 'dev' || command === 'start') {
|
|
266
|
+
bootstrap();
|
|
267
|
+
} else {
|
|
268
|
+
console.log(`Usage: bro dev | bro start | bro init`);
|
|
269
|
+
}
|