@uniflowed/router 0.0.0-alpha.40 → 0.0.0-alpha.41

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/native.js CHANGED
@@ -28,6 +28,9 @@
28
28
 
29
29
  import type { RouteParams, RouteRecord, RouteTable } from "./internal/routing.js";
30
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";
31
34
 
32
35
  export type NativeNavigationKind = "push" | "replace" | "prefetch";
33
36
 
@@ -221,7 +224,7 @@ export function resolveNativeNavigation(
221
224
  to: string,
222
225
  kind?: NativeNavigationKind = "push",
223
226
  ): NativeNavigationEvent {
224
- const href = normalizeNativeHref(to);
227
+ const href = normalizeNativeHref(to, table);
225
228
  const { pathname, search } = splitUrl(href);
226
229
  const matched = matchRoute(table.routes, pathname);
227
230
  if (matched == null) {
@@ -285,8 +288,13 @@ export function nativeScreenNavigationState(
285
288
  };
286
289
  }
287
290
 
288
- function normalizeNativeHref(to: string): string {
291
+ function normalizeNativeHref(
292
+ to: string,
293
+ table: RouteTable<mixed, mixed, mixed, mixed, mixed>,
294
+ ): string {
289
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;
290
298
  throw new NativeNavigationError(
291
299
  "external-url",
292
300
  `@uniflowed/router/native: ${to} is an external URL, and a native navigator needs an app route`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/router",
3
- "version": "0.0.0-alpha.40",
3
+ "version": "0.0.0-alpha.41",
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",
@@ -25,7 +25,9 @@
25
25
  "./routing": "./routing.js",
26
26
  "./package.json": "./package.json",
27
27
  "./handler": "./handler.js",
28
- "./middleware": "./middleware.js"
28
+ "./middleware": "./middleware.js",
29
+ "./native-navigation": "./native-navigation.js",
30
+ "./http-client": "./http-client.js"
29
31
  },
30
32
  "files": [
31
33
  "action.js",
@@ -41,12 +43,16 @@
41
43
  "rsc.js",
42
44
  "server-components.js",
43
45
  "server.js",
46
+ "native-navigation.js",
47
+ "http-client.js",
44
48
  "!*.test.js"
45
49
  ],
46
50
  "peerDependencies": {
47
51
  "react": ">=19.2.3",
48
52
  "react-dom": ">=19.2.3",
49
- "react-server-dom-parcel": ">=19.3.0"
53
+ "react-server-dom-parcel": ">=19.3.0",
54
+ "react-native": ">=0.86",
55
+ "@react-navigation/native": "^7.0.0"
50
56
  },
51
57
  "peerDependenciesMeta": {
52
58
  "react-dom": {
@@ -54,10 +60,16 @@
54
60
  },
55
61
  "react-server-dom-parcel": {
56
62
  "optional": true
63
+ },
64
+ "react-native": {
65
+ "optional": true
66
+ },
67
+ "@react-navigation/native": {
68
+ "optional": true
57
69
  }
58
70
  },
59
71
  "dependencies": {
60
- "@uniflowed/hooks": "0.0.0-alpha.40",
61
- "@uniflowed/server": "0.0.0-alpha.40"
72
+ "@uniflowed/hooks": "0.0.0-alpha.41",
73
+ "@uniflowed/server": "0.0.0-alpha.41"
62
74
  }
63
75
  }
package/rsc-ssr.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  installServerModules,
28
28
  readPayload,
29
29
  } from "./internal/flight-ssr.js";
30
- import { FLIGHT_CONTENT_TYPE, flightUrl } from "./internal/flight.js";
30
+ import { FLIGHT_CONTENT_TYPE, INTERCEPTED_FROM_HEADER, flightUrl } from "./internal/flight.js";
31
31
  import { type StreamRecord, streamReporter } from "./internal/inspector.js";
32
32
  import { requireServerComponentsReact } from "./internal/react-version.js";
33
33
  import { RedirectError } from "./internal/routing.js";
@@ -312,9 +312,15 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
312
312
 
313
313
  async function flight(
314
314
  url: string,
315
- settings?: {| readonly onError?: (error: mixed) => void |},
315
+ settings?: {|
316
+ readonly onError?: (error: mixed) => void,
317
+ readonly interceptedFrom?: string,
318
+ |},
316
319
  ): Promise<FlightResponse> {
317
- const rendered = await renderFlight(url, { onError: settings?.onError });
320
+ const rendered = await renderFlight(url, {
321
+ onError: settings?.onError,
322
+ interceptedFrom: settings?.interceptedFrom,
323
+ });
318
324
  if (rendered.kind === "redirect") {
319
325
  const { location } = rendered;
320
326
  // A redirect on this origin points at its target's payload, so the
@@ -330,7 +336,7 @@ export function createDocumentRenderer(options: DocumentRendererOptions): Render
330
336
  }
331
337
  return {
332
338
  status: rendered.status,
333
- headers: { "content-type": FLIGHT_CONTENT_TYPE },
339
+ headers: { "content-type": FLIGHT_CONTENT_TYPE, vary: INTERCEPTED_FROM_HEADER },
334
340
  stream: rendered.stream,
335
341
  error: rendered.failure,
336
342
  };
package/rsc.js CHANGED
@@ -59,7 +59,7 @@ import { ErrorRoutePage } from "./internal/error-view.js";
59
59
  import { type FlightRoot, routeState } from "./internal/flight.js";
60
60
  import { requireServerComponentsReact } from "./internal/react-version.js";
61
61
  import type { ErrorModule, PageModule, ResolvedRoute, ResolvedSlot } from "./internal/resolve.js";
62
- import { resolveFailure, resolveMatch } from "./internal/resolve.js";
62
+ import { resolveFailure, resolveInterception, resolveMatch } from "./internal/resolve.js";
63
63
  import type { RouteParams, RouteTable, SearchParams } from "./internal/routing.js";
64
64
  import { RedirectError, nearestBoundary } from "./internal/routing.js";
65
65
  import { withServerRoute } from "./internal/server-route.js";
@@ -95,6 +95,14 @@ export type FlightOptions = {|
95
95
  * did not.
96
96
  */
97
97
  readonly failure?: {| readonly error: mixed |},
98
+ /**
99
+ * The page a browser was showing when it asked for this payload.
100
+ *
101
+ * A document request never sets it. A client navigation may, because only the
102
+ * browser knows the page it is navigating from, while only this renderer can
103
+ * render the intercepted tree.
104
+ */
105
+ readonly interceptedFrom?: string,
98
106
  /** Stops the render, for a reader that went away. */
99
107
  readonly signal?: AbortSignal,
100
108
  |};
@@ -155,12 +163,22 @@ export function createFlightRenderer(options: {|
155
163
  let resolved: ResolvedRoute;
156
164
  try {
157
165
  const failure = settings?.failure;
158
- resolved =
159
- failure == null
160
- ? // `onMatch` records the route pattern on the request before the
161
- // loader runs, so every line the loader logs names its route.
162
- await resolveMatch(table, url, { defer: settings?.defer !== false, onMatch: noteRoute })
163
- : await resolveFailure(table, url, failure.error);
166
+ if (failure == null) {
167
+ const defer = settings?.defer !== false;
168
+ const intercepted = await resolveFlightInterception(
169
+ table,
170
+ url,
171
+ settings?.interceptedFrom,
172
+ defer,
173
+ );
174
+ resolved =
175
+ intercepted ??
176
+ // `onMatch` records the route pattern on the request before the
177
+ // loader runs, so every line the loader logs names its route.
178
+ (await resolveMatch(table, url, { defer, onMatch: noteRoute }));
179
+ } else {
180
+ resolved = await resolveFailure(table, url, failure.error);
181
+ }
164
182
  } catch (error) {
165
183
  if (error instanceof RedirectError) {
166
184
  return { kind: "redirect", status: error.permanent ? 308 : 307, location: error.to };
@@ -196,6 +214,35 @@ export function createFlightRenderer(options: {|
196
214
  };
197
215
  }
198
216
 
217
+ async function resolveFlightInterception(
218
+ table: RouteTable,
219
+ url: string,
220
+ from: ?string,
221
+ defer: boolean,
222
+ ): Promise<?ResolvedRoute> {
223
+ const baseUrl = usableInterceptionBase(from);
224
+ if (baseUrl == null) {
225
+ return null;
226
+ }
227
+ try {
228
+ const base = await resolveMatch(table, baseUrl, { defer });
229
+ return await resolveInterception(table, base, url);
230
+ } catch (error) {
231
+ if (error instanceof RedirectError) {
232
+ return null;
233
+ }
234
+ throw error;
235
+ }
236
+ }
237
+
238
+ function usableInterceptionBase(from: ?string): ?string {
239
+ if (from == null || !from.startsWith("/") || from.startsWith("//")) {
240
+ return null;
241
+ }
242
+ const hash = from.indexOf("#");
243
+ return hash === -1 ? from : from.slice(0, hash);
244
+ }
245
+
199
246
  /**
200
247
  * The page element: the route's page with its data, the deferred page that
201
248
  * waits for its loader, or the error route's page.
@@ -71,7 +71,7 @@ export type {
71
71
  TwitterCard,
72
72
  } from "./internal/runtime.js";
73
73
 
74
- export { Link, RouteView, RouterProvider, routerView } from "./internal/runtime.js";
74
+ export { Link, RouteView, RouterProvider, routerView, useLinkStatus } from "./internal/runtime.js";
75
75
  // `app.router.basePath`, for a Server Component that builds an address itself.
76
76
  // The rsc entry installs it; `./internal/base-path.js` has no directive, so this
77
77
  // graph gets the function rather than a reference to it.
package/server.js CHANGED
@@ -23,7 +23,7 @@
23
23
  // `internal/stream.js` holds the mechanics and says which React renderer serves
24
24
  // which.
25
25
 
26
- import { noteRoute } from "@uniflowed/server/host";
26
+ import { currentNonce, noteRoute } from "@uniflowed/server/host";
27
27
  import * as React from "react";
28
28
 
29
29
  import {
@@ -44,7 +44,6 @@ import {
44
44
  } from "./internal/runtime.js";
45
45
 
46
46
  import { type StreamDiagnostic, streamReporter } from "./internal/inspector.js";
47
- import { currentNonce } from "@uniflowed/server/host";
48
47
 
49
48
  import { redirectDocument, redirectResult, shellFor } from "./internal/shell.js";
50
49
 
@@ -288,7 +287,10 @@ export type Renderer = {|
288
287
  */
289
288
  readonly flight?: (
290
289
  url: string,
291
- options?: {| readonly onError?: (error: mixed) => void |},
290
+ options?: {|
291
+ readonly onError?: (error: mixed) => void,
292
+ readonly interceptedFrom?: string,
293
+ |},
292
294
  ) => Promise<FlightResponse>,
293
295
  |};
294
296