@rangojs/router 0.0.0-experimental.8678bb02 → 0.0.0-experimental.87

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.
Files changed (147) hide show
  1. package/README.md +126 -38
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +847 -384
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +5 -5
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/handler-use/SKILL.md +362 -0
  9. package/skills/hooks/SKILL.md +28 -20
  10. package/skills/intercept/SKILL.md +20 -0
  11. package/skills/layout/SKILL.md +22 -0
  12. package/skills/links/SKILL.md +91 -17
  13. package/skills/loader/SKILL.md +35 -2
  14. package/skills/middleware/SKILL.md +34 -3
  15. package/skills/migrate-nextjs/SKILL.md +560 -0
  16. package/skills/migrate-react-router/SKILL.md +765 -0
  17. package/skills/parallel/SKILL.md +59 -0
  18. package/skills/prerender/SKILL.md +110 -68
  19. package/skills/rango/SKILL.md +24 -22
  20. package/skills/response-routes/SKILL.md +8 -0
  21. package/skills/route/SKILL.md +24 -0
  22. package/skills/router-setup/SKILL.md +35 -0
  23. package/skills/streams-and-websockets/SKILL.md +283 -0
  24. package/skills/typesafety/SKILL.md +3 -1
  25. package/src/__internal.ts +1 -1
  26. package/src/browser/app-shell.ts +52 -0
  27. package/src/browser/app-version.ts +14 -0
  28. package/src/browser/navigation-bridge.ts +87 -6
  29. package/src/browser/navigation-client.ts +128 -77
  30. package/src/browser/navigation-store.ts +68 -9
  31. package/src/browser/partial-update.ts +60 -7
  32. package/src/browser/prefetch/cache.ts +129 -21
  33. package/src/browser/prefetch/fetch.ts +156 -18
  34. package/src/browser/prefetch/queue.ts +36 -5
  35. package/src/browser/rango-state.ts +53 -13
  36. package/src/browser/react/Link.tsx +72 -8
  37. package/src/browser/react/NavigationProvider.tsx +57 -11
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-navigation.ts +22 -2
  41. package/src/browser/react/use-params.ts +11 -1
  42. package/src/browser/react/use-router.ts +29 -9
  43. package/src/browser/rsc-router.tsx +60 -9
  44. package/src/browser/scroll-restoration.ts +10 -8
  45. package/src/browser/segment-reconciler.ts +36 -14
  46. package/src/browser/server-action-bridge.ts +8 -18
  47. package/src/browser/types.ts +33 -5
  48. package/src/build/generate-manifest.ts +6 -6
  49. package/src/build/generate-route-types.ts +3 -0
  50. package/src/build/route-trie.ts +50 -24
  51. package/src/build/route-types/include-resolution.ts +8 -1
  52. package/src/build/route-types/router-processing.ts +211 -72
  53. package/src/build/route-types/scan-filter.ts +8 -1
  54. package/src/cache/cf/cf-cache-store.ts +5 -7
  55. package/src/client.tsx +84 -230
  56. package/src/deps/browser.ts +0 -1
  57. package/src/handle.ts +40 -0
  58. package/src/index.rsc.ts +6 -1
  59. package/src/index.ts +49 -6
  60. package/src/outlet-context.ts +1 -1
  61. package/src/prerender/store.ts +5 -4
  62. package/src/prerender.ts +138 -77
  63. package/src/response-utils.ts +28 -0
  64. package/src/reverse.ts +27 -2
  65. package/src/route-definition/dsl-helpers.ts +210 -35
  66. package/src/route-definition/helpers-types.ts +61 -14
  67. package/src/route-definition/index.ts +3 -0
  68. package/src/route-definition/redirect.ts +9 -1
  69. package/src/route-definition/resolve-handler-use.ts +155 -0
  70. package/src/route-types.ts +18 -0
  71. package/src/router/content-negotiation.ts +100 -1
  72. package/src/router/handler-context.ts +70 -17
  73. package/src/router/intercept-resolution.ts +9 -4
  74. package/src/router/lazy-includes.ts +6 -6
  75. package/src/router/loader-resolution.ts +153 -21
  76. package/src/router/manifest.ts +22 -13
  77. package/src/router/match-api.ts +127 -192
  78. package/src/router/match-middleware/cache-lookup.ts +28 -8
  79. package/src/router/match-middleware/segment-resolution.ts +53 -0
  80. package/src/router/match-result.ts +82 -4
  81. package/src/router/middleware-types.ts +2 -28
  82. package/src/router/middleware.ts +32 -7
  83. package/src/router/navigation-snapshot.ts +182 -0
  84. package/src/router/pattern-matching.ts +60 -9
  85. package/src/router/prerender-match.ts +110 -10
  86. package/src/router/preview-match.ts +30 -102
  87. package/src/router/request-classification.ts +310 -0
  88. package/src/router/route-snapshot.ts +245 -0
  89. package/src/router/router-interfaces.ts +36 -4
  90. package/src/router/router-options.ts +37 -11
  91. package/src/router/segment-resolution/fresh.ts +70 -5
  92. package/src/router/segment-resolution/revalidation.ts +87 -9
  93. package/src/router/trie-matching.ts +10 -4
  94. package/src/router/url-params.ts +49 -0
  95. package/src/router.ts +54 -7
  96. package/src/rsc/handler.ts +478 -399
  97. package/src/rsc/helpers.ts +69 -41
  98. package/src/rsc/loader-fetch.ts +18 -3
  99. package/src/rsc/manifest-init.ts +5 -1
  100. package/src/rsc/progressive-enhancement.ts +14 -3
  101. package/src/rsc/response-route-handler.ts +14 -1
  102. package/src/rsc/rsc-rendering.ts +15 -2
  103. package/src/rsc/server-action.ts +10 -2
  104. package/src/rsc/ssr-setup.ts +2 -2
  105. package/src/rsc/types.ts +6 -4
  106. package/src/segment-content-promise.ts +67 -0
  107. package/src/segment-loader-promise.ts +122 -0
  108. package/src/segment-system.tsx +11 -61
  109. package/src/server/context.ts +65 -5
  110. package/src/server/handle-store.ts +19 -0
  111. package/src/server/loader-registry.ts +9 -8
  112. package/src/server/request-context.ts +142 -55
  113. package/src/ssr/index.tsx +3 -0
  114. package/src/static-handler.ts +18 -6
  115. package/src/types/cache-types.ts +4 -4
  116. package/src/types/handler-context.ts +17 -43
  117. package/src/types/loader-types.ts +37 -11
  118. package/src/types/request-scope.ts +126 -0
  119. package/src/types/route-entry.ts +12 -1
  120. package/src/types/segments.ts +1 -1
  121. package/src/urls/include-helper.ts +24 -14
  122. package/src/urls/path-helper-types.ts +39 -6
  123. package/src/urls/path-helper.ts +47 -12
  124. package/src/urls/pattern-types.ts +12 -0
  125. package/src/urls/response-types.ts +18 -16
  126. package/src/use-loader.tsx +77 -5
  127. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  128. package/src/vite/discovery/discover-routers.ts +5 -1
  129. package/src/vite/discovery/prerender-collection.ts +128 -74
  130. package/src/vite/discovery/state.ts +13 -4
  131. package/src/vite/index.ts +4 -0
  132. package/src/vite/plugin-types.ts +60 -5
  133. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  134. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  135. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  136. package/src/vite/plugins/expose-id-utils.ts +12 -0
  137. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  138. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  139. package/src/vite/plugins/performance-tracks.ts +64 -206
  140. package/src/vite/plugins/refresh-cmd.ts +88 -26
  141. package/src/vite/rango.ts +40 -18
  142. package/src/vite/router-discovery.ts +237 -37
  143. package/src/vite/utils/banner.ts +1 -1
  144. package/src/vite/utils/package-resolution.ts +1 -1
  145. package/src/vite/utils/prerender-utils.ts +37 -5
  146. package/src/vite/utils/shared-utils.ts +3 -2
  147. package/src/browser/debug-channel.ts +0 -93
