lopata 0.0.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 +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import type { Plugin, ViteDevServer } from "vite";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
|
|
5
|
+
interface DevServerPluginOptions {
|
|
6
|
+
configPath?: string;
|
|
7
|
+
envName: string;
|
|
8
|
+
auxiliaryWorkers?: { configPath: string }[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Main Vite dev server middleware plugin. Intercepts SSR requests and
|
|
13
|
+
* dispatches them through the worker's fetch() handler with Bunflare
|
|
14
|
+
* bindings as the env object.
|
|
15
|
+
*
|
|
16
|
+
* Returns a callback from configureServer (post-middleware) so that
|
|
17
|
+
* framework plugins (React Router, SolidStart, etc.) get first crack
|
|
18
|
+
* at requests. Bunflare acts as the fallback.
|
|
19
|
+
*
|
|
20
|
+
* Also sets up:
|
|
21
|
+
* - Request-level tracing (startSpan around fetch)
|
|
22
|
+
* - Dashboard routes (/__dashboard)
|
|
23
|
+
* - WebSocket trace streaming (/__dashboard/api/traces/ws)
|
|
24
|
+
* - Error page rendering with trace context
|
|
25
|
+
*
|
|
26
|
+
* The plugin is externalized by Vite's config bundler (it's in node_modules
|
|
27
|
+
* via link:), so dynamic imports here run through Bun's native loader.
|
|
28
|
+
*/
|
|
29
|
+
export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
30
|
+
let server: ViteDevServer;
|
|
31
|
+
let config: any;
|
|
32
|
+
let env: Record<string, unknown>;
|
|
33
|
+
let registry: any;
|
|
34
|
+
let workerRegistry: any;
|
|
35
|
+
|
|
36
|
+
// Lazy-loaded runtime functions
|
|
37
|
+
let wireClassRefs: Function;
|
|
38
|
+
let setGlobalEnv: Function;
|
|
39
|
+
let ExecutionContext: new () => any;
|
|
40
|
+
|
|
41
|
+
// Tracing functions (lazy-loaded)
|
|
42
|
+
let startSpan: Function;
|
|
43
|
+
let setSpanAttribute: Function;
|
|
44
|
+
let getActiveContext: Function;
|
|
45
|
+
let renderErrorPage: Function;
|
|
46
|
+
let handleDashboardRequest: Function;
|
|
47
|
+
let getTraceStore: Function;
|
|
48
|
+
|
|
49
|
+
// Track current module to detect when Vite HMR invalidates it
|
|
50
|
+
let currentModule: Record<string, unknown> | null = null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: "bunflare:dev-server",
|
|
54
|
+
|
|
55
|
+
async configureServer(viteServer: ViteDevServer) {
|
|
56
|
+
server = viteServer;
|
|
57
|
+
const projectRoot = server.config.root;
|
|
58
|
+
|
|
59
|
+
// Deeper stacks in dev mode
|
|
60
|
+
Error.stackTraceLimit = 50;
|
|
61
|
+
|
|
62
|
+
// Lazy import runtime modules — runs through Bun's native loader
|
|
63
|
+
const configMod = await import("../config.ts");
|
|
64
|
+
const envMod = await import("../env.ts");
|
|
65
|
+
const ecMod = await import("../execution-context.ts");
|
|
66
|
+
const spanMod = await import("../tracing/span.ts");
|
|
67
|
+
const ctxMod = await import("../tracing/context.ts");
|
|
68
|
+
const errorPageMod = await import("../error-page/build.ts");
|
|
69
|
+
const dashboardMod = await import("../dashboard/api.ts");
|
|
70
|
+
const traceMod = await import("../tracing/store.ts");
|
|
71
|
+
|
|
72
|
+
wireClassRefs = envMod.wireClassRefs;
|
|
73
|
+
setGlobalEnv = envMod.setGlobalEnv;
|
|
74
|
+
ExecutionContext = ecMod.ExecutionContext;
|
|
75
|
+
startSpan = spanMod.startSpan;
|
|
76
|
+
setSpanAttribute = spanMod.setSpanAttribute;
|
|
77
|
+
getActiveContext = ctxMod.getActiveContext;
|
|
78
|
+
renderErrorPage = errorPageMod.renderErrorPage;
|
|
79
|
+
handleDashboardRequest = dashboardMod.handleDashboardRequest;
|
|
80
|
+
getTraceStore = traceMod.getTraceStore;
|
|
81
|
+
|
|
82
|
+
// 1. Load wrangler config
|
|
83
|
+
if (options.configPath) {
|
|
84
|
+
config = await configMod.loadConfig(resolve(projectRoot, options.configPath));
|
|
85
|
+
} else {
|
|
86
|
+
config = await configMod.autoLoadConfig(projectRoot);
|
|
87
|
+
}
|
|
88
|
+
console.log(`[bunflare:vite] Loaded config: ${config.name}`);
|
|
89
|
+
|
|
90
|
+
// 2. Build env with bindings
|
|
91
|
+
const built = envMod.buildEnv(config, projectRoot);
|
|
92
|
+
env = built.env;
|
|
93
|
+
registry = built.registry;
|
|
94
|
+
|
|
95
|
+
// Set globalEnv immediately so that top-level module code
|
|
96
|
+
// (e.g. `import { env } from "cloudflare:workers"`) sees bindings
|
|
97
|
+
// before the first request triggers worker module import.
|
|
98
|
+
// Also set globalThis.__bunflare_env — the modules-plugin env proxy
|
|
99
|
+
// reads from this, bridging the Vite SSR runner ↔ native module graphs.
|
|
100
|
+
setGlobalEnv(env);
|
|
101
|
+
(globalThis as any).__bunflare_env = env;
|
|
102
|
+
(globalThis as any).__bunflare_startSpan = startSpan;
|
|
103
|
+
(globalThis as any).__bunflare_setSpanStatus = spanMod.setSpanStatus;
|
|
104
|
+
|
|
105
|
+
// Propagate string vars/secrets to process.env so libraries
|
|
106
|
+
// that read process.env (e.g. better-auth, Sentry) see them.
|
|
107
|
+
for (const [key, value] of Object.entries(env)) {
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
process.env[key] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. Set up dashboard context
|
|
114
|
+
dashboardMod.setDashboardConfig(config);
|
|
115
|
+
|
|
116
|
+
// 4. Set up auxiliary workers (if configured)
|
|
117
|
+
if (options.auxiliaryWorkers && options.auxiliaryWorkers.length > 0) {
|
|
118
|
+
await import("../plugin.ts");
|
|
119
|
+
|
|
120
|
+
const { WorkerRegistry } = await import("../worker-registry.ts");
|
|
121
|
+
const { GenerationManager } = await import("../generation-manager.ts");
|
|
122
|
+
|
|
123
|
+
workerRegistry = new WorkerRegistry();
|
|
124
|
+
|
|
125
|
+
const mainAdapter = {
|
|
126
|
+
config,
|
|
127
|
+
gracePeriodMs: 0,
|
|
128
|
+
get active() {
|
|
129
|
+
return currentModule ? { workerModule: currentModule, env } : null;
|
|
130
|
+
},
|
|
131
|
+
list() {
|
|
132
|
+
return [];
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
workerRegistry.register(config.name, mainAdapter as any, true);
|
|
136
|
+
|
|
137
|
+
for (const workerDef of options.auxiliaryWorkers) {
|
|
138
|
+
const auxConfigPath = resolve(projectRoot, workerDef.configPath);
|
|
139
|
+
const auxBaseDir = dirname(auxConfigPath);
|
|
140
|
+
const auxConfig = await configMod.loadConfig(auxConfigPath);
|
|
141
|
+
console.log(`[bunflare:vite] Auxiliary worker: ${auxConfig.name}`);
|
|
142
|
+
|
|
143
|
+
const auxManager = new GenerationManager(auxConfig, auxBaseDir, {
|
|
144
|
+
workerName: auxConfig.name,
|
|
145
|
+
workerRegistry,
|
|
146
|
+
isMain: false,
|
|
147
|
+
});
|
|
148
|
+
workerRegistry.register(auxConfig.name, auxManager);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const gen = await auxManager.reload();
|
|
152
|
+
console.log(`[bunflare:vite] Auxiliary worker "${auxConfig.name}" loaded (gen ${gen.id})`);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`[bunflare:vite] Failed to load auxiliary worker "${auxConfig.name}":`, err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
dashboardMod.setWorkerRegistry(workerRegistry);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 5. Set up WebSocket trace streaming on httpServer
|
|
162
|
+
setupTraceWebSocket(server);
|
|
163
|
+
|
|
164
|
+
// 6. Return middleware callback (post-middleware — runs after framework plugins)
|
|
165
|
+
return () => {
|
|
166
|
+
server.middlewares.use(async (req: IncomingMessage, res: ServerResponse, next: Function) => {
|
|
167
|
+
const url = req.url;
|
|
168
|
+
if (!url) return next();
|
|
169
|
+
|
|
170
|
+
// Skip Vite internal paths
|
|
171
|
+
if (url.startsWith("/@") || url.startsWith("/__vite") || url.startsWith("/node_modules/")) {
|
|
172
|
+
return next();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Dashboard routes (HTML, assets, RPC)
|
|
176
|
+
if (url.startsWith("/__dashboard")) {
|
|
177
|
+
// WebSocket upgrades are handled separately via httpServer upgrade event
|
|
178
|
+
if (url.startsWith("/__dashboard/api/traces/ws")) return next();
|
|
179
|
+
try {
|
|
180
|
+
const request = nodeReqToRequest(req);
|
|
181
|
+
const response = await (handleDashboardRequest as (r: Request) => Response | Promise<Response>)(request);
|
|
182
|
+
writeResponse(response, res);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error("[bunflare:vite] Dashboard error:", err);
|
|
185
|
+
if (!res.headersSent) {
|
|
186
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
187
|
+
res.end(String(err));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const ssrEnv = server.environments[options.envName];
|
|
195
|
+
if (!ssrEnv || !("runner" in ssrEnv)) {
|
|
196
|
+
console.error(`[bunflare:vite] SSR environment "${options.envName}" not found or has no runner`);
|
|
197
|
+
return next();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const entrypoint = resolve(server.config.root, config.main);
|
|
201
|
+
const workerModule = await (ssrEnv as any).runner.import(entrypoint) as Record<string, unknown>;
|
|
202
|
+
|
|
203
|
+
// Re-wire class refs when module changes (HMR invalidation)
|
|
204
|
+
if (workerModule !== currentModule) {
|
|
205
|
+
currentModule = workerModule;
|
|
206
|
+
|
|
207
|
+
// Invalidate virtual modules in SSR environment.
|
|
208
|
+
// Framework plugins (e.g. React Router) may only invalidate
|
|
209
|
+
// virtual modules in the client module graph during HMR,
|
|
210
|
+
// leaving stale versions in the SSR environment.
|
|
211
|
+
invalidateVirtualModules(ssrEnv);
|
|
212
|
+
|
|
213
|
+
wireClassRefs(registry, workerModule, env, workerRegistry);
|
|
214
|
+
setGlobalEnv(env);
|
|
215
|
+
console.log("[bunflare:vite] Worker module (re)loaded, classes wired");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const request = nodeReqToRequest(req);
|
|
219
|
+
const parsedUrl = new URL(request.url);
|
|
220
|
+
|
|
221
|
+
const handler = workerModule.default as Record<string, unknown>;
|
|
222
|
+
if (!handler || typeof handler.fetch !== "function") {
|
|
223
|
+
console.error("[bunflare:vite] Worker module default export has no fetch() method");
|
|
224
|
+
return next();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Capture caller stack before entering the worker (for async stack stitching)
|
|
228
|
+
const callerStack = new Error();
|
|
229
|
+
|
|
230
|
+
const response = await (startSpan as Function)({
|
|
231
|
+
name: `${request.method} ${parsedUrl.pathname}`,
|
|
232
|
+
kind: "server",
|
|
233
|
+
attributes: { "http.method": request.method, "http.url": request.url },
|
|
234
|
+
}, async () => {
|
|
235
|
+
try {
|
|
236
|
+
const ctx = new ExecutionContext();
|
|
237
|
+
const resp = await (handler.fetch as Function).call(handler, request, env, ctx) as Response;
|
|
238
|
+
(setSpanAttribute as Function)("http.status_code", resp.status);
|
|
239
|
+
ctx._awaitAll().catch(() => {});
|
|
240
|
+
return resp;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err instanceof Error) {
|
|
243
|
+
stitchAsyncStack(err, callerStack);
|
|
244
|
+
}
|
|
245
|
+
console.error("[bunflare:vite] Request error:\n" + (err instanceof Error ? err.stack : String(err)));
|
|
246
|
+
return (renderErrorPage as Function)(err, request, env, config);
|
|
247
|
+
}
|
|
248
|
+
}) as Response;
|
|
249
|
+
|
|
250
|
+
writeResponse(response, res);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error("[bunflare:vite] Request error:", err);
|
|
253
|
+
if (!res.headersSent) {
|
|
254
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
255
|
+
res.end(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function setupTraceWebSocket(server: ViteDevServer) {
|
|
264
|
+
const httpServer = (server as any).httpServer;
|
|
265
|
+
if (!httpServer) return;
|
|
266
|
+
|
|
267
|
+
// Dynamically import ws (available as Vite dependency)
|
|
268
|
+
import("ws").then(({ WebSocketServer }) => {
|
|
269
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
270
|
+
|
|
271
|
+
httpServer.on("upgrade", (req: IncomingMessage, socket: any, head: Buffer) => {
|
|
272
|
+
const url = req.url ?? "";
|
|
273
|
+
if (!url.startsWith("/__dashboard/api/traces/ws")) return;
|
|
274
|
+
|
|
275
|
+
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
276
|
+
const store = getTraceStore();
|
|
277
|
+
let filter: { path?: string; status?: string; attributeFilters?: Array<{ key: string; value: string; type: "include" | "exclude" }> } = {};
|
|
278
|
+
let buffer: any[] = [];
|
|
279
|
+
const MAX_BUFFER = 1000;
|
|
280
|
+
const allowedTraces = new Set<string>();
|
|
281
|
+
const excludedTraces = new Set<string>();
|
|
282
|
+
|
|
283
|
+
function isRootSpanFiltered(span: { name: string; status: string; parentSpanId: string | null; attributes: Record<string, unknown> }): boolean {
|
|
284
|
+
if (filter.status && filter.status !== "all") {
|
|
285
|
+
if (span.status !== "unset" && span.status !== filter.status) return true;
|
|
286
|
+
}
|
|
287
|
+
if (filter.path) {
|
|
288
|
+
if (!matchGlob(span.name, filter.path)) return true;
|
|
289
|
+
}
|
|
290
|
+
if (filter.attributeFilters && filter.attributeFilters.length > 0) {
|
|
291
|
+
const attrs = span.attributes;
|
|
292
|
+
for (const af of filter.attributeFilters) {
|
|
293
|
+
const val = attrs[af.key];
|
|
294
|
+
const matches = val !== undefined && String(val).toLowerCase().includes(af.value.toLowerCase());
|
|
295
|
+
if (af.type === "include" && !matches) return true;
|
|
296
|
+
if (af.type === "exclude" && matches) return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const unsubscribe = store.subscribe((event: any) => {
|
|
303
|
+
const traceId = event.type === "span.event" ? event.event.traceId : event.span.traceId;
|
|
304
|
+
if ((event.type === "span.start" || event.type === "span.end") && event.span.parentSpanId === null) {
|
|
305
|
+
if (isRootSpanFiltered(event.span)) {
|
|
306
|
+
excludedTraces.add(traceId);
|
|
307
|
+
allowedTraces.delete(traceId);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
excludedTraces.delete(traceId);
|
|
311
|
+
allowedTraces.add(traceId);
|
|
312
|
+
} else {
|
|
313
|
+
if (excludedTraces.has(traceId)) return;
|
|
314
|
+
}
|
|
315
|
+
if (buffer.length < MAX_BUFFER) {
|
|
316
|
+
buffer.push(event);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const interval = setInterval(() => {
|
|
321
|
+
if (buffer.length > 0) {
|
|
322
|
+
ws.send(JSON.stringify({ type: "batch", events: buffer }));
|
|
323
|
+
buffer = [];
|
|
324
|
+
}
|
|
325
|
+
}, 500);
|
|
326
|
+
|
|
327
|
+
// Parse filter from query params
|
|
328
|
+
try {
|
|
329
|
+
const reqUrl = new URL(req.url ?? "", `http://${req.headers.host ?? "localhost"}`);
|
|
330
|
+
const statusParam = reqUrl.searchParams.get("status");
|
|
331
|
+
const pathParam = reqUrl.searchParams.get("path");
|
|
332
|
+
if (statusParam) filter.status = statusParam;
|
|
333
|
+
if (pathParam) filter.path = pathParam;
|
|
334
|
+
} catch {}
|
|
335
|
+
|
|
336
|
+
let sinceMs = 15 * 60 * 1000;
|
|
337
|
+
const since = Date.now() - sinceMs;
|
|
338
|
+
const recent = store.getRecentTraces(since, 200);
|
|
339
|
+
ws.send(JSON.stringify({ type: "initial", traces: recent }));
|
|
340
|
+
|
|
341
|
+
ws.on("message", (data: any) => {
|
|
342
|
+
try {
|
|
343
|
+
const msg = JSON.parse(typeof data === "string" ? data : data.toString());
|
|
344
|
+
if (msg.type === "filter") {
|
|
345
|
+
filter = { path: msg.path, status: msg.status, attributeFilters: msg.attributeFilters };
|
|
346
|
+
if (msg.sinceMs !== undefined) sinceMs = msg.sinceMs;
|
|
347
|
+
allowedTraces.clear();
|
|
348
|
+
excludedTraces.clear();
|
|
349
|
+
const freshSince = sinceMs > 0 ? Date.now() - sinceMs : 0;
|
|
350
|
+
const freshTraces = store.getRecentTraces(freshSince, 200);
|
|
351
|
+
ws.send(JSON.stringify({ type: "initial", traces: freshTraces }));
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
ws.on("close", () => {
|
|
357
|
+
unsubscribe();
|
|
358
|
+
clearInterval(interval);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
console.log("[bunflare:vite] Dashboard: http://localhost:5173/__dashboard");
|
|
364
|
+
}).catch(() => {
|
|
365
|
+
// ws not available — trace streaming disabled
|
|
366
|
+
console.log("[bunflare:vite] Dashboard available (trace streaming disabled — ws package not found)");
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function invalidateVirtualModules(ssrEnv: any): void {
|
|
372
|
+
// Only invalidate Bunflare's own virtual modules, NOT framework modules
|
|
373
|
+
// (e.g. React Router's virtual:react-router/server-manifest uses
|
|
374
|
+
// Math.random() for dev version — invalidating it generates a new version
|
|
375
|
+
// that mismatches the client's cached manifest, causing
|
|
376
|
+
// "manifest version mismatch during eager route discovery" errors).
|
|
377
|
+
const isBunflareModule = (id: string) =>
|
|
378
|
+
id.includes("\0cloudflare:") || id.includes("\0@cloudflare/");
|
|
379
|
+
|
|
380
|
+
// Server-side: clear transformResult so fetchModule returns fresh code
|
|
381
|
+
const modGraph = ssrEnv.moduleGraph;
|
|
382
|
+
if (modGraph?.idToModuleMap) {
|
|
383
|
+
for (const [id, mod] of modGraph.idToModuleMap) {
|
|
384
|
+
if (isBunflareModule(id)) {
|
|
385
|
+
modGraph.invalidateModule(mod);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Runner-side: clear evaluated state so next import re-evaluates
|
|
391
|
+
const runner = ssrEnv.runner;
|
|
392
|
+
const evaluatedModules = runner?.evaluatedModules;
|
|
393
|
+
if (evaluatedModules?.idToModuleMap) {
|
|
394
|
+
for (const [id, node] of evaluatedModules.idToModuleMap) {
|
|
395
|
+
if (isBunflareModule(id)) {
|
|
396
|
+
evaluatedModules.invalidateModule(node);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stitchAsyncStack(err: Error, callerError: Error | null): void {
|
|
403
|
+
if (!callerError) return;
|
|
404
|
+
if (!err.stack || !callerError.stack) return;
|
|
405
|
+
if (err.stack.includes("--- async ---")) return;
|
|
406
|
+
|
|
407
|
+
const errFrames = err.stack.split("\n").filter(l => l.trim().startsWith("at "));
|
|
408
|
+
const looksShort = errFrames.length <= 5 || err.stack.includes("processTicksAndRejections");
|
|
409
|
+
if (!looksShort) return;
|
|
410
|
+
|
|
411
|
+
const callerLines = callerError.stack.split("\n").slice(1);
|
|
412
|
+
const filtered = callerLines.filter(l => !l.includes("/bunflare/runtime/"));
|
|
413
|
+
if (filtered.length === 0) return;
|
|
414
|
+
|
|
415
|
+
err.stack += "\n --- async ---\n" + filtered.join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function matchGlob(text: string, pattern: string): boolean {
|
|
419
|
+
const regex = pattern
|
|
420
|
+
.replace(/\*\*/g, "\0")
|
|
421
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
422
|
+
.replace(/\0/g, ".*")
|
|
423
|
+
.replace(/\*/g, "[^/]*");
|
|
424
|
+
return new RegExp(`^${regex}`).test(text);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function nodeReqToRequest(req: IncomingMessage): Request {
|
|
428
|
+
const protocol = "http";
|
|
429
|
+
const host = req.headers.host ?? "localhost";
|
|
430
|
+
const url = `${protocol}://${host}${req.url}`;
|
|
431
|
+
|
|
432
|
+
const headers = new Headers();
|
|
433
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
434
|
+
if (value === undefined) continue;
|
|
435
|
+
if (Array.isArray(value)) {
|
|
436
|
+
for (const v of value) headers.append(key, v);
|
|
437
|
+
} else {
|
|
438
|
+
headers.set(key, value);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const method = req.method ?? "GET";
|
|
443
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
444
|
+
|
|
445
|
+
return new Request(url, {
|
|
446
|
+
method,
|
|
447
|
+
headers,
|
|
448
|
+
body: hasBody ? nodeStreamToReadable(req) : undefined,
|
|
449
|
+
// @ts-expect-error duplex is needed for streaming request bodies
|
|
450
|
+
duplex: hasBody ? "half" : undefined,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function nodeStreamToReadable(stream: IncomingMessage): ReadableStream<Uint8Array> {
|
|
455
|
+
return new ReadableStream({
|
|
456
|
+
start(controller) {
|
|
457
|
+
stream.on("data", (chunk: Buffer) => {
|
|
458
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
459
|
+
});
|
|
460
|
+
stream.on("end", () => {
|
|
461
|
+
controller.close();
|
|
462
|
+
});
|
|
463
|
+
stream.on("error", (err) => {
|
|
464
|
+
controller.error(err);
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function writeResponse(response: Response, res: ServerResponse): Promise<void> {
|
|
471
|
+
const headerRecord: Record<string, string | string[]> = {};
|
|
472
|
+
response.headers.forEach((value, key) => {
|
|
473
|
+
headerRecord[key] = value;
|
|
474
|
+
});
|
|
475
|
+
res.writeHead(response.status, headerRecord);
|
|
476
|
+
|
|
477
|
+
if (!response.body) {
|
|
478
|
+
res.end();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const reader = response.body.getReader();
|
|
483
|
+
try {
|
|
484
|
+
while (true) {
|
|
485
|
+
const { done, value } = await reader.read();
|
|
486
|
+
if (done) break;
|
|
487
|
+
res.write(value);
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
reader.releaseLock();
|
|
491
|
+
res.end();
|
|
492
|
+
}
|
|
493
|
+
}
|