@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -5,13 +5,13 @@ import {
5
5
  runWithRequestContext,
6
6
  type RequestContext,
7
7
  } from "../server/request-context.js";
8
- import { contextGet, contextSet } from "../context-var.js";
8
+ import { contextGet, contextSet, hasContextVars } from "../context-var.js";
9
9
  import {
10
10
  createPrerenderContext,
11
11
  createStaticContext,
12
12
  createReverseFunction,
13
13
  } from "./handler-context.js";
14
- import { isPrerenderPassthrough } from "../prerender.js";
14
+ import { detectPrerenderPassthrough } from "../prerender.js";
15
15
  import { isRouteRootScoped } from "../route-map-builder.js";
16
16
  import { setupBuildUse } from "./loader-resolution.js";
17
17
  import { loadManifest } from "./manifest.js";
@@ -82,6 +82,17 @@ export async function matchForPrerender<TEnv = any>(
82
82
  // Build RouterContext for loadManifest/traverseBack
83
83
  const routerCtx = deps.buildRouterContext();
84
84
 
85
+ // Passthrough sentinel result: an unknown-param Passthrough route falls
86
+ // through to the live handler at runtime, so no artifact is baked. A fresh
87
+ // object is returned per call (no site mutates or identity-compares it).
88
+ const passthroughResult = () => ({
89
+ segments: [],
90
+ handles: "",
91
+ routeName: matched.routeKey,
92
+ params: matchedParams,
93
+ passthrough: true as const,
94
+ });
95
+
85
96
  return runWithRouterContext(routerCtx, async () => {
86
97
  // 2. Load the manifest entry tree
87
98
  const manifestEntry = await loadManifest(
@@ -145,19 +156,10 @@ export async function matchForPrerender<TEnv = any>(
145
156
  );
146
157
  });
147
158
  if (!isKnown) {
148
- return {
149
- segments: [],
150
- handles: "",
151
- routeName: matched.routeKey,
152
- params: matchedParams,
153
- passthrough: true as const,
154
- };
159
+ return passthroughResult();
155
160
  }
156
161
  // Preserve vars set by getParams() for the render context
157
- if (
158
- Object.keys(probeBuildVars).length > 0 ||
159
- Object.getOwnPropertySymbols(probeBuildVars).length > 0
160
- ) {
162
+ if (hasContextVars(probeBuildVars)) {
161
163
  devProbeBuildVars = probeBuildVars;
162
164
  }
163
165
  } catch (err: any) {
@@ -165,13 +167,7 @@ export async function matchForPrerender<TEnv = any>(
165
167
  // Skip errors are intentional — treat as passthrough.
166
168
  // All other errors propagate so dev surfaces them.
167
169
  if (err?.name === "Skip") {
168
- return {
169
- segments: [],
170
- handles: "",
171
- routeName: matched.routeKey,
172
- params: matchedParams,
173
- passthrough: true as const,
174
- };
170
+ return passthroughResult();
175
171
  }
176
172
  throw err;
177
173
  }
@@ -264,17 +260,13 @@ export async function matchForPrerender<TEnv = any>(
264
260
  { skipLoaders: true },
265
261
  );
266
262
 
267
- // 9. Detect passthrough sentinel: handler returned ctx.passthrough()
268
- for (const seg of allSegments) {
269
- if (isPrerenderPassthrough(seg.component)) {
270
- return {
271
- segments: [],
272
- handles: "",
273
- routeName: matched.routeKey,
274
- params: matchedParams,
275
- passthrough: true as const,
276
- };
277
- }
263
+ // 9. Detect passthrough sentinel: handler returned ctx.passthrough().
264
+ // When the route declares loading(), the handler result is deferred so the
265
+ // component is a thenable resolving to the sentinel — detectPrerenderPassthrough
266
+ // resolves thenables before testing (a sync check would miss it and bake a
267
+ // corrupt artifact).
268
+ if (await detectPrerenderPassthrough(allSegments)) {
269
+ return passthroughResult();
278
270
  }
279
271
 
280
272
  // 10. Filter out any loader segments (belt-and-suspenders)
@@ -319,24 +311,14 @@ export async function matchForPrerender<TEnv = any>(
319
311
  }[] = [];
320
312
  let current: EntryData | null = manifestEntry;
321
313
  while (current) {
322
- if (current.intercept && current.intercept.length > 0) {
323
- for (const ic of current.intercept) {
314
+ // Flatten the entry and its sibling layouts into one source list, the
315
+ // same traversal findInterceptForRoute uses; the build keeps ALL matches
316
+ // (not just the innermost) and skips when(). intercept/layout are
317
+ // non-optional arrays, so empty ones are a no-op here.
318
+ for (const source of [current, ...current.layout]) {
319
+ for (const ic of source.intercept) {
324
320
  if (ic.routeName === matched.routeKey) {
325
- foundIntercepts.push({ intercept: ic, entry: current });
326
- }
327
- }
328
- }
329
- if (current.layout && current.layout.length > 0) {
330
- for (const siblingLayout of current.layout) {
331
- if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
332
- for (const ic of siblingLayout.intercept) {
333
- if (ic.routeName === matched.routeKey) {
334
- foundIntercepts.push({
335
- intercept: ic,
336
- entry: siblingLayout,
337
- });
338
- }
339
- }
321
+ foundIntercepts.push({ intercept: ic, entry: source });
340
322
  }
341
323
  }
342
324
  }
@@ -347,6 +329,18 @@ export async function matchForPrerender<TEnv = any>(
347
329
  const interceptResolvedSegments: typeof nonLoaderSegments = [];
348
330
 
349
331
  for (const { intercept, entry: parentEntry } of foundIntercepts) {
332
+ // setupBuildUse keys handle pushes by ctx._currentSegmentId. The main
333
+ // resolveAllSegments pass left it on the route's segment id, so pin it
334
+ // to THIS intercept's slot id before resolving the handler/layout --
335
+ // otherwise the intercept's ctx.use() handle pushes land in the wrong
336
+ // bucket and getDataForSegment(seg.id) below drops them from the baked
337
+ // artifact. Re-pinned per iteration so multiple intercepts targeting
338
+ // the same route each get their own id (mirrors fresh.ts parallel-slot
339
+ // pinning).
340
+ const interceptSegmentId = `${parentEntry.shortCode}.${intercept.slotName}`;
341
+ (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
342
+ interceptSegmentId;
343
+
350
344
  // Resolve handler
351
345
  const handlerRaw =
352
346
  typeof intercept.handler === "function"
@@ -373,7 +367,7 @@ export async function matchForPrerender<TEnv = any>(
373
367
  }
374
368
 
375
369
  interceptResolvedSegments.push({
376
- id: `${parentEntry.shortCode}.${intercept.slotName}`,
370
+ id: interceptSegmentId,
377
371
  namespace: `intercept:${intercept.routeName}`,
378
372
  type: "parallel" as const,
379
373
  index: 0,
@@ -503,14 +497,25 @@ export async function renderStaticSegment<TEnv = any>(
503
497
  setupBuildUse(buildCtx);
504
498
 
505
499
  const raw = await handler(buildCtx);
506
- const component = raw?.type ? raw : raw;
500
+
501
+ // Static handlers must return a ReactNode. A returned Response (e.g. an
502
+ // accidental redirect()) would otherwise be serialized as a corrupt build
503
+ // artifact; fail loudly instead. The fresh/revalidation paths route handler
504
+ // results through handleHandlerResult, which throws Responses; the static
505
+ // build path bypasses that, so guard here.
506
+ if (raw instanceof Response) {
507
+ throw new TypeError(
508
+ `Static handler "${routeName}" returned a Response. Static handlers must return a ReactNode; ` +
509
+ `Responses (redirects, file responses) are not supported during static pre-rendering.`,
510
+ );
511
+ }
507
512
 
508
513
  const segment: ResolvedSegment = {
509
514
  id: handlerId,
510
515
  namespace: handlerId,
511
516
  type: "layout",
512
517
  index: 0,
513
- component,
518
+ component: raw,
514
519
  params: {},
515
520
  belongsToRoute: false,
516
521
  };
@@ -23,10 +23,6 @@ import { negotiateRoute } from "./content-negotiation.js";
23
23
  import { stripInternalParams } from "./handler-context.js";
24
24
  import { resolveRoute, type RouteSnapshot } from "./route-snapshot.js";
25
25
 
26
- // ---------------------------------------------------------------------------
27
- // RequestPlan — discriminated union
28
- // ---------------------------------------------------------------------------
29
-
30
26
  interface RedirectPlan<TEnv = any> {
31
27
  mode: "redirect";
32
28
  route: RouteSnapshot<TEnv>;
@@ -124,10 +120,6 @@ export type {
124
120
  PartialRenderPlan,
125
121
  };
126
122
 
127
- // ---------------------------------------------------------------------------
128
- // classifyRequest — the single authoritative classification step
129
- // ---------------------------------------------------------------------------
130
-
131
123
  export interface ClassifyRequestDeps<TEnv = any> {
132
124
  findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
133
125
  routerVersion: string;
@@ -157,15 +149,12 @@ export async function classifyRequest<TEnv = any>(
157
149
  const isAction =
158
150
  request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
159
151
 
160
- // Version mismatch — runs BEFORE route resolution so stale clients
161
- // requesting removed routes get a reload, not a 404.
162
152
  const clientVersion = url.searchParams.get("_rsc_v");
163
153
  if (
164
154
  deps.routerVersion &&
165
155
  clientVersion &&
166
156
  clientVersion !== deps.routerVersion
167
157
  ) {
168
- // Strip internal _rsc_* params so the browser reloads to a clean URL
169
158
  let reloadUrl = stripInternalParams(url).toString();
170
159
  if (isAction) {
171
160
  const referer = request.headers.get("referer");
@@ -175,9 +164,7 @@ export async function classifyRequest<TEnv = any>(
175
164
  if (refererUrl.origin === url.origin) {
176
165
  reloadUrl = referer;
177
166
  }
178
- } catch {
179
- // Malformed referer, fall back to stripped url
180
- }
167
+ } catch {}
181
168
  }
182
169
  }
183
170
 
@@ -187,18 +174,6 @@ export async function classifyRequest<TEnv = any>(
187
174
  };
188
175
  }
189
176
 
190
- // App switch — also runs BEFORE route resolution (like version-mismatch
191
- // above), and for the same reason: a cross-app SPA navigation must reload
192
- // even when the target route does NOT exist in the target app. If we resolved
193
- // first, a missing route would throw RouteNotFoundError and the 404 would
194
- // render in-place under the SOURCE app's document — violating the invariant
195
- // that crossing a host-router app boundary is always a full document load.
196
- // A mismatched routerId (_rsc_rid) means the navigation crossed an app
197
- // boundary; force a real document navigation. A soft swap can't faithfully
198
- // re-establish the target app's document (stylesheets shared across apps are
199
- // dropped by React 19's by-href dedup; theme/warmup/prefetch-TTL are
200
- // document-lifetime — see browser/app-shell.ts). Only SPA (`_rsc_partial`)
201
- // requests need this; a direct full load already IS the document navigation.
202
177
  const clientRouterId = url.searchParams.get("_rsc_rid");
203
178
  if (
204
179
  clientRouterId &&
@@ -211,9 +186,6 @@ export async function classifyRequest<TEnv = any>(
211
186
  };
212
187
  }
213
188
 
214
- // No metricsStore — classification is a lightweight gating step.
215
- // Route-matching and manifest-loading metrics belong in the match path
216
- // (createMatchContextForFull/Partial) which runs the authoritative resolution.
217
189
  const result = await resolveRoute<TEnv>(pathname, {
218
190
  findMatch: deps.findMatch,
219
191
  lite: true,
@@ -225,7 +197,6 @@ export async function classifyRequest<TEnv = any>(
225
197
  });
226
198
  }
227
199
 
228
- // Redirect
229
200
  if (result.type === "redirect") {
230
201
  const snapshot: RouteSnapshot<TEnv> = {
231
202
  matched: result as any,
@@ -247,7 +218,6 @@ export async function classifyRequest<TEnv = any>(
247
218
 
248
219
  const snapshot = result.snapshot;
249
220
 
250
- // Response route — non-RSC short-circuit (JSON, streaming, etc.)
251
221
  const responseResult = await classifyResponseRoute(
252
222
  request,
253
223
  pathname,
@@ -257,7 +227,6 @@ export async function classifyRequest<TEnv = any>(
257
227
  return responseResult;
258
228
  }
259
229
 
260
- // Mode detection from request signals
261
230
  const actionId =
262
231
  request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
263
232
  const isLoaderFetch = url.searchParams.has("_rsc_loader");
@@ -275,7 +244,6 @@ export async function classifyRequest<TEnv = any>(
275
244
  return { mode: "loader", route: snapshot };
276
245
  }
277
246
 
278
- // PE detection: POST with form content-type, but not a server action
279
247
  const contentType = request.headers.get("content-type") || "";
280
248
  const isFormSubmission =
281
249
  contentType.includes("multipart/form-data") ||
@@ -291,10 +259,6 @@ export async function classifyRequest<TEnv = any>(
291
259
  return { mode: "full-render", route: snapshot, negotiated };
292
260
  }
293
261
 
294
- // ---------------------------------------------------------------------------
295
- // Content negotiation for response routes
296
- // ---------------------------------------------------------------------------
297
-
298
262
  /**
299
263
  * Check if the route is a response route and perform content negotiation
300
264
  * if negotiate variants exist. Returns a ResponseRoutePlan if the route
@@ -305,7 +269,6 @@ async function classifyResponseRoute<TEnv>(
305
269
  pathname: string,
306
270
  snapshot: RouteSnapshot<TEnv>,
307
271
  ): Promise<ResponseRoutePlan<TEnv> | null> {
308
- // negotiateRoute returns the response plan (variant or plain) or null for RSC.
309
272
  const negotiation = await negotiateRoute(request, pathname, snapshot);
310
273
  return negotiation
311
274
  ? { mode: "response", route: snapshot, ...negotiation }
@@ -14,6 +14,7 @@ import {
14
14
  import type { RevalidationTraceEntry } from "./logging.js";
15
15
  import { _getRequestContext } from "../server/request-context.js";
16
16
  import { isAutoGeneratedRouteName } from "../route-name.js";
17
+ import { paramsEqual } from "./params-util.js";
17
18
 
18
19
  /**
19
20
  * Resolve a server-action reference's stable id, mirroring how the action
@@ -32,16 +33,21 @@ function resolveActionRefId(ref: unknown): string | undefined {
32
33
  }
33
34
 
34
35
  /**
35
- * Build the `isAction()` helper bound to the current action's id. Matches a
36
- * single imported action reference, several (variadic), or any export of a
37
- * namespace import (`import * as Mod`). Returns `false` when there is no action
38
- * (plain navigation) or nothing matches.
36
+ * Build the `isAction()` helper bound to the current action's id. Called with no
37
+ * arguments it answers "is this request an action at all?" (any action) `true`
38
+ * during action handling, `false` on plain navigation. Called with one or more
39
+ * action references it narrows to those: a single imported action, several
40
+ * (variadic), or any export of a namespace import (`import * as Mod`). Returns
41
+ * `false` when there is no action (plain navigation) or nothing matches.
39
42
  */
40
43
  function makeIsAction(
41
44
  currentActionId: string | undefined,
42
45
  ): (...actions: ActionRef[]) => boolean {
43
46
  return (...actions: ActionRef[]): boolean => {
44
47
  if (!currentActionId) return false;
48
+ // Bare isAction(): an action is in flight (currentActionId is set) and the
49
+ // caller did not narrow to a specific action, so this is "any action".
50
+ if (actions.length === 0) return true;
45
51
  for (const action of actions) {
46
52
  if (typeof action === "function") {
47
53
  if (resolveActionRefId(action) === currentActionId) return true;
@@ -56,22 +62,6 @@ function makeIsAction(
56
62
  };
57
63
  }
58
64
 
59
- function paramsEqual(
60
- a: Record<string, string>,
61
- b: Record<string, string>,
62
- ): boolean {
63
- if (a === b) return true;
64
-
65
- const keysA = Object.keys(a);
66
- if (keysA.length !== Object.keys(b).length) return false;
67
-
68
- for (const key of keysA) {
69
- if (a[key] !== b[key]) return false;
70
- }
71
-
72
- return true;
73
- }
74
-
75
65
  /**
76
66
  * Options for revalidation evaluation
77
67
  */
@@ -136,8 +126,6 @@ export async function evaluateRevalidation<TEnv>(
136
126
  const paramsChanged = !paramsEqual(nextParams, prevParams);
137
127
  const searchChanged = prevUrl.search !== nextUrl.search;
138
128
 
139
- // Trace helper: push a structured entry to the request-scoped trace buffer.
140
- // Guarded by isTraceActive() so object construction is skipped in production.
141
129
  function pushTrace(
142
130
  defaultVal: boolean,
143
131
  finalVal: boolean,
@@ -156,43 +144,28 @@ export async function evaluateRevalidation<TEnv>(
156
144
  });
157
145
  }
158
146
 
159
- // Calculate default revalidation based on segment type and request method
160
147
  let defaultShouldRevalidate: boolean;
161
148
  let defaultReason: string;
162
149
 
163
150
  if (defaultOverride) {
164
- // Caller injected the seed (e.g. parallel slot not in clientSegmentIds).
165
- // Skip the type-derived heuristic — caller knows better in this context.
166
151
  defaultShouldRevalidate = defaultOverride.value;
167
152
  defaultReason = defaultOverride.reason;
168
153
  } else if (request.method === "POST") {
169
- // Actions: revalidate segments that belong to the route, skip parent chain
170
154
  if (segment.type === "route") {
171
- // Route segment always revalidates on actions
172
155
  defaultShouldRevalidate = true;
173
156
  defaultReason = "action:route-segment";
174
157
  } else if (segment.type === "loader") {
175
- // Loaders always revalidate on actions - they often contain action-sensitive data
176
- // (e.g., cart count after add-to-cart action)
177
158
  defaultShouldRevalidate = true;
178
159
  defaultReason = "action:loader-segment";
179
160
  } else if (segment.belongsToRoute) {
180
- // Segment belongs to route (orphan layouts/parallels) - revalidate
181
161
  defaultShouldRevalidate = true;
182
162
  defaultReason = "action:belongs-to-route";
183
163
  } else {
184
- // Parent chain segment (shared layouts/parallels) - don't revalidate
185
164
  defaultShouldRevalidate = false;
186
165
  defaultReason = "action:parent-chain-skip";
187
166
  }
188
167
  } else {
189
- // Navigation (GET): Conservative defaults to minimize unnecessary revalidations
190
- // Only the route segment revalidates by default - all others require explicit opt-in
191
-
192
168
  if (segment.type === "route") {
193
- // Route segments revalidate when path params OR search params change.
194
- // Search params (e.g., ?page=2&sort=price) are server-parsed via ctx.search,
195
- // so the handler must re-execute to produce updated content.
196
169
  const routeChanged = paramsChanged || searchChanged;
197
170
  defaultShouldRevalidate = routeChanged;
198
171
  defaultReason = paramsChanged
@@ -208,8 +181,6 @@ export async function evaluateRevalidation<TEnv>(
208
181
  });
209
182
  }
210
183
  } else if (segment.belongsToRoute && (paramsChanged || searchChanged)) {
211
- // Children of the route path (loaders, orphan layouts/parallels)
212
- // revalidate when path params or search params change
213
184
  defaultShouldRevalidate = true;
214
185
  defaultReason = paramsChanged
215
186
  ? "nav:route-child-params-changed"
@@ -221,9 +192,6 @@ export async function evaluateRevalidation<TEnv>(
221
192
  searchChanged,
222
193
  });
223
194
  } else {
224
- // Parent layouts and parallels default to no revalidation
225
- // Cannot assume these segments depend on params without explicit declaration
226
- // Use custom revalidation functions to opt-in when needed
227
195
  defaultShouldRevalidate = false;
228
196
  defaultReason = "nav:non-route-skip";
229
197
  debugLog("revalidation", "non-route segment skipped by default", {
@@ -233,7 +201,6 @@ export async function evaluateRevalidation<TEnv>(
233
201
  }
234
202
  }
235
203
 
236
- // No custom revalidations defined - return default behavior without prev segment
237
204
  if (revalidations.length === 0) {
238
205
  if (defaultShouldRevalidate) {
239
206
  debugLog("revalidation", "default revalidate=true", {
@@ -250,14 +217,10 @@ export async function evaluateRevalidation<TEnv>(
250
217
  return defaultShouldRevalidate;
251
218
  }
252
219
 
253
- // Custom revalidations exist - may need full prev segment
254
- // Lazy load prev segment only if getPrevSegment provided
255
220
  const prevSegment = getPrevSegment ? await getPrevSegment() : null;
256
221
 
257
- // Execute revalidation functions with soft/hard decision pattern
258
222
  let currentSuggestion = defaultShouldRevalidate;
259
223
 
260
- // Compute public route names (filtered: undefined for auto-generated routes)
261
224
  const toRouteName =
262
225
  routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
263
226
  const reqCtx = _getRequestContext();
@@ -285,20 +248,14 @@ export async function evaluateRevalidation<TEnv>(
285
248
  actionUrl: actionContext?.actionUrl,
286
249
  actionResult: actionContext?.actionResult,
287
250
  formData: actionContext?.formData,
288
- method: request.method, // GET for navigation, POST for actions
289
- routeName: toRouteName, // Navigation target route name (filtered)
290
- fromRouteName, // Navigation source route name (filtered)
291
- toRouteName, // Navigation target route name (filtered)
292
- // Stale cache context (only true for background revalidation after stale cache render)
251
+ method: request.method,
252
+ routeName: toRouteName,
253
+ fromRouteName,
254
+ toRouteName,
293
255
  stale,
294
256
  });
295
257
 
296
- // Check return type:
297
- // - boolean: hard decision, short-circuit immediately
298
- // - { defaultShouldRevalidate: boolean }: soft decision, update suggestion and continue
299
- // - null/undefined: use default behavior (equivalent to returning { defaultShouldRevalidate })
300
258
  if (typeof result === "boolean") {
301
- // Hard decision - short-circuit
302
259
  debugLog("revalidation", "hard decision", {
303
260
  segmentId: segment.id,
304
261
  revalidator: name,
@@ -311,7 +268,6 @@ export async function evaluateRevalidation<TEnv>(
311
268
  typeof result === "object" &&
312
269
  "defaultShouldRevalidate" in result
313
270
  ) {
314
- // Soft decision - update suggestion and continue
315
271
  currentSuggestion = result.defaultShouldRevalidate;
316
272
  debugLog("revalidation", "soft decision", {
317
273
  segmentId: segment.id,
@@ -319,18 +275,14 @@ export async function evaluateRevalidation<TEnv>(
319
275
  revalidate: currentSuggestion,
320
276
  });
321
277
  } else if (result === null || result === undefined) {
322
- // Defer to default - equivalent to { defaultShouldRevalidate: currentSuggestion }
323
- // This means "I don't care, use whatever the default is"
324
278
  debugLog("revalidation", "deferred to current default", {
325
279
  segmentId: segment.id,
326
280
  revalidator: name,
327
281
  revalidate: currentSuggestion,
328
282
  });
329
- // currentSuggestion stays the same, continue to next function
330
283
  }
331
284
  }
332
285
 
333
- // All revalidators completed - use final suggestion
334
286
  debugLog("revalidation", "final decision", {
335
287
  segmentId: segment.id,
336
288
  revalidate: currentSuggestion,
@@ -230,7 +230,6 @@ export function createRouteSnapshot<TEnv = any>(
230
230
  entry: {} as any,
231
231
  routeKey: "test",
232
232
  params: {},
233
- optionalParams: new Set(),
234
233
  } as RouteMatchResult<TEnv>,
235
234
  manifestEntry: { type: "route", shortCode: "R0", parent: null } as any,
236
235
  entries: [],
@@ -54,10 +54,8 @@ export interface InterceptResult {
54
54
  * Instead of passing 20+ parameters, middleware calls getRouterContext() to access them.
55
55
  */
56
56
  export interface RouterContext<TEnv = any> {
57
- // Route matching
58
57
  findMatch: (pathname: string) => RouteMatchResult | null;
59
58
 
60
- // Manifest loading
61
59
  loadManifest: (
62
60
  entry: any,
63
61
  routeKey: string,
@@ -66,10 +64,8 @@ export interface RouterContext<TEnv = any> {
66
64
  isSSR?: boolean,
67
65
  ) => Promise<EntryData>;
68
66
 
69
- // Entry traversal
70
67
  traverseBack: (entry: EntryData) => Generator<EntryData>;
71
68
 
72
- // Handler context creation
73
69
  createHandlerContext: (
74
70
  params: Record<string, string>,
75
71
  request: Request,
@@ -83,7 +79,6 @@ export interface RouterContext<TEnv = any> {
83
79
  isPassthroughRoute?: boolean,
84
80
  ) => HandlerContext<any, TEnv>;
85
81
 
86
- // Loader setup
87
82
  setupLoaderAccess: (
88
83
  ctx: HandlerContext<any, TEnv>,
89
84
  loaderPromises: Map<string, Promise<any>>,
@@ -94,7 +89,6 @@ export interface RouterContext<TEnv = any> {
94
89
  loaderPromises: Map<string, Promise<any>>,
95
90
  ) => void;
96
91
 
97
- // Context access
98
92
  getContext: () => {
99
93
  getOrCreateStore: (key: string) => any;
100
94
  runWithStore: <T>(
@@ -105,16 +99,13 @@ export interface RouterContext<TEnv = any> {
105
99
  ) => T;
106
100
  };
107
101
 
108
- // Metrics
109
102
  getMetricsStore: () => MetricsStore | undefined;
110
103
 
111
- // Cache
112
104
  createCacheScope: (
113
105
  cacheConfig: any,
114
106
  parent: CacheScope | null,
115
107
  ) => CacheScope | null;
116
108
 
117
- // Intercept detection
118
109
  findInterceptForRoute: (
119
110
  routeKey: string,
120
111
  parentEntry: EntryData | null,
@@ -122,7 +113,6 @@ export interface RouterContext<TEnv = any> {
122
113
  isAction: boolean,
123
114
  ) => InterceptResult | null;
124
115
 
125
- // Segment resolution (with revalidation)
126
116
  resolveAllSegmentsWithRevalidation: (
127
117
  entries: EntryData[],
128
118
  routeKey: string,
@@ -133,7 +123,6 @@ export interface RouterContext<TEnv = any> {
133
123
  request: Request,
134
124
  prevUrl: URL,
135
125
  nextUrl: URL,
136
- loaderPromises: Map<string, Promise<any>>,
137
126
  actionContext: any | undefined,
138
127
  interceptResult: InterceptResult | null,
139
128
  localRouteName: string,
@@ -166,12 +155,10 @@ export interface RouterContext<TEnv = any> {
166
155
  revalidationContext?: RevalidationContext,
167
156
  ) => Promise<ResolvedSegment[]>;
168
157
 
169
- // Collect with markers
170
158
  collectWithMarkers?: <T>(
171
159
  gen: AsyncGenerator<T | { __type: "id"; id: string }>,
172
160
  ) => Promise<{ items: T[]; matchedIds: string[] }>;
173
161
 
174
- // Revalidation evaluation
175
162
  evaluateRevalidation: (params: {
176
163
  segment: ResolvedSegment;
177
164
  prevParams: Record<string, string>;
@@ -195,7 +182,6 @@ export interface RouterContext<TEnv = any> {
195
182
  | "intercept-loader";
196
183
  }) => Promise<boolean>;
197
184
 
198
- // Request context
199
185
  getRequestContext: () =>
200
186
  | {
201
187
  waitUntil: (fn: () => Promise<void>) => void;
@@ -203,7 +189,6 @@ export interface RouterContext<TEnv = any> {
203
189
  }
204
190
  | undefined;
205
191
 
206
- // Simple segment resolution (without revalidation - for full match)
207
192
  resolveAllSegments: (
208
193
  entries: EntryData[],
209
194
  routeKey: string,
@@ -213,7 +198,6 @@ export interface RouterContext<TEnv = any> {
213
198
  options?: { skipLoaders?: boolean },
214
199
  ) => Promise<ResolvedSegment[]>;
215
200
 
216
- // Generator-based simple resolution
217
201
  resolveAllSegmentsGenerator?: (
218
202
  entries: EntryData[],
219
203
  routeKey: string,
@@ -222,21 +206,17 @@ export interface RouterContext<TEnv = any> {
222
206
  loaderPromises: Map<string, Promise<any>>,
223
207
  ) => AsyncGenerator<ResolvedSegment | { __type: "id"; id: string }>;
224
208
 
225
- // Collect segments from generator
226
209
  collectSegmentsFromGenerator?: <T>(
227
210
  gen: AsyncGenerator<T | { __type: "id"; id: string }>,
228
211
  ) => Promise<T[]>;
229
212
 
230
- // Handle store
231
213
  createHandleStore: () => any;
232
214
 
233
- // Loaders-only resolution (for full match cache hit - no revalidation)
234
215
  resolveLoadersOnly?: (
235
216
  entries: EntryData[],
236
217
  handlerContext: HandlerContext<any, TEnv>,
237
218
  ) => Promise<ResolvedSegment[]>;
238
219
 
239
- // Loaders-only resolution (for cache hit scenarios)
240
220
  resolveLoadersOnlyWithRevalidation?: (
241
221
  entries: EntryData[],
242
222
  handlerContext: HandlerContext<any, TEnv>,
@@ -258,10 +238,8 @@ export interface RouterContext<TEnv = any> {
258
238
  // Telemetry sink (optional, no-op when undefined)
259
239
  telemetry?: TelemetrySink;
260
240
 
261
- // Request ID for telemetry span correlation (set per-request in match handlers)
262
241
  requestId?: string;
263
242
 
264
- // Intercept loaders only (for cache hit + intercept scenarios)
265
243
  resolveInterceptLoadersOnly?: (
266
244
  intercept: InterceptEntry,
267
245
  entry: EntryData,
@@ -284,7 +262,6 @@ export interface RouterContext<TEnv = any> {
284
262
  } | null>;
285
263
  }
286
264
 
287
- // AsyncLocalStorage instance for router context
288
265
  const routerContext = new AsyncLocalStorage<RouterContext<any>>();
289
266
 
290
267
  /**
@@ -308,10 +285,6 @@ export function getRouterContext<TEnv = any>(): RouterContext<TEnv> {
308
285
  *
309
286
  * All async code within fn() can call getRouterContext() to access router closures.
310
287
  * This works across async boundaries thanks to AsyncLocalStorage.
311
- *
312
- * @param deps Router dependencies to make available
313
- * @param fn Function to run with dependencies available
314
- * @returns Result of fn()
315
288
  */
316
289
  export function runWithRouterContext<T, TEnv = any>(
317
290
  deps: RouterContext<TEnv>,