@rangojs/router 0.33.1 → 0.34.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 (63) hide show
  1. package/dist/bin/{chunk-TTXC2U3G.js → chunk-4HHFQSDC.js} +10 -2
  2. package/dist/bin/{chunk-3B7CKBRO.js → chunk-MWOHBDPM.js} +9 -9
  3. package/dist/bin/{client-urls-projection-DQLWPDAT.js → client-urls-projection-4Q5EQDAL.js} +2 -2
  4. package/dist/bin/rango.js +1 -1
  5. package/dist/bin/{runtime-EPUDEHO4.js → runtime-6WLCJCN7.js} +1 -1
  6. package/dist/bin/{runtime-discovery-3HH6BP3K.js → runtime-discovery-2ZTYRZ3V.js} +2 -2
  7. package/dist/bin/{temp-rsc-server-JALTIWLT.js → temp-rsc-server-EUNKRFJD.js} +3 -3
  8. package/dist/testing/vitest.js +5 -0
  9. package/dist/types/browser/history-state.d.ts +21 -0
  10. package/dist/types/browser/navigation-transaction.d.ts +2 -0
  11. package/dist/types/client-urls/server-projection.d.ts +7 -0
  12. package/dist/types/client-urls/types.d.ts +13 -14
  13. package/dist/types/errors.d.ts +5 -0
  14. package/dist/types/loader-store.d.ts +11 -1
  15. package/dist/types/prefetch-markers.d.ts +13 -4
  16. package/dist/types/route-definition/helpers-types.d.ts +14 -10
  17. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +39 -21
  18. package/dist/types/server/context.d.ts +6 -5
  19. package/dist/types/testing/render-route.d.ts +21 -1
  20. package/dist/types/testing/serve-shell-request.d.ts +7 -7
  21. package/dist/types/testing/vitest-stubs/plugin-rsc-browser.d.ts +33 -0
  22. package/dist/types/types/loader-types.d.ts +10 -8
  23. package/dist/types/urls/path-helper-types.d.ts +9 -8
  24. package/dist/vite/index.js +30 -13
  25. package/package.json +1 -1
  26. package/skills/app-structure/large-app.md +3 -0
  27. package/skills/client-urls/SKILL.md +13 -10
  28. package/skills/hooks/data.md +5 -0
  29. package/skills/hooks/state.md +13 -11
  30. package/skills/intercept/SKILL.md +8 -4
  31. package/skills/links/SKILL.md +15 -12
  32. package/skills/loader/SKILL.md +52 -39
  33. package/skills/ppr/SKILL.md +1 -1
  34. package/skills/route/SKILL.md +15 -9
  35. package/skills/testing/SKILL.md +1 -0
  36. package/skills/testing/client-components.md +67 -19
  37. package/skills/testing/serve-shell-request.md +19 -7
  38. package/skills/ux-architecture/SKILL.md +16 -11
  39. package/src/browser/history-state.ts +48 -0
  40. package/src/browser/navigation-bridge.ts +12 -1
  41. package/src/browser/navigation-transaction.ts +14 -6
  42. package/src/browser/partial-update.ts +12 -4
  43. package/src/browser/prefetch/fetch.ts +3 -3
  44. package/src/client-urls/client-urls.ts +11 -7
  45. package/src/client-urls/server-projection.ts +21 -3
  46. package/src/client-urls/types.ts +13 -14
  47. package/src/errors.ts +17 -0
  48. package/src/loader-store.ts +41 -4
  49. package/src/prefetch-markers.ts +18 -4
  50. package/src/route-definition/dsl-helpers.ts +13 -6
  51. package/src/route-definition/helpers-types.ts +14 -10
  52. package/src/router/loader-resolution.ts +4 -4
  53. package/src/router/match-api.ts +1 -3
  54. package/src/router/segment-resolution/prefetch-deferral.ts +108 -50
  55. package/src/rsc/rsc-rendering.ts +1 -1
  56. package/src/server/context.ts +6 -5
  57. package/src/testing/render-route.tsx +122 -7
  58. package/src/testing/serve-shell-request.ts +7 -7
  59. package/src/testing/vitest-stubs/plugin-rsc-browser.ts +61 -0
  60. package/src/testing/vitest.ts +5 -0
  61. package/src/types/loader-types.ts +10 -8
  62. package/src/urls/path-helper-types.ts +9 -8
  63. package/src/use-loader.tsx +29 -7
