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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2103 -861
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +66 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +26 -4
  19. package/skills/layout/SKILL.md +6 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +12 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +238 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +33 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/tailwind/SKILL.md +27 -3
  37. package/skills/typesafety/SKILL.md +319 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +116 -0
  42. package/src/browser/action-coordinator.ts +53 -36
  43. package/src/browser/app-shell.ts +39 -0
  44. package/src/browser/event-controller.ts +86 -70
  45. package/src/browser/history-state.ts +21 -0
  46. package/src/browser/index.ts +3 -3
  47. package/src/browser/navigation-bridge.ts +29 -9
  48. package/src/browser/navigation-client.ts +99 -77
  49. package/src/browser/navigation-store.ts +7 -8
  50. package/src/browser/navigation-transaction.ts +10 -28
  51. package/src/browser/partial-update.ts +60 -40
  52. package/src/browser/prefetch/cache.ts +196 -49
  53. package/src/browser/prefetch/fetch.ts +203 -59
  54. package/src/browser/prefetch/queue.ts +36 -5
  55. package/src/browser/rango-state.ts +37 -13
  56. package/src/browser/react/Link.tsx +18 -13
  57. package/src/browser/react/NavigationProvider.tsx +75 -31
  58. package/src/browser/react/filter-segment-order.ts +51 -7
  59. package/src/browser/react/index.ts +3 -0
  60. package/src/browser/react/location-state-shared.ts +175 -4
  61. package/src/browser/react/location-state.ts +39 -13
  62. package/src/browser/react/use-handle.ts +17 -9
  63. package/src/browser/react/use-navigation.ts +22 -2
  64. package/src/browser/react/use-params.ts +20 -8
  65. package/src/browser/react/use-reverse.ts +106 -0
  66. package/src/browser/react/use-router.ts +23 -2
  67. package/src/browser/react/use-segments.ts +11 -8
  68. package/src/browser/response-adapter.ts +52 -1
  69. package/src/browser/rsc-router.tsx +71 -22
  70. package/src/browser/scroll-restoration.ts +22 -14
  71. package/src/browser/segment-reconciler.ts +10 -14
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +44 -30
  74. package/src/browser/types.ts +12 -2
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +45 -1
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-runtime.ts +17 -5
  89. package/src/cache/cache-scope.ts +51 -49
  90. package/src/cache/cf/cf-cache-store.ts +502 -32
  91. package/src/cache/cf/index.ts +3 -0
  92. package/src/cache/handle-snapshot.ts +103 -0
  93. package/src/cache/index.ts +3 -0
  94. package/src/cache/memory-segment-store.ts +3 -2
  95. package/src/cache/types.ts +10 -6
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +96 -205
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -4
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -21
  107. package/src/index.rsc.ts +10 -6
  108. package/src/index.ts +17 -8
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender/store.ts +9 -7
  115. package/src/prerender.ts +4 -4
  116. package/src/response-utils.ts +37 -0
  117. package/src/reverse.ts +65 -39
  118. package/src/route-content-wrapper.tsx +6 -28
  119. package/src/route-definition/dsl-helpers.ts +253 -265
  120. package/src/route-definition/helper-factories.ts +29 -139
  121. package/src/route-definition/helpers-types.ts +43 -15
  122. package/src/route-definition/resolve-handler-use.ts +6 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +26 -41
  125. package/src/router/content-negotiation.ts +15 -2
  126. package/src/router/error-handling.ts +1 -1
  127. package/src/router/find-match.ts +54 -6
  128. package/src/router/handler-context.ts +21 -41
  129. package/src/router/intercept-resolution.ts +4 -18
  130. package/src/router/lazy-includes.ts +41 -22
  131. package/src/router/loader-resolution.ts +82 -36
  132. package/src/router/manifest.ts +41 -19
  133. package/src/router/match-api.ts +4 -3
  134. package/src/router/match-handlers.ts +1 -0
  135. package/src/router/match-middleware/cache-lookup.ts +57 -95
  136. package/src/router/match-middleware/cache-store.ts +3 -2
  137. package/src/router/match-result.ts +53 -32
  138. package/src/router/metrics.ts +1 -1
  139. package/src/router/middleware-types.ts +15 -26
  140. package/src/router/middleware.ts +99 -84
  141. package/src/router/pattern-matching.ts +116 -19
  142. package/src/router/prerender-match.ts +40 -15
  143. package/src/router/preview-match.ts +3 -1
  144. package/src/router/request-classification.ts +40 -37
  145. package/src/router/revalidation.ts +58 -2
  146. package/src/router/router-interfaces.ts +51 -35
  147. package/src/router/router-options.ts +25 -1
  148. package/src/router/router-registry.ts +2 -5
  149. package/src/router/segment-resolution/fresh.ts +27 -6
  150. package/src/router/segment-resolution/revalidation.ts +147 -106
  151. package/src/router/segment-resolution/static-store.ts +19 -5
  152. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  153. package/src/router/substitute-pattern-params.ts +56 -0
  154. package/src/router/trie-matching.ts +40 -16
  155. package/src/router/types.ts +8 -0
  156. package/src/router/url-params.ts +49 -0
  157. package/src/router.ts +37 -25
  158. package/src/rsc/handler-context.ts +2 -2
  159. package/src/rsc/handler.ts +58 -77
  160. package/src/rsc/helpers.ts +72 -43
  161. package/src/rsc/index.ts +1 -1
  162. package/src/rsc/manifest-init.ts +28 -41
  163. package/src/rsc/origin-guard.ts +30 -10
  164. package/src/rsc/progressive-enhancement.ts +4 -0
  165. package/src/rsc/response-error.ts +79 -12
  166. package/src/rsc/response-route-handler.ts +76 -61
  167. package/src/rsc/rsc-rendering.ts +45 -51
  168. package/src/rsc/runtime-warnings.ts +9 -10
  169. package/src/rsc/server-action.ts +33 -39
  170. package/src/rsc/ssr-setup.ts +16 -0
  171. package/src/rsc/types.ts +8 -2
  172. package/src/search-params.ts +4 -4
  173. package/src/segment-content-promise.ts +67 -0
  174. package/src/segment-loader-promise.ts +122 -0
  175. package/src/segment-system.tsx +132 -116
  176. package/src/serialize.ts +243 -0
  177. package/src/server/context.ts +175 -53
  178. package/src/server/cookie-store.ts +28 -4
  179. package/src/server/request-context.ts +57 -51
  180. package/src/ssr/index.tsx +5 -1
  181. package/src/static-handler.ts +1 -1
  182. package/src/types/global-namespace.ts +39 -26
  183. package/src/types/handler-context.ts +68 -50
  184. package/src/types/index.ts +1 -0
  185. package/src/types/loader-types.ts +11 -9
  186. package/src/types/request-scope.ts +126 -0
  187. package/src/types/route-entry.ts +11 -0
  188. package/src/types/segments.ts +35 -2
  189. package/src/urls/include-helper.ts +34 -67
  190. package/src/urls/index.ts +1 -5
  191. package/src/urls/path-helper-types.ts +17 -3
  192. package/src/urls/path-helper.ts +17 -52
  193. package/src/urls/pattern-types.ts +36 -19
  194. package/src/urls/response-types.ts +22 -29
  195. package/src/urls/type-extraction.ts +58 -139
  196. package/src/urls/urls-function.ts +1 -5
  197. package/src/use-loader.tsx +413 -42
  198. package/src/vite/debug.ts +185 -0
  199. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  200. package/src/vite/discovery/discover-routers.ts +106 -75
  201. package/src/vite/discovery/discovery-errors.ts +194 -0
  202. package/src/vite/discovery/gate-state.ts +171 -0
  203. package/src/vite/discovery/prerender-collection.ts +72 -31
  204. package/src/vite/discovery/route-types-writer.ts +40 -84
  205. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  206. package/src/vite/discovery/state.ts +33 -0
  207. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  208. package/src/vite/index.ts +2 -0
  209. package/src/vite/plugin-types.ts +67 -0
  210. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  211. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  212. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  213. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  214. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  215. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  216. package/src/vite/plugins/expose-action-id.ts +54 -30
  217. package/src/vite/plugins/expose-id-utils.ts +12 -8
  218. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  219. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  220. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  221. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  222. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  223. package/src/vite/plugins/performance-tracks.ts +29 -25
  224. package/src/vite/plugins/use-cache-transform.ts +65 -50
  225. package/src/vite/plugins/version-injector.ts +39 -23
  226. package/src/vite/plugins/version-plugin.ts +59 -2
  227. package/src/vite/plugins/virtual-entries.ts +2 -2
  228. package/src/vite/rango.ts +116 -29
  229. package/src/vite/router-discovery.ts +753 -104
  230. package/src/vite/utils/ast-handler-extract.ts +15 -15
  231. package/src/vite/utils/banner.ts +1 -1
  232. package/src/vite/utils/bundle-analysis.ts +4 -2
  233. package/src/vite/utils/client-chunks.ts +190 -0
  234. package/src/vite/utils/forward-user-plugins.ts +193 -0
  235. package/src/vite/utils/manifest-utils.ts +8 -59
  236. package/src/vite/utils/package-resolution.ts +41 -1
  237. package/src/vite/utils/prerender-utils.ts +5 -4
  238. package/src/vite/utils/shared-utils.ts +107 -26
  239. package/src/browser/action-response-classifier.ts +0 -99
