@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
@@ -14,7 +14,6 @@ import { traverseBack } from "./pattern-matching.js";
14
14
  import type { RouteMatchResult } from "./pattern-matching.js";
15
15
  import type { RouteSnapshot } from "./route-snapshot.js";
16
16
 
17
- // Response type -> MIME type used for Accept header matching
18
17
  export const RESPONSE_TYPE_MIME: Record<string, string> = {
19
18
  json: "application/json",
20
19
  text: "text/plain",
@@ -23,7 +22,6 @@ export const RESPONSE_TYPE_MIME: Record<string, string> = {
23
22
  md: "text/markdown",
24
23
  };
25
24
 
26
- // Reverse lookup: MIME type -> response type tag (e.g. "text/html" -> "html")
27
25
  export const MIME_RESPONSE_TYPE: Record<string, string> = Object.fromEntries(
28
26
  Object.entries(RESPONSE_TYPE_MIME).map(([tag, mime]) => [mime, tag]),
29
27
  );
@@ -71,12 +69,10 @@ export function parseAcceptTypes(accept: string): AcceptEntry[] {
71
69
  }
72
70
  entries.push({ mime, q, order: i });
73
71
  }
74
- // Sort: highest q first, then lowest client order first (stable)
75
72
  entries.sort((a, b) => b.q - a.q || a.order - b.order);
76
73
  return entries;
77
74
  }
78
75
 
79
- // Sentinel response type for RSC routes in negotiation candidates
80
76
  export const RSC_RESPONSE_TYPE = "__rsc__";
81
77
 
82
78
  /**
@@ -89,7 +85,6 @@ export function pickNegotiateVariant(
89
85
  acceptEntries: AcceptEntry[],
90
86
  candidates: Array<{ routeKey: string; responseType: string }>,
91
87
  ): { routeKey: string; responseType: string } {
92
- // Build a MIME -> candidate lookup for O(1) matching
93
88
  const byCandidateMime = new Map<
94
89
  string,
95
90
  { routeKey: string; responseType: string }
@@ -106,9 +101,7 @@ export function pickNegotiateVariant(
106
101
 
107
102
  for (const entry of acceptEntries) {
108
103
  if (entry.q === 0) continue;
109
- // Wildcard matches first candidate
110
104
  if (entry.mime === "*/*") return candidates[0]!;
