@docusaurus/core 3.0.1 → 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/bin/docusaurus.mjs +24 -4
- package/lib/client/BrokenLinksContext.d.ts +19 -0
- package/lib/client/BrokenLinksContext.js +34 -0
- package/lib/client/exports/Link.js +13 -5
- 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 +202 -82
- package/lib/server/configValidation.d.ts +3 -2
- package/lib/server/configValidation.js +26 -14
- package/lib/server/getHostPort.js +4 -1
- package/lib/server/routes.js +9 -1
- package/package.json +10 -10
- package/lib/client/LinksCollector.d.ts +0 -20
- package/lib/client/LinksCollector.js +0 -26
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;
|
|
@@ -0,0 +1,34 @@
|
|
|
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 React, { useContext } from 'react';
|
|
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();
|
|
11
|
+
const allLinks = new Set();
|
|
12
|
+
return {
|
|
13
|
+
collectAnchor: (anchor) => {
|
|
14
|
+
typeof anchor !== 'undefined' && allAnchors.add(anchor);
|
|
15
|
+
},
|
|
16
|
+
collectLink: (link) => {
|
|
17
|
+
typeof link !== 'undefined' && allLinks.add(link);
|
|
18
|
+
},
|
|
19
|
+
getCollectedAnchors: () => [...allAnchors],
|
|
20
|
+
getCollectedLinks: () => [...allLinks],
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const Context = React.createContext({
|
|
24
|
+
collectAnchor: () => {
|
|
25
|
+
// No-op for client
|
|
26
|
+
},
|
|
27
|
+
collectLink: () => {
|
|
28
|
+
// No-op for client
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
export const useBrokenLinksContext = () => useContext(Context);
|
|
32
|
+
export function BrokenLinksProvider({ children, brokenLinks, }) {
|
|
33
|
+
return <Context.Provider value={brokenLinks}>{children}</Context.Provider>;
|
|
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
|
|
@@ -97,10 +97,18 @@ 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
|
-
|
|
103
|
-
|
|
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)) {
|
|
108
|
+
brokenLinks.collectLink(targetLink);
|
|
109
|
+
}
|
|
110
|
+
if (props.id) {
|
|
111
|
+
brokenLinks.collectAnchor(props.id);
|
|
104
112
|
}
|
|
105
113
|
return isRegularHtmlLink ? (
|
|
106
114
|
// 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,36 +8,116 @@
|
|
|
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
|
-
|
|
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);
|
|
22
21
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
|
59
|
+
function isPathBrokenLink(linkPath) {
|
|
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;
|
|
30
68
|
}
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
function isAnchorBrokenLink(linkPath) {
|
|
70
|
+
const { pathname, hash } = linkPath;
|
|
71
|
+
// Link has no hash: it can't be a broken anchor link
|
|
72
|
+
if (hash === undefined) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
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));
|
|
82
|
+
// link with anchor to a page that does not exist (or did not collect any
|
|
83
|
+
// link/anchor) is considered as a broken anchor
|
|
84
|
+
if (!targetPage) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
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;
|
|
39
93
|
}
|
|
40
|
-
return
|
|
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) => {
|
|
104
|
+
const linkPath = (0, utils_1.parseURLPath)(link, pagePath);
|
|
105
|
+
if (helper.isPathBrokenLink(linkPath)) {
|
|
106
|
+
brokenLinks.push({
|
|
107
|
+
link,
|
|
108
|
+
resolvedLink: (0, utils_1.serializeURLPath)(linkPath),
|
|
109
|
+
anchor: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (helper.isAnchorBrokenLink(linkPath)) {
|
|
113
|
+
brokenLinks.push({
|
|
114
|
+
link,
|
|
115
|
+
resolvedLink: (0, utils_1.serializeURLPath)(linkPath),
|
|
116
|
+
anchor: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return brokenLinks;
|
|
41
121
|
}
|
|
42
122
|
/**
|
|
43
123
|
* The route defs can be recursive, and have a parent match-all route. We don't
|
|
@@ -49,25 +129,60 @@ function filterIntermediateRoutes(routesInput) {
|
|
|
49
129
|
const routesWithout404 = routesInput.filter((route) => route.path !== '*');
|
|
50
130
|
return (0, utils_2.getAllFinalRoutes)(routesWithout404);
|
|
51
131
|
}
|
|
52
|
-
function
|
|
132
|
+
function getBrokenLinks({ collectedLinks, routes, }) {
|
|
53
133
|
const filteredRoutes = filterIntermediateRoutes(routes);
|
|
54
|
-
const
|
|
55
|
-
|
|
134
|
+
const helper = createBrokenLinksHelper({
|
|
135
|
+
collectedLinks,
|
|
136
|
+
routes: filteredRoutes,
|
|
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;
|
|
56
153
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function pageBrokenLinksMessage(pagePath, brokenLinks) {
|
|
66
|
-
return `
|
|
67
|
-
- On source page path = ${pagePath}:
|
|
154
|
+
function brokenLinkMessage(brokenLink) {
|
|
155
|
+
const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink;
|
|
156
|
+
return `${brokenLink.link}${showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : ''}`;
|
|
157
|
+
}
|
|
158
|
+
function createBrokenLinksMessage(pagePath, brokenLinks) {
|
|
159
|
+
const type = brokenLinks[0]?.anchor === true ? 'anchor' : 'link';
|
|
160
|
+
const anchorMessage = brokenLinks.length > 0
|
|
161
|
+
? `- Broken ${type} on source page path = ${pagePath}:
|
|
68
162
|
-> linking to ${brokenLinks
|
|
69
163
|
.map(brokenLinkMessage)
|
|
70
|
-
.join('\n -> linking to ')}
|
|
164
|
+
.join('\n -> linking to ')}`
|
|
165
|
+
: '';
|
|
166
|
+
return `${anchorMessage}`;
|
|
167
|
+
}
|
|
168
|
+
function createBrokenAnchorsMessage(brokenAnchors) {
|
|
169
|
+
if (Object.keys(brokenAnchors).length === 0) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return `Docusaurus found broken anchors!
|
|
173
|
+
|
|
174
|
+
Please check the pages of your site in the list below, and make sure you don't reference any anchor that does not exist.
|
|
175
|
+
Note: it's possible to ignore broken anchors with the 'onBrokenAnchors' Docusaurus configuration, and let the build pass.
|
|
176
|
+
|
|
177
|
+
Exhaustive list of all broken anchors found:
|
|
178
|
+
${Object.entries(brokenAnchors)
|
|
179
|
+
.map(([pagePath, brokenLinks]) => createBrokenLinksMessage(pagePath, brokenLinks))
|
|
180
|
+
.join('\n')}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function createBrokenPathsMessage(brokenPathsMap) {
|
|
184
|
+
if (Object.keys(brokenPathsMap).length === 0) {
|
|
185
|
+
return undefined;
|
|
71
186
|
}
|
|
72
187
|
/**
|
|
73
188
|
* If there's a broken link appearing very often, it is probably a broken link
|
|
@@ -75,7 +190,7 @@ function getBrokenLinksErrorMessage(allBrokenLinks) {
|
|
|
75
190
|
* this out. See https://github.com/facebook/docusaurus/issues/3567#issuecomment-706973805
|
|
76
191
|
*/
|
|
77
192
|
function getLayoutBrokenLinksHelpMessage() {
|
|
78
|
-
const flatList = Object.entries(
|
|
193
|
+
const flatList = Object.entries(brokenPathsMap).flatMap(([pagePage, brokenLinks]) => brokenLinks.map((brokenLink) => ({ pagePage, brokenLink })));
|
|
79
194
|
const countedBrokenLinks = lodash_1.default.countBy(flatList, (item) => item.brokenLink.link);
|
|
80
195
|
const FrequencyThreshold = 5; // Is this a good value?
|
|
81
196
|
const frequentLinks = Object.entries(countedBrokenLinks)
|
|
@@ -97,60 +212,65 @@ Please check the pages of your site in the list below, and make sure you don't r
|
|
|
97
212
|
Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.${getLayoutBrokenLinksHelpMessage()}
|
|
98
213
|
|
|
99
214
|
Exhaustive list of all broken links found:
|
|
100
|
-
${Object.entries(
|
|
101
|
-
.map(([pagePath,
|
|
215
|
+
${Object.entries(brokenPathsMap)
|
|
216
|
+
.map(([pagePath, brokenPaths]) => createBrokenLinksMessage(pagePath, brokenPaths))
|
|
102
217
|
.join('\n')}
|
|
103
218
|
`;
|
|
104
219
|
}
|
|
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'));
|
|
220
|
+
function splitBrokenLinks(brokenLinks) {
|
|
221
|
+
const brokenPaths = {};
|
|
222
|
+
const brokenAnchors = {};
|
|
223
|
+
Object.entries(brokenLinks).forEach(([pathname, pageBrokenLinks]) => {
|
|
224
|
+
const [anchorBrokenLinks, pathBrokenLinks] = lodash_1.default.partition(pageBrokenLinks, (link) => link.anchor);
|
|
225
|
+
if (pathBrokenLinks.length > 0) {
|
|
226
|
+
brokenPaths[pathname] = pathBrokenLinks;
|
|
125
227
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
228
|
+
if (anchorBrokenLinks.length > 0) {
|
|
229
|
+
brokenAnchors[pathname] = anchorBrokenLinks;
|
|
130
230
|
}
|
|
131
|
-
|
|
231
|
+
});
|
|
232
|
+
return { brokenPaths, brokenAnchors };
|
|
233
|
+
}
|
|
234
|
+
function reportBrokenLinks({ brokenLinks, onBrokenLinks, onBrokenAnchors, }) {
|
|
235
|
+
// We need to split the broken links reporting in 2 for better granularity
|
|
236
|
+
// This is because we need to report broken path/anchors independently
|
|
237
|
+
// For v3.x retro-compatibility, we can't throw by default for broken anchors
|
|
238
|
+
// TODO Docusaurus v4: make onBrokenAnchors throw by default?
|
|
239
|
+
const { brokenPaths, brokenAnchors } = splitBrokenLinks(brokenLinks);
|
|
240
|
+
const pathErrorMessage = createBrokenPathsMessage(brokenPaths);
|
|
241
|
+
if (pathErrorMessage) {
|
|
242
|
+
logger_1.default.report(onBrokenLinks)(pathErrorMessage);
|
|
132
243
|
}
|
|
133
|
-
|
|
244
|
+
const anchorErrorMessage = createBrokenAnchorsMessage(brokenAnchors);
|
|
245
|
+
if (anchorErrorMessage) {
|
|
246
|
+
logger_1.default.report(onBrokenAnchors)(anchorErrorMessage);
|
|
247
|
+
}
|
|
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;
|
|
134
265
|
}
|
|
135
|
-
async function handleBrokenLinks({
|
|
136
|
-
if (onBrokenLinks === 'ignore') {
|
|
266
|
+
async function handleBrokenLinks({ collectedLinks, onBrokenLinks, onBrokenAnchors, routes, }) {
|
|
267
|
+
if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
|
|
137
268
|
return;
|
|
138
269
|
}
|
|
139
|
-
|
|
140
|
-
// file system. It is not a broken link, it may simply be a link to an
|
|
141
|
-
// existing static file...
|
|
142
|
-
const allCollectedLinksFiltered = await filterExistingFileLinks({
|
|
143
|
-
allCollectedLinks,
|
|
144
|
-
baseUrl,
|
|
145
|
-
outDir,
|
|
146
|
-
});
|
|
147
|
-
const allBrokenLinks = getAllBrokenLinks({
|
|
148
|
-
allCollectedLinks: allCollectedLinksFiltered,
|
|
270
|
+
const brokenLinks = getBrokenLinks({
|
|
149
271
|
routes,
|
|
272
|
+
collectedLinks: normalizeCollectedLinks(collectedLinks),
|
|
150
273
|
});
|
|
151
|
-
|
|
152
|
-
if (errorMessage) {
|
|
153
|
-
logger_1.default.report(onBrokenLinks)(errorMessage);
|
|
154
|
-
}
|
|
274
|
+
reportBrokenLinks({ brokenLinks, onBrokenLinks, onBrokenAnchors });
|
|
155
275
|
}
|
|
156
276
|
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}',
|
|
@@ -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.
|
|
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.
|
|
47
|
-
"@docusaurus/logger": "3.
|
|
48
|
-
"@docusaurus/mdx-loader": "3.
|
|
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.
|
|
51
|
-
"@docusaurus/utils-common": "3.
|
|
52
|
-
"@docusaurus/utils-validation": "3.
|
|
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.
|
|
108
|
-
"@docusaurus/types": "3.
|
|
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
|
}
|
|
@@ -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 {};
|
|
@@ -1,26 +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 React, { useContext } from 'react';
|
|
8
|
-
export const createStatefulLinksCollector = () => {
|
|
9
|
-
// Set to dedup, as it's not useful to collect multiple times the same link
|
|
10
|
-
const allLinks = new Set();
|
|
11
|
-
return {
|
|
12
|
-
collectLink: (link) => {
|
|
13
|
-
allLinks.add(link);
|
|
14
|
-
},
|
|
15
|
-
getCollectedLinks: () => [...allLinks],
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
const Context = React.createContext({
|
|
19
|
-
collectLink: () => {
|
|
20
|
-
// No-op for client. We only use the broken links checker server-side.
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
export const useLinksCollector = () => useContext(Context);
|
|
24
|
-
export function LinksCollectorProvider({ children, linksCollector, }) {
|
|
25
|
-
return <Context.Provider value={linksCollector}>{children}</Context.Provider>;
|
|
26
|
-
}
|