@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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import type { LocationStateDefinition } from "./location-state-shared.js";
|
|
5
5
|
|
|
6
|
-
// Re-export shared utilities and types
|
|
7
6
|
export {
|
|
8
7
|
createLocationState,
|
|
9
8
|
isLocationStateEntry,
|
|
@@ -13,6 +12,24 @@ export {
|
|
|
13
12
|
type LocationStateOptions,
|
|
14
13
|
} from "./location-state-shared.js";
|
|
15
14
|
|
|
15
|
+
function readLocationStateValue<TState>(
|
|
16
|
+
key: string | undefined,
|
|
17
|
+
): TState | undefined {
|
|
18
|
+
if (typeof window === "undefined") return undefined;
|
|
19
|
+
if (key) {
|
|
20
|
+
return window.history.state?.[key] as TState | undefined;
|
|
21
|
+
}
|
|
22
|
+
// Plain state: stored under history.state.state
|
|
23
|
+
return window.history.state?.state as TState | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasHydrated(): boolean {
|
|
27
|
+
return (
|
|
28
|
+
typeof document !== "undefined" &&
|
|
29
|
+
document.documentElement.hasAttribute("data-hydrated")
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* Hook to read location state from history.state
|
|
18
35
|
*
|
|
@@ -48,30 +65,33 @@ export function useLocationState<TArgs extends unknown[], TState>(
|
|
|
48
65
|
const key = definition?.__rsc_ls_key;
|
|
49
66
|
const isFlash = definition?.__rsc_ls_flash ?? false;
|
|
50
67
|
|
|
68
|
+
// Track whether the initial render returned undefined because the page
|
|
69
|
+
// hadn't hydrated yet. If so, the mount effect catches up by reading
|
|
70
|
+
// history.state once. If not, we already have the right value and must
|
|
71
|
+
// not re-read on mount — under StrictMode, the flash-cleanup effect runs
|
|
72
|
+
// before the second setup pass, so a re-read would clobber the captured
|
|
73
|
+
// value with the now-cleared `undefined`.
|
|
74
|
+
const initialReadDeferredRef = useRef(false);
|
|
75
|
+
|
|
51
76
|
const [state, setState] = useState<TState | undefined>(() => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
return
|
|
77
|
+
if (!hasHydrated()) {
|
|
78
|
+
initialReadDeferredRef.current = true;
|
|
79
|
+
return undefined;
|
|
55
80
|
}
|
|
56
|
-
|
|
57
|
-
return window.history.state?.state as TState | undefined;
|
|
81
|
+
return readLocationStateValue<TState>(key);
|
|
58
82
|
});
|
|
59
83
|
|
|
60
84
|
// Subscribe to popstate and programmatic state changes
|
|
61
85
|
useEffect(() => {
|
|
62
86
|
const handlePopstate = () => {
|
|
63
|
-
|
|
64
|
-
setState(window.history.state?.[key] as TState | undefined);
|
|
65
|
-
} else {
|
|
66
|
-
setState(window.history.state?.state as TState | undefined);
|
|
67
|
-
}
|
|
87
|
+
setState(readLocationStateValue<TState>(key));
|
|
68
88
|
};
|
|
69
89
|
|
|
70
90
|
// Handle programmatic state changes (same-page navigation with
|
|
71
91
|
// ctx.setLocationState where components don't remount)
|
|
72
92
|
const handleLocationState = () => {
|
|
73
93
|
if (key) {
|
|
74
|
-
const val =
|
|
94
|
+
const val = readLocationStateValue<TState>(key);
|
|
75
95
|
if (isFlash) {
|
|
76
96
|
// For flash state, only update if there's a new value
|
|
77
97
|
if (val !== undefined) {
|
|
@@ -81,10 +101,15 @@ export function useLocationState<TArgs extends unknown[], TState>(
|
|
|
81
101
|
setState(val);
|
|
82
102
|
}
|
|
83
103
|
} else {
|
|
84
|
-
setState(
|
|
104
|
+
setState(readLocationStateValue<TState>(key));
|
|
85
105
|
}
|
|
86
106
|
};
|
|
87
107
|
|
|
108
|
+
if (initialReadDeferredRef.current) {
|
|
109
|
+
initialReadDeferredRef.current = false;
|
|
110
|
+
setState(readLocationStateValue<TState>(key));
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
window.addEventListener("popstate", handlePopstate);
|
|
89
114
|
window.addEventListener("__rsc_locationstate", handleLocationState);
|
|
90
115
|
return () => {
|
|
@@ -24,32 +24,24 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
24
24
|
result: null,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Normalize action ID - returns the ID as-is
|
|
29
|
-
*
|
|
30
|
-
* Server actions have IDs like "hash#actionName" or "src/actions.ts#actionName".
|
|
31
|
-
* When using function references, we use the full ID for exact matching.
|
|
32
|
-
* When using strings, the event controller supports suffix matching
|
|
33
|
-
* (e.g., "addToCart" matches "hash#addToCart").
|
|
34
|
-
*/
|
|
35
|
-
function normalizeActionId(actionId: string): string {
|
|
36
|
-
return actionId;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
27
|
/**
|
|
40
28
|
* Extract action ID from a server action function or string.
|
|
41
29
|
*
|
|
42
30
|
* Actions passed as props from server components lose their metadata
|
|
43
31
|
* during RSC serialization - use a string action name instead.
|
|
32
|
+
*
|
|
33
|
+
* The extracted $$id (e.g. "hash#actionName" or "src/actions.ts#actionName")
|
|
34
|
+
* is returned as-is. Suffix-vs-exact matching against this ID happens
|
|
35
|
+
* downstream in the event controller, not here.
|
|
44
36
|
*/
|
|
45
|
-
|
|
37
|
+
function getActionId(action: ServerActionFunction | string): string {
|
|
46
38
|
invariant(
|
|
47
39
|
typeof action === "function" || typeof action === "string",
|
|
48
40
|
`useAction: action must be a function or string, got ${typeof action}`,
|
|
49
41
|
);
|
|
50
42
|
const actionId = (action as any)?.$$id;
|
|
51
43
|
if (actionId) {
|
|
52
|
-
return
|
|
44
|
+
return actionId;
|
|
53
45
|
}
|
|
54
46
|
|
|
55
47
|
// If action is a string, use it directly
|
|
@@ -162,7 +154,6 @@ export function useAction<T>(
|
|
|
162
154
|
});
|
|
163
155
|
const prevSelected = useRef(baseState);
|
|
164
156
|
prevSelected.current = baseState;
|
|
165
|
-
// useOptimistic allows immediate updates during transitions/actions
|
|
166
157
|
const [optimisticState, setOptimisticState] = useOptimistic<
|
|
167
158
|
T | TrackedActionState
|
|
168
159
|
>(null!);
|
|
@@ -9,64 +9,11 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { Handle } from "../../handle.js";
|
|
12
|
-
import {
|
|
12
|
+
import { collectHandleData } from "../../handle.js";
|
|
13
13
|
import type { HandleData } from "../types.js";
|
|
14
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
15
|
import { shallowEqual } from "./shallow-equal.js";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Resolve the collect function for a handle.
|
|
19
|
-
* Handle objects are plain { __brand, $$id } - collect is stored in the registry
|
|
20
|
-
* (populated when createHandle runs on the client).
|
|
21
|
-
*/
|
|
22
|
-
function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
23
|
-
// Look up collect from the registry (populated when the handle module is imported).
|
|
24
|
-
const registered = getCollectFn(handle.$$id);
|
|
25
|
-
if (registered) {
|
|
26
|
-
return registered as (segments: T[][]) => A;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Fall back to default flat collect with a dev warning.
|
|
30
|
-
if (process.env.NODE_ENV !== "production") {
|
|
31
|
-
console.warn(
|
|
32
|
-
`[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
|
|
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.`,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return ((segments: unknown[][]) => segments.flat()) as unknown as (
|
|
38
|
-
segments: T[][],
|
|
39
|
-
) => A;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Collect handle data from segments and transform to final value.
|
|
44
|
-
*/
|
|
45
|
-
function collectHandle<T, A>(
|
|
46
|
-
handle: Handle<T, A>,
|
|
47
|
-
data: HandleData,
|
|
48
|
-
segmentOrder: string[],
|
|
49
|
-
): A {
|
|
50
|
-
const collect = resolveCollect(handle);
|
|
51
|
-
const segmentData = data[handle.$$id];
|
|
52
|
-
|
|
53
|
-
if (!segmentData) {
|
|
54
|
-
return collect([]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Build array of segment arrays in parent -> child order
|
|
58
|
-
const segmentArrays: T[][] = [];
|
|
59
|
-
for (const segmentId of segmentOrder) {
|
|
60
|
-
const entries = segmentData[segmentId];
|
|
61
|
-
if (entries && entries.length > 0) {
|
|
62
|
-
segmentArrays.push(entries as T[]);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Call collect once with all segment data
|
|
67
|
-
return collect(segmentArrays);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
17
|
/**
|
|
71
18
|
* Hook to access collected handle data.
|
|
72
19
|
*
|
|
@@ -85,51 +32,54 @@ function collectHandle<T, A>(
|
|
|
85
32
|
* const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
86
33
|
* ```
|
|
87
34
|
*/
|
|
88
|
-
export function useHandle<T, A>(handle: Handle<T, A>): A
|
|
35
|
+
export function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
|
|
89
36
|
export function useHandle<T, A, S>(
|
|
90
37
|
handle: Handle<T, A>,
|
|
91
|
-
selector: (data: A) => S,
|
|
38
|
+
selector: (data: Rango.FlightSerialize<A>) => S,
|
|
92
39
|
): S;
|
|
93
40
|
export function useHandle<T, A, S>(
|
|
94
41
|
handle: Handle<T, A>,
|
|
95
|
-
selector?: (data: A) => S,
|
|
96
|
-
): A | S {
|
|
42
|
+
selector?: (data: Rango.FlightSerialize<A>) => S,
|
|
43
|
+
): Rango.FlightSerialize<A> | S {
|
|
97
44
|
const ctx = useContext(NavigationStoreContext);
|
|
98
45
|
|
|
99
|
-
|
|
100
|
-
const [value, setValue] = useState<A | S>(() => {
|
|
46
|
+
const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
|
|
101
47
|
if (!ctx) {
|
|
102
|
-
const collected =
|
|
48
|
+
const collected = collectHandleData(
|
|
49
|
+
handle,
|
|
50
|
+
{},
|
|
51
|
+
[],
|
|
52
|
+
) as Rango.FlightSerialize<A>;
|
|
103
53
|
return selector ? selector(collected) : collected;
|
|
104
54
|
}
|
|
105
55
|
|
|
106
|
-
// On client, use event controller state
|
|
107
56
|
const state = ctx.eventController.getHandleState();
|
|
108
|
-
const collected =
|
|
57
|
+
const collected = collectHandleData(
|
|
58
|
+
handle,
|
|
59
|
+
state.data,
|
|
60
|
+
state.segmentOrder,
|
|
61
|
+
) as Rango.FlightSerialize<A>;
|
|
109
62
|
return selector ? selector(collected) : collected;
|
|
110
63
|
});
|
|
111
64
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
112
65
|
|
|
113
|
-
// Track previous value for shallow comparison
|
|
114
66
|
const prevValueRef = useRef(value);
|
|
115
67
|
prevValueRef.current = value;
|
|
116
68
|
|
|
117
|
-
// Ref keeps the latest selector without re-subscribing on every render.
|
|
118
69
|
const selectorRef = useRef(selector);
|
|
119
70
|
selectorRef.current = selector;
|
|
120
71
|
|
|
121
|
-
// Subscribe to handle data changes (client only)
|
|
122
72
|
useEffect(() => {
|
|
123
73
|
if (!ctx) return;
|
|
124
74
|
|
|
125
75
|
// Sync current state for the (possibly new) handle so that switching
|
|
126
76
|
// handles on an idle page doesn't leave stale data from the old handle.
|
|
127
77
|
const currentHandleState = ctx.eventController.getHandleState();
|
|
128
|
-
const currentCollected =
|
|
78
|
+
const currentCollected = collectHandleData(
|
|
129
79
|
handle,
|
|
130
80
|
currentHandleState.data,
|
|
131
81
|
currentHandleState.segmentOrder,
|
|
132
|
-
)
|
|
82
|
+
) as Rango.FlightSerialize<A>;
|
|
133
83
|
const currentValue = selectorRef.current
|
|
134
84
|
? selectorRef.current(currentCollected)
|
|
135
85
|
: currentCollected;
|
|
@@ -142,7 +92,11 @@ export function useHandle<T, A, S>(
|
|
|
142
92
|
const state = ctx.eventController.getHandleState();
|
|
143
93
|
const isAction =
|
|
144
94
|
ctx.eventController.getState().inflightActions.length > 0;
|
|
145
|
-
const collected =
|
|
95
|
+
const collected = collectHandleData(
|
|
96
|
+
handle,
|
|
97
|
+
state.data,
|
|
98
|
+
state.segmentOrder,
|
|
99
|
+
) as Rango.FlightSerialize<A>;
|
|
146
100
|
const nextValue = selectorRef.current
|
|
147
101
|
? selectorRef.current(collected)
|
|
148
102
|
: collected;
|
|
@@ -82,11 +82,9 @@ export function useLinkStatus(): LinkStatus {
|
|
|
82
82
|
const linkTo = useContext(LinkContext);
|
|
83
83
|
const ctx = useContext(NavigationStoreContext);
|
|
84
84
|
|
|
85
|
-
// Get origin for URL normalization (stable across renders)
|
|
86
85
|
const origin =
|
|
87
86
|
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
88
87
|
|
|
89
|
-
// Base state for useOptimistic
|
|
90
88
|
const [basePending, setBasePending] = useState<boolean>(() => {
|
|
91
89
|
if (!ctx || linkTo === null) {
|
|
92
90
|
return false;
|
|
@@ -97,7 +95,6 @@ export function useLinkStatus(): LinkStatus {
|
|
|
97
95
|
|
|
98
96
|
const prevPending = useRef(basePending);
|
|
99
97
|
|
|
100
|
-
// useOptimistic allows immediate updates during transitions
|
|
101
98
|
const [pending, setOptimisticPending] = useOptimistic(basePending);
|
|
102
99
|
|
|
103
100
|
useEffect(() => {
|
|
@@ -105,7 +102,6 @@ export function useLinkStatus(): LinkStatus {
|
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
// Subscribe to navigation state changes
|
|
109
105
|
return ctx.eventController.subscribe(() => {
|
|
110
106
|
const state = ctx.eventController.getState();
|
|
111
107
|
const isPending = isPendingFor(linkTo, state.pendingUrl, origin);
|
|
@@ -46,14 +46,18 @@ export function useNavigation<T>(
|
|
|
46
46
|
throw new Error("useNavigation must be used within NavigationProvider");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Base state for useOptimistic
|
|
50
49
|
const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
|
|
51
50
|
const publicState = toPublicState(ctx.eventController.getState());
|
|
52
51
|
return selector ? selector(publicState) : publicState;
|
|
53
52
|
});
|
|
54
53
|
const prevState = useRef(baseValue);
|
|
55
54
|
|
|
56
|
-
//
|
|
55
|
+
// Tracks whether the most recent setOptimisticValue call pinned the value
|
|
56
|
+
// to a non-idle state. Used to decide whether to emit a release update when
|
|
57
|
+
// returning to idle, so the optimistic store doesn't stay pinned if a
|
|
58
|
+
// parent transition (e.g. <Link> click) is still pending.
|
|
59
|
+
const optimisticPinnedRef = useRef(false);
|
|
60
|
+
|
|
57
61
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
58
62
|
|
|
59
63
|
// Store selector in a ref so the subscription callback always uses the
|
|
@@ -66,7 +70,6 @@ export function useNavigation<T>(
|
|
|
66
70
|
|
|
67
71
|
// Subscribe to event controller state changes (only runs on client)
|
|
68
72
|
useEffect(() => {
|
|
69
|
-
// Subscribe to updates from event controller
|
|
70
73
|
return ctx.eventController.subscribe(() => {
|
|
71
74
|
const currentState = ctx.eventController.getState();
|
|
72
75
|
const publicState = toPublicState(currentState);
|
|
@@ -82,11 +85,25 @@ export function useNavigation<T>(
|
|
|
82
85
|
const hasInflightActions =
|
|
83
86
|
ctx.eventController.getInflightActions().size > 0;
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
const shouldPin = hasInflightActions || publicState.state !== "idle";
|
|
89
|
+
|
|
90
|
+
if (shouldPin) {
|
|
91
|
+
// Pin the optimistic store so the loading value shows immediately
|
|
92
|
+
// even if a parent transition (e.g. <Link> click) defers the
|
|
93
|
+
// urgent setBaseValue commit.
|
|
94
|
+
startTransition(() => {
|
|
95
|
+
setOptimisticValue(nextSelected);
|
|
96
|
+
});
|
|
97
|
+
optimisticPinnedRef.current = true;
|
|
98
|
+
} else if (optimisticPinnedRef.current) {
|
|
99
|
+
// Release a previously-pinned optimistic value. Without this,
|
|
100
|
+
// useOptimistic keeps returning the stale loading value while
|
|
101
|
+
// any parent transition is still pending, even after baseValue
|
|
102
|
+
// flipped to idle.
|
|
87
103
|
startTransition(() => {
|
|
88
104
|
setOptimisticValue(nextSelected);
|
|
89
105
|
});
|
|
106
|
+
optimisticPinnedRef.current = false;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
// Always update base state so UI reflects current state
|
|
@@ -4,6 +4,8 @@ import { useContext, useState, useEffect, useRef } from "react";
|
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { shallowEqual } from "./shallow-equal.js";
|
|
6
6
|
|
|
7
|
+
const EMPTY_PARAMS: Record<string, string> = Object.freeze({});
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Hook to access the current route params.
|
|
9
11
|
*
|
|
@@ -16,30 +18,38 @@ import { shallowEqual } from "./shallow-equal.js";
|
|
|
16
18
|
* const params = useParams();
|
|
17
19
|
* // { productId: "123" }
|
|
18
20
|
*
|
|
21
|
+
* // Annotate the expected shape via a generic
|
|
22
|
+
* const { productId } = useParams<{ productId: string }>();
|
|
23
|
+
*
|
|
19
24
|
* // With selector
|
|
20
25
|
* const productId = useParams(p => p.productId);
|
|
21
26
|
* ```
|
|
22
27
|
*/
|
|
23
|
-
|
|
28
|
+
// `T extends object` (not `Record<string, string | undefined>`) so that
|
|
29
|
+
// interface shapes pass the constraint — interfaces lack an implicit
|
|
30
|
+
// index signature and would otherwise be rejected. The generic is a
|
|
31
|
+
// shape annotation, not a runtime check; the body always returns the
|
|
32
|
+
// underlying params map unchanged. The default and selector input use
|
|
33
|
+
// `string | undefined` because absent optional params are omitted from
|
|
34
|
+
// the params record at runtime — the type must reflect that so callers
|
|
35
|
+
// don't write `p.locale.length` and crash when the segment is absent.
|
|
36
|
+
export function useParams<
|
|
37
|
+
T extends object = Record<string, string | undefined>,
|
|
38
|
+
>(): Readonly<T>;
|
|
24
39
|
export function useParams<T>(
|
|
25
|
-
selector: (params: Record<string, string>) => T,
|
|
40
|
+
selector: (params: Record<string, string | undefined>) => T,
|
|
26
41
|
): T;
|
|
27
42
|
export function useParams<T>(
|
|
28
|
-
selector?: (params: Record<string, string>) => T,
|
|
29
|
-
): T | Record<string, string> {
|
|
43
|
+
selector?: (params: Record<string, string | undefined>) => T,
|
|
44
|
+
): T | Record<string, string | undefined> {
|
|
30
45
|
const ctx = useContext(NavigationStoreContext);
|
|
31
46
|
|
|
32
47
|
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
33
|
-
|
|
34
|
-
return selector ? selector({}) : {};
|
|
35
|
-
}
|
|
36
|
-
const params = ctx.eventController.getParams();
|
|
48
|
+
const params = ctx ? ctx.eventController.getParams() : EMPTY_PARAMS;
|
|
37
49
|
return selector ? selector(params) : params;
|
|
38
50
|
});
|
|
39
51
|
|
|
40
52
|
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
53
|
const selectorRef = useRef(selector);
|
|
44
54
|
selectorRef.current = selector;
|
|
45
55
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import type { LocalReverseFunction } from "../../reverse.js";
|
|
5
|
+
import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
|
|
6
|
+
import { serializeSearchParams } from "../../search-params.js";
|
|
7
|
+
import { useMount } from "./use-mount.js";
|
|
8
|
+
import { useParams } from "./use-params.js";
|
|
9
|
+
|
|
10
|
+
type RouteEntry = string | { readonly path: string };
|
|
11
|
+
type LocalRouteMap = Readonly<Record<string, RouteEntry>>;
|
|
12
|
+
|
|
13
|
+
function getPattern(entry: RouteEntry | undefined): string | undefined {
|
|
14
|
+
if (entry === undefined) return undefined;
|
|
15
|
+
return typeof entry === "string" ? entry : entry.path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Join an include mount prefix with a mount-relative pattern.
|
|
20
|
+
*
|
|
21
|
+
* `pattern === "/"` is the index of the local module — under a non-root
|
|
22
|
+
* mount it must collapse so `/` under `/blog` becomes `/blog`, not
|
|
23
|
+
* `/blog/`. This matches `ctx.reverse(".index")` on the server.
|
|
24
|
+
*/
|
|
25
|
+
function joinMount(mount: string, pattern: string): string {
|
|
26
|
+
if (pattern === "/") {
|
|
27
|
+
if (mount === "" || mount === "/") return "/";
|
|
28
|
+
return mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
29
|
+
}
|
|
30
|
+
const normalizedMount = mount === "/" ? "" : mount.replace(/\/+$/, "");
|
|
31
|
+
return normalizedMount + pattern;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mount-aware reverse function for a locally-imported `routes` map.
|
|
36
|
+
*
|
|
37
|
+
* The `routes` map you pass IS the scope: `reverse("name")` looks the name up
|
|
38
|
+
* in that map (verbatim), prefixes the result with the surrounding `include()`
|
|
39
|
+
* mount path via `useMount()`, and substitutes params — auto-filling from the
|
|
40
|
+
* current matched route's params, with explicit params overriding. A module's
|
|
41
|
+
* components can therefore reverse their own routes without knowing where the
|
|
42
|
+
* module is mounted: include it under any prefix and the URLs resolve correctly.
|
|
43
|
+
*
|
|
44
|
+
* The leading dot is optional and cosmetic: `reverse("post")` and
|
|
45
|
+
* `reverse(".post")` resolve identically. The dot exists only as a readability
|
|
46
|
+
* convention and for parity with `ctx.reverse(".name")` on the server; here the
|
|
47
|
+
* passed map is the scope, so there is no separate global namespace to
|
|
48
|
+
* disambiguate and the dot carries no meaning.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* "use client";
|
|
53
|
+
* import { Link, useReverse } from "@rangojs/router/client";
|
|
54
|
+
* import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
55
|
+
*
|
|
56
|
+
* function BlogNav() {
|
|
57
|
+
* const reverse = useReverse(blogRoutes);
|
|
58
|
+
* return (
|
|
59
|
+
* <>
|
|
60
|
+
* <Link to={reverse("index")}>Blog</Link>
|
|
61
|
+
* <Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
62
|
+
* </>
|
|
63
|
+
* );
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function useReverse<const TRoutes extends LocalRouteMap>(
|
|
68
|
+
routes: TRoutes,
|
|
69
|
+
): LocalReverseFunction<TRoutes> {
|
|
70
|
+
const mount = useMount();
|
|
71
|
+
const currentParams = useParams();
|
|
72
|
+
|
|
73
|
+
return useCallback(
|
|
74
|
+
((
|
|
75
|
+
name: string,
|
|
76
|
+
explicitParams?: Record<string, string | undefined>,
|
|
77
|
+
search?: Record<string, unknown>,
|
|
78
|
+
): string => {
|
|
79
|
+
// The leading dot is optional. The passed map IS the scope, so a dot to
|
|
80
|
+
// signal "local" is unnecessary — "detail" and ".detail" resolve the same.
|
|
81
|
+
// A dot is accepted (and stripped) for readability / ctx.reverse parity.
|
|
82
|
+
const lookupName = name.startsWith(".") ? name.slice(1) : name;
|
|
83
|
+
const entry = (routes as LocalRouteMap)[lookupName];
|
|
84
|
+
const pattern = getPattern(entry);
|
|
85
|
+
if (pattern === undefined) {
|
|
86
|
+
throw new Error(`Unknown route: "${name}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const joined = joinMount(mount, pattern);
|
|
90
|
+
|
|
91
|
+
const mergedParams = explicitParams
|
|
92
|
+
? { ...currentParams, ...explicitParams }
|
|
93
|
+
: currentParams;
|
|
94
|
+
|
|
95
|
+
const substituted = substitutePatternParams(joined, mergedParams, name);
|
|
96
|
+
|
|
97
|
+
if (search) {
|
|
98
|
+
const qs = serializeSearchParams(search);
|
|
99
|
+
if (qs) return `${substituted}?${qs}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return substituted;
|
|
103
|
+
}) as LocalReverseFunction<TRoutes>,
|
|
104
|
+
[routes, mount, currentParams],
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useContext, useMemo } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { prefetchDirect } from "../prefetch/fetch.js";
|
|
6
|
+
import { getAppVersion } from "../app-version.js";
|
|
6
7
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -12,6 +13,11 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
|
12
13
|
* useRouter() do not re-render on navigation state changes.
|
|
13
14
|
* For reactive navigation state, use useNavigation() instead.
|
|
14
15
|
*
|
|
16
|
+
* Methods read `basename` from the context on each call. It is set once from
|
|
17
|
+
* the initial payload and is stable within a session — a cross-app navigation
|
|
18
|
+
* is a full document load (X-RSC-Reload), so the target app mounts fresh with
|
|
19
|
+
* its own basename.
|
|
20
|
+
*
|
|
15
21
|
* @example
|
|
16
22
|
* ```tsx
|
|
17
23
|
* const router = useRouter();
|
|
@@ -28,36 +34,65 @@ export function useRouter(): RouterInstance {
|
|
|
28
34
|
throw new Error("useRouter must be used within NavigationProvider");
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
// Stable reference: ctx is
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
// Stable reference: ctx itself is stable, and reads on each method call
|
|
38
|
+
// pick up live basename values from the context (backed by a live ref
|
|
39
|
+
// in NavigationProvider), so app-switch transitions are reflected without
|
|
40
|
+
// recreating this object.
|
|
41
|
+
return useMemo<RouterInstance>(() => {
|
|
42
|
+
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
43
|
+
function withBasename(url: string): string {
|
|
44
|
+
const bn = ctx!.basename;
|
|
45
|
+
if (!bn || !url.startsWith("/") || url.startsWith(bn + "/") || url === bn)
|
|
46
|
+
return url;
|
|
47
|
+
return url === "/" ? bn : bn + url;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
34
51
|
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
35
|
-
return ctx.navigate(url, { ...options, replace: false });
|
|
52
|
+
return ctx.navigate(withBasename(url), { ...options, replace: false });
|
|
36
53
|
},
|
|
37
54
|
|
|
38
55
|
replace(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
39
|
-
return ctx.navigate(url, { ...options, replace: true });
|
|
56
|
+
return ctx.navigate(withBasename(url), { ...options, replace: true });
|
|
40
57
|
},
|
|
41
58
|
|
|
42
59
|
refresh(): Promise<void> {
|
|
43
60
|
return ctx.refresh();
|
|
44
61
|
},
|
|
45
62
|
|
|
46
|
-
prefetch(url: string): void {
|
|
63
|
+
prefetch(url: string, options?: { key?: ":source" }): void {
|
|
47
64
|
const segmentState = ctx.store?.getSegmentState();
|
|
48
65
|
if (segmentState) {
|
|
49
|
-
prefetchDirect(
|
|
66
|
+
prefetchDirect(
|
|
67
|
+
withBasename(url),
|
|
68
|
+
segmentState.currentSegmentIds,
|
|
69
|
+
getAppVersion(),
|
|
70
|
+
ctx.store?.getRouterId?.(),
|
|
71
|
+
options?.key,
|
|
72
|
+
);
|
|
50
73
|
}
|
|
51
74
|
},
|
|
52
75
|
|
|
53
76
|
back(): void {
|
|
54
|
-
|
|
77
|
+
// Avoid escaping the host on the first entry of this session.
|
|
78
|
+
// Prefer the Navigation API; fall back to the router-stamped
|
|
79
|
+
// history.state.idx (set by pushHistoryWithIdx) for older browsers.
|
|
80
|
+
const nav = (window as { navigation?: { canGoBack: boolean } })
|
|
81
|
+
.navigation;
|
|
82
|
+
const canGoBack =
|
|
83
|
+
nav && typeof nav.canGoBack === "boolean"
|
|
84
|
+
? nav.canGoBack
|
|
85
|
+
: ((window.history.state as { idx?: number } | null)?.idx ?? 0) > 0;
|
|
86
|
+
if (canGoBack) {
|
|
87
|
+
window.history.back();
|
|
88
|
+
} else {
|
|
89
|
+
ctx.navigate(withBasename("/"), { replace: true });
|
|
90
|
+
}
|
|
55
91
|
},
|
|
56
92
|
|
|
57
93
|
forward(): void {
|
|
58
94
|
window.history.forward();
|
|
59
95
|
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
);
|
|
96
|
+
};
|
|
97
|
+
}, []);
|
|
63
98
|
}
|
|
@@ -24,9 +24,6 @@ import type { ReadonlyURLSearchParams } from "../types.js";
|
|
|
24
24
|
export function useSearchParams(): ReadonlyURLSearchParams {
|
|
25
25
|
const ctx = useContext(NavigationStoreContext);
|
|
26
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
27
|
const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
|
|
31
28
|
() => new URLSearchParams(),
|
|
32
29
|
);
|
|
@@ -41,12 +38,10 @@ export function useSearchParams(): ReadonlyURLSearchParams {
|
|
|
41
38
|
const nextSearch = location.searchParams.toString();
|
|
42
39
|
if (nextSearch !== prevSearch.current) {
|
|
43
40
|
prevSearch.current = nextSearch;
|
|
44
|
-
// Create a snapshot so callers cannot mutate the source URLSearchParams
|
|
45
41
|
setSearchParams(new URLSearchParams(nextSearch));
|
|
46
42
|
}
|
|
47
43
|
};
|
|
48
44
|
|
|
49
|
-
// Sync on mount (picks up search params from browser URL)
|
|
50
45
|
update();
|
|
51
46
|
|
|
52
47
|
return ctx.eventController.subscribe(update);
|