@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/native.js ADDED
@@ -0,0 +1,416 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/router/native`: the React Native navigator contract.
4
+ //
5
+ // Native routes use the same generated table as the web router, but not the
6
+ // browser's URL bar or History API. This adapter keeps the shared part small:
7
+ // it resolves a uf route path against the table, refuses destinations the
8
+ // native bundle cannot render, and hands a navigator-shaped event to the app's
9
+ // own navigation runtime. Rendering the tree is still the renderer/host-config
10
+ // half of the React Native target.
11
+ //
12
+ // # No interception here, deliberately
13
+ //
14
+ // An intercepting route — a slot's `(.)photo` — is not one of this adapter's
15
+ // features, and that is a decision rather than a gap. What interception does
16
+ // is a browser's: the address bar says `/feed/photo/1` while the page
17
+ // underneath stays on screen with the photo in a slot over it, and a reload
18
+ // renders the page the URL names instead. A native navigator has no address bar
19
+ // for the screen to disagree with, and it already has the thing interception
20
+ // imitates — a screen presented modally over the one below it, with its own
21
+ // back gesture. Doing both would give a native app two answers to "open this
22
+ // over the feed", one of them the router's and one the navigator's.
23
+ //
24
+ // So `resolveNativeNavigation` matches `table.routes` and nothing else:
25
+ // `/feed/photo/1` resolves to the ordinary `app/feed/photo/[id]` route every
26
+ // time, a slot's `intercepts` are never read, and presenting that screen as a
27
+ // modal is the navigator's call, made where the rest of its presentation is.
28
+
29
+ import type { RouteParams, RouteRecord, RouteTable } from "./internal/routing.js";
30
+ import { hasClientPage, matchRoute, splitUrl } from "./internal/routing.js";
31
+ import { nativeLinkHref } from "./internal/native-links.js";
32
+ export { createNativeLinking } from "./internal/native-links.js";
33
+ export type { NativeLinkSource } from "./internal/native-links.js";
34
+
35
+ export type NativeNavigationKind = "push" | "replace" | "prefetch";
36
+
37
+ export type NativeNavigationEvent = {|
38
+ readonly kind: NativeNavigationKind,
39
+ readonly href: string,
40
+ readonly pathname: string,
41
+ readonly search: string,
42
+ readonly route: string,
43
+ readonly params: RouteParams,
44
+ |};
45
+
46
+ export type NativeScreenMap = {
47
+ readonly [route: string]: string,
48
+ };
49
+
50
+ export type NativeScreenEntry = {|
51
+ readonly screen: string,
52
+ readonly route: string,
53
+ readonly file: string,
54
+ |};
55
+
56
+ export type NativeScreenManifest = {|
57
+ readonly screens: NativeScreenMap,
58
+ readonly entries: $ReadOnlyArray<NativeScreenEntry>,
59
+ |};
60
+
61
+ export type NativeScreenNameRoute = {|
62
+ readonly path: string,
63
+ readonly file: string,
64
+ |};
65
+
66
+ export type NativeScreenManifestOptions = {|
67
+ readonly name?: (route: NativeScreenNameRoute) => string,
68
+ |};
69
+
70
+ export type NativeScreenPayload = {|
71
+ readonly screen: string,
72
+ readonly href: string,
73
+ readonly pathname: string,
74
+ readonly search: string,
75
+ readonly route: string,
76
+ readonly params: RouteParams,
77
+ |};
78
+
79
+ export type NativeScreenNavigationState = {|
80
+ readonly params: RouteParams,
81
+ readonly href: string,
82
+ readonly pathname: string,
83
+ readonly search: string,
84
+ readonly route: string,
85
+ |};
86
+
87
+ export type NativeNavigator = {|
88
+ readonly push?: (event: NativeNavigationEvent) => mixed | Promise<mixed>,
89
+ readonly replace?: (event: NativeNavigationEvent) => mixed | Promise<mixed>,
90
+ readonly prefetch?: (event: NativeNavigationEvent) => mixed | Promise<mixed>,
91
+ |};
92
+
93
+ export type NativeScreenNavigator = {|
94
+ readonly push?: (screen: string, state: NativeScreenNavigationState) => mixed | Promise<mixed>,
95
+ readonly replace?: (screen: string, state: NativeScreenNavigationState) => mixed | Promise<mixed>,
96
+ readonly prefetch?: (
97
+ screen: string,
98
+ state: NativeScreenNavigationState,
99
+ ) => mixed | Promise<mixed>,
100
+ |};
101
+
102
+ export type NativeRouter = {|
103
+ readonly push: (to: string) => Promise<void>,
104
+ readonly replace: (to: string) => Promise<void>,
105
+ readonly prefetch: (to: string) => Promise<void>,
106
+ readonly resolve: (to: string, kind?: NativeNavigationKind) => NativeNavigationEvent,
107
+ |};
108
+
109
+ export type NativeNavigationErrorCode =
110
+ | "duplicate-screen"
111
+ | "external-url"
112
+ | "fragment"
113
+ | "relative-url"
114
+ | "missing-route"
115
+ | "missing-screen"
116
+ | "server-only-route"
117
+ | "missing-navigator-method";
118
+
119
+ export class NativeNavigationError extends Error {
120
+ code: NativeNavigationErrorCode;
121
+ href: string;
122
+ route: ?string;
123
+
124
+ constructor(code: NativeNavigationErrorCode, message: string, href: string, route?: ?string) {
125
+ super(message);
126
+ this.name = "NativeNavigationError";
127
+ this.code = code;
128
+ this.href = href;
129
+ this.route = route ?? null;
130
+ }
131
+ }
132
+
133
+ export function createNativeRouter(
134
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
135
+ navigator: NativeNavigator,
136
+ ): NativeRouter {
137
+ const resolve = (to: string, kind?: NativeNavigationKind = "push"): NativeNavigationEvent =>
138
+ resolveNativeNavigation(table, to, kind);
139
+
140
+ return {
141
+ resolve,
142
+ push: async (to) => {
143
+ const event = resolve(to, "push");
144
+ await invokeNavigator(navigator, event);
145
+ },
146
+ replace: async (to) => {
147
+ const event = resolve(to, "replace");
148
+ await invokeNavigator(navigator, event);
149
+ },
150
+ prefetch: async (to) => {
151
+ const event = resolve(to, "prefetch");
152
+ await loadNativeRoute(table, event.pathname);
153
+ if (typeof navigator.prefetch === "function") {
154
+ await navigator.prefetch(event);
155
+ }
156
+ },
157
+ };
158
+ }
159
+
160
+ export function createNativeScreenRouter(
161
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
162
+ screens: NativeScreenMap,
163
+ navigator: NativeScreenNavigator,
164
+ ): NativeRouter {
165
+ return createNativeRouter(table, {
166
+ push: (event) => invokeScreenNavigator(navigator, screens, event),
167
+ replace: (event) => invokeScreenNavigator(navigator, screens, event),
168
+ prefetch: (event) => {
169
+ if (typeof navigator.prefetch !== "function") return;
170
+ return invokeScreenNavigator(navigator, screens, event);
171
+ },
172
+ });
173
+ }
174
+
175
+ export function createNativeScreenManifest(
176
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
177
+ options?: NativeScreenManifestOptions,
178
+ ): NativeScreenManifest {
179
+ const screens: { [string]: string } = {};
180
+ const entries: Array<NativeScreenEntry> = [];
181
+ const seen = new Map<string, string>();
182
+ for (const route of table.routes) {
183
+ if (!hasClientPage(route)) {
184
+ continue;
185
+ }
186
+ const screen = screenNameFor(route, options);
187
+ const already = seen.get(screen);
188
+ if (already != null) {
189
+ throw new NativeNavigationError(
190
+ "duplicate-screen",
191
+ `@uniflowed/router/native: ${route.path} and ${already} both map to native screen ${screen}`,
192
+ route.path,
193
+ route.path,
194
+ );
195
+ }
196
+ seen.set(screen, route.path);
197
+ screens[route.path] = screen;
198
+ entries.push({ screen, route: route.path, file: route.file });
199
+ }
200
+ return { screens, entries };
201
+ }
202
+
203
+ export function nativeScreenName(routePath: string): string {
204
+ const segments = routePath.split("/").filter((segment) => segment !== "");
205
+ if (segments.length === 0) {
206
+ return "Home";
207
+ }
208
+ const name = segments
209
+ .map((segment) => {
210
+ if (segment.startsWith(":") && segment.endsWith("*")) {
211
+ return `All${titlePart(segment.slice(1, -1))}`;
212
+ }
213
+ if (segment.startsWith(":")) {
214
+ return `By${titlePart(segment.slice(1))}`;
215
+ }
216
+ return titlePart(segment);
217
+ })
218
+ .join("");
219
+ return name === "" ? "Screen" : name;
220
+ }
221
+
222
+ export function resolveNativeNavigation(
223
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
224
+ to: string,
225
+ kind?: NativeNavigationKind = "push",
226
+ ): NativeNavigationEvent {
227
+ const href = normalizeNativeHref(to, table);
228
+ const { pathname, search } = splitUrl(href);
229
+ const matched = matchRoute(table.routes, pathname);
230
+ if (matched == null) {
231
+ throw new NativeNavigationError(
232
+ "missing-route",
233
+ `@uniflowed/router/native: ${href} does not match a native route in this table`,
234
+ href,
235
+ );
236
+ }
237
+ if (!hasClientPage(matched.route)) {
238
+ throw new NativeNavigationError(
239
+ "server-only-route",
240
+ `@uniflowed/router/native: ${matched.route.path} is not in the native route table; ` +
241
+ "it has no page module for this bundle",
242
+ href,
243
+ matched.route.path,
244
+ );
245
+ }
246
+ return {
247
+ kind,
248
+ href,
249
+ pathname,
250
+ search,
251
+ route: matched.route.path,
252
+ params: matched.params,
253
+ };
254
+ }
255
+
256
+ export function nativeScreenPayload(
257
+ event: NativeNavigationEvent,
258
+ screens: NativeScreenMap,
259
+ ): NativeScreenPayload {
260
+ const screen = screens[event.route];
261
+ if (typeof screen !== "string" || screen === "") {
262
+ throw new NativeNavigationError(
263
+ "missing-screen",
264
+ `@uniflowed/router/native: ${event.route} has no native screen mapping`,
265
+ event.href,
266
+ event.route,
267
+ );
268
+ }
269
+ return {
270
+ screen,
271
+ href: event.href,
272
+ pathname: event.pathname,
273
+ search: event.search,
274
+ route: event.route,
275
+ params: event.params,
276
+ };
277
+ }
278
+
279
+ export function nativeScreenNavigationState(
280
+ payload: NativeScreenPayload,
281
+ ): NativeScreenNavigationState {
282
+ return {
283
+ params: payload.params,
284
+ href: payload.href,
285
+ pathname: payload.pathname,
286
+ search: payload.search,
287
+ route: payload.route,
288
+ };
289
+ }
290
+
291
+ function normalizeNativeHref(
292
+ to: string,
293
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
294
+ ): string {
295
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(to) || to.startsWith("//")) {
296
+ const claimed = nativeLinkHref(table, to);
297
+ if (claimed != null) return claimed;
298
+ throw new NativeNavigationError(
299
+ "external-url",
300
+ `@uniflowed/router/native: ${to} is an external URL, and a native navigator needs an app route`,
301
+ to,
302
+ );
303
+ }
304
+ if (!to.startsWith("/")) {
305
+ throw new NativeNavigationError(
306
+ "relative-url",
307
+ `@uniflowed/router/native: ${to} is relative, and native navigation has no document URL to resolve it against`,
308
+ to,
309
+ );
310
+ }
311
+ if (to.includes("#")) {
312
+ throw new NativeNavigationError(
313
+ "fragment",
314
+ `@uniflowed/router/native: ${to} contains a fragment, and native routes do not have document anchors`,
315
+ to,
316
+ );
317
+ }
318
+ return splitUrl(to).pathname + splitUrl(to).search;
319
+ }
320
+
321
+ function screenNameFor(
322
+ route: RouteRecord<mixed, mixed, mixed, mixed, mixed>,
323
+ options?: NativeScreenManifestOptions,
324
+ ): string {
325
+ const screen =
326
+ options?.name?.({ path: route.path, file: route.file }) ?? nativeScreenName(route.path);
327
+ if (screen === "") {
328
+ throw new NativeNavigationError(
329
+ "missing-screen",
330
+ `@uniflowed/router/native: ${route.path} mapped to an empty native screen name`,
331
+ route.path,
332
+ route.path,
333
+ );
334
+ }
335
+ return screen;
336
+ }
337
+
338
+ function titlePart(segment: string): string {
339
+ const cleaned = segment
340
+ .replace(/^\[+|\]+$/g, "")
341
+ .replace(/[^A-Za-z0-9]+/g, " ")
342
+ .trim();
343
+ if (cleaned === "") {
344
+ return "Segment";
345
+ }
346
+ return cleaned
347
+ .split(/\s+/)
348
+ .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
349
+ .join("");
350
+ }
351
+
352
+ async function invokeNavigator(
353
+ navigator: NativeNavigator,
354
+ event: NativeNavigationEvent,
355
+ ): Promise<void> {
356
+ if (event.kind === "push") {
357
+ if (typeof navigator.push !== "function") {
358
+ throw missingMethod(event);
359
+ }
360
+ await navigator.push(event);
361
+ return;
362
+ }
363
+ if (event.kind === "replace") {
364
+ if (typeof navigator.replace !== "function") {
365
+ throw missingMethod(event);
366
+ }
367
+ await navigator.replace(event);
368
+ }
369
+ }
370
+
371
+ function missingMethod(event: NativeNavigationEvent): NativeNavigationError {
372
+ return new NativeNavigationError(
373
+ "missing-navigator-method",
374
+ `@uniflowed/router/native: the native navigator does not implement ${event.kind}()`,
375
+ event.href,
376
+ event.route,
377
+ );
378
+ }
379
+
380
+ async function invokeScreenNavigator(
381
+ navigator: NativeScreenNavigator,
382
+ screens: NativeScreenMap,
383
+ event: NativeNavigationEvent,
384
+ ): Promise<void> {
385
+ const payload = nativeScreenPayload(event, screens);
386
+ const state = nativeScreenNavigationState(payload);
387
+ if (event.kind === "push") {
388
+ if (typeof navigator.push !== "function") {
389
+ throw missingMethod(event);
390
+ }
391
+ await navigator.push(payload.screen, state);
392
+ return;
393
+ }
394
+ if (event.kind === "replace") {
395
+ if (typeof navigator.replace !== "function") {
396
+ throw missingMethod(event);
397
+ }
398
+ await navigator.replace(payload.screen, state);
399
+ return;
400
+ }
401
+ if (typeof navigator.prefetch !== "function") {
402
+ throw missingMethod(event);
403
+ }
404
+ await navigator.prefetch(payload.screen, state);
405
+ }
406
+
407
+ async function loadNativeRoute(
408
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
409
+ pathname: string,
410
+ ): Promise<void> {
411
+ const matched = matchRoute(table.routes, pathname);
412
+ if (matched == null || !hasClientPage(matched.route)) {
413
+ return;
414
+ }
415
+ await Promise.all([matched.route.page?.(), ...matched.route.layouts.map((layout) => layout())]);
416
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/router",
3
- "version": "0.0.0-alpha.8",
3
+ "version": "0.1.0",
4
4
  "description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,26 +11,67 @@
11
11
  "directory": "packages/router"
12
12
  },
13
13
  "exports": {
14
- ".": "./index.js",
14
+ ".": {
15
+ "react-server": "./server-components.js",
16
+ "default": "./index.js"
17
+ },
18
+ "./action": "./action.js",
15
19
  "./client": "./client.js",
20
+ "./native": "./native.js",
21
+ "./rsc": "./rsc.js",
22
+ "./rsc/client": "./rsc-client.js",
23
+ "./rsc/ssr": "./rsc-ssr.js",
16
24
  "./server": "./server.js",
25
+ "./routing": "./routing.js",
17
26
  "./package.json": "./package.json",
18
27
  "./handler": "./handler.js",
19
- "./middleware": "./middleware.js"
28
+ "./middleware": "./middleware.js",
29
+ "./native-navigation": "./native-navigation.js",
30
+ "./http-client": "./http-client.js",
31
+ "./instrumentation": "./instrumentation.js"
20
32
  },
21
33
  "files": [
34
+ "action.js",
22
35
  "client.js",
23
36
  "handler.js",
24
37
  "index.js",
25
38
  "internal",
26
39
  "middleware.js",
27
- "server.js"
40
+ "native.js",
41
+ "routing.js",
42
+ "rsc-client.js",
43
+ "rsc-ssr.js",
44
+ "rsc.js",
45
+ "server-components.js",
46
+ "server.js",
47
+ "native-navigation.js",
48
+ "http-client.js",
49
+ "instrumentation.js",
50
+ "!*.test.js"
28
51
  ],
29
52
  "peerDependencies": {
30
- "react": ">=19",
31
- "react-dom": ">=19"
53
+ "react": ">=19.2.3",
54
+ "react-dom": ">=19.2.3",
55
+ "react-server-dom-parcel": ">=19.3.0",
56
+ "react-native": ">=0.86",
57
+ "@react-navigation/native": "^7.0.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "react-dom": {
61
+ "optional": true
62
+ },
63
+ "react-server-dom-parcel": {
64
+ "optional": true
65
+ },
66
+ "react-native": {
67
+ "optional": true
68
+ },
69
+ "@react-navigation/native": {
70
+ "optional": true
71
+ }
32
72
  },
33
73
  "dependencies": {
34
- "@uniflowed/server": "0.0.0-alpha.8"
74
+ "@uniflowed/hooks": "0.1.0",
75
+ "@uniflowed/server": "0.1.0"
35
76
  }
36
77
  }
package/routing.js ADDED
@@ -0,0 +1,51 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/router/routing`: React-free route table helpers.
4
+ //
5
+ // Import this from server-oriented modules that need matching, URL building,
6
+ // or router control errors without importing the client router, React
7
+ // components, or hooks.
8
+
9
+ export type {
10
+ ErrorBoundary,
11
+ LoadingRecord,
12
+ NotFoundBoundary,
13
+ RouteError,
14
+ RouteMatch,
15
+ RouteModule,
16
+ RouteParamSpec,
17
+ RouteParams,
18
+ RouteRecord,
19
+ RouteTable,
20
+ SearchParams,
21
+ SlotRecord,
22
+ SlotRouteRecord,
23
+ TemplateRecord,
24
+ } from "./internal/routing.js";
25
+ export type {
26
+ ResolvedBoundarySummary,
27
+ ResolvedErrorBoundarySummary,
28
+ ResolvedRouteErrorSummary,
29
+ ResolvedRouteSummary,
30
+ ResolvedSlotSummary,
31
+ ResolvedTemplateSummary,
32
+ } from "./internal/resolved-summary.js";
33
+
34
+ export {
35
+ ForbiddenError,
36
+ NotFoundError,
37
+ RedirectError,
38
+ UnauthorizedError,
39
+ buildRoute,
40
+ forbidden,
41
+ hasClientPage,
42
+ matchRoute,
43
+ notFound,
44
+ parseSearch,
45
+ permanentRedirect,
46
+ redirect,
47
+ routeErrorStatus,
48
+ splitUrl,
49
+ unauthorized,
50
+ } from "./internal/routing.js";
51
+ export { summarizeResolvedRoute } from "./internal/resolved-summary.js";
package/rsc-client.js ADDED
@@ -0,0 +1,120 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/router/rsc/client`: starting, in the browser, an application that
4
+ // React Server Components rendered.
5
+ //
6
+ // `virtual:uf/client` imports this entry when routes render as Server
7
+ // Components, which is the default. It imports `@uniflowed/router/client` when
8
+ // they render from their modules (`app.rsc: false`) or into an empty shell
9
+ // (`app.rendering.modes: ["csr"]`). This is an entry of its own because it loads
10
+ // React's Flight client, `react-server-dom-parcel`. That package is an optional
11
+ // peer and needs React 19.3, while the rest of the router runs on the React
12
+ // 19.2.3 that Expo SDK 57 and React Native 0.87 ship (ubugeeei-prod/uf#992). A
13
+ // bundler resolves every import in the graph it is given, whether or not
14
+ // anything calls it, so a browser bundle that renders no Server Component
15
+ // leaves the package out only if nothing it imports names it.
16
+ // `crates/uf_lib/tests/package_surface.rs` holds the router to that.
17
+ //
18
+ // The same reason keeps the payload fetch out of `./internal/runtime.js`.
19
+ // Navigation there serves every application, so `hydrateFlight` hands it the
20
+ // fetch before the first render.
21
+
22
+ import * as React from "react";
23
+ import { StrictMode, startTransition } from "react";
24
+ import { hydrateRoot } from "react-dom/client";
25
+
26
+ import { type TrailingSlash, applicationPathOf } from "./internal/base-path.js";
27
+ import { ROOT_ID } from "./internal/document.js";
28
+ import {
29
+ fetchFlight,
30
+ installBrowserModules,
31
+ readDocumentPayload,
32
+ } from "./internal/flight-browser.js";
33
+ import { domObserver } from "./internal/payload-rows.js";
34
+ import { prepareDocumentForHydration } from "./internal/prepare-document.js";
35
+ import { requireServerComponentsReact } from "./internal/react-version.js";
36
+ import {
37
+ type AppProps,
38
+ type Navigation,
39
+ installFlightFetch,
40
+ installNavigation,
41
+ installRouting,
42
+ installStaleTime,
43
+ } from "./internal/runtime.js";
44
+
45
+ /**
46
+ * Hydrate a document React Server Components rendered.
47
+ *
48
+ * `hydrate` in `./client.js` resolves the route from its modules and renders it
49
+ * again over the server's markup. This one resolves nothing and imports no route
50
+ * module: the document carries the Flight payload its tree was rendered from,
51
+ * React's own client reads it, and the tree the browser hydrates is the tree the
52
+ * server rendered — a Server Component is markup and a reference, and a client
53
+ * component is the one kind of module this page loads. See
54
+ * ubugeeei-prod/uf#519.
55
+ *
56
+ * The payload is read while the document is still arriving. Row 0 is in the
57
+ * shell, so hydration starts as soon as the module script runs, and every row
58
+ * after it lands in a later chunk that the reader picks up as it is parsed — so
59
+ * a boundary the server completes after hydration began resolves then, with no
60
+ * second request.
61
+ *
62
+ * Everything else is `hydrate`'s, for the reasons written there: the navigation
63
+ * mode is installed before the first render, the development hydration report
64
+ * captures the server's markup before React repairs it, and Strict Mode wraps
65
+ * the root.
66
+ *
67
+ * On a React older than 19.3 it refuses before it touches the page, naming the
68
+ * version it found; see `./internal/react-version.js`.
69
+ */
70
+ export async function hydrateFlight(options: {|
71
+ readonly App: React.ComponentType<AppProps>,
72
+ readonly strictMode?: boolean,
73
+ readonly navigation?: Navigation,
74
+ readonly basePath?: string,
75
+ readonly trailingSlash?: TrailingSlash,
76
+ readonly staleTime?: number,
77
+ |}): Promise<void> {
78
+ requireServerComponentsReact("@uniflowed/router/rsc/client");
79
+ installNavigation(options.navigation ?? "client");
80
+ installRouting({ basePath: options.basePath, trailingSlash: options.trailingSlash });
81
+ installStaleTime(options.staleTime ?? 0);
82
+ installFlightFetch(fetchFlight);
83
+ installBrowserModules();
84
+ const flight = readDocumentPayload(document, domObserver(document));
85
+
86
+ // The route table has no base path in it, and the address bar does.
87
+ const url =
88
+ (applicationPathOf(window.location.pathname) ?? window.location.pathname) +
89
+ window.location.search;
90
+ const { App } = options;
91
+ const container = document.getElementById(ROOT_ID) ?? document;
92
+ prepareDocumentForHydration(document);
93
+
94
+ let recovery = null;
95
+ let restoreDevHead = null;
96
+ if (import.meta.hot != null) {
97
+ const { captureServerMarkup, hydrationErrorHandler, prepareDevHeadForHydration } =
98
+ await import("./internal/hydration.js");
99
+ restoreDevHead = prepareDevHeadForHydration(document);
100
+ recovery = hydrationErrorHandler(container, captureServerMarkup(container), document);
101
+ }
102
+
103
+ const tree = <App url={url} flight={flight} />;
104
+
105
+ startTransition(() => {
106
+ hydrateRoot(
107
+ container,
108
+ options.strictMode === true ? <StrictMode>{tree}</StrictMode> : tree,
109
+ recovery == null ? undefined : { onRecoverableError: recovery },
110
+ );
111
+ if (restoreDevHead != null) {
112
+ setTimeout(restoreDevHead, 250);
113
+ }
114
+ });
115
+
116
+ if (import.meta.hot != null) {
117
+ const { reportDevtools } = await import("./internal/devtools.js");
118
+ reportDevtools(window);
119
+ }
120
+ }