react-bun-ssr 0.1.1 → 0.3.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.
- package/README.md +25 -17
- package/framework/cli/commands.ts +104 -246
- package/framework/cli/dev-client-watch.ts +288 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +142 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +134 -13
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +150 -159
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/config.ts +7 -2
- package/framework/runtime/index.ts +1 -1
- package/framework/runtime/link.tsx +3 -11
- package/framework/runtime/markdown-routes.ts +1 -14
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +75 -25
- package/framework/runtime/render.tsx +150 -22
- package/framework/runtime/route-api.ts +1 -1
- package/framework/runtime/router.ts +75 -4
- package/framework/runtime/server.ts +57 -106
- package/framework/runtime/tree.tsx +24 -2
- package/framework/runtime/types.ts +13 -2
- package/framework/runtime/utils.ts +3 -0
- package/package.json +13 -7
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { hydrateRoot, type Root } from "react-dom/client";
|
|
2
|
+
import {
|
|
3
|
+
consumeTransitionChunkText,
|
|
4
|
+
createTransitionChunkParserState,
|
|
5
|
+
flushTransitionChunkText,
|
|
6
|
+
isStaleNavigationToken,
|
|
7
|
+
matchClientPageRoute,
|
|
8
|
+
sanitizePrefetchCache,
|
|
9
|
+
shouldHardNavigateForRedirectDepth,
|
|
10
|
+
shouldSkipSoftNavigation,
|
|
11
|
+
} from "./client-transition-core";
|
|
2
12
|
import { isDeferredToken } from "./deferred";
|
|
3
13
|
import {
|
|
4
14
|
addNavigationNavigateListener,
|
|
@@ -21,14 +31,12 @@ import {
|
|
|
21
31
|
} from "./tree";
|
|
22
32
|
import { isRouteErrorResponse } from "./route-errors";
|
|
23
33
|
import type {
|
|
24
|
-
ClientRouteSnapshot,
|
|
25
34
|
ClientRouterSnapshot,
|
|
26
|
-
Params,
|
|
27
35
|
RenderPayload,
|
|
28
36
|
RouteModule,
|
|
29
37
|
RouteModuleBundle,
|
|
30
|
-
TransitionChunk,
|
|
31
38
|
TransitionDeferredChunk,
|
|
39
|
+
TransitionDocumentChunk,
|
|
32
40
|
TransitionInitialChunk,
|
|
33
41
|
TransitionRedirectChunk,
|
|
34
42
|
} from "./types";
|
|
@@ -52,6 +60,7 @@ interface NavigateOptions {
|
|
|
52
60
|
interface NavigateResult {
|
|
53
61
|
from: string;
|
|
54
62
|
to: string;
|
|
63
|
+
nextUrl: URL;
|
|
55
64
|
status: number;
|
|
56
65
|
kind: "page" | "not_found" | "catch" | "error";
|
|
57
66
|
redirected: boolean;
|
|
@@ -61,7 +70,7 @@ interface NavigateResult {
|
|
|
61
70
|
interface PrefetchEntry {
|
|
62
71
|
createdAt: number;
|
|
63
72
|
modulePromise: Promise<void>;
|
|
64
|
-
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | null>;
|
|
73
|
+
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null>;
|
|
65
74
|
donePromise: Promise<void>;
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -71,7 +80,7 @@ interface TransitionRequestOptions {
|
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
interface TransitionRequestHandle {
|
|
74
|
-
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | null>;
|
|
83
|
+
initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null>;
|
|
75
84
|
donePromise: Promise<void>;
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -120,86 +129,82 @@ interface RuntimeState {
|
|
|
120
129
|
transitionAbortController: AbortController | null;
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
interface ClientRuntimeSingleton {
|
|
133
|
+
moduleRegistry: Map<string, RouteModuleBundle>;
|
|
134
|
+
pendingNavigationTransitions: Map<string, PendingNavigationTransition>;
|
|
135
|
+
navigationListeners: Set<(info: NavigateResult) => void>;
|
|
136
|
+
runtimeState: RuntimeState | null;
|
|
137
|
+
popstateBound: boolean;
|
|
138
|
+
navigationApiListenerBound: boolean;
|
|
139
|
+
navigationApiTransitionCounter: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
declare global {
|
|
124
143
|
interface Window {
|
|
125
144
|
__RBSSR_DEFERRED__?: DeferredClientRuntime;
|
|
126
145
|
}
|
|
127
146
|
}
|
|
128
147
|
|
|
129
|
-
const PREFETCH_TTL_MS = 30_000;
|
|
130
148
|
const NAVIGATION_API_PENDING_TIMEOUT_MS = 1_500;
|
|
131
149
|
const NAVIGATION_API_PENDING_MATCH_WINDOW_MS = 10_000;
|
|
132
150
|
const ROUTE_ANNOUNCER_ID = "__rbssr-route-announcer";
|
|
133
|
-
const
|
|
134
|
-
const pendingNavigationTransitions = new Map<string, PendingNavigationTransition>();
|
|
135
|
-
let runtimeState: RuntimeState | null = null;
|
|
136
|
-
let popstateBound = false;
|
|
137
|
-
let navigationApiListenerBound = false;
|
|
138
|
-
let navigationApiTransitionCounter = 0;
|
|
139
|
-
|
|
140
|
-
function normalizePathname(pathname: string): string[] {
|
|
141
|
-
if (!pathname || pathname === "/") {
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return pathname
|
|
146
|
-
.replace(/^\/+/, "")
|
|
147
|
-
.replace(/\/+$/, "")
|
|
148
|
-
.split("/")
|
|
149
|
-
.filter(Boolean)
|
|
150
|
-
.map(part => decodeURIComponent(part));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function matchSegments(segments: ClientRouteSnapshot["segments"], pathname: string): Params | null {
|
|
154
|
-
const pathParts = normalizePathname(pathname);
|
|
155
|
-
const params: Params = {};
|
|
151
|
+
const CLIENT_RUNTIME_SINGLETON_KEY = Symbol.for("react-bun-ssr.client-runtime");
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
function getClientRuntimeSingleton(): ClientRuntimeSingleton {
|
|
154
|
+
const globalRegistry = globalThis as typeof globalThis & {
|
|
155
|
+
[CLIENT_RUNTIME_SINGLETON_KEY]?: ClientRuntimeSingleton;
|
|
156
|
+
};
|
|
157
|
+
const existing = globalRegistry[CLIENT_RUNTIME_SINGLETON_KEY];
|
|
158
|
+
if (existing) {
|
|
159
|
+
return existing;
|
|
160
|
+
}
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
const singleton: ClientRuntimeSingleton = {
|
|
163
|
+
moduleRegistry: new Map(),
|
|
164
|
+
pendingNavigationTransitions: new Map(),
|
|
165
|
+
navigationListeners: new Set(),
|
|
166
|
+
runtimeState: null,
|
|
167
|
+
popstateBound: false,
|
|
168
|
+
navigationApiListenerBound: false,
|
|
169
|
+
navigationApiTransitionCounter: 0,
|
|
170
|
+
};
|
|
171
|
+
globalRegistry[CLIENT_RUNTIME_SINGLETON_KEY] = singleton;
|
|
172
|
+
return singleton;
|
|
173
|
+
}
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
if (current === undefined) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
175
|
+
const clientRuntimeSingleton = getClientRuntimeSingleton();
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
177
|
+
function emitNavigation(info: NavigateResult): void {
|
|
178
|
+
for (const listener of clientRuntimeSingleton.navigationListeners) {
|
|
179
|
+
try {
|
|
180
|
+
listener(info);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.warn("[rbssr] router navigation listener failed", error);
|
|
179
184
|
}
|
|
180
|
-
|
|
181
|
-
i += 1;
|
|
182
|
-
j += 1;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (j !== pathParts.length) {
|
|
186
|
-
return null;
|
|
187
185
|
}
|
|
186
|
+
}
|
|
188
187
|
|
|
189
|
-
|
|
188
|
+
function pickOptionalClientModuleExport<T>(
|
|
189
|
+
moduleValue: Record<string, unknown>,
|
|
190
|
+
exportName: string,
|
|
191
|
+
): T | undefined {
|
|
192
|
+
const value = moduleValue[exportName];
|
|
193
|
+
return typeof value === "function" ? (value as T) : undefined;
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
):
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
export function projectClientModule(
|
|
197
|
+
defaultExport: RouteModule["default"],
|
|
198
|
+
moduleValue: Record<string, unknown>,
|
|
199
|
+
): RouteModule {
|
|
200
|
+
return {
|
|
201
|
+
default: defaultExport,
|
|
202
|
+
Loading: pickOptionalClientModuleExport<RouteModule["Loading"]>(moduleValue, "Loading"),
|
|
203
|
+
ErrorComponent: pickOptionalClientModuleExport<RouteModule["ErrorComponent"]>(moduleValue, "ErrorComponent"),
|
|
204
|
+
CatchBoundary: pickOptionalClientModuleExport<RouteModule["CatchBoundary"]>(moduleValue, "CatchBoundary"),
|
|
205
|
+
ErrorBoundary: pickOptionalClientModuleExport<RouteModule["ErrorBoundary"]>(moduleValue, "ErrorBoundary"),
|
|
206
|
+
NotFound: pickOptionalClientModuleExport<RouteModule["NotFound"]>(moduleValue, "NotFound"),
|
|
207
|
+
};
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
function withVersionQuery(url: string, version?: number): string {
|
|
@@ -327,18 +332,18 @@ function readNavigationDestinationHref(event: NavigateEventLike): string | null
|
|
|
327
332
|
}
|
|
328
333
|
|
|
329
334
|
function clearPendingNavigationTransition(id: string): void {
|
|
330
|
-
const entry = pendingNavigationTransitions.get(id);
|
|
335
|
+
const entry = clientRuntimeSingleton.pendingNavigationTransitions.get(id);
|
|
331
336
|
if (!entry) {
|
|
332
337
|
return;
|
|
333
338
|
}
|
|
334
339
|
|
|
335
340
|
clearTimeout(entry.timeoutId);
|
|
336
|
-
pendingNavigationTransitions.delete(id);
|
|
341
|
+
clientRuntimeSingleton.pendingNavigationTransitions.delete(id);
|
|
337
342
|
}
|
|
338
343
|
|
|
339
344
|
function findPendingTransitionForEvent(event: NavigateEventLike): PendingNavigationTransition | null {
|
|
340
345
|
if (isFrameworkNavigationInfo(event.info)) {
|
|
341
|
-
return pendingNavigationTransitions.get(event.info.id) ?? null;
|
|
346
|
+
return clientRuntimeSingleton.pendingNavigationTransitions.get(event.info.id) ?? null;
|
|
342
347
|
}
|
|
343
348
|
|
|
344
349
|
if (event.userInitiated) {
|
|
@@ -352,7 +357,7 @@ function findPendingTransitionForEvent(event: NavigateEventLike): PendingNavigat
|
|
|
352
357
|
|
|
353
358
|
const now = Date.now();
|
|
354
359
|
let bestMatch: PendingNavigationTransition | null = null;
|
|
355
|
-
for (const candidate of pendingNavigationTransitions.values()) {
|
|
360
|
+
for (const candidate of clientRuntimeSingleton.pendingNavigationTransitions.values()) {
|
|
356
361
|
if (candidate.destinationHref !== destinationHref) {
|
|
357
362
|
continue;
|
|
358
363
|
}
|
|
@@ -395,20 +400,11 @@ function reviveDeferredPayload(payload: RenderPayload): RenderPayload {
|
|
|
395
400
|
}
|
|
396
401
|
|
|
397
402
|
function ensureRuntimeState(): RuntimeState {
|
|
398
|
-
if (!runtimeState) {
|
|
403
|
+
if (!clientRuntimeSingleton.runtimeState) {
|
|
399
404
|
throw new Error("Client runtime is not initialized. Ensure hydrateInitialRoute() ran first.");
|
|
400
405
|
}
|
|
401
406
|
|
|
402
|
-
return runtimeState;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function sanitizePrefetchCache(cache: Map<string, PrefetchEntry>): void {
|
|
406
|
-
const now = Date.now();
|
|
407
|
-
for (const [key, entry] of cache.entries()) {
|
|
408
|
-
if (now - entry.createdAt > PREFETCH_TTL_MS) {
|
|
409
|
-
cache.delete(key);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
407
|
+
return clientRuntimeSingleton.runtimeState;
|
|
412
408
|
}
|
|
413
409
|
|
|
414
410
|
function createTransitionUrl(toUrl: URL): URL {
|
|
@@ -417,35 +413,17 @@ function createTransitionUrl(toUrl: URL): URL {
|
|
|
417
413
|
return transitionUrl;
|
|
418
414
|
}
|
|
419
415
|
|
|
420
|
-
function splitLines(buffer: string): { lines: string[]; rest: string } {
|
|
421
|
-
const lines: string[] = [];
|
|
422
|
-
let start = 0;
|
|
423
|
-
|
|
424
|
-
for (let index = 0; index < buffer.length; index += 1) {
|
|
425
|
-
if (buffer[index] !== "\n") {
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const line = buffer.slice(start, index).trim();
|
|
430
|
-
if (line.length > 0) {
|
|
431
|
-
lines.push(line);
|
|
432
|
-
}
|
|
433
|
-
start = index + 1;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
lines,
|
|
438
|
-
rest: buffer.slice(start),
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
416
|
function startTransitionRequest(
|
|
443
417
|
toUrl: URL,
|
|
444
418
|
options: TransitionRequestOptions = {},
|
|
445
419
|
): TransitionRequestHandle {
|
|
446
|
-
let resolveInitial: (
|
|
420
|
+
let resolveInitial: (
|
|
421
|
+
value: TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null,
|
|
422
|
+
) => void = () => undefined;
|
|
447
423
|
let rejectInitial: (reason?: unknown) => void = () => undefined;
|
|
448
|
-
const initialPromise = new Promise<
|
|
424
|
+
const initialPromise = new Promise<
|
|
425
|
+
TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null
|
|
426
|
+
>((resolve, reject) => {
|
|
449
427
|
resolveInitial = resolve;
|
|
450
428
|
rejectInitial = reject;
|
|
451
429
|
});
|
|
@@ -464,8 +442,7 @@ function startTransitionRequest(
|
|
|
464
442
|
|
|
465
443
|
const reader = response.body.getReader();
|
|
466
444
|
const decoder = new TextDecoder();
|
|
467
|
-
let
|
|
468
|
-
let textBuffer = "";
|
|
445
|
+
let parserState = createTransitionChunkParserState();
|
|
469
446
|
|
|
470
447
|
while (true) {
|
|
471
448
|
const { done, value } = await reader.read();
|
|
@@ -473,38 +450,35 @@ function startTransitionRequest(
|
|
|
473
450
|
break;
|
|
474
451
|
}
|
|
475
452
|
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
453
|
+
const previousInitialChunk = parserState.initialChunk;
|
|
454
|
+
const previousDeferredCount = parserState.deferredChunks.length;
|
|
455
|
+
parserState = consumeTransitionChunkText(
|
|
456
|
+
parserState,
|
|
457
|
+
decoder.decode(value, { stream: true }),
|
|
458
|
+
);
|
|
479
459
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (!initialChunk) {
|
|
484
|
-
initialChunk = chunk;
|
|
485
|
-
resolveInitial(initialChunk);
|
|
486
|
-
}
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
460
|
+
if (!previousInitialChunk && parserState.initialChunk) {
|
|
461
|
+
resolveInitial(parserState.initialChunk);
|
|
462
|
+
}
|
|
489
463
|
|
|
464
|
+
for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
|
|
490
465
|
options.onDeferredChunk?.(chunk);
|
|
491
466
|
}
|
|
492
467
|
}
|
|
493
468
|
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
resolveInitial(initialChunk);
|
|
501
|
-
}
|
|
502
|
-
} else {
|
|
503
|
-
options.onDeferredChunk?.(chunk);
|
|
504
|
-
}
|
|
469
|
+
const previousInitialChunk = parserState.initialChunk;
|
|
470
|
+
const previousDeferredCount = parserState.deferredChunks.length;
|
|
471
|
+
parserState = flushTransitionChunkText(parserState);
|
|
472
|
+
|
|
473
|
+
if (!previousInitialChunk && parserState.initialChunk) {
|
|
474
|
+
resolveInitial(parserState.initialChunk);
|
|
505
475
|
}
|
|
506
476
|
|
|
507
|
-
|
|
477
|
+
for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
|
|
478
|
+
options.onDeferredChunk?.(chunk);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!parserState.initialChunk) {
|
|
508
482
|
resolveInitial(null);
|
|
509
483
|
}
|
|
510
484
|
})();
|
|
@@ -534,7 +508,7 @@ function applyDeferredChunk(chunk: TransitionDeferredChunk): void {
|
|
|
534
508
|
}
|
|
535
509
|
|
|
536
510
|
async function ensureRouteModuleLoaded(routeId: string, snapshot: ClientRouterSnapshot): Promise<void> {
|
|
537
|
-
if (moduleRegistry.has(routeId)) {
|
|
511
|
+
if (clientRuntimeSingleton.moduleRegistry.has(routeId)) {
|
|
538
512
|
return;
|
|
539
513
|
}
|
|
540
514
|
|
|
@@ -877,6 +851,7 @@ async function renderTransitionInitial(
|
|
|
877
851
|
return {
|
|
878
852
|
from: options.fromPath,
|
|
879
853
|
to: toUrl.pathname + toUrl.search + toUrl.hash,
|
|
854
|
+
nextUrl: new URL(toUrl.toString()),
|
|
880
855
|
status: chunk.status,
|
|
881
856
|
kind: chunk.kind,
|
|
882
857
|
redirected: options.redirected ?? false,
|
|
@@ -917,11 +892,11 @@ async function navigateToInternal(
|
|
|
917
892
|
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
918
893
|
const targetPath = toUrl.pathname + toUrl.search + toUrl.hash;
|
|
919
894
|
|
|
920
|
-
if (currentPath
|
|
895
|
+
if (shouldSkipSoftNavigation(currentPath, targetPath, options)) {
|
|
921
896
|
return null;
|
|
922
897
|
}
|
|
923
898
|
|
|
924
|
-
const matched =
|
|
899
|
+
const matched = matchClientPageRoute(state.routerSnapshot.pages, toUrl.pathname);
|
|
925
900
|
const routeId = matched?.route.id ?? null;
|
|
926
901
|
|
|
927
902
|
if (state.transitionAbortController) {
|
|
@@ -942,7 +917,7 @@ async function navigateToInternal(
|
|
|
942
917
|
|
|
943
918
|
try {
|
|
944
919
|
await prefetchEntry.modulePromise;
|
|
945
|
-
if (
|
|
920
|
+
if (isStaleNavigationToken(state.navigationToken, navigationToken)) {
|
|
946
921
|
return null;
|
|
947
922
|
}
|
|
948
923
|
|
|
@@ -965,7 +940,7 @@ async function navigateToInternal(
|
|
|
965
940
|
}
|
|
966
941
|
|
|
967
942
|
const initialChunk = await prefetchEntry.initialPromise;
|
|
968
|
-
if (
|
|
943
|
+
if (isStaleNavigationToken(state.navigationToken, navigationToken)) {
|
|
969
944
|
return null;
|
|
970
945
|
}
|
|
971
946
|
|
|
@@ -973,6 +948,11 @@ async function navigateToInternal(
|
|
|
973
948
|
throw new Error("Transition response did not include an initial payload.");
|
|
974
949
|
}
|
|
975
950
|
|
|
951
|
+
if (initialChunk.type === "document") {
|
|
952
|
+
hardNavigate(new URL(initialChunk.location, window.location.origin));
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
|
|
976
956
|
if (initialChunk.type === "redirect") {
|
|
977
957
|
const redirectUrl = new URL(initialChunk.location, window.location.origin);
|
|
978
958
|
if (!isInternalUrl(redirectUrl)) {
|
|
@@ -981,7 +961,7 @@ async function navigateToInternal(
|
|
|
981
961
|
}
|
|
982
962
|
|
|
983
963
|
const depth = (options.redirectDepth ?? 0) + 1;
|
|
984
|
-
if (depth
|
|
964
|
+
if (shouldHardNavigateForRedirectDepth(depth)) {
|
|
985
965
|
hardNavigate(redirectUrl);
|
|
986
966
|
return null;
|
|
987
967
|
}
|
|
@@ -991,6 +971,9 @@ async function navigateToInternal(
|
|
|
991
971
|
replace: true,
|
|
992
972
|
redirected: true,
|
|
993
973
|
redirectDepth: depth,
|
|
974
|
+
// The intercepted navigation has already committed the source URL.
|
|
975
|
+
// The redirected target must update history explicitly.
|
|
976
|
+
historyManagedByNavigationApi: false,
|
|
994
977
|
});
|
|
995
978
|
}
|
|
996
979
|
|
|
@@ -1000,6 +983,7 @@ async function navigateToInternal(
|
|
|
1000
983
|
fromPath: currentPath,
|
|
1001
984
|
});
|
|
1002
985
|
options.onNavigate?.(result);
|
|
986
|
+
emitNavigation(result);
|
|
1003
987
|
return result;
|
|
1004
988
|
} catch {
|
|
1005
989
|
hardNavigate(toUrl);
|
|
@@ -1012,8 +996,8 @@ async function navigateToInternal(
|
|
|
1012
996
|
}
|
|
1013
997
|
|
|
1014
998
|
function nextNavigationTransitionId(): string {
|
|
1015
|
-
navigationApiTransitionCounter += 1;
|
|
1016
|
-
return `rbssr-nav-${Date.now()}-${navigationApiTransitionCounter}`;
|
|
999
|
+
clientRuntimeSingleton.navigationApiTransitionCounter += 1;
|
|
1000
|
+
return `rbssr-nav-${Date.now()}-${clientRuntimeSingleton.navigationApiTransitionCounter}`;
|
|
1017
1001
|
}
|
|
1018
1002
|
|
|
1019
1003
|
function settlePendingNavigationTransition(
|
|
@@ -1030,7 +1014,7 @@ function settlePendingNavigationTransition(
|
|
|
1030
1014
|
}
|
|
1031
1015
|
|
|
1032
1016
|
function cancelPendingNavigationTransition(id: string): void {
|
|
1033
|
-
const pending = pendingNavigationTransitions.get(id);
|
|
1017
|
+
const pending = clientRuntimeSingleton.pendingNavigationTransitions.get(id);
|
|
1034
1018
|
if (!pending || pending.settled) {
|
|
1035
1019
|
return;
|
|
1036
1020
|
}
|
|
@@ -1066,7 +1050,7 @@ function createPendingNavigationTransition(options: {
|
|
|
1066
1050
|
}): Promise<NavigateResult | null> {
|
|
1067
1051
|
return new Promise(resolve => {
|
|
1068
1052
|
const timeoutId = window.setTimeout(() => {
|
|
1069
|
-
const pending = pendingNavigationTransitions.get(options.id);
|
|
1053
|
+
const pending = clientRuntimeSingleton.pendingNavigationTransitions.get(options.id);
|
|
1070
1054
|
if (!pending || pending.settled) {
|
|
1071
1055
|
return;
|
|
1072
1056
|
}
|
|
@@ -1082,7 +1066,7 @@ function createPendingNavigationTransition(options: {
|
|
|
1082
1066
|
});
|
|
1083
1067
|
}, NAVIGATION_API_PENDING_TIMEOUT_MS);
|
|
1084
1068
|
|
|
1085
|
-
pendingNavigationTransitions.set(options.id, {
|
|
1069
|
+
clientRuntimeSingleton.pendingNavigationTransitions.set(options.id, {
|
|
1086
1070
|
id: options.id,
|
|
1087
1071
|
destinationHref: options.toUrl.toString(),
|
|
1088
1072
|
replace: options.replace,
|
|
@@ -1097,7 +1081,7 @@ function createPendingNavigationTransition(options: {
|
|
|
1097
1081
|
}
|
|
1098
1082
|
|
|
1099
1083
|
function bindNavigationApiNavigateListener(): void {
|
|
1100
|
-
if (navigationApiListenerBound || typeof window === "undefined") {
|
|
1084
|
+
if (clientRuntimeSingleton.navigationApiListenerBound || typeof window === "undefined") {
|
|
1101
1085
|
return;
|
|
1102
1086
|
}
|
|
1103
1087
|
|
|
@@ -1157,15 +1141,15 @@ function bindNavigationApiNavigateListener(): void {
|
|
|
1157
1141
|
return;
|
|
1158
1142
|
}
|
|
1159
1143
|
|
|
1160
|
-
navigationApiListenerBound = true;
|
|
1144
|
+
clientRuntimeSingleton.navigationApiListenerBound = true;
|
|
1161
1145
|
}
|
|
1162
1146
|
|
|
1163
1147
|
function bindPopstate(): void {
|
|
1164
|
-
if (popstateBound || typeof window === "undefined") {
|
|
1148
|
+
if (clientRuntimeSingleton.popstateBound || typeof window === "undefined") {
|
|
1165
1149
|
return;
|
|
1166
1150
|
}
|
|
1167
1151
|
|
|
1168
|
-
popstateBound = true;
|
|
1152
|
+
clientRuntimeSingleton.popstateBound = true;
|
|
1169
1153
|
window.addEventListener("popstate", () => {
|
|
1170
1154
|
const targetUrl = new URL(window.location.href);
|
|
1171
1155
|
void navigateToInternal(targetUrl, {
|
|
@@ -1181,16 +1165,16 @@ export async function prefetchTo(to: string): Promise<void> {
|
|
|
1181
1165
|
return;
|
|
1182
1166
|
}
|
|
1183
1167
|
|
|
1184
|
-
if (!runtimeState) {
|
|
1168
|
+
if (!clientRuntimeSingleton.runtimeState) {
|
|
1185
1169
|
return;
|
|
1186
1170
|
}
|
|
1187
|
-
const state = runtimeState;
|
|
1171
|
+
const state = clientRuntimeSingleton.runtimeState;
|
|
1188
1172
|
const toUrl = new URL(to, window.location.href);
|
|
1189
1173
|
if (!isInternalUrl(toUrl)) {
|
|
1190
1174
|
return;
|
|
1191
1175
|
}
|
|
1192
1176
|
|
|
1193
|
-
const matched =
|
|
1177
|
+
const matched = matchClientPageRoute(state.routerSnapshot.pages, toUrl.pathname);
|
|
1194
1178
|
const routeId = matched?.route.id ?? null;
|
|
1195
1179
|
getOrCreatePrefetchEntry(toUrl, routeId, state.routerSnapshot);
|
|
1196
1180
|
}
|
|
@@ -1209,7 +1193,7 @@ export async function navigateWithNavigationApiOrFallback(
|
|
|
1209
1193
|
return null;
|
|
1210
1194
|
}
|
|
1211
1195
|
|
|
1212
|
-
if (!runtimeState) {
|
|
1196
|
+
if (!clientRuntimeSingleton.runtimeState) {
|
|
1213
1197
|
hardNavigate(toUrl);
|
|
1214
1198
|
return null;
|
|
1215
1199
|
}
|
|
@@ -1264,7 +1248,7 @@ export async function navigateTo(to: string, options: NavigateOptions = {}): Pro
|
|
|
1264
1248
|
return null;
|
|
1265
1249
|
}
|
|
1266
1250
|
|
|
1267
|
-
if (!runtimeState) {
|
|
1251
|
+
if (!clientRuntimeSingleton.runtimeState) {
|
|
1268
1252
|
hardNavigate(toUrl);
|
|
1269
1253
|
return null;
|
|
1270
1254
|
}
|
|
@@ -1272,21 +1256,28 @@ export async function navigateTo(to: string, options: NavigateOptions = {}): Pro
|
|
|
1272
1256
|
return navigateToInternal(toUrl, options);
|
|
1273
1257
|
}
|
|
1274
1258
|
|
|
1259
|
+
export function subscribeToNavigation(listener: (info: NavigateResult) => void): () => void {
|
|
1260
|
+
clientRuntimeSingleton.navigationListeners.add(listener);
|
|
1261
|
+
return () => {
|
|
1262
|
+
clientRuntimeSingleton.navigationListeners.delete(listener);
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1275
1266
|
export function registerRouteModules(routeId: string, modules: RouteModuleBundle): void {
|
|
1276
|
-
moduleRegistry.set(routeId, modules);
|
|
1277
|
-
if (runtimeState) {
|
|
1278
|
-
runtimeState.moduleRegistry.set(routeId, modules);
|
|
1267
|
+
clientRuntimeSingleton.moduleRegistry.set(routeId, modules);
|
|
1268
|
+
if (clientRuntimeSingleton.runtimeState) {
|
|
1269
|
+
clientRuntimeSingleton.runtimeState.moduleRegistry.set(routeId, modules);
|
|
1279
1270
|
}
|
|
1280
1271
|
}
|
|
1281
1272
|
|
|
1282
1273
|
export function hydrateInitialRoute(routeId: string): void {
|
|
1283
|
-
if (typeof document === "undefined" || runtimeState) {
|
|
1274
|
+
if (typeof document === "undefined" || clientRuntimeSingleton.runtimeState) {
|
|
1284
1275
|
return;
|
|
1285
1276
|
}
|
|
1286
1277
|
|
|
1287
1278
|
const payload = reviveDeferredPayload(getScriptJson<RenderPayload>(RBSSR_PAYLOAD_SCRIPT_ID));
|
|
1288
1279
|
const routerSnapshot = getScriptJson<ClientRouterSnapshot>(RBSSR_ROUTER_SCRIPT_ID);
|
|
1289
|
-
const modules = moduleRegistry.get(routeId);
|
|
1280
|
+
const modules = clientRuntimeSingleton.moduleRegistry.get(routeId);
|
|
1290
1281
|
if (!modules) {
|
|
1291
1282
|
throw new Error(`Missing module registry for initial route "${routeId}"`);
|
|
1292
1283
|
}
|
|
@@ -1306,13 +1297,13 @@ export function hydrateInitialRoute(routeId: string): void {
|
|
|
1306
1297
|
}
|
|
1307
1298
|
|
|
1308
1299
|
const root = hydrateRoot(container, appTree);
|
|
1309
|
-
runtimeState = {
|
|
1300
|
+
clientRuntimeSingleton.runtimeState = {
|
|
1310
1301
|
root,
|
|
1311
1302
|
currentPayload: payload,
|
|
1312
1303
|
currentRouteId: routeId,
|
|
1313
1304
|
currentModules: modules,
|
|
1314
1305
|
routerSnapshot,
|
|
1315
|
-
moduleRegistry,
|
|
1306
|
+
moduleRegistry: clientRuntimeSingleton.moduleRegistry,
|
|
1316
1307
|
prefetchCache: new Map(),
|
|
1317
1308
|
navigationToken: 0,
|
|
1318
1309
|
transitionAbortController: null,
|