@rangojs/router 0.24.4 → 0.25.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 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -101,7 +101,7 @@ Navigating between `/`, `/about`, and `/contact` cross-fades the `<Outlet />` co
|
|
|
101
101
|
|
|
102
102
|
## Direction-aware transitions
|
|
103
103
|
|
|
104
|
-
`ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward navigations restored from the client cache as `"navigation-back"` (a back/forward that has to refetch from the server is tagged `"navigation"`):
|
|
104
|
+
`ViewTransitionClass` accepts an object form keyed by transition type. Every commit the router runs in a transition carries its kind (`"push"`, `"replace"`, `"pop"`, `"action"`, `"revalidate"`, see [Transition kinds](#transition-kinds)). Under `transition()`, Rango also tags forward navigations as `"navigation"` and back/forward navigations restored from the client cache as `"navigation-back"` (a back/forward that has to refetch from the server is tagged `"navigation"`). The restore runs a view transition: it commits on the next task after the `popstate` event, with its saved scroll applied before the new snapshot is taken (see [Back and Forward](#back-and-forward)):
|
|
105
105
|
|
|
106
106
|
```tsx
|
|
107
107
|
layout(<ShopShell />, () => [
|
|
@@ -132,7 +132,40 @@ layout(<ShopShell />, () => [
|
|
|
132
132
|
/* @keyframes slide-out-to-left, slide-in-from-right, ... defined as usual */
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
### Transition kinds
|
|
136
|
+
|
|
137
|
+
Every commit the router runs in a transition adds its `transition({ when })` kind as a transition type, first, before the types above. A map keyed by kind tells a link navigation from a server action's revalidation, with or without `transition()` on the route:
|
|
138
|
+
|
|
139
|
+
| Kind | Commits |
|
|
140
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
141
|
+
| `"push"` | A link, `router.push()`, `navigate()`: the same-route content hold (search or catch-all change), a fully prefetched click, an intercept, the commit after a `clientUrls()` optimistic swap |
|
|
142
|
+
| `"replace"` | `router.replace()`, `<Link replace>` |
|
|
143
|
+
| `"pop"` | A back/forward: one that refetches, and under `transition()` one restored from the history cache |
|
|
144
|
+
| `"action"` | A server action's revalidation and its error-boundary commit |
|
|
145
|
+
| `"revalidate"` | `router.refresh()`, a stale entry's background revalidation |
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// Animate navigations; keep action revalidations and background refreshes still.
|
|
149
|
+
<ViewTransition update={{ push: "page", replace: "page", default: "none" }}>
|
|
150
|
+
<ProductGrid />
|
|
151
|
+
</ViewTransition>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- Tagging alone animates nothing: React starts a view transition only where a `<ViewTransition>` resolves a class other than `"none"` for the commit's types.
|
|
155
|
+
- A commit outside a transition carries no type: a cold click that mounts new segments, a gated-off commit (`when` returned `false`), and a back/forward restored from the history cache on a route without `transition()` (it commits inside the `popstate` event). Under `transition()` that restore carries `"pop"` and `"navigation-back"`.
|
|
156
|
+
- Under `transition()`, a stale entry's revalidation carries `"revalidate"` and `"action"`. An `action`-keyed map that must skip it also keys `revalidate: "none"` (`"none"` wins over any other match).
|
|
157
|
+
- The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `useHandle()` and `useSegments()` commit with the page, and a `useNavigation()` value that moves while neither the state nor the URL does (`pendingUrl` as the response starts streaming, `isStreaming` as it ends) commits with the page while the reader has a held value, urgently otherwise, so it starts no view transition of its own. A transition your own code starts while React holds a navigation's page can still take them. Handle values on a held page land in the commit right after the page, by design; see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design). Use a kind map to choose an animation, never for correctness.
|
|
158
|
+
|
|
159
|
+
### Back and Forward
|
|
160
|
+
|
|
161
|
+
A back/forward the router restores from its history cache animates like a link under `transition()`:
|
|
162
|
+
|
|
163
|
+
- It commits on the next task after the `popstate` event, in a view transition whose types are `"pop"` and `"navigation-back"`. React commits a transition started inside the event synchronously, with no view transition, so the router waits that one task; the page being left stays on screen until then, and the location hooks change with the restored page.
|
|
164
|
+
- The saved scroll position is applied in the commit's layout effect, before the view transition takes its new snapshot, so the restored page animates in at the position it was left.
|
|
165
|
+
- A traversal the browser animated itself (`PopStateEvent.hasUAVisualTransition`: the iOS Safari and Chrome Android back swipe) commits at once and is not animated again.
|
|
166
|
+
- A second Back or Forward before the first one's restore commits wins; the first restore is dropped.
|
|
167
|
+
- Without `transition()` the restore commits inside the event, with no view transition and no type.
|
|
168
|
+
- So does an entry whose scroll the browser restores itself (`history.scrollRestoration` is `"auto"`: the document renders no `<Html.ScrollRestoration>`, which Rango's default document does, or the entry predates it). The browser restores that scroll right after the event, on whatever page is then on screen.
|
|
136
169
|
|
|
137
170
|
## Wrapper form: applying transition to a group of routes
|
|
138
171
|
|
|
@@ -234,7 +267,7 @@ interface TransitionConfig {
|
|
|
234
267
|
```
|
|
235
268
|
|
|
236
269
|
- `default` is the catch-all if a phase-specific prop is unset.
|
|
237
|
-
- The object form keys are React transition types tagged by rango: `"navigation"` (forward navigations), `"navigation-back"` (popstate cache restores)
|
|
270
|
+
- The object form keys are React transition types tagged by rango: the commit's kind (`"push"`, `"replace"`, `"pop"`, `"action"`, `"revalidate"`, see [Transition kinds](#transition-kinds)) and, under `transition()`, `"navigation"` (forward navigations), `"navigation-back"` (popstate cache restores) and `"action"` (action and stale revalidations).
|
|
238
271
|
- `name` lets you participate in cross-page morphs by name (advanced; you usually don't need this on a layout/route-level wrap).
|
|
239
272
|
- `viewTransition` toggles whether rango places its own `<ViewTransition>` boundary. `"auto"` (default) wraps as described above; `false` opts out — see the next section.
|
|
240
273
|
|
|
@@ -380,13 +413,13 @@ interface TransitionWhenContext {
|
|
|
380
413
|
- In a `clientUrls()` group, `false` also opts a same-route navigation out of
|
|
381
414
|
the group's default hold.
|
|
382
415
|
|
|
383
|
-
`kind`
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
`"
|
|
389
|
-
|
|
416
|
+
`kind` is also the transition type the commit carries (see "Transition
|
|
417
|
+
kinds"): `{ push: "page", default: "none" }` animates the commits `when` sees
|
|
418
|
+
as `"push"`. Under `transition()` the older keys stay: a `"pop"` restored from
|
|
419
|
+
the history cache adds `"navigation-back"` (a pop that refetches adds
|
|
420
|
+
`"navigation"`); `"push"`, `"replace"` and a `router.refresh()`
|
|
421
|
+
(`"revalidate"`) add `"navigation"`; a stale entry's revalidation adds
|
|
422
|
+
`"action"`.
|
|
390
423
|
|
|
391
424
|
### Opting out per navigation
|
|
392
425
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import "./augment.js";
|
|
9
9
|
import type {
|
|
10
10
|
Handler,
|
|
11
|
+
LoaderContext,
|
|
11
12
|
MiddlewareContext,
|
|
12
13
|
ResponseHandlerContext,
|
|
13
14
|
RouteLocation,
|
|
@@ -17,6 +18,7 @@ import type {
|
|
|
17
18
|
TransitionWhenFn,
|
|
18
19
|
} from "../index.js";
|
|
19
20
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
21
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
20
22
|
import type { InterceptWhenFn } from "../server/context.js";
|
|
21
23
|
import { href } from "../href-client.js";
|
|
22
24
|
import { rewrite, type Rewrite } from "../route-definition/rewrite.js";
|
|
@@ -62,6 +64,32 @@ function responseReverse(ctx: ResponseHandlerContext): void {
|
|
|
62
64
|
}
|
|
63
65
|
void responseReverse;
|
|
64
66
|
|
|
67
|
+
// Loader ctx.reverse and getRequestContext().reverse take the same optional
|
|
68
|
+
// params as handlers: after the route match the runtime fills them from the
|
|
69
|
+
// matched route's params.
|
|
70
|
+
function loaderReverse(ctx: LoaderContext): void {
|
|
71
|
+
ctx.reverse("home");
|
|
72
|
+
ctx.reverse("blog.post");
|
|
73
|
+
ctx.reverse("blog.post", { slug: "hello" });
|
|
74
|
+
ctx.reverse("search", {}, { q: "hats" });
|
|
75
|
+
// @ts-expect-error - unknown param name is rejected
|
|
76
|
+
ctx.reverse("blog.post", { slugg: "hello" });
|
|
77
|
+
// @ts-expect-error - unknown global name is rejected
|
|
78
|
+
ctx.reverse("nope");
|
|
79
|
+
}
|
|
80
|
+
void loaderReverse;
|
|
81
|
+
|
|
82
|
+
function requestContextReverse(ctx: RequestContext): void {
|
|
83
|
+
ctx.reverse("home");
|
|
84
|
+
ctx.reverse("blog.post");
|
|
85
|
+
ctx.reverse("blog.post", { slug: "hello" });
|
|
86
|
+
// @ts-expect-error - unknown param name is rejected
|
|
87
|
+
ctx.reverse("blog.post", { slugg: "hello" });
|
|
88
|
+
// @ts-expect-error - unknown global name is rejected
|
|
89
|
+
ctx.reverse("nope");
|
|
90
|
+
}
|
|
91
|
+
void requestContextReverse;
|
|
92
|
+
|
|
65
93
|
function middlewareReverse(ctx: MiddlewareContext): void {
|
|
66
94
|
ctx.reverse("blog.post", { slug: "hello" });
|
|
67
95
|
// @ts-expect-error - dot-local name; no include() scope to resolve it
|
|
@@ -72,6 +72,8 @@ export interface ActionEntry {
|
|
|
72
72
|
result?: unknown;
|
|
73
73
|
/** Error from action (set on failure) */
|
|
74
74
|
error?: unknown;
|
|
75
|
+
/** A loader that failed while it re-ran after the action (set on success) */
|
|
76
|
+
revalidationError?: unknown;
|
|
75
77
|
/** Segment IDs that were revalidated by this action */
|
|
76
78
|
revalidatedSegments: string[];
|
|
77
79
|
/** Timestamp when action started */
|
|
@@ -117,9 +119,15 @@ export type StateListener = () => void;
|
|
|
117
119
|
export type ActionStateListener = (state: TrackedActionState) => void;
|
|
118
120
|
|
|
119
121
|
/**
|
|
120
|
-
* Handle state listener
|
|
122
|
+
* Handle state listener. `signal`, from a payload update: "anchor" when it
|
|
123
|
+
* hands React a page in a transition (route state on screen until it
|
|
124
|
+
* commits), "release" when it hands one urgently; "release" again once the
|
|
125
|
+
* page committed (useCommitAnchoredState, react/use-handle.ts).
|
|
121
126
|
*/
|
|
122
|
-
export type HandleListener = () => void;
|
|
127
|
+
export type HandleListener = (signal?: RouteStateSignal) => void;
|
|
128
|
+
|
|
129
|
+
/** See HandleListener. */
|
|
130
|
+
export type RouteStateSignal = "anchor" | "release";
|
|
123
131
|
|
|
124
132
|
/**
|
|
125
133
|
* Internal handle state stored in controller.
|
|
@@ -200,8 +208,8 @@ export interface ActionHandle extends Disposable {
|
|
|
200
208
|
* later-initiated winner's response lands and overwrites it.
|
|
201
209
|
*/
|
|
202
210
|
claimLocationState(state: Record<string, unknown>): Record<string, unknown>;
|
|
203
|
-
/** Complete the action with result */
|
|
204
|
-
complete(result?: unknown): void;
|
|
211
|
+
/** Complete the action with result, and a loader rerun's error, if any */
|
|
212
|
+
complete(result?: unknown, revalidationError?: unknown): void;
|
|
205
213
|
/** Fail the action with error */
|
|
206
214
|
fail(error: unknown): void;
|
|
207
215
|
/** Whether action was completed (success or failure) */
|
|
@@ -260,8 +268,18 @@ export interface EventController {
|
|
|
260
268
|
* Deliver queued location, params, navigation, and handle notifications now.
|
|
261
269
|
* NavigationProvider calls this inside the payload update so React assigns
|
|
262
270
|
* every route-state hook update to the same normal or transition lane.
|
|
271
|
+
* `signal`: handle readers learn how the page is handed over. After
|
|
272
|
+
* "anchor" (a page in a transition) values that arrive before
|
|
273
|
+
* releaseRouteState() wait for it (useCommitAnchoredState,
|
|
274
|
+
* react/use-handle.ts).
|
|
275
|
+
*/
|
|
276
|
+
flushRouteState(signal?: RouteStateSignal): void;
|
|
277
|
+
/**
|
|
278
|
+
* The page the last flushRouteState("anchor") announced has committed:
|
|
279
|
+
* NavigationProvider calls this from its layout effect, and handle readers
|
|
280
|
+
* apply what waited, in that task.
|
|
263
281
|
*/
|
|
264
|
-
|
|
282
|
+
releaseRouteState(): void;
|
|
265
283
|
|
|
266
284
|
// Handle operations
|
|
267
285
|
setHandleData(
|
|
@@ -403,12 +421,14 @@ export function subscribeToLocationChange(
|
|
|
403
421
|
};
|
|
404
422
|
}
|
|
405
423
|
|
|
406
|
-
|
|
424
|
+
/** An action with no tracked entry: idle, nothing reported. */
|
|
425
|
+
export const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
407
426
|
state: "idle",
|
|
408
427
|
actionId: null,
|
|
409
428
|
payload: null,
|
|
410
429
|
error: null,
|
|
411
430
|
result: null,
|
|
431
|
+
revalidationError: null,
|
|
412
432
|
};
|
|
413
433
|
|
|
414
434
|
// Shared empty inflight-actions list. getState() hands back this exact reference
|
|
@@ -437,20 +457,23 @@ function matchesActionId(
|
|
|
437
457
|
|
|
438
458
|
interface DebouncedNotifier {
|
|
439
459
|
schedule(): void;
|
|
440
|
-
flush(): void;
|
|
460
|
+
flush(signal?: RouteStateSignal): void;
|
|
441
461
|
}
|
|
442
462
|
|
|
443
463
|
// Batch rapid notifications into one task to prevent render storms. The
|
|
444
|
-
// explicit flush lets a React update owner preserve its scheduling lane
|
|
445
|
-
|
|
464
|
+
// explicit flush lets a React update owner preserve its scheduling lane; a
|
|
465
|
+
// signal is delivered even with nothing queued.
|
|
466
|
+
function makeDebouncedNotifier(
|
|
467
|
+
listeners: Set<(signal?: RouteStateSignal) => void>,
|
|
468
|
+
): DebouncedNotifier {
|
|
446
469
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
447
|
-
const flush = () => {
|
|
448
|
-
if (timeout === null) return;
|
|
449
|
-
clearTimeout(timeout);
|
|
470
|
+
const flush = (signal?: RouteStateSignal) => {
|
|
471
|
+
if (timeout === null && !signal) return;
|
|
472
|
+
clearTimeout(timeout ?? undefined);
|
|
450
473
|
timeout = null;
|
|
451
474
|
notifyListeners(
|
|
452
475
|
[...listeners],
|
|
453
|
-
(listener) => listener(),
|
|
476
|
+
(listener) => listener(signal),
|
|
454
477
|
(listener) => listeners.has(listener),
|
|
455
478
|
);
|
|
456
479
|
};
|
|
@@ -613,9 +636,9 @@ export function createEventController(
|
|
|
613
636
|
|
|
614
637
|
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
615
638
|
|
|
616
|
-
function flushRouteState(): void {
|
|
639
|
+
function flushRouteState(signal?: RouteStateSignal): void {
|
|
617
640
|
notifyStateListeners.flush();
|
|
618
|
-
notifyHandles.flush();
|
|
641
|
+
notifyHandles.flush(signal);
|
|
619
642
|
}
|
|
620
643
|
|
|
621
644
|
function getState(): DerivedNavigationState {
|
|
@@ -700,6 +723,7 @@ export function createEventController(
|
|
|
700
723
|
payload: entry.payload,
|
|
701
724
|
error: entry.error ?? null,
|
|
702
725
|
result: entry.result ?? null,
|
|
726
|
+
revalidationError: entry.revalidationError ?? null,
|
|
703
727
|
};
|
|
704
728
|
}
|
|
705
729
|
|
|
@@ -871,7 +895,7 @@ export function createEventController(
|
|
|
871
895
|
let actionCompleted = false;
|
|
872
896
|
let cohortReleased = false;
|
|
873
897
|
let pendingResult:
|
|
874
|
-
| { type: "success"; value?: unknown }
|
|
898
|
+
| { type: "success"; value?: unknown; revalidationError?: unknown }
|
|
875
899
|
| { type: "error"; value: unknown }
|
|
876
900
|
| null = null;
|
|
877
901
|
|
|
@@ -922,6 +946,7 @@ export function createEventController(
|
|
|
922
946
|
entry.error = pendingResult.value;
|
|
923
947
|
} else if (pendingResult?.type === "success") {
|
|
924
948
|
entry.result = pendingResult.value;
|
|
949
|
+
entry.revalidationError = pendingResult.revalidationError;
|
|
925
950
|
}
|
|
926
951
|
entry.phase = "settling";
|
|
927
952
|
notify();
|
|
@@ -997,8 +1022,8 @@ export function createEventController(
|
|
|
997
1022
|
return winning;
|
|
998
1023
|
},
|
|
999
1024
|
|
|
1000
|
-
complete(result?: unknown) {
|
|
1001
|
-
settleWith({ type: "success", value: result });
|
|
1025
|
+
complete(result?: unknown, revalidationError?: unknown) {
|
|
1026
|
+
settleWith({ type: "success", value: result, revalidationError });
|
|
1002
1027
|
},
|
|
1003
1028
|
|
|
1004
1029
|
fail(error: unknown) {
|
|
@@ -1258,6 +1283,7 @@ export function createEventController(
|
|
|
1258
1283
|
subscribeToAction,
|
|
1259
1284
|
subscribeToHandles,
|
|
1260
1285
|
flushRouteState,
|
|
1286
|
+
releaseRouteState: () => notifyHandles.flush("release"),
|
|
1261
1287
|
|
|
1262
1288
|
// Direct access
|
|
1263
1289
|
getCurrentNavigation: () => currentNavigation,
|
|
@@ -4,10 +4,11 @@ import type { SlotState } from "../types.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Check if a segment is an intercept segment.
|
|
6
6
|
* Intercept segments have namespace starting with "intercept:" — both the
|
|
7
|
-
* parallel container (@modal) and its
|
|
7
|
+
* parallel container (@modal) and its loader segments receive this namespace
|
|
8
8
|
* from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
|
|
9
|
+
* Server-safe: the cache and match pipeline share it.
|
|
9
10
|
*/
|
|
10
|
-
export function isInterceptSegment(s:
|
|
11
|
+
export function isInterceptSegment(s: { namespace?: string }): boolean {
|
|
11
12
|
return s.namespace?.startsWith("intercept:") === true;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -1,90 +1,5 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
2
|
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
3
|
-
import { settledValue } from "../thenable-status.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Merge partial loader data from server with cached loader data.
|
|
7
|
-
*
|
|
8
|
-
* During partial revalidation (stale or action), the server may return only
|
|
9
|
-
* some loaders that pass the revalidation check, none when every one is held.
|
|
10
|
-
* The component still needs all loader data, so we merge fresh data with
|
|
11
|
-
* cached data. Two settled aggregates merge into the array itself: a fresh
|
|
12
|
-
* Promise.all over settled parts is a promise React has not read, and content
|
|
13
|
-
* on screen handed one suspends (docs/internal/suspense-contract.md, rule 2).
|
|
14
|
-
*
|
|
15
|
-
* @param fromServer - Segment returned from server with partial loaders
|
|
16
|
-
* @param fromCache - Cached segment with full loader data
|
|
17
|
-
* @returns Merged segment with complete loader data
|
|
18
|
-
*/
|
|
19
|
-
export function mergeSegmentLoaders(
|
|
20
|
-
fromServer: ResolvedSegment,
|
|
21
|
-
fromCache: ResolvedSegment,
|
|
22
|
-
): ResolvedSegment {
|
|
23
|
-
const serverLoaderIds = fromServer.loaderIds || [];
|
|
24
|
-
const cachedLoaderIds = fromCache.loaderIds || [];
|
|
25
|
-
|
|
26
|
-
if (IS_BROWSER_DEBUG) {
|
|
27
|
-
debugLog(
|
|
28
|
-
`[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Fresh data for the loaders the server re-ran, cached data for the rest.
|
|
33
|
-
const merge = (newData: any[], cachedData: any[]): any[] =>
|
|
34
|
-
cachedLoaderIds.map((id: string, i: number) => {
|
|
35
|
-
const newIndex = serverLoaderIds.indexOf(id);
|
|
36
|
-
return newIndex !== -1 ? newData[newIndex] : cachedData[i];
|
|
37
|
-
});
|
|
38
|
-
const settledNew = settledValue(fromServer.loaderDataPromise!);
|
|
39
|
-
const settledCached = settledValue(fromCache.loaderDataPromise!);
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
...fromCache,
|
|
43
|
-
// The server re-ran the slot's handler and layout: their render applies,
|
|
44
|
-
// as it does when every loader re-runs. Only the loaders it held keep
|
|
45
|
-
// their cached data.
|
|
46
|
-
component: fromServer.component,
|
|
47
|
-
layout: fromServer.layout,
|
|
48
|
-
// Every loader held: the cached aggregate itself, so a boundary still
|
|
49
|
-
// waiting on it keeps its one thenable (suspense-contract.md, rule 1).
|
|
50
|
-
loaderDataPromise:
|
|
51
|
-
serverLoaderIds.length === 0
|
|
52
|
-
? fromCache.loaderDataPromise
|
|
53
|
-
: settledNew && settledCached
|
|
54
|
-
? merge(settledNew, settledCached)
|
|
55
|
-
: Promise.all([
|
|
56
|
-
fromServer.loaderDataPromise!,
|
|
57
|
-
fromCache.loaderDataPromise!,
|
|
58
|
-
]).then(([newData, cachedData]) => merge(newData, cachedData)),
|
|
59
|
-
// Keep all loader IDs from cache
|
|
60
|
-
loaderIds: fromCache.loaderIds,
|
|
61
|
-
// The rebuilt aggregate is the source of truth. Cached per-loader
|
|
62
|
-
// streams would make LoaderResolver ignore loaderDataPromise.
|
|
63
|
-
loaderStreams: undefined,
|
|
64
|
-
awaitedLoaderIds: undefined,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if segments need loader merging during partial revalidation.
|
|
70
|
-
*
|
|
71
|
-
* Returns true when:
|
|
72
|
-
* - Server returned fewer loaders than cached (partial revalidation)
|
|
73
|
-
* - Both segments have loader data promises
|
|
74
|
-
*/
|
|
75
|
-
export function needsLoaderMerge(
|
|
76
|
-
fromServer: ResolvedSegment,
|
|
77
|
-
fromCache: ResolvedSegment | undefined,
|
|
78
|
-
): fromCache is ResolvedSegment {
|
|
79
|
-
return !!(
|
|
80
|
-
fromCache &&
|
|
81
|
-
fromServer.loaderIds &&
|
|
82
|
-
fromCache.loaderIds &&
|
|
83
|
-
fromServer.loaderIds.length < fromCache.loaderIds.length &&
|
|
84
|
-
fromServer.loaderDataPromise &&
|
|
85
|
-
fromCache.loaderDataPromise
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
3
|
|
|
89
4
|
/**
|
|
90
5
|
* Insert diff segments that aren't in the matched array into allSegments.
|