react-bun-ssr 0.4.0 → 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 +2 -0
- package/framework/runtime/client-runtime.tsx +65 -181
- package/framework/runtime/request-executor.ts +1705 -0
- package/framework/runtime/route-wire-protocol.ts +486 -0
- package/framework/runtime/server.ts +8 -1631
- package/framework/runtime/tree.tsx +15 -90
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,6 +109,8 @@ Read more:
|
|
|
109
109
|
|
|
110
110
|
For a page request, the framework resolves the matching route, runs global and nested middleware, executes the matched loader or action, and then renders an HTML response or returns a direct `Response` when the route short-circuits. API routes use the same route tree and middleware model, but return handler responses instead of page HTML.
|
|
111
111
|
|
|
112
|
+
Under the hood, page HTML, API, internal action, and internal transition requests now share the same runtime request boundary, which keeps middleware, redirects, and response finalization behavior aligned across request kinds.
|
|
113
|
+
|
|
112
114
|
Read more:
|
|
113
115
|
|
|
114
116
|
- https://react-bun-ssr.dev/docs/routing/middleware
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { hydrateRoot, type Root } from "react-dom/client";
|
|
2
2
|
import {
|
|
3
|
-
consumeTransitionChunkText,
|
|
4
|
-
createTransitionChunkParserState,
|
|
5
|
-
flushTransitionChunkText,
|
|
6
3
|
isStaleNavigationToken,
|
|
7
4
|
matchClientPageRoute,
|
|
8
5
|
sanitizePrefetchCache,
|
|
9
|
-
shouldHardNavigateForRedirectDepth,
|
|
10
6
|
shouldSkipSoftNavigation,
|
|
11
7
|
} from "./client-transition-core";
|
|
12
|
-
import { isDeferredToken } from "./deferred";
|
|
13
8
|
import {
|
|
14
9
|
addNavigationNavigateListener,
|
|
15
10
|
canNavigationNavigateWithIntercept,
|
|
@@ -21,6 +16,12 @@ import {
|
|
|
21
16
|
RBSSR_ROUTER_SCRIPT_ID,
|
|
22
17
|
} from "./runtime-constants";
|
|
23
18
|
import { replaceManagedHead } from "./head-reconcile";
|
|
19
|
+
import {
|
|
20
|
+
applyRouteWireDeferredChunk,
|
|
21
|
+
completeRouteWireTransition,
|
|
22
|
+
createRouteWireProtocol,
|
|
23
|
+
reviveRouteWirePayload,
|
|
24
|
+
} from "./route-wire-protocol";
|
|
24
25
|
import {
|
|
25
26
|
createCatchAppTree,
|
|
26
27
|
createErrorAppTree,
|
|
@@ -34,7 +35,6 @@ import type {
|
|
|
34
35
|
RenderPayload,
|
|
35
36
|
RouteModule,
|
|
36
37
|
RouteModuleBundle,
|
|
37
|
-
TransitionDeferredChunk,
|
|
38
38
|
TransitionDocumentChunk,
|
|
39
39
|
TransitionInitialChunk,
|
|
40
40
|
TransitionRedirectChunk,
|
|
@@ -73,16 +73,6 @@ interface PrefetchEntry {
|
|
|
73
73
|
donePromise: Promise<void>;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
interface TransitionRequestOptions {
|
|
77
|
-
onDeferredChunk?: (chunk: TransitionDeferredChunk) => void;
|
|
78
|
-
signal?: AbortSignal;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface TransitionRequestHandle {
|
|
82
|
-
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null>;
|
|
83
|
-
donePromise: Promise<void>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
interface FrameworkNavigationInfo {
|
|
87
77
|
__rbssrTransition: true;
|
|
88
78
|
id: string;
|
|
@@ -173,6 +163,28 @@ function getClientRuntimeSingleton(): ClientRuntimeSingleton {
|
|
|
173
163
|
|
|
174
164
|
const clientRuntimeSingleton = getClientRuntimeSingleton();
|
|
175
165
|
|
|
166
|
+
function readCurrentWindowUrl(): URL | null {
|
|
167
|
+
if (typeof window === "undefined") {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return new URL(window.location.href);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getClientRouteWireProtocol() {
|
|
175
|
+
return createRouteWireProtocol({
|
|
176
|
+
getCurrentUrl: readCurrentWindowUrl,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getDeferredRuntime(): DeferredClientRuntime | undefined {
|
|
181
|
+
if (typeof window === "undefined") {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return window.__RBSSR_DEFERRED__;
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
function emitNavigation(info: NavigateResult): void {
|
|
177
189
|
for (const listener of clientRuntimeSingleton.navigationListeners) {
|
|
178
190
|
try {
|
|
@@ -373,31 +385,6 @@ function findPendingTransitionForEvent(event: NavigateEventLike): PendingNavigat
|
|
|
373
385
|
return bestMatch;
|
|
374
386
|
}
|
|
375
387
|
|
|
376
|
-
function reviveDeferredPayload(payload: RenderPayload): RenderPayload {
|
|
377
|
-
const sourceData = payload.loaderData;
|
|
378
|
-
if (!sourceData || Array.isArray(sourceData) || typeof sourceData !== "object") {
|
|
379
|
-
return payload;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const runtime = window.__RBSSR_DEFERRED__;
|
|
383
|
-
if (!runtime) {
|
|
384
|
-
return payload;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const revivedData = { ...(sourceData as Record<string, unknown>) };
|
|
388
|
-
for (const [key, value] of Object.entries(revivedData)) {
|
|
389
|
-
if (!isDeferredToken(value)) {
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
revivedData[key] = runtime.get(value.__rbssrDeferred);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
...payload,
|
|
397
|
-
loaderData: revivedData,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
388
|
function ensureRuntimeState(): RuntimeState {
|
|
402
389
|
if (!clientRuntimeSingleton.runtimeState) {
|
|
403
390
|
throw new Error("Client runtime is not initialized. Ensure hydrateInitialRoute() ran first.");
|
|
@@ -406,106 +393,6 @@ function ensureRuntimeState(): RuntimeState {
|
|
|
406
393
|
return clientRuntimeSingleton.runtimeState;
|
|
407
394
|
}
|
|
408
395
|
|
|
409
|
-
function createTransitionUrl(toUrl: URL): URL {
|
|
410
|
-
const transitionUrl = new URL("/__rbssr/transition", window.location.origin);
|
|
411
|
-
transitionUrl.searchParams.set("to", toUrl.pathname + toUrl.search + toUrl.hash);
|
|
412
|
-
return transitionUrl;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function startTransitionRequest(
|
|
416
|
-
toUrl: URL,
|
|
417
|
-
options: TransitionRequestOptions = {},
|
|
418
|
-
): TransitionRequestHandle {
|
|
419
|
-
let resolveInitial: (
|
|
420
|
-
value: TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null,
|
|
421
|
-
) => void = () => undefined;
|
|
422
|
-
let rejectInitial: (reason?: unknown) => void = () => undefined;
|
|
423
|
-
const initialPromise = new Promise<
|
|
424
|
-
TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null
|
|
425
|
-
>((resolve, reject) => {
|
|
426
|
-
resolveInitial = resolve;
|
|
427
|
-
rejectInitial = reject;
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const donePromise = (async () => {
|
|
431
|
-
const endpoint = createTransitionUrl(toUrl);
|
|
432
|
-
const response = await fetch(endpoint.toString(), {
|
|
433
|
-
method: "GET",
|
|
434
|
-
credentials: "same-origin",
|
|
435
|
-
signal: options.signal,
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
if (!response.ok || !response.body) {
|
|
439
|
-
throw new Error(`Transition request failed with status ${response.status}`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const reader = response.body.getReader();
|
|
443
|
-
const decoder = new TextDecoder();
|
|
444
|
-
let parserState = createTransitionChunkParserState();
|
|
445
|
-
|
|
446
|
-
while (true) {
|
|
447
|
-
const { done, value } = await reader.read();
|
|
448
|
-
if (done) {
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const previousInitialChunk = parserState.initialChunk;
|
|
453
|
-
const previousDeferredCount = parserState.deferredChunks.length;
|
|
454
|
-
parserState = consumeTransitionChunkText(
|
|
455
|
-
parserState,
|
|
456
|
-
decoder.decode(value, { stream: true }),
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
if (!previousInitialChunk && parserState.initialChunk) {
|
|
460
|
-
resolveInitial(parserState.initialChunk);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
|
|
464
|
-
options.onDeferredChunk?.(chunk);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const previousInitialChunk = parserState.initialChunk;
|
|
469
|
-
const previousDeferredCount = parserState.deferredChunks.length;
|
|
470
|
-
parserState = flushTransitionChunkText(parserState);
|
|
471
|
-
|
|
472
|
-
if (!previousInitialChunk && parserState.initialChunk) {
|
|
473
|
-
resolveInitial(parserState.initialChunk);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
|
|
477
|
-
options.onDeferredChunk?.(chunk);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (!parserState.initialChunk) {
|
|
481
|
-
resolveInitial(null);
|
|
482
|
-
}
|
|
483
|
-
})();
|
|
484
|
-
|
|
485
|
-
donePromise.catch(error => {
|
|
486
|
-
rejectInitial(error);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
return {
|
|
490
|
-
initialPromise,
|
|
491
|
-
donePromise,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function applyDeferredChunk(chunk: TransitionDeferredChunk): void {
|
|
496
|
-
const runtime = window.__RBSSR_DEFERRED__;
|
|
497
|
-
if (!runtime) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (chunk.ok) {
|
|
502
|
-
runtime.resolve(chunk.id, chunk.value);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
runtime.reject(chunk.id, chunk.error ?? "Deferred value rejected");
|
|
507
|
-
}
|
|
508
|
-
|
|
509
396
|
async function ensureRouteModuleLoaded(routeId: string, snapshot: ClientRouterSnapshot): Promise<void> {
|
|
510
397
|
if (clientRuntimeSingleton.moduleRegistry.has(routeId)) {
|
|
511
398
|
return;
|
|
@@ -538,8 +425,11 @@ function getOrCreatePrefetchEntry(
|
|
|
538
425
|
? ensureRouteModuleLoaded(routeId, snapshot).catch(() => undefined)
|
|
539
426
|
: Promise.resolve();
|
|
540
427
|
|
|
541
|
-
const transitionRequest =
|
|
542
|
-
|
|
428
|
+
const transitionRequest = getClientRouteWireProtocol().startTransition({
|
|
429
|
+
to: toUrl,
|
|
430
|
+
onDeferredChunk: chunk => {
|
|
431
|
+
applyRouteWireDeferredChunk(chunk, getDeferredRuntime());
|
|
432
|
+
},
|
|
543
433
|
signal,
|
|
544
434
|
});
|
|
545
435
|
const initialPromise = transitionRequest.initialPromise.catch(() => {
|
|
@@ -574,7 +464,7 @@ async function renderTransitionInitial(
|
|
|
574
464
|
options: NavigateOptions & { prefetched: boolean; fromPath: string },
|
|
575
465
|
): Promise<NavigateResult> {
|
|
576
466
|
const state = ensureRuntimeState();
|
|
577
|
-
const revivedPayload =
|
|
467
|
+
const revivedPayload = reviveRouteWirePayload(chunk.payload, getDeferredRuntime());
|
|
578
468
|
let modules: RouteModuleBundle | null = null;
|
|
579
469
|
let tree = null as ReturnType<typeof createPageAppTree> | ReturnType<typeof createNotFoundAppTree>;
|
|
580
470
|
|
|
@@ -733,43 +623,34 @@ async function navigateToInternal(
|
|
|
733
623
|
throw new Error("Transition response did not include an initial payload.");
|
|
734
624
|
}
|
|
735
625
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const result = await renderTransitionInitial(initialChunk, toUrl, {
|
|
766
|
-
...options,
|
|
767
|
-
prefetched: usedPrefetch,
|
|
768
|
-
fromPath: currentPath,
|
|
626
|
+
return completeRouteWireTransition(initialChunk, {
|
|
627
|
+
currentUrl: new URL(window.location.href),
|
|
628
|
+
redirectDepth: options.redirectDepth,
|
|
629
|
+
render: async chunk => {
|
|
630
|
+
const result = await renderTransitionInitial(chunk, toUrl, {
|
|
631
|
+
...options,
|
|
632
|
+
prefetched: usedPrefetch,
|
|
633
|
+
fromPath: currentPath,
|
|
634
|
+
});
|
|
635
|
+
options.onNavigate?.(result);
|
|
636
|
+
emitNavigation(result);
|
|
637
|
+
return result;
|
|
638
|
+
},
|
|
639
|
+
softNavigate: async (location, redirectInfo) => {
|
|
640
|
+
return navigateToInternal(new URL(location, window.location.href), {
|
|
641
|
+
...options,
|
|
642
|
+
replace: redirectInfo.replace,
|
|
643
|
+
redirected: redirectInfo.redirected,
|
|
644
|
+
redirectDepth: redirectInfo.redirectDepth,
|
|
645
|
+
// The intercepted navigation has already committed the source URL.
|
|
646
|
+
// The redirected target must update history explicitly.
|
|
647
|
+
historyManagedByNavigationApi: false,
|
|
648
|
+
});
|
|
649
|
+
},
|
|
650
|
+
hardNavigate: location => {
|
|
651
|
+
hardNavigate(new URL(location, window.location.href));
|
|
652
|
+
},
|
|
769
653
|
});
|
|
770
|
-
options.onNavigate?.(result);
|
|
771
|
-
emitNavigation(result);
|
|
772
|
-
return result;
|
|
773
654
|
} catch {
|
|
774
655
|
hardNavigate(toUrl);
|
|
775
656
|
return null;
|
|
@@ -1060,7 +941,10 @@ export function hydrateInitialRoute(routeId: string): void {
|
|
|
1060
941
|
return;
|
|
1061
942
|
}
|
|
1062
943
|
|
|
1063
|
-
const payload =
|
|
944
|
+
const payload = reviveRouteWirePayload(
|
|
945
|
+
getScriptJson<RenderPayload>(RBSSR_PAYLOAD_SCRIPT_ID),
|
|
946
|
+
getDeferredRuntime(),
|
|
947
|
+
);
|
|
1064
948
|
const routerSnapshot = getScriptJson<ClientRouterSnapshot>(RBSSR_ROUTER_SCRIPT_ID);
|
|
1065
949
|
const modules = clientRuntimeSingleton.moduleRegistry.get(routeId);
|
|
1066
950
|
if (!modules) {
|