react-bun-ssr 0.1.1 → 0.2.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.
@@ -1,18 +1,23 @@
1
- import { watch, type FSWatcher } from "node:fs";
2
1
  import path from "node:path";
3
2
  import {
4
3
  buildRouteManifest,
5
4
  bundleClientEntries,
6
5
  copyDirRecursive,
7
6
  createBuildManifest,
8
- discoverFileSignature,
9
7
  ensureCleanDirectory,
10
8
  generateClientEntries,
11
9
  } from "../runtime/build-tools";
12
10
  import { loadUserConfig, resolveConfig } from "../runtime/config";
13
- import { ensureDir, existsPath, listEntries, removePath, writeText } from "../runtime/io";
14
- import { createServer } from "../runtime/server";
15
- import type { BuildRouteAsset, FrameworkConfig, ResolvedConfig } from "../runtime/types";
11
+ import { ensureDir, existsPath, writeText, writeTextIfChanged } from "../runtime/io";
12
+ import type { FrameworkConfig, ResolvedConfig } from "../runtime/types";
13
+ import {
14
+ createDevHotEntrypointSource,
15
+ createProductionServerEntrypointSource,
16
+ createTestCommands,
17
+ createTypecheckCommand,
18
+ parseFlags,
19
+ RBSSR_DEV_RESTART_EXIT_CODE,
20
+ } from "./internal";
16
21
  import { scaffoldApp } from "./scaffold";
17
22
 
