@docusaurus/core 3.0.1 → 3.1.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/bin/docusaurus.mjs +24 -4
- package/lib/client/BrokenLinksContext.d.ts +19 -0
- package/lib/client/{LinksCollector.js → BrokenLinksContext.js} +14 -6
- package/lib/client/exports/Link.js +3 -3
- package/lib/client/exports/useBrokenLinks.d.ts +9 -0
- package/lib/client/exports/useBrokenLinks.js +10 -0
- package/lib/client/serverEntry.js +9 -5
- package/lib/commands/build.js +6 -7
- package/lib/server/brokenLinks.d.ts +9 -5
- package/lib/server/brokenLinks.js +113 -80
- package/lib/server/configValidation.d.ts +3 -2
- package/lib/server/configValidation.js +26 -14
- package/package.json +10 -10
- package/lib/client/LinksCollector.d.ts +0 -20
package/bin/docusaurus.mjs
CHANGED
|
@@ -218,6 +218,9 @@ cli.arguments('<command>').action((cmd) => {
|
|
|
218
218
|
logger.error` Unknown command name=${cmd}.`;
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
// === The above is the commander configuration ===
|
|
222
|
+
// They don't start any code execution yet until cli.parse() is called below
|
|
223
|
+
|
|
221
224
|
/**
|
|
222
225
|
* @param {string | undefined} command
|
|
223
226
|
*/
|
|
@@ -237,12 +240,29 @@ function isInternalCommand(command) {
|
|
|
237
240
|
);
|
|
238
241
|
}
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
// process.argv always looks like this:
|
|
244
|
+
// [
|
|
245
|
+
// '/path/to/node',
|
|
246
|
+
// '/path/to/docusaurus.mjs',
|
|
247
|
+
// '<subcommand>',
|
|
248
|
+
// ...subcommandArgs
|
|
249
|
+
// ]
|
|
243
250
|
|
|
244
|
-
|
|
251
|
+
// There is no subcommand
|
|
252
|
+
// TODO: can we use commander to handle this case?
|
|
253
|
+
if (process.argv.length < 3 || process.argv[2]?.startsWith('--')) {
|
|
245
254
|
cli.outputHelp();
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// There is an unrecognized subcommand
|
|
259
|
+
// Let plugins extend the CLI before parsing
|
|
260
|
+
if (!isInternalCommand(process.argv[2])) {
|
|
261
|
+
// TODO: in this step, we must assume default site structure because there's
|
|
262
|
+
// no way to know the siteDir/config yet. Maybe the root cli should be
|
|
263
|
+
// responsible for parsing these arguments?
|
|
264
|
+
// https://github.com/facebook/docusaurus/issues/8903
|
|
265
|
+
await externalCommand(cli);
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
cli.parse(process.argv);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
/// <reference types="@docusaurus/module-type-aliases" />
|
|
8
|
+
import { type ReactNode } from 'react';
|
|
9
|
+
import type { BrokenLinks } from '@docusaurus/useBrokenLinks';
|
|
10
|
+
export type StatefulBrokenLinks = BrokenLinks & {
|
|
11
|
+
getCollectedLinks: () => string[];
|
|
12
|
+
getCollectedAnchors: () => string[];
|
|
13
|
+
};
|
|
14
|
+
export declare const createStatefulBrokenLinks: () => StatefulBrokenLinks;
|
|
15
|
+
export declare const useBrokenLinksContext: () => BrokenLinks;
|
|
16
|
+
export declare function BrokenLinksProvider({ children, brokenLinks, }: {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
brokenLinks: BrokenLinks;
|
|
19
|
+
}): JSX.Element;
|
|
@@ -5,22 +5,30 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import React, { useContext } from 'react';
|
|
8
|
-
export const
|
|
9
|
-
// Set to dedup, as it's not useful to collect multiple times the same
|
|
8
|
+
export const createStatefulBrokenLinks = () => {
|
|
9
|
+
// Set to dedup, as it's not useful to collect multiple times the same value
|
|
10
|
+
const allAnchors = new Set();
|
|
10
11
|
const allLinks = new Set();
|
|
11
12
|
return {
|
|
13
|
+
collectAnchor: (anchor) => {
|
|
14
|
+
allAnchors.add(anchor);
|
|
15
|
+
},
|
|
12
16
|
collectLink: (link) => {
|
|
13
17
|
allLinks.add(link);
|
|
14
18
|
},
|
|
19
|
+
getCollectedAnchors: () => [...allAnchors],
|
|
15
20
|
getCollectedLinks: () => [...allLinks],
|
|
16
21
|
};
|
|
17
22
|
};
|
|
18
23
|
const Context = React.createContext({
|
|
24
|
+
collectAnchor: () => {
|
|
25
|
+
// No-op for client
|
|
26
|
+
},
|
|
19
27
|
collectLink: () => {
|
|
20
|
-
// No-op for client
|
|
28
|
+
// No-op for client
|
|
21
29
|
},
|
|
22
30
|
});
|
|
23
|
-
export const
|
|
24
|
-
export function
|
|
25
|
-
return <Context.Provider value={
|
|
31
|
+
export const useBrokenLinksContext = () => useContext(Context);
|
|
32
|
+
export function BrokenLinksProvider({ children, brokenLinks, }) {
|
|
33
|
+
return <Context.Provider value={brokenLinks}>{children}</Context.Provider>;
|
|
26
34
|
}
|
|
@@ -10,7 +10,7 @@ import { applyTrailingSlash } from '@docusaurus/utils-common';
|
|
|
10
10
|
import useDocusaurusContext from './useDocusaurusContext';
|
|
11
11
|
import isInternalUrl from './isInternalUrl';
|
|
12
12
|
import ExecutionEnvironment from './ExecutionEnvironment';
|
|
13
|
-
import
|
|
13
|
+
import useBrokenLinks from './useBrokenLinks';
|
|
14
14
|
import { useBaseUrlUtils } from './useBaseUrl';
|
|
15
15
|
// TODO all this wouldn't be necessary if we used ReactRouter basename feature
|
|
16
16
|
// We don't automatically add base urls to all links,
|
|
@@ -21,7 +21,7 @@ const shouldAddBaseUrlAutomatically = (to) => to.startsWith('/');
|
|
|
21
21
|
function Link({ isNavLink, to, href, activeClassName, isActive, 'data-noBrokenLinkCheck': noBrokenLinkCheck, autoAddBaseUrl = true, ...props }, forwardedRef) {
|
|
22
22
|
const { siteConfig: { trailingSlash, baseUrl }, } = useDocusaurusContext();
|
|
23
23
|
const { withBaseUrl } = useBaseUrlUtils();
|
|
24
|
-
const
|
|
24
|
+
const brokenLinks = useBrokenLinks();
|
|
25
25
|
const innerRef = useRef(null);
|
|
26
26
|
useImperativeHandle(forwardedRef, () => innerRef.current);
|
|
27
27
|
// IMPORTANT: using to or href should not change anything
|
|
@@ -100,7 +100,7 @@ function Link({ isNavLink, to, href, activeClassName, isActive, 'data-noBrokenLi
|
|
|
100
100
|
const isAnchorLink = targetLink?.startsWith('#') ?? false;
|
|
101
101
|
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
|
|
102
102
|
if (!isRegularHtmlLink && !noBrokenLinkCheck) {
|
|
103
|
-
|
|
103
|
+
brokenLinks.collectLink(targetLink);
|
|
104
104
|
}
|
|
105
105
|
return isRegularHtmlLink ? (
|
|
106
106
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
/// <reference types="@docusaurus/module-type-aliases" />
|
|
8
|
+
import type { BrokenLinks } from '@docusaurus/useBrokenLinks';
|
|
9
|
+
export default function useBrokenLinks(): BrokenLinks;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { useBrokenLinksContext } from '../BrokenLinksContext';
|
|
8
|
+
export default function useBrokenLinks() {
|
|
9
|
+
return useBrokenLinksContext();
|
|
10
|
+
}
|
|
@@ -18,7 +18,7 @@ import { minify } from 'html-minifier-terser';
|
|
|
18
18
|
import { renderStaticApp } from './serverRenderer';
|
|
19
19
|
import preload from './preload';
|
|
20
20
|
import App from './App';
|
|
21
|
-
import {
|
|
21
|
+
import { createStatefulBrokenLinks, BrokenLinksProvider, } from './BrokenLinksContext';
|
|
22
22
|
const getCompiledSSRTemplate = _.memoize((template) => eta.compile(template.trim(), {
|
|
23
23
|
rmWhitespace: true,
|
|
24
24
|
}));
|
|
@@ -61,20 +61,24 @@ async function doRender(locals) {
|
|
|
61
61
|
const modules = new Set();
|
|
62
62
|
const routerContext = {};
|
|
63
63
|
const helmetContext = {};
|
|
64
|
-
const
|
|
64
|
+
const statefulBrokenLinks = createStatefulBrokenLinks();
|
|
65
65
|
const app = (
|
|
66
66
|
// @ts-expect-error: we are migrating away from react-loadable anyways
|
|
67
67
|
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
|
68
68
|
<HelmetProvider context={helmetContext}>
|
|
69
69
|
<StaticRouter location={location} context={routerContext}>
|
|
70
|
-
<
|
|
70
|
+
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
|
71
71
|
<App />
|
|
72
|
-
</
|
|
72
|
+
</BrokenLinksProvider>
|
|
73
73
|
</StaticRouter>
|
|
74
74
|
</HelmetProvider>
|
|
75
75
|
</Loadable.Capture>);
|
|
76
76
|
const appHtml = await renderStaticApp(app);
|
|
77
|
-
onLinksCollected(
|
|
77
|
+
onLinksCollected({
|
|
78
|
+
staticPagePath: location,
|
|
79
|
+
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
|
80
|
+
links: statefulBrokenLinks.getCollectedLinks(),
|
|
81
|
+
});
|
|
78
82
|
const { helmet } = helmetContext;
|
|
79
83
|
const htmlAttributes = helmet.htmlAttributes.toString();
|
|
80
84
|
const bodyAttributes = helmet.bodyAttributes.toString();
|
package/lib/commands/build.js
CHANGED
|
@@ -100,7 +100,7 @@ async function buildLocale({ siteDir, locale, cliOptions, forceTerminate, isLast
|
|
|
100
100
|
localizePath: cliOptions.locale ? false : undefined,
|
|
101
101
|
});
|
|
102
102
|
// Apply user webpack config.
|
|
103
|
-
const { outDir, generatedFilesDir, plugins, siteConfig: {
|
|
103
|
+
const { outDir, generatedFilesDir, plugins, siteConfig: { onBrokenLinks, onBrokenAnchors, staticDirectories: staticDirectoriesOption, }, routes, } = props;
|
|
104
104
|
const clientManifestPath = path_1.default.join(generatedFilesDir, 'client-manifest.json');
|
|
105
105
|
let clientConfig = (0, webpack_merge_1.default)(await (0, client_1.default)(props, cliOptions.minify, true), {
|
|
106
106
|
plugins: [
|
|
@@ -115,12 +115,12 @@ async function buildLocale({ siteDir, locale, cliOptions, forceTerminate, isLast
|
|
|
115
115
|
}),
|
|
116
116
|
].filter((x) => Boolean(x)),
|
|
117
117
|
});
|
|
118
|
-
const
|
|
118
|
+
const collectedLinks = {};
|
|
119
119
|
const headTags = {};
|
|
120
120
|
let serverConfig = await (0, server_2.default)({
|
|
121
121
|
props,
|
|
122
|
-
onLinksCollected: (staticPagePath, links) => {
|
|
123
|
-
|
|
122
|
+
onLinksCollected: ({ staticPagePath, links, anchors }) => {
|
|
123
|
+
collectedLinks[staticPagePath] = { links, anchors };
|
|
124
124
|
},
|
|
125
125
|
onHeadTagsCollected: (staticPagePath, tags) => {
|
|
126
126
|
headTags[staticPagePath] = tags;
|
|
@@ -190,11 +190,10 @@ async function buildLocale({ siteDir, locale, cliOptions, forceTerminate, isLast
|
|
|
190
190
|
});
|
|
191
191
|
}));
|
|
192
192
|
await (0, brokenLinks_1.handleBrokenLinks)({
|
|
193
|
-
|
|
193
|
+
collectedLinks,
|
|
194
194
|
routes,
|
|
195
195
|
onBrokenLinks,
|
|
196
|
-
|
|
197
|
-
baseUrl,
|
|
196
|
+
onBrokenAnchors,
|
|
198
197
|
});
|
|
199
198
|
logger_1.default.success `Generated static files in path=${path_1.default.relative(process.cwd(), outDir)}.`;
|
|
200
199
|
if (isLastLocale) {
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import type { RouteConfig, ReportingSeverity } from '@docusaurus/types';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
type CollectedLinks = {
|
|
9
|
+
[pathname: string]: {
|
|
10
|
+
links: string[];
|
|
11
|
+
anchors: string[];
|
|
11
12
|
};
|
|
13
|
+
};
|
|
14
|
+
export declare function handleBrokenLinks({ collectedLinks, onBrokenLinks, onBrokenAnchors, routes, }: {
|
|
15
|
+
collectedLinks: CollectedLinks;
|
|
12
16
|
onBrokenLinks: ReportingSeverity;
|
|
17
|
+
onBrokenAnchors: ReportingSeverity;
|
|
13
18
|
routes: RouteConfig[];
|
|
14
|
-
baseUrl: string;
|
|
15
|
-
outDir: string;
|
|
16
19
|
}): Promise<void>;
|
|
20
|
+
export {};
|
|
@@ -8,28 +8,15 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.handleBrokenLinks = void 0;
|
|
10
10
|
const tslib_1 = require("tslib");
|
|
11
|
-
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
12
|
-
const path_1 = tslib_1.__importDefault(require("path"));
|
|
13
11
|
const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
|
14
12
|
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
|
|
15
|
-
const combine_promises_1 = tslib_1.__importDefault(require("combine-promises"));
|
|
16
13
|
const react_router_config_1 = require("react-router-config");
|
|
17
14
|
const utils_1 = require("@docusaurus/utils");
|
|
18
15
|
const utils_2 = require("./utils");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function getPageBrokenLinks({ pagePath, pageLinks, routes, }) {
|
|
24
|
-
// ReactRouter is able to support links like ./../somePath but `matchRoutes`
|
|
25
|
-
// does not do this resolution internally. We must resolve the links before
|
|
26
|
-
// using `matchRoutes`. `resolvePathname` is used internally by React Router
|
|
27
|
-
function resolveLink(link) {
|
|
28
|
-
const resolvedLink = (0, utils_1.resolvePathname)(onlyPathname(link), pagePath);
|
|
29
|
-
return { link, resolvedLink };
|
|
30
|
-
}
|
|
31
|
-
function isBrokenLink(link) {
|
|
32
|
-
const matchedRoutes = [link, decodeURI(link)]
|
|
16
|
+
function getBrokenLinksForPage({ collectedLinks, pagePath, pageLinks, routes, }) {
|
|
17
|
+
// console.log('routes:', routes);
|
|
18
|
+
function isPathBrokenLink(linkPath) {
|
|
19
|
+
const matchedRoutes = [linkPath.pathname, decodeURI(linkPath.pathname)]
|
|
33
20
|
// @ts-expect-error: React router types RouteConfig with an actual React
|
|
34
21
|
// component, but we load route components with string paths.
|
|
35
22
|
// We don't actually access component here, so it's fine.
|
|
@@ -37,7 +24,45 @@ function getPageBrokenLinks({ pagePath, pageLinks, routes, }) {
|
|
|
37
24
|
.flat();
|
|
38
25
|
return matchedRoutes.length === 0;
|
|
39
26
|
}
|
|
40
|
-
|
|
27
|
+
function isAnchorBrokenLink(linkPath) {
|
|
28
|
+
const { pathname, hash } = linkPath;
|
|
29
|
+
// Link has no hash: it can't be a broken anchor link
|
|
30
|
+
if (hash === undefined) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const targetPage = collectedLinks[pathname] || collectedLinks[decodeURI(pathname)];
|
|
34
|
+
// link with anchor to a page that does not exist (or did not collect any
|
|
35
|
+
// link/anchor) is considered as a broken anchor
|
|
36
|
+
if (!targetPage) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// it's a broken anchor if the target page exists
|
|
40
|
+
// but the anchor does not exist on that page
|
|
41
|
+
return !targetPage.anchors.includes(hash);
|
|
42
|
+
}
|
|
43
|
+
const brokenLinks = pageLinks.flatMap((link) => {
|
|
44
|
+
const linkPath = (0, utils_1.parseURLPath)(link, pagePath);
|
|
45
|
+
if (isPathBrokenLink(linkPath)) {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
link,
|
|
49
|
+
resolvedLink: (0, utils_1.serializeURLPath)(linkPath),
|
|
50
|
+
anchor: false,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
if (isAnchorBrokenLink(linkPath)) {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
link,
|
|
58
|
+
resolvedLink: (0, utils_1.serializeURLPath)(linkPath),
|
|
59
|
+
anchor: true,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
});
|
|
65
|
+
return brokenLinks;
|
|
41
66
|
}
|
|
42
67
|
/**
|
|
43
68
|
* The route defs can be recursive, and have a parent match-all route. We don't
|
|
@@ -49,25 +74,48 @@ function filterIntermediateRoutes(routesInput) {
|
|
|
49
74
|
const routesWithout404 = routesInput.filter((route) => route.path !== '*');
|
|
50
75
|
return (0, utils_2.getAllFinalRoutes)(routesWithout404);
|
|
51
76
|
}
|
|
52
|
-
function
|
|
77
|
+
function getBrokenLinks({ collectedLinks, routes, }) {
|
|
53
78
|
const filteredRoutes = filterIntermediateRoutes(routes);
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
return lodash_1.default.mapValues(collectedLinks, (pageCollectedData, pagePath) => getBrokenLinksForPage({
|
|
80
|
+
collectedLinks,
|
|
81
|
+
pageLinks: pageCollectedData.links,
|
|
82
|
+
pageAnchors: pageCollectedData.anchors,
|
|
83
|
+
pagePath,
|
|
84
|
+
routes: filteredRoutes,
|
|
85
|
+
}));
|
|
56
86
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function pageBrokenLinksMessage(pagePath, brokenLinks) {
|
|
66
|
-
return `
|
|
67
|
-
- On source page path = ${pagePath}:
|
|
87
|
+
function brokenLinkMessage(brokenLink) {
|
|
88
|
+
const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink;
|
|
89
|
+
return `${brokenLink.link}${showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : ''}`;
|
|
90
|
+
}
|
|
91
|
+
function createBrokenLinksMessage(pagePath, brokenLinks) {
|
|
92
|
+
const type = brokenLinks[0]?.anchor === true ? 'anchor' : 'link';
|
|
93
|
+
const anchorMessage = brokenLinks.length > 0
|
|
94
|
+
? `- Broken ${type} on source page path = ${pagePath}:
|
|
68
95
|
-> linking to ${brokenLinks
|
|
69
96
|
.map(brokenLinkMessage)
|
|
70
|
-
.join('\n -> linking to ')}
|
|
97
|
+
.join('\n -> linking to ')}`
|
|
98
|
+
: '';
|
|
99
|
+
return `${anchorMessage}`;
|
|
100
|
+
}
|
|
101
|
+
function createBrokenAnchorsMessage(brokenAnchors) {
|
|
102
|
+
if (Object.keys(brokenAnchors).length === 0) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return `Docusaurus found broken anchors!
|
|
106
|
+
|
|
107
|
+
Please check the pages of your site in the list below, and make sure you don't reference any anchor that does not exist.
|
|
108
|
+
Note: it's possible to ignore broken anchors with the 'onBrokenAnchors' Docusaurus configuration, and let the build pass.
|
|
109
|
+
|
|
110
|
+
Exhaustive list of all broken anchors found:
|
|
111
|
+
${Object.entries(brokenAnchors)
|
|
112
|
+
.map(([pagePath, brokenLinks]) => createBrokenLinksMessage(pagePath, brokenLinks))
|
|
113
|
+
.join('\n')}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
function createBrokenPathsMessage(brokenPathsMap) {
|
|
117
|
+
if (Object.keys(brokenPathsMap).length === 0) {
|
|
118
|
+
return undefined;
|
|
71
119
|
}
|
|
72
120
|
/**
|
|
73
121
|
* If there's a broken link appearing very often, it is probably a broken link
|
|
@@ -75,7 +123,7 @@ function getBrokenLinksErrorMessage(allBrokenLinks) {
|
|
|
75
123
|
* this out. See https://github.com/facebook/docusaurus/issues/3567#issuecomment-706973805
|
|
76
124
|
*/
|
|
77
125
|
function getLayoutBrokenLinksHelpMessage() {
|
|
78
|
-
const flatList = Object.entries(
|
|
126
|
+
const flatList = Object.entries(brokenPathsMap).flatMap(([pagePage, brokenLinks]) => brokenLinks.map((brokenLink) => ({ pagePage, brokenLink })));
|
|
79
127
|
const countedBrokenLinks = lodash_1.default.countBy(flatList, (item) => item.brokenLink.link);
|
|
80
128
|
const FrequencyThreshold = 5; // Is this a good value?
|
|
81
129
|
const frequentLinks = Object.entries(countedBrokenLinks)
|
|
@@ -97,60 +145,45 @@ Please check the pages of your site in the list below, and make sure you don't r
|
|
|
97
145
|
Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.${getLayoutBrokenLinksHelpMessage()}
|
|
98
146
|
|
|
99
147
|
Exhaustive list of all broken links found:
|
|
100
|
-
${Object.entries(
|
|
101
|
-
.map(([pagePath,
|
|
148
|
+
${Object.entries(brokenPathsMap)
|
|
149
|
+
.map(([pagePath, brokenPaths]) => createBrokenLinksMessage(pagePath, brokenPaths))
|
|
102
150
|
.join('\n')}
|
|
103
151
|
`;
|
|
104
152
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
// If a file actually exist on the file system, we know the link is valid
|
|
114
|
-
// even if docusaurus does not know about this file, so we don't report it
|
|
115
|
-
async function filterExistingFileLinks({ baseUrl, outDir, allCollectedLinks, }) {
|
|
116
|
-
async function linkFileExists(link) {
|
|
117
|
-
// /baseUrl/javadoc/ -> /outDir/javadoc
|
|
118
|
-
const baseFilePath = onlyPathname((0, utils_1.removeSuffix)(`${outDir}/${(0, utils_1.removePrefix)(link, baseUrl)}`, '/'));
|
|
119
|
-
// -> /outDir/javadoc
|
|
120
|
-
// -> /outDir/javadoc.html
|
|
121
|
-
// -> /outDir/javadoc/index.html
|
|
122
|
-
const filePathsToTry = [baseFilePath];
|
|
123
|
-
if (!path_1.default.extname(baseFilePath)) {
|
|
124
|
-
filePathsToTry.push(`${baseFilePath}.html`, path_1.default.join(baseFilePath, 'index.html'));
|
|
153
|
+
function splitBrokenLinks(brokenLinks) {
|
|
154
|
+
const brokenPaths = {};
|
|
155
|
+
const brokenAnchors = {};
|
|
156
|
+
Object.entries(brokenLinks).forEach(([pathname, pageBrokenLinks]) => {
|
|
157
|
+
const [anchorBrokenLinks, pathBrokenLinks] = lodash_1.default.partition(pageBrokenLinks, (link) => link.anchor);
|
|
158
|
+
if (pathBrokenLinks.length > 0) {
|
|
159
|
+
brokenPaths[pathname] = pathBrokenLinks;
|
|
125
160
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
161
|
+
if (anchorBrokenLinks.length > 0) {
|
|
162
|
+
brokenAnchors[pathname] = anchorBrokenLinks;
|
|
130
163
|
}
|
|
131
|
-
|
|
164
|
+
});
|
|
165
|
+
return { brokenPaths, brokenAnchors };
|
|
166
|
+
}
|
|
167
|
+
function reportBrokenLinks({ brokenLinks, onBrokenLinks, onBrokenAnchors, }) {
|
|
168
|
+
// We need to split the broken links reporting in 2 for better granularity
|
|
169
|
+
// This is because we need to report broken path/anchors independently
|
|
170
|
+
// For v3.x retro-compatibility, we can't throw by default for broken anchors
|
|
171
|
+
// TODO Docusaurus v4: make onBrokenAnchors throw by default?
|
|
172
|
+
const { brokenPaths, brokenAnchors } = splitBrokenLinks(brokenLinks);
|
|
173
|
+
const pathErrorMessage = createBrokenPathsMessage(brokenPaths);
|
|
174
|
+
if (pathErrorMessage) {
|
|
175
|
+
logger_1.default.report(onBrokenLinks)(pathErrorMessage);
|
|
176
|
+
}
|
|
177
|
+
const anchorErrorMessage = createBrokenAnchorsMessage(brokenAnchors);
|
|
178
|
+
if (anchorErrorMessage) {
|
|
179
|
+
logger_1.default.report(onBrokenAnchors)(anchorErrorMessage);
|
|
132
180
|
}
|
|
133
|
-
return (0, combine_promises_1.default)(lodash_1.default.mapValues(allCollectedLinks, async (links) => (await Promise.all(links.map(async (link) => ((await linkFileExists(link)) ? '' : link)))).filter(Boolean)));
|
|
134
181
|
}
|
|
135
|
-
async function handleBrokenLinks({
|
|
136
|
-
if (onBrokenLinks === 'ignore') {
|
|
182
|
+
async function handleBrokenLinks({ collectedLinks, onBrokenLinks, onBrokenAnchors, routes, }) {
|
|
183
|
+
if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
|
|
137
184
|
return;
|
|
138
185
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// existing static file...
|
|
142
|
-
const allCollectedLinksFiltered = await filterExistingFileLinks({
|
|
143
|
-
allCollectedLinks,
|
|
144
|
-
baseUrl,
|
|
145
|
-
outDir,
|
|
146
|
-
});
|
|
147
|
-
const allBrokenLinks = getAllBrokenLinks({
|
|
148
|
-
allCollectedLinks: allCollectedLinksFiltered,
|
|
149
|
-
routes,
|
|
150
|
-
});
|
|
151
|
-
const errorMessage = getBrokenLinksErrorMessage(allBrokenLinks);
|
|
152
|
-
if (errorMessage) {
|
|
153
|
-
logger_1.default.report(onBrokenLinks)(errorMessage);
|
|
154
|
-
}
|
|
186
|
+
const brokenLinks = getBrokenLinks({ routes, collectedLinks });
|
|
187
|
+
reportBrokenLinks({ brokenLinks, onBrokenLinks, onBrokenAnchors });
|
|
155
188
|
}
|
|
156
189
|
exports.handleBrokenLinks = handleBrokenLinks;
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import { Joi } from '@docusaurus/utils-validation';
|
|
8
|
-
import type { DocusaurusConfig, I18nConfig } from '@docusaurus/types';
|
|
8
|
+
import type { DocusaurusConfig, I18nConfig, MarkdownConfig } from '@docusaurus/types';
|
|
9
9
|
export declare const DEFAULT_I18N_CONFIG: I18nConfig;
|
|
10
|
-
export declare const
|
|
10
|
+
export declare const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig;
|
|
11
|
+
export declare const DEFAULT_CONFIG: Pick<DocusaurusConfig, 'i18n' | 'onBrokenLinks' | 'onBrokenAnchors' | 'onBrokenMarkdownLinks' | 'onDuplicateRoutes' | 'plugins' | 'themes' | 'presets' | 'headTags' | 'stylesheets' | 'scripts' | 'clientModules' | 'customFields' | 'themeConfig' | 'titleDelimiter' | 'noIndex' | 'tagline' | 'baseUrlIssueBanner' | 'staticDirectories' | 'markdown'>;
|
|
11
12
|
export declare const ConfigSchema: Joi.ObjectSchema<DocusaurusConfig>;
|
|
12
13
|
export declare function validateConfig(config: unknown, siteConfigPath: string): DocusaurusConfig;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.validateConfig = exports.ConfigSchema = exports.DEFAULT_CONFIG = exports.DEFAULT_I18N_CONFIG = void 0;
|
|
9
|
+
exports.validateConfig = exports.ConfigSchema = exports.DEFAULT_CONFIG = exports.DEFAULT_MARKDOWN_CONFIG = exports.DEFAULT_I18N_CONFIG = void 0;
|
|
10
10
|
const utils_1 = require("@docusaurus/utils");
|
|
11
11
|
const utils_validation_1 = require("@docusaurus/utils-validation");
|
|
12
12
|
const DEFAULT_I18N_LOCALE = 'en';
|
|
@@ -16,9 +16,22 @@ exports.DEFAULT_I18N_CONFIG = {
|
|
|
16
16
|
locales: [DEFAULT_I18N_LOCALE],
|
|
17
17
|
localeConfigs: {},
|
|
18
18
|
};
|
|
19
|
+
exports.DEFAULT_MARKDOWN_CONFIG = {
|
|
20
|
+
format: 'mdx',
|
|
21
|
+
mermaid: false,
|
|
22
|
+
preprocessor: undefined,
|
|
23
|
+
parseFrontMatter: utils_1.DEFAULT_PARSE_FRONT_MATTER,
|
|
24
|
+
mdx1Compat: {
|
|
25
|
+
comments: true,
|
|
26
|
+
admonitions: true,
|
|
27
|
+
headingIds: true,
|
|
28
|
+
},
|
|
29
|
+
remarkRehypeOptions: undefined,
|
|
30
|
+
};
|
|
19
31
|
exports.DEFAULT_CONFIG = {
|
|
20
32
|
i18n: exports.DEFAULT_I18N_CONFIG,
|
|
21
33
|
onBrokenLinks: 'throw',
|
|
34
|
+
onBrokenAnchors: 'warn',
|
|
22
35
|
onBrokenMarkdownLinks: 'warn',
|
|
23
36
|
onDuplicateRoutes: 'warn',
|
|
24
37
|
plugins: [],
|
|
@@ -35,25 +48,15 @@ exports.DEFAULT_CONFIG = {
|
|
|
35
48
|
tagline: '',
|
|
36
49
|
baseUrlIssueBanner: true,
|
|
37
50
|
staticDirectories: [utils_1.DEFAULT_STATIC_DIR_NAME],
|
|
38
|
-
markdown:
|
|
39
|
-
format: 'mdx',
|
|
40
|
-
mermaid: false,
|
|
41
|
-
preprocessor: undefined,
|
|
42
|
-
mdx1Compat: {
|
|
43
|
-
comments: true,
|
|
44
|
-
admonitions: true,
|
|
45
|
-
headingIds: true,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
51
|
+
markdown: exports.DEFAULT_MARKDOWN_CONFIG,
|
|
48
52
|
};
|
|
49
53
|
function createPluginSchema(theme) {
|
|
50
|
-
return
|
|
54
|
+
return utils_validation_1.Joi.alternatives()
|
|
51
55
|
.try(utils_validation_1.Joi.function(), utils_validation_1.Joi.array()
|
|
52
56
|
.ordered(utils_validation_1.Joi.function().required(), utils_validation_1.Joi.object().required())
|
|
53
57
|
.length(2), utils_validation_1.Joi.string(), utils_validation_1.Joi.array()
|
|
54
58
|
.ordered(utils_validation_1.Joi.string().required(), utils_validation_1.Joi.object().required())
|
|
55
59
|
.length(2), utils_validation_1.Joi.any().valid(false, null))
|
|
56
|
-
// @ts-expect-error: bad lib def, doesn't recognize an array of reports
|
|
57
60
|
.error((errors) => {
|
|
58
61
|
errors.forEach((error) => {
|
|
59
62
|
const validConfigExample = theme
|
|
@@ -82,7 +85,7 @@ ${validConfigExample}
|
|
|
82
85
|
`;
|
|
83
86
|
});
|
|
84
87
|
return errors;
|
|
85
|
-
})
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
90
|
const PluginSchema = createPluginSchema(false);
|
|
88
91
|
const ThemeSchema = createPluginSchema(true);
|
|
@@ -147,6 +150,9 @@ exports.ConfigSchema = utils_validation_1.Joi.object({
|
|
|
147
150
|
onBrokenLinks: utils_validation_1.Joi.string()
|
|
148
151
|
.equal('ignore', 'log', 'warn', 'throw')
|
|
149
152
|
.default(exports.DEFAULT_CONFIG.onBrokenLinks),
|
|
153
|
+
onBrokenAnchors: utils_validation_1.Joi.string()
|
|
154
|
+
.equal('ignore', 'log', 'warn', 'throw')
|
|
155
|
+
.default(exports.DEFAULT_CONFIG.onBrokenAnchors),
|
|
150
156
|
onBrokenMarkdownLinks: utils_validation_1.Joi.string()
|
|
151
157
|
.equal('ignore', 'log', 'warn', 'throw')
|
|
152
158
|
.default(exports.DEFAULT_CONFIG.onBrokenMarkdownLinks),
|
|
@@ -214,6 +220,7 @@ exports.ConfigSchema = utils_validation_1.Joi.object({
|
|
|
214
220
|
format: utils_validation_1.Joi.string()
|
|
215
221
|
.equal('mdx', 'md', 'detect')
|
|
216
222
|
.default(exports.DEFAULT_CONFIG.markdown.format),
|
|
223
|
+
parseFrontMatter: utils_validation_1.Joi.function().default(() => exports.DEFAULT_CONFIG.markdown.parseFrontMatter),
|
|
217
224
|
mermaid: utils_validation_1.Joi.boolean().default(exports.DEFAULT_CONFIG.markdown.mermaid),
|
|
218
225
|
preprocessor: utils_validation_1.Joi.function()
|
|
219
226
|
.arity(1)
|
|
@@ -224,6 +231,11 @@ exports.ConfigSchema = utils_validation_1.Joi.object({
|
|
|
224
231
|
admonitions: utils_validation_1.Joi.boolean().default(exports.DEFAULT_CONFIG.markdown.mdx1Compat.admonitions),
|
|
225
232
|
headingIds: utils_validation_1.Joi.boolean().default(exports.DEFAULT_CONFIG.markdown.mdx1Compat.headingIds),
|
|
226
233
|
}).default(exports.DEFAULT_CONFIG.markdown.mdx1Compat),
|
|
234
|
+
remarkRehypeOptions:
|
|
235
|
+
// add proper external options validation?
|
|
236
|
+
// Not sure if it's a good idea, validation is likely to become stale
|
|
237
|
+
// See https://github.com/remarkjs/remark-rehype#options
|
|
238
|
+
utils_validation_1.Joi.object().unknown(),
|
|
227
239
|
}).default(exports.DEFAULT_CONFIG.markdown),
|
|
228
240
|
}).messages({
|
|
229
241
|
'docusaurus.configValidationWarning': 'Docusaurus config validation warning. Field {#label}: {#warningMessage}',
|
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.0
|
|
4
|
+
"version": "3.1.0",
|
|
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.0
|
|
47
|
-
"@docusaurus/logger": "3.0
|
|
48
|
-
"@docusaurus/mdx-loader": "3.0
|
|
46
|
+
"@docusaurus/cssnano-preset": "3.1.0",
|
|
47
|
+
"@docusaurus/logger": "3.1.0",
|
|
48
|
+
"@docusaurus/mdx-loader": "3.1.0",
|
|
49
49
|
"@docusaurus/react-loadable": "5.5.2",
|
|
50
|
-
"@docusaurus/utils": "3.0
|
|
51
|
-
"@docusaurus/utils-common": "3.0
|
|
52
|
-
"@docusaurus/utils-validation": "3.0
|
|
50
|
+
"@docusaurus/utils": "3.1.0",
|
|
51
|
+
"@docusaurus/utils-common": "3.1.0",
|
|
52
|
+
"@docusaurus/utils-validation": "3.1.0",
|
|
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.0
|
|
108
|
-
"@docusaurus/types": "3.0
|
|
107
|
+
"@docusaurus/module-type-aliases": "3.1.0",
|
|
108
|
+
"@docusaurus/types": "3.1.0",
|
|
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": "a5e675821f0e8b70b591fcebf19fd60a70d55548"
|
|
128
128
|
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the MIT license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/
|
|
7
|
-
import { type ReactNode } from 'react';
|
|
8
|
-
type LinksCollector = {
|
|
9
|
-
collectLink: (link: string) => void;
|
|
10
|
-
};
|
|
11
|
-
type StatefulLinksCollector = LinksCollector & {
|
|
12
|
-
getCollectedLinks: () => string[];
|
|
13
|
-
};
|
|
14
|
-
export declare const createStatefulLinksCollector: () => StatefulLinksCollector;
|
|
15
|
-
export declare const useLinksCollector: () => LinksCollector;
|
|
16
|
-
export declare function LinksCollectorProvider({ children, linksCollector, }: {
|
|
17
|
-
children: ReactNode;
|
|
18
|
-
linksCollector: LinksCollector;
|
|
19
|
-
}): JSX.Element;
|
|
20
|
-
export {};
|