react-bun-ssr 0.3.2 → 0.4.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 +39 -5
- package/framework/cli/dev-runtime.ts +18 -1
- package/framework/runtime/action-stub.ts +26 -0
- package/framework/runtime/client-runtime.tsx +66 -182
- package/framework/runtime/helpers.ts +75 -1
- package/framework/runtime/index.ts +53 -23
- package/framework/runtime/module-loader.ts +197 -35
- package/framework/runtime/render.tsx +1 -1
- package/framework/runtime/request-executor.ts +1705 -0
- package/framework/runtime/response-context.ts +206 -0
- package/framework/runtime/route-api.ts +51 -18
- package/framework/runtime/route-scanner.ts +104 -12
- package/framework/runtime/route-wire-protocol.ts +486 -0
- package/framework/runtime/server.ts +8 -1295
- package/framework/runtime/tree.tsx +45 -4
- package/framework/runtime/types.ts +71 -10
- package/package.json +1 -1
|
@@ -1,1305 +1,18 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { createElement } from "react";
|
|
3
|
-
import { createBunRouteAdapter, type BunRouteAdapter } from "./bun-route-adapter";
|
|
4
|
-
import {
|
|
5
|
-
isDeferredLoaderResult,
|
|
6
|
-
prepareDeferredPayload,
|
|
7
|
-
type DeferredSettleEntry,
|
|
8
|
-
} from "./deferred";
|
|
9
|
-
import { statPath } from "./io";
|
|
10
|
-
import type {
|
|
11
|
-
ActionContext,
|
|
12
|
-
BuildRouteAsset,
|
|
13
|
-
ClientRouteSnapshot,
|
|
14
|
-
ClientRouterSnapshot,
|
|
15
|
-
FrameworkConfig,
|
|
16
|
-
HydrationDocumentAssets,
|
|
17
|
-
LoaderContext,
|
|
18
|
-
PageRouteDefinition,
|
|
19
|
-
RouteManifest,
|
|
20
|
-
RequestContext,
|
|
21
|
-
RouteCatchContext,
|
|
22
|
-
RouteErrorContext,
|
|
23
|
-
RouteErrorPhase,
|
|
24
|
-
RouteErrorResponse,
|
|
25
|
-
ResolvedConfig,
|
|
26
|
-
RouteModule,
|
|
27
|
-
RouteModuleBundle,
|
|
28
|
-
ServerRuntimeOptions,
|
|
29
|
-
TransitionChunk,
|
|
30
|
-
TransitionDeferredChunk,
|
|
31
|
-
TransitionDocumentChunk,
|
|
32
|
-
TransitionInitialChunk,
|
|
33
|
-
TransitionRedirectChunk,
|
|
34
|
-
} from "./types";
|
|
35
1
|
import { resolveConfig } from "./config";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
extractRouteMiddleware,
|
|
39
|
-
loadApiRouteModule,
|
|
40
|
-
loadGlobalMiddleware,
|
|
41
|
-
loadNestedMiddleware,
|
|
42
|
-
loadRouteModule,
|
|
43
|
-
loadRouteModules,
|
|
44
|
-
} from "./module-loader";
|
|
45
|
-
import {
|
|
46
|
-
collectHeadMarkup,
|
|
47
|
-
collectHeadElements,
|
|
48
|
-
createManagedHeadMarkup,
|
|
49
|
-
renderDocumentStream,
|
|
50
|
-
} from "./render";
|
|
51
|
-
import { runMiddlewareChain } from "./middleware";
|
|
52
|
-
import {
|
|
53
|
-
createCatchAppTree,
|
|
54
|
-
createErrorAppTree,
|
|
55
|
-
createNotFoundAppTree,
|
|
56
|
-
createPageAppTree,
|
|
57
|
-
} from "./tree";
|
|
58
|
-
import {
|
|
59
|
-
sanitizeRouteErrorResponse,
|
|
60
|
-
toRouteErrorHttpResponse,
|
|
61
|
-
toRouteErrorResponse,
|
|
62
|
-
} from "./route-errors";
|
|
63
|
-
import {
|
|
64
|
-
ensureWithin,
|
|
65
|
-
isMutatingMethod,
|
|
66
|
-
normalizeSlashes,
|
|
67
|
-
parseCookieHeader,
|
|
68
|
-
sanitizeErrorMessage,
|
|
69
|
-
stableHash,
|
|
70
|
-
} from "./utils";
|
|
71
|
-
import { sortRoutesBySpecificity } from "./route-order";
|
|
2
|
+
import { createRequestExecutor } from "./request-executor";
|
|
3
|
+
import type { FrameworkConfig, ServerRuntimeOptions } from "./types";
|
|
72
4
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const HASHED_CLIENT_CHUNK_RE = /^\/client\/.+-[A-Za-z0-9]{6,}\.(?:js|css)$/;
|
|
76
|
-
const STATIC_IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
77
|
-
const STATIC_DEFAULT_CACHE = "public, max-age=3600";
|
|
78
|
-
|
|
79
|
-
function toRedirectResponse(location: string, status = 302): Response {
|
|
80
|
-
return Response.redirect(location, status);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isResponse(value: unknown): value is Response {
|
|
84
|
-
return value instanceof Response;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function resolveThrownRedirect(error: unknown): Response | null {
|
|
88
|
-
if (!(error instanceof Response)) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const location = error.headers.get("location");
|
|
93
|
-
if (location && isRedirectStatus(error.status)) {
|
|
94
|
-
return Response.redirect(location, error.status);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getLifecycleModules(modules: RouteModuleBundle): RouteModule[] {
|
|
101
|
-
return [
|
|
102
|
-
modules.route,
|
|
103
|
-
...[...modules.layouts].reverse(),
|
|
104
|
-
modules.root,
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function toRouteErrorContextBase(options: {
|
|
109
|
-
requestContext: RequestContext;
|
|
110
|
-
routeId: string;
|
|
111
|
-
phase: RouteErrorPhase;
|
|
112
|
-
dev: boolean;
|
|
113
|
-
}): Omit<RouteErrorContext, "error"> {
|
|
114
|
-
return {
|
|
115
|
-
request: options.requestContext.request,
|
|
116
|
-
url: options.requestContext.url,
|
|
117
|
-
params: options.requestContext.params,
|
|
118
|
-
routeId: options.routeId,
|
|
119
|
-
phase: options.phase,
|
|
120
|
-
dev: options.dev,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function notifyErrorHooks(options: {
|
|
125
|
-
modules: RouteModuleBundle;
|
|
126
|
-
context: RouteErrorContext;
|
|
127
|
-
}): Promise<void> {
|
|
128
|
-
const targets = getLifecycleModules(options.modules);
|
|
129
|
-
for (const moduleValue of targets) {
|
|
130
|
-
if (typeof moduleValue.onError !== "function") {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
await moduleValue.onError(options.context);
|
|
136
|
-
} catch (hookError) {
|
|
137
|
-
// eslint-disable-next-line no-console
|
|
138
|
-
console.warn("[rbssr] route onError hook failed", Bun.inspect(hookError));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function notifyCatchHooks(options: {
|
|
144
|
-
modules: RouteModuleBundle;
|
|
145
|
-
context: RouteCatchContext;
|
|
146
|
-
}): Promise<void> {
|
|
147
|
-
const targets = getLifecycleModules(options.modules);
|
|
148
|
-
for (const moduleValue of targets) {
|
|
149
|
-
if (typeof moduleValue.onCatch !== "function") {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await moduleValue.onCatch(options.context);
|
|
155
|
-
} catch (hookError) {
|
|
156
|
-
// eslint-disable-next-line no-console
|
|
157
|
-
console.warn("[rbssr] route onCatch hook failed", Bun.inspect(hookError));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function toUncaughtErrorPayload(
|
|
163
|
-
error: unknown,
|
|
164
|
-
production: boolean,
|
|
165
|
-
): { message: string } {
|
|
166
|
-
return {
|
|
167
|
-
message: sanitizeErrorMessage(error, production),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function toCaughtErrorPayload(
|
|
172
|
-
routeErrorResponse: RouteErrorResponse,
|
|
173
|
-
production: boolean,
|
|
174
|
-
): RouteErrorResponse {
|
|
175
|
-
return sanitizeRouteErrorResponse(routeErrorResponse, production);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function toHtmlStreamResponse(stream: ReadableStream<Uint8Array>, status: number): Response {
|
|
179
|
-
return new Response(stream, {
|
|
180
|
-
status,
|
|
181
|
-
headers: {
|
|
182
|
-
"content-type": "text/html; charset=utf-8",
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function applyFrameworkDefaultHeaders(options: {
|
|
188
|
-
headers: Headers;
|
|
189
|
-
dev: boolean;
|
|
190
|
-
kind: ResponseKind;
|
|
191
|
-
pathname: string;
|
|
192
|
-
}): void {
|
|
193
|
-
const { headers, dev, kind, pathname } = options;
|
|
194
|
-
|
|
195
|
-
if (kind === "internal-dev" || kind === "internal-transition") {
|
|
196
|
-
if (!headers.has("cache-control")) {
|
|
197
|
-
headers.set("cache-control", "no-store");
|
|
198
|
-
}
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (dev) {
|
|
203
|
-
if (kind === "static") {
|
|
204
|
-
headers.set("cache-control", "no-store");
|
|
205
|
-
headers.set("pragma", "no-cache");
|
|
206
|
-
headers.set("expires", "0");
|
|
207
|
-
}
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (kind === "static" && !headers.has("cache-control")) {
|
|
212
|
-
headers.set(
|
|
213
|
-
"cache-control",
|
|
214
|
-
HASHED_CLIENT_CHUNK_RE.test(pathname) ? STATIC_IMMUTABLE_CACHE : STATIC_DEFAULT_CACHE,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function applyConfiguredHeaders(options: {
|
|
220
|
-
headers: Headers;
|
|
221
|
-
pathname: string;
|
|
222
|
-
config: ResolvedConfig;
|
|
223
|
-
}): void {
|
|
224
|
-
const { headers, pathname, config } = options;
|
|
225
|
-
for (const rule of config.headerRules) {
|
|
226
|
-
if (!rule.matcher.test(pathname)) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
for (const [name, value] of Object.entries(rule.headers)) {
|
|
231
|
-
if (value === null) {
|
|
232
|
-
headers.delete(name);
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
headers.set(name, value);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function finalizeResponseHeaders(options: {
|
|
242
|
-
response: Response;
|
|
243
|
-
request: Request;
|
|
244
|
-
pathname: string;
|
|
245
|
-
kind: ResponseKind;
|
|
246
|
-
dev: boolean;
|
|
247
|
-
config: ResolvedConfig;
|
|
248
|
-
}): Response {
|
|
249
|
-
const headers = new Headers(options.response.headers);
|
|
250
|
-
|
|
251
|
-
applyFrameworkDefaultHeaders({
|
|
252
|
-
headers,
|
|
253
|
-
dev: options.dev,
|
|
254
|
-
kind: options.kind,
|
|
255
|
-
pathname: options.pathname,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
applyConfiguredHeaders({
|
|
259
|
-
headers,
|
|
260
|
-
pathname: options.pathname,
|
|
261
|
-
config: options.config,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
return new Response(options.request.method.toUpperCase() === "HEAD" ? null : options.response.body, {
|
|
265
|
-
status: options.response.status,
|
|
266
|
-
statusText: options.response.statusText,
|
|
267
|
-
headers,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function tryServeStatic(baseDir: string, pathname: string): Promise<Response | null> {
|
|
272
|
-
if (!pathname || pathname === "/") {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const decodedPath = decodeURIComponent(pathname);
|
|
277
|
-
const relativePath = decodedPath.replace(/^\/+/, "");
|
|
278
|
-
|
|
279
|
-
if (!relativePath) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const resolved = ensureWithin(baseDir, relativePath);
|
|
284
|
-
if (!resolved) {
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const stat = await statPath(resolved);
|
|
289
|
-
if (!stat?.isFile()) {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return new Response(Bun.file(resolved));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function getMethodHandler(moduleValue: Record<string, unknown>, method: string): unknown {
|
|
297
|
-
const upper = method.toUpperCase();
|
|
298
|
-
if (upper === "HEAD" && typeof moduleValue.HEAD !== "function" && typeof moduleValue.GET === "function") {
|
|
299
|
-
return moduleValue.GET;
|
|
300
|
-
}
|
|
301
|
-
return moduleValue[upper];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function getAllowedMethods(moduleValue: Record<string, unknown>): string[] {
|
|
305
|
-
return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].filter(method => {
|
|
306
|
-
return typeof moduleValue[method] === "function";
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function parseActionBody(request: Request): Promise<Pick<ActionContext, "formData" | "json">> {
|
|
311
|
-
const contentType = request.headers.get("content-type") ?? "";
|
|
312
|
-
|
|
313
|
-
if (contentType.includes("application/json")) {
|
|
314
|
-
try {
|
|
315
|
-
return { json: await request.json() };
|
|
316
|
-
} catch {
|
|
317
|
-
return {};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
contentType.includes("multipart/form-data") ||
|
|
323
|
-
contentType.includes("application/x-www-form-urlencoded")
|
|
324
|
-
) {
|
|
325
|
-
try {
|
|
326
|
-
return { formData: await request.formData() };
|
|
327
|
-
} catch {
|
|
328
|
-
return {};
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return {};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function loadRootOnlyModule(
|
|
336
|
-
rootModulePath: string,
|
|
337
|
-
options: {
|
|
338
|
-
cacheBustKey?: string;
|
|
339
|
-
serverBytecode: boolean;
|
|
340
|
-
devSourceImports?: boolean;
|
|
341
|
-
},
|
|
342
|
-
): Promise<RouteModule> {
|
|
343
|
-
return loadRouteModule(rootModulePath, options);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function resolveRouteAssets(
|
|
347
|
-
routeId: string,
|
|
348
|
-
options: {
|
|
349
|
-
dev: boolean;
|
|
350
|
-
runtimeOptions: ServerRuntimeOptions;
|
|
351
|
-
},
|
|
352
|
-
): BuildRouteAsset | null {
|
|
353
|
-
const { dev, runtimeOptions } = options;
|
|
354
|
-
if (dev) {
|
|
355
|
-
const assets = runtimeOptions.getDevAssets?.() ?? runtimeOptions.devAssets ?? {};
|
|
356
|
-
return assets[routeId] ?? null;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const manifest = runtimeOptions.buildManifest;
|
|
360
|
-
if (!manifest) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return manifest.routes[routeId] ?? null;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function resolveAllRouteAssets(options: {
|
|
368
|
-
dev: boolean;
|
|
369
|
-
runtimeOptions: ServerRuntimeOptions;
|
|
370
|
-
}): Record<string, BuildRouteAsset> {
|
|
371
|
-
if (options.dev) {
|
|
372
|
-
return options.runtimeOptions.getDevAssets?.() ?? options.runtimeOptions.devAssets ?? {};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return options.runtimeOptions.buildManifest?.routes ?? {};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export function toClientRouteSnapshots(routes: PageRouteDefinition[]): ClientRouteSnapshot[] {
|
|
379
|
-
return sortRoutesBySpecificity([...routes]).map(route => ({
|
|
380
|
-
id: route.id,
|
|
381
|
-
routePath: route.routePath,
|
|
382
|
-
segments: route.segments,
|
|
383
|
-
score: route.score,
|
|
384
|
-
}));
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function createRouterSnapshot(options: {
|
|
388
|
-
manifest: RouteManifest;
|
|
389
|
-
routeAssets: Record<string, BuildRouteAsset>;
|
|
390
|
-
devVersion?: number;
|
|
391
|
-
}): ClientRouterSnapshot {
|
|
392
|
-
return {
|
|
393
|
-
pages: toClientRouteSnapshots(options.manifest.pages),
|
|
394
|
-
assets: options.routeAssets,
|
|
395
|
-
devVersion: options.devVersion,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function resolveFallbackHtmlAssets(options: {
|
|
400
|
-
routeAssets: Record<string, BuildRouteAsset>;
|
|
401
|
-
devVersion?: number;
|
|
402
|
-
}): HydrationDocumentAssets {
|
|
403
|
-
const css = Array.from(
|
|
404
|
-
new Set(
|
|
405
|
-
Object.values(options.routeAssets).flatMap((asset) => asset.css),
|
|
406
|
-
),
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
script: undefined,
|
|
411
|
-
css,
|
|
412
|
-
devVersion: options.devVersion,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function toTransitionStreamResponse(
|
|
417
|
-
stream: ReadableStream<Uint8Array>,
|
|
418
|
-
baseHeaders?: HeadersInit,
|
|
419
|
-
): Response {
|
|
420
|
-
const headers = new Headers(baseHeaders);
|
|
421
|
-
headers.set("content-type", "application/x-ndjson; charset=utf-8");
|
|
422
|
-
if (!headers.has("cache-control")) {
|
|
423
|
-
headers.set("cache-control", "no-store");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return new Response(stream, {
|
|
427
|
-
headers,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function toTransitionChunkLine(chunk: TransitionChunk): Uint8Array {
|
|
432
|
-
return new TextEncoder().encode(`${JSON.stringify(chunk)}\n`);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function isRedirectStatus(status: number): boolean {
|
|
436
|
-
return status >= 300 && status < 400;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function toRedirectChunk(location: string, status: number): TransitionRedirectChunk {
|
|
440
|
-
return {
|
|
441
|
-
type: "redirect",
|
|
442
|
-
location,
|
|
443
|
-
status,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function toDocumentChunk(location: string, status: number): TransitionDocumentChunk {
|
|
448
|
-
return {
|
|
449
|
-
type: "document",
|
|
450
|
-
location,
|
|
451
|
-
status,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function createTransitionStream(options: {
|
|
456
|
-
initialChunk?: TransitionInitialChunk;
|
|
457
|
-
controlChunk?: TransitionRedirectChunk | TransitionDocumentChunk;
|
|
458
|
-
deferredSettleEntries?: DeferredSettleEntry[];
|
|
459
|
-
sanitizeDeferredError: (message: string) => string;
|
|
460
|
-
}): ReadableStream<Uint8Array> {
|
|
461
|
-
return new ReadableStream<Uint8Array>({
|
|
462
|
-
async start(controller) {
|
|
463
|
-
try {
|
|
464
|
-
if (options.controlChunk) {
|
|
465
|
-
controller.enqueue(toTransitionChunkLine(options.controlChunk));
|
|
466
|
-
controller.close();
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (options.initialChunk) {
|
|
471
|
-
controller.enqueue(toTransitionChunkLine(options.initialChunk));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const settleEntries = options.deferredSettleEntries ?? [];
|
|
475
|
-
if (settleEntries.length === 0) {
|
|
476
|
-
controller.close();
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
await Promise.all(
|
|
481
|
-
settleEntries.map(async entry => {
|
|
482
|
-
const settled = await entry.settled;
|
|
483
|
-
const chunk: TransitionDeferredChunk = settled.ok
|
|
484
|
-
? {
|
|
485
|
-
type: "deferred",
|
|
486
|
-
id: entry.id,
|
|
487
|
-
ok: true,
|
|
488
|
-
value: settled.value,
|
|
489
|
-
}
|
|
490
|
-
: {
|
|
491
|
-
type: "deferred",
|
|
492
|
-
id: entry.id,
|
|
493
|
-
ok: false,
|
|
494
|
-
error: options.sanitizeDeferredError(settled.error),
|
|
495
|
-
};
|
|
496
|
-
controller.enqueue(toTransitionChunkLine(chunk));
|
|
497
|
-
}),
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
controller.close();
|
|
501
|
-
} catch (error) {
|
|
502
|
-
controller.error(error);
|
|
503
|
-
}
|
|
504
|
-
},
|
|
505
|
-
});
|
|
506
|
-
}
|
|
5
|
+
export { toClientRouteSnapshots } from "./request-executor";
|
|
507
6
|
|
|
508
7
|
export function createServer(
|
|
509
8
|
config: FrameworkConfig = {},
|
|
510
9
|
runtimeOptions: ServerRuntimeOptions = {},
|
|
511
10
|
): { fetch(req: Request): Promise<Response> } {
|
|
512
|
-
const resolvedConfig
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const pendingAdapterCache = new Map<string, Promise<BunRouteAdapter>>();
|
|
518
|
-
|
|
519
|
-
const getAdapterKey = (activeConfig: ResolvedConfig): string => {
|
|
520
|
-
const routeVersion = dev
|
|
521
|
-
? runtimeOptions.routeManifestVersion?.() ?? runtimeOptions.reloadVersion?.() ?? 0
|
|
522
|
-
: 0;
|
|
523
|
-
return `${normalizeSlashes(activeConfig.routesDir)}|${dev ? "dev" : "prod"}|${routeVersion}`;
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const trimAdapterCache = (): void => {
|
|
527
|
-
if (!dev || adapterCache.size <= 3) {
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const keys = [...adapterCache.keys()];
|
|
532
|
-
while (keys.length > 3) {
|
|
533
|
-
const oldestKey = keys.shift();
|
|
534
|
-
if (!oldestKey) {
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
adapterCache.delete(oldestKey);
|
|
538
|
-
pendingAdapterCache.delete(oldestKey);
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
const getRouteAdapter = async (activeConfig: ResolvedConfig): Promise<BunRouteAdapter> => {
|
|
543
|
-
const cacheKey = getAdapterKey(activeConfig);
|
|
544
|
-
const cached = adapterCache.get(cacheKey);
|
|
545
|
-
if (cached) {
|
|
546
|
-
return cached;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const pending = pendingAdapterCache.get(cacheKey);
|
|
550
|
-
if (pending) {
|
|
551
|
-
return pending;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const routesHash = stableHash(normalizeSlashes(activeConfig.routesDir));
|
|
555
|
-
const projectionRootDir = dev
|
|
556
|
-
? path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", `dev-${routesHash}`)
|
|
557
|
-
: path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", "prod", routesHash);
|
|
558
|
-
|
|
559
|
-
const buildAdapterPromise = createBunRouteAdapter({
|
|
560
|
-
routesDir: activeConfig.routesDir,
|
|
561
|
-
generatedMarkdownRootDir: path.resolve(activeConfig.cwd, ".rbssr/generated/markdown-routes"),
|
|
562
|
-
projectionRootDir,
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
pendingAdapterCache.set(cacheKey, buildAdapterPromise);
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
const adapter = await buildAdapterPromise;
|
|
569
|
-
adapterCache.set(cacheKey, adapter);
|
|
570
|
-
trimAdapterCache();
|
|
571
|
-
return adapter;
|
|
572
|
-
} finally {
|
|
573
|
-
pendingAdapterCache.delete(cacheKey);
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
const fetchHandler = async (request: Request): Promise<Response> => {
|
|
578
|
-
const runtimePaths = runtimeOptions.resolvePaths?.() ?? {};
|
|
579
|
-
const activeConfig: ResolvedConfig = {
|
|
580
|
-
...resolvedConfig,
|
|
581
|
-
...runtimePaths,
|
|
582
|
-
};
|
|
583
|
-
const devClientDir = path.resolve(resolvedConfig.cwd, ".rbssr/dev/client");
|
|
584
|
-
|
|
585
|
-
const url = new URL(request.url);
|
|
586
|
-
const finalize = (response: Response, kind: ResponseKind): Response => {
|
|
587
|
-
return finalizeResponseHeaders({
|
|
588
|
-
response,
|
|
589
|
-
request,
|
|
590
|
-
pathname: url.pathname,
|
|
591
|
-
kind,
|
|
592
|
-
dev,
|
|
593
|
-
config: activeConfig,
|
|
594
|
-
});
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
if (dev && url.pathname === "/__rbssr/version") {
|
|
598
|
-
const version = runtimeOptions.reloadVersion?.() ?? 0;
|
|
599
|
-
return finalize(new Response(String(version), {
|
|
600
|
-
headers: {
|
|
601
|
-
"content-type": "text/plain; charset=utf-8",
|
|
602
|
-
"cache-control": "no-store",
|
|
603
|
-
},
|
|
604
|
-
}), "internal-dev");
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (dev && url.pathname.startsWith("/__rbssr/client/")) {
|
|
608
|
-
const relative = url.pathname.replace(/^\/__rbssr\/client\//, "");
|
|
609
|
-
const response = await tryServeStatic(devClientDir, relative);
|
|
610
|
-
if (response) {
|
|
611
|
-
return finalize(response, "static");
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (!dev && url.pathname.startsWith("/client/")) {
|
|
616
|
-
const relative = url.pathname.replace(/^\/client\//, "");
|
|
617
|
-
const response = await tryServeStatic(path.join(activeConfig.distDir, "client"), relative);
|
|
618
|
-
if (response) {
|
|
619
|
-
return finalize(response, "static");
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (!dev) {
|
|
624
|
-
const builtPublicResponse = await tryServeStatic(path.join(activeConfig.distDir, "client"), url.pathname);
|
|
625
|
-
if (builtPublicResponse) {
|
|
626
|
-
return finalize(builtPublicResponse, "static");
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const publicResponse = await tryServeStatic(activeConfig.publicDir, url.pathname);
|
|
631
|
-
if (publicResponse) {
|
|
632
|
-
return finalize(publicResponse, "static");
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
await runtimeOptions.onBeforeRequest?.();
|
|
636
|
-
|
|
637
|
-
const routeAdapter = await getRouteAdapter(activeConfig);
|
|
638
|
-
const devCacheBustKey = dev ? String(runtimeOptions.reloadVersion?.() ?? 0) : undefined;
|
|
639
|
-
const nodeEnv: "development" | "production" = dev ? "development" : "production";
|
|
640
|
-
const routeModuleLoadOptions = {
|
|
641
|
-
cacheBustKey: devCacheBustKey,
|
|
642
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
643
|
-
devSourceImports: false,
|
|
644
|
-
nodeEnv,
|
|
645
|
-
};
|
|
646
|
-
const requestModuleLoadOptions = {
|
|
647
|
-
cacheBustKey: undefined,
|
|
648
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
649
|
-
devSourceImports: dev,
|
|
650
|
-
nodeEnv,
|
|
651
|
-
};
|
|
652
|
-
const routeAssetsById = resolveAllRouteAssets({
|
|
653
|
-
dev,
|
|
654
|
-
runtimeOptions,
|
|
655
|
-
});
|
|
656
|
-
const routerSnapshot = createRouterSnapshot({
|
|
657
|
-
manifest: routeAdapter.manifest,
|
|
658
|
-
routeAssets: routeAssetsById,
|
|
659
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
660
|
-
});
|
|
661
|
-
const fallbackHtmlAssets = resolveFallbackHtmlAssets({
|
|
662
|
-
routeAssets: routeAssetsById,
|
|
663
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
if (request.method.toUpperCase() === "GET" && url.pathname === "/__rbssr/transition") {
|
|
667
|
-
const toParam = url.searchParams.get("to");
|
|
668
|
-
if (!toParam) {
|
|
669
|
-
return finalize(new Response("Missing required `to` query parameter.", { status: 400 }), "internal-transition");
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
let targetUrl: URL;
|
|
673
|
-
try {
|
|
674
|
-
targetUrl = new URL(toParam, url);
|
|
675
|
-
} catch {
|
|
676
|
-
return finalize(new Response("Invalid `to` URL.", { status: 400 }), "internal-transition");
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (targetUrl.origin !== url.origin) {
|
|
680
|
-
return finalize(new Response("Cross-origin transitions are not allowed.", { status: 400 }), "internal-transition");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const transitionPageMatch = routeAdapter.matchPage(targetUrl.pathname);
|
|
684
|
-
if (!transitionPageMatch) {
|
|
685
|
-
const rootModule = await loadRootOnlyModule(activeConfig.rootModule, {
|
|
686
|
-
...routeModuleLoadOptions,
|
|
687
|
-
});
|
|
688
|
-
const fallbackRoute: RouteModule = {
|
|
689
|
-
default: () => null,
|
|
690
|
-
NotFound: rootModule.NotFound,
|
|
691
|
-
};
|
|
692
|
-
const payload = {
|
|
693
|
-
routeId: "__not_found__",
|
|
694
|
-
data: null,
|
|
695
|
-
params: {},
|
|
696
|
-
url: targetUrl.toString(),
|
|
697
|
-
};
|
|
698
|
-
const modules = {
|
|
699
|
-
root: rootModule,
|
|
700
|
-
layouts: [],
|
|
701
|
-
route: fallbackRoute,
|
|
702
|
-
};
|
|
703
|
-
const initialChunk: TransitionInitialChunk = {
|
|
704
|
-
type: "initial",
|
|
705
|
-
kind: "not_found",
|
|
706
|
-
status: 404,
|
|
707
|
-
payload,
|
|
708
|
-
head: createManagedHeadMarkup({
|
|
709
|
-
headMarkup: collectHeadMarkup(modules, payload),
|
|
710
|
-
assets: fallbackHtmlAssets,
|
|
711
|
-
}),
|
|
712
|
-
redirected: false,
|
|
713
|
-
};
|
|
714
|
-
const stream = createTransitionStream({
|
|
715
|
-
initialChunk,
|
|
716
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
717
|
-
});
|
|
718
|
-
return finalize(toTransitionStreamResponse(stream), "internal-transition");
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const [routeModules, globalMiddleware, nestedMiddleware] = await Promise.all([
|
|
722
|
-
loadRouteModules({
|
|
723
|
-
rootFilePath: activeConfig.rootModule,
|
|
724
|
-
layoutFiles: transitionPageMatch.route.layoutFiles,
|
|
725
|
-
routeFilePath: transitionPageMatch.route.filePath,
|
|
726
|
-
...routeModuleLoadOptions,
|
|
727
|
-
}),
|
|
728
|
-
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
729
|
-
loadNestedMiddleware(transitionPageMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
730
|
-
]);
|
|
731
|
-
const moduleMiddleware = extractRouteMiddleware(routeModules.route);
|
|
732
|
-
const routeAssets = routeAssetsById[transitionPageMatch.route.id] ?? null;
|
|
733
|
-
const transitionRequest = new Request(targetUrl.toString(), {
|
|
734
|
-
method: "GET",
|
|
735
|
-
headers: request.headers,
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
const requestContext: RequestContext = {
|
|
739
|
-
request: transitionRequest,
|
|
740
|
-
url: targetUrl,
|
|
741
|
-
params: transitionPageMatch.params,
|
|
742
|
-
cookies: parseCookieHeader(request.headers.get("cookie")),
|
|
743
|
-
locals: {},
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
let transitionInitialChunk: TransitionInitialChunk | undefined;
|
|
747
|
-
let deferredSettleEntries: DeferredSettleEntry[] = [];
|
|
748
|
-
let transitionPhase: RouteErrorPhase = "middleware";
|
|
749
|
-
|
|
750
|
-
let middlewareResponse: Response;
|
|
751
|
-
try {
|
|
752
|
-
middlewareResponse = await runMiddlewareChain(
|
|
753
|
-
[...globalMiddleware, ...nestedMiddleware, ...moduleMiddleware],
|
|
754
|
-
requestContext,
|
|
755
|
-
async () => {
|
|
756
|
-
let dataForRender: unknown = null;
|
|
757
|
-
let dataForPayload: unknown = null;
|
|
758
|
-
|
|
759
|
-
if (routeModules.route.loader) {
|
|
760
|
-
transitionPhase = "loader";
|
|
761
|
-
const loaderCtx: LoaderContext = requestContext;
|
|
762
|
-
const loaderResult = await routeModules.route.loader(loaderCtx);
|
|
763
|
-
|
|
764
|
-
if (isResponse(loaderResult)) {
|
|
765
|
-
return loaderResult;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (isRedirectResult(loaderResult)) {
|
|
769
|
-
return toRedirectResponse(loaderResult.location, loaderResult.status);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (isDeferredLoaderResult(loaderResult)) {
|
|
773
|
-
const prepared = prepareDeferredPayload(transitionPageMatch.route.id, loaderResult);
|
|
774
|
-
dataForRender = prepared.dataForRender;
|
|
775
|
-
dataForPayload = prepared.dataForPayload;
|
|
776
|
-
deferredSettleEntries = prepared.settleEntries;
|
|
777
|
-
} else {
|
|
778
|
-
dataForRender = loaderResult;
|
|
779
|
-
dataForPayload = loaderResult;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const renderPayload = {
|
|
784
|
-
routeId: transitionPageMatch.route.id,
|
|
785
|
-
data: dataForRender,
|
|
786
|
-
params: transitionPageMatch.params,
|
|
787
|
-
url: targetUrl.toString(),
|
|
788
|
-
};
|
|
789
|
-
const payload = {
|
|
790
|
-
...renderPayload,
|
|
791
|
-
data: dataForPayload,
|
|
792
|
-
};
|
|
793
|
-
transitionInitialChunk = {
|
|
794
|
-
type: "initial",
|
|
795
|
-
kind: "page",
|
|
796
|
-
status: 200,
|
|
797
|
-
payload,
|
|
798
|
-
head: createManagedHeadMarkup({
|
|
799
|
-
headMarkup: collectHeadMarkup(routeModules, renderPayload),
|
|
800
|
-
assets: {
|
|
801
|
-
script: routeAssets?.script,
|
|
802
|
-
css: routeAssets?.css ?? [],
|
|
803
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
804
|
-
},
|
|
805
|
-
}),
|
|
806
|
-
redirected: false,
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
return new Response(null, { status: 204 });
|
|
810
|
-
},
|
|
811
|
-
);
|
|
812
|
-
} catch (error) {
|
|
813
|
-
const redirectResponse = resolveThrownRedirect(error);
|
|
814
|
-
if (redirectResponse) {
|
|
815
|
-
const location = redirectResponse.headers.get("location");
|
|
816
|
-
if (location) {
|
|
817
|
-
const stream = createTransitionStream({
|
|
818
|
-
controlChunk: toRedirectChunk(location, redirectResponse.status),
|
|
819
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
820
|
-
});
|
|
821
|
-
return finalize(toTransitionStreamResponse(stream, redirectResponse.headers), "internal-transition");
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const contextBase = toRouteErrorContextBase({
|
|
826
|
-
requestContext,
|
|
827
|
-
routeId: transitionPageMatch.route.id,
|
|
828
|
-
phase: transitionPhase,
|
|
829
|
-
dev,
|
|
830
|
-
});
|
|
831
|
-
const caught = toRouteErrorResponse(error);
|
|
832
|
-
if (caught) {
|
|
833
|
-
const payload = {
|
|
834
|
-
routeId: transitionPageMatch.route.id,
|
|
835
|
-
data: null,
|
|
836
|
-
params: transitionPageMatch.params,
|
|
837
|
-
url: targetUrl.toString(),
|
|
838
|
-
error: toCaughtErrorPayload(caught, !dev),
|
|
839
|
-
};
|
|
840
|
-
await notifyCatchHooks({
|
|
841
|
-
modules: routeModules,
|
|
842
|
-
context: {
|
|
843
|
-
...contextBase,
|
|
844
|
-
error: caught,
|
|
845
|
-
},
|
|
846
|
-
});
|
|
847
|
-
const initialChunk: TransitionInitialChunk = {
|
|
848
|
-
type: "initial",
|
|
849
|
-
kind: "catch",
|
|
850
|
-
status: caught.status,
|
|
851
|
-
payload,
|
|
852
|
-
head: createManagedHeadMarkup({
|
|
853
|
-
headMarkup: collectHeadMarkup(routeModules, payload),
|
|
854
|
-
assets: {
|
|
855
|
-
script: routeAssets?.script,
|
|
856
|
-
css: routeAssets?.css ?? [],
|
|
857
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
858
|
-
},
|
|
859
|
-
}),
|
|
860
|
-
redirected: false,
|
|
861
|
-
};
|
|
862
|
-
const stream = createTransitionStream({
|
|
863
|
-
initialChunk,
|
|
864
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
865
|
-
});
|
|
866
|
-
return finalize(toTransitionStreamResponse(stream), "internal-transition");
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
await notifyErrorHooks({
|
|
870
|
-
modules: routeModules,
|
|
871
|
-
context: {
|
|
872
|
-
...contextBase,
|
|
873
|
-
error,
|
|
874
|
-
},
|
|
875
|
-
});
|
|
876
|
-
const renderPayload = {
|
|
877
|
-
routeId: transitionPageMatch.route.id,
|
|
878
|
-
data: null,
|
|
879
|
-
params: transitionPageMatch.params,
|
|
880
|
-
url: targetUrl.toString(),
|
|
881
|
-
error: toUncaughtErrorPayload(error, !dev),
|
|
882
|
-
};
|
|
883
|
-
const initialChunk: TransitionInitialChunk = {
|
|
884
|
-
type: "initial",
|
|
885
|
-
kind: "error",
|
|
886
|
-
status: 500,
|
|
887
|
-
payload: renderPayload,
|
|
888
|
-
head: createManagedHeadMarkup({
|
|
889
|
-
headMarkup: collectHeadMarkup(routeModules, renderPayload),
|
|
890
|
-
assets: {
|
|
891
|
-
script: routeAssets?.script,
|
|
892
|
-
css: routeAssets?.css ?? [],
|
|
893
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
894
|
-
},
|
|
895
|
-
}),
|
|
896
|
-
redirected: false,
|
|
897
|
-
};
|
|
898
|
-
const stream = createTransitionStream({
|
|
899
|
-
initialChunk,
|
|
900
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
901
|
-
});
|
|
902
|
-
return finalize(toTransitionStreamResponse(stream), "internal-transition");
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const redirectLocation = middlewareResponse.headers.get("location");
|
|
906
|
-
if (redirectLocation && isRedirectStatus(middlewareResponse.status)) {
|
|
907
|
-
const stream = createTransitionStream({
|
|
908
|
-
controlChunk: toRedirectChunk(redirectLocation, middlewareResponse.status),
|
|
909
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
910
|
-
});
|
|
911
|
-
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
if (!transitionInitialChunk) {
|
|
915
|
-
const stream = createTransitionStream({
|
|
916
|
-
controlChunk: toDocumentChunk(targetUrl.toString(), middlewareResponse.status),
|
|
917
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
918
|
-
});
|
|
919
|
-
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const stream = createTransitionStream({
|
|
923
|
-
initialChunk: transitionInitialChunk,
|
|
924
|
-
deferredSettleEntries,
|
|
925
|
-
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
926
|
-
});
|
|
927
|
-
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const apiMatch = routeAdapter.matchApi(url.pathname);
|
|
931
|
-
if (apiMatch) {
|
|
932
|
-
const apiModule = await loadApiRouteModule(apiMatch.route.filePath, requestModuleLoadOptions);
|
|
933
|
-
const methodHandler = getMethodHandler(apiModule as Record<string, unknown>, request.method);
|
|
934
|
-
|
|
935
|
-
if (typeof methodHandler !== "function") {
|
|
936
|
-
const allow = getAllowedMethods(apiModule as Record<string, unknown>);
|
|
937
|
-
return finalize(new Response("Method Not Allowed", {
|
|
938
|
-
status: 405,
|
|
939
|
-
headers: {
|
|
940
|
-
allow: allow.join(", "),
|
|
941
|
-
},
|
|
942
|
-
}), "api");
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const requestContext: RequestContext = {
|
|
946
|
-
request,
|
|
947
|
-
url,
|
|
948
|
-
params: apiMatch.params,
|
|
949
|
-
cookies: parseCookieHeader(request.headers.get("cookie")),
|
|
950
|
-
locals: {},
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
const [globalMiddleware, routeMiddleware] = await Promise.all([
|
|
954
|
-
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
955
|
-
loadNestedMiddleware(apiMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
956
|
-
]);
|
|
957
|
-
const allMiddleware = [...globalMiddleware, ...routeMiddleware];
|
|
958
|
-
let apiPhase: RouteErrorPhase = "middleware";
|
|
959
|
-
|
|
960
|
-
let response: Response;
|
|
961
|
-
try {
|
|
962
|
-
response = await runMiddlewareChain(allMiddleware, requestContext, async () => {
|
|
963
|
-
apiPhase = "api";
|
|
964
|
-
const result = await (methodHandler as (ctx: RequestContext) => unknown)(requestContext);
|
|
965
|
-
|
|
966
|
-
if (isResponse(result)) {
|
|
967
|
-
return result;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
if (isRedirectResult(result)) {
|
|
971
|
-
return toRedirectResponse(result.location, result.status);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
return json(result);
|
|
975
|
-
});
|
|
976
|
-
} catch (error) {
|
|
977
|
-
const redirectResponse = resolveThrownRedirect(error);
|
|
978
|
-
if (redirectResponse) {
|
|
979
|
-
return finalize(redirectResponse, "api");
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const caught = toRouteErrorResponse(error);
|
|
983
|
-
if (caught) {
|
|
984
|
-
return finalize(toRouteErrorHttpResponse(toCaughtErrorPayload(caught, !dev)), "api");
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const apiErrorHook = (apiModule as Record<string, unknown>).onError;
|
|
988
|
-
if (typeof apiErrorHook === "function") {
|
|
989
|
-
try {
|
|
990
|
-
await (apiErrorHook as (ctx: RouteErrorContext) => void | Promise<void>)({
|
|
991
|
-
error,
|
|
992
|
-
request: requestContext.request,
|
|
993
|
-
url: requestContext.url,
|
|
994
|
-
params: requestContext.params,
|
|
995
|
-
routeId: apiMatch.route.id,
|
|
996
|
-
phase: apiPhase,
|
|
997
|
-
dev,
|
|
998
|
-
});
|
|
999
|
-
} catch (hookError) {
|
|
1000
|
-
// eslint-disable-next-line no-console
|
|
1001
|
-
console.warn("[rbssr] api onError hook failed", Bun.inspect(hookError));
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return finalize(json(
|
|
1006
|
-
{
|
|
1007
|
-
error: sanitizeErrorMessage(error, !dev),
|
|
1008
|
-
},
|
|
1009
|
-
{ status: 500 },
|
|
1010
|
-
), "api");
|
|
1011
|
-
}
|
|
1012
|
-
return finalize(response, "api");
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
const pageMatch = routeAdapter.matchPage(url.pathname);
|
|
1016
|
-
|
|
1017
|
-
if (!pageMatch) {
|
|
1018
|
-
const rootModule = await loadRootOnlyModule(activeConfig.rootModule, {
|
|
1019
|
-
...routeModuleLoadOptions,
|
|
1020
|
-
});
|
|
1021
|
-
const fallbackRoute: RouteModule = {
|
|
1022
|
-
default: () => null,
|
|
1023
|
-
NotFound: rootModule.NotFound,
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
|
-
const payload = {
|
|
1027
|
-
routeId: "__not_found__",
|
|
1028
|
-
data: null,
|
|
1029
|
-
params: {},
|
|
1030
|
-
url: url.toString(),
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
const modules = {
|
|
1034
|
-
root: rootModule,
|
|
1035
|
-
layouts: [],
|
|
1036
|
-
route: fallbackRoute,
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
const appTree = createNotFoundAppTree(modules, payload) ?? createElement(
|
|
1040
|
-
"main",
|
|
1041
|
-
null,
|
|
1042
|
-
createElement("h1", null, "404"),
|
|
1043
|
-
createElement("p", null, "Page not found."),
|
|
1044
|
-
);
|
|
1045
|
-
const stream = await renderDocumentStream({
|
|
1046
|
-
appTree,
|
|
1047
|
-
payload,
|
|
1048
|
-
assets: fallbackHtmlAssets,
|
|
1049
|
-
headElements: collectHeadElements(modules, payload),
|
|
1050
|
-
routerSnapshot,
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
return finalize(toHtmlStreamResponse(stream, 404), "html");
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
const [routeModules, globalMiddleware, nestedMiddleware] = await Promise.all([
|
|
1057
|
-
loadRouteModules({
|
|
1058
|
-
rootFilePath: activeConfig.rootModule,
|
|
1059
|
-
layoutFiles: pageMatch.route.layoutFiles,
|
|
1060
|
-
routeFilePath: pageMatch.route.filePath,
|
|
1061
|
-
...routeModuleLoadOptions,
|
|
1062
|
-
}),
|
|
1063
|
-
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
1064
|
-
loadNestedMiddleware(pageMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
1065
|
-
]);
|
|
1066
|
-
const moduleMiddleware = extractRouteMiddleware(routeModules.route);
|
|
1067
|
-
|
|
1068
|
-
const requestContext: RequestContext = {
|
|
1069
|
-
request,
|
|
1070
|
-
url,
|
|
1071
|
-
params: pageMatch.params,
|
|
1072
|
-
cookies: parseCookieHeader(request.headers.get("cookie")),
|
|
1073
|
-
locals: {},
|
|
1074
|
-
};
|
|
1075
|
-
|
|
1076
|
-
const routeAssets = routeAssetsById[pageMatch.route.id] ?? null;
|
|
1077
|
-
let pagePhase: RouteErrorPhase = "middleware";
|
|
1078
|
-
|
|
1079
|
-
const renderFailureDocument = async (
|
|
1080
|
-
failure: unknown,
|
|
1081
|
-
phase: RouteErrorPhase,
|
|
1082
|
-
): Promise<Response> => {
|
|
1083
|
-
const contextBase = toRouteErrorContextBase({
|
|
1084
|
-
requestContext,
|
|
1085
|
-
routeId: pageMatch.route.id,
|
|
1086
|
-
phase,
|
|
1087
|
-
dev,
|
|
1088
|
-
});
|
|
1089
|
-
const caught = toRouteErrorResponse(failure);
|
|
1090
|
-
if (caught) {
|
|
1091
|
-
const serializedCatch = toCaughtErrorPayload(caught, !dev);
|
|
1092
|
-
await notifyCatchHooks({
|
|
1093
|
-
modules: routeModules,
|
|
1094
|
-
context: {
|
|
1095
|
-
...contextBase,
|
|
1096
|
-
error: caught,
|
|
1097
|
-
},
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
const basePayload = {
|
|
1101
|
-
routeId: pageMatch.route.id,
|
|
1102
|
-
data: null,
|
|
1103
|
-
params: pageMatch.params,
|
|
1104
|
-
url: url.toString(),
|
|
1105
|
-
};
|
|
1106
|
-
const catchPayload = {
|
|
1107
|
-
...basePayload,
|
|
1108
|
-
error: serializedCatch,
|
|
1109
|
-
};
|
|
1110
|
-
|
|
1111
|
-
if (serializedCatch.status === 404) {
|
|
1112
|
-
const notFoundTree = createNotFoundAppTree(routeModules, catchPayload);
|
|
1113
|
-
if (notFoundTree) {
|
|
1114
|
-
const stream = await renderDocumentStream({
|
|
1115
|
-
appTree: notFoundTree,
|
|
1116
|
-
payload: catchPayload,
|
|
1117
|
-
assets: {
|
|
1118
|
-
script: routeAssets?.script,
|
|
1119
|
-
css: routeAssets?.css ?? [],
|
|
1120
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
1121
|
-
},
|
|
1122
|
-
headElements: collectHeadElements(routeModules, catchPayload),
|
|
1123
|
-
routerSnapshot,
|
|
1124
|
-
});
|
|
1125
|
-
return toHtmlStreamResponse(stream, 404);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
const catchTree = createCatchAppTree(routeModules, catchPayload, serializedCatch);
|
|
1130
|
-
if (catchTree) {
|
|
1131
|
-
const stream = await renderDocumentStream({
|
|
1132
|
-
appTree: catchTree,
|
|
1133
|
-
payload: catchPayload,
|
|
1134
|
-
assets: {
|
|
1135
|
-
script: routeAssets?.script,
|
|
1136
|
-
css: routeAssets?.css ?? [],
|
|
1137
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
1138
|
-
},
|
|
1139
|
-
headElements: collectHeadElements(routeModules, catchPayload),
|
|
1140
|
-
routerSnapshot,
|
|
1141
|
-
});
|
|
1142
|
-
return toHtmlStreamResponse(stream, serializedCatch.status);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
return toRouteErrorHttpResponse(serializedCatch);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
await notifyErrorHooks({
|
|
1149
|
-
modules: routeModules,
|
|
1150
|
-
context: {
|
|
1151
|
-
...contextBase,
|
|
1152
|
-
error: failure,
|
|
1153
|
-
},
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
const renderPayload = {
|
|
1157
|
-
routeId: pageMatch.route.id,
|
|
1158
|
-
data: null,
|
|
1159
|
-
params: pageMatch.params,
|
|
1160
|
-
url: url.toString(),
|
|
1161
|
-
};
|
|
1162
|
-
const errorPayload = {
|
|
1163
|
-
...renderPayload,
|
|
1164
|
-
error: toUncaughtErrorPayload(failure, !dev),
|
|
1165
|
-
};
|
|
1166
|
-
const boundaryTree = createErrorAppTree(routeModules, errorPayload, failure);
|
|
1167
|
-
if (boundaryTree) {
|
|
1168
|
-
const stream = await renderDocumentStream({
|
|
1169
|
-
appTree: boundaryTree,
|
|
1170
|
-
payload: errorPayload,
|
|
1171
|
-
assets: {
|
|
1172
|
-
script: routeAssets?.script,
|
|
1173
|
-
css: routeAssets?.css ?? [],
|
|
1174
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
1175
|
-
},
|
|
1176
|
-
headElements: collectHeadElements(routeModules, errorPayload),
|
|
1177
|
-
routerSnapshot,
|
|
1178
|
-
});
|
|
1179
|
-
return toHtmlStreamResponse(stream, 500);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
return new Response(sanitizeErrorMessage(failure, !dev), { status: 500 });
|
|
1183
|
-
};
|
|
1184
|
-
|
|
1185
|
-
let response: Response;
|
|
1186
|
-
try {
|
|
1187
|
-
response = await runMiddlewareChain(
|
|
1188
|
-
[...globalMiddleware, ...nestedMiddleware, ...moduleMiddleware],
|
|
1189
|
-
requestContext,
|
|
1190
|
-
async () => {
|
|
1191
|
-
const method = request.method.toUpperCase();
|
|
1192
|
-
let dataForRender: unknown = null;
|
|
1193
|
-
let dataForPayload: unknown = null;
|
|
1194
|
-
let deferredSettleEntries: DeferredSettleEntry[] = [];
|
|
1195
|
-
|
|
1196
|
-
if (isMutatingMethod(method)) {
|
|
1197
|
-
if (!routeModules.route.action) {
|
|
1198
|
-
return new Response("Method Not Allowed", { status: 405 });
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
pagePhase = "action";
|
|
1202
|
-
const body = await parseActionBody(request.clone());
|
|
1203
|
-
const actionCtx: ActionContext = {
|
|
1204
|
-
...requestContext,
|
|
1205
|
-
...body,
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
const actionResult = await routeModules.route.action(actionCtx);
|
|
1209
|
-
|
|
1210
|
-
if (isResponse(actionResult)) {
|
|
1211
|
-
return actionResult;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
if (isRedirectResult(actionResult)) {
|
|
1215
|
-
return toRedirectResponse(actionResult.location, actionResult.status);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
if (isDeferredLoaderResult(actionResult)) {
|
|
1219
|
-
return new Response("defer() is only supported in route loaders", { status: 500 });
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
dataForRender = actionResult;
|
|
1223
|
-
dataForPayload = actionResult;
|
|
1224
|
-
} else {
|
|
1225
|
-
if (routeModules.route.loader) {
|
|
1226
|
-
pagePhase = "loader";
|
|
1227
|
-
const loaderCtx: LoaderContext = requestContext;
|
|
1228
|
-
const loaderResult = await routeModules.route.loader(loaderCtx);
|
|
1229
|
-
|
|
1230
|
-
if (isResponse(loaderResult)) {
|
|
1231
|
-
return loaderResult;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (isRedirectResult(loaderResult)) {
|
|
1235
|
-
return toRedirectResponse(loaderResult.location, loaderResult.status);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
if (isDeferredLoaderResult(loaderResult)) {
|
|
1239
|
-
const prepared = prepareDeferredPayload(pageMatch.route.id, loaderResult);
|
|
1240
|
-
dataForRender = prepared.dataForRender;
|
|
1241
|
-
dataForPayload = prepared.dataForPayload;
|
|
1242
|
-
deferredSettleEntries = prepared.settleEntries;
|
|
1243
|
-
} else {
|
|
1244
|
-
dataForRender = loaderResult;
|
|
1245
|
-
dataForPayload = loaderResult;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const renderPayload = {
|
|
1251
|
-
routeId: pageMatch.route.id,
|
|
1252
|
-
data: dataForRender,
|
|
1253
|
-
params: pageMatch.params,
|
|
1254
|
-
url: url.toString(),
|
|
1255
|
-
};
|
|
1256
|
-
|
|
1257
|
-
const clientPayload = {
|
|
1258
|
-
...renderPayload,
|
|
1259
|
-
data: dataForPayload,
|
|
1260
|
-
};
|
|
1261
|
-
|
|
1262
|
-
let appTree: ReturnType<typeof createPageAppTree>;
|
|
1263
|
-
try {
|
|
1264
|
-
pagePhase = "render";
|
|
1265
|
-
appTree = createPageAppTree(routeModules, renderPayload);
|
|
1266
|
-
} catch (error) {
|
|
1267
|
-
const fallbackResponse = await renderFailureDocument(error, pagePhase);
|
|
1268
|
-
return fallbackResponse;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
const stream = await renderDocumentStream({
|
|
1272
|
-
appTree,
|
|
1273
|
-
payload: clientPayload,
|
|
1274
|
-
assets: {
|
|
1275
|
-
script: routeAssets?.script,
|
|
1276
|
-
css: routeAssets?.css ?? [],
|
|
1277
|
-
devVersion: dev ? runtimeOptions.reloadVersion?.() ?? 0 : undefined,
|
|
1278
|
-
},
|
|
1279
|
-
headElements: collectHeadElements(routeModules, renderPayload),
|
|
1280
|
-
routerSnapshot,
|
|
1281
|
-
deferredSettleEntries,
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
return toHtmlStreamResponse(stream, 200);
|
|
1285
|
-
},
|
|
1286
|
-
);
|
|
1287
|
-
} catch (error) {
|
|
1288
|
-
const redirectResponse = resolveThrownRedirect(error);
|
|
1289
|
-
if (redirectResponse) {
|
|
1290
|
-
return finalize(redirectResponse, "html");
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
const fallbackResponse = await renderFailureDocument(error, pagePhase);
|
|
1294
|
-
return finalize(fallbackResponse, "html");
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
return finalize(response, "html");
|
|
1298
|
-
};
|
|
1299
|
-
|
|
1300
|
-
return {
|
|
1301
|
-
fetch: fetchHandler,
|
|
1302
|
-
};
|
|
11
|
+
const resolvedConfig = resolveConfig(config);
|
|
12
|
+
return createRequestExecutor({
|
|
13
|
+
config: resolvedConfig,
|
|
14
|
+
runtimeOptions,
|
|
15
|
+
});
|
|
1303
16
|
}
|
|
1304
17
|
|
|
1305
18
|
export function startHttpServer(options: {
|