@@ -23,10 +23,12 @@ 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,
29
30
  } from "./intercept-utils.js";
31
+ import { createAppShellRef } from "./app-shell.js";
30
32
 
31
33
  // Vite HMR types are provided by vite/client
32
34
 
@@ -113,6 +115,13 @@ export interface BrowserAppContext {
113
115
  warmupEnabled?: boolean;
114
116
  /** App version for prefetch version mismatch detection */
115
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;
116
125
  }
117
126
 
118
127
  // Module-level state for the initialized app
@@ -139,7 +148,6 @@ export async function initBrowserApp(
139
148
  initialTheme,
140
149
  } = options;
141
150
 
142
- // Load initial payload from SSR-injected __FLIGHT_DATA__
143
151
  const initialPayload =
144
152
  await deps.createFromReadableStream<RscPayload>(rscStream);
145
153
 
@@ -164,6 +172,12 @@ export async function initBrowserApp(
164
172
  ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
165
173
  });
166
174
 
175
+ // Seed router identity from the initial SSR payload so the first
176
+ // cross-app SPA navigation can detect the app switch.
177
+ if (initialPayload.metadata?.routerId) {
178
+ store.setRouterId?.(initialPayload.metadata.routerId);
179
+ }
180
+
167
181
  // Create event controller for reactive state management
