react-bun-ssr 0.1.0 → 0.1.1
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/README.md +116 -132
- package/framework/runtime/build-tools.ts +152 -48
- package/framework/runtime/client-runtime.tsx +1277 -15
- package/framework/runtime/config.ts +4 -1
- package/framework/runtime/index.ts +6 -0
- package/framework/runtime/io.ts +1 -1
- package/framework/runtime/link.tsx +205 -0
- package/framework/runtime/markdown-headings.ts +54 -0
- package/framework/runtime/markdown-routes.ts +8 -26
- package/framework/runtime/module-loader.ts +172 -47
- package/framework/runtime/navigation-api.ts +223 -0
- package/framework/runtime/render.tsx +56 -92
- package/framework/runtime/route-api.ts +6 -0
- package/framework/runtime/route-errors.ts +166 -0
- package/framework/runtime/router.ts +80 -0
- package/framework/runtime/runtime-constants.ts +4 -0
- package/framework/runtime/server.ts +696 -71
- package/framework/runtime/tree.tsx +171 -3
- package/framework/runtime/types.ts +70 -3
- package/framework/runtime/utils.ts +6 -5
- package/package.json +18 -5
|
@@ -4,54 +4,33 @@ import {
|
|
|
4
4
|
isValidElement,
|
|
5
5
|
Suspense,
|
|
6
6
|
use,
|
|
7
|
-
type ComponentType,
|
|
8
7
|
type ReactElement,
|
|
9
8
|
type ReactNode,
|
|
10
9
|
} from "react";
|
|
11
10
|
import { renderToReadableStream, renderToStaticMarkup, renderToString } from "react-dom/server";
|
|
12
11
|
import type { DeferredSettleEntry } from "./deferred";
|
|
13
12
|
import type {
|
|
13
|
+
ClientRouterSnapshot,
|
|
14
14
|
HydrationDocumentAssets,
|
|
15
15
|
RenderPayload,
|
|
16
16
|
RouteModule,
|
|
17
17
|
RouteModuleBundle,
|
|
18
18
|
} from "./types";
|
|
19
19
|
import { safeJsonSerialize } from "./utils";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return modules.root.ErrorBoundary ?? null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveNotFoundBoundary(modules: RouteModuleBundle): ComponentType | null {
|
|
38
|
-
if (modules.route.NotFound) {
|
|
39
|
-
return modules.route.NotFound;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (let index = modules.layouts.length - 1; index >= 0; index -= 1) {
|
|
43
|
-
const candidate = modules.layouts[index]!.NotFound;
|
|
44
|
-
if (candidate) {
|
|
45
|
-
return candidate;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return modules.root.NotFound ?? null;
|
|
50
|
-
}
|
|
20
|
+
import {
|
|
21
|
+
RBSSR_HEAD_MARKER_END_ATTR,
|
|
22
|
+
RBSSR_HEAD_MARKER_START_ATTR,
|
|
23
|
+
RBSSR_PAYLOAD_SCRIPT_ID,
|
|
24
|
+
RBSSR_ROUTER_SCRIPT_ID,
|
|
25
|
+
} from "./runtime-constants";
|
|
26
|
+
import {
|
|
27
|
+
createErrorAppTree,
|
|
28
|
+
createNotFoundAppTree,
|
|
29
|
+
createPageAppTree,
|
|
30
|
+
} from "./tree";
|
|
51
31
|
|
|
52
32
|
export function renderPageApp(modules: RouteModuleBundle, payload: RenderPayload): string {
|
|
53
|
-
|
|
54
|
-
return renderToString(createRouteTree(modules, <Leaf />, payload));
|
|
33
|
+
return renderToString(createPageAppTree(modules, payload));
|
|
55
34
|
}
|
|
56
35
|
|
|
57
36
|
export function renderErrorApp(
|
|
@@ -68,49 +47,6 @@ export function renderNotFoundApp(modules: RouteModuleBundle, payload: RenderPay
|
|
|
68
47
|
return tree ? renderToString(tree) : null;
|
|
69
48
|
}
|
|
70
49
|
|
|
71
|
-
export function createPageAppTree(modules: RouteModuleBundle, payload: RenderPayload): ReactElement {
|
|
72
|
-
const Leaf = modules.route.default;
|
|
73
|
-
return createRouteTree(modules, <Leaf />, payload);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function createErrorAppTree(
|
|
77
|
-
modules: RouteModuleBundle,
|
|
78
|
-
payload: RenderPayload,
|
|
79
|
-
error: unknown,
|
|
80
|
-
): ReactElement | null {
|
|
81
|
-
const Boundary = resolveErrorBoundary(modules);
|
|
82
|
-
if (!Boundary) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const boundaryPayload: RenderPayload = {
|
|
87
|
-
...payload,
|
|
88
|
-
error: {
|
|
89
|
-
message: error instanceof Error ? error.message : String(error),
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
return createRouteTree(modules, <Boundary error={error} />, boundaryPayload);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function createNotFoundAppTree(modules: RouteModuleBundle, payload: RenderPayload): ReactElement | null {
|
|
97
|
-
const Boundary = resolveNotFoundBoundary(modules);
|
|
98
|
-
if (!Boundary) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return createRouteTree(modules, <Boundary />, payload);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function escapeHtml(value: string): string {
|
|
106
|
-
return value
|
|
107
|
-
.replace(/&/g, "&")
|
|
108
|
-
.replace(/</g, "<")
|
|
109
|
-
.replace(/>/g, ">")
|
|
110
|
-
.replace(/\"/g, """)
|
|
111
|
-
.replace(/'/g, "'");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
50
|
function toTitleText(children: ReactNode): string {
|
|
115
51
|
return Children.toArray(children)
|
|
116
52
|
.map(child => {
|
|
@@ -214,6 +150,20 @@ function withVersionQuery(url: string, version?: number): string {
|
|
|
214
150
|
return `${url}${separator}v=${version}`;
|
|
215
151
|
}
|
|
216
152
|
|
|
153
|
+
function createVersionedCssHrefs(assets: HydrationDocumentAssets): string[] {
|
|
154
|
+
return assets.css.map(href => withVersionQuery(href, assets.devVersion));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createManagedHeadMarkup(options: {
|
|
158
|
+
headMarkup: string;
|
|
159
|
+
assets: HydrationDocumentAssets;
|
|
160
|
+
}): string {
|
|
161
|
+
const cssLinks = createVersionedCssHrefs(options.assets)
|
|
162
|
+
.map(href => `<link rel="stylesheet" href="${Bun.escapeHTML(href)}"/>`)
|
|
163
|
+
.join("");
|
|
164
|
+
return `${options.headMarkup}${cssLinks}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
217
167
|
function toDeferredScript(entry: DeferredSettleEntry): ReactElement {
|
|
218
168
|
return (
|
|
219
169
|
<Suspense fallback={null} key={entry.id}>
|
|
@@ -236,13 +186,13 @@ function HtmlDocument(options: {
|
|
|
236
186
|
appTree: ReactElement;
|
|
237
187
|
payload: RenderPayload;
|
|
238
188
|
assets: HydrationDocumentAssets;
|
|
239
|
-
|
|
189
|
+
managedHeadElements: ReactNode[];
|
|
190
|
+
routerSnapshot: ClientRouterSnapshot;
|
|
240
191
|
deferredSettleEntries: DeferredSettleEntry[];
|
|
241
192
|
}): ReactElement {
|
|
242
|
-
const { appTree, payload, assets,
|
|
193
|
+
const { appTree, payload, assets, managedHeadElements, deferredSettleEntries } = options;
|
|
243
194
|
const versionedScript = assets.script ? withVersionQuery(assets.script, assets.devVersion) : undefined;
|
|
244
|
-
const cssLinks = assets.
|
|
245
|
-
const versionedHref = withVersionQuery(href, assets.devVersion);
|
|
195
|
+
const cssLinks = createVersionedCssHrefs(assets).map((versionedHref, index) => {
|
|
246
196
|
return <link key={`css:${index}:${versionedHref}`} rel="stylesheet" href={versionedHref} />;
|
|
247
197
|
});
|
|
248
198
|
|
|
@@ -251,8 +201,10 @@ function HtmlDocument(options: {
|
|
|
251
201
|
<head>
|
|
252
202
|
<meta charSet="utf-8" />
|
|
253
203
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
254
|
-
{
|
|
204
|
+
<meta {...{ [RBSSR_HEAD_MARKER_START_ATTR]: "1" }} />
|
|
205
|
+
{managedHeadElements}
|
|
255
206
|
{cssLinks}
|
|
207
|
+
<meta {...{ [RBSSR_HEAD_MARKER_END_ATTR]: "1" }} />
|
|
256
208
|
</head>
|
|
257
209
|
<body>
|
|
258
210
|
<div id="rbssr-root">{appTree}</div>
|
|
@@ -262,10 +214,15 @@ function HtmlDocument(options: {
|
|
|
262
214
|
}}
|
|
263
215
|
/>
|
|
264
216
|
<script
|
|
265
|
-
id=
|
|
217
|
+
id={RBSSR_PAYLOAD_SCRIPT_ID}
|
|
266
218
|
type="application/json"
|
|
267
219
|
dangerouslySetInnerHTML={{ __html: safeJsonSerialize(payload) }}
|
|
268
220
|
/>
|
|
221
|
+
<script
|
|
222
|
+
id={RBSSR_ROUTER_SCRIPT_ID}
|
|
223
|
+
type="application/json"
|
|
224
|
+
dangerouslySetInnerHTML={{ __html: safeJsonSerialize(options.routerSnapshot) }}
|
|
225
|
+
/>
|
|
269
226
|
{versionedScript ? <script type="module" src={versionedScript} /> : null}
|
|
270
227
|
{typeof assets.devVersion === "number" ? (
|
|
271
228
|
<script
|
|
@@ -315,6 +272,7 @@ export async function renderDocumentStream(options: {
|
|
|
315
272
|
payload: RenderPayload;
|
|
316
273
|
assets: HydrationDocumentAssets;
|
|
317
274
|
headElements: ReactNode[];
|
|
275
|
+
routerSnapshot: ClientRouterSnapshot;
|
|
318
276
|
deferredSettleEntries?: DeferredSettleEntry[];
|
|
319
277
|
}): Promise<ReadableStream<Uint8Array>> {
|
|
320
278
|
const stream = await renderToReadableStream(
|
|
@@ -322,7 +280,8 @@ export async function renderDocumentStream(options: {
|
|
|
322
280
|
appTree={options.appTree}
|
|
323
281
|
payload={options.payload}
|
|
324
282
|
assets={options.assets}
|
|
325
|
-
|
|
283
|
+
managedHeadElements={options.headElements}
|
|
284
|
+
routerSnapshot={options.routerSnapshot}
|
|
326
285
|
deferredSettleEntries={options.deferredSettleEntries ?? []}
|
|
327
286
|
/>,
|
|
328
287
|
);
|
|
@@ -335,16 +294,19 @@ export function renderDocument(options: {
|
|
|
335
294
|
payload: RenderPayload;
|
|
336
295
|
assets: HydrationDocumentAssets;
|
|
337
296
|
headMarkup: string;
|
|
297
|
+
routerSnapshot: ClientRouterSnapshot;
|
|
338
298
|
}): string {
|
|
339
|
-
const { appMarkup, payload, assets, headMarkup } = options;
|
|
299
|
+
const { appMarkup, payload, assets, headMarkup, routerSnapshot } = options;
|
|
340
300
|
const versionedScript = assets.script ? withVersionQuery(assets.script, assets.devVersion) : undefined;
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
301
|
+
const managedHeadMarkup = createManagedHeadMarkup({
|
|
302
|
+
headMarkup,
|
|
303
|
+
assets,
|
|
304
|
+
});
|
|
344
305
|
|
|
345
|
-
const payloadScript = `<script id="
|
|
306
|
+
const payloadScript = `<script id="${RBSSR_PAYLOAD_SCRIPT_ID}" type="application/json">${safeJsonSerialize(payload)}</script>`;
|
|
307
|
+
const routerScript = `<script id="${RBSSR_ROUTER_SCRIPT_ID}" type="application/json">${safeJsonSerialize(routerSnapshot)}</script>`;
|
|
346
308
|
const entryScript = versionedScript
|
|
347
|
-
? `<script type="module" src="${
|
|
309
|
+
? `<script type="module" src="${Bun.escapeHTML(versionedScript)}"></script>`
|
|
348
310
|
: "";
|
|
349
311
|
const devScript = typeof assets.devVersion === "number"
|
|
350
312
|
? `<script>${buildDevReloadClientScript(assets.devVersion)}</script>`
|
|
@@ -356,13 +318,15 @@ export function renderDocument(options: {
|
|
|
356
318
|
<head>
|
|
357
319
|
<meta charset="utf-8" />
|
|
358
320
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
359
|
-
${
|
|
360
|
-
${
|
|
321
|
+
<meta ${RBSSR_HEAD_MARKER_START_ATTR}="1" />
|
|
322
|
+
${managedHeadMarkup}
|
|
323
|
+
<meta ${RBSSR_HEAD_MARKER_END_ATTR}="1" />
|
|
361
324
|
</head>
|
|
362
325
|
<body>
|
|
363
326
|
<div id="rbssr-root">${appMarkup}</div>
|
|
364
327
|
${deferredBootstrapScript}
|
|
365
328
|
${payloadScript}
|
|
329
|
+
${routerScript}
|
|
366
330
|
${entryScript}
|
|
367
331
|
${devScript}
|
|
368
332
|
</body>
|
|
@@ -11,7 +11,13 @@ export type {
|
|
|
11
11
|
Params,
|
|
12
12
|
RedirectResult,
|
|
13
13
|
RequestContext,
|
|
14
|
+
RouteCatchContext,
|
|
15
|
+
RouteErrorContext,
|
|
16
|
+
RouteErrorResponse,
|
|
14
17
|
} from "./types";
|
|
15
18
|
|
|
16
19
|
export { defer, json, redirect } from "./helpers";
|
|
20
|
+
export { isRouteErrorResponse, notFound, routeError } from "./route-errors";
|
|
21
|
+
export { Link, type LinkProps } from "./link";
|
|
22
|
+
export { useRouter, type Router, type RouterNavigateOptions } from "./router";
|
|
17
23
|
export { Outlet, useLoaderData, useParams, useRequestUrl, useRouteError } from "./tree";
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { RouteErrorResponse } from "./types";
|
|
2
|
+
|
|
3
|
+
const ROUTE_ERROR_SYMBOL = Symbol.for("rbssr.route_error");
|
|
4
|
+
const REDIRECT_MIN = 300;
|
|
5
|
+
const REDIRECT_MAX = 399;
|
|
6
|
+
|
|
7
|
+
interface InternalRouteError {
|
|
8
|
+
[ROUTE_ERROR_SYMBOL]: true;
|
|
9
|
+
response: RouteErrorResponse;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRedirectStatus(status: number): boolean {
|
|
13
|
+
return status >= REDIRECT_MIN && status <= REDIRECT_MAX;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeStatus(status: number): number {
|
|
17
|
+
if (!Number.isFinite(status) || status < 100 || status > 599) {
|
|
18
|
+
throw new Error("routeError(status, ...) requires an HTTP status between 100 and 599.");
|
|
19
|
+
}
|
|
20
|
+
return Math.trunc(status);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function defaultStatusText(status: number): string {
|
|
24
|
+
try {
|
|
25
|
+
return new Response(null, { status }).statusText || "Error";
|
|
26
|
+
} catch {
|
|
27
|
+
return "Error";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function headersToRecord(headersInit?: HeadersInit): Record<string, string> | undefined {
|
|
32
|
+
if (!headersInit) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const headers = new Headers(headersInit);
|
|
36
|
+
const record: Record<string, string> = {};
|
|
37
|
+
let count = 0;
|
|
38
|
+
headers.forEach((value, key) => {
|
|
39
|
+
record[key] = value;
|
|
40
|
+
count += 1;
|
|
41
|
+
});
|
|
42
|
+
return count > 0 ? record : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createRouteErrorResponse(
|
|
46
|
+
status: number,
|
|
47
|
+
data?: unknown,
|
|
48
|
+
init: {
|
|
49
|
+
statusText?: string;
|
|
50
|
+
headers?: HeadersInit;
|
|
51
|
+
} = {},
|
|
52
|
+
): RouteErrorResponse {
|
|
53
|
+
const normalizedStatus = normalizeStatus(status);
|
|
54
|
+
return {
|
|
55
|
+
type: "route_error",
|
|
56
|
+
status: normalizedStatus,
|
|
57
|
+
statusText: init.statusText ?? defaultStatusText(normalizedStatus),
|
|
58
|
+
data,
|
|
59
|
+
headers: headersToRecord(init.headers),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isInternalRouteError(value: unknown): value is InternalRouteError {
|
|
64
|
+
return Boolean(
|
|
65
|
+
value
|
|
66
|
+
&& typeof value === "object"
|
|
67
|
+
&& (value as Partial<InternalRouteError>)[ROUTE_ERROR_SYMBOL] === true
|
|
68
|
+
&& isRouteErrorResponse((value as Partial<InternalRouteError>).response),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function routeError(
|
|
73
|
+
status: number,
|
|
74
|
+
data?: unknown,
|
|
75
|
+
init: {
|
|
76
|
+
statusText?: string;
|
|
77
|
+
headers?: HeadersInit;
|
|
78
|
+
} = {},
|
|
79
|
+
): never {
|
|
80
|
+
const response = createRouteErrorResponse(status, data, init);
|
|
81
|
+
throw {
|
|
82
|
+
[ROUTE_ERROR_SYMBOL]: true,
|
|
83
|
+
response,
|
|
84
|
+
} satisfies InternalRouteError;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function notFound(data?: unknown): never {
|
|
88
|
+
return routeError(404, data, { statusText: "Not Found" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isRouteErrorResponse(value: unknown): value is RouteErrorResponse {
|
|
92
|
+
if (!value || typeof value !== "object") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidate = value as Partial<RouteErrorResponse>;
|
|
97
|
+
return (
|
|
98
|
+
candidate.type === "route_error"
|
|
99
|
+
&& typeof candidate.status === "number"
|
|
100
|
+
&& Number.isFinite(candidate.status)
|
|
101
|
+
&& typeof candidate.statusText === "string"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function toRouteErrorResponse(value: unknown): RouteErrorResponse | null {
|
|
106
|
+
if (isInternalRouteError(value)) {
|
|
107
|
+
return value.response;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isRouteErrorResponse(value)) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (value instanceof Response) {
|
|
115
|
+
if (isRedirectStatus(value.status)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return createRouteErrorResponse(value.status, undefined, {
|
|
120
|
+
statusText: value.statusText,
|
|
121
|
+
headers: value.headers,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sanitizeRouteErrorResponse(
|
|
129
|
+
routeErrorResponse: RouteErrorResponse,
|
|
130
|
+
production: boolean,
|
|
131
|
+
): RouteErrorResponse {
|
|
132
|
+
if (!production || routeErrorResponse.status < 500) {
|
|
133
|
+
return routeErrorResponse;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
...routeErrorResponse,
|
|
138
|
+
statusText: "Internal Server Error",
|
|
139
|
+
data: undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function toRouteErrorHttpResponse(routeErrorResponse: RouteErrorResponse): Response {
|
|
144
|
+
const headers = new Headers(routeErrorResponse.headers);
|
|
145
|
+
const init = {
|
|
146
|
+
status: routeErrorResponse.status,
|
|
147
|
+
statusText: routeErrorResponse.statusText,
|
|
148
|
+
headers,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (routeErrorResponse.data === undefined || routeErrorResponse.data === null) {
|
|
152
|
+
return new Response(null, init);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof routeErrorResponse.data === "string") {
|
|
156
|
+
if (!headers.has("content-type")) {
|
|
157
|
+
headers.set("content-type", "text/plain; charset=utf-8");
|
|
158
|
+
}
|
|
159
|
+
return new Response(routeErrorResponse.data, init);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!headers.has("content-type")) {
|
|
163
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
164
|
+
}
|
|
165
|
+
return new Response(JSON.stringify(routeErrorResponse.data), init);
|
|
166
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { goBack, goForward, reloadPage } from "./navigation-api";
|
|
3
|
+
|
|
4
|
+
export interface RouterNavigateOptions {
|
|
5
|
+
scroll?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Router {
|
|
9
|
+
push(href: string, options?: RouterNavigateOptions): void;
|
|
10
|
+
replace(href: string, options?: RouterNavigateOptions): void;
|
|
11
|
+
prefetch(href: string): void;
|
|
12
|
+
back(): void;
|
|
13
|
+
forward(): void;
|
|
14
|
+
refresh(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toAbsoluteHref(href: string): string {
|
|
18
|
+
if (typeof window === "undefined") {
|
|
19
|
+
return href;
|
|
20
|
+
}
|
|
21
|
+
return new URL(href, window.location.href).toString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SERVER_ROUTER: Router = {
|
|
25
|
+
push: () => undefined,
|
|
26
|
+
replace: () => undefined,
|
|
27
|
+
prefetch: () => undefined,
|
|
28
|
+
back: () => undefined,
|
|
29
|
+
forward: () => undefined,
|
|
30
|
+
refresh: () => undefined,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function createClientRouter(): Router {
|
|
34
|
+
return {
|
|
35
|
+
push: (href, options) => {
|
|
36
|
+
const absoluteHref = toAbsoluteHref(href);
|
|
37
|
+
void import("./client-runtime")
|
|
38
|
+
.then(runtime => runtime.navigateWithNavigationApiOrFallback(absoluteHref, {
|
|
39
|
+
replace: false,
|
|
40
|
+
scroll: options?.scroll,
|
|
41
|
+
}))
|
|
42
|
+
.catch(() => {
|
|
43
|
+
window.location.assign(absoluteHref);
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
replace: (href, options) => {
|
|
47
|
+
const absoluteHref = toAbsoluteHref(href);
|
|
48
|
+
void import("./client-runtime")
|
|
49
|
+
.then(runtime => runtime.navigateWithNavigationApiOrFallback(absoluteHref, {
|
|
50
|
+
replace: true,
|
|
51
|
+
scroll: options?.scroll,
|
|
52
|
+
}))
|
|
53
|
+
.catch(() => {
|
|
54
|
+
window.location.replace(absoluteHref);
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
prefetch: href => {
|
|
58
|
+
const absoluteHref = toAbsoluteHref(href);
|
|
59
|
+
void import("./client-runtime")
|
|
60
|
+
.then(runtime => runtime.prefetchTo(absoluteHref))
|
|
61
|
+
.catch(() => undefined);
|
|
62
|
+
},
|
|
63
|
+
back: () => {
|
|
64
|
+
goBack();
|
|
65
|
+
},
|
|
66
|
+
forward: () => {
|
|
67
|
+
goForward();
|
|
68
|
+
},
|
|
69
|
+
refresh: () => {
|
|
70
|
+
reloadPage();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function useRouter(): Router {
|
|
76
|
+
return useMemo(
|
|
77
|
+
() => (typeof window === "undefined" ? SERVER_ROUTER : createClientRouter()),
|
|
78
|
+
[],
|
|
79
|
+
);
|
|
80
|
+
}
|