@docusaurus/utils 3.9.2 → 3.10.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.
Files changed (74) hide show
  1. package/lib/index.d.ts +5 -4
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.js +13 -11
  4. package/lib/index.js.map +1 -1
  5. package/lib/lastUpdateUtils.d.ts +2 -6
  6. package/lib/lastUpdateUtils.d.ts.map +1 -1
  7. package/lib/lastUpdateUtils.js +13 -60
  8. package/lib/lastUpdateUtils.js.map +1 -1
  9. package/lib/markdownHeadingIdUtils.d.ts +49 -0
  10. package/lib/markdownHeadingIdUtils.d.ts.map +1 -0
  11. package/lib/markdownHeadingIdUtils.js +148 -0
  12. package/lib/markdownHeadingIdUtils.js.map +1 -0
  13. package/lib/markdownUtils.d.ts +0 -31
  14. package/lib/markdownUtils.d.ts.map +1 -1
  15. package/lib/markdownUtils.js +0 -89
  16. package/lib/markdownUtils.js.map +1 -1
  17. package/lib/moduleUtils.d.ts.map +1 -1
  18. package/lib/moduleUtils.js +4 -4
  19. package/lib/moduleUtils.js.map +1 -1
  20. package/lib/{gitUtils.d.ts → vcs/gitUtils.d.ts} +18 -0
  21. package/lib/vcs/gitUtils.d.ts.map +1 -0
  22. package/lib/vcs/gitUtils.js +343 -0
  23. package/lib/vcs/gitUtils.js.map +1 -0
  24. package/lib/vcs/vcs.d.ts +19 -0
  25. package/lib/vcs/vcs.d.ts.map +1 -0
  26. package/lib/vcs/vcs.js +46 -0
  27. package/lib/vcs/vcs.js.map +1 -0
  28. package/lib/vcs/vcsDefaultV1.d.ts +13 -0
  29. package/lib/vcs/vcsDefaultV1.d.ts.map +1 -0
  30. package/lib/vcs/vcsDefaultV1.js +33 -0
  31. package/lib/vcs/vcsDefaultV1.js.map +1 -0
  32. package/lib/vcs/vcsDefaultV2.d.ts +13 -0
  33. package/lib/vcs/vcsDefaultV2.d.ts.map +1 -0
  34. package/lib/vcs/vcsDefaultV2.js +33 -0
  35. package/lib/vcs/vcsDefaultV2.js.map +1 -0
  36. package/lib/vcs/vcsDisabled.d.ts +12 -0
  37. package/lib/vcs/vcsDisabled.d.ts.map +1 -0
  38. package/lib/vcs/vcsDisabled.js +24 -0
  39. package/lib/vcs/vcsDisabled.js.map +1 -0
  40. package/lib/vcs/vcsGitAdHoc.d.ts +16 -0
  41. package/lib/vcs/vcsGitAdHoc.d.ts.map +1 -0
  42. package/lib/vcs/vcsGitAdHoc.js +29 -0
  43. package/lib/vcs/vcsGitAdHoc.js.map +1 -0
  44. package/lib/vcs/vcsGitEager.d.ts +10 -0
  45. package/lib/vcs/vcsGitEager.d.ts.map +1 -0
  46. package/lib/vcs/vcsGitEager.js +89 -0
  47. package/lib/vcs/vcsGitEager.js.map +1 -0
  48. package/lib/vcs/vcsHardcoded.d.ts +17 -0
  49. package/lib/vcs/vcsHardcoded.d.ts.map +1 -0
  50. package/lib/vcs/vcsHardcoded.js +41 -0
  51. package/lib/vcs/vcsHardcoded.js.map +1 -0
  52. package/package.json +7 -7
  53. package/src/index.ts +11 -8
  54. package/src/lastUpdateUtils.ts +18 -76
  55. package/src/markdownHeadingIdUtils.ts +209 -0
  56. package/src/markdownUtils.ts +0 -119
  57. package/src/moduleUtils.ts +6 -8
  58. package/src/vcs/gitUtils.ts +541 -0
  59. package/src/vcs/vcs.ts +54 -0
  60. package/src/vcs/vcsDefaultV1.ts +33 -0
  61. package/src/vcs/vcsDefaultV2.ts +33 -0
  62. package/src/vcs/vcsDisabled.ts +25 -0
  63. package/src/vcs/vcsGitAdHoc.ts +30 -0
  64. package/src/vcs/vcsGitEager.ts +135 -0
  65. package/src/vcs/vcsHardcoded.ts +45 -0
  66. package/lib/cliUtils.d.ts +0 -14
  67. package/lib/cliUtils.d.ts.map +0 -1
  68. package/lib/cliUtils.js +0 -49
  69. package/lib/cliUtils.js.map +0 -1
  70. package/lib/gitUtils.d.ts.map +0 -1
  71. package/lib/gitUtils.js +0 -103
  72. package/lib/gitUtils.js.map +0 -1
  73. package/src/cliUtils.ts +0 -65
  74. package/src/gitUtils.ts +0 -200
