@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4

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 (253) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2151 -846
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  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/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +364 -0
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +46 -4
  18. package/skills/layout/SKILL.md +28 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +47 -12
  22. package/skills/migrate-nextjs/SKILL.md +562 -0
  23. package/skills/migrate-react-router/SKILL.md +769 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +71 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -22
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +57 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +647 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +84 -11
  47. package/src/browser/navigation-client.ts +76 -28
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +64 -26
  51. package/src/browser/prefetch/cache.ts +129 -21
  52. package/src/browser/prefetch/fetch.ts +148 -16
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +30 -2
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-reconciler.ts +36 -14
  71. package/src/browser/segment-structure-assert.ts +2 -2
  72. package/src/browser/server-action-bridge.ts +23 -30
  73. package/src/browser/types.ts +21 -0
  74. package/src/build/collect-fallback-refs.ts +107 -0
  75. package/src/build/generate-manifest.ts +60 -35
  76. package/src/build/generate-route-types.ts +2 -0
  77. package/src/build/index.ts +2 -0
  78. package/src/build/route-trie.ts +52 -25
  79. package/src/build/route-types/codegen.ts +4 -4
  80. package/src/build/route-types/include-resolution.ts +1 -1
  81. package/src/build/route-types/per-module-writer.ts +7 -4
  82. package/src/build/route-types/router-processing.ts +55 -14
  83. package/src/build/route-types/scan-filter.ts +1 -1
  84. package/src/build/route-types/source-scan.ts +118 -0
  85. package/src/build/runtime-discovery.ts +9 -20
  86. package/src/cache/cache-scope.ts +28 -42
  87. package/src/cache/cf/cf-cache-store.ts +54 -13
  88. package/src/client.rsc.tsx +3 -0
  89. package/src/client.tsx +92 -182
  90. package/src/context-var.ts +5 -5
  91. package/src/decode-loader-results.ts +36 -0
  92. package/src/errors.ts +30 -1
  93. package/src/handle.ts +26 -13
  94. package/src/host/index.ts +2 -2
  95. package/src/host/router.ts +129 -57
  96. package/src/host/types.ts +31 -2
  97. package/src/host/utils.ts +1 -1
  98. package/src/href-client.ts +140 -20
  99. package/src/index.rsc.ts +9 -4
  100. package/src/index.ts +53 -15
  101. package/src/loader-store.ts +500 -0
  102. package/src/loader.rsc.ts +2 -5
  103. package/src/loader.ts +3 -10
  104. package/src/missing-id-error.ts +68 -0
  105. package/src/outlet-context.ts +1 -1
  106. package/src/prerender.ts +4 -4
  107. package/src/response-utils.ts +37 -0
  108. package/src/reverse.ts +65 -36
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +384 -257
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +100 -28
  113. package/src/route-definition/resolve-handler-use.ts +6 -0
  114. package/src/route-definition/use-item-types.ts +32 -0
  115. package/src/route-types.ts +26 -41
  116. package/src/router/basename.ts +14 -0
  117. package/src/router/content-negotiation.ts +15 -2
  118. package/src/router/error-handling.ts +1 -1
  119. package/src/router/handler-context.ts +21 -38
  120. package/src/router/intercept-resolution.ts +4 -18
  121. package/src/router/lazy-includes.ts +8 -8
  122. package/src/router/loader-resolution.ts +19 -2
  123. package/src/router/manifest.ts +22 -13
  124. package/src/router/match-api.ts +4 -3
  125. package/src/router/match-handlers.ts +63 -20
  126. package/src/router/match-middleware/cache-lookup.ts +44 -91
  127. package/src/router/match-middleware/cache-store.ts +3 -2
  128. package/src/router/match-result.ts +53 -32
  129. package/src/router/metrics.ts +1 -1
  130. package/src/router/middleware-types.ts +15 -26
  131. package/src/router/middleware.ts +99 -84
  132. package/src/router/pattern-matching.ts +101 -17
  133. package/src/router/prerender-match.ts +1 -1
  134. package/src/router/preview-match.ts +3 -1
  135. package/src/router/request-classification.ts +4 -28
  136. package/src/router/revalidation.ts +58 -2
  137. package/src/router/router-interfaces.ts +45 -28
  138. package/src/router/router-options.ts +40 -1
  139. package/src/router/router-registry.ts +2 -5
  140. package/src/router/segment-resolution/fresh.ts +27 -6
  141. package/src/router/segment-resolution/revalidation.ts +147 -106
  142. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  143. package/src/router/substitute-pattern-params.ts +56 -0
  144. package/src/router/telemetry.ts +99 -0
  145. package/src/router/trie-matching.ts +18 -13
  146. package/src/router/types.ts +8 -0
  147. package/src/router/url-params.ts +49 -0
  148. package/src/router.ts +38 -23
  149. package/src/rsc/handler-context.ts +2 -2
  150. package/src/rsc/handler.ts +28 -69
  151. package/src/rsc/helpers.ts +91 -43
  152. package/src/rsc/index.ts +1 -1
  153. package/src/rsc/origin-guard.ts +28 -10
  154. package/src/rsc/progressive-enhancement.ts +4 -0
  155. package/src/rsc/response-route-handler.ts +46 -53
  156. package/src/rsc/rsc-rendering.ts +35 -51
  157. package/src/rsc/runtime-warnings.ts +9 -10
  158. package/src/rsc/server-action.ts +17 -37
  159. package/src/rsc/ssr-setup.ts +16 -0
  160. package/src/rsc/types.ts +8 -2
  161. package/src/search-params.ts +4 -4
  162. package/src/segment-content-promise.ts +67 -0
  163. package/src/segment-loader-promise.ts +122 -0
  164. package/src/segment-system.tsx +132 -116
  165. package/src/serialize.ts +243 -0
  166. package/src/server/context.ts +143 -53
  167. package/src/server/cookie-store.ts +28 -4
  168. package/src/server/request-context.ts +20 -42
  169. package/src/ssr/index.tsx +5 -1
  170. package/src/static-handler.ts +1 -1
  171. package/src/testing/cache-status.ts +166 -0
  172. package/src/testing/collect-handle.ts +63 -0
  173. package/src/testing/dispatch.ts +440 -0
  174. package/src/testing/dom.entry.ts +22 -0
  175. package/src/testing/e2e/fixture.ts +154 -0
  176. package/src/testing/e2e/index.ts +149 -0
  177. package/src/testing/e2e/matchers.ts +51 -0
  178. package/src/testing/e2e/page-helpers.ts +272 -0
  179. package/src/testing/e2e/parity.ts +306 -0
  180. package/src/testing/e2e/server.ts +183 -0
  181. package/src/testing/flight-matchers.ts +104 -0
  182. package/src/testing/flight-runtime.d.ts +21 -0
  183. package/src/testing/flight.entry.ts +22 -0
  184. package/src/testing/flight.ts +182 -0
  185. package/src/testing/generated-routes.ts +223 -0
  186. package/src/testing/index.ts +105 -0
  187. package/src/testing/internal/context.ts +193 -0
  188. package/src/testing/render-route.tsx +536 -0
  189. package/src/testing/run-loader.ts +296 -0
  190. package/src/testing/run-middleware.ts +170 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +183 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/route-entry.ts +11 -0
  202. package/src/types/segments.ts +35 -2
  203. package/src/urls/include-helper.ts +34 -67
  204. package/src/urls/index.ts +0 -3
  205. package/src/urls/path-helper-types.ts +41 -7
  206. package/src/urls/path-helper.ts +17 -52
  207. package/src/urls/pattern-types.ts +36 -19
  208. package/src/urls/response-types.ts +22 -29
  209. package/src/urls/type-extraction.ts +26 -116
  210. package/src/urls/urls-function.ts +1 -5
  211. package/src/use-loader.tsx +413 -42
  212. package/src/vite/debug.ts +185 -0
  213. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  214. package/src/vite/discovery/discover-routers.ts +101 -51
  215. package/src/vite/discovery/discovery-errors.ts +194 -0
  216. package/src/vite/discovery/gate-state.ts +171 -0
  217. package/src/vite/discovery/prerender-collection.ts +67 -26
  218. package/src/vite/discovery/route-types-writer.ts +40 -84
  219. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  220. package/src/vite/discovery/state.ts +33 -0
  221. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  222. package/src/vite/index.ts +2 -0
  223. package/src/vite/plugin-types.ts +67 -0
  224. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  225. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  226. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  228. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  229. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  230. package/src/vite/plugins/expose-action-id.ts +54 -30
  231. package/src/vite/plugins/expose-id-utils.ts +12 -8
  232. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  233. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  234. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  235. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  236. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  237. package/src/vite/plugins/performance-tracks.ts +29 -25
  238. package/src/vite/plugins/use-cache-transform.ts +65 -50
  239. package/src/vite/plugins/version-injector.ts +39 -23
  240. package/src/vite/plugins/version-plugin.ts +59 -2
  241. package/src/vite/plugins/virtual-entries.ts +2 -2
  242. package/src/vite/rango.ts +116 -29
  243. package/src/vite/router-discovery.ts +750 -100
  244. package/src/vite/utils/ast-handler-extract.ts +15 -15
  245. package/src/vite/utils/banner.ts +1 -1
  246. package/src/vite/utils/bundle-analysis.ts +4 -2
  247. package/src/vite/utils/client-chunks.ts +190 -0
  248. package/src/vite/utils/forward-user-plugins.ts +193 -0
  249. package/src/vite/utils/manifest-utils.ts +21 -5
  250. package/src/vite/utils/package-resolution.ts +41 -1
  251. package/src/vite/utils/prerender-utils.ts +21 -6
  252. package/src/vite/utils/shared-utils.ts +107 -26
  253. package/src/browser/action-response-classifier.ts +0 -99
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
18
18
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
19
19
  import { isAutoGeneratedRouteName } from "../route-name.js";
