bro-framework 3.0.1 → 3.0.3

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/bin/bro.js CHANGED
@@ -45,7 +45,7 @@ async function bootstrap() {
45
45
  db = globalConfig.db;
46
46
  }
47
47
 
48
- const { app, server, routes, reload, reloadLocale, shutdown } = await createServer(globalConfig, routesDir, db);
48
+ const { app, server, routes, reload, reloadLocale, reloadTasks, shutdown } = await createServer(globalConfig, routesDir, db);
49
49
  let currentRoutes = routes;
50
50
 
51
51
  server.listen(port, () => {
@@ -55,7 +55,7 @@ async function bootstrap() {
55
55
 
56
56
  if (process.argv.includes('--ui')) {
57
57
  import('../src/dashboard.js').then(({ startDashboard }) => {
58
- startDashboard(globalConfig, currentRoutes);
58
+ startDashboard(globalConfig, () => currentRoutes);
59
59
  }).catch(err => console.error('[bro.js] Error starting dashboard:', err));
60
60
  }
61
61
  } else if (command === 'start') {
@@ -93,7 +93,7 @@ async function bootstrap() {
93
93
  if (isLocaleFile) {
94
94
  await reloadLocale();
95
95
  } else if (isTaskFile) {
96
- // Tasks reload
96
+ await reloadTasks();
97
97
  } else {
98
98
  currentRoutes = await reload();
99
99
  }
@@ -166,7 +166,7 @@ export default defineConfig({
166
166
 
167
167
  // Enable OpenTelemetry W3C trace propagation and HTTP span generation
168
168
  openTelemetry: true
169
- }
169
+ },
170
170
 
171
171
  // Optional file-based API translations
172
172
  // Add locale/en.js, locale/fr.js, etc.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,286 @@
1
+ /* Minimalist High-Contrast Monochrome Design */
2
+ :root {
3
+ --bg-color: #000000;
4
+ --bg-card: #09090b;
5
+ --bg-card-hover: #121214;
6
+ --border-color: #27272a;
7
+ --text-primary: #fafafa;
8
+ --text-secondary: #a1a1aa;
9
+ --text-muted: #71717a;
10
+ --accent-color: #ffffff;
11
+ --success-color: #10b981;
12
+ --error-color: #ef4444;
13
+ --warning-color: #f59e0b;
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
24
+ background-color: var(--bg-color);
25
+ color: var(--text-primary);
26
+ line-height: 1.5;
27
+ -webkit-font-smoothing: antialiased;
28
+ }
29
+
30
+ header {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ padding: 1rem 2rem;
35
+ border-bottom: 1px solid var(--border-color);
36
+ background-color: var(--bg-card);
37
+ position: sticky;
38
+ top: 0;
39
+ z-index: 10;
40
+ }
41
+
42
+ .header-title {
43
+ font-size: 1.25rem;
44
+ font-weight: 600;
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 0.5rem;
48
+ }
49
+
50
+ .status-dot {
51
+ width: 8px;
52
+ height: 8px;
53
+ border-radius: 50%;
54
+ background-color: var(--success-color);
55
+ box-shadow: 0 0 8px var(--success-color);
56
+ animation: pulse 2s infinite;
57
+ }
58
+
59
+ @keyframes pulse {
60
+ 0% { opacity: 1; }
61
+ 50% { opacity: 0.5; }
62
+ 100% { opacity: 1; }
63
+ }
64
+
65
+ .header-controls {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 1rem;
69
+ }
70
+
71
+ .port-label {
72
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
73
+ color: var(--text-secondary);
74
+ font-size: 0.875rem;
75
+ }
76
+
77
+ .btn {
78
+ background: var(--bg-card);
79
+ border: 1px solid var(--border-color);
80
+ color: var(--text-primary);
81
+ padding: 0.5rem 1rem;
82
+ border-radius: 6px;
83
+ font-size: 0.875rem;
84
+ cursor: pointer;
85
+ transition: all 0.2s;
86
+ }
87
+
88
+ .btn:hover {
89
+ background: var(--bg-card-hover);
90
+ border-color: var(--text-secondary);
91
+ }
92
+
93
+ main {
94
+ max-width: 1200px;
95
+ margin: 0 auto;
96
+ padding: 2rem;
97
+ }
98
+
99
+ /* Grid layout for metrics */
100
+ .metrics-grid {
101
+ display: grid;
102
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
103
+ gap: 1.5rem;
104
+ margin-bottom: 2rem;
105
+ }
106
+
107
+ .card {
108
+ background: var(--bg-card);
109
+ border: 1px solid var(--border-color);
110
+ border-radius: 8px;
111
+ padding: 1.5rem;
112
+ display: flex;
113
+ flex-direction: column;
114
+ }
115
+
116
+ .card-title {
117
+ font-size: 0.875rem;
118
+ color: var(--text-secondary);
119
+ text-transform: uppercase;
120
+ letter-spacing: 0.05em;
121
+ margin-bottom: 0.5rem;
122
+ }
123
+
124
+ .card-value {
125
+ font-size: 1.5rem;
126
+ font-weight: 600;
127
+ }
128
+
129
+ .card-subtitle {
130
+ font-size: 0.875rem;
131
+ color: var(--text-muted);
132
+ margin-top: 0.5rem;
133
+ }
134
+
135
+ /* Search bar */
136
+ .search-container {
137
+ margin-bottom: 1.5rem;
138
+ }
139
+
140
+ .search-input {
141
+ width: 100%;
142
+ padding: 0.75rem 1rem;
143
+ background: var(--bg-card);
144
+ border: 1px solid var(--border-color);
145
+ color: var(--text-primary);
146
+ border-radius: 8px;
147
+ font-size: 1rem;
148
+ transition: border-color 0.2s;
149
+ }
150
+
151
+ .search-input:focus {
152
+ outline: none;
153
+ border-color: var(--accent-color);
154
+ }
155
+
156
+ /* Routes Table */
157
+ .routes-list {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 0.5rem;
161
+ }
162
+
163
+ .route-item {
164
+ background: var(--bg-card);
165
+ border: 1px solid var(--border-color);
166
+ border-radius: 8px;
167
+ padding: 1rem;
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 1rem;
171
+ transition: background 0.2s;
172
+ }
173
+
174
+ .route-item:hover {
175
+ background: var(--bg-card-hover);
176
+ }
177
+
178
+ .route-header {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 1rem;
182
+ cursor: pointer;
183
+ }
184
+
185
+ .method-pill {
186
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
187
+ font-size: 0.75rem;
188
+ font-weight: 700;
189
+ padding: 0.25rem 0.5rem;
190
+ border-radius: 4px;
191
+ border: 1px solid var(--border-color);
192
+ width: 60px;
193
+ text-align: center;
194
+ }
195
+
196
+ .method-GET { color: #38bdf8; border-color: #0369a1; background: rgba(3, 105, 161, 0.1); }
197
+ .method-POST { color: #10b981; border-color: #047857; background: rgba(4, 120, 87, 0.1); }
198
+ .method-PUT { color: #f59e0b; border-color: #b45309; background: rgba(180, 83, 9, 0.1); }
199
+ .method-PATCH { color: #f59e0b; border-color: #b45309; background: rgba(180, 83, 9, 0.1); }
200
+ .method-DELETE { color: #ef4444; border-color: #b91c1c; background: rgba(185, 28, 28, 0.1); }
201
+ .method-ANY { color: #a1a1aa; border-color: #52525b; background: rgba(82, 82, 91, 0.1); }
202
+
203
+ .route-path {
204
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
205
+ font-size: 0.875rem;
206
+ flex-grow: 1;
207
+ }
208
+
209
+ .badges {
210
+ display: flex;
211
+ gap: 0.5rem;
212
+ }
213
+
214
+ .badge {
215
+ font-size: 0.75rem;
216
+ padding: 0.1rem 0.4rem;
217
+ border-radius: 4px;
218
+ background: #18181b;
219
+ border: 1px solid var(--border-color);
220
+ color: var(--text-secondary);
221
+ }
222
+
223
+ .badge.active {
224
+ color: var(--text-primary);
225
+ border-color: var(--text-secondary);
226
+ }
227
+
228
+ .route-details {
229
+ display: none;
230
+ border-top: 1px dashed var(--border-color);
231
+ padding-top: 1rem;
232
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
233
+ font-size: 0.875rem;
234
+ color: var(--text-secondary);
235
+ }
236
+
237
+ .route-details.expanded {
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 0.5rem;
241
+ }
242
+
243
+ .detail-row {
244
+ display: flex;
245
+ gap: 1rem;
246
+ }
247
+ .detail-label {
248
+ color: var(--text-muted);
249
+ width: 80px;
250
+ }
251
+ .detail-value {
252
+ color: var(--text-primary);
253
+ }
254
+
255
+ /* Config Grid */
256
+ .config-container {
257
+ margin-top: 3rem;
258
+ border-top: 1px solid var(--border-color);
259
+ padding-top: 2rem;
260
+ }
261
+
262
+ .config-container h2 {
263
+ font-size: 1.25rem;
264
+ margin-bottom: 1rem;
265
+ }
266
+
267
+ .config-grid {
268
+ display: grid;
269
+ grid-template-columns: max-content 1fr;
270
+ gap: 0.75rem 2rem;
271
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
272
+ font-size: 0.875rem;
273
+ background: var(--bg-card);
274
+ border: 1px solid var(--border-color);
275
+ padding: 1.5rem;
276
+ border-radius: 8px;
277
+ overflow-x: auto;
278
+ }
279
+
280
+ .config-key {
281
+ color: var(--text-secondary);
282
+ }
283
+
284
+ .config-val {
285
+ color: var(--text-primary);
286
+ }
@@ -0,0 +1,196 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>bro.js | Dev Dashboard</title>
7
+ <link rel="stylesheet" href="/dashboard.css">
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <div class="header-title">
12
+ <div class="status-dot"></div>
13
+ bro.js <span style="color: var(--text-muted); font-size: 1rem;">v3.0.0</span>
14
+ </div>
15
+ <div class="header-controls">
16
+ <span class="port-label" id="portLabel"></span>
17
+ <button class="btn" id="refreshBtn">Refresh</button>
18
+ </div>
19
+ </header>
20
+
21
+ <main>
22
+ <div class="metrics-grid">
23
+ <div class="card">
24
+ <div class="card-title">Active Routes</div>
25
+ <div class="card-value" id="routesCount">-</div>
26
+ <div class="card-subtitle">Endpoints loaded in memory</div>
27
+ </div>
28
+ <div class="card">
29
+ <div class="card-title">Security State</div>
30
+ <div class="card-value" id="securityState">Strict</div>
31
+ <div class="card-subtitle" id="securitySubtitle">CORS / Trust Proxy</div>
32
+ </div>
33
+ <div class="card">
34
+ <div class="card-title">Environment</div>
35
+ <div class="card-value" id="envState">Node.js</div>
36
+ <div class="card-subtitle" id="envSubtitle">Version</div>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="search-container">
41
+ <input type="text" id="searchInput" class="search-input" placeholder="Search routes by path or method (e.g. GET /users)...">
42
+ </div>
43
+
44
+ <div class="routes-list" id="routesList">
45
+ <!-- Routes injected here -->
46
+ </div>
47
+
48
+ <div class="config-container">
49
+ <h2>Configuration Inspector</h2>
50
+ <div class="config-grid" id="configGrid">
51
+ <!-- Config injected here -->
52
+ </div>
53
+ </div>
54
+ </main>
55
+
56
+ <script>
57
+ let allRoutes = [];
58
+
59
+ async function fetchState() {
60
+ try {
61
+ const res = await fetch('/api/state');
62
+ if (!res.ok) throw new Error('Failed to fetch state');
63
+ const data = await res.json();
64
+
65
+ // Update port
66
+ document.getElementById('portLabel').textContent = window.location.origin;
67
+
68
+ // Update Routes Count
69
+ document.getElementById('routesCount').textContent = data.routes.length;
70
+
71
+ // Update Env
72
+ document.getElementById('envState').textContent = data.env.platform;
73
+ document.getElementById('envSubtitle').textContent = data.env.nodeVersion;
74
+
75
+ // Update Security
76
+ const isStrict = data.config.server?.cors === false || data.config.server?.cors === undefined;
77
+ document.getElementById('securityState').textContent = isStrict ? 'Strict' : 'Permissive';
78
+ document.getElementById('securitySubtitle').textContent = `Proxy: ${data.config.trustProxy ? 'Trusted' : 'Untrusted'}`;
79
+
80
+ allRoutes = data.routes;
81
+ renderRoutes(allRoutes);
82
+ renderConfig(data.config);
83
+ } catch (err) {
84
+ console.error(err);
85
+ }
86
+ }
87
+
88
+ function copyToClipboard(text) {
89
+ navigator.clipboard.writeText(text).then(() => {
90
+ // Could show a toast here
91
+ });
92
+ }
93
+
94
+ function toggleRoute(index) {
95
+ const el = document.getElementById(`details-${index}`);
96
+ if (el) {
97
+ el.classList.toggle('expanded');
98
+ }
99
+ }
100
+
101
+ function renderRoutes(routes) {
102
+ const container = document.getElementById('routesList');
103
+ container.innerHTML = '';
104
+
105
+ if (routes.length === 0) {
106
+ container.innerHTML = '<div style="color: var(--text-muted); padding: 1rem;">No routes found.</div>';
107
+ return;
108
+ }
109
+
110
+ routes.forEach((r, idx) => {
111
+ const method = r.method.toUpperCase();
112
+ const path = r.path;
113
+
114
+ const item = document.createElement('div');
115
+ item.className = 'route-item';
116
+
117
+ item.innerHTML = `
118
+ <div class="route-header" onclick="toggleRoute(${idx})">
119
+ <div class="method-pill method-${method}">${method}</div>
120
+ <div class="route-path" title="Click to expand/collapse">${path}</div>
121
+ <div class="badges">
122
+ <div class="badge ${r.auth !== 'public' ? 'active' : ''}">AUTH: ${r.auth}</div>
123
+ <div class="badge ${r.rateLimit !== 'none' ? 'active' : ''}">RL: ${r.rateLimit}</div>
124
+ <div class="badge ${r.cache !== 'none' ? 'active' : ''}">CACHE: ${r.cache}</div>
125
+ </div>
126
+ </div>
127
+ <div class="route-details" id="details-${idx}">
128
+ <div class="detail-row">
129
+ <div class="detail-label">Body</div>
130
+ <div class="detail-value">${r.hasBodySchema ? 'Schema Validated' : 'Any'}</div>
131
+ </div>
132
+ <div class="detail-row">
133
+ <div class="detail-label">Query</div>
134
+ <div class="detail-value">${r.hasQuerySchema ? 'Schema Validated' : 'Any'}</div>
135
+ </div>
136
+ <div class="detail-row">
137
+ <div class="detail-label">Params</div>
138
+ <div class="detail-value">${r.hasParamsSchema ? 'Schema Validated' : 'Any'}</div>
139
+ </div>
140
+ <div class="detail-row">
141
+ <div class="detail-label">Response</div>
142
+ <div class="detail-value">${r.hasResponseSchema ? 'Schema Validated' : 'Any'}</div>
143
+ </div>
144
+ </div>
145
+ `;
146
+
147
+ container.appendChild(item);
148
+ });
149
+ }
150
+
151
+ function renderConfig(config, prefix = '') {
152
+ const container = document.getElementById('configGrid');
153
+ if (prefix === '') container.innerHTML = '';
154
+
155
+ for (const [key, val] of Object.entries(config)) {
156
+ const fullKey = prefix ? `${prefix}.${key}` : key;
157
+
158
+ if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
159
+ renderConfig(val, fullKey);
160
+ } else {
161
+ let displayVal = val;
162
+ if (typeof val === 'string') displayVal = `"${val}"`;
163
+ if (Array.isArray(val)) displayVal = `[${val.join(', ')}]`;
164
+ if (val === undefined) displayVal = 'undefined';
165
+ if (val === null) displayVal = 'null';
166
+
167
+ const keyEl = document.createElement('div');
168
+ keyEl.className = 'config-key';
169
+ keyEl.textContent = fullKey;
170
+
171
+ const valEl = document.createElement('div');
172
+ valEl.className = 'config-val';
173
+ valEl.textContent = displayVal;
174
+
175
+ container.appendChild(keyEl);
176
+ container.appendChild(valEl);
177
+ }
178
+ }
179
+ }
180
+
181
+ document.getElementById('searchInput').addEventListener('input', (e) => {
182
+ const query = e.target.value.toLowerCase();
183
+ const filtered = allRoutes.filter(r =>
184
+ r.path.toLowerCase().includes(query) ||
185
+ r.method.toLowerCase().includes(query)
186
+ );
187
+ renderRoutes(filtered);
188
+ });
189
+
190
+ document.getElementById('refreshBtn').addEventListener('click', fetchState);
191
+
192
+ // Initial load
193
+ fetchState();
194
+ </script>
195
+ </body>
196
+ </html>
package/src/dashboard.js CHANGED
@@ -1,17 +1,20 @@
1
1
  import http from 'http';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import url from 'url';
5
+
6
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
4
7
 
5
8
  /**
6
9
  * Local Developer Dashboard Server
7
10
  * Serves a dashboard UI that visualizes routes, telemetry, and configuration.
8
11
  */
9
- export function startDashboard(globalConfig, routes) {
12
+ export function startDashboard(globalConfig, routeRegistryOrGetter) {
10
13
  const host = '127.0.0.1'; // Strict local binding
11
14
  const basePort = globalConfig.server?.port || globalConfig.port || process.env.PORT || 3000;
12
15
  const port = Number(basePort) + 1;
13
16
 
14
- const server = http.createServer((req, res) => {
17
+ const server = http.createServer(async (req, res) => {
15
18
  // Restrict access to localhost strictly
16
19
  if (req.socket.remoteAddress !== '127.0.0.1' && req.socket.remoteAddress !== '::1') {
17
20
  res.writeHead(403);
@@ -20,52 +23,56 @@ export function startDashboard(globalConfig, routes) {
20
23
  }
21
24
 
22
25
  if (req.url === '/') {
23
- res.writeHead(200, { 'Content-Type': 'text/html' });
24
- res.end(`
25
- <!DOCTYPE html>
26
- <html>
27
- <head>
28
- <title>bro.js Dev Dashboard</title>
29
- <style>
30
- body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
31
- h1 { color: #38bdf8; }
32
- .card { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; }
33
- pre { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; }
34
- </style>
35
- </head>
36
- <body>
37
- <h1>bro.js Dev Dashboard</h1>
38
-
39
- <div class="card">
40
- <h2>Configuration (Redacted)</h2>
41
- <pre id="config"></pre>
42
- </div>
43
-
44
- <div class="card">
45
- <h2>Active Routes</h2>
46
- <pre id="routes"></pre>
47
- </div>
48
-
49
- <script>
50
- fetch('/api/state').then(r => r.json()).then(data => {
51
- document.getElementById('config').textContent = JSON.stringify(data.config, null, 2);
52
- document.getElementById('routes').textContent = JSON.stringify(data.routes, null, 2);
53
- });
54
- </script>
55
- </body>
56
- </html>
57
- `);
26
+ try {
27
+ const html = await fs.promises.readFile(path.join(__dirname, 'dashboard', 'dashboard.html'), 'utf-8');
28
+ res.writeHead(200, { 'Content-Type': 'text/html' });
29
+ res.end(html);
30
+ } catch (err) {
31
+ res.writeHead(500);
32
+ res.end('Error loading dashboard UI');
33
+ }
34
+ } else if (req.url === '/dashboard.css') {
35
+ try {
36
+ const css = await fs.promises.readFile(path.join(__dirname, 'dashboard', 'dashboard.css'), 'utf-8');
37
+ res.writeHead(200, { 'Content-Type': 'text/css' });
38
+ res.end(css);
39
+ } catch (err) {
40
+ res.writeHead(500);
41
+ res.end('Error loading dashboard CSS');
42
+ }
58
43
  } else if (req.url === '/api/state') {
59
44
  res.writeHead(200, { 'Content-Type': 'application/json' });
60
45
 
61
46
  // Strict Redaction of Secrets
62
- const redactedConfig = JSON.parse(JSON.stringify(globalConfig));
47
+ const redactedConfig = JSON.parse(JSON.stringify(globalConfig, (k, v) => typeof v === 'function' ? '[Function]' : v));
63
48
  if (redactedConfig.auth?.jwtSecret) redactedConfig.auth.jwtSecret = '***REDACTED***';
64
49
  if (redactedConfig.db) redactedConfig.db = '[Database Instance]';
50
+ if (redactedConfig.redisUrl) redactedConfig.redisUrl = '***REDACTED***';
51
+
52
+ // Dynamic live state resolution
53
+ const routes = typeof routeRegistryOrGetter === 'function'
54
+ ? routeRegistryOrGetter()
55
+ : (routeRegistryOrGetter.getRoutes ? routeRegistryOrGetter.getRoutes() : routeRegistryOrGetter);
56
+
57
+ const normalizedRoutes = (routes || []).map(r => ({
58
+ method: r.method || 'GET',
59
+ path: r.path || r.routePath || r.url || '/',
60
+ auth: r.auth ? (typeof r.auth === 'string' ? r.auth : 'required') : 'public',
61
+ rateLimit: r.rateLimit ? `${r.rateLimit.max} req / ${r.rateLimit.windowMs / 1000}s` : 'none',
62
+ cache: r.cache ? `${r.cache}s` : 'none',
63
+ hasBodySchema: Boolean(r.body || r.schema?.body),
64
+ hasQuerySchema: Boolean(r.query || r.schema?.query),
65
+ hasParamsSchema: Boolean(r.params || r.schema?.params),
66
+ hasResponseSchema: Boolean(r.response || r.schema?.response)
67
+ }));
65
68
 
66
69
  res.end(JSON.stringify({
67
70
  config: redactedConfig,
68
- routes: routes.map(r => ({ method: r.method, path: r.routePath, auth: r.auth }))
71
+ routes: normalizedRoutes,
72
+ env: {
73
+ platform: process.platform,
74
+ nodeVersion: process.version
75
+ }
69
76
  }));
70
77
  } else {
71
78
  res.writeHead(404);
@@ -74,7 +81,7 @@ export function startDashboard(globalConfig, routes) {
74
81
  });
75
82
 
76
83
  server.listen(port, host, () => {
77
- console.log(`[bro.js] 🛠️ Dev Dashboard running locally at http://${host}:${port}`);
84
+ console.log(`[bro.js] Dev Dashboard running locally at http://${host}:${port}`);
78
85
  });
79
86
 
80
87
  return server;
package/src/locale.js CHANGED
@@ -81,8 +81,15 @@ export async function loadLocale(directory, options = {}) {
81
81
 
82
82
  const messages = {};
83
83
  for (const file of files) {
84
- const module = await import(`${pathToFileURL(path.join(directory, file)).href}?update=${crypto.randomUUID()}`);
85
- const catalog = module.default || module.messages || module;
84
+ const fullPath = path.join(directory, file);
85
+ let catalog;
86
+ if (file.endsWith('.json')) {
87
+ catalog = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
88
+ } else {
89
+ const module = await import(`${pathToFileURL(fullPath).href}?update=${crypto.randomUUID()}`);
90
+ catalog = module.default || module.messages || module;
91
+ }
92
+
86
93
  if (catalog && typeof catalog === 'object') {
87
94
  messages[localeFromFilename(file)] = catalog;
88
95
  }
package/src/tasks.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
+ import crypto from 'node:crypto';
4
5
  import cron from 'node-cron';
5
6
  import { colors } from './logger.js';
6
7
  import { scanDir } from './router.js';
@@ -27,7 +28,7 @@ export async function scanTasks(ctx) {
27
28
  let count = 0;
28
29
  for (const file of files) {
29
30
  try {
30
- const moduleUrl = pathToFileURL(file).href;
31
+ const moduleUrl = `${pathToFileURL(file).href}?update=${crypto.randomUUID()}`;
31
32
  const taskModule = await import(moduleUrl);
32
33
 
33
34
  if (taskModule.cron && typeof taskModule.handler === 'function') {