@uniflowed/router 0.0.0-alpha.4 → 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.
Files changed (46) hide show
  1. package/action.js +301 -0
  2. package/client.js +295 -8
  3. package/handler.js +127 -106
  4. package/http-client.js +104 -0
  5. package/index.js +72 -4
  6. package/internal/action-endpoint.js +435 -0
  7. package/internal/action-wire.js +608 -0
  8. package/internal/base-path.js +175 -0
  9. package/internal/boundaries.js +481 -0
  10. package/internal/boundary-data.js +88 -0
  11. package/internal/compose.js +490 -0
  12. package/internal/devtools.js +131 -0
  13. package/internal/diagnostics.js +169 -0
  14. package/internal/error-view.js +193 -0
  15. package/internal/flight-browser.js +238 -0
  16. package/internal/flight-chunks.js +205 -0
  17. package/internal/flight-ssr.js +78 -0
  18. package/internal/flight.js +183 -0
  19. package/internal/head.js +219 -0
  20. package/internal/hydration.js +1085 -0
  21. package/internal/inspector.js +626 -0
  22. package/internal/native-links.js +67 -0
  23. package/internal/native-tree.js +89 -0
  24. package/internal/navigation-cache.js +181 -0
  25. package/internal/payload-rows.js +270 -0
  26. package/internal/payload.js +685 -0
  27. package/internal/prepare-document.js +49 -0
  28. package/internal/react-version.js +77 -0
  29. package/internal/request.js +43 -0
  30. package/internal/resolve.js +1613 -0
  31. package/internal/resolved-summary.js +199 -0
  32. package/internal/routing.js +478 -0
  33. package/internal/runtime.js +1613 -548
  34. package/internal/server-route.js +58 -0
  35. package/internal/shell.js +115 -0
  36. package/internal/stream.js +1099 -0
  37. package/middleware.js +350 -0
  38. package/native-navigation.js +217 -0
  39. package/native.js +416 -0
  40. package/package.json +48 -7
  41. package/routing.js +51 -0
  42. package/rsc-client.js +120 -0
  43. package/rsc-ssr.js +446 -0
  44. package/rsc.js +381 -0
  45. package/server-components.js +159 -0
  46. package/server.js +450 -75
