@uniflowed/router 0.0.0-alpha.4 → 0.0.0-alpha.41
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 +301 -0
- package/client.js +295 -8
- package/handler.js +127 -106
- package/http-client.js +104 -0
- package/index.js +72 -4
- package/internal/action-endpoint.js +435 -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/devtools.js +131 -0
- package/internal/diagnostics.js +169 -0
- package/internal/error-view.js +193 -0
- package/internal/flight-browser.js +238 -0
- package/internal/flight-chunks.js +205 -0
- package/internal/flight-ssr.js +78 -0
- package/internal/flight.js +183 -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 +49 -0
- package/internal/react-version.js +77 -0
- package/internal/request.js +43 -0
- package/internal/resolve.js +1613 -0
- package/internal/resolved-summary.js +199 -0
- package/internal/routing.js +478 -0
- package/internal/runtime.js +1613 -548
- package/internal/server-route.js +58 -0
- package/internal/shell.js +115 -0
- package/internal/stream.js +1099 -0
- package/middleware.js +350 -0
- 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 +446 -0
- package/rsc.js +381 -0
- package/server-components.js +159 -0
- package/server.js +450 -75
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: React Server Components, in the browser.
|
|
4
|
+
//
|
|
5
|
+
// A route's tree reaches the browser as React's Flight payload
|
|
6
|
+
// (ubugeeei-prod/uf#519), by one of two doors. The first page arrives inside
|
|
7
|
+
// its document, as the chunks `./flight-chunks.js` describes, and is read here
|
|
8
|
+
// while the document is still streaming so that `hydrateRoot` can start before
|
|
9
|
+
// the slowest boundary has resolved. Every page after that is a `fetch` of the
|
|
10
|
+
// route's payload URL (`./flight.js`), read the same way by React's own client.
|
|
11
|
+
//
|
|
12
|
+
// # The one hook React's Parcel client asks for
|
|
13
|
+
//
|
|
14
|
+
// `react-server-dom-parcel` resolves a client reference through a global
|
|
15
|
+
// `parcelRequire`: `load(url)` for each chunk a reference names, then
|
|
16
|
+
// `parcelRequire(id)` for the module, synchronously, once they have loaded. uf
|
|
17
|
+
// has no Parcel; this module is that hook. A reference's id *is* its chunk's
|
|
18
|
+
// URL — `@uniflowed/vite` writes it that way — so loading a chunk and
|
|
19
|
+
// remembering its namespace under the same string is the whole of it.
|
|
20
|
+
//
|
|
21
|
+
// It imports same-origin URLs and nothing else. The payload comes from the
|
|
22
|
+
// page's own origin, so a URL that points somewhere else is not a URL uf wrote,
|
|
23
|
+
// and turning it into an `import()` would be letting bytes decide which script
|
|
24
|
+
// runs.
|
|
25
|
+
|
|
26
|
+
import { createFromFetch, createFromReadableStream } from "react-server-dom-parcel/client.browser";
|
|
27
|
+
|
|
28
|
+
import { FLIGHT_CHUNK_ATTRIBUTE, flightChunkBytes } from "./flight-chunks.js";
|
|
29
|
+
import {
|
|
30
|
+
FLIGHT_CONTENT_TYPE,
|
|
31
|
+
INTERCEPTED_FROM_HEADER,
|
|
32
|
+
type FetchedFlight,
|
|
33
|
+
type FlightFetchOptions,
|
|
34
|
+
type FlightRoot,
|
|
35
|
+
documentPathOf,
|
|
36
|
+
flightUrl,
|
|
37
|
+
} from "./flight.js";
|
|
38
|
+
import { requireServerComponentsReact } from "./react-version.js";
|
|
39
|
+
|
|
40
|
+
/** A module namespace, as far as the loader looks into one. */
|
|
41
|
+
type ModuleNamespace = { +[string]: mixed };
|
|
42
|
+
|
|
43
|
+
/** The entry a refusal names: the one an application reaches this module through. */
|
|
44
|
+
const ENTRY = "@uniflowed/router/rsc/client";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Install the module hook React's Flight client resolves references through.
|
|
48
|
+
*
|
|
49
|
+
* Once per page. Defined rather than assigned, for the reason
|
|
50
|
+
* `./boundaries.js` gives about its own global: a name a page already defined
|
|
51
|
+
* as an accessor cannot be assigned to, and hydration must not throw over it.
|
|
52
|
+
*/
|
|
53
|
+
export function installBrowserModules(): void {
|
|
54
|
+
requireServerComponentsReact(ENTRY);
|
|
55
|
+
const loaded: Map<string, ModuleNamespace> = new Map();
|
|
56
|
+
// A function with three properties, which is the shape React's Parcel client
|
|
57
|
+
// calls: `parcelRequire(id)` for a module, `parcelRequire.load(url)` for the
|
|
58
|
+
// chunk it lives in. Declared and then given its properties, so the hook is
|
|
59
|
+
// built rather than merged into something that already existed.
|
|
60
|
+
function parcelRequire(id: string): ModuleNamespace {
|
|
61
|
+
const namespace = loaded.get(id);
|
|
62
|
+
if (namespace == null) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`@uniflowed/router: the client module ${id} was required before it loaded. React's ` +
|
|
65
|
+
"Flight client loads every chunk a reference names before it asks for the module.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return namespace;
|
|
69
|
+
}
|
|
70
|
+
parcelRequire.load = (url: string): Promise<void> => {
|
|
71
|
+
const target = new URL(url, window.location.href);
|
|
72
|
+
if (target.origin !== window.location.origin) {
|
|
73
|
+
return Promise.reject(
|
|
74
|
+
new Error(
|
|
75
|
+
`@uniflowed/router: a payload named the client module ${url}, which is not on this ` +
|
|
76
|
+
"page's origin. uf writes same-origin chunk URLs only, so it is not loaded.",
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return import(target.href).then((namespace: ModuleNamespace) => {
|
|
81
|
+
loaded.set(url, namespace);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
parcelRequire.extendImportMap = (): void => {
|
|
85
|
+
throw new Error("@uniflowed/router: a payload asked for an import map, which uf never writes");
|
|
86
|
+
};
|
|
87
|
+
parcelRequire.meta = { publicUrl: "", devServer: null };
|
|
88
|
+
Object.defineProperty(globalThis, "parcelRequire", {
|
|
89
|
+
value: parcelRequire,
|
|
90
|
+
writable: true,
|
|
91
|
+
configurable: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The parts of a `Document` the reader uses. */
|
|
96
|
+
type DocumentLike = interface {
|
|
97
|
+
readonly querySelectorAll: (selector: string) => Iterable<ElementLike>,
|
|
98
|
+
readonly documentElement: mixed,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** The parts of an `Element` the reader uses. */
|
|
102
|
+
type ElementLike = interface {
|
|
103
|
+
readonly textContent: string | null,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The payload a document carries, as the byte stream React's client reads.
|
|
108
|
+
*
|
|
109
|
+
* Every chunk element already in the document is read at once, in document
|
|
110
|
+
* order, and a `MutationObserver` reads the ones the server has not written
|
|
111
|
+
* yet. The stream ends at the end marker and the observer is disconnected with
|
|
112
|
+
* it, so a prerendered document — every chunk already there — never installs
|
|
113
|
+
* one.
|
|
114
|
+
*
|
|
115
|
+
* `observe` is passed in rather than reached for, so the reader can be driven
|
|
116
|
+
* by a test with a document it mutates by hand; `domObserver` in
|
|
117
|
+
* `./payload-rows.js` is the browser's.
|
|
118
|
+
*/
|
|
119
|
+
export function documentPayload(
|
|
120
|
+
document: DocumentLike,
|
|
121
|
+
observe: ?(callback: () => void) => (() => void) | null,
|
|
122
|
+
): ReadableStream<Uint8Array> {
|
|
123
|
+
const seen: WeakSet<ElementLike> = new WeakSet();
|
|
124
|
+
let stop: (() => void) | null = null;
|
|
125
|
+
let done = false;
|
|
126
|
+
return new ReadableStream({
|
|
127
|
+
start(controller) {
|
|
128
|
+
const sweep = () => {
|
|
129
|
+
if (done) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const element of document.querySelectorAll(`script[${FLIGHT_CHUNK_ATTRIBUTE}]`)) {
|
|
133
|
+
if (seen.has(element)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
seen.add(element);
|
|
137
|
+
let bytes;
|
|
138
|
+
try {
|
|
139
|
+
bytes = flightChunkBytes(element.textContent ?? "null");
|
|
140
|
+
} catch (error) {
|
|
141
|
+
done = true;
|
|
142
|
+
stop?.();
|
|
143
|
+
controller.error(error);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (bytes == null) {
|
|
147
|
+
done = true;
|
|
148
|
+
stop?.();
|
|
149
|
+
controller.close();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
controller.enqueue(bytes);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
sweep();
|
|
156
|
+
if (!done && observe != null) {
|
|
157
|
+
stop = observe(sweep);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
cancel() {
|
|
161
|
+
done = true;
|
|
162
|
+
stop?.();
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Read the payload a document carries into its root value. */
|
|
168
|
+
export function readDocumentPayload(
|
|
169
|
+
document: DocumentLike,
|
|
170
|
+
observe: ?(callback: () => void) => (() => void) | null,
|
|
171
|
+
): Promise<FlightRoot> {
|
|
172
|
+
requireServerComponentsReact(ENTRY);
|
|
173
|
+
return createFromReadableStream(documentPayload(document, observe));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetch `url`'s payload.
|
|
178
|
+
*
|
|
179
|
+
* A redirect is followed by `fetch`, and the route the payload is for is read
|
|
180
|
+
* back off the URL the answer came from — so a loader's `redirect()` during a
|
|
181
|
+
* navigation lands the history entry on the target, as a document request
|
|
182
|
+
* would have. Anything that is not a payload is handed back as a document to
|
|
183
|
+
* load instead of being fed to React's client.
|
|
184
|
+
*
|
|
185
|
+
* The status is not what decides, the content type is. A route that resolved
|
|
186
|
+
* to its not-found or error boundary is answered with a 404 or a 500 *and a
|
|
187
|
+
* payload*, and that payload is the page to show; what is not a payload — a
|
|
188
|
+
* static host's `404.html` for a file it does not have, a middleware's own
|
|
189
|
+
* refusal — is a document, whatever its status.
|
|
190
|
+
*/
|
|
191
|
+
export async function fetchFlight(
|
|
192
|
+
url: string,
|
|
193
|
+
options?: FlightFetchOptions,
|
|
194
|
+
): Promise<FetchedFlight> {
|
|
195
|
+
// Outside the `try` below, which answers every failure to fetch with a
|
|
196
|
+
// document load: too old a React is not a network failure, and a navigation
|
|
197
|
+
// that quietly reloaded the page would hide it.
|
|
198
|
+
requireServerComponentsReact(ENTRY);
|
|
199
|
+
const headers = new Headers({ accept: FLIGHT_CONTENT_TYPE });
|
|
200
|
+
const interceptedFrom = options?.interceptedFrom;
|
|
201
|
+
if (interceptedFrom != null) {
|
|
202
|
+
headers.set(INTERCEPTED_FROM_HEADER, interceptedFrom);
|
|
203
|
+
}
|
|
204
|
+
let response: Response;
|
|
205
|
+
try {
|
|
206
|
+
response = await fetch(flightUrl(url), {
|
|
207
|
+
credentials: "same-origin",
|
|
208
|
+
headers,
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
// A request that could not be made or followed: a dropped connection, or a
|
|
212
|
+
// redirect to another origin — a sign-in page — which `fetch` may not
|
|
213
|
+
// follow without CORS. The browser can still load the document, and a
|
|
214
|
+
// top-level navigation follows any redirect, so that is what the router
|
|
215
|
+
// does rather than leave the click doing nothing.
|
|
216
|
+
return { kind: "document", url };
|
|
217
|
+
}
|
|
218
|
+
const landed = new URL(response.url, window.location.href);
|
|
219
|
+
const document = documentPathOf(landed.pathname);
|
|
220
|
+
const type = response.headers.get("content-type") ?? "";
|
|
221
|
+
if (
|
|
222
|
+
landed.origin !== window.location.origin ||
|
|
223
|
+
document == null ||
|
|
224
|
+
!type.startsWith(FLIGHT_CONTENT_TYPE)
|
|
225
|
+
) {
|
|
226
|
+
void response.body?.cancel();
|
|
227
|
+
// The URL the reader asked for when the redirect did not end on a payload,
|
|
228
|
+
// rather than the one it ended on. A middleware that answered with its
|
|
229
|
+
// sign-in page wrote a `next=` naming the payload URL; loading the document
|
|
230
|
+
// URL lets it see the document and name that instead.
|
|
231
|
+
return { kind: "document", url: document == null ? url : `${document}${landed.search}` };
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
kind: "flight",
|
|
235
|
+
url: `${document}${landed.search}`,
|
|
236
|
+
root: createFromFetch(Promise.resolve(response)),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: a Flight payload, written into a document
|
|
4
|
+
// and read back out of one.
|
|
5
|
+
//
|
|
6
|
+
// A document a server streams carries the payload the browser hydrates from,
|
|
7
|
+
// beside the HTML rendered from it: every chunk React's Flight renderer wrote
|
|
8
|
+
// is copied into a `<script type="application/json" data-uf-flight>` element
|
|
9
|
+
// as the HTML streams, and the browser reads the elements back into the byte
|
|
10
|
+
// stream React's Flight client consumes. This module is the encoding of one
|
|
11
|
+
// chunk in each direction, and nothing else, so the writer (`./stream.js`) and
|
|
12
|
+
// the reader (`./flight-browser.js`) cannot disagree about it.
|
|
13
|
+
//
|
|
14
|
+
// # `application/json`, and not a script that runs
|
|
15
|
+
//
|
|
16
|
+
// The obvious mechanism is an inline script that pushes each chunk into a
|
|
17
|
+
// global. `./runtime.js` and `./payload-rows.js` have already made the case
|
|
18
|
+
// against it for uf's documents: a script that runs is a script a content
|
|
19
|
+
// security policy has to allow, and application data in a script element with
|
|
20
|
+
// no type is application data handed to the JavaScript parser. A JSON script
|
|
21
|
+
// runs nothing, and a `MutationObserver` notices each one as it lands.
|
|
22
|
+
//
|
|
23
|
+
// # Text, and bytes that are not text
|
|
24
|
+
//
|
|
25
|
+
// A payload is bytes. Almost all of it is UTF-8 text — rows of JSON — and it is
|
|
26
|
+
// written as a JSON string, which is what makes it readable in a document. A
|
|
27
|
+
// typed array a server component passed as a prop is a row of raw bytes that
|
|
28
|
+
// need not be valid UTF-8, and a decoder that replaced them would hand React a
|
|
29
|
+
// different array. So a run of bytes that does not decode strictly is written
|
|
30
|
+
// as base64 instead, and the reader turns each element back into exactly the
|
|
31
|
+
// bytes the writer was given.
|
|
32
|
+
//
|
|
33
|
+
// A chunk boundary can fall inside a multi-byte character, which is not an
|
|
34
|
+
// invalid sequence and must not be treated as one: the writer keeps an
|
|
35
|
+
// incomplete tail back and prepends it to the next chunk.
|
|
36
|
+
|
|
37
|
+
/** The attribute every payload chunk element carries. */
|
|
38
|
+
export const FLIGHT_CHUNK_ATTRIBUTE: string = "data-uf-flight";
|
|
39
|
+
|
|
40
|
+
/** What one element holds: text, bytes that are not text, or the end. */
|
|
41
|
+
export type FlightChunk = string | {| readonly bytes: string |} | null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* An element's text: a JSON value with the three escapes an inline element
|
|
45
|
+
* needs.
|
|
46
|
+
*
|
|
47
|
+
* `<` so a payload holding `</script>` cannot close the element early, and
|
|
48
|
+
* U+2028 and U+2029 because JSON is sometimes read as JavaScript. The same
|
|
49
|
+
* escape `./payload.js` applies, for the same reasons.
|
|
50
|
+
*/
|
|
51
|
+
function elementJson(value: FlightChunk): string {
|
|
52
|
+
return JSON.stringify(value)
|
|
53
|
+
.replace(/</g, "\\u003c")
|
|
54
|
+
.replace(/\u2028/g, "\\u2028")
|
|
55
|
+
.replace(/\u2029/g, "\\u2029");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The element for one chunk, or the end marker for `null`.
|
|
60
|
+
*
|
|
61
|
+
* `nonce` is this response's, and it is carried even though the element is
|
|
62
|
+
* `application/json` and therefore a data block the browser never executes.
|
|
63
|
+
* Two reasons, and neither is "in case the spec changes". A uf document's
|
|
64
|
+
* invariant is that every `<script>` in it carries the nonce — that is what
|
|
65
|
+
* `packages/router/streaming.test.js` pins, and an invariant with an exception
|
|
66
|
+
* in it is one a reader has to check every new script against. And these are
|
|
67
|
+
* written as text into a stream rather than rendered by React, so unlike the
|
|
68
|
+
* payload rows in `./runtime.js` there is no second render on the client to
|
|
69
|
+
* disagree with; the attribute costs a comparison nobody makes.
|
|
70
|
+
*/
|
|
71
|
+
export function flightChunkElement(chunk: FlightChunk, nonce?: string | null): string {
|
|
72
|
+
const carried = nonce == null ? "" : ` nonce="${escapeNonce(nonce)}"`;
|
|
73
|
+
return `<script type="application/json"${carried} ${FLIGHT_CHUNK_ATTRIBUTE}>${elementJson(chunk)}</script>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A nonce, as an attribute value.
|
|
78
|
+
*
|
|
79
|
+
* uf's own nonces are base64 and hold none of these, so this is about the one a
|
|
80
|
+
* project supplied: the value reaches a document that a browser parses as
|
|
81
|
+
* HTML, and a `"` in it would end the attribute and open whatever follows.
|
|
82
|
+
*/
|
|
83
|
+
function escapeNonce(value: string): string {
|
|
84
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A writer's state: the bytes of an incomplete character, carried to the next
|
|
89
|
+
* chunk.
|
|
90
|
+
*
|
|
91
|
+
* One per document, made by [`createChunkEncoder`], because the carry is a fact
|
|
92
|
+
* about the stream and two documents must never share one.
|
|
93
|
+
*/
|
|
94
|
+
export type ChunkEncoder = {|
|
|
95
|
+
/** The elements for one chunk of payload bytes; possibly none. */
|
|
96
|
+
readonly encode: (bytes: Uint8Array) => string,
|
|
97
|
+
/** The elements for whatever is carried, and the end marker. */
|
|
98
|
+
readonly end: () => string,
|
|
99
|
+
|};
|
|
100
|
+
|
|
101
|
+
export function createChunkEncoder(nonce?: string | null): ChunkEncoder {
|
|
102
|
+
const strict = new TextDecoder("utf-8", { fatal: true });
|
|
103
|
+
let carry: Uint8Array = new Uint8Array(0);
|
|
104
|
+
|
|
105
|
+
function encode(bytes: Uint8Array): string {
|
|
106
|
+
const joined = concat(carry, bytes);
|
|
107
|
+
const complete = completeLength(joined);
|
|
108
|
+
carry = joined.slice(complete);
|
|
109
|
+
if (complete === 0) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
const body = joined.subarray(0, complete);
|
|
113
|
+
try {
|
|
114
|
+
return flightChunkElement(strict.decode(body), nonce);
|
|
115
|
+
} catch {
|
|
116
|
+
return flightChunkElement({ bytes: base64(body) }, nonce);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function end(): string {
|
|
121
|
+
const rest = carry;
|
|
122
|
+
carry = new Uint8Array(0);
|
|
123
|
+
const tail = rest.length === 0 ? "" : flightChunkElement({ bytes: base64(rest) }, nonce);
|
|
124
|
+
return `${tail}${flightChunkElement(null, nonce)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { encode, end };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The bytes one element holds, or `null` for the end marker.
|
|
132
|
+
*
|
|
133
|
+
* Refuses anything that is not one of the three shapes the writer produces,
|
|
134
|
+
* rather than guessing: a document carrying some other value in an element
|
|
135
|
+
* with this attribute was not written by this module.
|
|
136
|
+
*/
|
|
137
|
+
export function flightChunkBytes(text: string): Uint8Array | null {
|
|
138
|
+
const value: mixed = JSON.parse(text);
|
|
139
|
+
if (value === null) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value === "string") {
|
|
143
|
+
return new TextEncoder().encode(value);
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
typeof value === "object" &&
|
|
147
|
+
!Array.isArray(value) &&
|
|
148
|
+
typeof value.bytes === "string" &&
|
|
149
|
+
Object.keys(value).length === 1
|
|
150
|
+
) {
|
|
151
|
+
return fromBase64(value.bytes);
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`@uniflowed/router: a ${FLIGHT_CHUNK_ATTRIBUTE} element holds no payload chunk`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* How many leading bytes of `bytes` end on a character boundary.
|
|
158
|
+
*
|
|
159
|
+
* Walks back over at most three continuation bytes to the lead byte of the last
|
|
160
|
+
* character and keeps it back when the character is not yet whole. A byte that
|
|
161
|
+
* is not a valid lead at all is left in — the strict decoder is what says so,
|
|
162
|
+
* and holding it back would carry it forever.
|
|
163
|
+
*/
|
|
164
|
+
function completeLength(bytes: Uint8Array): number {
|
|
165
|
+
const length = bytes.length;
|
|
166
|
+
let at = length - 1;
|
|
167
|
+
let continuation = 0;
|
|
168
|
+
while (at >= 0 && continuation < 3 && (bytes[at] & 0xc0) === 0x80) {
|
|
169
|
+
at -= 1;
|
|
170
|
+
continuation += 1;
|
|
171
|
+
}
|
|
172
|
+
if (at < 0) {
|
|
173
|
+
return length;
|
|
174
|
+
}
|
|
175
|
+
const lead = bytes[at];
|
|
176
|
+
const needed = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
|
|
177
|
+
return length - at >= needed ? length : at;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function concat(first: Uint8Array, second: Uint8Array): Uint8Array {
|
|
181
|
+
if (first.length === 0) {
|
|
182
|
+
return second;
|
|
183
|
+
}
|
|
184
|
+
const joined = new Uint8Array(first.length + second.length);
|
|
185
|
+
joined.set(first, 0);
|
|
186
|
+
joined.set(second, first.length);
|
|
187
|
+
return joined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function base64(bytes: Uint8Array): string {
|
|
191
|
+
let binary = "";
|
|
192
|
+
for (const byte of bytes) {
|
|
193
|
+
binary += String.fromCharCode(byte);
|
|
194
|
+
}
|
|
195
|
+
return btoa(binary);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fromBase64(text: string): Uint8Array {
|
|
199
|
+
const binary = atob(text);
|
|
200
|
+
const bytes = new Uint8Array(binary.length);
|
|
201
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
202
|
+
bytes[index] = binary.charCodeAt(index);
|
|
203
|
+
}
|
|
204
|
+
return bytes;
|
|
205
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/router`: React Server Components, while HTML renders.
|
|
4
|
+
//
|
|
5
|
+
// The HTML renderer does not render a route's modules. It reads the payload
|
|
6
|
+
// the Flight renderer wrote (`../rsc.js`) with React's own Flight client and
|
|
7
|
+
// renders what comes out, so the document is the payload's tree and not a
|
|
8
|
+
// second rendering of the same modules — which is what lets a Server Component
|
|
9
|
+
// be a module that neither this graph nor the browser ever evaluates.
|
|
10
|
+
//
|
|
11
|
+
// # Where a client component's server copy comes from
|
|
12
|
+
//
|
|
13
|
+
// A client reference in a payload names a chunk URL, which is the browser's
|
|
14
|
+
// module. HTML still has to be rendered from the component, on the server, so
|
|
15
|
+
// `@uniflowed/vite` builds a server copy of every client module and hands the
|
|
16
|
+
// renderer a table from the browser's URL to that copy. React's Parcel client
|
|
17
|
+
// resolves references through a global `parcelRequire`, exactly as in the
|
|
18
|
+
// browser (`./flight-browser.js`), and this module is that hook on a server.
|
|
19
|
+
//
|
|
20
|
+
// The same global serves one application per process, which is what a uf
|
|
21
|
+
// server is. `meta.publicUrl` is empty because the URLs a reference names are
|
|
22
|
+
// already absolute paths: React asks the document to preload each chunk while
|
|
23
|
+
// it renders, and those tags are right exactly as the payload spells them.
|
|
24
|
+
|
|
25
|
+
import { createFromReadableStream } from "react-server-dom-parcel/client.edge";
|
|
26
|
+
|
|
27
|
+
import type { FlightRoot } from "./flight.js";
|
|
28
|
+
import { requireServerComponentsReact } from "./react-version.js";
|
|
29
|
+
|
|
30
|
+
/** A module namespace, as far as the hook looks into one. */
|
|
31
|
+
type ModuleNamespace = { +[string]: mixed };
|
|
32
|
+
|
|
33
|
+
/** The server copy of the client module at a browser chunk URL. */
|
|
34
|
+
export type ClientModuleLoader = (url: string) => Promise<ModuleNamespace>;
|
|
35
|
+
|
|
36
|
+
/** The entry a refusal names: the one an application reaches this module through. */
|
|
37
|
+
const ENTRY = "@uniflowed/router/rsc/ssr";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Install the module hook React's Flight client resolves references through.
|
|
41
|
+
*
|
|
42
|
+
* Idempotent over the same loader and replaced by a different one, so a dev
|
|
43
|
+
* server that rebuilds its table installs the new table rather than keeping a
|
|
44
|
+
* stale one.
|
|
45
|
+
*/
|
|
46
|
+
export function installServerModules(load: ClientModuleLoader): void {
|
|
47
|
+
requireServerComponentsReact(ENTRY);
|
|
48
|
+
const loaded: Map<string, ModuleNamespace> = new Map();
|
|
49
|
+
// The browser's hook, on a server; `./flight-browser.js` has the shape.
|
|
50
|
+
function parcelRequire(id: string): ModuleNamespace {
|
|
51
|
+
const namespace = loaded.get(id);
|
|
52
|
+
if (namespace == null) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`@uniflowed/router: the client module ${id} was required before its server copy loaded`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return namespace;
|
|
58
|
+
}
|
|
59
|
+
parcelRequire.load = (url: string): Promise<void> =>
|
|
60
|
+
load(url).then((namespace) => {
|
|
61
|
+
loaded.set(url, namespace);
|
|
62
|
+
});
|
|
63
|
+
parcelRequire.extendImportMap = (): void => {
|
|
64
|
+
throw new Error("@uniflowed/router: a payload asked for an import map, which uf never writes");
|
|
65
|
+
};
|
|
66
|
+
parcelRequire.meta = { publicUrl: "", devServer: null };
|
|
67
|
+
Object.defineProperty(globalThis, "parcelRequire", {
|
|
68
|
+
value: parcelRequire,
|
|
69
|
+
writable: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read a payload into its root value, as React's client does in the browser. */
|
|
75
|
+
export function readPayload(stream: ReadableStream<Uint8Array>): Promise<FlightRoot> {
|
|
76
|
+
requireServerComponentsReact(ENTRY);
|
|
77
|
+
return createFromReadableStream(stream);
|
|
78
|
+
}
|