@rangojs/router 0.34.1 → 0.35.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/README.md +1 -1
- package/dist/bin/{chunk-4HHFQSDC.js → chunk-3CQXDX3A.js} +5 -5
- package/dist/bin/{client-urls-projection-4Q5EQDAL.js → client-urls-projection-2GBDM5YK.js} +1 -1
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-discovery-RY6NMQM3.js → runtime-discovery-PA3V3IFQ.js} +2 -2
- package/dist/bin/{temp-rsc-server-F7AHOPV4.js → temp-rsc-server-WDXEHDY7.js} +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +1 -1
- package/dist/types/client-urls/types.d.ts +6 -6
- package/dist/types/decode-loader-results.d.ts +1 -1
- package/dist/types/errors.d.ts +3 -3
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/loader-store.d.ts +1 -1
- package/dist/types/outlet-context.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +5 -5
- package/dist/types/router/loader-resolution.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +7 -7
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
- package/dist/types/server/context.d.ts +3 -3
- package/dist/types/server/request-context.d.ts +1 -1
- package/dist/types/ssr/index.d.ts +1 -1
- package/dist/types/ssr/ssr-root.d.ts +1 -1
- package/dist/types/ssr-suspension-warning.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +19 -19
- package/dist/types/types/segments.d.ts +2 -2
- package/dist/types/urls/path-helper-types.d.ts +4 -4
- package/dist/types/urls/pattern-types.d.ts +1 -1
- package/dist/types/vite/plugin-types.d.ts +1 -1
- package/dist/vite/index.js +7 -7
- package/package.json +1 -1
- package/skills/app-structure/e-commerce.md +1 -1
- package/skills/breadcrumbs/SKILL.md +5 -5
- package/skills/cache-guide/SKILL.md +4 -4
- package/skills/caching/SKILL.md +1 -1
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +20 -20
- package/skills/comparison/references/framework-comparison.md +6 -6
- package/skills/defer-hydration/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +5 -5
- package/skills/hooks/data.md +1 -1
- package/skills/hooks/handle-and-actions.md +9 -9
- package/skills/loader/SKILL.md +52 -47
- package/skills/middleware/SKILL.md +2 -2
- package/skills/migrate-nextjs/SKILL.md +10 -10
- package/skills/migrate-react-router/SKILL.md +1 -1
- package/skills/migrate-react-router/component-migration.md +2 -2
- package/skills/migrate-react-router/data-and-actions.md +2 -2
- package/skills/migrate-react-router/route-mapping.md +2 -2
- package/skills/parallel/SKILL.md +2 -2
- package/skills/ppr/SKILL.md +91 -92
- package/skills/prerender/SKILL.md +5 -5
- package/skills/rango/SKILL.md +7 -7
- package/skills/route/SKILL.md +1 -1
- package/skills/router-setup/SKILL.md +2 -2
- package/skills/scripts/SKILL.md +3 -3
- package/skills/shell-manifest/SKILL.md +3 -3
- package/skills/testing/serve-shell-request.md +1 -1
- package/skills/theme/SKILL.md +2 -2
- package/skills/typesafety/env-and-bindings.md +2 -2
- package/skills/use-cache/SKILL.md +1 -1
- package/skills/ux-architecture/SKILL.md +7 -7
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +1 -1
- package/src/client-urls/client-urls.ts +16 -9
- package/src/client-urls/server-projection.ts +4 -4
- package/src/client-urls/types.ts +6 -6
- package/src/decode-loader-results.ts +1 -1
- package/src/errors.ts +9 -9
- package/src/handles/meta.ts +2 -2
- package/src/loader-store.ts +1 -1
- package/src/outlet-context.ts +1 -1
- package/src/route-definition/dsl-helpers.ts +4 -8
- package/src/route-definition/helpers-types.ts +5 -5
- package/src/router/loader-resolution.ts +4 -4
- package/src/router/segment-resolution/loader-cache.ts +8 -8
- package/src/router/segment-resolution/prefetch-deferral.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +3 -3
- package/src/router/types.ts +1 -1
- package/src/rsc/shell-capture-constants.ts +3 -3
- package/src/rsc/shell-capture.ts +12 -12
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +5 -5
- package/src/server/cookie-store.ts +1 -1
- package/src/server/request-context.ts +1 -1
- package/src/ssr/index.tsx +6 -6
- package/src/ssr/ssr-root.tsx +1 -1
- package/src/ssr-suspension-warning.ts +4 -4
- package/src/types/loader-types.ts +19 -19
- package/src/types/segments.ts +2 -2
- package/src/urls/path-helper-types.ts +4 -4
- package/src/urls/pattern-types.ts +1 -1
- package/src/vite/discovery/shell-prerender-phase.ts +1 -1
- package/src/vite/plugin-types.ts +1 -1
|
@@ -21,7 +21,7 @@ The handler code and its imports are removed from the production bundle.
|
|
|
21
21
|
## Not this skill if…
|
|
22
22
|
|
|
23
23
|
- You want a cached HTML shell captured at runtime, with holes staying live
|
|
24
|
-
per request (loaders without `ssr:
|
|
24
|
+
per request (loaders without `ssr: "await"` run per request; `ssr: "await"`
|
|
25
25
|
loaders bake) — see `/ppr`.
|
|
26
26
|
- You want runtime segment caching with TTL/SWR — that is the `cache()` DSL:
|
|
27
27
|
see `/caching`. Prerender is the same cache filled at build time.
|
|
@@ -355,12 +355,12 @@ own shell identity and is captured at runtime. The runtime shell store is read
|
|
|
355
355
|
first, so a runtime capture supersedes the baked entry once it exists.
|
|
356
356
|
|
|
357
357
|
Handle values behave as on any `ppr` shell (`/ppr` → "Handles on a shell
|
|
358
|
-
HIT"): a value an `ssr:
|
|
358
|
+
HIT"): a value an `ssr: "await"` loader pushes settled is in the shell's HTML and
|
|
359
359
|
in the data a HIT hydrates with, and one it pushes as a promise arrives after
|
|
360
360
|
hydration. The prerendered payload holds only the handler's pushes (loaders
|
|
361
361
|
never run at build), so the shell entry keeps the loader pushes its HTML
|
|
362
362
|
rendered, on a build-baked shell and on one captured at runtime alike. A shell
|
|
363
|
-
stored by router 0.21 or earlier lacks them, and a promise-free `ssr:
|
|
363
|
+
stored by router 0.21 or earlier lacks them, and a promise-free `ssr: "await"`
|
|
364
364
|
loader's settled push fails hydration on its HITs until the shell is replaced
|
|
365
365
|
(#1057).
|
|
366
366
|
|
|
@@ -384,7 +384,7 @@ entry STILL serves and a runtime recapture is scheduled that upgrades it in plac
|
|
|
384
384
|
`Prerender` handler is evicted from the production bundle, that recapture never
|
|
385
385
|
re-runs the handler — it replays the same build-time segments and only refreshes
|
|
386
386
|
what the capture reads again: `cache()`-scoped data baked into the shell, and
|
|
387
|
-
the data of `ssr:
|
|
387
|
+
the data of `ssr: "await"` loaders, which run at every capture. **If the shell
|
|
388
388
|
bakes neither, `ttl` has nothing to refresh** — reach for `updateTag` (or a
|
|
389
389
|
redeploy) instead of a shorter `ttl`.
|
|
390
390
|
|
|
@@ -661,7 +661,7 @@ Passthrough entries are logged distinctly:
|
|
|
661
661
|
Loaders on pre-rendered routes run at request time. They are bundled normally
|
|
662
662
|
and need `cache()` for caching. Do not use build-only APIs in loaders.
|
|
663
663
|
|
|
664
|
-
The exception is an `ssr:
|
|
664
|
+
The exception is an `ssr: "await"` loader on a `Prerender` + `ppr` route: the
|
|
665
665
|
build-time shell capture runs it and bakes its settled data into the shell,
|
|
666
666
|
and a shell HIT runs it only when its return carries promises (`/ppr` → "The
|
|
667
667
|
bake lane"). The Flight payload collection itself still skips loaders.
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -27,7 +27,7 @@ with the shape, then pick a primitive.
|
|
|
27
27
|
**stream**, so data latency overlaps first paint instead of blocking it (a
|
|
28
28
|
cache hit streams UI instantly while loaders resolve fresh alongside). Opt into
|
|
29
29
|
caching explicitly. See `/loader` → "Parallel and streaming". The one
|
|
30
|
-
exception is a `ppr` route's `ssr:
|
|
30
|
+
exception is a `ppr` route's `ssr: "await"` loader: it bakes into the shell
|
|
31
31
|
and runs on a shell HIT only when its return carries promises (`/ppr`).
|
|
32
32
|
- **One identity, one store** — loaders, handles, cached fns, and actions are all
|
|
33
33
|
`path#export`; all caches share one store. Entries expire by TTL/SWR, and are
|
|
@@ -71,7 +71,7 @@ are immune to partial-revalidation staleness by construction.
|
|
|
71
71
|
|
|
72
72
|
1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
|
|
73
73
|
pass — full renders, action revalidations, cache hits, PPR shell HITs (on a
|
|
74
|
-
`ppr` route, keep it off `ssr:
|
|
74
|
+
`ppr` route, keep it off `ssr: "await"`, which bakes it into the shell).
|
|
75
75
|
Nothing to keep in sync. If the data can be a loader, make it a loader.
|
|
76
76
|
2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
|
|
77
77
|
including post-action revalidation and PE re-renders, so its variables are
|
|
@@ -117,7 +117,7 @@ stated, greppable contract.
|
|
|
117
117
|
| modal / soft navigation | `intercept()` | /intercept |
|
|
118
118
|
| route group of client components | `clientUrls()` in `"use client"` | /client-urls |
|
|
119
119
|
| set meta/breadcrumbs from loader data | `ctx.use(Handle)` in the loader | /loader |
|
|
120
|
-
| guarantee loader output in the SSR HTML | `loader(L, { ssr:
|
|
120
|
+
| guarantee loader output in the SSR HTML | `loader(L, { ssr: "await" })` | /loader |
|
|
121
121
|
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
122
122
|
| make a route ready before traffic | `router.prerender({ env })(url)` | /prerender |
|
|
123
123
|
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
@@ -130,7 +130,7 @@ stated, greppable contract.
|
|
|
130
130
|
- `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
|
|
131
131
|
- **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
|
|
132
132
|
- Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
|
|
133
|
-
The exception is a `ppr` route's `ssr:
|
|
133
|
+
The exception is a `ppr` route's `ssr: "await"` loader: it bakes into the shell
|
|
134
134
|
and runs on a shell HIT only when its return carries promises.
|
|
135
135
|
- **The consumption-lane rule.** For every shared artifact (`cache()`,
|
|
136
136
|
`"use cache"`, the PPR shell): server-side handler consumption
|
|
@@ -143,14 +143,14 @@ stated, greppable contract.
|
|
|
143
143
|
`loader()` binding, or the cached function itself) and REFUSE a PPR capture: the route serves uncached until
|
|
144
144
|
the read moves to a live loader. Client-side consumption
|
|
145
145
|
(`useLoader` in a `"use client"` component) is the LIVE lane. DSL
|
|
146
|
-
`loader()` segments follow their PPR lane (only `ssr:
|
|
146
|
+
`loader()` segments follow their PPR lane (only `ssr: "await"` bakes; see
|
|
147
147
|
`/ppr` → The loader lane rule).
|
|
148
148
|
- **A PPR shell HIT never runs a handler.** Everything a handler produces
|
|
149
149
|
(promises it passes under `<Suspense>`, async server components, handle
|
|
150
150
|
pushes, loaders it awaits) is shell material, as under `cache()`; a HIT
|
|
151
|
-
runs middleware and live loaders only (an `ssr:
|
|
151
|
+
runs middleware and live loaders only (an `ssr: "await"` loader runs there
|
|
152
152
|
only when its return carries promises). Live data belongs in a loader without
|
|
153
|
-
`ssr:
|
|
153
|
+
`ssr: "await"`, read with `useLoader` under `loading()` or an inline
|
|
154
154
|
`<Suspense>`.
|
|
155
155
|
- Inside `"use cache"`: `cookies()`/`headers()`/`ctx.request.headers` and `ctx` side-effects
|
|
156
156
|
(`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
|
package/skills/route/SKILL.md
CHANGED
|
@@ -482,7 +482,7 @@ loader re-runs on every hit — but a `"use cache"` function a loader calls is
|
|
|
482
482
|
not, and neither is a loader bound with its own `cache()` without a `key()` (any
|
|
483
483
|
`key()` switches the check off, so it must include the value; `/loader` → "Cache
|
|
484
484
|
Key"). On a `ppr` route the exemption does not cover a loader the shell capture
|
|
485
|
-
waits for (`ssr:
|
|
485
|
+
waits for (`ssr: "await"`, or awaited by a handler via `ctx.use()`): the read
|
|
486
486
|
refuses the capture. Cacheable vars (the default) can be read freely inside
|
|
487
487
|
cache scopes. See `/cache-guide` → "Context Variable Cache Safety".
|
|
488
488
|
|
|
@@ -254,7 +254,7 @@ Notes:
|
|
|
254
254
|
them.
|
|
255
255
|
- `progressiveChunkSize` sets React's Fizz `progressiveChunkSize` for document
|
|
256
256
|
renders and PPR shell capture. Unset, documents whose route has a
|
|
257
|
-
`loader(Def, { ssr:
|
|
257
|
+
`loader(Def, { ssr: "await" })` raise it automatically so that content stays
|
|
258
258
|
in place; see `/loader`.
|
|
259
259
|
- `encryptionKey` is the key that encrypts inline server-action bound
|
|
260
260
|
arguments: base64-encoded 32 bytes (`openssl rand -base64 32`). Pass it as
|
|
@@ -504,7 +504,7 @@ When `notFound()` is thrown, the router uses the first of:
|
|
|
504
504
|
Handler-thrown and no-match 404s always set HTTP status 404. A loader-thrown
|
|
505
505
|
`notFound()` on a document request always renders the not-found UI, but the
|
|
506
506
|
404 status is set only if the loader rejects before the document Response is
|
|
507
|
-
constructed (loaders stream). Register the loader as `loader(Def, { ssr:
|
|
507
|
+
constructed (loaders stream). Register the loader as `loader(Def, { ssr: "await" })` to
|
|
508
508
|
make the 404 status deterministic. On client navigations the 404 UI swaps in
|
|
509
509
|
with the URL preserved and the payload stays 200. See `/loader` → "Loader
|
|
510
510
|
Authority".
|
package/skills/scripts/SKILL.md
CHANGED
|
@@ -112,11 +112,11 @@ during navigation.
|
|
|
112
112
|
> settles before the handler barrier. A push that lands after a slow fetch
|
|
113
113
|
> arrives post-hydration — and for an inline/ordered script the frozen set
|
|
114
114
|
> means it is silently dropped. If a loader must contribute an inline script
|
|
115
|
-
> to the document, register it `loader(Def, { ssr:
|
|
115
|
+
> to the document, register it `loader(Def, { ssr: "await" })` so the
|
|
116
116
|
> document render awaits the push; otherwise push from a handler (or use an
|
|
117
117
|
> `async` config, which stays reactive). On a `ppr` route a loader without
|
|
118
|
-
> `ssr:
|
|
119
|
-
> script is dropped on every shell HIT; with `ssr:
|
|
118
|
+
> `ssr: "await"` never runs during the shell capture, so its inline/ordered
|
|
119
|
+
> script is dropped on every shell HIT; with `ssr: "await"` the push bakes into
|
|
120
120
|
> the shell.
|
|
121
121
|
|
|
122
122
|
**Nonce caveat for soft-nav async.** The "nonce is applied automatically" claim
|
|
@@ -9,7 +9,7 @@ argument-hint:
|
|
|
9
9
|
Use this when a cached or prerendered shell has dynamic holes, and the live
|
|
10
10
|
data layer needs to know **what the shell actually contains** — which
|
|
11
11
|
products, which slots, which keys. The frozen render describes itself
|
|
12
|
-
through a handle; live loaders (no `ssr:
|
|
12
|
+
through a handle; live loaders (no `ssr: "await"`) read that description and
|
|
13
13
|
fetch exactly the dynamic data the shell needs, in one batch.
|
|
14
14
|
|
|
15
15
|
Canonical case: a prerendered product list where prices must stay live.
|
|
@@ -36,7 +36,7 @@ batched.
|
|
|
36
36
|
cache miss for `cache()`. (Loader bodies can push handles too — see `/loader`
|
|
37
37
|
— but a live loader's push is request-time and is NOT part of the replayed
|
|
38
38
|
artifact; a manifest handle must be pushed by the code that gets frozen with
|
|
39
|
-
the shell. An `ssr:
|
|
39
|
+
the shell. An `ssr: "await"` loader on a `ppr` route is the exception: its
|
|
40
40
|
settled pushes are recorded with the shell. On a HIT, one whose return has no
|
|
41
41
|
promises does not run (unless the capture marked it to run), so the recorded
|
|
42
42
|
pushes are what the page shows; one whose return carries promises runs on
|
|
@@ -157,7 +157,7 @@ cache({ ttl: 600, tags: ["products"] }, () => [
|
|
|
157
157
|
fetchable/standalone loader calls that run outside a route render, in
|
|
158
158
|
handler-invoked loaders (a handler already awaiting the loader via
|
|
159
159
|
`ctx.use()` is a detected deadlock), and in loaders registered with
|
|
160
|
-
`{ ssr:
|
|
160
|
+
`{ ssr: "await" }` (the document render awaits the loader before
|
|
161
161
|
the barrier — a cycle by construction; see `/loader`).
|
|
162
162
|
- **The reading loader serializes after the shell.** `await ctx.rendered()`
|
|
163
163
|
deliberately gives up loader/render parallelism — on a miss the loader
|
|
@@ -250,7 +250,7 @@ Vitest runs no Vite id transform, so give each router an explicit `id` when a te
|
|
|
250
250
|
| `hydration` | What the document hydrates with (`metadata.handles`, read to its end before hydration starts). For a `partial` request, the last state it streamed. |
|
|
251
251
|
| `late` | The states that arrived on the late channel (`metadata.handlesLate`), in order. Each replaces the client's handle data after hydration. Often `[]`. |
|
|
252
252
|
|
|
253
|
-
On a HIT the client hydrates with exactly what the shell was rendered from, so `hydration` is also what the shell's HTML holds (the handlers' pushes and the `ssr:
|
|
253
|
+
On a HIT the client hydrates with exactly what the shell was rendered from, so `hydration` is also what the shell's HTML holds (the handlers' pushes and the `ssr: "await"` loaders', a pushed promise resolved), and whatever this request's loaders push, replace or drop is in `late` (see `/ppr`, "Handles on a shell HIT"):
|
|
254
254
|
|
|
255
255
|
```ts
|
|
256
256
|
// The handler pushed ctx.use(Notes)("from-handler"); a live loader pushes
|
package/skills/theme/SKILL.md
CHANGED
|
@@ -141,10 +141,10 @@ Read the theme where it is per request instead:
|
|
|
141
141
|
visitor with no stored theme sees the default, and one with a stored theme
|
|
142
142
|
sees it after hydration. Any other render, a 404 included, starts at the
|
|
143
143
|
visitor's own theme.
|
|
144
|
-
- A live loader (no `ssr:
|
|
144
|
+
- A live loader (no `ssr: "await"`). Loaders do not get `ctx.theme` or
|
|
145
145
|
`ctx.setTheme`; read the cookie directly. A live loader runs on every
|
|
146
146
|
request, HITs included, and on a `ppr` route needs `loading()` or an inline
|
|
147
|
-
`<Suspense>` above its reader. A bake-lane loader (`ssr:
|
|
147
|
+
`<Suspense>` above its reader. A bake-lane loader (`ssr: "await"`) runs at
|
|
148
148
|
capture, where this `cookies()` read refuses the capture, and so does a
|
|
149
149
|
loader a handler awaits with `ctx.use()`.
|
|
150
150
|
|
|
@@ -118,9 +118,9 @@ marks a specific write as non-cacheable even if the var itself is cacheable.
|
|
|
118
118
|
"Least cacheable wins" — if either says `cache: false`, the value throws on
|
|
119
119
|
read inside `cache()` or `"use cache"`. On a `ppr` route the read also throws
|
|
120
120
|
anywhere the shell capture waits for it (a handler, a promise it passes or
|
|
121
|
-
pushes, an async server component, an `ssr:
|
|
121
|
+
pushes, an async server component, an `ssr: "await"` loader, a loader a handler
|
|
122
122
|
awaits) and refuses the capture: the route keeps rendering without a shell,
|
|
123
|
-
and a warning fires once per key. A loader without `ssr:
|
|
123
|
+
and a warning fires once per key. A loader without `ssr: "await"` that no
|
|
124
124
|
handler awaits may read it, unless it is bound with its own `cache()` and no
|
|
125
125
|
`key()`: that miss fails, since the entry is shared (`/loader` → "Cache
|
|
126
126
|
Key").
|
|
@@ -655,7 +655,7 @@ this isolate's invalidations are caught; ttl+swr bounds the rest.
|
|
|
655
655
|
|
|
656
656
|
Inside a `ppr` shell, a `"use cache"` value that renders as shell material is
|
|
657
657
|
pinned at capture time for the life of that shell (see `/ppr` → Pitfalls). A
|
|
658
|
-
hole's own read (in a loader without `ssr:
|
|
658
|
+
hole's own read (in a loader without `ssr: "await"` under `loading()` or an
|
|
659
659
|
inline `<Suspense>`) always returns the store's current entry, never the
|
|
660
660
|
shell's copy, even when a loader the capture ran read the same entry.
|
|
661
661
|
|
|
@@ -53,10 +53,10 @@ hand, is a router bug: report it.
|
|
|
53
53
|
|
|
54
54
|
### 2.1 A server urls() page or a clientUrls() group?
|
|
55
55
|
|
|
56
|
-
| Pick | When | What you get
|
|
57
|
-
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
-
| A server `urls()` route with a handler | Pages people mostly reach by a document load (marketing, docs, articles, landing pages). Pages built from server components you don't want to ship as client code. Routes that need `cache()`, `Prerender()`, `parallel()`, `middleware()` or an `errorBoundary()` on the route itself. | The handler renders on the server, and `cache()` or a prerender can store its output. A click with no prefetch waits for the server before the page changes; a prefetched click does not.
|
|
59
|
-
| A `clientUrls()` group | An area where people move a lot inside one layout: dashboards, admin, settings, tabs, filters, master and detail. Pages that are client components anyway. | The browser matches the group, so a click to another route of the group renders the destination at once, before the server answers. Param and search changes on one route reconcile and hold. There are no handlers: every read is a `useLoader` under its own boundary. A document load is still rendered on the server, and `loader(Def, { ssr:
|
|
56
|
+
| Pick | When | What you get |
|
|
57
|
+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
58
|
+
| A server `urls()` route with a handler | Pages people mostly reach by a document load (marketing, docs, articles, landing pages). Pages built from server components you don't want to ship as client code. Routes that need `cache()`, `Prerender()`, `parallel()`, `middleware()` or an `errorBoundary()` on the route itself. | The handler renders on the server, and `cache()` or a prerender can store its output. A click with no prefetch waits for the server before the page changes; a prefetched click does not. |
|
|
59
|
+
| A `clientUrls()` group | An area where people move a lot inside one layout: dashboards, admin, settings, tabs, filters, master and detail. Pages that are client components anyway. | The browser matches the group, so a click to another route of the group renders the destination at once, before the server answers. Param and search changes on one route reconcile and hold. There are no handlers: every read is a `useLoader` under its own boundary. A document load is still rendered on the server, and `loader(Def, { ssr: "await" })` puts a loader's data and handles in that HTML. |
|
|
60
60
|
|
|
61
61
|
Two limits of a group decide some cases:
|
|
62
62
|
|
|
@@ -436,7 +436,7 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
436
436
|
`ctx.use(Breadcrumbs)({ label, href })`. This is how a group, which has no
|
|
437
437
|
handlers, sets them (`/breadcrumbs`, "Pushing from Loaders").
|
|
438
438
|
- Read them in chrome with `useHandle(Breadcrumbs)`, in a layout that stays.
|
|
439
|
-
- `loader(Def, { ssr:
|
|
439
|
+
- `loader(Def, { ssr: "await" })` puts its pushes in the document's HTML (a
|
|
440
440
|
title for search engines, a real 404 from `notFound()`). The document waits
|
|
441
441
|
for that loader; flag only the one that needs it.
|
|
442
442
|
- A loader an action skips keeps its title and crumbs
|
|
@@ -633,7 +633,7 @@ export default clientUrls(({ path, layout, loader, revalidate }) => [
|
|
|
633
633
|
loader(ProductsLoader, () => [revalidate(bySearch)]),
|
|
634
634
|
]),
|
|
635
635
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
636
|
-
loader(ProductLoader, { ssr:
|
|
636
|
+
loader(ProductLoader, { ssr: "await" }, () => [revalidate(bySlug)]),
|
|
637
637
|
loader(RelatedLoader, () => [revalidate(bySlug)]),
|
|
638
638
|
loader(ReviewsLoader, { prefetch: false }, () => [revalidate(bySlug)]),
|
|
639
639
|
]),
|
|
@@ -647,7 +647,7 @@ Why each placement:
|
|
|
647
647
|
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
648
648
|
| Catalog and product in one group | A card click renders the product page at the click; the filters and tabs are same-route navigations that hold. |
|
|
649
649
|
| `CartLoader` on the server layout, ternary predicate | Held on every navigation (a server layout loader's default), re-run only after a cart action. It survives entering and leaving the group. The badge updates in the add-to-cart response itself. |
|
|
650
|
-
| `ProductLoader` with `ssr:
|
|
650
|
+
| `ProductLoader` with `ssr: "await"` | The product's title and a real 404 for an unknown slug are in the document's HTML. Client navigations still stream it. |
|
|
651
651
|
| `bySlug` on the product loaders | Product to product re-runs them; a tab switch and an add to cart hold them. One action, three outcomes: the badge refreshes, product and related keep their data (`client-shop-cart.test.ts`). |
|
|
652
652
|
| `ReviewsLoader` flagged `prefetch: false` | Every visible card prefetches; reviews are the expensive, rarely read part. The click still shows the product at once and the reviews fill behind their own boundary. A prefetch from another product page (the related links) still runs it: only a segment new to the page is deferred. |
|
|
653
653
|
| One `<Suspense>` per section | A slow product loader fills its section without holding the related list, and the other way round (the demo's product loader takes 2 s). |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* that must agree with them byte for byte. The handler layer's part of that
|
|
6
6
|
* payload is the capture's own doc segment record: the capture writes it
|
|
7
7
|
* first, renders its prelude from it, and every HIT replays it (no handler
|
|
8
|
-
* runs on a HIT). The loader layer's part is the bake-lane (`ssr:
|
|
8
|
+
* runs on a HIT). The loader layer's part is the bake-lane (`ssr: "await"`)
|
|
9
9
|
* loader pins: a promise-free bake-lane loader is served from its pin and
|
|
10
10
|
* does not run; one whose return carries promises (`holes`), or whose pushes
|
|
11
11
|
* the record could not keep (`runs`), runs on the HIT. See
|
package/src/cache/types.ts
CHANGED
|
@@ -841,7 +841,7 @@ export type SegmentHandleData = Record<string, unknown[]>;
|
|
|
841
841
|
/**
|
|
842
842
|
* segmentId -> handleName -> the owning loader id of each recorded value, by
|
|
843
843
|
* index (null: not owned). Only a PPR shell capture writes owners: the
|
|
844
|
-
* settled pushes of the loader bodies it ran (an `ssr:
|
|
844
|
+
* settled pushes of the loader bodies it ran (an `ssr: "await"` loader's own,
|
|
845
845
|
* the loaders it awaits, and its own cache() replays), which the record keeps
|
|
846
846
|
* because the prelude rendered them. A restore of the record
|
|
847
847
|
* (handle-snapshot.ts restoreHandles) keeps an owner's value where the
|
|
@@ -141,7 +141,7 @@ interface LoaderItem extends ItemBase {
|
|
|
141
141
|
readonly type: "loader";
|
|
142
142
|
readonly definition: LoaderDefinition<any, any>;
|
|
143
143
|
readonly revalidate: ClientUrlLoaderRecord["revalidate"];
|
|
144
|
-
readonly ssr?:
|
|
144
|
+
readonly ssr?: "await";
|
|
145
145
|
readonly prefetch?: PrefetchFlag;
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -348,14 +348,21 @@ export const loader: ClientUrlHelpers["loader"] = (
|
|
|
348
348
|
const use = typeof optionsOrUse === "function" ? optionsOrUse : maybeUse;
|
|
349
349
|
if ((options as { stream?: unknown } | undefined)?.stream !== undefined) {
|
|
350
350
|
throw new Error(
|
|
351
|
-
|
|
351
|
+
'clientUrls() loader() stream was replaced: use loader(Def, { ssr: "await" }) to await the loader before first flush on document requests.',
|
|
352
352
|
);
|
|
353
353
|
}
|
|
354
|
-
// Inline, not errors.ts
|
|
355
|
-
// client router chunk, where the helper costs
|
|
356
|
-
if (
|
|
354
|
+
// Inline, not errors.ts assertLoaderSsrOption: this module is in the
|
|
355
|
+
// client router chunk, where the helper costs bytes on every page.
|
|
356
|
+
if (
|
|
357
|
+
options?.ssr !== undefined &&
|
|
358
|
+
options.ssr !== "stream" &&
|
|
359
|
+
options.ssr !== "await"
|
|
360
|
+
) {
|
|
357
361
|
throw new Error(
|
|
358
|
-
|
|
362
|
+
process.env.NODE_ENV !== "production" &&
|
|
363
|
+
(options.ssr as unknown) === false
|
|
364
|
+
? 'clientUrls() loader() ssr: false was replaced: use loader(Def, { ssr: "await" }) to await the loader before first flush on document requests.'
|
|
365
|
+
: `clientUrls() loader() ssr must be "stream" or "await" (got ${JSON.stringify(options.ssr)}). Omit it (or pass "stream") to stream on every render.`,
|
|
359
366
|
);
|
|
360
367
|
}
|
|
361
368
|
if (
|
|
@@ -380,7 +387,7 @@ export const loader: ClientUrlHelpers["loader"] = (
|
|
|
380
387
|
item.type === "revalidate",
|
|
381
388
|
)
|
|
382
389
|
.map((item) => item.fn),
|
|
383
|
-
...(options?.ssr ===
|
|
390
|
+
...(options?.ssr === "await" ? { ssr: "await" as const } : {}),
|
|
384
391
|
...(isPrefetchFlag(options?.prefetch)
|
|
385
392
|
? { prefetch: options.prefetch }
|
|
386
393
|
: {}),
|
|
@@ -580,7 +587,7 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
580
587
|
// await can never apply, so accepting the flag would be silently inert.
|
|
581
588
|
if (item.type === "loader" && item.ssr !== undefined) {
|
|
582
589
|
throw new Error(
|
|
583
|
-
|
|
590
|
+
'clientUrls() intercept() loaders cannot use ssr: "await" — intercepts render on client navigations only',
|
|
584
591
|
);
|
|
585
592
|
}
|
|
586
593
|
}
|
|
@@ -633,7 +640,7 @@ function applyConfig(
|
|
|
633
640
|
Object.freeze({
|
|
634
641
|
loader: item.definition,
|
|
635
642
|
revalidate: Object.freeze([...item.revalidate]),
|
|
636
|
-
...(item.ssr ===
|
|
643
|
+
...(item.ssr === "await" ? { ssr: "await" as const } : {}),
|
|
637
644
|
...(item.prefetch !== undefined ? { prefetch: item.prefetch } : {}),
|
|
638
645
|
}),
|
|
639
646
|
);
|
|
@@ -103,7 +103,7 @@ export interface ClientUrlProjectionRoute {
|
|
|
103
103
|
readonly options: ClientUrlProjectionOptions;
|
|
104
104
|
readonly loaderIds: readonly string[];
|
|
105
105
|
readonly hasLoading: boolean;
|
|
106
|
-
/** Indices into loaderIds of loaders declared loader(Def, { ssr:
|
|
106
|
+
/** Indices into loaderIds of loaders declared loader(Def, { ssr: "await" });
|
|
107
107
|
* materialization passes the option through to the server loader() so
|
|
108
108
|
* document renders await them before first flush. Absent (= none) in
|
|
109
109
|
* projections serialized before the option existed. */
|
|
@@ -348,7 +348,7 @@ function serializeRoute(
|
|
|
348
348
|
|
|
349
349
|
const awaitedLoaderIndices = indicesWhere(
|
|
350
350
|
route.loaders,
|
|
351
|
-
({ ssr }) => ssr ===
|
|
351
|
+
({ ssr }) => ssr === "await",
|
|
352
352
|
);
|
|
353
353
|
|
|
354
354
|
const transition = serializeTransition(route);
|
|
@@ -832,7 +832,7 @@ function materializeRouteItems(
|
|
|
832
832
|
};
|
|
833
833
|
|
|
834
834
|
// The loader() items of a route's or an intercept's projected loaders: each
|
|
835
|
-
// stub with its `ssr:
|
|
835
|
+
// stub with its `ssr: "await"` and `prefetch` flags and its revalidate()
|
|
836
836
|
// predicates.
|
|
837
837
|
const materializeLoaders = (owner: ProjectedLoaders) =>
|
|
838
838
|
owner.projected.loaderIds.map((id, loaderIndex) => {
|
|
@@ -841,7 +841,7 @@ function materializeRouteItems(
|
|
|
841
841
|
createLoaderStub(id),
|
|
842
842
|
{
|
|
843
843
|
...(owner.projected.awaitedLoaderIndices?.includes(loaderIndex) && {
|
|
844
|
-
ssr:
|
|
844
|
+
ssr: "await" as const,
|
|
845
845
|
}),
|
|
846
846
|
...(owner.projected.deferrableLoaderIndices?.includes(
|
|
847
847
|
loaderIndex,
|
package/src/client-urls/types.ts
CHANGED
|
@@ -79,12 +79,12 @@ export interface ClientUrlLoaderRecord {
|
|
|
79
79
|
| boolean
|
|
80
80
|
)[];
|
|
81
81
|
/**
|
|
82
|
-
* loader(Def, { ssr:
|
|
82
|
+
* loader(Def, { ssr: "await" }): document renders await this loader before
|
|
83
83
|
* first flush (see {@link LoaderOptions}). Projected into the server tree,
|
|
84
84
|
* where the per-isSSR entry stamping applies — client navigations stream
|
|
85
85
|
* regardless.
|
|
86
86
|
*/
|
|
87
|
-
readonly ssr?:
|
|
87
|
+
readonly ssr?: "await";
|
|
88
88
|
/**
|
|
89
89
|
* loader(Def, { prefetch: false | "intent" }): a prefetch of the route, or
|
|
90
90
|
* of an intercept's target from its source, defers this loader and the
|
|
@@ -160,10 +160,10 @@ export interface ClientUrlHelpers {
|
|
|
160
160
|
* Attach a projected loader. The optional use callback may contain
|
|
161
161
|
* revalidate() only: a per-loader predicate the server runs, as in urls().
|
|
162
162
|
*
|
|
163
|
-
* Pass `{ ssr:
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
163
|
+
* Pass `{ ssr: "await" }` to await this loader before first flush on
|
|
164
|
+
* DOCUMENT requests (see {@link LoaderOptions}): the opt-in for loaders
|
|
165
|
+
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
166
|
+
* SSR'd HTML.
|
|
167
167
|
* Per-loader: a streaming sibling keeps streaming. Under a `ppr` group
|
|
168
168
|
* route the flag BAKES: the loader executes at shell capture and its
|
|
169
169
|
* settled return freezes into the shell (nested promises stay live
|
|
@@ -26,7 +26,7 @@ import { LoaderRedirect } from "./loader-redirect.js";
|
|
|
26
26
|
* decodeLoaderResults runs during the tree build on settled results, where a
|
|
27
27
|
* throw would collapse the whole payload — it routes signals through the
|
|
28
28
|
* errorFallback slot instead (same visual: the slot replaces the children
|
|
29
|
-
* under OutletProvider). The document lane's `{ ssr:
|
|
29
|
+
* under OutletProvider). The document lane's `{ ssr: "await" }` results take
|
|
30
30
|
* this route (segment-system buildLoaderStreams): they are settled before the
|
|
31
31
|
* tree is built, and the read-site throw would land in the Fizz shell with no
|
|
32
32
|
* boundary to absorb it.
|
package/src/errors.ts
CHANGED
|
@@ -406,17 +406,17 @@ export function invariant(condition: any, message: string): asserts condition {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
409
|
-
* Rejects a
|
|
410
|
-
* passes)
|
|
409
|
+
* Rejects a loader `ssr` option other than `"stream"` or `"await"`
|
|
410
|
+
* (undefined passes). clientUrls() inlines the same checks.
|
|
411
411
|
*/
|
|
412
|
-
export function
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
)
|
|
412
|
+
export function assertLoaderSsrOption(value: unknown, helper: string): void {
|
|
413
|
+
invariant(
|
|
414
|
+
value !== false,
|
|
415
|
+
`${helper} ssr: false was replaced: use loader(Def, { ssr: "await" }) to await the loader before first flush on document requests.`,
|
|
416
|
+
);
|
|
417
417
|
invariant(
|
|
418
|
-
value === undefined ||
|
|
419
|
-
`${
|
|
418
|
+
value === undefined || value === "stream" || value === "await",
|
|
419
|
+
`${helper} ssr must be "stream" or "await" (got ${JSON.stringify(value)}). Omit it (or pass "stream") to stream on every render.`,
|
|
420
420
|
);
|
|
421
421
|
}
|
|
422
422
|
|
package/src/handles/meta.ts
CHANGED
|
@@ -232,9 +232,9 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
232
232
|
* title-templating identically to synchronous descriptors. A descriptor that
|
|
233
233
|
* resolves to `null`/`undefined` (or rejects) is dropped.
|
|
234
234
|
*
|
|
235
|
-
* On a ppr route, a descriptor a handler or an `ssr:
|
|
235
|
+
* On a ppr route, a descriptor a handler or an `ssr: "await"` loader pushes,
|
|
236
236
|
* promise or not, is in the shell's `<head>` on every HIT (#1182). One a
|
|
237
|
-
* loader without `ssr:
|
|
237
|
+
* loader without `ssr: "await"` pushes is live: on a HIT it arrives after
|
|
238
238
|
* hydration.
|
|
239
239
|
*/
|
|
240
240
|
export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
|
package/src/loader-store.ts
CHANGED
|
@@ -575,7 +575,7 @@ export class LoaderStore {
|
|
|
575
575
|
/**
|
|
576
576
|
* Register a still-streaming per-loader result for `loaderId`. No-op for a
|
|
577
577
|
* settled Flight chunk (unwrapsSynchronously / "rejected"): the forceAwait
|
|
578
|
-
* and action lanes commit settled chunks, and a { ssr:
|
|
578
|
+
* and action lanes commit settled chunks, and a { ssr: "await" } loader is
|
|
579
579
|
* awaited before flush. Released as the stream settles, either way.
|
|
580
580
|
*/
|
|
581
581
|
trackPendingStream(loaderId: string, stream: Promise<unknown>): void {
|
package/src/outlet-context.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface OutletContextValue {
|
|
|
20
20
|
loaderStreams?: Record<string, unknown>;
|
|
21
21
|
/**
|
|
22
22
|
* Loader $$ids on this segment that the render awaited before first flush
|
|
23
|
-
* (loader(Def, { ssr:
|
|
23
|
+
* (loader(Def, { ssr: "await" })). Dev-diagnostic input only: useLoader uses
|
|
24
24
|
* it to warn when a read still suspends during SSR on a render that awaited
|
|
25
25
|
* flagged loaders (ssr-suspension-warning.ts). Present only where a
|
|
26
26
|
* segment carries flagged loaders (document and shell-capture renders, and
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type LoaderEntry,
|
|
27
27
|
} from "../server/context";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
assertLoaderSsrOption,
|
|
30
30
|
assertPrefetchOption,
|
|
31
31
|
invariant,
|
|
32
32
|
} from "../errors";
|
|
@@ -920,13 +920,9 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
920
920
|
);
|
|
921
921
|
invariant(
|
|
922
922
|
(optionsGiven as { stream?: unknown } | undefined)?.stream === undefined,
|
|
923
|
-
|
|
924
|
-
);
|
|
925
|
-
assertBooleanOption(
|
|
926
|
-
optionsGiven?.ssr,
|
|
927
|
-
"loader() ssr",
|
|
928
|
-
"stream on every render",
|
|
923
|
+
'loader() stream was replaced: use loader(Def, { ssr: "await" }) to await the loader before first flush on document requests.',
|
|
929
924
|
);
|
|
925
|
+
assertLoaderSsrOption(optionsGiven?.ssr, "loader()");
|
|
930
926
|
assertPrefetchOption(
|
|
931
927
|
optionsGiven?.prefetch,
|
|
932
928
|
"loader() prefetch",
|
|
@@ -942,7 +938,7 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
942
938
|
const loaderEntry: LoaderEntry = {
|
|
943
939
|
loader: loaderDef,
|
|
944
940
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
945
|
-
...(optionsGiven?.ssr ===
|
|
941
|
+
...(optionsGiven?.ssr === "await" && { bake: true as const }),
|
|
946
942
|
...(isPrefetchFlag(optionsGiven?.prefetch) && {
|
|
947
943
|
prefetch: optionsGiven.prefetch,
|
|
948
944
|
}),
|
|
@@ -323,14 +323,14 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
323
323
|
* return <div>{data.name}</div>;
|
|
324
324
|
* }
|
|
325
325
|
* ```
|
|
326
|
-
* Pass `{ ssr:
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
326
|
+
* Pass `{ ssr: "await" }` to await this loader before first flush on
|
|
327
|
+
* DOCUMENT requests (see {@link LoaderOptions}): the opt-in for loaders
|
|
328
|
+
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
329
|
+
* SSR'd HTML.
|
|
330
330
|
* Per-loader: a streaming sibling in the same segment keeps streaming.
|
|
331
331
|
*
|
|
332
332
|
* ```typescript
|
|
333
|
-
* loader(ProductLoader, { ssr:
|
|
333
|
+
* loader(ProductLoader, { ssr: "await" }, () => [cache()]),
|
|
334
334
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
335
335
|
* ```
|
|
336
336
|
*
|
|
@@ -160,7 +160,7 @@ function pushHandleValue(
|
|
|
160
160
|
* This has a simplified signature for internal use - the caller (typically
|
|
161
161
|
* wrapLoaderPromise in router.ts) is responsible for bridging to the full
|
|
162
162
|
* OnErrorCallback with complete request context (request, url, env, etc.).
|
|
163
|
-
* @param useDefaultErrorBoundary - An `ssr:
|
|
163
|
+
* @param useDefaultErrorBoundary - An `ssr: "await"` loader on a document or
|
|
164
164
|
* capture render: an error with no declared boundary gets
|
|
165
165
|
* `DefaultErrorFallback` (it would otherwise reject the tree), and every
|
|
166
166
|
* error answers 500.
|
|
@@ -758,7 +758,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
758
758
|
}
|
|
759
759
|
|
|
760
760
|
// awaitBeforeFlush cycle: segment resolution awaits this loader
|
|
761
|
-
// (loader(Def, { ssr:
|
|
761
|
+
// (loader(Def, { ssr: "await" })), the barrier awaits segment
|
|
762
762
|
// resolution, and rendered() awaits the barrier — waiting here can
|
|
763
763
|
// never complete. Fail fast with the cause instead of hanging the
|
|
764
764
|
// document render. Only document renders populate the set (the flag is
|
|
@@ -767,10 +767,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
767
767
|
if (reqCtx._awaitBeforeFlushLoaderIds?.has(currentLoaderId)) {
|
|
768
768
|
throw new Error(
|
|
769
769
|
`Deadlock: loader "${currentLoaderId}" is registered with ` +
|
|
770
|
-
`ssr:
|
|
770
|
+
`ssr: "await", so the document render awaits it before ` +
|
|
771
771
|
`the render barrier resolves — ctx.rendered() (and the ` +
|
|
772
772
|
`ctx.get(handle) read it gates) can never settle here. Drop ` +
|
|
773
|
-
`ssr:
|
|
773
|
+
`ssr: "await" on this loader or move the handle read to ` +
|
|
774
774
|
`a component.`,
|
|
775
775
|
);
|
|
776
776
|
}
|