@rangojs/router 0.24.4 → 0.25.0

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 (267) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -21,6 +21,7 @@ import type {
21
21
  import type { PathHelpers } from "../urls/path-helper-types.js";
22
22
  import type { AllUseItems } from "../route-types.js";
23
23
  import { urls } from "../urls/urls-function.js";
24
+ import { toShouldRevalidateFn } from "../router/revalidation.js";
24
25
  import { isHoistedRevalidateRef } from "./hoisted-revalidate.js";
25
26
  import { getClientUrlRevalidator } from "./revalidate-registry.js";
26
27
  import { setLayoutLoaderCopies } from "./layout-loader-copies.js";
@@ -112,11 +113,11 @@ export interface ClientUrlProjectionRoute {
112
113
  * loader(), so a prefetch of the route defers them. Absent (= none) in
113
114
  * projections serialized before the option existed. */
114
115
  readonly deferrableLoaderIndices?: readonly number[];
115
- /** Per loader (aligned with loaderIds), the keys of its hoisted
116
- * revalidate() predicates in declaration order; materialization attaches
117
- * the registered predicates (revalidate-registry.ts). Absent when no
118
- * loader declares one. */
119
- readonly loaderRevalidate?: readonly (readonly string[])[];
116
+ /** Per loader (aligned with loaderIds), its revalidate() arguments in
117
+ * declaration order: a hoisted predicate's key, whose registration
118
+ * materialization attaches (revalidate-registry.ts), or a boolean
119
+ * literal. Absent when no loader declares one. */
120
+ readonly loaderRevalidate?: readonly (readonly (string | boolean)[])[];
120
121
  /** One per leading loader its layout()s declare
121
122
  * (ClientUrlRouteRecord.layoutLoaders): the record's ordinal in the
122
123
  * definition. Two routes whose loader at an index carries the same ordinal
@@ -137,6 +138,10 @@ export interface ClientUrlProjectionIntercept {
137
138
  readonly targetName: string;
138
139
  readonly loaderIds: readonly string[];
139
140
  readonly hasLoading: boolean;
141
+ /** As on a route (ClientUrlProjectionRoute.deferrableLoaderIndices). */
142
+ readonly deferrableLoaderIndices?: readonly number[];
143
+ /** As on a route (ClientUrlProjectionRoute.loaderRevalidate). */
144
+ readonly loaderRevalidate?: readonly (readonly (string | boolean)[])[];
140
145
  }
141
146
 
142
147
  export interface ClientUrlProjection {
@@ -332,29 +337,10 @@ function serializeRoute(
332
337
  return loader.$$id;
333
338
  });
334
339
 
335
- const loaderRevalidate = route.loaders.map(({ loader, revalidate }) =>
336
- revalidate.flatMap((entry) => {
337
- if (isHoistedRevalidateRef(entry)) return [entry.$$rangoRevalidate];
338
- if (options.hoisted) {
339
- throw new Error(
340
- `clientUrls() revalidate() on loader "${loader.$$id}" of route ` +
341
- `"${route.pattern}" was not hoisted to the server: call the ` +
342
- "builder's revalidate() helper with an inline function or an " +
343
- "imported predicate.",
344
- );
345
- }
346
- return [];
347
- }),
348
- );
349
-
350
340
  const awaitedLoaderIndices = indicesWhere(
351
341
  route.loaders,
352
342
  ({ ssr }) => ssr === false,
353
343
  );
354
- const deferrableLoaderIndices = indicesWhere(
355
- route.loaders,
356
- ({ prefetch }) => prefetch === false,
357
- );
358
344
 
359
345
  const transition = serializeTransition(route);
360
346
  return Object.freeze({
@@ -367,6 +353,55 @@ function serializeRoute(
367
353
  ...(awaitedLoaderIndices.length > 0
368
354
  ? { awaitedLoaderIndices: Object.freeze(awaitedLoaderIndices) }
369
355
  : {}),
356
+ ...serializeLoaderOptions(
357
+ route.loaders,
358
+ `route "${route.pattern}"`,
359
+ options,
360
+ ),
361
+ ...(layoutLoaders.length > 0
362
+ ? { layoutLoaders: Object.freeze(layoutLoaders) }
363
+ : {}),
364
+ ...(transition ? { transition } : {}),
365
+ ...(typeof route.transition?.when === "function"
366
+ ? { transitionWhen: true as const }
367
+ : {}),
368
+ });
369
+ }
370
+
371
+ /**
372
+ * The loader options a route and an intercept project alike: the `prefetch:
373
+ * false` indices and each loader's revalidate() keys and boolean literals.
374
+ * Under `options.hoisted` a predicate the build left in place fails loudly.
375
+ */
376
+ function serializeLoaderOptions(
377
+ loaders: readonly ClientUrlLoaderRecord[],
378
+ owner: string,
379
+ options: SerializeClientUrlPatternsOptions,
380
+ ): Pick<
381
+ ClientUrlProjectionRoute,
382
+ "deferrableLoaderIndices" | "loaderRevalidate"
383
+ > {
384
+ const loaderRevalidate = loaders.map(({ loader, revalidate }) =>
385
+ revalidate.flatMap((entry): (string | boolean)[] => {
386
+ if (isHoistedRevalidateRef(entry)) return [entry.$$rangoRevalidate];
387
+ if (typeof entry === "boolean") return [entry];
388
+ if (options.hoisted) {
389
+ throw new Error(
390
+ `clientUrls() revalidate() on loader "${loader.$$id}" of ${owner} ` +
391
+ "was not hoisted to the server: call revalidate() (the builder's " +
392
+ 'helper, or the one "@rangojs/router/client" exports for a ' +
393
+ "component's .use) with an inline function, an imported " +
394
+ "predicate or a boolean.",
395
+ );
396
+ }
397
+ return [];
398
+ }),
399
+ );
400
+ const deferrableLoaderIndices = indicesWhere(
401
+ loaders,
402
+ ({ prefetch }) => prefetch === false,
403
+ );
404
+ return {
370
405
  ...(deferrableLoaderIndices.length > 0
371
406
  ? { deferrableLoaderIndices: Object.freeze(deferrableLoaderIndices) }
372
407
  : {}),
@@ -377,19 +412,13 @@ function serializeRoute(
377
412
  ),
378
413
  }
379
414
  : {}),
380
- ...(layoutLoaders.length > 0
381
- ? { layoutLoaders: Object.freeze(layoutLoaders) }
382
- : {}),
383
- ...(transition ? { transition } : {}),
384
- ...(typeof route.transition?.when === "function"
385
- ? { transitionWhen: true as const }
386
- : {}),
387
- });
415
+ };
388
416
  }
