@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.
@@ -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
- if (!isInternalCommand(process.argv.slice(2)[0])) {
241
- await externalCommand(cli);
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
- if (!process.argv.slice(2).length) {
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 createStatefulLinksCollector = () => {
9
- // Set to dedup, as it's not useful to collect multiple times the same link
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. We only use the broken links checker server-side.
28
+ // No-op for client
21
29
  },
22
30
  });
23
- export const useLinksCollector = () => useContext(Context);
24
- export function LinksCollectorProvider({ children, linksCollector, }) {
25
- return <Context.Provider value={linksCollector}>{children}</Context.Provider>;
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 { useLinksCollector } from '../LinksCollector';
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 linksCollector = useLinksCollector();
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
- linksCollector.collectLink(targetLink);
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 { createStatefulLinksCollector, LinksCollectorProvider, } from './LinksCollector';
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 linksCollector = createStatefulLinksCollector();
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
- <LinksCollectorProvider linksCollector={linksCollector}>
70
+ <BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
71
71
  <App />
72
- </LinksCollectorProvider>
72
+ </BrokenLinksProvider>
73
73
  </StaticRouter>
74
74
  </HelmetProvider>
75
75
  </Loadable.Capture>);
76
76
  const appHtml = await renderStaticApp(app);
77
- onLinksCollected(location, linksCollector.getCollectedLinks());
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();
@@ -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: { baseUrl, onBrokenLinks, staticDirectories: staticDirectoriesOption, }, routes, } = props;
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 allCollectedLinks = {};
118
+ const collectedLinks = {};
119
119
  const headTags = {};
120
120
  let serverConfig = await (0, server_2.default)({
121
121
  props,
122
- onLinksCollected: (staticPagePath, links) => {
123
- allCollectedLinks[staticPagePath] = links;
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
- allCollectedLinks,
193
+ collectedLinks,
194
194
  routes,
195
195
  onBrokenLinks,
196
- outDir,
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
- export declare function handleBrokenLinks({ allCollectedLinks, onBrokenLinks, routes, baseUrl, outDir, }: {
9
- allCollectedLinks: {
10
- [location: string]: string[];
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
- // matchRoutes does not support qs/anchors, so we remove it!
20
- function onlyPathname(link) {
21
- return link.split('#')[0].split('?')[0];
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
- return pageLinks.map(resolveLink).filter((l) => isBrokenLink(l.resolvedLink));
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 getAllBrokenLinks({ allCollectedLinks, routes, }) {
77
+ function getBrokenLinks({ collectedLinks, routes, }) {
53
78
  const filteredRoutes = filterIntermediateRoutes(routes);
54
- const allBrokenLinks = lodash_1.default.mapValues(allCollectedLinks, (pageLinks, pagePath) => getPageBrokenLinks({ pageLinks, pagePath, routes: filteredRoutes }));
55
- return lodash_1.default.pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0);
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 getBrokenLinksErrorMessage(allBrokenLinks) {
58
- if (Object.keys(allBrokenLinks).length === 0) {
59
- return undefined;
60
- }
61
- function brokenLinkMessage(brokenLink) {
62
- const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink;
63
- return `${brokenLink.link}${showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : ''}`;
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(allBrokenLinks).flatMap(([pagePage, brokenLinks]) => brokenLinks.map((brokenLink) => ({ pagePage, brokenLink })));
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(allBrokenLinks)
101
- .map(([pagePath, brokenLinks]) => pageBrokenLinksMessage(pagePath, brokenLinks))
148
+ ${Object.entries(brokenPathsMap)
149
+ .map(([pagePath, brokenPaths]) => createBrokenLinksMessage(pagePath, brokenPaths))
102
150
  .join('\n')}
103
151
  `;
104
152
  }
105
- async function isExistingFile(filePath) {
106
- try {
107
- return (await fs_extra_1.default.stat(filePath)).isFile();
108
- }
109
- catch {
110
- return false;
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
- for (const file of filePathsToTry) {
127
- if (await isExistingFile(file)) {
128
- return true;
129
- }
161
+ if (anchorBrokenLinks.length > 0) {
162
+ brokenAnchors[pathname] = anchorBrokenLinks;
130
163
  }
131
- return false;
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({ allCollectedLinks, onBrokenLinks, routes, baseUrl, outDir, }) {
136
- if (onBrokenLinks === 'ignore') {
182
+ async function handleBrokenLinks({ collectedLinks, onBrokenLinks, onBrokenAnchors, routes, }) {
183
+ if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
137
184
  return;
138
185
  }
139
- // If we link to a file like /myFile.zip, and the file actually exist for the
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,
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 DEFAULT_CONFIG: Pick<DocusaurusConfig, 'i18n' | 'onBrokenLinks' | 'onBrokenMarkdownLinks' | 'onDuplicateRoutes' | 'plugins' | 'themes' | 'presets' | 'headTags' | 'stylesheets' | 'scripts' | 'clientModules' | 'customFields' | 'themeConfig' | 'titleDelimiter' | 'noIndex' | 'tagline' | 'baseUrlIssueBanner' | 'staticDirectories' | 'markdown'>;
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 (utils_validation_1.Joi.alternatives()
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.1",
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.1",
47
- "@docusaurus/logger": "3.0.1",
48
- "@docusaurus/mdx-loader": "3.0.1",
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.1",
51
- "@docusaurus/utils-common": "3.0.1",
52
- "@docusaurus/utils-validation": "3.0.1",
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.1",
108
- "@docusaurus/types": "3.0.1",
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": "29d816067fd0022d4ca3bd5fdc42098dac9f7ffc"
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 {};