@docusaurus/utils 3.10.2 → 4.0.0-canary-6809

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.
Files changed (69) hide show
  1. package/lib/constants.js +1 -1
  2. package/lib/constants.js.map +1 -1
  3. package/lib/contentVisibilityUtils.js +1 -1
  4. package/lib/contentVisibilityUtils.js.map +1 -1
  5. package/lib/dataFileUtils.js.map +1 -1
  6. package/lib/emitUtils.js.map +1 -1
  7. package/lib/globUtils.d.ts +5 -3
  8. package/lib/globUtils.d.ts.map +1 -1
  9. package/lib/globUtils.js +9 -4
  10. package/lib/globUtils.js.map +1 -1
  11. package/lib/hashUtils.js.map +1 -1
  12. package/lib/i18nUtils.js.map +1 -1
  13. package/lib/index.d.ts +0 -2
  14. package/lib/index.d.ts.map +1 -1
  15. package/lib/index.js +2 -10
  16. package/lib/index.js.map +1 -1
  17. package/lib/jsUtils.js.map +1 -1
  18. package/lib/lastUpdateUtils.js +2 -2
  19. package/lib/lastUpdateUtils.js.map +1 -1
  20. package/lib/markdownHeadingIdUtils.js.map +1 -1
  21. package/lib/markdownLinks.js +1 -1
  22. package/lib/markdownLinks.js.map +1 -1
  23. package/lib/markdownUtils.d.ts.map +1 -1
  24. package/lib/markdownUtils.js +58 -20
  25. package/lib/markdownUtils.js.map +1 -1
  26. package/lib/moduleUtils.d.ts +3 -1
  27. package/lib/moduleUtils.d.ts.map +1 -1
  28. package/lib/moduleUtils.js +25 -14
  29. package/lib/moduleUtils.js.map +1 -1
  30. package/lib/pathUtils.d.ts.map +1 -1
  31. package/lib/pathUtils.js.map +1 -1
  32. package/lib/routeUtils.js.map +1 -1
  33. package/lib/slugger.js.map +1 -1
  34. package/lib/tags.js.map +1 -1
  35. package/lib/urlUtils.d.ts +3 -3
  36. package/lib/urlUtils.d.ts.map +1 -1
  37. package/lib/urlUtils.js +20 -25
  38. package/lib/urlUtils.js.map +1 -1
  39. package/lib/vcs/gitUtils.d.ts.map +1 -1
  40. package/lib/vcs/gitUtils.js +27 -60
  41. package/lib/vcs/gitUtils.js.map +1 -1
  42. package/lib/vcs/vcs.js.map +1 -1
  43. package/lib/vcs/vcsGitEager.d.ts.map +1 -1
  44. package/lib/vcs/vcsGitEager.js +3 -2
  45. package/lib/vcs/vcsGitEager.js.map +1 -1
  46. package/lib/vcs/vcsHardcoded.js.map +1 -1
  47. package/lib/webpackUtils.d.ts.map +1 -1
  48. package/lib/webpackUtils.js +0 -1
  49. package/lib/webpackUtils.js.map +1 -1
  50. package/package.json +23 -25
  51. package/src/constants.ts +1 -1
  52. package/src/contentVisibilityUtils.ts +1 -1
  53. package/src/dataFileUtils.ts +1 -1
  54. package/src/globUtils.ts +10 -4
  55. package/src/index.ts +0 -8
  56. package/src/lastUpdateUtils.ts +2 -2
  57. package/src/markdownHeadingIdUtils.ts +2 -2
  58. package/src/markdownLinks.ts +1 -1
  59. package/src/markdownUtils.ts +83 -20
  60. package/src/moduleUtils.ts +35 -14
  61. package/src/urlUtils.ts +25 -29
  62. package/src/vcs/gitUtils.ts +34 -91
  63. package/src/vcs/vcsGitEager.ts +4 -2
  64. package/src/webpackUtils.ts +0 -1
  65. package/lib/regExpUtils.d.ts +0 -8
  66. package/lib/regExpUtils.d.ts.map +0 -1
  67. package/lib/regExpUtils.js +0 -15
  68. package/lib/regExpUtils.js.map +0 -1
  69. package/src/regExpUtils.ts +0 -12