111
- // Type wildcard (e.g. "text/*") -- match first candidate with that type
112
105
  if (entry.mime.endsWith("/*")) {
113
106
  const typePrefix = entry.mime.slice(0, entry.mime.indexOf("/"));
114
107
  for (const [mime, candidate] of byCandidateMime) {
@@ -119,7 +112,6 @@ export function pickNegotiateVariant(
119
112
  const match = byCandidateMime.get(entry.mime);
120
113
  if (match) return match;
121
114
  }
122
- // No match -- use first candidate as default
123
115
  return candidates[0]!;
124
116
  }
125
117
 
@@ -173,7 +165,6 @@ export async function negotiateRoute(
173
165
 
174
166
  const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
175
167
 
176
- // Build candidate list preserving definition order.
177
168
  const variants = matched.negotiateVariants;
178
169
  let candidates: Array<{ routeKey: string; responseType: string }>;
179
170
  if (responseType) {
@@ -190,12 +181,10 @@ export async function negotiateRoute(
190
181
 
191
182
  const variant = pickNegotiateVariant(acceptEntries, candidates);
192
183
 
193
- // RSC won negotiation
194
184
  if (variant.responseType === RSC_RESPONSE_TYPE) {
195
185
  return null;
196
186
  }
197
187
 
198
- // Primary response-type won — use existing manifest entry and middleware
199
188
  if (responseType && variant.routeKey === matched.routeKey) {
200
189
  return {
201
190
  responseType,
@@ -205,8 +194,6 @@ export async function negotiateRoute(
205
194
  negotiated: true,
206
195
  };
207
196
  }
208
-
209
- // Different variant won — load its manifest entry
210
197
  const negotiateEntry = await loadManifest(
211
198
  matched.entry,
212
199
  variant.routeKey,
@@ -117,16 +117,10 @@ export function findNearestErrorBoundary(
117
117
  let current: EntryData | null = entry;
118
118
 
119
119
  while (current) {
120
- // Check if this entry has error boundaries defined
121
120
  if (current.errorBoundary && current.errorBoundary.length > 0) {
122
- // Return the last error boundary (most recently defined takes precedence)
123
121
  return current.errorBoundary[current.errorBoundary.length - 1];
124
122
  }
125
123
 
126
- // Check orphan layouts for error boundaries
127
- // Orphan layouts are siblings that render alongside the main route chain
128
- // They can define error boundaries that catch errors from routes in the same route group
129
- // Check from first to last (first sibling takes precedence as the "outer" wrapper)
130
124
  if (current.layout && current.layout.length > 0) {
131
125
  for (const orphan of current.layout) {
132
126
  if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
@@ -153,11 +147,21 @@ export function findNearestNotFoundBoundary(
153
147
  let current: EntryData | null = entry;
154
148
 
155
149
  while (current) {
156
- // Check if this entry has notFound boundaries defined
157
150
  if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
158
- // Return the last notFound boundary (most recently defined takes precedence)
159
151
  return current.notFoundBoundary[current.notFoundBoundary.length - 1];
160
152
  }
153
+
154
+ // Check orphan layouts mirroring findNearestErrorBoundary: notFoundBoundary
155
+ // attaches identically (onto parent.notFoundBoundary), and an orphan layout
156
+ // (parent=null) is reachable only via this scan. First sibling is "outer".
157
+ if (current.layout && current.layout.length > 0) {
158
+ for (const orphan of current.layout) {
159
+ if (orphan.notFoundBoundary && orphan.notFoundBoundary.length > 0) {
160
+ return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
161
+ }
162
+ }
163
+ }
164
+
161
165
  current = current.parent;
162
166
  }
163
167
 
@@ -207,22 +211,17 @@ export function createErrorSegment(
207
211
  entry: EntryData,
208
212
  params: Record<string, string>,
209
213
  ): ResolvedSegment {
210
- // Determine the component to render
211
214
  let component: ReactNode;
212
215
 
213
216
  if (typeof fallback === "function") {
214
- // ErrorBoundaryHandler - call with error info
215
217
  const props: ErrorBoundaryFallbackProps = {
216
218
  error: errorInfo,
217
219
  };
218
220
  component = fallback(props);
219
221
  } else {
220
- // Static ReactNode fallback
221
222
  component = fallback;
222
223
  }
223
224
 
224
- // Error segment uses the same ID as the layout that has the error boundary
225
- // The error boundary content replaces the layout's outlet content
226
225
  return {
227
226
  id: entry.shortCode,
228
227
  namespace: entry.id,
@@ -261,17 +260,14 @@ export function createNotFoundSegment(
261
260
  entry: EntryData,
262
261
  params: Record<string, string>,
263
262
  ): ResolvedSegment {
264
- // Determine the component to render
265
263
  let component: ReactNode;
266
264
 
267
265
  if (typeof fallback === "function") {
268
- // NotFoundBoundaryHandler - call with props
269
266
  const props: NotFoundBoundaryFallbackProps = {
270
267
  notFound: notFoundInfo,
271
268
  };
272
269
  component = fallback(props);
273
270
  } else {
274
- // Static ReactNode fallback
275
271
  component = fallback;
276
272
  }
277
273
 
@@ -8,13 +8,10 @@ import {
8
8
  import type { MetricsStore } from "../server/context";
9
9
  import type { RouteEntry } from "../types";
10
10
 
11
- // Return a shallow copy with an independent `params` object. The single-entry
12
- // cache below is module-lifetime and keyed only on pathname, so the same result
13
- // object is handed to every same-pathname request in the isolate. ctx.params
14
- // aliases this `params` (see request-context), so without an own copy a handler
15
- // that mutates ctx.params would corrupt the cached entry for later requests.
16
- // `entry` and the flags are intentionally shared by reference: they are
17
- // read-only, and entry identity is compared in match-api (prevMatch.entry).
11
+ // The single-entry cache is module-lifetime, keyed only on pathname, so the same
12
+ // result object is handed to every same-pathname request. ctx.params aliases
13
+ // this object, so handlers mutating it would corrupt the cache for later requests.
14
+ // Clone params; entry/flags are read-only and shared safely.
18
15
  function cloneMatchResult<TEnv>(
19
16
  r: RouteMatchResult<TEnv> | null,
20
17
  ): RouteMatchResult<TEnv> | null {
@@ -40,21 +37,14 @@ export function createFindMatch<TEnv = any>(
40
37
  let lastFindMatchPathname: string | null = null;
41
38
  let lastFindMatchResult: RouteMatchResult<TEnv> | null = null;
42
39
 
43
- // Wrapper for findMatch that uses routesEntries
44
- // Handles lazy evaluation by evaluating lazy entries on first match.
45
- // Phase 1: try O(path_length) trie match.
46
- // Phase 2: fall back to regex iteration.
47
40
  return function findMatch(
48
41
  pathname: string,
49
42
  ms?: MetricsStore,
50
43
  ): RouteMatchResult<TEnv> | null {
51
- // Return cached result if same pathname (avoids double-match per request).
52
- // Clone so a caller mutating ctx.params cannot corrupt the shared cache.
53
44
  if (lastFindMatchPathname === pathname) {
54
45
  return cloneMatchResult(lastFindMatchResult);
55
46
  }
56
47
 
57
- // Helper to push sub-metrics
58
48
  const pushMetric = ms
59
49
  ? (label: string, start: number) => {
60
50
  ms.metrics.push({
@@ -65,16 +55,7 @@ export function createFindMatch<TEnv = any>(
65
55
  }
66
56
  : undefined;
67
57
 
68
- // Phase 1: Try trie match (O(path_length))
69
- // Only use the per-router trie. The global trie merges routes from ALL
70
- // routers and must not be used — in multi-router setups (host routing)
71
- // overlapping paths like "/" would match the wrong app's route.
72
58
  const routeTrie = getRouterTrie(deps.routerId);
73
- // Whether the trie produced a match for this pathname (independent of
74
- // whether the owning RouteEntry was resolvable yet). Used to suppress the
75
- // R3 dev warning below: if the trie DID match but we fell through to the
76
- // regex fallback only because a lazy entry was not spliced in yet, that is
77
- // not a trie gap.
78
59
  let trieMatched = false;
79
60
  if (routeTrie) {
80
61
  const trieStart = performance.now();
@@ -104,12 +85,8 @@ export function createFindMatch<TEnv = any>(
104
85
  }
105
86
  }
106
87
 
107
- // If no entry had the route in its routes map, use the first matching
108
- // entry as fallback (handles main entry with inline routes not yet
109
- // reflected in its routes object).
110
88
  if (!entry) entry = fallbackEntry;
111
89
 
112
- // If entry not found (nested include not yet discovered), evaluate parent
113
90
  if (!entry) {
114
91
  const parent = deps.routesEntries.find(
115
92
  (e) =>
@@ -133,7 +110,6 @@ export function createFindMatch<TEnv = any>(
133
110
  entry,
134
111
  routeKey: trieResult.routeKey,
135
112
  params: trieResult.params,
136
- optionalParams: new Set(trieResult.optionalParams || []),
137
113
  redirectTo: trieResult.redirectTo,
138
114
  ...(trieResult.pr ? { pr: true } : {}),
139
115
  ...(trieResult.pt ? { pt: true } : {}),
@@ -150,12 +126,9 @@ export function createFindMatch<TEnv = any>(
150
126
  }
151
127
  }
152
128
 
153
- // Phase 2: Fall back to existing matching (regex iteration)
154
129
  const regexStart = performance.now();
155
130
  let result = findRouteMatch(pathname, deps.routesEntries);
156
131
 
157
- // If we hit a lazy entry that needs evaluation, evaluate and retry.
158
- // Cap iterations to prevent infinite loops from pathological nesting.
159
132
  const MAX_LAZY_ITERATIONS = 100;
160
133
  let iterations = 0;
161
134
  while (isLazyEvaluationNeeded(result)) {
@@ -21,7 +21,10 @@ import { getRequestContext } from "../server/request-context.js";
21
21
  import { executeInterceptMiddleware } from "./middleware.js";
22
22
  import { createReverseFunction } from "./handler-context.js";
23
23
  import { getGlobalRouteMap } from "../route-map-builder.js";
24
- import { handleHandlerResult } from "./segment-resolution.js";
24
+ import {
25
+ handleHandlerResult,
26
+ warnOnStreamedResponse,
27
+ } from "./segment-resolution.js";
25
28
  import type { SegmentResolutionDeps } from "./types.js";
26
29
  import { debugLog } from "./logging.js";
27
30
  import { runInsideLoaderScope } from "../server/context.js";
@@ -228,6 +231,12 @@ export async function resolveInterceptEntry<TEnv>(
228
231
  let loaderDataPromise: Promise<any[]> | any[] | undefined;
229
232
 
230
233
  if (interceptEntry.loading && loaderPromises.length > 0) {
234
+ if (handlerResult instanceof Promise) {
235
+ warnOnStreamedResponse(
236
+ handlerResult,
237
+ `intercept ${interceptEntry.slotName}`,
238
+ );
239
+ }
231
240
  component =
232
241
  handlerResult instanceof Promise
233
242
  ? handlerResult
@@ -18,9 +18,6 @@ export interface LazyEvalDeps<TEnv = any> {
18
18
  routerId?: string;
19
19
  }
20
20
 
21
- // Detect lazy includes in handler result and create placeholder entries
22
- // Lazy includes are IncludeItem with lazy: true and _lazyContext
23
- // Moved to outer scope so it can be reused by evaluateLazyEntry for nested includes
24
21
  export function findLazyIncludes<TEnv = any>(
25
22
  items: AllUseItems[],
26
23
  ): Array<{
@@ -56,7 +53,6 @@ export function findLazyIncludes<TEnv = any>(
56
53
  });
57
54
  }
58
55
  }
59
- // Recursively check nested items (in layouts, etc.)
60
56
  if ((item as any).uses && Array.isArray((item as any).uses)) {
61
57
  lazyItems.push(...findLazyIncludes((item as any).uses));
62
58
  }
@@ -78,19 +74,6 @@ export function evaluateLazyEntry<TEnv = any>(
78
74
  return;
79
75
  }
80
76
 
81
- // Check for pre-computed routes from build-time data.
82
- // Only leaf nodes (no nested includes) are precomputed, so entries with
83
- // nested lazy includes fall through to the handler below.
84
- //
85
- // The load-bearing protection against two includes sharing a staticPrefix
86
- // lives UPSTREAM in buildPrecomputedByPrefix (build/prefix-tree-utils): a
87
- // shared staticPrefix is omitted from the map entirely, so currentPrecomputed
88
- // never returns routes for it and the shortcut is skipped. The live-count
89
- // check below is a secondary guard only — it is TIMING-BLIND (it counts
90
- // routesEntries, which cannot see a nested sibling that has not been spliced
91
- // in yet), so it must NOT be relied on alone. Kept as defense-in-depth for the
92
- // all-siblings-live case (e.g. several include("/", ...) placeholders created
93
- // up front).
94
77
  const currentPrecomputed = deps.getPrecomputedByPrefix();
95
78
  if (currentPrecomputed) {
96
79
  const routes = currentPrecomputed.get(entry.staticPrefix);
@@ -110,33 +93,18 @@ export function evaluateLazyEntry<TEnv = any>(
110
93
  }
111
94
  }
112
95
 
113
- // Mark as evaluated immediately to prevent concurrent evaluation.
114
- // JS is single-threaded but handlers.handler() could theoretically yield,
115
- // and the while-loop in findMatch retries after evaluation.
116
96
  entry.lazyEvaluated = true;
117
97
 
118
98
  const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
119
99
  const lazyContext = entry.lazyContext;
120
100
 
121
- // Create a new context for evaluating the lazy patterns.
122
- // KNOWN REDUNDANCY (LP3, docs/internal/matching-and-lazy-discovery.md): this
123
- // runs lazyPatterns.handler() purely to extract `patterns` (route name ->
124
- // pattern) for matching, and DISCARDS the EntryData `manifest` it builds.
125
- // loadManifest() then runs the SAME handler again on the first request to
126
- // build the EntryData tree for rendering. Unifying the two runs is deferred
127
- // (the two run in different contexts — see the LP3 todo in
128
- // lazy-include-perf.test.ts). The precomputed-entries shortcut above avoids
129
- // THIS run entirely for leaf includes.
130
101
  const manifest = new Map<string, EntryData>();
131
102
  const patterns = new Map<string, string>();
132
103
  const patternsByPrefix = new Map<string, Map<string, string>>();
133
104
  const trailingSlashMap = new Map<string, TrailingSlashMode>();
134
105
 
135
- // Capture the handler result to detect nested lazy includes
136
106
  let handlerResult: AllUseItems[] = [];
137
107
 
138
- // Merge captured counters from include() to maintain consistent
139
- // shortCode indices with sibling entries from pattern extraction
140
108
  const lazyCounters: Record<string, number> = {};
141
109
  if (lazyContext?.counters) {
142
110
  for (const [key, value] of Object.entries(lazyContext.counters)) {
@@ -158,11 +126,6 @@ export function evaluateLazyEntry<TEnv = any>(
158
126
  includeScope: lazyContext?.includeScope,
159
127
  },
160
128
  () => {
161
- // Run the lazy patterns handler with the original context prefixes.
162
- // The prefix comes from the IncludeItem stored in lazyPatterns. Use the
163
- // slash-collapsing join so a trailing-slash parent prefix does not bake a
164
- // double slash into the registered route patterns (entry.routes,
165
- // reverse(), EntryData.pattern, mountPath) when the handler runs.
166
129
  const includePrefix = (entry as any)._lazyPrefix || "";
167
130
  const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
168
131
 
@@ -176,11 +139,9 @@ export function evaluateLazyEntry<TEnv = any>(
176
139
  },
177
140
  );
178
141
 
179
- // Populate the entry's routes from the patterns
180
142
  const routesObject: Record<string, string> = {};
181
143
  for (const [name, pattern] of patterns.entries()) {
182
144
  routesObject[name] = pattern;
183
- // Also add to merged route map for reverse() support
184
145
  const existingPattern = deps.mergedRouteMap[name];
185
146
  if (existingPattern !== undefined && existingPattern !== pattern) {
186
147
  console.warn(
@@ -191,24 +152,14 @@ export function evaluateLazyEntry<TEnv = any>(
191
152
  deps.mergedRouteMap[name] = pattern;
192
153
  }
193
154
 
194
- // Update the entry in-place
195
155
  entry.routes = routesObject as ResolvedRouteMap<any>;
196
156
 
197
- // Note: Do NOT clear lazyPatterns/lazyContext here.
198
- // loadManifest() needs them on every request to re-run the handler
199
- // in the correct AsyncLocalStorage context (Store.manifest).
200
-
201
- // Update trailing slash config if available
202
157
  if (trailingSlashMap.size > 0) {
203
158
  entry.trailingSlash = Object.fromEntries(trailingSlashMap);
204
159
  }
205
160
 
206
- // Detect nested lazy includes and register them as new entries
207
161
  const nestedLazyIncludes = findLazyIncludes(handlerResult);
208
162
  for (const lazyInclude of nestedLazyIncludes) {
209
- // Compute the full URL prefix (combining parent prefix if any). Use the
210
- // slash-collapsing join so a trailing-slash parent prefix does not produce
211
- // a double-slash staticPrefix the trie's sp can never match.
212
163
  const fullPrefix = joinPrefix(
213
164
  lazyInclude.context.urlPrefix,
214
165
  lazyInclude.prefix,
@@ -217,23 +168,17 @@ export function evaluateLazyEntry<TEnv = any>(
217
168
  const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
218
169
  prefix: "",
219
170
  staticPrefix: extractStaticPrefix(fullPrefix),
220
- routes: {} as ResolvedRouteMap<any>, // Empty until first match
171
+ routes: {} as ResolvedRouteMap<any>,
221
172
  trailingSlash: entry.trailingSlash,
222
173
  handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
223
174
  mountIndex: deps.nextMountIndex(),
224
175
  routerId: deps.routerId,
225
- // Lazy evaluation fields
226
176
  lazy: true,
227
177
  lazyPatterns: lazyInclude.patterns,
228
178
  lazyContext: lazyInclude.context,
229
179
  lazyEvaluated: false,
230
- // Store the include prefix for evaluation
231
180
  _lazyPrefix: lazyInclude.prefix,
232
181
  };
233
- // Insert nested lazy entry before any entry whose staticPrefix is a
234
- // prefix of (but shorter than) this lazy entry's staticPrefix.
235
- // This ensures more specific lazy includes are matched before
236
- // less specific eager entries (e.g., "/href/nested" before "/href/:id").
237
182
  const nestedPrefix = nestedEntry.staticPrefix;
238
183
  let insertIndex = deps.routesEntries.length;
239
184
  if (nestedPrefix) {
@@ -251,6 +196,5 @@ export function evaluateLazyEntry<TEnv = any>(
251
196
  deps.routesEntries.splice(insertIndex, 0, nestedEntry);
252
197
  }
253
198
 
254
- // Re-register route map for runtime reverse() usage
255
199
  registerRouteMap(deps.mergedRouteMap);
256
200
  }
@@ -19,8 +19,8 @@ import type {
19
19
  ErrorBoundaryFallbackProps,
20
20
  ErrorInfo,
21
21
  } from "../types";
22
- import type { LoaderRevalidationResult, ActionContext } from "./types";
23
22
  import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.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";
@@ -71,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
71
71
  ) => ErrorInfo,
72
72
  onError?: LoaderErrorCallback,
73
73
  ): Promise<LoaderDataResult<T>> {
74
- // Extract loader name from segmentId (format: "M1L0D0.loaderName")
74
+ // Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
75
+ // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
76
+ // not a clean display name.
75
77
  const loaderName = segmentId.split(".").pop() || "unknown";
76
78
 
77
79
  return Promise.resolve(promise)
@@ -443,28 +445,28 @@ export function setupLoaderAccess<TEnv>(
443
445
  );
444
446
  }
445
447
 
446
- return (
447
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
448
- ) => {
449
- if (!store) return;
450
-
451
- if (typeof dataOrFn === "function") {
452
- // Run the callback inside the push-callback scope so ctx.use(loader)
453
- // calls it makes including after its own awaits, for an async
454
- // callback are not registered as handler-to-loader deps and do not
455
- // trip the deadlock guard. A pushed promise value is not tracked by
456
- // handleStore.settled and does not block segment resolution, so it
457
- // cannot form a rendered() deadlock. The ALS scope (not a plain
458
- // boolean) is what survives the callback's awaits.
459
- const result = runInsidePushCallbackScope(() =>
460
- (dataOrFn as () => Promise<unknown>)(),
461
- );
462
- store.push(handle.$$id, segmentId, result);
463
- return;
464
- }
448
+ return withDefer(
449
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
450
+ if (!store) return;
451
+
452
+ if (typeof dataOrFn === "function") {
453
+ // Run the callback inside the push-callback scope so ctx.use(loader)
454
+ // calls it makes including after its own awaits, for an async
455
+ // callback are not registered as handler-to-loader deps and do not
456
+ // trip the deadlock guard. A pushed promise value is not tracked by
457
+ // handleStore.settled and does not block segment resolution, so it
458
+ // cannot form a rendered() deadlock. The ALS scope (not a plain
459
+ // boolean) is what survives the callback's awaits.
460
+ const result = runInsidePushCallbackScope(() =>
461
+ (dataOrFn as () => Promise<unknown>)(),
462
+ );
463
+ store.push(handle.$$id, segmentId, result);
464
+ return;
465
+ }
465
466
 
466
- store.push(handle.$$id, segmentId, dataOrFn);
467
- };
467
+ store.push(handle.$$id, segmentId, dataOrFn);
468
+ },
469
+ );
468
470
  }
469
471
 
470
472
  // Deadlock guard and handler-to-loader dependency tracking.
@@ -1,8 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
3
 
4
- // -- Revalidation trace types --
5
-
6
4
  export interface RevalidationTraceEntry {
7
5
  segmentId: string;
8
6
  segmentType: string;
@@ -36,8 +34,6 @@ export interface RevalidationTrace {
36
34
  entries: RevalidationTraceEntry[];
37
35
  }
38
36
 
39
- // -- Log context --
40
-
41
37
  interface RouterLogContext {
42
38
  requestId: string;
43
39
  transactionId: string;
@@ -195,8 +191,6 @@ export function debugWarn(
195
191
  console.warn(`${prefix} ${message}`);
196
192
  }
197
193
 
198
- // -- Revalidation trace helpers --
199
-
200
194
  export function isTraceActive(): boolean {
201
195
  if (!INTERNAL_RANGO_DEBUG) return false;
202
196
  const ctx = routerLogContext.getStore();
@@ -1,9 +1,3 @@
1
- /**
2
- * Router Manifest Loading
3
- *
4
- * Handles lazy loading and validation of route manifests.
5
- */
6
-
7
1
  import { invariant, RouteNotFoundError } from "../errors";
8
2
  import { createRouteHelpers } from "../route-definition";
9
3
  import {
@@ -37,14 +31,6 @@ import { VERSION } from "@rangojs/router:version";
37
31
  // When VERSION changes, this module re-evaluates and the cache is recreated empty.
38
32
  const manifestModuleCache = new Map<string, Map<string, EntryData>>();
39
33
 
40
- /**
41
- * Load manifest from route entry with AsyncLocalStorage context
42
- * Handles lazy imports, unwrapping, and validation
43
- *
44
- * Results are cached at module level after first execution. Subsequent calls
45
- * for the same (routerId, mountIndex, routeKey, isSSR) within the same isolate
46
- * return cached data without re-executing the DSL handler.
47
- */
48
34
  /**
49
35
  * Clear the module-level manifest cache.
50
36
  * Called on HMR to ensure stale handler references are discarded.
@@ -98,20 +84,15 @@ export async function loadManifest(
98
84
  const storeSetupStart = performance.now();
99
85
  const Store = getContext().getOrCreateStore(routeKey);
100
86
 
101
- // Set mount index in store for unique shortCode prefixes
102
87
  Store.mountIndex = mountIndex;
103
-
104
- // Set isSSR flag so loading() can check if we're in SSR
105
88
  Store.isSSR = isSSR;
106
89
 
107
- // Attach metrics store to context if provided
108
90
  if (metricsStore) {
109
91
  Store.metrics = metricsStore;
110
92
  }
111
93
 
112
94
  pushMetric?.("manifest:store-setup", storeSetupStart);
113
95
 
114
- // Clear manifest before rebuilding to prevent stale entry mutations
115
96
  const clearStart = performance.now();
116
97
  Store.manifest.clear();
117
98
  pushMetric?.("manifest:clear", clearStart);
@@ -199,20 +180,16 @@ export async function loadManifest(
199
180
  return lazyPatterns.handler();
200
181
  }
201
182
 
202
- // Wrap handler execution in root layout so routes get correct parent
203
- // This ensures all routes are registered with the layout as their parent
204
183
  let promiseResult: Promise<any> | null = null;
205
184
  const wrappedItems = helpers.layout(MapRootLayout, () => {
206
185
  const result = entry.handler();
207
186
  if (result instanceof Promise) {
208
- // Lazy handler detected - capture promise for async handling
209
187
  promiseResult = result;
210
- return []; // Return empty, we'll discard this wrapped result
188
+ return [];
211
189
  }
212
190
  return result;
213
191
  });
214
192
 
215
- // Handle lazy (Promise-based) handlers
216
193
  if (promiseResult !== null) {
217
194
  const load = await (promiseResult as Promise<any>);
218
195
  if (
@@ -246,7 +223,6 @@ export async function loadManifest(
246
223
  );
247
224
  }
248
225
 
249
- // Inline handler - routes were registered with correct parent inside layout
250
226
  return [wrappedItems].flat(3);
251
227
  },
252
228
  );
@@ -1,10 +1,3 @@
1
- /**
2
- * Match API
3
- *
4
- * Extracted from createRouter closure. Contains match context creation functions
5
- * and the matchError function for error boundary resolution.
6
- */
7
-
8
1
  import { CacheScope, createCacheScope } from "../cache/cache-scope.js";
9
2
  import { RouteNotFoundError } from "../errors";
10
3
  import {
@@ -59,8 +52,6 @@ export async function createMatchContextForFull<TEnv>(
59
52
 
60
53
  const metricsStore = deps.getMetricsStore();
61
54
 
62
- // Full renders always resolve fresh with isSSR: true because loadManifest
63
- // keys its cache on isSSR and stamps Store.isSSR for downstream behavior.
64
55
  const result = await resolveRoute<TEnv>(pathname, {
65
56
  findMatch: (p) => deps.findMatch(p, metricsStore),
66
57
  metricsStore,
@@ -84,12 +75,10 @@ export async function createMatchContextForFull<TEnv>(
84
75
 
85
76
  const { matched } = snapshot;
86
77
 
87
- // Backward compat: downstream middleware reads matched.pt
88
78
  if (snapshot.isPassthrough) {
89
79
  matched.pt = true;
90
80
  }
91
81
 
92
- // Clean URL without internal _rsc* params for userland access
93
82
  const cleanUrl = stripInternalParams(url);
94
83
 
95
84
  const handlerContext = createHandlerContext(
@@ -231,7 +220,6 @@ export async function createMatchContextForPartial<TEnv>(
231
220
  matched.pt = true;
232
221
  }
233
222
 
234
- // Navigation state (prev + intercept-source findMatch calls)
235
223
  const nav = resolveNavigation(request, url, matched.routeKey, {
236
224
  findMatch: deps.findMatch,
237
225
  });
@@ -239,10 +227,6 @@ export async function createMatchContextForPartial<TEnv>(
239
227
  return null;
240
228
  }
241
229
 
242
- // Push route-matching metric. On the fresh path this covers all findMatch
243
- // calls (current + prev + intercept-source). On the reuse path, current-route
244
- // findMatch was already timed during classification, so this only covers
245
- // the nav lookups (prev + intercept-source).
246
230
  if (metricsStore) {
247
231
  const isReuse = !!classifiedRoute;
248
232
  metricsStore.metrics.push({
@@ -259,7 +243,6 @@ export async function createMatchContextForPartial<TEnv>(
259
243
  });
260
244
  }
261
245
 
262
- // Clean URL without internal _rsc* params for userland access
263
246
  const cleanUrl = stripInternalParams(url);
264
247
 
265
248
  const handlerContext = createHandlerContext(
@@ -304,9 +287,6 @@ export async function createMatchContextForPartial<TEnv>(
304
287
  });
305
288
  }
306
289
 
307
- // Store previous route key on the request context for revalidation
308
- // fromRouteName. Uses effectiveFromMatch so intercept-source navigations
309
- // see the intercept origin route, not the plain previous URL route.
310
290
  setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
311
291
 
312
292
  const interceptSelectorContext: InterceptSelectorContext = {