From 264774a550a1a748cab88cfeec4d7e5b19737041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Wed, 28 May 2025 14:03:10 +0200 Subject: [PATCH] perf: avoid duplicated `git log` calls in `loadContent()` and `postBuild()` for untracked Git files (#11211) Co-authored-by: slorber <749374+slorber@users.noreply.github.com> --- .../src/plugin-content-blog.d.ts | 4 +- .../src/__tests__/createSitemapItem.test.ts | 61 +++++++++++++++++++ .../src/createSitemapItem.ts | 10 ++- .../src/theme-classic.d.ts | 8 +-- packages/docusaurus-types/src/routing.d.ts | 11 +++- .../src/__tests__/lastUpdateUtils.test.ts | 30 +++++++++ packages/docusaurus-utils/src/gitUtils.ts | 8 +-- .../docusaurus-utils/src/lastUpdateUtils.ts | 40 +++++++++--- .../src/commands/build/buildLocale.ts | 5 ++ website/docusaurus.config.ts | 4 ++ 10 files changed, 160 insertions(+), 21 deletions(-) diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index 610fa1052a..625aaf94df 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -521,9 +521,9 @@ declare module '@docusaurus/plugin-content-blog' { readingTime: ReadingTimeFunctionOption; /** Governs the direction of blog post sorting. */ sortPosts: 'ascending' | 'descending'; - /** Whether to display the last date the doc was updated. */ + /** Whether to display the last date the blog post was updated. */ showLastUpdateTime: boolean; - /** Whether to display the author who last updated the doc. */ + /** Whether to display the author who last updated the blog post. */ showLastUpdateAuthor: boolean; /** An optional function which can be used to transform blog posts * (filter, modify, delete, etc...). diff --git a/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemapItem.test.ts b/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemapItem.test.ts index 8982334fd2..4aa4df3a8f 100644 --- a/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemapItem.test.ts +++ b/packages/docusaurus-plugin-sitemap/src/__tests__/createSitemapItem.test.ts @@ -225,5 +225,66 @@ describe('createSitemapItem', () => { `); }); }); + + describe('read from both - route metadata lastUpdatedAt null', () => { + const route = { + path: '/routePath', + metadata: { + sourceFilePath: 'route/file.md', + lastUpdatedAt: null, + }, + }; + + it('lastmod default option', async () => { + await expect( + test({ + route, + }), + ).resolves.toMatchInlineSnapshot(` + { + "changefreq": "weekly", + "lastmod": null, + "priority": 0.5, + "url": "https://example.com/routePath", + } + `); + }); + + it('lastmod date option', async () => { + await expect( + test({ + route, + options: { + lastmod: 'date', + }, + }), + ).resolves.toMatchInlineSnapshot(` + { + "changefreq": "weekly", + "lastmod": null, + "priority": 0.5, + "url": "https://example.com/routePath", + } + `); + }); + + it('lastmod datetime option', async () => { + await expect( + test({ + route, + options: { + lastmod: 'datetime', + }, + }), + ).resolves.toMatchInlineSnapshot(` + { + "changefreq": "weekly", + "lastmod": null, + "priority": 0.5, + "url": "https://example.com/routePath", + } + `); + }); + }); }); }); diff --git a/packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts b/packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts index 83c96cedf0..ebd6bba44c 100644 --- a/packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts +++ b/packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts @@ -13,13 +13,19 @@ import type {PluginOptions} from './options'; async function getRouteLastUpdatedAt( route: RouteConfig, -): Promise { +): Promise { + // Important to bail-out early here + // This can lead to duplicated getLastUpdate() calls and performance problems + // See https://github.com/facebook/docusaurus/pull/11211 + if (route.metadata?.lastUpdatedAt === null) { + return null; + } if (route.metadata?.lastUpdatedAt) { return route.metadata?.lastUpdatedAt; } if (route.metadata?.sourceFilePath) { const lastUpdate = await getLastUpdate(route.metadata?.sourceFilePath); - return lastUpdate?.lastUpdatedAt; + return lastUpdate?.lastUpdatedAt ?? null; } return undefined; diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index fd6c864980..a23b1bd8aa 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -852,8 +852,8 @@ declare module '@theme/EditMetaRow' { export interface Props { readonly className: string; readonly editUrl: string | null | undefined; - readonly lastUpdatedAt: number | undefined; - readonly lastUpdatedBy: string | undefined; + readonly lastUpdatedAt: number | null | undefined; + readonly lastUpdatedBy: string | null | undefined; } export default function EditMetaRow(props: Props): ReactNode; } @@ -1024,8 +1024,8 @@ declare module '@theme/LastUpdated' { import type {ReactNode} from 'react'; export interface Props { - readonly lastUpdatedAt?: number; - readonly lastUpdatedBy?: string; + readonly lastUpdatedAt?: number | null; + readonly lastUpdatedBy?: string | null; } export default function LastUpdated(props: Props): ReactNode; diff --git a/packages/docusaurus-types/src/routing.d.ts b/packages/docusaurus-types/src/routing.d.ts index 698797b7d1..a5a51f914b 100644 --- a/packages/docusaurus-types/src/routing.d.ts +++ b/packages/docusaurus-types/src/routing.d.ts @@ -56,12 +56,19 @@ export type RouteMetadata = { /** * The last updated date of this route * This is generally read from the Git history of the sourceFilePath - * but can also be provided through other means (usually front matter) + * but can also be provided through other means (usually front matter). * * This has notably been introduced for adding "lastmod" support to the * sitemap plugin, see https://github.com/facebook/docusaurus/pull/9954 + * + * `undefined` means we haven't tried to compute the value for this route. + * This is usually the case for routes created by third-party plugins that do + * not need this metadata. + * + * `null` means we already tried to compute a lastUpdatedAt, but we know for + * sure there isn't any. This usually happens for untracked Git files. */ - lastUpdatedAt?: number; + lastUpdatedAt?: number | null; }; /** diff --git a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts index 48dbe85220..3743c4973f 100644 --- a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts @@ -14,8 +14,10 @@ import execa from 'execa'; import { getGitLastUpdate, LAST_UPDATE_FALLBACK, + LAST_UPDATE_UNTRACKED_GIT_FILEPATH, readLastUpdateData, } from '../lastUpdateUtils'; +import type {FrontMatterLastUpdate} from '../lastUpdateUtils'; describe('getGitLastUpdate', () => { const {repoDir} = createTempRepo(); @@ -109,6 +111,34 @@ describe('readLastUpdateData', () => { const testTimestamp = new Date(testDate).getTime(); const testAuthor = 'ozaki'; + describe('on untracked Git file', () => { + function test(lastUpdateFrontMatter: FrontMatterLastUpdate | undefined) { + return readLastUpdateData( + LAST_UPDATE_UNTRACKED_GIT_FILEPATH, + {showLastUpdateAuthor: true, showLastUpdateTime: true}, + lastUpdateFrontMatter, + ); + } + + it('reads null at/by from Git', async () => { + const {lastUpdatedAt, lastUpdatedBy} = await test({}); + expect(lastUpdatedAt).toBeNull(); + expect(lastUpdatedBy).toBeNull(); + }); + + it('reads null at from Git and author from front matter', async () => { + const {lastUpdatedAt, lastUpdatedBy} = await test({author: testAuthor}); + expect(lastUpdatedAt).toBeNull(); + expect(lastUpdatedBy).toEqual(testAuthor); + }); + + it('reads null by from Git and date from front matter', async () => { + const {lastUpdatedAt, lastUpdatedBy} = await test({date: testDate}); + expect(lastUpdatedBy).toBeNull(); + expect(lastUpdatedAt).toEqual(testTimestamp); + }); + }); + it('read last time show author time', async () => { const {lastUpdatedAt, lastUpdatedBy} = await readLastUpdateData( '', diff --git a/packages/docusaurus-utils/src/gitUtils.ts b/packages/docusaurus-utils/src/gitUtils.ts index fff2659922..890c35bd89 100644 --- a/packages/docusaurus-utils/src/gitUtils.ts +++ b/packages/docusaurus-utils/src/gitUtils.ts @@ -154,12 +154,12 @@ export async function getFileCommitDate( file, )}"`; - const result = (await GitCommandQueue.add(() => - execa(command, { + const result = (await GitCommandQueue.add(() => { + return execa(command, { cwd: path.dirname(file), shell: true, - }), - ))!; + }); + }))!; if (result.exitCode !== 0) { throw new Error( diff --git a/packages/docusaurus-utils/src/lastUpdateUtils.ts b/packages/docusaurus-utils/src/lastUpdateUtils.ts index 5b024d0067..32c936ee25 100644 --- a/packages/docusaurus-utils/src/lastUpdateUtils.ts +++ b/packages/docusaurus-utils/src/lastUpdateUtils.ts @@ -15,10 +15,18 @@ import { import type {PluginOptions} from '@docusaurus/types'; export type LastUpdateData = { - /** A timestamp in **milliseconds**, usually read from `git log` */ - lastUpdatedAt?: number; - /** The author's name, usually coming from `git log` */ - lastUpdatedBy?: string; + /** + * A timestamp in **milliseconds**, usually read from `git log` + * `undefined`: not read + * `null`: no value to read (usual for untracked files) + */ + lastUpdatedAt: number | undefined | null; + /** + * The author's name, usually coming from `git log` + * `undefined`: not read + * `null`: no value to read (usual for untracked files) + */ + lastUpdatedBy: string | undefined | null; }; let showedGitRequirementError = false; @@ -68,9 +76,15 @@ export const LAST_UPDATE_FALLBACK: LastUpdateData = { lastUpdatedBy: 'Author', }; +// Not proud of this, but convenient for tests :/ +export const LAST_UPDATE_UNTRACKED_GIT_FILEPATH = `file/path/${Math.random()}.mdx`; + export async function getLastUpdate( filePath: string, ): Promise { + if (filePath === LAST_UPDATE_UNTRACKED_GIT_FILEPATH) { + return null; + } if ( process.env.NODE_ENV !== 'production' || process.env.DOCUSAURUS_DISABLE_LAST_UPDATE === 'true' @@ -103,7 +117,7 @@ export async function readLastUpdateData( const {showLastUpdateAuthor, showLastUpdateTime} = options; if (!showLastUpdateAuthor && !showLastUpdateTime) { - return {}; + return {lastUpdatedBy: undefined, lastUpdatedAt: undefined}; } const frontMatterAuthor = lastUpdateFrontMatter?.author; @@ -116,9 +130,21 @@ export async function readLastUpdateData( // If all the data is provided as front matter, we do not call it const getLastUpdateMemoized = _.memoize(() => getLastUpdate(filePath)); const getLastUpdateBy = () => - getLastUpdateMemoized().then((update) => update?.lastUpdatedBy); + getLastUpdateMemoized().then((update) => { + // Important, see https://github.com/facebook/docusaurus/pull/11211 + if (update === null) { + return null; + } + return update?.lastUpdatedBy; + }); const getLastUpdateAt = () => - getLastUpdateMemoized().then((update) => update?.lastUpdatedAt); + getLastUpdateMemoized().then((update) => { + // Important, see https://github.com/facebook/docusaurus/pull/11211 + if (update === null) { + return null; + } + return update?.lastUpdatedAt; + }); const lastUpdatedBy = showLastUpdateAuthor ? frontMatterAuthor ?? (await getLastUpdateBy()) diff --git a/packages/docusaurus/src/commands/build/buildLocale.ts b/packages/docusaurus/src/commands/build/buildLocale.ts index f6b5822394..33ae8d346e 100644 --- a/packages/docusaurus/src/commands/build/buildLocale.ts +++ b/packages/docusaurus/src/commands/build/buildLocale.ts @@ -35,6 +35,7 @@ export type BuildLocaleParams = { }; const SkipBundling = process.env.DOCUSAURUS_SKIP_BUNDLING === 'true'; +const ExitAfterLoading = process.env.DOCUSAURUS_EXIT_AFTER_LOADING === 'true'; const ExitAfterBundling = process.env.DOCUSAURUS_EXIT_AFTER_BUNDLING === 'true'; export async function buildLocale({ @@ -59,6 +60,10 @@ export async function buildLocale({ }), ); + if (ExitAfterLoading) { + return process.exit(0); + } + const {props} = site; const {outDir, plugins, siteConfig} = props; diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 027b6b382e..8dd44715cb 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -316,6 +316,10 @@ export default async function createConfigAsync() { './src/plugins/changelog/index.ts', { blogTitle: 'Docusaurus changelog', + // Not useful, but permits to run git commands earlier + // Otherwise the sitemap plugin will run them in postBuild() + showLastUpdateAuthor: true, + showLastUpdateTime: true, blogDescription: 'Keep yourself up-to-date about new features in every release', blogSidebarCount: 'ALL',