Skip to content

@rangojs/router 0.32.0

Choose a tag to compare

@ivogt ivogt released this 30 Oct 20:12
· 171 commits to main since this release
27d591b

Fixed: a prefetch held by a revalidate() that read the source page no longer shows another page's layout (#1333)

Before. A layout whose revalidate() compares the page you leave with
the page you go to (currentParams, currentUrl, fromRouteName), such as
a template that shows a category hero on category pages, could show the
wrong copy of itself. A prefetch of /cart sent from a product page kept
the layout, which is right for the product page. The prefetch is shared by
every page, so a click on the same link from a category page took the same
answer and kept the category page's layout: the cart rendered under the
category hero, and stayed there on the next navigation. Production's
"viewport" default made this likely; development, which prefetches
nothing by default, did not show it.

Now. When a revalidate() reads the page navigated away from and keeps
the segment, the response is stored for the page that prefetched it
(x-rsc-prefetch-scope: source), as an intercept target's already is. A
click from another page prefetches its own answer, or asks the server. A
revalidate() that re-renders, or that reads only the destination
(nextParams, nextUrl, isAction(), toRouteName), keeps one shared
prefetch.

A prerendered target is covered too: its stored entry runs a held layout's
revalidate() only when the params change, so on equal params the kept
layout scopes the prefetch the same way. So does a prerendered page kept on
a search-only change (/item/1?tab=a to /item/1?tab=b): a click from
/item/2 no longer shows item 2.

No change in your code. prefetchKey=":source" stays for a response that
differs by source in ways the server cannot see; the router now scopes the
revalidate()-driven case itself.

Fixed: rango generate --runtime resolves through plugins that set up in configResolved (#1332)

Before. rango generate --runtime forwarded the app's resolver plugins
straight from the unresolved Vite config, so their config and
configResolved hooks never ran. A resolver that builds its state there,
such as vite-tsconfig-paths, matched nothing, and discovery failed with
Cannot find module '~routes/shop'. A plugin with apply: "build" was
forwarded into the serve-mode discovery too.

Now. --runtime resolves the Vite config as vite dev would, without
rango() and rsc(), and forwards the resolver plugins from the result. An
alias added by a plugin's config() hook resolves, and apply: "build"
plugins, string or function form, are left out. define and
resolve.conditions set from a plugin's config hook are still not picked
up (docs/manifests.md).

Added: location state on document responses (#1334)

Before. ctx.setLocationState() and redirect(url, { state }) reached
history.state only through client navigations and actions. A document
response dropped the state: a <form action={fn}> posted without JS (or
before hydration), a payment provider redirecting the shopper back to a
handler, a link from an email.

Now. A document response carries it:

  • a page load or no-JS form post that renders a page carries the state in
    its bootstrap payload; the client writes it into the entry when it starts,
    and useLocationState returns it once the page has hydrated (as it does
    for stored state);
  • a document request that redirects carries the state in a one-shot cookie
    (<stateCookieName>-ls, __Host- on https, HttpOnly, SameSite=Lax,
    60 seconds) that the target's document request reads and clears, so a
    refresh does not show a flash again. A prefetch or fetch() of the target
    (Sec-Fetch-Dest other than a browsing context) leaves it to the
    navigation. The cookie holds JSON: give state
    that crosses a document redirect plain JSON data. State over 4000 bytes,
    or that JSON cannot encode, is dropped with a warning;
  • such a document, and the redirect carrying the cookie, are sent
    Cache-Control: private, no-store; the document cache neither stores nor
    serves them. A ppr route keeps its shell: state set by middleware or the
    cookie rides the live payload of a HIT or MISS (no shell holds it), and a
    route whose handler sets state is not captured and renders in full.
// Works the same for a client action, a no-JS form post and a request from
// outside the app.
path("/checkout/callback", async (ctx) =>
  redirect("/orders/confirmed", { state: [Flash({ text: "Paid" })] }),
);

If a handler or middleware calls setLocationState on every request, it is
now delivered on document loads too; a clearOnReload slot it sets applies
on the load it arrived with. On a ppr route, set state from middleware: a
shell HIT runs no handler. Without JS the state reaches no reader (only the
client applies it), and a no-store page is kept out of the browser's
back/forward cache. A document redirect from one router to a path another
router on the same origin serves loses its state: each router reads only its
own cookie.

Docs: one location-state definition keeps only its last write (#1334)

Entries accumulate across definitions, but for one definition the last write
wins: two setLocationState(Toasts(...)) calls in one response, or
redirect(url, { state: [Toasts(a), Toasts(b)] }), deliver only the later
value. The setLocationState, createLocationState and redirect JSDoc and
the /hooks state.md skill now say so. Write an array once to combine
messages.