@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7
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 +8 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3373 -1176
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- 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 +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +235 -9
- 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 +77 -7
- package/skills/prerender/SKILL.md +172 -125
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +70 -5
- package/skills/router-setup/SKILL.md +65 -8
- 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 +130 -0
- 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 +122 -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 +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -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/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -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/navigation-bridge.ts +131 -30
- package/src/browser/navigation-client.ts +186 -100
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +165 -112
- package/src/browser/prefetch/cache.ts +205 -62
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +295 -119
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- 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 +99 -8
- package/src/browser/rsc-router.tsx +114 -24
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -72
- package/src/browser/types.ts +102 -12
- 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 +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- 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 +260 -94
- 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 +224 -41
- package/src/cache/cache-scope.ts +188 -82
- 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 +1989 -378
- 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 +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- 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 +18 -6
- 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 +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- 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 -2
- 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 +99 -13
- package/src/index.ts +139 -19
- 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 +37 -41
- 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 +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +143 -69
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- 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 +44 -23
- package/src/router/handler-context.ts +52 -31
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +268 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -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 +40 -24
- package/src/router/match-middleware/cache-lookup.ts +170 -276
- package/src/router/match-middleware/cache-store.ts +64 -52
- 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 +266 -169
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +162 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +351 -321
- 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 +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -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 +163 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +157 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +655 -529
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +122 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +131 -14
- 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 +109 -74
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -115
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +29 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +236 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +224 -52
- 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 +38 -46
- package/src/server/request-context.ts +401 -173
- package/src/ssr/index.tsx +24 -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 +105 -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 +357 -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/run-transition-when.ts +164 -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 +0 -7
- 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 +125 -71
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- 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 -19
- 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 +214 -132
- 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 +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +178 -5
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- 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 +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 +64 -170
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +118 -114
- package/src/vite/router-discovery.ts +941 -142
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- 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/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 +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -28,6 +28,60 @@ 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 { startConnectionWarmup } from "../connection-warmup.js";
|
|
33
|
+
import { debugLog } from "../logging.js";
|
|
34
|
+
import { cloneHandleData } from "../navigation-store.js";
|
|
35
|
+
import { collectHandleData } from "../../handle.js";
|
|
36
|
+
import { Meta } from "../../handles/meta.js";
|
|
37
|
+
import type { MetaDescriptor } from "../../router/types.js";
|
|
38
|
+
import {
|
|
39
|
+
HEAD_RESOLVE_HANDLE_NAMES,
|
|
40
|
+
hasDeferredHandleValue,
|
|
41
|
+
resolveDeferredHandleValues,
|
|
42
|
+
} from "./deferred-handle-resolution.js";
|
|
43
|
+
|
|
44
|
+
/** Meta handle-name key. Meta is the only head-placed handle whose consumer
|
|
45
|
+
* use()s a deferred value above the route <Suspense>, so it must be resolved in
|
|
46
|
+
* the store before apply; every other handle keeps the promise contract. */
|
|
47
|
+
const META = "__rsc_router_meta__";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Carry the previous page's COLLECTED Meta forward so the title is kept (no
|
|
51
|
+
* blank) while a deferred Meta resolves on a soft navigation.
|
|
52
|
+
*
|
|
53
|
+
* Why a carry-forward and not just preserving the previous Meta data: handle
|
|
54
|
+
* collection (useHandle/MetaTags) is driven by the event controller's
|
|
55
|
+
* `segmentOrder`, which becomes the NEW route's order so the synchronous
|
|
56
|
+
* breadcrumbs render immediately. The previous route's title lives under a
|
|
57
|
+
* segment that is NOT in the new order, so it would stop being collected — the
|
|
58
|
+
* title would fall back to the layout default. Re-keying the previous COLLECTED
|
|
59
|
+
* descriptors under a segment that IS in the new order keeps them visible.
|
|
60
|
+
*
|
|
61
|
+
* Title descriptors are wrapped as `{ title: { absolute } }` so re-collection
|
|
62
|
+
* under a (possibly template-bearing) new layout does not re-apply a title
|
|
63
|
+
* template to an already-final title. Promise and default (charSet/viewport)
|
|
64
|
+
* descriptors are dropped: Promise ones would suspend MetaTags, and the defaults
|
|
65
|
+
* are re-added by collectMeta.
|
|
66
|
+
*/
|
|
67
|
+
function carriedPreviousMeta(prev: MetaDescriptor[]): MetaDescriptor[] {
|
|
68
|
+
const out: MetaDescriptor[] = [];
|
|
69
|
+
for (const d of prev) {
|
|
70
|
+
if (d && typeof (d as { then?: unknown }).then === "function") continue;
|
|
71
|
+
const base = d as Exclude<MetaDescriptor, Promise<unknown>>;
|
|
72
|
+
if ("charSet" in base) continue;
|
|
73
|
+
if ("name" in base && (base as { name?: unknown }).name === "viewport") {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if ("title" in base) {
|
|
77
|
+
const t = (base as { title: unknown }).title;
|
|
78
|
+
out.push({ title: { absolute: typeof t === "string" ? t : String(t) } });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out.push(base);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
31
85
|
|
|
32
86
|
/**
|
|
33
87
|
* Process handles from an async generator, updating the event controller
|
|
@@ -46,10 +100,35 @@ async function processHandles(
|
|
|
46
100
|
store: NavigationStore;
|
|
47
101
|
matched?: string[];
|
|
48
102
|
isPartial?: boolean;
|
|
103
|
+
/** Server's `resolvedIds`: every segment re-resolved this request,
|
|
104
|
+
* including null-component ones excluded from `diff`/`segments`.
|
|
105
|
+
* Drives cleanup of stale handle buckets when a re-resolved segment
|
|
106
|
+
* pushed nothing. */
|
|
107
|
+
resolvedIds?: string[];
|
|
49
108
|
historyKey: string;
|
|
50
109
|
},
|
|
51
110
|
): Promise<void> {
|
|
52
|
-
const {
|
|
111
|
+
const {
|
|
112
|
+
eventController,
|
|
113
|
+
store,
|
|
114
|
+
matched,
|
|
115
|
+
isPartial,
|
|
116
|
+
resolvedIds,
|
|
117
|
+
historyKey,
|
|
118
|
+
} = opts;
|
|
119
|
+
|
|
120
|
+
// This nav's instance token, captured before any await — processHandles runs
|
|
121
|
+
// right after its own commit, so this is that commit's token. generateHistoryKey
|
|
122
|
+
// is URL-only, so an A->B->A revisit reuses the key; the token lets a late
|
|
123
|
+
// resolution tell its own visit apart from a newer same-URL visit, so a stale
|
|
124
|
+
// nav can never clobber a fresher one's live state or cache (P1).
|
|
125
|
+
const myInstance = store.getNavInstance();
|
|
126
|
+
|
|
127
|
+
// True while this nav still owns the live page: same history key AND the most
|
|
128
|
+
// recent commit is still ours (no newer nav has committed since).
|
|
129
|
+
const stillLive = (): boolean =>
|
|
130
|
+
historyKey === store.getHistoryKey() &&
|
|
131
|
+
myInstance === store.getNavInstance();
|
|
53
132
|
|
|
54
133
|
let yieldCount = 0;
|
|
55
134
|
for await (const handleData of handlesGenerator) {
|
|
@@ -57,14 +136,139 @@ async function processHandles(
|
|
|
57
136
|
// This prevents handle data from cancelled navigations polluting
|
|
58
137
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
59
138
|
if (historyKey !== store.getHistoryKey()) {
|
|
60
|
-
|
|
139
|
+
debugLog(
|
|
61
140
|
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
62
141
|
);
|
|
63
142
|
return;
|
|
64
143
|
}
|
|
65
144
|
|
|
66
145
|
yieldCount++;
|
|
67
|
-
|
|
146
|
+
|
|
147
|
+
// Resolve ONLY Meta in the store before applying. Meta is the sole
|
|
148
|
+
// head-placed handle whose consumer use()s a deferred value above the route
|
|
149
|
+
// <Suspense>; an uncontained suspension there would revert the just-committed
|
|
150
|
+
// route and hide its loading fallback. Every other handle (Breadcrumbs,
|
|
151
|
+
// custom handles) keeps the DeferredHandleEntry contract: its deferred values
|
|
152
|
+
// reach the consumer AS A PROMISE and are narrowed via isThenable(). So sync
|
|
153
|
+
// handles AND non-Meta deferred promises apply/stream through immediately —
|
|
154
|
+
// only Meta is held back and swapped in once resolved.
|
|
155
|
+
const metaDeferred = hasDeferredHandleValue(
|
|
156
|
+
handleData,
|
|
157
|
+
HEAD_RESOLVE_HANDLE_NAMES,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Apply now. The non-deferred-Meta case applies the whole snapshot in one
|
|
161
|
+
// call (Meta included). When Meta IS deferred, replace the deferred Meta with
|
|
162
|
+
// the previous page's COLLECTED Meta (stale-while-revalidate — never a blank
|
|
163
|
+
// title) keyed under one of the NEW route's Meta segments, so it stays
|
|
164
|
+
// collected under the new segment order while the synchronous and non-Meta
|
|
165
|
+
// deferred handles update with normal cleanup. The resolved Meta is swapped
|
|
166
|
+
// in by the partial merge below.
|
|
167
|
+
if (metaDeferred) {
|
|
168
|
+
const immediate: HandleData = { ...handleData };
|
|
169
|
+
const metaSegments = handleData[META] ?? {};
|
|
170
|
+
// Anchor: the last new Meta segment in matched order (collected after the
|
|
171
|
+
// shared layout, so its carried title wins). Falls back to any new Meta
|
|
172
|
+
// segment if matched ordering does not surface one.
|
|
173
|
+
const metaSegmentIds = Object.keys(metaSegments);
|
|
174
|
+
const ordered = (matched ?? []).filter((id) =>
|
|
175
|
+
metaSegmentIds.includes(id),
|
|
176
|
+
);
|
|
177
|
+
const anchor = ordered.at(-1) ?? metaSegmentIds.at(-1);
|
|
178
|
+
|
|
179
|
+
const prevState = eventController.getHandleState();
|
|
180
|
+
const prevCollected = collectHandleData(
|
|
181
|
+
Meta,
|
|
182
|
+
prevState.data,
|
|
183
|
+
prevState.segmentOrder,
|
|
184
|
+
) as MetaDescriptor[];
|
|
185
|
+
const carried = carriedPreviousMeta(prevCollected);
|
|
186
|
+
|
|
187
|
+
if (anchor && carried.length > 0) {
|
|
188
|
+
immediate[META] = { [anchor]: carried };
|
|
189
|
+
} else {
|
|
190
|
+
// No previous Meta to carry and/or no anchor: leave Meta unset until it
|
|
191
|
+
// resolves (the documented no-previous-Meta behavior).
|
|
192
|
+
delete immediate[META];
|
|
193
|
+
}
|
|
194
|
+
eventController.setHandleData(immediate, matched, isPartial, resolvedIds);
|
|
195
|
+
} else {
|
|
196
|
+
eventController.setHandleData(
|
|
197
|
+
handleData,
|
|
198
|
+
matched,
|
|
199
|
+
isPartial,
|
|
200
|
+
resolvedIds,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Snapshot of the nav's full applied handle state (sync handles, non-Meta
|
|
205
|
+
// deferred promises, and — when Meta is deferred — the carried previous Meta).
|
|
206
|
+
// Captured AFTER applying so it reflects what is actually on screen now.
|
|
207
|
+
const baseSnapshot = cloneHandleData(eventController.getHandleState().data);
|
|
208
|
+
|
|
209
|
+
if (!metaDeferred) {
|
|
210
|
+
// Non-deferred: the applied snapshot is final. Keep the cache in sync and
|
|
211
|
+
// fresh. The token guard stops a stale same-URL nav writing a newer entry.
|
|
212
|
+
if (store.getCacheEntryInstance(historyKey) === myInstance) {
|
|
213
|
+
store.updateCacheHandleData(historyKey, baseSnapshot, false);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Meta is deferred-pending. The applied snapshot carries the PREVIOUS page's
|
|
219
|
+
// Meta (or none), not this route's final title, so the cache entry must NOT
|
|
220
|
+
// be served as fresh on a popstate return. Mark it STALE and handlesPending
|
|
221
|
+
// (token-guarded). This is the P1 fix: the deferred Meta is a SERVER-side
|
|
222
|
+
// promise streamed via Flight, so a navigate-away ABORTS the stream and the
|
|
223
|
+
// client's deferred-Meta promise never resolves — the .then below never
|
|
224
|
+
// fires. stale makes a popstate return revalidate; handlesPending makes that
|
|
225
|
+
// revalidation a FULL re-render (no client segment IDs) so the server
|
|
226
|
+
// re-streams the handles. A diff-only revalidation would omit the unchanged
|
|
227
|
+
// segments' handles and the deferred Meta would never land — see the
|
|
228
|
+
// segmentIds branch in navigation-bridge.ts.
|
|
229
|
+
if (store.getCacheEntryInstance(historyKey) === myInstance) {
|
|
230
|
+
store.updateCacheHandleData(historyKey, baseSnapshot, true, true);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Resolve Meta late, then swap it in. The swap is a PARTIAL merge with
|
|
234
|
+
// resolvedIds=undefined so the stale-clear loop (which scans all handle
|
|
235
|
+
// names under resolvedIds) cannot wipe the non-Meta buckets we already
|
|
236
|
+
// applied. When the deferred Meta DOES resolve while this nav still owns the
|
|
237
|
+
// entry (no navigate-away abort), write the resolved handle data and clear
|
|
238
|
+
// stale + handlesPending — the entry is now complete, so a popstate return
|
|
239
|
+
// serves it without revalidating.
|
|
240
|
+
//
|
|
241
|
+
// Order-safety: each stream yield is a full cumulative snapshot and a
|
|
242
|
+
// segment's handle array is atomic, so concurrent Meta resolutions of
|
|
243
|
+
// different yields write identical per-segment arrays or touch disjoint
|
|
244
|
+
// segments — neither can clobber the other.
|
|
245
|
+
void resolveDeferredHandleValues(
|
|
246
|
+
handleData,
|
|
247
|
+
HEAD_RESOLVE_HANDLE_NAMES,
|
|
248
|
+
).then((resolved) => {
|
|
249
|
+
const cacheValue = { ...baseSnapshot, [META]: resolved[META] };
|
|
250
|
+
if (stillLive()) {
|
|
251
|
+
// Still on the live page: swap Meta in and refresh the cache as fresh.
|
|
252
|
+
eventController.setHandleData(
|
|
253
|
+
{ [META]: resolved[META] },
|
|
254
|
+
matched,
|
|
255
|
+
true,
|
|
256
|
+
undefined,
|
|
257
|
+
);
|
|
258
|
+
store.updateCacheHandleData(
|
|
259
|
+
historyKey,
|
|
260
|
+
eventController.getHandleState().data,
|
|
261
|
+
false,
|
|
262
|
+
false,
|
|
263
|
+
);
|
|
264
|
+
} else if (store.getCacheEntryInstance(historyKey) === myInstance) {
|
|
265
|
+
// Navigated away, but THIS nav still owns the target cache entry: write
|
|
266
|
+
// the resolved data and clear stale + handlesPending so a popstate return
|
|
267
|
+
// is fresh.
|
|
268
|
+
store.updateCacheHandleData(historyKey, cacheValue, false, false);
|
|
269
|
+
}
|
|
270
|
+
// else: a newer nav to the same URL superseded us — do nothing.
|
|
271
|
+
});
|
|
68
272
|
}
|
|
69
273
|
|
|
70
274
|
// Check again before final updates
|
|
@@ -72,19 +276,19 @@ async function processHandles(
|
|
|
72
276
|
return;
|
|
73
277
|
}
|
|
74
278
|
|
|
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.
|
|
279
|
+
// For partial updates where the generator yielded nothing (every
|
|
280
|
+
// re-resolved handler pushed nothing), still call setHandleData so the
|
|
281
|
+
// cleanup pass can clear out stale buckets for those segments.
|
|
79
282
|
if (yieldCount === 0 && matched) {
|
|
80
|
-
eventController.setHandleData({}, matched, true);
|
|
283
|
+
eventController.setHandleData({}, matched, true, resolvedIds);
|
|
81
284
|
}
|
|
82
285
|
|
|
83
286
|
// After handles processing completes, update the cache's handleData.
|
|
84
287
|
// This fixes a race condition where commit() caches stale handleData before
|
|
85
288
|
// the async handles processing completes.
|
|
86
|
-
// Only update if we're still on the same page
|
|
87
|
-
|
|
289
|
+
// Only update if we're still on the same page AND this is still the live nav
|
|
290
|
+
// (the token guard stops a stale same-URL nav writing a newer nav's state).
|
|
291
|
+
if (stillLive()) {
|
|
88
292
|
const finalHandleData = eventController.getHandleState().data;
|
|
89
293
|
store.updateCacheHandleData(historyKey, finalHandleData);
|
|
90
294
|
}
|
|
@@ -133,10 +337,33 @@ export interface NavigationProviderProps {
|
|
|
133
337
|
warmupEnabled?: boolean;
|
|
134
338
|
|
|
135
339
|
/**
|
|
136
|
-
* App version from server payload
|
|
137
|
-
*
|
|
340
|
+
* App version from server payload.
|
|
341
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
138
342
|
*/
|
|
139
343
|
version?: string;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
347
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
348
|
+
*/
|
|
349
|
+
basename?: string;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* App-shell ref. When provided, the context's `basename` and `version` are
|
|
353
|
+
* read through it (live getters) so they don't close over a stale snapshot or
|
|
354
|
+
* invalidate the memoized context value. The shell is set once at init and is
|
|
355
|
+
* not swapped within a session — a cross-app navigation is a full document
|
|
356
|
+
* load (X-RSC-Reload), so the target app establishes its own shell on load.
|
|
357
|
+
*/
|
|
358
|
+
appShellRef?: AppShellRef;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* CSP nonce to expose via NonceContext. Production leaves this undefined — the
|
|
362
|
+
* browser has no nonce (it is a server-side HTML concern), and SSR provides the
|
|
363
|
+
* nonce through its own NonceContext.Provider. Test harnesses (renderRoute) set
|
|
364
|
+
* it to seed a nonce so components calling useNonce() can be exercised.
|
|
365
|
+
*/
|
|
366
|
+
nonce?: string;
|
|
140
367
|
}
|
|
141
368
|
|
|
142
369
|
/**
|
|
@@ -169,6 +396,9 @@ export function NavigationProvider({
|
|
|
169
396
|
initialTheme,
|
|
170
397
|
warmupEnabled,
|
|
171
398
|
version,
|
|
399
|
+
basename,
|
|
400
|
+
appShellRef,
|
|
401
|
+
nonce,
|
|
172
402
|
}: NavigationProviderProps): ReactNode {
|
|
173
403
|
// Track current payload for rendering (this triggers re-renders)
|
|
174
404
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -190,103 +420,43 @@ export function NavigationProvider({
|
|
|
190
420
|
await bridge.refresh();
|
|
191
421
|
}, []);
|
|
192
422
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
423
|
+
// basename/version are always read through a shell ref so the context value
|
|
424
|
+
// has a single shape. Both are set once: a supplied appShellRef is seeded
|
|
425
|
+
// from the init payload (a cross-app navigation reloads, so it is not swapped
|
|
426
|
+
// in-session), and the standalone fallback wraps the mount-time props.
|
|
427
|
+
const fallbackShellRef = useRef<AppShellRef | null>(null);
|
|
428
|
+
if (!fallbackShellRef.current) {
|
|
429
|
+
fallbackShellRef.current = createAppShellRef({ basename, version });
|
|
430
|
+
}
|
|
431
|
+
const shellRef = appShellRef ?? fallbackShellRef.current;
|
|
432
|
+
|
|
433
|
+
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
434
|
+
const value = {
|
|
196
435
|
store,
|
|
197
436
|
eventController,
|
|
198
437
|
navigate,
|
|
199
438
|
refresh,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
439
|
+
} as NavigationStoreContextValue;
|
|
440
|
+
Object.defineProperty(value, "basename", {
|
|
441
|
+
configurable: true,
|
|
442
|
+
enumerable: true,
|
|
443
|
+
get: () => shellRef.get().basename,
|
|
444
|
+
});
|
|
445
|
+
Object.defineProperty(value, "version", {
|
|
446
|
+
configurable: true,
|
|
447
|
+
enumerable: true,
|
|
448
|
+
get: () => shellRef.get().version,
|
|
449
|
+
});
|
|
450
|
+
return value;
|
|
451
|
+
}, []);
|
|
204
452
|
|
|
205
|
-
// Connection warmup: keep TLS alive after idle periods.
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
453
|
+
// Connection warmup: keep TLS alive after idle periods. After 60s of no
|
|
454
|
+
// interaction the connection is marked cold; the next pointer/touch
|
|
455
|
+
// interaction or visibility change warms TLS via a HEAD request before the
|
|
456
|
+
// user clicks a link. State machine lives in connection-warmup.ts.
|
|
209
457
|
useEffect(() => {
|
|
210
458
|
if (!warmupEnabled) return;
|
|
211
|
-
|
|
212
|
-
const IDLE_TIMEOUT = 60_000;
|
|
213
|
-
const DEBOUNCE_DELAY = 150;
|
|
214
|
-
|
|
215
|
-
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
216
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
217
|
-
let isCold = false;
|
|
218
|
-
let warmupListenersAttached = false;
|
|
219
|
-
|
|
220
|
-
function sendWarmup() {
|
|
221
|
-
isCold = false;
|
|
222
|
-
fetch("/?_rsc_warmup", { method: "HEAD" }).catch(() => {});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function triggerWarmup() {
|
|
226
|
-
if (!isCold) return;
|
|
227
|
-
clearTimeout(debounceTimer);
|
|
228
|
-
debounceTimer = setTimeout(() => {
|
|
229
|
-
sendWarmup();
|
|
230
|
-
detachWarmupListeners();
|
|
231
|
-
resetIdleTimer();
|
|
232
|
-
}, DEBOUNCE_DELAY);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function onVisibilityChange() {
|
|
236
|
-
if (document.visibilityState === "visible" && isCold) {
|
|
237
|
-
triggerWarmup();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function attachWarmupListeners() {
|
|
242
|
-
if (warmupListenersAttached) return;
|
|
243
|
-
warmupListenersAttached = true;
|
|
244
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
245
|
-
document.addEventListener("mousemove", triggerWarmup, { once: true });
|
|
246
|
-
document.addEventListener("touchstart", triggerWarmup, { once: true });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function detachWarmupListeners() {
|
|
250
|
-
warmupListenersAttached = false;
|
|
251
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
252
|
-
document.removeEventListener("mousemove", triggerWarmup);
|
|
253
|
-
document.removeEventListener("touchstart", triggerWarmup);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function markCold() {
|
|
257
|
-
isCold = true;
|
|
258
|
-
attachWarmupListeners();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function resetIdleTimer() {
|
|
262
|
-
clearTimeout(idleTimer);
|
|
263
|
-
isCold = false;
|
|
264
|
-
idleTimer = setTimeout(markCold, IDLE_TIMEOUT);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Activity events that reset the idle timer
|
|
268
|
-
const activityEvents = [
|
|
269
|
-
"mousemove",
|
|
270
|
-
"keydown",
|
|
271
|
-
"touchstart",
|
|
272
|
-
"scroll",
|
|
273
|
-
] as const;
|
|
274
|
-
const activityOptions: AddEventListenerOptions = { passive: true };
|
|
275
|
-
|
|
276
|
-
for (const event of activityEvents) {
|
|
277
|
-
document.addEventListener(event, resetIdleTimer, activityOptions);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
resetIdleTimer();
|
|
281
|
-
|
|
282
|
-
return () => {
|
|
283
|
-
clearTimeout(idleTimer);
|
|
284
|
-
clearTimeout(debounceTimer);
|
|
285
|
-
detachWarmupListeners();
|
|
286
|
-
for (const event of activityEvents) {
|
|
287
|
-
document.removeEventListener(event, resetIdleTimer);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
459
|
+
return startConnectionWarmup();
|
|
290
460
|
}, [warmupEnabled]);
|
|
291
461
|
|
|
292
462
|
// Cancel non-matching prefetches when navigation starts.
|
|
@@ -338,8 +508,12 @@ export function NavigationProvider({
|
|
|
338
508
|
metadata: update.metadata,
|
|
339
509
|
});
|
|
340
510
|
|
|
341
|
-
// Update route params
|
|
342
|
-
|
|
511
|
+
// Update route params. Only reset when the server actually sends a params
|
|
512
|
+
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
513
|
+
// responses that omitted params). Explicit `{}` still clears correctly.
|
|
514
|
+
if (update.metadata.params !== undefined) {
|
|
515
|
+
eventController.setParams(update.metadata.params);
|
|
516
|
+
}
|
|
343
517
|
|
|
344
518
|
// Update handle data progressively as it streams in
|
|
345
519
|
if (update.metadata.handles) {
|
|
@@ -352,24 +526,20 @@ export function NavigationProvider({
|
|
|
352
526
|
store,
|
|
353
527
|
matched: update.metadata.matched,
|
|
354
528
|
isPartial: update.metadata.isPartial,
|
|
529
|
+
resolvedIds: update.metadata.resolvedIds,
|
|
355
530
|
historyKey,
|
|
356
531
|
}).catch((err) =>
|
|
357
532
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
358
533
|
);
|
|
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
534
|
} else if (update.metadata.matched) {
|
|
368
|
-
//
|
|
535
|
+
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
536
|
+
// partial cleanup of segments no longer matched.
|
|
537
|
+
const cached = update.metadata.cachedHandleData;
|
|
369
538
|
eventController.setHandleData(
|
|
370
|
-
{},
|
|
539
|
+
cached ?? {},
|
|
371
540
|
update.metadata.matched,
|
|
372
|
-
|
|
541
|
+
cached === undefined,
|
|
542
|
+
cached === undefined ? update.metadata.resolvedIds : undefined,
|
|
373
543
|
);
|
|
374
544
|
}
|
|
375
545
|
});
|
|
@@ -382,7 +552,8 @@ export function NavigationProvider({
|
|
|
382
552
|
payload.root instanceof Promise ? use(payload.root) : payload.root;
|
|
383
553
|
|
|
384
554
|
// Wrap content in RootErrorBoundary to catch:
|
|
385
|
-
// 1. Errors from
|
|
555
|
+
// 1. Errors from RenderErrorThrower (network failures and unprocessable
|
|
556
|
+
// navigation responses, routed here by the navigation bridge)
|
|
386
557
|
// 2. Client component errors that occur before/outside the segment tree's error boundary
|
|
387
558
|
// 3. Errors during promise resolution or navigation state updates
|
|
388
559
|
// This acts as a safety net - the segment tree has its own RootErrorBoundary that
|
|
@@ -391,7 +562,11 @@ export function NavigationProvider({
|
|
|
391
562
|
// Build the content tree
|
|
392
563
|
let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
|
|
393
564
|
|
|
394
|
-
// Wrap with ThemeProvider when theme is enabled
|
|
565
|
+
// Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
|
|
566
|
+
// document-lifetime: its config comes from the initial load and persists for
|
|
567
|
+
// the session. It sits above the segment tree and is not remounted in-session;
|
|
568
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the target
|
|
569
|
+
// app's theme config takes effect on its own load.
|
|
395
570
|
if (themeConfig) {
|
|
396
571
|
content = (
|
|
397
572
|
<ThemeProvider config={themeConfig} initialTheme={initialTheme}>
|
|
@@ -402,9 +577,10 @@ export function NavigationProvider({
|
|
|
402
577
|
|
|
403
578
|
// Match SSR tree shape: NonceContext.Provider is always present so
|
|
404
579
|
// hydration sees the same component tree. Value is undefined on the
|
|
405
|
-
// client — CSP nonces are a server-side HTML concern
|
|
580
|
+
// client — CSP nonces are a server-side HTML concern — unless a test
|
|
581
|
+
// harness seeded one via the `nonce` prop.
|
|
406
582
|
content = (
|
|
407
|
-
<NonceContext.Provider value={
|
|
583
|
+
<NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
|
|
408
584
|
);
|
|
409
585
|
|
|
410
586
|
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
|
/**
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { HandleData } from "../types.js";
|
|
2
|
+
import { isThenable } from "../../handles/is-thenable.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The set of handle names whose deferred (Promise) values MUST be resolved in
|
|
6
|
+
* the store BEFORE the snapshot is applied during client navigation.
|
|
7
|
+
*
|
|
8
|
+
* The boundary: a handle belongs here only if its consumer `use()`s a promise in
|
|
9
|
+
* <head>, above the route's <Suspense>. Suspending there would revert the
|
|
10
|
+
* just-committed route and hide its loading fallback. Today that is Meta alone
|
|
11
|
+
* (MetaTags lives in <head> and use()s deferred descriptors). Every OTHER handle
|
|
12
|
+
* keeps the public DeferredHandleEntry contract: its deferred value reaches the
|
|
13
|
+
* consumer AS A PROMISE during soft navigation, narrowed via isThenable().
|
|
14
|
+
*
|
|
15
|
+
* If a future head-placed handle starts use()-ing promises, add its name here.
|
|
16
|
+
*/
|
|
17
|
+
export const HEAD_RESOLVE_HANDLE_NAMES: readonly string[] = [
|
|
18
|
+
"__rsc_router_meta__",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when a handle value in this snapshot is a deferred (Promise) value.
|
|
23
|
+
*
|
|
24
|
+
* When `onlyHandleNames` is given, only those handle buckets are considered;
|
|
25
|
+
* deferred values under any other handle are ignored (they pass through to the
|
|
26
|
+
* consumer as promises, by contract).
|
|
27
|
+
*/
|
|
28
|
+
export function hasDeferredHandleValue(
|
|
29
|
+
data: HandleData,
|
|
30
|
+
onlyHandleNames?: readonly string[],
|
|
31
|
+
): boolean {
|
|
32
|
+
const scope = onlyHandleNames ? new Set(onlyHandleNames) : null;
|
|
33
|
+
for (const [handleName, segments] of Object.entries(data)) {
|
|
34
|
+
if (scope && !scope.has(handleName)) continue;
|
|
35
|
+
for (const values of Object.values(segments)) {
|
|
36
|
+
if (values.some(isThenable)) return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Snapshot with deferred (Promise) values awaited; a rejected deferred is
|
|
44
|
+
* dropped (it contributes nothing), mirroring the render-side REJECTED_META.
|
|
45
|
+
* Promise.allSettled treats non-promise values as already-fulfilled, so plain
|
|
46
|
+
* values pass through unchanged.
|
|
47
|
+
*
|
|
48
|
+
* When `onlyHandleNames` is given, ONLY those handle buckets are resolved; every
|
|
49
|
+
* other bucket is copied through by reference (its deferred values keep their
|
|
50
|
+
* promise identity so the consumer can narrow them).
|
|
51
|
+
*/
|
|
52
|
+
export async function resolveDeferredHandleValues(
|
|
53
|
+
data: HandleData,
|
|
54
|
+
onlyHandleNames?: readonly string[],
|
|
55
|
+
): Promise<HandleData> {
|
|
56
|
+
const scope = onlyHandleNames ? new Set(onlyHandleNames) : null;
|
|
57
|
+
const out: HandleData = {};
|
|
58
|
+
await Promise.all(
|
|
59
|
+
Object.entries(data).flatMap(([handleName, segments]) => {
|
|
60
|
+
// Out-of-scope buckets pass through untouched (promise identity kept).
|
|
61
|
+
if (scope && !scope.has(handleName)) {
|
|
62
|
+
out[handleName] = segments;
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
out[handleName] = {};
|
|
66
|
+
return Object.entries(segments).map(async ([segmentId, values]) => {
|
|
67
|
+
const settled = await Promise.allSettled(values);
|
|
68
|
+
out[handleName][segmentId] = settled
|
|
69
|
+
.filter((r) => r.status === "fulfilled")
|
|
70
|
+
.map((r) => (r as PromiseFulfilledResult<unknown>).value);
|
|
71
|
+
});
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
return out;
|
|
75
|
+
}
|