@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918
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 +211 -43
- package/dist/bin/rango.js +279 -102
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3313 -1160
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +62 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +63 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +124 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +223 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +66 -5
- package/skills/router-setup/SKILL.md +38 -3
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +81 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +66 -14
- package/src/browser/navigation-client.ts +172 -109
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +79 -93
- package/src/browser/prefetch/cache.ts +180 -62
- package/src/browser/prefetch/fetch.ts +252 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +72 -10
- package/src/browser/react/NavigationProvider.tsx +83 -31
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +111 -24
- package/src/browser/scroll-restoration.ts +29 -19
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -62
- package/src/browser/types.ts +60 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +6 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +333 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2256 -241
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/browser.ts +0 -1
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +69 -10
- package/src/index.ts +112 -14
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +413 -275
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +107 -32
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +50 -8
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +46 -30
- package/src/router/intercept-resolution.ts +23 -23
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +207 -30
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +161 -262
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +169 -140
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +119 -65
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +329 -305
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +128 -42
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -409
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +28 -4
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +53 -56
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -69
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +22 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +208 -201
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +211 -52
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +289 -124
- package/src/ssr/index.tsx +22 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +124 -70
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +36 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +236 -6
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +65 -207
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +46 -17
- package/src/vite/rango.ts +207 -125
- package/src/vite/router-discovery.ts +931 -133
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +97 -44
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -1,112 +1,194 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rango State
|
|
3
3
|
*
|
|
4
|
-
* Manages a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Manages a session-cookie-based state value for HTTP cache invalidation. The
|
|
5
|
+
* value is sent as the `X-Rango-State` header on prefetch and navigation
|
|
6
|
+
* requests; the server responds with `Vary: X-Rango-State`, so the browser HTTP
|
|
7
|
+
* cache keys responses by (URL, X-Rango-State value).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* - Build version changes on deploy, busting all cached prefetches.
|
|
11
|
-
* - Timestamp
|
|
9
|
+
* Value format: `{buildVersion}:{invalidationTimestamp}`
|
|
10
|
+
* - Build version changes on deploy, busting all cached prefetches at boot.
|
|
11
|
+
* - Timestamp rotates on invalidation (server action, invalidateClientCache).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Storage is a session cookie named by the server-resolved name passed to
|
|
14
|
+
* initRangoState (`{prefix}_{routerId}`, default prefix `rango-state`). The
|
|
15
|
+
* cookie jar is shared across tabs, so a per-request read IS the cross-tab
|
|
16
|
+
* value sync — no `storage` event is needed. An in-memory mirror is a
|
|
17
|
+
* write-through copy that is authoritative only when the cookie is unreadable
|
|
18
|
+
* (e.g. a sandboxed frame, or site data blocked wholesale): the failure
|
|
19
|
+
* direction is always toward freshness.
|
|
20
|
+
*
|
|
21
|
+
* Precedence is load-bearing: when `document.cookie` is readable, the
|
|
22
|
+
* per-request read wins; the mirror is a fallback, never a cache of the read.
|
|
23
|
+
* Caching the read across requests would reintroduce the staleness this
|
|
24
|
+
* mechanism removes.
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_STATE_COOKIE_PREFIX,
|
|
29
|
+
decodeStateValue,
|
|
30
|
+
getRawCookieValue,
|
|
31
|
+
mintStateValue,
|
|
32
|
+
serializeStateCookie,
|
|
33
|
+
} from "./cookie-name.js";
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
// Initialized from localStorage on first access or by initRangoState().
|
|
22
|
-
let cachedState: string | null = null;
|
|
35
|
+
let cookieName: string = DEFAULT_STATE_COOKIE_PREFIX;
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
// to localStorage, keeping cachedState fresh without polling.
|
|
26
|
-
let storageListenerAttached = false;
|
|
37
|
+
let currentVersion = "0";
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
cachedState = e.newValue;
|
|
33
|
-
});
|
|
34
|
-
storageListenerAttached = true;
|
|
35
|
-
}
|
|
39
|
+
let mirror: string | null = null;
|
|
40
|
+
let cookieBacked = false;
|
|
41
|
+
|
|
42
|
+
let externalRotationObserver: ((value: string) => void) | null = null;
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* (preserves invalidation state across refresh). Otherwise writes a new key.
|
|
45
|
+
* Register the observer invoked when a read detects an EXTERNAL rotation (a
|
|
46
|
+
* sibling tab, a server `Set-Cookie`, or a cookie clear). Self-rotations
|
|
47
|
+
* (invalidateRangoState) update the mirror synchronously and never fire it.
|
|
42
48
|
*/
|
|
43
|
-
export function
|
|
44
|
-
|
|
49
|
+
export function setRangoStateObserver(
|
|
50
|
+
observer: ((value: string) => void) | null,
|
|
51
|
+
): void {
|
|
52
|
+
externalRotationObserver = observer;
|
|
53
|
+
}
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
function notifyExternalRotation(value: string): void {
|
|
56
|
+
externalRotationObserver?.(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface CookieRead {
|
|
60
|
+
/** False when there is no document or the read threw (sandboxed frame). */
|
|
61
|
+
readable: boolean;
|
|
62
|
+
/** The cookie value, or null when readable but absent. */
|
|
63
|
+
value: string | null;
|
|
64
|
+
}
|
|
47
65
|
|
|
66
|
+
function readCookie(name: string): CookieRead {
|
|
67
|
+
if (typeof document === "undefined") return { readable: false, value: null };
|
|
68
|
+
let raw: string;
|
|
48
69
|
try {
|
|
49
|
-
|
|
50
|
-
if (existing) {
|
|
51
|
-
const colonIdx = existing.indexOf(":");
|
|
52
|
-
if (colonIdx > 0) {
|
|
53
|
-
const existingVersion = existing.slice(0, colonIdx);
|
|
54
|
-
if (existingVersion === version) {
|
|
55
|
-
cachedState = existing;
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// New version or first load
|
|
61
|
-
const newState = `${version}:${Date.now()}`;
|
|
62
|
-
localStorage.setItem(STORAGE_KEY, newState);
|
|
63
|
-
cachedState = newState;
|
|
70
|
+
raw = document.cookie;
|
|
64
71
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
return { readable: false, value: null };
|
|
73
|
+
}
|
|
74
|
+
return { readable: true, value: getRawCookieValue(raw, name) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeCookie(name: string, value: string): void {
|
|
78
|
+
if (typeof document === "undefined") return;
|
|
79
|
+
const secure =
|
|
80
|
+
typeof location !== "undefined" && location.protocol === "https:";
|
|
81
|
+
try {
|
|
82
|
+
document.cookie = serializeStateCookie(name, value, secure);
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mintValue(): string {
|
|
87
|
+
return mintStateValue(currentVersion, mirror);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize the Rango state cookie at app startup. `version` is the build
|
|
92
|
+
* version; `stateCookieName` is the server-resolved cookie name from payload
|
|
93
|
+
* metadata (falls back to the bare default prefix when a payload arrives
|
|
94
|
+
* without it). Keeps an existing matching-version cookie (preserves the cache
|
|
95
|
+
* key across reloads); mints fresh on a version change or a missing cookie.
|
|
96
|
+
*/
|
|
97
|
+
export function initRangoState(
|
|
98
|
+
version: string,
|
|
99
|
+
stateCookieName?: string,
|
|
100
|
+
): void {
|
|
101
|
+
currentVersion = version;
|
|
102
|
+
cookieName = stateCookieName || DEFAULT_STATE_COOKIE_PREFIX;
|
|
103
|
+
cleanupLegacyStorage();
|
|
104
|
+
|
|
105
|
+
const read = readCookie(cookieName);
|
|
106
|
+
if (!read.readable) {
|
|
107
|
+
// Cookies unreadable: the mirror is the source of truth for this session.
|
|
108
|
+
mirror = mintValue();
|
|
109
|
+
cookieBacked = false;
|
|
110
|
+
return;
|
|
67
111
|
}
|
|
112
|
+
if (read.value !== null) {
|
|
113
|
+
const decoded = decodeStateValue(read.value);
|
|
114
|
+
if (decoded && decoded.version === version) {
|
|
115
|
+
// Keep: a matching-version cookie survives the reload warm.
|
|
116
|
+
mirror = read.value;
|
|
117
|
+
cookieBacked = true;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Absent, malformed, or a version change (deploy): mint fresh and write.
|
|
122
|
+
mirror = mintValue();
|
|
123
|
+
cookieBacked = false;
|
|
124
|
+
writeCookie(cookieName, mirror);
|
|
68
125
|
}
|
|
69
126
|
|
|
70
127
|
/**
|
|
71
|
-
* Get the current Rango state
|
|
72
|
-
*
|
|
128
|
+
* Get the current Rango state value, used as the `X-Rango-State` header on
|
|
129
|
+
* prefetch and navigation requests. Reads the cookie every call (the read is
|
|
130
|
+
* the cross-tab sync channel) and reconciles the mirror.
|
|
73
131
|
*/
|
|
74
132
|
export function getRangoState(): string {
|
|
75
|
-
|
|
133
|
+
const read = readCookie(cookieName);
|
|
76
134
|
|
|
77
|
-
if (
|
|
135
|
+
if (!read.readable) {
|
|
136
|
+
// Mirror authoritative when the jar is unreadable.
|
|
137
|
+
return mirror ?? "0:0";
|
|
138
|
+
}
|
|
78
139
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
140
|
+
if (read.value !== null) {
|
|
141
|
+
if (read.value !== mirror) {
|
|
142
|
+
// External rotation (sibling tab / server Set-Cookie): adopt it. The
|
|
143
|
+
// mirror update makes this idempotent across a burst of reads.
|
|
144
|
+
mirror = read.value;
|
|
145
|
+
cookieBacked = true;
|
|
146
|
+
notifyExternalRotation(read.value);
|
|
147
|
+
} else {
|
|
148
|
+
cookieBacked = true;
|
|
84
149
|
}
|
|
85
|
-
|
|
86
|
-
// Fallback for unavailable localStorage
|
|
150
|
+
return read.value;
|
|
87
151
|
}
|
|
88
152
|
|
|
89
|
-
|
|
153
|
+
// Readable but absent.
|
|
154
|
+
if (cookieBacked) {
|
|
155
|
+
// present -> absent: an external clear. Mint fresh, write back, and notify
|
|
156
|
+
// once (cookieBacked flips to false so we don't re-fire on the next read).
|
|
157
|
+
mirror = mintValue();
|
|
158
|
+
cookieBacked = false;
|
|
159
|
+
writeCookie(cookieName, mirror);
|
|
160
|
+
notifyExternalRotation(mirror);
|
|
161
|
+
} else if (mirror === null) {
|
|
162
|
+
// First access with no cookie yet (pre-boot): mint silently — there is
|
|
163
|
+
// nothing to invalidate.
|
|
164
|
+
mirror = mintValue();
|
|
165
|
+
writeCookie(cookieName, mirror);
|
|
166
|
+
}
|
|
167
|
+
return mirror;
|
|
90
168
|
}
|
|
91
169
|
|
|
92
170
|
/**
|
|
93
|
-
* Invalidate the Rango state
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
171
|
+
* Invalidate the Rango state (self-rotation). Called when the client clears its
|
|
172
|
+
* prefetch caches (e.g. via the server-action bridge). Rotates the timestamp,
|
|
173
|
+
* keeps the version, writes the cookie, and updates the mirror synchronously so
|
|
174
|
+
* the external-rotation observer is NOT triggered by our own write.
|
|
97
175
|
*/
|
|
98
176
|
export function invalidateRangoState(): void {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
cachedState = newState;
|
|
104
|
-
|
|
105
|
-
if (typeof window === "undefined") return;
|
|
177
|
+
mirror = mintValue();
|
|
178
|
+
cookieBacked = false;
|
|
179
|
+
writeCookie(cookieName, mirror);
|
|
180
|
+
}
|
|
106
181
|
|
|
182
|
+
function cleanupLegacyStorage(): void {
|
|
183
|
+
if (typeof localStorage === "undefined") return;
|
|
107
184
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
185
|
+
const toRemove: string[] = [];
|
|
186
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
187
|
+
const key = localStorage.key(i);
|
|
188
|
+
if (key === "rango-state" || (key && key.startsWith("rango-state:"))) {
|
|
189
|
+
toRemove.push(key);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const key of toRemove) localStorage.removeItem(key);
|
|
193
|
+
} catch {}
|
|
112
194
|
}
|
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useCallback,
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
|
+
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
type ForwardRefExoticComponent,
|
|
10
11
|
type RefAttributes,
|
|
@@ -32,13 +33,12 @@ export type LinkState =
|
|
|
32
33
|
| StateOrGetter<Record<string, unknown>>;
|
|
33
34
|
|
|
34
35
|
import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
|
|
36
|
+
import { getAppVersion } from "../app-version.js";
|
|
35
37
|
import {
|
|
36
38
|
observeForPrefetch,
|
|
37
39
|
unobserveForPrefetch,
|
|
38
40
|
} from "../prefetch/observer.js";
|
|
39
41
|
|
|
40
|
-
// Touch device detection for adaptive strategy.
|
|
41
|
-
// Checked once at module load (Link.tsx is "use client", runs only in browser).
|
|
42
42
|
const isTouchDevice =
|
|
43
43
|
typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
|
|
44
44
|
|
|
@@ -95,6 +95,31 @@ export interface LinkProps extends Omit<
|
|
|
95
95
|
* @default "none"
|
|
96
96
|
*/
|
|
97
97
|
prefetch?: PrefetchStrategy;
|
|
98
|
+
/**
|
|
99
|
+
* Opt-in override for the prefetch cache scope.
|
|
100
|
+
*
|
|
101
|
+
* The default cache is source-agnostic: one shared entry per target,
|
|
102
|
+
* keyed on Rango state + target URL. This is correct for routes whose
|
|
103
|
+
* response shape doesn't depend on where the user navigates from.
|
|
104
|
+
*
|
|
105
|
+
* Set `":source"` when this Link's response would legitimately differ
|
|
106
|
+
* based on the source page — typically when the target route (or one
|
|
107
|
+
* of its layouts) uses a custom `revalidate()` handler that reads
|
|
108
|
+
* `currentUrl` / `currentParams`, and the wildcard entry would
|
|
109
|
+
* therefore serve the wrong diff to a navigation from a different
|
|
110
|
+
* source.
|
|
111
|
+
*
|
|
112
|
+
* Intercept responses are auto-scoped to the source via a server-side
|
|
113
|
+
* tag, so `":source"` is only needed for custom revalidation logic.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* // Route uses a `revalidate()` that branches on currentUrl — opt in
|
|
118
|
+
* // so prefetches don't bleed across source pages.
|
|
119
|
+
* <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
prefetchKey?: ":source";
|
|
98
123
|
/**
|
|
99
124
|
* State to pass to history.pushState/replaceState.
|
|
100
125
|
* Accessible via useLocationState() hook.
|
|
@@ -182,6 +207,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
182
207
|
reloadDocument = false,
|
|
183
208
|
revalidate,
|
|
184
209
|
prefetch = "none",
|
|
210
|
+
prefetchKey,
|
|
185
211
|
state,
|
|
186
212
|
children,
|
|
187
213
|
onClick,
|
|
@@ -192,6 +218,16 @@ export const Link: ForwardRefExoticComponent<
|
|
|
192
218
|
const ctx = useContext(NavigationStoreContext);
|
|
193
219
|
const isExternal = isExternalUrl(to);
|
|
194
220
|
|
|
221
|
+
// Auto-prefix with basename for app-local paths.
|
|
222
|
+
// Skip if external, already prefixed, or not a root-relative path.
|
|
223
|
+
const resolvedTo = useMemo(() => {
|
|
224
|
+
if (isExternal) return to;
|
|
225
|
+
const bn = ctx?.basename;
|
|
226
|
+
if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
|
|
227
|
+
return to;
|
|
228
|
+
return to === "/" ? bn : bn + to;
|
|
229
|
+
}, [to, isExternal, ctx?.basename]);
|
|
230
|
+
|
|
195
231
|
// Resolve adaptive: viewport on touch devices, hover on pointer devices
|
|
196
232
|
const resolvedStrategy =
|
|
197
233
|
prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
|
|
@@ -273,9 +309,23 @@ export const Link: ForwardRefExoticComponent<
|
|
|
273
309
|
resolvedState = currentState;
|
|
274
310
|
}
|
|
275
311
|
|
|
276
|
-
ctx.navigate(
|
|
312
|
+
ctx.navigate(resolvedTo, {
|
|
313
|
+
replace,
|
|
314
|
+
scroll,
|
|
315
|
+
state: resolvedState,
|
|
316
|
+
revalidate,
|
|
317
|
+
});
|
|
277
318
|
},
|
|
278
|
-
[
|
|
319
|
+
[
|
|
320
|
+
resolvedTo,
|
|
321
|
+
isExternal,
|
|
322
|
+
reloadDocument,
|
|
323
|
+
replace,
|
|
324
|
+
scroll,
|
|
325
|
+
revalidate,
|
|
326
|
+
ctx,
|
|
327
|
+
onClick,
|
|
328
|
+
],
|
|
279
329
|
);
|
|
280
330
|
|
|
281
331
|
const handleMouseEnter = useCallback(() => {
|
|
@@ -289,9 +339,15 @@ export const Link: ForwardRefExoticComponent<
|
|
|
289
339
|
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
290
340
|
// deduplicates if the viewport prefetch already completed.
|
|
291
341
|
const segmentState = ctx.store.getSegmentState();
|
|
292
|
-
prefetchDirect(
|
|
342
|
+
prefetchDirect(
|
|
343
|
+
resolvedTo,
|
|
344
|
+
segmentState.currentSegmentIds,
|
|
345
|
+
getAppVersion(),
|
|
346
|
+
ctx.store.getRouterId?.(),
|
|
347
|
+
prefetchKey,
|
|
348
|
+
);
|
|
293
349
|
}
|
|
294
|
-
}, [resolvedStrategy,
|
|
350
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
295
351
|
|
|
296
352
|
// Viewport/render prefetch: waits for idle before starting,
|
|
297
353
|
// uses concurrency-limited queue to avoid flooding.
|
|
@@ -308,7 +364,13 @@ export const Link: ForwardRefExoticComponent<
|
|
|
308
364
|
const triggerPrefetch = () => {
|
|
309
365
|
if (cancelled) return;
|
|
310
366
|
const segmentState = ctx.store.getSegmentState();
|
|
311
|
-
prefetchQueued(
|
|
367
|
+
prefetchQueued(
|
|
368
|
+
resolvedTo,
|
|
369
|
+
segmentState.currentSegmentIds,
|
|
370
|
+
getAppVersion(),
|
|
371
|
+
ctx.store.getRouterId?.(),
|
|
372
|
+
prefetchKey,
|
|
373
|
+
);
|
|
312
374
|
};
|
|
313
375
|
|
|
314
376
|
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
@@ -347,12 +409,12 @@ export const Link: ForwardRefExoticComponent<
|
|
|
347
409
|
unobserveForPrefetch(observedElement);
|
|
348
410
|
}
|
|
349
411
|
};
|
|
350
|
-
}, [resolvedStrategy,
|
|
412
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
351
413
|
|
|
352
414
|
return (
|
|
353
415
|
<a
|
|
354
416
|
ref={setRef}
|
|
355
|
-
href={
|
|
417
|
+
href={resolvedTo}
|
|
356
418
|
onClick={handleClick}
|
|
357
419
|
onMouseEnter={handleMouseEnter}
|
|
358
420
|
data-link-component
|
|
@@ -362,7 +424,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
362
424
|
data-revalidate={revalidate === false ? "false" : undefined}
|
|
363
425
|
{...props}
|
|
364
426
|
>
|
|
365
|
-
<LinkContext.Provider value={
|
|
427
|
+
<LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
|
|
366
428
|
</a>
|
|
367
429
|
);
|
|
368
430
|
});
|
|
@@ -28,6 +28,8 @@ import { NonceContext } from "./nonce-context.js";
|
|
|
28
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
29
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
30
|
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
31
|
+
import { createAppShellRef, type AppShellRef } from "../app-shell.js";
|
|
32
|
+
import { debugLog } from "../logging.js";
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Process handles from an async generator, updating the event controller
|
|
@@ -46,10 +48,22 @@ async function processHandles(
|
|
|
46
48
|
store: NavigationStore;
|
|
47
49
|
matched?: string[];
|
|
48
50
|
isPartial?: boolean;
|
|
51
|
+
/** Server's `resolvedIds`: every segment re-resolved this request,
|
|
52
|
+
* including null-component ones excluded from `diff`/`segments`.
|
|
53
|
+
* Drives cleanup of stale handle buckets when a re-resolved segment
|
|
54
|
+
* pushed nothing. */
|
|
55
|
+
resolvedIds?: string[];
|
|
49
56
|
historyKey: string;
|
|
50
57
|
},
|
|
51
58
|
): Promise<void> {
|
|
52
|
-
const {
|
|
59
|
+
const {
|
|
60
|
+
eventController,
|
|
61
|
+
store,
|
|
62
|
+
matched,
|
|
63
|
+
isPartial,
|
|
64
|
+
resolvedIds,
|
|
65
|
+
historyKey,
|
|
66
|
+
} = opts;
|
|
53
67
|
|
|
54
68
|
let yieldCount = 0;
|
|
55
69
|
for await (const handleData of handlesGenerator) {
|
|
@@ -57,14 +71,14 @@ async function processHandles(
|
|
|
57
71
|
// This prevents handle data from cancelled navigations polluting
|
|
58
72
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
59
73
|
if (historyKey !== store.getHistoryKey()) {
|
|
60
|
-
|
|
74
|
+
debugLog(
|
|
61
75
|
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
62
76
|
);
|
|
63
77
|
return;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
yieldCount++;
|
|
67
|
-
eventController.setHandleData(handleData, matched, isPartial);
|
|
81
|
+
eventController.setHandleData(handleData, matched, isPartial, resolvedIds);
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
// Check again before final updates
|
|
@@ -72,12 +86,11 @@ async function processHandles(
|
|
|
72
86
|
return;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
// For partial updates where the generator yielded nothing (
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// route might not push any breadcrumbs, but we still need to remove the old ones.
|
|
89
|
+
// For partial updates where the generator yielded nothing (every
|
|
90
|
+
// re-resolved handler pushed nothing), still call setHandleData so the
|
|
91
|
+
// cleanup pass can clear out stale buckets for those segments.
|
|
79
92
|
if (yieldCount === 0 && matched) {
|
|
80
|
-
eventController.setHandleData({}, matched, true);
|
|
93
|
+
eventController.setHandleData({}, matched, true, resolvedIds);
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
// After handles processing completes, update the cache's handleData.
|
|
@@ -133,10 +146,25 @@ export interface NavigationProviderProps {
|
|
|
133
146
|
warmupEnabled?: boolean;
|
|
134
147
|
|
|
135
148
|
/**
|
|
136
|
-
* App version from server payload
|
|
137
|
-
*
|
|
149
|
+
* App version from server payload.
|
|
150
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
138
151
|
*/
|
|
139
152
|
version?: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
156
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
157
|
+
*/
|
|
158
|
+
basename?: string;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* App-shell ref. When provided, the context's `basename` and `version` are
|
|
162
|
+
* read through it (live getters) so they don't close over a stale snapshot or
|
|
163
|
+
* invalidate the memoized context value. The shell is set once at init and is
|
|
164
|
+
* not swapped within a session — a cross-app navigation is a full document
|
|
165
|
+
* load (X-RSC-Reload), so the target app establishes its own shell on load.
|
|
166
|
+
*/
|
|
167
|
+
appShellRef?: AppShellRef;
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
/**
|
|
@@ -169,6 +197,8 @@ export function NavigationProvider({
|
|
|
169
197
|
initialTheme,
|
|
170
198
|
warmupEnabled,
|
|
171
199
|
version,
|
|
200
|
+
basename,
|
|
201
|
+
appShellRef,
|
|
172
202
|
}: NavigationProviderProps): ReactNode {
|
|
173
203
|
// Track current payload for rendering (this triggers re-renders)
|
|
174
204
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -190,17 +220,35 @@ export function NavigationProvider({
|
|
|
190
220
|
await bridge.refresh();
|
|
191
221
|
}, []);
|
|
192
222
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
223
|
+
// basename/version are always read through a shell ref so the context value
|
|
224
|
+
// has a single shape. Both are set once: a supplied appShellRef is seeded
|
|
225
|
+
// from the init payload (a cross-app navigation reloads, so it is not swapped
|
|
226
|
+
// in-session), and the standalone fallback wraps the mount-time props.
|
|
227
|
+
const fallbackShellRef = useRef<AppShellRef | null>(null);
|
|
228
|
+
if (!fallbackShellRef.current) {
|
|
229
|
+
fallbackShellRef.current = createAppShellRef({ basename, version });
|
|
230
|
+
}
|
|
231
|
+
const shellRef = appShellRef ?? fallbackShellRef.current;
|
|
232
|
+
|
|
233
|
+
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
234
|
+
const value = {
|
|
196
235
|
store,
|
|
197
236
|
eventController,
|
|
198
237
|
navigate,
|
|
199
238
|
refresh,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
239
|
+
} as NavigationStoreContextValue;
|
|
240
|
+
Object.defineProperty(value, "basename", {
|
|
241
|
+
configurable: true,
|
|
242
|
+
enumerable: true,
|
|
243
|
+
get: () => shellRef.get().basename,
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(value, "version", {
|
|
246
|
+
configurable: true,
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: () => shellRef.get().version,
|
|
249
|
+
});
|
|
250
|
+
return value;
|
|
251
|
+
}, []);
|
|
204
252
|
|
|
205
253
|
// Connection warmup: keep TLS alive after idle periods.
|
|
206
254
|
// After 60s of no user interaction, marks connection as "cold".
|
|
@@ -338,8 +386,12 @@ export function NavigationProvider({
|
|
|
338
386
|
metadata: update.metadata,
|
|
339
387
|
});
|
|
340
388
|
|
|
341
|
-
// Update route params
|
|
342
|
-
|
|
389
|
+
// Update route params. Only reset when the server actually sends a params
|
|
390
|
+
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
391
|
+
// responses that omitted params). Explicit `{}` still clears correctly.
|
|
392
|
+
if (update.metadata.params !== undefined) {
|
|
393
|
+
eventController.setParams(update.metadata.params);
|
|
394
|
+
}
|
|
343
395
|
|
|
344
396
|
// Update handle data progressively as it streams in
|
|
345
397
|
if (update.metadata.handles) {
|
|
@@ -352,24 +404,20 @@ export function NavigationProvider({
|
|
|
352
404
|
store,
|
|
353
405
|
matched: update.metadata.matched,
|
|
354
406
|
isPartial: update.metadata.isPartial,
|
|
407
|
+
resolvedIds: update.metadata.resolvedIds,
|
|
355
408
|
historyKey,
|
|
356
409
|
}).catch((err) =>
|
|
357
410
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
358
411
|
);
|
|
359
|
-
} else if (update.metadata.cachedHandleData) {
|
|
360
|
-
// For back/forward navigation from cache, restore the cached handleData
|
|
361
|
-
// This restores breadcrumbs to the exact state they were when the page was cached
|
|
362
|
-
eventController.setHandleData(
|
|
363
|
-
update.metadata.cachedHandleData,
|
|
364
|
-
update.metadata.matched,
|
|
365
|
-
false, // full replace - restore entire cached state
|
|
366
|
-
);
|
|
367
412
|
} else if (update.metadata.matched) {
|
|
368
|
-
//
|
|
413
|
+
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
414
|
+
// partial cleanup of segments no longer matched.
|
|
415
|
+
const cached = update.metadata.cachedHandleData;
|
|
369
416
|
eventController.setHandleData(
|
|
370
|
-
{},
|
|
417
|
+
cached ?? {},
|
|
371
418
|
update.metadata.matched,
|
|
372
|
-
|
|
419
|
+
cached === undefined,
|
|
420
|
+
cached === undefined ? update.metadata.resolvedIds : undefined,
|
|
373
421
|
);
|
|
374
422
|
}
|
|
375
423
|
});
|
|
@@ -391,7 +439,11 @@ export function NavigationProvider({
|
|
|
391
439
|
// Build the content tree
|
|
392
440
|
let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
|
|
393
441
|
|
|
394
|
-
// Wrap with ThemeProvider when theme is enabled
|
|
442
|
+
// Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
|
|
443
|
+
// document-lifetime: its config comes from the initial load and persists for
|
|
444
|
+
// the session. It sits above the segment tree and is not remounted in-session;
|
|
445
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the target
|
|
446
|
+
// app's theme config takes effect on its own load.
|
|
395
447
|
if (themeConfig) {
|
|
396
448
|
content = (
|
|
397
449
|
<ThemeProvider config={themeConfig} initialTheme={initialTheme}>
|
|
@@ -14,17 +14,21 @@ export interface ScrollRestorationProps {
|
|
|
14
14
|
* Return location.pathname to restore scroll based on path
|
|
15
15
|
* (useful for keeping scroll position on the same page).
|
|
16
16
|
*
|
|
17
|
+
* Provide a stable reference: a module-level function or one wrapped in
|
|
18
|
+
* useCallback. The init effect re-runs when getKey's identity changes, and
|
|
19
|
+
* teardown clears in-memory scroll positions — a fresh inline arrow on every
|
|
20
|
+
* parent render would discard unpersisted positions mid-session.
|
|
21
|
+
*
|
|
17
22
|
* @example
|
|
18
23
|
* ```tsx
|
|
24
|
+
* // Stable module-level getKey (recommended)
|
|
25
|
+
* const byPathname = (location) => location.pathname;
|
|
26
|
+
*
|
|
19
27
|
* // Restore based on pathname (same URL = same scroll)
|
|
20
|
-
* <ScrollRestoration
|
|
21
|
-
* getKey={(location) => location.pathname}
|
|
22
|
-
* />
|
|
28
|
+
* <ScrollRestoration getKey={byPathname} />
|
|
23
29
|
*
|
|
24
30
|
* // Restore based on unique history entry (default)
|
|
25
|
-
* <ScrollRestoration
|
|
26
|
-
* getKey={(location) => location.key}
|
|
27
|
-
* />
|
|
31
|
+
* // <ScrollRestoration /> — omit getKey to use location.key
|
|
28
32
|
* ```
|
|
29
33
|
*/
|
|
30
34
|
getKey?: (location: {
|