@@ -3,7 +3,7 @@ import {
3
3
  prefetchDirect,
4
4
  prefetchQueued,
5
5
  schedulePrefetchWhenRouterIdle
6
- } from "./chunk-3B7CKBRO.js";
6
+ } from "./chunk-MWOHBDPM.js";
7
7
  import {
8
8
  findTsFiles,
9
9
  firstCodeMatchIndex,
@@ -488,12 +488,15 @@ import {
488
488
  } from "react";
489
489
 
490
490
  // src/loader-store.ts
491
+ var NO_ROUTE_DATA = /* @__PURE__ */ new Set();
491
492
  var EMPTY_SNAPSHOT = Object.freeze({
492
493
  value: void 0,
493
494
  hasValue: false,
494
495
  error: null,
495
496
  isLoading: false,
496
- requestId: 0
497
+ requestId: 0,
498
+ valueFor: NO_ROUTE_DATA,
499
+ requestFor: NO_ROUTE_DATA
497
500
  });
498
501
 
499
502
  // src/client-urls/hoisted-revalidate.ts
@@ -1383,8 +1386,13 @@ function serializeLoaderOptions(loaders, owner, options) {
1383
1386
  loaders,
1384
1387
  ({ prefetch }) => prefetch === false
1385
1388
  );
1389
+ const intentLoaderIndices = indicesWhere(
1390
+ loaders,
1391
+ ({ prefetch }) => prefetch === "intent"
1392
+ );
1386
1393
  return {
1387
1394
  ...deferrableLoaderIndices.length > 0 ? { deferrableLoaderIndices: Object.freeze(deferrableLoaderIndices) } : {},
1395
+ ...intentLoaderIndices.length > 0 ? { intentLoaderIndices: Object.freeze(intentLoaderIndices) } : {},
1388
1396
  ...loaderRevalidate.some((keys) => keys.length > 0) ? {
1389
1397
  loaderRevalidate: Object.freeze(
1390
1398
  loaderRevalidate.map((keys) => Object.freeze(keys))
@@ -1,3 +1,8 @@
1
+ // src/prefetch-markers.ts
2
+ var PREFETCH_HEADER = "X-Rango-Prefetch";
3
+ var PREFETCH_INTENT = "intent";
4
+ var PREFETCH_FAILED_ROW = ":H_0\n";
5
+
1
6
  // src/internal-debug.ts
2
7
  var INTERNAL_RANGO_DEBUG = typeof process !== "undefined" && Boolean(process.env?.INTERNAL_RANGO_DEBUG);
3
8
 
@@ -209,7 +214,7 @@ var requestGeneration = 0;
209
214
  function loadRuntime() {
210
215
  if (runtime) return Promise.resolve(runtime);
211
216
  if (!runtimePromise) {
212
- runtimePromise = import("./runtime-EPUDEHO4.js").then(
217
+ runtimePromise = import("./runtime-6WLCJCN7.js").then(
213
218
  (loaded) => {
214
219
  runtime = loaded;
215
220
  if (decoder) loaded.setPrefetchDecoder(decoder);
@@ -423,12 +428,10 @@ function isActionFenceActive() {
423
428
  return fenceCount > 0;
424
429
  }
425
430
 
426
- // src/prefetch-markers.ts
427
- var PREFETCH_HEADER = "X-Rango-Prefetch";
428
- var PREFETCH_INTENT = "intent";
429
- var PREFETCH_FAILED_ROW = ":H_0\n";
430
-
431
431
  export {
432
+ PREFETCH_HEADER,
433
+ PREFETCH_INTENT,
434
+ PREFETCH_FAILED_ROW,
432
435
  IS_BROWSER_DEBUG,
433
436
  debugLog,
434
437
  notifyListeners,
@@ -454,9 +457,6 @@ export {
454
457
  clearPrefetchInflight,
455
458
  isFragmentPassthroughEnabled,
456
459
  isActionFenceActive,
457
- PREFETCH_HEADER,
458
- PREFETCH_INTENT,
459
- PREFETCH_FAILED_ROW,
460
460
  prefetchDirect,
461
461
  prefetchQueued,
462
462
  schedulePrefetchWhenRouterIdle
@@ -10,8 +10,8 @@ import {
10
10
  refreshRecordedClientUrlProjections,
11
11
  resolveClientUrlsSource,
12
12
  scanForClientUrlModules
13
- } from "./chunk-TTXC2U3G.js";
14
- import "./chunk-3B7CKBRO.js";
13
+ } from "./chunk-4HHFQSDC.js";
14
+ import "./chunk-MWOHBDPM.js";
15
15
  import "./chunk-LD7UBHT4.js";
16
16
  export {
17
17
  CLIENT_IMPORT_RE,
package/dist/bin/rango.js CHANGED
@@ -229,7 +229,7 @@ ${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
229
229
  }
230
230
  let discoverAndWriteRouteTypes;
231
231
  try {
232
- const mod = await import("./runtime-discovery-3HH6BP3K.js");
232
+ const mod = await import("./runtime-discovery-2ZTYRZ3V.js");
233
233
  discoverAndWriteRouteTypes = mod.discoverAndWriteRouteTypes;
234
234
  } catch (err) {
235
235
  if (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND") {
@@ -26,7 +26,7 @@ import {
26
26
  setInflightPromiseWithAliases,
27
27
  stateEpoch,
28
28
  storePrefetch
29
- } from "./chunk-3B7CKBRO.js";
29
+ } from "./chunk-MWOHBDPM.js";
30
30
 
31
31
  // src/browser/prefetch/resource-ready.ts
32
32
  function waitForViewportImages() {
@@ -163,9 +163,9 @@ async function discoverAndWriteRouteTypes(opts) {
163
163
  "Runtime discovery requires '@vitejs/plugin-rsc'. Install it with: pnpm add -D @vitejs/plugin-rsc"
164
164
  );
165
165
  }
166
- const { createTempRscServer } = await import("./temp-rsc-server-JALTIWLT.js");
166
+ const { createTempRscServer } = await import("./temp-rsc-server-EUNKRFJD.js");
167
167
  const { createDiscoveryState } = await import("./state-ACD2MGKJ.js");
168
- const { discoverClientUrlProjections } = await import("./client-urls-projection-DQLWPDAT.js");
168
+ const { discoverClientUrlProjections } = await import("./client-urls-projection-4Q5EQDAL.js");
169
169
  const {
170
170
  isFrameworkPlugin,
171
171
  pickForwardedRunnerConfig,
@@ -27,8 +27,8 @@ import {
27
27
  propertyKeyIs,
28
28
  recordClientUrlsModule,
29
29
  unwrapExpression
30
- } from "./chunk-TTXC2U3G.js";
31
- import "./chunk-3B7CKBRO.js";
30
+ } from "./chunk-4HHFQSDC.js";
31
+ import "./chunk-MWOHBDPM.js";
32
32
  import {
33
33
  codeMatchIndices,
34
34
  isSkippedSourceScanDir
@@ -47,7 +47,7 @@ var DEFAULT_ROUTER_VERSIONS_KEY = "*";
47
47
  // package.json
48
48
  var package_default = {
49
49
  name: "@rangojs/router",
50
- version: "0.33.1",
50
+ version: "0.34.0",
51
51
  description: "Django-inspired RSC router with composable URL patterns",
52
52
  keywords: [
53
53
  "react",
@@ -296,6 +296,11 @@ function rangoTestAliases(opts = {}) {
296
296
  {
297
297
  find: /^@vitejs\/plugin-rsc\/rsc(\/(server|client))?$/,
298
298
  replacement: here("src/testing/vitest-stubs/plugin-rsc.ts")
299
+ },
300
+ // useLoader().load() decodes renderRoute's loader answers through it.
301
+ {
302
+ find: /^@vitejs\/plugin-rsc\/browser$/,
303
+ replacement: here("src/testing/vitest-stubs/plugin-rsc-browser.ts")
299
304
  }
300
305
  ];
301
306
  if (rsdServerEdgePath) {
@@ -1,3 +1,4 @@
1
+ import { type LocationStateSnapshot } from "./react/location-state-shared.js";
1
2
  /**
2
3
  * Resolve navigation state - handles both LocationStateEntry[] and plain formats
3
4
  */
@@ -11,6 +12,26 @@ export declare function buildHistoryState(userState: unknown, routerState?: {
11
12
  intercept?: boolean;
12
13
  sourceUrl?: string;
13
14
  }, serverState?: Record<string, unknown>): Record<string, unknown> | null;
15
+ /**
16
+ * The entry a commit onto the entry on screen writes (router.refresh(), the
17
+ * cross-tab refresh): `entryState`, read when the commit lands, keeps its
18
+ * location state. Its scroll key and idx go (pushHistoryWithIdx and
19
+ * ensureHistoryKey write them again), and its intercept marks are this
20
+ * commit's: a refresh renders the URL's full page. Server-set state merges
21
+ * over the kept state. A flash a reader already cleared from the entry stays
22
+ * cleared: the entry is copied, never rebuilt from what readers show.
23
+ */
24
+ export declare function keptEntryState(entryState: unknown, routerState?: {
25
+ intercept?: boolean;
26
+ sourceUrl?: string;
27
+ }, serverState?: Record<string, unknown>): Record<string, unknown> | null;
28
+ /**
29
+ * What readers get from a commit onto the entry on screen: the state they
30
+ * show, plus the server's, recorded under the client's version. Not re-read
31
+ * from the entry, so a flash value on screen stays until the next navigation
32
+ * or back/forward.
33
+ */
34
+ export declare function shownLocationState(shown: LocationStateSnapshot, serverState?: Record<string, unknown>): Record<string, unknown>;
14
35
  /** Check if a history state object contains location state keys. */
15
36
  export declare function hasLocationState(state: unknown): boolean;
16
37
  /**
@@ -41,6 +41,8 @@ interface CommitOptions {
41
41
  * screen stays (EventController.commitLocationState).
42
42
  */
43
43
  treeless?: boolean;
44
+ /** Commits onto the entry on screen, keeping its location state (see keptEntryState). */
45
+ sameEntry?: boolean;
44
46
  }
45
47
  /**
46
48
  * Options that can override the pre-configured commit settings
@@ -38,6 +38,11 @@ export interface ClientUrlProjectionRoute {
38
38
  * loader(), so a prefetch of the route defers them. Absent (= none) in
39
39
  * projections serialized before the option existed. */
40
40
  readonly deferrableLoaderIndices?: readonly number[];
41
+ /** Indices into loaderIds of loaders declared loader(Def, { prefetch:
42
+ * "intent" }); materialization passes the option through, so a
43
+ * speculative prefetch defers them and an intent prefetch runs them.
44
+ * Absent (= none) in projections serialized before the option existed. */
45
+ readonly intentLoaderIndices?: readonly number[];
41
46
  /** Per loader (aligned with loaderIds), its revalidate() arguments in
42
47
  * declaration order: a hoisted predicate's key, whose registration
43
48
  * materialization attaches (revalidate-registry.ts), or a boolean
@@ -64,6 +69,8 @@ export interface ClientUrlProjectionIntercept {
64
69
  readonly hasLoading: boolean;
65
70
  /** As on a route (ClientUrlProjectionRoute.deferrableLoaderIndices). */
66
71
  readonly deferrableLoaderIndices?: readonly number[];
72
+ /** As on a route (ClientUrlProjectionRoute.intentLoaderIndices). */
73
+ readonly intentLoaderIndices?: readonly number[];
67
74
  /** As on a route (ClientUrlProjectionRoute.loaderRevalidate). */
68
75
  readonly loaderRevalidate?: readonly (readonly (string | boolean)[])[];
69
76
  }
@@ -2,6 +2,7 @@ import type { ComponentType, ReactNode } from "react";
2
2
  import type { LoaderDefinition, LoaderOptions, ShouldRevalidateFn, TransitionConfig } from "../types.js";
3
3
  import type { DefaultEnv } from "../types/global-namespace.js";
4
4
  import type { HoistedRevalidateRef } from "./hoisted-revalidate.js";
5
+ import type { PrefetchFlag } from "../prefetch-markers.js";
5
6
  import type { TrieMatchResult } from "../router/trie-matching.js";
6
7
  import type { PathOptions } from "../urls/pattern-types.js";
7
8
  import type { SearchSchema } from "../search-params.js";
@@ -48,13 +49,12 @@ export interface ClientUrlLoaderRecord {
48
49
  */
49
50
  readonly ssr?: false;
50
51
  /**
51
- * loader(Def, { prefetch: false }): a viewport or render prefetch of the
52
- * route, or of an intercept's target from its source, defers this loader
53
- * (a hover prefetch runs it) and the click
54
- * that adopts it runs it in its fill request (see {@link LoaderOptions}).
55
- * Projected onto the materialized stub.
52
+ * loader(Def, { prefetch: false | "intent" }): a prefetch of the route, or
53
+ * of an intercept's target from its source, defers this loader and the
54
+ * click that adopts it runs it in its fill request (see
55
+ * {@link LoaderOptions}). Projected onto the materialized stub.
56
56
  */
57
- readonly prefetch?: false;
57
+ readonly prefetch?: PrefetchFlag;
58
58
  }
59
59
  /**
60
60
  * The TransitionConfig subset a clientUrls() route may declare: ViewTransition
@@ -118,14 +118,13 @@ export interface ClientUrlHelpers {
118
118
  * settled return freezes into the shell (nested promises stay live
119
119
  * holes).
120
120
  *
121
- * Pass `{ prefetch: false }` to keep this loader out of the route's
122
- * viewport and render prefetches; a hover prefetch runs it (see
123
- * {@link LoaderOptions}). An in-group click still shows
124
- * the destination at once, commits the adopted prefetch with the other
125
- * loaders' data, and runs only the flagged loaders in its one fill
126
- * request. A `layout()` loader with the flag is flagged on every route
127
- * below it. Inside intercept() it defers the loader in a viewport or
128
- * render prefetch of the intercept from its source, as in urls().
121
+ * Pass `{ prefetch: false | "intent" }` to keep this loader out of the
122
+ * route's prefetches (see {@link LoaderOptions}). An in-group click still
123
+ * shows the destination at once, commits the adopted prefetch with the
124
+ * other loaders' data, and runs only the flagged loaders in its one fill
125
+ * request. A `layout()` loader with the
126
+ * flag is flagged on every route below it. Inside intercept() it defers
127
+ * the loader in a prefetch of the intercept from its source, as in urls().
129
128
  */
130
129
  readonly loader: <TData>(definition: LoaderDefinition<TData>, optionsOrUse?: Pick<LoaderOptions, "ssr" | "prefetch"> | ClientUrlUse, use?: ClientUrlUse) => ClientUrlItem;
131
130
  readonly loading: (component: ReactNode) => ClientUrlItem;
@@ -247,6 +247,11 @@ export declare function invariant(condition: any, message: string): asserts cond
247
247
  * passes), as an invariant.
248
248
  */
249
249
  export declare function assertBooleanOption(value: unknown, option: string, effect: string): void;
250
+ /**
251
+ * Rejects a `prefetch` DSL option that is neither a boolean nor `"intent"`
252
+ * (undefined passes), as an invariant.
253
+ */
254
+ export declare function assertPrefetchOption(value: unknown, option: string, effect: string): void;
250
255
  /**
251
256
  * Sanitize errors for production - prevents leaking sensitive information
252
257
  *
@@ -53,6 +53,14 @@ export interface LoaderEntry<T = unknown> {
53
53
  readonly isLoading: boolean;
54
54
  /** Identifies the request that produced this snapshot. 0 means "no request". */
55
55
  readonly requestId: number;
56
+ /**
57
+ * Route data (a route reader's `contextData`, by identity) of the readers on
58
+ * screen when `value` was requested. A route reader shows `value` only while
59
+ * its own route data is in this set.
60
+ */
61
+ readonly valueFor: ReadonlySet<unknown>;
62
+ /** The same for the latest request: `isLoading` and `error`. */
63
+ readonly requestFor: ReadonlySet<unknown>;
56
64
  }
57
65
  /**
58
66
  * Options for `subscribe`.
@@ -92,6 +100,8 @@ export interface SubscribeOptions {
92
100
  * its own lifecycle.
93
101
  */
94
102
  onStreamPending?: () => void;
103
+ /** The subscriber's committed route data. Route-context readers only. */
104
+ routeData?: () => unknown;
95
105
  }
96
106
  export declare class LoaderStore {
97
107
  private readonly entries;
@@ -172,7 +182,7 @@ export declare class LoaderStore {
172
182
  * error during the new request. Gated on `requestId === latestRequestId`
173
183
  * for symmetry with the other mutators.
174
184
  */
175
- beginRequest(bucketKey: string, requestId: number): void;
185
+ beginRequest(bucketKey: string, requestId: number, initiatorRouteData?: () => unknown): void;
176
186
  /**
177
187
  * Commit a successful result. No-op if `requestId` is not the latest
178
188
  * (a newer `load()` was issued or `clear()` ran). Clearing `error` is
@@ -10,12 +10,21 @@ export declare const PREFETCH_HEADER = "X-Rango-Prefetch";
10
10
  /**
11
11
  * PREFETCH_HEADER's value on an intent prefetch: a hover or an explicit
12
12
  * `prefetch()` call (browser/prefetch/fetch.ts prefetchDirect). The server
13
- * defers nothing in it: `prefetch: false` loaders and the work behind a
14
- * flagged `loading()` run, as in the navigation it starts early. A
15
- * speculative prefetch (viewport, render) sends `1`, and any value other than
16
- * this one reads as speculative.
13
+ * runs the work flagged `prefetch: "intent"` in it and defers the work
14
+ * flagged `prefetch: false`. A speculative prefetch (viewport, render) sends
15
+ * `1` and defers both; any value other than this one reads as speculative.
17
16
  */
18
17
  export declare const PREFETCH_INTENT = "intent";
18
+ /**
19
+ * A recorded `prefetch` flag of loader() or loading(): `false` defers the
20
+ * work in every prefetch, `"intent"` in a speculative one only
21
+ * (router/segment-resolution/prefetch-deferral.ts).
22
+ */
23
+ export type PrefetchFlag = false | typeof PREFETCH_INTENT;
24
+ /** A `prefetch` DSL option as written: a boolean, or `"intent"`. */
25
+ export type PrefetchOption = boolean | PrefetchFlag;
26
+ /** The option asks for deferral: `prefetch: false` or `prefetch: "intent"`. */
27
+ export declare function isPrefetchFlag(value: unknown): value is PrefetchFlag;
19
28
  /**
20
29
  * The last row of a prefetch whose render reported an error
21
30
  * (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
2
2
  import type { ExtractRouteParams, Handler, PartialCacheOptions, ErrorBoundaryHandler, LoaderDefinition, LoaderOptions, MiddlewareFn, NotFoundBoundaryHandler, ResolvedRouteMap, RouteDefinition, ShouldRevalidateFn, TransitionConfig } from "../types.js";
3
3
  import type { AllUseItems, LayoutItem, RouteItem, ParallelItem, InterceptItem, MiddlewareItem, RevalidateItem, LoaderItem, LoadingItem, ErrorBoundaryItem, NotFoundBoundaryItem, LayoutUseItem, RouteUseItem, ParallelUseItem, InterceptUseItem, LoaderUseItem, CacheItem, TransitionItem, UseItems } from "../route-types.js";
4
4
  import type { StaticHandlerRef } from "../static-handler.js";
5
+ import type { PrefetchOption } from "../prefetch-markers.js";
5
6
  export type { AllUseItems, LayoutItem, RouteItem, ParallelItem, InterceptItem, MiddlewareItem, RevalidateItem, LoaderItem, ErrorBoundaryItem, NotFoundBoundaryItem, LayoutUseItem, RouteUseItem, ParallelUseItem, InterceptUseItem, CacheItem, } from "../route-types.js";
6
7
  export type { InterceptConfig, InterceptSelectorContext, InterceptSegmentsState, InterceptWhenFn, } from "../server/context";
7
8
  /**
@@ -231,15 +232,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
231
232
  * loader(RecommendationsLoader), // still streams behind loading()
232
233
  * ```
233
234
  *
234
- * Pass `{ prefetch: false }` to keep the loader out of speculative
235
- * prefetches (viewport, render): such a prefetch skips it and the click
236
- * that adopts it fetches it in one follow-up request; a hover prefetch runs
237
- * it (see {@link LoaderOptions}). Only on a segment the
235
+ * Pass `{ prefetch: false }` to keep the loader out of prefetches: a
236
+ * prefetch skips it and the click that adopts it fetches it in one
237
+ * follow-up request. `{ prefetch: "intent" }` defers it in viewport and
238
+ * render prefetches only (see {@link LoaderOptions}). Only on a segment the
238
239
  * client does not have yet: a loader the client already holds runs in the
239
240
  * prefetch when it revalidates.
240
241
  *
241
242
  * ```typescript
242
243
  * loader(ReviewsLoader, { prefetch: false }),
244
+ * loader(RelatedLoader, { prefetch: "intent" }),
243
245
  * ```
244
246
  *
245
247
  * @param loaderDef - Loader created with createLoader()
@@ -256,16 +258,18 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
256
258
  * // Skip loading on SSR, only show on client navigation
257
259
  * loading(<Skeleton />, { ssr: false })
258
260
  *
259
- * // Keep everything behind this fallback out of viewport and render
260
- * // prefetches (a hover prefetch runs it)
261
+ * // Keep everything behind this fallback out of prefetches
261
262
  * loading(<Skeleton />, { prefetch: false })
263
+ *
264
+ * // Deferred in viewport and render prefetches only
265
+ * loading(<Skeleton />, { prefetch: "intent" })
262
266
  * ```
263
267
  * @param component - The loading UI to show during navigation
264
268
  * @param options - Configuration options
265
269
  * @param options.ssr - If false, skip showing loading on document requests (SSR)
266
- * @param options.prefetch - If false, the flag applies when this entry's
267
- * segment is new to the client. A viewport or render prefetch then skips
268
- * the whole unit (a hover prefetch runs it): the
270
+ * @param options.prefetch - `false` or `"intent"` (see
271
+ * {@link LoaderOptions}); the flag applies when this entry's segment is
272
+ * new to the client. A prefetch then skips the whole unit: the
269
273
  * handler, its loaders, its orphan layouts and parallel slots and, for a
270
274
  * layout, every deeper entry; the click that adopts the prefetch shows
271
275
  * this fallback and fetches them in one follow-up request, which reads
@@ -280,7 +284,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
280
284
  */
281
285
  loading: (component: ReactNode | (() => ReactNode), options?: {
282
286
  ssr?: boolean;
283
- prefetch?: boolean;
287
+ prefetch?: PrefetchOption;
284
288
  }) => LoadingItem;
285
289
  /**
286
290
  * Attach an error boundary to catch errors in this segment and children
@@ -1,6 +1,9 @@
1
1
  /**
2
- * `prefetch: false`: what a prefetch request defers and what its fill request
3
- * runs (docs/design/prefetch-false.md).
2
+ * `prefetch: false` and `prefetch: "intent"`: what a prefetch request defers
3
+ * and what its fill request runs (docs/design/prefetch-false.md). A
4
+ * speculative prefetch (viewport, render) defers both flags; an intent
5
+ * prefetch (a hover, `prefetch()`) defers `false` only and runs `"intent"`
6
+ * work as if it were unflagged.
4
7
  *
5
8
  * Two layers. The SCOPE is static per matched chain: which loader
6
9
  * registrations are deferrable and which entries can be a handler unit. The
@@ -30,20 +33,15 @@ import type { CacheScope } from "../../cache/cache-scope.js";
30
33
  /**
31
34
  * What a request says it is, read once when its context is created
32
35
  * (RequestContext._requestKind). A fill is marked on the raw URL and wins
33
- * over the prefetch header. `"prefetch"` is a speculative prefetch (viewport,
34
- * render) and `"intent"` a hover or an explicit `prefetch()` call
35
- * (PREFETCH_INTENT): both are prefetches, and only the first defers. Whether
36
- * a prefetch defers anything is decided per match, by partialDeferralMode: a
37
- * shell capture inherits the context of the request that scheduled it and is
38
- * never a prefetch.
36
+ * over the prefetch header; an intent prefetch carries PREFETCH_INTENT.
39
37
  */
40
38
  export declare function requestKind(request: Request, rawUrl: URL): RequestKind | undefined;
41
39
  /** requestKind's answer for a marked request. */
42
40
  export type RequestKind = "prefetch" | "intent" | "fill";
43
41
  /** A prefetch of either kind: speculative or intent. */
44
42
  export declare function isPrefetchKind(kind: RequestKind | undefined): boolean;
45
- /** Static, per matched chain. */
46
- interface DeferralScope {
43
+ /** What one kind of prefetch can defer in a matched chain. */
44
+ interface DeferralSet {
47
45
  /**
48
46
  * Loader registrations a prefetch can defer, each with what makes it so:
49
47
  * `true` for its own flag, else the segment ids of the flagged entries whose
@@ -51,8 +49,6 @@ interface DeferralScope {
51
49
  * segments is new to the client.
52
50
  */
53
51
  readonly loaders: ReadonlyMap<LoaderEntry, true | readonly string[]>;
54
- /** `$$id`s of `loaders`: the loaders that cannot call ctx.rendered(). */
55
- readonly loaderIds: ReadonlySet<string>;
56
52
  /**
57
53
  * Entries whose handler a prefetch can skip (a flagged loading() with a
58
54
  * fallback to show). `index` is the chain index that owns the entry: its
@@ -62,6 +58,23 @@ interface DeferralScope {
62
58
  */
63
59
  readonly units: ReadonlyMap<EntryData, DeferralUnit>;
64
60
  }
61
+ /**
62
+ * Static, per matched chain: what a speculative prefetch defers (every flag,
63
+ * `false` or `"intent"`), and in `intent` what an intent prefetch defers.
64
+ */
65
+ interface DeferralScope extends DeferralSet {
66
+ /**
67
+ * `$$id`s of `loaders`: the loaders that cannot call ctx.rendered(). The
68
+ * speculative set on every request: a loader an intent prefetch runs is
69
+ * still run by a speculative prefetch's fill, where its handler does not.
70
+ */
71
+ readonly loaderIds: ReadonlySet<string>;
72
+ /**
73
+ * What an intent prefetch defers: the scope with every `"intent"` flag
74
+ * read as unflagged. Absent when no flag is `"intent"`: the scope itself.
75
+ */
76
+ readonly intent?: DeferralSet;
77
+ }
65
78
  interface DeferralUnit {
66
79
  readonly index: number;
67
80
  readonly ids: readonly string[];
@@ -69,6 +82,11 @@ interface DeferralUnit {
69
82
  /** Per match: InternalHandlerContext._prefetchDeferral. */
70
83
  export interface PrefetchDeferral {
71
84
  readonly scope: DeferralScope;
85
+ /**
86
+ * What this request defers: `scope`, or `scope.intent` for an intent
87
+ * prefetch. The ctx.rendered() guards read `scope`.
88
+ */
89
+ readonly defers: DeferralSet;
72
90
  /**
73
91
  * "prefetch": deferral replaces execution. "fill": the request that follows
74
92
  * an adopted prefetch; a held segment is skipped outright. Unset on every
@@ -151,7 +169,7 @@ export interface DeferredLoader {
151
169
  /**
152
170
  * The deferral scope of a matched chain, memoized on its leaf entry (entries
153
171
  * are per-isSSR manifest objects, replaced on HMR). EMPTY_SCOPE when nothing
154
- * in the tree declares `prefetch: false`.
172
+ * in the tree declares `prefetch: false` or `prefetch: "intent"`.
155
173
  */
156
174
  export declare function resolveDeferralScope(entries: readonly EntryData[]): DeferralScope;
157
175
  /**
@@ -164,10 +182,8 @@ export declare function resolveDeferralScope(entries: readonly EntryData[]): Def
164
182
  */
165
183
  export declare function resolveInterceptDeferralScope(intercept: InterceptEntry): DeferralScope;
166
184
  /**
167
- * Which half of the feature a partial request is, if either (`fill` and
168
- * `prefetch` are requestKind's answer). `prefetch` is a speculative prefetch
169
- * only: an intent prefetch defers nothing, like the navigation it starts
170
- * early (R16). Both halves are GETs: a request of
185
+ * The kind a partial request plans its deferral as, if any (requestKind's
186
+ * answer, filtered). Both halves of the feature are GETs: a request of
171
187
  * another method that carries either marker is a normal request. A prefetch
172
188
  * defers only on a plain GET navigation: an action re-renders what it
173
189
  * changed, and a shell capture is never a prefetch whatever the request that
@@ -175,20 +191,22 @@ export declare function resolveInterceptDeferralScope(intercept: InterceptEntry)
175
191
  * capture runs the full match): each one states its own reason.
176
192
  */
177
193
  export declare function partialDeferralMode(request: {
178
- fill: boolean;
179
- prefetch: boolean;
194
+ kind: RequestKind | undefined;
180
195
  method: string;
181
196
  isAction: boolean;
182
197
  isShellCapture: boolean;
183
- }): PrefetchDeferral["mode"];
198
+ }): RequestKind | undefined;
184
199
  export declare const NOTHING_HELD: ReadonlySet<string>;
185
200
  /**
186
201
  * The plan for one match, or undefined when the tree declares no flag and the
187
202
  * request is not a fill (the common case: nothing to carry). `held` is the
188
203
  * client's segment ids as the request listed them. `scope` is the chain's
189
204
  * unless the request resolves an intercept (resolveInterceptDeferralScope).
205
+ * An intent prefetch plans as a prefetch over `scope.intent`: a tree whose
206
+ * flags are all `"intent"` defers nothing in it, and its response is the
207
+ * navigation's (no `_payloadDeferred`, so no source scope).
190
208
  */
191
- export declare function planPrefetchDeferral(entries: readonly EntryData[], mode: PrefetchDeferral["mode"], matched: {
209
+ export declare function planPrefetchDeferral(entries: readonly EntryData[], requested: RequestKind | undefined, matched: {
192
210
  pr?: boolean;
193
211
  od?: boolean;
194
212
  }, cacheScope: CacheScope | null | undefined, held?: ReadonlySet<string>, scope?: DeferralScope): PrefetchDeferral | undefined;
@@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import type { ReactNode } from "react";
4
4
  import type { PartialCacheOptions, ErrorBoundaryHandler, Handler, LoaderDefinition, MiddlewareFn, NotFoundBoundaryHandler, ShouldRevalidateFn, TransitionConfig } from "../types";
5
5
  import type { ContextVar } from "../context-var.js";
6
+ import type { PrefetchFlag } from "../prefetch-markers.js";
6
7
  import { type WarmContext } from "../prerender/warm-request.js";
7
8
  /**
8
9
  * Performance metric entry for a single measured operation
@@ -91,13 +92,13 @@ export type EntryPropDatas = {
91
92
  export type EntryPropRender = {
92
93
  loading?: ReactNode | false;
93
94
  /**
94
- * loading(fallback, { prefetch: false }) with a renderable fallback: a
95
- * speculative prefetch defers what the fallback covers while the entry's
95
+ * loading(fallback, { prefetch: false | "intent" }) with a renderable
96
+ * fallback: a prefetch defers what the fallback covers while the entry's
96
97
  * segment is new to the client
97
98
  * (router/segment-resolution/prefetch-deferral.ts). Set on both DSL
98
99
  * evaluations, whatever `ssr` did to `loading`.
99
100
  */
100
- loadingPrefetch?: false;
101
+ loadingPrefetch?: PrefetchFlag;
101
102
  /**
102
103
  * loading(fallback, { ssr: false }), on both DSL evaluations; a document
103
104
  * then sets `loading` to false (applyDocumentModeLoading).
@@ -137,11 +138,11 @@ export type LoaderEntry = {
137
138
  */
138
139
  bake?: true;
139
140
  /**
140
- * loader(Def, { prefetch: false }): a speculative prefetch defers this
141
+ * loader(Def, { prefetch: false | "intent" }): a prefetch defers this
141
142
  * registration while its segment is new to the client
142
143
  * (router/segment-resolution/prefetch-deferral.ts).
143
144
  */
144
- prefetch?: false;
145
+ prefetch?: PrefetchFlag;
145
146
  /**
146
147
  * The declaring layout's / parallel's / cache()'s include scope, for the
147
148
  * loader body's `ctx.reverse(".name")` (its DSL kickoff carries it,
@@ -45,6 +45,13 @@
45
45
  * lane production's Back uses (useNavigation() idle), with the loader
46
46
  * data current then. The prefetch request, its decode and the HTTP cache
47
47
  * are e2e territory.
48
+ * - useLoader().load() and a refreshGroup refresh reach the loader endpoint:
49
+ * renderRoute answers a GET for `_rsc_loader=<id>` with the last value the
50
+ * test gave that loader (the render seeds or respondToLoads()),
51
+ * unencoded; a pending Promise holds the answer until it settles, and a
52
+ * rejection is the loader's error. Needs the testing kit's
53
+ * `@vitejs/plugin-rsc/browser` alias (rangoTestAliases). A load() with params
54
+ * is answered with a loaderError; a body and non-GET loads are not answered.
48
55
  * - A spec's transition({ when }) decides every commit with production's
49
56
  * browser decision: navigate() and useRouter().push model kind "push",
50
57
  * useRouter().replace kind "replace", refresh() kind "revalidate", and a
@@ -428,8 +435,10 @@ export interface TestRouterHandle {
428
435
  /**
429
436
  * Re-render the current location, as router.refresh() does: a spec's
430
437
  * transition({ when }) decides it with kind "revalidate" (`to` is `from`).
438
+ * `options.loaders` is the loader data the response carries, merged over
439
+ * the render-time seeds as in navigate().
431
440
  */
432
- refresh(): Promise<void>;
441
+ refresh(options?: Pick<RenderRouteOptions, "loaders">): Promise<void>;
433
442
  /**
434
443
  * Prefetch `url`, as a `<Link>` hover or viewport trigger does: the page is
435
444
  * kept with the loader data its response would carry now (the render-time
@@ -469,6 +478,17 @@ export interface TestRouterHandle {
469
478
  action(options?: Pick<RenderRouteOptions, "loaders"> & {
470
479
  keepClientCache?: boolean;
471
480
  }): Promise<void>;
481
+ /**
482
+ * Set what the loader endpoint answers from now on: a later
483
+ * `useLoader().load()` or refreshGroup refresh of each loader resolves to
484
+ * its value (a pending Promise holds the answer, a rejection is the
485
+ * loader's error). Renders nothing; the page on screen keeps its data.
486
+ *
487
+ * @example
488
+ * router.respondToLoads({ loaders: [[CartLoader, { count: 3 }]] });
489
+ * fireEvent.click(getByText("Refresh")); // load(): count 3
490
+ */
491
+ respondToLoads(options: Pick<RenderRouteOptions, "loaders">): void;
472
492
  /**
473
493
  * The background revalidations a click on a stale prefetch started, in
474
494
  * order: the page's pathname and search, and the segment ids the request