@rangojs/router 0.0.0-experimental.0f44aca1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- 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 +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NavigationBridge,
|
|
3
|
+
NavigationBridgeConfig,
|
|
4
|
+
NavigateOptionsInternal,
|
|
5
|
+
ResolvedSegment,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { startTransition } from "react";
|
|
9
|
+
import {
|
|
10
|
+
createNavigationTransaction,
|
|
11
|
+
resolveNavigationState,
|
|
12
|
+
} from "./navigation-transaction.js";
|
|
13
|
+
import { buildHistoryState } from "./history-state.js";
|
|
14
|
+
import {
|
|
15
|
+
handleNavigationStart,
|
|
16
|
+
handleNavigationEnd,
|
|
17
|
+
ensureHistoryKey,
|
|
18
|
+
} from "./scroll-restoration.js";
|
|
19
|
+
|
|
20
|
+
// addTransitionType is only available in React experimental
|
|
21
|
+
const addTransitionType: ((type: string) => void) | undefined =
|
|
22
|
+
"addTransitionType" in React ? (React as any).addTransitionType : undefined;
|
|
23
|
+
|
|
24
|
+
import { setupLinkInterception } from "./link-interceptor.js";
|
|
25
|
+
import { createPartialUpdater } from "./partial-update.js";
|
|
26
|
+
import { generateHistoryKey } from "./navigation-store.js";
|
|
27
|
+
import type { EventController } from "./event-controller.js";
|
|
28
|
+
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
29
|
+
import {
|
|
30
|
+
toNetworkError,
|
|
31
|
+
emitNetworkError,
|
|
32
|
+
isBackgroundSuppressible,
|
|
33
|
+
} from "./network-error-handler.js";
|
|
34
|
+
import { debugLog } from "./logging.js";
|
|
35
|
+
import { ServerRedirect } from "../errors.js";
|
|
36
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
37
|
+
|
|
38
|
+
// Polyfill Symbol.dispose for Safari and older browsers
|
|
39
|
+
if (typeof Symbol.dispose === "undefined") {
|
|
40
|
+
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Get IDs of non-loader segments (layouts, routes, parallels). */
|
|
44
|
+
function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
|
|
45
|
+
return segments.filter((s) => s.type !== "loader").map((s) => s.id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { createNavigationTransaction };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extended configuration for navigation bridge with event controller
|
|
52
|
+
*/
|
|
53
|
+
export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
|
|
54
|
+
eventController: EventController;
|
|
55
|
+
/** RSC version from initial payload metadata */
|
|
56
|
+
version?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a navigation bridge for handling client-side navigation
|
|
61
|
+
*
|
|
62
|
+
* The bridge coordinates all navigation operations:
|
|
63
|
+
* - Link click interception
|
|
64
|
+
* - Browser back/forward (popstate)
|
|
65
|
+
* - Programmatic navigation
|
|
66
|
+
*
|
|
67
|
+
* Uses the event controller for reactive state management.
|
|
68
|
+
*
|
|
69
|
+
* @param config - Bridge configuration
|
|
70
|
+
* @returns NavigationBridge instance
|
|
71
|
+
*/
|
|
72
|
+
export function createNavigationBridge(
|
|
73
|
+
config: NavigationBridgeConfigWithController,
|
|
74
|
+
): NavigationBridge {
|
|
75
|
+
const { store, client, eventController, onUpdate, renderSegments, version } =
|
|
76
|
+
config;
|
|
77
|
+
|
|
78
|
+
// Create shared partial updater
|
|
79
|
+
const fetchPartialUpdate = createPartialUpdater({
|
|
80
|
+
store,
|
|
81
|
+
client,
|
|
82
|
+
onUpdate,
|
|
83
|
+
renderSegments,
|
|
84
|
+
version,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
/**
|
|
89
|
+
* Navigate to a URL
|
|
90
|
+
* Uses cached segments for SWR revalidation when available
|
|
91
|
+
*/
|
|
92
|
+
async navigate(
|
|
93
|
+
url: string,
|
|
94
|
+
options?: NavigateOptionsInternal,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
// Resolve LocationStateEntry[] to flat object if needed
|
|
97
|
+
const resolvedState =
|
|
98
|
+
options?.state !== undefined
|
|
99
|
+
? resolveNavigationState(options.state)
|
|
100
|
+
: undefined;
|
|
101
|
+
|
|
102
|
+
// Cross-origin URLs are not handled by SPA navigation.
|
|
103
|
+
// Fall back to a full browser navigation for http/https only.
|
|
104
|
+
let targetUrl: URL;
|
|
105
|
+
try {
|
|
106
|
+
targetUrl = new URL(url, window.location.origin);
|
|
107
|
+
} catch {
|
|
108
|
+
console.warn(`[rango] navigate() ignored: malformed URL "${url}"`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (targetUrl.origin !== window.location.origin) {
|
|
112
|
+
if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
|
|
113
|
+
console.error(
|
|
114
|
+
`[rango] navigate() blocked: unsupported scheme "${targetUrl.protocol}"`,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
window.location.href = targetUrl.href;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Shallow navigation: skip RSC fetch when revalidate is false
|
|
123
|
+
// and the pathname hasn't changed (search param / hash only change).
|
|
124
|
+
if (
|
|
125
|
+
options?.revalidate === false &&
|
|
126
|
+
targetUrl.pathname === new URL(window.location.href).pathname
|
|
127
|
+
) {
|
|
128
|
+
// Preserve intercept context from the current history entry so that
|
|
129
|
+
// popstate uses the correct cache key (:intercept suffix) and restores
|
|
130
|
+
// the right full-page vs modal semantics.
|
|
131
|
+
const currentHistoryState = window.history.state;
|
|
132
|
+
const isIntercept = currentHistoryState?.intercept === true;
|
|
133
|
+
const interceptSourceUrl = isIntercept
|
|
134
|
+
? currentHistoryState?.sourceUrl
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
137
|
+
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
138
|
+
|
|
139
|
+
// Copy current segments to the new history key so back/forward restores instantly
|
|
140
|
+
const currentKey = store.getHistoryKey();
|
|
141
|
+
const currentCache = store.getCachedSegments(currentKey);
|
|
142
|
+
if (currentCache?.segments) {
|
|
143
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
144
|
+
store.cacheSegmentsForHistory(
|
|
145
|
+
historyKey,
|
|
146
|
+
currentCache.segments,
|
|
147
|
+
currentHandleData,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Save current scroll position before changing URL
|
|
152
|
+
handleNavigationStart();
|
|
153
|
+
|
|
154
|
+
// Snapshot old state before pushState/replaceState overwrites it
|
|
155
|
+
const oldState = window.history.state;
|
|
156
|
+
|
|
157
|
+
// Update browser URL (carry intercept context into history state)
|
|
158
|
+
const historyState = buildHistoryState(
|
|
159
|
+
resolvedState,
|
|
160
|
+
{
|
|
161
|
+
intercept: isIntercept || undefined,
|
|
162
|
+
sourceUrl: interceptSourceUrl,
|
|
163
|
+
},
|
|
164
|
+
{},
|
|
165
|
+
);
|
|
166
|
+
if (options.replace) {
|
|
167
|
+
window.history.replaceState(historyState, "", url);
|
|
168
|
+
} else {
|
|
169
|
+
window.history.pushState(historyState, "", url);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Ensure new history entry has a scroll restoration key
|
|
173
|
+
ensureHistoryKey();
|
|
174
|
+
|
|
175
|
+
// Notify useLocationState() hooks when state changes
|
|
176
|
+
const hasOldState =
|
|
177
|
+
oldState &&
|
|
178
|
+
typeof oldState === "object" &&
|
|
179
|
+
("state" in oldState ||
|
|
180
|
+
Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
|
|
181
|
+
const hasNewState =
|
|
182
|
+
historyState &&
|
|
183
|
+
("state" in historyState ||
|
|
184
|
+
Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
|
|
185
|
+
if (hasOldState || hasNewState) {
|
|
186
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Update store history key so future navigations reference the right cache
|
|
190
|
+
store.setHistoryKey(historyKey);
|
|
191
|
+
store.setCurrentUrl(url);
|
|
192
|
+
|
|
193
|
+
// Notify hooks — location updates, state stays idle
|
|
194
|
+
eventController.setLocation(targetUrl);
|
|
195
|
+
|
|
196
|
+
// Handle post-navigation scroll
|
|
197
|
+
handleNavigationEnd({ scroll: options.scroll });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Only abort pending requests when navigating to a different route
|
|
202
|
+
// Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
|
|
203
|
+
const currentPath = new URL(window.location.href).pathname;
|
|
204
|
+
const targetPath = targetUrl.pathname;
|
|
205
|
+
if (currentPath !== targetPath) {
|
|
206
|
+
eventController.abortNavigation();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if we're "leaving intercept" - navigating from intercept to same URL without intercept
|
|
210
|
+
// This happens when clicking "View Full Details" in an intercept modal
|
|
211
|
+
const currentHistoryState = window.history.state;
|
|
212
|
+
const isCurrentlyIntercept = currentHistoryState?.intercept === true;
|
|
213
|
+
const isSamePathNavigation = currentPath === targetPath;
|
|
214
|
+
const isLeavingIntercept = isCurrentlyIntercept && isSamePathNavigation;
|
|
215
|
+
|
|
216
|
+
if (isLeavingIntercept) {
|
|
217
|
+
debugLog(
|
|
218
|
+
"[Browser] Leaving intercept - same URL navigation from intercept",
|
|
219
|
+
);
|
|
220
|
+
// Clear intercept source URL to ensure server doesn't treat this as intercept
|
|
221
|
+
store.setInterceptSourceUrl(null);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Before navigating away, update the source page's cache with the latest handleData.
|
|
225
|
+
// This ensures the cache has correct handleData even if handles were streaming.
|
|
226
|
+
const sourceHistoryKey = store.getHistoryKey();
|
|
227
|
+
const sourceCached = store.getCachedSegments(sourceHistoryKey);
|
|
228
|
+
if (sourceCached?.segments && sourceCached.segments.length > 0) {
|
|
229
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
230
|
+
store.cacheSegmentsForHistory(
|
|
231
|
+
sourceHistoryKey,
|
|
232
|
+
sourceCached.segments,
|
|
233
|
+
currentHandleData,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if we have cached segments for target URL
|
|
238
|
+
const historyKey = generateHistoryKey(url);
|
|
239
|
+
const cached = store.getCachedSegments(historyKey);
|
|
240
|
+
|
|
241
|
+
// For shared segments (same ID on current and target), use current page's version
|
|
242
|
+
// since it may have fresher data after an action revalidation.
|
|
243
|
+
// This avoids unnecessary server round-trips for shared layout loaders.
|
|
244
|
+
let cachedSegments = cached?.segments;
|
|
245
|
+
const cachedHandleData = cached?.handleData;
|
|
246
|
+
if (cachedSegments && sourceCached?.segments) {
|
|
247
|
+
const sourceSegmentMap = new Map(
|
|
248
|
+
sourceCached.segments.map((s) => [s.id, s]),
|
|
249
|
+
);
|
|
250
|
+
cachedSegments = cachedSegments.map((targetSeg) => {
|
|
251
|
+
const sourceSeg = sourceSegmentMap.get(targetSeg.id);
|
|
252
|
+
// Use source (current page) version for shared segments - it's fresher
|
|
253
|
+
return sourceSeg || targetSeg;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Also check if there's an intercept cache entry for this URL
|
|
258
|
+
// If so, this URL CAN be intercepted, and we shouldn't use the non-intercept cache
|
|
259
|
+
// because the navigation might result in an intercept (depending on source URL)
|
|
260
|
+
const interceptHistoryKey = generateHistoryKey(url, { intercept: true });
|
|
261
|
+
const hasInterceptCache = store.hasHistoryCache(interceptHistoryKey);
|
|
262
|
+
|
|
263
|
+
// Skip cached SWR for:
|
|
264
|
+
// 1. intercept caches - interception depends on source page context
|
|
265
|
+
// 2. routes that CAN be intercepted - we don't know if this navigation will intercept
|
|
266
|
+
// 3. when leaving intercept - we need fresh non-intercept segments from server
|
|
267
|
+
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
268
|
+
const hasUsableCache =
|
|
269
|
+
cachedSegments &&
|
|
270
|
+
cachedSegments.length > 0 &&
|
|
271
|
+
!isInterceptOnlyCache(cachedSegments) &&
|
|
272
|
+
!hasInterceptCache &&
|
|
273
|
+
!isLeavingIntercept &&
|
|
274
|
+
!options?._skipCache;
|
|
275
|
+
|
|
276
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
277
|
+
...options,
|
|
278
|
+
state: resolvedState,
|
|
279
|
+
skipLoadingState: hasUsableCache,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// REVALIDATE: Fetch fresh data from server
|
|
283
|
+
try {
|
|
284
|
+
await fetchPartialUpdate(
|
|
285
|
+
url,
|
|
286
|
+
hasUsableCache
|
|
287
|
+
? getNonLoaderSegmentIds(cachedSegments!)
|
|
288
|
+
: options?._skipCache
|
|
289
|
+
? [] // Action redirect: send no segments so server renders everything fresh
|
|
290
|
+
: undefined,
|
|
291
|
+
false,
|
|
292
|
+
tx.handle.signal,
|
|
293
|
+
tx.with({
|
|
294
|
+
url,
|
|
295
|
+
replace: options?.replace,
|
|
296
|
+
scroll: options?.scroll,
|
|
297
|
+
state: resolvedState,
|
|
298
|
+
}),
|
|
299
|
+
hasUsableCache
|
|
300
|
+
? {
|
|
301
|
+
type: "navigate" as const,
|
|
302
|
+
targetCacheSegments: cachedSegments,
|
|
303
|
+
targetCacheHandleData: cachedHandleData,
|
|
304
|
+
}
|
|
305
|
+
: isLeavingIntercept
|
|
306
|
+
? { type: "leave-intercept" as const }
|
|
307
|
+
: undefined,
|
|
308
|
+
);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
// Server-side redirect with location state: the current transaction's
|
|
311
|
+
// cleanup resets loading state. Re-navigate to the redirect
|
|
312
|
+
// target carrying the server-set state into history.pushState.
|
|
313
|
+
if (error instanceof ServerRedirect) {
|
|
314
|
+
const redirectUrl = validateRedirectOrigin(
|
|
315
|
+
error.url,
|
|
316
|
+
window.location.origin,
|
|
317
|
+
);
|
|
318
|
+
if (!redirectUrl) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
return this.navigate(redirectUrl, {
|
|
322
|
+
state: error.state,
|
|
323
|
+
replace: options?.replace,
|
|
324
|
+
_skipCache: true,
|
|
325
|
+
} as NavigateOptionsInternal);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
329
|
+
debugLog("[Browser] Navigation aborted by newer navigation");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const networkError = toNetworkError(error, {
|
|
334
|
+
url,
|
|
335
|
+
operation: "navigation",
|
|
336
|
+
});
|
|
337
|
+
if (networkError) {
|
|
338
|
+
console.error(
|
|
339
|
+
"[Browser] Network error during navigation:",
|
|
340
|
+
networkError,
|
|
341
|
+
);
|
|
342
|
+
emitNetworkError(onUpdate, networkError, url);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
throw error;
|
|
347
|
+
} finally {
|
|
348
|
+
tx[Symbol.dispose]();
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Refresh current route
|
|
354
|
+
*/
|
|
355
|
+
async refresh(): Promise<void> {
|
|
356
|
+
eventController.abortNavigation();
|
|
357
|
+
|
|
358
|
+
const tx = createNavigationTransaction(
|
|
359
|
+
store,
|
|
360
|
+
eventController,
|
|
361
|
+
window.location.href,
|
|
362
|
+
{ replace: true },
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
// Refetch with empty segments to get everything fresh
|
|
367
|
+
await fetchPartialUpdate(
|
|
368
|
+
window.location.href,
|
|
369
|
+
[],
|
|
370
|
+
false,
|
|
371
|
+
tx.handle.signal,
|
|
372
|
+
tx.with({ url: window.location.href, replace: true, scroll: false }),
|
|
373
|
+
);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const networkError = toNetworkError(error, {
|
|
376
|
+
url: window.location.href,
|
|
377
|
+
operation: "revalidation",
|
|
378
|
+
});
|
|
379
|
+
if (networkError) {
|
|
380
|
+
console.error(
|
|
381
|
+
"[Browser] Network error during refresh:",
|
|
382
|
+
networkError,
|
|
383
|
+
);
|
|
384
|
+
emitNetworkError(onUpdate, networkError, window.location.href);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
throw error;
|
|
388
|
+
} finally {
|
|
389
|
+
tx[Symbol.dispose]();
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Handle browser back/forward navigation
|
|
395
|
+
* Uses cached segments when available for instant restoration
|
|
396
|
+
*/
|
|
397
|
+
async handlePopstate(): Promise<void> {
|
|
398
|
+
// Abort any pending navigation to prevent race conditions
|
|
399
|
+
eventController.abortNavigation();
|
|
400
|
+
|
|
401
|
+
const url = window.location.href;
|
|
402
|
+
|
|
403
|
+
// Check if this history entry is an intercept
|
|
404
|
+
const historyState = window.history.state;
|
|
405
|
+
const isIntercept = historyState?.intercept === true;
|
|
406
|
+
const interceptSourceUrl = historyState?.sourceUrl;
|
|
407
|
+
|
|
408
|
+
// Check if intercept context is changing (same URL, different intercept state)
|
|
409
|
+
// If so, abort in-flight actions - their results would be for wrong context
|
|
410
|
+
const currentInterceptSource = store.getInterceptSourceUrl();
|
|
411
|
+
const newInterceptSource = interceptSourceUrl ?? null;
|
|
412
|
+
if (currentInterceptSource !== newInterceptSource) {
|
|
413
|
+
debugLog(
|
|
414
|
+
`[Browser] Intercept context changing (${currentInterceptSource} -> ${newInterceptSource}), aborting in-flight actions`,
|
|
415
|
+
);
|
|
416
|
+
eventController.abortAllActions();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Compute history key from URL (with intercept suffix if applicable)
|
|
420
|
+
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
421
|
+
|
|
422
|
+
debugLog(
|
|
423
|
+
"[Browser] Popstate -",
|
|
424
|
+
isIntercept ? "intercept" : "normal",
|
|
425
|
+
"key:",
|
|
426
|
+
historyKey,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Update location in event controller
|
|
430
|
+
eventController.setLocation(new URL(url));
|
|
431
|
+
|
|
432
|
+
// If this is an intercept, restore the intercept context
|
|
433
|
+
if (isIntercept && interceptSourceUrl) {
|
|
434
|
+
store.setInterceptSourceUrl(interceptSourceUrl);
|
|
435
|
+
} else {
|
|
436
|
+
store.setInterceptSourceUrl(null);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Helper to check if streaming is in progress
|
|
440
|
+
const isStreaming = () => eventController.getState().isStreaming;
|
|
441
|
+
|
|
442
|
+
// Check if we can restore from history cache
|
|
443
|
+
const cached = store.getCachedSegments(historyKey);
|
|
444
|
+
const cachedSegments = cached?.segments;
|
|
445
|
+
const cachedHandleData = cached?.handleData;
|
|
446
|
+
const isStale = cached?.stale ?? false;
|
|
447
|
+
|
|
448
|
+
if (cachedSegments && cachedSegments.length > 0) {
|
|
449
|
+
// Update store to point to this history entry
|
|
450
|
+
store.setHistoryKey(historyKey);
|
|
451
|
+
store.setSegmentIds(cachedSegments.map((s) => s.id));
|
|
452
|
+
store.setCurrentUrl(url);
|
|
453
|
+
store.setPath(new URL(url).pathname);
|
|
454
|
+
|
|
455
|
+
// Render from cache - force await to skip loading fallbacks
|
|
456
|
+
try {
|
|
457
|
+
const root = await renderSegments(cachedSegments, {
|
|
458
|
+
forceAwait: true,
|
|
459
|
+
});
|
|
460
|
+
// Merge params from cached segments for useParams restoration.
|
|
461
|
+
// Set params on event controller before onUpdate so both location
|
|
462
|
+
// and params are current when the debounced notify() fires.
|
|
463
|
+
const cachedParams: Record<string, string> = {};
|
|
464
|
+
for (const s of cachedSegments) {
|
|
465
|
+
if (s.params) Object.assign(cachedParams, s.params);
|
|
466
|
+
}
|
|
467
|
+
eventController.setParams(cachedParams);
|
|
468
|
+
|
|
469
|
+
const popstateUpdate = {
|
|
470
|
+
root,
|
|
471
|
+
metadata: {
|
|
472
|
+
pathname: new URL(url).pathname,
|
|
473
|
+
segments: cachedSegments,
|
|
474
|
+
isPartial: true,
|
|
475
|
+
matched: cachedSegments.map((s) => s.id),
|
|
476
|
+
diff: [],
|
|
477
|
+
cachedHandleData,
|
|
478
|
+
params: cachedParams,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const hasTransition = cachedSegments.some((s) => s.transition);
|
|
482
|
+
if (hasTransition) {
|
|
483
|
+
startTransition(() => {
|
|
484
|
+
if (addTransitionType) {
|
|
485
|
+
addTransitionType("navigation-back");
|
|
486
|
+
}
|
|
487
|
+
onUpdate(popstateUpdate);
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
onUpdate(popstateUpdate);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Restore scroll position for back/forward navigation
|
|
494
|
+
handleNavigationEnd({ restore: true, isStreaming });
|
|
495
|
+
|
|
496
|
+
// SWR: If stale, trigger background revalidation
|
|
497
|
+
if (isStale) {
|
|
498
|
+
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
499
|
+
// Background revalidation - don't await, just fire and forget
|
|
500
|
+
const segmentIds = getNonLoaderSegmentIds(cachedSegments);
|
|
501
|
+
|
|
502
|
+
const tx = createNavigationTransaction(
|
|
503
|
+
store,
|
|
504
|
+
eventController,
|
|
505
|
+
url,
|
|
506
|
+
{ skipLoadingState: true, replace: true },
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
fetchPartialUpdate(
|
|
510
|
+
url,
|
|
511
|
+
segmentIds,
|
|
512
|
+
false,
|
|
513
|
+
tx.handle.signal,
|
|
514
|
+
tx.with({
|
|
515
|
+
url,
|
|
516
|
+
replace: true,
|
|
517
|
+
scroll: false,
|
|
518
|
+
intercept: isIntercept,
|
|
519
|
+
interceptSourceUrl,
|
|
520
|
+
cacheOnly: true,
|
|
521
|
+
}),
|
|
522
|
+
{ type: "stale-revalidation", interceptSourceUrl },
|
|
523
|
+
)
|
|
524
|
+
.catch((error) => {
|
|
525
|
+
if (isBackgroundSuppressible(error)) return;
|
|
526
|
+
console.error(
|
|
527
|
+
"[Browser] Background revalidation failed:",
|
|
528
|
+
error,
|
|
529
|
+
);
|
|
530
|
+
})
|
|
531
|
+
.finally(() => {
|
|
532
|
+
tx[Symbol.dispose]();
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.warn(
|
|
538
|
+
"[Browser] Failed to render from cache, fetching:",
|
|
539
|
+
error,
|
|
540
|
+
);
|
|
541
|
+
// Fall through to fetch
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
debugLog("[Browser] History cache miss for key:", historyKey);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Fetch if not cached
|
|
548
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
549
|
+
replace: true,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
await fetchPartialUpdate(
|
|
554
|
+
url,
|
|
555
|
+
undefined,
|
|
556
|
+
false,
|
|
557
|
+
tx.handle.signal,
|
|
558
|
+
tx.with({
|
|
559
|
+
url,
|
|
560
|
+
replace: true,
|
|
561
|
+
scroll: false,
|
|
562
|
+
intercept: isIntercept,
|
|
563
|
+
interceptSourceUrl,
|
|
564
|
+
}),
|
|
565
|
+
isIntercept ? { type: "navigate", interceptSourceUrl } : undefined,
|
|
566
|
+
);
|
|
567
|
+
// Restore scroll position after fetch completes
|
|
568
|
+
handleNavigationEnd({ restore: true, isStreaming });
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
571
|
+
debugLog("[Browser] Popstate navigation aborted");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const networkError = toNetworkError(error, {
|
|
576
|
+
url,
|
|
577
|
+
operation: "navigation",
|
|
578
|
+
});
|
|
579
|
+
if (networkError) {
|
|
580
|
+
console.error(
|
|
581
|
+
"[Browser] Network error during popstate:",
|
|
582
|
+
networkError,
|
|
583
|
+
);
|
|
584
|
+
emitNetworkError(onUpdate, networkError, url);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
throw error;
|
|
589
|
+
} finally {
|
|
590
|
+
tx[Symbol.dispose]();
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Register link interception
|
|
596
|
+
* @returns Cleanup function
|
|
597
|
+
*/
|
|
598
|
+
registerLinkInterception(): () => void {
|
|
599
|
+
const cleanupLinks = setupLinkInterception((url, options) => {
|
|
600
|
+
this.navigate(url, options);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const handlePopstate = () => {
|
|
604
|
+
this.handlePopstate();
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// When the browser restores a page from bfcache (back-forward cache),
|
|
608
|
+
// any in-flight navigation state is stale. This happens when:
|
|
609
|
+
// 1. A navigation triggers X-RSC-Reload (e.g., response route hit via SPA)
|
|
610
|
+
// 2. window.location.href does a hard navigation
|
|
611
|
+
// 3. The user presses back and the browser restores from bfcache
|
|
612
|
+
// At that point, currentNavigation is still set from step 1, so
|
|
613
|
+
// getState() returns "loading" and the progress bar shows.
|
|
614
|
+
// Abort the stale navigation to reset state to idle.
|
|
615
|
+
const handlePageShow = (event: PageTransitionEvent) => {
|
|
616
|
+
if (event.persisted) {
|
|
617
|
+
debugLog(
|
|
618
|
+
"[Browser] Page restored from bfcache, resetting navigation state",
|
|
619
|
+
);
|
|
620
|
+
eventController.abortNavigation();
|
|
621
|
+
// pagehide flips scrollRestoration to "auto" for bfcache compat;
|
|
622
|
+
// restore "manual" so the router controls scroll on SPA navigations.
|
|
623
|
+
window.history.scrollRestoration = "manual";
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Register cross-tab refresh callback with the store
|
|
628
|
+
store.setCrossTabRefreshCallback(() => {
|
|
629
|
+
this.refresh();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
window.addEventListener("popstate", handlePopstate);
|
|
633
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
634
|
+
debugLog("[Browser] Navigation bridge ready");
|
|
635
|
+
|
|
636
|
+
return () => {
|
|
637
|
+
cleanupLinks();
|
|
638
|
+
window.removeEventListener("popstate", handlePopstate);
|
|
639
|
+
window.removeEventListener("pageshow", handlePageShow);
|
|
640
|
+
};
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export { createNavigationBridge as default };
|