@@ -0,0 +1,541 @@
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
+
8
+ import path from 'path';
9
+ import fs from 'fs-extra';
10
+ import os from 'os';
11
+ import _ from 'lodash';
12
+ import execa from 'execa';
13
+ import PQueue from 'p-queue';
14
+ import logger from '@docusaurus/logger';
15
+
16
+ // Quite high/conservative concurrency value (it was previously "Infinity")
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;
23
+
24
+ const GitCommandConcurrencyEnv = process.env.DOCUSAURUS_GIT_COMMAND_CONCURRENCY
25
+ ? parseInt(process.env.DOCUSAURUS_GIT_COMMAND_CONCURRENCY, 10)
26
+ : undefined;
27
+
28
+ const GitCommandConcurrency =
29
+ GitCommandConcurrencyEnv && GitCommandConcurrencyEnv > 0
30
+ ? GitCommandConcurrencyEnv
31
+ : DefaultGitCommandConcurrency;
32
+
33
+ // We use a queue to avoid running too many concurrent Git commands at once
34
+ // See https://github.com/facebook/docusaurus/issues/10348
35
+ const GitCommandQueue = new PQueue({
36
+ concurrency: GitCommandConcurrency,
37
+ });
38
+
39
+ const realHasGitFn = () => {
40
+ try {
41
+ return execa.sync('git', ['--version']).exitCode === 0;
42
+ } catch (error) {
43
+ return false;
44
+ }
45
+ };
46
+
47
+ // The hasGit call is synchronous IO so we memoize it
48
+ // The user won't install Git in the middle of a build anyway...
49
+ const hasGit =
50
+ process.env.NODE_ENV === 'test' ? realHasGitFn : _.memoize(realHasGitFn);
51
+
52
+ // TODO Docusaurus v4: remove this
53
+ // Exceptions are not made for control flow logic
54
+ /** Custom error thrown when git is not found in `PATH`. */
55
+ export class GitNotFoundError extends Error {}
56
+
57
+ // TODO Docusaurus v4: remove this, only kept for retro-compatibility
58
+ // Exceptions are not made for control flow logic
59
+ /** Custom error thrown when the current file is not tracked by git. */
60
+ export class FileNotTrackedError extends Error {}
61
+
62
+ /**
63
+ * Fetches the git history of a file and returns a relevant commit date.
64
+ * It gets the commit date instead of author date so that amended commits
65
+ * can have their dates updated.
66
+ *
67
+ * @throws {@link GitNotFoundError} If git is not found in `PATH`.
68
+ * @throws {@link FileNotTrackedError} If the current file is not tracked by git.
69
+ * @throws Also throws when `git log` exited with non-zero, or when it outputs
70
+ * unexpected text.
71
+ */
72
+ export async function getFileCommitDate(
73
+ /** Absolute path to the file. */
74
+ file: string,
75
+ args: {
76
+ /**
77
+ * `"oldest"` is the commit that added the file, following renames;
78
+ * `"newest"` is the last commit that edited the file.
79
+ */
80
+ age?: 'oldest' | 'newest';
81
+ /** Use `includeAuthor: true` to get the author information as well. */
82
+ includeAuthor?: false;
83
+ },
84
+ ): Promise<{
85
+ /** Relevant commit date. */
86
+ date: Date; // TODO duplicate data, not really useful?
87
+ /** Timestamp returned from git, converted to **milliseconds**. */
88
+ timestamp: number;
89
+ }>;
90
+ /**
91
+ * Fetches the git history of a file and returns a relevant commit date.
92
+ * It gets the commit date instead of author date so that amended commits
93
+ * can have their dates updated.
94
+ *
95
+ * @throws {@link GitNotFoundError} If git is not found in `PATH`.
96
+ * @throws {@link FileNotTrackedError} If the current file is not tracked by git.
97
+ * @throws Also throws when `git log` exited with non-zero, or when it outputs
98
+ * unexpected text.
99
+ */
100
+ export async function getFileCommitDate(
101
+ /** Absolute path to the file. */
102
+ file: string,
103
+ args: {
104
+ /**
105
+ * `"oldest"` is the commit that added the file, following renames;
106
+ * `"newest"` is the last commit that edited the file.
107
+ */
108
+ age?: 'oldest' | 'newest';
109
+ includeAuthor: true;
110
+ },
111
+ ): Promise<{
112
+ /** Relevant commit date. */
113
+ date: Date;
114
+ /** Timestamp returned from git, converted to **milliseconds**. */
115
+ timestamp: number;
116
+ /** The author's name, as returned from git. */
117
+ author: string;
118
+ }>;
119
+
120
+ export async function getFileCommitDate(
121
+ file: string,
122
+ {
123
+ age = 'oldest',
124
+ includeAuthor = false,
125
+ }: {
126
+ age?: 'oldest' | 'newest';
127
+ includeAuthor?: boolean;
128
+ },
129
+ ): Promise<{
130
+ date: Date;
131
+ timestamp: number;
132
+ author?: string;
133
+ }> {
134
+ if (!hasGit()) {
135
+ throw new GitNotFoundError(
136
+ `Failed to retrieve git history for "${file}" because git is not installed.`,
137
+ );
138
+ }
139
+
140
+ if (!(await fs.pathExists(file))) {
141
+ throw new Error(
142
+ `Failed to retrieve git history for "${file}" because the file does not exist.`,
143
+ );
144
+ }
145
+
146
+ // We add a "RESULT:" prefix to make parsing easier
147
+ // See why: https://github.com/facebook/docusaurus/pull/10022
148
+ const resultFormat = includeAuthor ? 'RESULT:%ct,%an' : 'RESULT:%ct';
149
+
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
+ 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}`,
174
+ );
175
+ }
176
+
177
+ // We only parse the output line starting with our "RESULT:" prefix
178
+ // See why https://github.com/facebook/docusaurus/pull/10022
179
+ const regex = includeAuthor
180
+ ? /(?:^|\n)RESULT:(?<timestamp>\d+),(?<author>.+)(?:$|\n)/
181
+ : /(?:^|\n)RESULT:(?<timestamp>\d+)(?:$|\n)/;
182
+
183
+ const output = result.stdout.trim();
184
+
185
+ if (!output) {
186
+ throw new FileNotTrackedError(
187
+ `Failed to retrieve the git history for file "${file}" because the file is not tracked by git.`,
188
+ );
189
+ }
190
+
191
+ const match = output.match(regex);
192
+
193
+ if (!match) {
194
+ throw new Error(
195
+ `Failed to retrieve the git history for file "${file}" with unexpected output: ${output}`,
196
+ );
197
+ }
198
+
199
+ const timestampInSeconds = Number(match.groups!.timestamp);
200
+ const timestamp = timestampInSeconds * 1_000;
201
+ const date = new Date(timestamp);
202
+
203
+ if (includeAuthor) {
204
+ return {date, timestamp, author: match.groups!.author!};
205
+ }
206
+ return {date, timestamp};
207
+ }
208
+
209
+ let showedGitRequirementError = false;
210
+ let showedFileNotTrackedError = false;
211
+
212
+ type GitCommitInfo = {timestamp: number; author: string};
213
+
214
+ async function getGitCommitInfo(
215
+ filePath: string,
216
+ age: 'oldest' | 'newest',
217
+ ): Promise<GitCommitInfo | null> {
218
+ if (!filePath) {
219
+ return null;
220
+ }
221
+ // Wrap in try/catch in case the shell commands fail
222
+ // (e.g. project doesn't use Git, etc).
223
+ try {
224
+ const result = await getFileCommitDate(filePath, {
225
+ age,
226
+ includeAuthor: true,
227
+ });
228
+ return {timestamp: result.timestamp, author: result.author};
229
+ } catch (err) {
230
+ // TODO legacy perf issue: do not use exceptions for control flow!
231
+ if (err instanceof GitNotFoundError) {
232
+ if (!showedGitRequirementError) {
233
+ logger.warn('Sorry, the last update options require Git.');
234
+ showedGitRequirementError = true;
235
+ }
236
+ } else if (err instanceof FileNotTrackedError) {
237
+ if (!showedFileNotTrackedError) {
238
+ logger.warn(
239
+ 'Cannot infer the update date for some files, as they are not tracked by git.',
240
+ );
241
+ showedFileNotTrackedError = true;
242
+ }
243
+ } else {
244
+ throw new Error(
245
+ `An error occurred when trying to get the file ${
246
+ age === 'oldest' ? 'creation' : 'last update'
247
+ } date from Git`,
248
+ {cause: err},
249
+ );
250
+ }
251
+ return null;
252
+ }
253
+ }
254
+
255
+ export async function getGitLastUpdate(
256
+ filePath: string,
257
+ ): Promise<GitCommitInfo | null> {
258
+ return getGitCommitInfo(filePath, 'newest');
259
+ }
260
+
261
+ export async function getGitCreation(
262
+ filePath: string,
263
+ ): Promise<GitCommitInfo | null> {
264
+ return getGitCommitInfo(filePath, 'oldest');
265
+ }
266
+
267
+ export async function isGitInsideWorktree(cwd: string): Promise<boolean> {
268
+ try {
269
+ const result = await execa('git', ['rev-parse', '--is-inside-work-tree'], {
270
+ cwd,
271
+ reject: false,
272
+ });
273
+ return result.exitCode === 0;
274
+ } catch (error) {
275
+ throw new Error(
276
+ `Couldn't check if this directory is within a Git worktree: ${cwd}`,
277
+ {cause: error},
278
+ );
279
+ }
280
+ }
281
+
282
+ 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
+ const result = await execa('git', ['rev-parse', '--show-toplevel'], {
291
+ cwd,
292
+ }).catch((error) => {
293
+ // We enter this rejection when cwd is not a dir for example
294
+ throw new Error(
295
+ `${createErrorMessageBase()}
296
+ The command executed throws an error: ${error.message}`,
297
+ {cause: error},
298
+ );
299
+ });
300
+
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
+ return fs.realpath.native(result.stdout.trim());
311
+ }
312
+
313
+ // A Git "superproject" is a Git repository that contains submodules
314
+ // See https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt---show-superproject-working-tree
315
+ // See https://git-scm.com/book/en/v2/Git-Tools-Submodules
316
+ export async function getGitSuperProjectRoot(
317
+ cwd: string,
318
+ ): 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
+ const result = await execa(
327
+ 'git',
328
+ ['rev-parse', '--show-superproject-working-tree'],
329
+ {
330
+ cwd,
331
+ },
332
+ ).catch((error) => {
333
+ // We enter this rejection when cwd is not a dir for example
334
+ throw new Error(
335
+ `${createErrorMessageBase()}
336
+ The command executed throws an error: ${error.message}`,
337
+ {cause: error},
338
+ );
339
+ });
340
+
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
+ const output = result.stdout.trim();
351
+ // this command only works when inside submodules
352
+ // otherwise it doesn't return anything when we are inside the main repo
353
+ if (output) {
354
+ return fs.realpath.native(output);
355
+ }
356
+ return getGitRepoRoot(cwd);
357
+ }
358
+
359
+ // See https://git-scm.com/book/en/v2/Git-Tools-Submodules
360
+ 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
+ const result = await execa('git', ['submodule', 'status'], {
369
+ cwd,
370
+ }).catch((error) => {
371
+ // We enter this rejection when cwd is not a dir for example
372
+ throw new Error(
373
+ `${createErrorMessageBase()}
374
+ The command executed throws an error: ${error.message}`,
375
+ {cause: error},
376
+ );
377
+ });
378
+
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
+ const output = result.stdout.trim();
389
+
390
+ if (!output) {
391
+ return [];
392
+ }
393
+
394
+ /* The output may contain a space/-/+/U prefix, for example
395
+ 1234567e3e35d1f5b submodules/foo (heads/main)
396
+ -9ab1f1d3a2d77b0a4 submodules/bar (heads/dev)
397
+ +f00ba42e1b3ddead submodules/baz (remotes/origin/main)
398
+ Udeadbeefcafe1234 submodules/qux
399
+ */
400
+ const getSubmodulePath = async (line: string) => {
401
+ const submodulePath = line.substring(1).split(' ')[1];
402
+ if (!submodulePath) {
403
+ throw new Error(`Failed to parse git submodule line: ${line}`);
404
+ }
405
+ return submodulePath;
406
+ };
407
+
408
+ return Promise.all(output.split('\n').map(getSubmodulePath));
409
+ }
410
+
411
+ // Find the root git repository alongside all its submodules, if any
412
+ export async function getGitAllRepoRoots(cwd: string): Promise<string[]> {
413
+ try {
414
+ const superProjectRoot = await getGitSuperProjectRoot(cwd);
415
+ if (!superProjectRoot) {
416
+ return [];
417
+ }
418
+ let submodulePaths = await getGitSubmodulePaths(superProjectRoot);
419
+ submodulePaths = await Promise.all(
420
+ submodulePaths.map((submodulePath) =>
421
+ fs.realpath.native(path.resolve(superProjectRoot, submodulePath)),
422
+ ),
423
+ );
424
+ return [superProjectRoot, ...submodulePaths];
425
+ } catch (error) {
426
+ throw new Error(
427
+ `Could not get all the git repository root paths (superproject + submodules) from cwd=${cwd}`,
428
+ {cause: error},
429
+ );
430
+ }
431
+ }
432
+
433
+ // Useful information about a file tracked in a Git repository
434
+ export type GitFileInfo = {
435
+ creation: GitCommitInfo;
436
+ lastUpdate: GitCommitInfo;
437
+ };
438
+
439
+ // A map of all the files tracked in a Git repository
440
+ export type GitFileInfoMap = Map<string, GitFileInfo>;
441
+
442
+ // Logic inspired from Astro Starlight:
443
+ // See https://bsky.app/profile/bluwy.me/post/3lyihod6qos2a
444
+ // See https://github.com/withastro/starlight/blob/c417f1efd463be63b7230617d72b120caed098cd/packages/starlight/utils/git.ts#L58
445
+ export async function getGitRepositoryFilesInfo(
446
+ cwd: string,
447
+ ): Promise<GitFileInfoMap> {
448
+ // git --no-pager -c log.showSignature=false log --format=t:%ct,a:%an --name-status
449
+ const result = await execa(
450
+ 'git',
451
+ [
452
+ '--no-pager',
453
+ // Do not include GPG signature in the log output
454
+ // See https://github.com/facebook/docusaurus/pull/10022
455
+ '-c',
456
+ 'log.showSignature=false',
457
+ // The git command we want to run
458
+ 'log',
459
+ // Format each history entry as t:<seconds since epoch>
460
+ '--format=t:%ct,a:%an',
461
+ // In each entry include the name and status for each modified file
462
+ '--name-status',
463
+
464
+ // For creation info, should we use --follow --find-renames=100% ???
465
+ ],
466
+ {
467
+ cwd,
468
+ encoding: 'utf-8',
469
+ // TODO use streaming to avoid a large buffer
470
+ // See https://github.com/withastro/starlight/issues/3154
471
+ maxBuffer: 20 * 1024 * 1024,
472
+ },
473
+ );
474
+
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
+ const logLines = result.stdout.split('\n');
483
+
484
+ const now = Date.now();
485
+
486
+ // TODO not fail-fast
487
+ let runningDate = now;
488
+ let runningAuthor = 'N/A';
489
+ const runningMap: GitFileInfoMap = new Map();
490
+
491
+ for (const logLine of logLines) {
492
+ if (logLine.startsWith('t:')) {
493
+ // t:<timestamp>,a:<author name>
494
+ const [timestampStr, authorStr] = logLine.split(',') as [string, string];
495
+ const timestamp = Number.parseInt(timestampStr.slice(2), 10) * 1000;
496
+ const author = authorStr.slice(2);
497
+
498
+ runningDate = timestamp;
499
+ runningAuthor = author;
500
+ }
501
+
502
+ // TODO the code below doesn't handle delete/move/rename operations properly
503
+ // it returns files that no longer exist in the repo (deleted/moved)
504
+
505
+ // - Added files take the format `A\t<file>`
506
+ // - Modified files take the format `M\t<file>`
507
+ // - Deleted files take the format `D\t<file>`
508
+ // - Renamed files take the format `R<count>\t<old>\t<new>`
509
+ // - Copied files take the format `C<count>\t<old>\t<new>`
510
+ // The name of the file as of the commit being processed is always
511
+ // the last part of the log line.
512
+ const tabSplit = logLine.lastIndexOf('\t');
513
+ if (tabSplit === -1) {
514
+ continue;
515
+ }
516
+ const relativeFile = logLine.slice(tabSplit + 1);
517
+
518
+ const currentFileInfo = runningMap.get(relativeFile);
519
+
520
+ const currentCreationTime = currentFileInfo?.creation.timestamp || now;
521
+ const newCreationTime = Math.min(currentCreationTime, runningDate);
522
+ const newCreation: GitCommitInfo =
523
+ !currentFileInfo || newCreationTime !== currentCreationTime
524
+ ? {timestamp: newCreationTime, author: runningAuthor}
525
+ : currentFileInfo.creation;
526
+
527
+ const currentLastUpdateTime = currentFileInfo?.lastUpdate.timestamp || 0;
528
+ const newLastUpdateTime = Math.max(currentLastUpdateTime, runningDate);
529
+ const newLastUpdate: GitCommitInfo =
530
+ !currentFileInfo || newLastUpdateTime !== currentLastUpdateTime
531
+ ? {timestamp: newLastUpdateTime, author: runningAuthor}
532
+ : currentFileInfo.lastUpdate;
533
+
534
+ runningMap.set(relativeFile, {
535
+ creation: newCreation,
536
+ lastUpdate: newLastUpdate,
537
+ });
538
+ }
539
+
540
+ return runningMap;
541
+ }
package/src/vcs/vcs.ts ADDED
@@ -0,0 +1,54 @@
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
+
8
+ import {
9
+ VCS_HARDCODED_CREATION_INFO,
10
+ VCS_HARDCODED_LAST_UPDATE_INFO,
11
+ VCS_HARDCODED_UNTRACKED_FILE_PATH,
12
+ VcsHardcoded,
13
+ } from './vcsHardcoded';
14
+ import {VcsGitAdHoc} from './vcsGitAdHoc';
15
+ import {VscGitEager} from './vcsGitEager';
16
+ import {VcsDisabled} from './vcsDisabled';
17
+ import {VcsDefaultV1} from './vcsDefaultV1';
18
+ import {VcsDefaultV2} from './vcsDefaultV2';
19
+ import type {VcsConfig, VcsPreset} from '@docusaurus/types';
20
+
21
+ const VcsPresets: Record<VcsPreset, VcsConfig> = {
22
+ 'git-ad-hoc': VcsGitAdHoc,
23
+ 'git-eager': VscGitEager,
24
+ hardcoded: VcsHardcoded,
25
+ disabled: VcsDisabled,
26
+
27
+ 'default-v1': VcsDefaultV1,
28
+ 'default-v2': VcsDefaultV2,
29
+ };
30
+
31
+ export const VcsPresetNames = Object.keys(VcsPresets) as VcsPreset[];
32
+
33
+ export function findVcsPreset(presetName: string): VcsConfig | undefined {
34
+ return VcsPresets[presetName as VcsPreset];
35
+ }
36
+
37
+ export function getVcsPreset(presetName: VcsPreset): VcsConfig {
38
+ const vcs = findVcsPreset(presetName);
39
+ if (vcs) {
40
+ return vcs;
41
+ } else {
42
+ throw new Error(
43
+ `Unknown Docusaurus VCS preset name: ${process.env.DOCUSAURUS_VCS}`,
44
+ );
45
+ }
46
+ }
47
+
48
+ // Convenient export for writing unit tests depending on VCS
49
+ export const TEST_VCS = {
50
+ CREATION_INFO: VCS_HARDCODED_CREATION_INFO,
51
+ LAST_UPDATE_INFO: VCS_HARDCODED_LAST_UPDATE_INFO,
52
+ UNTRACKED_FILE_PATH: VCS_HARDCODED_UNTRACKED_FILE_PATH,
53
+ ...VcsHardcoded,
54
+ };
@@ -0,0 +1,33 @@
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
+
8
+ import {VcsHardcoded} from './vcsHardcoded';
9
+ import {VcsGitAdHoc} from './vcsGitAdHoc';
10
+ import type {VcsConfig} from '@docusaurus/types';
11
+
12
+ function getDynamicStrategy(): VcsConfig {
13
+ return process.env.NODE_ENV === 'development' ||
14
+ process.env.NODE_ENV === 'test'
15
+ ? VcsHardcoded
16
+ : VcsGitAdHoc;
17
+ }
18
+
19
+ /**
20
+ * This VCS implements the historical Git automatic strategy.
21
+ * It is only enabled in production mode, using ad-hoc git log commands.
22
+ */
23
+ export const VcsDefaultV1: VcsConfig = {
24
+ initialize: (...params) => {
25
+ return getDynamicStrategy().initialize(...params);
26
+ },
27
+ getFileCreationInfo: (...params) => {
28
+ return getDynamicStrategy().getFileCreationInfo(...params);
29
+ },
30
+ getFileLastUpdateInfo: (...params) => {
31
+ return getDynamicStrategy().getFileLastUpdateInfo(...params);
32
+ },
33
+ };
@@ -0,0 +1,33 @@
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
+
8
+ import {VcsHardcoded} from './vcsHardcoded';
9
+ import {VscGitEager} from './vcsGitEager';
10
+ import type {VcsConfig} from '@docusaurus/types';
11
+
12
+ function getStrategy(): VcsConfig {
13
+ return process.env.NODE_ENV === 'development' ||
14
+ process.env.NODE_ENV === 'test'
15
+ ? VcsHardcoded
16
+ : VscGitEager;
17
+ }
18
+
19
+ /**
20
+ * This VCS implements the new eager Git automatic strategy.
21
+ * It is only enabled in production mode, reading the git repository eagerly.
22
+ */
23
+ export const VcsDefaultV2: VcsConfig = {
24
+ initialize: (...params) => {
25
+ return getStrategy().initialize(...params);
26
+ },
27
+ getFileCreationInfo: (...params) => {
28
+ return getStrategy().getFileCreationInfo(...params);
29
+ },
30
+ getFileLastUpdateInfo: (...params) => {
31
+ return getStrategy().getFileLastUpdateInfo(...params);
32
+ },
33
+ };
@@ -0,0 +1,25 @@
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
+
8
+ import type {VcsConfig} from '@docusaurus/types';
9
+
10
+ /**
11
+ * This VCS implementation always returns null values
12
+ */
13
+ export const VcsDisabled: VcsConfig = {
14
+ initialize: () => {
15
+ // Noop
16
+ },
17
+
18
+ getFileCreationInfo: async (_filePath) => {
19
+ return null;
20
+ },
21
+
22
+ getFileLastUpdateInfo: async (_ilePath) => {
23
+ return null;
24
+ },
25
+ };