168
182
  const eventController = createEventController({
169
183
  initialLocation: new URL(window.location.href),
@@ -198,13 +212,24 @@ export async function initBrowserApp(
198
212
  // Create composable utilities
199
213
  const client = createNavigationClient(deps);
200
214
 
201
- // Extract rootLayout and version from metadata for browser-side re-renders
202
- const rootLayout = initialPayload.metadata?.rootLayout;
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.
203
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
+ });
204
227
 
205
228
  // Initialize the localStorage state key for cache invalidation.
206
- // Uses the build version so a new deploy automatically busts all cached prefetches.
207
- initRangoState(version ?? "0");
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);
232
+ setAppVersion(version);
208
233
 
209
234
  // Initialize the in-memory prefetch cache TTL from server config.
210
235
  // A value of 0 disables the cache; undefined falls back to the module default.
@@ -213,11 +238,17 @@ export async function initBrowserApp(
213
238
  initPrefetchCache(prefetchCacheTTL);
214
239
  }
215
240
 
216
- // Create a bound renderSegments that includes rootLayout
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.
217
244
  const renderSegments = (
218
245
  segments: ResolvedSegment[],
219
246
  options?: RenderSegmentsOptions,
220
- ) => baseRenderSegments(segments, { ...options, rootLayout });
247
+ ) =>
248
+ baseRenderSegments(segments, {
249
+ ...options,
250
+ rootLayout: appShellRef.get().rootLayout,
251
+ });
221
252
 
222
253
  // Lazy reference for navigation bridge — the action bridge is created first
223
254
  // but may need to trigger SPA navigation for action redirects.
@@ -231,7 +262,6 @@ export async function initBrowserApp(
231
262
  deps,
232
263
  onUpdate: (update) => store.emitUpdate(update),
233
264
  renderSegments,
234
- version,
235
265
  onNavigate: (url, options) => {
236
266
  if (!navigateFn) {
237
267
  window.location.href = url;
@@ -249,7 +279,8 @@ export async function initBrowserApp(
249
279
  client,
250
280
  onUpdate: (update) => store.emitUpdate(update),
251
281
  renderSegments,
252
- version,
282
+ version: version,
283
+ appShellRef,
253
284
  });
254
285
 
255
286
  // Connect action redirect → navigation bridge (now that both are initialized)
@@ -316,6 +347,7 @@ export async function initBrowserApp(
316
347
  segmentIds: [],
317
348
  previousUrl: store.getSegmentState().currentUrl,
318
349
  interceptSourceUrl: interceptSourceUrl || undefined,
350
+ routerId: store.getRouterId?.(),
319
351
  hmr: true,
320
352
  signal: abort.signal,
321
353
  });
@@ -329,6 +361,21 @@ export async function initBrowserApp(
329
361
  throw new Error("HMR refetch returned invalid payload");
330
362
  }
331
363
 
364
+ // Update version BEFORE rebuilding state so that
365
+ // clearHistoryCache() runs first, then the fresh segment
366
+ // cache entry we create below survives.
367
+ const newVersion = payload.metadata.version;
368
+ if (newVersion && newVersion !== version) {
369
+ console.log(
370
+ "[RSCRouter] HMR: version changed",
371
+ version,
372
+ "→",
373
+ newVersion,
374
+ "clearing caches",
375
+ );
376
+ navigationBridge.updateVersion(newVersion);
377
+ }
378
+
332
379
  if (payload.metadata?.isPartial) {
333
380
  const segments = payload.metadata.segments || [];
334
381
  const matched = payload.metadata.matched || [];
@@ -394,6 +441,7 @@ export async function initBrowserApp(
394
441
  initialTheme: effectiveInitialTheme,
395
442
  warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
396
443
  version,
444
+ appShellRef,
397
445
  };
398
446
  browserAppContext = context;
399
447
 
@@ -459,6 +507,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
459
507
  initialTheme,
460
508
  warmupEnabled,
461
509
  version,
510
+ appShellRef,
462
511
  } = getBrowserAppContext();
463
512
 
464
513
  // Signal that the React tree has hydrated. useEffect only fires after
@@ -478,6 +527,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
478
527
  initialTheme={initialTheme}
479
528
  warmupEnabled={warmupEnabled}
480
529
  version={version}
530
+ basename={initialPayload.metadata?.basename}
531
+ appShellRef={appShellRef}
481
532
  />
482
533
  );
