@rangojs/router 0.0.0-experimental.002d056c
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/AGENTS.md +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Segments state returned by useSegments hook
|
|
9
|
+
*/
|
|
10
|
+
export interface SegmentsState {
|
|
11
|
+
/** URL path segments (e.g., /shop/products/123 → ["shop", "products", "123"]) */
|
|
12
|
+
path: readonly string[];
|
|
13
|
+
/** Matched segment IDs in order (layouts and routes only, e.g., ["L0", "L0L1", "L0L1R0"]) */
|
|
14
|
+
segmentIds: readonly string[];
|
|
15
|
+
/** Current URL location */
|
|
16
|
+
location: URL;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse pathname into path segments
|
|
21
|
+
* /shop/products/123 → ["shop", "products", "123"]
|
|
22
|
+
*/
|
|
23
|
+
function parsePathname(pathname: string): string[] {
|
|
24
|
+
return pathname.split("/").filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build segments state from event controller
|
|
29
|
+
*/
|
|
30
|
+
function buildSegmentsState(
|
|
31
|
+
location: URL,
|
|
32
|
+
segmentOrder: string[],
|
|
33
|
+
): SegmentsState {
|
|
34
|
+
return {
|
|
35
|
+
path: parsePathname(location.pathname),
|
|
36
|
+
segmentIds: segmentOrder,
|
|
37
|
+
location,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hook to access current route segments with optional selector for performance
|
|
43
|
+
*
|
|
44
|
+
* Provides information about the current URL path and matched route segments.
|
|
45
|
+
* Uses the event controller for reactive state management.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* // Get full segments state
|
|
50
|
+
* const { path, segmentIds, location } = useSegments();
|
|
51
|
+
*
|
|
52
|
+
* // Use selector for specific values (better performance)
|
|
53
|
+
* const path = useSegments(s => s.path);
|
|
54
|
+
* const isShopRoute = useSegments(s => s.path[0] === "shop");
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useSegments(): SegmentsState;
|
|
58
|
+
export function useSegments<T>(selector: (state: SegmentsState) => T): T;
|
|
59
|
+
export function useSegments<T>(
|
|
60
|
+
selector?: (state: SegmentsState) => T,
|
|
61
|
+
): T | SegmentsState {
|
|
62
|
+
const ctx = useContext(NavigationStoreContext);
|
|
63
|
+
|
|
64
|
+
// Build initial state from event controller when context exists.
|
|
65
|
+
// Inlined rather than calling recompute() because the segmentsCache ref
|
|
66
|
+
// is not yet initialized during the useState initializer.
|
|
67
|
+
const [state, setState] = useState<T | SegmentsState>(() => {
|
|
68
|
+
if (!ctx) {
|
|
69
|
+
const fallbackLocation = new URL("/", "http://localhost");
|
|
70
|
+
const fallbackState = buildSegmentsState(fallbackLocation, []);
|
|
71
|
+
return selector ? selector(fallbackState) : fallbackState;
|
|
72
|
+
}
|
|
73
|
+
const location = ctx.eventController.getLocation();
|
|
74
|
+
const handleState = ctx.eventController.getHandleState();
|
|
75
|
+
const segmentsState = buildSegmentsState(
|
|
76
|
+
location as URL,
|
|
77
|
+
handleState.segmentOrder,
|
|
78
|
+
);
|
|
79
|
+
return selector ? selector(segmentsState) : segmentsState;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const prevState = useRef(state);
|
|
83
|
+
const selectorRef = useRef(selector);
|
|
84
|
+
selectorRef.current = selector;
|
|
85
|
+
|
|
86
|
+
// Track selector identity to detect when the selector function changes.
|
|
87
|
+
// Only then do we eagerly recompute during render to avoid staleness.
|
|
88
|
+
// Without this guard, no-selector mode causes infinite re-renders because
|
|
89
|
+
// buildSegmentsState creates fresh arrays that fail Object.is checks.
|
|
90
|
+
const prevSelectorIdentity = useRef(selector);
|
|
91
|
+
|
|
92
|
+
// Cache SegmentsState to stabilize nested references (path, segmentIds
|
|
93
|
+
// arrays) so selectors returning composite values don't cause spurious
|
|
94
|
+
// render-time setState calls.
|
|
95
|
+
const segmentsCache = useRef<{
|
|
96
|
+
location: URL;
|
|
97
|
+
segmentOrder: string[];
|
|
98
|
+
state: SegmentsState;
|
|
99
|
+
} | null>(null);
|
|
100
|
+
|
|
101
|
+
// Recompute selected value from current store state and apply selector.
|
|
102
|
+
// Shared by the render-time eager check and the subscription callback.
|
|
103
|
+
function recompute(
|
|
104
|
+
sel: ((state: SegmentsState) => T) | undefined,
|
|
105
|
+
): T | SegmentsState {
|
|
106
|
+
const location = ctx!.eventController.getLocation();
|
|
107
|
+
const handleState = ctx!.eventController.getHandleState();
|
|
108
|
+
|
|
109
|
+
// Reuse cached state when inputs haven't changed by reference,
|
|
110
|
+
// keeping array/object references stable for composite selectors.
|
|
111
|
+
const cache = segmentsCache.current;
|
|
112
|
+
let segmentsState: SegmentsState;
|
|
113
|
+
if (
|
|
114
|
+
cache &&
|
|
115
|
+
cache.location === location &&
|
|
116
|
+
cache.segmentOrder === handleState.segmentOrder
|
|
117
|
+
) {
|
|
118
|
+
segmentsState = cache.state;
|
|
119
|
+
} else {
|
|
120
|
+
segmentsState = buildSegmentsState(
|
|
121
|
+
location as URL,
|
|
122
|
+
handleState.segmentOrder,
|
|
123
|
+
);
|
|
124
|
+
segmentsCache.current = {
|
|
125
|
+
location: location as URL,
|
|
126
|
+
segmentOrder: handleState.segmentOrder,
|
|
127
|
+
state: segmentsState,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return sel ? sel(segmentsState) : segmentsState;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ctx && selector !== prevSelectorIdentity.current) {
|
|
134
|
+
prevSelectorIdentity.current = selector;
|
|
135
|
+
const nextSelected = recompute(selector);
|
|
136
|
+
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
137
|
+
prevState.current = nextSelected;
|
|
138
|
+
setState(nextSelected);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Subscribe to store changes. The eager block above handles selector
|
|
143
|
+
// changes and SSR drift, so no initial updateState() call is needed.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!ctx) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const updateState = () => {
|
|
150
|
+
const nextSelected = recompute(selectorRef.current);
|
|
151
|
+
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
152
|
+
prevState.current = nextSelected;
|
|
153
|
+
setState(nextSelected);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const unsubscribeNav = ctx.eventController.subscribe(updateState);
|
|
158
|
+
const unsubscribeHandles =
|
|
159
|
+
ctx.eventController.subscribeToHandles(updateState);
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
unsubscribeNav();
|
|
163
|
+
unsubscribeHandles();
|
|
164
|
+
};
|
|
165
|
+
// Stable subscription: selector changes are handled via selectorRef,
|
|
166
|
+
// state comparison uses prevState ref. No re-subscribe needed.
|
|
167
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
return state as T | SegmentsState;
|
|
171
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
2
|
+
|
|
3
|
+
type HeaderResult = { url: string } | "blocked" | null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
|
|
7
|
+
* Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
|
|
8
|
+
*/
|
|
9
|
+
export function extractRscHeaderUrl(
|
|
10
|
+
response: Response,
|
|
11
|
+
header: string,
|
|
12
|
+
): HeaderResult {
|
|
13
|
+
const raw = response.headers.get(header);
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
const url = validateRedirectOrigin(raw, window.location.origin);
|
|
16
|
+
return url ? { url } : "blocked";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Empty 200 response that won't choke Flight parsing.
|
|
21
|
+
* Used when a header URL is blocked by origin validation.
|
|
22
|
+
*/
|
|
23
|
+
export function emptyResponse(): Response {
|
|
24
|
+
return new Response(null, { status: 200 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
|
+
* Returns a new Response with one branch; the other is consumed to detect
|
|
30
|
+
* end-of-stream, calling onComplete when done.
|
|
31
|
+
*
|
|
32
|
+
* If the response has no body, onComplete fires synchronously.
|
|
33
|
+
* If signal is provided, an abort cancels the tracking reader.
|
|
34
|
+
*/
|
|
35
|
+
export function teeWithCompletion(
|
|
36
|
+
response: Response,
|
|
37
|
+
onComplete: () => void,
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): Response {
|
|
40
|
+
if (!response.body) {
|
|
41
|
+
onComplete();
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [rscStream, trackingStream] = response.body.tee();
|
|
46
|
+
|
|
47
|
+
(async () => {
|
|
48
|
+
const reader = trackingStream.getReader();
|
|
49
|
+
const onAbort = signal ? reader.cancel.bind(reader) : undefined;
|
|
50
|
+
if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
|
|
51
|
+
try {
|
|
52
|
+
while (true) {
|
|
53
|
+
const { done } = await reader.read();
|
|
54
|
+
if (done) break;
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
if (onAbort) signal!.removeEventListener("abort", onAbort);
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
onComplete();
|
|
60
|
+
}
|
|
61
|
+
})().catch((error) => {
|
|
62
|
+
if (!signal?.aborted) {
|
|
63
|
+
console.error("[Browser] Error reading tracking stream:", error);
|
|
64
|
+
}
|
|
65
|
+
onComplete();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return new Response(rscStream, {
|
|
69
|
+
headers: response.headers,
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
renderSegments as baseRenderSegments,
|
|
4
|
+
type RenderSegmentsOptions,
|
|
5
|
+
} from "../segment-system.js";
|
|
6
|
+
import {
|
|
7
|
+
createNavigationStore,
|
|
8
|
+
generateHistoryKey,
|
|
9
|
+
} from "./navigation-store.js";
|
|
10
|
+
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { createNavigationClient } from "./navigation-client.js";
|
|
12
|
+
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
|
+
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
14
|
+
import { NavigationProvider } from "./react/index.js";
|
|
15
|
+
import type {
|
|
16
|
+
RscPayload,
|
|
17
|
+
RscBrowserDependencies,
|
|
18
|
+
ResolvedSegment,
|
|
19
|
+
NavigationStore,
|
|
20
|
+
NavigationBridge,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
import type { EventController } from "./event-controller.js";
|
|
23
|
+
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
|
+
import { initRangoState } from "./rango-state.js";
|
|
25
|
+
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
26
|
+
import {
|
|
27
|
+
isInterceptSegment,
|
|
28
|
+
splitInterceptSegments,
|
|
29
|
+
} from "./intercept-utils.js";
|
|
30
|
+
|
|
31
|
+
// Vite HMR types are provided by vite/client
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for initializing the browser app
|
|
35
|
+
*/
|
|
36
|
+
export interface InitBrowserAppOptions {
|
|
37
|
+
/**
|
|
38
|
+
* RSC stream containing the initial payload (from rsc-html-stream/client)
|
|
39
|
+
*/
|
|
40
|
+
rscStream: ReadableStream<Uint8Array>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* RSC browser dependencies from @vitejs/plugin-rsc/browser
|
|
44
|
+
*/
|
|
45
|
+
deps: RscBrowserDependencies;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional store configuration
|
|
49
|
+
*/
|
|
50
|
+
storeOptions?: {
|
|
51
|
+
/**
|
|
52
|
+
* Maximum number of history entries to cache
|
|
53
|
+
* @default 10
|
|
54
|
+
*/
|
|
55
|
+
cacheSize?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enable global link interception for SPA navigation.
|
|
60
|
+
* When enabled, clicks on same-origin anchor elements are intercepted
|
|
61
|
+
* and handled via client-side navigation instead of full page loads.
|
|
62
|
+
*
|
|
63
|
+
* Links rendered with the Link component handle their own navigation
|
|
64
|
+
* regardless of this setting.
|
|
65
|
+
*
|
|
66
|
+
* Set to false to disable global interception and rely solely on
|
|
67
|
+
* Link components for SPA navigation.
|
|
68
|
+
*
|
|
69
|
+
* @default true
|
|
70
|
+
*/
|
|
71
|
+
linkInterception?: boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Theme configuration from router.
|
|
75
|
+
* When provided, enables theme support via useTheme hook.
|
|
76
|
+
* Pass router.themeConfig here to enable theme features.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* import { router } from "./router.js";
|
|
81
|
+
*
|
|
82
|
+
* await initBrowserApp({
|
|
83
|
+
* rscStream,
|
|
84
|
+
* deps: rscBrowser,
|
|
85
|
+
* themeConfig: router.themeConfig,
|
|
86
|
+
* initialTheme: document.documentElement.className.includes("dark") ? "dark" : "light",
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
themeConfig?: ResolvedThemeConfig | null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Initial theme from server (typically read from cookie).
|
|
94
|
+
* Only used when themeConfig is provided.
|
|
95
|
+
*/
|
|
96
|
+
initialTheme?: Theme;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Result from initializing the browser app
|
|
101
|
+
*/
|
|
102
|
+
export interface BrowserAppContext {
|
|
103
|
+
store: NavigationStore;
|
|
104
|
+
eventController: EventController;
|
|
105
|
+
bridge: NavigationBridge;
|
|
106
|
+
initialPayload: RscPayload;
|
|
107
|
+
initialTree: React.ReactNode | Promise<React.ReactNode>;
|
|
108
|
+
/** Theme configuration (null if theme not enabled) */
|
|
109
|
+
themeConfig?: ResolvedThemeConfig | null;
|
|
110
|
+
/** Initial theme from server */
|
|
111
|
+
initialTheme?: Theme;
|
|
112
|
+
/** Whether connection warmup is enabled */
|
|
113
|
+
warmupEnabled?: boolean;
|
|
114
|
+
/** App version for prefetch version mismatch detection */
|
|
115
|
+
version?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Module-level state for the initialized app
|
|
119
|
+
let browserAppContext: BrowserAppContext | null = null;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Initialize the browser app. Must be called before rendering RSCRouter.
|
|
123
|
+
*
|
|
124
|
+
* This function:
|
|
125
|
+
* - Loads the initial RSC payload from the stream
|
|
126
|
+
* - Creates the navigation store and event controller
|
|
127
|
+
* - Sets up action and navigation bridges
|
|
128
|
+
* - Configures HMR support
|
|
129
|
+
*/
|
|
130
|
+
export async function initBrowserApp(
|
|
131
|
+
options: InitBrowserAppOptions,
|
|
132
|
+
): Promise<BrowserAppContext> {
|
|
133
|
+
const {
|
|
134
|
+
rscStream,
|
|
135
|
+
deps,
|
|
136
|
+
storeOptions,
|
|
137
|
+
linkInterception = true,
|
|
138
|
+
themeConfig,
|
|
139
|
+
initialTheme,
|
|
140
|
+
} = options;
|
|
141
|
+
|
|
142
|
+
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
143
|
+
const initialPayload =
|
|
144
|
+
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
145
|
+
|
|
146
|
+
// Extract themeConfig and initialTheme from payload if not explicitly provided
|
|
147
|
+
// This allows virtual entries to work without importing the router
|
|
148
|
+
const effectiveThemeConfig =
|
|
149
|
+
themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
|
|
150
|
+
const effectiveInitialTheme =
|
|
151
|
+
initialTheme ?? initialPayload.metadata?.initialTheme;
|
|
152
|
+
|
|
153
|
+
// Get initial segments and compute history key from current URL
|
|
154
|
+
const initialSegments = (initialPayload.metadata?.segments ??
|
|
155
|
+
[]) as ResolvedSegment[];
|
|
156
|
+
const initialHistoryKey = generateHistoryKey(window.location.href);
|
|
157
|
+
|
|
158
|
+
// Create navigation store with history-based caching
|
|
159
|
+
const store = createNavigationStore({
|
|
160
|
+
initialLocation: window.location,
|
|
161
|
+
initialSegmentIds: initialSegments.map((s) => s.id),
|
|
162
|
+
initialHistoryKey,
|
|
163
|
+
initialSegments,
|
|
164
|
+
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Create event controller for reactive state management
|
|
168
|
+
const eventController = createEventController({
|
|
169
|
+
initialLocation: new URL(window.location.href),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Initialize event controller with segment order (even without handles)
|
|
173
|
+
eventController.setHandleData({}, initialPayload.metadata?.matched);
|
|
174
|
+
|
|
175
|
+
// Initialize route params
|
|
176
|
+
eventController.setParams(initialPayload.metadata?.params ?? {});
|
|
177
|
+
|
|
178
|
+
// Initialize handle data from initial payload BEFORE hydration
|
|
179
|
+
// This ensures useHandle returns correct data during hydration to avoid mismatch
|
|
180
|
+
// The handles property is an async generator that yields on each push
|
|
181
|
+
if (initialPayload.metadata?.handles) {
|
|
182
|
+
const handlesGenerator = initialPayload.metadata.handles;
|
|
183
|
+
let lastHandleData: Record<string, Record<string, unknown[]>> = {};
|
|
184
|
+
for await (const handleData of handlesGenerator) {
|
|
185
|
+
lastHandleData = handleData;
|
|
186
|
+
}
|
|
187
|
+
// Initialize event controller with initial handle state before hydration.
|
|
188
|
+
eventController.setHandleData(
|
|
189
|
+
lastHandleData,
|
|
190
|
+
initialPayload.metadata?.matched,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Update the initial cache entry with the processed handleData
|
|
194
|
+
// The cache entry was created by createNavigationStore but without handleData
|
|
195
|
+
store.updateCacheHandleData(initialHistoryKey, lastHandleData);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Create composable utilities
|
|
199
|
+
const client = createNavigationClient(deps);
|
|
200
|
+
|
|
201
|
+
// Extract rootLayout and version from metadata for browser-side re-renders
|
|
202
|
+
const rootLayout = initialPayload.metadata?.rootLayout;
|
|
203
|
+
const version = initialPayload.metadata?.version;
|
|
204
|
+
|
|
205
|
+
// Initialize the localStorage state key for cache invalidation.
|
|
206
|
+
// Uses the build version so a new deploy automatically busts all cached prefetches.
|
|
207
|
+
initRangoState(version ?? "0");
|
|
208
|
+
|
|
209
|
+
// Initialize the in-memory prefetch cache TTL from server config.
|
|
210
|
+
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
211
|
+
const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
|
|
212
|
+
if (prefetchCacheTTL !== undefined) {
|
|
213
|
+
initPrefetchCache(prefetchCacheTTL);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create a bound renderSegments that includes rootLayout
|
|
217
|
+
const renderSegments = (
|
|
218
|
+
segments: ResolvedSegment[],
|
|
219
|
+
options?: RenderSegmentsOptions,
|
|
220
|
+
) => baseRenderSegments(segments, { ...options, rootLayout });
|
|
221
|
+
|
|
222
|
+
// Lazy reference for navigation bridge — the action bridge is created first
|
|
223
|
+
// but may need to trigger SPA navigation for action redirects.
|
|
224
|
+
let navigateFn: ((url: string, options?: any) => Promise<void>) | null = null;
|
|
225
|
+
|
|
226
|
+
// Setup server action bridge
|
|
227
|
+
const actionBridge = createServerActionBridge({
|
|
228
|
+
store,
|
|
229
|
+
eventController,
|
|
230
|
+
client,
|
|
231
|
+
deps,
|
|
232
|
+
onUpdate: (update) => store.emitUpdate(update),
|
|
233
|
+
renderSegments,
|
|
234
|
+
version,
|
|
235
|
+
onNavigate: (url, options) => {
|
|
236
|
+
if (!navigateFn) {
|
|
237
|
+
window.location.href = url;
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
}
|
|
240
|
+
return navigateFn(url, options);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
actionBridge.register();
|
|
244
|
+
|
|
245
|
+
// Setup navigation bridge
|
|
246
|
+
const navigationBridge = createNavigationBridge({
|
|
247
|
+
store,
|
|
248
|
+
eventController,
|
|
249
|
+
client,
|
|
250
|
+
onUpdate: (update) => store.emitUpdate(update),
|
|
251
|
+
renderSegments,
|
|
252
|
+
version,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
256
|
+
navigateFn = (url, options) => navigationBridge.navigate(url, options);
|
|
257
|
+
|
|
258
|
+
// Optionally enable global link interception
|
|
259
|
+
if (linkInterception) {
|
|
260
|
+
navigationBridge.registerLinkInterception();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Build initial tree with rootLayout
|
|
264
|
+
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
265
|
+
|
|
266
|
+
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
267
|
+
// fire many rsc:update events in quick succession. Without debouncing,
|
|
268
|
+
// each event triggers a fetchPartial() which on slow routes can pile up
|
|
269
|
+
// and overwhelm the worker (cross-request promise issues, 500s).
|
|
270
|
+
if (import.meta.hot) {
|
|
271
|
+
let hmrTimer: ReturnType<typeof setTimeout> | null = null;
|
|
272
|
+
let hmrAbort: AbortController | null = null;
|
|
273
|
+
|
|
274
|
+
import.meta.hot.on("rsc:update", () => {
|
|
275
|
+
// Cancel any pending debounce timer
|
|
276
|
+
if (hmrTimer !== null) {
|
|
277
|
+
clearTimeout(hmrTimer);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Abort any in-flight HMR fetch so it doesn't race with the next one
|
|
281
|
+
if (hmrAbort) {
|
|
282
|
+
hmrAbort.abort();
|
|
283
|
+
hmrAbort = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Debounce: wait 200ms of quiet before fetching
|
|
287
|
+
hmrTimer = setTimeout(async () => {
|
|
288
|
+
hmrTimer = null;
|
|
289
|
+
console.log("[RSCRouter] HMR: Server update, refetching RSC");
|
|
290
|
+
|
|
291
|
+
const abort = new AbortController();
|
|
292
|
+
hmrAbort = abort;
|
|
293
|
+
|
|
294
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
295
|
+
replace: true,
|
|
296
|
+
});
|
|
297
|
+
const streamingToken = handle.startStreaming();
|
|
298
|
+
|
|
299
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
303
|
+
targetUrl: window.location.href,
|
|
304
|
+
segmentIds: [],
|
|
305
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
306
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
307
|
+
hmr: true,
|
|
308
|
+
signal: abort.signal,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (abort.signal.aborted) return;
|
|
312
|
+
|
|
313
|
+
if (payload.metadata?.isPartial) {
|
|
314
|
+
const segments = payload.metadata.segments || [];
|
|
315
|
+
const matched = payload.metadata.matched || [];
|
|
316
|
+
|
|
317
|
+
// Derive intercept state from the returned payload, not the
|
|
318
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
319
|
+
// behavior, the response won't contain intercept segments.
|
|
320
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
321
|
+
|
|
322
|
+
// Sync store intercept state with what the server returned
|
|
323
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
324
|
+
store.setInterceptSourceUrl(null);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
store.setSegmentIds(matched);
|
|
328
|
+
store.setCurrentUrl(window.location.href);
|
|
329
|
+
|
|
330
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
331
|
+
intercept: responseIsIntercept,
|
|
332
|
+
});
|
|
333
|
+
store.setHistoryKey(historyKey);
|
|
334
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
335
|
+
store.cacheSegmentsForHistory(
|
|
336
|
+
historyKey,
|
|
337
|
+
segments,
|
|
338
|
+
currentHandleData,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
342
|
+
store.emitUpdate({
|
|
343
|
+
root: renderSegments(main, {
|
|
344
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
345
|
+
}),
|
|
346
|
+
metadata: payload.metadata,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await streamComplete;
|
|
351
|
+
handle.complete(new URL(window.location.href));
|
|
352
|
+
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (abort.signal.aborted) return;
|
|
355
|
+
console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
|
|
356
|
+
window.location.reload();
|
|
357
|
+
return;
|
|
358
|
+
} finally {
|
|
359
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
360
|
+
streamingToken.end();
|
|
361
|
+
handle[Symbol.dispose]();
|
|
362
|
+
}
|
|
363
|
+
}, 200);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Store context for RSCRouter component
|
|
368
|
+
const context: BrowserAppContext = {
|
|
369
|
+
store,
|
|
370
|
+
eventController,
|
|
371
|
+
bridge: navigationBridge,
|
|
372
|
+
initialPayload,
|
|
373
|
+
initialTree,
|
|
374
|
+
themeConfig: effectiveThemeConfig,
|
|
375
|
+
initialTheme: effectiveInitialTheme,
|
|
376
|
+
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
377
|
+
version,
|
|
378
|
+
};
|
|
379
|
+
browserAppContext = context;
|
|
380
|
+
|
|
381
|
+
return context;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get the browser app context. Throws if initBrowserApp hasn't been called.
|
|
386
|
+
*/
|
|
387
|
+
export function getBrowserAppContext(): BrowserAppContext {
|
|
388
|
+
if (!browserAppContext) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
"RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return browserAppContext;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Reset the browser app context (for testing)
|
|
398
|
+
*/
|
|
399
|
+
export function resetBrowserAppContext(): void {
|
|
400
|
+
browserAppContext = null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Props for the RSCRouter component
|
|
405
|
+
*/
|
|
406
|
+
export interface RSCRouterProps {}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* RSCRouter component - renders the RSC router with all internal wiring.
|
|
410
|
+
*
|
|
411
|
+
* Must be called after initBrowserApp() has completed.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```tsx
|
|
415
|
+
* import { initBrowserApp, RSCRouter } from "rsc-router/browser";
|
|
416
|
+
* import { rscStream } from "rsc-html-stream/client";
|
|
417
|
+
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
418
|
+
*
|
|
419
|
+
* async function main() {
|
|
420
|
+
* await initBrowserApp({ rscStream, deps: rscBrowser });
|
|
421
|
+
*
|
|
422
|
+
* hydrateRoot(
|
|
423
|
+
* document,
|
|
424
|
+
* <React.StrictMode>
|
|
425
|
+
* <RSCRouter />
|
|
426
|
+
* </React.StrictMode>
|
|
427
|
+
* );
|
|
428
|
+
* }
|
|
429
|
+
* main();
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
433
|
+
const {
|
|
434
|
+
store,
|
|
435
|
+
eventController,
|
|
436
|
+
bridge,
|
|
437
|
+
initialPayload,
|
|
438
|
+
initialTree,
|
|
439
|
+
themeConfig,
|
|
440
|
+
initialTheme,
|
|
441
|
+
warmupEnabled,
|
|
442
|
+
version,
|
|
443
|
+
} = getBrowserAppContext();
|
|
444
|
+
|
|
445
|
+
// Signal that the React tree has hydrated. useEffect only fires after
|
|
446
|
+
// hydration completes, so this attribute is a stable readiness marker
|
|
447
|
+
// that does not depend on React internals like __reactFiber.
|
|
448
|
+
React.useEffect(() => {
|
|
449
|
+
document.documentElement.dataset.hydrated = "";
|
|
450
|
+
}, []);
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<NavigationProvider
|
|
454
|
+
store={store}
|
|
455
|
+
eventController={eventController}
|
|
456
|
+
initialPayload={{ root: initialTree, metadata: initialPayload.metadata! }}
|
|
457
|
+
bridge={bridge}
|
|
458
|
+
themeConfig={themeConfig}
|
|
459
|
+
initialTheme={initialTheme}
|
|
460
|
+
warmupEnabled={warmupEnabled}
|
|
461
|
+
version={version}
|
|
462
|
+
/>
|
|
463
|
+
);
|
|
464
|
+
}
|