20
20
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
21
+ import { substitutePatternParams } from "./substitute-pattern-params.js";
22
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
21
23
 
22
24
  /**
23
25
  * Strip internal _rsc* query params from a URL.
@@ -158,48 +160,14 @@ export function createReverseFunction(
158
160
  );
159
161
  }
160
162
 
161
- let result = pattern;
162
-
163
163
  // Merge current request params as defaults, explicit params override
164
164
  const effectiveParams = currentParams
165
165
  ? { ...currentParams, ...hrefParams }
166
166
  : hrefParams;
167
167
 
168
- // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
169
- // Optional params (:param?) are omitted when not provided
170
- if (effectiveParams) {
171
- let hadOmittedOptional = false;
172
- // First pass: optional params (trailing ?)
173
- result = result.replace(
174
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
175
- (_, key) => {
176
- const value = effectiveParams[key];
177
- if (value === undefined) {
178
- hadOmittedOptional = true;
179
- return "";
180
- }
181
- return encodeURIComponent(value);
182
- },
183
- );
184
- // Second pass: required params (no trailing ?)
185
- result = result.replace(
186
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
187
- (_, key) => {
188
- const value = effectiveParams[key];
189
- if (value === undefined) {
190
- throw new Error(`Missing param "${key}" for route "${name}"`);
191
- }
192
- return encodeURIComponent(value);
193
- },
194
- );
195
- // Clean up slashes only when an optional param was actually omitted,
196
- // so intentional trailing-slash patterns like "/blog/" are preserved.
197
- if (hadOmittedOptional) {
198
- const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
199
- result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
200
- if (hadTrailingSlash && !result.endsWith("/")) result += "/";
201
- }
202
- }
168
+ let result = effectiveParams
169
+ ? substitutePatternParams(pattern, effectiveParams, name)
170
+ : pattern;
203
171
 
204
172
  // Append search params as query string
205
173
  if (search) {
@@ -278,8 +246,12 @@ export function createHandlerContext<TEnv>(
278
246
  search: searchSchema ? resolvedSearchParams : {},
279
247
  pathname,
280
248
  url,
281
- originalUrl: new URL(request.url),
249
+ originalUrl: requestContext?.originalUrl ?? new URL(request.url),
282
250
  env: bindings,
251
+ waitUntil: requestContext
252
+ ? requestContext.waitUntil.bind(requestContext)
253
+ : fireAndForgetWaitUntil,
254
+ executionContext: requestContext?.executionContext,
283
255
  _variables: variables,
284
256
  get: ((keyOrVar: any) => {
285
257
  // Read-time guard: non-cacheable var inside cache() → throw.
@@ -384,6 +356,12 @@ export function createPrerenderContext<TEnv>(
384
356
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
385
357
  );
386
358
  },
359
+ // Build-time prerender has no live request. waitUntil is a true no-op
360
+ // (running fn() here would fire side effects during build, which is
361
+ // incorrect — these are meant to outlive the live response).
362
+ // executionContext is absent for the same reason.
363
+ waitUntil: () => {},
364
+ executionContext: undefined,
387
365
  _variables: variables,
388
366
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
389
367
  set: ((keyOrVar: any, value: any) => {
@@ -473,6 +451,11 @@ export function createStaticContext<TEnv>(
473
451
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
474
452
  );
475
453
  },
454
+ // Static() handlers have no live request. waitUntil is a true no-op
455
+ // (running fn() here would fire side effects during build, which is
456
+ // incorrect). executionContext is absent for the same reason.
457
+ waitUntil: () => {},
458
+ executionContext: undefined,
476
459
  _variables: variables,
477
460
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
478
461
  set: ((keyOrVar: any, value: any) => {
@@ -66,28 +66,14 @@ export function findInterceptForRoute(
66
66
  let current: EntryData | null = fromEntry;
67
67
 
68
68
  while (current) {
69
- if (current.intercept && current.intercept.length > 0) {
70
- for (const intercept of current.intercept) {
69
+ // current first, then its sibling layouts — same order as before.
70
+ for (const source of [current, ...current.layout]) {
71
+ for (const intercept of source.intercept) {
71
72
  if (
72
73
  intercept.routeName === targetRouteKey &&
73
74
  evaluateInterceptWhen(intercept, selectorContext, isAction)
74
75
  ) {
75
- return { intercept, entry: current };
76
- }
77
- }
78
- }
79
-
80
- if (current.layout && current.layout.length > 0) {
81
- for (const siblingLayout of current.layout) {
82
- if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
83
- for (const intercept of siblingLayout.intercept) {
84
- if (
85
- intercept.routeName === targetRouteKey &&
86
- evaluateInterceptWhen(intercept, selectorContext, isAction)
87
- ) {
88
- return { intercept, entry: siblingLayout };
89
- }
90
- }
76
+ return { intercept, entry: source };
91
77
  }
92
78
  }
93
79
  }
@@ -1,8 +1,8 @@
1
1
  import { registerRouteMap } from "../route-map-builder.js";
2
2
  import { extractStaticPrefix } from "./pattern-matching.js";
3
3
  import {
4
- EntryData,
5
- RSCRouterContext,
4
+ type EntryData,
5
+ RangoContext,
6
6
  runWithPrefixes,
7
7
  getIsolatedLazyParent,
8
8
  } from "../server/context";
@@ -125,14 +125,13 @@ export function evaluateLazyEntry<TEnv = any>(
125
125
  // Merge captured counters from include() to maintain consistent
126
126
  // shortCode indices with sibling entries from pattern extraction
127
127
  const lazyCounters: Record<string, number> = {};
128
- if (lazyContext && (lazyContext as any).counters) {
129
- const captured = (lazyContext as any).counters as Record<string, number>;
130
- for (const [key, value] of Object.entries(captured)) {
128
+ if (lazyContext?.counters) {
129
+ for (const [key, value] of Object.entries(lazyContext.counters)) {
131
130
  lazyCounters[key] = value;
132
131
  }
133
132
  }
134
133
 
135
- RSCRouterContext.run(
134
+ RangoContext.run(
136
135
  {
137
136
  manifest,
138
137
  patterns,
@@ -141,8 +140,9 @@ export function evaluateLazyEntry<TEnv = any>(
141
140
  namespace: "lazy",
142
141
  parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
143
142
  counters: lazyCounters,
144
- cacheProfiles: (lazyContext as any)?.cacheProfiles,
145
- rootScoped: (lazyContext as any)?.rootScoped,
143
+ cacheProfiles: lazyContext?.cacheProfiles,
144
+ rootScoped: lazyContext?.rootScoped,
145
+ includeScope: lazyContext?.includeScope,
146
146
  },
147
147
  () => {
148
148
  // Run the lazy patterns handler with the original context prefixes
@@ -24,7 +24,10 @@ import { isHandle, collectHandleData, type Handle } from "../handle.js";
24
24
  import { buildHandleSnapshot } from "../server/handle-store.js";
25
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
26
26
  import { _getRequestContext } from "../server/request-context.js";
27
- import { isInsideLoaderScope } from "../server/context.js";
27
+ import {
28
+ isInsideLoaderScope,
29
+ runInsideLoaderBodyScope,
30
+ } from "../server/context.js";
28
31
  import { debugLog } from "./logging.js";
29
32
 
30
33
  /**
@@ -266,7 +269,10 @@ function createLoaderExecutor<TEnv>(
266
269
  search: (ctx as any).search,
267
270
  pathname: ctx.pathname,
268
271
  url: ctx.url,
272
+ originalUrl: ctx.originalUrl,
269
273
  env: ctx.env,
274
+ waitUntil: ctx.waitUntil.bind(ctx),
275
+ executionContext: ctx.executionContext,
270
276
  get: ((keyOrVar: any) =>
271
277
  contextGet(variables, keyOrVar)) as typeof ctx.get,
272
278
  use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
@@ -350,8 +356,19 @@ function createLoaderExecutor<TEnv>(
350
356
  };
351
357
 
352
358
  const doneLoader = track(`loader:${loader.$$id}`, 2);
359
+ // Run the loader body inside loader scope so request-scoped reads
360
+ // (cookies()/headers() and non-cacheable ctx.get) are exempt from the
361
+ // cache-purity guards: loaders always run fresh, so their reads never leak
362
+ // into a cached segment. DSL loaders are already wrapped by fresh.ts; this
363
+ // also covers handler-invoked loaders (ctx.use(Loader) from a handler),
364
+ // which otherwise execute in the caller's cache scope and would wrongly
365
+ // throw. rendered() gating uses the captured isDslLoader (above), so this
366
+ // does not grant rendered() to handler-invoked loaders. Uses a body-only
367
+ // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
353
368
  const promise = Promise.resolve(
354
- loaderFn(loaderCtx as LoaderContext<any, TEnv>),
369
+ runInsideLoaderBodyScope(() =>
370
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
371
+ ),
355
372
  ).finally(() => {
356
373
  pendingLoaders.delete(loader.$$id);
357
374
  doneLoader();
@@ -126,28 +126,37 @@ export async function loadManifest(
126
126
  // were created during pattern extraction. This prevents shortCode
127
127
  // collisions between lazy and non-lazy entries under the same parent
128
128
  // (e.g., ArticlesLayout and BlogLayout both under NavLayout).
129
- if (lazyContext && (lazyContext as any).counters) {
130
- const captured = (lazyContext as any).counters as Record<string, number>;
131
- for (const [key, value] of Object.entries(captured)) {
129
+ if (lazyContext?.counters) {
130
+ for (const [key, value] of Object.entries(lazyContext.counters)) {
132
131
  Store.counters[key] = Math.max(Store.counters[key] ?? 0, value);
133
132
  }
134
133
  }
135
134
 
136
135
  // Propagate cache profiles for DSL-time cache("profileName") resolution.
137
136
  // Non-lazy entries carry profiles directly; lazy entries carry them
138
- // in the captured lazyContext from include() time.
139
- const entryProfiles =
140
- entry.cacheProfiles ?? (lazyContext as any)?.cacheProfiles;
141
- if (entryProfiles) {
142
- Store.cacheProfiles = entryProfiles;
143
- }
137
+ // in the captured lazyContext from include() time. Always write
138
+ // (including clearing to undefined) so a prior lazy build's profile
139
+ // map cannot leak into a later non-lazy build on the same ALS-backed
140
+ // Store — which would otherwise let cache("name") resolve a profile
141
+ // from an unrelated entry.
142
+ Store.cacheProfiles = entry.cacheProfiles ?? lazyContext?.cacheProfiles;
144
143
 
145
144
  // Propagate rootScoped from lazyContext so that routes inside
146
145
  // nested { name: "sub" } under { name: "" } keep inherited root scope
147
- // when the manifest is rebuilt on each request.
148
- if (lazyContext && (lazyContext as any).rootScoped !== undefined) {
149
- Store.rootScoped = (lazyContext as any).rootScoped;
150
- }
146
+ // when the manifest is rebuilt on each request. Always write
147
+ // (including clearing to undefined, which makes getRootScoped()
148
+ // return its true default) so a prior lazy build's scope cannot leak
149
+ // into a later non-lazy build on the same ALS-backed Store — which
150
+ // would otherwise mis-register plain routes as non-root-scoped and
151
+ // break dot-local reverse resolution.
152
+ Store.rootScoped = lazyContext?.rootScoped;
153
+
154
+ // Propagate includeScope from lazyContext so that direct-descendant
155
+ // shortCodes of this include use the correct scoped counter namespace
156
+ // on every manifest rebuild. Always write (including clearing to
157
+ // undefined) so a prior lazy build's scope cannot leak into a later
158
+ // non-lazy build on the same ALS-backed Store.
159
+ Store.includeScope = lazyContext?.includeScope;
151
160
 
152
161
  const handlerExecStart = performance.now();
153
162
  const useItems = await getContext().runWithStore(
@@ -22,10 +22,10 @@ import { collectRouteMiddleware } from "./middleware.js";
22
22
  import { traverseBack } from "./pattern-matching.js";
23
23
  import { DefaultErrorFallback } from "../default-error-boundary.js";
24
24
  import {
25
- EntryData,
26
- LoaderEntry,
25
+ type EntryData,
26
+ type LoaderEntry,
27
27
  getContext,
28
- InterceptSelectorContext,
28
+ type InterceptSelectorContext,
29
29
  } from "../server/context";
30
30
  import type { ErrorBoundaryHandler, ErrorInfo, MatchResult } from "../types";
31
31
  import type { ReactNode } from "react";
@@ -550,6 +550,7 @@ export async function matchError<TEnv>(
550
550
  segments: [errorSegment],
551
551
  matched: matchedIds,
552
552
  diff: [errorSegment.id],
553
+ resolvedIds: [errorSegment.id],
553
554
  params: matched.params,
554
555
  };
555
556
  }
@@ -33,10 +33,13 @@ import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
33
33
  import type { MiddlewareFn } from "./middleware.js";
34
34
  import {
35
35
  type TelemetrySink,
36
+ type CacheSegmentSignal,
36
37
  safeEmit,
37
38
  resolveSink,
38
39
  getRequestId,
40
+ buildCacheSignalSegments,
39
41
  } from "./telemetry.js";
42
+ import { _getRequestContext } from "../server/request-context.js";
40
43
 
41
44
  export interface MatchHandlerDeps<TEnv = any> {
42
45
  buildRouterContext: () => RouterContext<TEnv>;
@@ -51,6 +54,12 @@ export interface MatchHandlerDeps<TEnv = any> {
51
54
  isAction: boolean,
52
55
  ) => { intercept: InterceptEntry; entry: EntryData } | null;
53
56
  telemetry?: TelemetrySink;
57
+ /**
58
+ * DEVELOPMENT/TEST ONLY gate for the X-Rango-Cache debug header. When true,
59
+ * match/matchPartial stash a coarse route-level cache signal on the request
60
+ * context for the response-finalization path to emit. Default off.
61
+ */
62
+ cacheSignalEnabled?: boolean;
54
63
  }
