@rangojs/router 0.33.2 → 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.
- package/dist/bin/{chunk-YP4HENIU.js → chunk-4HHFQSDC.js} +6 -1
- package/dist/bin/{chunk-3B7CKBRO.js → chunk-MWOHBDPM.js} +9 -9
- package/dist/bin/{client-urls-projection-J2BGDKQ4.js → client-urls-projection-4Q5EQDAL.js} +2 -2
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-EPUDEHO4.js → runtime-6WLCJCN7.js} +1 -1
- package/dist/bin/{runtime-discovery-O6KQ5KO2.js → runtime-discovery-2ZTYRZ3V.js} +2 -2
- package/dist/bin/{temp-rsc-server-BOHXBOR7.js → temp-rsc-server-EUNKRFJD.js} +3 -3
- package/dist/types/client-urls/server-projection.d.ts +7 -0
- package/dist/types/client-urls/types.d.ts +13 -14
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/prefetch-markers.d.ts +13 -4
- package/dist/types/route-definition/helpers-types.d.ts +14 -10
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +39 -21
- package/dist/types/server/context.d.ts +6 -5
- package/dist/types/testing/serve-shell-request.d.ts +7 -7
- package/dist/types/types/loader-types.d.ts +10 -8
- package/dist/types/urls/path-helper-types.d.ts +9 -8
- package/dist/vite/index.js +26 -12
- package/package.json +1 -1
- package/skills/app-structure/large-app.md +3 -0
- package/skills/client-urls/SKILL.md +13 -10
- package/skills/intercept/SKILL.md +8 -4
- package/skills/links/SKILL.md +15 -12
- package/skills/loader/SKILL.md +52 -39
- package/skills/ppr/SKILL.md +1 -1
- package/skills/route/SKILL.md +15 -9
- package/skills/testing/serve-shell-request.md +19 -7
- package/skills/ux-architecture/SKILL.md +16 -11
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/partial-update.ts +12 -4
- package/src/browser/prefetch/fetch.ts +3 -3
- package/src/client-urls/client-urls.ts +11 -7
- package/src/client-urls/server-projection.ts +21 -3
- package/src/client-urls/types.ts +13 -14
- package/src/errors.ts +17 -0
- package/src/prefetch-markers.ts +18 -4
- package/src/route-definition/dsl-helpers.ts +13 -6
- package/src/route-definition/helpers-types.ts +14 -10
- package/src/router/loader-resolution.ts +4 -4
- package/src/router/match-api.ts +1 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +108 -50
- package/src/rsc/rsc-rendering.ts +1 -1
- package/src/server/context.ts +6 -5
- package/src/testing/serve-shell-request.ts +7 -7
- package/src/types/loader-types.ts +10 -8
- package/src/urls/path-helper-types.ts +9 -8
package/dist/vite/index.js
CHANGED
|
@@ -111,6 +111,17 @@ var init_debug = __esm({
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// src/prefetch-markers.ts
|
|
115
|
+
var PREFETCH_HEADER, PREFETCH_INTENT, PREFETCH_FAILED_ROW;
|
|
116
|
+
var init_prefetch_markers = __esm({
|
|
117
|
+
"src/prefetch-markers.ts"() {
|
|
118
|
+
"use strict";
|
|
119
|
+
PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
120
|
+
PREFETCH_INTENT = "intent";
|
|
121
|
+
PREFETCH_FAILED_ROW = ":H_0\n";
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
114
125
|
// src/internal-debug.ts
|
|
115
126
|
var INTERNAL_RANGO_DEBUG;
|
|
116
127
|
var init_internal_debug = __esm({
|
|
@@ -725,17 +736,6 @@ var init_validate_redirect_origin = __esm({
|
|
|
725
736
|
}
|
|
726
737
|
});
|
|
727
738
|
|
|
728
|
-
// src/prefetch-markers.ts
|
|
729
|
-
var PREFETCH_HEADER, PREFETCH_INTENT, PREFETCH_FAILED_ROW;
|
|
730
|
-
var init_prefetch_markers = __esm({
|
|
731
|
-
"src/prefetch-markers.ts"() {
|
|
732
|
-
"use strict";
|
|
733
|
-
PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
734
|
-
PREFETCH_INTENT = "intent";
|
|
735
|
-
PREFETCH_FAILED_ROW = ":H_0\n";
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
|
|
739
739
|
// src/browser/response-adapter.ts
|
|
740
740
|
function isForeignRouterId(response, expectedId) {
|
|
741
741
|
const got = response.headers.get("X-RSC-Router-Id");
|
|
@@ -4594,6 +4594,9 @@ import * as React4 from "react";
|
|
|
4594
4594
|
// src/server/context.ts
|
|
4595
4595
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
4596
4596
|
|
|
4597
|
+
// src/errors.ts
|
|
4598
|
+
init_prefetch_markers();
|
|
4599
|
+
|
|
4597
4600
|
// src/cache/cache-key-utils.ts
|
|
4598
4601
|
function routerKeyPrefix(routerId2) {
|
|
4599
4602
|
return `${encodeURIComponent(routerId2)}@`;
|
|
@@ -5363,6 +5366,9 @@ function insertSegments(node, segments, index, leafBase, paramNames) {
|
|
|
5363
5366
|
}
|
|
5364
5367
|
}
|
|
5365
5368
|
|
|
5369
|
+
// src/client-urls/client-urls.ts
|
|
5370
|
+
init_prefetch_markers();
|
|
5371
|
+
|
|
5366
5372
|
// src/browser/react/use-navigation.ts
|
|
5367
5373
|
import { useContext as useContext9, useEffect as useEffect7, useRef as useRef5 } from "react";
|
|
5368
5374
|
|
|
@@ -5924,6 +5930,9 @@ import { jsx as jsx9 } from "react/jsx-runtime";
|
|
|
5924
5930
|
// src/server/root-layout.tsx
|
|
5925
5931
|
import { Fragment as Fragment3, jsx as jsx10 } from "react/jsx-runtime";
|
|
5926
5932
|
|
|
5933
|
+
// src/route-definition/dsl-helpers.ts
|
|
5934
|
+
init_prefetch_markers();
|
|
5935
|
+
|
|
5927
5936
|
// src/prerender.ts
|
|
5928
5937
|
var PRERENDER_PASSTHROUGH = Object.freeze({
|
|
5929
5938
|
__brand: "prerenderPassthrough"
|
|
@@ -6318,8 +6327,13 @@ function serializeLoaderOptions(loaders, owner, options) {
|
|
|
6318
6327
|
loaders,
|
|
6319
6328
|
({ prefetch }) => prefetch === false
|
|
6320
6329
|
);
|
|
6330
|
+
const intentLoaderIndices = indicesWhere(
|
|
6331
|
+
loaders,
|
|
6332
|
+
({ prefetch }) => prefetch === "intent"
|
|
6333
|
+
);
|
|
6321
6334
|
return {
|
|
6322
6335
|
...deferrableLoaderIndices.length > 0 ? { deferrableLoaderIndices: Object.freeze(deferrableLoaderIndices) } : {},
|
|
6336
|
+
...intentLoaderIndices.length > 0 ? { intentLoaderIndices: Object.freeze(intentLoaderIndices) } : {},
|
|
6323
6337
|
...loaderRevalidate.some((keys) => keys.length > 0) ? {
|
|
6324
6338
|
loaderRevalidate: Object.freeze(
|
|
6325
6339
|
loaderRevalidate.map((keys) => Object.freeze(keys))
|
|
@@ -7330,7 +7344,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7330
7344
|
// package.json
|
|
7331
7345
|
var package_default = {
|
|
7332
7346
|
name: "@rangojs/router",
|
|
7333
|
-
version: "0.
|
|
7347
|
+
version: "0.34.0",
|
|
7334
7348
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7335
7349
|
keywords: [
|
|
7336
7350
|
"react",
|
package/package.json
CHANGED
|
@@ -706,6 +706,9 @@ Rules of thumb:
|
|
|
706
706
|
never become clicks: activity feeds, analytics panels, reviews below the
|
|
707
707
|
fold, a list behind a tab.
|
|
708
708
|
- Do not flag what the likely click needs first: the page's main content.
|
|
709
|
+
- Flag a loader worth starting on hover `prefetch: "intent"` instead: viewport
|
|
710
|
+
and render prefetches skip it, and a hover or `useRouter().prefetch()` runs
|
|
711
|
+
it, so that click needs no fill for it.
|
|
709
712
|
- Read a flagged loader inside its own `<Suspense>`; with no boundary, React
|
|
710
713
|
keeps the page being left on screen until the fill answers.
|
|
711
714
|
- A same-route click (`/app/projects/1` to `/app/projects/2`) prefetches a
|
|
@@ -132,7 +132,7 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
|
|
|
132
132
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
133
133
|
| `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see `/ppr` → The loader lane rule); no response variants. Use callback may contain `loader()`, `loading()`, `transition()` |
|
|
134
134
|
| `layout()` | `layout(Component, () => [...])` — children are required and must contain at least one `path()`; may also hold `loader()`, `loading()`, `intercept()`, nested `layout()` |
|
|
135
|
-
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false, prefetch: false }, use?)` — see below
|
|
135
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false, prefetch: false \| "intent" }, use?)` — see below |
|
|
136
136
|
| `loading()` | Route-level boundary around the optimistic render; inline `<Suspense>` at read sites keeps the destination's chrome visible while only the reads wait |
|
|
137
137
|
| `revalidate()` | Valid **inside a loader() use callback only**; runs on the server (the build hoists it out of the `"use client"` module) |
|
|
138
138
|
| `transition()` | Inside a `path()` use callback only (at most one). ViewTransition config plus a browser-run `when` predicate (see `/view-transitions`); same-route navs in a group already hold without it |
|
|
@@ -623,25 +623,27 @@ shell, such as a `useActionState` form (React suspends it once to encode its
|
|
|
623
623
|
action), is in the first flush too: the router gives React up to three more
|
|
624
624
|
turns before it, and none when the document is already complete.
|
|
625
625
|
|
|
626
|
-
## `prefetch: false` — keep a loader out of
|
|
626
|
+
## `prefetch: false` — keep a loader out of prefetches
|
|
627
627
|
|
|
628
628
|
A `<Link>` prefetch of a group route runs its loaders. Flag the expensive one
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
click
|
|
629
|
+
`prefetch: false` and every prefetch skips it. Flag it `prefetch: "intent"`
|
|
630
|
+
and only a viewport or render prefetch skips it: a hover prefetch, and
|
|
631
|
+
`useRouter().prefetch()`, is intent to click, runs it, and the click sends
|
|
632
|
+
no fill for it (`/loader` → "`prefetch: false`").
|
|
632
633
|
|
|
633
634
|
```tsx
|
|
634
635
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
635
636
|
loader(PriceLoader),
|
|
636
|
-
loader(
|
|
637
|
+
loader(RelatedLoader, { prefetch: "intent" }), // kept out of viewport prefetches
|
|
638
|
+
loader(ReviewsLoader, { prefetch: false }), // kept out of every prefetch
|
|
637
639
|
loading(<ProductSkeleton />),
|
|
638
640
|
]),
|
|
639
641
|
```
|
|
640
642
|
|
|
641
643
|
A click inside the group still shows the destination at once. With a
|
|
642
|
-
|
|
644
|
+
prefetch received, the URL and the other loaders' data move with the click
|
|
643
645
|
too, `useOutlet().pending` ends with that commit, and one follow-up request
|
|
644
|
-
(the fill) runs only the
|
|
646
|
+
(the fill) runs only the loaders it skipped. Read the flagged loader inside its
|
|
645
647
|
own `<Suspense>` to show the page and that fallback at once; with no boundary
|
|
646
648
|
React holds the page being left until the fill answers, as it would with no
|
|
647
649
|
prefetch. Same rules as on a server route (`/loader` → "`prefetch: false`"):
|
|
@@ -660,8 +662,9 @@ rest of the destination; otherwise it is held. A same-route click
|
|
|
660
662
|
- `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
|
|
661
663
|
on a server layout above the group defers nothing for the group's routes.
|
|
662
664
|
- On an `intercept()` loader the flag works as in `urls()`: an in-group
|
|
663
|
-
|
|
664
|
-
|
|
665
|
+
prefetch of the intercept's target defers it, and the click opens the
|
|
666
|
+
modal at once with one fill for it; a hover's runs a loader marked
|
|
667
|
+
`"intent"`.
|
|
665
668
|
- With `ssr: false` on a `ppr` group route the flag works as on a server
|
|
666
669
|
route: deferred unless the prefetch serves the loader from a pin of a
|
|
667
670
|
document-captured shell (`/ppr` → "`prefetch: false` on a `ppr` route").
|
|
@@ -69,6 +69,7 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
|
|
|
69
69
|
| Click link `/shop/product/abc` | `<ProductModal />` in `@modal`, background preserved |
|
|
70
70
|
| Direct URL `/shop/product/abc`, or a reload | Full `<ProductPage />` page |
|
|
71
71
|
| `router.refresh()`, or another tab's action | Full `<ProductPage />` page, as a reload |
|
|
72
|
+
| Link inside the intercept to another intercepted URL | That intercept, over the same source page |
|
|
72
73
|
| Back from the open intercept | The intercept closes; the source page stays |
|
|
73
74
|
| Back/Forward onto the intercept's entry | The intercept over its source page |
|
|
74
75
|
| The same, uncached, from a page other than the source | Full `<ProductPage />` page, as a document load |
|
|
@@ -311,13 +312,16 @@ intercepted: it gets the full page.
|
|
|
311
312
|
### `prefetch: false` on an intercept's loaders
|
|
312
313
|
|
|
313
314
|
`loader(Def, { prefetch: false })` inside `intercept()` keeps that loader out
|
|
314
|
-
of
|
|
315
|
-
|
|
315
|
+
of every prefetch of the target from the source page. `{ prefetch: "intent" }`
|
|
316
|
+
keeps it out of viewport and render prefetches only: a hover prefetch runs
|
|
317
|
+
it, and when every flagged loader of the modal is `"intent"` its click opens
|
|
318
|
+
the modal complete with no fill:
|
|
316
319
|
|
|
317
320
|
```typescript
|
|
318
321
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
319
322
|
loader(ProductLoader),
|
|
320
|
-
loader(
|
|
323
|
+
loader(PhotosLoader, { prefetch: "intent" }), // skipped in a viewport prefetch, runs on hover
|
|
324
|
+
loader(ReviewsLoader, { prefetch: false }), // skipped in every prefetch
|
|
321
325
|
loading(<ModalSkeleton />),
|
|
322
326
|
]);
|
|
323
327
|
```
|
|
@@ -330,7 +334,7 @@ click shows nothing until its response. The modal's loaders resolve together,
|
|
|
330
334
|
so its `loading()` shows until the last one arrives. A handler that reads the
|
|
331
335
|
flagged loader with `ctx.use()` before its first `await` (or one with no
|
|
332
336
|
`loading()`) gets it run in the prefetch instead: one run, no fill for it.
|
|
333
|
-
`
|
|
337
|
+
A `prefetch` option on `loading()` inside `intercept()` defers nothing.
|
|
334
338
|
|
|
335
339
|
A prefetch of a route an intercept targets, or one sent from such a route, is
|
|
336
340
|
stored per source page, whether or not the intercept applies from that page
|
package/skills/links/SKILL.md
CHANGED
|
@@ -619,14 +619,15 @@ A prefetch is a navigation sent early (a partial request with an
|
|
|
619
619
|
`X-Rango-Prefetch` header), and by default the server renders everything for
|
|
620
620
|
it: every handler and every loader. That is the point, a click then commits
|
|
621
621
|
without a request, but it is also forty renders for a grid of forty links.
|
|
622
|
-
Keep the expensive part out of
|
|
623
|
-
the route, not on the link:
|
|
622
|
+
Keep the expensive part out of prefetches on the route, not on the link:
|
|
624
623
|
|
|
625
624
|
```tsx
|
|
626
|
-
// skip one loader
|
|
625
|
+
// skip one loader in every prefetch
|
|
627
626
|
loader(ReviewsLoader, { prefetch: false }),
|
|
628
627
|
// skip what the fallback covers
|
|
629
628
|
loading(<OrdersSkeleton />, { prefetch: false }),
|
|
629
|
+
// skip it in viewport and render prefetches only: a hover runs it
|
|
630
|
+
loader(RelatedLoader, { prefetch: "intent" }),
|
|
630
631
|
```
|
|
631
632
|
|
|
632
633
|
A click that adopts such a prefetch commits what was prefetched, shows the
|
|
@@ -635,13 +636,15 @@ is new to the page is deferred: a segment already on screen (the layout of the
|
|
|
635
636
|
section you are in, the route itself on `/product/a` to `/product/b`) is
|
|
636
637
|
prefetched as usual.
|
|
637
638
|
|
|
638
|
-
A hover prefetch is treated as intent
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
639
|
+
A hover prefetch is treated as intent (`X-Rango-Prefetch: intent`, where a
|
|
640
|
+
viewport or render prefetch sends `1`); so is `useRouter().prefetch()`. It
|
|
641
|
+
runs the work marked `prefetch: "intent"` and still skips the work marked
|
|
642
|
+
`prefetch: false`, so on a route whose flags are all `"intent"` its click
|
|
643
|
+
commits complete with no extra request. A hover sends that request only when
|
|
644
|
+
the link has no prefetch cached or in flight: on a viewport link whose
|
|
645
|
+
prefetch has already answered, it sends nothing, and the click fetches the
|
|
646
|
+
deferred part as above. Middleware that skips side effects in prefetches
|
|
647
|
+
should test for the header's presence, not for `=== "1"`. See
|
|
645
648
|
`/loader` → "`prefetch: false`" and `/route` → "`loading()` options". To stop
|
|
646
649
|
a link or a whole section from prefetching at all, use the `prefetch` prop or
|
|
647
650
|
`data-prefetch-scope` above.
|
|
@@ -668,8 +671,8 @@ The answer is page-only (`x-rsc-prefetch-scope: source`) when:
|
|
|
668
671
|
|
|
669
672
|
- An intercept targets the route, or the page that sent the prefetch,
|
|
670
673
|
whether or not it applies here.
|
|
671
|
-
- The answer defers a `prefetch: false`
|
|
672
|
-
|
|
674
|
+
- The answer defers a `prefetch: false` or `prefetch: "intent"` loader or
|
|
675
|
+
`loading()` that the page does not show yet.
|
|
673
676
|
- A `revalidate()` on the target read `currentUrl` and kept its segment.
|
|
674
677
|
- A `revalidate()` on the route, or on a segment inside its `path()`, read
|
|
675
678
|
the page being left and kept the segment through the route's default
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -1191,20 +1191,22 @@ The costs and constraints:
|
|
|
1191
1191
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
1192
1192
|
loader-heavy shape makes it most useful.
|
|
1193
1193
|
|
|
1194
|
-
## `prefetch: false` — Keep a Loader Out of
|
|
1194
|
+
## `prefetch: false` and `prefetch: "intent"` — Keep a Loader Out of Prefetches
|
|
1195
1195
|
|
|
1196
1196
|
A `<Link>` prefetch is a navigation sent early: the server renders every
|
|
1197
1197
|
handler and every loader for it. With the default `"viewport"` strategy that
|
|
1198
1198
|
means a grid of forty product links runs forty copies of your slowest loader
|
|
1199
1199
|
for pages most visitors never open. Pass `prefetch: false` to keep one loader
|
|
1200
|
-
out of
|
|
1200
|
+
out of every prefetch, or `prefetch: "intent"` to keep it out of the
|
|
1201
|
+
speculative ones only:
|
|
1201
1202
|
|
|
1202
1203
|
```tsx
|
|
1203
1204
|
import { Suspense } from "react";
|
|
1204
1205
|
|
|
1205
1206
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
1206
|
-
loader(PriceLoader), // cheap:
|
|
1207
|
-
loader(
|
|
1207
|
+
loader(PriceLoader), // cheap: runs in every prefetch
|
|
1208
|
+
loader(RelatedLoader, { prefetch: "intent" }), // skipped in a viewport prefetch, runs on hover
|
|
1209
|
+
loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in every prefetch
|
|
1208
1210
|
loading(<ProductSkeleton />),
|
|
1209
1211
|
]),
|
|
1210
1212
|
```
|
|
@@ -1212,22 +1214,30 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
1212
1214
|
(`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
|
|
1213
1215
|
`{ ssr: false, prefetch: false }` if you need both.)
|
|
1214
1216
|
|
|
1215
|
-
|
|
1217
|
+
| Request | unflagged | `prefetch: false` | `prefetch: "intent"` |
|
|
1218
|
+
| ------------------------------------------------------------ | ---------- | ------------------------ | ------------------------ |
|
|
1219
|
+
| Document, navigation | runs | runs | runs |
|
|
1220
|
+
| Viewport or render prefetch (`X-Rango-Prefetch: 1`) | runs | skipped, filled at click | skipped, filled at click |
|
|
1221
|
+
| Hover, `useRouter().prefetch()` (`X-Rango-Prefetch: intent`) | runs | skipped, filled at click | runs, no fill |
|
|
1222
|
+
| A prefetch already cached or in flight when the hover lands | used as is | used as is | used as is |
|
|
1223
|
+
|
|
1224
|
+
A hover means the visitor is about to click. A hover prefetch (on a
|
|
1216
1225
|
`"hover"`, `"viewport"` or `"adaptive"` link, or a delegated plain anchor) and
|
|
1217
|
-
`useRouter().prefetch()`
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1226
|
+
`useRouter().prefetch()` run a loader marked `"intent"` with the rest of the
|
|
1227
|
+
page; when every flagged loader and `loading()` on the route is marked
|
|
1228
|
+
`"intent"`, the click commits complete and sends no second request. A hover
|
|
1229
|
+
sends its prefetch only when the link has none cached or in flight: on a link
|
|
1230
|
+
a viewport prefetch already answered, the hover sends nothing and the click
|
|
1231
|
+
fills the deferred loader as below. The cost of `"intent"` is plain: each
|
|
1232
|
+
hover that misses the prefetch cache runs the loader, so a pointer passing
|
|
1233
|
+
over a list of `"hover"` links runs it once per link. Keep a loader that is
|
|
1234
|
+
too expensive for that on `false`. Touch devices have no hover (`"adaptive"`
|
|
1235
|
+
resolves to viewport there) and keep the fill.
|
|
1236
|
+
|
|
1237
|
+
What happens in a prefetch that skips the loader:
|
|
1238
|
+
|
|
1239
|
+
- The prefetch does not run the loader and does not read its `cache()`; the
|
|
1240
|
+
payload marks it deferred. The rest of the page prefetches as usual.
|
|
1231
1241
|
- Only on a loader new to the page. A loader already on screen is never
|
|
1232
1242
|
deferred: if its revalidation (its `revalidate()` or the default) re-runs
|
|
1233
1243
|
it, it runs in the prefetch with the rest of the destination, exactly as
|
|
@@ -1277,18 +1287,18 @@ function Reviews() {
|
|
|
1277
1287
|
|
|
1278
1288
|
The limits:
|
|
1279
1289
|
|
|
1280
|
-
- **A click before its
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
prefetch
|
|
1286
|
-
flag, and sends the fill when the prefetch answers. The page shows no later
|
|
1290
|
+
- **A click before its prefetch has answered waits for it, then fetches the
|
|
1291
|
+
deferred part.** The flag is there to keep expensive or slow work out of
|
|
1292
|
+
prefetches, so that an API that is costly to call is not called for every
|
|
1293
|
+
link a visitor might click. A click that comes while the link's prefetch is
|
|
1294
|
+
still in flight reuses that prefetch, as it does without the flag, and
|
|
1295
|
+
sends the fill when the prefetch answers. The page shows no later
|
|
1287
1296
|
than with no prefetch; the deferred part shows its fallback until the fill
|
|
1288
1297
|
lands, and can complete later than a click with no prefetch, by up to what
|
|
1289
1298
|
was left of the prefetch. That is the price of keeping the work out of the
|
|
1290
|
-
prefetch. A click while a hover's prefetch is in flight
|
|
1291
|
-
fill: the loader is already
|
|
1299
|
+
prefetch. A click while a hover's prefetch is in flight, on a route whose
|
|
1300
|
+
flags are all `"intent"`, adopts it with no fill: the loader is already
|
|
1301
|
+
running in it.
|
|
1292
1302
|
- **A navigation started before the fill answers cancels it.** It is
|
|
1293
1303
|
cancelled when the newer navigation starts, not when that one commits. If
|
|
1294
1304
|
the newer navigation then ends without replacing the page (a redirect the
|
|
@@ -1313,8 +1323,9 @@ The limits:
|
|
|
1313
1323
|
every request kind (a document load included, so you see it the first time
|
|
1314
1324
|
you open the page in development) if it calls `await ctx.rendered()`. The
|
|
1315
1325
|
fill does not re-run the entry's handler, so there is no render to wait for
|
|
1316
|
-
and no handle data to read.
|
|
1317
|
-
|
|
1326
|
+
and no handle data to read. That holds for `"intent"` too, on every request.
|
|
1327
|
+
Remove the `prefetch` flag from the loader, or stop calling
|
|
1328
|
+
`ctx.rendered()`. Pushing a handle from the loader is fine.
|
|
1318
1329
|
- **`ctx.set()` values from held handlers are not visible.** The fill runs the
|
|
1319
1330
|
deferred work and middleware only. A handler or layout the client already
|
|
1320
1331
|
holds does not run again, so a variable it set with `ctx.set()` is missing
|
|
@@ -1376,16 +1387,17 @@ The limits:
|
|
|
1376
1387
|
need it), or drop the flag from a loader the page cannot render without.
|
|
1377
1388
|
|
|
1378
1389
|
- **`clientUrls()` routes take it on `loader()`.** In a `clientUrls()`
|
|
1379
|
-
definition `loader(Def, { prefetch: false })`
|
|
1390
|
+
definition `loader(Def, { prefetch: false })` and `{ prefetch: "intent" }`
|
|
1391
|
+
work as here: a click
|
|
1380
1392
|
inside the group shows the destination at once, commits the prefetched
|
|
1381
1393
|
data with the URL, and one fill runs the flagged loaders (see
|
|
1382
1394
|
`/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
|
|
1383
1395
|
on a group route, from the group or from a server layout above it.
|
|
1384
|
-
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
click opens the modal at once, with its `loading()`, and one fill,
|
|
1388
|
-
from the source, runs the loader. The page under the modal stays. With no
|
|
1396
|
+
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a prefetch of
|
|
1397
|
+
the intercept's target from its source page defers the flagged loader (a
|
|
1398
|
+
hover's runs a loader marked `"intent"`, and its click sends no fill for
|
|
1399
|
+
it): the click opens the modal at once, with its `loading()`, and one fill,
|
|
1400
|
+
sent from the source, runs the loader. The page under the modal stays. With no
|
|
1389
1401
|
`loading()` the slot is empty until the fill answers, as on a plain click
|
|
1390
1402
|
until its response. The modal's loaders resolve together: it shows its
|
|
1391
1403
|
`loading()` until every one has arrived. A flagged `loading()` inside
|
|
@@ -1401,13 +1413,14 @@ The limits:
|
|
|
1401
1413
|
`/caching`, `/prerender`, `/ppr` → "`prefetch: false` on a `ppr` route").
|
|
1402
1414
|
|
|
1403
1415
|
To skip a handler along with its loaders, flag the `loading()` instead (see
|
|
1404
|
-
`/route` → "`loading()` options"). `loader(Def, { prefetch
|
|
1405
|
-
|
|
1416
|
+
`/route` → "`loading()` options"). `loader(Def, { prefetch })` accepts a
|
|
1417
|
+
boolean or `"intent"`; anything else throws
|
|
1418
|
+
`loader() prefetch must be a boolean or "intent"`.
|
|
1406
1419
|
|
|
1407
1420
|
Test it with `serveShellRequest(router, url, { partial: { prefetch: true } })`
|
|
1408
1421
|
and `result.readDeferred()` (see `/testing`). That is a speculative prefetch;
|
|
1409
1422
|
add `headers: { "X-Rango-Prefetch": "intent" }` to serve a hover's, which
|
|
1410
|
-
defers
|
|
1423
|
+
runs the `"intent"` loaders and defers the `false` ones.
|
|
1411
1424
|
|
|
1412
1425
|
## Fetchable Loaders
|
|
1413
1426
|
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -1176,7 +1176,7 @@ document request captured:
|
|
|
1176
1176
|
| Replays a shell a document request captured (`HIT`) | Served from its pin, not deferred, never run; the click sends nothing |
|
|
1177
1177
|
| Replays a navigation-only shell, or misses (cold page) | Deferred: the click sends one fill that runs it |
|
|
1178
1178
|
| Serves an explicit `cache()` hit | Deferred: the click sends one fill that runs it |
|
|
1179
|
-
| Hover prefetch (`X-Rango-Prefetch: intent`) |
|
|
1179
|
+
| Hover prefetch (`X-Rango-Prefetch: intent`) | As the rows above; a loader marked `prefetch: "intent"` runs instead |
|
|
1180
1180
|
|
|
1181
1181
|
A cold prefetch still schedules the background capture that stores the
|
|
1182
1182
|
page's navigation shell, and that capture runs the loader once. Once a
|
package/skills/route/SKILL.md
CHANGED
|
@@ -242,10 +242,11 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
242
242
|
|
|
243
243
|
`loading(fallback, options?)` takes two options:
|
|
244
244
|
|
|
245
|
-
| Option
|
|
246
|
-
|
|
|
247
|
-
| `ssr: false`
|
|
248
|
-
| `prefetch: false`
|
|
245
|
+
| Option | Effect |
|
|
246
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
247
|
+
| `ssr: false` | Skip the fallback on document requests; it shows on client navigations only. |
|
|
248
|
+
| `prefetch: false` | Keep everything behind this fallback out of every prefetch, hovers included; the click shows the fallback and fetches that part itself. |
|
|
249
|
+
| `prefetch: "intent"` | The same for viewport and render prefetches; a hover prefetch or `useRouter().prefetch()` runs it, and its click sends no second request. |
|
|
249
250
|
|
|
250
251
|
```tsx
|
|
251
252
|
path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
@@ -255,13 +256,17 @@ path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
|
255
256
|
```
|
|
256
257
|
|
|
257
258
|
**What `prefetch: false` defers.** The flag applies when the entry is new to
|
|
258
|
-
the browser: the page the prefetch is sent from does not show it. A
|
|
259
|
-
|
|
259
|
+
the browser: the page the prefetch is sent from does not show it. A prefetch
|
|
260
|
+
then skips the entry's handler, its loaders, its orphan layouts and its
|
|
260
261
|
parallel slots. On a `layout()` the fallback covers the whole outlet, so every
|
|
261
262
|
deeper entry is skipped with it. A parallel slot with its own flagged
|
|
262
263
|
`loading()` is a separate unit: the route beside it still renders in the
|
|
263
264
|
prefetch. The prefetch payload carries a placeholder for each skipped piece,
|
|
264
|
-
marked deferred, with its fallback: the page's skeleton.
|
|
265
|
+
marked deferred, with its fallback: the page's skeleton. `prefetch: "intent"`
|
|
266
|
+
defers the same pieces in a viewport or render prefetch; a hover's prefetch
|
|
267
|
+
renders them, so a route whose flags are all `"intent"` is prefetched whole
|
|
268
|
+
on hover. Everything below about `false` holds for `"intent"` in the
|
|
269
|
+
prefetches that defer it.
|
|
265
270
|
|
|
266
271
|
**An entry the browser already holds is never deferred.** Inside a section
|
|
267
272
|
whose `layout()` has a flagged `loading()`, the layout is on screen, so its
|
|
@@ -347,7 +352,7 @@ the block's work, its loaders included, out of prefetches. See `/caching`,
|
|
|
347
352
|
flag does nothing there. The last `loading()` on an entry wins, flag
|
|
348
353
|
included.
|
|
349
354
|
- A loader behind the fallback cannot call `ctx.rendered()`; it throws on every
|
|
350
|
-
request kind. See `/loader` → "`prefetch: false`" for this and the other
|
|
355
|
+
request kind, for either flag. See `/loader` → "`prefetch: false`" for this and the other
|
|
351
356
|
loader limits (`ctx.set()` values, late `redirect()`/`notFound()`, a loader a
|
|
352
357
|
handler awaits).
|
|
353
358
|
- A layout the client already holds is not deferred (its fallback would not
|
|
@@ -373,7 +378,8 @@ the block's work, its loaders included, out of prefetches. See `/caching`,
|
|
|
373
378
|
- `clientUrls()` routes and `intercept()` ignore the flag.
|
|
374
379
|
|
|
375
380
|
To defer a single loader and keep the handler in the prefetch, use
|
|
376
|
-
`loader(Def, { prefetch: false })` instead (see
|
|
381
|
+
`loader(Def, { prefetch: false })` or `{ prefetch: "intent" }` instead (see
|
|
382
|
+
`/loader`).
|
|
377
383
|
|
|
378
384
|
A `loading()` skeleton that has appeared stays at least about 300 ms before React reveals what replaces it (React's fallback throttle, [#1078](https://github.com/rangojs/rango/issues/1078)); see "Streaming with Suspense" in [loader](../loader/SKILL.md).
|
|
379
385
|
|
|
@@ -20,13 +20,13 @@ A `Prerender` route is served as in production: from the artifact `router.matchF
|
|
|
20
20
|
|
|
21
21
|
### Options — `ServeShellRequestOptions`
|
|
22
22
|
|
|
23
|
-
| Field | Type | Meaning
|
|
24
|
-
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
-
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store.
|
|
26
|
-
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them.
|
|
27
|
-
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`.
|
|
28
|
-
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends the speculative kind a viewport or render `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false` work is deferred; add `headers: { "X-Rango-Prefetch": "intent" }` to send a hover's or `prefetch()`'s intent prefetch instead, which defers
|
|
29
|
-
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`.
|
|
23
|
+
| Field | Type | Meaning |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
|
|
26
|
+
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
|
|
27
|
+
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
|
|
28
|
+
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends the speculative kind a viewport or render `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false` and `prefetch: "intent"` work is deferred; add `headers: { "X-Rango-Prefetch": "intent" }` to send a hover's or `prefetch()`'s intent prefetch instead, which runs the `"intent"` work and defers the `false` work. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error segment of a thrown action's boundary the page at `from` shows (`_rsc_boundary`), as the background revalidation an earlier action owes that page sends it, with `stale: true` and no `segments`: the response renders every segment above the boundary and nothing at or below it, claims the boundary in `matched` and sends no segment for it. The server reads it only beside `stale`. |
|
|
29
|
+
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
|
|
30
30
|
|
|
31
31
|
### Returns — `ServeShellRequestResult`
|
|
32
32
|
|
|
@@ -294,6 +294,18 @@ expect(await fill.readDeferred()).toEqual([]); // a fill defers nothing
|
|
|
294
294
|
|
|
295
295
|
`deliveredIds` is the payload's `metadata.matched`, read from `prefetch.flight`. Count the loader's runs in your own fixture: the prefetch ran it zero times, the fill once.
|
|
296
296
|
|
|
297
|
+
For a loader marked `prefetch: "intent"`, send the hover's prefetch too: it runs that loader and still defers a `prefetch: false` one beside it.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const hover = await serveShellRequest(router, "/product/1", {
|
|
301
|
+
cacheStore,
|
|
302
|
+
headers: { "X-Rango-Prefetch": "intent" },
|
|
303
|
+
partial: { from: "/", prefetch: true },
|
|
304
|
+
});
|
|
305
|
+
// loader(RelatedLoader, { prefetch: "intent" }) ran; ReviewsLoader is deferred.
|
|
306
|
+
expect(await hover.readDeferred()).toHaveLength(1);
|
|
307
|
+
```
|
|
308
|
+
|
|
297
309
|
### Intercepts
|
|
298
310
|
|
|
299
311
|
A prefetch of an `intercept()`'s target from its source page defers the intercept's flagged loaders, loader segments of its slot (`${parent}.@modalD${i}.${$$id}`). The browser sends the fill of an intercept from the source, with the intercept source header: pass both.
|