@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430
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 +5 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4474 -867
- package/package.json +60 -51
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +50 -21
- 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/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +89 -30
- package/skills/loader/SKILL.md +388 -38
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +78 -1
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +226 -14
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +318 -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/event-controller.ts +87 -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 +285 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +258 -308
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +185 -73
- package/src/browser/react/NavigationProvider.tsx +51 -11
- package/src/browser/react/context.ts +6 -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 +32 -79
- 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 +22 -63
- 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 +63 -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 +107 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +504 -599
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +109 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -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 +411 -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 +469 -0
- package/src/build/route-types/scan-filter.ts +78 -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 +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -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 +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +106 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +15 -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 +153 -19
- package/src/index.ts +211 -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 +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +211 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +59 -8
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +374 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +148 -35
- package/src/router/match-api.ts +620 -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 +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -28
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -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 +289 -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 +77 -3
- package/src/router.ts +692 -4257
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +764 -754
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +38 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +25 -13
- package/src/server/context.ts +182 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +430 -70
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +100 -31
- package/src/static-handler.ts +114 -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 +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -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 +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -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 +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -1133
- package/src/vite/plugin-types.ts +131 -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 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -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 +254 -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 +510 -0
- package/src/vite/router-discovery.ts +785 -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 +189 -0
- package/src/vite/utils/shared-utils.ts +169 -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
|
@@ -12,40 +12,15 @@ import type { Handle } from "../../handle.js";
|
|
|
12
12
|
import { getCollectFn } from "../../handle.js";
|
|
13
13
|
import type { HandleData } from "../types.js";
|
|
14
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* SSR module-level state.
|
|
18
|
-
* Populated by initHandleDataSync before React renders.
|
|
19
|
-
* Used by useState initializer during SSR.
|
|
20
|
-
*/
|
|
21
|
-
let ssrHandleData: HandleData = {};
|
|
22
|
-
let ssrSegmentOrder: string[] = [];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Filter segment IDs to only include routes and layouts.
|
|
26
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
27
|
-
*/
|
|
28
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
29
|
-
return matched.filter((id) => {
|
|
30
|
-
if (id.includes(".@")) return false;
|
|
31
|
-
if (/D\d+\./.test(id)) return false;
|
|
32
|
-
return true;
|
|
33
|
-
});
|
|
34
|
-
}
|
|
15
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
35
16
|
|
|
36
17
|
/**
|
|
37
18
|
* Resolve the collect function for a handle.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* on the client), then fall back to flat array default.
|
|
19
|
+
* Handle objects are plain { __brand, $$id } - collect is stored in the registry
|
|
20
|
+
* (populated when createHandle runs on the client).
|
|
41
21
|
*/
|
|
42
22
|
function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
43
|
-
|
|
44
|
-
return handle.collect;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Handle was deserialized from RSC prop (toJSON stripped collect).
|
|
48
|
-
// Try the registry first (populated if the handle module was imported on client).
|
|
23
|
+
// Look up collect from the registry (populated when the handle module is imported).
|
|
49
24
|
const registered = getCollectFn(handle.$$id);
|
|
50
25
|
if (registered) {
|
|
51
26
|
return registered as (segments: T[][]) => A;
|
|
@@ -55,11 +30,13 @@ function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
|
55
30
|
if (process.env.NODE_ENV !== "production") {
|
|
56
31
|
console.warn(
|
|
57
32
|
`[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
|
|
58
|
-
|
|
59
|
-
|
|
33
|
+
`function could not be resolved. Falling back to flat array. ` +
|
|
34
|
+
`Import the handle module in a client component to register its collect function.`,
|
|
60
35
|
);
|
|
61
36
|
}
|
|
62
|
-
return ((segments: unknown[][]) => segments.flat()) as unknown as (
|
|
37
|
+
return ((segments: unknown[][]) => segments.flat()) as unknown as (
|
|
38
|
+
segments: T[][],
|
|
39
|
+
) => A;
|
|
63
40
|
}
|
|
64
41
|
|
|
65
42
|
/**
|
|
@@ -68,7 +45,7 @@ function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
|
68
45
|
function collectHandle<T, A>(
|
|
69
46
|
handle: Handle<T, A>,
|
|
70
47
|
data: HandleData,
|
|
71
|
-
segmentOrder: string[]
|
|
48
|
+
segmentOrder: string[],
|
|
72
49
|
): A {
|
|
73
50
|
const collect = resolveCollect(handle);
|
|
74
51
|
const segmentData = data[handle.$$id];
|
|
@@ -90,45 +67,6 @@ function collectHandle<T, A>(
|
|
|
90
67
|
return collect(segmentArrays);
|
|
91
68
|
}
|
|
92
69
|
|
|
93
|
-
/**
|
|
94
|
-
* Shallow equality check for selector results.
|
|
95
|
-
*/
|
|
96
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
97
|
-
if (Object.is(a, b)) return true;
|
|
98
|
-
if (
|
|
99
|
-
typeof a !== "object" ||
|
|
100
|
-
a === null ||
|
|
101
|
-
typeof b !== "object" ||
|
|
102
|
-
b === null
|
|
103
|
-
) {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
const keysA = Object.keys(a);
|
|
107
|
-
const keysB = Object.keys(b);
|
|
108
|
-
if (keysA.length !== keysB.length) return false;
|
|
109
|
-
for (const key of keysA) {
|
|
110
|
-
if (
|
|
111
|
-
!Object.hasOwn(b, key) ||
|
|
112
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
113
|
-
) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Initialize handle data synchronously for SSR.
|
|
122
|
-
* Called before rendering to populate state for useState initializer.
|
|
123
|
-
*
|
|
124
|
-
* @param data - Handle data from RSC payload
|
|
125
|
-
* @param matched - Segment order for reduction
|
|
126
|
-
*/
|
|
127
|
-
export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
128
|
-
ssrHandleData = data;
|
|
129
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
70
|
/**
|
|
133
71
|
* Hook to access collected handle data.
|
|
134
72
|
*
|
|
@@ -150,19 +88,18 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
|
150
88
|
export function useHandle<T, A>(handle: Handle<T, A>): A;
|
|
151
89
|
export function useHandle<T, A, S>(
|
|
152
90
|
handle: Handle<T, A>,
|
|
153
|
-
selector: (data: A) => S
|
|
91
|
+
selector: (data: A) => S,
|
|
154
92
|
): S;
|
|
155
93
|
export function useHandle<T, A, S>(
|
|
156
94
|
handle: Handle<T, A>,
|
|
157
|
-
selector?: (data: A) => S
|
|
95
|
+
selector?: (data: A) => S,
|
|
158
96
|
): A | S {
|
|
159
97
|
const ctx = useContext(NavigationStoreContext);
|
|
160
98
|
|
|
161
|
-
// Initial state from
|
|
99
|
+
// Initial state from context event controller, or empty fallback without provider.
|
|
162
100
|
const [value, setValue] = useState<A | S>(() => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
|
|
101
|
+
if (!ctx) {
|
|
102
|
+
const collected = collectHandle(handle, {}, []);
|
|
166
103
|
return selector ? selector(collected) : collected;
|
|
167
104
|
}
|
|
168
105
|
|
|
@@ -177,7 +114,7 @@ export function useHandle<T, A, S>(
|
|
|
177
114
|
const prevValueRef = useRef(value);
|
|
178
115
|
prevValueRef.current = value;
|
|
179
116
|
|
|
180
|
-
//
|
|
117
|
+
// Ref keeps the latest selector without re-subscribing on every render.
|
|
181
118
|
const selectorRef = useRef(selector);
|
|
182
119
|
selectorRef.current = selector;
|
|
183
120
|
|
|
@@ -185,6 +122,22 @@ export function useHandle<T, A, S>(
|
|
|
185
122
|
useEffect(() => {
|
|
186
123
|
if (!ctx) return;
|
|
187
124
|
|
|
125
|
+
// Sync current state for the (possibly new) handle so that switching
|
|
126
|
+
// handles on an idle page doesn't leave stale data from the old handle.
|
|
127
|
+
const currentHandleState = ctx.eventController.getHandleState();
|
|
128
|
+
const currentCollected = collectHandle(
|
|
129
|
+
handle,
|
|
130
|
+
currentHandleState.data,
|
|
131
|
+
currentHandleState.segmentOrder,
|
|
132
|
+
);
|
|
133
|
+
const currentValue = selectorRef.current
|
|
134
|
+
? selectorRef.current(currentCollected)
|
|
135
|
+
: currentCollected;
|
|
136
|
+
if (!shallowEqual(currentValue, prevValueRef.current)) {
|
|
137
|
+
prevValueRef.current = currentValue;
|
|
138
|
+
setValue(currentValue);
|
|
139
|
+
}
|
|
140
|
+
|
|
188
141
|
return ctx.eventController.subscribeToHandles(() => {
|
|
189
142
|
const state = ctx.eventController.getHandleState();
|
|
190
143
|
const isAction =
|
|
@@ -34,7 +34,7 @@ import { useMount } from "./use-mount.js";
|
|
|
34
34
|
* }
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export function useHref(): (path:
|
|
37
|
+
export function useHref(): (path: `/${string}`) => string {
|
|
38
38
|
const mount = useMount();
|
|
39
|
-
return (path:
|
|
39
|
+
return (path: `/${string}`) => href(path as ValidPaths, mount);
|
|
40
40
|
}
|
|
@@ -16,7 +16,9 @@ import { NavigationStoreContext } from "./context.js";
|
|
|
16
16
|
* Context for Link component to provide its destination URL
|
|
17
17
|
* Used by useLinkStatus to determine if this specific link is pending
|
|
18
18
|
*/
|
|
19
|
-
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
19
|
+
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
20
|
+
null,
|
|
21
|
+
);
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Link status returned by useLinkStatus hook
|
|
@@ -46,7 +48,7 @@ function normalizeUrl(url: string, origin: string): string {
|
|
|
46
48
|
function isPendingFor(
|
|
47
49
|
linkTo: string | null,
|
|
48
50
|
pendingUrl: string | null,
|
|
49
|
-
origin: string
|
|
51
|
+
origin: string,
|
|
50
52
|
): boolean {
|
|
51
53
|
if (linkTo === null || pendingUrl === null) {
|
|
52
54
|
return false;
|
|
@@ -81,9 +83,8 @@ export function useLinkStatus(): LinkStatus {
|
|
|
81
83
|
const ctx = useContext(NavigationStoreContext);
|
|
82
84
|
|
|
83
85
|
// Get origin for URL normalization (stable across renders)
|
|
84
|
-
const origin =
|
|
85
|
-
? window.location.origin
|
|
86
|
-
: "http://localhost";
|
|
86
|
+
const origin =
|
|
87
|
+
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
87
88
|
|
|
88
89
|
// Base state for useOptimistic
|
|
89
90
|
const [basePending, setBasePending] = useState<boolean>(() => {
|
|
@@ -9,36 +9,10 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { NavigationStoreContext } from "./context.js";
|
|
12
|
-
import
|
|
12
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
13
|
+
import type { PublicNavigationState } from "../types.js";
|
|
13
14
|
import type { DerivedNavigationState } from "../event-controller.js";
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Shallow equality check for selector results
|
|
17
|
-
*/
|
|
18
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
19
|
-
if (Object.is(a, b)) return true;
|
|
20
|
-
if (
|
|
21
|
-
typeof a !== "object" ||
|
|
22
|
-
a === null ||
|
|
23
|
-
typeof b !== "object" ||
|
|
24
|
-
b === null
|
|
25
|
-
) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const keysA = Object.keys(a);
|
|
29
|
-
const keysB = Object.keys(b);
|
|
30
|
-
if (keysA.length !== keysB.length) return false;
|
|
31
|
-
for (const key of keysA) {
|
|
32
|
-
if (
|
|
33
|
-
!Object.hasOwn(b, key) ||
|
|
34
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
35
|
-
) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
16
|
/**
|
|
43
17
|
* Convert derived state to public version (strips inflightActions)
|
|
44
18
|
*/
|
|
@@ -47,45 +21,29 @@ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
|
|
|
47
21
|
return publicState;
|
|
48
22
|
}
|
|
49
23
|
|
|
50
|
-
|
|
51
24
|
/**
|
|
52
|
-
*
|
|
53
|
-
*/
|
|
54
|
-
export interface NavigationMethods {
|
|
55
|
-
navigate: (url: string, options?: NavigateOptions) => Promise<void>;
|
|
56
|
-
refresh: () => Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Full value returned when no selector is provided
|
|
61
|
-
*/
|
|
62
|
-
export type NavigationValue = PublicNavigationState & NavigationMethods;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Hook to access navigation state with optional selector for performance
|
|
25
|
+
* Hook to access reactive navigation state with optional selector for performance.
|
|
66
26
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
27
|
+
* Returns state only. For actions (push, replace, refresh, prefetch),
|
|
28
|
+
* use useRouter() instead.
|
|
69
29
|
*
|
|
70
30
|
* @example
|
|
71
31
|
* ```tsx
|
|
72
|
-
* const state = useNavigation(
|
|
32
|
+
* const { state, location } = useNavigation();
|
|
73
33
|
* const isLoading = useNavigation(nav => nav.state === 'loading');
|
|
74
34
|
* ```
|
|
75
35
|
*/
|
|
76
|
-
export function useNavigation():
|
|
36
|
+
export function useNavigation(): PublicNavigationState;
|
|
77
37
|
export function useNavigation<T>(
|
|
78
38
|
selector: (state: PublicNavigationState) => T,
|
|
79
39
|
): T;
|
|
80
40
|
export function useNavigation<T>(
|
|
81
41
|
selector?: (state: PublicNavigationState) => T,
|
|
82
|
-
): T |
|
|
42
|
+
): T | PublicNavigationState {
|
|
83
43
|
const ctx = useContext(NavigationStoreContext);
|
|
84
44
|
|
|
85
45
|
if (!ctx) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
"useNavigation must be used within NavigationStoreContext.Provider"
|
|
88
|
-
);
|
|
46
|
+
throw new Error("useNavigation must be used within NavigationProvider");
|
|
89
47
|
}
|
|
90
48
|
|
|
91
49
|
// Base state for useOptimistic
|
|
@@ -98,13 +56,23 @@ export function useNavigation<T>(
|
|
|
98
56
|
// useOptimistic allows immediate updates during transitions/actions
|
|
99
57
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
100
58
|
|
|
59
|
+
// Store selector in a ref so the subscription callback always uses the
|
|
60
|
+
// latest selector without re-subscribing on every render (inline functions
|
|
61
|
+
// have a new identity each render). This is event-driven by design: the
|
|
62
|
+
// value updates when the store emits, not when the selector changes.
|
|
63
|
+
// Between events there is nothing new to select from.
|
|
64
|
+
const selectorRef = useRef(selector);
|
|
65
|
+
selectorRef.current = selector;
|
|
66
|
+
|
|
101
67
|
// Subscribe to event controller state changes (only runs on client)
|
|
102
68
|
useEffect(() => {
|
|
103
69
|
// Subscribe to updates from event controller
|
|
104
70
|
return ctx.eventController.subscribe(() => {
|
|
105
71
|
const currentState = ctx.eventController.getState();
|
|
106
72
|
const publicState = toPublicState(currentState);
|
|
107
|
-
const nextSelected =
|
|
73
|
+
const nextSelected = selectorRef.current
|
|
74
|
+
? selectorRef.current(publicState)
|
|
75
|
+
: publicState;
|
|
108
76
|
|
|
109
77
|
// Check if selected value has changed
|
|
110
78
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
@@ -125,16 +93,7 @@ export function useNavigation<T>(
|
|
|
125
93
|
setBaseValue(nextSelected);
|
|
126
94
|
}
|
|
127
95
|
});
|
|
128
|
-
}, [
|
|
129
|
-
|
|
130
|
-
// If no selector, include navigation methods
|
|
131
|
-
if (!selector) {
|
|
132
|
-
return {
|
|
133
|
-
...(value as PublicNavigationState),
|
|
134
|
-
navigate: ctx.navigate,
|
|
135
|
-
refresh: ctx.refresh,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
96
|
+
}, []);
|
|
138
97
|
|
|
139
|
-
return value as T;
|
|
98
|
+
return value as T | PublicNavigationState;
|
|
140
99
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access the current route params.
|
|
9
|
+
*
|
|
10
|
+
* Returns the merged route params from the matched route.
|
|
11
|
+
* Updates when navigation completes, not during pending navigation.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // Route: /products/:productId
|
|
16
|
+
* const params = useParams();
|
|
17
|
+
* // { productId: "123" }
|
|
18
|
+
*
|
|
19
|
+
* // With selector
|
|
20
|
+
* const productId = useParams(p => p.productId);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useParams(): Record<string, string>;
|
|
24
|
+
export function useParams<T>(
|
|
25
|
+
selector: (params: Record<string, string>) => T,
|
|
26
|
+
): T;
|
|
27
|
+
export function useParams<T>(
|
|
28
|
+
selector?: (params: Record<string, string>) => T,
|
|
29
|
+
): T | Record<string, string> {
|
|
30
|
+
const ctx = useContext(NavigationStoreContext);
|
|
31
|
+
|
|
32
|
+
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
33
|
+
if (!ctx) {
|
|
34
|
+
return selector ? selector({}) : {};
|
|
35
|
+
}
|
|
36
|
+
const params = ctx.eventController.getParams();
|
|
37
|
+
return selector ? selector(params) : params;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const prevValue = useRef(value);
|
|
41
|
+
// Ref keeps the latest selector without re-subscribing. Event-driven by
|
|
42
|
+
// design: value updates on store events, not on selector identity change.
|
|
43
|
+
const selectorRef = useRef(selector);
|
|
44
|
+
selectorRef.current = selector;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!ctx) return;
|
|
48
|
+
|
|
49
|
+
const update = () => {
|
|
50
|
+
const params = ctx.eventController.getParams();
|
|
51
|
+
const next = selectorRef.current ? selectorRef.current(params) : params;
|
|
52
|
+
|
|
53
|
+
if (!shallowEqual(next, prevValue.current)) {
|
|
54
|
+
prevValue.current = next;
|
|
55
|
+
setValue(next);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
update();
|
|
60
|
+
|
|
61
|
+
return ctx.eventController.subscribe(update);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to access the current pathname.
|
|
8
|
+
*
|
|
9
|
+
* Returns the committed pathname string (excludes search params and hash).
|
|
10
|
+
* Updates when navigation completes, not during pending navigation.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const pathname = usePathname();
|
|
15
|
+
* // "/products/123"
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function usePathname(): string {
|
|
19
|
+
const ctx = useContext(NavigationStoreContext);
|
|
20
|
+
|
|
21
|
+
const [pathname, setPathname] = useState<string>(() => {
|
|
22
|
+
if (!ctx) {
|
|
23
|
+
return "/";
|
|
24
|
+
}
|
|
25
|
+
return (ctx.eventController.getState().location as URL).pathname;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const prevPathname = useRef(pathname);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!ctx) return;
|
|
32
|
+
|
|
33
|
+
const update = () => {
|
|
34
|
+
const next = (ctx.eventController.getState().location as URL).pathname;
|
|
35
|
+
if (next !== prevPathname.current) {
|
|
36
|
+
prevPathname.current = next;
|
|
37
|
+
setPathname(next);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
update();
|
|
42
|
+
|
|
43
|
+
return ctx.eventController.subscribe(update);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return pathname;
|
|
47
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useMemo } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
import { prefetchDirect } from "../prefetch/fetch.js";
|
|
6
|
+
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to access router actions (push, replace, refresh, prefetch, back, forward).
|
|
10
|
+
*
|
|
11
|
+
* Returns a STABLE reference that never changes, so components using
|
|
12
|
+
* useRouter() do not re-render on navigation state changes.
|
|
13
|
+
* For reactive navigation state, use useNavigation() instead.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const router = useRouter();
|
|
18
|
+
* router.push("/products");
|
|
19
|
+
* router.replace("/login", { scroll: false });
|
|
20
|
+
* router.prefetch("/dashboard");
|
|
21
|
+
* router.back();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useRouter(): RouterInstance {
|
|
25
|
+
const ctx = useContext(NavigationStoreContext);
|
|
26
|
+
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new Error("useRouter must be used within NavigationProvider");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
|
|
32
|
+
return useMemo<RouterInstance>(
|
|
33
|
+
() => ({
|
|
34
|
+
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
35
|
+
return ctx.navigate(url, { ...options, replace: false });
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
replace(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
39
|
+
return ctx.navigate(url, { ...options, replace: true });
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
refresh(): Promise<void> {
|
|
43
|
+
return ctx.refresh();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
prefetch(url: string): void {
|
|
47
|
+
const segmentState = ctx.store?.getSegmentState();
|
|
48
|
+
if (segmentState) {
|
|
49
|
+
prefetchDirect(url, segmentState.currentSegmentIds, ctx.version);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
back(): void {
|
|
54
|
+
window.history.back();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
forward(): void {
|
|
58
|
+
window.history.forward();
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
|
+
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
import type { ReadonlyURLSearchParams } from "../types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access the current URL search params.
|
|
9
|
+
*
|
|
10
|
+
* Returns a read-only URLSearchParams object from the committed location.
|
|
11
|
+
* Updates when navigation completes, not during pending navigation.
|
|
12
|
+
*
|
|
13
|
+
* Note: During SSR the search params are not available (the server only sends
|
|
14
|
+
* the pathname). The hook returns empty params during SSR and syncs from
|
|
15
|
+
* the browser URL on mount.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const searchParams = useSearchParams();
|
|
20
|
+
* const query = searchParams.get("q"); // "react"
|
|
21
|
+
* const page = searchParams.get("page"); // "2"
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useSearchParams(): ReadonlyURLSearchParams {
|
|
25
|
+
const ctx = useContext(NavigationStoreContext);
|
|
26
|
+
|
|
27
|
+
// Always initialize with empty URLSearchParams to match SSR output
|
|
28
|
+
// and avoid hydration mismatch. The useEffect below syncs from
|
|
29
|
+
// the real URL after hydration.
|
|
30
|
+
const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
|
|
31
|
+
() => new URLSearchParams(),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const prevSearch = useRef("");
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!ctx) return;
|
|
38
|
+
|
|
39
|
+
const update = () => {
|
|
40
|
+
const location = ctx.eventController.getState().location as URL;
|
|
41
|
+
const nextSearch = location.searchParams.toString();
|
|
42
|
+
if (nextSearch !== prevSearch.current) {
|
|
43
|
+
prevSearch.current = nextSearch;
|
|
44
|
+
// Create a snapshot so callers cannot mutate the source URLSearchParams
|
|
45
|
+
setSearchParams(new URLSearchParams(nextSearch));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Sync on mount (picks up search params from browser URL)
|
|
50
|
+
update();
|
|
51
|
+
|
|
52
|
+
return ctx.eventController.subscribe(update);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return searchParams;
|
|
56
|
+
}
|