55
64
 
56
65
  export interface MatchHandlers<TEnv = any> {
@@ -113,6 +122,25 @@ export function createMatchHandlers<TEnv = any>(
113
122
  } = deps;
114
123
  const hasTelemetry = !!deps.telemetry;
115
124
  const telemetry = resolveSink(deps.telemetry);
125
+ const cacheSignalEnabled = !!deps.cacheSignalEnabled;
126
+ // Compute the coarse cache signal when EITHER telemetry needs it (for the
127
+ // cache.decision event) OR the debug header gate is on. When neither is set,
128
+ // this is never called — zero extra work on the hot path.
129
+ const buildSignal = (
130
+ routeKey: string,
131
+ state: {
132
+ cacheHit: boolean;
133
+ cacheSource?: "runtime" | "prerender";
134
+ shouldRevalidate?: boolean;
135
+ },
136
+ ): CacheSegmentSignal[] => buildCacheSignalSegments(routeKey, state);
137
+ // Stash the signal on the request context for the response path to emit as
138
+ // the X-Rango-Cache header. Only when the debug gate is on.
139
+ const recordSignalIfEnabled = (segments: CacheSegmentSignal[]): void => {
140
+ if (!cacheSignalEnabled) return;
141
+ const reqCtx = _getRequestContext();
142
+ if (reqCtx) reqCtx._cacheSignal = segments;
143
+ };
116
144
 
117
145
  async function createMatchContextForFull(
118
146
  request: Request,
@@ -196,6 +224,7 @@ export function createMatchHandlers<TEnv = any>(
196
224
  segments: [],
197
225
  matched: [],
198
226
  diff: [],
227
+ resolvedIds: [],
199
228
  params: {},
200
229
  redirect: result.redirectUrl,
201
230
  };
@@ -207,17 +236,24 @@ export function createMatchHandlers<TEnv = any>(
207
236
  const state = createPipelineState();
208
237
  const pipeline = createMatchPartialPipeline(ctx, state);
209
238
  const matchResult = await collectMatchResult(pipeline, ctx, state);
239
+ if (hasTelemetry || cacheSignalEnabled) {
240
+ const signalSegments = buildSignal(ctx.routeKey, state);
241
+ recordSignalIfEnabled(signalSegments);
242
+ if (hasTelemetry) {
243
+ safeEmit(telemetry, {
244
+ type: "cache.decision",
245
+ timestamp: performance.now(),
246
+ requestId,
247
+ pathname,
248
+ routeKey: ctx.routeKey,
249
+ hit: state.cacheHit,
250
+ shouldRevalidate: !!state.shouldRevalidate,
251
+ source: state.cacheSource,
252
+ segments: signalSegments,
253
+ });
254
+ }
255
+ }
210
256
  if (hasTelemetry) {
211
- safeEmit(telemetry, {
212
- type: "cache.decision",
213
- timestamp: performance.now(),
214
- requestId,
215
- pathname,
216
- routeKey: ctx.routeKey,
217
- hit: state.cacheHit,
218
- shouldRevalidate: !!state.shouldRevalidate,
219
- source: state.cacheSource,
220
- });
221
257
  safeEmit(telemetry, {
222
258
  type: "request.end",
223
259
  timestamp: performance.now(),
@@ -362,17 +398,24 @@ export function createMatchHandlers<TEnv = any>(
362
398
  state,
363
399
  );
364
400
  flushRevalidationTrace();
401
+ if (hasTelemetry || cacheSignalEnabled) {
402
+ const signalSegments = buildSignal(ctx.routeKey, state);
403
+ recordSignalIfEnabled(signalSegments);
404
+ if (hasTelemetry) {
405
+ safeEmit(telemetry, {
406
+ type: "cache.decision",
407
+ timestamp: performance.now(),
408
+ requestId: partialRequestId,
409
+ pathname,
410
+ routeKey: ctx.routeKey,
411
+ hit: state.cacheHit,
412
+ shouldRevalidate: !!state.shouldRevalidate,
413
+ source: state.cacheSource,
414
+ segments: signalSegments,
415
+ });
416
+ }
417
+ }
365
418
  if (hasTelemetry) {
366
- safeEmit(telemetry, {
367
- type: "cache.decision",
368
- timestamp: performance.now(),
369
- requestId: partialRequestId,
370
- pathname,
371
- routeKey: ctx.routeKey,
372
- hit: state.cacheHit,
373
- shouldRevalidate: !!state.shouldRevalidate,
374
- source: state.cacheSource,
375
- });
376
419
  safeEmit(telemetry, {
377
420
  type: "request.end",
378
421
  timestamp: performance.now(),
@@ -282,6 +282,38 @@ async function* yieldFromStore<TEnv>(
282
282
  }
283
283
  }
284
284
 
285
+ /**
286
+ * Look up a prerendered (build-time cached) entry for the current route and, on
287
+ * a hit, yield its segments. Returns true when an entry was served (the caller
288
+ * should stop the pipeline) and false on a miss. Intercept navigations consult
289
+ * only the intercept-specific entry (`paramHash + "/i"`); a miss there falls
290
+ * through to the normal pipeline so intercept-resolution can run. Callers must
291
+ * guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
292
+ */
293
+ async function* tryPrerenderLookup<TEnv>(
294
+ ctx: MatchContext<TEnv>,
295
+ state: MatchPipelineState,
296
+ pipelineStart: number,
297
+ handleStoreRef?: HandleStore,
298
+ ): AsyncGenerator<ResolvedSegment, boolean> {
299
+ const paramHash = _hashParams!(ctx.matched.params);
300
+ const isPassthroughPrerenderRoute = ctx.entries.some(
301
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
302
+ );
303
+ const lookupHash = ctx.isIntercept ? paramHash + "/i" : paramHash;
304
+ const entry = await prerenderStoreInstance!.get(
305
+ ctx.matched.routeKey,
306
+ lookupHash,
307
+ {
308
+ pathname: ctx.pathname,
309
+ isPassthroughRoute: isPassthroughPrerenderRoute,
310
+ },
311
+ );
312
+ if (!entry) return false;
313
+ yield* yieldFromStore(entry, ctx, state, pipelineStart, handleStoreRef);
314
+ return true;
315
+ }
316
+
285
317
  /**
286
318
  * Async generator middleware type
287
319
  */
@@ -334,54 +366,13 @@ export function withCacheLookup<TEnv>(
334
366
  if (!ctx.isAction && !isHmr && ctx.matched.pr) {
335
367
  await ensurePrerenderDeps();
336
368
  if (prerenderStoreInstance) {
337
- const paramHash = _hashParams!(ctx.matched.params);
338
- const isPassthroughPrerenderRoute = ctx.entries.some(
339
- (entry) => entry.type === "route" && entry.isPassthrough === true,
369
+ const served = yield* tryPrerenderLookup(
370
+ ctx,
371
+ state,
372
+ pipelineStart,
373
+ handleStoreRef,
340
374
  );
341
-
342
- if (ctx.isIntercept) {
343
- // Intercept navigation: try intercept-specific prerender entry
344
- const entry = await prerenderStoreInstance.get(
345
- ctx.matched.routeKey,
346
- paramHash + "/i",
347
- {
348
- pathname: ctx.pathname,
349
- isPassthroughRoute: isPassthroughPrerenderRoute,
350
- },
351
- );
352
- if (entry) {
353
- yield* yieldFromStore(
354
- entry,
355
- ctx,
356
- state,
357
- pipelineStart,
358
- handleStoreRef,
359
- );
360
- return;
361
- }
362
- // No intercept prerender -- fall through to normal pipeline
363
- // (skip non-intercept prerender to let intercept-resolution run)
364
- } else {
365
- // Normal navigation: existing behavior
366
- const entry = await prerenderStoreInstance.get(
367
- ctx.matched.routeKey,
368
- paramHash,
369
- {
370
- pathname: ctx.pathname,
371
- isPassthroughRoute: isPassthroughPrerenderRoute,
372
- },
373
- );
374
- if (entry) {
375
- yield* yieldFromStore(
376
- entry,
377
- ctx,
378
- state,
379
- pipelineStart,
380
- handleStoreRef,
381
- );
382
- return;
383
- }
384
- }
375
+ if (served) return;
385
376
  }
386
377
  }
387
378
 
@@ -404,51 +395,13 @@ export function withCacheLookup<TEnv>(
404
395
  if (hasStatic) {
405
396
  await ensurePrerenderDeps();
406
397
  if (prerenderStoreInstance) {
407
- const paramHash = _hashParams!(ctx.matched.params);
408
- const isPassthroughPrerenderRoute = ctx.entries.some(
409
- (entry) => entry.type === "route" && entry.isPassthrough === true,
398
+ const served = yield* tryPrerenderLookup(
399
+ ctx,
400
+ state,
401
+ pipelineStart,
402
+ handleStoreRef,
410
403
  );
411
-
412
- if (ctx.isIntercept) {
413
- const entry = await prerenderStoreInstance.get(
414
- ctx.matched.routeKey,
415
- paramHash + "/i",
416
- {
417
- pathname: ctx.pathname,
418
- isPassthroughRoute: isPassthroughPrerenderRoute,
419
- },
420
- );
421
- if (entry) {
422
- yield* yieldFromStore(
423
- entry,
424
- ctx,
425
- state,
426
- pipelineStart,
427
- handleStoreRef,
428
- );
429
- return;
430
- }
431
- // No intercept prerender -- fall through to normal pipeline
432
- } else {
433
- const entry = await prerenderStoreInstance.get(
434
- ctx.matched.routeKey,
435
- paramHash,
436
- {
437
- pathname: ctx.pathname,
438
- isPassthroughRoute: isPassthroughPrerenderRoute,
439
- },
440
- );
441
- if (entry) {
442
- yield* yieldFromStore(
443
- entry,
444
- ctx,
445
- state,
446
- pipelineStart,
447
- handleStoreRef,
448
- );
449
- return;
450
- }
451
- }
404
+ if (served) return;
452
405
  }
453
406
  }
454
407
  }
@@ -169,10 +169,11 @@ export function withCacheStore<TEnv>(
169
169
  // skip (client already had them). Segments where the handler intentionally
170
170
  // returned null are not revalidation skips — re-rendering them will still
171
171
  // produce null, so proactive caching would be wasted work.
172
- const clientIdSet = new Set(ctx.clientSegmentIds);
173
172
  const hasNullComponents = allSegmentsToCache.some(
174
173
  (s) =>
175
- s.component === null && s.type !== "loader" && clientIdSet.has(s.id),
174
+ s.component === null &&
175
+ s.type !== "loader" &&
176
+ ctx.clientSegmentSet.has(s.id),
176
177
  );
177
178
 
178
179
  const requestCtx = getRequestContext();