Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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...).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
`);
});
});
});
});
10 changes: 8 additions & 2 deletions packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import type {PluginOptions} from './options';

async function getRouteLastUpdatedAt(
route: RouteConfig,
): Promise<number | undefined> {
): Promise<number | null | undefined> {
// 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;
Expand Down
8 changes: 4 additions & 4 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 9 additions & 2 deletions packages/docusaurus-types/src/routing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
30 changes: 30 additions & 0 deletions packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(
'',
Expand Down
8 changes: 4 additions & 4 deletions packages/docusaurus-utils/src/gitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
40 changes: 33 additions & 7 deletions packages/docusaurus-utils/src/lastUpdateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LastUpdateData | null> {
if (filePath === LAST_UPDATE_UNTRACKED_GIT_FILEPATH) {
return null;
}
if (
process.env.NODE_ENV !== 'production' ||
process.env.DOCUSAURUS_DISABLE_LAST_UPDATE === 'true'
Expand Down Expand Up @@ -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;
Expand All @@ -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())
Expand Down
5 changes: 5 additions & 0 deletions packages/docusaurus/src/commands/build/buildLocale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -59,6 +60,10 @@ export async function buildLocale({
}),
);

if (ExitAfterLoading) {
return process.exit(0);
}

const {props} = site;
const {outDir, plugins, siteConfig} = props;

Expand Down
4 changes: 4 additions & 0 deletions website/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +319 to +322
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in the end what we really want is that the sitemap shouldn't read from Git for changelog generated MDX files.

I'm not sure we should add another sitemap API to do that (although createSitemapItem() could work too), and this would probably be better solved by a global siteConfig.readLastUpdateData() hook, as described in #11208

blogDescription:
'Keep yourself up-to-date about new features in every release',
blogSidebarCount: 'ALL',
Expand Down
Loading