@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
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 +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -4
- 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 +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- 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 +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- 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 +469 -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 +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- 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 +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- 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 +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- 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 +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- 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 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- 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 +1241 -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 +289 -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 +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -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 +235 -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 +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- 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 -726
- package/src/use-loader.tsx +85 -77
- 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 +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -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/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- 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 +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -11,8 +11,7 @@ import { createEventController } from "./event-controller.js";
|
|
|
11
11
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
12
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
13
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
14
|
-
import { NavigationProvider
|
|
15
|
-
import { initThemeConfigSync } from "../theme/theme-context.js";
|
|
14
|
+
import { NavigationProvider } from "./react/index.js";
|
|
16
15
|
import type {
|
|
17
16
|
RscPayload,
|
|
18
17
|
RscBrowserDependencies,
|
|
@@ -22,6 +21,12 @@ import type {
|
|
|
22
21
|
} from "./types.js";
|
|
23
22
|
import type { EventController } from "./event-controller.js";
|
|
24
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";
|
|
25
30
|
|
|
26
31
|
// Vite HMR types are provided by vite/client
|
|
27
32
|
|
|
@@ -104,6 +109,10 @@ export interface BrowserAppContext {
|
|
|
104
109
|
themeConfig?: ResolvedThemeConfig | null;
|
|
105
110
|
/** Initial theme from server */
|
|
106
111
|
initialTheme?: Theme;
|
|
112
|
+
/** Whether connection warmup is enabled */
|
|
113
|
+
warmupEnabled?: boolean;
|
|
114
|
+
/** App version for prefetch version mismatch detection */
|
|
115
|
+
version?: string;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
// Module-level state for the initialized app
|
|
@@ -119,9 +128,16 @@ let browserAppContext: BrowserAppContext | null = null;
|
|
|
119
128
|
* - Configures HMR support
|
|
120
129
|
*/
|
|
121
130
|
export async function initBrowserApp(
|
|
122
|
-
options: InitBrowserAppOptions
|
|
131
|
+
options: InitBrowserAppOptions,
|
|
123
132
|
): Promise<BrowserAppContext> {
|
|
124
|
-
const {
|
|
133
|
+
const {
|
|
134
|
+
rscStream,
|
|
135
|
+
deps,
|
|
136
|
+
storeOptions,
|
|
137
|
+
linkInterception = true,
|
|
138
|
+
themeConfig,
|
|
139
|
+
initialTheme,
|
|
140
|
+
} = options;
|
|
125
141
|
|
|
126
142
|
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
127
143
|
const initialPayload =
|
|
@@ -129,8 +145,10 @@ export async function initBrowserApp(
|
|
|
129
145
|
|
|
130
146
|
// Extract themeConfig and initialTheme from payload if not explicitly provided
|
|
131
147
|
// This allows virtual entries to work without importing the router
|
|
132
|
-
const effectiveThemeConfig =
|
|
133
|
-
|
|
148
|
+
const effectiveThemeConfig =
|
|
149
|
+
themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
|
|
150
|
+
const effectiveInitialTheme =
|
|
151
|
+
initialTheme ?? initialPayload.metadata?.initialTheme;
|
|
134
152
|
|
|
135
153
|
// Get initial segments and compute history key from current URL
|
|
136
154
|
const initialSegments = (initialPayload.metadata?.segments ??
|
|
@@ -151,15 +169,12 @@ export async function initBrowserApp(
|
|
|
151
169
|
initialLocation: new URL(window.location.href),
|
|
152
170
|
});
|
|
153
171
|
|
|
154
|
-
// Initialize segments state BEFORE hydration to avoid mismatch
|
|
155
|
-
initSegmentsSync(initialPayload.metadata?.matched, initialPayload.metadata?.pathname);
|
|
156
|
-
|
|
157
|
-
// Initialize theme config for MetaTags (must match SSR state)
|
|
158
|
-
initThemeConfigSync(effectiveThemeConfig);
|
|
159
|
-
|
|
160
172
|
// Initialize event controller with segment order (even without handles)
|
|
161
173
|
eventController.setHandleData({}, initialPayload.metadata?.matched);
|
|
162
174
|
|
|
175
|
+
// Initialize route params
|
|
176
|
+
eventController.setParams(initialPayload.metadata?.params ?? {});
|
|
177
|
+
|
|
163
178
|
// Initialize handle data from initial payload BEFORE hydration
|
|
164
179
|
// This ensures useHandle returns correct data during hydration to avoid mismatch
|
|
165
180
|
// The handles property is an async generator that yields on each push
|
|
@@ -169,16 +184,17 @@ export async function initBrowserApp(
|
|
|
169
184
|
for await (const handleData of handlesGenerator) {
|
|
170
185
|
lastHandleData = handleData;
|
|
171
186
|
}
|
|
172
|
-
// Initialize
|
|
173
|
-
eventController.setHandleData(
|
|
174
|
-
|
|
187
|
+
// Initialize event controller with initial handle state before hydration.
|
|
188
|
+
eventController.setHandleData(
|
|
189
|
+
lastHandleData,
|
|
190
|
+
initialPayload.metadata?.matched,
|
|
191
|
+
);
|
|
175
192
|
|
|
176
193
|
// Update the initial cache entry with the processed handleData
|
|
177
194
|
// The cache entry was created by createNavigationStore but without handleData
|
|
178
195
|
store.updateCacheHandleData(initialHistoryKey, lastHandleData);
|
|
179
196
|
}
|
|
180
197
|
|
|
181
|
-
|
|
182
198
|
// Create composable utilities
|
|
183
199
|
const client = createNavigationClient(deps);
|
|
184
200
|
|
|
@@ -186,12 +202,27 @@ export async function initBrowserApp(
|
|
|
186
202
|
const rootLayout = initialPayload.metadata?.rootLayout;
|
|
187
203
|
const version = initialPayload.metadata?.version;
|
|
188
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
|
+
|
|
189
216
|
// Create a bound renderSegments that includes rootLayout
|
|
190
217
|
const renderSegments = (
|
|
191
218
|
segments: ResolvedSegment[],
|
|
192
|
-
options?: RenderSegmentsOptions
|
|
219
|
+
options?: RenderSegmentsOptions,
|
|
193
220
|
) => baseRenderSegments(segments, { ...options, rootLayout });
|
|
194
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
|
+
|
|
195
226
|
// Setup server action bridge
|
|
196
227
|
const actionBridge = createServerActionBridge({
|
|
197
228
|
store,
|
|
@@ -201,6 +232,13 @@ export async function initBrowserApp(
|
|
|
201
232
|
onUpdate: (update) => store.emitUpdate(update),
|
|
202
233
|
renderSegments,
|
|
203
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
|
+
},
|
|
204
242
|
});
|
|
205
243
|
actionBridge.register();
|
|
206
244
|
|
|
@@ -214,6 +252,9 @@ export async function initBrowserApp(
|
|
|
214
252
|
version,
|
|
215
253
|
});
|
|
216
254
|
|
|
255
|
+
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
256
|
+
navigateFn = (url, options) => navigationBridge.navigate(url, options);
|
|
257
|
+
|
|
217
258
|
// Optionally enable global link interception
|
|
218
259
|
if (linkInterception) {
|
|
219
260
|
navigationBridge.registerLinkInterception();
|
|
@@ -232,37 +273,61 @@ export async function initBrowserApp(
|
|
|
232
273
|
});
|
|
233
274
|
const streamingToken = handle.startStreaming();
|
|
234
275
|
|
|
276
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
277
|
+
|
|
235
278
|
try {
|
|
236
279
|
const { payload, streamComplete } = await client.fetchPartial({
|
|
237
280
|
targetUrl: window.location.href,
|
|
238
281
|
segmentIds: [],
|
|
239
282
|
previousUrl: store.getSegmentState().currentUrl,
|
|
283
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
284
|
+
hmr: true,
|
|
240
285
|
});
|
|
241
286
|
|
|
242
287
|
if (payload.metadata?.isPartial) {
|
|
243
288
|
const segments = payload.metadata.segments || [];
|
|
244
289
|
const matched = payload.metadata.matched || [];
|
|
245
290
|
|
|
291
|
+
// Derive intercept state from the returned payload, not the
|
|
292
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
293
|
+
// behavior, the response won't contain intercept segments.
|
|
294
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
295
|
+
|
|
296
|
+
// Sync store intercept state with what the server returned
|
|
297
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
298
|
+
store.setInterceptSourceUrl(null);
|
|
299
|
+
}
|
|
300
|
+
|
|
246
301
|
store.setSegmentIds(matched);
|
|
247
302
|
store.setCurrentUrl(window.location.href);
|
|
248
303
|
|
|
249
|
-
const historyKey = generateHistoryKey(window.location.href
|
|
304
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
305
|
+
intercept: responseIsIntercept,
|
|
306
|
+
});
|
|
250
307
|
store.setHistoryKey(historyKey);
|
|
251
308
|
const currentHandleData = eventController.getHandleState().data;
|
|
252
|
-
store.cacheSegmentsForHistory(
|
|
309
|
+
store.cacheSegmentsForHistory(
|
|
310
|
+
historyKey,
|
|
311
|
+
segments,
|
|
312
|
+
currentHandleData,
|
|
313
|
+
);
|
|
253
314
|
|
|
315
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
254
316
|
store.emitUpdate({
|
|
255
|
-
root: renderSegments(
|
|
317
|
+
root: renderSegments(main, {
|
|
318
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
319
|
+
}),
|
|
256
320
|
metadata: payload.metadata,
|
|
257
321
|
});
|
|
258
322
|
}
|
|
259
323
|
|
|
260
324
|
await streamComplete;
|
|
325
|
+
handle.complete(new URL(window.location.href));
|
|
326
|
+
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
261
327
|
} finally {
|
|
262
328
|
streamingToken.end();
|
|
329
|
+
handle[Symbol.dispose]();
|
|
263
330
|
}
|
|
264
|
-
handle.complete(new URL(window.location.href));
|
|
265
|
-
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
266
331
|
});
|
|
267
332
|
}
|
|
268
333
|
|
|
@@ -275,6 +340,8 @@ export async function initBrowserApp(
|
|
|
275
340
|
initialTree,
|
|
276
341
|
themeConfig: effectiveThemeConfig,
|
|
277
342
|
initialTheme: effectiveInitialTheme,
|
|
343
|
+
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
344
|
+
version,
|
|
278
345
|
};
|
|
279
346
|
browserAppContext = context;
|
|
280
347
|
|
|
@@ -287,7 +354,7 @@ export async function initBrowserApp(
|
|
|
287
354
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
288
355
|
if (!browserAppContext) {
|
|
289
356
|
throw new Error(
|
|
290
|
-
"RSCRouter: initBrowserApp() must be called before rendering RSCRouter"
|
|
357
|
+
"RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
|
|
291
358
|
);
|
|
292
359
|
}
|
|
293
360
|
return browserAppContext;
|
|
@@ -330,17 +397,35 @@ export interface RSCRouterProps {}
|
|
|
330
397
|
* ```
|
|
331
398
|
*/
|
|
332
399
|
export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
333
|
-
const {
|
|
334
|
-
|
|
400
|
+
const {
|
|
401
|
+
store,
|
|
402
|
+
eventController,
|
|
403
|
+
bridge,
|
|
404
|
+
initialPayload,
|
|
405
|
+
initialTree,
|
|
406
|
+
themeConfig,
|
|
407
|
+
initialTheme,
|
|
408
|
+
warmupEnabled,
|
|
409
|
+
version,
|
|
410
|
+
} = getBrowserAppContext();
|
|
411
|
+
|
|
412
|
+
// Signal that the React tree has hydrated. useEffect only fires after
|
|
413
|
+
// hydration completes, so this attribute is a stable readiness marker
|
|
414
|
+
// that does not depend on React internals like __reactFiber.
|
|
415
|
+
React.useEffect(() => {
|
|
416
|
+
document.documentElement.dataset.hydrated = "";
|
|
417
|
+
}, []);
|
|
335
418
|
|
|
336
419
|
return (
|
|
337
420
|
<NavigationProvider
|
|
338
421
|
store={store}
|
|
339
422
|
eventController={eventController}
|
|
340
|
-
initialPayload={{
|
|
423
|
+
initialPayload={{ root: initialTree, metadata: initialPayload.metadata! }}
|
|
341
424
|
bridge={bridge}
|
|
342
425
|
themeConfig={themeConfig}
|
|
343
426
|
initialTheme={initialTheme}
|
|
427
|
+
warmupEnabled={warmupEnabled}
|
|
428
|
+
version={version}
|
|
344
429
|
/>
|
|
345
430
|
);
|
|
346
431
|
}
|
|
@@ -8,8 +8,18 @@
|
|
|
8
8
|
* - Supports hash link scrolling
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { debugLog } from "./logging.js";
|
|
12
|
+
|
|
11
13
|
const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of scroll position entries to retain.
|
|
17
|
+
* When exceeded, the oldest entries (by insertion order) are evicted.
|
|
18
|
+
* 200 entries is well within sessionStorage limits while covering
|
|
19
|
+
* realistic back/forward navigation depth.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_SCROLL_ENTRIES = 200;
|
|
22
|
+
|
|
13
23
|
/**
|
|
14
24
|
* Interval for polling scroll restoration during streaming (ms).
|
|
15
25
|
* If content is still loading and we can't scroll to saved position,
|
|
@@ -29,6 +39,13 @@ const SCROLL_POLL_TIMEOUT_MS = 5000;
|
|
|
29
39
|
*/
|
|
30
40
|
let savedScrollPositions: Record<string, number> = {};
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Tracks insertion order of scroll position keys for LRU eviction.
|
|
44
|
+
* Most recent entries are at the end of the array.
|
|
45
|
+
* When a key is updated, it is moved to the end.
|
|
46
|
+
*/
|
|
47
|
+
let scrollKeyOrder: string[] = [];
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* Whether scroll restoration has been initialized
|
|
34
51
|
*/
|
|
@@ -37,9 +54,12 @@ let initialized = false;
|
|
|
37
54
|
/**
|
|
38
55
|
* Custom getKey function for determining scroll restoration key
|
|
39
56
|
*/
|
|
40
|
-
type GetScrollKeyFunction = (
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
type GetScrollKeyFunction = (location: {
|
|
58
|
+
pathname: string;
|
|
59
|
+
search: string;
|
|
60
|
+
hash: string;
|
|
61
|
+
key: string;
|
|
62
|
+
}) => string;
|
|
43
63
|
|
|
44
64
|
let customGetKey: GetScrollKeyFunction | null = null;
|
|
45
65
|
|
|
@@ -99,9 +119,13 @@ export function initScrollRestoration(options?: {
|
|
|
99
119
|
const stored = sessionStorage.getItem(SCROLL_STORAGE_KEY);
|
|
100
120
|
if (stored) {
|
|
101
121
|
savedScrollPositions = JSON.parse(stored);
|
|
122
|
+
// Rebuild key order from loaded positions.
|
|
123
|
+
// Exact original order is lost across page loads, but this is
|
|
124
|
+
// acceptable -- the important invariant is bounded size.
|
|
125
|
+
scrollKeyOrder = Object.keys(savedScrollPositions);
|
|
102
126
|
}
|
|
103
127
|
} catch (e) {
|
|
104
|
-
// Ignore parse errors
|
|
128
|
+
// Ignore parse errors, start with empty state
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
// Ensure current history entry has a key
|
|
@@ -117,31 +141,82 @@ export function initScrollRestoration(options?: {
|
|
|
117
141
|
|
|
118
142
|
window.addEventListener("pagehide", handlePageHide);
|
|
119
143
|
|
|
120
|
-
|
|
144
|
+
debugLog(
|
|
145
|
+
"[Scroll] Initialized, loaded positions:",
|
|
146
|
+
Object.keys(savedScrollPositions).length,
|
|
147
|
+
);
|
|
121
148
|
|
|
122
149
|
return () => {
|
|
150
|
+
cancelScrollRestorationPolling();
|
|
123
151
|
window.removeEventListener("pagehide", handlePageHide);
|
|
124
152
|
window.history.scrollRestoration = "auto";
|
|
125
153
|
initialized = false;
|
|
154
|
+
savedScrollPositions = {};
|
|
155
|
+
scrollKeyOrder = [];
|
|
126
156
|
};
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
/**
|
|
130
|
-
* Save the current scroll position for the current history entry
|
|
160
|
+
* Save the current scroll position for the current history entry.
|
|
161
|
+
* Maintains bounded size by evicting oldest entries when the limit is exceeded.
|
|
131
162
|
*/
|
|
132
163
|
export function saveCurrentScrollPosition(): void {
|
|
133
164
|
const key = getScrollKey();
|
|
165
|
+
|
|
166
|
+
// If this key already exists, remove it from its current position
|
|
167
|
+
// in the order array so it can be re-appended at the end (most recent).
|
|
168
|
+
const existingIndex = scrollKeyOrder.indexOf(key);
|
|
169
|
+
if (existingIndex !== -1) {
|
|
170
|
+
scrollKeyOrder.splice(existingIndex, 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
134
173
|
savedScrollPositions[key] = window.scrollY;
|
|
174
|
+
scrollKeyOrder.push(key);
|
|
175
|
+
|
|
176
|
+
// Evict oldest entries if we exceed the limit
|
|
177
|
+
while (scrollKeyOrder.length > MAX_SCROLL_ENTRIES) {
|
|
178
|
+
const oldestKey = scrollKeyOrder.shift()!;
|
|
179
|
+
delete savedScrollPositions[oldestKey];
|
|
180
|
+
}
|
|
135
181
|
}
|
|
136
182
|
|
|
137
183
|
/**
|
|
138
|
-
* Persist scroll positions to sessionStorage
|
|
184
|
+
* Persist scroll positions to sessionStorage.
|
|
185
|
+
* If the write fails due to quota exceeded, progressively evict the oldest
|
|
186
|
+
* entries and retry until it succeeds or the store is empty.
|
|
139
187
|
*/
|
|
140
188
|
function persistToSessionStorage(): void {
|
|
141
189
|
try {
|
|
142
|
-
sessionStorage.setItem(
|
|
190
|
+
sessionStorage.setItem(
|
|
191
|
+
SCROLL_STORAGE_KEY,
|
|
192
|
+
JSON.stringify(savedScrollPositions),
|
|
193
|
+
);
|
|
143
194
|
} catch (e) {
|
|
144
|
-
|
|
195
|
+
// Likely QuotaExceededError. Evict oldest entries and retry.
|
|
196
|
+
const evictCount = Math.max(1, Math.floor(scrollKeyOrder.length / 4));
|
|
197
|
+
for (let i = 0; i < evictCount && scrollKeyOrder.length > 0; i++) {
|
|
198
|
+
const oldestKey = scrollKeyOrder.shift()!;
|
|
199
|
+
delete savedScrollPositions[oldestKey];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
sessionStorage.setItem(
|
|
204
|
+
SCROLL_STORAGE_KEY,
|
|
205
|
+
JSON.stringify(savedScrollPositions),
|
|
206
|
+
);
|
|
207
|
+
} catch (retryErr) {
|
|
208
|
+
// Storage still full after eviction. Clear our key entirely so we
|
|
209
|
+
// don't block other sessionStorage consumers.
|
|
210
|
+
console.warn(
|
|
211
|
+
"[Scroll] Failed to persist to sessionStorage after eviction, clearing scroll data:",
|
|
212
|
+
retryErr,
|
|
213
|
+
);
|
|
214
|
+
try {
|
|
215
|
+
sessionStorage.removeItem(SCROLL_STORAGE_KEY);
|
|
216
|
+
} catch {
|
|
217
|
+
// Nothing more we can do
|
|
218
|
+
}
|
|
219
|
+
}
|
|
145
220
|
}
|
|
146
221
|
}
|
|
147
222
|
|
|
@@ -195,13 +270,13 @@ export function restoreScrollPosition(options?: {
|
|
|
195
270
|
|
|
196
271
|
if (canScrollToPosition) {
|
|
197
272
|
window.scrollTo(0, savedY);
|
|
198
|
-
|
|
273
|
+
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
199
274
|
return true;
|
|
200
275
|
}
|
|
201
276
|
|
|
202
277
|
// Scroll as far as we can for now
|
|
203
278
|
window.scrollTo(0, maxScrollY);
|
|
204
|
-
|
|
279
|
+
debugLog("[Scroll] Partial restore to:", maxScrollY, "target:", savedY);
|
|
205
280
|
|
|
206
281
|
// Poll while streaming until we can scroll to target position
|
|
207
282
|
if (options?.retryIfStreaming && options?.isStreaming?.()) {
|
|
@@ -210,23 +285,24 @@ export function restoreScrollPosition(options?: {
|
|
|
210
285
|
pendingPollInterval = setInterval(() => {
|
|
211
286
|
// Stop if we've exceeded the timeout
|
|
212
287
|
if (Date.now() - startTime > SCROLL_POLL_TIMEOUT_MS) {
|
|
213
|
-
|
|
288
|
+
debugLog("[Scroll] Polling timeout, giving up");
|
|
214
289
|
cancelScrollRestorationPolling();
|
|
215
290
|
return;
|
|
216
291
|
}
|
|
217
292
|
|
|
218
293
|
// Stop if streaming ended
|
|
219
294
|
if (!options.isStreaming?.()) {
|
|
220
|
-
|
|
295
|
+
debugLog("[Scroll] Streaming ended, stopping poll");
|
|
221
296
|
cancelScrollRestorationPolling();
|
|
222
297
|
return;
|
|
223
298
|
}
|
|
224
299
|
|
|
225
300
|
// Check if we can now scroll to the target position
|
|
226
|
-
const currentMaxScrollY =
|
|
301
|
+
const currentMaxScrollY =
|
|
302
|
+
document.documentElement.scrollHeight - window.innerHeight;
|
|
227
303
|
if (savedY <= currentMaxScrollY) {
|
|
228
304
|
window.scrollTo(0, savedY);
|
|
229
|
-
|
|
305
|
+
debugLog("[Scroll] Poll restored position:", savedY);
|
|
230
306
|
cancelScrollRestorationPolling();
|
|
231
307
|
}
|
|
232
308
|
}, SCROLL_POLL_INTERVAL_MS);
|
|
@@ -249,7 +325,7 @@ export function scrollToHash(): boolean {
|
|
|
249
325
|
const element = document.getElementById(id);
|
|
250
326
|
if (element) {
|
|
251
327
|
element.scrollIntoView();
|
|
252
|
-
|
|
328
|
+
debugLog("[Scroll] Scrolled to hash element:", id);
|
|
253
329
|
return true;
|
|
254
330
|
}
|
|
255
331
|
} catch (e) {
|