@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +63 -24
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +338 -126
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- 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/router/middleware-cookies.ts +0 -55
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import React, {
|
|
4
4
|
useState,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useCallback,
|
|
7
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
8
10
|
use,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from "react";
|
|
@@ -25,6 +27,9 @@ import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
|
25
27
|
import { NonceContext } from "./nonce-context.js";
|
|
26
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
27
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
|
+
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
31
|
+
import { createAppShellRef, type AppShellRef } from "../app-shell.js";
|
|
32
|
+
import { debugLog } from "../logging.js";
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* Process handles from an async generator, updating the event controller
|
|
@@ -43,10 +48,22 @@ async function processHandles(
|
|
|
43
48
|
store: NavigationStore;
|
|
44
49
|
matched?: string[];
|
|
45
50
|
isPartial?: boolean;
|
|
51
|
+
/** Server's `resolvedIds`: every segment re-resolved this request,
|
|
52
|
+
* including null-component ones excluded from `diff`/`segments`.
|
|
53
|
+
* Drives cleanup of stale handle buckets when a re-resolved segment
|
|
54
|
+
* pushed nothing. */
|
|
55
|
+
resolvedIds?: string[];
|
|
46
56
|
historyKey: string;
|
|
47
57
|
},
|
|
48
58
|
): Promise<void> {
|
|
49
|
-
const {
|
|
59
|
+
const {
|
|
60
|
+
eventController,
|
|
61
|
+
store,
|
|
62
|
+
matched,
|
|
63
|
+
isPartial,
|
|
64
|
+
resolvedIds,
|
|
65
|
+
historyKey,
|
|
66
|
+
} = opts;
|
|
50
67
|
|
|
51
68
|
let yieldCount = 0;
|
|
52
69
|
for await (const handleData of handlesGenerator) {
|
|
@@ -54,14 +71,14 @@ async function processHandles(
|
|
|
54
71
|
// This prevents handle data from cancelled navigations polluting
|
|
55
72
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
56
73
|
if (historyKey !== store.getHistoryKey()) {
|
|
57
|
-
|
|
74
|
+
debugLog(
|
|
58
75
|
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
59
76
|
);
|
|
60
77
|
return;
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
yieldCount++;
|
|
64
|
-
eventController.setHandleData(handleData, matched, isPartial);
|
|
81
|
+
eventController.setHandleData(handleData, matched, isPartial, resolvedIds);
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
// Check again before final updates
|
|
@@ -69,12 +86,11 @@ async function processHandles(
|
|
|
69
86
|
return;
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
// For partial updates where the generator yielded nothing (
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// route might not push any breadcrumbs, but we still need to remove the old ones.
|
|
89
|
+
// For partial updates where the generator yielded nothing (every
|
|
90
|
+
// re-resolved handler pushed nothing), still call setHandleData so the
|
|
91
|
+
// cleanup pass can clear out stale buckets for those segments.
|
|
76
92
|
if (yieldCount === 0 && matched) {
|
|
77
|
-
eventController.setHandleData({}, matched, true);
|
|
93
|
+
eventController.setHandleData({}, matched, true, resolvedIds);
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
// After handles processing completes, update the cache's handleData.
|
|
@@ -130,10 +146,25 @@ export interface NavigationProviderProps {
|
|
|
130
146
|
warmupEnabled?: boolean;
|
|
131
147
|
|
|
132
148
|
/**
|
|
133
|
-
* App version from server payload
|
|
134
|
-
*
|
|
149
|
+
* App version from server payload.
|
|
150
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
135
151
|
*/
|
|
136
152
|
version?: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
156
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
157
|
+
*/
|
|
158
|
+
basename?: string;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* App-shell ref. When provided, the context's `basename` and `version` are
|
|
162
|
+
* read through it (live getters) so they don't close over a stale snapshot or
|
|
163
|
+
* invalidate the memoized context value. The shell is set once at init and is
|
|
164
|
+
* not swapped within a session — a cross-app navigation is a full document
|
|
165
|
+
* load (X-RSC-Reload), so the target app establishes its own shell on load.
|
|
166
|
+
*/
|
|
167
|
+
appShellRef?: AppShellRef;
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
/**
|
|
@@ -166,6 +197,8 @@ export function NavigationProvider({
|
|
|
166
197
|
initialTheme,
|
|
167
198
|
warmupEnabled,
|
|
168
199
|
version,
|
|
200
|
+
basename,
|
|
201
|
+
appShellRef,
|
|
169
202
|
}: NavigationProviderProps): ReactNode {
|
|
170
203
|
// Track current payload for rendering (this triggers re-renders)
|
|
171
204
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -187,17 +220,35 @@ export function NavigationProvider({
|
|
|
187
220
|
await bridge.refresh();
|
|
188
221
|
}, []);
|
|
189
222
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
223
|
+
// basename/version are always read through a shell ref so the context value
|
|
224
|
+
// has a single shape. Both are set once: a supplied appShellRef is seeded
|
|
225
|
+
// from the init payload (a cross-app navigation reloads, so it is not swapped
|
|
226
|
+
// in-session), and the standalone fallback wraps the mount-time props.
|
|
227
|
+
const fallbackShellRef = useRef<AppShellRef | null>(null);
|
|
228
|
+
if (!fallbackShellRef.current) {
|
|
229
|
+
fallbackShellRef.current = createAppShellRef({ basename, version });
|
|
230
|
+
}
|
|
231
|
+
const shellRef = appShellRef ?? fallbackShellRef.current;
|
|
232
|
+
|
|
233
|
+
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
234
|
+
const value = {
|
|
193
235
|
store,
|
|
194
236
|
eventController,
|
|
195
237
|
navigate,
|
|
196
238
|
refresh,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
239
|
+
} as NavigationStoreContextValue;
|
|
240
|
+
Object.defineProperty(value, "basename", {
|
|
241
|
+
configurable: true,
|
|
242
|
+
enumerable: true,
|
|
243
|
+
get: () => shellRef.get().basename,
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(value, "version", {
|
|
246
|
+
configurable: true,
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: () => shellRef.get().version,
|
|
249
|
+
});
|
|
250
|
+
return value;
|
|
251
|
+
}, []);
|
|
201
252
|
|
|
202
253
|
// Connection warmup: keep TLS alive after idle periods.
|
|
203
254
|
// After 60s of no user interaction, marks connection as "cold".
|
|
@@ -286,31 +337,61 @@ export function NavigationProvider({
|
|
|
286
337
|
};
|
|
287
338
|
}, [warmupEnabled]);
|
|
288
339
|
|
|
289
|
-
// Cancel
|
|
290
|
-
//
|
|
340
|
+
// Cancel non-matching prefetches when navigation starts.
|
|
341
|
+
// Frees connections so the navigation fetch isn't competing with
|
|
342
|
+
// speculative prefetches. The prefetch matching the navigation target
|
|
343
|
+
// is kept alive so it can be reused via consumeInflightPrefetch.
|
|
291
344
|
useEffect(() => {
|
|
292
345
|
let wasIdle = true;
|
|
293
346
|
const unsub = eventController.subscribe(() => {
|
|
294
347
|
const state = eventController.getState();
|
|
295
348
|
const isIdle = state.state === "idle" && !state.isStreaming;
|
|
296
349
|
if (wasIdle && !isIdle) {
|
|
297
|
-
cancelAllPrefetches();
|
|
350
|
+
cancelAllPrefetches(state.pendingUrl);
|
|
298
351
|
}
|
|
299
352
|
wasIdle = isIdle;
|
|
300
353
|
});
|
|
301
354
|
return unsub;
|
|
302
355
|
}, [eventController]);
|
|
303
356
|
|
|
357
|
+
// Pending scroll action to apply after React commits
|
|
358
|
+
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
359
|
+
|
|
360
|
+
// Apply scroll after React commits the new content to the DOM
|
|
361
|
+
useLayoutEffect(() => {
|
|
362
|
+
const scrollAction = pendingScrollRef.current;
|
|
363
|
+
if (!scrollAction) return;
|
|
364
|
+
pendingScrollRef.current = undefined;
|
|
365
|
+
|
|
366
|
+
if (scrollAction.enabled === false) return;
|
|
367
|
+
|
|
368
|
+
handleNavigationEnd({
|
|
369
|
+
restore: scrollAction.restore,
|
|
370
|
+
scroll: scrollAction.enabled,
|
|
371
|
+
isStreaming: scrollAction.isStreaming,
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
304
375
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
305
376
|
useEffect(() => {
|
|
306
377
|
const unsubscribe = store.onUpdate((update) => {
|
|
378
|
+
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
379
|
+
// after React commits this state update to the DOM.
|
|
380
|
+
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
381
|
+
// so server actions or error updates don't accidentally replay old scroll.
|
|
382
|
+
pendingScrollRef.current = update.scroll;
|
|
383
|
+
|
|
307
384
|
setPayload({
|
|
308
385
|
root: update.root,
|
|
309
386
|
metadata: update.metadata,
|
|
310
387
|
});
|
|
311
388
|
|
|
312
|
-
// Update route params
|
|
313
|
-
|
|
389
|
+
// Update route params. Only reset when the server actually sends a params
|
|
390
|
+
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
391
|
+
// responses that omitted params). Explicit `{}` still clears correctly.
|
|
392
|
+
if (update.metadata.params !== undefined) {
|
|
393
|
+
eventController.setParams(update.metadata.params);
|
|
394
|
+
}
|
|
314
395
|
|
|
315
396
|
// Update handle data progressively as it streams in
|
|
316
397
|
if (update.metadata.handles) {
|
|
@@ -323,24 +404,20 @@ export function NavigationProvider({
|
|
|
323
404
|
store,
|
|
324
405
|
matched: update.metadata.matched,
|
|
325
406
|
isPartial: update.metadata.isPartial,
|
|
407
|
+
resolvedIds: update.metadata.resolvedIds,
|
|
326
408
|
historyKey,
|
|
327
409
|
}).catch((err) =>
|
|
328
410
|
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
329
411
|
);
|
|
330
|
-
} else if (update.metadata.cachedHandleData) {
|
|
331
|
-
// For back/forward navigation from cache, restore the cached handleData
|
|
332
|
-
// This restores breadcrumbs to the exact state they were when the page was cached
|
|
333
|
-
eventController.setHandleData(
|
|
334
|
-
update.metadata.cachedHandleData,
|
|
335
|
-
update.metadata.matched,
|
|
336
|
-
false, // full replace - restore entire cached state
|
|
337
|
-
);
|
|
338
412
|
} else if (update.metadata.matched) {
|
|
339
|
-
//
|
|
413
|
+
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
414
|
+
// partial cleanup of segments no longer matched.
|
|
415
|
+
const cached = update.metadata.cachedHandleData;
|
|
340
416
|
eventController.setHandleData(
|
|
341
|
-
{},
|
|
417
|
+
cached ?? {},
|
|
342
418
|
update.metadata.matched,
|
|
343
|
-
|
|
419
|
+
cached === undefined,
|
|
420
|
+
cached === undefined ? update.metadata.resolvedIds : undefined,
|
|
344
421
|
);
|
|
345
422
|
}
|
|
346
423
|
});
|
|
@@ -362,7 +439,11 @@ export function NavigationProvider({
|
|
|
362
439
|
// Build the content tree
|
|
363
440
|
let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
|
|
364
441
|
|
|
365
|
-
// Wrap with ThemeProvider when theme is enabled
|
|
442
|
+
// Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
|
|
443
|
+
// document-lifetime: its config comes from the initial load and persists for
|
|
444
|
+
// the session. It sits above the segment tree and is not remounted in-session;
|
|
445
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the target
|
|
446
|
+
// app's theme config takes effect on its own load.
|
|
366
447
|
if (themeConfig) {
|
|
367
448
|
content = (
|
|
368
449
|
<ThemeProvider config={themeConfig} initialTheme={initialTheme}>
|
|
@@ -14,17 +14,21 @@ export interface ScrollRestorationProps {
|
|
|
14
14
|
* Return location.pathname to restore scroll based on path
|
|
15
15
|
* (useful for keeping scroll position on the same page).
|
|
16
16
|
*
|
|
17
|
+
* Provide a stable reference: a module-level function or one wrapped in
|
|
18
|
+
* useCallback. The init effect re-runs when getKey's identity changes, and
|
|
19
|
+
* teardown clears in-memory scroll positions — a fresh inline arrow on every
|
|
20
|
+
* parent render would discard unpersisted positions mid-session.
|
|
21
|
+
*
|
|
17
22
|
* @example
|
|
18
23
|
* ```tsx
|
|
24
|
+
* // Stable module-level getKey (recommended)
|
|
25
|
+
* const byPathname = (location) => location.pathname;
|
|
26
|
+
*
|
|
19
27
|
* // Restore based on pathname (same URL = same scroll)
|
|
20
|
-
* <ScrollRestoration
|
|
21
|
-
* getKey={(location) => location.pathname}
|
|
22
|
-
* />
|
|
28
|
+
* <ScrollRestoration getKey={byPathname} />
|
|
23
29
|
*
|
|
24
30
|
* // Restore based on unique history entry (default)
|
|
25
|
-
* <ScrollRestoration
|
|
26
|
-
* getKey={(location) => location.key}
|
|
27
|
-
* />
|
|
31
|
+
* // <ScrollRestoration /> — omit getKey to use location.key
|
|
28
32
|
* ```
|
|
29
33
|
*/
|
|
30
34
|
getKey?: (location: {
|
|
@@ -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
|
/**
|
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Build the handle-collection segment order from a raw `matched` list.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Drop loader sub-ids ("D" followed by a digit, e.g. "M0L0D1.user") —
|
|
7
|
+
* loaders never push handles.
|
|
8
|
+
*
|
|
9
|
+
* 2. Place each parallel slot id (contains ".@") immediately after its
|
|
10
|
+
* parent layout/route id. Raw segment-resolution emission order does NOT
|
|
11
|
+
* guarantee this: route-mounted parallels are resolved/pushed BEFORE the
|
|
12
|
+
* route handler's segment is appended (see fresh.ts:resolveSegment for
|
|
13
|
+
* routes, and revalidation.ts ~915-919), so matched can read
|
|
14
|
+
* `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
|
|
15
|
+
* with later-wins semantics, so without normalization the route handler's
|
|
16
|
+
* Meta would override the slot's more-specific Meta — backwards.
|
|
17
|
+
*
|
|
18
|
+
* Slot-id format is `<parentShortCode>.@<slotName>`; `parentShortCode` never
|
|
19
|
+
* contains ".@", so splitting at the first ".@" reliably yields the parent.
|
|
4
20
|
*/
|
|
5
21
|
export function filterSegmentOrder(matched: string[]): string[] {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
const slotsByParent = new Map<string, string[]>();
|
|
23
|
+
const nonSlots: string[] = [];
|
|
24
|
+
const nonSlotSet = new Set<string>();
|
|
25
|
+
|
|
26
|
+
for (const id of matched) {
|
|
27
|
+
if (/D\d+\./.test(id)) continue;
|
|
28
|
+
const slotIdx = id.indexOf(".@");
|
|
29
|
+
if (slotIdx >= 0) {
|
|
30
|
+
const parent = id.slice(0, slotIdx);
|
|
31
|
+
const list = slotsByParent.get(parent);
|
|
32
|
+
if (list) {
|
|
33
|
+
list.push(id);
|
|
34
|
+
} else {
|
|
35
|
+
slotsByParent.set(parent, [id]);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
nonSlots.push(id);
|
|
39
|
+
nonSlotSet.add(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result: string[] = [];
|
|
44
|
+
for (const id of nonSlots) {
|
|
45
|
+
result.push(id);
|
|
46
|
+
const slots = slotsByParent.get(id);
|
|
47
|
+
if (slots) result.push(...slots);
|
|
48
|
+
}
|
|
49
|
+
for (const [parent, slots] of slotsByParent) {
|
|
50
|
+
if (!nonSlotSet.has(parent)) result.push(...slots);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
11
53
|
}
|
|
@@ -1,52 +1,4 @@
|
|
|
1
|
-
// React exports for browser navigation
|
|
2
|
-
|
|
3
|
-
// Hook with Zustand-style selectors
|
|
4
|
-
export { useNavigation } from "./use-navigation.js";
|
|
5
|
-
|
|
6
|
-
// Router actions hook (stable reference, no re-renders)
|
|
7
|
-
export { useRouter } from "./use-router.js";
|
|
8
|
-
|
|
9
|
-
// URL hooks
|
|
10
|
-
export { usePathname } from "./use-pathname.js";
|
|
11
|
-
export { useSearchParams } from "./use-search-params.js";
|
|
12
|
-
export { useParams } from "./use-params.js";
|
|
13
|
-
|
|
14
|
-
// Action state tracking hook
|
|
15
|
-
export { useAction, type TrackedActionState } from "./use-action.js";
|
|
16
|
-
|
|
17
|
-
// Segments state hook
|
|
18
|
-
export { useSegments, type SegmentsState } from "./use-segments.js";
|
|
19
|
-
|
|
20
|
-
// Handle data hook
|
|
21
|
-
export { useHandle } from "./use-handle.js";
|
|
22
|
-
|
|
23
|
-
// Client cache controls hook
|
|
24
|
-
export {
|
|
25
|
-
useClientCache,
|
|
26
|
-
type ClientCacheControls,
|
|
27
|
-
} from "./use-client-cache.js";
|
|
28
|
-
|
|
29
|
-
// Provider
|
|
30
1
|
export {
|
|
31
2
|
NavigationProvider,
|
|
32
3
|
type NavigationProviderProps,
|
|
33
4
|
} from "./NavigationProvider.js";
|
|
34
|
-
|
|
35
|
-
// Context (for advanced usage)
|
|
36
|
-
export {
|
|
37
|
-
NavigationStoreContext,
|
|
38
|
-
type NavigationStoreContextValue,
|
|
39
|
-
} from "./context.js";
|
|
40
|
-
|
|
41
|
-
// Link component
|
|
42
|
-
export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
|
|
43
|
-
|
|
44
|
-
// Link status hook
|
|
45
|
-
export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
|
|
46
|
-
|
|
47
|
-
// Scroll restoration
|
|
48
|
-
export {
|
|
49
|
-
ScrollRestoration,
|
|
50
|
-
useScrollRestoration,
|
|
51
|
-
type ScrollRestorationProps,
|
|
52
|
-
} from "./ScrollRestoration.js";
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared location state utilities - works in both RSC and client contexts
|
|
3
|
-
* No "use client" directive so it can be imported from RSC
|
|
4
|
-
*/
|
|
1
|
+
import type { ReactElement } from "react";
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Internal entry representing a state value with its unique key.
|
|
@@ -22,6 +19,80 @@ export interface LocationStateOptions {
|
|
|
22
19
|
flash?: boolean;
|
|
23
20
|
}
|
|
24
21
|
|
|
22
|
+
type LocationStateUnsafeFn = (...args: never[]) => unknown;
|
|
23
|
+
|
|
24
|
+
type LocationStateUnsafeCtor = abstract new (...args: never[]) => unknown;
|
|
25
|
+
|
|
26
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
27
|
+
type IsUnknown<T> =
|
|
28
|
+
IsAny<T> extends true ? false : unknown extends T ? true : false;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Branded error surfaced when a value that cannot live in location state is
|
|
32
|
+
* used. Location state is written into `history.state`, which uses the
|
|
33
|
+
* structured clone algorithm; React elements, functions, and symbols throw a
|
|
34
|
+
* `DataCloneError` at runtime. Carries a human-readable reason so the compile
|
|
35
|
+
* error explains the fix.
|
|
36
|
+
*/
|
|
37
|
+
export type LocationStateUnsafe<Reason extends string> = {
|
|
38
|
+
readonly __rango_location_state_unsafe: Reason;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps `T` to itself when it is safe to store in location state, or to a branded
|
|
43
|
+
* {@link LocationStateUnsafe} error for the disallowed parts: `unknown`, React
|
|
44
|
+
* elements (RSC/JSX content), functions, class constructors, and symbols.
|
|
45
|
+
* Recurses through arrays, `Map`, `Set`, and plain objects; structured-clone
|
|
46
|
+
* built-ins (`Date`, `RegExp`, typed arrays, `Blob`, `File`, `FormData`) pass
|
|
47
|
+
* through. Consumed by {@link ValidateLocationState}, which is intersected into a
|
|
48
|
+
* definition's value parameter so posting RSC content is a COMPILE error, not a
|
|
49
|
+
* runtime `DataCloneError`. (`any` is unguardable and remains an escape hatch.)
|
|
50
|
+
*/
|
|
51
|
+
export type LocationStateSafe<T> =
|
|
52
|
+
IsUnknown<T> extends true
|
|
53
|
+
? LocationStateUnsafe<"location state needs an explicit, concrete type; `unknown` cannot be verified as serializable">
|
|
54
|
+
: T extends LocationStateUnsafeFn
|
|
55
|
+
? LocationStateUnsafe<"functions cannot be stored in location state">
|
|
56
|
+
: T extends LocationStateUnsafeCtor
|
|
57
|
+
? LocationStateUnsafe<"class constructors cannot be stored in location state">
|
|
58
|
+
: T extends symbol
|
|
59
|
+
? LocationStateUnsafe<"symbols cannot be stored in location state">
|
|
60
|
+
: T extends ReactElement
|
|
61
|
+
? LocationStateUnsafe<"React/RSC content cannot be stored in location state; store plain data and render it on arrival">
|
|
62
|
+
: T extends string | number | boolean | bigint | null | undefined
|
|
63
|
+
? T
|
|
64
|
+
: T extends
|
|
65
|
+
| Date
|
|
66
|
+
| RegExp
|
|
67
|
+
| ArrayBuffer
|
|
68
|
+
| ArrayBufferView
|
|
69
|
+
| Blob
|
|
70
|
+
| File
|
|
71
|
+
| FormData
|
|
72
|
+
? T
|
|
73
|
+
: T extends ReadonlyMap<infer K, infer V>
|
|
74
|
+
? ReadonlyMap<LocationStateSafe<K>, LocationStateSafe<V>>
|
|
75
|
+
: T extends ReadonlySet<infer V>
|
|
76
|
+
? ReadonlySet<LocationStateSafe<V>>
|
|
77
|
+
: T extends readonly unknown[]
|
|
78
|
+
? { [K in keyof T]: LocationStateSafe<T[K]> }
|
|
79
|
+
: T extends object
|
|
80
|
+
? { [K in keyof T]: LocationStateSafe<T[K]> }
|
|
81
|
+
: T;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `unknown` (a no-op) when `T` is safe to store in location state, otherwise a
|
|
85
|
+
* branded {@link LocationStateUnsafe} object. Intersected into the value
|
|
86
|
+
* parameter of a definition's call and `write()` so POSTING RSC content (or any
|
|
87
|
+
* non-serializable value) is a compile error whose text carries the reason —
|
|
88
|
+
* without a `TState extends ...` self-constraint, which TypeScript rejects as
|
|
89
|
+
* circular (TS2313). For safe `T`, `value & unknown` collapses back to `value`,
|
|
90
|
+
* so valid usage is unchanged.
|
|
91
|
+
*/
|
|
92
|
+
export type ValidateLocationState<T> = [T] extends [LocationStateSafe<T>]
|
|
93
|
+
? unknown
|
|
94
|
+
: LocationStateUnsafe<"location state must be serializable: React/RSC content, functions, and symbols cannot be stored — pass plain data and render it on arrival">;
|
|
95
|
+
|
|
25
96
|
/**
|
|
26
97
|
* Type-safe location state definition
|
|
27
98
|
*
|
|
@@ -34,8 +105,43 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
34
105
|
__rsc_ls_key: string;
|
|
35
106
|
/** Whether this state auto-clears after first read */
|
|
36
107
|
readonly __rsc_ls_flash: boolean;
|
|
37
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Read the current value from history.state.
|
|
110
|
+
*
|
|
111
|
+
* Returns undefined during SSR (no `window`). To stay hydration-safe, do
|
|
112
|
+
* NOT call read() inline during the initial render — the server returns
|
|
113
|
+
* undefined while the client may have a value preserved in history.state
|
|
114
|
+
* (e.g. after a hard reload of an entry that earlier called write()),
|
|
115
|
+
* which causes a hydration mismatch. Call read() inside an event handler
|
|
116
|
+
* or a useEffect post-mount instead, or use useLocationState() if you
|
|
117
|
+
* want React to manage subscription/hydration for you.
|
|
118
|
+
*/
|
|
38
119
|
read(): TState | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Statically write the value into the current history entry under this
|
|
122
|
+
* definition's key, preserving any other keys already on history.state
|
|
123
|
+
* (e.g. router bookkeeping, other LocationState slots).
|
|
124
|
+
*
|
|
125
|
+
* This is the non-reactive counterpart to read(): it does not dispatch any
|
|
126
|
+
* event, so components reading via useLocationState() will NOT re-render
|
|
127
|
+
* until the next navigation/popstate. Use it when you only need the value
|
|
128
|
+
* to be there on the next read() or on the next mount (including after
|
|
129
|
+
* back/forward and hard refresh of the same entry).
|
|
130
|
+
*
|
|
131
|
+
* Client-only: throws when called on the server (no history available).
|
|
132
|
+
*/
|
|
133
|
+
write(value: TState & ValidateLocationState<TState>): void;
|
|
134
|
+
/**
|
|
135
|
+
* Statically remove this definition's slot from the current history entry,
|
|
136
|
+
* leaving any other keys on history.state untouched. Idempotent: removing
|
|
137
|
+
* a slot that isn't present is a no-op.
|
|
138
|
+
*
|
|
139
|
+
* Same non-reactive semantics as write(): no event is dispatched, so
|
|
140
|
+
* useLocationState() readers will NOT re-render until the next navigation.
|
|
141
|
+
*
|
|
142
|
+
* Client-only: throws when called on the server (no history available).
|
|
143
|
+
*/
|
|
144
|
+
delete(): void;
|
|
39
145
|
}
|
|
40
146
|
|
|
41
147
|
/**
|
|
@@ -70,18 +176,30 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
70
176
|
*
|
|
71
177
|
* // Read without hook (snapshot, client-side only)
|
|
72
178
|
* const snap = ProductState.read();
|
|
179
|
+
*
|
|
180
|
+
* // Static write to current history entry (non-reactive, client-side only).
|
|
181
|
+
* // Survives back/forward and hard refresh; useLocationState() readers will
|
|
182
|
+
* // NOT see the new value until the next navigation. Pair with .read() or a
|
|
183
|
+
* // fresh mount.
|
|
184
|
+
* ProductState.write({ name: "Widget", price: 9.99 });
|
|
185
|
+
*
|
|
186
|
+
* // Manually clear the slot (non-reactive, client-side only).
|
|
187
|
+
* ProductState.delete();
|
|
73
188
|
* ```
|
|
74
189
|
*/
|
|
75
190
|
export function createLocationState<TState>(
|
|
76
191
|
options?: LocationStateOptions,
|
|
77
|
-
): LocationStateDefinition<
|
|
192
|
+
): LocationStateDefinition<
|
|
193
|
+
[(TState | (() => TState)) & ValidateLocationState<TState>],
|
|
194
|
+
TState
|
|
195
|
+
> {
|
|
78
196
|
const flash = options?.flash ?? false;
|
|
79
197
|
let _key: string | undefined;
|
|
80
198
|
|
|
81
199
|
function getKey(): string {
|
|
82
200
|
if (!_key && process.env.NODE_ENV === "development") {
|
|
83
201
|
throw new Error(
|
|
84
|
-
"[
|
|
202
|
+
"[rango] createLocationState key not set. " +
|
|
85
203
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
86
204
|
"the state is exported with: export const MyState = createLocationState(...)",
|
|
87
205
|
);
|
|
@@ -128,7 +246,47 @@ export function createLocationState<TState>(
|
|
|
128
246
|
enumerable: true,
|
|
129
247
|
});
|
|
130
248
|
|
|
131
|
-
|
|
249
|
+
Object.defineProperty(fn, "write", {
|
|
250
|
+
value: (value: TState): void => {
|
|
251
|
+
if (typeof window === "undefined") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"[rango] LocationState.write() is client-only. " +
|
|
254
|
+
"It mutates window.history.state and cannot run on the server.",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const key = getKey();
|
|
258
|
+
const current = window.history.state ?? {};
|
|
259
|
+
window.history.replaceState(
|
|
260
|
+
{ ...current, [key]: value },
|
|
261
|
+
"",
|
|
262
|
+
window.location.href,
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
enumerable: true,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
Object.defineProperty(fn, "delete", {
|
|
269
|
+
value: (): void => {
|
|
270
|
+
if (typeof window === "undefined") {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"[rango] LocationState.delete() is client-only. " +
|
|
273
|
+
"It mutates window.history.state and cannot run on the server.",
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const key = getKey();
|
|
277
|
+
const current = window.history.state;
|
|
278
|
+
if (current == null || !(key in current)) return;
|
|
279
|
+
const next = { ...current };
|
|
280
|
+
delete next[key];
|
|
281
|
+
window.history.replaceState(next, "", window.location.href);
|
|
282
|
+
},
|
|
283
|
+
enumerable: true,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return fn as unknown as LocationStateDefinition<
|
|
287
|
+
[(TState | (() => TState)) & ValidateLocationState<TState>],
|
|
288
|
+
TState
|
|
289
|
+
>;
|
|
132
290
|
}
|
|
133
291
|
|
|
134
292
|
/**
|