@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.
Files changed (95) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/{chunk-4HHFQSDC.js → chunk-3CQXDX3A.js} +5 -5
  3. package/dist/bin/{client-urls-projection-4Q5EQDAL.js → client-urls-projection-2GBDM5YK.js} +1 -1
  4. package/dist/bin/rango.js +1 -1
  5. package/dist/bin/{runtime-discovery-RY6NMQM3.js → runtime-discovery-PA3V3IFQ.js} +2 -2
  6. package/dist/bin/{temp-rsc-server-F7AHOPV4.js → temp-rsc-server-WDXEHDY7.js} +2 -2
  7. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  8. package/dist/types/cache/types.d.ts +1 -1
  9. package/dist/types/client-urls/server-projection.d.ts +1 -1
  10. package/dist/types/client-urls/types.d.ts +6 -6
  11. package/dist/types/decode-loader-results.d.ts +1 -1
  12. package/dist/types/errors.d.ts +3 -3
  13. package/dist/types/handles/meta.d.ts +2 -2
  14. package/dist/types/loader-store.d.ts +1 -1
  15. package/dist/types/outlet-context.d.ts +1 -1
  16. package/dist/types/route-definition/helpers-types.d.ts +5 -5
  17. package/dist/types/router/loader-resolution.d.ts +1 -1
  18. package/dist/types/router/segment-resolution/loader-cache.d.ts +7 -7
  19. package/dist/types/router/types.d.ts +1 -1
  20. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  21. package/dist/types/server/context.d.ts +3 -3
  22. package/dist/types/server/request-context.d.ts +1 -1
  23. package/dist/types/ssr/index.d.ts +1 -1
  24. package/dist/types/ssr/ssr-root.d.ts +1 -1
  25. package/dist/types/ssr-suspension-warning.d.ts +1 -1
  26. package/dist/types/types/loader-types.d.ts +19 -19
  27. package/dist/types/types/segments.d.ts +2 -2
  28. package/dist/types/urls/path-helper-types.d.ts +4 -4
  29. package/dist/types/urls/pattern-types.d.ts +1 -1
  30. package/dist/types/vite/plugin-types.d.ts +1 -1
  31. package/dist/vite/index.js +7 -7
  32. package/package.json +1 -1
  33. package/skills/app-structure/e-commerce.md +1 -1
  34. package/skills/breadcrumbs/SKILL.md +5 -5
  35. package/skills/cache-guide/SKILL.md +4 -4
  36. package/skills/caching/SKILL.md +1 -1
  37. package/skills/catalog.json +2 -2
  38. package/skills/client-urls/SKILL.md +20 -20
  39. package/skills/comparison/references/framework-comparison.md +6 -6
  40. package/skills/defer-hydration/SKILL.md +2 -2
  41. package/skills/handler-use/SKILL.md +5 -5
  42. package/skills/hooks/data.md +1 -1
  43. package/skills/hooks/handle-and-actions.md +9 -9
  44. package/skills/loader/SKILL.md +52 -47
  45. package/skills/middleware/SKILL.md +2 -2
  46. package/skills/migrate-nextjs/SKILL.md +10 -10
  47. package/skills/migrate-react-router/SKILL.md +1 -1
  48. package/skills/migrate-react-router/component-migration.md +2 -2
  49. package/skills/migrate-react-router/data-and-actions.md +2 -2
  50. package/skills/migrate-react-router/route-mapping.md +2 -2
  51. package/skills/parallel/SKILL.md +2 -2
  52. package/skills/ppr/SKILL.md +91 -92
  53. package/skills/prerender/SKILL.md +5 -5
  54. package/skills/rango/SKILL.md +7 -7
  55. package/skills/route/SKILL.md +1 -1
  56. package/skills/router-setup/SKILL.md +2 -2
  57. package/skills/scripts/SKILL.md +3 -3
  58. package/skills/shell-manifest/SKILL.md +3 -3
  59. package/skills/testing/serve-shell-request.md +1 -1
  60. package/skills/theme/SKILL.md +2 -2
  61. package/skills/typesafety/env-and-bindings.md +2 -2
  62. package/skills/use-cache/SKILL.md +1 -1
  63. package/skills/ux-architecture/SKILL.md +7 -7
  64. package/src/cache/shell-snapshot.ts +1 -1
  65. package/src/cache/types.ts +1 -1
  66. package/src/client-urls/client-urls.ts +16 -9
  67. package/src/client-urls/server-projection.ts +4 -4
  68. package/src/client-urls/types.ts +6 -6
  69. package/src/decode-loader-results.ts +1 -1
  70. package/src/errors.ts +9 -9
  71. package/src/handles/meta.ts +2 -2
  72. package/src/loader-store.ts +1 -1
  73. package/src/outlet-context.ts +1 -1
  74. package/src/route-definition/dsl-helpers.ts +4 -8
  75. package/src/route-definition/helpers-types.ts +5 -5
  76. package/src/router/loader-resolution.ts +4 -4
  77. package/src/router/segment-resolution/loader-cache.ts +8 -8
  78. package/src/router/segment-resolution/prefetch-deferral.ts +1 -1
  79. package/src/router/segment-resolution/revalidation.ts +3 -3
  80. package/src/router/types.ts +1 -1
  81. package/src/rsc/shell-capture-constants.ts +3 -3
  82. package/src/rsc/shell-capture.ts +12 -12
  83. package/src/segment-system.tsx +3 -3
  84. package/src/server/context.ts +5 -5
  85. package/src/server/cookie-store.ts +1 -1
  86. package/src/server/request-context.ts +1 -1
  87. package/src/ssr/index.tsx +6 -6
  88. package/src/ssr/ssr-root.tsx +1 -1
  89. package/src/ssr-suspension-warning.ts +4 -4
  90. package/src/types/loader-types.ts +19 -19
  91. package/src/types/segments.ts +2 -2
  92. package/src/urls/path-helper-types.ts +4 -4
  93. package/src/urls/pattern-types.ts +1 -1
  94. package/src/vite/discovery/shell-prerender-phase.ts +1 -1
  95. 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: false` run per request; `ssr: false`
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: false` loader pushes settled is in the shell's HTML and
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: false`
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: false` loaders, which run at every capture. **If the shell
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: false` loader on a `Prerender` + `ppr` route: the
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.
@@ -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: false` loader: it bakes into the shell
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: false`, which bakes it into the shell).
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: false })` | /loader |
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: false` loader: it bakes into the shell
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: false` bakes; see
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: false` loader runs there
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: false`, read with `useLoader` under `loading()` or an inline
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)`
@@ -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: false`, or awaited by a handler via `ctx.use()`): the read
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: false })` raise it automatically so that content stays
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: false })` to
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".
@@ -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: false })` so the
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: false` never runs during the shell capture, so its inline/ordered
119
- > script is dropped on every shell HIT; with `ssr: false` the push bakes into
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: false`) read that description and
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: false` loader on a `ppr` route is the exception: its
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: false }` (the document render awaits the loader before
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: false` 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"):
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
@@ -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: false`). Loaders do not get `ctx.theme` or
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: false`) runs at
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: false` loader, a loader a handler
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: false` that no
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: false` under `loading()` or an
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: false })` puts a loader's data and handles in that HTML. |
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: false })` puts its pushes in the document's HTML (a
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: false }, () => [revalidate(bySlug)]),
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: false` | The product's title and a real 404 for an unknown slug are in the document's HTML. Client navigations still stream it. |
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: false`)
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
@@ -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: false` loader's own,
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?: false;
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
- "clientUrls() loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
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 assertBooleanOption: this module is in the
355
- // client router chunk, where the helper costs 20 B gzip.
356
- if (options?.ssr !== undefined && typeof options.ssr !== "boolean") {
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
- `clientUrls() loader() ssr must be a boolean (got ${JSON.stringify(options.ssr)}). Omit it (or pass true) to stream on every render.`,
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 === false ? { ssr: false as const } : {}),
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
- "clientUrls() intercept() loaders cannot use ssr: false — intercepts render on client navigations only",
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 === false ? { ssr: false as const } : {}),
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: false });
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 === false,
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: false` and `prefetch` flags and its revalidate()
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: false,
844
+ ssr: "await" as const,
845
845
  }),
846
846
  ...(owner.projected.deferrableLoaderIndices?.includes(
847
847
  loaderIndex,
@@ -79,12 +79,12 @@ export interface ClientUrlLoaderRecord {
79
79
  | boolean
80
80
  )[];
81
81
  /**
82
- * loader(Def, { ssr: false }): document renders await this loader before
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?: false;
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: false }` — the same knob as loading(fallback, { ssr:
164
- * false }) — to await this loader before first flush on DOCUMENT requests
165
- * (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
166
- * pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
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: false }` results take
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 boolean-only DSL option that holds anything else (undefined
410
- * passes), as an invariant.
409
+ * Rejects a loader `ssr` option other than `"stream"` or `"await"`
410
+ * (undefined passes). clientUrls() inlines the same checks.
411
411
  */
412
- export function assertBooleanOption(
413
- value: unknown,
414
- option: string,
415
- effect: string,
416
- ): void {
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 || typeof value === "boolean",
419
- `${option} must be a boolean (got ${JSON.stringify(value)}). Omit it (or pass true) to ${effect}.`,
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
 
@@ -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: false` loader pushes,
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: false` pushes is live: on a HIT it arrives after
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<
@@ -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: false } loader is
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 {
@@ -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: false })). Dev-diagnostic input only: useLoader uses
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
- assertBooleanOption,
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
- "loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
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 === false && { bake: true as const }),
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: false }` — the same knob as loading(fallback, { ssr:
327
- * false }) — to await this loader before first flush on DOCUMENT requests
328
- * (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
329
- * pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
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: false }, () => [cache()]),
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: false` loader on a document or
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: false })), the barrier awaits segment
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: false, so the document render awaits it before ` +
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: false on this loader or move the handle read to ` +
773
+ `ssr: "await" on this loader or move the handle read to ` +
774
774
  `a component.`,
775
775
  );
776
776
  }