@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.70
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 +4951 -930
- 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/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 +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +173 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +204 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +257 -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 +296 -558
- package/src/browser/navigation-client.ts +179 -69
- 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 +328 -313
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +150 -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 +230 -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 +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 +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 +221 -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 +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 +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 +105 -179
- 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 +223 -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 +351 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +982 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +434 -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 +70 -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 +435 -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 +154 -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 +459 -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 +55 -33
- 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 +743 -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 +1373 -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-system.tsx +165 -17
- package/src/server/context.ts +315 -58
- 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 +109 -0
- package/src/types/segments.ts +150 -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 +346 -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 -1129
- 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 +207 -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
|
@@ -0,0 +1,76 @@
|
|
|
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 { getAppVersion } from "../app-version.js";
|
|
7
|
+
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to access router actions (push, replace, refresh, prefetch, back, forward).
|
|
11
|
+
*
|
|
12
|
+
* Returns a STABLE reference that never changes, so components using
|
|
13
|
+
* useRouter() do not re-render on navigation state changes.
|
|
14
|
+
* For reactive navigation state, use useNavigation() instead.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const router = useRouter();
|
|
19
|
+
* router.push("/products");
|
|
20
|
+
* router.replace("/login", { scroll: false });
|
|
21
|
+
* router.prefetch("/dashboard");
|
|
22
|
+
* router.back();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function useRouter(): RouterInstance {
|
|
26
|
+
const ctx = useContext(NavigationStoreContext);
|
|
27
|
+
|
|
28
|
+
if (!ctx) {
|
|
29
|
+
throw new Error("useRouter must be used within NavigationProvider");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
|
|
33
|
+
return useMemo<RouterInstance>(() => {
|
|
34
|
+
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
35
|
+
function withBasename(url: string): string {
|
|
36
|
+
const bn = ctx!.basename;
|
|
37
|
+
if (!bn || !url.startsWith("/") || url.startsWith(bn + "/") || url === bn)
|
|
38
|
+
return url;
|
|
39
|
+
return url === "/" ? bn : bn + url;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
44
|
+
return ctx.navigate(withBasename(url), { ...options, replace: false });
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
replace(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
48
|
+
return ctx.navigate(withBasename(url), { ...options, replace: true });
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
refresh(): Promise<void> {
|
|
52
|
+
return ctx.refresh();
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
prefetch(url: string): void {
|
|
56
|
+
const segmentState = ctx.store?.getSegmentState();
|
|
57
|
+
if (segmentState) {
|
|
58
|
+
prefetchDirect(
|
|
59
|
+
withBasename(url),
|
|
60
|
+
segmentState.currentSegmentIds,
|
|
61
|
+
getAppVersion(),
|
|
62
|
+
ctx.store?.getRouterId?.(),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
back(): void {
|
|
68
|
+
window.history.back();
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
forward(): void {
|
|
72
|
+
window.history.forward();
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Segments state returned by useSegments hook
|
|
@@ -15,65 +16,6 @@ export interface SegmentsState {
|
|
|
15
16
|
location: URL;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
* SSR module-level state.
|
|
20
|
-
* Populated by initSegmentsSync before React renders.
|
|
21
|
-
* Used by useState initializer during SSR.
|
|
22
|
-
*/
|
|
23
|
-
let ssrSegmentOrder: string[] = [];
|
|
24
|
-
let ssrPathname: string = "/";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Filter segment IDs to only include routes and layouts.
|
|
28
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
29
|
-
*/
|
|
30
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
31
|
-
return matched.filter((id) => {
|
|
32
|
-
if (id.includes(".@")) return false;
|
|
33
|
-
if (/D\d+\./.test(id)) return false;
|
|
34
|
-
return true;
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Initialize segments data synchronously for SSR.
|
|
40
|
-
* Called before rendering to populate state for useState initializer.
|
|
41
|
-
*
|
|
42
|
-
* @param matched - Segment order from RSC metadata
|
|
43
|
-
* @param pathname - Current pathname
|
|
44
|
-
*/
|
|
45
|
-
export function initSegmentsSync(matched?: string[], pathname?: string): void {
|
|
46
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
47
|
-
ssrPathname = pathname ?? "/";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Shallow equality check for selector results
|
|
52
|
-
*/
|
|
53
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
54
|
-
if (Object.is(a, b)) return true;
|
|
55
|
-
if (
|
|
56
|
-
typeof a !== "object" ||
|
|
57
|
-
a === null ||
|
|
58
|
-
typeof b !== "object" ||
|
|
59
|
-
b === null
|
|
60
|
-
) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
const keysA = Object.keys(a);
|
|
64
|
-
const keysB = Object.keys(b);
|
|
65
|
-
if (keysA.length !== keysB.length) return false;
|
|
66
|
-
for (const key of keysA) {
|
|
67
|
-
if (
|
|
68
|
-
!Object.hasOwn(b, key) ||
|
|
69
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
70
|
-
) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
19
|
/**
|
|
78
20
|
* Parse pathname into path segments
|
|
79
21
|
* /shop/products/123 → ["shop", "products", "123"]
|
|
@@ -87,7 +29,7 @@ function parsePathname(pathname: string): string[] {
|
|
|
87
29
|
*/
|
|
88
30
|
function buildSegmentsState(
|
|
89
31
|
location: URL,
|
|
90
|
-
segmentOrder: string[]
|
|
32
|
+
segmentOrder: string[],
|
|
91
33
|
): SegmentsState {
|
|
92
34
|
return {
|
|
93
35
|
path: parsePathname(location.pathname),
|
|
@@ -96,18 +38,6 @@ function buildSegmentsState(
|
|
|
96
38
|
};
|
|
97
39
|
}
|
|
98
40
|
|
|
99
|
-
/**
|
|
100
|
-
* Build SSR state from module-level variables
|
|
101
|
-
*/
|
|
102
|
-
function buildSsrState(): SegmentsState {
|
|
103
|
-
const location = new URL(ssrPathname, "http://localhost");
|
|
104
|
-
return {
|
|
105
|
-
path: parsePathname(ssrPathname),
|
|
106
|
-
segmentIds: ssrSegmentOrder,
|
|
107
|
-
location,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
41
|
/**
|
|
112
42
|
* Hook to access current route segments with optional selector for performance
|
|
113
43
|
*
|
|
@@ -127,62 +57,115 @@ function buildSsrState(): SegmentsState {
|
|
|
127
57
|
export function useSegments(): SegmentsState;
|
|
128
58
|
export function useSegments<T>(selector: (state: SegmentsState) => T): T;
|
|
129
59
|
export function useSegments<T>(
|
|
130
|
-
selector?: (state: SegmentsState) => T
|
|
60
|
+
selector?: (state: SegmentsState) => T,
|
|
131
61
|
): T | SegmentsState {
|
|
132
62
|
const ctx = useContext(NavigationStoreContext);
|
|
133
63
|
|
|
134
|
-
// Build initial state from
|
|
64
|
+
// Build initial state from event controller when context exists.
|
|
65
|
+
// Inlined rather than calling recompute() because the segmentsCache ref
|
|
66
|
+
// is not yet initialized during the useState initializer.
|
|
135
67
|
const [state, setState] = useState<T | SegmentsState>(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
return selector ? selector(
|
|
68
|
+
if (!ctx) {
|
|
69
|
+
const fallbackLocation = new URL("/", "http://localhost");
|
|
70
|
+
const fallbackState = buildSegmentsState(fallbackLocation, []);
|
|
71
|
+
return selector ? selector(fallbackState) : fallbackState;
|
|
140
72
|
}
|
|
141
|
-
|
|
142
|
-
const navState = ctx.eventController.getState();
|
|
73
|
+
const location = ctx.eventController.getLocation();
|
|
143
74
|
const handleState = ctx.eventController.getHandleState();
|
|
144
75
|
const segmentsState = buildSegmentsState(
|
|
145
|
-
|
|
146
|
-
handleState.segmentOrder
|
|
76
|
+
location as URL,
|
|
77
|
+
handleState.segmentOrder,
|
|
147
78
|
);
|
|
148
79
|
return selector ? selector(segmentsState) : segmentsState;
|
|
149
80
|
});
|
|
150
81
|
|
|
151
82
|
const prevState = useRef(state);
|
|
83
|
+
const selectorRef = useRef(selector);
|
|
84
|
+
selectorRef.current = selector;
|
|
85
|
+
|
|
86
|
+
// Track selector identity to detect when the selector function changes.
|
|
87
|
+
// Only then do we eagerly recompute during render to avoid staleness.
|
|
88
|
+
// Without this guard, no-selector mode causes infinite re-renders because
|
|
89
|
+
// buildSegmentsState creates fresh arrays that fail Object.is checks.
|
|
90
|
+
const prevSelectorIdentity = useRef(selector);
|
|
91
|
+
|
|
92
|
+
// Cache SegmentsState to stabilize nested references (path, segmentIds
|
|
93
|
+
// arrays) so selectors returning composite values don't cause spurious
|
|
94
|
+
// render-time setState calls.
|
|
95
|
+
const segmentsCache = useRef<{
|
|
96
|
+
location: URL;
|
|
97
|
+
segmentOrder: string[];
|
|
98
|
+
state: SegmentsState;
|
|
99
|
+
} | null>(null);
|
|
100
|
+
|
|
101
|
+
// Recompute selected value from current store state and apply selector.
|
|
102
|
+
// Shared by the render-time eager check and the subscription callback.
|
|
103
|
+
function recompute(
|
|
104
|
+
sel: ((state: SegmentsState) => T) | undefined,
|
|
105
|
+
): T | SegmentsState {
|
|
106
|
+
const location = ctx!.eventController.getLocation();
|
|
107
|
+
const handleState = ctx!.eventController.getHandleState();
|
|
108
|
+
|
|
109
|
+
// Reuse cached state when inputs haven't changed by reference,
|
|
110
|
+
// keeping array/object references stable for composite selectors.
|
|
111
|
+
const cache = segmentsCache.current;
|
|
112
|
+
let segmentsState: SegmentsState;
|
|
113
|
+
if (
|
|
114
|
+
cache &&
|
|
115
|
+
cache.location === location &&
|
|
116
|
+
cache.segmentOrder === handleState.segmentOrder
|
|
117
|
+
) {
|
|
118
|
+
segmentsState = cache.state;
|
|
119
|
+
} else {
|
|
120
|
+
segmentsState = buildSegmentsState(
|
|
121
|
+
location as URL,
|
|
122
|
+
handleState.segmentOrder,
|
|
123
|
+
);
|
|
124
|
+
segmentsCache.current = {
|
|
125
|
+
location: location as URL,
|
|
126
|
+
segmentOrder: handleState.segmentOrder,
|
|
127
|
+
state: segmentsState,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return sel ? sel(segmentsState) : segmentsState;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ctx && selector !== prevSelectorIdentity.current) {
|
|
134
|
+
prevSelectorIdentity.current = selector;
|
|
135
|
+
const nextSelected = recompute(selector);
|
|
136
|
+
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
137
|
+
prevState.current = nextSelected;
|
|
138
|
+
setState(nextSelected);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
152
141
|
|
|
153
|
-
// Subscribe to
|
|
142
|
+
// Subscribe to store changes. The eager block above handles selector
|
|
143
|
+
// changes and SSR drift, so no initial updateState() call is needed.
|
|
154
144
|
useEffect(() => {
|
|
155
145
|
if (!ctx) {
|
|
156
146
|
return;
|
|
157
147
|
}
|
|
158
148
|
|
|
159
149
|
const updateState = () => {
|
|
160
|
-
const
|
|
161
|
-
const handleState = ctx.eventController.getHandleState();
|
|
162
|
-
const segmentsState = buildSegmentsState(
|
|
163
|
-
navState.location as URL,
|
|
164
|
-
handleState.segmentOrder
|
|
165
|
-
);
|
|
166
|
-
const nextSelected = selector ? selector(segmentsState) : segmentsState;
|
|
167
|
-
|
|
150
|
+
const nextSelected = recompute(selectorRef.current);
|
|
168
151
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
169
152
|
prevState.current = nextSelected;
|
|
170
153
|
setState(nextSelected);
|
|
171
154
|
}
|
|
172
155
|
};
|
|
173
156
|
|
|
174
|
-
// Initial update in case SSR state differs from client state
|
|
175
|
-
updateState();
|
|
176
|
-
|
|
177
|
-
// Subscribe to both state sources
|
|
178
157
|
const unsubscribeNav = ctx.eventController.subscribe(updateState);
|
|
179
|
-
const unsubscribeHandles =
|
|
158
|
+
const unsubscribeHandles =
|
|
159
|
+
ctx.eventController.subscribeToHandles(updateState);
|
|
180
160
|
|
|
181
161
|
return () => {
|
|
182
162
|
unsubscribeNav();
|
|
183
163
|
unsubscribeHandles();
|
|
184
164
|
};
|
|
185
|
-
|
|
165
|
+
// Stable subscription: selector changes are handled via selectorRef,
|
|
166
|
+
// state comparison uses prevState ref. No re-subscribe needed.
|
|
167
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
+
}, []);
|
|
186
169
|
|
|
187
170
|
return state as T | SegmentsState;
|
|
188
171
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
2
|
+
|
|
3
|
+
type HeaderResult = { url: string } | "blocked" | null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
|
|
7
|
+
* Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
|
|
8
|
+
*/
|
|
9
|
+
export function extractRscHeaderUrl(
|
|
10
|
+
response: Response,
|
|
11
|
+
header: string,
|
|
12
|
+
): HeaderResult {
|
|
13
|
+
const raw = response.headers.get(header);
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
const url = validateRedirectOrigin(raw, window.location.origin);
|
|
16
|
+
return url ? { url } : "blocked";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Empty 200 response that won't choke Flight parsing.
|
|
21
|
+
* Used when a header URL is blocked by origin validation.
|
|
22
|
+
*/
|
|
23
|
+
export function emptyResponse(): Response {
|
|
24
|
+
return new Response(null, { status: 200 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
|
+
* Returns a new Response with one branch; the other is consumed to detect
|
|
30
|
+
* end-of-stream, calling onComplete when done.
|
|
31
|
+
*
|
|
32
|
+
* If the response has no body, onComplete fires synchronously.
|
|
33
|
+
* If signal is provided, an abort cancels the tracking reader.
|
|
34
|
+
*/
|
|
35
|
+
export function teeWithCompletion(
|
|
36
|
+
response: Response,
|
|
37
|
+
onComplete: () => void,
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): Response {
|
|
40
|
+
if (!response.body) {
|
|
41
|
+
onComplete();
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [rscStream, trackingStream] = response.body.tee();
|
|
46
|
+
|
|
47
|
+
(async () => {
|
|
48
|
+
const reader = trackingStream.getReader();
|
|
49
|
+
const onAbort = signal ? reader.cancel.bind(reader) : undefined;
|
|
50
|
+
if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
|
|
51
|
+
try {
|
|
52
|
+
while (true) {
|
|
53
|
+
const { done } = await reader.read();
|
|
54
|
+
if (done) break;
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
if (onAbort) signal!.removeEventListener("abort", onAbort);
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
onComplete();
|
|
60
|
+
}
|
|
61
|
+
})().catch((error) => {
|
|
62
|
+
if (!signal?.aborted) {
|
|
63
|
+
console.error("[Browser] Error reading tracking stream:", error);
|
|
64
|
+
}
|
|
65
|
+
onComplete();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return new Response(rscStream, {
|
|
69
|
+
headers: response.headers,
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
});
|
|
73
|
+
}
|