@bakery-framework/plugin-dashboard 2.0.0-alpha.14 → 2.0.0-alpha.16
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/package.json +4 -4
- package/src/client/parts/logs.ts +9 -2
- package/src/endpoints/logs-socket.ts +72 -0
- package/src/setup.ts +40 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakery-framework/plugin-dashboard",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.16",
|
|
4
4
|
"description": "Bakery dashboard plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bakery",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"!src/tests"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bakery-framework/core": "^2.0.0-alpha.
|
|
37
|
-
"@bakery-framework/plugin-analytics": "^2.0.0-alpha.
|
|
36
|
+
"@bakery-framework/core": "^2.0.0-alpha.16",
|
|
37
|
+
"@bakery-framework/plugin-analytics": "^2.0.0-alpha.16"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@bakery-framework/orm": "^2.0.0-alpha.
|
|
40
|
+
"@bakery-framework/orm": "^2.0.0-alpha.16"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"bun": ">=1.4.0"
|
package/src/client/parts/logs.ts
CHANGED
|
@@ -77,12 +77,19 @@ export function initLogsWebSocket() {
|
|
|
77
77
|
'<div style="color: var(--text-muted);">Connecting to server log stream...</div>'
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
// `/_dashboard/logs`, not `/_livereload`. The live-reload socket is
|
|
81
|
+
// registered only under `DEV`, so this panel answered 400 on every
|
|
82
|
+
// production server and sat on "Connecting..." for ever. The console has
|
|
83
|
+
// its own socket now, behind the console's own door.
|
|
84
|
+
logsWs = new WebSocket(getWebSocketUrl('/_dashboard/logs'))
|
|
81
85
|
|
|
82
86
|
logsWs.onopen = () => {
|
|
83
87
|
consoleEl.innerHTML =
|
|
84
88
|
'<div style="color: var(--ok); display: flex; align-items: center; gap: 0.25rem;"><span>Connected to logs pipeline. Listening for events...</span></div>'
|
|
85
|
-
|
|
89
|
+
// No `subscribe_logger` frame: membership is the handler's `open`, so
|
|
90
|
+
// there is nothing to ask for. That message was `LiveReloadHandler`'s
|
|
91
|
+
// protocol, and it is what an app page still sends to forward its own
|
|
92
|
+
// console in development.
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
logsWs.onmessage = event => {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { WebSocketHandler } from '@bakery-framework/core/handlers'
|
|
2
|
+
import { connectedLoggers } from '@bakery-framework/core/logger'
|
|
3
|
+
import { isAnalyticsAuthorized } from '@bakery-framework/plugin-analytics/stats'
|
|
4
|
+
import type { ServerWebSocket } from 'bun'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The console's log stream.
|
|
8
|
+
*
|
|
9
|
+
* **The Logs panel was dead in production**, and only in production. Its client
|
|
10
|
+
* connected to `/_livereload`, which `LiveReloadHandler` serves — and that
|
|
11
|
+
* handler is registered only under `DEV` and refuses in `canHandle` besides.
|
|
12
|
+
* So a production server answered 400 on the upgrade, nothing ever joined
|
|
13
|
+
* `connectedLoggers`, and the panel sat on "Connecting to server log
|
|
14
|
+
* stream..." for the life of the process. Measured against `bun run serve`
|
|
15
|
+
* before this existed: `/_livereload` → 400, console → 200.
|
|
16
|
+
*
|
|
17
|
+
* Same shape as the bundle ETag bug and the timescale import before it: a dev
|
|
18
|
+
* server takes a different branch, so the thing works on the machine where it
|
|
19
|
+
* is written and nowhere else.
|
|
20
|
+
*
|
|
21
|
+
* **Not fixed by registering `LiveReloadHandler` in production.** That socket
|
|
22
|
+
* is the *watcher's*: it publishes `force_reload` to every subscriber and
|
|
23
|
+
* echoes `client_log` frames between browsers. Shipping it to production would
|
|
24
|
+
* be adding a surface, not repairing one. The console owns its own panel, so
|
|
25
|
+
* it owns its own socket, which is the same split analytics already has with
|
|
26
|
+
* `/_analytics_ws`.
|
|
27
|
+
*
|
|
28
|
+
* Membership is the only thing this does. Core owns the registry
|
|
29
|
+
* (`logger/clients.ts`) and the dashboard's `setLogCallback` does the
|
|
30
|
+
* broadcasting; in development `LiveReloadHandler` adds app pages to the same
|
|
31
|
+
* Set when they forward their console, which is why those lines show up here
|
|
32
|
+
* too.
|
|
33
|
+
*/
|
|
34
|
+
export class DashboardLogsHandler extends WebSocketHandler {
|
|
35
|
+
static override readonly namespace = '/_dashboard'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* **The door is checked here because there is nowhere later.**
|
|
39
|
+
*
|
|
40
|
+
* `router.ts` attempts the upgrade before `onRoute`, before `onRequest` and
|
|
41
|
+
* before any fetch handler resolves, so a plugin hook cannot gate a socket.
|
|
42
|
+
* `AnalyticsWSHandler` carries the same note for the same reason.
|
|
43
|
+
*
|
|
44
|
+
* It is the console's own door: `isAnalyticsAuthorized` is what guards
|
|
45
|
+
* `/_dashboard` itself, so a stream of every server log line is admitted by
|
|
46
|
+
* exactly what admits the page that renders it. Anything weaker would hand
|
|
47
|
+
* out log lines to someone who cannot open the console.
|
|
48
|
+
*
|
|
49
|
+
* Async because `authorize` may be a predicate the application supplies; the
|
|
50
|
+
* registry awaits a promise-returning `canHandle`.
|
|
51
|
+
*
|
|
52
|
+
* The path sits inside `/_dashboard`, which `DashboardHandler` claims for
|
|
53
|
+
* *fetch* at priority 120. That is not a collision: the upgrade is dispatched
|
|
54
|
+
* from a separate registry and runs first, so the fetch handler never sees
|
|
55
|
+
* this request.
|
|
56
|
+
*/
|
|
57
|
+
static override async canHandle(
|
|
58
|
+
path: string,
|
|
59
|
+
req?: Request,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
if (path !== '/_dashboard/logs') return false
|
|
62
|
+
return req ? await isAnalyticsAuthorized(req) : false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static override open(ws: ServerWebSocket<any>) {
|
|
66
|
+
connectedLoggers.add(ws)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static override close(ws: ServerWebSocket<any>) {
|
|
70
|
+
connectedLoggers.delete(ws)
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
handleGetSessions,
|
|
32
32
|
handleUpdateSession,
|
|
33
33
|
} from './endpoints/sessions'
|
|
34
|
+
import { DashboardLogsHandler } from './endpoints/logs-socket'
|
|
34
35
|
import { pluginPath } from './paths'
|
|
35
36
|
import renderDashboardShell from './shell'
|
|
36
37
|
|
|
@@ -129,6 +130,12 @@ export function setupDashboard(
|
|
|
129
130
|
// plugin's own directory — no bespoke asset route, no hand-rolled caching.
|
|
130
131
|
mountRoutes('/_dashboard', pluginPath('../public'))
|
|
131
132
|
|
|
133
|
+
// The console's log stream, and it is registered in every mode. The Logs
|
|
134
|
+
// panel used to ride on `/_livereload`, which exists only under `DEV` — so
|
|
135
|
+
// the panel worked on the machine it was written on and answered 400
|
|
136
|
+
// everywhere else. See `endpoints/logs-socket.ts`.
|
|
137
|
+
Bakery.handlers.websocket.set(DashboardLogsHandler)
|
|
138
|
+
|
|
132
139
|
Bakery.handlers.fetch.set(DashboardHandler, 120)
|
|
133
140
|
}
|
|
134
141
|
|
|
@@ -140,7 +147,7 @@ async function handleDashboardView() {
|
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
/**
|
|
143
|
-
*
|
|
150
|
+
* Why the bundle cannot run in a `<script>`, or `null`.
|
|
144
151
|
*
|
|
145
152
|
* `bundleModule` marks every *installed* package external, so a bare specifier
|
|
146
153
|
* survives into the output and resolves through the page's import map. That is
|
|
@@ -149,19 +156,35 @@ async function handleDashboardView() {
|
|
|
149
156
|
* apply to one, and a top-level `import` is a syntax error — so the *entire*
|
|
150
157
|
* bundle fails to execute and the console does nothing at all.
|
|
151
158
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
159
|
+
* **This asks the engine instead of matching a pattern, and the first version
|
|
160
|
+
* of it matched a pattern and was wrong.** It anchored on `^import`. In
|
|
161
|
+
* development the bundle is not minified and the import starts a line, so it
|
|
162
|
+
* looked right. Production minifies, the import lands mid-line, and the check
|
|
163
|
+
* that existed to catch exactly this missed it — measured by reintroducing the
|
|
164
|
+
* bug and watching a production server answer 200 with the import present.
|
|
165
|
+
*
|
|
166
|
+
* Against seven shapes, the anchored regex was wrong on three: a minified bare
|
|
167
|
+
* import, a side-effect-only `import"./x.css"`, and a top-level `await`. A
|
|
168
|
+
* widened regex got the first two and still missed the third. `new Function`
|
|
169
|
+
* is wrong on none of them, because it is not approximating the question — a
|
|
170
|
+
* function body rejects a top-level `import` and a top-level `await` for the
|
|
171
|
+
* same reasons a classic script does. It also costs less than the widened
|
|
172
|
+
* regex: 13.0 us against 26.4 on a 24 KB bundle, and 12.0 for the broken one.
|
|
173
|
+
*
|
|
174
|
+
* It parses without executing, and only on the build path — the cached branch
|
|
175
|
+
* returns the file and never reaches here.
|
|
157
176
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
177
|
+
* One known difference, and it is in the safe direction: a function body
|
|
178
|
+
* permits a top-level `return`, which a script does not. `Bun.build` does not
|
|
179
|
+
* emit one, and a false pass is a missed report rather than a broken page.
|
|
161
180
|
*/
|
|
162
|
-
export function
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
export function whyUnrunnable(bundle: string): string | null {
|
|
182
|
+
try {
|
|
183
|
+
new Function(bundle)
|
|
184
|
+
return null
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return (error as Error).message
|
|
187
|
+
}
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
/**
|
|
@@ -205,15 +228,15 @@ export async function handleJsAsset() {
|
|
|
205
228
|
)
|
|
206
229
|
}
|
|
207
230
|
|
|
208
|
-
// See `
|
|
231
|
+
// See `whyUnrunnable`. A build that "succeeded" and cannot run is worse than
|
|
209
232
|
// one that failed, because it is silent.
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
233
|
+
const unrunnable = whyUnrunnable(bundleResult.content as string)
|
|
234
|
+
if (unrunnable) {
|
|
212
235
|
pluginLog.DASHBOARD_BUNDLE_ERR({
|
|
213
|
-
error: `dashboard.js
|
|
236
|
+
error: `dashboard.js cannot run as a classic script: ${unrunnable}. The usual cause is an import across a package boundary, which the bundler leaves as a bare specifier for the import map — and an import map does not apply to a classic script.`,
|
|
214
237
|
})
|
|
215
238
|
return response.error(
|
|
216
|
-
`dashboard.js
|
|
239
|
+
`dashboard.js cannot run as a classic script: ${unrunnable}`,
|
|
217
240
|
500,
|
|
218
241
|
)
|
|
219
242
|
}
|