@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad
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 +17 -0
- package/README.md +291 -61
- package/dist/bin/rango.js +544 -143
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3744 -1329
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +67 -13
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +312 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +322 -19
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +282 -60
- package/skills/host-router/SKILL.md +278 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +297 -31
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +155 -111
- package/skills/rango/SKILL.md +251 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +104 -9
- package/skills/router-setup/SKILL.md +124 -11
- package/skills/scripts/SKILL.md +179 -0
- 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 +125 -222
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -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 +123 -0
- package/skills/typesafety/SKILL.md +357 -52
- package/skills/use-cache/SKILL.md +46 -14
- 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 +67 -40
- package/src/bin/rango.ts +18 -0
- 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/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +200 -30
- package/src/browser/navigation-client.ts +217 -58
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +312 -30
- package/src/browser/prefetch/fetch.ts +344 -47
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +125 -18
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -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-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- 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 +80 -5
- package/src/browser/rsc-router.tsx +226 -75
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +619 -442
- package/src/browser/types.ts +115 -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 +7 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +182 -37
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +392 -106
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +45 -21
- package/src/client.tsx +120 -336
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +95 -0
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -5
- package/src/host/pattern-matcher.ts +23 -52
- 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 +79 -29
- package/src/index.ts +149 -65
- package/src/internal-debug.ts +11 -10
- 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/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +63 -26
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -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 +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +53 -12
- 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 +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +110 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -49
- package/src/router/metrics.ts +217 -26
- package/src/router/middleware-types.ts +24 -110
- package/src/router/middleware.ts +384 -197
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +148 -91
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +199 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +144 -74
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +129 -36
- package/src/router/router-options.ts +185 -23
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +281 -76
- package/src/router/segment-resolution/helpers.ts +116 -31
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +493 -391
- 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 +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +180 -58
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +182 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +702 -460
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +98 -19
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +126 -106
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +269 -114
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +34 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +49 -41
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +317 -63
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +425 -177
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +25 -16
- 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 +701 -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 +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -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 +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +3 -19
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- 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 +236 -88
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +10 -45
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -19
- 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 +64 -91
- package/src/vite/discovery/discover-routers.ts +147 -88
- 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 +247 -145
- 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 +61 -13
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +10 -3
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- 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 +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +241 -287
- package/src/vite/router-discovery.ts +956 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -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 +141 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/CLAUDE.md +0 -5
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -6
- package/src/network-error-thrower.tsx +0 -23
- package/src/route-definition/route-function.ts +0 -119
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -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,31 +33,54 @@ 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Read current touch/no-hover capability. Evaluated at the point of use (per
|
|
44
|
+
* render) rather than once at module load, so `prefetch="adaptive"` reacts to
|
|
45
|
+
* input-capability changes on hybrid devices (touch laptops, tablets gaining or
|
|
46
|
+
* losing a pointer) and after SSR -> hydrate. The SSR guard returns a stable
|
|
47
|
+
* `false` (pointer/hover default) so the resolved strategy doesn't drift on the
|
|
48
|
+
* server vs the first client render.
|
|
49
|
+
*/
|
|
50
|
+
function isTouchDevice(): boolean {
|
|
51
|
+
if (typeof window === "undefined") return false;
|
|
52
|
+
return window.matchMedia("(hover: none)").matches;
|
|
53
|
+
}
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
56
|
* Prefetch strategy for the Link component
|
|
47
57
|
* - "hover": Prefetch on mouse enter (direct, no queue)
|
|
48
58
|
* - "viewport": Prefetch when link enters viewport (queued, waits for idle)
|
|
49
59
|
* - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
|
|
50
|
-
* - "
|
|
60
|
+
* - "adaptive": Hover on pointer devices, viewport on touch devices
|
|
51
61
|
* - "none": No prefetching (default)
|
|
52
62
|
*/
|
|
53
63
|
export type PrefetchStrategy =
|
|
54
64
|
| "hover"
|
|
55
65
|
| "viewport"
|
|
56
66
|
| "render"
|
|
57
|
-
| "
|
|
67
|
+
| "adaptive"
|
|
58
68
|
| "none";
|
|
59
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a prefetch strategy, expanding "adaptive" to the concrete strategy
|
|
72
|
+
* for the CURRENT input capability: "viewport" on touch (no-hover) devices,
|
|
73
|
+
* "hover" on pointer devices. Non-adaptive strategies pass through unchanged.
|
|
74
|
+
* Reads touch capability live (not a module-load snapshot) so the result
|
|
75
|
+
* tracks input-capability changes.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveAdaptiveStrategy(
|
|
78
|
+
prefetch: PrefetchStrategy,
|
|
79
|
+
): PrefetchStrategy {
|
|
80
|
+
if (prefetch !== "adaptive") return prefetch;
|
|
81
|
+
return isTouchDevice() ? "viewport" : "hover";
|
|
82
|
+
}
|
|
83
|
+
|
|
60
84
|
/**
|
|
61
85
|
* Link component props
|
|
62
86
|
*/
|
|
@@ -80,11 +104,46 @@ export interface LinkProps extends Omit<
|
|
|
80
104
|
* Force full document navigation instead of SPA
|
|
81
105
|
*/
|
|
82
106
|
reloadDocument?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Whether to revalidate server data on navigation.
|
|
109
|
+
* Set to `false` to skip the RSC server fetch and only update the URL.
|
|
110
|
+
*
|
|
111
|
+
* Only takes effect when the pathname stays the same (search param / hash changes).
|
|
112
|
+
* If the pathname changes, this option is ignored and a full navigation occurs.
|
|
113
|
+
*
|
|
114
|
+
* @default true
|
|
115
|
+
*/
|
|
116
|
+
revalidate?: boolean;
|
|
83
117
|
/**
|
|
84
118
|
* Prefetch strategy for the link destination
|
|
85
119
|
* @default "none"
|
|
86
120
|
*/
|
|
87
121
|
prefetch?: PrefetchStrategy;
|
|
122
|
+
/**
|
|
123
|
+
* Opt-in override for the prefetch cache scope.
|
|
124
|
+
*
|
|
125
|
+
* The default cache is source-agnostic: one shared entry per target,
|
|
126
|
+
* keyed on Rango state + target URL. This is correct for routes whose
|
|
127
|
+
* response shape doesn't depend on where the user navigates from.
|
|
128
|
+
*
|
|
129
|
+
* Set `":source"` when this Link's response would legitimately differ
|
|
130
|
+
* based on the source page — typically when the target route (or one
|
|
131
|
+
* of its layouts) uses a custom `revalidate()` handler that reads
|
|
132
|
+
* `currentUrl` / `currentParams`, and the wildcard entry would
|
|
133
|
+
* therefore serve the wrong diff to a navigation from a different
|
|
134
|
+
* source.
|
|
135
|
+
*
|
|
136
|
+
* Intercept responses are auto-scoped to the source via a server-side
|
|
137
|
+
* tag, so `":source"` is only needed for custom revalidation logic.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```tsx
|
|
141
|
+
* // Route uses a `revalidate()` that branches on currentUrl — opt in
|
|
142
|
+
* // so prefetches don't bleed across source pages.
|
|
143
|
+
* <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
prefetchKey?: ":source";
|
|
88
147
|
/**
|
|
89
148
|
* State to pass to history.pushState/replaceState.
|
|
90
149
|
* Accessible via useLocationState() hook.
|
|
@@ -170,7 +229,9 @@ export const Link: ForwardRefExoticComponent<
|
|
|
170
229
|
replace = false,
|
|
171
230
|
scroll = true,
|
|
172
231
|
reloadDocument = false,
|
|
232
|
+
revalidate,
|
|
173
233
|
prefetch = "none",
|
|
234
|
+
prefetchKey,
|
|
174
235
|
state,
|
|
175
236
|
children,
|
|
176
237
|
onClick,
|
|
@@ -181,9 +242,20 @@ export const Link: ForwardRefExoticComponent<
|
|
|
181
242
|
const ctx = useContext(NavigationStoreContext);
|
|
182
243
|
const isExternal = isExternalUrl(to);
|
|
183
244
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
245
|
+
// Auto-prefix with basename for app-local paths.
|
|
246
|
+
// Skip if external, already prefixed, or not a root-relative path.
|
|
247
|
+
const resolvedTo = useMemo(() => {
|
|
248
|
+
if (isExternal) return to;
|
|
249
|
+
const bn = ctx?.basename;
|
|
250
|
+
if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
|
|
251
|
+
return to;
|
|
252
|
+
return to === "/" ? bn : bn + to;
|
|
253
|
+
}, [to, isExternal, ctx?.basename]);
|
|
254
|
+
|
|
255
|
+
// Resolve adaptive: viewport on touch devices, hover on pointer devices.
|
|
256
|
+
// isTouchDevice() is read here (per render), not from a module-load snapshot,
|
|
257
|
+
// so a device whose input capability changes resolves to the current value.
|
|
258
|
+
const resolvedStrategy = resolveAdaptiveStrategy(prefetch);
|
|
187
259
|
|
|
188
260
|
// Internal ref for viewport observation; merge with forwarded ref
|
|
189
261
|
const internalRef = useRef<HTMLAnchorElement | null>(null);
|
|
@@ -262,17 +334,45 @@ export const Link: ForwardRefExoticComponent<
|
|
|
262
334
|
resolvedState = currentState;
|
|
263
335
|
}
|
|
264
336
|
|
|
265
|
-
ctx.navigate(
|
|
337
|
+
ctx.navigate(resolvedTo, {
|
|
338
|
+
replace,
|
|
339
|
+
scroll,
|
|
340
|
+
state: resolvedState,
|
|
341
|
+
revalidate,
|
|
342
|
+
});
|
|
266
343
|
},
|
|
267
|
-
[
|
|
344
|
+
[
|
|
345
|
+
resolvedTo,
|
|
346
|
+
isExternal,
|
|
347
|
+
reloadDocument,
|
|
348
|
+
replace,
|
|
349
|
+
scroll,
|
|
350
|
+
revalidate,
|
|
351
|
+
ctx,
|
|
352
|
+
onClick,
|
|
353
|
+
],
|
|
268
354
|
);
|
|
269
355
|
|
|
270
356
|
const handleMouseEnter = useCallback(() => {
|
|
271
|
-
if (
|
|
357
|
+
if (
|
|
358
|
+
(resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
|
|
359
|
+
!isExternal &&
|
|
360
|
+
ctx?.store
|
|
361
|
+
) {
|
|
362
|
+
// For "hover", this is the primary prefetch trigger.
|
|
363
|
+
// For "viewport", this upgrades/prioritizes a potentially queued
|
|
364
|
+
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
365
|
+
// deduplicates if the viewport prefetch already completed.
|
|
272
366
|
const segmentState = ctx.store.getSegmentState();
|
|
273
|
-
prefetchDirect(
|
|
367
|
+
prefetchDirect(
|
|
368
|
+
resolvedTo,
|
|
369
|
+
segmentState.currentSegmentIds,
|
|
370
|
+
getAppVersion(),
|
|
371
|
+
ctx.store.getRouterId?.(),
|
|
372
|
+
prefetchKey,
|
|
373
|
+
);
|
|
274
374
|
}
|
|
275
|
-
}, [resolvedStrategy,
|
|
375
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
276
376
|
|
|
277
377
|
// Viewport/render prefetch: waits for idle before starting,
|
|
278
378
|
// uses concurrency-limited queue to avoid flooding.
|
|
@@ -289,7 +389,13 @@ export const Link: ForwardRefExoticComponent<
|
|
|
289
389
|
const triggerPrefetch = () => {
|
|
290
390
|
if (cancelled) return;
|
|
291
391
|
const segmentState = ctx.store.getSegmentState();
|
|
292
|
-
prefetchQueued(
|
|
392
|
+
prefetchQueued(
|
|
393
|
+
resolvedTo,
|
|
394
|
+
segmentState.currentSegmentIds,
|
|
395
|
+
getAppVersion(),
|
|
396
|
+
ctx.store.getRouterId?.(),
|
|
397
|
+
prefetchKey,
|
|
398
|
+
);
|
|
293
399
|
};
|
|
294
400
|
|
|
295
401
|
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
@@ -328,21 +434,22 @@ export const Link: ForwardRefExoticComponent<
|
|
|
328
434
|
unobserveForPrefetch(observedElement);
|
|
329
435
|
}
|
|
330
436
|
};
|
|
331
|
-
}, [resolvedStrategy,
|
|
437
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
332
438
|
|
|
333
439
|
return (
|
|
334
440
|
<a
|
|
335
441
|
ref={setRef}
|
|
336
|
-
href={
|
|
442
|
+
href={resolvedTo}
|
|
337
443
|
onClick={handleClick}
|
|
338
444
|
onMouseEnter={handleMouseEnter}
|
|
339
445
|
data-link-component
|
|
340
446
|
data-external={isExternal ? "" : undefined}
|
|
341
447
|
data-scroll={scroll === false ? "false" : undefined}
|
|
342
448
|
data-replace={replace ? "true" : undefined}
|
|
449
|
+
data-revalidate={revalidate === false ? "false" : undefined}
|
|
343
450
|
{...props}
|
|
344
451
|
>
|
|
345
|
-
<LinkContext.Provider value={
|
|
452
|
+
<LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
|
|
346
453
|
</a>
|
|
347
454
|
);
|
|
348
455
|
});
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import React, {
|
|
4
4
|
useState,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useCallback,
|
|
7
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
8
10
|
use,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from "react";
|
|
@@ -25,6 +27,10 @@ import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
|
25
27
|
import { NonceContext } from "./nonce-context.js";
|
|
26
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
27
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
|
+
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
31
|
+
import { createAppShellRef, type AppShellRef } from "../app-shell.js";
|
|
32
|
+
import { startConnectionWarmup } from "../connection-warmup.js";
|
|
33
|
+
import { debugLog } from "../logging.js";
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* Process handles from an async generator, updating the event controller
|
|
@@ -43,10 +49,22 @@ async function processHandles(
|
|
|
43
49
|
store: NavigationStore;
|
|
44
50
|
matched?: string[];
|
|
45
51
|
isPartial?: boolean;
|
|
52
|
+
/** Server's `resolvedIds`: every segment re-resolved this request,
|
|
53
|
+
* including null-component ones excluded from `diff`/`segments`.
|
|
54
|
+
* Drives cleanup of stale handle buckets when a re-resolved segment
|
|
55
|
+
* pushed nothing. */
|
|
56
|
+
resolvedIds?: string[];
|
|
46
57
|
historyKey: string;
|
|
47
58
|
},
|
|
48
59
|
): Promise<void> {
|
|
49
|
-
const {
|
|
60
|
+
const {
|
|
61
|
+
eventController,
|
|
62
|
+
store,
|
|
63
|
+
matched,
|
|
64
|
+
isPartial,
|
|
65
|
+
resolvedIds,
|
|
66
|
+
historyKey,
|
|
67
|
+
} = opts;
|
|
50
68
|
|
|
51
69
|
let yieldCount = 0;
|
|
52
70
|
for await (const handleData of handlesGenerator) {
|
|
@@ -54,14 +72,14 @@ async function processHandles(
|
|
|
54
72
|
// This prevents handle data from cancelled navigations polluting
|
|
55
73
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
56
74
|
if (historyKey !== store.getHistoryKey()) {
|
|
57
|
-
|
|
75
|
+
debugLog(
|
|
58
76
|
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
59
77
|
);
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
yieldCount++;
|
|
64
|
-
eventController.setHandleData(handleData, matched, isPartial);
|
|
82
|
+
eventController.setHandleData(handleData, matched, isPartial, resolvedIds);
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
// Check again before final updates
|
|
@@ -69,12 +87,11 @@ async function processHandles(
|
|
|
69
87
|
return;
|
|
70
88
|
}
|
|
71
89
|
|
|
72
|
-
// For partial updates where the generator yielded nothing (
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// route might not push any breadcrumbs, but we still need to remove the old ones.
|
|
90
|
+
// For partial updates where the generator yielded nothing (every
|
|
91
|
+
// re-resolved handler pushed nothing), still call setHandleData so the
|
|
92
|
+
// cleanup pass can clear out stale buckets for those segments.
|
|
76
93
|
if (yieldCount === 0 && matched) {
|
|
77
|
-
eventController.setHandleData({}, matched, true);
|
|
94
|
+
eventController.setHandleData({}, matched, true, resolvedIds);
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
// After handles processing completes, update the cache's handleData.
|
|
@@ -130,10 +147,33 @@ export interface NavigationProviderProps {
|
|
|
130
147
|
warmupEnabled?: boolean;
|
|
131
148
|
|
|
132
149
|
/**
|
|
133
|
-
* App version from server payload
|
|
134
|
-
*
|
|
150
|
+
* App version from server payload.
|
|
151
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
135
152
|
*/
|
|
136
153
|
version?: string;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
157
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
158
|
+
*/
|
|
159
|
+
basename?: string;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* App-shell ref. When provided, the context's `basename` and `version` are
|
|
163
|
+
* read through it (live getters) so they don't close over a stale snapshot or
|
|
164
|
+
* invalidate the memoized context value. The shell is set once at init and is
|
|
165
|
+
* not swapped within a session — a cross-app navigation is a full document
|
|
166
|
+
* load (X-RSC-Reload), so the target app establishes its own shell on load.
|
|
167
|
+
*/
|
|
168
|
+
appShellRef?: AppShellRef;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* CSP nonce to expose via NonceContext. Production leaves this undefined — the
|
|
172
|
+
* browser has no nonce (it is a server-side HTML concern), and SSR provides the
|
|
173
|
+
* nonce through its own NonceContext.Provider. Test harnesses (renderRoute) set
|
|
174
|
+
* it to seed a nonce so components calling useNonce() can be exercised.
|
|
175
|
+
*/
|
|
176
|
+
nonce?: string;
|
|
137
177
|
}
|
|
138
178
|
|
|
139
179
|
/**
|
|
@@ -166,6 +206,9 @@ export function NavigationProvider({
|
|
|
166
206
|
initialTheme,
|
|
167
207
|
warmupEnabled,
|
|
168
208
|
version,
|
|
209
|
+
basename,
|
|
210
|
+
appShellRef,
|
|
211
|
+
nonce,
|
|
169
212
|
}: NavigationProviderProps): ReactNode {
|
|
170
213
|
// Track current payload for rendering (this triggers re-renders)
|
|
171
214
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -187,130 +230,100 @@ export function NavigationProvider({
|
|
|
187
230
|
await bridge.refresh();
|
|
188
231
|
}, []);
|
|
189
232
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
233
|
+
// basename/version are always read through a shell ref so the context value
|
|
234
|
+
// has a single shape. Both are set once: a supplied appShellRef is seeded
|
|
235
|
+
// from the init payload (a cross-app navigation reloads, so it is not swapped
|
|
236
|
+
// in-session), and the standalone fallback wraps the mount-time props.
|
|
237
|
+
const fallbackShellRef = useRef<AppShellRef | null>(null);
|
|
238
|
+
if (!fallbackShellRef.current) {
|
|
239
|
+
fallbackShellRef.current = createAppShellRef({ basename, version });
|
|
240
|
+
}
|
|
241
|
+
const shellRef = appShellRef ?? fallbackShellRef.current;
|
|
242
|
+
|
|
243
|
+
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
244
|
+
const value = {
|
|
193
245
|
store,
|
|
194
246
|
eventController,
|
|
195
247
|
navigate,
|
|
196
248
|
refresh,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
249
|
+
} as NavigationStoreContextValue;
|
|
250
|
+
Object.defineProperty(value, "basename", {
|
|
251
|
+
configurable: true,
|
|
252
|
+
enumerable: true,
|
|
253
|
+
get: () => shellRef.get().basename,
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(value, "version", {
|
|
256
|
+
configurable: true,
|
|
257
|
+
enumerable: true,
|
|
258
|
+
get: () => shellRef.get().version,
|
|
259
|
+
});
|
|
260
|
+
return value;
|
|
261
|
+
}, []);
|
|
201
262
|
|
|
202
|
-
// Connection warmup: keep TLS alive after idle periods.
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
263
|
+
// Connection warmup: keep TLS alive after idle periods. After 60s of no
|
|
264
|
+
// interaction the connection is marked cold; the next pointer/touch
|
|
265
|
+
// interaction or visibility change warms TLS via a HEAD request before the
|
|
266
|
+
// user clicks a link. State machine lives in connection-warmup.ts.
|
|
206
267
|
useEffect(() => {
|
|
207
268
|
if (!warmupEnabled) return;
|
|
208
|
-
|
|
209
|
-
const IDLE_TIMEOUT = 60_000;
|
|
210
|
-
const DEBOUNCE_DELAY = 150;
|
|
211
|
-
|
|
212
|
-
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
213
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
214
|
-
let isCold = false;
|
|
215
|
-
let warmupListenersAttached = false;
|
|
216
|
-
|
|
217
|
-
function sendWarmup() {
|
|
218
|
-
isCold = false;
|
|
219
|
-
fetch("/?_rsc_warmup", { method: "HEAD" }).catch(() => {});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function triggerWarmup() {
|
|
223
|
-
if (!isCold) return;
|
|
224
|
-
clearTimeout(debounceTimer);
|
|
225
|
-
debounceTimer = setTimeout(() => {
|
|
226
|
-
sendWarmup();
|
|
227
|
-
detachWarmupListeners();
|
|
228
|
-
resetIdleTimer();
|
|
229
|
-
}, DEBOUNCE_DELAY);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function onVisibilityChange() {
|
|
233
|
-
if (document.visibilityState === "visible" && isCold) {
|
|
234
|
-
triggerWarmup();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function attachWarmupListeners() {
|
|
239
|
-
if (warmupListenersAttached) return;
|
|
240
|
-
warmupListenersAttached = true;
|
|
241
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
242
|
-
document.addEventListener("mousemove", triggerWarmup, { once: true });
|
|
243
|
-
document.addEventListener("touchstart", triggerWarmup, { once: true });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function detachWarmupListeners() {
|
|
247
|
-
warmupListenersAttached = false;
|
|
248
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
249
|
-
document.removeEventListener("mousemove", triggerWarmup);
|
|
250
|
-
document.removeEventListener("touchstart", triggerWarmup);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function markCold() {
|
|
254
|
-
isCold = true;
|
|
255
|
-
attachWarmupListeners();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function resetIdleTimer() {
|
|
259
|
-
clearTimeout(idleTimer);
|
|
260
|
-
isCold = false;
|
|
261
|
-
idleTimer = setTimeout(markCold, IDLE_TIMEOUT);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Activity events that reset the idle timer
|
|
265
|
-
const activityEvents = [
|
|
266
|
-
"mousemove",
|
|
267
|
-
"keydown",
|
|
268
|
-
"touchstart",
|
|
269
|
-
"scroll",
|
|
270
|
-
] as const;
|
|
271
|
-
const activityOptions: AddEventListenerOptions = { passive: true };
|
|
272
|
-
|
|
273
|
-
for (const event of activityEvents) {
|
|
274
|
-
document.addEventListener(event, resetIdleTimer, activityOptions);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
resetIdleTimer();
|
|
278
|
-
|
|
279
|
-
return () => {
|
|
280
|
-
clearTimeout(idleTimer);
|
|
281
|
-
clearTimeout(debounceTimer);
|
|
282
|
-
detachWarmupListeners();
|
|
283
|
-
for (const event of activityEvents) {
|
|
284
|
-
document.removeEventListener(event, resetIdleTimer);
|
|
285
|
-
}
|
|
286
|
-
};
|
|
269
|
+
return startConnectionWarmup();
|
|
287
270
|
}, [warmupEnabled]);
|
|
288
271
|
|
|
289
|
-
// Cancel
|
|
290
|
-
//
|
|
272
|
+
// Cancel non-matching prefetches when navigation starts.
|
|
273
|
+
// Frees connections so the navigation fetch isn't competing with
|
|
274
|
+
// speculative prefetches. The prefetch matching the navigation target
|
|
275
|
+
// is kept alive so it can be reused via consumeInflightPrefetch.
|
|
291
276
|
useEffect(() => {
|
|
292
277
|
let wasIdle = true;
|
|
293
278
|
const unsub = eventController.subscribe(() => {
|
|
294
279
|
const state = eventController.getState();
|
|
295
280
|
const isIdle = state.state === "idle" && !state.isStreaming;
|
|
296
281
|
if (wasIdle && !isIdle) {
|
|
297
|
-
cancelAllPrefetches();
|
|
282
|
+
cancelAllPrefetches(state.pendingUrl);
|
|
298
283
|
}
|
|
299
284
|
wasIdle = isIdle;
|
|
300
285
|
});
|
|
301
286
|
return unsub;
|
|
302
287
|
}, [eventController]);
|
|
303
288
|
|
|
289
|
+
// Pending scroll action to apply after React commits
|
|
290
|
+
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
291
|
+
|
|
292
|
+
// Apply scroll after React commits the new content to the DOM
|
|
293
|
+
useLayoutEffect(() => {
|
|
294
|
+
const scrollAction = pendingScrollRef.current;
|
|
295
|
+
if (!scrollAction) return;
|
|
296
|
+
pendingScrollRef.current = undefined;
|
|
297
|
+
|
|
298
|
+
if (scrollAction.enabled === false) return;
|
|
299
|
+
|
|
300
|
+
handleNavigationEnd({
|
|
301
|
+
restore: scrollAction.restore,
|
|
302
|
+
scroll: scrollAction.enabled,
|
|
303
|
+
isStreaming: scrollAction.isStreaming,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
304
307
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
305
308
|
useEffect(() => {
|
|
306
309
|
const unsubscribe = store.onUpdate((update) => {
|
|
310
|
+
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
311
|
+
// after React commits this state update to the DOM.
|
|
312
|
+
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
313
|
+
// so server actions or error updates don't accidentally replay old scroll.
|
|
314
|
+
pendingScrollRef.current = update.scroll;
|
|
315
|
+
|
|
307
316
|
setPayload({
|
|
308
317
|
root: update.root,
|
|
309
318
|
metadata: update.metadata,
|
|
310
319
|
});
|
|
311
320
|
|
|
312
|
-
// Update route params
|
|
313
|
-
|
|
321
|
+
// Update route params. Only reset when the server actually sends a params
|
|
322
|
+
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
323
|
+
// responses that omitted params). Explicit `{}` still clears correctly.
|
|
324
|
+
if (update.metadata.params !== undefined) {
|
|
325
|
+
eventController.setParams(update.metadata.params);
|
|
326
|
+
}
|
|
314
327
|
|
|
315
328
|
// Update handle data progressively as it streams in
|
|
316
329
|
if (update.metadata.handles) {
|
|
@@ -323,24 +336,20 @@ export function NavigationProvider({
|
|
|
323
336
|
store,
|
|
324
337
|
matched: update.metadata.matched,
|
|
325
338
|
isPartial: update.metadata.isPartial,
|
|
339
|
+
resolvedIds: update.metadata.resolvedIds,
|
|
326
340
|
historyKey,
|
|
327
341
|
}).catch((err) =>
|
|
328
342
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
329
343
|
);
|
|
330
|
-
} else if (update.metadata.cachedHandleData) {
|
|
331
|
-
// For back/forward navigation from cache, restore the cached handleData
|
|
332
|
-
// This restores breadcrumbs to the exact state they were when the page was cached
|
|
333
|
-
eventController.setHandleData(
|
|
334
|
-
update.metadata.cachedHandleData,
|
|
335
|
-
update.metadata.matched,
|
|
336
|
-
false, // full replace - restore entire cached state
|
|
337
|
-
);
|
|
338
344
|
} else if (update.metadata.matched) {
|
|
339
|
-
//
|
|
345
|
+
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
346
|
+
// partial cleanup of segments no longer matched.
|
|
347
|
+
const cached = update.metadata.cachedHandleData;
|
|
340
348
|
eventController.setHandleData(
|
|
341
|
-
{},
|
|
349
|
+
cached ?? {},
|
|
342
350
|
update.metadata.matched,
|
|
343
|
-
|
|
351
|
+
cached === undefined,
|
|
352
|
+
cached === undefined ? update.metadata.resolvedIds : undefined,
|
|
344
353
|
);
|
|
345
354
|
}
|
|
346
355
|
});
|
|
@@ -353,7 +362,8 @@ export function NavigationProvider({
|
|
|
353
362
|
payload.root instanceof Promise ? use(payload.root) : payload.root;
|
|
354
363
|
|
|
355
364
|
// Wrap content in RootErrorBoundary to catch:
|
|
356
|
-
// 1. Errors from
|
|
365
|
+
// 1. Errors from RenderErrorThrower (network failures and unprocessable
|
|
366
|
+
// navigation responses, routed here by the navigation bridge)
|
|
357
367
|
// 2. Client component errors that occur before/outside the segment tree's error boundary
|
|
358
368
|
// 3. Errors during promise resolution or navigation state updates
|
|
359
369
|
// This acts as a safety net - the segment tree has its own RootErrorBoundary that
|
|
@@ -362,7 +372,11 @@ export function NavigationProvider({
|
|
|
362
372
|
// Build the content tree
|
|
363
373
|
let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
|
|
364
374
|
|
|
365
|
-
// Wrap with ThemeProvider when theme is enabled
|
|
375
|
+
// Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
|
|
376
|
+
// document-lifetime: its config comes from the initial load and persists for
|
|
377
|
+
// the session. It sits above the segment tree and is not remounted in-session;
|
|
378
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the target
|
|
379
|
+
// app's theme config takes effect on its own load.
|
|
366
380
|
if (themeConfig) {
|
|
367
381
|
content = (
|
|
368
382
|
<ThemeProvider config={themeConfig} initialTheme={initialTheme}>
|
|
@@ -373,9 +387,10 @@ export function NavigationProvider({
|
|
|
373
387
|
|
|
374
388
|
// Match SSR tree shape: NonceContext.Provider is always present so
|
|
375
389
|
// hydration sees the same component tree. Value is undefined on the
|
|
376
|
-
// client — CSP nonces are a server-side HTML concern
|
|
390
|
+
// client — CSP nonces are a server-side HTML concern — unless a test
|
|
391
|
+
// harness seeded one via the `nonce` prop.
|
|
377
392
|
content = (
|
|
378
|
-
<NonceContext.Provider value={
|
|
393
|
+
<NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
|
|
379
394
|
);
|
|
380
395
|
|
|
381
396
|
return (
|
|
@@ -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: {
|
|
@@ -43,10 +43,15 @@ export interface NavigationStoreContextValue {
|
|
|
43
43
|
refresh: () => Promise<void>;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* App version from server payload
|
|
47
|
-
* Used in prefetch requests for version mismatch detection.
|
|
46
|
+
* App version from the initial server payload.
|
|
48
47
|
*/
|
|
49
48
|
version: string | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
52
|
+
* Used by Link and useRouter() to auto-prefix app-local paths.
|
|
53
|
+
*/
|
|
54
|
+
basename: string | undefined;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|