@rangojs/router 0.0.0-experimental.55 → 0.0.0-experimental.56
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/bin/rango.js +128 -46
- package/dist/vite/index.js +119 -43
- package/package.json +1 -1
- package/skills/links/SKILL.md +3 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/router-setup/SKILL.md +35 -0
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +4 -0
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +17 -1
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/react/Link.tsx +43 -8
- package/src/browser/react/NavigationProvider.tsx +7 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/use-router.ts +20 -8
- package/src/browser/rsc-router.tsx +8 -0
- package/src/browser/server-action-bridge.ts +5 -0
- package/src/browser/types.ts +18 -4
- package/src/build/generate-manifest.ts +3 -0
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/route-definition/redirect.ts +9 -1
- package/src/router/handler-context.ts +5 -9
- package/src/router/intercept-resolution.ts +6 -2
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/middleware-types.ts +0 -6
- package/src/router/middleware.ts +0 -3
- package/src/router/prerender-match.ts +2 -2
- package/src/router/router-interfaces.ts +25 -4
- package/src/router/router-options.ts +37 -11
- package/src/router.ts +40 -4
- package/src/rsc/handler.ts +10 -1
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/rsc/types.ts +5 -0
- package/src/server/request-context.ts +8 -4
- package/src/ssr/index.tsx +3 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +5 -9
- package/src/types/loader-types.ts +0 -1
- package/src/urls/pattern-types.ts +12 -0
- package/src/vite/discovery/discover-routers.ts +5 -1
|
@@ -28,9 +28,15 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
28
28
|
// Maximum number of history entries to cache (URLs visited)
|
|
29
29
|
const HISTORY_CACHE_SIZE = 20;
|
|
30
30
|
|
|
31
|
-
// Cache entry: [url-key, segments, stale, handleData?]
|
|
31
|
+
// Cache entry: [url-key, segments, stale, handleData?, routerId?]
|
|
32
32
|
// stale=true means the data may be outdated and should be revalidated on access
|
|
33
|
-
type HistoryCacheEntry = [
|
|
33
|
+
type HistoryCacheEntry = [
|
|
34
|
+
string,
|
|
35
|
+
ResolvedSegment[],
|
|
36
|
+
boolean,
|
|
37
|
+
HandleData?,
|
|
38
|
+
string?,
|
|
39
|
+
];
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Shallow clone handleData to avoid reference sharing between cache entries.
|
|
@@ -258,6 +264,11 @@ export function createNavigationStore(
|
|
|
258
264
|
// Used to maintain intercept context during action revalidation
|
|
259
265
|
let interceptSourceUrl: string | null = null;
|
|
260
266
|
|
|
267
|
+
// Router identity - tracks which router is currently active.
|
|
268
|
+
// When this changes on a partial response, the client forces a full
|
|
269
|
+
// tree replacement instead of reconciling with stale segments.
|
|
270
|
+
let currentRouterId: string | undefined;
|
|
271
|
+
|
|
261
272
|
// Action state tracking (for useAction hook)
|
|
262
273
|
// Maps action function ID to its tracked state
|
|
263
274
|
const actionStates = new Map<string, TrackedActionState>();
|
|
@@ -571,10 +582,17 @@ export function createNavigationStore(
|
|
|
571
582
|
segments,
|
|
572
583
|
false,
|
|
573
584
|
clonedHandleData,
|
|
585
|
+
currentRouterId,
|
|
574
586
|
];
|
|
575
587
|
} else {
|
|
576
588
|
// Add new entry at the end (not stale)
|
|
577
|
-
historyCache.push([
|
|
589
|
+
historyCache.push([
|
|
590
|
+
historyKey,
|
|
591
|
+
segments,
|
|
592
|
+
false,
|
|
593
|
+
clonedHandleData,
|
|
594
|
+
currentRouterId,
|
|
595
|
+
]);
|
|
578
596
|
// Remove oldest entries if over limit
|
|
579
597
|
while (historyCache.length > cacheSize) {
|
|
580
598
|
historyCache.shift();
|
|
@@ -586,14 +604,22 @@ export function createNavigationStore(
|
|
|
586
604
|
* Get cached segments for a history entry
|
|
587
605
|
* Returns { segments, stale, handleData } or undefined if not cached
|
|
588
606
|
*/
|
|
589
|
-
getCachedSegments(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
607
|
+
getCachedSegments(historyKey: string):
|
|
608
|
+
| {
|
|
609
|
+
segments: ResolvedSegment[];
|
|
610
|
+
stale: boolean;
|
|
611
|
+
handleData?: HandleData;
|
|
612
|
+
routerId?: string;
|
|
613
|
+
}
|
|
593
614
|
| undefined {
|
|
594
615
|
const entry = historyCache.find(([key]) => key === historyKey);
|
|
595
616
|
if (!entry) return undefined;
|
|
596
|
-
return {
|
|
617
|
+
return {
|
|
618
|
+
segments: entry[1],
|
|
619
|
+
stale: entry[2],
|
|
620
|
+
handleData: entry[3],
|
|
621
|
+
routerId: entry[4],
|
|
622
|
+
};
|
|
597
623
|
},
|
|
598
624
|
|
|
599
625
|
/**
|
|
@@ -621,6 +647,7 @@ export function createNavigationStore(
|
|
|
621
647
|
entry[1],
|
|
622
648
|
entry[2],
|
|
623
649
|
clonedHandleData,
|
|
650
|
+
entry[4], // preserve routerId
|
|
624
651
|
];
|
|
625
652
|
}
|
|
626
653
|
},
|
|
@@ -687,6 +714,14 @@ export function createNavigationStore(
|
|
|
687
714
|
interceptSourceUrl = url;
|
|
688
715
|
},
|
|
689
716
|
|
|
717
|
+
getRouterId(): string | undefined {
|
|
718
|
+
return currentRouterId;
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
setRouterId(id: string): void {
|
|
722
|
+
currentRouterId = id;
|
|
723
|
+
},
|
|
724
|
+
|
|
690
725
|
// ========================================================================
|
|
691
726
|
// UI Update Notifications
|
|
692
727
|
// ========================================================================
|
|
@@ -200,6 +200,7 @@ export function createPartialUpdater(
|
|
|
200
200
|
staleRevalidation:
|
|
201
201
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
202
202
|
version: getVersion(),
|
|
203
|
+
routerId: store.getRouterId?.(),
|
|
203
204
|
});
|
|
204
205
|
// Mark navigation as streaming (response received, now parsing RSC).
|
|
205
206
|
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
@@ -212,6 +213,21 @@ export function createPartialUpdater(
|
|
|
212
213
|
streamingToken.end();
|
|
213
214
|
});
|
|
214
215
|
|
|
216
|
+
// Detect app switch: if routerId changed, the navigation crossed into
|
|
217
|
+
// a different router (e.g., via host router path mount). Downgrade
|
|
218
|
+
// partial to full so the entire tree is replaced without reconciliation
|
|
219
|
+
// against stale segments from the previous app.
|
|
220
|
+
if (payload.metadata?.routerId) {
|
|
221
|
+
const prevRouterId = store.getRouterId?.();
|
|
222
|
+
if (prevRouterId && prevRouterId !== payload.metadata.routerId) {
|
|
223
|
+
debugLog(
|
|
224
|
+
`[Browser] App switch detected (${prevRouterId} → ${payload.metadata.routerId}), forcing full update`,
|
|
225
|
+
);
|
|
226
|
+
payload.metadata.isPartial = false;
|
|
227
|
+
}
|
|
228
|
+
store.setRouterId?.(payload.metadata.routerId);
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
// Handle server-side redirect with state
|
|
216
232
|
if (payload.metadata?.redirect) {
|
|
217
233
|
if (signal?.aborted) {
|
|
@@ -265,7 +281,7 @@ export function createPartialUpdater(
|
|
|
265
281
|
existingSegments,
|
|
266
282
|
);
|
|
267
283
|
|
|
268
|
-
//
|
|
284
|
+
// tx.commit() cached the source page's handleData because
|
|
269
285
|
// eventController hasn't been updated yet. Overwrite with the
|
|
270
286
|
// correct cached handleData to prevent cache corruption on
|
|
271
287
|
// subsequent navigations to this same URL.
|
|
@@ -34,6 +34,7 @@ function buildPrefetchUrl(
|
|
|
34
34
|
url: string,
|
|
35
35
|
segmentIds: string[],
|
|
36
36
|
version?: string,
|
|
37
|
+
routerId?: string,
|
|
37
38
|
): URL | null {
|
|
38
39
|
let targetUrl: URL;
|
|
39
40
|
try {
|
|
@@ -51,6 +52,9 @@ function buildPrefetchUrl(
|
|
|
51
52
|
if (version) {
|
|
52
53
|
targetUrl.searchParams.set("_rsc_v", version);
|
|
53
54
|
}
|
|
55
|
+
if (routerId) {
|
|
56
|
+
targetUrl.searchParams.set("_rsc_rid", routerId);
|
|
57
|
+
}
|
|
54
58
|
return targetUrl;
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -108,10 +112,11 @@ export function prefetchDirect(
|
|
|
108
112
|
url: string,
|
|
109
113
|
segmentIds: string[],
|
|
110
114
|
version?: string,
|
|
115
|
+
routerId?: string,
|
|
111
116
|
): void {
|
|
112
117
|
if (!shouldPrefetch()) return;
|
|
113
118
|
|
|
114
|
-
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
119
|
+
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
115
120
|
if (!targetUrl) return;
|
|
116
121
|
const key = buildPrefetchKey(window.location.href, targetUrl);
|
|
117
122
|
if (hasPrefetch(key)) return;
|
|
@@ -127,9 +132,10 @@ export function prefetchQueued(
|
|
|
127
132
|
url: string,
|
|
128
133
|
segmentIds: string[],
|
|
129
134
|
version?: string,
|
|
135
|
+
routerId?: string,
|
|
130
136
|
): string {
|
|
131
137
|
if (!shouldPrefetch()) return "";
|
|
132
|
-
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
138
|
+
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
133
139
|
if (!targetUrl) return "";
|
|
134
140
|
const key = buildPrefetchKey(window.location.href, targetUrl);
|
|
135
141
|
if (hasPrefetch(key)) return key;
|
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useCallback,
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
|
+
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
type ForwardRefExoticComponent,
|
|
10
11
|
type RefAttributes,
|
|
@@ -193,6 +194,16 @@ export const Link: ForwardRefExoticComponent<
|
|
|
193
194
|
const ctx = useContext(NavigationStoreContext);
|
|
194
195
|
const isExternal = isExternalUrl(to);
|
|
195
196
|
|
|
197
|
+
// Auto-prefix with basename for app-local paths.
|
|
198
|
+
// Skip if external, already prefixed, or not a root-relative path.
|
|
199
|
+
const resolvedTo = useMemo(() => {
|
|
200
|
+
if (isExternal) return to;
|
|
201
|
+
const bn = ctx?.basename;
|
|
202
|
+
if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
|
|
203
|
+
return to;
|
|
204
|
+
return to === "/" ? bn : bn + to;
|
|
205
|
+
}, [to, isExternal, ctx?.basename]);
|
|
206
|
+
|
|
196
207
|
// Resolve adaptive: viewport on touch devices, hover on pointer devices
|
|
197
208
|
const resolvedStrategy =
|
|
198
209
|
prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
|
|
@@ -274,9 +285,23 @@ export const Link: ForwardRefExoticComponent<
|
|
|
274
285
|
resolvedState = currentState;
|
|
275
286
|
}
|
|
276
287
|
|
|
277
|
-
ctx.navigate(
|
|
288
|
+
ctx.navigate(resolvedTo, {
|
|
289
|
+
replace,
|
|
290
|
+
scroll,
|
|
291
|
+
state: resolvedState,
|
|
292
|
+
revalidate,
|
|
293
|
+
});
|
|
278
294
|
},
|
|
279
|
-
[
|
|
295
|
+
[
|
|
296
|
+
resolvedTo,
|
|
297
|
+
isExternal,
|
|
298
|
+
reloadDocument,
|
|
299
|
+
replace,
|
|
300
|
+
scroll,
|
|
301
|
+
revalidate,
|
|
302
|
+
ctx,
|
|
303
|
+
onClick,
|
|
304
|
+
],
|
|
280
305
|
);
|
|
281
306
|
|
|
282
307
|
const handleMouseEnter = useCallback(() => {
|
|
@@ -290,9 +315,14 @@ export const Link: ForwardRefExoticComponent<
|
|
|
290
315
|
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
291
316
|
// deduplicates if the viewport prefetch already completed.
|
|
292
317
|
const segmentState = ctx.store.getSegmentState();
|
|
293
|
-
prefetchDirect(
|
|
318
|
+
prefetchDirect(
|
|
319
|
+
resolvedTo,
|
|
320
|
+
segmentState.currentSegmentIds,
|
|
321
|
+
getAppVersion(),
|
|
322
|
+
ctx.store.getRouterId?.(),
|
|
323
|
+
);
|
|
294
324
|
}
|
|
295
|
-
}, [resolvedStrategy,
|
|
325
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx]);
|
|
296
326
|
|
|
297
327
|
// Viewport/render prefetch: waits for idle before starting,
|
|
298
328
|
// uses concurrency-limited queue to avoid flooding.
|
|
@@ -309,7 +339,12 @@ export const Link: ForwardRefExoticComponent<
|
|
|
309
339
|
const triggerPrefetch = () => {
|
|
310
340
|
if (cancelled) return;
|
|
311
341
|
const segmentState = ctx.store.getSegmentState();
|
|
312
|
-
prefetchQueued(
|
|
342
|
+
prefetchQueued(
|
|
343
|
+
resolvedTo,
|
|
344
|
+
segmentState.currentSegmentIds,
|
|
345
|
+
getAppVersion(),
|
|
346
|
+
ctx.store.getRouterId?.(),
|
|
347
|
+
);
|
|
313
348
|
};
|
|
314
349
|
|
|
315
350
|
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
@@ -348,12 +383,12 @@ export const Link: ForwardRefExoticComponent<
|
|
|
348
383
|
unobserveForPrefetch(observedElement);
|
|
349
384
|
}
|
|
350
385
|
};
|
|
351
|
-
}, [resolvedStrategy,
|
|
386
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx]);
|
|
352
387
|
|
|
353
388
|
return (
|
|
354
389
|
<a
|
|
355
390
|
ref={setRef}
|
|
356
|
-
href={
|
|
391
|
+
href={resolvedTo}
|
|
357
392
|
onClick={handleClick}
|
|
358
393
|
onMouseEnter={handleMouseEnter}
|
|
359
394
|
data-link-component
|
|
@@ -363,7 +398,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
363
398
|
data-revalidate={revalidate === false ? "false" : undefined}
|
|
364
399
|
{...props}
|
|
365
400
|
>
|
|
366
|
-
<LinkContext.Provider value={
|
|
401
|
+
<LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
|
|
367
402
|
</a>
|
|
368
403
|
);
|
|
369
404
|
});
|
|
@@ -137,6 +137,11 @@ export interface NavigationProviderProps {
|
|
|
137
137
|
* Forwarded to context for cache key building.
|
|
138
138
|
*/
|
|
139
139
|
version?: string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
143
|
+
*/
|
|
144
|
+
basename?: string;
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
/**
|
|
@@ -169,6 +174,7 @@ export function NavigationProvider({
|
|
|
169
174
|
initialTheme,
|
|
170
175
|
warmupEnabled,
|
|
171
176
|
version,
|
|
177
|
+
basename,
|
|
172
178
|
}: NavigationProviderProps): ReactNode {
|
|
173
179
|
// Track current payload for rendering (this triggers re-renders)
|
|
174
180
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -198,6 +204,7 @@ export function NavigationProvider({
|
|
|
198
204
|
navigate,
|
|
199
205
|
refresh,
|
|
200
206
|
version,
|
|
207
|
+
basename,
|
|
201
208
|
}),
|
|
202
209
|
[],
|
|
203
210
|
);
|
|
@@ -46,6 +46,12 @@ export interface NavigationStoreContextValue {
|
|
|
46
46
|
* App version from the initial server payload.
|
|
47
47
|
*/
|
|
48
48
|
version: string | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* URL prefix for all routes (from createRouter({ basename })).
|
|
52
|
+
* Used by Link and useRouter() to auto-prefix app-local paths.
|
|
53
|
+
*/
|
|
54
|
+
basename: string | undefined;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
/**
|
|
@@ -30,14 +30,22 @@ export function useRouter(): RouterInstance {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
|
|
33
|
-
return useMemo<RouterInstance>(
|
|
34
|
-
|
|
33
|
+
return useMemo<RouterInstance>(() => {
|
|
34
|
+
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
35
|
+
function withBasename(url: string): string {
|
|
36
|
+
const bn = ctx!.basename;
|
|
37
|
+
if (!bn || !url.startsWith("/") || url.startsWith(bn + "/") || url === bn)
|
|
38
|
+
return url;
|
|
39
|
+
return url === "/" ? bn : bn + url;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
35
43
|
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
36
|
-
return ctx.navigate(url, { ...options, replace: false });
|
|
44
|
+
return ctx.navigate(withBasename(url), { ...options, replace: false });
|
|
37
45
|
},
|
|
38
46
|
|
|
39
47
|
replace(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
40
|
-
return ctx.navigate(url, { ...options, replace: true });
|
|
48
|
+
return ctx.navigate(withBasename(url), { ...options, replace: true });
|
|
41
49
|
},
|
|
42
50
|
|
|
43
51
|
refresh(): Promise<void> {
|
|
@@ -47,7 +55,12 @@ export function useRouter(): RouterInstance {
|
|
|
47
55
|
prefetch(url: string): void {
|
|
48
56
|
const segmentState = ctx.store?.getSegmentState();
|
|
49
57
|
if (segmentState) {
|
|
50
|
-
prefetchDirect(
|
|
58
|
+
prefetchDirect(
|
|
59
|
+
withBasename(url),
|
|
60
|
+
segmentState.currentSegmentIds,
|
|
61
|
+
getAppVersion(),
|
|
62
|
+
ctx.store?.getRouterId?.(),
|
|
63
|
+
);
|
|
51
64
|
}
|
|
52
65
|
},
|
|
53
66
|
|
|
@@ -58,7 +71,6 @@ export function useRouter(): RouterInstance {
|
|
|
58
71
|
forward(): void {
|
|
59
72
|
window.history.forward();
|
|
60
73
|
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
64
76
|
}
|
|
@@ -164,6 +164,12 @@ export async function initBrowserApp(
|
|
|
164
164
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
// Seed router identity from the initial SSR payload so the first
|
|
168
|
+
// cross-app SPA navigation can detect the app switch.
|
|
169
|
+
if (initialPayload.metadata?.routerId) {
|
|
170
|
+
store.setRouterId?.(initialPayload.metadata.routerId);
|
|
171
|
+
}
|
|
172
|
+
|
|
167
173
|
// Create event controller for reactive state management
|
|
168
174
|
const eventController = createEventController({
|
|
169
175
|
initialLocation: new URL(window.location.href),
|
|
@@ -316,6 +322,7 @@ export async function initBrowserApp(
|
|
|
316
322
|
segmentIds: [],
|
|
317
323
|
previousUrl: store.getSegmentState().currentUrl,
|
|
318
324
|
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
325
|
+
routerId: store.getRouterId?.(),
|
|
319
326
|
hmr: true,
|
|
320
327
|
signal: abort.signal,
|
|
321
328
|
});
|
|
@@ -493,6 +500,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
493
500
|
initialTheme={initialTheme}
|
|
494
501
|
warmupEnabled={warmupEnabled}
|
|
495
502
|
version={version}
|
|
503
|
+
basename={initialPayload.metadata?.basename}
|
|
496
504
|
/>
|
|
497
505
|
);
|
|
498
506
|
}
|
|
@@ -167,6 +167,11 @@ export function createServerActionBridge(
|
|
|
167
167
|
if (version) {
|
|
168
168
|
url.searchParams.set("_rsc_v", version);
|
|
169
169
|
}
|
|
170
|
+
// Add router ID for app switch detection
|
|
171
|
+
const rid = store.getRouterId?.();
|
|
172
|
+
if (rid) {
|
|
173
|
+
url.searchParams.set("_rsc_rid", rid);
|
|
174
|
+
}
|
|
170
175
|
|
|
171
176
|
// Encode arguments
|
|
172
177
|
const encodedBody = await deps.encodeReply(args, { temporaryReferences });
|
package/src/browser/types.ts
CHANGED
|
@@ -32,6 +32,9 @@ export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
|
32
32
|
export interface RscMetadata {
|
|
33
33
|
pathname: string;
|
|
34
34
|
segments: ResolvedSegment[];
|
|
35
|
+
/** Router instance ID. When this changes between navigations, the client
|
|
36
|
+
* forces a full tree replacement (app switch via host router). */
|
|
37
|
+
routerId?: string;
|
|
35
38
|
isPartial?: boolean;
|
|
36
39
|
isError?: boolean;
|
|
37
40
|
matched?: string[];
|
|
@@ -70,6 +73,8 @@ export interface RscMetadata {
|
|
|
70
73
|
* Included when theme is enabled in router config.
|
|
71
74
|
*/
|
|
72
75
|
initialTheme?: Theme;
|
|
76
|
+
/** URL prefix for all routes (from createRouter({ basename })). */
|
|
77
|
+
basename?: string;
|
|
73
78
|
/** Whether connection warmup is enabled */
|
|
74
79
|
warmupEnabled?: boolean;
|
|
75
80
|
/** Server-side redirect with optional state (for partial requests) */
|
|
@@ -409,10 +414,13 @@ export interface NavigationStore {
|
|
|
409
414
|
segments: ResolvedSegment[],
|
|
410
415
|
handleData?: HandleData,
|
|
411
416
|
): void;
|
|
412
|
-
getCachedSegments(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
417
|
+
getCachedSegments(historyKey: string):
|
|
418
|
+
| {
|
|
419
|
+
segments: ResolvedSegment[];
|
|
420
|
+
stale: boolean;
|
|
421
|
+
handleData?: HandleData;
|
|
422
|
+
routerId?: string;
|
|
423
|
+
}
|
|
416
424
|
| undefined;
|
|
417
425
|
hasHistoryCache(historyKey: string): boolean;
|
|
418
426
|
updateCacheHandleData(historyKey: string, handleData: HandleData): void;
|
|
@@ -428,6 +436,10 @@ export interface NavigationStore {
|
|
|
428
436
|
getInterceptSourceUrl(): string | null;
|
|
429
437
|
setInterceptSourceUrl(url: string | null): void;
|
|
430
438
|
|
|
439
|
+
// Router identity tracking (for cross-app navigation detection)
|
|
440
|
+
getRouterId?(): string | undefined;
|
|
441
|
+
setRouterId?(id: string): void;
|
|
442
|
+
|
|
431
443
|
// UI update notifications
|
|
432
444
|
onUpdate(callback: UpdateSubscriber): () => void;
|
|
433
445
|
emitUpdate(update: NavigationUpdate): void;
|
|
@@ -458,6 +470,8 @@ export interface FetchPartialOptions {
|
|
|
458
470
|
interceptSourceUrl?: string;
|
|
459
471
|
/** RSC version for cache invalidation detection */
|
|
460
472
|
version?: string;
|
|
473
|
+
/** Current router ID — server detects app switch and returns full response */
|
|
474
|
+
routerId?: string;
|
|
461
475
|
/** If true, this is an HMR refetch - server should invalidate manifest cache */
|
|
462
476
|
hmr?: boolean;
|
|
463
477
|
}
|
|
@@ -285,6 +285,7 @@ export function generateManifest<TEnv>(
|
|
|
285
285
|
export function generateManifestFull<TEnv>(
|
|
286
286
|
urlpatterns: UrlPatterns<TEnv, any>,
|
|
287
287
|
mountIndex: number = 0,
|
|
288
|
+
options?: { urlPrefix?: string },
|
|
288
289
|
): FullManifest {
|
|
289
290
|
const routeManifest: Record<string, string> = {};
|
|
290
291
|
const routeAncestry: Record<string, string[]> = {};
|
|
@@ -310,6 +311,8 @@ export function generateManifestFull<TEnv>(
|
|
|
310
311
|
counters: {},
|
|
311
312
|
mountIndex,
|
|
312
313
|
trackedIncludes, // Enable include tracking
|
|
314
|
+
// basename sets the initial URL prefix for all path() registrations
|
|
315
|
+
...(options?.urlPrefix ? { urlPrefix: options.urlPrefix } : {}),
|
|
313
316
|
},
|
|
314
317
|
() => {
|
|
315
318
|
const helpers = createRouteHelpers();
|
|
@@ -25,6 +25,9 @@ export {
|
|
|
25
25
|
} from "./route-types/include-resolution.js";
|
|
26
26
|
export {
|
|
27
27
|
extractUrlsVariableFromRouter,
|
|
28
|
+
extractUrlsFromRouter,
|
|
29
|
+
extractBasenameFromRouter,
|
|
30
|
+
type UrlsExtractionResult,
|
|
28
31
|
buildCombinedRouteMapForRouterFile,
|
|
29
32
|
detectUnresolvableIncludes,
|
|
30
33
|
detectUnresolvableIncludesForUrlsFile,
|
|
@@ -357,12 +357,17 @@ function buildRouteMapFromBlock(
|
|
|
357
357
|
/**
|
|
358
358
|
* Build route map and search schemas together.
|
|
359
359
|
* Internal helper used by the include resolution path.
|
|
360
|
+
*
|
|
361
|
+
* @param inlineBlock - Optional pre-extracted code block (e.g. from an inline
|
|
362
|
+
* builder function). When provided, variableName is ignored and the block
|
|
363
|
+
* is parsed directly for path()/include() calls.
|
|
360
364
|
*/
|
|
361
365
|
export function buildCombinedRouteMapWithSearch(
|
|
362
366
|
filePath: string,
|
|
363
367
|
variableName?: string,
|
|
364
368
|
visited?: Set<string>,
|
|
365
369
|
diagnosticsOut?: UnresolvableInclude[],
|
|
370
|
+
inlineBlock?: string,
|
|
366
371
|
): {
|
|
367
372
|
routes: Record<string, string>;
|
|
368
373
|
searchSchemas: Record<string, Record<string, string>>;
|
|
@@ -384,7 +389,9 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
384
389
|
}
|
|
385
390
|
|
|
386
391
|
let block: string;
|
|
387
|
-
if (
|
|
392
|
+
if (inlineBlock) {
|
|
393
|
+
block = inlineBlock;
|
|
394
|
+
} else if (variableName) {
|
|
388
395
|
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
389
396
|
if (!extracted) return { routes: {}, searchSchemas: {} };
|
|
390
397
|
block = extracted;
|