@@ -9,7 +9,7 @@ type Env = 'production' | 'development';
9
9
 
10
10
  /**
11
11
  * To easily work on draft/unlisted in dev mode, use this env variable!
12
- * SIMULATE_PRODUCTION_VISIBILITY=true yarn start:website
12
+ * SIMULATE_PRODUCTION_VISIBILITY=true pnpm start:website
13
13
  */
14
14
  const simulateProductionVisibility =
15
15
  process.env.SIMULATE_PRODUCTION_VISIBILITY === 'true';
@@ -64,7 +64,7 @@ export async function readDataFile(params: DataFileParams): Promise<unknown> {
64
64
  process.cwd(),
65
65
  filePath,
66
66
  )} looks invalid (not Yaml nor JSON).`;
67
- throw new Error(msg, {cause: err as Error});
67
+ throw new Error(msg, {cause: err});
68
68
  }
69
69
  }
70
70
 
package/src/globUtils.ts CHANGED
@@ -10,11 +10,15 @@
10
10
  import path from 'path';
11
11
  import Micromatch from 'micromatch'; // Note: Micromatch is used by Globby
12
12
  import {addSuffix} from '@docusaurus/utils-common';
13
- import Globby from 'globby';
13
+ import * as Tinyglobby from 'tinyglobby';
14
14
  import {posixPath} from './pathUtils';
15
15
 
16
+ type GlobOptions = Tinyglobby.GlobOptions;
17
+
18
+ // TODO Docusaurus v4 refactor, hide lib behind home-made abstraction
19
+ // See https://github.com/facebook/docusaurus/pull/11042
16
20
  /** A re-export of the globby instance. */
17
- export {Globby};
21
+ export const Globby = Tinyglobby.glob;
18
22
 
19
23
  /**
20
24
  * The default glob patterns we ignore when sourcing content.
@@ -93,7 +97,7 @@ export function createAbsoluteFilePathMatcher(
93
97
  // See https://github.com/facebook/docusaurus/pull/4222#issuecomment-795517329
94
98
  export async function safeGlobby(
95
99
  patterns: string[],
96
- options?: Globby.GlobbyOptions,
100
+ options?: GlobOptions,
97
101
  ): Promise<string[]> {
98
102
  // Required for Windows support, as paths using \ should not be used by globby
99
103
  // (also using the windows hard drive prefix like c: is not a good idea)
@@ -132,6 +136,8 @@ export const isTranslatableSourceFile: (filePath: string) => boolean = (() => {
132
136
  export async function globTranslatableSourceFiles(
133
137
  patterns: string[],
134
138
  ): Promise<string[]> {
135
- const filePaths = await safeGlobby(patterns);
139
+ const filePaths = await safeGlobby(patterns, {
140
+ absolute: true,
141
+ });
136
142
  return filePaths.filter(isTranslatableSourceFile);
137
143
  }
package/src/index.ts CHANGED
@@ -24,13 +24,6 @@ export {
24
24
  WEBPACK_URL_LOADER_LIMIT,
25
25
  } from './constants';
26
26
  export {generate, readOutputHTMLFile} from './emitUtils';
27
- export {
28
- // TODO Docusaurus v4: remove these legacy exports,
29
- // they are only kept for retro-compatibility
30
- getFileCommitDate,
31
- FileNotTrackedError,
32
- GitNotFoundError,
33
- } from './vcs/gitUtils';
34
27
  export {
35
28
  mergeTranslations,
36
29
  updateTranslationFileMessages,
@@ -121,7 +114,6 @@ export {
121
114
  getFolderContainingFile,
122
115
  } from './dataFileUtils';
123
116
  export {isDraft, isUnlisted} from './contentVisibilityUtils';
124
- export {escapeRegexp} from './regExpUtils';
125
117
  export {flattenRoutes} from './routeUtils';
126
118
 
127
119
  export {
@@ -89,11 +89,11 @@ export async function readLastUpdateData(
89
89
  });
90
90
 
91
91
  const lastUpdatedBy = showLastUpdateAuthor
92
- ? frontMatterAuthor ?? (await getLastUpdateBy())
92
+ ? (frontMatterAuthor ?? (await getLastUpdateBy()))
93
93
  : undefined;
94
94
 
95
95
  const lastUpdatedAt = showLastUpdateTime
96
- ? frontMatterTimestamp ?? (await getLastUpdateAt())
96
+ ? (frontMatterTimestamp ?? (await getLastUpdateAt()))
97
97
  : undefined;
98
98
 
99
99
  return {
@@ -194,8 +194,8 @@ export function writeMarkdownHeadingId(
194
194
  const headingId = overwrite
195
195
  ? undefined
196
196
  : migrate
197
- ? parsedHeading.id
198
- : undefined;
197
+ ? parsedHeading.id
198
+ : undefined;
199
199
 
200
200
  return addHeadingId(
201
201
  parsedHeading.text,
@@ -98,6 +98,6 @@ export function resolveMarkdownLinkPathname(
98
98
  );
99
99
 
100
100
  return aliasedSourceMatch
101
- ? sourceToPermalink.get(aliasedSourceMatch) ?? null
101
+ ? (sourceToPermalink.get(aliasedSourceMatch) ?? null)
102
102
  : null;
103
103
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import logger from '@docusaurus/logger';
9
9
  import matter from '@11ty/gray-matter';
10
+
10
11
  import type {
11
12
  ParseFrontMatter,
12
13
  DefaultParseFrontMatter,
@@ -16,6 +17,41 @@ import type {
16
17
  // server-side when we infer metadata like `title` and `description` from the
17
18
  // content. Most parsing is still done in MDX through the mdx-loader.
18
19
 
20
+ const MDXEscapingUtils = (function () {
21
+ const MARKUP_CHARS = ['_', ':', '*', '<', '>', '~', '!', '[', ']', '{', '}'];
22
+
23
+ const markerOf = (char: string) => `\u{FFFE}${char.codePointAt(0)}\u{FFFF}`;
24
+
25
+ const MARKUP_CHARS_ESCAPE_MAP = new Map(
26
+ MARKUP_CHARS.map((char) => [char, markerOf(char)]),
27
+ );
28
+ const MARKUP_CHARS_UNESCAPE_MAP = new Map(
29
+ MARKUP_CHARS.map((char) => [markerOf(char), char]),
30
+ );
31
+
32
+ const MARKUP_CHARS_REGEX = new RegExp(
33
+ `[${RegExp.escape(MARKUP_CHARS.join(''))}]`,
34
+ 'g',
35
+ );
36
+ const MARKUP_MARKERS_REGEX = /\u{FFFE}\d+\u{FFFF}/gu;
37
+
38
+ function escapeMDX(str: string) {
39
+ return str.replace(
40
+ MARKUP_CHARS_REGEX,
41
+ (char) => MARKUP_CHARS_ESCAPE_MAP.get(char)!,
42
+ );
43
+ }
44
+
45
+ function unescapeMDX(str: string) {
46
+ return str.replace(
47
+ MARKUP_MARKERS_REGEX,
48
+ (marker) => MARKUP_CHARS_UNESCAPE_MAP.get(marker) ?? marker,
49
+ );
50
+ }
51
+
52
+ return {escapeMDX, unescapeMDX};
53
+ })();
54
+
19
55
  /**
20
56
  * Hacky temporary escape hatch for Crowdin bad MDX support
21
57
  * See https://docusaurus.io/docs/i18n/crowdin#mdx
@@ -92,6 +128,7 @@ export function createExcerpt(fileString: string): string | undefined {
92
128
  .split(/\r?\n/);
93
129
  let inCode = false;
94
130
  let inImport = false;
131
+ let inHTML = false;
95
132
  let lastCodeFence = '';
96
133
 
97
134
  for (const fileLine of fileLines) {
@@ -127,11 +164,44 @@ export function createExcerpt(fileString: string): string | undefined {
127
164
  continue;
128
165
  }
129
166
 
130
- const cleanedLine = fileLine
167
+ // Skip lines inside a multi-line JSX/HTML element. An opening "<Tag ..."
168
+ // with no ">" on the same line would otherwise leak into the excerpt
169
+ // (e.g. "<MyComponent"). Skip until the element's tag closes.
170
+ if (inHTML) {
171
+ if (fileLine.includes('>')) {
172
+ inHTML = false;
173
+ }
174
+ continue;
175
+ }
176
+ if (/^\s*<[a-z][^>]*$/i.test(fileLine)) {
177
+ inHTML = true;
178
+ continue;
179
+ }
180
+
181
+ // Pre/postprocessing to handle MDX special chars within inline code blocks
182
+ // See https://github.com/facebook/docusaurus/pull/11821
183
+ function preprocessLine(str: string) {
184
+ return (
185
+ str
186
+ // Ignore internal Unicode markers found in input
187
+ // This ensures no possible conflict with our MDX escaping logic
188
+ .replace(/[\u{FFFE}\u{FFFF}]/gu, '')
189
+ // Unwrap inline code and escape special MDX chars within it
190
+ .replace(/`(?<text>.+?)`/g, (_, text) => {
191
+ return MDXEscapingUtils.escapeMDX(text);
192
+ })
193
+ );
194
+ }
195
+ function postProcessLine(str: string) {
196
+ // Restore escaped MDX chars that have been previously escaped
197
+ return MDXEscapingUtils.unescapeMDX(str);
198
+ }
199
+
200
+ const cleanedLine = preprocessLine(fileLine)
131
201
  // Remove HTML tags.
132
202
  .replace(/<[^>]*>/g, '')
133
203
  // Remove Title headers
134
- .replace(/^#[^#]+#?/gm, '')
204
+ .replace(/^#(?!#).*/gm, '')
135
205
  // Remove Markdown + ATX-style headers
136
206
  .replace(/^#{1,6}\s*(?<text>[^#]*?)\s*#{0,6}/gm, '$1')
137
207
  // Remove emphasis.
@@ -144,8 +214,6 @@ export function createExcerpt(fileString: string): string | undefined {
144
214
  .replace(/\[\^.+?\](?:: .*$)?/g, '')
145
215
  // Remove inline links.
146
216
  .replace(/\[(?<alt>.*?)\][[(].*?[\])]/g, '$1')
147
- // Remove inline code.
148
- .replace(/`(?<text>.+?)`/g, '$1')
149
217
  // Remove blockquotes.
150
218
  .replace(/^\s{0,3}>\s?/g, '')
151
219
  // Remove admonition definition.
@@ -154,10 +222,12 @@ export function createExcerpt(fileString: string): string | undefined {
154
222
  .replace(/\s?:(?:::|[^:\n])+:/g, '')
155
223
  // Remove custom Markdown heading id.
156
224
  .replace(/\{#*[\w-]+\}/, '')
225
+ // Collapse whitespace left behind by the removals above.
226
+ .replace(/\s+/g, ' ')
157
227
  .trim();
158
228
 
159
229
  if (cleanedLine) {
160
- return cleanedLine;
230
+ return postProcessLine(cleanedLine);
161
231
  }
162
232
  }
163
233
 
@@ -191,14 +261,7 @@ export function parseFileContentFrontMatter(fileContent: string): {
191
261
  // Unfortunately, this becomes a problem when we mutate returned front matter
192
262
  // We want to make it possible as part of the parseFrontMatter API
193
263
  // So we make it safe to mutate by always providing a deep copy
194
- const frontMatter =
195
- // And of course structuredClone() doesn't work well with Date in Jest...
196
- // See https://github.com/jestjs/jest/issues/2549
197
- // So we parse again for tests with a {} option object
198
- // This undocumented empty option object disables gray-matter caching..
199
- process.env.JEST_WORKER_ID
200
- ? matter(fileContent, {}).data
201
- : structuredClone(data);
264
+ const frontMatter = structuredClone(data);
202
265
 
203
266
  return {
204
267
  frontMatter,
@@ -247,18 +310,18 @@ export function parseMarkdownContentTitle(
247
310
  const removeContentTitleOption = options?.removeContentTitle ?? false;
248
311
 
249
312
  const content = contentUntrimmed.trim();
250
- // We only need to detect import statements that will be parsed by MDX as
251
- // `import` nodes, as broken syntax can't render anyways. That means any block
252
- // that has `import` at the very beginning and surrounded by empty lines.
253
- const contentWithoutImport = content
254
- .replace(/^(?:import\s(?:.|\r?\n(?!\r?\n))*(?:\r?\n){2,})*/, '')
313
+ // We only need to detect import/export statements that will be parsed by MDX as
314
+ // `import` or `export` nodes, as broken syntax can't render anyways. That means any block
315
+ // that has `import` or `export` at the very beginning and surrounded by empty lines.
316
+ const contentWithoutImportExport = content
317
+ .replace(/^(?:(?:import|export)\s(?:.|\r?\n(?!\r?\n))*(?:\r?\n){2,})*/, '')
255
318
  .trim();
256
319
 
257
320
  const regularTitleMatch = /^#[ \t]+(?<title>[^ \t].*)(?:\r?\n|$)/.exec(
258
- contentWithoutImport,
321
+ contentWithoutImportExport,
259
322
  );
260
323
  const alternateTitleMatch = /^(?<title>.*)\r?\n=+(?:\r?\n|$)/.exec(
261
- contentWithoutImport,
324
+ contentWithoutImportExport,
262
325
  );
263
326
 
264
327
  const titleMatch = regularTitleMatch ?? alternateTitleMatch;
@@ -5,33 +5,54 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import jiti from 'jiti';
9
8
  import logger from '@docusaurus/logger';
9
+ import {createJiti} from 'jiti';
10
+
11
+ const DEBUG = false;
12
+
13
+ const jiti = createJiti(__filename, {
14
+ // Transpilation cache, can be safely enabled
15
+ fsCache: true,
16
+ // Bypass Node.js runtime require cache for hot reloads
17
+ moduleCache: false,
18
+
19
+ interopDefault: true,
20
+ debug: DEBUG,
21
+ });
10
22
 
11
23
  /*
12
24
  jiti is able to load ESM, CJS, JSON, TS modules
13
25
  */
14
- export async function loadFreshModule(modulePath: string): Promise<unknown> {
26
+ export async function loadFreshModule(
27
+ modulePath: string,
28
+ options?: {
29
+ default?: true; // Use this when only the default export matters
30
+ },
31
+ ): Promise<unknown> {
15
32
  if (typeof modulePath !== 'string') {
16
33
  throw new Error(
17
34
  logger.interpolate`Invalid module path of type "name=${typeof modulePath}" with value "name=${modulePath}"`,
18
35
  );
19
36
  }
20
37
  try {
21
- const load = jiti(__filename, {
22
- // Transpilation cache, can be safely enabled
23
- cache: true,
24
- // Bypass Node.js runtime require cache
25
- // Same as "import-fresh" package we used previously
26
- requireCache: false,
27
- // Only take into consideration the default export
28
- // For now we don't need named exports
29
- // This also helps normalize return value for both CJS/ESM/TS modules
30
- interopDefault: true,
31
- // debug: true,
38
+ const module = await jiti.import(modulePath, {
39
+ default: options?.default,
32
40
  });
33
41
 
34
- return load(modulePath);
42
+ if (DEBUG) {
43
+ console.log('Jiti module loaded', {
44
+ modulePath,
45
+ options,
46
+ type: typeof module,
47
+ keys:
48
+ module && typeof module === 'object'
49
+ ? Object.keys(module)
50
+ : undefined,
51
+ module,
52
+ });
53
+ }
54
+
55
+ return module;
35
56
  } catch (error) {
36
57
  throw new Error(
37
58
  logger.interpolate`Docusaurus could not load module at path path=${modulePath}`,
package/src/urlUtils.ts CHANGED
@@ -138,8 +138,8 @@ export function fileToPath(file: string): string {
138
138
  * Similar to `encodeURI`, but uses `encodeURIComponent` and assumes there's no
139
139
  * query.
140
140
  *
141
- * `encodeURI("/question?/answer")` => `"/question?/answer#section"`;
142
- * `encodePath("/question?/answer#section")` => `"/question%3F/answer%23foo"`
141
+ * `encodeURI("/question?/answer")` => `"/question?/answer"`;
142
+ * `encodePath("/question?/answer#section")` => `"/question%3F/answer%23section"`
143
143
  */
144
144
  export function encodePath(userPath: string): string {
145
145
  return userPath
@@ -156,26 +156,20 @@ export function isValidPathname(str: string): boolean {
156
156
  if (!str.startsWith('/')) {
157
157
  return false;
158
158
  }
159
- try {
160
- const parsedPathname = new URL(str, 'https://domain.com').pathname;
161
- return parsedPathname === str || parsedPathname === encodeURI(str);
162
- } catch {
159
+ const url = URL.parse(str, 'https://domain.com');
160
+ if (url === null) {
163
161
  return false;
164
162
  }
163
+ const parsedPathname = url.pathname;
164
+ return parsedPathname === str || parsedPathname === encodeURI(str);
165
165
  }
166
166
 
167
- export function parseURLOrPath(url: string, base?: string | URL): URL {
168
- try {
169
- // TODO Docusaurus v4: use URL.parse()
170
- // Node 24 supports it, use URL.parse could be faster?
171
- // see https://kilianvalkhof.com/2024/javascript/the-problem-with-new-url-and-how-url-parse-fixes-that/
172
- return new URL(url, base ?? 'https://example.com');
173
- } catch (e) {
174
- throw new Error(
175
- `Can't parse URL ${url}${base ? ` with base ${base}` : ''}`,
176
- {cause: e},
177
- );
167
+ export function parseURLOrPath(str: string, base?: string | URL): URL {
168
+ const url = URL.parse(str, base ?? 'https://example.com');
169
+ if (url) {
170
+ return url;
178
171
  }
172
+ throw new Error(`Can't parse URL ${str}${base ? ` with base ${base}` : ''}`);
179
173
  }
