@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -139,6 +139,37 @@ function resolveFromRouter(spec: string): string | undefined {
139
139
  const rsdServerEdgePath: string | undefined =
140
140
  resolveFromRouter(RSD_SERVER_EDGE_SPEC);
141
141
 
142
+ /** Marks the import of a clientUrls() module's untransformed source. */
143
+ const CLIENT_URLS_SOURCE_QUERY = "rango-client-urls-source";
144
+
145
+ /** `@rangojs/router/client` under the react-server condition. */
146
+ const CLIENT_ENTRY_RSC_PATH: string = here("src/client.rsc.tsx").replaceAll(
147
+ "\\",
148
+ "/",
149
+ );
150
+
151
+ /**
152
+ * Imported by absolute path so it shares the registry instance the router
153
+ * source under test materializes include() from.
154
+ */
155
+ const CLIENT_URLS_INSTALLER_PATH: string = here(
156
+ "src/testing/internal/client-urls-projection.ts",
157
+ ).replaceAll("\\", "/");
158
+
159
+ /**
160
+ * Appended to a transformed `"use client"` clientUrls() module: install the
161
+ * projection of its own source under its default export's reference id
162
+ * (`<id>#default`, as registerClientReference composes it).
163
+ */
164
+ function clientUrlsProjectionInstall(id: string): string {
165
+ const source = `${id}${id.includes("?") ? "&" : "?"}${CLIENT_URLS_SOURCE_QUERY}`;
166
+ return (
167
+ `\nimport * as $$RangoClientUrlsSource from ${JSON.stringify(source)};\n` +
168
+ `import { installClientUrlsProjection as $$rangoInstallClientUrlsProjection } from ${JSON.stringify(CLIENT_URLS_INSTALLER_PATH)};\n` +
169
+ `$$rangoInstallClientUrlsProjection(${JSON.stringify(`${id}#default`)}, $$RangoClientUrlsSource);\n`
170
+ );
171
+ }
172
+
142
173
  /**
143
174
  * Build the `resolve.alias` entries a consumer's node/DOM Vitest project needs to
144
175
  * import a real @rangojs/router app's router/loaders/middleware. Spread into a
@@ -241,6 +272,10 @@ export function rangoTestConfig(
241
272
  /** A minimal Vite plugin shape (avoids a hard dependency on Vite's types). */
242
273
  interface FlightTransformPlugin {
243
274
  name: string;
275
+ resolveId: {
276
+ order: "pre";
277
+ handler(source: string, importer: string | undefined): string | undefined;
278
+ };
244
279
  transform(
245
280
  code: string,
246
281
  id: string,
@@ -298,12 +333,39 @@ interface FlightTransformPlugin {
298
333
  * by the module's absolute path (the boundary id), the export name becoming the
299
334
  * boundary name. Modules without the directive (server components) are untouched,
300
335
  * so `renderToFlightString` of pure leaf trees is unaffected.
336
+ *
337
+ * A `"use client"` module whose default export is a `clientUrls()` definition
338
+ * also installs its server projection, as the rango plugin's discovery does for
339
+ * an app: the module imports its own source without this transform and projects
340
+ * it under its client reference. A router that mounts the group
341
+ * (`include("/shop", shopUrls)` or `.routes(shopUrls)`) then serves its routes
342
+ * through `serveShellRequest` and `dispatch` with no setup.
301
343
  */
302
344
  export function rangoUseClientTransform(): FlightTransformPlugin {
303
345
  return {
304
346
  name: "rango:testing-use-client",
347
+ resolveId: {
348
+ // Before Vite's resolver, which resolves the bare specifier first.
349
+ order: "pre",
350
+ handler(source, importer) {
351
+ // A clientUrls() module's source resolves the router's client entry
352
+ // as the RSC environment does (the react-server condition): this
353
+ // project resolves it to client.tsx, a "use client" module whose
354
+ // clientUrls is a client reference here. Both entries export the
355
+ // same clientUrls.
356
+ if (
357
+ source === "@rangojs/router/client" &&
358
+ importer?.includes(CLIENT_URLS_SOURCE_QUERY)
359
+ ) {
360
+ return CLIENT_ENTRY_RSC_PATH;
361
+ }
362
+ return undefined;
363
+ },
364
+ },
305
365
  async transform(code, id) {
306
366
  if (id.includes("/node_modules/")) return undefined;
367
+ // A clientUrls() module's own source, imported by its transformed copy.
368
+ if (id.includes(CLIENT_URLS_SOURCE_QUERY)) return undefined;
307
369
  // Fast path: only parse modules that mention the directive.
308
370
  if (!code.includes("use client")) return undefined;
309
371
  const { parseAstAsync } = await import("vite");
@@ -329,7 +391,7 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
329
391
  `${JSON.stringify(id)}, ${JSON.stringify(name)})`,
330
392
  });
331
393
  if (!result) return undefined;
332
- const { output } = result;
394
+ const { output, exportNames } = result;
333
395
  // Absolute file URL, not the bare specifier: the consumer's "use client"
334
396
  // module cannot resolve @vitejs/plugin-rsc (it is a router dependency).
335
397
  const rsdHref = rsdServerEdgePath
@@ -338,6 +400,10 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
338
400
  output.prepend(
339
401
  `import * as $$RangoRSD from ${JSON.stringify(rsdHref)};\n`,
340
402
  );
403
+ // The modules discovery records (recordClientUrlsModule).
404
+ if (code.includes("clientUrls(") && exportNames.includes("default")) {
405
+ output.append(clientUrlsProjectionInstall(id));
406
+ }
341
407
  return {
342
408
  code: output.toString(),
343
409
  map: output.generateMap({ hires: true }),
@@ -24,3 +24,14 @@ export const SETTLED_THENABLE: Promise<undefined> = Object.assign(
24
24
  Promise.resolve(undefined),
25
25
  { status: "fulfilled", value: undefined, _debugInfo: [] },
26
26
  );
27
+
28
+ /**
29
+ * The value React reads from `value` without suspending: `value` itself when
30
+ * it is not a promise, a promise's `value` once it carries React's or Flight's
31
+ * `status: "fulfilled"`. Undefined while it is pending or rejected.
32
+ */
33
+ export function settledValue<T>(value: T | Promise<T>): T | undefined {
34
+ if (!(value instanceof Promise)) return value;
35
+ const thenable = value as Promise<T> & { status?: string; value?: T };
36
+ return thenable.status === "fulfilled" ? thenable.value : undefined;
37
+ }
@@ -104,6 +104,18 @@ export function isLoaderDataResult(value: unknown): value is LoaderDataResult {
104
104
  );
105
105
  }
106
106
 
107
+ /**
108
+ * A loading skeleton is renderable only when it is a real ReactNode value;
109
+ * `false` counts as "not renderable". This is the three-term gate. The
110
+ * distinct two-term gate at the LoaderBoundary site (segment-system.tsx)
111
+ * deliberately treats `false` as "create a boundary without a
112
+ * RouteContentWrapper" (tree-structure.md), so it must NOT use this helper.
113
+ * Leaf module: shared by client (segment-system) and server (DSL, resolution).
114
+ */
115
+ export function isRenderableLoading(loading: unknown): boolean {
116
+ return loading !== undefined && loading !== null && loading !== false;
117
+ }
118
+
107
119
  export interface NotFoundInfo {
108
120
  message: string;
109
121
  segmentId: string;
@@ -492,6 +492,20 @@ export type InternalHandlerContext<
492
492
  * setupLoaderAccess.
493
493
  */
494
494
  _bindLoaderCacheTags?: (loaderId: string, tags: Set<string>) => void;
495
+ /**
496
+ * @internal This match's `prefetch: false` plan: what a prefetch defers,
497
+ * whether the request is a fill, and the loaders that cannot call
498
+ * ctx.rendered(). Set by match-api.ts when the tree declares a flag or the
499
+ * request is a fill. See router/segment-resolution/prefetch-deferral.ts.
500
+ */
501
+ _prefetchDeferral?: import("../router/segment-resolution/prefetch-deferral.js").PrefetchDeferral;
502
+ /**
503
+ * @internal Whether a loader was started in this request: a run of it (a
504
+ * handler's or another loader's ctx.use()) or its cache() binding. A
505
+ * prefetch delivers such a loader instead of deferring it. See
506
+ * setupLoaderAccess.
507
+ */
508
+ _loaderStarted?: (loaderId: string) => boolean;
495
509
  };
496
510
 
497
511
  /**
@@ -200,6 +200,36 @@ export type LoaderFn<
200
200
  */
201
201
  export type LoaderOptions = {
202
202
  ssr?: boolean;
203
+ /**
204
+ * `prefetch: false` keeps this loader out of prefetches. A `<Link>`
205
+ * prefetch (a partial request with `X-Rango-Prefetch`) does not execute it;
206
+ * the payload marks it deferred. A document request and a navigation with
207
+ * no prefetch to adopt run it as usual.
208
+ *
209
+ * A deferred loader never blocks the navigation that adopts the prefetch:
210
+ * it behaves like a loader that is still streaming. Its own `<Suspense>`
211
+ * shows at once; if the nearest boundary is the route's `loading()`, that
212
+ * shows; with no boundary React holds the page until the fill returns. The
213
+ * fill is the one follow-up request that runs the loader.
214
+ *
215
+ * The fill reads through the loader's own `cache()` and any `"use cache"`
216
+ * function it calls. A prefetch reads neither, by intent: looking would
217
+ * mean running the loader on a miss.
218
+ *
219
+ * The flag applies only to a segment the client does not have yet. A
220
+ * loader the client already holds (the same route with new params, say)
221
+ * runs in the prefetch when it revalidates, exactly as without the flag:
222
+ * a click that adopts a prefetch is never worse than the same click with
223
+ * no prefetch at all.
224
+ *
225
+ * Limits: a handler that awaits the loader with `ctx.use()` makes it run in
226
+ * the prefetch anyway; the loader cannot call `ctx.rendered()` (it throws
227
+ * on every request); on a `ppr` route the flag is ignored for an
228
+ * `ssr: false` loader (the bake lane), with a dev warning. In the fill
229
+ * request only deferred work and middleware run, so values a held handler
230
+ * would have set with `ctx.set()` are not visible.
231
+ */
232
+ prefetch?: boolean;
203
233
  };
204
234
 
205
235
  /**
@@ -63,6 +63,14 @@ export interface RequestScope<TEnv = DefaultEnv> {
63
63
  */
64
64
  originalUrl: URL;
65
65
 
66
+ /**
67
+ * The visitor's URL (the address bar, search included) when middleware
68
+ * rewrote this request with `throw rewrite()`; `undefined` otherwise. `url`,
69
+ * `pathname` and `searchParams` are the rewrite target's. After several
70
+ * rewrites this is still the first URL. Each context gets its own copy.
71
+ */
72
+ rewrittenFrom?: URL;
73
+
66
74
  /** URL pathname (same as `url.pathname`). */
67
75
  pathname: string;
68
76
 
@@ -172,6 +172,15 @@ export interface ResolvedSegment {
172
172
  */
173
173
  awaitBeforeFlush?: true;
174
174
  parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
175
+ /**
176
+ * A prefetch skipped this segment's work (`prefetch: false`): a loader
177
+ * carries no `loaderData`, a layout/route/parallel unit carries
178
+ * `component: null` and its fallback. The browser shows the fallback and
179
+ * sends a fill request for it (browser/partial-update.ts); until that
180
+ * lands the segment is a placeholder, never a held copy. Only ever on a
181
+ * prefetch response: see docs/design/prefetch-false.md.
182
+ */
183
+ deferred?: true;
175
184
  // Intercept loader fields (for streaming loader data in parallel segments)
176
185
  loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
177
186
  loaderIds?: string[]; // IDs ($$id) of loaders for this segment
@@ -253,10 +262,12 @@ export interface MatchResult {
253
262
  /**
254
263
  * Every segment id whose handler actually ran on the server this request,
255
264
  * including ones with `component === null` that get filtered out of
256
- * `segments`/`diff` to avoid wasted bytes. Drives the client's handle-
257
- * cleanup pass — a slot that re-resolves and pushes nothing must clear
258
- * its previous handle bucket, but `diff` doesn't carry it because the
259
- * segment payload doesn't either. A superset of `diff`.
265
+ * `segments`/`diff` to avoid wasted bytes, and every loader segment that
266
+ * ran. Drives the client's handle merge (handles/pushed-by.ts) — a slot
267
+ * that re-resolves and pushes nothing must clear its previous handle
268
+ * bucket, but `diff` doesn't carry it because the segment payload doesn't
269
+ * either; a loader that ran replaces its pushes, one that did not keeps
270
+ * them. A superset of `diff`.
260
271
  */
261
272
  resolvedIds: string[];
262
273
  /**
@@ -304,3 +315,14 @@ export interface MatchResult {
304
315
  params: Record<string, string>;
305
316
  }>;
306
317
  }
318
+
319
+ /**
320
+ * A `prefetch: false` placeholder (ResolvedSegment.deferred): a segment whose
321
+ * work a prefetch skipped, waiting for its fill. Never a held copy of the
322
+ * segment it stands for.
323
+ */
324
+ export function isPlaceholder(
325
+ segment: Pick<ResolvedSegment, "deferred">,
326
+ ): boolean {
327
+ return segment.deferred === true;
328
+ }
@@ -30,7 +30,6 @@ import type {
30
30
  InterceptUseItem,
31
31
  LoaderUseItem,
32
32
  TypedCacheItem,
33
- TransitionItem,
34
33
  TypedTransitionItem,
35
34
  TypedRouteItem,
36
35
  TypedIncludeItem,
@@ -362,6 +361,12 @@ export type PathHelpers<TEnv> = {
362
361
  * flag BAKES: the loader executes at shell capture and its settled return
363
362
  * freezes into the shell (nested promises stay live holes) — the pre-flush
364
363
  * promise applied to the prelude.
364
+ *
365
+ * Pass `{ prefetch: false }` to keep the loader out of prefetches: a
366
+ * prefetch skips it and the click that adopts the prefetch fetches it in
367
+ * one follow-up request (see {@link LoaderOptions}). Only on a segment the
368
+ * client does not have yet: a loader the client already holds runs in the
369
+ * prefetch when it revalidates.
365
370
  */
366
371
  loader: <TData>(
367
372
  loaderDef: LoaderDefinition<TData>,
@@ -370,11 +375,25 @@ export type PathHelpers<TEnv> = {
370
375
  ) => LoaderItem;
371
376
 
372
377
  /**
373
- * Attach a loading component to the current route/layout
378
+ * Attach a loading component to the current route/layout.
379
+ *
380
+ * `{ ssr: false }` skips the fallback on document requests. `{ prefetch:
381
+ * false }` keeps what the fallback covers out of prefetches, when this
382
+ * entry's segment is new to the client: the entry's handler, its loaders,
383
+ * its orphan layouts and parallel slots and, for a layout, every deeper
384
+ * entry. The click that adopts the prefetch shows the fallback and fetches
385
+ * them in one follow-up request, which reads cache() and "use cache" as
386
+ * any request does (a prefetch reads neither for skipped work). A segment
387
+ * the client already holds is never deferred: a prefetch renders it as
388
+ * usual when it revalidates, and what is below it follows its own flags.
389
+ * To keep a child out of prefetches while moving inside a section, flag
390
+ * the child's own loading(). An entry whose handler output is stored
391
+ * (cache(), ppr, Prerender/Static) is served as usual and only its loaders
392
+ * are skipped.
374
393
  */
375
394
  loading: (
376
395
  component: ReactNode | (() => ReactNode),
377
- options?: { ssr?: boolean },
396
+ options?: { ssr?: boolean; prefetch?: boolean },
378
397
  ) => LoadingItem;
379
398
 
380
399
  /**
@@ -422,8 +441,11 @@ export type PathHelpers<TEnv> = {
422
441
  * the browser (never on the server); see TransitionWhenFn.
423
442
  */
424
443
  transition: {
425
- (): TransitionItem;
426
- (config: TransitionConfig): TransitionItem;
444
+ // Typed {} phantoms, not bare TransitionItem: a phantom-less item matches
445
+ // `TypedTransitionItem<infer R>` with R inferred as Record<string, any>,
446
+ // which disables route-name checking for the sibling map.
447
+ (): TypedTransitionItem<{}, {}>;
448
+ (config: TransitionConfig): TypedTransitionItem<{}, {}>;
427
449
  <const TChildren extends readonly (AllUseItems | readonly AllUseItems[])[]>(
428
450
  children: () => TChildren,
429
451
  ): TypedTransitionItem<
@@ -12,6 +12,11 @@ import type { LocalOnlyInclude, UnnamedRoute } from "./pattern-types.js";
12
12
  // Route Type Extraction Utilities
13
13
  // ============================================================================
14
14
 
15
+ /** Drops string index signatures, keeping literal and template keys. */
16
+ type StripStringIndex<T> = {
17
+ [K in keyof T as string extends K ? never : K]: T[K];
18
+ };
19
+
15
20
  /**
16
21
  * Prefix route names with a given prefix (e.g., "blog" + "post" = "blog.post")
17
22
  *
@@ -117,15 +122,21 @@ type ExtractRoutesFromItem<T> =
117
122
  : TUrlPrefix extends string
118
123
  ? PrefixPatterns<TRoutes, TUrlPrefix>
119
124
  : TRoutes
120
- : // TypedLayoutItem: extract child routes from phantom type
125
+ : // Phantom-less items (global transition/cache/layout, factories typed
126
+ // as a use item) match the Typed* shapes with the child map inferred
127
+ // as the Record<string, any> constraint; the string index signature
128
+ // turns route-name checking off for the whole map. Strip only the
129
+ // index and keep literal keys, so a loosely typed include() beside
130
+ // named routes still contributes those names.
131
+ // TypedLayoutItem: extract child routes from phantom type
121
132
  T extends TypedLayoutItem<infer TChildRoutes>
122
- ? TChildRoutes
133
+ ? StripStringIndex<TChildRoutes>
123
134
  : // TypedCacheItem: extract child routes from phantom type
124
135
  T extends TypedCacheItem<infer TChildRoutes>
125
- ? TChildRoutes
136
+ ? StripStringIndex<TChildRoutes>
126
137
  : // TypedTransitionItem: extract child routes from phantom type
127
138
  T extends TypedTransitionItem<infer TChildRoutes>
128
- ? TChildRoutes
139
+ ? StripStringIndex<TChildRoutes>
129
140
  : // Fallback (won't extract routes)
130
141
  {};
131
142
 
@@ -206,16 +217,25 @@ type ExtractResponsesFromItem<T> =
206
217
  ? TResponses
207
218
  : {}
208
219
  : T extends TypedLayoutItem<any, infer TChildResponses>
209
- ? TChildResponses extends Record<string, unknown>
210
- ? TChildResponses
220
+ ? StripStringIndex<TChildResponses> extends infer R extends Record<
221
+ string,
222
+ unknown
223
+ >
224
+ ? R
211
225
  : {}
212
226
  : T extends TypedCacheItem<any, infer TChildResponses>
213
- ? TChildResponses extends Record<string, unknown>
214
- ? TChildResponses
227
+ ? StripStringIndex<TChildResponses> extends infer R extends Record<
228
+ string,
229
+ unknown
230
+ >
231
+ ? R
215
232
  : {}
216
233
  : T extends TypedTransitionItem<any, infer TChildResponses>
217
- ? TChildResponses extends Record<string, unknown>
218
- ? TChildResponses
234
+ ? StripStringIndex<TChildResponses> extends infer R extends Record<
235
+ string,
236
+ unknown
237
+ >
238
+ ? R
219
239
  : {}
220
240
  : {};
221
241
 
@@ -20,6 +20,7 @@ import {
20
20
  type LoaderStore,
21
21
  } from "./loader-store.js";
22
22
  import { NavigationStoreContext } from "./browser/react/context.js";
23
+ import { Defer, readDefer } from "./browser/defer.js";
23
24
  import { decodeLoaderEntry } from "./decode-loader-results.js";
24
25
  import { warnAwaitedSsrSuspension } from "./ssr-suspension-warning.js";
25
26
  import { SETTLED_THENABLE } from "./thenable-status.js";
@@ -51,12 +52,14 @@ const NOT_FOUND = Symbol("not-found");
51
52
  * available value from a still-streaming per-loader promise. A pending stream
52
53
  * is `use()`d at the read site (implicit suspension); the promise identity is
53
54
  * stable per commit (it comes from the provider's memoized context value /
54
- * element props), which use() requires across replays.
55
+ * element props), which use() requires across replays. A `prefetch: false`
56
+ * hole is a Defer (browser/defer.ts), read by its rule: every reader of the
57
+ * loader reads the one defer.
55
58
  */
56
59
  type LoaderLookup =
57
60
  | typeof NOT_FOUND
58
61
  | { value: unknown }
59
- | { stream: Promise<unknown> };
62
+ | { stream: Promise<unknown> | Defer<unknown> };
60
63
 
61
64
  function lookupLoaderStreams(
62
65
  loaderStreams: Record<string, unknown> | undefined,
@@ -64,9 +67,9 @@ function lookupLoaderStreams(
64
67
  ): LoaderLookup {
65
68
  if (!loaderStreams || !(loaderId in loaderStreams)) return NOT_FOUND;
66
69
  const entry = loaderStreams[loaderId];
67
- // Within the loaderStreams channel a Promise always means "still streaming";
68
- // settled entries are undecoded result values, never promises.
69
- return entry instanceof Promise
70
+ // Within the loaderStreams channel a Promise or a Defer always means "still
71
+ // streaming"; settled entries are undecoded result values, never promises.
72
+ return entry instanceof Promise || entry instanceof Defer
70
73
  ? { stream: entry }
71
74
  : { value: decodeLoaderEntry(entry) };
72
75
  }
@@ -163,7 +166,7 @@ function useLoaderInternal<T>(
163
166
  const walk = useMemo((): {
164
167
  contextData: T | undefined;
165
168
  hasContextData: boolean;
166
- pendingStream?: Promise<unknown>;
169
+ pendingStream?: Promise<unknown> | Defer<unknown>;
167
170
  /** { ssr: false } loader ids seen on the chain up to (and including) the
168
171
  * level that owned the read — input for the dev SSR suspension warning. */
169
172
  awaitedLoaderIds?: string[];
@@ -217,18 +220,20 @@ function useLoaderInternal<T>(
217
220
  // every COMPLETED render.
218
221
  const { hasContextData } = walk;
219
222
  let contextData = walk.contextData;
223
+ const stream = walk.pendingStream;
220
224
  // Dev only (suspense-audit.ts): the stream this reader is handed. The test
221
225
  // is a build-time constant, so the hook is called on every render or never.
222
226
  if (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT) {
223
- useReadAudit(loader.$$id, walk.pendingStream);
224
- }
225
- if (walk.pendingStream) {
226
- warnAwaitedSsrSuspension(
227
+ useReadAudit(
227
228
  loader.$$id,
228
- walk.awaitedLoaderIds,
229
- walk.pendingStream,
229
+ stream instanceof Defer ? stream.promise : stream,
230
230
  );
231
- contextData = decodeLoaderEntry(use(walk.pendingStream)) as T;
231
+ }
232
+ if (stream instanceof Defer) {
233
+ contextData = decodeLoaderEntry(readDefer(stream)) as T;
234
+ } else if (stream) {
235
+ warnAwaitedSsrSuspension(loader.$$id, walk.awaitedLoaderIds, stream);
236
+ contextData = decodeLoaderEntry(use(stream)) as T;
232
237
  } else if (hasContextData) {
233
238
  // React logs a render that finishes without use() at a place whose reader
234
239
  // suspended in use() on mount, so every read with route context calls it.
@@ -2,10 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { normalizePath, parseAst } from "vite";
4
4
  import {
5
- serializeClientUrlPatterns,
5
+ projectClientUrlsModule,
6
6
  type ClientUrlProjection,
7
7
  } from "../../client-urls/server-projection.js";
8
- import type { ClientUrlPatterns } from "../../client-urls/types.js";
9
8
  import {
10
9
  computeProductionHash,
11
10
  hashRefKey,
@@ -191,22 +190,19 @@ export async function refreshRecordedClientUrlProjections(
191
190
  }
192
191
 
193
192
  for (const [source, referenceIds] of referenceIdsBySource) {
194
- let definition: unknown;
193
+ let module: Record<string, unknown>;
195
194
  try {
196
- definition = (await ssrEnv.runner.import(source)).default;
195
+ module = await ssrEnv.runner.import(source);
197
196
  } catch {
198
197
  continue;
199
198
  }
200
- if (
201
- (typeof definition !== "object" && typeof definition !== "function") ||
202
- definition === null ||
203
- (definition as { __brand?: unknown }).__brand !== "client-urls"
204
- ) {
199
+ let projection: ClientUrlProjection | undefined;
200
+ try {
201
+ projection = projectClientUrlsModule(module, { hoisted: true });
202
+ } catch {
205
203
  continue;
206
204
  }
207
- const projection = serializeClientUrlPatterns(
208
- definition as ClientUrlPatterns,
209
- );
205
+ if (!projection) continue;
210
206
  debug?.(
211
207
  "pre-entry refresh %s -> [%s]",
212
208
  source,
@@ -288,21 +284,24 @@ export async function discoverClientUrlProjections(
288
284
  cause,
289
285
  );
290
286
  }
291
- const definition = module.default;
292
- if (
293
- (typeof definition !== "object" && typeof definition !== "function") ||
294
- definition === null ||
295
- (definition as { __brand?: unknown }).__brand !== "client-urls"
296
- ) {
287
+ let projection: ClientUrlProjection | undefined;
288
+ try {
289
+ projection = projectClientUrlsModule(module, { hoisted: true });
290
+ } catch (cause) {
291
+ throw projectionError(
292
+ referenceIds[0],
293
+ source,
294
+ cause instanceof Error ? cause.message : String(cause),
295
+ cause,
296
+ );
297
+ }
298
+ if (!projection) {
297
299
  throw projectionError(
298
300
  referenceIds[0],
299
301
  source,
300
302
  'the module default export must be created by clientUrls() and have __brand === "client-urls"',
301
303
  );
302
304
  }
303
- const projection = serializeClientUrlPatterns(
304
- definition as ClientUrlPatterns,
305
- );
306
305
  for (const referenceId of referenceIds) {
307
306
  nextProjections.set(referenceId, projection);
308
307
  }