@rangojs/router 0.0.0-experimental.6fe6a3cc → 0.0.0-experimental.78a48627
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +43 -9
- package/src/browser/react/NavigationProvider.tsx +27 -0
- package/src/browser/scroll-restoration.ts +19 -7
- package/src/browser/types.ts +9 -0
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +2 -0
- package/src/router/logging.ts +4 -1
- package/src/router/manifest.ts +3 -1
- package/src/router/router-context.ts +4 -1
- package/src/router/segment-resolution/revalidation.ts +2 -0
- package/src/router.ts +4 -0
- package/src/types/route-entry.ts +7 -0
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.78a48627",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
package/package.json
CHANGED
|
@@ -472,6 +472,7 @@ export function createNavigationBridge(
|
|
|
472
472
|
cachedHandleData,
|
|
473
473
|
params: cachedParams,
|
|
474
474
|
},
|
|
475
|
+
scroll: { restore: true, isStreaming },
|
|
475
476
|
};
|
|
476
477
|
const hasTransition = cachedSegments.some((s) => s.transition);
|
|
477
478
|
if (hasTransition) {
|
|
@@ -485,9 +486,6 @@ export function createNavigationBridge(
|
|
|
485
486
|
onUpdate(popstateUpdate);
|
|
486
487
|
}
|
|
487
488
|
|
|
488
|
-
// Restore scroll position for back/forward navigation
|
|
489
|
-
handleNavigationEnd({ restore: true, isStreaming });
|
|
490
|
-
|
|
491
489
|
// SWR: If stale, trigger background revalidation
|
|
492
490
|
if (isStale) {
|
|
493
491
|
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
8
8
|
import {
|
|
9
9
|
handleNavigationStart,
|
|
10
|
-
handleNavigationEnd,
|
|
11
10
|
ensureHistoryKey,
|
|
12
11
|
} from "./scroll-restoration.js";
|
|
13
12
|
import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
@@ -81,11 +80,12 @@ export interface BoundTransaction {
|
|
|
81
80
|
readonly currentUrl: string;
|
|
82
81
|
/** Start streaming and get a token to end it when the stream completes */
|
|
83
82
|
startStreaming(): StreamingToken;
|
|
83
|
+
/** Commit the navigation. Returns the effective scroll option for the caller to handle. */
|
|
84
84
|
commit(
|
|
85
85
|
segmentIds: string[],
|
|
86
86
|
segments: ResolvedSegment[],
|
|
87
87
|
overrides?: BoundCommitOverrides,
|
|
88
|
-
):
|
|
88
|
+
): { scroll?: boolean };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -93,7 +93,7 @@ export interface BoundTransaction {
|
|
|
93
93
|
* Uses the event controller handle for lifecycle management
|
|
94
94
|
*/
|
|
95
95
|
interface NavigationTransaction extends Disposable {
|
|
96
|
-
commit(options: CommitOptions):
|
|
96
|
+
commit(options: CommitOptions): { scroll?: boolean };
|
|
97
97
|
with(
|
|
98
98
|
options: Omit<CommitOptions, "segmentIds" | "segments">,
|
|
99
99
|
): BoundTransaction;
|
|
@@ -120,7 +120,7 @@ export function createNavigationTransaction(
|
|
|
120
120
|
/**
|
|
121
121
|
* Commit the navigation - updates store and URL atomically
|
|
122
122
|
*/
|
|
123
|
-
function commit(opts: CommitOptions):
|
|
123
|
+
function commit(opts: CommitOptions): { scroll?: boolean } {
|
|
124
124
|
committed = true;
|
|
125
125
|
|
|
126
126
|
const {
|
|
@@ -150,7 +150,7 @@ export function createNavigationTransaction(
|
|
|
150
150
|
// Without this, the entry lingers and weakens state-machine invariants.
|
|
151
151
|
handle.complete(parsedUrl);
|
|
152
152
|
debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
153
|
-
return;
|
|
153
|
+
return { scroll: false };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Save current scroll position before navigating
|
|
@@ -172,7 +172,7 @@ export function createNavigationTransaction(
|
|
|
172
172
|
debugLog("[Browser] Store updated (action)");
|
|
173
173
|
// Complete navigation to clear loading state
|
|
174
174
|
handle.complete(parsedUrl);
|
|
175
|
-
return;
|
|
175
|
+
return { scroll: false };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Build history state - include user state, intercept info, and server-set state
|
|
@@ -205,14 +205,16 @@ export function createNavigationTransaction(
|
|
|
205
205
|
// Complete the navigation in event controller (sets idle state, updates location)
|
|
206
206
|
handle.complete(parsedUrl);
|
|
207
207
|
|
|
208
|
-
//
|
|
209
|
-
|
|
208
|
+
// NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
|
|
209
|
+
// scroll AFTER onUpdate() so React has the new content before we scroll.
|
|
210
210
|
|
|
211
211
|
debugLog(
|
|
212
212
|
"[Browser] Navigation committed, historyKey:",
|
|
213
213
|
historyKey,
|
|
214
214
|
intercept ? "(intercept)" : "",
|
|
215
215
|
);
|
|
216
|
+
|
|
217
|
+
return { scroll };
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
return {
|
|
@@ -263,7 +265,7 @@ export function createNavigationTransaction(
|
|
|
263
265
|
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
264
266
|
// Server-set location state: only from overrides (set by partial-update)
|
|
265
267
|
const serverState = overrides?.serverState;
|
|
266
|
-
commit({
|
|
268
|
+
return commit({
|
|
267
269
|
...opts,
|
|
268
270
|
segmentIds,
|
|
269
271
|
segments,
|
|
@@ -246,7 +246,10 @@ export function createPartialUpdater(
|
|
|
246
246
|
forceAwait: true,
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
tx.commit(
|
|
249
|
+
const { scroll: commitScroll } = tx.commit(
|
|
250
|
+
matchedIds,
|
|
251
|
+
existingSegments,
|
|
252
|
+
);
|
|
250
253
|
|
|
251
254
|
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
252
255
|
// breadcrumbs and other handle data from cache.
|
|
@@ -260,6 +263,10 @@ export function createPartialUpdater(
|
|
|
260
263
|
...metadataWithoutHandles,
|
|
261
264
|
cachedHandleData: mode.targetCacheHandleData,
|
|
262
265
|
},
|
|
266
|
+
scroll:
|
|
267
|
+
commitScroll !== false
|
|
268
|
+
? { enabled: commitScroll }
|
|
269
|
+
: { enabled: false },
|
|
263
270
|
};
|
|
264
271
|
|
|
265
272
|
const cachedHasTransition = existingSegments.some(
|
|
@@ -290,11 +297,18 @@ export function createPartialUpdater(
|
|
|
290
297
|
forceAwait: true,
|
|
291
298
|
});
|
|
292
299
|
|
|
293
|
-
tx.commit(
|
|
300
|
+
const { scroll: leaveScroll } = tx.commit(
|
|
301
|
+
matchedIds,
|
|
302
|
+
existingSegments,
|
|
303
|
+
);
|
|
294
304
|
|
|
295
305
|
onUpdate({
|
|
296
306
|
root: newTree,
|
|
297
307
|
metadata: payload.metadata,
|
|
308
|
+
scroll:
|
|
309
|
+
leaveScroll !== false
|
|
310
|
+
? { enabled: leaveScroll }
|
|
311
|
+
: { enabled: false },
|
|
298
312
|
});
|
|
299
313
|
|
|
300
314
|
debugLog("[Browser] Navigation complete (left intercept)");
|
|
@@ -426,7 +440,11 @@ export function createPartialUpdater(
|
|
|
426
440
|
: serverLocationState
|
|
427
441
|
? { serverState: serverLocationState }
|
|
428
442
|
: undefined;
|
|
429
|
-
tx.commit(
|
|
443
|
+
const { scroll: navScroll } = tx.commit(
|
|
444
|
+
allSegmentIds,
|
|
445
|
+
reconciled.segments,
|
|
446
|
+
overrides,
|
|
447
|
+
);
|
|
430
448
|
|
|
431
449
|
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
432
450
|
if (mode.type === "stale-revalidation") {
|
|
@@ -441,8 +459,13 @@ export function createPartialUpdater(
|
|
|
441
459
|
|
|
442
460
|
debugLog("[partial-update] updating document");
|
|
443
461
|
|
|
444
|
-
// Emit update to trigger React render
|
|
462
|
+
// Emit update to trigger React render.
|
|
463
|
+
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
445
464
|
const hasTransition = reconciled.mainSegments.some((s) => s.transition);
|
|
465
|
+
const scrollPayload =
|
|
466
|
+
navScroll !== false
|
|
467
|
+
? { enabled: navScroll }
|
|
468
|
+
: { enabled: false as const };
|
|
446
469
|
|
|
447
470
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
448
471
|
startTransition(() => {
|
|
@@ -452,6 +475,7 @@ export function createPartialUpdater(
|
|
|
452
475
|
onUpdate({
|
|
453
476
|
root: newTree,
|
|
454
477
|
metadata: payload.metadata!,
|
|
478
|
+
scroll: scrollPayload,
|
|
455
479
|
});
|
|
456
480
|
});
|
|
457
481
|
} else if (hasTransition) {
|
|
@@ -462,12 +486,14 @@ export function createPartialUpdater(
|
|
|
462
486
|
onUpdate({
|
|
463
487
|
root: newTree,
|
|
464
488
|
metadata: payload.metadata!,
|
|
489
|
+
scroll: scrollPayload,
|
|
465
490
|
});
|
|
466
491
|
});
|
|
467
492
|
} else {
|
|
468
493
|
onUpdate({
|
|
469
494
|
root: newTree,
|
|
470
495
|
metadata: payload.metadata!,
|
|
496
|
+
scroll: scrollPayload,
|
|
471
497
|
});
|
|
472
498
|
}
|
|
473
499
|
|
|
@@ -494,15 +520,19 @@ export function createPartialUpdater(
|
|
|
494
520
|
}
|
|
495
521
|
|
|
496
522
|
const fullUpdateServerState = payload.metadata?.locationState;
|
|
497
|
-
|
|
498
|
-
tx.commit(segmentIds, segments, {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
523
|
+
const { scroll: fullScroll } = fullUpdateServerState
|
|
524
|
+
? tx.commit(segmentIds, segments, {
|
|
525
|
+
serverState: fullUpdateServerState,
|
|
526
|
+
})
|
|
527
|
+
: tx.commit(segmentIds, segments);
|
|
502
528
|
|
|
503
529
|
const fullHasTransition = segments.some(
|
|
504
530
|
(s: ResolvedSegment) => s.transition,
|
|
505
531
|
);
|
|
532
|
+
const fullScrollPayload =
|
|
533
|
+
fullScroll !== false
|
|
534
|
+
? { enabled: fullScroll }
|
|
535
|
+
: { enabled: false as const };
|
|
506
536
|
|
|
507
537
|
if (mode.type === "stale-revalidation") {
|
|
508
538
|
await rawStreamComplete;
|
|
@@ -513,6 +543,7 @@ export function createPartialUpdater(
|
|
|
513
543
|
onUpdate({
|
|
514
544
|
root: newTree,
|
|
515
545
|
metadata: payload.metadata!,
|
|
546
|
+
scroll: fullScrollPayload,
|
|
516
547
|
});
|
|
517
548
|
});
|
|
518
549
|
} else if (mode.type === "action") {
|
|
@@ -523,6 +554,7 @@ export function createPartialUpdater(
|
|
|
523
554
|
onUpdate({
|
|
524
555
|
root: newTree,
|
|
525
556
|
metadata: payload.metadata!,
|
|
557
|
+
scroll: fullScrollPayload,
|
|
526
558
|
});
|
|
527
559
|
});
|
|
528
560
|
} else if (fullHasTransition) {
|
|
@@ -533,12 +565,14 @@ export function createPartialUpdater(
|
|
|
533
565
|
onUpdate({
|
|
534
566
|
root: newTree,
|
|
535
567
|
metadata: payload.metadata!,
|
|
568
|
+
scroll: fullScrollPayload,
|
|
536
569
|
});
|
|
537
570
|
});
|
|
538
571
|
} else {
|
|
539
572
|
onUpdate({
|
|
540
573
|
root: newTree,
|
|
541
574
|
metadata: payload.metadata!,
|
|
575
|
+
scroll: fullScrollPayload,
|
|
542
576
|
});
|
|
543
577
|
}
|
|
544
578
|
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import React, {
|
|
4
4
|
useState,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useCallback,
|
|
7
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
8
10
|
use,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from "react";
|
|
@@ -25,6 +27,7 @@ import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
|
25
27
|
import { NonceContext } from "./nonce-context.js";
|
|
26
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
27
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
|
+
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Process handles from an async generator, updating the event controller
|
|
@@ -301,9 +304,33 @@ export function NavigationProvider({
|
|
|
301
304
|
return unsub;
|
|
302
305
|
}, [eventController]);
|
|
303
306
|
|
|
307
|
+
// Pending scroll action to apply after React commits
|
|
308
|
+
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
309
|
+
|
|
310
|
+
// Apply scroll after React commits the new content to the DOM
|
|
311
|
+
useLayoutEffect(() => {
|
|
312
|
+
const scrollAction = pendingScrollRef.current;
|
|
313
|
+
if (!scrollAction) return;
|
|
314
|
+
pendingScrollRef.current = undefined;
|
|
315
|
+
|
|
316
|
+
if (scrollAction.enabled === false) return;
|
|
317
|
+
|
|
318
|
+
handleNavigationEnd({
|
|
319
|
+
restore: scrollAction.restore,
|
|
320
|
+
scroll: scrollAction.enabled,
|
|
321
|
+
isStreaming: scrollAction.isStreaming,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
304
325
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
305
326
|
useEffect(() => {
|
|
306
327
|
const unsubscribe = store.onUpdate((update) => {
|
|
328
|
+
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
329
|
+
// after React commits this state update to the DOM.
|
|
330
|
+
if (update.scroll) {
|
|
331
|
+
pendingScrollRef.current = update.scroll;
|
|
332
|
+
}
|
|
333
|
+
|
|
307
334
|
setPayload({
|
|
308
335
|
root: update.root,
|
|
309
336
|
metadata: update.metadata,
|
|
@@ -288,7 +288,11 @@ export function restoreScrollPosition(options?: {
|
|
|
288
288
|
// Not streaming — scroll after React commits and browser paints.
|
|
289
289
|
// startTransition defers the DOM commit, so scrolling synchronously
|
|
290
290
|
// would be overwritten when React replaces the content.
|
|
291
|
-
|
|
291
|
+
const defer =
|
|
292
|
+
typeof requestAnimationFrame === "function"
|
|
293
|
+
? requestAnimationFrame
|
|
294
|
+
: (fn: () => void) => setTimeout(fn, 0);
|
|
295
|
+
defer(() => {
|
|
292
296
|
window.scrollTo(0, savedY);
|
|
293
297
|
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
294
298
|
});
|
|
@@ -366,13 +370,21 @@ export function handleNavigationEnd(options: {
|
|
|
366
370
|
// Fall through to hash or top if no saved position
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
// Defer hash and scroll-to-top to after React paints the new content,
|
|
374
|
+
// so the user doesn't see the current page jump before the new route appears.
|
|
375
|
+
const defer =
|
|
376
|
+
typeof requestAnimationFrame === "function"
|
|
377
|
+
? requestAnimationFrame
|
|
378
|
+
: (fn: () => void) => setTimeout(fn, 0);
|
|
379
|
+
defer(() => {
|
|
380
|
+
// Try hash scrolling first
|
|
381
|
+
if (scrollToHash()) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
373
384
|
|
|
374
|
-
|
|
375
|
-
|
|
385
|
+
// Default: scroll to top
|
|
386
|
+
scrollToTop();
|
|
387
|
+
});
|
|
376
388
|
}
|
|
377
389
|
|
|
378
390
|
/**
|
package/src/browser/types.ts
CHANGED
|
@@ -215,6 +215,15 @@ export interface SegmentState {
|
|
|
215
215
|
export interface NavigationUpdate {
|
|
216
216
|
root: ReactNode | Promise<ReactNode>;
|
|
217
217
|
metadata: RscMetadata;
|
|
218
|
+
/** Scroll behavior to apply after React commits this update */
|
|
219
|
+
scroll?: {
|
|
220
|
+
/** For back/forward: restore saved position */
|
|
221
|
+
restore?: boolean;
|
|
222
|
+
/** Set to false to disable scrolling entirely */
|
|
223
|
+
enabled?: boolean;
|
|
224
|
+
/** Function to check if streaming is in progress */
|
|
225
|
+
isStreaming?: () => boolean;
|
|
226
|
+
};
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
/**
|
package/src/route-map-builder.ts
CHANGED
|
@@ -199,7 +199,13 @@ export function registerRouterManifestLoader(
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
202
|
-
|
|
202
|
+
// Check both manifest AND trie. The virtual module's setRouterManifest()
|
|
203
|
+
// pre-sets the manifest at startup, but the per-router trie is only
|
|
204
|
+
// available from the lazy loader. Without this, the lazy loader never
|
|
205
|
+
// runs and findMatch falls back to the global merged trie — which
|
|
206
|
+
// contains routes from ALL routers and breaks multi-router setups.
|
|
207
|
+
if (perRouterManifestMap.has(routerId) && perRouterTrieMap.has(routerId))
|
|
208
|
+
return;
|
|
203
209
|
const loader = routerManifestLoaders.get(routerId);
|
|
204
210
|
if (loader) {
|
|
205
211
|
const mod = await loader();
|
package/src/router/find-match.ts
CHANGED
|
@@ -52,8 +52,10 @@ export function createFindMatch<TEnv = any>(
|
|
|
52
52
|
: undefined;
|
|
53
53
|
|
|
54
54
|
// Phase 1: Try trie match (O(path_length))
|
|
55
|
-
//
|
|
56
|
-
|
|
55
|
+
// Only use the per-router trie. The global trie merges routes from ALL
|
|
56
|
+
// routers and must not be used — in multi-router setups (host routing)
|
|
57
|
+
// overlapping paths like "/" would match the wrong app's route.
|
|
58
|
+
const routeTrie = getRouterTrie(deps.routerId);
|
|
57
59
|
if (routeTrie) {
|
|
58
60
|
const trieStart = performance.now();
|
|
59
61
|
const trieResult = tryTrieMatch(routeTrie, pathname);
|
|
@@ -188,6 +188,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
188
188
|
context,
|
|
189
189
|
actionContext,
|
|
190
190
|
stale,
|
|
191
|
+
traceSource: "intercept-loader",
|
|
191
192
|
});
|
|
192
193
|
|
|
193
194
|
if (!shouldRevalidate) {
|
|
@@ -355,6 +356,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
355
356
|
context,
|
|
356
357
|
actionContext,
|
|
357
358
|
stale,
|
|
359
|
+
traceSource: "intercept-loader",
|
|
358
360
|
});
|
|
359
361
|
|
|
360
362
|
if (!shouldRevalidate) {
|
|
@@ -14,6 +14,7 @@ export interface LazyEvalDeps<TEnv = any> {
|
|
|
14
14
|
mergedRouteMap: Record<string, string>;
|
|
15
15
|
nextMountIndex: () => number;
|
|
16
16
|
getPrecomputedByPrefix: () => Map<string, Record<string, string>> | null;
|
|
17
|
+
routerId?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
// Detect lazy includes in handler result and create placeholder entries
|
|
@@ -200,6 +201,7 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
200
201
|
trailingSlash: entry.trailingSlash,
|
|
201
202
|
handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
|
|
202
203
|
mountIndex: deps.nextMountIndex(),
|
|
204
|
+
routerId: deps.routerId,
|
|
203
205
|
// Lazy evaluation fields
|
|
204
206
|
lazy: true,
|
|
205
207
|
lazyPatterns: lazyInclude.patterns,
|
package/src/router/logging.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface RevalidationTraceEntry {
|
|
|
12
12
|
| "cache-hit"
|
|
13
13
|
| "loader"
|
|
14
14
|
| "parallel"
|
|
15
|
-
| "orphan-layout"
|
|
15
|
+
| "orphan-layout"
|
|
16
|
+
| "route-handler"
|
|
17
|
+
| "layout-handler"
|
|
18
|
+
| "intercept-loader";
|
|
16
19
|
defaultShouldRevalidate: boolean;
|
|
17
20
|
finalShouldRevalidate: boolean;
|
|
18
21
|
reason: string;
|
package/src/router/manifest.ts
CHANGED
|
@@ -65,7 +65,9 @@ export async function loadManifest(
|
|
|
65
65
|
const mountIndex = entry.mountIndex;
|
|
66
66
|
|
|
67
67
|
// Check module-level cache (persists across requests within same isolate)
|
|
68
|
-
|
|
68
|
+
// Include routerId so multi-router setups (host routing) don't share cached
|
|
69
|
+
// EntryData across routers with overlapping mountIndex + routeKey combinations.
|
|
70
|
+
const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
|
|
69
71
|
const cached = manifestModuleCache.get(cacheKey);
|
|
70
72
|
if (cached) {
|
|
71
73
|
const cacheStart = performance.now();
|
|
@@ -189,7 +189,10 @@ export interface RouterContext<TEnv = any> {
|
|
|
189
189
|
| "cache-hit"
|
|
190
190
|
| "loader"
|
|
191
191
|
| "parallel"
|
|
192
|
-
| "orphan-layout"
|
|
192
|
+
| "orphan-layout"
|
|
193
|
+
| "route-handler"
|
|
194
|
+
| "layout-handler"
|
|
195
|
+
| "intercept-loader";
|
|
193
196
|
}) => Promise<boolean>;
|
|
194
197
|
|
|
195
198
|
// Request context
|
package/src/router.ts
CHANGED
|
@@ -560,6 +560,7 @@ export function createRouter<TEnv = any>(
|
|
|
560
560
|
mergedRouteMap,
|
|
561
561
|
nextMountIndex: () => mountIndex++,
|
|
562
562
|
getPrecomputedByPrefix,
|
|
563
|
+
routerId,
|
|
563
564
|
};
|
|
564
565
|
|
|
565
566
|
function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
|
|
@@ -751,6 +752,7 @@ export function createRouter<TEnv = any>(
|
|
|
751
752
|
trailingSlash: trailingSlashConfig,
|
|
752
753
|
handler: urlPatterns.handler,
|
|
753
754
|
mountIndex: currentMountIndex,
|
|
755
|
+
routerId,
|
|
754
756
|
cacheProfiles: resolvedCacheProfiles,
|
|
755
757
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
756
758
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -770,6 +772,7 @@ export function createRouter<TEnv = any>(
|
|
|
770
772
|
trailingSlash: trailingSlashConfig,
|
|
771
773
|
handler: urlPatterns.handler,
|
|
772
774
|
mountIndex: currentMountIndex,
|
|
775
|
+
routerId,
|
|
773
776
|
cacheProfiles: resolvedCacheProfiles,
|
|
774
777
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
775
778
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -813,6 +816,7 @@ export function createRouter<TEnv = any>(
|
|
|
813
816
|
trailingSlash: trailingSlashConfig,
|
|
814
817
|
handler: urlPatterns.handler,
|
|
815
818
|
mountIndex: mountIndex++,
|
|
819
|
+
routerId,
|
|
816
820
|
// Lazy evaluation fields
|
|
817
821
|
lazy: true,
|
|
818
822
|
lazyPatterns: lazyInclude.patterns,
|
package/src/types/route-entry.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface RouteEntry<TEnv = any> {
|
|
|
55
55
|
| Promise<() => Array<AllUseItems>>;
|
|
56
56
|
mountIndex: number;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Router ID that owns this entry. Used to namespace the manifest cache
|
|
60
|
+
* so multi-router setups (host routing) don't share cached EntryData
|
|
61
|
+
* across routers with overlapping mountIndex + routeKey combinations.
|
|
62
|
+
*/
|
|
63
|
+
routerId?: string;
|
|
64
|
+
|
|
58
65
|
/**
|
|
59
66
|
* Route keys in this entry that have pre-render handlers.
|
|
60
67
|
* Used by the non-trie match path to set the `pr` flag.
|