389
417
 
390
418
  function serializeIntercept(
391
419
  record: ClientUrlInterceptRecord,
392
420
  index: number,
421
+ options: SerializeClientUrlPatternsOptions,
393
422
  ): ClientUrlProjectionIntercept {
394
423
  const loaderIds = record.loaders.map(({ loader }, loaderIndex) => {
395
424
  if (typeof loader.$$id !== "string" || loader.$$id.length === 0) {
@@ -406,6 +435,11 @@ function serializeIntercept(
406
435
  targetName: record.targetName,
407
436
  loaderIds: Object.freeze(loaderIds),
408
437
  hasLoading: record.loading !== undefined,
438
+ ...serializeLoaderOptions(
439
+ record.loaders,
440
+ `intercept ".${record.targetName}"`,
441
+ options,
442
+ ),
409
443
  });
410
444
  }
411
445
 
@@ -420,7 +454,9 @@ export function serializeClientUrlPatterns(
420
454
  patterns.routes.map((route) => serializeRoute(route, options, ordinals)),
421
455
  ),
422
456
  intercepts: Object.freeze(
423
- (patterns.intercepts ?? []).map(serializeIntercept),
457
+ (patterns.intercepts ?? []).map((record, index) =>
458
+ serializeIntercept(record, index, options),
459
+ ),
424
460
  ),
425
461
  });
426
462
  }
@@ -536,28 +572,48 @@ export function clearClientUrlProjections(): void {
536
572
  clientUrlProjections.clear();
537
573
  }
