@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- 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 +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -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 +0 -65
- 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/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 +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- 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 +17 -14
- 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 +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- 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 +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- 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 +117 -23
- 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 +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- 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 +36 -61
- 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 +31 -23
- 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 +96 -12
- package/src/index.ts +94 -14
- 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 +32 -37
- package/src/prerender.ts +61 -6
- 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 +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- 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 +159 -285
- 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 +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -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 +272 -320
- 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 +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- 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 +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- 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 +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -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 +96 -68
- 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 +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- 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-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- 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 +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- 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 +109 -118
- package/src/vite/router-discovery.ts +718 -119
- 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 +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- 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 -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,15 +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;
|
|
140
344
|
|
|
141
345
|
/**
|
|
142
346
|
* URL prefix for all routes (from createRouter({ basename })).
|
|
347
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
143
348
|
*/
|
|
144
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;
|
|
145
367
|
}
|
|
146
368
|
|
|
147
369
|
/**
|
|
@@ -175,6 +397,8 @@ export function NavigationProvider({
|
|
|
175
397
|
warmupEnabled,
|
|
176
398
|
version,
|
|
177
399
|
basename,
|
|
400
|
+
appShellRef,
|
|
401
|
+
nonce,
|
|
178
402
|
}: NavigationProviderProps): ReactNode {
|
|
179
403
|
// Track current payload for rendering (this triggers re-renders)
|
|
180
404
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -196,104 +420,43 @@ export function NavigationProvider({
|
|
|
196
420
|
await bridge.refresh();
|
|
197
421
|
}, []);
|
|
198
422
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
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 = {
|
|
202
435
|
store,
|
|
203
436
|
eventController,
|
|
204
437
|
navigate,
|
|
205
438
|
refresh,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}, []);
|
|
211
452
|
|
|
212
|
-
// Connection warmup: keep TLS alive after idle periods.
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
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.
|
|
216
457
|
useEffect(() => {
|
|
217
458
|
if (!warmupEnabled) return;
|
|
218
|
-
|
|
219
|
-
const IDLE_TIMEOUT = 60_000;
|
|
220
|
-
const DEBOUNCE_DELAY = 150;
|
|
221
|
-
|
|
222
|
-
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
223
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
224
|
-
let isCold = false;
|
|
225
|
-
let warmupListenersAttached = false;
|
|
226
|
-
|
|
227
|
-
function sendWarmup() {
|
|
228
|
-
isCold = false;
|
|
229
|
-
fetch("/?_rsc_warmup", { method: "HEAD" }).catch(() => {});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function triggerWarmup() {
|
|
233
|
-
if (!isCold) return;
|
|
234
|
-
clearTimeout(debounceTimer);
|
|
235
|
-
debounceTimer = setTimeout(() => {
|
|
236
|
-
sendWarmup();
|
|
237
|
-
detachWarmupListeners();
|
|
238
|
-
resetIdleTimer();
|
|
239
|
-
}, DEBOUNCE_DELAY);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function onVisibilityChange() {
|
|
243
|
-
if (document.visibilityState === "visible" && isCold) {
|
|
244
|
-
triggerWarmup();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function attachWarmupListeners() {
|
|
249
|
-
if (warmupListenersAttached) return;
|
|
250
|
-
warmupListenersAttached = true;
|
|
251
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
252
|
-
document.addEventListener("mousemove", triggerWarmup, { once: true });
|
|
253
|
-
document.addEventListener("touchstart", triggerWarmup, { once: true });
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function detachWarmupListeners() {
|
|
257
|
-
warmupListenersAttached = false;
|
|
258
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
259
|
-
document.removeEventListener("mousemove", triggerWarmup);
|
|
260
|
-
document.removeEventListener("touchstart", triggerWarmup);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function markCold() {
|
|
264
|
-
isCold = true;
|
|
265
|
-
attachWarmupListeners();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function resetIdleTimer() {
|
|
269
|
-
clearTimeout(idleTimer);
|
|
270
|
-
isCold = false;
|
|
271
|
-
idleTimer = setTimeout(markCold, IDLE_TIMEOUT);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Activity events that reset the idle timer
|
|
275
|
-
const activityEvents = [
|
|
276
|
-
"mousemove",
|
|
277
|
-
"keydown",
|
|
278
|
-
"touchstart",
|
|
279
|
-
"scroll",
|
|
280
|
-
] as const;
|
|
281
|
-
const activityOptions: AddEventListenerOptions = { passive: true };
|
|
282
|
-
|
|
283
|
-
for (const event of activityEvents) {
|
|
284
|
-
document.addEventListener(event, resetIdleTimer, activityOptions);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
resetIdleTimer();
|
|
288
|
-
|
|
289
|
-
return () => {
|
|
290
|
-
clearTimeout(idleTimer);
|
|
291
|
-
clearTimeout(debounceTimer);
|
|
292
|
-
detachWarmupListeners();
|
|
293
|
-
for (const event of activityEvents) {
|
|
294
|
-
document.removeEventListener(event, resetIdleTimer);
|
|
295
|
-
}
|
|
296
|
-
};
|
|
459
|
+
return startConnectionWarmup();
|
|
297
460
|
}, [warmupEnabled]);
|
|
298
461
|
|
|
299
462
|
// Cancel non-matching prefetches when navigation starts.
|
|
@@ -363,24 +526,20 @@ export function NavigationProvider({
|
|
|
363
526
|
store,
|
|
364
527
|
matched: update.metadata.matched,
|
|
365
528
|
isPartial: update.metadata.isPartial,
|
|
529
|
+
resolvedIds: update.metadata.resolvedIds,
|
|
366
530
|
historyKey,
|
|
367
531
|
}).catch((err) =>
|
|
368
532
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
369
533
|
);
|
|
370
|
-
} else if (update.metadata.cachedHandleData) {
|
|
371
|
-
// For back/forward navigation from cache, restore the cached handleData
|
|
372
|
-
// This restores breadcrumbs to the exact state they were when the page was cached
|
|
373
|
-
eventController.setHandleData(
|
|
374
|
-
update.metadata.cachedHandleData,
|
|
375
|
-
update.metadata.matched,
|
|
376
|
-
false, // full replace - restore entire cached state
|
|
377
|
-
);
|
|
378
534
|
} else if (update.metadata.matched) {
|
|
379
|
-
//
|
|
535
|
+
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
536
|
+
// partial cleanup of segments no longer matched.
|
|
537
|
+
const cached = update.metadata.cachedHandleData;
|
|
380
538
|
eventController.setHandleData(
|
|
381
|
-
{},
|
|
539
|
+
cached ?? {},
|
|
382
540
|
update.metadata.matched,
|
|
383
|
-
|
|
541
|
+
cached === undefined,
|
|
542
|
+
cached === undefined ? update.metadata.resolvedIds : undefined,
|
|
384
543
|
);
|
|
385
544
|
}
|
|
386
545
|
});
|
|
@@ -393,7 +552,8 @@ export function NavigationProvider({
|
|
|
393
552
|
payload.root instanceof Promise ? use(payload.root) : payload.root;
|
|
394
553
|
|
|
395
554
|
// Wrap content in RootErrorBoundary to catch:
|
|
396
|
-
// 1. Errors from
|
|
555
|
+
// 1. Errors from RenderErrorThrower (network failures and unprocessable
|
|
556
|
+
// navigation responses, routed here by the navigation bridge)
|
|
397
557
|
// 2. Client component errors that occur before/outside the segment tree's error boundary
|
|
398
558
|
// 3. Errors during promise resolution or navigation state updates
|
|
399
559
|
// This acts as a safety net - the segment tree has its own RootErrorBoundary that
|
|
@@ -402,7 +562,11 @@ export function NavigationProvider({
|
|
|
402
562
|
// Build the content tree
|
|
403
563
|
let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
|
|
404
564
|
|
|
405
|
-
// 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.
|
|
406
570
|
if (themeConfig) {
|
|
407
571
|
content = (
|
|
408
572
|
<ThemeProvider config={themeConfig} initialTheme={initialTheme}>
|
|
@@ -413,9 +577,10 @@ export function NavigationProvider({
|
|
|
413
577
|
|
|
414
578
|
// Match SSR tree shape: NonceContext.Provider is always present so
|
|
415
579
|
// hydration sees the same component tree. Value is undefined on the
|
|
416
|
-
// 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.
|
|
417
582
|
content = (
|
|
418
|
-
<NonceContext.Provider value={
|
|
583
|
+
<NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
|
|
419
584
|
);
|
|
420
585
|
|
|
421
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: {
|
|
@@ -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
|
+
}
|