@uniflowed/router 0.5.0 → 0.7.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 +94 -57
- package/index.js +26 -2
- package/internal/action-endpoint.js +21 -13
- package/internal/error-view.js +60 -6
- package/internal/flight-browser.js +25 -1
- package/internal/flight.js +16 -2
- package/internal/resolve.js +37 -2
- package/internal/resolved-summary.js +7 -3
- package/internal/routing.js +121 -7
- package/internal/runtime.js +108 -6
- package/internal/search-params.js +234 -0
- package/middleware.js +6 -0
- package/native.js +3 -0
- package/package.json +4 -3
- package/routing.js +4 -0
- package/rsc-client.js +2 -0
- package/rsc-ssr.js +9 -2
- package/rsc.js +57 -15
- package/server-components.js +3 -1
- package/server.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@uniflowed/hooks": "0.
|
|
77
|
-
"@uniflowed/server": "0.
|
|
76
|
+
"@uniflowed/hooks": "0.7.0",
|
|
77
|
+
"@uniflowed/server": "0.7.0",
|
|
78
|
+
"@uniflowed/validator": "0.7.0"
|
|
78
79
|
}
|
|
79
80
|
}
|
package/routing.js
CHANGED
|
@@ -18,6 +18,8 @@ export type {
|
|
|
18
18
|
RouteRecord,
|
|
19
19
|
RouteTable,
|
|
20
20
|
SearchParams,
|
|
21
|
+
SearchParamsAll,
|
|
22
|
+
SearchParamsIssue,
|
|
21
23
|
SlotRecord,
|
|
22
24
|
SlotRouteRecord,
|
|
23
25
|
TemplateRecord,
|
|
@@ -35,6 +37,7 @@ export {
|
|
|
35
37
|
ForbiddenError,
|
|
36
38
|
NotFoundError,
|
|
37
39
|
RedirectError,
|
|
40
|
+
SearchParamsError,
|
|
38
41
|
UnauthorizedError,
|
|
39
42
|
buildRoute,
|
|
40
43
|
forbidden,
|
|
@@ -42,6 +45,7 @@ export {
|
|
|
42
45
|
matchRoute,
|
|
43
46
|
notFound,
|
|
44
47
|
parseSearch,
|
|
48
|
+
parseSearchAll,
|
|
45
49
|
permanentRedirect,
|
|
46
50
|
redirect,
|
|
47
51
|
routeErrorStatus,
|
package/rsc-client.js
CHANGED
|
@@ -28,6 +28,7 @@ import { ROOT_ID } from "./internal/document.js";
|
|
|
28
28
|
import {
|
|
29
29
|
fetchFlight,
|
|
30
30
|
installBrowserModules,
|
|
31
|
+
installServerCallback,
|
|
31
32
|
readDocumentPayload,
|
|
32
33
|
} from "./internal/flight-browser.js";
|
|
33
34
|
import { domObserver } from "./internal/payload-rows.js";
|
|
@@ -83,6 +84,7 @@ export async function hydrateFlight(options: {|
|
|
|
83
84
|
installStaleTime(options.staleTime ?? 0);
|
|
84
85
|
installFlightFetch(fetchFlight);
|
|
85
86
|
installBrowserModules();
|
|
87
|
+
installServerCallback();
|
|
86
88
|
const flight = readDocumentPayload(document, domObserver(document));
|
|
87
89
|
|
|
88
90
|
// The route table has no base path in it, and the address bar does.
|
package/rsc-ssr.js
CHANGED
|
@@ -28,7 +28,12 @@ import {
|
|
|
28
28
|
installServerModules,
|
|
29
29
|
readPayload,
|
|
30
30
|
} from "./internal/flight-ssr.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
FLIGHT_CONTENT_TYPE,
|
|
33
|
+
INTERCEPTED_FROM_HEADER,
|
|
34
|
+
NOT_FOUND_HEADER,
|
|
35
|
+
flightUrl,
|
|
36
|
+
} from "./internal/flight.js";
|
|
32
37
|
import { type StreamRecord, streamReporter } from "./internal/inspector.js";
|
|
33
38
|
import { requireServerComponentsReact } from "./internal/react-version.js";
|
|
34
39
|
import { RedirectError } from "./internal/routing.js";
|
|
@@ -456,11 +461,13 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
|
|
|
456
461
|
settings?: {|
|
|
457
462
|
readonly onError?: (error: mixed) => void,
|
|
458
463
|
readonly interceptedFrom?: string,
|
|
464
|
+
readonly notFound?: boolean,
|
|
459
465
|
|},
|
|
460
466
|
): Promise<FlightResponse> {
|
|
461
467
|
const rendered = await renderFlight(url, {
|
|
462
468
|
onError: settings?.onError,
|
|
463
469
|
interceptedFrom: settings?.interceptedFrom,
|
|
470
|
+
notFound: settings?.notFound,
|
|
464
471
|
});
|
|
465
472
|
if (rendered.kind === "redirect") {
|
|
466
473
|
const { location } = rendered;
|
|
@@ -479,7 +486,7 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
|
|
|
479
486
|
status: rendered.status,
|
|
480
487
|
headers: {
|
|
481
488
|
"content-type": FLIGHT_CONTENT_TYPE,
|
|
482
|
-
vary: INTERCEPTED_FROM_HEADER,
|
|
489
|
+
vary: `${INTERCEPTED_FROM_HEADER}, ${NOT_FOUND_HEADER}`,
|
|
483
490
|
// The payload carries the page's strings as they were rendered — a
|
|
484
491
|
// comment body with `<img onerror>` in it included — and its URL is one
|
|
485
492
|
// anybody can open as a document. `text/x-component` is not a type a
|
package/rsc.js
CHANGED
|
@@ -50,7 +50,7 @@ import * as React from "react";
|
|
|
50
50
|
import { use } from "react";
|
|
51
51
|
// React's Flight server. Only this graph can load it: it refuses to evaluate
|
|
52
52
|
// unless `react` resolved under the `react-server` condition.
|
|
53
|
-
import { renderToReadableStream } from "react-server-dom-parcel/server";
|
|
53
|
+
import { registerServerReference, renderToReadableStream } from "react-server-dom-parcel/server";
|
|
54
54
|
|
|
55
55
|
import { noteRoute } from "@uniflowed/server/host";
|
|
56
56
|
import { reportRequestError } from "@uniflowed/server/instrumentation";
|
|
@@ -68,9 +68,14 @@ import type {
|
|
|
68
68
|
ResolvedSlot,
|
|
69
69
|
RouteTable,
|
|
70
70
|
} from "./internal/resolve.js";
|
|
71
|
-
import {
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
import {
|
|
72
|
+
pageSearchParams,
|
|
73
|
+
resolveFailure,
|
|
74
|
+
resolveInterception,
|
|
75
|
+
resolveMatch,
|
|
76
|
+
} from "./internal/resolve.js";
|
|
77
|
+
import type { RouteParams } from "./internal/routing.js";
|
|
78
|
+
import { NotFoundError, RedirectError, nearestBoundary } from "./internal/routing.js";
|
|
74
79
|
import { withServerRoute } from "./internal/server-route.js";
|
|
75
80
|
|
|
76
81
|
export type { FlightRoot, RouteState } from "./internal/flight.js";
|
|
@@ -81,6 +86,36 @@ export { installRouting } from "./internal/base-path.js";
|
|
|
81
86
|
// same instance of every module they share (ubugeeei-prod/uf#1469). The host
|
|
82
87
|
// reaches it through the bridge, as it reaches `renderFlight`.
|
|
83
88
|
export { createActionDispatcher } from "./internal/action-endpoint.js";
|
|
89
|
+
// And the route handlers and the middleware, for the same reason: a `POST` a
|
|
90
|
+
// `$route.js` answers writes to the same instance of a module that the page
|
|
91
|
+
// reading it renders from (ubugeeei-prod/uf#1487).
|
|
92
|
+
export { createDispatcher } from "./handler.js";
|
|
93
|
+
export { createMiddlewareRunner } from "./middleware.js";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Make a server action a value Flight can send to a Client Component.
|
|
97
|
+
*
|
|
98
|
+
* `@uniflowed/vite` calls this in the rsc graph on every callable export of a
|
|
99
|
+
* `"use server"` module, with the action id the build derived for it and its
|
|
100
|
+
* `module#export` name. React's `registerServerReference` marks the function
|
|
101
|
+
* as a server reference, so a Server Component may pass it, or a
|
|
102
|
+
* `.bind(null, …)` of it, to a Client Component as a prop. Flight writes the
|
|
103
|
+
* reference and any bound arguments instead of refusing a function, and the
|
|
104
|
+
* browser decodes it into a call over uf's JSON action wire
|
|
105
|
+
* (`callServerReference` in `./action.js`). See ubugeeei-prod/uf#1359.
|
|
106
|
+
*
|
|
107
|
+
* Calling the function is unchanged, and anything that is not a function is
|
|
108
|
+
* returned untouched. A function that no manifest row registered is still
|
|
109
|
+
* refused by Flight, which is what keeps an ordinary server function from
|
|
110
|
+
* being handed to the browser by accident.
|
|
111
|
+
*/
|
|
112
|
+
export function registerServerFunction<T>(fn: T, id: string, name: string): T {
|
|
113
|
+
if (typeof fn !== "function") {
|
|
114
|
+
return fn;
|
|
115
|
+
}
|
|
116
|
+
registerServerReference(fn, id, name);
|
|
117
|
+
return fn;
|
|
118
|
+
}
|
|
84
119
|
|
|
85
120
|
/** What a host may tell the renderer about one render. */
|
|
86
121
|
export type FlightOptions = {|
|
|
@@ -117,6 +152,16 @@ export type FlightOptions = {|
|
|
|
117
152
|
* render the intercepted tree.
|
|
118
153
|
*/
|
|
119
154
|
readonly interceptedFrom?: string,
|
|
155
|
+
/**
|
|
156
|
+
* Render the URL's not-found page rather than its route: what a loader's
|
|
157
|
+
* `notFound()` would have rendered for it.
|
|
158
|
+
*
|
|
159
|
+
* What a browser asks for when a hydrated server action called `notFound()`
|
|
160
|
+
* and the route's error boundary caught it (ubugeeei-prod/uf#1489). The
|
|
161
|
+
* error is made here, in this graph, because the resolver tells a not-found
|
|
162
|
+
* by `instanceof` against its own copy of the class.
|
|
163
|
+
*/
|
|
164
|
+
readonly notFound?: boolean,
|
|
120
165
|
/** Stops the render, for a reader that went away. */
|
|
121
166
|
readonly signal?: AbortSignal,
|
|
122
167
|
|};
|
|
@@ -126,7 +171,7 @@ export type FlightRender =
|
|
|
126
171
|
| {| readonly kind: "redirect", readonly status: 307 | 308, readonly location: string |}
|
|
127
172
|
| {|
|
|
128
173
|
readonly kind: "route",
|
|
129
|
-
readonly status: 200 | 401 | 403 | 404 | 500,
|
|
174
|
+
readonly status: 200 | 400 | 401 | 403 | 404 | 500,
|
|
130
175
|
/** The payload, as React writes it. Read exactly once. */
|
|
131
176
|
readonly stream: ReadableStream<Uint8Array>,
|
|
132
177
|
/**
|
|
@@ -183,7 +228,8 @@ export function createFlightRenderer(options: {|
|
|
|
183
228
|
return async function renderFlight(url: string, settings?: FlightOptions): Promise<FlightRender> {
|
|
184
229
|
let resolved: ResolvedRoute;
|
|
185
230
|
try {
|
|
186
|
-
const failure =
|
|
231
|
+
const failure =
|
|
232
|
+
settings?.failure ?? (settings?.notFound === true ? { error: new NotFoundError() } : null);
|
|
187
233
|
if (failure == null) {
|
|
188
234
|
const defer = settings?.defer !== false;
|
|
189
235
|
const intercepted = await resolveFlightInterception(
|
|
@@ -296,7 +342,7 @@ function pageElement(route: ResolvedRoute): React.Node {
|
|
|
296
342
|
<DeferredPage
|
|
297
343
|
page={route.page}
|
|
298
344
|
params={route.params}
|
|
299
|
-
searchParams={route
|
|
345
|
+
searchParams={pageSearchParams(route)}
|
|
300
346
|
loader={deferred}
|
|
301
347
|
/>
|
|
302
348
|
);
|
|
@@ -305,19 +351,14 @@ function pageElement(route: ResolvedRoute): React.Node {
|
|
|
305
351
|
<RoutePage
|
|
306
352
|
page={route.page}
|
|
307
353
|
params={route.params}
|
|
308
|
-
searchParams={route
|
|
354
|
+
searchParams={pageSearchParams(route)}
|
|
309
355
|
data={route.data}
|
|
310
356
|
/>
|
|
311
357
|
);
|
|
312
358
|
}
|
|
313
359
|
|
|
314
360
|
/** A route's page, with its loader's answer. */
|
|
315
|
-
component RoutePage(
|
|
316
|
-
page: PageModule,
|
|
317
|
-
params: RouteParams,
|
|
318
|
-
searchParams: SearchParams,
|
|
319
|
-
data: mixed,
|
|
320
|
-
) {
|
|
361
|
+
component RoutePage(page: PageModule, params: RouteParams, searchParams: mixed, data: mixed) {
|
|
321
362
|
const Page = pageComponent(page);
|
|
322
363
|
// The route module's own export, looked up by route: `pageComponent` hands
|
|
323
364
|
// back its `default` or `Page` as it is, so this is the same component on
|
|
@@ -337,7 +378,7 @@ component RoutePage(
|
|
|
337
378
|
component DeferredPage(
|
|
338
379
|
page: PageModule,
|
|
339
380
|
params: RouteParams,
|
|
340
|
-
searchParams:
|
|
381
|
+
searchParams: mixed,
|
|
341
382
|
loader: Promise<mixed>,
|
|
342
383
|
) {
|
|
343
384
|
return <RoutePage page={page} params={params} searchParams={searchParams} data={use(loader)} />;
|
|
@@ -412,6 +453,7 @@ function renderFailure(resolved: ResolvedRoute): mixed {
|
|
|
412
453
|
}
|
|
413
454
|
return match (resolved.error) {
|
|
414
455
|
{kind: "thrown", error: const error} => error,
|
|
456
|
+
{kind: "badRequest", ...} => undefined,
|
|
415
457
|
{kind: "unauthorized"} => undefined,
|
|
416
458
|
{kind: "forbidden"} => undefined,
|
|
417
459
|
};
|
package/server-components.js
CHANGED
|
@@ -37,7 +37,7 @@ import type { Metadata } from "./internal/resolve.js";
|
|
|
37
37
|
import type { RouteInfo, Router } from "./internal/runtime.js";
|
|
38
38
|
import { serverRoute } from "./internal/server-route.js";
|
|
39
39
|
|
|
40
|
-
export type { ErrorProps, LayoutProps, PageProps } from "./index.js";
|
|
40
|
+
export type { ErrorProps, LayoutProps, PageProps, SearchParamsOf } from "./index.js";
|
|
41
41
|
|
|
42
42
|
export type {
|
|
43
43
|
AppProps,
|
|
@@ -81,6 +81,7 @@ export {
|
|
|
81
81
|
ForbiddenError,
|
|
82
82
|
NotFoundError,
|
|
83
83
|
RedirectError,
|
|
84
|
+
SearchParamsError,
|
|
84
85
|
UnauthorizedError,
|
|
85
86
|
buildRoute,
|
|
86
87
|
forbidden,
|
|
@@ -88,6 +89,7 @@ export {
|
|
|
88
89
|
matchRoute,
|
|
89
90
|
notFound,
|
|
90
91
|
parseSearch,
|
|
92
|
+
parseSearchAll,
|
|
91
93
|
permanentRedirect,
|
|
92
94
|
redirect,
|
|
93
95
|
routeErrorStatus,
|
package/server.js
CHANGED
|
@@ -537,6 +537,7 @@ function renderFailure(resolved: ResolvedRoute): mixed {
|
|
|
537
537
|
}
|
|
538
538
|
return match (resolved.error) {
|
|
539
539
|
{kind: "thrown", error: const error} => error,
|
|
540
|
+
{kind: "badRequest", ...} => undefined,
|
|
540
541
|
{kind: "unauthorized"} => undefined,
|
|
541
542
|
{kind: "forbidden"} => undefined,
|
|
542
543
|
};
|