@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +647 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +76 -28
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- 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-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -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 +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -15,10 +15,12 @@ import { getRangoState } from "./rango-state.js";
|
|
|
15
15
|
import {
|
|
16
16
|
extractRscHeaderUrl,
|
|
17
17
|
emptyResponse,
|
|
18
|
+
handleReloadHeader,
|
|
18
19
|
teeWithCompletion,
|
|
19
20
|
} from "./response-adapter.js";
|
|
20
21
|
import {
|
|
21
22
|
buildPrefetchKey,
|
|
23
|
+
buildSourceKey,
|
|
22
24
|
consumeInflightPrefetch,
|
|
23
25
|
consumePrefetch,
|
|
24
26
|
} from "./prefetch/cache.js";
|
|
@@ -30,8 +32,10 @@ import {
|
|
|
30
32
|
* deserializing the response using the RSC runtime.
|
|
31
33
|
*
|
|
32
34
|
* Checks the in-memory prefetch cache before making a network request.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* Tries the source-scoped key first (populated when the server tagged
|
|
36
|
+
* the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
|
|
37
|
+
* and falls back to the Rango-state-keyed wildcard slot used for the
|
|
38
|
+
* common source-agnostic case.
|
|
35
39
|
*
|
|
36
40
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
37
41
|
* @returns NavigationClient instance
|
|
@@ -93,18 +97,42 @@ export function createNavigationClient(
|
|
|
93
97
|
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
// Check completed in-memory prefetch cache before making a network
|
|
97
|
-
//
|
|
98
|
-
//
|
|
100
|
+
// Check completed in-memory prefetch cache before making a network
|
|
101
|
+
// request. Try the source-scoped key first (populated when the server
|
|
102
|
+
// tagged the prefetch response as source-sensitive, e.g. intercepts,
|
|
103
|
+
// or when a Link opted in with `prefetchKey=":source"`), then fall
|
|
104
|
+
// back to the wildcard slot shared across source pages.
|
|
105
|
+
// Both keys embed the Rango state, so state rotation (deploy or
|
|
106
|
+
// server-action invalidation) auto-invalidates both scopes.
|
|
99
107
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
100
108
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
101
|
-
//
|
|
102
109
|
const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
const rangoState = getRangoState();
|
|
111
|
+
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
112
|
+
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
113
|
+
|
|
114
|
+
let cachedResponse: Response | null = null;
|
|
115
|
+
let hitKey: string | null = null;
|
|
116
|
+
if (canUsePrefetch) {
|
|
117
|
+
cachedResponse = consumePrefetch(cacheKey);
|
|
118
|
+
if (cachedResponse) {
|
|
119
|
+
hitKey = cacheKey;
|
|
120
|
+
} else {
|
|
121
|
+
cachedResponse = consumePrefetch(wildcardKey);
|
|
122
|
+
if (cachedResponse) hitKey = wildcardKey;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let inflightResponsePromise: Promise<Response | null> | null = null;
|
|
127
|
+
if (canUsePrefetch && !cachedResponse) {
|
|
128
|
+
inflightResponsePromise = consumeInflightPrefetch(cacheKey);
|
|
129
|
+
if (inflightResponsePromise) {
|
|
130
|
+
hitKey = cacheKey;
|
|
131
|
+
} else {
|
|
132
|
+
inflightResponsePromise = consumeInflightPrefetch(wildcardKey);
|
|
133
|
+
if (inflightResponsePromise) hitKey = wildcardKey;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
108
136
|
// Track when the stream completes
|
|
109
137
|
let resolveStreamComplete: () => void;
|
|
110
138
|
const streamComplete = new Promise<void>((resolve) => {
|
|
@@ -121,21 +149,17 @@ export function createNavigationClient(
|
|
|
121
149
|
source: string,
|
|
122
150
|
): Response | Promise<Response> => {
|
|
123
151
|
// Version mismatch — server wants a full page reload
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
window.location.href = reload.url;
|
|
136
|
-
// Block further processing — page is reloading
|
|
137
|
-
return new Promise<Response>(() => {});
|
|
138
|
-
}
|
|
152
|
+
const reloadResult = handleReloadHeader(response, {
|
|
153
|
+
onBlocked: resolveStreamComplete,
|
|
154
|
+
onReload: (url) => {
|
|
155
|
+
if (tx) {
|
|
156
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
157
|
+
reloadUrl: url,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
if (reloadResult) return reloadResult;
|
|
139
163
|
|
|
140
164
|
// Server-side redirect without state: the server returned 204 with
|
|
141
165
|
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
@@ -197,7 +221,10 @@ export function createNavigationClient(
|
|
|
197
221
|
|
|
198
222
|
if (cachedResponse) {
|
|
199
223
|
if (tx) {
|
|
200
|
-
browserDebugLog(tx, "prefetch cache hit", {
|
|
224
|
+
browserDebugLog(tx, "prefetch cache hit", {
|
|
225
|
+
key: hitKey,
|
|
226
|
+
wildcard: hitKey === wildcardKey,
|
|
227
|
+
});
|
|
201
228
|
}
|
|
202
229
|
responsePromise = Promise.resolve(cachedResponse).then((response) => {
|
|
203
230
|
const validated = validateRscHeaders(response, "prefetch cache");
|
|
@@ -214,8 +241,12 @@ export function createNavigationClient(
|
|
|
214
241
|
});
|
|
215
242
|
} else if (inflightResponsePromise) {
|
|
216
243
|
if (tx) {
|
|
217
|
-
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
244
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
245
|
+
key: hitKey,
|
|
246
|
+
wildcard: hitKey === wildcardKey,
|
|
247
|
+
});
|
|
218
248
|
}
|
|
249
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
219
250
|
responsePromise = inflightResponsePromise.then(async (response) => {
|
|
220
251
|
if (!response) {
|
|
221
252
|
if (tx) {
|
|
@@ -224,6 +255,23 @@ export function createNavigationClient(
|
|
|
224
255
|
return doFreshFetch();
|
|
225
256
|
}
|
|
226
257
|
|
|
258
|
+
// Cross-source safety: an inflight promise adopted via the
|
|
259
|
+
// wildcard key may turn out to be source-scoped (server emitted
|
|
260
|
+
// `X-RSC-Prefetch-Scope: source`), which means it was built for
|
|
261
|
+
// a different source page. Discard and refetch.
|
|
262
|
+
if (
|
|
263
|
+
adoptedViaWildcard &&
|
|
264
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
265
|
+
) {
|
|
266
|
+
if (tx) {
|
|
267
|
+
browserDebugLog(
|
|
268
|
+
tx,
|
|
269
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return doFreshFetch();
|
|
273
|
+
}
|
|
274
|
+
|
|
227
275
|
const validated = validateRscHeaders(response, "inflight prefetch");
|
|
228
276
|
if (validated instanceof Promise) return validated;
|
|
229
277
|
|
|
@@ -12,7 +12,10 @@ import type {
|
|
|
12
12
|
ActionStateListener,
|
|
13
13
|
HandleData,
|
|
14
14
|
} from "./types.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
clearPrefetchCache,
|
|
17
|
+
clearPrefetchCacheLocal,
|
|
18
|
+
} from "./prefetch/cache.js";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Default action state (idle with no payload)
|
|
@@ -280,18 +283,17 @@ export function createNavigationStore(
|
|
|
280
283
|
/**
|
|
281
284
|
* Create a debounced function that batches rapid calls
|
|
282
285
|
*/
|
|
286
|
+
// A non-keyed notifier is the keyed one restricted to a single constant key;
|
|
287
|
+
// its own keyed instance means the "" key never collides with action keys.
|
|
283
288
|
function createDebouncedNotifier<T extends (...args: any[]) => void>(
|
|
284
289
|
fn: T,
|
|
285
290
|
ms: number = 20,
|
|
286
291
|
): T {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
fn(...args);
|
|
293
|
-
}, ms);
|
|
294
|
-
}) as T;
|
|
292
|
+
const keyed = createKeyedDebouncedNotifier(
|
|
293
|
+
(_key: string, ...args: any[]) => fn(...args),
|
|
294
|
+
ms,
|
|
295
|
+
);
|
|
296
|
+
return ((...args: Parameters<T>) => keyed("", ...args)) as T;
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
/**
|
|
@@ -335,6 +337,18 @@ export function createNavigationStore(
|
|
|
335
337
|
clearPrefetchCache();
|
|
336
338
|
}
|
|
337
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Drop this tab's navigation + prefetch caches without broadcasting or
|
|
342
|
+
* rotating shared state. Used when the local session changes in a way that
|
|
343
|
+
* doesn't affect other tabs — e.g. this tab crosses into a different app
|
|
344
|
+
* via a cross-router navigation. Other tabs in the old app keep their
|
|
345
|
+
* caches and their X-Rango-State token.
|
|
346
|
+
*/
|
|
347
|
+
function clearCacheInternalLocal(): void {
|
|
348
|
+
historyCache.length = 0;
|
|
349
|
+
clearPrefetchCacheLocal();
|
|
350
|
+
}
|
|
351
|
+
|
|
338
352
|
/**
|
|
339
353
|
* Mark all cache entries as stale (internal - does not broadcast)
|
|
340
354
|
*/
|
|
@@ -668,6 +682,15 @@ export function createNavigationStore(
|
|
|
668
682
|
clearCacheAndBroadcast();
|
|
669
683
|
},
|
|
670
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Drop this tab's navigation + prefetch caches locally without
|
|
687
|
+
* broadcasting or rotating shared state. Intended for cross-app
|
|
688
|
+
* transitions where the session state diverges for this tab only.
|
|
689
|
+
*/
|
|
690
|
+
clearHistoryCacheLocal(): void {
|
|
691
|
+
clearCacheInternalLocal();
|
|
692
|
+
},
|
|
693
|
+
|
|
671
694
|
/**
|
|
672
695
|
* Mark cache as stale and broadcast to other tabs
|
|
673
696
|
* Called after server actions - allows SWR pattern for popstate
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./scroll-restoration.js";
|
|
12
12
|
import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
|
-
import { buildHistoryState } from "./history-state.js";
|
|
14
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
15
15
|
|
|
16
16
|
// Re-export for consumers that import from navigation-transaction
|
|
17
17
|
export { resolveNavigationState } from "./history-state.js";
|
|
@@ -186,12 +186,8 @@ export function createNavigationTransaction(
|
|
|
186
186
|
// Used to detect when location state is being cleared.
|
|
187
187
|
const oldState = window.history.state;
|
|
188
188
|
|
|
189
|
-
// Update browser URL
|
|
190
|
-
|
|
191
|
-
window.history.replaceState(historyState, "", url);
|
|
192
|
-
} else {
|
|
193
|
-
window.history.pushState(historyState, "", url);
|
|
194
|
-
}
|
|
189
|
+
// Update browser URL (stamps history.state.idx for back() first-entry detection)
|
|
190
|
+
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
195
191
|
// Ensure new history entry has a scroll restoration key
|
|
196
192
|
ensureHistoryKey();
|
|
197
193
|
|
|
@@ -240,30 +236,16 @@ export function createNavigationTransaction(
|
|
|
240
236
|
segments: ResolvedSegment[],
|
|
241
237
|
overrides?: BoundCommitOverrides,
|
|
242
238
|
) => {
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
// Allow overrides to force replace (e.g., for intercepts)
|
|
247
|
-
const finalReplace =
|
|
248
|
-
overrides?.replace !== undefined ? overrides.replace : opts.replace;
|
|
249
|
-
// Intercept info: overrides take precedence, fallback to opts
|
|
250
|
-
const intercept =
|
|
251
|
-
overrides?.intercept !== undefined
|
|
252
|
-
? overrides.intercept
|
|
253
|
-
: opts.intercept;
|
|
239
|
+
const finalScroll = overrides?.scroll ?? opts.scroll;
|
|
240
|
+
const finalReplace = overrides?.replace ?? opts.replace;
|
|
241
|
+
const intercept = overrides?.intercept ?? opts.intercept;
|
|
254
242
|
const interceptSourceUrl =
|
|
255
|
-
overrides?.interceptSourceUrl
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
const cacheOnly =
|
|
260
|
-
overrides?.cacheOnly !== undefined
|
|
261
|
-
? overrides.cacheOnly
|
|
262
|
-
: opts.cacheOnly;
|
|
263
|
-
// User state: overrides take precedence, fallback to opts
|
|
243
|
+
overrides?.interceptSourceUrl ?? opts.interceptSourceUrl;
|
|
244
|
+
const cacheOnly = overrides?.cacheOnly ?? opts.cacheOnly;
|
|
245
|
+
// state is `unknown` (null is meaningful) so `??` would wrongly drop a
|
|
246
|
+
// null override; serverState always comes from overrides, never opts.
|
|
264
247
|
const state =
|
|
265
248
|
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
266
|
-
// Server-set location state: only from overrides (set by partial-update)
|
|
267
249
|
const serverState = overrides?.serverState;
|
|
268
250
|
return commit({
|
|
269
251
|
...opts,
|
|
@@ -14,7 +14,10 @@ const addTransitionType: ((type: string) => void) | undefined =
|
|
|
14
14
|
import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
15
15
|
import { reconcileSegments } from "./segment-reconciler.js";
|
|
16
16
|
import type { ReconcileActor } from "./segment-reconciler.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
hasActiveIntercept as hasActiveInterceptSlots,
|
|
19
|
+
isInterceptSegment,
|
|
20
|
+
} from "./intercept-utils.js";
|
|
18
21
|
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
19
22
|
import { ServerRedirect } from "../errors.js";
|
|
20
23
|
import { debugLog } from "./logging.js";
|
|
@@ -28,6 +31,23 @@ function toScrollPayload(
|
|
|
28
31
|
return { enabled: scroll !== false ? scroll : false };
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether to wrap an update in startViewTransition.
|
|
36
|
+
*
|
|
37
|
+
* Intercept-driven updates only mutate the parallel slot — the main outlet
|
|
38
|
+
* shows the same content — so transitions on the underlying main segments
|
|
39
|
+
* shouldn't fire (otherwise their elements get hoisted above the modal).
|
|
40
|
+
*/
|
|
41
|
+
function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
42
|
+
let hasIntercept = false;
|
|
43
|
+
let hasTransition = false;
|
|
44
|
+
for (const s of segments) {
|
|
45
|
+
if (isInterceptSegment(s)) hasIntercept = true;
|
|
46
|
+
else if (s.transition) hasTransition = true;
|
|
47
|
+
}
|
|
48
|
+
return !hasIntercept && hasTransition;
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
/**
|
|
32
52
|
* Configuration for creating a partial updater
|
|
33
53
|
*/
|
|
@@ -41,6 +61,13 @@ export interface PartialUpdateConfig {
|
|
|
41
61
|
) => Promise<ReactNode> | ReactNode;
|
|
42
62
|
/** RSC version getter — returns the current version (may change after HMR) */
|
|
43
63
|
getVersion?: () => string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Replace the active app-shell when a cross-app navigation is detected.
|
|
66
|
+
* Called before the full-update tree replacement renders, so the new
|
|
67
|
+
* payload's rootLayout, basename, and version are picked up. Theme,
|
|
68
|
+
* warmup, and prefetch TTL are not part of the shell — see AppShell.
|
|
69
|
+
*/
|
|
70
|
+
applyAppShell?: (next: import("./app-shell.js").AppShell) => void;
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
/**
|
|
@@ -76,7 +103,7 @@ export type UpdateMode =
|
|
|
76
103
|
/** Source URL for intercept restore (popstate cache miss) */
|
|
77
104
|
interceptSourceUrl?: string;
|
|
78
105
|
}
|
|
79
|
-
| { type: "leave-intercept" }
|
|
106
|
+
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
80
107
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
81
108
|
| { type: "action"; interceptSourceUrl?: string };
|
|
82
109
|
|
|
@@ -110,6 +137,7 @@ export function createPartialUpdater(
|
|
|
110
137
|
onUpdate,
|
|
111
138
|
renderSegments,
|
|
112
139
|
getVersion = () => undefined,
|
|
140
|
+
applyAppShell,
|
|
113
141
|
} = config;
|
|
114
142
|
|
|
115
143
|
/**
|
|
@@ -141,13 +169,7 @@ export function createPartialUpdater(
|
|
|
141
169
|
// Capture history key at start for stale revalidation consistency check
|
|
142
170
|
const historyKeyAtStart = store.getHistoryKey();
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
const interceptSourceUrl =
|
|
146
|
-
mode.type === "stale-revalidation" ||
|
|
147
|
-
mode.type === "action" ||
|
|
148
|
-
mode.type === "navigate"
|
|
149
|
-
? mode.interceptSourceUrl
|
|
150
|
-
: undefined;
|
|
172
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
151
173
|
|
|
152
174
|
// When leaving intercept, filter out intercept-specific segments
|
|
153
175
|
let segments: string[];
|
|
@@ -167,9 +189,16 @@ export function createPartialUpdater(
|
|
|
167
189
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
168
190
|
}
|
|
169
191
|
|
|
170
|
-
// For intercept revalidation, use the intercept source URL as previousUrl
|
|
192
|
+
// For intercept revalidation, use the intercept source URL as previousUrl.
|
|
193
|
+
// For leave-intercept, tx.currentUrl captures window.location.href at tx
|
|
194
|
+
// creation, which on popstate is already the destination URL and would
|
|
195
|
+
// tell the server "from == to". segmentState.currentUrl still points at
|
|
196
|
+
// the URL the cached segments render (the intercept URL), which is the
|
|
197
|
+
// correct "from" for the server's diff computation.
|
|
171
198
|
const previousUrl =
|
|
172
|
-
|
|
199
|
+
mode.type === "leave-intercept"
|
|
200
|
+
? segmentState.currentUrl || tx.currentUrl
|
|
201
|
+
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
173
202
|
|
|
174
203
|
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
175
204
|
debugLog(`[Browser] From: ${previousUrl}`);
|
|
@@ -183,11 +212,14 @@ export function createPartialUpdater(
|
|
|
183
212
|
// When navigating with targetCacheSegments, use those for consistency.
|
|
184
213
|
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
185
214
|
const targetCache =
|
|
186
|
-
mode.type === "navigate"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
215
|
+
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
216
|
+
? mode.targetCacheSegments
|
|
217
|
+
: undefined;
|
|
218
|
+
const cachedSegs = targetCache ?? getCurrentCachedSegments();
|
|
219
|
+
const cachedSegsSource = targetCache ? "history-cache" : "current-page";
|
|
220
|
+
debugLog(
|
|
221
|
+
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
222
|
+
);
|
|
191
223
|
|
|
192
224
|
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
193
225
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
@@ -216,7 +248,12 @@ export function createPartialUpdater(
|
|
|
216
248
|
// Detect app switch: if routerId changed, the navigation crossed into
|
|
217
249
|
// a different router (e.g., via host router path mount). Downgrade
|
|
218
250
|
// partial to full so the entire tree is replaced without reconciliation
|
|
219
|
-
// against stale segments from the previous app
|
|
251
|
+
// against stale segments from the previous app, and replace the app
|
|
252
|
+
// shell (rootLayout, basename, version) so the target app's document
|
|
253
|
+
// and router config take effect instead of remaining captured from the
|
|
254
|
+
// initial load. Theme, warmup, and prefetch TTL are intentionally
|
|
255
|
+
// document-lifetime (see AppShell doc); a new document navigation
|
|
256
|
+
// applies them.
|
|
220
257
|
if (payload.metadata?.routerId) {
|
|
221
258
|
const prevRouterId = store.getRouterId?.();
|
|
222
259
|
if (prevRouterId && prevRouterId !== payload.metadata.routerId) {
|
|
@@ -224,6 +261,12 @@ export function createPartialUpdater(
|
|
|
224
261
|
`[Browser] App switch detected (${prevRouterId} → ${payload.metadata.routerId}), forcing full update`,
|
|
225
262
|
);
|
|
226
263
|
payload.metadata.isPartial = false;
|
|
264
|
+
applyAppShell?.({
|
|
265
|
+
routerId: payload.metadata.routerId,
|
|
266
|
+
rootLayout: payload.metadata.rootLayout,
|
|
267
|
+
basename: payload.metadata.basename,
|
|
268
|
+
version: payload.metadata.version,
|
|
269
|
+
});
|
|
227
270
|
}
|
|
228
271
|
store.setRouterId?.(payload.metadata.routerId);
|
|
229
272
|
}
|
|
@@ -267,7 +310,7 @@ export function createPartialUpdater(
|
|
|
267
310
|
.filter(Boolean) as ResolvedSegment[];
|
|
268
311
|
|
|
269
312
|
// When navigating with cached segments to a different route, render them.
|
|
270
|
-
if (mode.type === "navigate" && targetCache
|
|
313
|
+
if (mode.type === "navigate" && targetCache) {
|
|
271
314
|
debugLog(
|
|
272
315
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
273
316
|
);
|
|
@@ -307,10 +350,7 @@ export function createPartialUpdater(
|
|
|
307
350
|
scroll: toScrollPayload(commitScroll),
|
|
308
351
|
};
|
|
309
352
|
|
|
310
|
-
|
|
311
|
-
(s) => s.transition,
|
|
312
|
-
);
|
|
313
|
-
if (cachedHasTransition) {
|
|
353
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
314
354
|
startTransition(() => {
|
|
315
355
|
if (addTransitionType) {
|
|
316
356
|
addTransitionType("navigation");
|
|
@@ -496,7 +536,7 @@ export function createPartialUpdater(
|
|
|
496
536
|
|
|
497
537
|
// Emit update to trigger React render.
|
|
498
538
|
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
499
|
-
const hasTransition = reconciled.
|
|
539
|
+
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
500
540
|
const scrollPayload = toScrollPayload(navScroll);
|
|
501
541
|
|
|
502
542
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
@@ -558,9 +598,7 @@ export function createPartialUpdater(
|
|
|
558
598
|
})
|
|
559
599
|
: tx.commit(segmentIds, segments);
|
|
560
600
|
|
|
561
|
-
const fullHasTransition = segments
|
|
562
|
-
(s: ResolvedSegment) => s.transition,
|
|
563
|
-
);
|
|
601
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
564
602
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
565
603
|
|
|
566
604
|
if (mode.type === "stale-revalidation") {
|