@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/handler.js +42 -105
- package/http-client.js +104 -0
- package/index.js +1 -0
- package/internal/action-endpoint.js +3 -2
- package/internal/flight-browser.js +12 -2
- package/internal/flight.js +25 -0
- package/internal/native-links.js +67 -0
- package/internal/native-tree.js +89 -0
- package/internal/navigation-cache.js +37 -0
- package/internal/resolve.js +6 -8
- package/internal/routing.js +4 -0
- package/internal/runtime.js +136 -39
- package/internal/stream.js +24 -9
- package/native-navigation.js +217 -0
- package/native.js +10 -2
- package/package.json +17 -5
- package/rsc-ssr.js +10 -4
- package/rsc.js +54 -7
- package/server-components.js +1 -1
- package/server.js +5 -3
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(
|
|
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.
|
|
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.
|
|
61
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
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?: {|
|
|
315
|
+
settings?: {|
|
|
316
|
+
readonly onError?: (error: mixed) => void,
|
|
317
|
+
readonly interceptedFrom?: string,
|
|
318
|
+
|},
|
|
316
319
|
): Promise<FlightResponse> {
|
|
317
|
-
const rendered = await renderFlight(url, {
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
package/server-components.js
CHANGED
|
@@ -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?: {|
|
|
290
|
+
options?: {|
|
|
291
|
+
readonly onError?: (error: mixed) => void,
|
|
292
|
+
readonly interceptedFrom?: string,
|
|
293
|
+
|},
|
|
292
294
|
) => Promise<FlightResponse>,
|
|
293
295
|
|};
|
|
294
296
|
|