@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945

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 (239) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2103 -861
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +66 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +26 -4
  19. package/skills/layout/SKILL.md +6 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +12 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +238 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +33 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/tailwind/SKILL.md +27 -3
  37. package/skills/typesafety/SKILL.md +319 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +116 -0
  42. package/src/browser/action-coordinator.ts +53 -36
  43. package/src/browser/app-shell.ts +39 -0
  44. package/src/browser/event-controller.ts +86 -70
  45. package/src/browser/history-state.ts +21 -0
  46. package/src/browser/index.ts +3 -3
  47. package/src/browser/navigation-bridge.ts +29 -9
  48. package/src/browser/navigation-client.ts +99 -77
  49. package/src/browser/navigation-store.ts +7 -8
  50. package/src/browser/navigation-transaction.ts +10 -28
  51. package/src/browser/partial-update.ts +60 -40
  52. package/src/browser/prefetch/cache.ts +196 -49
  53. package/src/browser/prefetch/fetch.ts +203 -59
  54. package/src/browser/prefetch/queue.ts +36 -5
  55. package/src/browser/rango-state.ts +37 -13
  56. package/src/browser/react/Link.tsx +18 -13
  57. package/src/browser/react/NavigationProvider.tsx +75 -31
  58. package/src/browser/react/filter-segment-order.ts +51 -7
  59. package/src/browser/react/index.ts +3 -0
  60. package/src/browser/react/location-state-shared.ts +175 -4
  61. package/src/browser/react/location-state.ts +39 -13
  62. package/src/browser/react/use-handle.ts +17 -9
  63. package/src/browser/react/use-navigation.ts +22 -2
  64. package/src/browser/react/use-params.ts +20 -8
  65. package/src/browser/react/use-reverse.ts +106 -0
  66. package/src/browser/react/use-router.ts +23 -2
  67. package/src/browser/react/use-segments.ts +11 -8
  68. package/src/browser/response-adapter.ts +52 -1
  69. package/src/browser/rsc-router.tsx +71 -22
  70. package/src/browser/scroll-restoration.ts +22 -14
  71. package/src/browser/segment-reconciler.ts +10 -14
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +44 -30
  74. package/src/browser/types.ts +12 -2
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +45 -1
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-runtime.ts +17 -5
  89. package/src/cache/cache-scope.ts +51 -49
  90. package/src/cache/cf/cf-cache-store.ts +502 -32
  91. package/src/cache/cf/index.ts +3 -0
  92. package/src/cache/handle-snapshot.ts +103 -0
  93. package/src/cache/index.ts +3 -0
  94. package/src/cache/memory-segment-store.ts +3 -2
  95. package/src/cache/types.ts +10 -6
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +96 -205
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -4
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -21
  107. package/src/index.rsc.ts +10 -6
  108. package/src/index.ts +17 -8
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender/store.ts +9 -7
  115. package/src/prerender.ts +4 -4
  116. package/src/response-utils.ts +37 -0
  117. package/src/reverse.ts +65 -39
  118. package/src/route-content-wrapper.tsx +6 -28
  119. package/src/route-definition/dsl-helpers.ts +253 -265
  120. package/src/route-definition/helper-factories.ts +29 -139
  121. package/src/route-definition/helpers-types.ts +43 -15
  122. package/src/route-definition/resolve-handler-use.ts +6 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +26 -41
  125. package/src/router/content-negotiation.ts +15 -2
  126. package/src/router/error-handling.ts +1 -1
  127. package/src/router/find-match.ts +54 -6
  128. package/src/router/handler-context.ts +21 -41
  129. package/src/router/intercept-resolution.ts +4 -18
  130. package/src/router/lazy-includes.ts +41 -22
  131. package/src/router/loader-resolution.ts +82 -36
  132. package/src/router/manifest.ts +41 -19
  133. package/src/router/match-api.ts +4 -3
  134. package/src/router/match-handlers.ts +1 -0
  135. package/src/router/match-middleware/cache-lookup.ts +57 -95
  136. package/src/router/match-middleware/cache-store.ts +3 -2
  137. package/src/router/match-result.ts +53 -32
  138. package/src/router/metrics.ts +1 -1
  139. package/src/router/middleware-types.ts +15 -26
  140. package/src/router/middleware.ts +99 -84
  141. package/src/router/pattern-matching.ts +116 -19
  142. package/src/router/prerender-match.ts +40 -15
  143. package/src/router/preview-match.ts +3 -1
  144. package/src/router/request-classification.ts +40 -37
  145. package/src/router/revalidation.ts +58 -2
  146. package/src/router/router-interfaces.ts +51 -35
  147. package/src/router/router-options.ts +25 -1
  148. package/src/router/router-registry.ts +2 -5
  149. package/src/router/segment-resolution/fresh.ts +27 -6
  150. package/src/router/segment-resolution/revalidation.ts +147 -106
  151. package/src/router/segment-resolution/static-store.ts +19 -5
  152. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  153. package/src/router/substitute-pattern-params.ts +56 -0
  154. package/src/router/trie-matching.ts +40 -16
  155. package/src/router/types.ts +8 -0
  156. package/src/router/url-params.ts +49 -0
  157. package/src/router.ts +37 -25
  158. package/src/rsc/handler-context.ts +2 -2
  159. package/src/rsc/handler.ts +58 -77
  160. package/src/rsc/helpers.ts +72 -43
  161. package/src/rsc/index.ts +1 -1
  162. package/src/rsc/manifest-init.ts +28 -41
  163. package/src/rsc/origin-guard.ts +30 -10
  164. package/src/rsc/progressive-enhancement.ts +4 -0
  165. package/src/rsc/response-error.ts +79 -12
  166. package/src/rsc/response-route-handler.ts +76 -61
  167. package/src/rsc/rsc-rendering.ts +45 -51
  168. package/src/rsc/runtime-warnings.ts +9 -10
  169. package/src/rsc/server-action.ts +33 -39
  170. package/src/rsc/ssr-setup.ts +16 -0
  171. package/src/rsc/types.ts +8 -2
  172. package/src/search-params.ts +4 -4
  173. package/src/segment-content-promise.ts +67 -0
  174. package/src/segment-loader-promise.ts +122 -0
  175. package/src/segment-system.tsx +132 -116
  176. package/src/serialize.ts +243 -0
  177. package/src/server/context.ts +175 -53
  178. package/src/server/cookie-store.ts +28 -4
  179. package/src/server/request-context.ts +57 -51
  180. package/src/ssr/index.tsx +5 -1
  181. package/src/static-handler.ts +1 -1
  182. package/src/types/global-namespace.ts +39 -26
  183. package/src/types/handler-context.ts +68 -50
  184. package/src/types/index.ts +1 -0
  185. package/src/types/loader-types.ts +11 -9
  186. package/src/types/request-scope.ts +126 -0
  187. package/src/types/route-entry.ts +11 -0
  188. package/src/types/segments.ts +35 -2
  189. package/src/urls/include-helper.ts +34 -67
  190. package/src/urls/index.ts +1 -5
  191. package/src/urls/path-helper-types.ts +17 -3
  192. package/src/urls/path-helper.ts +17 -52
  193. package/src/urls/pattern-types.ts +36 -19
  194. package/src/urls/response-types.ts +22 -29
  195. package/src/urls/type-extraction.ts +58 -139
  196. package/src/urls/urls-function.ts +1 -5
  197. package/src/use-loader.tsx +413 -42
  198. package/src/vite/debug.ts +185 -0
  199. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  200. package/src/vite/discovery/discover-routers.ts +106 -75
  201. package/src/vite/discovery/discovery-errors.ts +194 -0
  202. package/src/vite/discovery/gate-state.ts +171 -0
  203. package/src/vite/discovery/prerender-collection.ts +72 -31
  204. package/src/vite/discovery/route-types-writer.ts +40 -84
  205. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  206. package/src/vite/discovery/state.ts +33 -0
  207. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  208. package/src/vite/index.ts +2 -0
  209. package/src/vite/plugin-types.ts +67 -0
  210. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  211. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  212. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  213. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  214. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  215. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  216. package/src/vite/plugins/expose-action-id.ts +54 -30
  217. package/src/vite/plugins/expose-id-utils.ts +12 -8
  218. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  219. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  220. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  221. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  222. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  223. package/src/vite/plugins/performance-tracks.ts +29 -25
  224. package/src/vite/plugins/use-cache-transform.ts +65 -50
  225. package/src/vite/plugins/version-injector.ts +39 -23
  226. package/src/vite/plugins/version-plugin.ts +59 -2
  227. package/src/vite/plugins/virtual-entries.ts +2 -2
  228. package/src/vite/rango.ts +116 -29
  229. package/src/vite/router-discovery.ts +753 -104
  230. package/src/vite/utils/ast-handler-extract.ts +15 -15
  231. package/src/vite/utils/banner.ts +1 -1
  232. package/src/vite/utils/bundle-analysis.ts +4 -2
  233. package/src/vite/utils/client-chunks.ts +190 -0
  234. package/src/vite/utils/forward-user-plugins.ts +193 -0
  235. package/src/vite/utils/manifest-utils.ts +8 -59
  236. package/src/vite/utils/package-resolution.ts +41 -1
  237. package/src/vite/utils/prerender-utils.ts +5 -4
  238. package/src/vite/utils/shared-utils.ts +107 -26
  239. package/src/browser/action-response-classifier.ts +0 -99
