@uniflowed/router 0.0.0-alpha.33 → 0.0.0-alpha.35
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/client.js +65 -0
- package/index.js +7 -0
- package/internal/boundaries.js +48 -25
- package/internal/compose.js +478 -0
- package/internal/error-view.js +189 -0
- package/internal/flight-browser.js +230 -0
- package/internal/flight-chunks.js +181 -0
- package/internal/flight-ssr.js +72 -0
- package/internal/flight.js +132 -0
- package/internal/head.js +219 -0
- package/internal/resolve.js +1615 -0
- package/internal/routing.js +20 -0
- package/internal/runtime.js +664 -2218
- package/internal/server-route.js +52 -0
- package/internal/stream.js +156 -3
- package/native.js +17 -0
- package/package.json +18 -6
- package/rsc.js +323 -0
- package/server-components.js +155 -0
- package/server.js +435 -8
package/client.js
CHANGED
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
import { DATA_ID, ROOT_ID } from "./internal/document.js";
|
|
109
109
|
import { decodePayload } from "./internal/payload.js";
|
|
110
110
|
import { createPayloadReader, domObserver } from "./internal/payload-rows.js";
|
|
111
|
+
import { installBrowserModules, readDocumentPayload } from "./internal/flight-browser.js";
|
|
111
112
|
|
|
112
113
|
/**
|
|
113
114
|
* Hydrate the current document.
|
|
@@ -217,6 +218,70 @@ export async function hydrate(options: {|
|
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Hydrate a document React Server Components rendered.
|
|
223
|
+
*
|
|
224
|
+
* [`hydrate`] resolves the route from its modules and renders it again over the
|
|
225
|
+
* server's markup. This one resolves nothing and imports no route module: the
|
|
226
|
+
* document carries the Flight payload its tree was rendered from, React's own
|
|
227
|
+
* client reads it, and the tree the browser hydrates is the tree the server
|
|
228
|
+
* rendered — a Server Component is markup and a reference, and a client
|
|
229
|
+
* component is the one kind of module this page loads. See
|
|
230
|
+
* ubugeeei-prod/uf#519.
|
|
231
|
+
*
|
|
232
|
+
* The payload is read while the document is still arriving. Row 0 is in the
|
|
233
|
+
* shell, so hydration starts as soon as the module script runs, and every row
|
|
234
|
+
* after it lands in a later chunk that the reader picks up as it is parsed — so
|
|
235
|
+
* a boundary the server completes after hydration began resolves then, with no
|
|
236
|
+
* second request.
|
|
237
|
+
*
|
|
238
|
+
* Everything else is [`hydrate`]'s, for the reasons written there: the
|
|
239
|
+
* navigation mode is installed before the first render, the development
|
|
240
|
+
* hydration report captures the server's markup before React repairs it, and
|
|
241
|
+
* Strict Mode wraps the root.
|
|
242
|
+
*/
|
|
243
|
+
export async function hydrateFlight(options: {|
|
|
244
|
+
readonly App: React.ComponentType<AppProps>,
|
|
245
|
+
readonly strictMode?: boolean,
|
|
246
|
+
readonly navigation?: Navigation,
|
|
247
|
+
|}): Promise<void> {
|
|
248
|
+
installNavigation(options.navigation ?? "client");
|
|
249
|
+
installBrowserModules();
|
|
250
|
+
const flight = readDocumentPayload(document, domObserver(document));
|
|
251
|
+
|
|
252
|
+
const url = window.location.pathname + window.location.search;
|
|
253
|
+
const { App } = options;
|
|
254
|
+
const container = document.getElementById(ROOT_ID) ?? document;
|
|
255
|
+
prepareDocumentForHydration(document);
|
|
256
|
+
|
|
257
|
+
let recovery = null;
|
|
258
|
+
let restoreDevHead = null;
|
|
259
|
+
if (import.meta.hot != null) {
|
|
260
|
+
const { captureServerMarkup, hydrationErrorHandler, prepareDevHeadForHydration } =
|
|
261
|
+
await import("./internal/hydration.js");
|
|
262
|
+
restoreDevHead = prepareDevHeadForHydration(document);
|
|
263
|
+
recovery = hydrationErrorHandler(container, captureServerMarkup(container), document);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const tree = <App url={url} flight={flight} />;
|
|
267
|
+
|
|
268
|
+
startTransition(() => {
|
|
269
|
+
hydrateRoot(
|
|
270
|
+
container,
|
|
271
|
+
options.strictMode === true ? <StrictMode>{tree}</StrictMode> : tree,
|
|
272
|
+
recovery == null ? undefined : { onRecoverableError: recovery },
|
|
273
|
+
);
|
|
274
|
+
if (restoreDevHead != null) {
|
|
275
|
+
setTimeout(restoreDevHead, 250);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (import.meta.hot != null) {
|
|
280
|
+
const { reportDevtools } = await import("./internal/devtools.js");
|
|
281
|
+
reportDevtools(window);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
220
285
|
function prepareDocumentForHydration(document: Document): void {
|
|
221
286
|
const head = document.head;
|
|
222
287
|
const envelope = head.querySelector('meta[name="uf:render"]');
|
package/index.js
CHANGED
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
// URL the page is, so one URL renders two subtrees at once, and
|
|
23
23
|
// `$default.js` is what a slot renders when the URL matched none of its
|
|
24
24
|
// routes. See ubugeeei-prod/uf#267.
|
|
25
|
+
//
|
|
26
|
+
// A directory named `(.)photo` inside a slot is an intercepting route: a client
|
|
27
|
+
// navigation that starts on a page the slot is on, and reaches the URL the
|
|
28
|
+
// directory stands in for, renders it in the slot and leaves the page
|
|
29
|
+
// underneath where it was. A document request for that URL — a reload, a
|
|
30
|
+
// shared link, a prerender — renders the ordinary page.
|
|
25
31
|
|
|
26
32
|
import * as React from "react";
|
|
27
33
|
|
|
@@ -31,6 +37,7 @@ export type {
|
|
|
31
37
|
AppProps,
|
|
32
38
|
ErrorBoundary,
|
|
33
39
|
ErrorModule,
|
|
40
|
+
Interception,
|
|
34
41
|
JsonLd,
|
|
35
42
|
LayoutModule,
|
|
36
43
|
LinkPrefetch,
|
package/internal/boundaries.js
CHANGED
|
@@ -83,8 +83,10 @@
|
|
|
83
83
|
// package is `sideEffects: false`, so with the references folded away the
|
|
84
84
|
// module is dropped rather than merely unused.
|
|
85
85
|
|
|
86
|
+
"use client";
|
|
87
|
+
|
|
86
88
|
import * as React from "react";
|
|
87
|
-
import { useEffect,
|
|
89
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
88
90
|
|
|
89
91
|
import { SYNTHESISED_SOURCE } from "./boundary-data.js";
|
|
90
92
|
import { reportDiagnostic } from "./diagnostics.js";
|
|
@@ -114,13 +116,32 @@ export const BOUNDARY_GLOBAL: string = "__ufBoundaries";
|
|
|
114
116
|
/**
|
|
115
117
|
* Whether an edge that mounts now should be in the DOM immediately.
|
|
116
118
|
*
|
|
117
|
-
* Latched by the first edge to mount and never cleared
|
|
118
|
-
* `
|
|
119
|
-
* difference between "
|
|
119
|
+
* Latched by the first edge to mount and never cleared, and read through
|
|
120
|
+
* `useSyncExternalStore` rather than during the render body, which is the
|
|
121
|
+
* difference between "a value React asked for" and "a module variable a
|
|
120
122
|
* memoising compiler is entitled to hold on to".
|
|
121
123
|
*/
|
|
122
124
|
let marksAreLive = false;
|
|
123
125
|
|
|
126
|
+
/** The edges waiting to hear that marks have gone live. */
|
|
127
|
+
const liveListeners: Set<() => void> = new Set();
|
|
128
|
+
|
|
129
|
+
function subscribeToLiveMarks(listener: () => void): () => void {
|
|
130
|
+
liveListeners.add(listener);
|
|
131
|
+
return () => {
|
|
132
|
+
liveListeners.delete(listener);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function marksAreLiveNow(): boolean {
|
|
137
|
+
return marksAreLive;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** What a server rendered, and so what every hydrating edge renders: nothing. */
|
|
141
|
+
function noMarksOnTheServer(): boolean {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
/**
|
|
125
146
|
* One end of one boundary.
|
|
126
147
|
*
|
|
@@ -128,12 +149,25 @@ let marksAreLive = false;
|
|
|
128
149
|
* This used to be a `<template>`, but React 19.3 reports template insertion
|
|
129
150
|
* during document-root hydration as a browser error. A `span hidden` carries
|
|
130
151
|
* the same marker data without entering layout or the accessibility tree.
|
|
152
|
+
*
|
|
153
|
+
* # Why the server snapshot, and not a first state
|
|
154
|
+
*
|
|
155
|
+
* An edge used to take `marksAreLive` as its first state, which is right only
|
|
156
|
+
* if every edge on a page hydrates in the same pass. Under React Server
|
|
157
|
+
* Components they do not: a client reference loads when the payload names it,
|
|
158
|
+
* so the part of the tree above it hydrates, commits and runs this effect
|
|
159
|
+
* first, and an edge that hydrates afterwards read `true` and rendered a mark
|
|
160
|
+
* the server never wrote — a hydration mismatch on every page with a boundary
|
|
161
|
+
* below a client component, under `uf dev` only. `useSyncExternalStore` hands a
|
|
162
|
+
* hydrating edge the server's answer whenever it hydrates, and an edge mounted
|
|
163
|
+
* by a navigation or by HMR the live one, in its own commit, as before.
|
|
131
164
|
*/
|
|
132
|
-
component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
|
|
133
|
-
const
|
|
165
|
+
export component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
|
|
166
|
+
const live = useSyncExternalStore(subscribeToLiveMarks, marksAreLiveNow, noMarksOnTheServer);
|
|
134
167
|
useEffect(() => {
|
|
168
|
+
if (marksAreLive) return;
|
|
135
169
|
marksAreLive = true;
|
|
136
|
-
|
|
170
|
+
for (const listener of [...liveListeners]) listener();
|
|
137
171
|
}, []);
|
|
138
172
|
if (!live) {
|
|
139
173
|
return null;
|
|
@@ -154,25 +188,14 @@ component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
|
|
|
154
188
|
/**
|
|
155
189
|
* `children`, between the two marks of `boundary`.
|
|
156
190
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
191
|
+
* Kept importable from here, where the marks are, and written in
|
|
192
|
+
* `./compose.js`, where they are placed. This module is a client module — an
|
|
193
|
+
* edge has state and an effect — and a server composing a tree for React Server
|
|
194
|
+
* Components calls the factory rather than rendering it, so the factory has to
|
|
195
|
+
* live in a module that graph evaluates while the edges it places stay
|
|
196
|
+
* references to this one. See ubugeeei-prod/uf#519.
|
|
163
197
|
*/
|
|
164
|
-
export
|
|
165
|
-
if (boundary == null) {
|
|
166
|
-
return children;
|
|
167
|
-
}
|
|
168
|
-
return (
|
|
169
|
-
<>
|
|
170
|
-
<BoundaryEdge boundary={boundary} edge="open" />
|
|
171
|
-
{children}
|
|
172
|
-
<BoundaryEdge boundary={boundary} edge="close" />
|
|
173
|
-
</>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
198
|
+
export { insideBoundary } from "./compose.js";
|
|
176
199
|
|
|
177
200
|
/**
|
|
178
201
|
* How far a walk between two marks will go before giving up.
|