538
574
 
575
+ /** The projected loaders of a route or an intercept. */
576
+ interface ProjectedLoaders {
577
+ readonly projected: Pick<
578
+ ClientUrlProjectionRoute,
579
+ | "loaderIds"
580
+ | "loaderRevalidate"
581
+ | "awaitedLoaderIndices"
582
+ | "deferrableLoaderIndices"
583
+ >;
584
+ /** Names the owner in an error: `route "/a"`, `intercept ".b"`. */
585
+ readonly label: string;
586
+ /** The owner's loader records in a definition object. */
587
+ readonly inline: (
588
+ patterns: ClientUrlPatterns,
589
+ ) => readonly ClientUrlLoaderRecord[] | undefined;
590
+ }
591
+
539
592
  /**
540
- * The revalidate() predicates of one materialized loader. A definition
541
- * object (unit tests, the testing primitives) carries its functions. A
542
- * client reference resolves the projection's content keys in the registry
543
- * the group's hoisted module filled when it loaded; a key with no
544
- * registration fails loudly here, at materialization, never at a request.
593
+ * The revalidate() predicates of one materialized loader, a boolean as its
594
+ * constant predicate. A definition object (unit tests, the testing
595
+ * primitives) carries its functions. A client reference resolves the
596
+ * projection's content keys in the registry the group's hoisted module
597
+ * filled when it loaded; a key with no registration fails loudly here, at
598
+ * materialization, never at a request.
545
599
  */
