@uniflowed/router 0.0.0-alpha.9 → 0.2.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 (51) hide show
  1. package/action.js +344 -0
  2. package/client.js +263 -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 +646 -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/form-action.js +243 -0
  23. package/internal/head.js +219 -0
  24. package/internal/hydrate-options.js +38 -0
  25. package/internal/hydration.js +1085 -0
  26. package/internal/inspector.js +626 -0
  27. package/internal/native-links.js +67 -0
  28. package/internal/native-tree.js +89 -0
  29. package/internal/navigation-cache.js +181 -0
  30. package/internal/payload-rows.js +270 -0
  31. package/internal/payload.js +685 -0
  32. package/internal/prepare-document.js +54 -0
  33. package/internal/react-version.js +77 -0
  34. package/internal/resolve.js +1617 -0
  35. package/internal/resolved-summary.js +199 -0
  36. package/internal/routing.js +478 -0
  37. package/internal/runtime.js +1593 -1341
  38. package/internal/server-instrumentation.js +12 -0
  39. package/internal/server-route.js +58 -0
  40. package/internal/shell.js +132 -0
  41. package/internal/stream.js +766 -21
  42. package/middleware.js +274 -22
  43. package/native-navigation.js +217 -0
  44. package/native.js +416 -0
  45. package/package.json +48 -7
  46. package/routing.js +51 -0
  47. package/rsc-client.js +122 -0
  48. package/rsc-ssr.js +641 -0
  49. package/rsc.js +402 -0
  50. package/server-components.js +159 -0
  51. package/server.js +263 -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,132 @@
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 FormState, formStateScript } from "./form-action.js";
19
+ import type { RedirectError } from "./routing.js";
20
+ import { type DocumentShell, bodyOfText } from "./stream.js";
21
+
22
+ /** A redirect, as the finished document `prerender` answers with. */
23
+ export async function redirectResult(document: RenderResult): Promise<PrerenderResult> {
24
+ return {
25
+ status: document.status,
26
+ headers: document.headers,
27
+ html: await document.text(),
28
+ };
29
+ }
30
+
31
+ export function redirectDocument(error: RedirectError): RenderResult {
32
+ // Under `app.router.basePath`, and in the trailing-slash policy's spelling:
33
+ // `redirect("/sign-in")` names an application path, and a browser follows
34
+ // an address.
35
+ const address = addressOf(error.to);
36
+ const target = escapeAttribute(address);
37
+ // A document rather than an empty body, because a redirect is still an answer
38
+ // a browser may be shown; it goes through the same three methods as a
39
+ // rendered one so that a host has one shape to write, not two.
40
+ const body = bodyOfText(
41
+ `<!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`,
42
+ );
43
+ return {
44
+ status: error.permanent ? 308 : 307,
45
+ headers: { Location: address },
46
+ pipe: body.pipe,
47
+ stream: body.stream,
48
+ text: body.text,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * The document uf writes around the app's markup.
54
+ *
55
+ * The same two shapes `assemble` chose between, decided from the same evidence
56
+ * — whether the markup opens with `<html>` — but stated up front instead of
57
+ * afterwards, because a stream has no "afterwards" in which to splice a head.
58
+ * An app whose root layout renders `<html>` owns the whole document and the
59
+ * client hydrates `document`, so uf contributes only the tags that go before
60
+ * `</head>`. An app that renders only content is wrapped in a minimal shell
61
+ * around `<div id="uf-root">`, which is what the client hydrates instead.
62
+ *
63
+ * `internal/stream.js` picks between them on the opening bytes React writes;
64
+ * everything either shape is made of is here, so what a uf document contains is
65
+ * still readable in one place.
66
+ *
67
+ * # Why the shell is three strings and not one
68
+ *
69
+ * Because uf's own `<head>` has to still be open when React's head tags arrive.
70
+ * React hoists a `<title>`, a `<meta>` and a `<link>` into the head it wrote
71
+ * itself, and here it wrote none — so with one string this shell closed its
72
+ * head before the app had rendered a byte, and every `og:` tag and the
73
+ * `<link rel="canonical">` landed in the body, where a crawler ignores them.
74
+ * `open` is uf's head up to that point, `body` is the rest of it and the
75
+ * wrapper, and what goes between them is whatever `assembled` lifts out of the
76
+ * app's own markup. See ubugeeei-prod/uf#547.
77
+ *
78
+ * That is also why no `<title>` is written here any more. It was, from
79
+ * `resolved.metadata.title` — the same string `Head` renders — so a document
80
+ * carried two of them, one in each place, and only one was where a browser
81
+ * looks. Hoisting the rendered one leaves the metadata with a single source.
82
+ */
83
+ export function shellFor(
84
+ assets: RenderAssets,
85
+ nonce?: string | null,
86
+ formState?: FormState,
87
+ ): DocumentShell {
88
+ // A postback's form state goes first, before the client entry that reads it:
89
+ // data rather than a script, so it needs no nonce. See `./form-action.js`.
90
+ const head = (formState == null ? "" : formStateScript(formState)) + headTags(assets, nonce);
91
+ return {
92
+ head,
93
+ open: `<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">`,
94
+ body: `${head}</head><body><div id="${ROOT_ID}">`,
95
+ close: `</div></body></html>\n`,
96
+ };
97
+ }
98
+
99
+ function headTags(assets: RenderAssets, nonce?: string | null): string {
100
+ let tags = "";
101
+ // Which build this document is, first, because everything after it is a URL
102
+ // that build wrote. The browser reads it once and names it on every action
103
+ // call and payload request, so a server on another build can refuse rather
104
+ // than answer with ids and chunks this page does not have. See
105
+ // `./deployment.js`.
106
+ const deployment = assets.deployment;
107
+ if (deployment != null && deployment !== "") {
108
+ tags += `<meta name="${DEPLOYMENT_META}" content="${escapeAttribute(deployment)}">`;
109
+ }
110
+ for (const href of assets.styles) {
111
+ tags += `<link rel="stylesheet" href="${escapeAttribute(href)}">`;
112
+ }
113
+ for (const href of assets.preloads) {
114
+ tags += `<link rel="modulepreload" href="${escapeAttribute(href)}">`;
115
+ }
116
+ // The nonce goes on the client entry even though it is `src` rather than
117
+ // inline, because a policy of `script-src 'nonce-…'` admits *no* script
118
+ // without one — a nonce policy is not an inline policy with an exception in
119
+ // it. A project whose policy also names `'self'` pays nothing for the
120
+ // attribute being here, and one whose policy is `'strict-dynamic'` needs it:
121
+ // that is the directive under which this script is the root of trust every
122
+ // chunk it imports inherits from.
123
+ const carried = nonce == null ? "" : ` nonce="${escapeAttribute(nonce)}"`;
124
+ for (const src of assets.scripts) {
125
+ tags += `<script type="module" src="${escapeAttribute(src)}"${carried}></script>`;
126
+ }
127
+ return tags;
128
+ }
129
+
130
+ function escapeAttribute(value: string): string {
131
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
132
+ }