@uniflowed/router 0.0.0-alpha.34 → 0.0.0-alpha.37

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/rsc.js ADDED
@@ -0,0 +1,329 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/router/rsc`: a route, rendered as React Server Components.
4
+ //
5
+ // This module runs in the module graph uf resolves under the `react-server`
6
+ // export condition, which is the graph where `react` is the build with no
7
+ // `useState` in it and where React's Flight renderer is allowed to load
8
+ // (ubugeeei-prod/uf#519). It does three things, and each is a piece that
9
+ // already existed somewhere else:
10
+ //
11
+ // 1. **Resolve the URL** — `resolveMatch`, the same resolution a single-page
12
+ // application runs in the browser: the match, the modules, the loader, the
13
+ // metadata, the boundaries.
14
+ // 2. **Compose the tree** — `composeRoute`, the same walk `RouteView` makes,
15
+ // so a layout, a fallback, a template and a slot land where they would in
16
+ // any other render.
17
+ // 3. **Render it as a Flight payload** — `renderToReadableStream` from
18
+ // `react-server-dom-parcel`, which is React's own renderer and React's own
19
+ // wire format. uf writes no serialiser here and no decoder anywhere.
20
+ //
21
+ // The payload's root value is a [`FlightRoot`]: the route, as the part of it a
22
+ // hook reads, and the tree. The HTML renderer (`./server.js`) reads it to write
23
+ // the document, and the browser (`./client.js`) reads the same bytes to hydrate
24
+ // and, after that, to navigate — so what the browser renders is exactly what
25
+ // the server rendered, and no page, layout or loader module is ever the
26
+ // browser's to evaluate. A `"use client"` module is the one thing that crosses,
27
+ // as a reference to a chunk the browser loads.
28
+ //
29
+ // # What leaves before the payload does
30
+ //
31
+ // A redirect, and the status. Both are decided while the route resolves, which
32
+ // is before anything renders, so `renderFlight` answers a redirect instead of a
33
+ // stream and a route with the status it resolved to. That is the head-before-
34
+ // the-first-byte property `./internal/stream.js` depends on, kept from the
35
+ // other side: nothing the renderer learns while streaming can change the
36
+ // response line.
37
+ //
38
+ // # Error boundaries have to be client modules
39
+ //
40
+ // An `$error.js` catches a throw while the *browser* renders, so its component
41
+ // is the browser's, and a server component cannot be passed to a client one —
42
+ // Flight refuses a function where a reference was expected. So each boundary's
43
+ // component crosses as the client reference its `"use client"` directive made it
44
+ // in this graph, and a boundary module without the directive is refused here,
45
+ // with its file named, rather than by React with a sentence about functions.
46
+
47
+ import * as React from "react";
48
+ import { use } from "react";
49
+ // React's Flight server. Only this graph can load it: it refuses to evaluate
50
+ // unless `react` resolved under the `react-server` condition.
51
+ import { renderToReadableStream } from "react-server-dom-parcel/server";
52
+
53
+ import { noteRoute } from "@uniflowed/server/host";
54
+
55
+ import { BoundaryReporter } from "./internal/boundaries.js";
56
+ import { routeBoundaries } from "./internal/boundary-data.js";
57
+ import { composeRoute, pageComponent } from "./internal/compose.js";
58
+ import { ErrorRoutePage } from "./internal/error-view.js";
59
+ import { type FlightRoot, routeState } from "./internal/flight.js";
60
+ import { requireServerComponentsReact } from "./internal/react-version.js";
61
+ import type { ErrorModule, PageModule, ResolvedRoute, ResolvedSlot } from "./internal/resolve.js";
62
+ import { resolveFailure, resolveMatch } from "./internal/resolve.js";
63
+ import type { RouteParams, RouteTable, SearchParams } from "./internal/routing.js";
64
+ import { RedirectError, nearestBoundary } from "./internal/routing.js";
65
+ import { withServerRoute } from "./internal/server-route.js";
66
+
67
+ export type { FlightRoot, RouteState } from "./internal/flight.js";
68
+ // For `virtual:uf/rsc`, so a Server Component's `basePath()` is the project's.
69
+ export { installRouting } from "./internal/base-path.js";
70
+
71
+ /** What a host may tell the renderer about one render. */
72
+ export type FlightOptions = {|
73
+ /**
74
+ * Whether a loader may be left running into a `$loading.js` boundary.
75
+ *
76
+ * On by default: a payload streams, so a slow loader is a fallback now and
77
+ * its answer later. A prerender turns it off, because a file has no "later".
78
+ */
79
+ readonly defer?: boolean,
80
+ /**
81
+ * Every exception the render recovered from, including the late ones.
82
+ *
83
+ * What it returns becomes the exception's digest. React writes the digest
84
+ * into the row that stands in for the part of the tree that failed, and sets
85
+ * it on the error its Flight client rebuilds from that row — which is how
86
+ * `./server.js` recognises, in the HTML renderer, the copy of an exception
87
+ * this renderer has already reported.
88
+ */
89
+ readonly onError?: (error: mixed) => ?string,
90
+ /**
91
+ * Render the error boundary for this exception instead of resolving the URL.
92
+ *
93
+ * What the HTML renderer asks for when the shell it was streaming from the
94
+ * payload threw before its first byte: the route resolved, and rendering it
95
+ * did not.
96
+ */
97
+ readonly failure?: {| readonly error: mixed |},
98
+ /** Stops the render, for a reader that went away. */
99
+ readonly signal?: AbortSignal,
100
+ |};
101
+
102
+ /** A render: a redirect to answer with, or a route and its payload. */
103
+ export type FlightRender =
104
+ | {| readonly kind: "redirect", readonly status: 307 | 308, readonly location: string |}
105
+ | {|
106
+ readonly kind: "route",
107
+ readonly status: 200 | 401 | 403 | 404 | 500,
108
+ /** The payload, as React writes it. Read exactly once. */
109
+ readonly stream: ReadableStream<Uint8Array>,
110
+ /**
111
+ * The exception this route resolved to its error boundary for, when it did.
112
+ *
113
+ * The same field `./server.js` has always carried as `error`: `uf build`
114
+ * fails a route that set it and `uf dev` reports it. `forbidden()` and
115
+ * `unauthorized()` do not set it.
116
+ */
117
+ readonly failure: mixed,
118
+ |};
119
+
120
+ /** Render one URL. */
121
+ export type FlightRenderer = (url: string, options?: FlightOptions) => Promise<FlightRender>;
122
+
123
+ /**
124
+ * Whether this bundle marks the boundaries it renders.
125
+ *
126
+ * `BOUNDARY_MARKS` in `./internal/runtime.js` has the argument; a build replaces
127
+ * `import.meta.hot` with `undefined` and every use below folds away.
128
+ */
129
+ const BOUNDARY_MARKS: boolean = import.meta.hot != null;
130
+
131
+ /** How a client reference says what it is. React's symbol, not uf's. */
132
+ const CLIENT_REFERENCE: symbol = Symbol.for("react.client.reference");
133
+
134
+ /**
135
+ * The renderer for one route table.
136
+ *
137
+ * The table is the server's whole one — every page, layout and boundary module
138
+ * — and it is `virtual:uf/routes` as this graph generates it.
139
+ */
140
+ export function createFlightRenderer(options: {|
141
+ readonly routes: RouteTable["routes"],
142
+ readonly notFound: RouteTable["notFound"],
143
+ readonly errors: RouteTable["errors"],
144
+ |}): FlightRenderer {
145
+ // Before anything else: on a React older than 19.3 nothing below can render,
146
+ // and React's Flight renderer would only say so from inside a render.
147
+ requireServerComponentsReact("@uniflowed/router/rsc");
148
+ const table: RouteTable = {
149
+ routes: options.routes,
150
+ notFound: options.notFound,
151
+ errors: options.errors,
152
+ };
153
+
154
+ return async function renderFlight(url: string, settings?: FlightOptions): Promise<FlightRender> {
155
+ let resolved: ResolvedRoute;
156
+ try {
157
+ const failure = settings?.failure;
158
+ resolved =
159
+ failure == null
160
+ ? // `onMatch` records the route pattern on the request before the
161
+ // loader runs, so every line the loader logs names its route.
162
+ await resolveMatch(table, url, { defer: settings?.defer !== false, onMatch: noteRoute })
163
+ : await resolveFailure(table, url, failure.error);
164
+ } catch (error) {
165
+ if (error instanceof RedirectError) {
166
+ return { kind: "redirect", status: error.permanent ? 308 : 307, location: error.to };
167
+ }
168
+ throw error;
169
+ }
170
+
171
+ const errorFile = nearestBoundary(table.errors, resolved.pathname)?.file ?? null;
172
+ const route = forTheBrowser(resolved, errorFile);
173
+ const state = routeState(route);
174
+ const marks = BOUNDARY_MARKS ? routeBoundaries(route, errorFile) : null;
175
+ const tree = (
176
+ <>
177
+ {composeRoute(route, { page: pageElement(route), marks })}
178
+ {BOUNDARY_MARKS && marks != null ? (
179
+ <BoundaryReporter path={route.path} boundaries={marks} />
180
+ ) : null}
181
+ </>
182
+ );
183
+ const root: FlightRoot = { route: state, tree };
184
+ const report = settings?.onError;
185
+ // Inside the route's store, so a server component's `useRoute()` finds the
186
+ // route however many `await`s into the render it asks.
187
+ const stream = withServerRoute(state, () =>
188
+ renderToReadableStream(root, {
189
+ // Handed over as it is, so that the digest it returns reaches the row.
190
+ // Absent, React logs the exception to the console itself.
191
+ onError: report,
192
+ signal: settings?.signal,
193
+ }),
194
+ );
195
+ return { kind: "route", status: route.status, stream, failure: renderFailure(route) };
196
+ };
197
+ }
198
+
199
+ /**
200
+ * The page element: the route's page with its data, the deferred page that
201
+ * waits for its loader, or the error route's page.
202
+ *
203
+ * An element for a component rather than a call, so that a page module with no
204
+ * component in it throws while React renders — inside the boundaries the
205
+ * composition placed — rather than before the render has begun.
206
+ */
207
+ function pageElement(route: ResolvedRoute): React.Node {
208
+ const error = route.error;
209
+ if (error != null) {
210
+ return <ErrorRoutePage module={route.errorBoundary.module} error={error} />;
211
+ }
212
+ const deferred = route.deferred;
213
+ if (deferred != null) {
214
+ return (
215
+ <DeferredPage
216
+ page={route.page}
217
+ params={route.params}
218
+ searchParams={route.searchParams}
219
+ loader={deferred}
220
+ />
221
+ );
222
+ }
223
+ return (
224
+ <RoutePage
225
+ page={route.page}
226
+ params={route.params}
227
+ searchParams={route.searchParams}
228
+ data={route.data}
229
+ />
230
+ );
231
+ }
232
+
233
+ /** A route's page, with its loader's answer. */
234
+ component RoutePage(
235
+ page: PageModule,
236
+ params: RouteParams,
237
+ searchParams: SearchParams,
238
+ data: mixed,
239
+ ) {
240
+ const Page = pageComponent(page);
241
+ return <Page params={params} searchParams={searchParams} data={data} />;
242
+ }
243
+
244
+ /**
245
+ * The same page, once the loader the router deferred has answered.
246
+ *
247
+ * `use` rather than an `async` component: it suspends at the same point, inside
248
+ * the innermost `$loading.js` boundary, and it is the one spelling the
249
+ * browser's `AwaitedPage` already uses for the same wait.
250
+ */
251
+ component DeferredPage(
252
+ page: PageModule,
253
+ params: RouteParams,
254
+ searchParams: SearchParams,
255
+ loader: Promise<mixed>,
256
+ ) {
257
+ return <RoutePage page={page} params={params} searchParams={searchParams} data={use(loader)} />;
258
+ }
259
+
260
+ /**
261
+ * The resolved route with every error boundary as the browser receives it.
262
+ *
263
+ * An error module is the server's module; what the browser needs is its
264
+ * component, and in this graph a `"use client"` component *is* a client
265
+ * reference. So each boundary becomes `{ default: <reference> }` — a plain
266
+ * object Flight can carry — and a boundary whose component is not a reference
267
+ * is refused with its file named.
268
+ */
269
+ function forTheBrowser(resolved: ResolvedRoute, file: string | null): ResolvedRoute {
270
+ const boundary = resolved.errorBoundary;
271
+ return {
272
+ ...resolved,
273
+ errorBoundary: { above: boundary.above, module: clientErrorModule(boundary.module, file) },
274
+ slots: resolved.slots.map(slotForTheBrowser),
275
+ };
276
+ }
277
+
278
+ function slotForTheBrowser(slot: ResolvedSlot): ResolvedSlot {
279
+ const boundary = slot.errorBoundary;
280
+ return {
281
+ ...slot,
282
+ errorBoundary:
283
+ boundary == null
284
+ ? null
285
+ : { above: boundary.above, module: clientErrorModule(boundary.module, null) },
286
+ slots: slot.slots.map(slotForTheBrowser),
287
+ };
288
+ }
289
+
290
+ function clientErrorModule(module: ?ErrorModule, file: string | null): ?ErrorModule {
291
+ if (module == null) {
292
+ return null;
293
+ }
294
+ const component = module.default ?? module.Error;
295
+ if (component == null) {
296
+ throw new Error(
297
+ "@uniflowed/router: an error module must export a component as `default` or `Error`",
298
+ );
299
+ }
300
+ if (!isClientReference(component)) {
301
+ throw new Error(
302
+ `@uniflowed/router: ${file ?? "an `$error.js` inside a slot"} is an error boundary, and an ` +
303
+ "error boundary catches a throw while the browser renders — so its component runs in " +
304
+ "the browser, and the module has to open with the use client directive, as its first " +
305
+ "statement.",
306
+ );
307
+ }
308
+ return { default: component };
309
+ }
310
+
311
+ function isClientReference(value: mixed): boolean {
312
+ if (value == null || (typeof value !== "object" && typeof value !== "function")) {
313
+ return false;
314
+ }
315
+ const tagged: { +$$typeof?: mixed, ... } = (value: $FlowFixMe);
316
+ return tagged.$$typeof === CLIENT_REFERENCE;
317
+ }
318
+
319
+ /** The exception a resolved route fell back to its error boundary for. */
320
+ function renderFailure(resolved: ResolvedRoute): mixed {
321
+ if (resolved.error == null) {
322
+ return undefined;
323
+ }
324
+ return match (resolved.error) {
325
+ {kind: "thrown", error: const error} => error,
326
+ {kind: "unauthorized"} => undefined,
327
+ {kind: "forbidden"} => undefined,
328
+ };
329
+ }
@@ -0,0 +1,159 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/router`, as a React Server Component imports it.
4
+ //
5
+ // The package root has two entries and an export condition picks between them:
6
+ // a module graph resolved under `react-server` — the one uf renders server
7
+ // components in (ubugeeei-prod/uf#519) — gets this file, and every other graph
8
+ // gets `./index.js`. Both export the same names, which is the point. A layout
9
+ // imports `Link` and `useRoute` from `@uniflowed/router` and means the same
10
+ // thing wherever it is rendered; what differs is what the names *are* here.
11
+ //
12
+ // * `Link`, `RouterProvider`, `RouteView` and `routerView` come from
13
+ // `./internal/runtime.js`, a `"use client"` module, so in this graph each one
14
+ // is a client reference: rendered as markup on the server and hydrated in
15
+ // the browser, with its code shipped for the browser's half only.
16
+ // * `useRoute`, `useLoaderData` and `useSeo` are server implementations. A
17
+ // server component has no context to read, so they read the route the
18
+ // Flight renderer is rendering — `./internal/server-route.js` — which is the
19
+ // same route the browser's hooks read back out of the payload. This
20
+ // repository's documentation site highlights the section a reader is in
21
+ // from a server layout, and would otherwise have needed a client component to
22
+ // find out where it was.
23
+ // * `useRouter` refuses, by name. Navigation happens in the browser, and a
24
+ // router a server component could hold would be methods that can never do
25
+ // anything.
26
+ // * `useIsServer` answers `true`, which is what it has always answered while a
27
+ // server renders.
28
+ //
29
+ // Everything else — matching, the router's control errors, resolution — is
30
+ // React-free and is the same code `./index.js` exports.
31
+
32
+ import * as React from "react";
33
+ import { use } from "react";
34
+
35
+ import { Head } from "./internal/head.js";
36
+ import type { Metadata } from "./internal/resolve.js";
37
+ import type { RouteInfo, Router } from "./internal/runtime.js";
38
+ import { serverRoute } from "./internal/server-route.js";
39
+
40
+ export type { ErrorProps, LayoutProps, PageProps } from "./index.js";
41
+
42
+ export type {
43
+ AppProps,
44
+ ErrorBoundary,
45
+ ErrorModule,
46
+ Interception,
47
+ JsonLd,
48
+ LayoutModule,
49
+ LinkPrefetch,
50
+ LoaderArgs,
51
+ Metadata,
52
+ MetadataArgs,
53
+ NavigateOptions,
54
+ NotFoundBoundary,
55
+ PageModule,
56
+ ResolvedRoute,
57
+ Robots,
58
+ RouteError,
59
+ RouteInfo,
60
+ RouteMatch,
61
+ RouteParamSpec,
62
+ RouteParams,
63
+ RouteRecord,
64
+ RouteTable,
65
+ Router,
66
+ ResolvedSlot,
67
+ SearchParams,
68
+ SlotRecord,
69
+ SlotRouteRecord,
70
+ TemplateModule,
71
+ TwitterCard,
72
+ } from "./internal/runtime.js";
73
+
74
+ export { Link, RouteView, RouterProvider, routerView } from "./internal/runtime.js";
75
+ // `app.router.basePath`, for a Server Component that builds an address itself.
76
+ // The rsc entry installs it; `./internal/base-path.js` has no directive, so this
77
+ // graph gets the function rather than a reference to it.
78
+ export { basePath } from "./internal/base-path.js";
79
+
80
+ export {
81
+ ForbiddenError,
82
+ NotFoundError,
83
+ RedirectError,
84
+ UnauthorizedError,
85
+ buildRoute,
86
+ forbidden,
87
+ hasClientPage,
88
+ matchRoute,
89
+ notFound,
90
+ parseSearch,
91
+ permanentRedirect,
92
+ redirect,
93
+ routeErrorStatus,
94
+ splitUrl,
95
+ unauthorized,
96
+ } from "./internal/routing.js";
97
+
98
+ export { resolveFailure, resolveMatch } from "./internal/resolve.js";
99
+
100
+ /**
101
+ * The route this server component is rendering in.
102
+ *
103
+ * `pending` is always `false`: a pending navigation is a fact about a browser
104
+ * that has asked for the next route and not received it, and a server renders
105
+ * the route it was asked for. `data` waits for a loader the router deferred,
106
+ * which is what the browser's `useRoute` does too, so a component that reads
107
+ * the data suspends at the same boundary on both sides.
108
+ */
109
+ export hook useRoute(): RouteInfo {
110
+ const route = serverRoute("useRoute");
111
+ return {
112
+ path: route.path,
113
+ pathname: route.pathname,
114
+ params: route.params,
115
+ searchParams: route.searchParams,
116
+ data: route.deferred == null ? route.data : use(route.deferred),
117
+ pending: false,
118
+ };
119
+ }
120
+
121
+ /** The current page's loader data; see `useRoute` for what a deferred loader does. */
122
+ export hook useLoaderData(): mixed {
123
+ const route = serverRoute("useLoaderData");
124
+ return route.deferred == null ? route.data : use(route.deferred);
125
+ }
126
+
127
+ /**
128
+ * A refusal: navigation is the browser's.
129
+ *
130
+ * Thrown rather than handed back as methods that do nothing, because a server
131
+ * component that called `router.push` in response to something would be
132
+ * waiting for a navigation that cannot happen, and nothing would say so.
133
+ */
134
+ export hook useRouter(): Router {
135
+ throw new Error(
136
+ "@uniflowed/router: useRouter() was called in a Server Component, and navigation happens " +
137
+ "in the browser. Call it from a client component — a module that opens with the use " +
138
+ "client directive — or render a <Link>, which is one, and which a Server Component can " +
139
+ "render.",
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Head elements a server component contributes, with the route's
145
+ * `metadataBase` applied to the relative URLs it names.
146
+ *
147
+ * The same component the browser's `useSeo` renders, and the same base: the one
148
+ * the root layout declared, read from the route rather than from context.
149
+ */
150
+ export hook useSeo(seo: Metadata): React.Node {
151
+ const route = serverRoute("useSeo");
152
+ const base = seo.metadataBase ?? route.metadata.metadataBase;
153
+ return <Head metadata={base == null ? seo : { ...seo, metadataBase: base }} />;
154
+ }
155
+
156
+ /** Whether the app is being rendered on the server: here, always. */
157
+ export hook useIsServer(): boolean {
158
+ return true;
159
+ }
package/server.js CHANGED
@@ -24,14 +24,11 @@
24
24
  // which.
25
25
 
26
26
  import { noteRoute } from "@uniflowed/server/host";
27
- import { ROOT_ID } from "./internal/document.js";
28
27
  import * as React from "react";
29
28
 
30
29
  import {
31
30
  type DocumentBody,
32
- type DocumentShell,
33
31
  type WritableLike,
34
- bodyOfText,
35
32
  prerenderDocument,
36
33
  renderDocument,
37
34
  } from "./internal/stream.js";
@@ -47,6 +44,7 @@ import {
47
44
  } from "./internal/runtime.js";
48
45
 
49
46
  import { type StreamDiagnostic, streamReporter } from "./internal/inspector.js";
47
+ import { redirectDocument, redirectResult, shellFor } from "./internal/shell.js";
50
48
 
51
49
  /** Asset URLs to reference from the document. */
52
50
  export type RenderAssets = {|
@@ -101,6 +99,31 @@ export type PrerenderResult = {|
101
99
  readonly headers?: { readonly [string]: string },
102
100
  /** The exception this render fell back to its error boundary for; see [`RenderResult`]. */
103
101
  readonly error?: mixed,
102
+ /**
103
+ * The Flight payload the document was rendered from, for a document React
104
+ * Server Components rendered.
105
+ *
106
+ * `uf build` writes it beside the document as the route's payload file, so a
107
+ * browser that navigates to a prerendered route fetches a file rather than
108
+ * asking a server — which is what makes a static host able to serve client
109
+ * navigation at all. Absent for a document rendered from its modules.
110
+ */
111
+ readonly payload?: Uint8Array,
112
+ |};
113
+
114
+ /**
115
+ * A route's payload, as a browser navigating to it is answered.
116
+ *
117
+ * `stream` is `null` for a redirect, whose `location` is already the target's
118
+ * payload URL when the target is on this origin: `fetch` follows it and lands
119
+ * on a payload.
120
+ */
121
+ export type FlightResponse = {|
122
+ readonly status: number,
123
+ readonly headers: { readonly [string]: string },
124
+ readonly stream: ReadableStream<Uint8Array> | null,
125
+ /** The exception the route resolved to its error boundary for; see [`RenderResult`]. */
126
+ readonly error?: mixed,
104
127
  |};
105
128
 
106
129
  /** What a host may tell the renderer about one request. */
@@ -208,6 +231,14 @@ export type {
208
231
  } from "./middleware.js";
209
232
  export { createMiddlewareRunner } from "./middleware.js";
210
233
 
234
+ /**
235
+ * `app.router.basePath` and `trailingSlash`, for `virtual:uf/server` to install
236
+ * before the first render, and `basePath()` for a middleware or a route handler
237
+ * that builds an address itself. See `./internal/base-path.js`.
238
+ */
239
+ export type { RoutingSettings, TrailingSlash } from "./internal/base-path.js";
240
+ export { basePath, installRouting } from "./internal/base-path.js";
241
+
211
242
  /**
212
243
  * The endpoint a `"use server"` export is dialled at.
213
244
  *
@@ -237,15 +268,6 @@ type Resolution =
237
268
  | {| readonly kind: "route", readonly route: ResolvedRoute |}
238
269
  | {| readonly kind: "redirect", readonly error: RedirectError |};
239
270
 
240
- /** A redirect, as the finished document `prerender` answers with. */
241
- async function redirectResult(document: RenderResult): Promise<PrerenderResult> {
242
- return {
243
- status: document.status,
244
- headers: document.headers,
245
- html: await document.text(),
246
- };
247
- }
248
-
249
271
  /** The two ways one app answers for a URL. */
250
272
  export type Renderer = {|
251
273
  readonly render: (
@@ -258,6 +280,14 @@ export type Renderer = {|
258
280
  assets: RenderAssets,
259
281
  options?: RenderOptions,
260
282
  ) => Promise<PrerenderResult>,
283
+ /**
284
+ * A route's payload, for a browser that is navigating rather than loading a
285
+ * document. Only a renderer for React Server Components has one.
286
+ */
287
+ readonly flight?: (
288
+ url: string,
289
+ options?: {| readonly onError?: (error: mixed) => void |},
290
+ ) => Promise<FlightResponse>,
261
291
  |};
262
292
 
263
293
  export function createRenderer(options: {|
@@ -442,54 +472,6 @@ function renderFailure(resolved: ResolvedRoute): mixed {
442
472
  };
443
473
  }
444
474
 
445
- function redirectDocument(error: RedirectError): RenderResult {
446
- const target = escapeAttribute(error.to);
447
- // A document rather than an empty body, because a redirect is still an answer
448
- // a browser may be shown; it goes through the same three methods as a
449
- // rendered one so that a host has one shape to write, not two.
450
- const body = bodyOfText(
451
- `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0; url=${target}"><title>Redirecting</title></head><body><a href="${target}">Redirecting…</a></body></html>\n`,
452
- );
453
- return {
454
- status: error.permanent ? 308 : 307,
455
- headers: { Location: error.to },
456
- pipe: body.pipe,
457
- stream: body.stream,
458
- text: body.text,
459
- };
460
- }
461
-
462
- /**
463
- * The document uf writes around the app's markup.
464
- *
465
- * The same two shapes `assemble` chose between, decided from the same evidence
466
- * — whether the markup opens with `<html>` — but stated up front instead of
467
- * afterwards, because a stream has no "afterwards" in which to splice a head.
468
- * An app whose root layout renders `<html>` owns the whole document and the
469
- * client hydrates `document`, so uf contributes only the tags that go before
470
- * `</head>`. An app that renders only content is wrapped in a minimal shell
471
- * around `<div id="uf-root">`, which is what the client hydrates instead.
472
- *
473
- * `internal/stream.js` picks between them on the opening bytes React writes;
474
- * everything either shape is made of is here, so what a uf document contains is
475
- * still readable in one place.
476
- *
477
- * # Why the shell is three strings and not one
478
- *
479
- * Because uf's own `<head>` has to still be open when React's head tags arrive.
480
- * React hoists a `<title>`, a `<meta>` and a `<link>` into the head it wrote
481
- * itself, and here it wrote none — so with one string this shell closed its
482
- * head before the app had rendered a byte, and every `og:` tag and the
483
- * `<link rel="canonical">` landed in the body, where a crawler ignores them.
484
- * `open` is uf's head up to that point, `body` is the rest of it and the
485
- * wrapper, and what goes between them is whatever `assembled` lifts out of the
486
- * app's own markup. See ubugeeei-prod/uf#547.
487
- *
488
- * That is also why no `<title>` is written here any more. It was, from
489
- * `resolved.metadata.title` — the same string `Head` renders — so a document
490
- * carried two of them, one in each place, and only one was where a browser
491
- * looks. Hoisting the rendered one leaves the metadata with a single source.
492
- */
493
475
  /**
494
476
  * The document a single-page build writes, and the only one it writes.
495
477
  *
@@ -519,31 +501,3 @@ export function shellDocument(assets: RenderAssets): string {
519
501
  const shell = shellFor(assets);
520
502
  return `${shell.open}${shell.body}${shell.close}`;
521
503
  }
522
-
523
- function shellFor(assets: RenderAssets): DocumentShell {
524
- const head = headTags(assets);
525
- return {
526
- head,
527
- open: `<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">`,
528
- body: `${head}</head><body><div id="${ROOT_ID}">`,
529
- close: `</div></body></html>\n`,
530
- };
531
- }
532
-
533
- function headTags(assets: RenderAssets): string {
534
- let tags = "";
535
- for (const href of assets.styles) {
536
- tags += `<link rel="stylesheet" href="${escapeAttribute(href)}">`;
537
- }
538
- for (const href of assets.preloads) {
539
- tags += `<link rel="modulepreload" href="${escapeAttribute(href)}">`;
540
- }
541
- for (const src of assets.scripts) {
542
- tags += `<script type="module" src="${escapeAttribute(src)}"></script>`;
543
- }
544
- return tags;
545
- }
546
-
547
- function escapeAttribute(value: string): string {
548
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
549
- }