react-server675fbba4
react-servercommit5353c12fb5bd

feat: typed outlets (#409)

Summary

Adds typed, per-outlet bound ReactServerComponent exposed through a new virtual module @lazarv/react-server/outlets, alongside the existing @lazarv/react-server/routes. Where @lazarv/react-server/routes types the consuming side of outlets (a layout's createLayout props), this PR types and packages the producing side, so call sites no longer hand-write <ReactServerComponent outlet="sidebar" url="/dashboard/nav" /> — instead they import a namespace per outlet and write <sidebar.Outlet url="/dashboard/nav" />.

The bound component closes over its outlet name (no stringly-typed prop), accepts a url typed against the same RouteImpl<T> union as Link.to (so typos are rejected and dynamic segments must be concrete), and returns a branded Outlet<"sidebar"> so the value can satisfy a createLayout slot of the same name without casts.

By default the outlet preloads on the server. When the bound component is rendered in a server component, the runtime resolves the url against the file-router manifest, locates the matching @outletName/…page.tsx (or the @outletName.default.tsx fallback), renders it, and passes the result as children to ReactServerComponent — so the SSR HTML contains the outlet content on first paint, no client round-trip. defer={true} and explicit children opt out (client-only fetch and full override respectively). To make this work cleanly, Outlet<Name> from the routes module is now exported so the outlets module can produce values that satisfy the same brand consumed by createLayout.

Implementation notes

The file-router plugin now collects unique outlet names from the manifest and generates two artifacts: a virtual @lazarv/react-server/outlets runtime module and a react-server-outlets.d.ts declaration written into .react-server/. The runtime module ships in two flavours selected by Vite environment — an async preloading variant for any server-side environment (RSC and SSR), and a sync forwarder for the client environment where the preload's server-only imports would crash a browser bundle. Build-mode SSR/client builds receive the simple variant through the resources fallback plugin's store, the same way @lazarv/react-server/routes is delivered today. Outlet names that aren't valid JavaScript identifiers (e.g. hyphenated directory names) are skipped from the module with a logger warning and stay reachable via the bare <ReactServerComponent outlet="…" /> form. Both generateRoutesDts and generateOutletsDts were rewritten as single template literals composed via .map(...).join("\n\n"), replacing the previous lines.push chains; the emitted output is byte-identical apart from now-exported Outlet<Name>.

Adjacent fix: default <Link> navigation with named outlets active

Surfacing this feature exposed a pre-existing bug in ClientProvider.navigate(). With outlets.size > 1 (which now happens any time a page mounts a <*.Outlet />), a default <Link> click — where target/local/root are unset and Link.jsx therefore passes outlet: undefined — entered the broadcast branch and fanned the navigation out to every active non-root outlet, skipping PAGE_ROOT. The user stayed on the previous page with the outlet slots rendering null. Fixed by narrowing the implicit broadcast: when options.outlet is undefined and the target pathname differs from the current pathname, default to PAGE_ROOT. Same-pathname updates (?filter=active-style URL changes) still broadcast, preserving the multi-pane filter use case the broadcast was designed for.

Author
Viktor Lázár <lazarv1982@gmail.com>
Date
Commit
5353c12fb5bda51dfe2243ca4eb19da8662c5c4e
12 files changed+745 -125