180
174
 
181
175
  export type URLPath = {pathname: string; search?: string; hash?: string};
@@ -183,15 +177,19 @@ export type URLPath = {pathname: string; search?: string; hash?: string};
183
177
  export function toURLPath(url: URL): URLPath {
184
178
  const {pathname} = url;
185
179
 
180
+ // Only the part before the fragment can contain the query string. A "?"
181
+ // inside the hash (e.g. "/foo#bar?baz") must not be read as an empty query.
182
+ const beforeHash = url.hash ? url.href.slice(0, -url.hash.length) : url.href;
183
+
186
184
  // Fixes annoying url.search behavior
187
185
  // "" => undefined
188
186
  // "?" => ""
189
187
  // "?param => "param"
190
188
  const search = url.search
191
189
  ? url.search.slice(1)
192
- : url.href.includes('?')
193
- ? ''
194
- : undefined;
190
+ : beforeHash.includes('?')
191
+ ? ''
192
+ : undefined;
195
193
 
196
194
  // Fixes annoying url.hash behavior
197
195
  // "" => undefined
@@ -200,8 +198,8 @@ export function toURLPath(url: URL): URLPath {
200
198
  const hash = url.hash
201
199
  ? url.hash.slice(1)
202
200
  : url.href.includes('#')
203
- ? ''
204
- : undefined;
201
+ ? ''
202
+ : undefined;
205
203
 
206
204
  return {
207
205
  pathname,
@@ -319,13 +317,11 @@ export function buildHttpsUrl(
319
317
  * `git@github.com:facebook/docusaurus.git`.
320
318
  */
321
319
  export function hasSSHProtocol(sourceRepoUrl: string): boolean {
322
- try {
323
- if (new URL(sourceRepoUrl).protocol === 'ssh:') {
324
- return true;
325
- }
326
- return false;
327
- } catch {
328
- // Fails when there isn't a protocol
320
+ const url = URL.parse(sourceRepoUrl);
321
+ if (url === null) {
322
+ // Recognizes SCP-style addresses, implying SSH
323
+ // Example: git@github.com:facebook/docusaurus.git
329
324
  return /^(?:[\w-]+@)?[\w.-]+:[\w./-]+/.test(sourceRepoUrl);
330
325
  }
326
+ return url.protocol === 'ssh:';
331
327
  }
@@ -9,17 +9,13 @@ import path from 'path';
9
9
  import fs from 'fs-extra';
10
10
  import os from 'os';
11
11
  import _ from 'lodash';
12
- import execa from 'execa';
12
+ import {execa, execaSync} from 'execa';
13
13
  import PQueue from 'p-queue';
14
14
  import logger from '@docusaurus/logger';
15
15
 
16
16
  // Quite high/conservative concurrency value (it was previously "Infinity")
17
17
  // See https://github.com/facebook/docusaurus/pull/10915
18
- const DefaultGitCommandConcurrency =
19
- // TODO Docusaurus v4: bump node, availableParallelism() now always exists
20
- (typeof os.availableParallelism === 'function'
21
- ? os.availableParallelism()
22
- : os.cpus().length) * 4;
18
+ const DefaultGitCommandConcurrency = os.availableParallelism() * 4;
23
19
 
24
20
  const GitCommandConcurrencyEnv = process.env.DOCUSAURUS_GIT_COMMAND_CONCURRENCY
25
21
  ? parseInt(process.env.DOCUSAURUS_GIT_COMMAND_CONCURRENCY, 10)
@@ -38,8 +34,8 @@ const GitCommandQueue = new PQueue({
38
34
 
39
35
  const realHasGitFn = () => {
40
36
  try {
41
- return execa.sync('git', ['--version']).exitCode === 0;
42
- } catch (error) {
37
+ return execaSync('git', ['--version']).exitCode === 0;
38
+ } catch {
43
39
  return false;
44
40
  }
45
41
  };
@@ -147,32 +143,26 @@ export async function getFileCommitDate(
147
143
  // See why: https://github.com/facebook/docusaurus/pull/10022
148
144
  const resultFormat = includeAuthor ? 'RESULT:%ct,%an' : 'RESULT:%ct';
149
145
 
150
- const args = [
151
- `--format=${resultFormat}`,
152
- '--max-count=1',
153
- age === 'oldest' ? '--follow --diff-filter=A' : undefined,
154
- ]
155
- .filter(Boolean)
156
- .join(' ');
157
-
158
- // Do not include GPG signature in the log output
159
- // See https://github.com/facebook/docusaurus/pull/10022
160
- const command = `git -c log.showSignature=false log ${args} -- "${path.basename(
161
- file,
162
- )}"`;
163
-
164
146
  const result = (await GitCommandQueue.add(() => {
165
- return execa(command, {
166
- cwd: path.dirname(file),
167
- shell: true,
168
- });
169
- }))!;
170
-
171
- if (result.exitCode !== 0) {
172
- throw new Error(
173
- `Failed to retrieve the git history for file "${file}" with exit code ${result.exitCode}: ${result.stderr}`,
147
+ return execa(
148
+ 'git',
149
+ [
150
+ // Do not include GPG signature in the log output
151
+ // See https://github.com/facebook/docusaurus/pull/10022
152
+ '-c',
153
+ 'log.showSignature=false',
154
+ 'log',
155
+ `--format=${resultFormat}`,
156
+ '--max-count=1',
157
+ ...(age === 'oldest' ? ['--follow', '--diff-filter=A'] : []),
158
+ '--',
159
+ path.basename(file),
160
+ ],
161
+ {
162
+ cwd: path.dirname(file),
163
+ },
174
164
  );
175
- }
165
+ }))!;
176
166
 
177
167
  // We only parse the output line starting with our "RESULT:" prefix
178
168
  // See why https://github.com/facebook/docusaurus/pull/10022
@@ -280,33 +270,20 @@ export async function isGitInsideWorktree(cwd: string): Promise<boolean> {
280
270
  }
281
271
 
282
272
  export async function getGitRepoRoot(cwd: string): Promise<string> {
283
- const createErrorMessageBase = () => {
284
- return `Couldn't find the git repository root directory
285
- Failure while running ${logger.code(
286
- 'git rev-parse --show-toplevel',
287
- )} from cwd=${logger.path(cwd)}`;
288
- };
289
-
290
273
  const result = await execa('git', ['rev-parse', '--show-toplevel'], {
291
274
  cwd,
292
275
  }).catch((error) => {
293
276
  // We enter this rejection when cwd is not a dir for example
294
277
  throw new Error(
295
- `${createErrorMessageBase()}
278
+ `Couldn't find the git repository root directory
279
+ Failure while running ${logger.code(
280
+ 'git rev-parse --show-toplevel',
281
+ )} from cwd=${logger.path(cwd)}
296
282
  The command executed throws an error: ${error.message}`,
297
283
  {cause: error},
298
284
  );
299
285
  });
300
286
 
301
- if (result.exitCode !== 0) {
302
- throw new Error(
303
- `${createErrorMessageBase()}
304
- The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
305
- result.stderr,
306
- )}`,
307
- );
308
- }
309
-
310
287
  return fs.realpath.native(result.stdout.trim());
311
288
  }
312
289
 
@@ -316,13 +293,6 @@ The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
316
293
  export async function getGitSuperProjectRoot(
317
294
  cwd: string,
318
295
  ): Promise<string | null> {
319
- const createErrorMessageBase = () => {
320
- return `Couldn't find the git superproject root directory
321
- Failure while running ${logger.code(
322
- 'git rev-parse --show-superproject-working-tree',
323
- )} from cwd=${logger.path(cwd)}`;
324
- };
325
-
326
296
  const result = await execa(
327
297
  'git',
328
298
  ['rev-parse', '--show-superproject-working-tree'],
@@ -332,21 +302,15 @@ Failure while running ${logger.code(
332
302
  ).catch((error) => {
333
303
  // We enter this rejection when cwd is not a dir for example
334
304
  throw new Error(
335
- `${createErrorMessageBase()}
305
+ `Couldn't find the git superproject root directory
306
+ Failure while running ${logger.code(
307
+ 'git rev-parse --show-superproject-working-tree',
308
+ )} from cwd=${logger.path(cwd)}
336
309
  The command executed throws an error: ${error.message}`,
337
310
  {cause: error},
338
311
  );
339
312
  });
340
313
 
341
- if (result.exitCode !== 0) {
342
- throw new Error(
343
- `${createErrorMessageBase()}
344
- The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
345
- result.stderr,
346
- )}`,
347
- );
348
- }
349
-
350
314
  const output = result.stdout.trim();
351
315
  // this command only works when inside submodules
352
316
  // otherwise it doesn't return anything when we are inside the main repo
@@ -358,33 +322,20 @@ The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
358
322
 
359
323
  // See https://git-scm.com/book/en/v2/Git-Tools-Submodules
360
324
  export async function getGitSubmodulePaths(cwd: string): Promise<string[]> {
361
- const createErrorMessageBase = () => {
362
- return `Couldn't read the list of git submodules
363
- Failure while running ${logger.code(
364
- 'git submodule status',
365
- )} from cwd=${logger.path(cwd)}`;
366
- };
367
-
368
325
  const result = await execa('git', ['submodule', 'status'], {
369
326
  cwd,
370
327
  }).catch((error) => {
371
328
  // We enter this rejection when cwd is not a dir for example
372
329
  throw new Error(
373
- `${createErrorMessageBase()}
330
+ `Couldn't read the list of git submodules
331
+ Failure while running ${logger.code(
332
+ 'git submodule status',
333
+ )} from cwd=${logger.path(cwd)}
374
334
  The command executed throws an error: ${error.message}`,
375
335
  {cause: error},
376
336
  );
377
337
  });
378
338
 
379
- if (result.exitCode !== 0) {
380
- throw new Error(
381
- `${createErrorMessageBase()}
382
- The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
383
- result.stderr,
384
- )}`,
385
- );
386
- }
387
-
388
339
  const output = result.stdout.trim();
389
340
 
390
341
  if (!output) {
@@ -465,20 +416,12 @@ export async function getGitRepositoryFilesInfo(
465
416
  ],
466
417
  {
467
418
  cwd,
468
- encoding: 'utf-8',
469
419
  // TODO use streaming to avoid a large buffer
470
420
  // See https://github.com/withastro/starlight/issues/3154
471
421
  maxBuffer: 20 * 1024 * 1024,
472
422
  },
473
423
  );
474
424
 
475
- if (result.exitCode !== 0) {
476
- throw new Error(
477
- `Docusaurus failed to run the 'git log' to retrieve tracked files last update date/author.
478
- The command exited with code ${result.exitCode}: ${result.stderr}`,
479
- );
480
- }
481
-
482
425
  const logLines = result.stdout.split('\n');
483
426
 
484
427
  const now = Date.now();