@uniflowed/router 0.2.0 → 0.4.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 +58 -1
- package/client.js +27 -8
- package/internal/action-endpoint.js +102 -2
- package/internal/action-wire.js +70 -8
- package/internal/compose.js +14 -4
- package/internal/deployment.js +3 -2
- package/internal/devtools.js +1 -1
- package/internal/error-view.js +1 -1
- package/internal/flight-browser.js +86 -10
- package/internal/flight-chunks.js +7 -7
- package/internal/flight-rows.js +7 -2
- package/internal/flight-ssr.js +10 -3
- package/internal/flight.js +32 -0
- package/internal/hydration.js +11 -6
- package/internal/navigation-cache.js +1 -1
- package/internal/payload-rows.js +5 -4
- package/internal/payload.js +18 -12
- package/internal/prepare-document.js +6 -0
- package/internal/resolve.js +17 -11
- package/internal/routing.js +77 -12
- package/internal/runtime.js +114 -6
- package/internal/stream.js +29 -14
- package/middleware.js +20 -4
- package/package.json +6 -4
- package/rsc-ssr.js +14 -5
- package/rsc.js +23 -7
- package/server.js +2 -2
- package/testing.js +980 -0
package/internal/runtime.js
CHANGED
|
@@ -100,7 +100,13 @@ import {
|
|
|
100
100
|
navigationKey,
|
|
101
101
|
routeNavigations,
|
|
102
102
|
} from "./navigation-cache.js";
|
|
103
|
-
import {
|
|
103
|
+
import {
|
|
104
|
+
hasClientPage,
|
|
105
|
+
matchRoute,
|
|
106
|
+
nearestBoundary,
|
|
107
|
+
refuseScriptUrl,
|
|
108
|
+
scriptSchemeOf,
|
|
109
|
+
} from "./routing.js";
|
|
104
110
|
import type { RouteParams, SearchParams } from "./routing.js";
|
|
105
111
|
import {
|
|
106
112
|
beneath,
|
|
@@ -352,6 +358,70 @@ export type Router = {|
|
|
|
352
358
|
readonly forward: () => void,
|
|
353
359
|
|};
|
|
354
360
|
|
|
361
|
+
/**
|
|
362
|
+
* The router of the `RouterProvider` on screen, or `null` when none is mounted.
|
|
363
|
+
*
|
|
364
|
+
* Module state, for the one caller that is not a component and still has to
|
|
365
|
+
* navigate: a server action reference whose action called `redirect()`
|
|
366
|
+
* (`../action.js`). A reference is a plain function that React calls from a
|
|
367
|
+
* form or a transition, with no hook to read the context through, and the
|
|
368
|
+
* navigation it owes is the one `router.push` would make — a client render
|
|
369
|
+
* under client navigation, a document load under document navigation.
|
|
370
|
+
*/
|
|
371
|
+
let mountedRouter: Router | null = null;
|
|
372
|
+
|
|
373
|
+
/** Publish `router` as [`mountedRouter`] while its provider is mounted. */
|
|
374
|
+
hook useMountedRouter(router: Router): void {
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
mountedRouter = router;
|
|
377
|
+
return () => {
|
|
378
|
+
if (mountedRouter === router) {
|
|
379
|
+
mountedRouter = null;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Go where a server action's `redirect()` pointed.
|
|
387
|
+
*
|
|
388
|
+
* `location` is the address the action endpoint answered with — already under
|
|
389
|
+
* `app.router.basePath` for a path on this application — and `from` is the page
|
|
390
|
+
* the call was made from, which a relative `location` is resolved against: the
|
|
391
|
+
* visitor may have navigated while the action ran, and `redirect("next")` means
|
|
392
|
+
* next to the page that called it. A path on this origin is the mounted
|
|
393
|
+
* router's `push`, so the page underneath stays hydrated and a layout keeps its
|
|
394
|
+
* state, exactly as a `Link` to it would; another origin, or no router on
|
|
395
|
+
* screen, is the browser's document load. The promise settles when the
|
|
396
|
+
* navigation has, so a `useActionState` that awaited the action is not left
|
|
397
|
+
* pending on a page that has moved on.
|
|
398
|
+
*
|
|
399
|
+
* Only `http:` and `https:` are followed. `redirect()` refuses a script URL
|
|
400
|
+
* where it is built, but `location` arrived over the network, and
|
|
401
|
+
* `location.assign("javascript:…")` runs it in this page; a scheme this
|
|
402
|
+
* function would not follow is thrown as an error instead.
|
|
403
|
+
*/
|
|
404
|
+
export async function followActionRedirect(location: string, from: string): Promise<void> {
|
|
405
|
+
const target = new URL(location, new URL(from, window.location.href));
|
|
406
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`@uniflowed/router: a server action redirected to a ${target.protocol} URL, which is not ` +
|
|
409
|
+
"followed. A redirect goes to an http or https address.",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const router = mountedRouter;
|
|
413
|
+
if (router == null || target.origin !== window.location.origin) {
|
|
414
|
+
window.location.assign(target.href);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const applicationPath = applicationPathOf(target.pathname);
|
|
418
|
+
if (applicationPath == null) {
|
|
419
|
+
window.location.assign(target.href);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await router.push(applicationPath + target.search + target.hash);
|
|
423
|
+
}
|
|
424
|
+
|
|
355
425
|
/** What `useRoute()` returns. */
|
|
356
426
|
export type RouteInfo = {|
|
|
357
427
|
readonly path: string,
|
|
@@ -679,6 +749,9 @@ component ModuleRouter(url: string, initial: ResolvedRoute, children: React.Node
|
|
|
679
749
|
if (!isBrowser()) {
|
|
680
750
|
return;
|
|
681
751
|
}
|
|
752
|
+
// Before anything else: every path below may end in `location.assign`,
|
|
753
|
+
// which runs a `javascript:` URL in this page. See `refuseScriptUrl`.
|
|
754
|
+
refuseScriptUrl(to, "a router navigation");
|
|
682
755
|
const target = new URL(addressOf(to), window.location.href);
|
|
683
756
|
// The application path the route table is asked about, and the address the
|
|
684
757
|
// history entry keeps: one URL, with and without `app.router.basePath`.
|
|
@@ -973,6 +1046,8 @@ component ModuleRouter(url: string, initial: ResolvedRoute, children: React.Node
|
|
|
973
1046
|
},
|
|
974
1047
|
};
|
|
975
1048
|
|
|
1049
|
+
useMountedRouter(router);
|
|
1050
|
+
|
|
976
1051
|
const value: RouterState = {
|
|
977
1052
|
route: routeState(resolved),
|
|
978
1053
|
view: { kind: "modules", resolved },
|
|
@@ -1019,6 +1094,8 @@ component FlightRouter(flight: Promise<FlightRoot>, children: React.Node) {
|
|
|
1019
1094
|
if (!isBrowser()) {
|
|
1020
1095
|
return;
|
|
1021
1096
|
}
|
|
1097
|
+
// As in `ModuleRouter`: a `javascript:` URL never reaches `location`.
|
|
1098
|
+
refuseScriptUrl(to, "a router navigation");
|
|
1022
1099
|
// A payload URL is an address, so it keeps the base path; the server takes
|
|
1023
1100
|
// it off.
|
|
1024
1101
|
const target = new URL(addressOf(to), window.location.href);
|
|
@@ -1243,6 +1320,8 @@ component FlightRouter(flight: Promise<FlightRoot>, children: React.Node) {
|
|
|
1243
1320
|
},
|
|
1244
1321
|
};
|
|
1245
1322
|
|
|
1323
|
+
useMountedRouter(router);
|
|
1324
|
+
|
|
1246
1325
|
const value: RouterState = {
|
|
1247
1326
|
route: root.route,
|
|
1248
1327
|
view: { kind: "flight", tree: root.tree },
|
|
@@ -1781,6 +1860,28 @@ export hook useLinkStatus(): {| readonly pending: boolean |} {
|
|
|
1781
1860
|
return { pending: useContext(LinkStatusContext) };
|
|
1782
1861
|
}
|
|
1783
1862
|
|
|
1863
|
+
/**
|
|
1864
|
+
* The click a `Link` hands its `onClick`: React's synthetic mouse event, as
|
|
1865
|
+
* much of it as a link reads.
|
|
1866
|
+
*
|
|
1867
|
+
* Flow's React library no longer declares `SyntheticMouseEvent`, so the name
|
|
1868
|
+
* this prop used did not resolve and every handler passed to it was unchecked.
|
|
1869
|
+
* Stated structurally, as `@uniflowed/ui`'s `PartEvent` is; the event React
|
|
1870
|
+
* passes has every member below.
|
|
1871
|
+
*/
|
|
1872
|
+
export type LinkClickEvent = {
|
|
1873
|
+
readonly defaultPrevented: boolean,
|
|
1874
|
+
readonly button: number,
|
|
1875
|
+
readonly altKey: boolean,
|
|
1876
|
+
readonly ctrlKey: boolean,
|
|
1877
|
+
readonly metaKey: boolean,
|
|
1878
|
+
readonly shiftKey: boolean,
|
|
1879
|
+
readonly currentTarget: mixed,
|
|
1880
|
+
readonly preventDefault: () => mixed,
|
|
1881
|
+
readonly stopPropagation: () => mixed,
|
|
1882
|
+
...
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1784
1885
|
/**
|
|
1785
1886
|
* A client-side navigation.
|
|
1786
1887
|
*
|
|
@@ -1813,8 +1914,10 @@ export component Link(
|
|
|
1813
1914
|
transition?: boolean = true,
|
|
1814
1915
|
children?: React.Node,
|
|
1815
1916
|
className?: string,
|
|
1816
|
-
onClick?: (event:
|
|
1817
|
-
|
|
1917
|
+
onClick?: (event: LinkClickEvent) => mixed,
|
|
1918
|
+
// `key` named out of the indexer: React never hands a component its key, and
|
|
1919
|
+
// the rest is spread onto the anchor, where a `mixed` key would not do.
|
|
1920
|
+
...rest: { readonly key?: empty, readonly [string]: mixed }
|
|
1818
1921
|
) {
|
|
1819
1922
|
const { router, navigation } = useRouterState();
|
|
1820
1923
|
const [linkPending, startLinkTransition] = useTransition();
|
|
@@ -1835,7 +1938,7 @@ export component Link(
|
|
|
1835
1938
|
}
|
|
1836
1939
|
});
|
|
1837
1940
|
|
|
1838
|
-
const handleClick = (event:
|
|
1941
|
+
const handleClick = (event: LinkClickEvent) => {
|
|
1839
1942
|
if (onClick != null) {
|
|
1840
1943
|
onClick(event);
|
|
1841
1944
|
}
|
|
@@ -1857,9 +1960,14 @@ export component Link(
|
|
|
1857
1960
|
try {
|
|
1858
1961
|
await router.push(to, { replace, transition });
|
|
1859
1962
|
} catch (error) {
|
|
1860
|
-
// A failed navigation falls back to the browser doing it
|
|
1963
|
+
// A failed navigation falls back to the browser doing it — unless what
|
|
1964
|
+
// failed was the router refusing a script URL, which the browser would
|
|
1965
|
+
// run rather than load. `isExternal` above does not catch every
|
|
1966
|
+
// spelling of one: ` javascript:` with a leading space is a path to it.
|
|
1861
1967
|
console.error(error);
|
|
1862
|
-
|
|
1968
|
+
if (scriptSchemeOf(to) == null) {
|
|
1969
|
+
window.location.assign(addressOf(to));
|
|
1970
|
+
}
|
|
1863
1971
|
}
|
|
1864
1972
|
});
|
|
1865
1973
|
};
|
package/internal/stream.js
CHANGED
|
@@ -100,12 +100,25 @@ type NodeDestination = {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
/** The controller a `ReadableStream` source is handed. */
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
/**
|
|
104
|
+
* What `renderToPipeableStream` hands back, as much of it as is used here.
|
|
105
|
+
*
|
|
106
|
+
* Stated because the callbacks passed to the call use its result, and Flow
|
|
107
|
+
* cannot type a value whose definition depends on itself without being told.
|
|
108
|
+
*/
|
|
109
|
+
type PipeableStream = {
|
|
110
|
+
readonly pipe: (destination: NodeDestination) => mixed,
|
|
111
|
+
readonly abort: (reason?: mixed) => void,
|
|
106
112
|
...
|
|
107
113
|
};
|
|
108
114
|
|
|
115
|
+
type StreamController = interface {
|
|
116
|
+
// Methods, as a `ReadableStreamDefaultController`'s are: a method cannot be
|
|
117
|
+
// read off its object as a function-valued property.
|
|
118
|
+
enqueue(chunk: Uint8Array): mixed,
|
|
119
|
+
close(): mixed,
|
|
120
|
+
};
|
|
121
|
+
|
|
109
122
|
/**
|
|
110
123
|
* A stream of bytes, as much of one as this module reads.
|
|
111
124
|
*
|
|
@@ -113,13 +126,13 @@ type StreamController = {
|
|
|
113
126
|
* result and `react-dom/static`'s `prelude` — and neither is typed by anything
|
|
114
127
|
* uf can import, so the shape it is used through is stated here.
|
|
115
128
|
*/
|
|
116
|
-
type ByteSource = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
...
|
|
129
|
+
type ByteSource = interface {
|
|
130
|
+
// Methods, as a `ReadableStream`'s are: a class instance is not a subtype of
|
|
131
|
+
// an object type with function-valued properties.
|
|
132
|
+
getReader(): interface {
|
|
133
|
+
read(): Promise<{ readonly done?: boolean, readonly value?: Uint8Array, ... }>,
|
|
134
|
+
releaseLock(): mixed,
|
|
121
135
|
},
|
|
122
|
-
...
|
|
123
136
|
};
|
|
124
137
|
|
|
125
138
|
/** How the document is assembled around the app's markup. */
|
|
@@ -732,7 +745,7 @@ export function renderDocument(node: React.Node, options: RenderOptions): Promis
|
|
|
732
745
|
const queue = new ChunkQueue();
|
|
733
746
|
return new Promise((resolve, reject) => {
|
|
734
747
|
if (typeof ReactDOMServer.renderToPipeableStream === "function") {
|
|
735
|
-
const { pipe, abort } = ReactDOMServer.renderToPipeableStream(node, {
|
|
748
|
+
const { pipe, abort }: PipeableStream = ReactDOMServer.renderToPipeableStream(node, {
|
|
736
749
|
onShellReady() {
|
|
737
750
|
pipe(queueDestination(queue));
|
|
738
751
|
resolve(
|
|
@@ -1355,13 +1368,15 @@ function advanced(boundary: Boundary, html: string, rootDepth: number): Boundary
|
|
|
1355
1368
|
*
|
|
1356
1369
|
* `prerenderToNodeStream` hands back a Node `Readable` and `prerender` a web
|
|
1357
1370
|
* `ReadableStream`, and which one a build has depends on which React entry
|
|
1358
|
-
* point exists — so the union is real rather than defensive
|
|
1359
|
-
* is
|
|
1371
|
+
* point exists — so the union is real rather than defensive. Which one it is
|
|
1372
|
+
* is asked of the class: a web stream is a `ReadableStream`, and a Node
|
|
1373
|
+
* `Readable` is not. A test of a `getReader` property answered the same at run
|
|
1374
|
+
* time, but a union of two structural types cannot be narrowed by one.
|
|
1360
1375
|
*/
|
|
1361
1376
|
async function* preludeChunks(
|
|
1362
|
-
prelude:
|
|
1377
|
+
prelude: ReadableStream | AsyncIterable<string | Uint8Array>,
|
|
1363
1378
|
): AsyncGenerator<string, void, void> {
|
|
1364
|
-
if (
|
|
1379
|
+
if (prelude instanceof ReadableStream) {
|
|
1365
1380
|
yield* decoded(prelude);
|
|
1366
1381
|
return;
|
|
1367
1382
|
}
|
package/middleware.js
CHANGED
|
@@ -395,12 +395,28 @@ function withPathname(url: URL, pathname: string): URL {
|
|
|
395
395
|
/**
|
|
396
396
|
* `request` at another URL: same method, headers, signal and body.
|
|
397
397
|
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
398
|
+
* Every member of `RequestInit` that a `Request` carries, named — what passing
|
|
399
|
+
* the request itself as the init would read, spelled out so that the checker
|
|
400
|
+
* can see it. A streamed body is handed on rather than read, which is why a
|
|
401
|
+
* body comes with `duplex: "half"`, the one mode a streamed request body has.
|
|
400
402
|
*/
|
|
401
403
|
function requestAt(request: Request, url: URL): Request {
|
|
402
|
-
|
|
403
|
-
return new Request(url.href,
|
|
404
|
+
const body = request.body;
|
|
405
|
+
return new Request(url.href, {
|
|
406
|
+
method: request.method,
|
|
407
|
+
headers: request.headers,
|
|
408
|
+
body,
|
|
409
|
+
...(body == null ? {} : { duplex: "half" }),
|
|
410
|
+
signal: request.signal,
|
|
411
|
+
cache: request.cache,
|
|
412
|
+
credentials: request.credentials,
|
|
413
|
+
integrity: request.integrity,
|
|
414
|
+
keepalive: request.keepalive,
|
|
415
|
+
mode: request.mode,
|
|
416
|
+
redirect: request.redirect,
|
|
417
|
+
referrer: request.referrer,
|
|
418
|
+
referrerPolicy: request.referrerPolicy,
|
|
419
|
+
});
|
|
404
420
|
}
|
|
405
421
|
|
|
406
422
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./middleware": "./middleware.js",
|
|
29
29
|
"./native-navigation": "./native-navigation.js",
|
|
30
30
|
"./http-client": "./http-client.js",
|
|
31
|
-
"./instrumentation": "./instrumentation.js"
|
|
31
|
+
"./instrumentation": "./instrumentation.js",
|
|
32
|
+
"./testing": "./testing.js"
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
34
35
|
"action.js",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"native-navigation.js",
|
|
48
49
|
"http-client.js",
|
|
49
50
|
"instrumentation.js",
|
|
51
|
+
"testing.js",
|
|
50
52
|
"!*.test.js"
|
|
51
53
|
],
|
|
52
54
|
"peerDependencies": {
|
|
@@ -71,7 +73,7 @@
|
|
|
71
73
|
}
|
|
72
74
|
},
|
|
73
75
|
"dependencies": {
|
|
74
|
-
"@uniflowed/hooks": "0.
|
|
75
|
-
"@uniflowed/server": "0.
|
|
76
|
+
"@uniflowed/hooks": "0.4.0",
|
|
77
|
+
"@uniflowed/server": "0.4.0"
|
|
76
78
|
}
|
|
77
79
|
}
|
package/rsc-ssr.js
CHANGED
|
@@ -144,7 +144,7 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
|
|
|
144
144
|
assets: RenderAssets,
|
|
145
145
|
settings?: RenderOptions,
|
|
146
146
|
): Promise<RenderResult> {
|
|
147
|
-
const report = settings?.onError ?? (() => {});
|
|
147
|
+
const report = settings?.onError ?? ((_error: mixed) => {});
|
|
148
148
|
const send = settings?.onStream;
|
|
149
149
|
const onStream = send == null ? undefined : streamReporter(url, send);
|
|
150
150
|
const transformHead = settings?.transformHead;
|
|
@@ -248,7 +248,7 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
|
|
|
248
248
|
assets: RenderAssets,
|
|
249
249
|
settings?: RenderOptions,
|
|
250
250
|
): Promise<PrerenderResult> {
|
|
251
|
-
const report = settings?.onError ?? (() => {});
|
|
251
|
+
const report = settings?.onError ?? ((_error: mixed) => {});
|
|
252
252
|
const ledger = createErrorLedger();
|
|
253
253
|
// A read of the request, while the build may leave it for the request, is
|
|
254
254
|
// not an exception to report: it is how the build finds the holes. Its row
|
|
@@ -405,7 +405,7 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
|
|
|
405
405
|
shell: PrerenderedShell,
|
|
406
406
|
settings?: RenderOptions,
|
|
407
407
|
): Promise<RenderResult> {
|
|
408
|
-
const report = settings?.onError ?? (() => {});
|
|
408
|
+
const report = settings?.onError ?? ((_error: mixed) => {});
|
|
409
409
|
const ledger = createErrorLedger();
|
|
410
410
|
const stop = new AbortController();
|
|
411
411
|
const rendered = await renderFlight(url, {
|
|
@@ -477,7 +477,16 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
|
|
|
477
477
|
}
|
|
478
478
|
return {
|
|
479
479
|
status: rendered.status,
|
|
480
|
-
headers: {
|
|
480
|
+
headers: {
|
|
481
|
+
"content-type": FLIGHT_CONTENT_TYPE,
|
|
482
|
+
vary: INTERCEPTED_FROM_HEADER,
|
|
483
|
+
// The payload carries the page's strings as they were rendered — a
|
|
484
|
+
// comment body with `<img onerror>` in it included — and its URL is one
|
|
485
|
+
// anybody can open as a document. `text/x-component` is not a type a
|
|
486
|
+
// browser sniffs into HTML today; `nosniff` makes that a promise rather
|
|
487
|
+
// than a property of today's browsers.
|
|
488
|
+
"x-content-type-options": "nosniff",
|
|
489
|
+
},
|
|
481
490
|
stream: rendered.stream,
|
|
482
491
|
error: rendered.failure,
|
|
483
492
|
};
|
|
@@ -601,7 +610,7 @@ function digestOf(error: mixed): string | null {
|
|
|
601
610
|
if (error == null || typeof error !== "object") {
|
|
602
611
|
return null;
|
|
603
612
|
}
|
|
604
|
-
const tagged: {
|
|
613
|
+
const tagged: { readonly digest?: mixed, ... } = error as $FlowFixMe;
|
|
605
614
|
return typeof tagged.digest === "string" ? tagged.digest : null;
|
|
606
615
|
}
|
|
607
616
|
|
package/rsc.js
CHANGED
|
@@ -59,17 +59,28 @@ import { BoundaryReporter } from "./internal/boundaries.js";
|
|
|
59
59
|
import { routeBoundaries } from "./internal/boundary-data.js";
|
|
60
60
|
import { composeRoute, pageComponent } from "./internal/compose.js";
|
|
61
61
|
import { ErrorRoutePage } from "./internal/error-view.js";
|
|
62
|
-
import { type FlightRoot, routeState } from "./internal/flight.js";
|
|
62
|
+
import { type FlightRoot, crossableRouteError, routeState } from "./internal/flight.js";
|
|
63
63
|
import { requireServerComponentsReact } from "./internal/react-version.js";
|
|
64
|
-
import type {
|
|
64
|
+
import type {
|
|
65
|
+
ErrorModule,
|
|
66
|
+
PageModule,
|
|
67
|
+
ResolvedRoute,
|
|
68
|
+
ResolvedSlot,
|
|
69
|
+
RouteTable,
|
|
70
|
+
} from "./internal/resolve.js";
|
|
65
71
|
import { resolveFailure, resolveInterception, resolveMatch } from "./internal/resolve.js";
|
|
66
|
-
import type { RouteParams,
|
|
72
|
+
import type { RouteParams, SearchParams } from "./internal/routing.js";
|
|
67
73
|
import { RedirectError, nearestBoundary } from "./internal/routing.js";
|
|
68
74
|
import { withServerRoute } from "./internal/server-route.js";
|
|
69
75
|
|
|
70
76
|
export type { FlightRoot, RouteState } from "./internal/flight.js";
|
|
71
77
|
// For `virtual:uf/rsc`, so a Server Component's `basePath()` is the project's.
|
|
72
78
|
export { installRouting } from "./internal/base-path.js";
|
|
79
|
+
// For `virtual:uf/rsc` too: the server-action endpoint is built in this graph,
|
|
80
|
+
// beside the pages, so an action and the page that shows what it wrote run the
|
|
81
|
+
// same instance of every module they share (ubugeeei-prod/uf#1469). The host
|
|
82
|
+
// reaches it through the bridge, as it reaches `renderFlight`.
|
|
83
|
+
export { createActionDispatcher } from "./internal/action-endpoint.js";
|
|
73
84
|
|
|
74
85
|
/** What a host may tell the renderer about one render. */
|
|
75
86
|
export type FlightOptions = {|
|
|
@@ -209,9 +220,11 @@ export function createFlightRenderer(options: {|
|
|
|
209
220
|
</>
|
|
210
221
|
);
|
|
211
222
|
const root: FlightRoot = { route: state, tree, deployment: options.deployment ?? null };
|
|
212
|
-
|
|
223
|
+
// From the route as it resolved, not the copy for the browser: the host is
|
|
224
|
+
// told what was actually thrown. See `crossableRouteError`.
|
|
225
|
+
const failure = renderFailure(resolved);
|
|
213
226
|
if (failure != null) reportRequestError(failure, "render");
|
|
214
|
-
const report = (error) => {
|
|
227
|
+
const report = (error: mixed) => {
|
|
215
228
|
reportRequestError(error, "render");
|
|
216
229
|
if (settings?.onError != null) return settings.onError(error);
|
|
217
230
|
console.error(error);
|
|
@@ -227,7 +240,7 @@ export function createFlightRenderer(options: {|
|
|
|
227
240
|
signal: settings?.signal,
|
|
228
241
|
}),
|
|
229
242
|
);
|
|
230
|
-
return { kind: "route", status: route.status, stream, failure
|
|
243
|
+
return { kind: "route", status: route.status, stream, failure };
|
|
231
244
|
};
|
|
232
245
|
}
|
|
233
246
|
|
|
@@ -343,6 +356,9 @@ function forTheBrowser(resolved: ResolvedRoute, file: string | null): ResolvedRo
|
|
|
343
356
|
const boundary = resolved.errorBoundary;
|
|
344
357
|
return {
|
|
345
358
|
...resolved,
|
|
359
|
+
// Only an `Error` has React's production rule of carrying no message; see
|
|
360
|
+
// `crossableRouteError`.
|
|
361
|
+
error: crossableRouteError(resolved.error),
|
|
346
362
|
errorBoundary: { above: boundary.above, module: clientErrorModule(boundary.module, file) },
|
|
347
363
|
slots: resolved.slots.map(slotForTheBrowser),
|
|
348
364
|
};
|
|
@@ -385,7 +401,7 @@ function isClientReference(value: mixed): boolean {
|
|
|
385
401
|
if (value == null || (typeof value !== "object" && typeof value !== "function")) {
|
|
386
402
|
return false;
|
|
387
403
|
}
|
|
388
|
-
const tagged: {
|
|
404
|
+
const tagged: { readonly $$typeof?: mixed, ... } = value as $FlowFixMe;
|
|
389
405
|
return tagged.$$typeof === CLIENT_REFERENCE;
|
|
390
406
|
}
|
|
391
407
|
|
package/server.js
CHANGED
|
@@ -412,7 +412,7 @@ export function createRenderer(options: {|
|
|
|
412
412
|
return redirectDocument(resolution.error);
|
|
413
413
|
}
|
|
414
414
|
let resolved: ResolvedRoute = resolution.route;
|
|
415
|
-
const report = settings?.onError ?? (() => {});
|
|
415
|
+
const report = settings?.onError ?? ((_error: mixed) => {});
|
|
416
416
|
// Built once and shared by both renders below, so a page that threw its
|
|
417
417
|
// shell away and rendered its error boundary instead reports the stream the
|
|
418
418
|
// browser was actually sent rather than the one that was abandoned.
|
|
@@ -505,7 +505,7 @@ export function createRenderer(options: {|
|
|
|
505
505
|
return redirectResult(redirectDocument(resolution.error));
|
|
506
506
|
}
|
|
507
507
|
let resolved: ResolvedRoute = resolution.route;
|
|
508
|
-
const report = settings?.onError ?? (() => {});
|
|
508
|
+
const report = settings?.onError ?? ((_error: mixed) => {});
|
|
509
509
|
|
|
510
510
|
let html: string;
|
|
511
511
|
try {
|