@uniflowed/router 0.0.0-alpha.12 → 0.0.0-alpha.14
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/action.js +198 -0
- package/client.js +35 -1
- package/handler.js +74 -6
- package/index.js +10 -1
- package/internal/action-endpoint.js +422 -0
- package/internal/action-wire.js +382 -0
- package/internal/diagnostics.js +169 -0
- package/internal/hydration.js +892 -0
- package/internal/runtime.js +1038 -120
- package/internal/stream.js +122 -12
- package/middleware.js +14 -5
- package/package.json +5 -2
- package/server.js +68 -39
package/action.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/router/action`: a server action, as the browser holds it.
|
|
4
|
+
//
|
|
5
|
+
// A `"use client"` module writes an ordinary import and an ordinary call:
|
|
6
|
+
//
|
|
7
|
+
// // app/counter/_components/Counter.js
|
|
8
|
+
// "use client";
|
|
9
|
+
// import { recordClick } from "../_actions/clicks.js";
|
|
10
|
+
// …
|
|
11
|
+
// <button onClick={() => { void recordClick(count); }}>
|
|
12
|
+
//
|
|
13
|
+
// On the server that import is the function. In the browser it is a reference
|
|
14
|
+
// built here: `@uniflowed/vite` replaces the `"use server"` module, in the
|
|
15
|
+
// client graph only, with one `createServerReference` per callable export, so
|
|
16
|
+
// what crosses the import is an id and a `fetch` — and the module's body, its
|
|
17
|
+
// imports and every secret they reached stay where they were written.
|
|
18
|
+
//
|
|
19
|
+
// # Why this module is where it is
|
|
20
|
+
//
|
|
21
|
+
// It is the browser's half, so it may not live in `@uniflowed/server`: that
|
|
22
|
+
// package is in `SERVER_ONLY_PACKAGES` and importing it from a client
|
|
23
|
+
// component is the error the RSC graph exists to produce. It holds no
|
|
24
|
+
// platform API beyond `fetch` and `location`, and it imports one internal
|
|
25
|
+
// module, which is the grammar the server applies to the same bytes.
|
|
26
|
+
//
|
|
27
|
+
// # The call
|
|
28
|
+
//
|
|
29
|
+
// `POST` to the page's own URL, `uf-action: <id>`, `application/json`, and
|
|
30
|
+
// `{"args":[…]}`. The URL is the page rather than a path uf reserves so that
|
|
31
|
+
// the middleware guarding that path runs above the call exactly as it does
|
|
32
|
+
// above the page — and it is not a substitute for the action authorizing
|
|
33
|
+
// itself; `internal/action-endpoint.js` says why in the paragraph that matters.
|
|
34
|
+
//
|
|
35
|
+
// # What Flow checks, and where
|
|
36
|
+
//
|
|
37
|
+
// Flow reads `app/_actions/clicks.js`, not the reference the bundler
|
|
38
|
+
// substitutes for it, so `recordClick("nine")` against
|
|
39
|
+
// `recordClick(count: number)` is a `uf check` error rather than a `400`. That
|
|
40
|
+
// is the half that needs nothing from this module.
|
|
41
|
+
//
|
|
42
|
+
// What a declaration alone does not say is whether those arguments and that
|
|
43
|
+
// result can cross a wire at all, and `ActionArguments` and `ActionResult` are
|
|
44
|
+
// that half: `uf prepare` writes one instantiation of each into
|
|
45
|
+
// `server-actions.js`, over every action in the project at once, so an action
|
|
46
|
+
// taking a callback or returning a `Map` is a `uf check` error naming the
|
|
47
|
+
// offending type. Without them the same mistake is a request that arrives with
|
|
48
|
+
// `{}` where an object was passed — the kind of bug found in production by a
|
|
49
|
+
// column that stopped being written.
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
ACTION_CONTENT_TYPE,
|
|
53
|
+
ACTION_HEADER,
|
|
54
|
+
type ActionValue,
|
|
55
|
+
decodeActionResult,
|
|
56
|
+
encodeActionArguments,
|
|
57
|
+
} from "./internal/action-wire.js";
|
|
58
|
+
|
|
59
|
+
export type { ActionValue } from "./internal/action-wire.js";
|
|
60
|
+
export {
|
|
61
|
+
ACTION_CONTENT_TYPE,
|
|
62
|
+
ACTION_HEADER,
|
|
63
|
+
ActionValueError,
|
|
64
|
+
MAX_ACTION_ARGUMENTS,
|
|
65
|
+
MAX_ACTION_BODY_BYTES,
|
|
66
|
+
MAX_ACTION_DEPTH,
|
|
67
|
+
MAX_ACTION_VALUES,
|
|
68
|
+
} from "./internal/action-wire.js";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A function that can be a server action.
|
|
72
|
+
*
|
|
73
|
+
* Both halves of the signature are the wire grammar: an action is called with
|
|
74
|
+
* values that can cross and answers with one that can. `void` is a result and
|
|
75
|
+
* not an argument, because JSON has no `undefined` and an action declared to
|
|
76
|
+
* take one would be taking something the caller cannot send.
|
|
77
|
+
*/
|
|
78
|
+
export type ServerActionFunction = (...args: Array<ActionValue>) => Promise<ActionValue | void>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* An action's arguments, held against what a wire can carry.
|
|
82
|
+
*
|
|
83
|
+
* A bound on a tuple rather than a bound on the function, and the difference
|
|
84
|
+
* is the whole reason this is two types instead of one. A function type puts
|
|
85
|
+
* its parameters in a contravariant position: `F extends (…args:
|
|
86
|
+
* Array<ActionValue>) => …` asks whether `F` accepts *every* `ActionValue`,
|
|
87
|
+
* which `createUser(name: string)` does not and should not. `Parameters<F>` is
|
|
88
|
+
* the same list read covariantly, where the question is the one worth asking —
|
|
89
|
+
* is each argument something that can cross?
|
|
90
|
+
*
|
|
91
|
+
* `uf prepare` writes one instantiation over the whole project:
|
|
92
|
+
*
|
|
93
|
+
* export type ServerActionArgsFitTheWire =
|
|
94
|
+
* ActionArguments<ServerActionArgs<ServerActionName>>;
|
|
95
|
+
*
|
|
96
|
+
* `ServerActionName` is every action's name, so `ServerActionArgs` of it is
|
|
97
|
+
* every action's argument list, and one line checks all of them.
|
|
98
|
+
*/
|
|
99
|
+
export type ActionArguments<TArgs extends $ReadOnlyArray<ActionValue>> = TArgs;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* An action's result, held against the same grammar.
|
|
103
|
+
*
|
|
104
|
+
* A promise, because a server action is always async — the RSC graph rejects
|
|
105
|
+
* one that is not — and `void` is allowed because an action that returns
|
|
106
|
+
* nothing is ordinary. `Promise` is covariant in Flow, so the bound reaches
|
|
107
|
+
* the resolved type without any of the contortion the arguments needed.
|
|
108
|
+
*/
|
|
109
|
+
export type ActionResult<TResult extends Promise<ActionValue | void>> = TResult;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A server action that did not answer.
|
|
113
|
+
*
|
|
114
|
+
* Carries the status and the action's build-time name and nothing else,
|
|
115
|
+
* because nothing else came back: the endpoint answers every refusal with a
|
|
116
|
+
* fixed body, so there is no message from the server to relay. What went wrong
|
|
117
|
+
* is in the server's log, which is where an application's internals belong.
|
|
118
|
+
*/
|
|
119
|
+
export class ServerActionError extends Error {
|
|
120
|
+
/** The HTTP status the endpoint answered with. */
|
|
121
|
+
status: number;
|
|
122
|
+
/** `module#export` of the action that was called. */
|
|
123
|
+
action: string;
|
|
124
|
+
|
|
125
|
+
constructor(action: string, status: number) {
|
|
126
|
+
super(
|
|
127
|
+
`@uniflowed/router: the server action \`${action}\` answered ${String(status)}. ` +
|
|
128
|
+
"The endpoint reports every failure the same way; the reason is in the server's log.",
|
|
129
|
+
);
|
|
130
|
+
this.name = "ServerActionError";
|
|
131
|
+
this.status = status;
|
|
132
|
+
this.action = action;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The reference the client bundle holds in place of one server action.
|
|
138
|
+
*
|
|
139
|
+
* Generated, never written by hand: `@uniflowed/vite` emits one call per
|
|
140
|
+
* callable export of a `"use server"` module, with the id
|
|
141
|
+
* `crates/uf_rsc/src/action.rs` derived for it and the `module#export` name
|
|
142
|
+
* that only ever appears in an error.
|
|
143
|
+
*
|
|
144
|
+
* The returned function is `async` and refuses before it sends: an argument
|
|
145
|
+
* outside the wire grammar throws an `ActionValueError` naming the argument's
|
|
146
|
+
* position, at the call site, rather than becoming a `400` with nothing in it.
|
|
147
|
+
*/
|
|
148
|
+
export function createServerReference(id: string, name: string): ServerActionFunction {
|
|
149
|
+
return async function callServerAction(...args: Array<ActionValue>): Promise<ActionValue | void> {
|
|
150
|
+
const body = encodeActionArguments(args);
|
|
151
|
+
// Built rather than written as a literal, because the header's name is a
|
|
152
|
+
// constant and a computed key in an object literal is a shape Flow
|
|
153
|
+
// declines to track.
|
|
154
|
+
const headers: { [string]: string } = { "content-type": ACTION_CONTENT_TYPE };
|
|
155
|
+
headers[ACTION_HEADER] = id;
|
|
156
|
+
const response = await fetch(currentUrl(), {
|
|
157
|
+
method: "POST",
|
|
158
|
+
// Stated rather than left to the default, because the default is what a
|
|
159
|
+
// reader has to look up and because this one is load-bearing: the
|
|
160
|
+
// endpoint's `Origin` check is only meaningful for a request that
|
|
161
|
+
// carries the visitor's cookies in the first place.
|
|
162
|
+
credentials: "same-origin",
|
|
163
|
+
// Never a cached answer, and never one written to a cache: an action is
|
|
164
|
+
// a side effect, and `POST` responses are outside HTTP caching by
|
|
165
|
+
// default only until something decides otherwise.
|
|
166
|
+
cache: "no-store",
|
|
167
|
+
headers,
|
|
168
|
+
body,
|
|
169
|
+
});
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new ServerActionError(name, response.status);
|
|
172
|
+
}
|
|
173
|
+
return decodeActionResult(await response.text());
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The URL an action call is sent to: the page the browser is on.
|
|
179
|
+
*
|
|
180
|
+
* Path and query, not the whole URL, so the request is same-origin by
|
|
181
|
+
* construction rather than by a comparison somebody could get wrong. The hash
|
|
182
|
+
* is left off because it never reaches a server.
|
|
183
|
+
*
|
|
184
|
+
* A reference only exists in the client bundle — the server imports the real
|
|
185
|
+
* module — so there is no `location` here only if something has imported the
|
|
186
|
+
* browser's half into a server, and saying so is better than posting to a
|
|
187
|
+
* relative path that means nothing there.
|
|
188
|
+
*/
|
|
189
|
+
function currentUrl(): string {
|
|
190
|
+
const location = globalThis.location;
|
|
191
|
+
if (location == null) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
"@uniflowed/router: a server action reference was called where there is no `location`. " +
|
|
194
|
+
"A reference is the browser's half of an action; the server imports the module itself.",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return `${location.pathname}${location.search}`;
|
|
198
|
+
}
|
package/client.js
CHANGED
|
@@ -7,6 +7,17 @@
|
|
|
7
7
|
// *before* `hydrateRoot`, so the first client render is synchronous and
|
|
8
8
|
// matches the server's markup exactly.
|
|
9
9
|
//
|
|
10
|
+
// # A hydration that fails says what differed
|
|
11
|
+
//
|
|
12
|
+
// React reports a mismatch with one sentence and a list of the six things that
|
|
13
|
+
// usually cause it, and leaves the reader to find which node of the two
|
|
14
|
+
// thousand on the page was the one. This module is the only place that can do
|
|
15
|
+
// better, because it is the only place that runs between the parser finishing
|
|
16
|
+
// and React starting: `internal/hydration.js` takes a copy of the server's
|
|
17
|
+
// markup here, and compares it against the repaired tree when React reports.
|
|
18
|
+
// Development only, and dynamically imported so a production bundle has no path
|
|
19
|
+
// to it. See ubugeeei-prod/uf#508.
|
|
20
|
+
//
|
|
10
21
|
// # A route can decline to be hydrated
|
|
11
22
|
//
|
|
12
23
|
// uf's server-component analysis decides which routes have a `"use client"`
|
|
@@ -65,7 +76,30 @@ export async function hydrate(options: {|
|
|
|
65
76
|
|
|
66
77
|
const { App } = options;
|
|
67
78
|
const container = document.getElementById(ROOT_ID) ?? document;
|
|
79
|
+
|
|
80
|
+
// The server's markup, and the reporter that will read it, in development
|
|
81
|
+
// only. Both have to be in place *before* `hydrateRoot`: React repairs a
|
|
82
|
+
// mismatched subtree by rendering over it, so the bytes the server sent exist
|
|
83
|
+
// for exactly the moment between the parser finishing and this line.
|
|
84
|
+
//
|
|
85
|
+
// `import.meta.hot` is the gate because it is the one signal that is right in
|
|
86
|
+
// all three places this module is evaluated. Vite defines it while serving
|
|
87
|
+
// and replaces it with `undefined` in a build, so the branch is statically
|
|
88
|
+
// dead there; Node leaves it undefined, so `tests/library/rsc-split.test.js`
|
|
89
|
+
// imports this file without a bundler and gets the production path. The
|
|
90
|
+
// import is dynamic so that the overlay is not merely shaken out of a
|
|
91
|
+
// production bundle but never reachable from one.
|
|
92
|
+
let recovery = null;
|
|
93
|
+
if (import.meta.hot != null) {
|
|
94
|
+
const { captureServerMarkup, hydrationErrorHandler } = await import("./internal/hydration.js");
|
|
95
|
+
recovery = hydrationErrorHandler(container, captureServerMarkup(container), document);
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
startTransition(() => {
|
|
69
|
-
hydrateRoot(
|
|
99
|
+
hydrateRoot(
|
|
100
|
+
container,
|
|
101
|
+
<App url={url} initial={resolved} />,
|
|
102
|
+
recovery == null ? undefined : { onRecoverableError: recovery },
|
|
103
|
+
);
|
|
70
104
|
});
|
|
71
105
|
}
|
package/handler.js
CHANGED
|
@@ -26,6 +26,41 @@
|
|
|
26
26
|
// with the `Allow` header the specification requires — that is not the
|
|
27
27
|
// handler's business, and every handler would otherwise write it.
|
|
28
28
|
//
|
|
29
|
+
// # `QUERY`, and what refuses it
|
|
30
|
+
//
|
|
31
|
+
// `QUERY` is a `GET` with a body: safe, idempotent, cacheable, and the method
|
|
32
|
+
// that a search with more parameters than a URL can hold has been faking with a
|
|
33
|
+
// `POST` for twenty years. A handler exports it like any other verb, and this
|
|
34
|
+
// dispatcher matches it like any other verb, because there is nothing special
|
|
35
|
+
// about it *here*. What is special about it is the path between a client and
|
|
36
|
+
// this function, and that is the part worth writing down rather than leaving to
|
|
37
|
+
// be discovered in production.
|
|
38
|
+
//
|
|
39
|
+
// Three things refuse it, and they refuse it differently:
|
|
40
|
+
//
|
|
41
|
+
// * **A client that cannot send it.** The Fetch standard forbids `CONNECT`,
|
|
42
|
+
// `TRACE` and `TRACK` and allows any other token, so every browser and
|
|
43
|
+
// every runtime uf targets can send a `QUERY` today. `XMLHttpRequest` and
|
|
44
|
+
// `EventSource` cannot, and neither can a `<form>`.
|
|
45
|
+
// * **An intermediary that will not forward it.** This is the real one. A
|
|
46
|
+
// proxy, a CDN or a WAF that has a list of methods answers `405` or `501`
|
|
47
|
+
// itself, and the request never arrives — so the failure looks exactly like
|
|
48
|
+
// a route that does not exist, from a server that never saw it.
|
|
49
|
+
// `@uniflowed/fetch` names that case in the error rather than passing the
|
|
50
|
+
// status through, which is the whole of what "stated rather than
|
|
51
|
+
// discovered" can mean from the other end of a wire.
|
|
52
|
+
// * **A cache that does not know it is safe.** `QUERY` is cacheable in
|
|
53
|
+
// principle and the key includes the body, which almost nothing implements.
|
|
54
|
+
// uf's own route cache is `GET`-only and stays that way; anything in front
|
|
55
|
+
// of the application should be told not to store a `QUERY` at all.
|
|
56
|
+
//
|
|
57
|
+
// What uf deliberately does not do about any of it is accept a method-override
|
|
58
|
+
// header. `X-HTTP-Method-Override: QUERY` on a `POST` is the usual workaround
|
|
59
|
+
// and it is the shape of CVE-2025-29927: an inbound header steering dispatch,
|
|
60
|
+
// which `docs/security.md` forbids in the row about that CVE and in rule 3. A
|
|
61
|
+
// route that must work through hostile infrastructure exports `POST` as well
|
|
62
|
+
// and says so in its own file, where a reader can see it.
|
|
63
|
+
//
|
|
29
64
|
// It also does not establish the request a handler is inside. The host does,
|
|
30
65
|
// around the whole of it, so a handler and the guard above it share one
|
|
31
66
|
// context; see the same section in `./middleware.js`. This module used to
|
|
@@ -34,6 +69,8 @@
|
|
|
34
69
|
// `after()` promises. A handler that streams its body has not sent a byte at
|
|
35
70
|
// that point. See ubugeeei-prod/uf#389.
|
|
36
71
|
|
|
72
|
+
import { asResponder, noteRoute } from "@uniflowed/server/host";
|
|
73
|
+
|
|
37
74
|
import { requireRequest } from "./internal/request.js";
|
|
38
75
|
import type { RouteParams } from "./internal/runtime.js";
|
|
39
76
|
|
|
@@ -66,8 +103,18 @@ export type HandlerRecord = {|
|
|
|
66
103
|
* — and a module that exports a helper would then answer requests with it.
|
|
67
104
|
* `HEAD` falls back to `GET` with the body dropped, which is what a client
|
|
68
105
|
* asking for headers expects and what nobody remembers to write.
|
|
106
|
+
*
|
|
107
|
+
* It was closed in name only until `QUERY` was added. `pick` looked the method
|
|
108
|
+
* up on the module and this list decided nothing but the order of the `Allow`
|
|
109
|
+
* header, so a module exporting `PURGE` answered `PURGE` — the exact behaviour
|
|
110
|
+
* the paragraph above says is refused. Adding a verb was the moment to make the
|
|
111
|
+
* sentence true, because the alternative was adding one to a list nothing read.
|
|
112
|
+
*
|
|
113
|
+
* `QUERY` is here and `CONNECT` and `TRACE` are not, and the difference is not
|
|
114
|
+
* taste: the `fetch` specification forbids the last two outright, so a handler
|
|
115
|
+
* exporting either could never be reached by a browser.
|
|
69
116
|
*/
|
|
70
|
-
const METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
117
|
+
const METHODS = ["GET", "HEAD", "QUERY", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
71
118
|
|
|
72
119
|
/**
|
|
73
120
|
* Match a request against the handler table and run it.
|
|
@@ -94,6 +141,12 @@ export function createDispatcher(options: {|
|
|
|
94
141
|
continue;
|
|
95
142
|
}
|
|
96
143
|
|
|
144
|
+
// Before the module is loaded and before the method is checked, because
|
|
145
|
+
// this is the answer to "what was this request" and a `405` is as much
|
|
146
|
+
// this route's answer as a `200` is. A log of `/api/users/:id 405` is
|
|
147
|
+
// actionable; the same line with the path in it is a million lines.
|
|
148
|
+
noteRoute(record.path);
|
|
149
|
+
|
|
97
150
|
const module = await record.load();
|
|
98
151
|
const method = request.method.toUpperCase();
|
|
99
152
|
const handler = pick(module, method);
|
|
@@ -104,10 +157,15 @@ export function createDispatcher(options: {|
|
|
|
104
157
|
// In the host's request, so a handler that calls `headers()`,
|
|
105
158
|
// `cookies()` or `after()` answers about the same one its guard did, and
|
|
106
159
|
// what it defers is drained once, by the host, after the bytes are out.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
160
|
+
//
|
|
161
|
+
// And inside `asResponder`, which is the other half: a route handler is
|
|
162
|
+
// one of the two things that owns a response, so it is one of the two
|
|
163
|
+
// places `draftMode().enable()` is allowed — and the `Set-Cookie` it
|
|
164
|
+
// decided on is written onto the response below rather than left on an
|
|
165
|
+
// object the host is about to discard. See ubugeeei-prod/uf#282.
|
|
166
|
+
const response = await asResponder("a route handler", async () =>
|
|
167
|
+
handler(request, { params, searchParams: url.searchParams }),
|
|
168
|
+
);
|
|
111
169
|
|
|
112
170
|
// A `HEAD` answered by `GET` must not carry the body. The test is
|
|
113
171
|
// against the module's own `HEAD`, not `pick`'s — `pick` falls back to
|
|
@@ -126,8 +184,18 @@ export function createDispatcher(options: {|
|
|
|
126
184
|
};
|
|
127
185
|
}
|
|
128
186
|
|
|
129
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* The function for a method, falling back to `GET` for `HEAD`.
|
|
189
|
+
*
|
|
190
|
+
* The method is checked against `METHODS` first, which is what makes that list
|
|
191
|
+
* closed rather than decorative: without it a request could name any export,
|
|
192
|
+
* and a module's `PURGE` — or its `DEFAULT`, or a name a bundler added — would
|
|
193
|
+
* answer one.
|
|
194
|
+
*/
|
|
130
195
|
function pick(module: HandlerModule, method: string): Handler | null {
|
|
196
|
+
if (!METHODS.includes(method)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
131
199
|
const own = module[method];
|
|
132
200
|
if (typeof own === "function") {
|
|
133
201
|
return own as $FlowFixMe;
|
package/index.js
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
//
|
|
10
10
|
// `_uf.not-found.js` and `_uf.error.js` are the two boundaries: the page for a
|
|
11
11
|
// path that matched nothing, and what renders in place of a subtree that threw.
|
|
12
|
-
// Both are segment files, resolved by the nearest one above the path
|
|
12
|
+
// Both are segment files, resolved by the nearest one above the path — and the
|
|
13
|
+
// router root always has one of each, so a project that declares neither still
|
|
14
|
+
// answers a 404 inside its own layouts rather than beside them.
|
|
15
|
+
//
|
|
16
|
+
// `_uf.template.js` is a layout that remounts on every navigation, for the
|
|
17
|
+
// cases where a layout's persistence is the wrong default.
|
|
13
18
|
|
|
14
19
|
import * as React from "react";
|
|
15
20
|
|
|
@@ -19,6 +24,7 @@ export type {
|
|
|
19
24
|
AppProps,
|
|
20
25
|
ErrorBoundary,
|
|
21
26
|
ErrorModule,
|
|
27
|
+
JsonLd,
|
|
22
28
|
LayoutModule,
|
|
23
29
|
LinkPrefetch,
|
|
24
30
|
LoaderArgs,
|
|
@@ -28,6 +34,7 @@ export type {
|
|
|
28
34
|
NotFoundBoundary,
|
|
29
35
|
PageModule,
|
|
30
36
|
ResolvedRoute,
|
|
37
|
+
Robots,
|
|
31
38
|
RouteError,
|
|
32
39
|
RouteInfo,
|
|
33
40
|
RouteMatch,
|
|
@@ -37,6 +44,7 @@ export type {
|
|
|
37
44
|
RouteTable,
|
|
38
45
|
Router,
|
|
39
46
|
SearchParams,
|
|
47
|
+
TemplateModule,
|
|
40
48
|
TwitterCard,
|
|
41
49
|
} from "./internal/runtime.js";
|
|
42
50
|
|
|
@@ -65,6 +73,7 @@ export {
|
|
|
65
73
|
useLoaderData,
|
|
66
74
|
useRoute,
|
|
67
75
|
useRouter,
|
|
76
|
+
useSeo,
|
|
68
77
|
} from "./internal/runtime.js";
|
|
69
78
|
|
|
70
79
|
/** Props a page receives. */
|