@uniflowed/router 0.0.0-alpha.40 → 0.0.0-alpha.42
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/instrumentation.js +92 -0
- package/internal/action-endpoint.js +7 -3
- 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 +11 -9
- package/internal/routing.js +4 -0
- package/internal/runtime.js +144 -41
- package/internal/server-instrumentation.js +12 -0
- package/internal/stream.js +24 -9
- package/native-navigation.js +217 -0
- package/native.js +10 -2
- package/package.json +19 -5
- package/rsc-ssr.js +10 -4
- package/rsc.js +70 -9
- package/server-components.js +1 -1
- package/server.js +18 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Pressable } from "react-native";
|
|
4
|
+
import {
|
|
5
|
+
CommonActions,
|
|
6
|
+
NavigationContainer,
|
|
7
|
+
StackActions,
|
|
8
|
+
createNavigationContainerRef,
|
|
9
|
+
useRoute,
|
|
10
|
+
} from "@react-navigation/native";
|
|
11
|
+
import { resolveNativeNavigation, createNativeRouter, createNativeLinking } from "./native.js";
|
|
12
|
+
import type { NativeLinkSource } from "./native.js";
|
|
13
|
+
import type { RouteTable } from "./internal/routing.js";
|
|
14
|
+
import type {
|
|
15
|
+
NativeLayout,
|
|
16
|
+
NativeNode,
|
|
17
|
+
NativeTree,
|
|
18
|
+
NavigationPayload,
|
|
19
|
+
NavigationState,
|
|
20
|
+
} from "./internal/native-tree.js";
|
|
21
|
+
import { nativeTree, stateForPath, hrefFromState, paramsForPath } from "./internal/native-tree.js";
|
|
22
|
+
|
|
23
|
+
export interface NavigatorPair {
|
|
24
|
+
readonly Navigator: React.ComponentType<{ ... }>;
|
|
25
|
+
readonly Screen: React.ComponentType<{ ... }>;
|
|
26
|
+
}
|
|
27
|
+
export type FileRouter = {
|
|
28
|
+
readonly push: (href: string) => void,
|
|
29
|
+
readonly replace: (href: string) => void,
|
|
30
|
+
readonly back: () => void,
|
|
31
|
+
readonly prefetch: (href: string) => Promise<void>,
|
|
32
|
+
};
|
|
33
|
+
type Scope = {
|
|
34
|
+
tree: NativeTree,
|
|
35
|
+
node: NativeNode,
|
|
36
|
+
screens: Map<NativeNode, React.ComponentType<{ ... }>>,
|
|
37
|
+
stack: NavigatorPair,
|
|
38
|
+
tabs: ?NavigatorPair,
|
|
39
|
+
router: FileRouter,
|
|
40
|
+
};
|
|
41
|
+
const ScopeContext: React.Context<Scope | null> = React.createContext(null);
|
|
42
|
+
|
|
43
|
+
function useScope(): Scope {
|
|
44
|
+
const scope = React.useContext(ScopeContext);
|
|
45
|
+
if (scope == null)
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Native navigation: mount createNativeNavigation().Root before using its navigator or hooks",
|
|
48
|
+
);
|
|
49
|
+
return scope;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The application supplies its own React Navigation navigator factories. */
|
|
53
|
+
export function createNativeNavigation(options: {
|
|
54
|
+
table: RouteTable<>,
|
|
55
|
+
layouts: $ReadOnlyArray<NativeLayout>,
|
|
56
|
+
stack: NavigatorPair,
|
|
57
|
+
tabs?: NavigatorPair,
|
|
58
|
+
initialHref?: string,
|
|
59
|
+
links?: NativeLinkSource,
|
|
60
|
+
}): { Root: React.ComponentType<{ ... }>, router: FileRouter, linking: { ... } } {
|
|
61
|
+
const { table, layouts, stack, tabs } = options;
|
|
62
|
+
const tree = nativeTree(table, layouts);
|
|
63
|
+
const ref = createNavigationContainerRef();
|
|
64
|
+
const screens = new Map<NativeNode, React.ComponentType<{ ... }>>();
|
|
65
|
+
const resolve = (href: string): { names: $ReadOnlyArray<string>, payload: NavigationPayload } => {
|
|
66
|
+
const event = resolveNativeNavigation(table, href);
|
|
67
|
+
const names = tree.paths.get(event.route);
|
|
68
|
+
if (names == null) throw new Error(`Native navigation: no screen for ${event.route}`);
|
|
69
|
+
return { names, payload: { ufHref: event.href, ufParams: event.params } };
|
|
70
|
+
};
|
|
71
|
+
function actionFor(
|
|
72
|
+
names: $ReadOnlyArray<string>,
|
|
73
|
+
payload: NavigationPayload,
|
|
74
|
+
kind: "push" | "replace",
|
|
75
|
+
): { readonly [string]: mixed } {
|
|
76
|
+
if (!ref.isReady()) throw new Error("Native navigation: NavigationContainer is not ready");
|
|
77
|
+
let state: NavigationState = ref.getRootState();
|
|
78
|
+
let depth = 0;
|
|
79
|
+
while (depth < names.length - 1) {
|
|
80
|
+
const active = state.routes[state.index ?? 0];
|
|
81
|
+
if (active?.name !== names[depth] || active?.state == null) break;
|
|
82
|
+
state = active.state;
|
|
83
|
+
depth += 1;
|
|
84
|
+
}
|
|
85
|
+
const name = names[depth];
|
|
86
|
+
const params = paramsForPath(names.slice(depth + 1), payload);
|
|
87
|
+
const action =
|
|
88
|
+
state.type === "stack"
|
|
89
|
+
? StackActions[kind](name, params)
|
|
90
|
+
: CommonActions.navigate({ name, params });
|
|
91
|
+
return { ...action, target: state.key };
|
|
92
|
+
}
|
|
93
|
+
function navigate(href: string, kind: "push" | "replace"): void {
|
|
94
|
+
const { names, payload } = resolve(href);
|
|
95
|
+
ref.dispatch(actionFor(names, payload, kind));
|
|
96
|
+
}
|
|
97
|
+
const router: FileRouter = {
|
|
98
|
+
push: (href) => navigate(href, "push"),
|
|
99
|
+
replace: (href) => navigate(href, "replace"),
|
|
100
|
+
back: () => {
|
|
101
|
+
if (ref.canGoBack()) ref.goBack();
|
|
102
|
+
},
|
|
103
|
+
prefetch: createNativeRouter(table, {}).prefetch,
|
|
104
|
+
};
|
|
105
|
+
const source = options.links == null ? null : createNativeLinking(table, options.links);
|
|
106
|
+
const linking = {
|
|
107
|
+
prefixes: [""],
|
|
108
|
+
getInitialURL: source?.getInitialURL ?? (async () => null),
|
|
109
|
+
subscribe: source?.subscribe ?? (() => () => {}),
|
|
110
|
+
getStateFromPath: (href: string) => {
|
|
111
|
+
const { names, payload } = resolve(
|
|
112
|
+
href.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(href) ? href : `/${href}`,
|
|
113
|
+
);
|
|
114
|
+
return stateForPath(names, payload);
|
|
115
|
+
},
|
|
116
|
+
getPathFromState: (state: NavigationState) => hrefFromState(state) ?? "/",
|
|
117
|
+
getActionFromState: (state: NavigationState) => {
|
|
118
|
+
const href = hrefFromState(state);
|
|
119
|
+
if (href == null) return undefined;
|
|
120
|
+
const { names, payload } = resolve(href);
|
|
121
|
+
return actionFor(names, payload, "push");
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
function build(node: NativeNode): React.ComponentType<{ ... }> {
|
|
125
|
+
const load = node.module;
|
|
126
|
+
const Content =
|
|
127
|
+
load == null
|
|
128
|
+
? Stack
|
|
129
|
+
: React.lazy(async () => {
|
|
130
|
+
const module: $FlowFixMe = await load();
|
|
131
|
+
const Component = module.default ?? (node.layout ? module.Layout : module.Page);
|
|
132
|
+
if (
|
|
133
|
+
typeof Component !== "function" &&
|
|
134
|
+
(Component == null || typeof Component !== "object")
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Native navigation: ${node.name} must export ${node.layout ? "Layout" : "Page"} or default`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return { default: Component };
|
|
141
|
+
});
|
|
142
|
+
function Screen(props: { ... }): React.Node {
|
|
143
|
+
return (
|
|
144
|
+
<ScopeContext value={{ tree, node, screens, stack, tabs, router }}>
|
|
145
|
+
<React.Suspense fallback={null}>
|
|
146
|
+
<Content {...props} />
|
|
147
|
+
</React.Suspense>
|
|
148
|
+
</ScopeContext>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
screens.set(node, Screen);
|
|
152
|
+
for (const child of node.children) build(child);
|
|
153
|
+
return Screen;
|
|
154
|
+
}
|
|
155
|
+
const RootScreen = build(tree.root);
|
|
156
|
+
const initialState =
|
|
157
|
+
options.initialHref == null ? undefined : linking.getStateFromPath(options.initialHref);
|
|
158
|
+
function Root(props: { ... }): React.Node {
|
|
159
|
+
return (
|
|
160
|
+
<NavigationContainer {...props} ref={ref} linking={linking} initialState={initialState}>
|
|
161
|
+
<RootScreen />
|
|
162
|
+
</NavigationContainer>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return { Root, router, linking };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Any React Navigation navigator can consume this segment's direct children. */
|
|
169
|
+
export component Navigator(navigator: NavigatorPair, ...props: { ... }) {
|
|
170
|
+
const scope = useScope();
|
|
171
|
+
const Container = navigator.Navigator;
|
|
172
|
+
const Screen = navigator.Screen;
|
|
173
|
+
return (
|
|
174
|
+
<Container {...props}>
|
|
175
|
+
{scope.node.children.map((node) => (
|
|
176
|
+
<Screen key={node.name} name={node.name} component={scope.screens.get(node)} />
|
|
177
|
+
))}
|
|
178
|
+
</Container>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export component Stack(...props: { ... }) {
|
|
183
|
+
return <Navigator {...props} navigator={useScope().stack} />;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export component Tabs(...props: { ... }) {
|
|
187
|
+
const tabs = useScope().tabs;
|
|
188
|
+
if (tabs == null)
|
|
189
|
+
throw new Error("Native navigation: supply the app's tab navigator to createNativeNavigation");
|
|
190
|
+
return <Navigator {...props} navigator={tabs} />;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function useNativeRouter(): FileRouter {
|
|
194
|
+
return useScope().router;
|
|
195
|
+
}
|
|
196
|
+
export function useParams(): { readonly [string]: string | $ReadOnlyArray<string> } {
|
|
197
|
+
const route = useRoute();
|
|
198
|
+
return route.params?.ufParams ?? {};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export component Link(
|
|
202
|
+
href: string,
|
|
203
|
+
children: React.Node,
|
|
204
|
+
replace: boolean = false,
|
|
205
|
+
...props: { ... }
|
|
206
|
+
) {
|
|
207
|
+
const router = useNativeRouter();
|
|
208
|
+
return (
|
|
209
|
+
<Pressable
|
|
210
|
+
{...props}
|
|
211
|
+
accessibilityRole="link"
|
|
212
|
+
onPress={() => (replace ? router.replace(href) : router.push(href))}
|
|
213
|
+
>
|
|
214
|
+
{children}
|
|
215
|
+
</Pressable>
|
|
216
|
+
);
|
|
217
|
+
}
|
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.42",
|
|
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,10 @@
|
|
|
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",
|
|
31
|
+
"./instrumentation": "./instrumentation.js"
|
|
29
32
|
},
|
|
30
33
|
"files": [
|
|
31
34
|
"action.js",
|
|
@@ -41,12 +44,17 @@
|
|
|
41
44
|
"rsc.js",
|
|
42
45
|
"server-components.js",
|
|
43
46
|
"server.js",
|
|
47
|
+
"native-navigation.js",
|
|
48
|
+
"http-client.js",
|
|
49
|
+
"instrumentation.js",
|
|
44
50
|
"!*.test.js"
|
|
45
51
|
],
|
|
46
52
|
"peerDependencies": {
|
|
47
53
|
"react": ">=19.2.3",
|
|
48
54
|
"react-dom": ">=19.2.3",
|
|
49
|
-
"react-server-dom-parcel": ">=19.3.0"
|
|
55
|
+
"react-server-dom-parcel": ">=19.3.0",
|
|
56
|
+
"react-native": ">=0.86",
|
|
57
|
+
"@react-navigation/native": "^7.0.0"
|
|
50
58
|
},
|
|
51
59
|
"peerDependenciesMeta": {
|
|
52
60
|
"react-dom": {
|
|
@@ -54,10 +62,16 @@
|
|
|
54
62
|
},
|
|
55
63
|
"react-server-dom-parcel": {
|
|
56
64
|
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"react-native": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@react-navigation/native": {
|
|
70
|
+
"optional": true
|
|
57
71
|
}
|
|
58
72
|
},
|
|
59
73
|
"dependencies": {
|
|
60
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
61
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
74
|
+
"@uniflowed/hooks": "0.0.0-alpha.42",
|
|
75
|
+
"@uniflowed/server": "0.0.0-alpha.42"
|
|
62
76
|
}
|
|
63
77
|
}
|
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
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
// in this graph, and a boundary module without the directive is refused here,
|
|
45
45
|
// with its file named, rather than by React with a sentence about functions.
|
|
46
46
|
|
|
47
|
+
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
48
|
+
|
|
47
49
|
import * as React from "react";
|
|
48
50
|
import { use } from "react";
|
|
49
51
|
// React's Flight server. Only this graph can load it: it refuses to evaluate
|
|
@@ -51,6 +53,7 @@ import { use } from "react";
|
|
|
51
53
|
import { renderToReadableStream } from "react-server-dom-parcel/server";
|
|
52
54
|
|
|
53
55
|
import { noteRoute } from "@uniflowed/server/host";
|
|
56
|
+
import { reportRequestError } from "@uniflowed/server/instrumentation";
|
|
54
57
|
|
|
55
58
|
import { BoundaryReporter } from "./internal/boundaries.js";
|
|
56
59
|
import { routeBoundaries } from "./internal/boundary-data.js";
|
|
@@ -59,7 +62,7 @@ import { ErrorRoutePage } from "./internal/error-view.js";
|
|
|
59
62
|
import { type FlightRoot, routeState } from "./internal/flight.js";
|
|
60
63
|
import { requireServerComponentsReact } from "./internal/react-version.js";
|
|
61
64
|
import type { ErrorModule, PageModule, ResolvedRoute, ResolvedSlot } from "./internal/resolve.js";
|
|
62
|
-
import { resolveFailure, resolveMatch } from "./internal/resolve.js";
|
|
65
|
+
import { resolveFailure, resolveInterception, resolveMatch } from "./internal/resolve.js";
|
|
63
66
|
import type { RouteParams, RouteTable, SearchParams } from "./internal/routing.js";
|
|
64
67
|
import { RedirectError, nearestBoundary } from "./internal/routing.js";
|
|
65
68
|
import { withServerRoute } from "./internal/server-route.js";
|
|
@@ -95,6 +98,14 @@ export type FlightOptions = {|
|
|
|
95
98
|
* did not.
|
|
96
99
|
*/
|
|
97
100
|
readonly failure?: {| readonly error: mixed |},
|
|
101
|
+
/**
|
|
102
|
+
* The page a browser was showing when it asked for this payload.
|
|
103
|
+
*
|
|
104
|
+
* A document request never sets it. A client navigation may, because only the
|
|
105
|
+
* browser knows the page it is navigating from, while only this renderer can
|
|
106
|
+
* render the intercepted tree.
|
|
107
|
+
*/
|
|
108
|
+
readonly interceptedFrom?: string,
|
|
98
109
|
/** Stops the render, for a reader that went away. */
|
|
99
110
|
readonly signal?: AbortSignal,
|
|
100
111
|
|};
|
|
@@ -155,12 +166,22 @@ export function createFlightRenderer(options: {|
|
|
|
155
166
|
let resolved: ResolvedRoute;
|
|
156
167
|
try {
|
|
157
168
|
const failure = settings?.failure;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
if (failure == null) {
|
|
170
|
+
const defer = settings?.defer !== false;
|
|
171
|
+
const intercepted = await resolveFlightInterception(
|
|
172
|
+
table,
|
|
173
|
+
url,
|
|
174
|
+
settings?.interceptedFrom,
|
|
175
|
+
defer,
|
|
176
|
+
);
|
|
177
|
+
resolved =
|
|
178
|
+
intercepted ??
|
|
179
|
+
// `onMatch` records the route pattern on the request before the
|
|
180
|
+
// loader runs, so every line the loader logs names its route.
|
|
181
|
+
(await resolveMatch(table, url, { defer, onMatch: noteRoute, runLoader: traceLoader }));
|
|
182
|
+
} else {
|
|
183
|
+
resolved = await resolveFailure(table, url, failure.error);
|
|
184
|
+
}
|
|
164
185
|
} catch (error) {
|
|
165
186
|
if (error instanceof RedirectError) {
|
|
166
187
|
return { kind: "redirect", status: error.permanent ? 308 : 307, location: error.to };
|
|
@@ -181,13 +202,20 @@ export function createFlightRenderer(options: {|
|
|
|
181
202
|
</>
|
|
182
203
|
);
|
|
183
204
|
const root: FlightRoot = { route: state, tree };
|
|
184
|
-
const
|
|
205
|
+
const failure = renderFailure(route);
|
|
206
|
+
if (failure != null) reportRequestError(failure, "render");
|
|
207
|
+
const report = (error) => {
|
|
208
|
+
reportRequestError(error, "render");
|
|
209
|
+
if (settings?.onError != null) return settings.onError(error);
|
|
210
|
+
console.error(error);
|
|
211
|
+
return undefined;
|
|
212
|
+
};
|
|
185
213
|
// Inside the route's store, so a server component's `useRoute()` finds the
|
|
186
214
|
// route however many `await`s into the render it asks.
|
|
187
215
|
const stream = withServerRoute(state, () =>
|
|
188
216
|
renderToReadableStream(root, {
|
|
189
217
|
// Handed over as it is, so that the digest it returns reaches the row.
|
|
190
|
-
//
|
|
218
|
+
// Our callback preserves React's console fallback when none was given.
|
|
191
219
|
onError: report,
|
|
192
220
|
signal: settings?.signal,
|
|
193
221
|
}),
|
|
@@ -196,6 +224,39 @@ export function createFlightRenderer(options: {|
|
|
|
196
224
|
};
|
|
197
225
|
}
|
|
198
226
|
|
|
227
|
+
async function resolveFlightInterception(
|
|
228
|
+
table: RouteTable,
|
|
229
|
+
url: string,
|
|
230
|
+
from: ?string,
|
|
231
|
+
defer: boolean,
|
|
232
|
+
): Promise<?ResolvedRoute> {
|
|
233
|
+
const baseUrl = usableInterceptionBase(from);
|
|
234
|
+
if (baseUrl == null) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const base = await resolveMatch(table, baseUrl, {
|
|
239
|
+
defer,
|
|
240
|
+
onMatch: noteRoute,
|
|
241
|
+
runLoader: traceLoader,
|
|
242
|
+
});
|
|
243
|
+
return await resolveInterception(table, base, url);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof RedirectError) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function usableInterceptionBase(from: ?string): ?string {
|
|
253
|
+
if (from == null || !from.startsWith("/") || from.startsWith("//")) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const hash = from.indexOf("#");
|
|
257
|
+
return hash === -1 ? from : from.slice(0, hash);
|
|
258
|
+
}
|
|
259
|
+
|
|
199
260
|
/**
|
|
200
261
|
* The page element: the route's page with its data, the deferred page that
|
|
201
262
|
* 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,9 @@
|
|
|
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
|
+
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
28
|
+
|
|
27
29
|
import * as React from "react";
|
|
28
30
|
|
|
29
31
|
import {
|
|
@@ -44,7 +46,6 @@ import {
|
|
|
44
46
|
} from "./internal/runtime.js";
|
|
45
47
|
|
|
46
48
|
import { type StreamDiagnostic, streamReporter } from "./internal/inspector.js";
|
|
47
|
-
import { currentNonce } from "@uniflowed/server/host";
|
|
48
49
|
|
|
49
50
|
import { redirectDocument, redirectResult, shellFor } from "./internal/shell.js";
|
|
50
51
|
|
|
@@ -288,7 +289,10 @@ export type Renderer = {|
|
|
|
288
289
|
*/
|
|
289
290
|
readonly flight?: (
|
|
290
291
|
url: string,
|
|
291
|
-
options?: {|
|
|
292
|
+
options?: {|
|
|
293
|
+
readonly onError?: (error: mixed) => void,
|
|
294
|
+
readonly interceptedFrom?: string,
|
|
295
|
+
|},
|
|
292
296
|
) => Promise<FlightResponse>,
|
|
293
297
|
|};
|
|
294
298
|
|
|
@@ -333,7 +337,11 @@ export function createRenderer(options: {|
|
|
|
333
337
|
try {
|
|
334
338
|
return {
|
|
335
339
|
kind: "route",
|
|
336
|
-
route: await resolveMatch(table, url, {
|
|
340
|
+
route: await resolveMatch(table, url, {
|
|
341
|
+
defer,
|
|
342
|
+
onMatch: noteRoute,
|
|
343
|
+
runLoader: traceLoader,
|
|
344
|
+
}),
|
|
337
345
|
};
|
|
338
346
|
} catch (error) {
|
|
339
347
|
if (error instanceof RedirectError) {
|
|
@@ -510,3 +518,9 @@ export function shellDocument(assets: RenderAssets): string {
|
|
|
510
518
|
const shell = shellFor(assets);
|
|
511
519
|
return `${shell.open}${shell.body}${shell.close}`;
|
|
512
520
|
}
|
|
521
|
+
|
|
522
|
+
export {
|
|
523
|
+
createInstrumentation,
|
|
524
|
+
instrumentRender,
|
|
525
|
+
traceRequestPhase,
|
|
526
|
+
} from "@uniflowed/server/instrumentation";
|