@rangojs/router 0.0.0-experimental.66 → 0.0.0-experimental.66cdebe3
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 +112 -17
- package/dist/vite/index.js +1462 -422
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +54 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +45 -0
- package/skills/layout/SKILL.md +24 -0
- package/skills/links/SKILL.md +234 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +68 -0
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +48 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +9 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +151 -9
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +58 -12
- 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 +95 -44
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +17 -4
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/scroll-restoration.ts +69 -28
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +52 -25
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +87 -175
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +44 -9
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -36
- package/src/route-definition/dsl-helpers.ts +175 -23
- package/src/route-definition/helpers-types.ts +63 -14
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-types.ts +7 -0
- package/src/router/handler-context.ts +21 -38
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/cache-lookup.ts +2 -1
- package/src/router/match-result.ts +101 -4
- package/src/router/middleware-types.ts +14 -25
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +13 -0
- package/src/router/segment-resolution/revalidation.ts +135 -101
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +16 -8
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/server-action.ts +4 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +71 -70
- package/src/server/context.ts +26 -3
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- 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 +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +30 -4
- package/src/urls/response-types.ts +2 -10
- package/src/use-loader.tsx +4 -1
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +172 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- 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 +52 -28
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +545 -304
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +558 -53
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
isInterceptSegment,
|
|
29
29
|
splitInterceptSegments,
|
|
30
30
|
} from "./intercept-utils.js";
|
|
31
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
31
32
|
|
|
32
33
|
// Vite HMR types are provided by vite/client
|
|
33
34
|
|
|
@@ -114,6 +115,13 @@ export interface BrowserAppContext {
|
|
|
114
115
|
warmupEnabled?: boolean;
|
|
115
116
|
/** App version for prefetch version mismatch detection */
|
|
116
117
|
version?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Live app-shell ref. Cross-app navigations replace its contents so the
|
|
120
|
+
* NavigationProvider and renderSegments pick up the target app's
|
|
121
|
+
* rootLayout, basename, and version without consumer rerenders. Theme,
|
|
122
|
+
* warmup, and prefetch TTL are document-lifetime (see AppShell).
|
|
123
|
+
*/
|
|
124
|
+
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// Module-level state for the initialized app
|
|
@@ -204,13 +212,23 @@ export async function initBrowserApp(
|
|
|
204
212
|
// Create composable utilities
|
|
205
213
|
const client = createNavigationClient(deps);
|
|
206
214
|
|
|
207
|
-
//
|
|
208
|
-
|
|
215
|
+
// Capture the per-router app-shell so cross-app navigations can replace
|
|
216
|
+
// it atomically. rootLayout, basename, and version live here and are
|
|
217
|
+
// read through the ref at call time rather than closed over. Theme,
|
|
218
|
+
// warmup, and prefetch TTL are deliberately excluded — they are
|
|
219
|
+
// document-lifetime and stay stable across smooth cross-app transitions.
|
|
209
220
|
const version = initialPayload.metadata?.version;
|
|
221
|
+
const appShellRef = createAppShellRef({
|
|
222
|
+
routerId: initialPayload.metadata?.routerId,
|
|
223
|
+
rootLayout: initialPayload.metadata?.rootLayout,
|
|
224
|
+
basename: initialPayload.metadata?.basename,
|
|
225
|
+
version,
|
|
226
|
+
});
|
|
210
227
|
|
|
211
228
|
// Initialize the localStorage state key for cache invalidation.
|
|
212
|
-
//
|
|
213
|
-
|
|
229
|
+
// The build version busts cached prefetches on deploy; the routerId
|
|
230
|
+
// namespaces the key so sibling apps on the same origin don't collide.
|
|
231
|
+
initRangoState(version ?? "0", initialPayload.metadata?.routerId);
|
|
214
232
|
setAppVersion(version);
|
|
215
233
|
|
|
216
234
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
@@ -220,11 +238,17 @@ export async function initBrowserApp(
|
|
|
220
238
|
initPrefetchCache(prefetchCacheTTL);
|
|
221
239
|
}
|
|
222
240
|
|
|
223
|
-
// Create a bound renderSegments that
|
|
241
|
+
// Create a bound renderSegments that reads rootLayout through the shell
|
|
242
|
+
// ref. On app switch the ref is updated before the tree re-renders, so
|
|
243
|
+
// the new app's Document (rootLayout) replaces the previous one.
|
|
224
244
|
const renderSegments = (
|
|
225
245
|
segments: ResolvedSegment[],
|
|
226
246
|
options?: RenderSegmentsOptions,
|
|
227
|
-
) =>
|
|
247
|
+
) =>
|
|
248
|
+
baseRenderSegments(segments, {
|
|
249
|
+
...options,
|
|
250
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
251
|
+
});
|
|
228
252
|
|
|
229
253
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
230
254
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -256,6 +280,7 @@ export async function initBrowserApp(
|
|
|
256
280
|
onUpdate: (update) => store.emitUpdate(update),
|
|
257
281
|
renderSegments,
|
|
258
282
|
version: version,
|
|
283
|
+
appShellRef,
|
|
259
284
|
});
|
|
260
285
|
|
|
261
286
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -416,6 +441,7 @@ export async function initBrowserApp(
|
|
|
416
441
|
initialTheme: effectiveInitialTheme,
|
|
417
442
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
418
443
|
version,
|
|
444
|
+
appShellRef,
|
|
419
445
|
};
|
|
420
446
|
browserAppContext = context;
|
|
421
447
|
|
|
@@ -481,6 +507,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
481
507
|
initialTheme,
|
|
482
508
|
warmupEnabled,
|
|
483
509
|
version,
|
|
510
|
+
appShellRef,
|
|
484
511
|
} = getBrowserAppContext();
|
|
485
512
|
|
|
486
513
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -501,6 +528,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
501
528
|
warmupEnabled={warmupEnabled}
|
|
502
529
|
version={version}
|
|
503
530
|
basename={initialPayload.metadata?.basename}
|
|
531
|
+
appShellRef={appShellRef}
|
|
504
532
|
/>
|
|
505
533
|
);
|
|
506
534
|
}
|
|
@@ -10,15 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import { debugLog } from "./logging.js";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Defers a callback to the next animation frame.
|
|
15
|
-
* Falls back to setTimeout(0) in environments without requestAnimationFrame.
|
|
16
|
-
*/
|
|
17
|
-
const deferToNextPaint: (fn: () => void) => void =
|
|
18
|
-
typeof requestAnimationFrame === "function"
|
|
19
|
-
? requestAnimationFrame
|
|
20
|
-
: (fn) => setTimeout(fn, 0);
|
|
21
|
-
|
|
22
13
|
const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
|
|
23
14
|
|
|
24
15
|
/**
|
|
@@ -294,16 +285,69 @@ export function restoreScrollPosition(options?: {
|
|
|
294
285
|
return true;
|
|
295
286
|
}
|
|
296
287
|
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
288
|
+
// Defer the actual scrollTo to the next animation frame so the new
|
|
289
|
+
// tree's layout has been measured. Synchronous scrollTo from inside
|
|
290
|
+
// useLayoutEffect runs against an as-yet-unmeasured DOM and silently
|
|
291
|
+
// clamps savedY to the old (or zero) maxScroll — a tall destination
|
|
292
|
+
// restored from a short source then lands at Y=0 instead of the
|
|
293
|
+
// saved position.
|
|
294
|
+
//
|
|
295
|
+
// (scrollToTop / scrollToHash on forward navigations stay synchronous
|
|
296
|
+
// because Y=0 / a hash element are robust against unmeasured layout —
|
|
297
|
+
// they don't depend on the new tree's full scrollHeight, and sync
|
|
298
|
+
// scrolling lands before startViewTransition's snapshot capture.)
|
|
299
|
+
//
|
|
300
|
+
// If a view transition is in flight, scrollTo lands on the live DOM
|
|
301
|
+
// but the user sees the VT snapshot pseudos. When the VT finishes and
|
|
302
|
+
// pseudos are removed, the browser settles scrollY at whatever the
|
|
303
|
+
// pseudo-element layout left it at — often the new tree's max scroll
|
|
304
|
+
// — overwriting the restore. Detect active VT animations and re-apply
|
|
305
|
+
// scrollTo once they finish.
|
|
306
|
+
if (typeof requestAnimationFrame === "function") {
|
|
307
|
+
requestAnimationFrame(() => {
|
|
308
|
+
if (typeof window.scrollTo === "function") {
|
|
309
|
+
window.scrollTo(0, savedY);
|
|
310
|
+
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
311
|
+
}
|
|
312
|
+
const vtAnimations = getActiveViewTransitionAnimations();
|
|
313
|
+
if (vtAnimations.length > 0) {
|
|
314
|
+
Promise.allSettled(vtAnimations.map((a) => a.finished))
|
|
315
|
+
.then(() => {
|
|
316
|
+
if (typeof window.scrollTo === "function") {
|
|
317
|
+
window.scrollTo(0, savedY);
|
|
318
|
+
debugLog("[Scroll] Re-applied position after VT settle:", savedY);
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
.catch(() => {
|
|
322
|
+
// Animations.finished can reject if the animation is canceled
|
|
323
|
+
// (e.g., the user starts another navigation). In that case
|
|
324
|
+
// the next nav's scroll handling takes over.
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
} else if (typeof window.scrollTo === "function") {
|
|
301
329
|
window.scrollTo(0, savedY);
|
|
302
330
|
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
303
|
-
}
|
|
331
|
+
}
|
|
304
332
|
return true;
|
|
305
333
|
}
|
|
306
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Find any in-flight View Transition pseudo-element animations. Used to
|
|
337
|
+
* sequence scroll restoration after the transition settles, so VT
|
|
338
|
+
* pseudo-element teardown doesn't overwrite our scrollTo with the live
|
|
339
|
+
* DOM's post-transition scroll position.
|
|
340
|
+
*/
|
|
341
|
+
function getActiveViewTransitionAnimations(): Animation[] {
|
|
342
|
+
if (typeof document === "undefined") return [];
|
|
343
|
+
if (typeof document.getAnimations !== "function") return [];
|
|
344
|
+
return document.getAnimations().filter((a) => {
|
|
345
|
+
const effect = a.effect as KeyframeEffect | null;
|
|
346
|
+
const pseudo = effect?.pseudoElement;
|
|
347
|
+
return typeof pseudo === "string" && pseudo.startsWith("::view-transition");
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
307
351
|
/**
|
|
308
352
|
* Handle hash link scrolling.
|
|
309
353
|
* Scrolls to element with matching ID if hash is present.
|
|
@@ -332,6 +376,8 @@ export function scrollToHash(): boolean {
|
|
|
332
376
|
* Scroll to top of page
|
|
333
377
|
*/
|
|
334
378
|
export function scrollToTop(): void {
|
|
379
|
+
if (typeof window === "undefined") return;
|
|
380
|
+
if (typeof window.scrollTo !== "function") return;
|
|
335
381
|
window.scrollTo(0, 0);
|
|
336
382
|
}
|
|
337
383
|
|
|
@@ -374,20 +420,15 @@ export function handleNavigationEnd(options: {
|
|
|
374
420
|
// Fall through to hash or top if no saved position
|
|
375
421
|
}
|
|
376
422
|
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Default: scroll to top
|
|
389
|
-
scrollToTop();
|
|
390
|
-
});
|
|
423
|
+
// Sync hash scroll / scroll-to-top — see the long comment in
|
|
424
|
+
// restoreScrollPosition above. handleNavigationEnd runs from
|
|
425
|
+
// NavigationProvider's useLayoutEffect (post-commit, pre-paint) and from
|
|
426
|
+
// bridge sites that don't trigger a React commit, so a synchronous
|
|
427
|
+
// scrollTo is captured by the next paint / startViewTransition snapshot.
|
|
428
|
+
if (scrollToHash()) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
scrollToTop();
|
|
391
432
|
}
|
|
392
433
|
|
|
393
434
|
/**
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./merge-segment-loaders.js";
|
|
7
7
|
import { assertSegmentStructure } from "./segment-structure-assert.js";
|
|
8
8
|
import { splitInterceptSegments } from "./intercept-utils.js";
|
|
9
|
+
import { debugLog } from "./logging.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Determines the merging behavior for segment reconciliation.
|
|
@@ -85,14 +86,29 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
85
86
|
const cachedSegments = new Map<string, ResolvedSegment>();
|
|
86
87
|
input.cachedSegments.forEach((s) => cachedSegments.set(s.id, s));
|
|
87
88
|
|
|
89
|
+
const diffSet = new Set(diff);
|
|
90
|
+
debugLog(
|
|
91
|
+
`[reconcile] actor=${actor}, matched=${matched.length}, diff=${diff.length}`,
|
|
92
|
+
);
|
|
93
|
+
debugLog(
|
|
94
|
+
`[reconcile] server segments: ${[...serverSegments.keys()].join(", ")}`,
|
|
95
|
+
);
|
|
96
|
+
debugLog(
|
|
97
|
+
`[reconcile] cached segments: ${[...cachedSegments.keys()].join(", ")}`,
|
|
98
|
+
);
|
|
99
|
+
|
|
88
100
|
const segments = matched
|
|
89
101
|
.map((segId: string) => {
|
|
90
102
|
const fromServer = serverSegments.get(segId);
|
|
91
103
|
const fromCache = cachedSegments.get(segId);
|
|
92
104
|
|
|
93
105
|
if (fromServer) {
|
|
106
|
+
const inDiff = diffSet.has(segId);
|
|
94
107
|
// Merge partial loader data when server returns fewer loaders than cached
|
|
95
108
|
if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
|
|
109
|
+
debugLog(
|
|
110
|
+
`[reconcile] ${segId}: MERGE loaders (server partial, ${inDiff ? "in diff" : "not in diff"})`,
|
|
111
|
+
);
|
|
96
112
|
return mergeSegmentLoaders(fromServer, fromCache);
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -143,8 +159,14 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
143
159
|
// above fails to preserve a value it should have.
|
|
144
160
|
assertSegmentStructure(fromCache, merged, context);
|
|
145
161
|
|
|
162
|
+
debugLog(
|
|
163
|
+
`[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
|
|
164
|
+
);
|
|
146
165
|
return merged;
|
|
147
166
|
}
|
|
167
|
+
debugLog(
|
|
168
|
+
`[reconcile] ${segId}: SERVER only (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, no cache entry)`,
|
|
169
|
+
);
|
|
148
170
|
return fromServer;
|
|
149
171
|
}
|
|
150
172
|
|
|
@@ -158,20 +180,20 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
158
180
|
return fromCache;
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
debugLog(
|
|
184
|
+
`[reconcile] ${segId}: CACHE only (not from server, type=${fromCache.type}, component=${fromCache.component != null ? "yes" : "null"})`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Return the cached segment as-is, regardless of actor. We used to clear
|
|
188
|
+
// truthy `loading` here to prevent a stale Suspense fallback from
|
|
189
|
+
// committing against cached content, but that swapped the render tree
|
|
190
|
+
// from the LoaderBoundary branch to the plain OutletProvider branch
|
|
191
|
+
// inside renderSegments, causing React to unmount the entire chain
|
|
192
|
+
// (LoaderBoundary > Suspense > LoaderResolver > RouteContentWrapper >
|
|
193
|
+
// Suspender) every time the user opened an intercept or navigated back
|
|
194
|
+
// to a cached page. The flicker is now prevented by renderSegments'
|
|
195
|
+
// promise memoization keeping React's use() in "known fulfilled" state,
|
|
196
|
+
// so preserving `loading` keeps the element tree stable.
|
|
175
197
|
return fromCache;
|
|
176
198
|
})
|
|
177
199
|
.filter(Boolean) as ResolvedSegment[];
|
package/src/browser/types.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface RscMetadata {
|
|
|
39
39
|
isError?: boolean;
|
|
40
40
|
matched?: string[];
|
|
41
41
|
diff?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* All segment ids re-resolved on the server, including null-component
|
|
44
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
45
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
46
|
+
*/
|
|
47
|
+
resolvedIds?: string[];
|
|
42
48
|
/** Merged route params from the matched route */
|
|
43
49
|
params?: Record<string, string>;
|
|
44
50
|
/**
|
|
@@ -427,6 +433,12 @@ export interface NavigationStore {
|
|
|
427
433
|
markCacheAsStale(): void;
|
|
428
434
|
markCacheAsStaleAndBroadcast(): void;
|
|
429
435
|
clearHistoryCache(): void;
|
|
436
|
+
/**
|
|
437
|
+
* Clear this tab's nav + prefetch caches without broadcasting or rotating
|
|
438
|
+
* shared state. Intended for app-switch transitions that affect only this
|
|
439
|
+
* tab's session.
|
|
440
|
+
*/
|
|
441
|
+
clearHistoryCacheLocal(): void;
|
|
430
442
|
broadcastCacheInvalidation(): void;
|
|
431
443
|
|
|
432
444
|
// Cross-tab refresh callback (set by navigation bridge)
|
|
@@ -542,6 +554,13 @@ export interface NavigationBridge {
|
|
|
542
554
|
registerLinkInterception(): () => void;
|
|
543
555
|
/** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
|
|
544
556
|
updateVersion(newVersion: string): void;
|
|
557
|
+
/**
|
|
558
|
+
* Replace the active app-shell snapshot (rootLayout, basename, version)
|
|
559
|
+
* atomically. Used on cross-app navigations when the response's routerId
|
|
560
|
+
* indicates the user entered a different app. Theme, warmup, and prefetch
|
|
561
|
+
* TTL are document-lifetime and not part of the shell.
|
|
562
|
+
*/
|
|
563
|
+
updateAppShell(next: import("./app-shell.js").AppShell): void;
|
|
545
564
|
}
|
|
546
565
|
|
|
547
566
|
/**
|
package/src/build/route-trie.ts
CHANGED
|
@@ -20,7 +20,8 @@ export interface TrieLeaf {
|
|
|
20
20
|
sp: string;
|
|
21
21
|
/** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
|
|
22
22
|
a: string[];
|
|
23
|
-
/** Optional param names
|
|
23
|
+
/** Optional param names declared on the route. Absent params are
|
|
24
|
+
* omitted from the matched params record (read as `undefined`). */
|
|
24
25
|
op?: string[];
|
|
25
26
|
/** Constraint validation: paramName -> allowed values */
|
|
26
27
|
cv?: Record<string, string[]>;
|
|
@@ -98,8 +99,14 @@ export function buildRouteTrie(
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
|
-
* Insert a route into the trie
|
|
102
|
-
*
|
|
102
|
+
* Insert a route into the trie. Optional params expand into two branches at
|
|
103
|
+
* registration time (skip-first, then present), so each terminal lives at the
|
|
104
|
+
* correct depth for its number of bound params and carries a branch-local
|
|
105
|
+
* `pa` listing only those names. The trie's single-slot `node.p` is reused
|
|
106
|
+
* across branches because matching ignores `node.p.n` — the leaf's `pa` is
|
|
107
|
+
* the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
|
|
108
|
+
* last-wins rule produce greedy-leftmost semantics for free at any shared
|
|
109
|
+
* terminal depth.
|
|
103
110
|
*/
|
|
104
111
|
function insertRoute(
|
|
105
112
|
node: TrieNode,
|
|
@@ -107,14 +114,13 @@ function insertRoute(
|
|
|
107
114
|
index: number,
|
|
108
115
|
leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
|
|
109
116
|
): void {
|
|
110
|
-
//
|
|
111
|
-
|
|
117
|
+
// op (full optional list) and cv (full constraint map) are route-level and
|
|
118
|
+
// identical on every terminal, so compute them once on the shared base.
|
|
112
119
|
const optionalParams: string[] = [];
|
|
113
120
|
const constraints: Record<string, string[]> = {};
|
|
114
121
|
|
|
115
122
|
for (const seg of segments) {
|
|
116
123
|
if (seg.type === "param") {
|
|
117
|
-
paramNames.push(seg.value);
|
|
118
124
|
if (seg.optional) {
|
|
119
125
|
optionalParams.push(seg.value);
|
|
120
126
|
}
|
|
@@ -124,21 +130,15 @@ function insertRoute(
|
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
const
|
|
133
|
+
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
128
134
|
...leaf,
|
|
129
|
-
...(paramNames.length > 0 ? { pa: paramNames } : {}),
|
|
130
135
|
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
131
136
|
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
132
137
|
};
|
|
133
138
|
|
|
134
|
-
insertSegments(node, segments, index,
|
|
139
|
+
insertSegments(node, segments, index, leafBase, []);
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
/**
|
|
138
|
-
* Recursively insert segments into the trie.
|
|
139
|
-
* For optional params, we add a terminal at the current node (param absent)
|
|
140
|
-
* AND continue inserting into the param child (param present).
|
|
141
|
-
*/
|
|
142
142
|
/**
|
|
143
143
|
* Extract ancestry map from a built trie by visiting all leaf nodes.
|
|
144
144
|
* Returns { routeName: ancestryShortCodes[] } for every route in the trie.
|
|
@@ -218,15 +218,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
|
|
|
218
218
|
node.r = mergeLeaves(node.r, leaf);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
function buildLeaf(
|
|
222
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
223
|
+
paramNames: string[],
|
|
224
|
+
): TrieLeaf {
|
|
225
|
+
return paramNames.length > 0
|
|
226
|
+
? { ...leafBase, pa: [...paramNames] }
|
|
227
|
+
: { ...leafBase };
|
|
228
|
+
}
|
|
229
|
+
|
|
221
230
|
function insertSegments(
|
|
222
231
|
node: TrieNode,
|
|
223
232
|
segments: ParsedSegment[],
|
|
224
233
|
index: number,
|
|
225
|
-
|
|
234
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
235
|
+
paramNames: string[],
|
|
226
236
|
): void {
|
|
227
|
-
// Base case: all segments consumed, add terminal
|
|
237
|
+
// Base case: all segments consumed, add terminal with branch-local pa
|
|
228
238
|
if (index >= segments.length) {
|
|
229
|
-
mergeLeaf(node,
|
|
239
|
+
mergeLeaf(node, buildLeaf(leafBase, paramNames));
|
|
230
240
|
return;
|
|
231
241
|
}
|
|
232
242
|
|
|
@@ -235,12 +245,19 @@ function insertSegments(
|
|
|
235
245
|
if (segment.type === "static") {
|
|
236
246
|
if (!node.s) node.s = {};
|
|
237
247
|
if (!node.s[segment.value]) node.s[segment.value] = {};
|
|
238
|
-
insertSegments(
|
|
248
|
+
insertSegments(
|
|
249
|
+
node.s[segment.value],
|
|
250
|
+
segments,
|
|
251
|
+
index + 1,
|
|
252
|
+
leafBase,
|
|
253
|
+
paramNames,
|
|
254
|
+
);
|
|
239
255
|
} else if (segment.type === "param") {
|
|
240
256
|
if (segment.optional) {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
//
|
|
257
|
+
// SKIP first: continue at the same node without binding this name.
|
|
258
|
+
// Skip-first ordering means the present-branch's TAKE overwrites any
|
|
259
|
+
// shared terminal later, giving greedy-leftmost semantics.
|
|
260
|
+
insertSegments(node, segments, index + 1, leafBase, paramNames);
|
|
244
261
|
}
|
|
245
262
|
if (segment.suffix) {
|
|
246
263
|
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
@@ -248,16 +265,26 @@ function insertSegments(
|
|
|
248
265
|
if (!node.xp[segment.suffix]) {
|
|
249
266
|
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
267
|
}
|
|
251
|
-
insertSegments(node.xp[segment.suffix].c, segments, index + 1,
|
|
268
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
|
|
269
|
+
...paramNames,
|
|
270
|
+
segment.value,
|
|
271
|
+
]);
|
|
252
272
|
} else {
|
|
253
273
|
if (!node.p) {
|
|
254
274
|
node.p = { n: segment.value, c: {} };
|
|
255
275
|
}
|
|
256
|
-
insertSegments(node.p.c, segments, index + 1,
|
|
276
|
+
insertSegments(node.p.c, segments, index + 1, leafBase, [
|
|
277
|
+
...paramNames,
|
|
278
|
+
segment.value,
|
|
279
|
+
]);
|
|
257
280
|
}
|
|
258
281
|
} else if (segment.type === "wildcard") {
|
|
259
|
-
// Wildcard consumes all remaining segments
|
|
260
|
-
|
|
282
|
+
// Wildcard consumes all remaining segments. Carry any params bound before
|
|
283
|
+
// the wildcard in pa so they zip correctly against paramValues at match.
|
|
284
|
+
const wildLeaf: TrieLeaf & { pn: string } = {
|
|
285
|
+
...buildLeaf(leafBase, paramNames),
|
|
286
|
+
pn: "*",
|
|
287
|
+
};
|
|
261
288
|
const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
|
|
262
289
|
const merged = mergeLeaves(existing, wildLeaf);
|
|
263
290
|
node.w = merged as TrieLeaf & { pn: string };
|
|
@@ -67,13 +67,11 @@ export const MAX_REVALIDATION_INTERVAL = 30;
|
|
|
67
67
|
// Types
|
|
68
68
|
// ============================================================================
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
passThroughOnException(): void;
|
|
76
|
-
}
|
|
70
|
+
// Re-exported from the canonical home so cf-cache-store consumers keep
|
|
71
|
+
// importing `ExecutionContext` from this module without a second interface
|
|
72
|
+
// drifting over time.
|
|
73
|
+
export type { ExecutionContext } from "../../types/request-scope.js";
|
|
74
|
+
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
77
|
* Minimal Cloudflare KV Namespace interface.
|
package/src/client.rsc.tsx
CHANGED
|
@@ -78,6 +78,9 @@ export {
|
|
|
78
78
|
// Re-export useHref - it's a "use client" hook
|
|
79
79
|
export { useHref } from "./browser/react/use-href.js";
|
|
80
80
|
|
|
81
|
+
// Re-export useReverse - it's a "use client" hook
|
|
82
|
+
export { useReverse } from "./browser/react/use-reverse.js";
|
|
83
|
+
|
|
81
84
|
// Re-export useHandle - it's a "use client" hook
|
|
82
85
|
export { useHandle } from "./browser/react/use-handle.js";
|
|
83
86
|
|