@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.
- 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 +1597 -1329
- 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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { traceRequestPhase } from "@uniflowed/server/instrumentation";
|
|
4
|
+
import { NotFoundError, RedirectError } from "./routing.js";
|
|
5
|
+
|
|
6
|
+
export function traceLoader(body: () => mixed | Promise<mixed>): Promise<mixed> {
|
|
7
|
+
return traceRequestPhase(
|
|
8
|
+
"loader",
|
|
9
|
+
async () => body(),
|
|
10
|
+
(error) => error instanceof RedirectError || error instanceof NotFoundError,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: the route a server component is rendering in.
|
|
4
|
+
//
|
|
5
|
+
// `useRoute()` in the browser reads the router's context, and a server
|
|
6
|
+
// component has no context to read. The graph it renders in resolves `react`
|
|
7
|
+
// under the `react-server` condition, which is the build with no `useContext`
|
|
8
|
+
// in it (ubugeeei-prod/uf#519), and a layout that highlights the section it is
|
|
9
|
+
// in — this repository's own documentation site has two — would otherwise have
|
|
10
|
+
// to become a client component to find out where it is.
|
|
11
|
+
//
|
|
12
|
+
// So the Flight renderer runs each render inside a store holding the route it
|
|
13
|
+
// resolved, and the server half of the hooks reads it back. `AsyncLocalStorage`
|
|
14
|
+
// and not React's `cache`: `cache` finds its render through React's own request
|
|
15
|
+
// storage, which the build React ships for Deno does not have, so after the
|
|
16
|
+
// first `await` in an async server component it would stop finding the render
|
|
17
|
+
// at all. A store of this module's own follows every continuation of the
|
|
18
|
+
// render on every host `@uniflowed/server` already runs on, and it is one value
|
|
19
|
+
// per render, so two requests in flight cannot read each other's route.
|
|
20
|
+
//
|
|
21
|
+
// Server-only by construction: nothing in the browser's graph imports this.
|
|
22
|
+
|
|
23
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
24
|
+
|
|
25
|
+
import type { RouteState } from "./flight.js";
|
|
26
|
+
import { requireServerComponentsReact } from "./react-version.js";
|
|
27
|
+
|
|
28
|
+
const storage: AsyncLocalStorage<RouteState> = new AsyncLocalStorage();
|
|
29
|
+
|
|
30
|
+
/** Run `body` as a render of `route`. Everything it starts sees the route. */
|
|
31
|
+
export function withServerRoute<T>(route: RouteState, body: () => T): T {
|
|
32
|
+
return storage.run(route, body);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The route being rendered, or a refusal naming the caller.
|
|
37
|
+
*
|
|
38
|
+
* A refusal rather than `null`, because the only way to arrive here without a
|
|
39
|
+
* route is to call a router hook from a server module that is not being
|
|
40
|
+
* rendered by the router — a script, a route handler, a module evaluated at
|
|
41
|
+
* import time — and each of those is a mistake worth a sentence.
|
|
42
|
+
*
|
|
43
|
+
* The React version is checked first. On a React older than 19.3 the Flight
|
|
44
|
+
* renderer that would have put the hook inside a route refuses to start, so the
|
|
45
|
+
* sentence worth reading is that one, not "outside a route".
|
|
46
|
+
*/
|
|
47
|
+
export function serverRoute(caller: string): RouteState {
|
|
48
|
+
requireServerComponentsReact(`${caller}()`);
|
|
49
|
+
const route = storage.getStore();
|
|
50
|
+
if (route == null) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`@uniflowed/router: ${caller}() was called in a server module outside a route the router ` +
|
|
53
|
+
"is rendering. Server Components read the route they render in; a route handler reads " +
|
|
54
|
+
"the request it was given, and a module evaluated at import time has no route at all.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return route;
|
|
58
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: the documents an HTML renderer answers with
|
|
4
|
+
// that are not a route's own markup — uf's shell around that markup, and a
|
|
5
|
+
// redirect.
|
|
6
|
+
//
|
|
7
|
+
// `../server.js` renders a route from its modules, and `../rsc-ssr.js` renders
|
|
8
|
+
// one from the payload React Server Components wrote. They are two entries so
|
|
9
|
+
// that the second, which loads React's Flight client, stays out of every bundle
|
|
10
|
+
// that renders no Server Component (ubugeeei-prod/uf#992). Both write the same
|
|
11
|
+
// documents around what they render, which is why those documents live here and
|
|
12
|
+
// not in either entry.
|
|
13
|
+
|
|
14
|
+
import type { PrerenderResult, RenderAssets, RenderResult } from "../server.js";
|
|
15
|
+
import { addressOf } from "./base-path.js";
|
|
16
|
+
import { DEPLOYMENT_META } from "./deployment.js";
|
|
17
|
+
import { ROOT_ID } from "./document.js";
|
|
18
|
+
import type { RedirectError } from "./routing.js";
|
|
19
|
+
import { type DocumentShell, bodyOfText } from "./stream.js";
|
|
20
|
+
|
|
21
|
+
/** A redirect, as the finished document `prerender` answers with. */
|
|
22
|
+
export async function redirectResult(document: RenderResult): Promise<PrerenderResult> {
|
|
23
|
+
return {
|
|
24
|
+
status: document.status,
|
|
25
|
+
headers: document.headers,
|
|
26
|
+
html: await document.text(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function redirectDocument(error: RedirectError): RenderResult {
|
|
31
|
+
// Under `app.router.basePath`, and in the trailing-slash policy's spelling:
|
|
32
|
+
// `redirect("/sign-in")` names an application path, and a browser follows
|
|
33
|
+
// an address.
|
|
34
|
+
const address = addressOf(error.to);
|
|
35
|
+
const target = escapeAttribute(address);
|
|
36
|
+
// A document rather than an empty body, because a redirect is still an answer
|
|
37
|
+
// a browser may be shown; it goes through the same three methods as a
|
|
38
|
+
// rendered one so that a host has one shape to write, not two.
|
|
39
|
+
const body = bodyOfText(
|
|
40
|
+
`<!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`,
|
|
41
|
+
);
|
|
42
|
+
return {
|
|
43
|
+
status: error.permanent ? 308 : 307,
|
|
44
|
+
headers: { Location: address },
|
|
45
|
+
pipe: body.pipe,
|
|
46
|
+
stream: body.stream,
|
|
47
|
+
text: body.text,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The document uf writes around the app's markup.
|
|
53
|
+
*
|
|
54
|
+
* The same two shapes `assemble` chose between, decided from the same evidence
|
|
55
|
+
* — whether the markup opens with `<html>` — but stated up front instead of
|
|
56
|
+
* afterwards, because a stream has no "afterwards" in which to splice a head.
|
|
57
|
+
* An app whose root layout renders `<html>` owns the whole document and the
|
|
58
|
+
* client hydrates `document`, so uf contributes only the tags that go before
|
|
59
|
+
* `</head>`. An app that renders only content is wrapped in a minimal shell
|
|
60
|
+
* around `<div id="uf-root">`, which is what the client hydrates instead.
|
|
61
|
+
*
|
|
62
|
+
* `internal/stream.js` picks between them on the opening bytes React writes;
|
|
63
|
+
* everything either shape is made of is here, so what a uf document contains is
|
|
64
|
+
* still readable in one place.
|
|
65
|
+
*
|
|
66
|
+
* # Why the shell is three strings and not one
|
|
67
|
+
*
|
|
68
|
+
* Because uf's own `<head>` has to still be open when React's head tags arrive.
|
|
69
|
+
* React hoists a `<title>`, a `<meta>` and a `<link>` into the head it wrote
|
|
70
|
+
* itself, and here it wrote none — so with one string this shell closed its
|
|
71
|
+
* head before the app had rendered a byte, and every `og:` tag and the
|
|
72
|
+
* `<link rel="canonical">` landed in the body, where a crawler ignores them.
|
|
73
|
+
* `open` is uf's head up to that point, `body` is the rest of it and the
|
|
74
|
+
* wrapper, and what goes between them is whatever `assembled` lifts out of the
|
|
75
|
+
* app's own markup. See ubugeeei-prod/uf#547.
|
|
76
|
+
*
|
|
77
|
+
* That is also why no `<title>` is written here any more. It was, from
|
|
78
|
+
* `resolved.metadata.title` — the same string `Head` renders — so a document
|
|
79
|
+
* carried two of them, one in each place, and only one was where a browser
|
|
80
|
+
* looks. Hoisting the rendered one leaves the metadata with a single source.
|
|
81
|
+
*/
|
|
82
|
+
export function shellFor(assets: RenderAssets, nonce?: string | null): DocumentShell {
|
|
83
|
+
const head = headTags(assets, nonce);
|
|
84
|
+
return {
|
|
85
|
+
head,
|
|
86
|
+
open: `<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">`,
|
|
87
|
+
body: `${head}</head><body><div id="${ROOT_ID}">`,
|
|
88
|
+
close: `</div></body></html>\n`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function headTags(assets: RenderAssets, nonce?: string | null): string {
|
|
93
|
+
let tags = "";
|
|
94
|
+
// Which build this document is, first, because everything after it is a URL
|
|
95
|
+
// that build wrote. The browser reads it once and names it on every action
|
|
96
|
+
// call and payload request, so a server on another build can refuse rather
|
|
97
|
+
// than answer with ids and chunks this page does not have. See
|
|
98
|
+
// `./deployment.js`.
|
|
99
|
+
const deployment = assets.deployment;
|
|
100
|
+
if (deployment != null && deployment !== "") {
|
|
101
|
+
tags += `<meta name="${DEPLOYMENT_META}" content="${escapeAttribute(deployment)}">`;
|
|
102
|
+
}
|
|
103
|
+
for (const href of assets.styles) {
|
|
104
|
+
tags += `<link rel="stylesheet" href="${escapeAttribute(href)}">`;
|
|
105
|
+
}
|
|
106
|
+
for (const href of assets.preloads) {
|
|
107
|
+
tags += `<link rel="modulepreload" href="${escapeAttribute(href)}">`;
|
|
108
|
+
}
|
|
109
|
+
// The nonce goes on the client entry even though it is `src` rather than
|
|
110
|
+
// inline, because a policy of `script-src 'nonce-…'` admits *no* script
|
|
111
|
+
// without one — a nonce policy is not an inline policy with an exception in
|
|
112
|
+
// it. A project whose policy also names `'self'` pays nothing for the
|
|
113
|
+
// attribute being here, and one whose policy is `'strict-dynamic'` needs it:
|
|
114
|
+
// that is the directive under which this script is the root of trust every
|
|
115
|
+
// chunk it imports inherits from.
|
|
116
|
+
const carried = nonce == null ? "" : ` nonce="${escapeAttribute(nonce)}"`;
|
|
117
|
+
for (const src of assets.scripts) {
|
|
118
|
+
tags += `<script type="module" src="${escapeAttribute(src)}"${carried}></script>`;
|
|
119
|
+
}
|
|
120
|
+
return tags;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function escapeAttribute(value: string): string {
|
|
124
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
125
|
+
}
|