@docusaurus/core 3.1.0 → 3.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/lib/client/BrokenLinksContext.js +2 -2
- package/lib/client/exports/Link.js +10 -2
- package/lib/server/brokenLinks.js +124 -37
- package/lib/server/getHostPort.js +4 -1
- package/lib/server/routes.js +9 -1
- package/package.json +10 -10
|
@@ -11,10 +11,10 @@ export const createStatefulBrokenLinks = () => {
|
|
|
11
11
|
const allLinks = new Set();
|
|
12
12
|
return {
|
|
13
13
|
collectAnchor: (anchor) => {
|
|
14
|
-
allAnchors.add(anchor);
|
|
14
|
+
typeof anchor !== 'undefined' && allAnchors.add(anchor);
|
|
15
15
|
},
|
|
16
16
|
collectLink: (link) => {
|
|
17
|
-
allLinks.add(link);
|
|
17
|
+
typeof link !== 'undefined' && allLinks.add(link);
|
|
18
18
|
},
|
|
19
19
|
getCollectedAnchors: () => [...allAnchors],
|
|
20
20
|
getCollectedLinks: () => [...allLinks],
|
|
@@ -97,11 +97,19 @@ function Link({ isNavLink, to, href, activeClassName, isActive, 'data-noBrokenLi
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
}, [ioRef, targetLink, IOSupported, isInternal]);
|
|
100
|
+
// It is simple local anchor link targeting current page?
|
|
100
101
|
const isAnchorLink = targetLink?.startsWith('#') ?? false;
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
// See also RR logic:
|
|
103
|
+
// https://github.com/remix-run/react-router/blob/v5/packages/react-router-dom/modules/Link.js#L47
|
|
104
|
+
const hasInternalTarget = !props.target || props.target === '_self';
|
|
105
|
+
// Should we use a regular <a> tag instead of React-Router Link component?
|
|
106
|
+
const isRegularHtmlLink = !targetLink || !isInternal || !hasInternalTarget || isAnchorLink;
|
|
107
|
+
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
|
103
108
|
brokenLinks.collectLink(targetLink);
|
|
104
109
|
}
|
|
110
|
+
if (props.id) {
|
|
111
|
+
brokenLinks.collectAnchor(props.id);
|
|
112
|
+
}
|
|
105
113
|
return isRegularHtmlLink ? (
|
|
106
114
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
|
|
107
115
|
<a ref={innerRef} href={targetLink} {...(targetLinkUnprefixed &&
|
|
@@ -13,16 +13,58 @@ const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
|
|
|
13
13
|
const react_router_config_1 = require("react-router-config");
|
|
14
14
|
const utils_1 = require("@docusaurus/utils");
|
|
15
15
|
const utils_2 = require("./utils");
|
|
16
|
-
function
|
|
17
|
-
//
|
|
16
|
+
function matchRoutes(routeConfig, pathname) {
|
|
17
|
+
// @ts-expect-error: React router types RouteConfig with an actual React
|
|
18
|
+
// component, but we load route components with string paths.
|
|
19
|
+
// We don't actually access component here, so it's fine.
|
|
20
|
+
return (0, react_router_config_1.matchRoutes)(routeConfig, pathname);
|
|
21
|
+
}
|
|
22
|
+
function createBrokenLinksHelper({ collectedLinks, routes, }) {
|
|
23
|
+
const validPathnames = new Set(collectedLinks.keys());
|
|
24
|
+
// IMPORTANT: this is an optimization
|
|
25
|
+
// See https://github.com/facebook/docusaurus/issues/9754
|
|
26
|
+
// Matching against the route array can be expensive
|
|
27
|
+
// If the route is already in the valid pathnames,
|
|
28
|
+
// we can avoid matching against it
|
|
29
|
+
const remainingRoutes = (function filterRoutes() {
|
|
30
|
+
// Goal: unit tests should behave the same with this enabled or disabled
|
|
31
|
+
const disableOptimization = false;
|
|
32
|
+
if (disableOptimization) {
|
|
33
|
+
return routes;
|
|
34
|
+
}
|
|
35
|
+
// We must consider the "exact" and "strict" match attribute
|
|
36
|
+
// We can only infer pre-validated pathnames from a route from exact routes
|
|
37
|
+
const [validPathnameRoutes, otherRoutes] = lodash_1.default.partition(routes, (route) => route.exact && validPathnames.has(route.path));
|
|
38
|
+
// If a route is non-strict (non-sensitive to trailing slashes)
|
|
39
|
+
// We must pre-validate all possible paths
|
|
40
|
+
validPathnameRoutes.forEach((validPathnameRoute) => {
|
|
41
|
+
if (!validPathnameRoute.strict) {
|
|
42
|
+
validPathnames.add((0, utils_1.addTrailingSlash)(validPathnameRoute.path));
|
|
43
|
+
validPathnames.add((0, utils_1.removeTrailingSlash)(validPathnameRoute.path));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return otherRoutes;
|
|
47
|
+
})();
|
|
48
|
+
function isPathnameMatchingAnyRoute(pathname) {
|
|
49
|
+
if (matchRoutes(remainingRoutes, pathname).length > 0) {
|
|
50
|
+
// IMPORTANT: this is an optimization
|
|
51
|
+
// See https://github.com/facebook/docusaurus/issues/9754
|
|
52
|
+
// Large Docusaurus sites have many routes!
|
|
53
|
+
// We try to minimize calls to a possibly expensive matchRoutes function
|
|
54
|
+
validPathnames.add(pathname);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
18
59
|
function isPathBrokenLink(linkPath) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
const pathnames = [linkPath.pathname, decodeURI(linkPath.pathname)];
|
|
61
|
+
if (pathnames.some((p) => validPathnames.has(p))) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (pathnames.some(isPathnameMatchingAnyRoute)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
26
68
|
}
|
|
27
69
|
function isAnchorBrokenLink(linkPath) {
|
|
28
70
|
const { pathname, hash } = linkPath;
|
|
@@ -30,37 +72,50 @@ function getBrokenLinksForPage({ collectedLinks, pagePath, pageLinks, routes, })
|
|
|
30
72
|
if (hash === undefined) {
|
|
31
73
|
return false;
|
|
32
74
|
}
|
|
33
|
-
|
|
75
|
+
// Link has empty hash ("#", "/page#"...): we do not report it as broken
|
|
76
|
+
// Empty hashes are used for various weird reasons, by us and other users...
|
|
77
|
+
// See for example: https://github.com/facebook/docusaurus/pull/6003
|
|
78
|
+
if (hash === '') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const targetPage = collectedLinks.get(pathname) || collectedLinks.get(decodeURI(pathname));
|
|
34
82
|
// link with anchor to a page that does not exist (or did not collect any
|
|
35
83
|
// link/anchor) is considered as a broken anchor
|
|
36
84
|
if (!targetPage) {
|
|
37
85
|
return true;
|
|
38
86
|
}
|
|
39
|
-
// it's a broken anchor if the target page
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
// it's a not broken anchor if the anchor exists on the target page
|
|
88
|
+
if (targetPage.anchors.has(hash) ||
|
|
89
|
+
targetPage.anchors.has(decodeURIComponent(hash))) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
42
93
|
}
|
|
43
|
-
|
|
94
|
+
return {
|
|
95
|
+
collectedLinks,
|
|
96
|
+
isPathBrokenLink,
|
|
97
|
+
isAnchorBrokenLink,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function getBrokenLinksForPage({ pagePath, helper, }) {
|
|
101
|
+
const pageData = helper.collectedLinks.get(pagePath);
|
|
102
|
+
const brokenLinks = [];
|
|
103
|
+
pageData.links.forEach((link) => {
|
|
44
104
|
const linkPath = (0, utils_1.parseURLPath)(link, pagePath);
|
|
45
|
-
if (isPathBrokenLink(linkPath)) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
},
|
|
52
|
-
];
|
|
105
|
+
if (helper.isPathBrokenLink(linkPath)) {
|
|
106
|
+
brokenLinks.push({
|
|
107
|
+
link,
|
|
108
|
+
resolvedLink: (0, utils_1.serializeURLPath)(linkPath),
|
|
109
|
+
anchor: false,
|
|
110
|
+
});
|
|
53
111
|
}
|
|
54
|
-
if (isAnchorBrokenLink(linkPath)) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
];
|
|
112
|
+
else if (helper.isAnchorBrokenLink(linkPath)) {
|
|
113
|
+
brokenLinks.push({
|
|
114
|
+
link,
|
|
115
|
+
resolvedLink: (0, utils_1.serializeURLPath)(linkPath),
|
|
116
|
+
anchor: true,
|
|
117
|
+
});
|
|
62
118
|
}
|
|
63
|
-
return [];
|
|
64
119
|
});
|
|
65
120
|
return brokenLinks;
|
|
66
121
|
}
|
|
@@ -76,13 +131,25 @@ function filterIntermediateRoutes(routesInput) {
|
|
|
76
131
|
}
|
|
77
132
|
function getBrokenLinks({ collectedLinks, routes, }) {
|
|
78
133
|
const filteredRoutes = filterIntermediateRoutes(routes);
|
|
79
|
-
|
|
134
|
+
const helper = createBrokenLinksHelper({
|
|
80
135
|
collectedLinks,
|
|
81
|
-
pageLinks: pageCollectedData.links,
|
|
82
|
-
pageAnchors: pageCollectedData.anchors,
|
|
83
|
-
pagePath,
|
|
84
136
|
routes: filteredRoutes,
|
|
85
|
-
})
|
|
137
|
+
});
|
|
138
|
+
const result = {};
|
|
139
|
+
collectedLinks.forEach((_unused, pagePath) => {
|
|
140
|
+
try {
|
|
141
|
+
result[pagePath] = getBrokenLinksForPage({
|
|
142
|
+
pagePath,
|
|
143
|
+
helper,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
throw new Error(`Unable to get broken links for page ${pagePath}.`, {
|
|
148
|
+
cause: e,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return result;
|
|
86
153
|
}
|
|
87
154
|
function brokenLinkMessage(brokenLink) {
|
|
88
155
|
const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink;
|
|
@@ -179,11 +246,31 @@ function reportBrokenLinks({ brokenLinks, onBrokenLinks, onBrokenAnchors, }) {
|
|
|
179
246
|
logger_1.default.report(onBrokenAnchors)(anchorErrorMessage);
|
|
180
247
|
}
|
|
181
248
|
}
|
|
249
|
+
// Users might use the useBrokenLinks() API in weird unexpected ways
|
|
250
|
+
// JS users might call "collectLink(undefined)" for example
|
|
251
|
+
// TS users might call "collectAnchor('#hash')" with/without #
|
|
252
|
+
// We clean/normalize the collected data to avoid obscure errors being thrown
|
|
253
|
+
// We also use optimized data structures for a faster algorithm
|
|
254
|
+
function normalizeCollectedLinks(collectedLinks) {
|
|
255
|
+
const result = new Map();
|
|
256
|
+
Object.entries(collectedLinks).forEach(([pathname, pageCollectedData]) => {
|
|
257
|
+
result.set(pathname, {
|
|
258
|
+
links: new Set(pageCollectedData.links.filter(lodash_1.default.isString)),
|
|
259
|
+
anchors: new Set(pageCollectedData.anchors
|
|
260
|
+
.filter(lodash_1.default.isString)
|
|
261
|
+
.map((anchor) => (anchor.startsWith('#') ? anchor.slice(1) : anchor))),
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
182
266
|
async function handleBrokenLinks({ collectedLinks, onBrokenLinks, onBrokenAnchors, routes, }) {
|
|
183
267
|
if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
|
|
184
268
|
return;
|
|
185
269
|
}
|
|
186
|
-
const brokenLinks = getBrokenLinks({
|
|
270
|
+
const brokenLinks = getBrokenLinks({
|
|
271
|
+
routes,
|
|
272
|
+
collectedLinks: normalizeCollectedLinks(collectedLinks),
|
|
273
|
+
});
|
|
187
274
|
reportBrokenLinks({ brokenLinks, onBrokenLinks, onBrokenAnchors });
|
|
188
275
|
}
|
|
189
276
|
exports.handleBrokenLinks = handleBrokenLinks;
|
|
@@ -40,7 +40,10 @@ function getProcessForPort(port) {
|
|
|
40
40
|
*/
|
|
41
41
|
async function choosePort(host, defaultPort) {
|
|
42
42
|
try {
|
|
43
|
-
const port = await (0, detect_port_1.default)({
|
|
43
|
+
const port = await (0, detect_port_1.default)({
|
|
44
|
+
port: defaultPort,
|
|
45
|
+
...(host !== 'localhost' && { hostname: host }),
|
|
46
|
+
});
|
|
44
47
|
if (port === defaultPort) {
|
|
45
48
|
return port;
|
|
46
49
|
}
|
package/lib/server/routes.js
CHANGED
|
@@ -159,6 +159,14 @@ ${JSON.stringify(routeConfig)}`);
|
|
|
159
159
|
props,
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Old stuff
|
|
164
|
+
* As far as I understand, this is what permits to SSG the 404.html file
|
|
165
|
+
* This is rendered through the catch-all ComponentCreator("*") route
|
|
166
|
+
* Note CDNs only understand the 404.html file by convention
|
|
167
|
+
* The extension probably permits to avoid emitting "/404/index.html"
|
|
168
|
+
*/
|
|
169
|
+
const NotFoundRoutePath = '/404.html';
|
|
162
170
|
/**
|
|
163
171
|
* Routes are prepared into three temp files:
|
|
164
172
|
*
|
|
@@ -175,7 +183,7 @@ function loadRoutes(routeConfigs, baseUrl, onDuplicateRoutes) {
|
|
|
175
183
|
routesConfig: '',
|
|
176
184
|
routesChunkNames: {},
|
|
177
185
|
registry: {},
|
|
178
|
-
routesPaths: [(0, utils_1.normalizeUrl)([baseUrl,
|
|
186
|
+
routesPaths: [(0, utils_1.normalizeUrl)([baseUrl, NotFoundRoutePath])],
|
|
179
187
|
};
|
|
180
188
|
// `genRouteCode` would mutate `res`
|
|
181
189
|
const routeConfigSerialized = routeConfigs
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docusaurus/core",
|
|
3
3
|
"description": "Easy to Maintain Open Source Documentation Websites",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@babel/runtime": "^7.22.6",
|
|
44
44
|
"@babel/runtime-corejs3": "^7.22.6",
|
|
45
45
|
"@babel/traverse": "^7.22.8",
|
|
46
|
-
"@docusaurus/cssnano-preset": "3.1.
|
|
47
|
-
"@docusaurus/logger": "3.1.
|
|
48
|
-
"@docusaurus/mdx-loader": "3.1.
|
|
46
|
+
"@docusaurus/cssnano-preset": "3.1.1",
|
|
47
|
+
"@docusaurus/logger": "3.1.1",
|
|
48
|
+
"@docusaurus/mdx-loader": "3.1.1",
|
|
49
49
|
"@docusaurus/react-loadable": "5.5.2",
|
|
50
|
-
"@docusaurus/utils": "3.1.
|
|
51
|
-
"@docusaurus/utils-common": "3.1.
|
|
52
|
-
"@docusaurus/utils-validation": "3.1.
|
|
50
|
+
"@docusaurus/utils": "3.1.1",
|
|
51
|
+
"@docusaurus/utils-common": "3.1.1",
|
|
52
|
+
"@docusaurus/utils-validation": "3.1.1",
|
|
53
53
|
"@slorber/static-site-generator-webpack-plugin": "^4.0.7",
|
|
54
54
|
"@svgr/webpack": "^6.5.1",
|
|
55
55
|
"autoprefixer": "^10.4.14",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"webpackbar": "^5.0.2"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
|
-
"@docusaurus/module-type-aliases": "3.1.
|
|
108
|
-
"@docusaurus/types": "3.1.
|
|
107
|
+
"@docusaurus/module-type-aliases": "3.1.1",
|
|
108
|
+
"@docusaurus/types": "3.1.1",
|
|
109
109
|
"@types/detect-port": "^1.3.3",
|
|
110
110
|
"@types/react-dom": "^18.2.7",
|
|
111
111
|
"@types/react-router-config": "^5.0.7",
|
|
@@ -124,5 +124,5 @@
|
|
|
124
124
|
"engines": {
|
|
125
125
|
"node": ">=18.0"
|
|
126
126
|
},
|
|
127
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "8017f6a6776ba1bd7065e630a52fe2c2654e2f1b"
|
|
128
128
|
}
|