@uniflowed/router 0.0.0-alpha.8 → 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.
Files changed (49) hide show
  1. package/action.js +324 -0
  2. package/client.js +261 -7
  3. package/handler.js +113 -99
  4. package/http-client.js +104 -0
  5. package/index.js +49 -6
  6. package/instrumentation.js +92 -0
  7. package/internal/action-endpoint.js +438 -0
  8. package/internal/action-wire.js +608 -0
  9. package/internal/base-path.js +175 -0
  10. package/internal/boundaries.js +481 -0
  11. package/internal/boundary-data.js +88 -0
  12. package/internal/compose.js +490 -0
  13. package/internal/deployment.js +160 -0
  14. package/internal/devtools.js +131 -0
  15. package/internal/diagnostics.js +169 -0
  16. package/internal/error-view.js +193 -0
  17. package/internal/flight-browser.js +242 -0
  18. package/internal/flight-chunks.js +205 -0
  19. package/internal/flight-rows.js +135 -0
  20. package/internal/flight-ssr.js +91 -0
  21. package/internal/flight.js +192 -0
  22. package/internal/head.js +219 -0
  23. package/internal/hydration.js +1085 -0
  24. package/internal/inspector.js +626 -0
  25. package/internal/native-links.js +67 -0
  26. package/internal/native-tree.js +89 -0
  27. package/internal/navigation-cache.js +181 -0
  28. package/internal/payload-rows.js +270 -0
  29. package/internal/payload.js +685 -0
  30. package/internal/prepare-document.js +54 -0
  31. package/internal/react-version.js +77 -0
  32. package/internal/resolve.js +1617 -0
  33. package/internal/resolved-summary.js +199 -0
  34. package/internal/routing.js +478 -0
  35. package/internal/runtime.js +1597 -1329
  36. package/internal/server-instrumentation.js +12 -0
  37. package/internal/server-route.js +58 -0
  38. package/internal/shell.js +125 -0
  39. package/internal/stream.js +754 -21
  40. package/middleware.js +161 -22
  41. package/native-navigation.js +217 -0
  42. package/native.js +416 -0
  43. package/package.json +48 -7
  44. package/routing.js +51 -0
  45. package/rsc-client.js +120 -0
  46. package/rsc-ssr.js +637 -0
  47. package/rsc.js +402 -0
  48. package/server-components.js +159 -0
  49. 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
+ }