483
534
  }
@@ -356,19 +356,18 @@ export function handleNavigationEnd(options: {
356
356
  scroll?: boolean;
357
357
  isStreaming?: () => boolean;
358
358
  }): void {
359
- if (!initialized) {
360
- return;
361
- }
362
-
363
359
  const { restore = false, scroll = true, isStreaming } = options;
364
360
 
365
- // Don't scroll if explicitly disabled
366
- if (scroll === false) {
361
+ // Don't scroll if explicitly disabled or not in a browser
362
+ if (scroll === false || typeof window === "undefined") {
367
363
  return;
368
364
  }
369
365
 
370
- // For back/forward (restore), try to restore saved position
371
- if (restore) {
366
+ // Save/restore requires initialization (sessionStorage, history state).
367
+ // But basic scroll-to-top and hash scrolling work without it — this
368
+ // matters during cross-app navigation where ScrollRestoration unmounts
369
+ // and remounts, creating a brief window where initialized is false.
370
+ if (restore && initialized) {
372
371
  if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
373
372
  return;
374
373
  }
@@ -378,6 +377,9 @@ export function handleNavigationEnd(options: {
378
377
  // Defer hash and scroll-to-top to after React paints the new content,
379
378
  // so the user doesn't see the current page jump before the new route appears.
380
379
  deferToNextPaint(() => {
380
+ // Re-check: the deferred callback may fire after environment teardown
381
+ if (typeof window === "undefined") return;
382
+
381
383
  // Try hash scrolling first
382
384
  if (scrollToHash()) {
383
385
  return;
@@ -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
- // For non-action actors: cached segments the server decided not to re-render.
162
- // - Preserve loading=false (suppressed boundary) to maintain tree structure
163
- // - Preserve parallel segment loading so renderSegments can reconstruct
164
- // parallel-owned loader markers from the cached slot metadata
165
- // - Clear other truthy loading values to prevent suspense on cached content
166
- if (actor !== "action") {
167
- if (fromCache.type === "parallel" && fromCache.loading !== undefined) {
168
- return fromCache;
169
- }
170
- if (fromCache.loading !== undefined && fromCache.loading !== false) {
171
- return { ...fromCache, loading: undefined };
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[];
@@ -4,8 +4,6 @@ import type {
4
4
  RscPayload,
5
5
  } from "./types.js";
6
6
  import { createPartialUpdater } from "./partial-update.js";
7
- import { createClientDebugChannel, DEBUG_ID_HEADER } from "./debug-channel.js";
8
- import { findSourceMapURL } from "../deps/browser.js";
9
7
  import { createNavigationTransaction } from "./navigation-transaction.js";
10
8
  import {
11
9
  reconcileSegments,
@@ -31,6 +29,7 @@ import {
31
29
  } from "./response-adapter.js";
32
30
  import { mergeLocationState } from "./history-state.js";
33
31
  import { classifyActionOutcome } from "./action-coordinator.js";
32
+ import { getAppVersion } from "./app-version.js";
34
33
 
35
34
  // Polyfill Symbol.dispose/asyncDispose for Safari and older browsers
36
35
  if (typeof Symbol.dispose === "undefined") {
@@ -45,8 +44,6 @@ if (typeof Symbol.asyncDispose === "undefined") {
45
44
  */
46
45
  export interface ServerActionBridgeConfigWithController extends ServerActionBridgeConfig {
47
46
  eventController: EventController;
48
- /** RSC version from initial payload metadata */
49
- version?: string;
50
47
  /** Callback to trigger SPA navigation (for action redirects) */
51
48
  onNavigate?: (
52
49
  url: string,
@@ -77,7 +74,6 @@ export function createServerActionBridge(
77
74
  deps,
78
75
  onUpdate,
79
76
  renderSegments,
80
- version,
81
77
  onNavigate,
82
78
  } = config;
83
79
 
@@ -88,7 +84,7 @@ export function createServerActionBridge(
88
84
  client,
89
85
  onUpdate,
90
86
  renderSegments,
91
- version,
87
+ getVersion: getAppVersion,
92
88
  });
93
89
 
94
90
  /**
@@ -167,9 +163,15 @@ export function createServerActionBridge(
167
163
  segmentState.currentSegmentIds.join(","),
168
164
  );
169
165
  // Add version param for version mismatch detection
166
+ const version = getAppVersion();
170
167
  if (version) {
171
168
  url.searchParams.set("_rsc_v", version);
172
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
+ }
173
175
 
174
176
  // Encode arguments
175
177
  const encodedBody = await deps.encodeReply(args, { temporaryReferences });
@@ -201,14 +203,6 @@ export function createServerActionBridge(
201
203
  const onHandleAbort = () => fetchAbort.abort();
202
204
  handle.signal.addEventListener("abort", onHandleAbort, { once: true });
203
205
 
204
- // Dev-only: create debug channel for React Performance Tracks
205
- const debugId = (import.meta as any).hot
206
- ? crypto.randomUUID()
207
- : undefined;
208
- const debugChannel = debugId
209
- ? createClientDebugChannel(debugId)
210
- : undefined;
211
-
212
206
  // Send action request with stream tracking
213
207
  const responsePromise = fetch(url, {
214
208
  method: "POST",
@@ -216,11 +210,9 @@ export function createServerActionBridge(
216
210
  "rsc-action": id,
217
211
  "X-RSC-Router-Client-Path": segmentState.currentUrl,
218
212
  ...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
219
- // Send intercept source URL so server can maintain intercept context
220
213
  ...(interceptSourceUrl && {
221
214
  "X-RSC-Router-Intercept-Source": interceptSourceUrl,
222
215
  }),
223
- ...(debugId && { [DEBUG_ID_HEADER]: debugId }),
224
216
  },
225
217
  body: encodedBody,
226
218
  signal: fetchAbort.signal,
@@ -283,7 +275,6 @@ export function createServerActionBridge(
283
275
  try {
284
276
  payload = await deps.createFromFetch<RscPayload>(responsePromise, {
285
277
  temporaryReferences,
286
- ...(debugChannel && { debugChannel, findSourceMapURL }),
287
278
  });
288
279
  } catch (error) {
289
280
  // Clean up streaming token on error (may be null if fetch failed before .then() ran)
@@ -321,7 +312,6 @@ export function createServerActionBridge(
321
312
  matchedCount: payload.metadata?.matched?.length ?? 0,
322
313
  diffCount: payload.metadata?.diff?.length ?? 0,
323
314
  });
324
-
325
315
  // Guard: if the action was aborted while streaming (e.g., user navigated
326
316
  // away or abortAllActions fired), bail out before any reconcile/render/cache
327
317
  // writes to avoid overwriting the current UI with stale action results.
@@ -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) */
@@ -343,7 +348,6 @@ export interface RscBrowserDependencies {
343
348
  response: Promise<Response>,
344
349
  options?: {
345
350
  temporaryReferences?: any;
346
- debugChannel?: { readable?: ReadableStream; writable?: WritableStream };
347
351
  findSourceMapURL?: (
348
352
  filename: string,
349
353
  environmentName: string,
@@ -410,16 +414,25 @@ export interface NavigationStore {
410
414
  segments: ResolvedSegment[],
411
415
  handleData?: HandleData,
412
416
  ): void;
413
- getCachedSegments(
414
- historyKey: string,
415
- ):
416
- | { segments: ResolvedSegment[]; stale: boolean; handleData?: HandleData }
417
+ getCachedSegments(historyKey: string):
418
+ | {
419
+ segments: ResolvedSegment[];
420
+ stale: boolean;
421
+ handleData?: HandleData;
422
+ routerId?: string;
423
+ }
417
424
  | undefined;
418
425
  hasHistoryCache(historyKey: string): boolean;
419
426
  updateCacheHandleData(historyKey: string, handleData: HandleData): void;
420
427
  markCacheAsStale(): void;
421
428
  markCacheAsStaleAndBroadcast(): void;
422
429
  clearHistoryCache(): void;
430
+ /**
431
+ * Clear this tab's nav + prefetch caches without broadcasting or rotating
432
+ * shared state. Intended for app-switch transitions that affect only this
433
+ * tab's session.
434
+ */
435
+ clearHistoryCacheLocal(): void;
423
436
  broadcastCacheInvalidation(): void;
424
437
 
425
438
  // Cross-tab refresh callback (set by navigation bridge)
@@ -429,6 +442,10 @@ export interface NavigationStore {
429
442
  getInterceptSourceUrl(): string | null;
430
443
  setInterceptSourceUrl(url: string | null): void;
431
444
 
445
+ // Router identity tracking (for cross-app navigation detection)
446
+ getRouterId?(): string | undefined;
447
+ setRouterId?(id: string): void;
448
+
432
449
  // UI update notifications
433
450
  onUpdate(callback: UpdateSubscriber): () => void;
434
451
  emitUpdate(update: NavigationUpdate): void;
@@ -459,6 +476,8 @@ export interface FetchPartialOptions {
459
476
  interceptSourceUrl?: string;
460
477
  /** RSC version for cache invalidation detection */
461
478
  version?: string;
479
+ /** Current router ID — server detects app switch and returns full response */
480
+ routerId?: string;
462
481
  /** If true, this is an HMR refetch - server should invalidate manifest cache */
463
482
  hmr?: boolean;
464
483
  }
@@ -527,6 +546,15 @@ export interface NavigationBridge {
527
546
  refresh(): Promise<void>;
528
547
  handlePopstate(): Promise<void>;
529
548
  registerLinkInterception(): () => void;
549
+ /** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
550
+ updateVersion(newVersion: string): void;
551
+ /**
552
+ * Replace the active app-shell snapshot (rootLayout, basename, version)
553
+ * atomically. Used on cross-app navigations when the response's routerId
554
+ * indicates the user entered a different app. Theme, warmup, and prefetch
555
+ * TTL are document-lifetime and not part of the shell.
556
+ */
557
+ updateAppShell(next: import("./app-shell.js").AppShell): void;
530
558
  }
531
559
 
532
560
  /**
@@ -45,7 +45,7 @@ export interface GeneratedManifest {
45
45
  routeTrailingSlash?: Record<string, string>;
46
46
  /** Route names using Prerender (for dev-mode Node.js delegation) */
47
47
  prerenderRoutes?: string[];
48
- /** Route names with passthrough: true (handler kept in bundle for live fallback) */
48
+ /** Route names wrapped with Passthrough() (live handler for runtime fallback) */
49
49
  passthroughRoutes?: string[];
50
50
  /** Route name → response type for non-RSC routes */
51
51
  responseTypeRoutes?: Record<string, string>;
@@ -150,10 +150,7 @@ function buildPrefixTreeNode(
150
150
  if (prerenderDefs && entry.prerenderDef) {
151
151
  prerenderDefs[name] = entry.prerenderDef;
152
152
  }
153
- if (
154
- passthroughRoutes &&
155
- entry.prerenderDef?.options?.passthrough === true
156
- ) {
153
+ if (passthroughRoutes && entry.isPassthrough === true) {
157
154
  passthroughRoutes.push(name);
158
155
  }
159
156
  }
@@ -285,6 +282,7 @@ export function generateManifest<TEnv>(
285
282
  export function generateManifestFull<TEnv>(
286
283
  urlpatterns: UrlPatterns<TEnv, any>,
287
284
  mountIndex: number = 0,
285
+ options?: { urlPrefix?: string },
288
286
  ): FullManifest {
289
287
  const routeManifest: Record<string, string> = {};
290
288
  const routeAncestry: Record<string, string[]> = {};
@@ -310,6 +308,8 @@ export function generateManifestFull<TEnv>(
310
308
  counters: {},
311
309
  mountIndex,
312
310
  trackedIncludes, // Enable include tracking
311
+ // basename sets the initial URL prefix for all path() registrations
312
+ ...(options?.urlPrefix ? { urlPrefix: options.urlPrefix } : {}),
313
313
  },
314
314
  () => {
315
315
  const helpers = createRouteHelpers();
@@ -347,7 +347,7 @@ export function generateManifestFull<TEnv>(
347
347
  if (entry.prerenderDef) {
348
348
  prerenderDefs[name] = entry.prerenderDef;
349
349
  }
350
- if (entry.prerenderDef?.options?.passthrough === true) {
350
+ if (entry.isPassthrough === true) {
351
351
  passthroughRoutes.push(name);
352
352
  }
353
353
  }
@@ -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,