@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- 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 +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- 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 +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- 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 +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- 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/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- 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 +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- 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 +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- 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 +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -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/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 +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- 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 +31 -23
- 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 +63 -9
- package/src/index.ts +64 -9
- 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-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- 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 +0 -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 +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- 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 +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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 +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- 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
|
@@ -13,9 +13,10 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
|
13
13
|
* useRouter() do not re-render on navigation state changes.
|
|
14
14
|
* For reactive navigation state, use useNavigation() instead.
|
|
15
15
|
*
|
|
16
|
-
* Methods read `basename` from the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
19
20
|
*
|
|
20
21
|
* @example
|
|
21
22
|
* ```tsx
|
|
@@ -59,7 +60,7 @@ export function useRouter(): RouterInstance {
|
|
|
59
60
|
return ctx.refresh();
|
|
60
61
|
},
|
|
61
62
|
|
|
62
|
-
prefetch(url: string): void {
|
|
63
|
+
prefetch(url: string, options?: { key?: ":source" }): void {
|
|
63
64
|
const segmentState = ctx.store?.getSegmentState();
|
|
64
65
|
if (segmentState) {
|
|
65
66
|
prefetchDirect(
|
|
@@ -67,12 +68,26 @@ export function useRouter(): RouterInstance {
|
|
|
67
68
|
segmentState.currentSegmentIds,
|
|
68
69
|
getAppVersion(),
|
|
69
70
|
ctx.store?.getRouterId?.(),
|
|
71
|
+
options?.key,
|
|
70
72
|
);
|
|
71
73
|
}
|
|
72
74
|
},
|
|
73
75
|
|
|
74
76
|
back(): void {
|
|
75
|
-
|
|
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
|
+
}
|
|
76
91
|
},
|
|
77
92
|
|
|
78
93
|
forward(): void {
|
|
@@ -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);
|
|
@@ -86,31 +86,20 @@ export function useSegments<T>(
|
|
|
86
86
|
const selectorRef = useRef(selector);
|
|
87
87
|
selectorRef.current = selector;
|
|
88
88
|
|
|
89
|
-
// Track selector identity to detect when the selector function changes.
|
|
90
|
-
// Only then do we eagerly recompute during render to avoid staleness.
|
|
91
|
-
// Without this guard, no-selector mode causes infinite re-renders because
|
|
92
|
-
// buildSegmentsState creates fresh arrays that fail Object.is checks.
|
|
93
89
|
const prevSelectorIdentity = useRef(selector);
|
|
94
90
|
|
|
95
|
-
// Cache SegmentsState to stabilize nested references (path, segmentIds
|
|
96
|
-
// arrays) so selectors returning composite values don't cause spurious
|
|
97
|
-
// render-time setState calls.
|
|
98
91
|
const segmentsCache = useRef<{
|
|
99
92
|
location: URL;
|
|
100
93
|
routeSegmentIds: string[];
|
|
101
94
|
state: SegmentsState;
|
|
102
95
|
} | null>(null);
|
|
103
96
|
|
|
104
|
-
// Recompute selected value from current store state and apply selector.
|
|
105
|
-
// Shared by the render-time eager check and the subscription callback.
|
|
106
97
|
function recompute(
|
|
107
98
|
sel: ((state: SegmentsState) => T) | undefined,
|
|
108
99
|
): T | SegmentsState {
|
|
109
100
|
const location = ctx!.eventController.getLocation();
|
|
110
101
|
const handleState = ctx!.eventController.getHandleState();
|
|
111
102
|
|
|
112
|
-
// Reuse cached state when inputs haven't changed by reference,
|
|
113
|
-
// keeping array/object references stable for composite selectors.
|
|
114
103
|
const cache = segmentsCache.current;
|
|
115
104
|
let segmentsState: SegmentsState;
|
|
116
105
|
if (
|
|
@@ -165,8 +154,6 @@ export function useSegments<T>(
|
|
|
165
154
|
unsubscribeNav();
|
|
166
155
|
unsubscribeHandles();
|
|
167
156
|
};
|
|
168
|
-
// Stable subscription: selector changes are handled via selectorRef,
|
|
169
|
-
// state comparison uses prevState ref. No re-subscribe needed.
|
|
170
157
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
171
158
|
}, []);
|
|
172
159
|
|
|
@@ -24,6 +24,51 @@ export function emptyResponse(): Response {
|
|
|
24
24
|
return new Response(null, { status: 200 });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether an RSC content response carries a server-stamped router identity
|
|
29
|
+
* (`X-RSC-Router-Id`) that DIFFERS from the id this client expects (its own
|
|
30
|
+
* routerId, also sent as `_rsc_rid`). Pre-decode integrity check: lets a caller
|
|
31
|
+
* refuse a foreign app's payload before `createFromFetch` imports its chunks.
|
|
32
|
+
*
|
|
33
|
+
* True ONLY when both the header and the expected id are present and differ. An
|
|
34
|
+
* absent header (control-only reload/redirect responses are not stamped) or an
|
|
35
|
+
* absent expected id (e.g. before the client is seeded) is a pass-through —
|
|
36
|
+
* never a false reject.
|
|
37
|
+
*/
|
|
38
|
+
export function isForeignRouterId(
|
|
39
|
+
response: Response,
|
|
40
|
+
expectedId: string | undefined,
|
|
41
|
+
): boolean {
|
|
42
|
+
const got = response.headers.get("X-RSC-Router-Id");
|
|
43
|
+
if (!got || !expectedId) return false;
|
|
44
|
+
return got !== expectedId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle the X-RSC-Reload control header (server requests a full page reload on
|
|
49
|
+
* a version mismatch). Returns a short-circuit response when the header is
|
|
50
|
+
* present -- emptyResponse() if the URL was blocked by origin validation, or a
|
|
51
|
+
* never-resolving promise while the page reloads -- and null when absent, so
|
|
52
|
+
* the caller continues processing (e.g. the X-RSC-Redirect check). Scoped to
|
|
53
|
+
* X-RSC-Reload only; redirect handling differs between callers.
|
|
54
|
+
*/
|
|
55
|
+
export function handleReloadHeader(
|
|
56
|
+
response: Response,
|
|
57
|
+
opts: { onBlocked: () => void; onReload: (url: string) => void },
|
|
58
|
+
): Response | Promise<Response> | null {
|
|
59
|
+
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
60
|
+
if (reload === "blocked") {
|
|
61
|
+
opts.onBlocked();
|
|
62
|
+
return emptyResponse();
|
|
63
|
+
}
|
|
64
|
+
if (reload) {
|
|
65
|
+
opts.onReload(reload.url);
|
|
66
|
+
window.location.href = reload.url;
|
|
67
|
+
return new Promise<Response>(() => {});
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
27
72
|
/**
|
|
28
73
|
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
74
|
* Returns a new Response with one branch; the other is consumed to detect
|
|
@@ -31,11 +76,17 @@ export function emptyResponse(): Response {
|
|
|
31
76
|
*
|
|
32
77
|
* If the response has no body, onComplete fires synchronously.
|
|
33
78
|
* If signal is provided, an abort cancels the tracking reader.
|
|
79
|
+
*
|
|
80
|
+
* `silent` suppresses the stream-error log. Prefetch passes it: a speculative,
|
|
81
|
+
* low-priority prefetch that is aborted or never consumed can error its stream
|
|
82
|
+
* benignly, which is not worth surfacing. The fresh-navigation path keeps the
|
|
83
|
+
* log (default), where a stream error reflects a real failed navigation.
|
|
34
84
|
*/
|
|
35
85
|
export function teeWithCompletion(
|
|
36
86
|
response: Response,
|
|
37
87
|
onComplete: () => void,
|
|
38
88
|
signal?: AbortSignal,
|
|
89
|
+
silent = false,
|
|
39
90
|
): Response {
|
|
40
91
|
if (!response.body) {
|
|
41
92
|
onComplete();
|
|
@@ -59,7 +110,7 @@ export function teeWithCompletion(
|
|
|
59
110
|
onComplete();
|
|
60
111
|
}
|
|
61
112
|
})().catch((error) => {
|
|
62
|
-
if (!signal?.aborted) {
|
|
113
|
+
if (!silent && !signal?.aborted) {
|
|
63
114
|
console.error("[Browser] Error reading tracking stream:", error);
|
|
64
115
|
}
|
|
65
116
|
onComplete();
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
generateHistoryKey,
|
|
9
9
|
} from "./navigation-store.js";
|
|
10
10
|
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
11
12
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
13
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
14
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
@@ -22,7 +23,9 @@ import type {
|
|
|
22
23
|
import type { EventController } from "./event-controller.js";
|
|
23
24
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
25
|
import { initRangoState } from "./rango-state.js";
|
|
26
|
+
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
25
27
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
26
29
|
import { setAppVersion } from "./app-version.js";
|
|
27
30
|
import {
|
|
28
31
|
isInterceptSegment,
|
|
@@ -116,10 +119,12 @@ export interface BrowserAppContext {
|
|
|
116
119
|
/** App version for prefetch version mismatch detection */
|
|
117
120
|
version?: string;
|
|
118
121
|
/**
|
|
119
|
-
*
|
|
120
|
-
* NavigationProvider
|
|
121
|
-
*
|
|
122
|
-
*
|
|
122
|
+
* App-shell ref, read through on each render so renderSegments and the
|
|
123
|
+
* NavigationProvider see rootLayout/basename/version without closing over a
|
|
124
|
+
* stale snapshot. Set once from the initial payload and not swapped within a
|
|
125
|
+
* session: a cross-app navigation is a full document load (X-RSC-Reload), so
|
|
126
|
+
* the target app establishes its own shell on load. Theme, warmup, and
|
|
127
|
+
* prefetch TTL are document-lifetime too (see AppShell).
|
|
123
128
|
*/
|
|
124
129
|
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
125
130
|
}
|
|
@@ -128,7 +133,7 @@ export interface BrowserAppContext {
|
|
|
128
133
|
let browserAppContext: BrowserAppContext | null = null;
|
|
129
134
|
|
|
130
135
|
/**
|
|
131
|
-
* Initialize the browser app. Must be called before rendering
|
|
136
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
132
137
|
*
|
|
133
138
|
* This function:
|
|
134
139
|
* - Loads the initial RSC payload from the stream
|
|
@@ -172,6 +177,12 @@ export async function initBrowserApp(
|
|
|
172
177
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
173
178
|
});
|
|
174
179
|
|
|
180
|
+
// Register the active store on the module-level handle and wire the
|
|
181
|
+
// jar-divergence observer before any getRangoState() read can detect a
|
|
182
|
+
// cross-tab/server rotation. There is no global store singleton, so this
|
|
183
|
+
// handle is the live reference.
|
|
184
|
+
registerNavigationStore(store);
|
|
185
|
+
|
|
175
186
|
// Seed router identity from the initial SSR payload so the first
|
|
176
187
|
// cross-app SPA navigation can detect the app switch.
|
|
177
188
|
if (initialPayload.metadata?.routerId) {
|
|
@@ -212,11 +223,11 @@ export async function initBrowserApp(
|
|
|
212
223
|
// Create composable utilities
|
|
213
224
|
const client = createNavigationClient(deps);
|
|
214
225
|
|
|
215
|
-
// Capture the per-router app-shell
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
226
|
+
// Capture the per-router app-shell. rootLayout, basename, and version live
|
|
227
|
+
// here and are read through the ref at call time rather than closed over.
|
|
228
|
+
// It is set once from the initial payload and not swapped within a session:
|
|
229
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the
|
|
230
|
+
// target app establishes its own shell on load.
|
|
220
231
|
const version = initialPayload.metadata?.version;
|
|
221
232
|
const appShellRef = createAppShellRef({
|
|
222
233
|
routerId: initialPayload.metadata?.routerId,
|
|
@@ -225,10 +236,11 @@ export async function initBrowserApp(
|
|
|
225
236
|
version,
|
|
226
237
|
});
|
|
227
238
|
|
|
228
|
-
// Initialize the
|
|
229
|
-
//
|
|
230
|
-
// namespaces the
|
|
231
|
-
|
|
239
|
+
// Initialize the rango state cookie for cache invalidation. The build version
|
|
240
|
+
// busts cached prefetches on deploy; the server-resolved cookie name
|
|
241
|
+
// namespaces the cookie so sibling apps on the same origin don't collide
|
|
242
|
+
// (falls back to the bare default prefix if metadata lacks the name).
|
|
243
|
+
initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
|
|
232
244
|
setAppVersion(version);
|
|
233
245
|
|
|
234
246
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
@@ -238,9 +250,14 @@ export async function initBrowserApp(
|
|
|
238
250
|
initPrefetchCache(prefetchCacheTTL);
|
|
239
251
|
}
|
|
240
252
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
253
|
+
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
|
254
|
+
// client chunks (same createFromFetch the navigation client uses).
|
|
255
|
+
setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
|
|
256
|
+
|
|
257
|
+
// Create a bound renderSegments that reads rootLayout through the shell ref.
|
|
258
|
+
// The shell is set once at init and not swapped within a session (a cross-app
|
|
259
|
+
// navigation is a full document load), so this always renders this app's
|
|
260
|
+
// Document; reading through the ref just avoids closing over a stale value.
|
|
244
261
|
const renderSegments = (
|
|
245
262
|
segments: ResolvedSegment[],
|
|
246
263
|
options?: RenderSegmentsOptions,
|
|
@@ -264,7 +281,13 @@ export async function initBrowserApp(
|
|
|
264
281
|
renderSegments,
|
|
265
282
|
onNavigate: (url, options) => {
|
|
266
283
|
if (!navigateFn) {
|
|
267
|
-
|
|
284
|
+
// Navigation bridge not wired yet: hard-navigate, but re-validate
|
|
285
|
+
// same-origin defensively so this init-window fallback cannot become an
|
|
286
|
+
// open redirect (the normal path validates inside the navigation bridge).
|
|
287
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
288
|
+
if (safe) {
|
|
289
|
+
window.location.href = safe;
|
|
290
|
+
}
|
|
268
291
|
return Promise.resolve();
|
|
269
292
|
}
|
|
270
293
|
return navigateFn(url, options);
|
|
@@ -280,7 +303,6 @@ export async function initBrowserApp(
|
|
|
280
303
|
onUpdate: (update) => store.emitUpdate(update),
|
|
281
304
|
renderSegments,
|
|
282
305
|
version: version,
|
|
283
|
-
appShellRef,
|
|
284
306
|
});
|
|
285
307
|
|
|
286
308
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -325,11 +347,11 @@ export async function initBrowserApp(
|
|
|
325
347
|
// full lifecycle (fetching + streaming, before commit) without
|
|
326
348
|
// blocking on server actions.
|
|
327
349
|
if (eventController.getState().isNavigating) {
|
|
328
|
-
console.log("[
|
|
350
|
+
console.log("[Rango] HMR: Skipping — navigation in progress");
|
|
329
351
|
return;
|
|
330
352
|
}
|
|
331
353
|
|
|
332
|
-
console.log("[
|
|
354
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
333
355
|
|
|
334
356
|
const abort = new AbortController();
|
|
335
357
|
hmrAbort = abort;
|
|
@@ -364,11 +386,18 @@ export async function initBrowserApp(
|
|
|
364
386
|
// Update version BEFORE rebuilding state so that
|
|
365
387
|
// clearHistoryCache() runs first, then the fresh segment
|
|
366
388
|
// cache entry we create below survives.
|
|
389
|
+
//
|
|
390
|
+
// Compare against the bridge's live version, not the init-time
|
|
391
|
+
// `version` const: after the first HMR bump the const is stale, so a
|
|
392
|
+
// later update with an unchanged version would otherwise re-clear the
|
|
393
|
+
// cache and re-broadcast across tabs/apps. The live read fires only
|
|
394
|
+
// on a genuine version change.
|
|
367
395
|
const newVersion = payload.metadata.version;
|
|
368
|
-
|
|
396
|
+
const currentVersion = navigationBridge.getVersion();
|
|
397
|
+
if (newVersion && newVersion !== currentVersion) {
|
|
369
398
|
console.log(
|
|
370
|
-
"[
|
|
371
|
-
|
|
399
|
+
"[Rango] HMR: version changed",
|
|
400
|
+
currentVersion,
|
|
372
401
|
"→",
|
|
373
402
|
newVersion,
|
|
374
403
|
"clearing caches",
|
|
@@ -376,6 +405,13 @@ export async function initBrowserApp(
|
|
|
376
405
|
navigationBridge.updateVersion(newVersion);
|
|
377
406
|
}
|
|
378
407
|
|
|
408
|
+
// Apply only partial segment updates. A non-partial payload during
|
|
409
|
+
// HMR is transient: the worker route table is still rebuilding after
|
|
410
|
+
// the edit, so the URL momentarily resolves to not-found/catch-all.
|
|
411
|
+
// Skip it -- the debounced follow-up refetch returns the settled
|
|
412
|
+
// route's partial payload and renders it below. We never reload here:
|
|
413
|
+
// a paramless document GET would run the SSR path and surface the
|
|
414
|
+
// not-found page during that same transient.
|
|
379
415
|
if (payload.metadata?.isPartial) {
|
|
380
416
|
const segments = payload.metadata.segments || [];
|
|
381
417
|
const matched = payload.metadata.matched || [];
|
|
@@ -415,10 +451,10 @@ export async function initBrowserApp(
|
|
|
415
451
|
|
|
416
452
|
await streamComplete;
|
|
417
453
|
handle.complete(new URL(window.location.href));
|
|
418
|
-
console.log("[
|
|
454
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
419
455
|
} catch (err) {
|
|
420
456
|
if (abort.signal.aborted) return;
|
|
421
|
-
console.warn("[
|
|
457
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
422
458
|
window.location.reload();
|
|
423
459
|
return;
|
|
424
460
|
} finally {
|
|
@@ -430,7 +466,7 @@ export async function initBrowserApp(
|
|
|
430
466
|
});
|
|
431
467
|
}
|
|
432
468
|
|
|
433
|
-
// Store context for
|
|
469
|
+
// Store context for Rango component
|
|
434
470
|
const context: BrowserAppContext = {
|
|
435
471
|
store,
|
|
436
472
|
eventController,
|
|
@@ -454,7 +490,7 @@ export async function initBrowserApp(
|
|
|
454
490
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
455
491
|
if (!browserAppContext) {
|
|
456
492
|
throw new Error(
|
|
457
|
-
"
|
|
493
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
458
494
|
);
|
|
459
495
|
}
|
|
460
496
|
return browserAppContext;
|
|
@@ -468,18 +504,18 @@ export function resetBrowserAppContext(): void {
|
|
|
468
504
|
}
|
|
469
505
|
|
|
470
506
|
/**
|
|
471
|
-
* Props for the
|
|
507
|
+
* Props for the Rango component
|
|
472
508
|
*/
|
|
473
|
-
export interface
|
|
509
|
+
export interface RangoProps {}
|
|
474
510
|
|
|
475
511
|
/**
|
|
476
|
-
*
|
|
512
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
477
513
|
*
|
|
478
514
|
* Must be called after initBrowserApp() has completed.
|
|
479
515
|
*
|
|
480
516
|
* @example
|
|
481
517
|
* ```tsx
|
|
482
|
-
* import { initBrowserApp,
|
|
518
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
483
519
|
* import { rscStream } from "rsc-html-stream/client";
|
|
484
520
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
485
521
|
*
|
|
@@ -489,14 +525,14 @@ export interface RSCRouterProps {}
|
|
|
489
525
|
* hydrateRoot(
|
|
490
526
|
* document,
|
|
491
527
|
* <React.StrictMode>
|
|
492
|
-
* <
|
|
528
|
+
* <Rango />
|
|
493
529
|
* </React.StrictMode>
|
|
494
530
|
* );
|
|
495
531
|
* }
|
|
496
532
|
* main();
|
|
497
533
|
* ```
|
|
498
534
|
*/
|
|
499
|
-
export function
|
|
535
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
500
536
|
const {
|
|
501
537
|
store,
|
|
502
538
|
eventController,
|
|
@@ -332,6 +332,8 @@ export function scrollToHash(): boolean {
|
|
|
332
332
|
* Scroll to top of page
|
|
333
333
|
*/
|
|
334
334
|
export function scrollToTop(): void {
|
|
335
|
+
if (typeof window === "undefined") return;
|
|
336
|
+
if (typeof window.scrollTo !== "function") return;
|
|
335
337
|
window.scrollTo(0, 0);
|
|
336
338
|
}
|
|
337
339
|
|
|
@@ -374,20 +376,26 @@ export function handleNavigationEnd(options: {
|
|
|
374
376
|
// Fall through to hash or top if no saved position
|
|
375
377
|
}
|
|
376
378
|
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
379
|
+
// scrollToHash / scrollToTop run synchronously here.
|
|
380
|
+
// handleNavigationEnd is invoked from NavigationProvider's
|
|
381
|
+
// useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
|
|
382
|
+
// captured by the upcoming paint AND by startViewTransition's snapshot.
|
|
383
|
+
// Deferring via rAF here pushed the call past the snapshot capture,
|
|
384
|
+
// making forward navigations wrapped in a layout/route view transition
|
|
385
|
+
// skip scroll-to-top — the live DOM scrolled but the captured snapshot
|
|
386
|
+
// was at the previous scroll position, so the user-facing page stayed
|
|
387
|
+
// visually clamped at the source page's scrollY (often the new tree's
|
|
388
|
+
// max scroll for tall→short navs). Y=0 / a hash element are robust
|
|
389
|
+
// against unmeasured layout, so sync scroll is correct here even
|
|
390
|
+
// before the new tree's scrollHeight settles.
|
|
391
|
+
//
|
|
392
|
+
// (The restore branch above keeps deferToNextPaint because savedY
|
|
393
|
+
// depends on the new tree's max scroll; sync scrollTo against an
|
|
394
|
+
// unmeasured DOM would clamp savedY to whatever the old/zero max was.)
|
|
395
|
+
if (scrollToHash()) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
scrollToTop();
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
/**
|
|
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
|
|
|
48
48
|
|
|
49
49
|
if (cachedCategory !== incomingCategory) {
|
|
50
50
|
console.warn(
|
|
51
|
-
`[
|
|
51
|
+
`[Rango] Tree structure mismatch detected in ${context} ` +
|
|
52
52
|
`for segment "${cached.id}": loading category changed from ` +
|
|
53
53
|
`"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
|
|
54
54
|
`"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
|
|
@@ -64,7 +64,7 @@ export function assertSegmentStructure(
|
|
|
64
64
|
const incomingHasMount = !!incoming.mountPath;
|
|
65
65
|
if (cachedHasMount !== incomingHasMount) {
|
|
66
66
|
console.warn(
|
|
67
|
-
`[
|
|
67
|
+
`[Rango] MountContextProvider mismatch detected in ${context} ` +
|
|
68
68
|
`for segment "${cached.id}": mountPath changed from ` +
|
|
69
69
|
`${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
|
|
70
70
|
`${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
|