546
600
  function loaderRevalidators(
547
601
  reference: ClientUrlDefinitionSource,
548
- route: ClientUrlProjectionRoute,
602
+ { projected, label, inline }: ProjectedLoaders,
549
603
  loaderIndex: number,
550
604
  ): ShouldRevalidateFn<any, any>[] {
551
605
  const fromRegistry = (key: string): ShouldRevalidateFn<any, any> => {
552
606
  if (!getClientUrlRevalidator(key)) {
553
607
  const id = isClientUrlReference(reference) ? reference.$$id : "(inline)";
554
608
  throw new Error(
555
- `clientUrls() revalidate() on loader "${route.loaderIds[loaderIndex]}" ` +
556
- `of route "${route.pattern}" in "${id}" has no server ` +
609
+ `clientUrls() revalidate() on loader "${projected.loaderIds[loaderIndex]}" ` +
610
+ `of ${label} in "${id}" has no server ` +
557
611
  `registration (key "${key}"). The rango Vite plugin registers ` +
558
612
  "hoisted predicates when the group's module loads in the RSC " +
559
- "environment; a transform that changed the predicate's syntax " +
560
- "in one environment only also causes this.",
613
+ "environment, with those of the modules route discovery found " +
614
+ "its keys in (a component's .use). An argument that evaluated to " +
615
+ "neither a function nor a boolean there, or a transform that " +
616
+ "changed its syntax in one environment only, causes this.",
561
617
  );
562
618
  }
563
619
  // Looked up per call: a dev re-evaluation of the hoisted module
@@ -565,14 +621,17 @@ function loaderRevalidators(
565
621
  return (args) => getClientUrlRevalidator(key)!(args);
566
622
  };
567
623
  if (isClientUrlPatterns(reference)) {
568
- const record = reference.routes.find(({ id }) => id === route.id);
569
- return (record?.loaders[loaderIndex]?.revalidate ?? []).map((entry) =>
624
+ return (inline(reference)?.[loaderIndex]?.revalidate ?? []).map((entry) =>
570
625
  isHoistedRevalidateRef(entry)
571
626
  ? fromRegistry(entry.$$rangoRevalidate)
572
- : entry,
627
+ : toShouldRevalidateFn(entry),
573
628
  );
574
629
  }
575
- return (route.loaderRevalidate?.[loaderIndex] ?? []).map(fromRegistry);
630
+ return (projected.loaderRevalidate?.[loaderIndex] ?? []).map((entry) =>
631
+ toShouldRevalidateFn(
632
+ typeof entry === "boolean" ? entry : fromRegistry(entry),
633
+ ),
634
+ );
576
635
  }
577
636
 
578
637
  function createLoaderStub(id: string): LoaderDefinition<unknown> {
@@ -755,6 +814,26 @@ function materializeRouteItems(
755
814
  return foreign.length ? { foreign } : {};
756
815
  };
757
816
 
817
+ // The loader() items of a route's or an intercept's projected loaders: each
818
+ // stub with its `ssr: false` and `prefetch: false` flags and its
819
+ // revalidate() predicates.
820
+ const materializeLoaders = (owner: ProjectedLoaders) =>
821
+ owner.projected.loaderIds.map((id, loaderIndex) => {
822
+ const revalidators = loaderRevalidators(reference, owner, loaderIndex);
823
+ return loader(
824
+ createLoaderStub(id),
825
+ {
826
+ ...(owner.projected.awaitedLoaderIndices?.includes(loaderIndex) && {
827
+ ssr: false,
828
+ }),
829
+ ...(owner.projected.deferrableLoaderIndices?.includes(
830
+ loaderIndex,
831
+ ) && { prefetch: false }),
832
+ },
833
+ () => revalidators.map((fn) => revalidate(fn)),
834
+ );
835
+ });
836
+
758
837
  // Helper calls attach to the CURRENT ctx.parent as they execute, so these
759
838
  // builders must run where the items belong: at the module top level for the
760
839
  // flat (no-intercept) shape, or inside the group layout's children callback
@@ -803,24 +882,11 @@ function materializeRouteItems(
803
882
  }),
804
883
  materializedPathOptions(route, clientGroup),
805
884
  recordingCopies(routeIndex, () => [
806
- ...route.loaderIds.map((id, loaderIndex) => {
807
- const revalidators = loaderRevalidators(
808
- reference,
809
- route,
810
- loaderIndex,
811
- );
812
- return loader(
813
- createLoaderStub(id),
814
- {
815
- ...(route.awaitedLoaderIndices?.includes(loaderIndex) && {
816
- ssr: false,
817
- }),
818
- ...(route.deferrableLoaderIndices?.includes(loaderIndex) && {
819
- prefetch: false,
820
- }),
821
- },
822
- () => revalidators.map((fn) => revalidate(fn)),
823
- );
885
+ ...materializeLoaders({
886
+ projected: route,
887
+ label: `route "${route.pattern}"`,
888
+ inline: (patterns) =>
889
+ patterns.routes.find(({ id }) => id === route.id)?.loaders,
824
890
  }),
825
891
  ...(route.hasLoading
826
892
  ? [
@@ -907,7 +973,12 @@ function materializeRouteItems(
907
973
  }),
908
974
  { when: isInGroupOrigin },
909
975
  () => [
910
- ...record.loaderIds.map((id) => loader(createLoaderStub(id))),
976
+ ...materializeLoaders({
977
+ projected: record,
978
+ label: `intercept ".${record.targetName}"`,
979
+ inline: (patterns) =>
980
+ patterns.intercepts?.[interceptIndex]?.loaders,
981
+ }),
911
982
  ...(record.hasLoading
912
983
  ? [
913
984
  loading(
@@ -28,6 +28,18 @@ export type ClientUrlItems = readonly ClientUrlItemInput[];
28
28
 
29
29
  export type ClientUrlUse = () => ClientUrlItems;
30
30
 
31
+ /**
32
+ * A component path(), layout() or intercept() mounts. Its optional `use`
33
+ * carries its own items, as a server handler's `.use` does, built with the
34
+ * helpers `@rangojs/router/client` exports: they come before the mount
35
+ * site's explicit items (an explicit loading() or transition() wins, loaders
36
+ * accumulate). A page component may carry loader(), loading() and
37
+ * transition(); a layout or intercept component loader() and loading().
38
+ */
39
+ export type ClientUrlComponent = ComponentType & {
40
+ readonly use?: ClientUrlUse;
41
+ };
42
+
31
43
  export type ClientPathOptions<
32
44
  TName extends string = string,
33
45
  TSearch extends SearchSchema = SearchSchema,
@@ -42,13 +54,13 @@ export type ClientPathFn = <
42
54
  const TSearch extends SearchSchema = {},
43
55
  >(
44
56
  pattern: TPattern,
45
- component: ComponentType,
57
+ component: ClientUrlComponent,
46
58
  optionsOrUse?: ClientPathOptions<TName, TSearch> | ClientUrlUse,
47
59
  use?: ClientUrlUse,
48
60
  ) => ClientUrlItem & TypedRouteItem<TName, TPattern, unknown, TSearch>;
49
61
 
50
62
  export type ClientLayoutFn = <const TItems extends ClientUrlItems>(
51
- component: ComponentType,
63
+ component: ClientUrlComponent,
52
64
  children: () => TItems,
53
65
  ) => ClientUrlItem & TypedLayoutItem<ExtractRoutes<TItems>>;
54
66
 
@@ -57,11 +69,13 @@ export interface ClientUrlLoaderRecord {
57
69
  /**
58
70
  * Per-loader revalidate() predicates, run on the server; empty = the
59
71
  * router's defaults. Under the rango Vite plugin the browser and SSR copies
60
- * hold the marker the build left in place of each hoisted predicate.
72
+ * hold the marker the build left in place of each hoisted predicate; a
73
+ * boolean literal stays in place.
61
74
  */
62
75
  readonly revalidate: readonly (
63
76
  | ShouldRevalidateFn<any, DefaultEnv>
64
77
  | HoistedRevalidateRef
78
+ | boolean
65
79
  )[];
66
80
  /**
67
81
  * loader(Def, { ssr: false }): document renders await this loader before
@@ -71,11 +85,10 @@ export interface ClientUrlLoaderRecord {
71
85
  */
72
86
  readonly ssr?: false;
73
87
  /**
74
- * loader(Def, { prefetch: false }): a prefetch of the route defers this
75
- * loader and the click that adopts it runs it in its fill request (see
76
- * {@link LoaderOptions}). Projected onto the materialized stub. Inside
77
- * intercept() it is not recorded: intercept loaders ignore the flag, as in
78
- * urls().
88
+ * loader(Def, { prefetch: false }): a prefetch of the route, or of an
89
+ * intercept's target from its source, defers this loader and the click
90
+ * that adopts it runs it in its fill request (see {@link LoaderOptions}).
91
+ * Projected onto the materialized stub.
79
92
  */
80
93
  readonly prefetch?: false;
81
94
  }
@@ -126,7 +139,7 @@ export interface ClientUrlRouteRecord {
126
139
 
127
140
  /**
128
141
  * A restricted intercept declared inside clientUrls(). Compared to the server
129
- * intercept() there is no `when` selector, no middleware, and the target must
142
+ * intercept() there is no `when` selector, and the target must
130
143
  * be a dot-local NAMED route in the same definition — every field is
131
144
  * JSON-projectable, which is what makes the declaration legal in a
132
145
  * "use client" module. `targetName` is stored bare (no leading dot).
@@ -160,8 +173,8 @@ export interface ClientUrlHelpers {
160
173
  * the destination at once, commits the adopted prefetch with the other
161
174
  * loaders' data, and runs only the flagged loaders in its one fill
162
175
  * request. A `layout()` loader with the flag is flagged on every route
163
- * below it. Inside intercept() the flag is accepted and ignored, as in
164
- * urls().
176
+ * below it. Inside intercept() it defers the loader in a prefetch of the
177
+ * intercept from its source, as in urls().
165
178
  */
166
179
  readonly loader: <TData>(
167
180
  definition: LoaderDefinition<TData>,
@@ -176,10 +189,11 @@ export interface ClientUrlHelpers {
176
189
  * names, ...): the build moves it out of this "use client" module. It may
177
190
  * reference imports and globals only; a binding of this module, `this`,
178
191
  * `import()`, a browser global, an `async` function or an import from a
179
- * "use client" module fails the build with its file and line.
192
+ * "use client" module fails the build with its file and line. A boolean
193
+ * is shorthand for `() => true` / `() => false`.
180
194
  */
181
195
  readonly revalidate: (
182
- fn: ShouldRevalidateFn<any, DefaultEnv>,
196
+ fn: ShouldRevalidateFn<any, DefaultEnv> | boolean,
183
197
  ) => ClientUrlItem;
184
198
  /**
185
199
  * Declare an intercept for a named route in THIS definition. The target is
@@ -190,7 +204,7 @@ export interface ClientUrlHelpers {
190
204
  readonly intercept: (
191
205
  slotName: `@${string}`,
192
206
  targetName: `.${string}`,
193
- component: ComponentType,
207
+ component: ClientUrlComponent,
194
208
  use?: ClientUrlUse,
195
209
  ) => ClientUrlItem;
196
210
  /**
@@ -198,7 +212,8 @@ export interface ClientUrlHelpers {
198
212
  * holds previous content instead of re-streaming the loading() fallback,
199
213
  * and on experimental React the config's ViewTransition classes apply.
200
214
  * Data-only — no `when` gate (server-executed; declare it in the server
201
- * tree). Valid inside a path() use callback only.
215
+ * tree). Valid inside a path() use callback, or a page component's
216
+ * `.use`.
202
217
  */
203
218
  readonly transition: (config: ClientTransitionConfig) => ClientUrlItem;
204
219
  }
@@ -36,10 +36,20 @@ export {
36
36
 
37
37
  export { createLoader } from "./route-definition.js";
38
38
 
39
- export { clientUrls } from "./client-urls/client-urls.js";
39
+ // A module that is not "use client" and declares a component's `.use` can
40
+ // evaluate in the RSC realm too.
41
+ export {
42
+ clientUrls,
43
+ loader,
44
+ loading,
45
+ revalidate,
46
+ transition,
47
+ } from "./client-urls/client-urls.js";
40
48
  export type {
49
+ ClientUrlComponent,
41
50
  ClientUrlPatterns,
42
51
  ClientUrlRouteRecord,
52
+ ClientUrlUse,
43
53
  } from "./client-urls/client-urls.js";
44
54
 
45
55
  // "use client" hooks the default ./client entry exports. They are client
package/src/client.tsx CHANGED
@@ -309,10 +309,18 @@ export {
309
309
  type UseLoaderOptions,
310
310
  } from "./use-loader.js";
311
311
 
312
- export { clientUrls } from "./client-urls/client-urls.js";
312
+ export {
313
+ clientUrls,
314
+ loader,
315
+ loading,
316
+ revalidate,
317
+ transition,
318
+ } from "./client-urls/client-urls.js";
313
319
  export type {
320
+ ClientUrlComponent,
314
321
  ClientUrlPatterns,
315
322
  ClientUrlRouteRecord,
323
+ ClientUrlUse,
316
324
  } from "./client-urls/client-urls.js";
317
325
 
318
326
  /**
@@ -1,6 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { createElement } from "react";
3
- import { isLoaderDataResult } from "./types.js";
3
+ import {
4
+ isLoaderDataResult,
5
+ type ErrorInfo,
6
+ type ResolvedSegment,
7
+ } from "./types.js";
8
+ import { isPlaceholder } from "./types/segments.js";
4
9
  import { LoaderRedirect } from "./loader-redirect.js";
5
10
 
6
11
  /**
@@ -15,13 +20,13 @@ import { LoaderRedirect } from "./loader-redirect.js";
15
20
  * Only decodeLoaderEntry (the read-site path — client components, so the
16
21
  * throw lands in the router-owned boundary; during document SSR, Fizz emits
17
22
  * the Suspense fallback and replays the throw at hydration) throws these.
18
- * decodeLoaderResults runs during the SERVER tree build on forceAwait/action
19
- * lanes, where a throw would collapse the whole payload — it routes signals
20
- * through the errorFallback slot instead (same visual: the slot replaces the
21
- * children under OutletProvider). The document lane's `{ ssr: false }`
22
- * results take the same non-throwing route (segment-system
23
- * buildLoaderStreams): they are settled before the tree is built, and the
24
- * read-site throw would land in the Fizz shell with no boundary to absorb it.
23
+ * decodeLoaderResults runs during the tree build on settled results, where a
24
+ * throw would collapse the whole payload — it routes signals through the
25
+ * errorFallback slot instead (same visual: the slot replaces the children
26
+ * under OutletProvider). The document lane's `{ ssr: false }` results take
27
+ * this route (segment-system buildLoaderStreams): they are settled before the
28
+ * tree is built, and the read-site throw would land in the Fizz shell with no
29
+ * boundary to absorb it.
25
30
  */
26
31
  export const LOADER_NOT_FOUND_FALLBACK: unique symbol = Symbol(
27
32
  "rango.loaderNotFound",
@@ -65,6 +70,52 @@ function signalFallback(result: any): ReactNode | undefined {
65
70
  return undefined;
66
71
  }
67
72
 
73
+ /**
74
+ * A failed loader entry's error, rebuilt with the ErrorInfo identity
75
+ * (name/stack/code/cause) instead of a stripped generic Error.
76
+ */
77
+ export function loaderResultError(result: { error: ErrorInfo }): Error {
78
+ const info = result.error;
79
+ const err = new Error(
80
+ info.message,
81
+ info.cause !== undefined ? { cause: info.cause } : undefined,
82
+ );
83
+ if (info.name) err.name = info.name;
84
+ if (info.stack) err.stack = info.stack;
85
+ if (info.code !== undefined) (err as { code?: string }).code = info.code;
86
+ return err;
87
+ }
88
+
89
+ /** A loader that threw: not ok, and not a notFound()/redirect() signal. */
90
+ export function isLoaderError(
91
+ result: unknown,
92
+ ): result is { error: ErrorInfo; fallback: ReactNode | null } {
93
+ return (
94
+ isLoaderDataResult(result) &&
95
+ !result.ok &&
96
+ !result.notFound &&
97
+ !result.redirect
98
+ );
99
+ }
100
+
101
+ /**
102
+ * The error of the first loader that threw while it re-ran in an action's
103
+ * revalidation, for useAction()'s `revalidationError`. The loader itself
104
+ * throws at its read (segment-system buildLoaderStreams), and the action
105
+ * resolves. A stale revalidation rejects with it instead and commits nothing
106
+ * (partial-update.ts). A notFound()/redirect() is a signal, not an error.
107
+ */
108
+ export async function revalidationErrorOf(
109
+ segments: ResolvedSegment[] = [],
110
+ ): Promise<Error | undefined> {
111
+ for (const s of segments) {
112
+ if (s.type !== "loader" || isPlaceholder(s)) continue;
113
+ const entry = await s.loaderData;
114
+ if (isLoaderError(entry)) return loaderResultError(entry);
115
+ }
116
+ return undefined;
117
+ }
118
+
68
119
  // Shared by segment-system (server) and LoaderResolver (client) so the
69
120
  // legacy/ok/error-fallback/throw decode of resolved loader values lives once.
70
121
  // Last failing loader wins errorFallback; an error without a fallback throws.
@@ -90,10 +141,9 @@ export function decodeLoaderResults(
90
141
  }
91
142
 
92
143
  // Authority signals take the errorFallback slot (this decode runs during
93
- // the server tree build on forceAwait/action lanes — throwing here would
94
- // collapse the payload). A signal outranks a plain error fallback and
95
- // stops the scan: notFound renders the server-rendered 404 UI, redirect
96
- // mounts LoaderRedirect which navigates.
144
+ // the tree build — throwing here would collapse the payload). A signal
145
+ // outranks a plain error fallback and stops the scan: notFound renders the
146
+ // server-rendered 404 UI, redirect mounts LoaderRedirect which navigates.
97
147
  const signal = signalFallback(result);
98
148
  if (signal !== undefined) {
99
149
  return { loaderData, errorFallback: signal };
@@ -108,17 +158,7 @@ export function decodeLoaderResults(
108
158
  if (result.fallback != null) {
109
159
  errorFallback = result.fallback;
110
160
  } else {
111
- // No boundary: rethrow preserving the ErrorInfo identity (name/stack/
112
- // code/cause) instead of a stripped generic Error.
113
- const info = result.error;
114
- const err = new Error(
115
- info.message,
116
- info.cause !== undefined ? { cause: info.cause } : undefined,
117
- );
118
- if (info.name) err.name = info.name;
119
- if (info.stack) err.stack = info.stack;
120
- if (info.code !== undefined) (err as { code?: string }).code = info.code;
121
- throw err;
161
+ throw loaderResultError(result);
122
162
  }
123
163
  }
124
164
 
@@ -153,14 +193,7 @@ export function decodeLoaderEntry(result: any): any {
153
193
  return result.data;
154
194
  }
155
195
  throwSignal(result);
156
- const info = result.error;
157
- const err = new Error(
158
- info.message,
159
- info.cause !== undefined ? { cause: info.cause } : undefined,
160
- );
161
- if (info.name) err.name = info.name;
162
- if (info.stack) err.stack = info.stack;
163
- if (info.code !== undefined) (err as { code?: string }).code = info.code;
196
+ const err = loaderResultError(result);
164
197
  if (result.fallback != null) {
165
198
  (err as any)[LOADER_ERROR_FALLBACK] = result.fallback;
166
199
  }
@@ -274,7 +274,9 @@ declare global {
274
274
  * Type-safe href function for client-side use
275
275
  *
276
276
  * Without mount: identity function, validates absolute paths at compile time.
277
- * With mount: prepends mount path, for use with useMount() inside include() scopes.
277
+ * With mount: prepends the mount as given. `useMount()` under a parameterized
278
+ * include returns its pattern (`/:accountId`), which this does not fill: use
279
+ * `useHref()` there, which substitutes the mount params.
278
280
  *
279
281
  * @param path - A valid path matching one of the registered route patterns
280
282
  * @param mount - Optional mount prefix from useMount() for include-scoped paths
package/src/index.rsc.ts CHANGED
@@ -124,6 +124,7 @@ export {
124
124
  rewrite,
125
125
  type Rewrite,
126
126
  type RewriteFunction,
127
+ type RewriteShell,
127
128
  } from "./route-definition/rewrite.js";
128
129
  export { RewriteLoopError } from "./errors.js";
129
130
 
package/src/index.ts CHANGED
@@ -134,6 +134,14 @@ function serverOnlyStubError(name: string): Error {
134
134
  );
135
135
  }
136
136
 
137
+ /** A stub whose name `@rangojs/router/client` exports for clientUrls(). */
138
+ function clientUrlsHelperStubError(name: string): Error {
139
+ return new Error(
140
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
141
+ `In a "use client" module (a clientUrls() component's .use), import ${name}() from "@rangojs/router/client".`,
142
+ );
143
+ }
144
+
137
145
  /**
138
146
  * Error-throwing stub for server-only `urls` function.
139
147
  */
@@ -276,7 +284,7 @@ export function middleware(): never {
276
284
  throw serverOnlyStubError("middleware");
277
285
  }
278
286
  export function revalidate(): never {
279
- throw serverOnlyStubError("revalidate");
287
+ throw clientUrlsHelperStubError("revalidate");
280
288
  }
281
289
  // Cache tag APIs are server-only (real implementations in index.rsc.ts). These
282
290
  // stubs keep the named-export shape identical under the default/non-react-server
@@ -291,10 +299,10 @@ export function revalidateTag(): never {
291
299
  throw serverOnlyStubError("revalidateTag");
292
300
  }
293
301
  export function loader(): never {
294
- throw serverOnlyStubError("loader");
302
+ throw clientUrlsHelperStubError("loader");
295
303
  }
296
304
  export function loading(): never {
297
- throw serverOnlyStubError("loading");
305
+ throw clientUrlsHelperStubError("loading");
298
306
  }
299
307
  export function parallel(): never {
300
308
  throw serverOnlyStubError("parallel");
@@ -309,7 +317,7 @@ export function notFoundBoundary(): never {
309
317
  throw serverOnlyStubError("notFoundBoundary");
310
318
  }
311
319
  export function transition(): never {
312
- throw serverOnlyStubError("transition");
320
+ throw clientUrlsHelperStubError("transition");
313
321
  }
314
322
 
315
323
  // Request context type (safe for client)