@@ -114,6 +114,9 @@ let _deserializeSegments:
114
114
  let _restoreHandles:
115
115
  | typeof import("../../cache/handle-snapshot.js").restoreHandles
116
116
  | undefined;
117
+ let _decodeHandles:
118
+ | typeof import("../../cache/handle-snapshot.js").decodeHandles
119
+ | undefined;
117
120
  let _hashParams:
118
121
  | typeof import("../../prerender/param-hash.js").hashParams
119
122
  | undefined;
@@ -148,6 +151,7 @@ async function ensurePrerenderDeps() {
148
151
  ]);
149
152
  _deserializeSegments = codec.deserializeSegments;
150
153
  _restoreHandles = snapshot.restoreHandles;
154
+ _decodeHandles = snapshot.decodeHandles;
151
155
  _hashParams = paramHash.hashParams;
152
156
  _lazyGetRequestContext = reqCtx.getRequestContext;
153
157
  if (prerenderStoreInstance === undefined) {
@@ -174,6 +178,7 @@ async function* yieldFromStore<TEnv>(
174
178
  if (
175
179
  !_deserializeSegments ||
176
180
  !_restoreHandles ||
181
+ !_decodeHandles ||
177
182
  !_hashParams ||
178
183
  !_lazyGetRequestContext
179
184
  ) {
@@ -182,11 +187,15 @@ async function* yieldFromStore<TEnv>(
182
187
 
183
188
  const segments = await _deserializeSegments(entry.segments);
184
189
 
185
- // Replay handle data (same as runtime cache hit path).
186
- // Prefer the eagerly-captured handleStoreRef to avoid ALS disruption in workerd.
190
+ // Replay handle data (same as runtime cache hit path). entry.handles is a
191
+ // Flight-encoded string ("" when none) decode before restore so
192
+ // Promise/ReactNode handle values are revived, not the corrupted JSON form.
187
193
  const handleStore = handleStoreRef ?? _lazyGetRequestContext()?._handleStore;
188
- if (handleStore) {
189
- _restoreHandles(entry.handles, handleStore);
194
+ if (handleStore && entry.handles) {
195
+ const handlesRecord = await _decodeHandles(entry.handles);
196
+ if (handlesRecord) {
197
+ _restoreHandles(handlesRecord, handleStore);
198
+ }
190
199
  }
191
200
 
192
201
  state.cacheHit = true;
@@ -282,6 +291,38 @@ async function* yieldFromStore<TEnv>(
282
291
  }
283
292
  }
284
293
 
294
+ /**
295
+ * Look up a prerendered (build-time cached) entry for the current route and, on
296
+ * a hit, yield its segments. Returns true when an entry was served (the caller
297
+ * should stop the pipeline) and false on a miss. Intercept navigations consult
298
+ * only the intercept-specific entry (`paramHash + "/i"`); a miss there falls
299
+ * through to the normal pipeline so intercept-resolution can run. Callers must
300
+ * guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
301
+ */
302
+ async function* tryPrerenderLookup<TEnv>(
303
+ ctx: MatchContext<TEnv>,
304
+ state: MatchPipelineState,
305
+ pipelineStart: number,
306
+ handleStoreRef?: HandleStore,
307
+ ): AsyncGenerator<ResolvedSegment, boolean> {
308
+ const paramHash = _hashParams!(ctx.matched.params);
309
+ const isPassthroughPrerenderRoute = ctx.entries.some(
310
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
311
+ );
312
+ const lookupHash = ctx.isIntercept ? paramHash + "/i" : paramHash;
313
+ const entry = await prerenderStoreInstance!.get(
314
+ ctx.matched.routeKey,
315
+ lookupHash,
316
+ {
317
+ pathname: ctx.pathname,
318
+ isPassthroughRoute: isPassthroughPrerenderRoute,
319
+ },
320
+ );
321
+ if (!entry) return false;
322
+ yield* yieldFromStore(entry, ctx, state, pipelineStart, handleStoreRef);
323
+ return true;
324
+ }
325
+
285
326
  /**
286
327
  * Async generator middleware type
287
328
  */
@@ -334,54 +375,13 @@ export function withCacheLookup<TEnv>(
334
375
  if (!ctx.isAction && !isHmr && ctx.matched.pr) {
335
376
  await ensurePrerenderDeps();
336
377
  if (prerenderStoreInstance) {
337
- const paramHash = _hashParams!(ctx.matched.params);
338
- const isPassthroughPrerenderRoute = ctx.entries.some(
339
- (entry) => entry.type === "route" && entry.isPassthrough === true,
378
+ const served = yield* tryPrerenderLookup(
379
+ ctx,
380
+ state,
381
+ pipelineStart,
382
+ handleStoreRef,
340
383
  );
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
- }
384
+ if (served) return;
385
385
  }
386
386
  }
387
387
 
@@ -404,51 +404,13 @@ export function withCacheLookup<TEnv>(
404
404
  if (hasStatic) {
405
405
  await ensurePrerenderDeps();
406
406
  if (prerenderStoreInstance) {
407
- const paramHash = _hashParams!(ctx.matched.params);
408
- const isPassthroughPrerenderRoute = ctx.entries.some(
409
- (entry) => entry.type === "route" && entry.isPassthrough === true,
407
+ const served = yield* tryPrerenderLookup(
408
+ ctx,
409
+ state,
410
+ pipelineStart,
411
+ handleStoreRef,
410
412
  );
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
- }
413
+ if (served) return;
452
414
  }
453
415
  }
454
416
  }
@@ -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();
@@ -138,34 +138,38 @@ export async function collectSegments(
138
138
  function deduplicateLoaderSegments(
139
139
  segments: ResolvedSegment[],
140
140
  logPrefix: string,
141
- ): ResolvedSegment[] {
142
- // First pass: collect loaderIds of original (non-inherited) segments
143
- // and whether their parent entry uses loading()
141
+ ): { segments: ResolvedSegment[]; removedIds: Set<string> } {
142
+ // Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
143
+ // namespace, and namespaces of segments that declare loading().
144
144
  const originalLoaders = new Set<string>();
145
- const loadersWithLoading = new Set<string>();
145
+ const loaderIdsByNamespace = new Map<string, string[]>();
146
+ const namespacesWithLoading = new Set<string>();
146
147
  for (const s of segments) {
147
- if (s.type === "loader" && s.loaderId && !s._inherited) {
148
- originalLoaders.add(s.loaderId);
149
- // If the segment has a sibling with loading, the parent uses loading()
150
- // We detect this by checking if any non-loader segment in the same
151
- // namespace has loading defined
148
+ if (s.type === "loader" && s.loaderId) {
149
+ if (!s._inherited) originalLoaders.add(s.loaderId);
150
+ const ids = loaderIdsByNamespace.get(s.namespace);
151
+ if (ids) ids.push(s.loaderId);
152
+ else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
153
+ } else if (
154
+ s.type !== "loader" &&
155
+ s.loading !== undefined &&
156
+ s.loading !== false
157
+ ) {
158
+ namespacesWithLoading.add(s.namespace);
152
159
  }
153
160
  }
154
- // Check if any layout/route segment has loading — if a loader's namespace
155
- // matches a segment with loading, the inherited copy is needed
156
- for (const s of segments) {
157
- if (s.type !== "loader" && s.loading !== undefined && s.loading !== false) {
158
- // Find loaders in this namespace
159
- for (const l of segments) {
160
- if (l.type === "loader" && l.namespace === s.namespace && l.loaderId) {
161
- loadersWithLoading.add(l.loaderId);
162
- }
163
- }
161
+
162
+ // An inherited loader is needed when it shares a namespace with a
163
+ // loading-bearing segment (its data sits behind that LoaderBoundary).
164
+ const loadersWithLoading = new Set<string>();
165
+ for (const ns of namespacesWithLoading) {
166
+ for (const id of loaderIdsByNamespace.get(ns) ?? []) {
167
+ loadersWithLoading.add(id);
164
168
  }
165
169
  }
166
170
 
167
171
  const result: ResolvedSegment[] = [];
168
- let dedupCount = 0;
172
+ const removedIds = new Set<string>();
169
173
 
170
174
  for (const s of segments) {
171
175
  if (
@@ -175,17 +179,20 @@ function deduplicateLoaderSegments(
175
179
  originalLoaders.has(s.loaderId) &&
176
180
  !loadersWithLoading.has(s.loaderId)
177
181
  ) {
178
- dedupCount++;
182
+ removedIds.add(s.id);
179
183
  continue;
180
184
  }
181
185
  result.push(s);
182
186
  }
183
187
 
184
- if (dedupCount > 0) {
185
- debugLog(logPrefix, `deduped ${dedupCount} inherited loader segment(s)`);
188
+ if (removedIds.size > 0) {
189
+ debugLog(
190
+ logPrefix,
191
+ `deduped ${removedIds.size} inherited loader segment(s)`,
192
+ );
186
193
  }
187
194
 
188
- return result;
195
+ return { segments: result, removedIds };
189
196
  }
190
197
 
191
198
  /**
@@ -244,7 +251,7 @@ export function buildMatchResult<TEnv>(
244
251
  );
245
252
  }
246
253
 
247
- const dedupedSegments = deduplicateLoaderSegments(
254
+ const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
248
255
  segmentsToRender,
249
256
  logPrefix,
250
257
  );
@@ -262,18 +269,32 @@ export function buildMatchResult<TEnv>(
262
269
 
263
270
  // Remove deduped loader IDs from matched so the client doesn't treat
264
271
  // them as missing segments and trigger a fallback refetch.
265
- const removedIds = new Set(
266
- segmentsToRender
267
- .filter((s) => !dedupedSegments.includes(s))
268
- .map((s) => s.id),
269
- );
270
272
  const matchedIds =
271
273
  removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
272
274
 
275
+ // resolvedIds: every segment whose handler actually ran this request.
276
+ // For full-match every segment is fresh; for partial-match we filter by
277
+ // the internal `_handlerRan` flag set in revalidation.ts. Drives the
278
+ // client's handle-bucket cleanup — a slot that re-resolved and pushed
279
+ // nothing must have its previous handle data cleared, but `diff` won't
280
+ // carry it because the segment payload skips null-component cached
281
+ // segments to save bytes.
282
+ const resolvedIds = ctx.isFullMatch
283
+ ? allSegments.map((s) => s.id)
284
+ : allSegments.filter((s) => s._handlerRan).map((s) => s.id);
285
+
286
+ // Strip internal-only fields from the segments going on the wire.
287
+ const cleanedSegments = dedupedSegments.map((s) => {
288
+ if (s._handlerRan === undefined) return s;
289
+ const { _handlerRan: _drop, ...rest } = s;
290
+ return rest as ResolvedSegment;
291
+ });
292
+
273
293
  return {
274
- segments: dedupedSegments,
294
+ segments: cleanedSegments,
275
295
  matched: matchedIds,
276
- diff: dedupedSegments.map((s) => s.id),
296
+ diff: cleanedSegments.map((s) => s.id),
297
+ resolvedIds,
277
298
  params: ctx.matched.params,
278
299
  routeName: ctx.routeKey,
279
300
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Router Metrics Utilities
3
3
  *
4
- * Performance metrics collection and reporting for RSC Router.
4
+ * Performance metrics collection and reporting for Rango.
5
5
  */
6
6
 
7
7
  import type { MetricsStore, PerformanceMetric } from "../server/context";
@@ -14,6 +14,7 @@ import type {
14
14
  import type { ScopedReverseFunction } from "../reverse.js";
15
15
  import type { Theme } from "../theme/types.js";
16
16
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
17
+ import type { RequestScope } from "../types/request-scope.js";
17
18
 
18
19
  /**
19
20
  * Get variable function type
@@ -52,33 +53,15 @@ export interface CookieOptions {
52
53
  * Context passed to middleware
53
54
  *
54
55
  * @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
55
- * @template TParams - URL params type (typed for route middleware, Record<string, string> for global middleware)
56
+ * @template TParams - URL params type (typed for route middleware,
57
+ * `Record<string, string | undefined>` for global middleware — absent
58
+ * optional segments are omitted from the params record at runtime, so
59
+ * the index signature must include `undefined`)
56
60
  */
57
61
  export interface MiddlewareContext<
58
62
  TEnv = any,
59
- TParams = Record<string, string>,
60
- > {
61
- /** Original request */
62
- request: Request;
63
-
64
- /** Parsed URL (with internal `_rsc*` params stripped) */
65
- url: URL;
66
-
67
- /**
68
- * The original request URL with all parameters intact, including
69
- * internal `_rsc*` transport params.
70
- */
71
- originalUrl: URL;
72
-
73
- /** URL pathname */
74
- pathname: string;
75
-
76
- /** URL search params */
77
- searchParams: URLSearchParams;
78
-
79
- /** Platform bindings (Cloudflare, etc.) */
80
- env: TEnv;
81
-
63
+ TParams = Record<string, string | undefined>,
64
+ > extends RequestScope<TEnv> {
82
65
  /** URL params extracted from route/middleware pattern */
83
66
  params: TParams;
84
67
 
@@ -157,7 +140,7 @@ export interface MiddlewareContext<
157
140
  * @template TEnv - Environment type - defaults to any for internal flexibility
158
141
  * @template TParams - URL params type (typed for route middleware)
159
142
  *
160
- * When using middleware with global augmentation (RSCRouter.Env), explicitly
143
+ * When using middleware with global augmentation (Rango.Env), explicitly
161
144
  * annotate your middleware functions, or the types will be inferred from context:
162
145
  *
163
146
  * @example
@@ -169,7 +152,10 @@ export interface MiddlewareContext<
169
152
  * router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
170
153
  * ```
171
154
  */
172
- export type MiddlewareFn<TEnv = any, TParams = Record<string, string>> = (
155
+ export type MiddlewareFn<
156
+ TEnv = any,
157
+ TParams = Record<string, string | undefined>,
158
+ > = (
173
159
  ctx: MiddlewareContext<TEnv, TParams>,
174
160
  next: () => Promise<Response>,
175
161
  ) => Response | void | Promise<Response | void>;
@@ -216,5 +202,8 @@ export interface MiddlewareCollectableEntry {
216
202
  */
217
203
  export interface CollectedMiddleware {
218
204
  handler: MiddlewareFn<any, any>;
205
+ // Internal shape only. The user-facing `MiddlewareContext.params` is
206
+ // typed `Record<string, string | undefined>` to reflect that absent
207
+ // optional segments are omitted from the params record at runtime.
219
208
  params: Record<string, string>;
220
209
  }