@@ -13,8 +13,9 @@ import {
13
13
  } from "../server/request-context.js";
14
14
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
15
15
  import { appendMetric } from "../router/metrics.js";
16
- import { getSSRSetup } from "./ssr-setup.js";
16
+ import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
17
17
  import type { RscPayload } from "./types.js";
18
+ import type { MatchResult } from "../types.js";
18
19
  import {
19
20
  createResponseWithMergedHeaders,
20
21
  createSimpleRedirectResponse,
@@ -35,6 +36,28 @@ export async function handleRscRendering<TEnv>(
35
36
  let payload: RscPayload;
36
37
  let hasInterceptSlots = false;
37
38
 
39
+ // Shared by the partial-fallback and full-render paths. The partial-success
40
+ // payload below is intentionally different (omits rootLayout/theme, adds slots).
41
+ const buildFullPayload = (m: MatchResult): RscPayload => ({
42
+ metadata: {
43
+ pathname: url.pathname,
44
+ routerId: ctx.router.id,
45
+ basename: ctx.router.basename,
46
+ segments: m.segments,
47
+ matched: m.matched,
48
+ diff: m.diff,
49
+ resolvedIds: m.resolvedIds,
50
+ params: m.params,
51
+ isPartial: false,
52
+ rootLayout: ctx.router.rootLayout,
53
+ handles: handleStore.stream(),
54
+ version: ctx.version,
55
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
56
+ themeConfig: ctx.router.themeConfig,
57
+ initialTheme: reqCtx.theme,
58
+ },
59
+ });
60
+
38
61
  if (isPartial) {
39
62
  // Partial render (navigation)
40
63
  const result = await ctx.router.matchPartial(request, { env });
@@ -51,24 +74,7 @@ export async function handleRscRendering<TEnv>(
51
74
  return createSimpleRedirectResponse(match.redirect);
52
75
  }
53
76
 
54
- payload = {
55
- metadata: {
56
- pathname: url.pathname,
57
- routerId: ctx.router.id,
58
- basename: ctx.router.basename,
59
- segments: match.segments,
60
- matched: match.matched,
61
- diff: match.diff,
62
- params: match.params,
63
- isPartial: false,
64
- rootLayout: ctx.router.rootLayout,
65
- handles: handleStore.stream(),
66
- version: ctx.version,
67
- prefetchCacheTTL: ctx.router.prefetchCacheTTL,
68
- themeConfig: ctx.router.themeConfig,
69
- initialTheme: reqCtx.theme,
70
- },
71
- };
77
+ payload = buildFullPayload(match);
72
78
  } else {
73
79
  setRequestContextParams(result.params, result.routeName);
74
80
 
@@ -77,10 +83,16 @@ export async function handleRscRendering<TEnv>(
77
83
  payload = {
78
84
  metadata: {
79
85
  pathname: url.pathname,
86
+ // routerId is serialized on every payload (including within-session
87
+ // ones) so the frontend can read the current app/router identity. It
88
+ // always equals the current app's id: a cross-app navigation is
89
+ // intercepted server-side (X-RSC-Reload) and never delivers a
90
+ // different-router payload to the client.
80
91
  routerId: ctx.router.id,
81
92
  segments: result.segments,
82
93
  matched: result.matched,
83
94
  diff: result.diff,
95
+ resolvedIds: result.resolvedIds,
84
96
  params: result.params,
85
97
  isPartial: true,
86
98
  slots: result.slots,
@@ -133,27 +145,7 @@ export async function handleRscRendering<TEnv>(
133
145
  { headers: { "Content-Type": "application/json" } },
134
146
  );
135
147
  } else {
136
- payload = {
137
- // Initial SSR can reconstruct the tree from segments + rootLayout,
138
- // so we omit root to avoid sending the same structure twice.
139
-
140
- metadata: {
141
- pathname: url.pathname,
142
- routerId: ctx.router.id,
143
- basename: ctx.router.basename,
144
- segments: match.segments,
145
- matched: match.matched,
146
- diff: match.diff,
147
- params: match.params,
148
- isPartial: false,
149
- rootLayout: ctx.router.rootLayout,
150
- handles: handleStore.stream(),
151
- version: ctx.version,
152
- prefetchCacheTTL: ctx.router.prefetchCacheTTL,
153
- themeConfig: ctx.router.themeConfig,
154
- initialTheme: reqCtx.theme,
155
- },
156
- };
148
+ payload = buildFullPayload(match);
157
149
  }
158
150
  }
159
151
 
@@ -187,23 +179,25 @@ export async function handleRscRendering<TEnv>(
187
179
  rscSerializeDur,
188
180
  );
189
181
 
190
- // Determine if this is an RSC request or HTML request.
191
- // Partial requests (_rsc_partial) are always RSC -- they come from client-side
192
- // navigation or prefetch fetch(). We cannot rely on Accept alone since some
193
- // browsers may send Accept: text/html for non-HTML requests.
194
- const isRscRequest =
195
- isPartial ||
196
- (!request.headers.get("accept")?.includes("text/html") &&
197
- !url.searchParams.has("__html")) ||
198
- url.searchParams.has("__rsc");
199
-
200
- if (isRscRequest) {
182
+ if (isRscRequest(request, url, isPartial)) {
201
183
  const renderDur = performance.now() - renderStart;
202
184
  appendMetric(metricsStore, "render:total", renderStart, renderDur);
203
185
  const rscHeaders: Record<string, string> = {
204
186
  "content-type": "text/x-component;charset=utf-8",
205
187
  vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
188
+ // Router identity, so the client can verify pre-decode (before importing
189
+ // chunks) that this content payload belongs to its app and refuse a
190
+ // foreign one (cache/proxy/bug). Control-only reload/redirect responses
191
+ // are deliberately NOT stamped. See browser/response-adapter.ts.
192
+ "X-RSC-Router-Id": ctx.router.id,
206
193
  };
194
+ // Tell the client's prefetch cache to scope this response to its source
195
+ // URL (instead of the default source-agnostic wildcard). Intercept
196
+ // responses depend on the source page matching an intercept rule, so
197
+ // they must not be reused for navigations from other sources.
198
+ if (hasInterceptSlots) {
199
+ rscHeaders["x-rsc-prefetch-scope"] = "source";
200
+ }
207
201
  // Enable browser HTTP caching for prefetch responses only.
208
202
  // Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
209
203
  // non-intercept context (intercept responses depend on source page),
@@ -8,6 +8,7 @@ import {
8
8
  createResponseWithMergedHeaders,
9
9
  carryOverRedirectHeaders,
10
10
  } from "./helpers.js";
11
+ import { isRedirectResponse } from "../response-utils.js";
11
12
 
12
13
  // W3 -----------------------------------------------------------------------
13
14
 
@@ -18,16 +19,14 @@ import {
18
19
  */
19
20
  export function extractRedirectResponse(value: unknown): Response | null {
20
21
  if (!(value instanceof Response)) return null;
21
- const location = value.headers.get("Location");
22
- if (value.status >= 300 && value.status < 400 && location) {
23
- const redirect = createResponseWithMergedHeaders(null, {
24
- status: value.status,
25
- headers: { Location: location },
26
- });
27
- carryOverRedirectHeaders(value, redirect);
28
- return redirect;
29
- }
30
- return null;
22
+ if (!isRedirectResponse(value)) return null;
23
+ const location = value.headers.get("Location")!;
24
+ const redirect = createResponseWithMergedHeaders(null, {
25
+ status: value.status,
26
+ headers: { Location: location },
27
+ });
28
+ carryOverRedirectHeaders(value, redirect);
29
+ return redirect;
31
30
  }
32
31
 
33
32
  /**
@@ -27,7 +27,7 @@ import {
27
27
  hasBodyContent,
28
28
  createResponseWithMergedHeaders,
29
29
  createSimpleRedirectResponse,
30
- carryOverRedirectHeaders,
30
+ interceptRedirectForPartial,
31
31
  } from "./helpers.js";
32
32
  import type { HandlerContext } from "./handler-context.js";
33
33
 
@@ -111,49 +111,25 @@ export async function executeServerAction<TEnv>(
111
111
  loadedAction = await ctx.loadServerAction(actionId);
112
112
  const data = await loadedAction!.apply(null, args);
113
113
 
114
- // Intercept redirect responses from actions. Without this, the redirect
115
- // Response would be serialized as the action returnValue (which fails)
116
- // and the revalidation step would run unnecessarily.
114
+ // Intercept redirect Responses: serializing one as the action returnValue
115
+ // would fail, and revalidation would run needlessly.
117
116
  if (data instanceof Response) {
118
- const redirectUrl = data.headers.get("Location");
119
- const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
120
- if (isRedirect) {
121
- const locationState = getLocationState();
122
- let redirect: Response;
123
- if (locationState) {
124
- redirect = ctx.createRedirectFlightResponse(
125
- redirectUrl,
126
- resolveLocationStateEntries(locationState),
127
- );
128
- } else {
129
- redirect = createSimpleRedirectResponse(redirectUrl);
130
- }
131
- carryOverRedirectHeaders(data, redirect);
132
- return redirect;
133
- }
117
+ const intercepted = interceptRedirectForPartial(
118
+ data,
119
+ ctx.createRedirectFlightResponse,
120
+ );
121
+ if (intercepted) return intercepted;
134
122
  }
135
123
 
136
124
  returnValue = { ok: true, data };
137
125
  } catch (error) {
138
126
  // Handle thrown redirect (e.g., throw redirect('/path'))
139
127
  if (error instanceof Response) {
140
- const redirectUrl = error.headers.get("Location");
141
- const isRedirect =
142
- error.status >= 300 && error.status < 400 && redirectUrl;
143
- if (isRedirect) {
144
- const locationState = getLocationState();
145
- let redirect: Response;
146
- if (locationState) {
147
- redirect = ctx.createRedirectFlightResponse(
148
- redirectUrl,
149
- resolveLocationStateEntries(locationState),
150
- );
151
- } else {
152
- redirect = createSimpleRedirectResponse(redirectUrl);
153
- }
154
- carryOverRedirectHeaders(error, redirect);
155
- return redirect;
156
- }
128
+ const intercepted = interceptRedirectForPartial(
129
+ error,
130
+ ctx.createRedirectFlightResponse,
131
+ );
132
+ if (intercepted) return intercepted;
157
133
 
158
134
  // Non-redirect Response thrown from action — this will be treated
159
135
  // as a regular error and routed to the error boundary. Warn in dev
@@ -208,11 +184,15 @@ export async function executeServerAction<TEnv>(
208
184
  const payload: RscPayload = {
209
185
  metadata: {
210
186
  pathname: url.pathname,
187
+ // routerId exposed for the frontend (current app identity); see
188
+ // rsc-rendering.ts partial branch.
211
189
  routerId: ctx.router.id,
212
190
  segments: errorResult.segments,
213
191
  isPartial: true,
214
192
  matched: errorResult.matched,
215
193
  diff: errorResult.diff,
194
+ resolvedIds: errorResult.resolvedIds,
195
+ params: errorResult.params,
216
196
  isError: true,
217
197
  handles: handleStore.stream(),
218
198
  version: ctx.version,
@@ -233,7 +213,12 @@ export async function executeServerAction<TEnv>(
233
213
 
234
214
  return createResponseWithMergedHeaders(rscStream, {
235
215
  status: actionStatus,
236
- headers: { "content-type": "text/x-component;charset=utf-8" },
216
+ headers: {
217
+ "content-type": "text/x-component;charset=utf-8",
218
+ // Router identity for the client's pre-decode integrity check (the
219
+ // action apply path has no post-decode guard). See response-adapter.
220
+ "X-RSC-Router-Id": ctx.router.id,
221
+ },
237
222
  });
238
223
  }
239
224
  }
@@ -318,11 +303,15 @@ export async function revalidateAfterAction<TEnv>(
318
303
  const payload: RscPayload = {
319
304
  metadata: {
320
305
  pathname: url.pathname,
306
+ // routerId exposed for the frontend (current app identity); see
307
+ // rsc-rendering.ts partial branch.
321
308
  routerId: ctx.router.id,
322
309
  segments: matchResult.segments,
323
310
  isPartial: true,
324
311
  matched: matchResult.matched,
325
312
  diff: matchResult.diff,
313
+ resolvedIds: matchResult.resolvedIds,
314
+ params: matchResult.params,
326
315
  slots: matchResult.slots,
327
316
  handles: handleStore.stream(),
328
317
  version: ctx.version,
@@ -351,6 +340,11 @@ export async function revalidateAfterAction<TEnv>(
351
340
 
352
341
  return createResponseWithMergedHeaders(rscStream, {
353
342
  status: actionStatus,
354
- headers: { "content-type": "text/x-component;charset=utf-8" },
343
+ headers: {
344
+ "content-type": "text/x-component;charset=utf-8",
345
+ // Router identity for the client's pre-decode integrity check (the action
346
+ // apply path has no post-decode guard). See response-adapter.
347
+ "X-RSC-Router-Id": ctx.router.id,
348
+ },
355
349
  });
356
350
  }
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
126
126
 
127
127
  return true;
128
128
  }
129
+
130
+ // Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
131
+ // from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
132
+ // Accept header as needing SSR; this treats it as RSC).
133
+ export function isRscRequest(
134
+ request: Request,
135
+ url: URL,
136
+ isPartial: boolean,
137
+ ): boolean {
138
+ return (
139
+ isPartial ||
140
+ (!request.headers.get("accept")?.includes("text/html") &&
141
+ !url.searchParams.has("__html")) ||
142
+ url.searchParams.has("__rsc")
143
+ );
144
+ }
package/src/rsc/types.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ResolvedSegment, SlotState } from "../types.js";
9
9
  import type { HandleData } from "../server/handle-store.js";
10
- import type { RSCRouterInternal } from "../router/router-interfaces.js";
10
+ import type { RangoInternal } from "../router/router-interfaces.js";
11
11
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
12
12
 
13
13
  /**
@@ -26,6 +26,12 @@ export interface RscPayload {
26
26
  isError?: boolean;
27
27
  matched?: string[];
28
28
  diff?: string[];
29
+ /**
30
+ * All segment ids re-resolved on the server, including null-component
31
+ * ones excluded from `segments`/`diff`. Drives client-side handle-bucket
32
+ * cleanup. Superset of `diff`. See MatchResult.resolvedIds.
33
+ */
34
+ resolvedIds?: string[];
29
35
  /** Merged route params from the matched route */
30
36
  params?: Record<string, string>;
31
37
  slots?: Record<string, SlotState>;
@@ -179,7 +185,7 @@ export interface CreateRSCHandlerOptions<
179
185
  /**
180
186
  * The RSC router instance
181
187
  */
182
- router: RSCRouterInternal<TEnv, TRoutes>;
188
+ router: RangoInternal<TEnv, TRoutes>;
183
189
 
184
190
  /**
185
191
  * RSC dependencies from @vitejs/plugin-rsc/rsc.
@@ -81,11 +81,11 @@ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
81
81
  // ============================================================================
82
82
 
83
83
  /** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
84
- type GlobalRouteMap = keyof RSCRouter.RegisteredRoutes extends never
85
- ? keyof RSCRouter.GeneratedRouteMap extends never
84
+ type GlobalRouteMap = keyof Rango.RegisteredRoutes extends never
85
+ ? keyof Rango.GeneratedRouteMap extends never
86
86
  ? Record<string, string>
87
- : RSCRouter.GeneratedRouteMap
88
- : RSCRouter.RegisteredRoutes;
87
+ : Rango.GeneratedRouteMap
88
+ : Rango.RegisteredRoutes;
89
89
 
90
90
  /**
91
91
  * Extract the resolved search params type for a named route.
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Stable Promise wrappers keyed on the component itself. Objects (React
5
+ * elements, functions, lazy payloads) land in a WeakMap so entries GC when
6
+ * the underlying component is released; primitives (string, number, boolean,
7
+ * null) land in a Map so memoization still applies to text-/null-backed
8
+ * segments like those in partial-update flows. Keeping this cache outside
9
+ * the segment eliminates preservation fields on ResolvedSegment — it survives
10
+ * reconciliation naturally because the component ref is what's stable.
11
+ *
12
+ * Browser-only. On the server each SSR render needs a fresh pending promise
13
+ * so Suspense can emit the loading fallback HTML before content streams. A
14
+ * shared already-resolved promise has `.status === "fulfilled"` attached by
15
+ * React on its first observation — subsequent `use()` calls return
16
+ * synchronously without suspending, so the Suspense fallback never makes it
17
+ * into the initial HTML. Route-definition components share refs across
18
+ * requests, so a global cache would leak tracked state between renders.
19
+ */
20
+ const IS_BROWSER = typeof window !== "undefined";
21
+ const objectContentCache = IS_BROWSER
22
+ ? new WeakMap<object, Promise<ReactNode>>()
23
+ : null;
24
+ const primitiveContentCache = IS_BROWSER
25
+ ? new Map<unknown, Promise<ReactNode>>()
26
+ : null;
27
+
28
+ /**
29
+ * Return a stable Promise wrapping `component`, memoized on the component ref.
30
+ *
31
+ * A fresh `Promise.resolve(component)` each render would suspend for one
32
+ * microtask and briefly commit the loading fallback inside Suspender — the
33
+ * intercept / parallel-slot flicker this indirection prevents. Reusing the
34
+ * same Promise ref keeps React's `use()` in "known fulfilled" state after
35
+ * the first observation.
36
+ *
37
+ * @internal
38
+ */
39
+ export function getMemoizedContentPromise(
40
+ component: ReactNode,
41
+ ): Promise<ReactNode> {
42
+ if (component instanceof Promise) {
43
+ return component as Promise<ReactNode>;
44
+ }
45
+
46
+ if (!objectContentCache || !primitiveContentCache) {
47
+ return Promise.resolve(component);
48
+ }
49
+
50
+ if (component !== null && typeof component === "object") {
51
+ const cached = objectContentCache.get(component);
52
+ if (cached) {
53
+ return cached;
54
+ }
55
+ const promise = Promise.resolve(component);
56
+ objectContentCache.set(component, promise);
57
+ return promise;
58
+ }
59
+
60
+ const cached = primitiveContentCache.get(component);
61
+ if (cached) {
62
+ return cached;
63
+ }
64
+ const promise = Promise.resolve(component);
65
+ primitiveContentCache.set(component, promise);
66
+ return promise;
67
+ }
@@ -0,0 +1,122 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+
3
+ /**
4
+ * Cache of aggregate Promise.all results keyed on the first loader's
5
+ * `loaderData` reference. Each entry holds the source refs it was built from
6
+ * plus the resulting Promise/array; lookup scans entries for the matching
7
+ * source array (typically a single entry, since distinct loader groups rarely
8
+ * share a first source). Object first-refs live in a WeakMap (auto-GC);
9
+ * primitive first-refs (strings/numbers/booleans/null) live in a Map so
10
+ * loaders that resolve to primitive data are memoized too — bounded in
11
+ * practice by the application's loader set.
12
+ *
13
+ * Keying externally means reconciliation's fresh segment objects no longer
14
+ * drop memoization — the cache survives as long as the underlying loader
15
+ * segments do, and GC collects entries when those loaders are released
16
+ * (object keys only).
17
+ *
18
+ * Browser-only. On the server each SSR render needs a fresh Promise so
19
+ * Suspense can actually suspend and emit the loading fallback HTML before
20
+ * content streams. A shared already-resolved promise has `.status` attached
21
+ * by React on first `use()`; subsequent observations return synchronously
22
+ * and skip the fallback. The zero-loader case is especially prone because
23
+ * every empty-loader site would otherwise share one promise across requests.
24
+ */
25
+ const IS_BROWSER = typeof window !== "undefined";
26
+
27
+ interface LoaderCacheEntry {
28
+ sources: any[];
29
+ promise: Promise<any[]> | any[];
30
+ }
31
+
32
+ const objectLoaderCache = IS_BROWSER
33
+ ? new WeakMap<object, LoaderCacheEntry[]>()
34
+ : null;
35
+ const primitiveLoaderCache = IS_BROWSER
36
+ ? new Map<unknown, LoaderCacheEntry[]>()
37
+ : null;
38
+
39
+ // In the browser, a single shared empty aggregate is safe (and desirable) —
40
+ // reusing the same resolved promise keeps React's `use()` in a known-fulfilled
41
+ // state across renders. On the server it would leak `.status = "fulfilled"`
42
+ // across requests and skip the Suspense fallback, so we rebuild on each call.
43
+ const SHARED_EMPTY_LOADER_PROMISE: Promise<any[]> | null = IS_BROWSER
44
+ ? Promise.resolve([])
45
+ : null;
46
+
47
+ function hasSameReferences(a: any[], b: any[]): boolean {
48
+ if (a.length !== b.length) {
49
+ return false;
50
+ }
51
+ for (let i = 0; i < a.length; i++) {
52
+ if (a[i] !== b[i]) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+
59
+ function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
60
+ if (loaders.length === 0) {
61
+ return Promise.resolve([]);
62
+ }
63
+ return Promise.all(
64
+ loaders.map((loader) =>
65
+ loader.loaderData instanceof Promise
66
+ ? loader.loaderData
67
+ : Promise.resolve(loader.loaderData),
68
+ ),
69
+ );
70
+ }
71
+
72
+ function isObjectLike(value: unknown): value is object {
73
+ return (
74
+ value !== null && (typeof value === "object" || typeof value === "function")
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Memoize an aggregate Promise.all for a set of loader segments. Reusing the
80
+ * same aggregate across renders — invalidated only when any underlying
81
+ * loader.loaderData ref changes — keeps React's `use()` in "known fulfilled"
82
+ * state and prevents a fresh Promise.all from suspending (and briefly
83
+ * committing the Suspense fallback) on every partial update that doesn't
84
+ * actually change loader data.
85
+ *
86
+ * @internal
87
+ */
88
+ export function getMemoizedLoaderPromise(
89
+ loaders: ResolvedSegment[],
90
+ ): Promise<any[]> | any[] {
91
+ if (loaders.length === 0) {
92
+ return SHARED_EMPTY_LOADER_PROMISE ?? buildLoaderPromise(loaders);
93
+ }
94
+ if (!objectLoaderCache || !primitiveLoaderCache) {
95
+ return buildLoaderPromise(loaders);
96
+ }
97
+
98
+ const sources = loaders.map((loader) => loader.loaderData);
99
+ const first = sources[0];
100
+ const entries = isObjectLike(first)
101
+ ? objectLoaderCache.get(first)
102
+ : primitiveLoaderCache.get(first);
103
+
104
+ if (entries) {
105
+ for (const entry of entries) {
106
+ if (hasSameReferences(entry.sources, sources)) {
107
+ return entry.promise;
108
+ }
109
+ }
110
+ }
111
+
112
+ const promise = buildLoaderPromise(loaders);
113
+ const newEntry: LoaderCacheEntry = { sources, promise };
114
+ if (entries) {
115
+ entries.push(newEntry);
116
+ } else if (isObjectLike(first)) {
117
+ objectLoaderCache.set(first, [newEntry]);
118
+ } else {
119
+ primitiveLoaderCache.set(first, [newEntry]);
120
+ }
121
+ return promise;
122
+ }