@docusaurus/core 3.1.0 → 3.1.1

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