package/middleware.js ADDED
@@ -0,0 +1,350 @@
1
+ // @flow
2
+ //
3
+ // Middleware: what runs before a path answers, whatever answers it.
4
+ //
5
+ // `app/dashboard/$middleware.js` guards `/dashboard` and everything under
6
+ // it — the pages, the route handlers, and the paths under it that match
7
+ // nothing at all. There is no `matcher` to write because the directory the
8
+ // file sits in *is* the matcher, which is the same composition rule layouts
9
+ // already use and the reason uf does not inherit Next's regular expressions.
10
+ //
11
+ // // app/dashboard/$middleware.js
12
+ // // @flow
13
+ // import { cookies } from "@uniflowed/server";
14
+ //
15
+ // export default function middleware(request: Request): Response | void {
16
+ // if (cookies().get("session") == null) {
17
+ // return Response.redirect(new URL("/sign-in", request.url), 302);
18
+ // }
19
+ // }
20
+ //
21
+ // # The signature, and what it deliberately does not say
22
+ //
23
+ // `(request, context) => Response | void`. Returning a `Response` *is* the
24
+ // answer: nothing after it runs, no page is resolved and no handler is called.
25
+ // Returning nothing continues to the next middleware and then to whatever the
26
+ // path would otherwise have done. Both halves are load-bearing — a middleware
27
+ // that could only observe would not be able to reject, and one that had to
28
+ // answer could not be a logger.
29
+ //
30
+ // There is no `next()`. A third answer is `rewrite(destination)`: serve another
31
+ // route of this application at the address the visitor asked for.
32
+ //
33
+ // // app/$middleware.js
34
+ // import { rewrite } from "@uniflowed/router/middleware";
35
+ //
36
+ // export default function middleware(request: Request) {
37
+ // if (cookies().get("beta") != null) return rewrite("/beta" + new URL(request.url).pathname);
38
+ // }
39
+ //
40
+ // A returned value rather than a returned `Request`, which was the other
41
+ // spelling on the table. A `Request` could change the method and the headers
42
+ // too, and `headers()` reads the request the host began — so a middleware that
43
+ // added a header would have handed the page one set of headers and `headers()`
44
+ // another. A rewrite changes the path, the query when it names one, and
45
+ // nothing else.
46
+ //
47
+ // # A rewrite runs the destination's middleware
48
+ //
49
+ // The chain starts again from the root over the middleware that has not run
50
+ // yet, against the new path. So a rewrite into `/admin` passes the guard on
51
+ // `/admin` exactly as a request for it would, and is never an unguarded way to
52
+ // a guarded page; a middleware that already ran for this request does not run
53
+ // a second time, which is also what makes the loop finite.
54
+ //
55
+ // # A payload request is its document
56
+ //
57
+ // A browser navigating a React Server Components application asks for
58
+ // `/pricing/__uf.flight` rather than `/pricing`. The chain is matched against,
59
+ // and every middleware is handed, the document's URL — so the check a
60
+ // middleware writes against `/pricing` holds for a client navigation too, and
61
+ // a rewrite of the document becomes a rewrite of its payload.
62
+ //
63
+ // # The request it runs inside
64
+ //
65
+ // The runner does not establish one. The host does — `beginRequest` in
66
+ // `@uniflowed/server/host`, once per request, around everything that answers
67
+ // it — and the guard, the handler or page underneath it, and the render all
68
+ // see that one context. So `cookies()` in a guard and `cookies()` in the page
69
+ // it guards are the same cookies, `draftMode().isEnabled` gives a guard and
70
+ // the page under it the same answer, and every `after()` on the request is one
71
+ // ordered list the host drains after the response has gone.
72
+ //
73
+ // *Changing* draft mode is not a guard's to do, and that is a separate rule
74
+ // with a separate reason: `enable()` writes a cookie, a cookie is part of a
75
+ // response, and a guard may decline — so a guard that turned draft mode on and
76
+ // then let the request through would have made a decision with nowhere to be
77
+ // written. `asResponder` marks the two calls that do own a response, a route
78
+ // handler and a server action, and `draftMode().enable()` refuses anywhere
79
+ // else by name. A guard that wants draft mode on answers with a redirect to
80
+ // the handler that turns it on. See ubugeeei-prod/uf#282.
81
+ //
82
+ // It used to be the other way, and it is worth saying why that was wrong
83
+ // rather than merely different: this module built its own context and drained
84
+ // it before returning, which is early in both outcomes. When the chain
85
+ // answered, the caller was several lines from writing a byte; when it
86
+ // declined, there was no response at all yet and the dispatcher below was
87
+ // about to build a second context nothing here could see. `after()` says "once
88
+ // the response has been sent". See ubugeeei-prod/uf#389.
89
+ //
90
+ // # Server only
91
+ //
92
+ // This module is imported by `virtual:uf/server` and by nothing the browser
93
+ // loads. That is not decoration: a middleware is where an application puts the
94
+ // check it does not want a user to be able to read, and the client entry
95
+ // importing the table it lives in would ship every one of them to the page.
96
+ // `routesModuleSource` keeps the middleware table in an export the client
97
+ // never imports, for the same reason it does that with route handlers.
98
+
99
+ import { documentPathOf, flightUrl } from "./internal/flight.js";
100
+ import { requireRequest } from "./internal/request.js";
101
+ import type { RouteParams } from "./internal/runtime.js";
102
+
103
+ /** What a middleware is given besides the request. */
104
+ export type MiddlewareContext = {|
105
+ /** The `[param]` segments of the *directory the middleware guards*. */
106
+ readonly params: RouteParams,
107
+ /** The parsed query string, for the common case of reading one value. */
108
+ readonly searchParams: URLSearchParams,
109
+ |};
110
+
111
+ /**
112
+ * What a middleware returns to serve another route at the requested address.
113
+ *
114
+ * Built by [`rewrite`] and read by the runner; a class so that the runner can
115
+ * tell it from a `Response` without trusting the shape of an object.
116
+ */
117
+ export class Rewrite {
118
+ readonly destination: string;
119
+
120
+ constructor(destination: string) {
121
+ this.destination = destination;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Serve `destination` — a path of this application — in place of the path the
127
+ * request named.
128
+ *
129
+ * Relative to the request, so `"/beta/pricing"` and `"../pricing"` both work.
130
+ * A destination that names no query keeps the request's; one that names a
131
+ * query replaces it. Another origin is refused when the middleware returns it:
132
+ * sending a visitor elsewhere is `Response.redirect`, and proxying to another
133
+ * server is a route handler that fetches.
134
+ */
135
+ export function rewrite(destination: string | URL): Rewrite {
136
+ return new Rewrite(typeof destination === "string" ? destination : destination.href);
137
+ }
138
+
139
+ /** One middleware function. */
140
+ export type Middleware = (
141
+ request: Request,
142
+ context: MiddlewareContext,
143
+ ) => Response | Rewrite | void | Promise<Response | Rewrite | void>;
144
+
145
+ /** A middleware module, as the generated table loads it. */
146
+ export type MiddlewareModule = { readonly [name: string]: mixed };
147
+
148
+ /** One entry of the generated middleware table. */
149
+ export type MiddlewareRecord = {|
150
+ /** The route path of the directory this middleware guards, `/` at the root. */
151
+ readonly path: string,
152
+ readonly file: string,
153
+ readonly load: () => Promise<MiddlewareModule>,
154
+ |};
155
+
156
+ /**
157
+ * Build the middleware runner for one application.
158
+ *
159
+ * Returns `null` when every middleware on the path declined, which is the
160
+ * caller's signal to carry on to the handler or the page. Returns a `Request`
161
+ * when one of them rewrote: the same request at the destination, which the
162
+ * caller carries on with instead — and which has already been past the
163
+ * destination's middleware.
164
+ *
165
+ * The runner is called once per request, above both the dispatcher and the
166
+ * renderer, rather than from inside each of them. Putting the call inside
167
+ * `createDispatcher` and again inside `createRenderer` was the first shape and
168
+ * it is wrong twice over: a request that matches neither — `/dashboard/typo`,
169
+ * which is a 404 — would have run no middleware at all, and a path that is
170
+ * both a page and a handler would have run it twice. Middleware is a property
171
+ * of the request, so it belongs where the request arrives.
172
+ */
173
+ export function createMiddlewareRunner(options: {|
174
+ readonly middleware: $ReadOnlyArray<MiddlewareRecord>,
175
+ |}): (request: Request) => Promise<Response | Request | null> {
176
+ // Root first, so an application-wide check runs before the one that guards a
177
+ // section of it. A shorter path is always an ancestor of a longer one that
178
+ // also matched, so segment count is the whole of the ordering.
179
+ const table = [...options.middleware].sort(
180
+ (a, b) => segmentsOf(a.path).length - segmentsOf(b.path).length,
181
+ );
182
+
183
+ return async function runMiddleware(request: Request): Promise<Response | Request | null> {
184
+ // Checked rather than assumed, and checked before the table so that a host
185
+ // is caught on its first request whether or not this project happens to
186
+ // have a middleware. `createApplicationHandler` makes the same argument
187
+ // about `entry.runMiddleware` itself — called rather than tested for, so a
188
+ // server bundle without it is a `TypeError` on the first request instead of
189
+ // an application whose auth check quietly stopped running. The same
190
+ // argument applies to the request this runs inside: without one, the first
191
+ // `cookies()` in somebody's guard would throw "called outside a request …
192
+ // a static prerender, a module's top level, or a client component", which
193
+ // is three wrong places to look, and an application with no `cookies()`
194
+ // anywhere would reach its render with no context at all and lose every
195
+ // `after()` to a different exception later.
196
+ requireRequest("runMiddleware");
197
+ if (table.length === 0) {
198
+ return null;
199
+ }
200
+
201
+ const arrived = new URL(request.url);
202
+ const document = documentPathOf(arrived.pathname);
203
+ // The URL the chain is matched against and every middleware is handed: the
204
+ // document's, for a payload request. See "A payload request is its
205
+ // document" above.
206
+ let url = document == null ? arrived : withPathname(arrived, document);
207
+ let seen = document == null ? request : requestAt(request, url);
208
+ let rewritten = false;
209
+ const ran: Set<MiddlewareRecord> = new Set();
210
+
211
+ for (let index = 0; index < table.length; index += 1) {
212
+ const record = table[index];
213
+ if (ran.has(record)) {
214
+ continue;
215
+ }
216
+ const params = matchPrefix(record.path, url.pathname);
217
+ if (params == null) {
218
+ continue;
219
+ }
220
+ ran.add(record);
221
+
222
+ const middleware = pick(await record.load(), record.file);
223
+ // In the host's context, not one of this module's own. Two middleware on
224
+ // the same path see the same cookies, and so does the handler or the page
225
+ // underneath them: `draftMode().isEnabled` is one answer for the whole
226
+ // request, and every `after()` on the request lands in one ordered list
227
+ // that the host drains once, after the response has gone.
228
+ const result = await middleware(seen, { params, searchParams: url.searchParams });
229
+ if (result == null) {
230
+ continue;
231
+ }
232
+ if (result instanceof Rewrite) {
233
+ url = destinationOf(result.destination, url, record.file);
234
+ seen = requestAt(seen, url);
235
+ rewritten = true;
236
+ // From the root again, over what has not run: the destination's guards
237
+ // are owed their say, and the ones that already had it are not asked
238
+ // twice.
239
+ index = -1;
240
+ continue;
241
+ }
242
+ return result;
243
+ }
244
+
245
+ if (!rewritten) {
246
+ return null;
247
+ }
248
+ return document == null
249
+ ? seen
250
+ : requestAt(seen, new URL(flightUrl(url.pathname + url.search), url));
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Where a rewrite goes, resolved against the URL the middleware was handed.
256
+ *
257
+ * Refused by name when it leaves the origin, because the one thing a rewrite
258
+ * promises is that this application answers.
259
+ */
260
+ function destinationOf(destination: string, base: URL, file: string): URL {
261
+ const next = new URL(destination, base);
262
+ if (next.origin !== base.origin) {
263
+ throw new Error(
264
+ `${file} rewrote ${base.pathname} to ${destination}, which is another origin. A rewrite ` +
265
+ "serves another route of this application: answer with `Response.redirect` to send the " +
266
+ "visitor elsewhere, or fetch the other server from a route handler.",
267
+ );
268
+ }
269
+ if (!destination.includes("?")) {
270
+ next.search = base.search;
271
+ }
272
+ next.hash = "";
273
+ return next;
274
+ }
275
+
276
+ function withPathname(url: URL, pathname: string): URL {
277
+ const next = new URL(url.href);
278
+ next.pathname = pathname;
279
+ return next;
280
+ }
281
+
282
+ /**
283
+ * `request` at another URL: same method, headers, signal and body.
284
+ *
285
+ * A `Request` is a valid `RequestInit`, so a streamed body is handed on rather
286
+ * than read.
287
+ */
288
+ function requestAt(request: Request, url: URL): Request {
289
+ // $FlowFixMe[incompatible-call] - a `Request` is read as the `RequestInit` it satisfies.
290
+ return new Request(url.href, request);
291
+ }
292
+
293
+ /**
294
+ * The function a middleware module exports.
295
+ *
296
+ * `default` or `middleware`, the same two spellings a page offers for its
297
+ * component. Anything else is an authoring mistake and throws rather than
298
+ * being skipped: a file named `$middleware.js` that the router quietly
299
+ * ignored is the bug this whole module exists to stop happening.
300
+ */
301
+ function pick(module: MiddlewareModule, file: string): Middleware {
302
+ // `rewrite` is an export a middleware module may well import, and is never
303
+ // the middleware itself.
304
+ const exported = typeof module.default === "function" ? module.default : module.middleware;
305
+ if (typeof exported !== "function") {
306
+ throw new Error(
307
+ `${file} is a middleware but exports no middleware function: export it as \`default\` or as \`middleware\`.`,
308
+ );
309
+ }
310
+ return exported as $FlowFixMe;
311
+ }
312
+
313
+ /**
314
+ * Match a middleware's directory path against a pathname, as a *prefix*.
315
+ *
316
+ * The difference from the dispatcher's `matchPath` is the whole point:
317
+ * `/dashboard` matches `/dashboard`, `/dashboard/settings` and
318
+ * `/dashboard/a/b`, because a middleware guards a subtree rather than a path.
319
+ * `null` when it does not match, so a route with no parameters is still
320
+ * distinguishable from a miss.
321
+ */
322
+ function matchPrefix(routePath: string, pathname: string): RouteParams | null {
323
+ const wanted = segmentsOf(routePath);
324
+ const given = segmentsOf(pathname);
325
+ const params: { [string]: string | Array<string> } = {};
326
+
327
+ for (let index = 0; index < wanted.length; index += 1) {
328
+ const segment = wanted[index];
329
+ if (segment.startsWith(":") && segment.endsWith("*")) {
330
+ params[segment.slice(1, -1)] = given.slice(index);
331
+ return params as $FlowFixMe;
332
+ }
333
+ if (index >= given.length) {
334
+ return null;
335
+ }
336
+ if (segment.startsWith(":")) {
337
+ params[segment.slice(1)] = given[index];
338
+ continue;
339
+ }
340
+ if (segment !== given[index]) {
341
+ return null;
342
+ }
343
+ }
344
+
345
+ return params as $FlowFixMe;
346
+ }
347
+
348
+ function segmentsOf(value: string): Array<string> {
349
+ return value.split("/").filter((segment) => segment !== "");
350
+ }
@@ -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
+ }