18
23
  function log(message: string): void {
@@ -20,12 +25,6 @@ function log(message: string): void {
20
25
  console.log(`[rbssr] ${message}`);
21
26
  }
22
27
 
23
- function parseFlags(args: string[]): { force: boolean } {
24
- return {
25
- force: args.includes("--force"),
26
- };
27
- }
28
-
29
28
  async function getConfig(cwd: string): Promise<{ userConfig: FrameworkConfig; resolved: ResolvedConfig }> {
30
29
  const userConfig = await loadUserConfig(cwd);
31
30
  const resolved = resolveConfig(userConfig, cwd);
@@ -37,29 +36,7 @@ async function writeProductionServerEntrypoint(options: { distDir: string }): Pr
37
36
  await ensureDir(serverDir);
38
37
 
39
38
  const serverEntryPath = path.join(serverDir, "server.mjs");
40
- const content = `import path from "node:path";
41
- import config from "../../rbssr.config.ts";
42
- import { startHttpServer } from "../../framework/runtime/index.ts";
43
-
44
- const rootDir = path.resolve(path.dirname(Bun.fileURLToPath(import.meta.url)), "../..");
45
- process.chdir(rootDir);
46
-
47
- const manifestPath = path.resolve(rootDir, "dist/manifest.json");
48
- const manifest = await Bun.file(manifestPath).json();
49
-
50
- startHttpServer({
51
- config: {
52
- ...(config ?? {}),
53
- mode: "production",
54
- },
55
- runtimeOptions: {
56
- dev: false,
57
- buildManifest: manifest,
58
- },
59
- });
60
- `;
61
-
62
- await writeText(serverEntryPath, content);
39
+ await writeText(serverEntryPath, createProductionServerEntrypointSource());
63
40
  }
64
41
 
65
42
  export async function runInit(args: string[], cwd = process.cwd()): Promise<void> {
@@ -109,200 +86,64 @@ export async function runBuild(cwd = process.cwd()): Promise<void> {
109
86
  }
110
87
 
111
88
  export async function runDev(cwd = process.cwd()): Promise<void> {
112
- const { userConfig, resolved } = await getConfig(cwd);
113
- const generatedDir = path.resolve(cwd, ".rbssr/generated/client-entries");
114
- const devClientDir = path.resolve(cwd, ".rbssr/dev/client");
115
- const serverSnapshotsRoot = path.resolve(cwd, ".rbssr/dev/server-snapshots");
116
- const docsSourceDir = path.resolve(cwd, "docs");
117
- const docsSnapshotDir = path.join(serverSnapshotsRoot, "docs");
118
-
119
- await Promise.all([
120
- ensureDir(generatedDir),
121
- ensureDir(devClientDir),
122
- ensureCleanDirectory(serverSnapshotsRoot),
123
- ]);
124
-
125
- let routeAssets: Record<string, BuildRouteAsset> = {};
126
- let signature = "";
127
- let version = 0;
128
- let currentServerSnapshotDir = resolved.appDir;
129
- const reloadListeners = new Set<(nextVersion: number) => void>();
130
- const docsDir = path.resolve(cwd, "docs");
131
-
132
- const watchedRoots = [resolved.appDir];
133
- if (await existsPath(docsDir)) {
134
- watchedRoots.push(docsDir);
135
- }
136
-
137
- const getSourceSignature = async (): Promise<string> => {
138
- const signatures = await Promise.all(
139
- watchedRoots.map(root => discoverFileSignature(root)),
140
- );
141
- return signatures.join(":");
89
+ await getConfig(cwd);
90
+
91
+ const generatedDevDir = path.resolve(cwd, ".rbssr/generated/dev");
92
+ const generatedEntryPath = path.join(generatedDevDir, "entry.ts");
93
+ await ensureDir(generatedDevDir);
94
+ await writeTextIfChanged(generatedEntryPath, createDevHotEntrypointSource({
95
+ cwd,
96
+ runtimeModulePath: path.resolve(import.meta.dir, "dev-runtime.ts"),
97
+ }));
98
+
99
+ let activeChild: Bun.Subprocess<"inherit", "inherit", "inherit"> | null = null;
100
+ let shuttingDown = false;
101
+
102
+ const forwardSignal = (signal: NodeJS.Signals): void => {
103
+ shuttingDown = true;
104
+ activeChild?.kill(signal);
142
105
  };
143
106
 
144
- const notifyReload = (): void => {
145
- for (const listener of reloadListeners) {
146
- listener(version);
147
- }
148
- };
149
-
150
- const rebuildIfNeeded = async (force = false): Promise<void> => {
151
- const nextSignature = await getSourceSignature();
152
- if (!force && nextSignature === signature) {
153
- return;
154
- }
155
-
156
- signature = nextSignature;
157
-
158
- const snapshotDir = path.join(serverSnapshotsRoot, `v${version + 1}`);
159
- const hasDocsSourceDir = await existsPath(docsSourceDir);
160
- await Promise.all([
161
- (async () => {
162
- await ensureCleanDirectory(snapshotDir);
163
- await copyDirRecursive(resolved.appDir, snapshotDir);
164
- })(),
165
- hasDocsSourceDir
166
- ? (async () => {
167
- await ensureCleanDirectory(docsSnapshotDir);
168
- await copyDirRecursive(docsSourceDir, docsSnapshotDir);
169
- })()
170
- : removePath(docsSnapshotDir),
171
- ]);
172
-
173
- const snapshotConfig: ResolvedConfig = {
174
- ...resolved,
175
- appDir: snapshotDir,
176
- routesDir: path.join(snapshotDir, "routes"),
177
- rootModule: path.join(snapshotDir, "root.tsx"),
178
- middlewareFile: path.join(snapshotDir, "middleware.ts"),
179
- };
180
-
181
- const manifest = await buildRouteManifest(snapshotConfig);
182
- const entries = await generateClientEntries({
183
- config: snapshotConfig,
184
- manifest,
185
- generatedDir,
186
- });
187
-
188
- await ensureCleanDirectory(devClientDir);
189
-
190
- routeAssets = await bundleClientEntries({
191
- entries,
192
- outDir: devClientDir,
193
- dev: true,
194
- publicPrefix: "/__rbssr/client/",
195
- });
107
+ process.once("SIGINT", () => {
108
+ forwardSignal("SIGINT");
109
+ });
196
110
 
197
- currentServerSnapshotDir = snapshotDir;
198
-
199
- const staleVersions = (await listEntries(serverSnapshotsRoot))
200
- .filter(entry => entry.isDirectory && /^v\d+$/.test(entry.name))
201
- .map(entry => entry.name)
202
- .sort((a, b) => {
203
- const aNum = Number(a.slice(1));
204
- const bNum = Number(b.slice(1));
205
- return bNum - aNum;
206
- })
207
- .slice(3);
208
- await Promise.all(staleVersions.map(stale => removePath(path.join(serverSnapshotsRoot, stale))));
209
-
210
- version += 1;
211
- notifyReload();
212
- log(`rebuilt client assets (version ${version})`);
213
- };
111
+ process.once("SIGTERM", () => {
112
+ forwardSignal("SIGTERM");
113
+ });
214
114
 
215
- let rebuildQueue: Promise<void> = Promise.resolve();
216
- const enqueueRebuild = (force = false): Promise<void> => {
217
- const task = rebuildQueue.then(() => rebuildIfNeeded(force));
218
- rebuildQueue = task.catch(error => {
219
- // eslint-disable-next-line no-console
220
- console.error("[rbssr] rebuild failed", error);
115
+ while (true) {
116
+ activeChild = Bun.spawn({
117
+ cmd: ["bun", "--hot", generatedEntryPath],
118
+ cwd,
119
+ stdin: "inherit",
120
+ stdout: "inherit",
121
+ stderr: "inherit",
122
+ env: {
123
+ ...process.env,
124
+ RBSSR_DEV_LAUNCHER: "1",
125
+ RBSSR_DEV_CHILD: "1",
126
+ },
221
127
  });
222
- return task;
223
- };
224
128
 
225
- await enqueueRebuild(true);
129
+ const exitCode = await activeChild.exited;
130
+ activeChild = null;
226
131
 
227
- let rebuildTimer: ReturnType<typeof setTimeout> | undefined;
228
- const scheduleRebuild = (): void => {
229
- if (rebuildTimer) {
230
- clearTimeout(rebuildTimer);
132
+ if (shuttingDown) {
133
+ return;
231
134
  }
232
135
 
233
- rebuildTimer = setTimeout(() => {
234
- rebuildTimer = undefined;
235
- void enqueueRebuild(false);
236
- }, 75);
237
- };
238
-
239
- const watchers: FSWatcher[] = [];
240
- for (const root of watchedRoots) {
241
- try {
242
- const watcher = watch(root, { recursive: true }, () => {
243
- scheduleRebuild();
244
- });
245
- watchers.push(watcher);
246
- } catch {
247
- log(`recursive file watching unavailable for ${root}; relying on request-time rebuild checks`);
136
+ if (exitCode === RBSSR_DEV_RESTART_EXIT_CODE) {
137
+ log("restarting dev child after config change");
138
+ continue;
248
139
  }
249
- }
250
140
 
251
- const cleanup = (): void => {
252
- if (rebuildTimer) {
253
- clearTimeout(rebuildTimer);
254
- rebuildTimer = undefined;
255
- }
256
- for (const watcher of watchers) {
257
- watcher.close();
141
+ if (exitCode !== 0) {
142
+ process.exit(exitCode);
258
143
  }
259
- };
260
-
261
- process.once("SIGINT", () => {
262
- cleanup();
263
- process.exit(0);
264
- });
265
144
 
266
- process.once("SIGTERM", () => {
267
- cleanup();
268
- process.exit(0);
269
- });
270
-
271
- const server = createServer(
272
- {
273
- ...userConfig,
274
- mode: "development",
275
- },
276
- {
277
- dev: true,
278
- getDevAssets: () => routeAssets,
279
- reloadVersion: () => version,
280
- subscribeReload: listener => {
281
- reloadListeners.add(listener);
282
- return () => {
283
- reloadListeners.delete(listener);
284
- };
285
- },
286
- resolvePaths: () => ({
287
- appDir: currentServerSnapshotDir,
288
- routesDir: path.join(currentServerSnapshotDir, "routes"),
289
- rootModule: path.join(currentServerSnapshotDir, "root.tsx"),
290
- middlewareFile: path.join(currentServerSnapshotDir, "middleware.ts"),
291
- }),
292
- onBeforeRequest: () => {
293
- return enqueueRebuild(false);
294
- },
295
- },
296
- );
297
-
298
- const bunServer = Bun.serve({
299
- hostname: resolved.host,
300
- port: resolved.port,
301
- fetch: server.fetch,
302
- development: true,
303
- });
304
-
305
- log(`dev server listening on ${bunServer.url}`);
145
+ return;
146
+ }
306
147
  }
307
148
 
308
149
  export async function runStart(cwd = process.cwd()): Promise<void> {
@@ -328,16 +169,11 @@ function runSubprocess(cmd: string[]): void {
328
169
  }
329
170
 
330
171
  export async function runTypecheck(): Promise<void> {
331
- runSubprocess(["bun", "x", "tsc", "--noEmit"]);
172
+ runSubprocess(createTypecheckCommand());
332
173
  }
333
174
 
334
175
  export async function runTest(extraArgs: string[]): Promise<void> {
335
- if (extraArgs.length > 0) {
336
- runSubprocess(["bun", "test", ...extraArgs]);
337
- return;
176
+ for (const cmd of createTestCommands(extraArgs)) {
177
+ runSubprocess(cmd);
338
178
  }
339
-
340
- runSubprocess(["bun", "test", "./tests/unit"]);
341
- runSubprocess(["bun", "test", "./tests/integration"]);
342
- runSubprocess(["bun", "x", "playwright", "test"]);
343
179
  }
@@ -0,0 +1,281 @@
1
+ import path from "node:path";
2
+ import {
3
+ createClientEntrySetSignature,
4
+ listBuildOutputFiles,
5
+ mapBuildOutputsFromMetafile,
6
+ type ClientEntryFile,
7
+ } from "../runtime/build-tools";
8
+ import { ensureDir, readText, removePath, statPath } from "../runtime/io";
9
+ import type { BuildRouteAsset } from "../runtime/types";
10
+
11
+ export interface DevClientBuildSnapshot {
12
+ entrySetSignature: string;
13
+ routeAssets: Record<string, BuildRouteAsset>;
14
+ outputFiles: string[];
15
+ buildCount: number;
16
+ }
17
+
18
+ export interface DevClientWatchHandle {
19
+ syncEntries(entries: ClientEntryFile[]): Promise<{ restarted: boolean; entrySetSignature: string }>;
20
+ getBuildCount(): number;
21
+ waitForBuildAfter(buildCount: number): Promise<void>;
22
+ waitUntilReady(): Promise<void>;
23
+ stop(): Promise<void>;
24
+ }
25
+
26
+ interface DevClientWatchState {
27
+ buildCount: number;
28
+ entrySetSignature: string;
29
+ entries: ClientEntryFile[];
30
+ readyPromise: Promise<void>;
31
+ resolveReady: () => void;
32
+ rejectReady: (error: unknown) => void;
33
+ outputFiles: Set<string>;
34
+ process: Bun.Subprocess<"ignore", "inherit", "inherit"> | null;
35
+ stopped: boolean;
36
+ waiters: Array<{
37
+ minBuildCount: number;
38
+ resolve: () => void;
39
+ reject: (error: unknown) => void;
40
+ }>;
41
+ }
42
+
43
+ function createDeferred(): {
44
+ promise: Promise<void>;
45
+ resolve: () => void;
46
+ reject: (error: unknown) => void;
47
+ } {
48
+ let resolve!: () => void;
49
+ let reject!: (error: unknown) => void;
50
+ const promise = new Promise<void>((res, rej) => {
51
+ resolve = res;
52
+ reject = rej;
53
+ });
54
+
55
+ return {
56
+ promise,
57
+ resolve,
58
+ reject,
59
+ };
60
+ }
61
+
62
+ export function createDevClientWatch(options: {
63
+ cwd: string;
64
+ outDir: string;
65
+ metafilePath: string;
66
+ entries: ClientEntryFile[];
67
+ publicPrefix: string;
68
+ onBuild: (snapshot: DevClientBuildSnapshot) => void | Promise<void>;
69
+ onLog?: (message: string) => void;
70
+ }): DevClientWatchHandle {
71
+ const state: DevClientWatchState = {
72
+ buildCount: 0,
73
+ entrySetSignature: createClientEntrySetSignature(options.entries),
74
+ entries: [...options.entries],
75
+ ...(() => {
76
+ const deferred = createDeferred();
77
+ return {
78
+ readyPromise: deferred.promise,
79
+ resolveReady: deferred.resolve,
80
+ rejectReady: deferred.reject,
81
+ };
82
+ })(),
83
+ outputFiles: new Set<string>(),
84
+ process: null,
85
+ stopped: false,
86
+ waiters: [],
87
+ };
88
+
89
+ let metafilePoller: ReturnType<typeof setInterval> | undefined;
90
+ let lastMetafileMtime = "";
91
+
92
+ const stopPolling = (): void => {
93
+ if (metafilePoller) {
94
+ clearInterval(metafilePoller);
95
+ metafilePoller = undefined;
96
+ }
97
+ };
98
+
99
+ const parseMetafile = async (): Promise<void> => {
100
+ const stat = await statPath(options.metafilePath);
101
+ if (!stat?.isFile()) {
102
+ return;
103
+ }
104
+
105
+ const nextMtime = stat.mtime.toISOString();
106
+ if (nextMtime === lastMetafileMtime) {
107
+ return;
108
+ }
109
+
110
+ let metafile: Bun.BuildMetafile;
111
+ try {
112
+ metafile = JSON.parse(await readText(options.metafilePath)) as Bun.BuildMetafile;
113
+ } catch {
114
+ return;
115
+ }
116
+
117
+ lastMetafileMtime = nextMtime;
118
+
119
+ const nextOutputFiles = new Set(listBuildOutputFiles(metafile));
120
+ const staleOutputFiles = [...state.outputFiles].filter((filePath) => !nextOutputFiles.has(filePath));
121
+
122
+ await Promise.all(
123
+ staleOutputFiles.map((filePath) => removePath(path.resolve(options.cwd, filePath))),
124
+ );
125
+
126
+ state.outputFiles = nextOutputFiles;
127
+ state.buildCount += 1;
128
+
129
+ const readyWaiters = state.waiters.filter((waiter) => state.buildCount > waiter.minBuildCount);
130
+ state.waiters = state.waiters.filter((waiter) => state.buildCount <= waiter.minBuildCount);
131
+ for (const waiter of readyWaiters) {
132
+ waiter.resolve();
133
+ }
134
+
135
+ await options.onBuild({
136
+ entrySetSignature: state.entrySetSignature,
137
+ routeAssets: mapBuildOutputsFromMetafile({
138
+ metafile,
139
+ entries: state.entries,
140
+ publicPrefix: options.publicPrefix,
141
+ }),
142
+ outputFiles: [...nextOutputFiles].sort(),
143
+ buildCount: state.buildCount,
144
+ });
145
+
146
+ state.resolveReady();
147
+ };
148
+
149
+ const startProcess = async (): Promise<void> => {
150
+ await ensureDir(path.dirname(options.metafilePath));
151
+ await ensureDir(options.outDir);
152
+ await removePath(options.metafilePath);
153
+
154
+ const previousOutputFiles = [...state.outputFiles];
155
+ const deferred = createDeferred();
156
+ state.readyPromise = deferred.promise;
157
+ state.resolveReady = deferred.resolve;
158
+ state.rejectReady = deferred.reject;
159
+ state.buildCount = 0;
160
+ state.outputFiles = new Set<string>();
161
+ lastMetafileMtime = "";
162
+
163
+ if (state.entries.length === 0) {
164
+ await Promise.all(
165
+ previousOutputFiles.map((filePath) => removePath(path.resolve(options.cwd, filePath))),
166
+ );
167
+ state.resolveReady();
168
+ return;
169
+ }
170
+
171
+ const cmd = [
172
+ "bun",
173
+ "build",
174
+ "--watch",
175
+ "--no-clear-screen",
176
+ "--target=browser",
177
+ "--format=esm",
178
+ "--splitting",
179
+ "--sourcemap=inline",
180
+ "--outdir",
181
+ options.outDir,
182
+ `--metafile=${options.metafilePath}`,
183
+ "--entry-naming",
184
+ "[name].[ext]",
185
+ "--chunk-naming",
186
+ "[name]-[hash].[ext]",
187
+ "--asset-naming",
188
+ "[name]-[hash].[ext]",
189
+ ...state.entries.map((entry) => entry.entryFilePath),
190
+ ];
191
+
192
+ state.process = Bun.spawn({
193
+ cmd,
194
+ cwd: options.cwd,
195
+ stdin: "ignore",
196
+ stdout: "inherit",
197
+ stderr: "inherit",
198
+ env: process.env,
199
+ });
200
+
201
+ void state.process.exited.then((exitCode) => {
202
+ if (state.stopped || exitCode === 0) {
203
+ return;
204
+ }
205
+ for (const waiter of state.waiters) {
206
+ waiter.reject(new Error(`bun build --watch exited with code ${exitCode}`));
207
+ }
208
+ state.waiters = [];
209
+ state.rejectReady(new Error(`bun build --watch exited with code ${exitCode}`));
210
+ });
211
+
212
+ metafilePoller = setInterval(() => {
213
+ void parseMetafile();
214
+ }, 75);
215
+ };
216
+
217
+ const stopProcess = async (): Promise<void> => {
218
+ stopPolling();
219
+
220
+ if (!state.process) {
221
+ return;
222
+ }
223
+
224
+ const active = state.process;
225
+ state.process = null;
226
+ active.kill();
227
+ await active.exited;
228
+ };
229
+
230
+ void startProcess();
231
+
232
+ return {
233
+ async syncEntries(entries) {
234
+ const nextEntrySetSignature = createClientEntrySetSignature(entries);
235
+ const restarted = nextEntrySetSignature !== state.entrySetSignature;
236
+ state.entries = [...entries];
237
+
238
+ if (!restarted) {
239
+ options.onLog?.("kept Bun client watch hot");
240
+ return {
241
+ restarted: false,
242
+ entrySetSignature: state.entrySetSignature,
243
+ };
244
+ }
245
+
246
+ state.entrySetSignature = nextEntrySetSignature;
247
+ await stopProcess();
248
+ await startProcess();
249
+ options.onLog?.("restarted Bun client watch after entry set change");
250
+
251
+ return {
252
+ restarted: true,
253
+ entrySetSignature: state.entrySetSignature,
254
+ };
255
+ },
256
+ getBuildCount() {
257
+ return state.buildCount;
258
+ },
259
+ waitForBuildAfter(buildCount) {
260
+ if (state.buildCount > buildCount || state.entries.length === 0) {
261
+ return Promise.resolve();
262
+ }
263
+
264
+ return new Promise<void>((resolve, reject) => {
265
+ state.waiters.push({
266
+ minBuildCount: buildCount,
267
+ resolve,
268
+ reject,
269
+ });
270
+ });
271
+ },
272
+ waitUntilReady() {
273
+ return state.readyPromise;
274
+ },
275
+ async stop() {
276
+ state.stopped = true;
277
+ stopPolling();
278
+ await stopProcess();
279
+ },
280
+ };
281
+ }
@@ -0,0 +1,71 @@
1
+ import { statPath } from "../runtime/io";
2
+ import type { RouteManifest } from "../runtime/types";
3
+ import { ensureWithin } from "../runtime/utils";
4
+
5
+ export const RBSSR_DEV_RELOAD_TOPIC = "rbssr:reload";
6
+ export const RBSSR_DEV_WS_PATH = "/__rbssr/ws";
7
+
8
+ export type DevReloadReason =
9
+ | "client-build"
10
+ | "route-structure"
11
+ | "server-runtime"
12
+ | "markdown-route"
13
+ | "config-restart";
14
+
15
+ export interface DevReloadMessage {
16
+ token: number;
17
+ reason: DevReloadReason;
18
+ }
19
+
20
+ function normalizeApiRoutePath(routePath: string): string {
21
+ return routePath.replace(/\*[A-Za-z0-9_]+/g, "*");
22
+ }
23
+
24
+ async function serveDevClientAsset(devClientDir: string, request: Request): Promise<Response> {
25
+ const url = new URL(request.url);
26
+ const relativePath = url.pathname.replace(/^\/__rbssr\/client\//, "");
27
+ const resolvedPath = ensureWithin(devClientDir, relativePath);
28
+ if (!resolvedPath) {
29
+ return new Response("Not found", { status: 404 });
30
+ }
31
+
32
+ const stat = await statPath(resolvedPath);
33
+ if (!stat?.isFile()) {
34
+ return new Response("Not found", { status: 404 });
35
+ }
36
+
37
+ return new Response(Bun.file(resolvedPath), {
38
+ headers: {
39
+ "cache-control": "no-store",
40
+ },
41
+ });
42
+ }
43
+
44
+ export function createDevRouteTable(options: {
45
+ devClientDir: string;
46
+ manifest: RouteManifest;
47
+ handleFrameworkFetch: (request: Request) => Promise<Response>;
48
+ }): Record<string, Response | ((request: Request, server: Bun.Server<undefined>) => Response | Promise<Response> | void)> {
49
+ const routes: Record<
50
+ string,
51
+ Response | ((request: Request, server: Bun.Server<undefined>) => Response | Promise<Response> | void)
52
+ > = {
53
+ [RBSSR_DEV_WS_PATH]: (request, server) => {
54
+ const upgraded = server.upgrade(request);
55
+ if (!upgraded) {
56
+ return new Response("WebSocket upgrade failed", { status: 400 });
57
+ }
58
+ },
59
+ "/__rbssr/client/*": (request) => {
60
+ return serveDevClientAsset(options.devClientDir, request);
61
+ },
62
+ };
63
+
64
+ for (const apiRoute of options.manifest.api) {
65
+ routes[normalizeApiRoutePath(apiRoute.routePath)] = (request) => {
66
+ return options.handleFrameworkFetch(request);
67
+ };
68
+ }
69
+
70
+ return routes;
71
+ }