@uniflowed/router 0.5.0 → 0.6.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/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 { FLIGHT_CONTENT_TYPE, INTERCEPTED_FROM_HEADER, flightUrl } from "./internal/flight.js";
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 { resolveFailure, resolveInterception, resolveMatch } from "./internal/resolve.js";
72
- import type { RouteParams, SearchParams } from "./internal/routing.js";
73
- import { RedirectError, nearestBoundary } from "./internal/routing.js";
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 = settings?.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.searchParams}
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.searchParams}
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: 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
  };
@@ -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
  };