@rpcbase/router 0.28.0 → 0.29.0
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/applyRouteLoaders.d.ts +4 -0
- package/dist/applyRouteLoaders.d.ts.map +1 -0
- package/dist/applyRouteLoaders.js +126 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/loadRoute.d.ts +13 -0
- package/dist/loadRoute.d.ts.map +1 -0
- package/dist/loadRoute.js +14 -0
- package/dist/useApplyMeta.d.ts +2 -0
- package/dist/useApplyMeta.d.ts.map +1 -0
- package/dist/useApplyMeta.js +56 -0
- package/package.json +22 -4
- package/src/applyRouteLoaders.ts +0 -166
- package/src/index.ts +0 -4
- package/src/loadRoute.tsx +0 -29
- package/src/useApplyMeta.tsx +0 -73
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyRouteLoaders.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/applyRouteLoaders.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAA;AAE/B,OAAO,EACL,oBAAoB,EAMrB,MAAM,SAAS,CAAA;AAsDhB,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,GAAG,EAAE,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAkG/B"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { matchRoutes, createPath, parsePath, } from "./index";
|
|
2
|
+
function createKey() {
|
|
3
|
+
return Math.random().toString(36).substring(2, 10);
|
|
4
|
+
}
|
|
5
|
+
function createLocation(current, to, state = null, key) {
|
|
6
|
+
const location = {
|
|
7
|
+
pathname: typeof current === "string" ? current : current.pathname,
|
|
8
|
+
search: "",
|
|
9
|
+
hash: "",
|
|
10
|
+
...(typeof to === "string" ? parsePath(to) : to),
|
|
11
|
+
state,
|
|
12
|
+
// TODO: This could be cleaned up. push/replace should probably just take
|
|
13
|
+
// full Locations now and avoid the need to run through this flow at all
|
|
14
|
+
// But that's a pretty big refactor to the current test suite so going to
|
|
15
|
+
// keep as is for the time being and just let any incoming keys take precedence
|
|
16
|
+
key: (to && to.key) || key || createKey(),
|
|
17
|
+
};
|
|
18
|
+
return location;
|
|
19
|
+
}
|
|
20
|
+
function getShortCircuitMatches(routes) {
|
|
21
|
+
// Prefer a root layout route if present, otherwise shim in a route object
|
|
22
|
+
const route = routes.length === 1
|
|
23
|
+
? routes[0]
|
|
24
|
+
: routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
25
|
+
id: "__shim-error-route__",
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
matches: [
|
|
29
|
+
{
|
|
30
|
+
params: {},
|
|
31
|
+
pathname: "",
|
|
32
|
+
pathnameBase: "",
|
|
33
|
+
route,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
route,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function applyRouteLoaders(req, dataRoutes) {
|
|
40
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
41
|
+
const url = new URL(req.originalUrl, baseUrl);
|
|
42
|
+
const method = req.method;
|
|
43
|
+
const location = createLocation("", createPath(url), null, "default");
|
|
44
|
+
const baseContext = {
|
|
45
|
+
basename: "",
|
|
46
|
+
location,
|
|
47
|
+
loaderHeaders: {},
|
|
48
|
+
actionHeaders: {},
|
|
49
|
+
};
|
|
50
|
+
// Match routes to the current location
|
|
51
|
+
const matches = matchRoutes(dataRoutes, location) || [];
|
|
52
|
+
// Handle 404 (no matches)
|
|
53
|
+
if (!matches) {
|
|
54
|
+
const error = {
|
|
55
|
+
status: 404,
|
|
56
|
+
message: `No route matches URL: ${req.originalUrl}`,
|
|
57
|
+
};
|
|
58
|
+
const { matches: notFoundMatches, route } = getShortCircuitMatches(dataRoutes);
|
|
59
|
+
return {
|
|
60
|
+
...baseContext,
|
|
61
|
+
matches: notFoundMatches,
|
|
62
|
+
loaderData: {},
|
|
63
|
+
actionData: null,
|
|
64
|
+
errors: { [route.id]: error },
|
|
65
|
+
statusCode: 404,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Skip if anything but GET
|
|
69
|
+
if (method !== "GET") {
|
|
70
|
+
return {
|
|
71
|
+
...baseContext,
|
|
72
|
+
matches,
|
|
73
|
+
loaderData: {},
|
|
74
|
+
actionData: null,
|
|
75
|
+
errors: null,
|
|
76
|
+
statusCode: 200,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Collect loader data and errors
|
|
80
|
+
const loaderPromisesResults = await Promise.allSettled(matches.map(async (match) => {
|
|
81
|
+
const { route, params } = match;
|
|
82
|
+
if (!route.loader)
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
return {
|
|
86
|
+
id: route.id,
|
|
87
|
+
data: await route.loader({
|
|
88
|
+
params,
|
|
89
|
+
ctx: { req },
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Include the route ID in the error for better traceability
|
|
95
|
+
throw { id: route.id, reason: error };
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
const loaderData = {};
|
|
99
|
+
// TODO: add i18n error handling
|
|
100
|
+
let errors = null;
|
|
101
|
+
for (const result of loaderPromisesResults) {
|
|
102
|
+
if (result.status === "fulfilled") {
|
|
103
|
+
if (result.value) {
|
|
104
|
+
loaderData[result.value.id] = result.value.data;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (result.status === "rejected") {
|
|
108
|
+
const id = result.reason?.id;
|
|
109
|
+
if (!id) {
|
|
110
|
+
throw new Error(`missing route ID in error: ${result.reason}`);
|
|
111
|
+
}
|
|
112
|
+
if (!errors) {
|
|
113
|
+
errors = {};
|
|
114
|
+
}
|
|
115
|
+
errors[id] = result.reason;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
...baseContext,
|
|
120
|
+
matches,
|
|
121
|
+
loaderData,
|
|
122
|
+
actionData: null,
|
|
123
|
+
errors,
|
|
124
|
+
statusCode: Object.keys(errors || {}).length > 0 ? 500 : 200,
|
|
125
|
+
};
|
|
126
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LoaderFunction } from "react-router";
|
|
2
|
+
import { Loader } from "@rpcbase/client";
|
|
3
|
+
type RouteModule = {
|
|
4
|
+
default: React.ComponentType<unknown>;
|
|
5
|
+
loader?: Loader;
|
|
6
|
+
};
|
|
7
|
+
type RouteWithLoader = {
|
|
8
|
+
Component: React.LazyExoticComponent<React.ComponentType<unknown>>;
|
|
9
|
+
loader?: LoaderFunction;
|
|
10
|
+
};
|
|
11
|
+
export declare const loadRoute: (importPromise: Promise<RouteModule>) => RouteWithLoader;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=loadRoute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadRoute.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/loadRoute.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAA;AAGpD,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,KAAK,eAAe,GAAG;IACrB,SAAS,EAAE,KAAK,CAAC,mBAAmB,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;IAClE,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,eAAe,OAAO,CAAC,WAAW,CAAC,KAAG,eAa/D,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
export const loadRoute = (importPromise) => {
|
|
3
|
+
const Component = lazy(async () => {
|
|
4
|
+
const module = await importPromise;
|
|
5
|
+
return { default: module.default };
|
|
6
|
+
});
|
|
7
|
+
const loader = async (args) => {
|
|
8
|
+
const module = await importPromise;
|
|
9
|
+
if (!module.loader)
|
|
10
|
+
return null;
|
|
11
|
+
return module.loader(args);
|
|
12
|
+
};
|
|
13
|
+
return { Component, loader: loader };
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useApplyMeta.d.ts","sourceRoot":"","sources":["../../../../pkg/router/src/useApplyMeta.tsx"],"names":[],"mappings":"AAkBA,eAAO,MAAM,YAAY,YAoExB,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useLocation } from "react-router";
|
|
3
|
+
export const useApplyMeta = () => {
|
|
4
|
+
const location = useLocation();
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const loadMeta = async () => {
|
|
7
|
+
let defaultTitle = "";
|
|
8
|
+
let defaultMeta = [];
|
|
9
|
+
let pagesMeta = {};
|
|
10
|
+
try {
|
|
11
|
+
const importPath = "@/static/meta";
|
|
12
|
+
const module = (await import(importPath));
|
|
13
|
+
defaultTitle = module.defaultTitle || defaultTitle;
|
|
14
|
+
defaultMeta = module.defaultMeta || defaultMeta;
|
|
15
|
+
pagesMeta = module.pagesMeta || pagesMeta;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (import.meta.env.MODE !== "production") {
|
|
19
|
+
console.warn("Failed to load meta data from '@/static/meta'.", error);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let pageMeta = pagesMeta[location.pathname];
|
|
24
|
+
if (!pageMeta) {
|
|
25
|
+
pageMeta = { title: defaultTitle, meta: defaultMeta };
|
|
26
|
+
}
|
|
27
|
+
document.title = pageMeta.title;
|
|
28
|
+
// Remove previous dynamically inserted tags
|
|
29
|
+
document.querySelectorAll("[data-react-meta]").forEach((tag) => tag.remove());
|
|
30
|
+
// Inject new tags
|
|
31
|
+
pageMeta.meta.forEach((meta) => {
|
|
32
|
+
const metaElement = document.createElement("meta");
|
|
33
|
+
metaElement.setAttribute("data-react-meta", "true");
|
|
34
|
+
// Set all attributes from the meta object
|
|
35
|
+
Object.entries(meta).forEach(([key, value]) => {
|
|
36
|
+
if (value) {
|
|
37
|
+
metaElement.setAttribute(key, value.toString());
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
document.head.appendChild(metaElement);
|
|
41
|
+
});
|
|
42
|
+
// Update canonical link
|
|
43
|
+
const canonicalUrl = `${window.location.origin}${location.pathname}`;
|
|
44
|
+
const existingCanonical = document.querySelector("link[rel=\"canonical\"]");
|
|
45
|
+
if (existingCanonical) {
|
|
46
|
+
existingCanonical.remove();
|
|
47
|
+
}
|
|
48
|
+
const canonicalLink = document.createElement("link");
|
|
49
|
+
canonicalLink.setAttribute("rel", "canonical");
|
|
50
|
+
canonicalLink.setAttribute("href", canonicalUrl);
|
|
51
|
+
canonicalLink.setAttribute("data-react-meta", "true");
|
|
52
|
+
document.head.appendChild(canonicalLink);
|
|
53
|
+
};
|
|
54
|
+
loadMeta();
|
|
55
|
+
}, [location.pathname]);
|
|
56
|
+
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
6
10
|
"scripts": {
|
|
7
|
-
"build": "
|
|
11
|
+
"build": "wireit",
|
|
8
12
|
"release": "wireit"
|
|
9
13
|
},
|
|
10
14
|
"wireit": {
|
|
15
|
+
"build": {
|
|
16
|
+
"command": "node ../../scripts/build-package.js router",
|
|
17
|
+
"files": [
|
|
18
|
+
"src/**/*",
|
|
19
|
+
"../../tsconfig.json",
|
|
20
|
+
"../../tsconfig.base.json",
|
|
21
|
+
"../../scripts/build-package.js"
|
|
22
|
+
],
|
|
23
|
+
"output": [
|
|
24
|
+
"dist/"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
11
27
|
"release": {
|
|
12
28
|
"command": "../../scripts/publish.js",
|
|
13
|
-
"dependencies": [
|
|
29
|
+
"dependencies": [
|
|
30
|
+
"build"
|
|
31
|
+
],
|
|
14
32
|
"files": [
|
|
15
33
|
"package.json",
|
|
16
34
|
"src/**/*"
|
package/src/applyRouteLoaders.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {Request} from "express"
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
StaticHandlerContext,
|
|
6
|
-
matchRoutes,
|
|
7
|
-
createPath,
|
|
8
|
-
Location,
|
|
9
|
-
parsePath,
|
|
10
|
-
To,
|
|
11
|
-
} from "./index"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function createKey() {
|
|
15
|
-
return Math.random().toString(36).substring(2, 10)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function createLocation(
|
|
19
|
-
current: string | Location,
|
|
20
|
-
to: To,
|
|
21
|
-
state: any = null,
|
|
22
|
-
key?: string,
|
|
23
|
-
): Readonly<Location> {
|
|
24
|
-
const location: Readonly<Location> = {
|
|
25
|
-
pathname: typeof current === "string" ? current : current.pathname,
|
|
26
|
-
search: "",
|
|
27
|
-
hash: "",
|
|
28
|
-
...(typeof to === "string" ? parsePath(to) : to),
|
|
29
|
-
state,
|
|
30
|
-
// TODO: This could be cleaned up. push/replace should probably just take
|
|
31
|
-
// full Locations now and avoid the need to run through this flow at all
|
|
32
|
-
// But that's a pretty big refactor to the current test suite so going to
|
|
33
|
-
// keep as is for the time being and just let any incoming keys take precedence
|
|
34
|
-
key: (to && (to as Location).key) || key || createKey(),
|
|
35
|
-
}
|
|
36
|
-
return location
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getShortCircuitMatches(routes: any[]): {
|
|
40
|
-
matches: any[];
|
|
41
|
-
route: any;
|
|
42
|
-
} {
|
|
43
|
-
// Prefer a root layout route if present, otherwise shim in a route object
|
|
44
|
-
const route =
|
|
45
|
-
routes.length === 1
|
|
46
|
-
? routes[0]
|
|
47
|
-
: routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
48
|
-
id: "__shim-error-route__",
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
matches: [
|
|
53
|
-
{
|
|
54
|
-
params: {},
|
|
55
|
-
pathname: "",
|
|
56
|
-
pathnameBase: "",
|
|
57
|
-
route,
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
route,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export async function applyRouteLoaders(
|
|
66
|
-
req: Request,
|
|
67
|
-
dataRoutes: any[],
|
|
68
|
-
): Promise<StaticHandlerContext> {
|
|
69
|
-
|
|
70
|
-
const baseUrl = `${req.protocol}://${req.get("host")}`
|
|
71
|
-
const url = new URL(req.originalUrl, baseUrl)
|
|
72
|
-
|
|
73
|
-
const method = req.method
|
|
74
|
-
const location = createLocation("", createPath(url), null, "default")
|
|
75
|
-
|
|
76
|
-
const baseContext = {
|
|
77
|
-
basename: "",
|
|
78
|
-
location,
|
|
79
|
-
loaderHeaders: {},
|
|
80
|
-
actionHeaders: {},
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Match routes to the current location
|
|
84
|
-
const matches = matchRoutes(dataRoutes, location) || []
|
|
85
|
-
|
|
86
|
-
// Handle 404 (no matches)
|
|
87
|
-
if (!matches) {
|
|
88
|
-
const error = {
|
|
89
|
-
status: 404,
|
|
90
|
-
message: `No route matches URL: ${req.originalUrl}`,
|
|
91
|
-
}
|
|
92
|
-
const { matches: notFoundMatches, route } = getShortCircuitMatches(dataRoutes)
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
...baseContext,
|
|
96
|
-
matches: notFoundMatches,
|
|
97
|
-
loaderData: {},
|
|
98
|
-
actionData: null,
|
|
99
|
-
errors: { [route.id]: error },
|
|
100
|
-
statusCode: 404,
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Skip if anything but GET
|
|
105
|
-
if (method !== "GET") {
|
|
106
|
-
return {
|
|
107
|
-
...baseContext,
|
|
108
|
-
matches,
|
|
109
|
-
loaderData: {},
|
|
110
|
-
actionData: null,
|
|
111
|
-
errors: null,
|
|
112
|
-
statusCode: 200,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Collect loader data and errors
|
|
117
|
-
const loaderPromisesResults = await Promise.allSettled(
|
|
118
|
-
matches.map(async (match) => {
|
|
119
|
-
const { route, params } = match
|
|
120
|
-
if (!route.loader) return null
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
return {
|
|
124
|
-
id: route.id,
|
|
125
|
-
data: await route.loader({
|
|
126
|
-
params,
|
|
127
|
-
ctx: {req},
|
|
128
|
-
}),
|
|
129
|
-
}
|
|
130
|
-
} catch (error) {
|
|
131
|
-
// Include the route ID in the error for better traceability
|
|
132
|
-
throw { id: route.id, reason: error }
|
|
133
|
-
}
|
|
134
|
-
}),
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
const loaderData: Record<string, any> = {}
|
|
138
|
-
// TODO: add i18n error handling
|
|
139
|
-
let errors: Record<string, any> | null = null
|
|
140
|
-
|
|
141
|
-
for (const result of loaderPromisesResults) {
|
|
142
|
-
if (result.status === "fulfilled") {
|
|
143
|
-
if (result.value) {
|
|
144
|
-
loaderData[result.value.id] = result.value.data
|
|
145
|
-
}
|
|
146
|
-
} else if (result.status === "rejected") {
|
|
147
|
-
const id = result.reason?.id
|
|
148
|
-
if (!id) {
|
|
149
|
-
throw new Error(`missing route ID in error: ${result.reason}`)
|
|
150
|
-
}
|
|
151
|
-
if (!errors) {
|
|
152
|
-
errors = {}
|
|
153
|
-
}
|
|
154
|
-
errors[id] = result.reason
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
...baseContext,
|
|
160
|
-
matches,
|
|
161
|
-
loaderData,
|
|
162
|
-
actionData: null,
|
|
163
|
-
errors,
|
|
164
|
-
statusCode: Object.keys(errors || {}).length > 0 ? 500 : 200,
|
|
165
|
-
}
|
|
166
|
-
}
|
package/src/index.ts
DELETED
package/src/loadRoute.tsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { lazy } from "react"
|
|
2
|
-
import { LoaderFunction } from "react-router"
|
|
3
|
-
import { Loader, LoaderArgs } from "@rpcbase/client"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type RouteModule = {
|
|
7
|
-
default: React.ComponentType<unknown>
|
|
8
|
-
loader?: Loader
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type RouteWithLoader = {
|
|
12
|
-
Component: React.LazyExoticComponent<React.ComponentType<unknown>>
|
|
13
|
-
loader?: LoaderFunction
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const loadRoute = (importPromise: Promise<RouteModule>): RouteWithLoader => {
|
|
17
|
-
const Component = lazy(async () => {
|
|
18
|
-
const module = await importPromise
|
|
19
|
-
return { default: module.default }
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const loader = async (args: LoaderArgs) => {
|
|
23
|
-
const module = await importPromise
|
|
24
|
-
if (!module.loader) return null
|
|
25
|
-
return module.loader(args)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return { Component, loader: loader as unknown as LoaderFunction }
|
|
29
|
-
}
|
package/src/useApplyMeta.tsx
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { useEffect } from "react"
|
|
2
|
-
import { useLocation } from "react-router"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const useApplyMeta = () => {
|
|
6
|
-
const location = useLocation()
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const loadMeta = async () => {
|
|
10
|
-
let defaultTitle = ""
|
|
11
|
-
let defaultMeta = []
|
|
12
|
-
let pagesMeta = {}
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const importPath = "@/static/meta"
|
|
16
|
-
const module = await import(importPath)
|
|
17
|
-
defaultTitle = module.defaultTitle || defaultTitle
|
|
18
|
-
defaultMeta = module.defaultMeta || defaultMeta
|
|
19
|
-
pagesMeta = module.pagesMeta || pagesMeta
|
|
20
|
-
} catch (error) {
|
|
21
|
-
if (import.meta.env.MODE !== "production") {
|
|
22
|
-
console.warn(
|
|
23
|
-
"Failed to load meta data from '@/static/meta'.",
|
|
24
|
-
error
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let pageMeta = pagesMeta[location.pathname]
|
|
31
|
-
|
|
32
|
-
if (!pageMeta) {
|
|
33
|
-
pageMeta = { title: defaultTitle, meta: defaultMeta }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
document.title = pageMeta.title
|
|
37
|
-
|
|
38
|
-
// Remove previous dynamically inserted tags
|
|
39
|
-
document.querySelectorAll("[data-react-meta]").forEach((tag) => tag.remove())
|
|
40
|
-
|
|
41
|
-
// Inject new tags
|
|
42
|
-
pageMeta.meta.forEach((meta) => {
|
|
43
|
-
const metaElement = document.createElement("meta")
|
|
44
|
-
metaElement.setAttribute("data-react-meta", "true")
|
|
45
|
-
|
|
46
|
-
// Set all attributes from the meta object
|
|
47
|
-
Object.entries(meta).forEach(([key, value]) => {
|
|
48
|
-
if (value) {
|
|
49
|
-
metaElement.setAttribute(key, value.toString())
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
document.head.appendChild(metaElement)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
// Update canonical link
|
|
57
|
-
const canonicalUrl = `${window.location.origin}${location.pathname}`
|
|
58
|
-
|
|
59
|
-
const existingCanonical = document.querySelector("link[rel=\"canonical\"]")
|
|
60
|
-
if (existingCanonical) {
|
|
61
|
-
existingCanonical.remove()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const canonicalLink = document.createElement("link")
|
|
65
|
-
canonicalLink.setAttribute("rel", "canonical")
|
|
66
|
-
canonicalLink.setAttribute("href", canonicalUrl)
|
|
67
|
-
canonicalLink.setAttribute("data-react-meta", "true")
|
|
68
|
-
document.head.appendChild(canonicalLink)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
loadMeta()
|
|
72
|
-
}, [location.pathname])
|
|
73
|
-
}
|