@uniflowed/router 0.0.0-alpha.9 → 0.1.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/action.js +324 -0
- package/client.js +261 -7
- package/handler.js +113 -99
- package/http-client.js +104 -0
- package/index.js +49 -6
- package/instrumentation.js +92 -0
- package/internal/action-endpoint.js +438 -0
- package/internal/action-wire.js +608 -0
- package/internal/base-path.js +175 -0
- package/internal/boundaries.js +481 -0
- package/internal/boundary-data.js +88 -0
- package/internal/compose.js +490 -0
- package/internal/deployment.js +160 -0
- package/internal/devtools.js +131 -0
- package/internal/diagnostics.js +169 -0
- package/internal/error-view.js +193 -0
- package/internal/flight-browser.js +242 -0
- package/internal/flight-chunks.js +205 -0
- package/internal/flight-rows.js +135 -0
- package/internal/flight-ssr.js +91 -0
- package/internal/flight.js +192 -0
- package/internal/head.js +219 -0
- package/internal/hydration.js +1085 -0
- package/internal/inspector.js +626 -0
- package/internal/native-links.js +67 -0
- package/internal/native-tree.js +89 -0
- package/internal/navigation-cache.js +181 -0
- package/internal/payload-rows.js +270 -0
- package/internal/payload.js +685 -0
- package/internal/prepare-document.js +54 -0
- package/internal/react-version.js +77 -0
- package/internal/resolve.js +1617 -0
- package/internal/resolved-summary.js +199 -0
- package/internal/routing.js +478 -0
- package/internal/runtime.js +1593 -1341
- package/internal/server-instrumentation.js +12 -0
- package/internal/server-route.js +58 -0
- package/internal/shell.js +125 -0
- package/internal/stream.js +754 -21
- package/middleware.js +161 -22
- package/native-navigation.js +217 -0
- package/native.js +416 -0
- package/package.json +48 -7
- package/routing.js +51 -0
- package/rsc-client.js +120 -0
- package/rsc-ssr.js +637 -0
- package/rsc.js +402 -0
- package/server-components.js +159 -0
- package/server.js +254 -106
package/rsc.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
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 { traceLoader } from "./internal/server-instrumentation.js";
|
|
48
|
+
|
|
49
|
+
import * as React from "react";
|
|
50
|
+
import { use } from "react";
|
|
51
|
+
// React's Flight server. Only this graph can load it: it refuses to evaluate
|
|
52
|
+
// unless `react` resolved under the `react-server` condition.
|
|
53
|
+
import { renderToReadableStream } from "react-server-dom-parcel/server";
|
|
54
|
+
|
|
55
|
+
import { noteRoute } from "@uniflowed/server/host";
|
|
56
|
+
import { reportRequestError } from "@uniflowed/server/instrumentation";
|
|
57
|
+
|
|
58
|
+
import { BoundaryReporter } from "./internal/boundaries.js";
|
|
59
|
+
import { routeBoundaries } from "./internal/boundary-data.js";
|
|
60
|
+
import { composeRoute, pageComponent } from "./internal/compose.js";
|
|
61
|
+
import { ErrorRoutePage } from "./internal/error-view.js";
|
|
62
|
+
import { type FlightRoot, routeState } from "./internal/flight.js";
|
|
63
|
+
import { requireServerComponentsReact } from "./internal/react-version.js";
|
|
64
|
+
import type { ErrorModule, PageModule, ResolvedRoute, ResolvedSlot } from "./internal/resolve.js";
|
|
65
|
+
import { resolveFailure, resolveInterception, resolveMatch } from "./internal/resolve.js";
|
|
66
|
+
import type { RouteParams, RouteTable, SearchParams } from "./internal/routing.js";
|
|
67
|
+
import { RedirectError, nearestBoundary } from "./internal/routing.js";
|
|
68
|
+
import { withServerRoute } from "./internal/server-route.js";
|
|
69
|
+
|
|
70
|
+
export type { FlightRoot, RouteState } from "./internal/flight.js";
|
|
71
|
+
// For `virtual:uf/rsc`, so a Server Component's `basePath()` is the project's.
|
|
72
|
+
export { installRouting } from "./internal/base-path.js";
|
|
73
|
+
|
|
74
|
+
/** What a host may tell the renderer about one render. */
|
|
75
|
+
export type FlightOptions = {|
|
|
76
|
+
/**
|
|
77
|
+
* Whether a loader may be left running into a `$loading.js` boundary.
|
|
78
|
+
*
|
|
79
|
+
* On by default: a payload streams, so a slow loader is a fallback now and
|
|
80
|
+
* its answer later. A prerender turns it off, because a file has no "later".
|
|
81
|
+
*/
|
|
82
|
+
readonly defer?: boolean,
|
|
83
|
+
/**
|
|
84
|
+
* Every exception the render recovered from, including the late ones.
|
|
85
|
+
*
|
|
86
|
+
* What it returns becomes the exception's digest. React writes the digest
|
|
87
|
+
* into the row that stands in for the part of the tree that failed, and sets
|
|
88
|
+
* it on the error its Flight client rebuilds from that row — which is how
|
|
89
|
+
* `./server.js` recognises, in the HTML renderer, the copy of an exception
|
|
90
|
+
* this renderer has already reported.
|
|
91
|
+
*/
|
|
92
|
+
readonly onError?: (error: mixed) => ?string,
|
|
93
|
+
/**
|
|
94
|
+
* Render the error boundary for this exception instead of resolving the URL.
|
|
95
|
+
*
|
|
96
|
+
* What the HTML renderer asks for when the shell it was streaming from the
|
|
97
|
+
* payload threw before its first byte: the route resolved, and rendering it
|
|
98
|
+
* did not.
|
|
99
|
+
*/
|
|
100
|
+
readonly failure?: {| readonly error: mixed |},
|
|
101
|
+
/**
|
|
102
|
+
* The page a browser was showing when it asked for this payload.
|
|
103
|
+
*
|
|
104
|
+
* A document request never sets it. A client navigation may, because only the
|
|
105
|
+
* browser knows the page it is navigating from, while only this renderer can
|
|
106
|
+
* render the intercepted tree.
|
|
107
|
+
*/
|
|
108
|
+
readonly interceptedFrom?: string,
|
|
109
|
+
/** Stops the render, for a reader that went away. */
|
|
110
|
+
readonly signal?: AbortSignal,
|
|
111
|
+
|};
|
|
112
|
+
|
|
113
|
+
/** A render: a redirect to answer with, or a route and its payload. */
|
|
114
|
+
export type FlightRender =
|
|
115
|
+
| {| readonly kind: "redirect", readonly status: 307 | 308, readonly location: string |}
|
|
116
|
+
| {|
|
|
117
|
+
readonly kind: "route",
|
|
118
|
+
readonly status: 200 | 401 | 403 | 404 | 500,
|
|
119
|
+
/** The payload, as React writes it. Read exactly once. */
|
|
120
|
+
readonly stream: ReadableStream<Uint8Array>,
|
|
121
|
+
/**
|
|
122
|
+
* The exception this route resolved to its error boundary for, when it did.
|
|
123
|
+
*
|
|
124
|
+
* The same field `./server.js` has always carried as `error`: `uf build`
|
|
125
|
+
* fails a route that set it and `uf dev` reports it. `forbidden()` and
|
|
126
|
+
* `unauthorized()` do not set it.
|
|
127
|
+
*/
|
|
128
|
+
readonly failure: mixed,
|
|
129
|
+
|};
|
|
130
|
+
|
|
131
|
+
/** Render one URL. */
|
|
132
|
+
export type FlightRenderer = (url: string, options?: FlightOptions) => Promise<FlightRender>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether this bundle marks the boundaries it renders.
|
|
136
|
+
*
|
|
137
|
+
* `BOUNDARY_MARKS` in `./internal/runtime.js` has the argument; a build replaces
|
|
138
|
+
* `import.meta.hot` with `undefined` and every use below folds away.
|
|
139
|
+
*/
|
|
140
|
+
const BOUNDARY_MARKS: boolean = import.meta.hot != null;
|
|
141
|
+
|
|
142
|
+
/** How a client reference says what it is. React's symbol, not uf's. */
|
|
143
|
+
const CLIENT_REFERENCE: symbol = Symbol.for("react.client.reference");
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The renderer for one route table.
|
|
147
|
+
*
|
|
148
|
+
* The table is the server's whole one — every page, layout and boundary module
|
|
149
|
+
* — and it is `virtual:uf/routes` as this graph generates it.
|
|
150
|
+
*/
|
|
151
|
+
export function createFlightRenderer(options: {|
|
|
152
|
+
readonly routes: RouteTable["routes"],
|
|
153
|
+
readonly notFound: RouteTable["notFound"],
|
|
154
|
+
readonly errors: RouteTable["errors"],
|
|
155
|
+
/**
|
|
156
|
+
* The build's deployment id, written into every payload's root so that a
|
|
157
|
+
* page on another build can tell — including from a payload that was
|
|
158
|
+
* prerendered into a file. `null` under `uf dev`. See
|
|
159
|
+
* `./internal/deployment.js`.
|
|
160
|
+
*/
|
|
161
|
+
readonly deployment?: string | null,
|
|
162
|
+
|}): FlightRenderer {
|
|
163
|
+
// Before anything else: on a React older than 19.3 nothing below can render,
|
|
164
|
+
// and React's Flight renderer would only say so from inside a render.
|
|
165
|
+
requireServerComponentsReact("@uniflowed/router/rsc");
|
|
166
|
+
const table: RouteTable = {
|
|
167
|
+
routes: options.routes,
|
|
168
|
+
notFound: options.notFound,
|
|
169
|
+
errors: options.errors,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return async function renderFlight(url: string, settings?: FlightOptions): Promise<FlightRender> {
|
|
173
|
+
let resolved: ResolvedRoute;
|
|
174
|
+
try {
|
|
175
|
+
const failure = settings?.failure;
|
|
176
|
+
if (failure == null) {
|
|
177
|
+
const defer = settings?.defer !== false;
|
|
178
|
+
const intercepted = await resolveFlightInterception(
|
|
179
|
+
table,
|
|
180
|
+
url,
|
|
181
|
+
settings?.interceptedFrom,
|
|
182
|
+
defer,
|
|
183
|
+
);
|
|
184
|
+
resolved =
|
|
185
|
+
intercepted ??
|
|
186
|
+
// `onMatch` records the route pattern on the request before the
|
|
187
|
+
// loader runs, so every line the loader logs names its route.
|
|
188
|
+
(await resolveMatch(table, url, { defer, onMatch: noteRoute, runLoader: traceLoader }));
|
|
189
|
+
} else {
|
|
190
|
+
resolved = await resolveFailure(table, url, failure.error);
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof RedirectError) {
|
|
194
|
+
return { kind: "redirect", status: error.permanent ? 308 : 307, location: error.to };
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const errorFile = nearestBoundary(table.errors, resolved.pathname)?.file ?? null;
|
|
200
|
+
const route = forTheBrowser(resolved, errorFile);
|
|
201
|
+
const state = routeState(route);
|
|
202
|
+
const marks = BOUNDARY_MARKS ? routeBoundaries(route, errorFile) : null;
|
|
203
|
+
const tree = (
|
|
204
|
+
<>
|
|
205
|
+
{composeRoute(route, { page: pageElement(route), marks })}
|
|
206
|
+
{BOUNDARY_MARKS && marks != null ? (
|
|
207
|
+
<BoundaryReporter path={route.path} boundaries={marks} />
|
|
208
|
+
) : null}
|
|
209
|
+
</>
|
|
210
|
+
);
|
|
211
|
+
const root: FlightRoot = { route: state, tree, deployment: options.deployment ?? null };
|
|
212
|
+
const failure = renderFailure(route);
|
|
213
|
+
if (failure != null) reportRequestError(failure, "render");
|
|
214
|
+
const report = (error) => {
|
|
215
|
+
reportRequestError(error, "render");
|
|
216
|
+
if (settings?.onError != null) return settings.onError(error);
|
|
217
|
+
console.error(error);
|
|
218
|
+
return undefined;
|
|
219
|
+
};
|
|
220
|
+
// Inside the route's store, so a server component's `useRoute()` finds the
|
|
221
|
+
// route however many `await`s into the render it asks.
|
|
222
|
+
const stream = withServerRoute(state, () =>
|
|
223
|
+
renderToReadableStream(root, {
|
|
224
|
+
// Handed over as it is, so that the digest it returns reaches the row.
|
|
225
|
+
// Our callback preserves React's console fallback when none was given.
|
|
226
|
+
onError: report,
|
|
227
|
+
signal: settings?.signal,
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
return { kind: "route", status: route.status, stream, failure: renderFailure(route) };
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function resolveFlightInterception(
|
|
235
|
+
table: RouteTable,
|
|
236
|
+
url: string,
|
|
237
|
+
from: ?string,
|
|
238
|
+
defer: boolean,
|
|
239
|
+
): Promise<?ResolvedRoute> {
|
|
240
|
+
const baseUrl = usableInterceptionBase(from);
|
|
241
|
+
if (baseUrl == null) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const base = await resolveMatch(table, baseUrl, {
|
|
246
|
+
defer,
|
|
247
|
+
onMatch: noteRoute,
|
|
248
|
+
runLoader: traceLoader,
|
|
249
|
+
});
|
|
250
|
+
return await resolveInterception(table, base, url);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error instanceof RedirectError) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function usableInterceptionBase(from: ?string): ?string {
|
|
260
|
+
if (from == null || !from.startsWith("/") || from.startsWith("//")) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const hash = from.indexOf("#");
|
|
264
|
+
return hash === -1 ? from : from.slice(0, hash);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The page element: the route's page with its data, the deferred page that
|
|
269
|
+
* waits for its loader, or the error route's page.
|
|
270
|
+
*
|
|
271
|
+
* An element for a component rather than a call, so that a page module with no
|
|
272
|
+
* component in it throws while React renders — inside the boundaries the
|
|
273
|
+
* composition placed — rather than before the render has begun.
|
|
274
|
+
*/
|
|
275
|
+
function pageElement(route: ResolvedRoute): React.Node {
|
|
276
|
+
const error = route.error;
|
|
277
|
+
if (error != null) {
|
|
278
|
+
return <ErrorRoutePage module={route.errorBoundary.module} error={error} />;
|
|
279
|
+
}
|
|
280
|
+
const deferred = route.deferred;
|
|
281
|
+
if (deferred != null) {
|
|
282
|
+
return (
|
|
283
|
+
<DeferredPage
|
|
284
|
+
page={route.page}
|
|
285
|
+
params={route.params}
|
|
286
|
+
searchParams={route.searchParams}
|
|
287
|
+
loader={deferred}
|
|
288
|
+
/>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return (
|
|
292
|
+
<RoutePage
|
|
293
|
+
page={route.page}
|
|
294
|
+
params={route.params}
|
|
295
|
+
searchParams={route.searchParams}
|
|
296
|
+
data={route.data}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A route's page, with its loader's answer. */
|
|
302
|
+
component RoutePage(
|
|
303
|
+
page: PageModule,
|
|
304
|
+
params: RouteParams,
|
|
305
|
+
searchParams: SearchParams,
|
|
306
|
+
data: mixed,
|
|
307
|
+
) {
|
|
308
|
+
const Page = pageComponent(page);
|
|
309
|
+
// The route module's own export, looked up by route: `pageComponent` hands
|
|
310
|
+
// back its `default` or `Page` as it is, so this is the same component on
|
|
311
|
+
// every render of the same route. The React Compiler cannot see through the
|
|
312
|
+
// lookup and reports a component created during render.
|
|
313
|
+
// uf-lint-disable-next-line react-compiler/static-components
|
|
314
|
+
return <Page params={params} searchParams={searchParams} data={data} />;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The same page, once the loader the router deferred has answered.
|
|
319
|
+
*
|
|
320
|
+
* `use` rather than an `async` component: it suspends at the same point, inside
|
|
321
|
+
* the innermost `$loading.js` boundary, and it is the one spelling the
|
|
322
|
+
* browser's `AwaitedPage` already uses for the same wait.
|
|
323
|
+
*/
|
|
324
|
+
component DeferredPage(
|
|
325
|
+
page: PageModule,
|
|
326
|
+
params: RouteParams,
|
|
327
|
+
searchParams: SearchParams,
|
|
328
|
+
loader: Promise<mixed>,
|
|
329
|
+
) {
|
|
330
|
+
return <RoutePage page={page} params={params} searchParams={searchParams} data={use(loader)} />;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The resolved route with every error boundary as the browser receives it.
|
|
335
|
+
*
|
|
336
|
+
* An error module is the server's module; what the browser needs is its
|
|
337
|
+
* component, and in this graph a `"use client"` component *is* a client
|
|
338
|
+
* reference. So each boundary becomes `{ default: <reference> }` — a plain
|
|
339
|
+
* object Flight can carry — and a boundary whose component is not a reference
|
|
340
|
+
* is refused with its file named.
|
|
341
|
+
*/
|
|
342
|
+
function forTheBrowser(resolved: ResolvedRoute, file: string | null): ResolvedRoute {
|
|
343
|
+
const boundary = resolved.errorBoundary;
|
|
344
|
+
return {
|
|
345
|
+
...resolved,
|
|
346
|
+
errorBoundary: { above: boundary.above, module: clientErrorModule(boundary.module, file) },
|
|
347
|
+
slots: resolved.slots.map(slotForTheBrowser),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function slotForTheBrowser(slot: ResolvedSlot): ResolvedSlot {
|
|
352
|
+
const boundary = slot.errorBoundary;
|
|
353
|
+
return {
|
|
354
|
+
...slot,
|
|
355
|
+
errorBoundary:
|
|
356
|
+
boundary == null
|
|
357
|
+
? null
|
|
358
|
+
: { above: boundary.above, module: clientErrorModule(boundary.module, null) },
|
|
359
|
+
slots: slot.slots.map(slotForTheBrowser),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function clientErrorModule(module: ?ErrorModule, file: string | null): ?ErrorModule {
|
|
364
|
+
if (module == null) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const component = module.default ?? module.Error;
|
|
368
|
+
if (component == null) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
"@uniflowed/router: an error module must export a component as `default` or `Error`",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (!isClientReference(component)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`@uniflowed/router: ${file ?? "an `$error.js` inside a slot"} is an error boundary, and an ` +
|
|
376
|
+
"error boundary catches a throw while the browser renders — so its component runs in " +
|
|
377
|
+
"the browser, and the module has to open with the use client directive, as its first " +
|
|
378
|
+
"statement.",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return { default: component };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isClientReference(value: mixed): boolean {
|
|
385
|
+
if (value == null || (typeof value !== "object" && typeof value !== "function")) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const tagged: { +$$typeof?: mixed, ... } = (value: $FlowFixMe);
|
|
389
|
+
return tagged.$$typeof === CLIENT_REFERENCE;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The exception a resolved route fell back to its error boundary for. */
|
|
393
|
+
function renderFailure(resolved: ResolvedRoute): mixed {
|
|
394
|
+
if (resolved.error == null) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
return match (resolved.error) {
|
|
398
|
+
{kind: "thrown", error: const error} => error,
|
|
399
|
+
{kind: "unauthorized"} => undefined,
|
|
400
|
+
{kind: "forbidden"} => undefined,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
@@ -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, useLinkStatus } 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
|
+
}
|