@richie-router/server 0.1.3 → 0.1.5
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/dist/cjs/index.cjs +287 -46
- package/dist/cjs/index.test.cjs +335 -0
- package/dist/esm/index.mjs +288 -46
- package/dist/esm/index.test.mjs +335 -0
- package/dist/types/index.d.ts +40 -2
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -39,8 +39,11 @@ var __export = (target, all) => {
|
|
|
39
39
|
// packages/server/src/index.ts
|
|
40
40
|
var exports_src = {};
|
|
41
41
|
__export(exports_src, {
|
|
42
|
+
matchesSpaPath: () => matchesSpaPath,
|
|
43
|
+
handleSpaRequest: () => handleSpaRequest,
|
|
42
44
|
handleRequest: () => handleRequest,
|
|
43
45
|
handleHeadTagRequest: () => handleHeadTagRequest,
|
|
46
|
+
handleHeadRequest: () => handleHeadRequest,
|
|
44
47
|
defineHeadTags: () => defineHeadTags
|
|
45
48
|
});
|
|
46
49
|
module.exports = __toCommonJS(exports_src);
|
|
@@ -54,32 +57,143 @@ function defineHeadTags(routeManifest, routerSchema, definitions) {
|
|
|
54
57
|
}
|
|
55
58
|
var HEAD_PLACEHOLDER = "<!--richie-router-head-->";
|
|
56
59
|
var MANAGED_HEAD_ATTRIBUTE = "data-richie-router-head";
|
|
60
|
+
var HEAD_RESPONSE_KIND_HEADER = "x-richie-router-head";
|
|
61
|
+
var EMPTY_HEAD = [];
|
|
62
|
+
function ensureLeadingSlash(value) {
|
|
63
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
64
|
+
}
|
|
65
|
+
function normalizeBasePath(basePath) {
|
|
66
|
+
if (!basePath) {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
const trimmed = basePath.trim();
|
|
70
|
+
if (trimmed === "" || trimmed === "/") {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
const normalized = ensureLeadingSlash(trimmed).replace(/\/+$/u, "");
|
|
74
|
+
return normalized === "/" ? "" : normalized;
|
|
75
|
+
}
|
|
76
|
+
function stripBasePathFromPathname(pathname, basePath) {
|
|
77
|
+
if (!basePath) {
|
|
78
|
+
return pathname;
|
|
79
|
+
}
|
|
80
|
+
if (pathname === basePath) {
|
|
81
|
+
return "/";
|
|
82
|
+
}
|
|
83
|
+
if (pathname.startsWith(`${basePath}/`)) {
|
|
84
|
+
return pathname.slice(basePath.length) || "/";
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function prependBasePathToPathname(pathname, basePath) {
|
|
89
|
+
if (!basePath) {
|
|
90
|
+
return pathname;
|
|
91
|
+
}
|
|
92
|
+
return pathname === "/" ? basePath : `${basePath}${ensureLeadingSlash(pathname)}`;
|
|
93
|
+
}
|
|
57
94
|
function routeHasRecord(value) {
|
|
58
95
|
return typeof value === "object" && value !== null;
|
|
59
96
|
}
|
|
60
|
-
function createHeadSnapshotScript(href, head) {
|
|
61
|
-
const payload = JSON.stringify({ href, head }).replaceAll("</script>", "<\\/script>");
|
|
97
|
+
function createHeadSnapshotScript(href, head, routeHeads) {
|
|
98
|
+
const payload = JSON.stringify({ href, head, routeHeads }).replaceAll("</script>", "<\\/script>");
|
|
62
99
|
return `<script ${MANAGED_HEAD_ATTRIBUTE}="true">window.__RICHIE_ROUTER_HEAD__=${payload}</script>`;
|
|
63
100
|
}
|
|
64
|
-
|
|
101
|
+
function createRichieRouterHead(href, head, routeHeads) {
|
|
102
|
+
const headHtml = import_core.serializeHeadConfig(head, {
|
|
103
|
+
managedAttribute: MANAGED_HEAD_ATTRIBUTE
|
|
104
|
+
});
|
|
105
|
+
return `${headHtml}${createHeadSnapshotScript(href, head, routeHeads)}`;
|
|
106
|
+
}
|
|
107
|
+
async function renderTemplate(html, ctx, options) {
|
|
65
108
|
const template = html.template;
|
|
66
109
|
if (typeof template === "function") {
|
|
67
110
|
return await template(ctx);
|
|
68
111
|
}
|
|
69
112
|
if (!template.includes(HEAD_PLACEHOLDER)) {
|
|
113
|
+
if (options?.requireHeadPlaceholder === false) {
|
|
114
|
+
return template;
|
|
115
|
+
}
|
|
70
116
|
throw new Error(`HTML template is missing required Richie Router placeholder: ${HEAD_PLACEHOLDER}`);
|
|
71
117
|
}
|
|
72
118
|
return template.replace(HEAD_PLACEHOLDER, ctx.richieRouterHead);
|
|
73
119
|
}
|
|
74
120
|
function jsonResponse(data, init) {
|
|
121
|
+
const headers = new Headers(init?.headers);
|
|
122
|
+
if (!headers.has("content-type")) {
|
|
123
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
124
|
+
}
|
|
75
125
|
return new Response(JSON.stringify(data), {
|
|
76
126
|
...init,
|
|
77
|
-
headers
|
|
78
|
-
"content-type": "application/json; charset=utf-8",
|
|
79
|
-
...init?.headers ?? {}
|
|
80
|
-
}
|
|
127
|
+
headers
|
|
81
128
|
});
|
|
82
129
|
}
|
|
130
|
+
function notFoundResult() {
|
|
131
|
+
return {
|
|
132
|
+
matched: false,
|
|
133
|
+
response: new Response("Not Found", { status: 404 })
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function htmlResponse(html, headers) {
|
|
137
|
+
const responseHeaders = new Headers(headers);
|
|
138
|
+
if (!responseHeaders.has("content-type")) {
|
|
139
|
+
responseHeaders.set("content-type", "text/html; charset=utf-8");
|
|
140
|
+
}
|
|
141
|
+
return new Response(html, {
|
|
142
|
+
status: 200,
|
|
143
|
+
headers: responseHeaders
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function withResponseHeaders(response, headersToSet) {
|
|
147
|
+
const headers = new Headers(response.headers);
|
|
148
|
+
new Headers(headersToSet).forEach((value, key) => {
|
|
149
|
+
headers.set(key, value);
|
|
150
|
+
});
|
|
151
|
+
return new Response(response.body, {
|
|
152
|
+
status: response.status,
|
|
153
|
+
statusText: response.statusText,
|
|
154
|
+
headers
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function formatHeadCacheControl(staleTime) {
|
|
158
|
+
if (staleTime === undefined) {
|
|
159
|
+
return "private, no-store";
|
|
160
|
+
}
|
|
161
|
+
return `private, max-age=${Math.max(0, Math.floor(staleTime / 1000))}`;
|
|
162
|
+
}
|
|
163
|
+
function createHeadResponseHeaders(kind, staleTime, headers) {
|
|
164
|
+
const responseHeaders = new Headers(headers);
|
|
165
|
+
responseHeaders.set(HEAD_RESPONSE_KIND_HEADER, kind);
|
|
166
|
+
responseHeaders.set("cache-control", formatHeadCacheControl(staleTime));
|
|
167
|
+
return responseHeaders;
|
|
168
|
+
}
|
|
169
|
+
function resolveDocumentRequest(request, basePathOption) {
|
|
170
|
+
return resolveDocumentPath(request.url, basePathOption);
|
|
171
|
+
}
|
|
172
|
+
function resolveDocumentPath(path, basePathOption) {
|
|
173
|
+
const url = new URL(path, "http://richie-router.local");
|
|
174
|
+
const basePath = normalizeBasePath(basePathOption);
|
|
175
|
+
const strippedPathname = stripBasePathFromPathname(url.pathname, basePath);
|
|
176
|
+
if (strippedPathname === null) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
basePath,
|
|
181
|
+
location: import_core.createParsedLocation(`${strippedPathname}${url.search}${url.hash}`, null, import_core.defaultParseSearch)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function renderDocumentResponse(request, html, richieRouterHead, head, options) {
|
|
185
|
+
const template = await renderTemplate(html, {
|
|
186
|
+
request,
|
|
187
|
+
richieRouterHead,
|
|
188
|
+
head
|
|
189
|
+
}, {
|
|
190
|
+
requireHeadPlaceholder: options?.requireHeadPlaceholder
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
matched: true,
|
|
194
|
+
response: htmlResponse(template, options?.headers)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
83
197
|
function resolveSearch(route, rawSearch) {
|
|
84
198
|
const fromSchema = route.searchSchema ? route.searchSchema.parse(rawSearch) : {};
|
|
85
199
|
if (routeHasRecord(fromSchema)) {
|
|
@@ -112,6 +226,29 @@ function buildMatches(routeManifest, location) {
|
|
|
112
226
|
};
|
|
113
227
|
});
|
|
114
228
|
}
|
|
229
|
+
function resolveSpaRoutes(spaRoutesManifest) {
|
|
230
|
+
if (!routeHasRecord(spaRoutesManifest)) {
|
|
231
|
+
throw new Error("Invalid spaRoutesManifest: expected an object.");
|
|
232
|
+
}
|
|
233
|
+
const { spaRoutes } = spaRoutesManifest;
|
|
234
|
+
if (!Array.isArray(spaRoutes) || spaRoutes.some((route) => typeof route !== "string")) {
|
|
235
|
+
throw new Error('Invalid spaRoutesManifest: expected "spaRoutes" to be an array of strings.');
|
|
236
|
+
}
|
|
237
|
+
return spaRoutes;
|
|
238
|
+
}
|
|
239
|
+
function matchesSpaLocation(options, location) {
|
|
240
|
+
if ("routeManifest" in options) {
|
|
241
|
+
return buildMatches(options.routeManifest, location).length > 0;
|
|
242
|
+
}
|
|
243
|
+
return resolveSpaRoutes(options.spaRoutesManifest).some((route) => import_core.matchPathname(route, location.pathname) !== null);
|
|
244
|
+
}
|
|
245
|
+
function matchesSpaPath(path, options) {
|
|
246
|
+
const documentRequest = resolveDocumentPath(path, options.basePath);
|
|
247
|
+
if (documentRequest === null) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return matchesSpaLocation(options, documentRequest.location);
|
|
251
|
+
}
|
|
115
252
|
async function executeHeadTag(request, headTags, routeId, params, rawSearch) {
|
|
116
253
|
const definition = headTags.definitions[routeId];
|
|
117
254
|
const schemaEntry = headTags.routerSchema[routeId];
|
|
@@ -131,29 +268,132 @@ async function executeHeadTag(request, headTags, routeId, params, rawSearch) {
|
|
|
131
268
|
}
|
|
132
269
|
async function resolveMatchedHead(request, headTags, matches) {
|
|
133
270
|
const resolvedHeadByRoute = new Map;
|
|
271
|
+
const routeHeads = [];
|
|
272
|
+
let staleTime;
|
|
134
273
|
for (const match of matches) {
|
|
135
274
|
if (!match.route.serverHead) {
|
|
136
275
|
continue;
|
|
137
276
|
}
|
|
138
277
|
const result = await executeHeadTag(request, headTags, match.route.fullPath, match.params, match.search);
|
|
139
278
|
resolvedHeadByRoute.set(match.route.fullPath, result.head);
|
|
279
|
+
routeHeads.push({
|
|
280
|
+
routeId: match.route.fullPath,
|
|
281
|
+
head: result.head,
|
|
282
|
+
staleTime: result.staleTime
|
|
283
|
+
});
|
|
284
|
+
if (result.staleTime !== undefined) {
|
|
285
|
+
staleTime = staleTime === undefined ? result.staleTime : Math.min(staleTime, result.staleTime);
|
|
286
|
+
}
|
|
140
287
|
}
|
|
141
|
-
return
|
|
288
|
+
return {
|
|
289
|
+
head: import_core.resolveHeadConfig(matches, resolvedHeadByRoute),
|
|
290
|
+
routeHeads,
|
|
291
|
+
staleTime
|
|
292
|
+
};
|
|
142
293
|
}
|
|
143
|
-
|
|
294
|
+
function createDocumentHeadRequest(sourceRequest, href) {
|
|
295
|
+
const requestUrl = new URL(sourceRequest.url);
|
|
296
|
+
const targetUrl = new URL(href, requestUrl);
|
|
297
|
+
return new Request(targetUrl.toString(), {
|
|
298
|
+
method: "GET",
|
|
299
|
+
headers: sourceRequest.headers
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async function handleDocumentHeadRequest(request, options, href) {
|
|
303
|
+
const documentRequest = createDocumentHeadRequest(request, href);
|
|
304
|
+
const resolvedDocumentRequest = resolveDocumentRequest(documentRequest, options.basePath);
|
|
305
|
+
if (resolvedDocumentRequest === null) {
|
|
306
|
+
return {
|
|
307
|
+
matched: true,
|
|
308
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
309
|
+
status: 404,
|
|
310
|
+
headers: createHeadResponseHeaders("document")
|
|
311
|
+
})
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const matches = buildMatches(options.headTags.routeManifest, resolvedDocumentRequest.location);
|
|
316
|
+
if (matches.length === 0) {
|
|
317
|
+
return {
|
|
318
|
+
matched: true,
|
|
319
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
320
|
+
status: 404,
|
|
321
|
+
headers: createHeadResponseHeaders("document")
|
|
322
|
+
})
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const { head, routeHeads, staleTime } = await resolveMatchedHead(documentRequest, options.headTags, matches);
|
|
326
|
+
return {
|
|
327
|
+
matched: true,
|
|
328
|
+
response: jsonResponse({
|
|
329
|
+
href: resolvedDocumentRequest.location.href,
|
|
330
|
+
head,
|
|
331
|
+
routeHeads,
|
|
332
|
+
staleTime,
|
|
333
|
+
richieRouterHead: createRichieRouterHead(resolvedDocumentRequest.location.href, head, routeHeads)
|
|
334
|
+
}, {
|
|
335
|
+
headers: createHeadResponseHeaders("document", staleTime)
|
|
336
|
+
})
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (import_core.isRedirect(error)) {
|
|
340
|
+
const redirectPath = prependBasePathToPathname(import_core.buildPath(error.options.to, error.options.params ?? {}), resolvedDocumentRequest.basePath);
|
|
341
|
+
const redirectSearch = import_core.defaultStringifySearch(error.options.search === true ? {} : error.options.search ?? {});
|
|
342
|
+
const redirectHash = error.options.hash ? `#${error.options.hash.replace(/^#/, "")}` : "";
|
|
343
|
+
const redirectUrl = `${redirectPath}${redirectSearch}${redirectHash}`;
|
|
344
|
+
return {
|
|
345
|
+
matched: true,
|
|
346
|
+
response: new Response(null, {
|
|
347
|
+
status: error.options.replace ? 307 : 302,
|
|
348
|
+
headers: createHeadResponseHeaders("document", undefined, {
|
|
349
|
+
location: redirectUrl
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (error instanceof Response) {
|
|
355
|
+
return {
|
|
356
|
+
matched: true,
|
|
357
|
+
response: withResponseHeaders(error, createHeadResponseHeaders("document"))
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (import_core.isNotFound(error)) {
|
|
361
|
+
return {
|
|
362
|
+
matched: true,
|
|
363
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
364
|
+
status: 404,
|
|
365
|
+
headers: createHeadResponseHeaders("document")
|
|
366
|
+
})
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function handleHeadRequest(request, options) {
|
|
144
373
|
const url = new URL(request.url);
|
|
145
|
-
const
|
|
374
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
375
|
+
const headBasePath = options.headBasePath ?? prependBasePathToPathname("/head-api", basePath);
|
|
146
376
|
if (url.pathname !== headBasePath) {
|
|
147
377
|
return {
|
|
148
378
|
matched: false,
|
|
149
379
|
response: new Response("Not Found", { status: 404 })
|
|
150
380
|
};
|
|
151
381
|
}
|
|
382
|
+
const href = url.searchParams.get("href");
|
|
383
|
+
if (href !== null) {
|
|
384
|
+
return await handleDocumentHeadRequest(request, {
|
|
385
|
+
...options,
|
|
386
|
+
basePath
|
|
387
|
+
}, href);
|
|
388
|
+
}
|
|
152
389
|
const routeId = url.searchParams.get("routeId");
|
|
153
390
|
if (!routeId) {
|
|
154
391
|
return {
|
|
155
392
|
matched: true,
|
|
156
|
-
response: jsonResponse({ message: "Missing routeId" }, {
|
|
393
|
+
response: jsonResponse({ message: "Missing routeId" }, {
|
|
394
|
+
status: 400,
|
|
395
|
+
headers: createHeadResponseHeaders("route")
|
|
396
|
+
})
|
|
157
397
|
};
|
|
158
398
|
}
|
|
159
399
|
const params = JSON.parse(url.searchParams.get("params") ?? "{}");
|
|
@@ -162,71 +402,72 @@ async function handleHeadTagRequest(request, options) {
|
|
|
162
402
|
const result = await executeHeadTag(request, options.headTags, routeId, params, search);
|
|
163
403
|
return {
|
|
164
404
|
matched: true,
|
|
165
|
-
response: jsonResponse(result
|
|
405
|
+
response: jsonResponse(result, {
|
|
406
|
+
headers: createHeadResponseHeaders("route", result.staleTime)
|
|
407
|
+
})
|
|
166
408
|
};
|
|
167
409
|
} catch (error) {
|
|
168
410
|
if (error instanceof Response) {
|
|
169
411
|
return {
|
|
170
412
|
matched: true,
|
|
171
|
-
response: error
|
|
413
|
+
response: withResponseHeaders(error, createHeadResponseHeaders("route"))
|
|
172
414
|
};
|
|
173
415
|
}
|
|
174
416
|
if (import_core.isNotFound(error)) {
|
|
175
417
|
return {
|
|
176
418
|
matched: true,
|
|
177
|
-
response: jsonResponse({ message: "Not Found" }, {
|
|
419
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
420
|
+
status: 404,
|
|
421
|
+
headers: createHeadResponseHeaders("route")
|
|
422
|
+
})
|
|
178
423
|
};
|
|
179
424
|
}
|
|
180
425
|
throw error;
|
|
181
426
|
}
|
|
182
427
|
}
|
|
428
|
+
async function handleHeadTagRequest(request, options) {
|
|
429
|
+
return await handleHeadRequest(request, options);
|
|
430
|
+
}
|
|
431
|
+
async function handleSpaRequest(request, options) {
|
|
432
|
+
const documentRequest = resolveDocumentRequest(request, options.basePath);
|
|
433
|
+
if (documentRequest === null) {
|
|
434
|
+
return notFoundResult();
|
|
435
|
+
}
|
|
436
|
+
if (!matchesSpaLocation(options, documentRequest.location)) {
|
|
437
|
+
return notFoundResult();
|
|
438
|
+
}
|
|
439
|
+
return await renderDocumentResponse(request, options.html, "", EMPTY_HEAD, {
|
|
440
|
+
headers: options.headers,
|
|
441
|
+
requireHeadPlaceholder: false
|
|
442
|
+
});
|
|
443
|
+
}
|
|
183
444
|
async function handleRequest(request, options) {
|
|
184
|
-
const
|
|
185
|
-
const routeBasePath = options.routeBasePath ?? "/";
|
|
445
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
186
446
|
const handledHeadTagRequest = await handleHeadTagRequest(request, {
|
|
187
447
|
headTags: options.headTags,
|
|
448
|
+
basePath,
|
|
188
449
|
headBasePath: options.headBasePath
|
|
189
450
|
});
|
|
190
451
|
if (handledHeadTagRequest.matched) {
|
|
191
452
|
return handledHeadTagRequest;
|
|
192
453
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
response: new Response("Not Found", { status: 404 })
|
|
197
|
-
};
|
|
454
|
+
const documentRequest = resolveDocumentRequest(request, basePath);
|
|
455
|
+
if (documentRequest === null) {
|
|
456
|
+
return notFoundResult();
|
|
198
457
|
}
|
|
199
|
-
const
|
|
200
|
-
const matches = buildMatches(options.routeManifest, location);
|
|
458
|
+
const matches = buildMatches(options.routeManifest, documentRequest.location);
|
|
201
459
|
if (matches.length === 0) {
|
|
202
|
-
return
|
|
203
|
-
matched: false,
|
|
204
|
-
response: new Response("Not Found", { status: 404 })
|
|
205
|
-
};
|
|
460
|
+
return notFoundResult();
|
|
206
461
|
}
|
|
207
462
|
try {
|
|
208
|
-
const head = await resolveMatchedHead(request, options.headTags, matches);
|
|
209
|
-
const
|
|
210
|
-
|
|
463
|
+
const { head, routeHeads } = await resolveMatchedHead(request, options.headTags, matches);
|
|
464
|
+
const richieRouterHead = createRichieRouterHead(documentRequest.location.href, head, routeHeads);
|
|
465
|
+
return await renderDocumentResponse(request, options.html, richieRouterHead, head, {
|
|
466
|
+
headers: options.headers
|
|
211
467
|
});
|
|
212
|
-
const richieRouterHead = `${headHtml}${createHeadSnapshotScript(location.href, head)}`;
|
|
213
|
-
const html = await renderTemplate(options.html, {
|
|
214
|
-
request,
|
|
215
|
-
richieRouterHead,
|
|
216
|
-
head
|
|
217
|
-
});
|
|
218
|
-
return {
|
|
219
|
-
matched: true,
|
|
220
|
-
response: new Response(html, {
|
|
221
|
-
status: 200,
|
|
222
|
-
headers: {
|
|
223
|
-
"content-type": "text/html; charset=utf-8"
|
|
224
|
-
}
|
|
225
|
-
})
|
|
226
|
-
};
|
|
227
468
|
} catch (error) {
|
|
228
469
|
if (import_core.isRedirect(error)) {
|
|
229
|
-
const redirectPath = import_core.buildPath(error.options.to, error.options.params ?? {});
|
|
470
|
+
const redirectPath = prependBasePathToPathname(import_core.buildPath(error.options.to, error.options.params ?? {}), documentRequest.basePath);
|
|
230
471
|
const redirectSearch = import_core.defaultStringifySearch(error.options.search === true ? {} : error.options.search ?? {});
|
|
231
472
|
const redirectHash = error.options.hash ? `#${error.options.hash.replace(/^#/, "")}` : "";
|
|
232
473
|
const redirectUrl = `${redirectPath}${redirectSearch}${redirectHash}`;
|