@rangojs/router 0.0.0-experimental.002d056c
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 +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -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 +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -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 +464 -0
- package/src/browser/scroll-restoration.ts +397 -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 +547 -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 +479 -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 +982 -0
- package/src/cache/cf/index.ts +29 -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 +44 -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 +281 -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 +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -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 +193 -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 +749 -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 +320 -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 +1242 -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 +170 -0
- package/src/router.ts +1006 -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 +237 -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 +920 -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 +109 -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 +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -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 +363 -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 +266 -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 +445 -0
- package/src/vite/router-discovery.ts +777 -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,582 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NavigationStore,
|
|
3
|
+
NavigationClient,
|
|
4
|
+
UpdateSubscriber,
|
|
5
|
+
ResolvedSegment,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { startTransition } from "react";
|
|
10
|
+
|
|
11
|
+
// addTransitionType is only available in React experimental
|
|
12
|
+
const addTransitionType: ((type: string) => void) | undefined =
|
|
13
|
+
"addTransitionType" in React ? (React as any).addTransitionType : undefined;
|
|
14
|
+
import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
15
|
+
import { reconcileSegments } from "./segment-reconciler.js";
|
|
16
|
+
import type { ReconcileActor } from "./segment-reconciler.js";
|
|
17
|
+
import { hasActiveIntercept as hasActiveInterceptSlots } from "./intercept-utils.js";
|
|
18
|
+
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
19
|
+
import { ServerRedirect } from "../errors.js";
|
|
20
|
+
import { debugLog } from "./logging.js";
|
|
21
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
22
|
+
import type { NavigationUpdate } from "./types.js";
|
|
23
|
+
|
|
24
|
+
/** Build a scroll payload from the commit's scroll option */
|
|
25
|
+
function toScrollPayload(
|
|
26
|
+
scroll: boolean | undefined,
|
|
27
|
+
): NonNullable<NavigationUpdate["scroll"]> {
|
|
28
|
+
return { enabled: scroll !== false ? scroll : false };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Configuration for creating a partial updater
|
|
33
|
+
*/
|
|
34
|
+
export interface PartialUpdateConfig {
|
|
35
|
+
store: NavigationStore;
|
|
36
|
+
client: NavigationClient;
|
|
37
|
+
onUpdate: UpdateSubscriber;
|
|
38
|
+
renderSegments: (
|
|
39
|
+
segments: ResolvedSegment[],
|
|
40
|
+
options?: RenderSegmentsOptions,
|
|
41
|
+
) => Promise<ReactNode> | ReactNode;
|
|
42
|
+
/** RSC version received from server (from initial payload metadata) */
|
|
43
|
+
version?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options that can override the pre-configured commit settings
|
|
48
|
+
*/
|
|
49
|
+
export interface CommitOverrides {
|
|
50
|
+
/** Override scroll behavior (e.g., disable for intercepts) */
|
|
51
|
+
scroll?: boolean;
|
|
52
|
+
/** Override replace behavior (e.g., force replace for intercepts) */
|
|
53
|
+
replace?: boolean;
|
|
54
|
+
/** Mark this as an intercept route */
|
|
55
|
+
intercept?: boolean;
|
|
56
|
+
/** Source URL where intercept was triggered from */
|
|
57
|
+
interceptSourceUrl?: string;
|
|
58
|
+
/** Server-set location state to merge into history.pushState */
|
|
59
|
+
serverState?: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Discriminated update mode for partial updates.
|
|
64
|
+
*/
|
|
65
|
+
export type UpdateMode =
|
|
66
|
+
| {
|
|
67
|
+
type: "navigate";
|
|
68
|
+
/** Cached segments for the target URL. When provided, these are used to build
|
|
69
|
+
* the segment map instead of the current page's segments. This ensures consistency
|
|
70
|
+
* when we send cached segment IDs to the server - if the server returns empty diff,
|
|
71
|
+
* we use the same segments we told the server we have. */
|
|
72
|
+
targetCacheSegments?: ResolvedSegment[];
|
|
73
|
+
/** Cached handle data for the target URL. When server returns empty diff and we're
|
|
74
|
+
* rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
|
|
75
|
+
targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
|
|
76
|
+
/** Source URL for intercept restore (popstate cache miss) */
|
|
77
|
+
interceptSourceUrl?: string;
|
|
78
|
+
}
|
|
79
|
+
| { type: "leave-intercept" }
|
|
80
|
+
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
81
|
+
| { type: "action"; interceptSourceUrl?: string };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type for the fetchPartialUpdate function
|
|
85
|
+
*/
|
|
86
|
+
export type PartialUpdater = (
|
|
87
|
+
targetUrl: string,
|
|
88
|
+
segmentIds: string[] | undefined,
|
|
89
|
+
isRetry: boolean,
|
|
90
|
+
signal: AbortSignal | undefined,
|
|
91
|
+
tx: BoundTransaction,
|
|
92
|
+
mode?: UpdateMode,
|
|
93
|
+
) => Promise<void>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a partial updater for fetching and applying RSC partial updates
|
|
97
|
+
*
|
|
98
|
+
* This function is shared between navigation-bridge and server-action-bridge
|
|
99
|
+
* to handle partial RSC updates with HMR resilience.
|
|
100
|
+
*
|
|
101
|
+
* @param config - Partial update configuration
|
|
102
|
+
* @returns fetchPartialUpdate function
|
|
103
|
+
*/
|
|
104
|
+
export function createPartialUpdater(
|
|
105
|
+
config: PartialUpdateConfig,
|
|
106
|
+
): PartialUpdater {
|
|
107
|
+
const { store, client, onUpdate, renderSegments, version } = config;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get current page's cached segments as an array
|
|
111
|
+
*/
|
|
112
|
+
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
113
|
+
const currentKey = store.getHistoryKey();
|
|
114
|
+
const cached = store.getCachedSegments(currentKey);
|
|
115
|
+
return cached?.segments || [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fetch partial update and trigger UI update
|
|
120
|
+
*
|
|
121
|
+
* @param tx - Transaction for committing segment state (required)
|
|
122
|
+
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
123
|
+
*/
|
|
124
|
+
async function fetchPartialUpdate(
|
|
125
|
+
targetUrl: string,
|
|
126
|
+
segmentIds: string[] | undefined,
|
|
127
|
+
isRetry: boolean,
|
|
128
|
+
signal: AbortSignal | undefined,
|
|
129
|
+
tx: BoundTransaction,
|
|
130
|
+
mode: UpdateMode = { type: "navigate" },
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const segmentState = store.getSegmentState();
|
|
133
|
+
const url = targetUrl || window.location.href;
|
|
134
|
+
|
|
135
|
+
// Capture history key at start for stale revalidation consistency check
|
|
136
|
+
const historyKeyAtStart = store.getHistoryKey();
|
|
137
|
+
|
|
138
|
+
// Derive interceptSourceUrl from modes that carry it
|
|
139
|
+
const interceptSourceUrl =
|
|
140
|
+
mode.type === "stale-revalidation" ||
|
|
141
|
+
mode.type === "action" ||
|
|
142
|
+
mode.type === "navigate"
|
|
143
|
+
? mode.interceptSourceUrl
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
// When leaving intercept, filter out intercept-specific segments
|
|
147
|
+
let segments: string[];
|
|
148
|
+
if (mode.type === "leave-intercept") {
|
|
149
|
+
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
150
|
+
const currentCached = getCurrentCachedSegments();
|
|
151
|
+
const interceptIds = new Set(
|
|
152
|
+
currentCached
|
|
153
|
+
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
154
|
+
.map((s) => s.id),
|
|
155
|
+
);
|
|
156
|
+
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
157
|
+
debugLog(
|
|
158
|
+
`[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// For intercept revalidation, use the intercept source URL as previousUrl
|
|
165
|
+
const previousUrl =
|
|
166
|
+
interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
167
|
+
|
|
168
|
+
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
169
|
+
debugLog(`[Browser] From: ${previousUrl}`);
|
|
170
|
+
debugLog(`[Browser] To: ${url}`);
|
|
171
|
+
debugLog(`[Browser] Segments to send: ${segments.join(", ")}`);
|
|
172
|
+
if (interceptSourceUrl) {
|
|
173
|
+
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get cached segments for merging with server diff.
|
|
177
|
+
// When navigating with targetCacheSegments, use those for consistency.
|
|
178
|
+
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
179
|
+
const targetCache =
|
|
180
|
+
mode.type === "navigate" ? mode.targetCacheSegments : undefined;
|
|
181
|
+
const cachedSegs =
|
|
182
|
+
targetCache && targetCache.length > 0
|
|
183
|
+
? targetCache
|
|
184
|
+
: getCurrentCachedSegments();
|
|
185
|
+
|
|
186
|
+
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
187
|
+
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
188
|
+
fetchResult = await client.fetchPartial({
|
|
189
|
+
targetUrl: url,
|
|
190
|
+
segmentIds: segments,
|
|
191
|
+
previousUrl,
|
|
192
|
+
// Mark stale when explicitly requested OR when no segments are sent
|
|
193
|
+
// (action redirect sends empty segments for a fresh render).
|
|
194
|
+
staleRevalidation:
|
|
195
|
+
mode.type === "stale-revalidation" || segments.length === 0,
|
|
196
|
+
version,
|
|
197
|
+
});
|
|
198
|
+
// Mark navigation as streaming (response received, now parsing RSC).
|
|
199
|
+
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
200
|
+
// allowing useLinkStatus to show per-link pending indicators.
|
|
201
|
+
const streamingToken = tx.startStreaming();
|
|
202
|
+
const { payload, streamComplete: rawStreamComplete } = fetchResult;
|
|
203
|
+
debugLog("payload.metadata", payload.metadata);
|
|
204
|
+
|
|
205
|
+
const streamComplete = rawStreamComplete.then(() => {
|
|
206
|
+
streamingToken.end();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Handle server-side redirect with state
|
|
210
|
+
if (payload.metadata?.redirect) {
|
|
211
|
+
if (signal?.aborted) {
|
|
212
|
+
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const redirectUrl = validateRedirectOrigin(
|
|
216
|
+
payload.metadata.redirect.url,
|
|
217
|
+
window.location.origin,
|
|
218
|
+
);
|
|
219
|
+
if (!redirectUrl) {
|
|
220
|
+
debugLog("[Browser] Ignoring blocked redirect payload");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const serverState = payload.metadata.locationState;
|
|
224
|
+
throw new ServerRedirect(redirectUrl, serverState);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (payload.metadata?.isPartial) {
|
|
228
|
+
const { segments: newSegments, matched, diff } = payload.metadata;
|
|
229
|
+
|
|
230
|
+
// Check if this navigation is stale (a newer one started)
|
|
231
|
+
if (signal?.aborted) {
|
|
232
|
+
debugLog("[Browser] Ignoring stale navigation (aborted)");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
237
|
+
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
238
|
+
|
|
239
|
+
// If diff is empty, nothing changed on server side.
|
|
240
|
+
if (!diff || diff.length === 0) {
|
|
241
|
+
const matchedIds = matched || [];
|
|
242
|
+
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
243
|
+
const existingSegments = matchedIds
|
|
244
|
+
.map((id: string) => cacheMap.get(id))
|
|
245
|
+
.filter(Boolean) as ResolvedSegment[];
|
|
246
|
+
|
|
247
|
+
// When navigating with cached segments to a different route, render them.
|
|
248
|
+
if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
|
|
249
|
+
debugLog(
|
|
250
|
+
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const newTree = await renderSegments(existingSegments, {
|
|
254
|
+
forceAwait: true,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const { scroll: commitScroll } = tx.commit(
|
|
258
|
+
matchedIds,
|
|
259
|
+
existingSegments,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
263
|
+
// breadcrumbs and other handle data from cache.
|
|
264
|
+
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
265
|
+
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
266
|
+
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
267
|
+
payload.metadata!;
|
|
268
|
+
const cachedUpdate = {
|
|
269
|
+
root: newTree,
|
|
270
|
+
metadata: {
|
|
271
|
+
...metadataWithoutHandles,
|
|
272
|
+
cachedHandleData: mode.targetCacheHandleData,
|
|
273
|
+
},
|
|
274
|
+
scroll: toScrollPayload(commitScroll),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const cachedHasTransition = existingSegments.some(
|
|
278
|
+
(s) => s.transition,
|
|
279
|
+
);
|
|
280
|
+
if (cachedHasTransition) {
|
|
281
|
+
startTransition(() => {
|
|
282
|
+
if (addTransitionType) {
|
|
283
|
+
addTransitionType("navigation");
|
|
284
|
+
}
|
|
285
|
+
onUpdate(cachedUpdate);
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
onUpdate(cachedUpdate);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
debugLog("[Browser] Navigation complete (rendered from cache)");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// When leaving intercept, force re-render even with empty diff
|
|
296
|
+
if (mode.type === "leave-intercept") {
|
|
297
|
+
debugLog(
|
|
298
|
+
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const newTree = await renderSegments(existingSegments, {
|
|
302
|
+
forceAwait: true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const { scroll: leaveScroll } = tx.commit(
|
|
306
|
+
matchedIds,
|
|
307
|
+
existingSegments,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
onUpdate({
|
|
311
|
+
root: newTree,
|
|
312
|
+
metadata: payload.metadata,
|
|
313
|
+
scroll: toScrollPayload(leaveScroll),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
debugLog("[Browser] Navigation complete (left intercept)");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Same route revalidation with no changes - skip UI update
|
|
321
|
+
debugLog(
|
|
322
|
+
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
323
|
+
);
|
|
324
|
+
tx.commit(matchedIds, existingSegments);
|
|
325
|
+
debugLog("[Browser] Navigation complete (no re-render)");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Reconcile server segments with cached segments (single source of truth)
|
|
330
|
+
const matchedIds = matched || [];
|
|
331
|
+
const actor: ReconcileActor =
|
|
332
|
+
mode.type === "stale-revalidation" || mode.type === "action"
|
|
333
|
+
? "stale-revalidation"
|
|
334
|
+
: "navigation";
|
|
335
|
+
|
|
336
|
+
const reconciled = reconcileSegments({
|
|
337
|
+
actor,
|
|
338
|
+
matched: matchedIds,
|
|
339
|
+
diff: diff || [],
|
|
340
|
+
serverSegments: newSegments || [],
|
|
341
|
+
cachedSegments: cachedSegs,
|
|
342
|
+
insertMissingDiff: true,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
346
|
+
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
347
|
+
const missingIds = matchedIds.filter(
|
|
348
|
+
(id: string) => !reconciledIdSet.has(id),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (missingIds.length > 0) {
|
|
352
|
+
const missingCount = missingIds.length;
|
|
353
|
+
|
|
354
|
+
if (isRetry) {
|
|
355
|
+
console.warn("Missing ids", { missingIds });
|
|
356
|
+
throw new Error(
|
|
357
|
+
`[Browser] Failed to fetch segments after retry. Missing: [${missingIds.join(", ")}]`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (signal?.aborted) {
|
|
361
|
+
debugLog(
|
|
362
|
+
"[Browser] Ignoring stale navigation (aborted during HMR retry)",
|
|
363
|
+
);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (mode.type === "action") {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.warn(
|
|
370
|
+
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Refetch with empty segments = server sends everything
|
|
374
|
+
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (signal?.aborted) {
|
|
378
|
+
debugLog("[Browser] Ignoring stale navigation (aborted before render)");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Rebuild tree on client (await for loader data resolution)
|
|
383
|
+
const renderOptions = {
|
|
384
|
+
isAction: mode.type === "action",
|
|
385
|
+
forceAwait: mode.type === "stale-revalidation",
|
|
386
|
+
interceptSegments:
|
|
387
|
+
reconciled.interceptSegments.length > 0
|
|
388
|
+
? reconciled.interceptSegments
|
|
389
|
+
: undefined,
|
|
390
|
+
};
|
|
391
|
+
const newTree = await (signal
|
|
392
|
+
? Promise.race([
|
|
393
|
+
renderSegments(reconciled.mainSegments, renderOptions),
|
|
394
|
+
new Promise<never>((_, reject) => {
|
|
395
|
+
if (signal.aborted) {
|
|
396
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
397
|
+
}
|
|
398
|
+
signal.addEventListener("abort", () => {
|
|
399
|
+
reject(new DOMException("Navigation aborted", "AbortError"));
|
|
400
|
+
});
|
|
401
|
+
}),
|
|
402
|
+
])
|
|
403
|
+
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
404
|
+
|
|
405
|
+
// Final abort check before committing - another navigation may have started
|
|
406
|
+
if (signal?.aborted) {
|
|
407
|
+
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check if this is an intercept response (any slot is active)
|
|
412
|
+
const isInterceptResponse = hasActiveInterceptSlots(
|
|
413
|
+
payload.metadata?.slots,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
417
|
+
// Use the authoritative source from mode/history state when restoring an
|
|
418
|
+
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
419
|
+
// intercept navigations.
|
|
420
|
+
const effectiveInterceptSource =
|
|
421
|
+
interceptSourceUrl || segmentState.currentUrl;
|
|
422
|
+
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
423
|
+
if (isInterceptResponse) {
|
|
424
|
+
store.setInterceptSourceUrl(effectiveInterceptSource);
|
|
425
|
+
} else {
|
|
426
|
+
store.setInterceptSourceUrl(null);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Commit navigation - use server's matched as the authoritative segment ID list.
|
|
431
|
+
// reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
|
|
432
|
+
// but the server's matched always includes all expected segment IDs.
|
|
433
|
+
const allSegmentIds = matchedIds;
|
|
434
|
+
const serverLocationState = payload.metadata?.locationState;
|
|
435
|
+
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
436
|
+
? {
|
|
437
|
+
scroll: false,
|
|
438
|
+
intercept: true,
|
|
439
|
+
interceptSourceUrl: effectiveInterceptSource,
|
|
440
|
+
...(serverLocationState && { serverState: serverLocationState }),
|
|
441
|
+
}
|
|
442
|
+
: serverLocationState
|
|
443
|
+
? { serverState: serverLocationState }
|
|
444
|
+
: undefined;
|
|
445
|
+
const { scroll: navScroll } = tx.commit(
|
|
446
|
+
allSegmentIds,
|
|
447
|
+
reconciled.segments,
|
|
448
|
+
overrides,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
452
|
+
if (mode.type === "stale-revalidation") {
|
|
453
|
+
const historyKeyNow = store.getHistoryKey();
|
|
454
|
+
if (historyKeyNow !== historyKeyAtStart) {
|
|
455
|
+
debugLog(
|
|
456
|
+
`[Browser] Stale revalidation: history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
|
|
457
|
+
);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
debugLog("[partial-update] updating document");
|
|
463
|
+
|
|
464
|
+
// Emit update to trigger React render.
|
|
465
|
+
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
466
|
+
const hasTransition = reconciled.mainSegments.some((s) => s.transition);
|
|
467
|
+
const scrollPayload = toScrollPayload(navScroll);
|
|
468
|
+
|
|
469
|
+
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
470
|
+
startTransition(() => {
|
|
471
|
+
if (hasTransition && addTransitionType) {
|
|
472
|
+
addTransitionType("action");
|
|
473
|
+
}
|
|
474
|
+
onUpdate({
|
|
475
|
+
root: newTree,
|
|
476
|
+
metadata: payload.metadata!,
|
|
477
|
+
scroll: scrollPayload,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
} else if (hasTransition) {
|
|
481
|
+
startTransition(() => {
|
|
482
|
+
if (addTransitionType) {
|
|
483
|
+
addTransitionType("navigation");
|
|
484
|
+
}
|
|
485
|
+
onUpdate({
|
|
486
|
+
root: newTree,
|
|
487
|
+
metadata: payload.metadata!,
|
|
488
|
+
scroll: scrollPayload,
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
} else {
|
|
492
|
+
onUpdate({
|
|
493
|
+
root: newTree,
|
|
494
|
+
metadata: payload.metadata!,
|
|
495
|
+
scroll: scrollPayload,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
debugLog("[Browser] Navigation complete");
|
|
500
|
+
return;
|
|
501
|
+
} else {
|
|
502
|
+
// Full update (fallback)
|
|
503
|
+
console.warn(`[Browser] Full update (fallback)`);
|
|
504
|
+
|
|
505
|
+
const segments = payload.metadata?.segments || [];
|
|
506
|
+
|
|
507
|
+
if (signal?.aborted) {
|
|
508
|
+
debugLog("[Browser] Ignoring stale navigation (aborted)");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const segmentIds = segments.map((s: ResolvedSegment) => s.id);
|
|
513
|
+
|
|
514
|
+
const newTree = await renderSegments(segments);
|
|
515
|
+
|
|
516
|
+
if (signal?.aborted) {
|
|
517
|
+
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const fullUpdateServerState = payload.metadata?.locationState;
|
|
522
|
+
const { scroll: fullScroll } = fullUpdateServerState
|
|
523
|
+
? tx.commit(segmentIds, segments, {
|
|
524
|
+
serverState: fullUpdateServerState,
|
|
525
|
+
})
|
|
526
|
+
: tx.commit(segmentIds, segments);
|
|
527
|
+
|
|
528
|
+
const fullHasTransition = segments.some(
|
|
529
|
+
(s: ResolvedSegment) => s.transition,
|
|
530
|
+
);
|
|
531
|
+
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
532
|
+
|
|
533
|
+
if (mode.type === "stale-revalidation") {
|
|
534
|
+
await rawStreamComplete;
|
|
535
|
+
startTransition(() => {
|
|
536
|
+
if (fullHasTransition && addTransitionType) {
|
|
537
|
+
addTransitionType("action");
|
|
538
|
+
}
|
|
539
|
+
onUpdate({
|
|
540
|
+
root: newTree,
|
|
541
|
+
metadata: payload.metadata!,
|
|
542
|
+
scroll: fullScrollPayload,
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
} else if (mode.type === "action") {
|
|
546
|
+
startTransition(async () => {
|
|
547
|
+
if (fullHasTransition && addTransitionType) {
|
|
548
|
+
addTransitionType("action");
|
|
549
|
+
}
|
|
550
|
+
onUpdate({
|
|
551
|
+
root: newTree,
|
|
552
|
+
metadata: payload.metadata!,
|
|
553
|
+
scroll: fullScrollPayload,
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
} else if (fullHasTransition) {
|
|
557
|
+
startTransition(() => {
|
|
558
|
+
if (addTransitionType) {
|
|
559
|
+
addTransitionType("navigation");
|
|
560
|
+
}
|
|
561
|
+
onUpdate({
|
|
562
|
+
root: newTree,
|
|
563
|
+
metadata: payload.metadata!,
|
|
564
|
+
scroll: fullScrollPayload,
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
} else {
|
|
568
|
+
onUpdate({
|
|
569
|
+
root: newTree,
|
|
570
|
+
metadata: payload.metadata!,
|
|
571
|
+
scroll: fullScrollPayload,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return fetchPartialUpdate;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export { createPartialUpdater as default };
|