@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -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";
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
} from "./context.js";
|
|
15
17
|
import type {
|
|
16
18
|
NavigationStore,
|
|
17
|
-
|
|
19
|
+
NavigationUpdate,
|
|
18
20
|
NavigateOptions,
|
|
19
21
|
NavigationBridge,
|
|
20
22
|
} from "../types.js";
|
|
@@ -22,7 +24,10 @@ import type { EventController } from "../event-controller.js";
|
|
|
22
24
|
import { RootErrorBoundary } from "../../root-error-boundary.js";
|
|
23
25
|
import type { HandleData } from "../types.js";
|
|
24
26
|
import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
27
|
+
import { NonceContext } from "./nonce-context.js";
|
|
25
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
29
|
+
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
|
+
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* Process handles from an async generator, updating the event controller
|
|
@@ -42,7 +47,7 @@ async function processHandles(
|
|
|
42
47
|
matched?: string[];
|
|
43
48
|
isPartial?: boolean;
|
|
44
49
|
historyKey: string;
|
|
45
|
-
}
|
|
50
|
+
},
|
|
46
51
|
): Promise<void> {
|
|
47
52
|
const { eventController, store, matched, isPartial, historyKey } = opts;
|
|
48
53
|
|
|
@@ -53,7 +58,7 @@ async function processHandles(
|
|
|
53
58
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
54
59
|
if (historyKey !== store.getHistoryKey()) {
|
|
55
60
|
console.log(
|
|
56
|
-
"[NavigationProvider] Stopping handle processing - user navigated away"
|
|
61
|
+
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
57
62
|
);
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
@@ -100,9 +105,9 @@ export interface NavigationProviderProps {
|
|
|
100
105
|
eventController: EventController;
|
|
101
106
|
|
|
102
107
|
/**
|
|
103
|
-
* Initial
|
|
108
|
+
* Initial rendered tree + metadata from server payload
|
|
104
109
|
*/
|
|
105
|
-
initialPayload:
|
|
110
|
+
initialPayload: NavigationUpdate;
|
|
106
111
|
|
|
107
112
|
/**
|
|
108
113
|
* Navigation bridge for handling navigation
|
|
@@ -126,6 +131,17 @@ export interface NavigationProviderProps {
|
|
|
126
131
|
* When true, keeps TLS alive by sending HEAD requests after idle periods.
|
|
127
132
|
*/
|
|
128
133
|
warmupEnabled?: boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* App version from server payload (stable, immutable).
|
|
137
|
+
* Forwarded to context for cache key building.
|
|
138
|
+
*/
|
|
139
|
+
version?: string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
143
|
+
*/
|
|
144
|
+
basename?: string;
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
/**
|
|
@@ -157,6 +173,8 @@ export function NavigationProvider({
|
|
|
157
173
|
themeConfig,
|
|
158
174
|
initialTheme,
|
|
159
175
|
warmupEnabled,
|
|
176
|
+
version,
|
|
177
|
+
basename,
|
|
160
178
|
}: NavigationProviderProps): ReactNode {
|
|
161
179
|
// Track current payload for rendering (this triggers re-renders)
|
|
162
180
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -168,7 +186,7 @@ export function NavigationProvider({
|
|
|
168
186
|
async (url: string, options?: NavigateOptions): Promise<void> => {
|
|
169
187
|
await bridge.navigate(url, options);
|
|
170
188
|
},
|
|
171
|
-
[]
|
|
189
|
+
[],
|
|
172
190
|
);
|
|
173
191
|
|
|
174
192
|
/**
|
|
@@ -185,8 +203,10 @@ export function NavigationProvider({
|
|
|
185
203
|
eventController,
|
|
186
204
|
navigate,
|
|
187
205
|
refresh,
|
|
206
|
+
version,
|
|
207
|
+
basename,
|
|
188
208
|
}),
|
|
189
|
-
[]
|
|
209
|
+
[],
|
|
190
210
|
);
|
|
191
211
|
|
|
192
212
|
// Connection warmup: keep TLS alive after idle periods.
|
|
@@ -252,7 +272,12 @@ export function NavigationProvider({
|
|
|
252
272
|
}
|
|
253
273
|
|
|
254
274
|
// Activity events that reset the idle timer
|
|
255
|
-
const activityEvents = [
|
|
275
|
+
const activityEvents = [
|
|
276
|
+
"mousemove",
|
|
277
|
+
"keydown",
|
|
278
|
+
"touchstart",
|
|
279
|
+
"scroll",
|
|
280
|
+
] as const;
|
|
256
281
|
const activityOptions: AddEventListenerOptions = { passive: true };
|
|
257
282
|
|
|
258
283
|
for (const event of activityEvents) {
|
|
@@ -271,14 +296,58 @@ export function NavigationProvider({
|
|
|
271
296
|
};
|
|
272
297
|
}, [warmupEnabled]);
|
|
273
298
|
|
|
299
|
+
// Cancel non-matching prefetches when navigation starts.
|
|
300
|
+
// Frees connections so the navigation fetch isn't competing with
|
|
301
|
+
// speculative prefetches. The prefetch matching the navigation target
|
|
302
|
+
// is kept alive so it can be reused via consumeInflightPrefetch.
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
let wasIdle = true;
|
|
305
|
+
const unsub = eventController.subscribe(() => {
|
|
306
|
+
const state = eventController.getState();
|
|
307
|
+
const isIdle = state.state === "idle" && !state.isStreaming;
|
|
308
|
+
if (wasIdle && !isIdle) {
|
|
309
|
+
cancelAllPrefetches(state.pendingUrl);
|
|
310
|
+
}
|
|
311
|
+
wasIdle = isIdle;
|
|
312
|
+
});
|
|
313
|
+
return unsub;
|
|
314
|
+
}, [eventController]);
|
|
315
|
+
|
|
316
|
+
// Pending scroll action to apply after React commits
|
|
317
|
+
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
318
|
+
|
|
319
|
+
// Apply scroll after React commits the new content to the DOM
|
|
320
|
+
useLayoutEffect(() => {
|
|
321
|
+
const scrollAction = pendingScrollRef.current;
|
|
322
|
+
if (!scrollAction) return;
|
|
323
|
+
pendingScrollRef.current = undefined;
|
|
324
|
+
|
|
325
|
+
if (scrollAction.enabled === false) return;
|
|
326
|
+
|
|
327
|
+
handleNavigationEnd({
|
|
328
|
+
restore: scrollAction.restore,
|
|
329
|
+
scroll: scrollAction.enabled,
|
|
330
|
+
isStreaming: scrollAction.isStreaming,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
274
334
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
275
335
|
useEffect(() => {
|
|
276
336
|
const unsubscribe = store.onUpdate((update) => {
|
|
337
|
+
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
338
|
+
// after React commits this state update to the DOM.
|
|
339
|
+
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
340
|
+
// so server actions or error updates don't accidentally replay old scroll.
|
|
341
|
+
pendingScrollRef.current = update.scroll;
|
|
342
|
+
|
|
277
343
|
setPayload({
|
|
278
344
|
root: update.root,
|
|
279
345
|
metadata: update.metadata,
|
|
280
346
|
});
|
|
281
347
|
|
|
348
|
+
// Update route params
|
|
349
|
+
eventController.setParams(update.metadata.params ?? {});
|
|
350
|
+
|
|
282
351
|
// Update handle data progressively as it streams in
|
|
283
352
|
if (update.metadata.handles) {
|
|
284
353
|
// Capture historyKey now - by the time async processing completes,
|
|
@@ -292,7 +361,7 @@ export function NavigationProvider({
|
|
|
292
361
|
isPartial: update.metadata.isPartial,
|
|
293
362
|
historyKey,
|
|
294
363
|
}).catch((err) =>
|
|
295
|
-
console.error("[NavigationProvider] Error consuming handles:", err)
|
|
364
|
+
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
296
365
|
);
|
|
297
366
|
} else if (update.metadata.cachedHandleData) {
|
|
298
367
|
// For back/forward navigation from cache, restore the cached handleData
|
|
@@ -300,14 +369,14 @@ export function NavigationProvider({
|
|
|
300
369
|
eventController.setHandleData(
|
|
301
370
|
update.metadata.cachedHandleData,
|
|
302
371
|
update.metadata.matched,
|
|
303
|
-
false // full replace - restore entire cached state
|
|
372
|
+
false, // full replace - restore entire cached state
|
|
304
373
|
);
|
|
305
374
|
} else if (update.metadata.matched) {
|
|
306
375
|
// For cached navigations without handleData, update segmentOrder to clean up stale data
|
|
307
376
|
eventController.setHandleData(
|
|
308
377
|
{}, // Empty data - all existing data not in matched will be cleaned up
|
|
309
378
|
update.metadata.matched,
|
|
310
|
-
true // partial update - will clean up segments not in matched
|
|
379
|
+
true, // partial update - will clean up segments not in matched
|
|
311
380
|
);
|
|
312
381
|
}
|
|
313
382
|
});
|
|
@@ -338,6 +407,13 @@ export function NavigationProvider({
|
|
|
338
407
|
);
|
|
339
408
|
}
|
|
340
409
|
|
|
410
|
+
// Match SSR tree shape: NonceContext.Provider is always present so
|
|
411
|
+
// hydration sees the same component tree. Value is undefined on the
|
|
412
|
+
// client — CSP nonces are a server-side HTML concern.
|
|
413
|
+
content = (
|
|
414
|
+
<NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
|
|
415
|
+
);
|
|
416
|
+
|
|
341
417
|
return (
|
|
342
418
|
<NavigationStoreContext.Provider value={contextValue}>
|
|
343
419
|
{content}
|
|
@@ -41,6 +41,17 @@ export interface NavigationStoreContextValue {
|
|
|
41
41
|
* @returns Promise that resolves when refresh is complete
|
|
42
42
|
*/
|
|
43
43
|
refresh: () => Promise<void>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* App version from the initial server payload.
|
|
47
|
+
*/
|
|
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;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter segment IDs to only include routes and layouts.
|
|
3
|
+
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
4
|
+
*/
|
|
5
|
+
export function filterSegmentOrder(matched: string[]): string[] {
|
|
6
|
+
return matched.filter((id) => {
|
|
7
|
+
if (id.includes(".@")) return false;
|
|
8
|
+
if (/D\d+\./.test(id)) return false;
|
|
9
|
+
return true;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
// React exports for browser navigation
|
|
2
2
|
|
|
3
3
|
// Hook with Zustand-style selectors
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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";
|
|
9
13
|
|
|
10
14
|
// Action state tracking hook
|
|
11
15
|
export { useAction, type TrackedActionState } from "./use-action.js";
|
|
12
16
|
|
|
13
17
|
// Segments state hook
|
|
14
|
-
export { useSegments,
|
|
18
|
+
export { useSegments, type SegmentsState } from "./use-segments.js";
|
|
15
19
|
|
|
16
20
|
// Handle data hook
|
|
17
|
-
export { useHandle
|
|
21
|
+
export { useHandle } from "./use-handle.js";
|
|
18
22
|
|
|
19
23
|
// Client cache controls hook
|
|
20
24
|
export {
|
|
@@ -35,11 +39,7 @@ export {
|
|
|
35
39
|
} from "./context.js";
|
|
36
40
|
|
|
37
41
|
// Link component
|
|
38
|
-
export {
|
|
39
|
-
Link,
|
|
40
|
-
type LinkProps,
|
|
41
|
-
type PrefetchStrategy,
|
|
42
|
-
} from "./Link.js";
|
|
42
|
+
export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
|
|
43
43
|
|
|
44
44
|
// Link status hook
|
|
45
45
|
export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
|
|
@@ -4,11 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Internal entry representing a state value with its unique key
|
|
7
|
+
* Internal entry representing a state value with its unique key.
|
|
8
|
+
* When __rsc_ls_lazy is true, __rsc_ls_value holds a getter function
|
|
9
|
+
* that is called at navigation time (not at entry creation time).
|
|
8
10
|
*/
|
|
9
11
|
export interface LocationStateEntry {
|
|
10
12
|
readonly __rsc_ls_key: string;
|
|
11
13
|
readonly __rsc_ls_value: unknown;
|
|
14
|
+
readonly __rsc_ls_lazy?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for createLocationState
|
|
19
|
+
*/
|
|
20
|
+
export interface LocationStateOptions {
|
|
21
|
+
/** When true, the state is cleared from history after first read (flash message pattern) */
|
|
22
|
+
flash?: boolean;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -19,84 +30,113 @@ export interface LocationStateEntry {
|
|
|
19
30
|
*/
|
|
20
31
|
export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
21
32
|
(...args: TArgs): LocationStateEntry;
|
|
22
|
-
|
|
33
|
+
/** Injected by Vite plugin - do not set manually */
|
|
34
|
+
__rsc_ls_key: string;
|
|
35
|
+
/** Whether this state auto-clears after first read */
|
|
36
|
+
readonly __rsc_ls_flash: boolean;
|
|
37
|
+
/** Read the current value from history.state (client-side only, undefined during SSR) */
|
|
38
|
+
read(): TState | undefined;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
// Track used keys to detect duplicates in development
|
|
26
|
-
const usedKeys = new Set<string>();
|
|
27
|
-
|
|
28
41
|
/**
|
|
29
42
|
* Create a type-safe location state definition
|
|
30
43
|
*
|
|
31
|
-
* The key is auto-
|
|
32
|
-
* file path and export name. No manual key required.
|
|
44
|
+
* The key is auto-injected by the Vite exposeInternalIds plugin as a property
|
|
45
|
+
* based on file path and export name. No manual key required.
|
|
33
46
|
*
|
|
34
|
-
* @param
|
|
47
|
+
* @param options Optional configuration
|
|
35
48
|
* @returns A typed state definition for use with Link and useLocationState
|
|
36
49
|
*
|
|
37
50
|
* @example
|
|
38
51
|
* ```typescript
|
|
39
|
-
* //
|
|
52
|
+
* // Persistent state (survives back/forward)
|
|
40
53
|
* export const ProductState = createLocationState<{ name: string; price: number }>();
|
|
41
54
|
*
|
|
42
|
-
* //
|
|
43
|
-
*
|
|
44
|
-
* View Product
|
|
45
|
-
* </Link>
|
|
55
|
+
* // Flash state (cleared after first read)
|
|
56
|
+
* export const FlashMessage = createLocationState<{ text: string }>({ flash: true });
|
|
46
57
|
*
|
|
47
|
-
* //
|
|
48
|
-
* <Link to="/
|
|
49
|
-
* Checkout
|
|
50
|
-
* </Link>
|
|
58
|
+
* // Use in Link
|
|
59
|
+
* <Link to="/product/123" state={[ProductState({ name: "Widget", price: 9.99 })]}>
|
|
51
60
|
*
|
|
52
|
-
* //
|
|
53
|
-
*
|
|
61
|
+
* // Just-in-time typed state (getter called at click time, not render time).
|
|
62
|
+
* // Must be in a client component — the getter function can't cross the RSC boundary.
|
|
63
|
+
* <Link
|
|
64
|
+
* to="/product/123"
|
|
65
|
+
* state={[ProductState(() => ({ name: product.name, price: product.price }))]}
|
|
66
|
+
* >
|
|
54
67
|
*
|
|
55
|
-
* // Read with
|
|
56
|
-
* const
|
|
57
|
-
*
|
|
68
|
+
* // Read with hook (reactive)
|
|
69
|
+
* const product = useLocationState(ProductState);
|
|
70
|
+
*
|
|
71
|
+
* // Read without hook (snapshot, client-side only)
|
|
72
|
+
* const snap = ProductState.read();
|
|
58
73
|
* ```
|
|
59
74
|
*/
|
|
60
75
|
export function createLocationState<TState>(
|
|
61
|
-
|
|
76
|
+
options?: LocationStateOptions,
|
|
62
77
|
): LocationStateDefinition<[TState | (() => TState)], TState> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"[rsc-router] createLocationState is missing a key. " +
|
|
66
|
-
"Make sure the exposeLocationStateId Vite plugin is enabled and " +
|
|
67
|
-
"the state is exported with: export const MyState = createLocationState(...)"
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const fullKey = `__rsc_ls_${key}`;
|
|
78
|
+
const flash = options?.flash ?? false;
|
|
79
|
+
let _key: string | undefined;
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
function getKey(): string {
|
|
82
|
+
if (!_key && process.env.NODE_ENV === "development") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"[rsc-router] createLocationState key not set. " +
|
|
85
|
+
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
86
|
+
"the state is exported with: export const MyState = createLocationState(...)",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return _key!;
|
|
78
90
|
}
|
|
79
|
-
usedKeys.add(fullKey);
|
|
80
91
|
|
|
81
|
-
const
|
|
82
|
-
(stateOrGetter
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
+
const fn = (stateOrGetter: TState | (() => TState)): LocationStateEntry => {
|
|
93
|
+
if (typeof stateOrGetter === "function") {
|
|
94
|
+
// Store getter as-is; resolved at navigation time by resolveLocationStateEntries()
|
|
95
|
+
return {
|
|
96
|
+
__rsc_ls_key: getKey(),
|
|
97
|
+
__rsc_ls_value: stateOrGetter,
|
|
98
|
+
__rsc_ls_lazy: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
__rsc_ls_key: getKey(),
|
|
103
|
+
__rsc_ls_value: stateOrGetter,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Use defineProperty for __rsc_ls_key to avoid Object.assign evaluating
|
|
108
|
+
// the getter during construction (before the Vite plugin sets the key).
|
|
109
|
+
Object.defineProperty(fn, "__rsc_ls_key", {
|
|
110
|
+
get: () => getKey(),
|
|
111
|
+
set: (k: string) => {
|
|
112
|
+
_key = k;
|
|
113
|
+
},
|
|
114
|
+
enumerable: true,
|
|
115
|
+
configurable: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
Object.defineProperty(fn, "__rsc_ls_flash", {
|
|
119
|
+
value: flash,
|
|
120
|
+
enumerable: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
Object.defineProperty(fn, "read", {
|
|
124
|
+
value: (): TState | undefined => {
|
|
125
|
+
if (typeof window === "undefined") return undefined;
|
|
126
|
+
return window.history.state?.[getKey()] as TState | undefined;
|
|
127
|
+
},
|
|
128
|
+
enumerable: true,
|
|
129
|
+
});
|
|
92
130
|
|
|
93
|
-
return
|
|
131
|
+
return fn as LocationStateDefinition<[TState | (() => TState)], TState>;
|
|
94
132
|
}
|
|
95
133
|
|
|
96
134
|
/**
|
|
97
135
|
* Check if a value is a LocationStateEntry
|
|
98
136
|
*/
|
|
99
|
-
export function isLocationStateEntry(
|
|
137
|
+
export function isLocationStateEntry(
|
|
138
|
+
value: unknown,
|
|
139
|
+
): value is LocationStateEntry {
|
|
100
140
|
return (
|
|
101
141
|
value !== null &&
|
|
102
142
|
typeof value === "object" &&
|
|
@@ -110,11 +150,13 @@ export function isLocationStateEntry(value: unknown): value is LocationStateEntr
|
|
|
110
150
|
* Resolve state entries into a flat object for history.state
|
|
111
151
|
*/
|
|
112
152
|
export function resolveLocationStateEntries(
|
|
113
|
-
entries: LocationStateEntry[]
|
|
153
|
+
entries: LocationStateEntry[],
|
|
114
154
|
): Record<string, unknown> {
|
|
115
155
|
const result: Record<string, unknown> = {};
|
|
116
156
|
for (const entry of entries) {
|
|
117
|
-
result[entry.__rsc_ls_key] = entry.
|
|
157
|
+
result[entry.__rsc_ls_key] = entry.__rsc_ls_lazy
|
|
158
|
+
? (entry.__rsc_ls_value as () => unknown)()
|
|
159
|
+
: entry.__rsc_ls_value;
|
|
118
160
|
}
|
|
119
161
|
return result;
|
|
120
162
|
}
|
|
@@ -10,53 +10,98 @@ export {
|
|
|
10
10
|
resolveLocationStateEntries,
|
|
11
11
|
type LocationStateEntry,
|
|
12
12
|
type LocationStateDefinition,
|
|
13
|
+
type LocationStateOptions,
|
|
13
14
|
} from "./location-state-shared.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Hook to read location state from history.state
|
|
17
18
|
*
|
|
19
|
+
* Behavior depends on the definition:
|
|
20
|
+
* - Normal state: persists across navigations, reactive to popstate
|
|
21
|
+
* - Flash state (created with { flash: true }): read once, cleared after paint
|
|
22
|
+
*
|
|
18
23
|
* Overloaded:
|
|
19
24
|
* - With definition: Returns typed state from the specific key
|
|
20
|
-
* - With type param only: Returns
|
|
25
|
+
* - With type param only: Returns plain state from history.state.state
|
|
21
26
|
*
|
|
22
27
|
* @example
|
|
23
28
|
* ```typescript
|
|
24
|
-
* //
|
|
25
|
-
* const ProductState = createLocationState<{ name: string }>(
|
|
29
|
+
* // Persistent state
|
|
30
|
+
* const ProductState = createLocationState<{ name: string }>();
|
|
26
31
|
* const state = useLocationState(ProductState);
|
|
27
|
-
* // state: { name: string } | undefined
|
|
28
32
|
*
|
|
29
|
-
* //
|
|
30
|
-
* const
|
|
33
|
+
* // Flash state (auto-clears after paint)
|
|
34
|
+
* const FlashMsg = createLocationState<{ text: string }>({ flash: true });
|
|
35
|
+
* const flash = useLocationState(FlashMsg);
|
|
36
|
+
*
|
|
37
|
+
* // Plain state access (reads from history.state.state)
|
|
38
|
+
* const state = useLocationState<{ from?: string }>();
|
|
31
39
|
* ```
|
|
32
40
|
*/
|
|
33
41
|
export function useLocationState<TArgs extends unknown[], TState>(
|
|
34
|
-
definition: LocationStateDefinition<TArgs, TState
|
|
42
|
+
definition: LocationStateDefinition<TArgs, TState>,
|
|
35
43
|
): TState | undefined;
|
|
36
44
|
export function useLocationState<T = unknown>(): T | undefined;
|
|
37
45
|
export function useLocationState<TArgs extends unknown[], TState>(
|
|
38
|
-
definition?: LocationStateDefinition<TArgs, TState
|
|
46
|
+
definition?: LocationStateDefinition<TArgs, TState>,
|
|
39
47
|
): TState | undefined {
|
|
48
|
+
const key = definition?.__rsc_ls_key;
|
|
49
|
+
const isFlash = definition?.__rsc_ls_flash ?? false;
|
|
50
|
+
|
|
40
51
|
const [state, setState] = useState<TState | undefined>(() => {
|
|
41
52
|
if (typeof window === "undefined") return undefined;
|
|
42
|
-
if (
|
|
43
|
-
return window.history.state?.[
|
|
53
|
+
if (key) {
|
|
54
|
+
return window.history.state?.[key] as TState | undefined;
|
|
44
55
|
}
|
|
45
|
-
//
|
|
56
|
+
// Plain state: stored under history.state.state
|
|
46
57
|
return window.history.state?.state as TState | undefined;
|
|
47
58
|
});
|
|
48
59
|
|
|
60
|
+
// Subscribe to popstate and programmatic state changes
|
|
49
61
|
useEffect(() => {
|
|
50
62
|
const handlePopstate = () => {
|
|
51
|
-
if (
|
|
52
|
-
setState(window.history.state?.[
|
|
63
|
+
if (key) {
|
|
64
|
+
setState(window.history.state?.[key] as TState | undefined);
|
|
53
65
|
} else {
|
|
54
66
|
setState(window.history.state?.state as TState | undefined);
|
|
55
67
|
}
|
|
56
68
|
};
|
|
69
|
+
|
|
70
|
+
// Handle programmatic state changes (same-page navigation with
|
|
71
|
+
// ctx.setLocationState where components don't remount)
|
|
72
|
+
const handleLocationState = () => {
|
|
73
|
+
if (key) {
|
|
74
|
+
const val = window.history.state?.[key] as TState | undefined;
|
|
75
|
+
if (isFlash) {
|
|
76
|
+
// For flash state, only update if there's a new value
|
|
77
|
+
if (val !== undefined) {
|
|
78
|
+
setState(val);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
setState(val);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
setState(window.history.state?.state as TState | undefined);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
57
88
|
window.addEventListener("popstate", handlePopstate);
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
window.addEventListener("__rsc_locationstate", handleLocationState);
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("popstate", handlePopstate);
|
|
92
|
+
window.removeEventListener("__rsc_locationstate", handleLocationState);
|
|
93
|
+
};
|
|
94
|
+
}, [key, isFlash]);
|
|
95
|
+
|
|
96
|
+
// Flash: clear from history.state after paint so subsequent navigations don't see it.
|
|
97
|
+
// Depends on `state` so it re-runs when state is set via the event listener.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (isFlash && key && state !== undefined) {
|
|
100
|
+
const cleaned = { ...window.history.state };
|
|
101
|
+
delete cleaned[key];
|
|
102
|
+
window.history.replaceState(cleaned, "", window.location.href);
|
|
103
|
+
}
|
|
104
|
+
}, [isFlash, key, state]);
|
|
60
105
|
|
|
61
106
|
return state;
|
|
62
107
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
createElement,
|
|
6
|
+
type Context,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Context for the current include() mount path.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context for CSP nonce propagation to client components during SSR.
|
|
5
|
+
*
|
|
6
|
+
* The SSR renderer wraps the tree with NonceContext.Provider so that
|
|
7
|
+
* client components (e.g. MetaTags) can apply nonces to inline scripts.
|
|
8
|
+
* On the browser side, no provider is needed — the default undefined
|
|
9
|
+
* is correct since CSP nonces are a server-side HTML concern.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createContext, useContext, type Context } from "react";
|
|
13
|
+
|
|
14
|
+
export const NonceContext: Context<string | undefined> = createContext<
|
|
15
|
+
string | undefined
|
|
16
|
+
>(undefined);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the CSP nonce during SSR. Returns undefined on the client.
|
|
20
|
+
*/
|
|
21
|
+
export function useNonce(): string | undefined {
|
|
22
|
+
return useContext(NonceContext);
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow equality check for selector results.
|
|
3
|
+
* Uses Object.is for value comparison (handles NaN and +-0 correctly).
|
|
4
|
+
*/
|
|
5
|
+
export function shallowEqual<T>(a: T, b: T): boolean {
|
|
6
|
+
if (Object.is(a, b)) return true;
|
|
7
|
+
if (
|
|
8
|
+
typeof a !== "object" ||
|
|
9
|
+
a === null ||
|
|
10
|
+
typeof b !== "object" ||
|
|
11
|
+
b === null
|
|
12
|
+
) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const keysA = Object.keys(a);
|
|
16
|
+
const keysB = Object.keys(b);
|
|
17
|
+
if (keysA.length !== keysB.length) return false;
|
|
18
|
+
for (const key of keysA) {
|
|
19
|
+
if (
|
|
20
|
+
!Object.hasOwn(b, key) ||
|
|
21
|
+
!Object.is((a as any)[key], (b as any)[key])
|
|
22
|
+
) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|