@rangojs/router 0.0.0-experimental.06a618c3 → 0.0.0-experimental.0b3f4e91
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 +211 -47
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +67 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +16 -3
- package/src/browser/navigation-client.ts +64 -40
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +37 -4
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +44 -8
- package/src/browser/react/NavigationProvider.tsx +13 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- 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/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +9 -1
- package/src/router/handler-context.ts +36 -17
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +9 -2
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +42 -3
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-result.ts +11 -5
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +2 -5
- package/src/router/prerender-match.ts +2 -2
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +25 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +22 -8
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +16 -4
- package/src/router/types.ts +1 -0
- package/src/router.ts +41 -4
- package/src/rsc/handler.ts +11 -2
- 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 +8 -1
- package/src/server/context.ts +36 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +50 -12
- package/src/ssr/index.tsx +3 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +125 -31
- package/src/types/loader-types.ts +4 -5
- package/src/urls/pattern-types.ts +12 -0
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/rango.ts +17 -1
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -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,
|
|
@@ -32,6 +33,7 @@ export type LinkState =
|
|
|
32
33
|
| StateOrGetter<Record<string, unknown>>;
|
|
33
34
|
|
|
34
35
|
import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
|
|
36
|
+
import { getAppVersion } from "../app-version.js";
|
|
35
37
|
import {
|
|
36
38
|
observeForPrefetch,
|
|
37
39
|
unobserveForPrefetch,
|
|
@@ -192,6 +194,16 @@ export const Link: ForwardRefExoticComponent<
|
|
|
192
194
|
const ctx = useContext(NavigationStoreContext);
|
|
193
195
|
const isExternal = isExternalUrl(to);
|
|
194
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
|
+
|
|
195
207
|
// Resolve adaptive: viewport on touch devices, hover on pointer devices
|
|
196
208
|
const resolvedStrategy =
|
|
197
209
|
prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
|
|
@@ -273,9 +285,23 @@ export const Link: ForwardRefExoticComponent<
|
|
|
273
285
|
resolvedState = currentState;
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
ctx.navigate(
|
|
288
|
+
ctx.navigate(resolvedTo, {
|
|
289
|
+
replace,
|
|
290
|
+
scroll,
|
|
291
|
+
state: resolvedState,
|
|
292
|
+
revalidate,
|
|
293
|
+
});
|
|
277
294
|
},
|
|
278
|
-
[
|
|
295
|
+
[
|
|
296
|
+
resolvedTo,
|
|
297
|
+
isExternal,
|
|
298
|
+
reloadDocument,
|
|
299
|
+
replace,
|
|
300
|
+
scroll,
|
|
301
|
+
revalidate,
|
|
302
|
+
ctx,
|
|
303
|
+
onClick,
|
|
304
|
+
],
|
|
279
305
|
);
|
|
280
306
|
|
|
281
307
|
const handleMouseEnter = useCallback(() => {
|
|
@@ -289,9 +315,14 @@ export const Link: ForwardRefExoticComponent<
|
|
|
289
315
|
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
290
316
|
// deduplicates if the viewport prefetch already completed.
|
|
291
317
|
const segmentState = ctx.store.getSegmentState();
|
|
292
|
-
prefetchDirect(
|
|
318
|
+
prefetchDirect(
|
|
319
|
+
resolvedTo,
|
|
320
|
+
segmentState.currentSegmentIds,
|
|
321
|
+
getAppVersion(),
|
|
322
|
+
ctx.store.getRouterId?.(),
|
|
323
|
+
);
|
|
293
324
|
}
|
|
294
|
-
}, [resolvedStrategy,
|
|
325
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx]);
|
|
295
326
|
|
|
296
327
|
// Viewport/render prefetch: waits for idle before starting,
|
|
297
328
|
// uses concurrency-limited queue to avoid flooding.
|
|
@@ -308,7 +339,12 @@ export const Link: ForwardRefExoticComponent<
|
|
|
308
339
|
const triggerPrefetch = () => {
|
|
309
340
|
if (cancelled) return;
|
|
310
341
|
const segmentState = ctx.store.getSegmentState();
|
|
311
|
-
prefetchQueued(
|
|
342
|
+
prefetchQueued(
|
|
343
|
+
resolvedTo,
|
|
344
|
+
segmentState.currentSegmentIds,
|
|
345
|
+
getAppVersion(),
|
|
346
|
+
ctx.store.getRouterId?.(),
|
|
347
|
+
);
|
|
312
348
|
};
|
|
313
349
|
|
|
314
350
|
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
@@ -347,12 +383,12 @@ export const Link: ForwardRefExoticComponent<
|
|
|
347
383
|
unobserveForPrefetch(observedElement);
|
|
348
384
|
}
|
|
349
385
|
};
|
|
350
|
-
}, [resolvedStrategy,
|
|
386
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx]);
|
|
351
387
|
|
|
352
388
|
return (
|
|
353
389
|
<a
|
|
354
390
|
ref={setRef}
|
|
355
|
-
href={
|
|
391
|
+
href={resolvedTo}
|
|
356
392
|
onClick={handleClick}
|
|
357
393
|
onMouseEnter={handleMouseEnter}
|
|
358
394
|
data-link-component
|
|
@@ -362,7 +398,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
362
398
|
data-revalidate={revalidate === false ? "false" : undefined}
|
|
363
399
|
{...props}
|
|
364
400
|
>
|
|
365
|
-
<LinkContext.Provider value={
|
|
401
|
+
<LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
|
|
366
402
|
</a>
|
|
367
403
|
);
|
|
368
404
|
});
|
|
@@ -134,9 +134,14 @@ export interface NavigationProviderProps {
|
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* App version from server payload (stable, immutable).
|
|
137
|
-
* Forwarded to
|
|
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
|
);
|
|
@@ -289,15 +296,17 @@ export function NavigationProvider({
|
|
|
289
296
|
};
|
|
290
297
|
}, [warmupEnabled]);
|
|
291
298
|
|
|
292
|
-
// Cancel
|
|
293
|
-
//
|
|
299
|
+
// Cancel non-matching prefetches when navigation starts.
|
|
300
|
+
// Frees connections so the navigation fetch isn't competing with
|
|
301
|
+
// speculative prefetches. The prefetch matching the navigation target
|
|
302
|
+
// is kept alive so it can be reused via consumeInflightPrefetch.
|
|
294
303
|
useEffect(() => {
|
|
295
304
|
let wasIdle = true;
|
|
296
305
|
const unsub = eventController.subscribe(() => {
|
|
297
306
|
const state = eventController.getState();
|
|
298
307
|
const isIdle = state.state === "idle" && !state.isStreaming;
|
|
299
308
|
if (wasIdle && !isIdle) {
|
|
300
|
-
cancelAllPrefetches();
|
|
309
|
+
cancelAllPrefetches(state.pendingUrl);
|
|
301
310
|
}
|
|
302
311
|
wasIdle = isIdle;
|
|
303
312
|
});
|
|
@@ -43,10 +43,15 @@ export interface NavigationStoreContextValue {
|
|
|
43
43
|
refresh: () => Promise<void>;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* App version from server payload
|
|
47
|
-
* Used in prefetch requests for version mismatch detection.
|
|
46
|
+
* App version from the initial server payload.
|
|
48
47
|
*/
|
|
49
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;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useContext, useMemo } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { prefetchDirect } from "../prefetch/fetch.js";
|
|
6
|
+
import { getAppVersion } from "../app-version.js";
|
|
6
7
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -29,14 +30,22 @@ export function useRouter(): RouterInstance {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// Stable reference: ctx is itself stable (NavigationProvider memoizes with [])
|
|
32
|
-
return useMemo<RouterInstance>(
|
|
33
|
-
|
|
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 {
|
|
34
43
|
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
35
|
-
return ctx.navigate(url, { ...options, replace: false });
|
|
44
|
+
return ctx.navigate(withBasename(url), { ...options, replace: false });
|
|
36
45
|
},
|
|
37
46
|
|
|
38
47
|
replace(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
39
|
-
return ctx.navigate(url, { ...options, replace: true });
|
|
48
|
+
return ctx.navigate(withBasename(url), { ...options, replace: true });
|
|
40
49
|
},
|
|
41
50
|
|
|
42
51
|
refresh(): Promise<void> {
|
|
@@ -46,7 +55,12 @@ export function useRouter(): RouterInstance {
|
|
|
46
55
|
prefetch(url: string): void {
|
|
47
56
|
const segmentState = ctx.store?.getSegmentState();
|
|
48
57
|
if (segmentState) {
|
|
49
|
-
prefetchDirect(
|
|
58
|
+
prefetchDirect(
|
|
59
|
+
withBasename(url),
|
|
60
|
+
segmentState.currentSegmentIds,
|
|
61
|
+
getAppVersion(),
|
|
62
|
+
ctx.store?.getRouterId?.(),
|
|
63
|
+
);
|
|
50
64
|
}
|
|
51
65
|
},
|
|
52
66
|
|
|
@@ -57,7 +71,6 @@ export function useRouter(): RouterInstance {
|
|
|
57
71
|
forward(): void {
|
|
58
72
|
window.history.forward();
|
|
59
73
|
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
63
76
|
}
|
|
@@ -23,6 +23,7 @@ import type { EventController } from "./event-controller.js";
|
|
|
23
23
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
24
|
import { initRangoState } from "./rango-state.js";
|
|
25
25
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
26
|
+
import { setAppVersion } from "./app-version.js";
|
|
26
27
|
import {
|
|
27
28
|
isInterceptSegment,
|
|
28
29
|
splitInterceptSegments,
|
|
@@ -139,7 +140,6 @@ export async function initBrowserApp(
|
|
|
139
140
|
initialTheme,
|
|
140
141
|
} = options;
|
|
141
142
|
|
|
142
|
-
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
143
143
|
const initialPayload =
|
|
144
144
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
145
145
|
|
|
@@ -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),
|
|
@@ -205,6 +211,7 @@ export async function initBrowserApp(
|
|
|
205
211
|
// Initialize the localStorage state key for cache invalidation.
|
|
206
212
|
// Uses the build version so a new deploy automatically busts all cached prefetches.
|
|
207
213
|
initRangoState(version ?? "0");
|
|
214
|
+
setAppVersion(version);
|
|
208
215
|
|
|
209
216
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
210
217
|
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
@@ -231,7 +238,6 @@ export async function initBrowserApp(
|
|
|
231
238
|
deps,
|
|
232
239
|
onUpdate: (update) => store.emitUpdate(update),
|
|
233
240
|
renderSegments,
|
|
234
|
-
version,
|
|
235
241
|
onNavigate: (url, options) => {
|
|
236
242
|
if (!navigateFn) {
|
|
237
243
|
window.location.href = url;
|
|
@@ -249,7 +255,7 @@ export async function initBrowserApp(
|
|
|
249
255
|
client,
|
|
250
256
|
onUpdate: (update) => store.emitUpdate(update),
|
|
251
257
|
renderSegments,
|
|
252
|
-
version,
|
|
258
|
+
version: version,
|
|
253
259
|
});
|
|
254
260
|
|
|
255
261
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -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
|
});
|
|
@@ -329,6 +336,21 @@ export async function initBrowserApp(
|
|
|
329
336
|
throw new Error("HMR refetch returned invalid payload");
|
|
330
337
|
}
|
|
331
338
|
|
|
339
|
+
// Update version BEFORE rebuilding state so that
|
|
340
|
+
// clearHistoryCache() runs first, then the fresh segment
|
|
341
|
+
// cache entry we create below survives.
|
|
342
|
+
const newVersion = payload.metadata.version;
|
|
343
|
+
if (newVersion && newVersion !== version) {
|
|
344
|
+
console.log(
|
|
345
|
+
"[RSCRouter] HMR: version changed",
|
|
346
|
+
version,
|
|
347
|
+
"→",
|
|
348
|
+
newVersion,
|
|
349
|
+
"clearing caches",
|
|
350
|
+
);
|
|
351
|
+
navigationBridge.updateVersion(newVersion);
|
|
352
|
+
}
|
|
353
|
+
|
|
332
354
|
if (payload.metadata?.isPartial) {
|
|
333
355
|
const segments = payload.metadata.segments || [];
|
|
334
356
|
const matched = payload.metadata.matched || [];
|
|
@@ -478,6 +500,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
478
500
|
initialTheme={initialTheme}
|
|
479
501
|
warmupEnabled={warmupEnabled}
|
|
480
502
|
version={version}
|
|
503
|
+
basename={initialPayload.metadata?.basename}
|
|
481
504
|
/>
|
|
482
505
|
);
|
|
483
506
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./response-adapter.js";
|
|
30
30
|
import { mergeLocationState } from "./history-state.js";
|
|
31
31
|
import { classifyActionOutcome } from "./action-coordinator.js";
|
|
32
|
+
import { getAppVersion } from "./app-version.js";
|
|
32
33
|
|
|
33
34
|
// Polyfill Symbol.dispose/asyncDispose for Safari and older browsers
|
|
34
35
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -43,8 +44,6 @@ if (typeof Symbol.asyncDispose === "undefined") {
|
|
|
43
44
|
*/
|
|
44
45
|
export interface ServerActionBridgeConfigWithController extends ServerActionBridgeConfig {
|
|
45
46
|
eventController: EventController;
|
|
46
|
-
/** RSC version from initial payload metadata */
|
|
47
|
-
version?: string;
|
|
48
47
|
/** Callback to trigger SPA navigation (for action redirects) */
|
|
49
48
|
onNavigate?: (
|
|
50
49
|
url: string,
|
|
@@ -75,7 +74,6 @@ export function createServerActionBridge(
|
|
|
75
74
|
deps,
|
|
76
75
|
onUpdate,
|
|
77
76
|
renderSegments,
|
|
78
|
-
version,
|
|
79
77
|
onNavigate,
|
|
80
78
|
} = config;
|
|
81
79
|
|
|
@@ -86,7 +84,7 @@ export function createServerActionBridge(
|
|
|
86
84
|
client,
|
|
87
85
|
onUpdate,
|
|
88
86
|
renderSegments,
|
|
89
|
-
|
|
87
|
+
getVersion: getAppVersion,
|
|
90
88
|
});
|
|
91
89
|
|
|
92
90
|
/**
|
|
@@ -165,9 +163,15 @@ export function createServerActionBridge(
|
|
|
165
163
|
segmentState.currentSegmentIds.join(","),
|
|
166
164
|
);
|
|
167
165
|
// Add version param for version mismatch detection
|
|
166
|
+
const version = getAppVersion();
|
|
168
167
|
if (version) {
|
|
169
168
|
url.searchParams.set("_rsc_v", version);
|
|
170
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
|
+
}
|
|
171
175
|
|
|
172
176
|
// Encode arguments
|
|
173
177
|
const encodedBody = await deps.encodeReply(args, { temporaryReferences });
|
|
@@ -206,7 +210,6 @@ export function createServerActionBridge(
|
|
|
206
210
|
"rsc-action": id,
|
|
207
211
|
"X-RSC-Router-Client-Path": segmentState.currentUrl,
|
|
208
212
|
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
209
|
-
// Send intercept source URL so server can maintain intercept context
|
|
210
213
|
...(interceptSourceUrl && {
|
|
211
214
|
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
212
215
|
}),
|
|
@@ -309,7 +312,6 @@ export function createServerActionBridge(
|
|
|
309
312
|
matchedCount: payload.metadata?.matched?.length ?? 0,
|
|
310
313
|
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
311
314
|
});
|
|
312
|
-
|
|
313
315
|
// Guard: if the action was aborted while streaming (e.g., user navigated
|
|
314
316
|
// away or abortAllActions fired), bail out before any reconcile/render/cache
|
|
315
317
|
// writes to avoid overwriting the current UI with stale action results.
|
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) */
|
|
@@ -341,7 +346,13 @@ export type ReadonlyURLSearchParams = Omit<
|
|
|
341
346
|
export interface RscBrowserDependencies {
|
|
342
347
|
createFromFetch: <T>(
|
|
343
348
|
response: Promise<Response>,
|
|
344
|
-
options?: {
|
|
349
|
+
options?: {
|
|
350
|
+
temporaryReferences?: any;
|
|
351
|
+
findSourceMapURL?: (
|
|
352
|
+
filename: string,
|
|
353
|
+
environmentName: string,
|
|
354
|
+
) => string | null;
|
|
355
|
+
},
|
|
345
356
|
) => Promise<T>;
|
|
346
357
|
createFromReadableStream: <T>(stream: ReadableStream) => Promise<T>;
|
|
347
358
|
encodeReply: (
|
|
@@ -403,10 +414,13 @@ export interface NavigationStore {
|
|
|
403
414
|
segments: ResolvedSegment[],
|
|
404
415
|
handleData?: HandleData,
|
|
405
416
|
): void;
|
|
406
|
-
getCachedSegments(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
getCachedSegments(historyKey: string):
|
|
418
|
+
| {
|
|
419
|
+
segments: ResolvedSegment[];
|
|
420
|
+
stale: boolean;
|
|
421
|
+
handleData?: HandleData;
|
|
422
|
+
routerId?: string;
|
|
423
|
+
}
|
|
410
424
|
| undefined;
|
|
411
425
|
hasHistoryCache(historyKey: string): boolean;
|
|
412
426
|
updateCacheHandleData(historyKey: string, handleData: HandleData): void;
|
|
@@ -422,6 +436,10 @@ export interface NavigationStore {
|
|
|
422
436
|
getInterceptSourceUrl(): string | null;
|
|
423
437
|
setInterceptSourceUrl(url: string | null): void;
|
|
424
438
|
|
|
439
|
+
// Router identity tracking (for cross-app navigation detection)
|
|
440
|
+
getRouterId?(): string | undefined;
|
|
441
|
+
setRouterId?(id: string): void;
|
|
442
|
+
|
|
425
443
|
// UI update notifications
|
|
426
444
|
onUpdate(callback: UpdateSubscriber): () => void;
|
|
427
445
|
emitUpdate(update: NavigationUpdate): void;
|
|
@@ -452,6 +470,8 @@ export interface FetchPartialOptions {
|
|
|
452
470
|
interceptSourceUrl?: string;
|
|
453
471
|
/** RSC version for cache invalidation detection */
|
|
454
472
|
version?: string;
|
|
473
|
+
/** Current router ID — server detects app switch and returns full response */
|
|
474
|
+
routerId?: string;
|
|
455
475
|
/** If true, this is an HMR refetch - server should invalidate manifest cache */
|
|
456
476
|
hmr?: boolean;
|
|
457
477
|
}
|
|
@@ -520,6 +540,8 @@ export interface NavigationBridge {
|
|
|
520
540
|
refresh(): Promise<void>;
|
|
521
541
|
handlePopstate(): Promise<void>;
|
|
522
542
|
registerLinkInterception(): () => void;
|
|
543
|
+
/** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
|
|
544
|
+
updateVersion(newVersion: string): void;
|
|
523
545
|
}
|
|
524
546
|
|
|
525
547
|
/**
|
|
@@ -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;
|