feat(blog): add LastUpdateAuthor & LastUpdateTime (#9912)

Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
ozaki 2024-03-15 12:50:06 +01:00 committed by GitHub
parent 7938803747
commit c745021b01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 833 additions and 359 deletions

View file

@ -444,19 +444,19 @@ describe('validateDocFrontMatter last_update', () => {
invalidFrontMatters: [
[
{last_update: null},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: {}},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: ''},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: {invalid: 'key'}},
'does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).',
'"last_update" does not look like a valid last update object. Please use an author key with a string or a date with a string or Date',
],
[
{last_update: {author: 'test author', date: 'I am not a date :('}},

View file

@ -1,117 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {jest} from '@jest/globals';
import fs from 'fs-extra';
import path from 'path';
import shell from 'shelljs';
import {createTempRepo} from '@testing-utils/git';
import {getFileLastUpdate} from '../lastUpdate';
describe('getFileLastUpdate', () => {
const existingFilePath = path.join(
__dirname,
'__fixtures__/simple-site/docs/hello.md',
);
it('existing test file in repository with Git timestamp', async () => {
const lastUpdateData = await getFileLastUpdate(existingFilePath);
expect(lastUpdateData).not.toBeNull();
const {author, timestamp} = lastUpdateData!;
expect(author).not.toBeNull();
expect(typeof author).toBe('string');
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
});
it('existing test file with spaces in path', async () => {
const filePathWithSpace = path.join(
__dirname,
'__fixtures__/simple-site/docs/doc with space.md',
);
const lastUpdateData = await getFileLastUpdate(filePathWithSpace);
expect(lastUpdateData).not.toBeNull();
const {author, timestamp} = lastUpdateData!;
expect(author).not.toBeNull();
expect(typeof author).toBe('string');
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
});
it('non-existing file', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const nonExistingFileName = '.nonExisting';
const nonExistingFilePath = path.join(
__dirname,
'__fixtures__',
nonExistingFileName,
);
await expect(getFileLastUpdate(nonExistingFilePath)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/because the file does not exist./),
);
consoleMock.mockRestore();
});
it('temporary created file that is not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const {repoDir} = createTempRepo();
const tempFilePath = path.join(repoDir, 'file.md');
await fs.writeFile(tempFilePath, 'Lorem ipsum :)');
await expect(getFileLastUpdate(tempFilePath)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath);
});
it('multiple files not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const {repoDir} = createTempRepo();
const tempFilePath1 = path.join(repoDir, 'file1.md');
const tempFilePath2 = path.join(repoDir, 'file2.md');
await fs.writeFile(tempFilePath1, 'Lorem ipsum :)');
await fs.writeFile(tempFilePath2, 'Lorem ipsum :)');
await expect(getFileLastUpdate(tempFilePath1)).resolves.toBeNull();
await expect(getFileLastUpdate(tempFilePath2)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath1);
await fs.unlink(tempFilePath2);
});
it('git does not exist', async () => {
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const lastUpdateData = await getFileLastUpdate(existingFilePath);
expect(lastUpdateData).toBeNull();
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(
/.*\[WARNING\].* Sorry, the docs plugin last update options require Git\..*/,
),
);
consoleMock.mockRestore();
mock.mockRestore();
});
});

View file

@ -20,12 +20,11 @@ import {
normalizeFrontMatterTags,
isUnlisted,
isDraft,
readLastUpdateData,
} from '@docusaurus/utils';
import {getFileLastUpdate} from './lastUpdate';
import {validateDocFrontMatter} from './frontMatter';
import getSlug from './slug';
import {stripPathNumberPrefixes} from './numberPrefix';
import {validateDocFrontMatter} from './frontMatter';
import {toDocNavigationLink, toNavigationLink} from './sidebars/utils';
import type {
MetadataOptions,
@ -34,61 +33,13 @@ import type {
DocMetadataBase,
DocMetadata,
PropNavigationLink,
LastUpdateData,
VersionMetadata,
LoadedVersion,
FileChange,
} from '@docusaurus/plugin-content-docs';
import type {LoadContext} from '@docusaurus/types';
import type {SidebarsUtils} from './sidebars/utils';
import type {DocFile} from './types';
type LastUpdateOptions = Pick<
PluginOptions,
'showLastUpdateAuthor' | 'showLastUpdateTime'
>;
async function readLastUpdateData(
filePath: string,
options: LastUpdateOptions,
lastUpdateFrontMatter: FileChange | undefined,
): Promise<LastUpdateData> {
const {showLastUpdateAuthor, showLastUpdateTime} = options;
if (showLastUpdateAuthor || showLastUpdateTime) {
const frontMatterTimestamp = lastUpdateFrontMatter?.date
? new Date(lastUpdateFrontMatter.date).getTime() / 1000
: undefined;
if (lastUpdateFrontMatter?.author && lastUpdateFrontMatter.date) {
return {
lastUpdatedAt: frontMatterTimestamp,
lastUpdatedBy: lastUpdateFrontMatter.author,
};
}
// Use fake data in dev for faster development.
const fileLastUpdateData =
process.env.NODE_ENV === 'production'
? await getFileLastUpdate(filePath)
: {
author: 'Author',
timestamp: 1539502055,
};
const {author, timestamp} = fileLastUpdateData ?? {};
return {
lastUpdatedBy: showLastUpdateAuthor
? lastUpdateFrontMatter?.author ?? author
: undefined,
lastUpdatedAt: showLastUpdateTime
? frontMatterTimestamp ?? timestamp
: undefined,
};
}
return {};
}
export async function readDocFile(
versionMetadata: Pick<
VersionMetadata,

View file

@ -4,7 +4,6 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
JoiFrontMatter as Joi, // Custom instance for front matter
URISchema,
@ -12,17 +11,15 @@ import {
FrontMatterTOCHeadingLevels,
validateFrontMatter,
ContentVisibilitySchema,
FrontMatterLastUpdateSchema,
} from '@docusaurus/utils-validation';
import type {DocFrontMatter} from '@docusaurus/plugin-content-docs';
const FrontMatterLastUpdateErrorMessage =
'{{#label}} does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).';
// NOTE: we don't add any default value on purpose here
// We don't want default values to magically appear in doc metadata and props
// While the user did not provide those values explicitly
// We use default values in code instead
const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
export const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
id: Joi.string(),
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
title: Joi.string().allow(''),
@ -45,15 +42,7 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
pagination_next: Joi.string().allow(null),
pagination_prev: Joi.string().allow(null),
...FrontMatterTOCHeadingLevels,
last_update: Joi.object({
author: Joi.string(),
date: Joi.date().raw(),
})
.or('author', 'date')
.messages({
'object.missing': FrontMatterLastUpdateErrorMessage,
'object.base': FrontMatterLastUpdateErrorMessage,
}),
last_update: FrontMatterLastUpdateSchema,
})
.unknown()
.concat(ContentVisibilitySchema);

View file

@ -1,52 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import logger from '@docusaurus/logger';
import {
getFileCommitDate,
FileNotTrackedError,
GitNotFoundError,
} from '@docusaurus/utils';
let showedGitRequirementError = false;
let showedFileNotTrackedError = false;
export async function getFileLastUpdate(
filePath: string,
): Promise<{timestamp: number; author: string} | null> {
if (!filePath) {
return null;
}
// Wrap in try/catch in case the shell commands fail
// (e.g. project doesn't use Git, etc).
try {
const result = await getFileCommitDate(filePath, {
age: 'newest',
includeAuthor: true,
});
return {timestamp: result.timestamp, author: result.author};
} catch (err) {
if (err instanceof GitNotFoundError) {
if (!showedGitRequirementError) {
logger.warn('Sorry, the docs plugin last update options require Git.');
showedGitRequirementError = true;
}
} else if (err instanceof FileNotTrackedError) {
if (!showedFileNotTrackedError) {
logger.warn(
'Cannot infer the update date for some files, as they are not tracked by git.',
);
showedFileNotTrackedError = true;
}
} else {
logger.warn(err);
}
return null;
}
}

View file

@ -16,6 +16,7 @@ declare module '@docusaurus/plugin-content-docs' {
TagsListItem,
TagModule,
Tag,
FrontMatterLastUpdate,
} from '@docusaurus/utils';
import type {Plugin, LoadContext} from '@docusaurus/types';
import type {Overwrite, Required} from 'utility-types';
@ -24,14 +25,6 @@ declare module '@docusaurus/plugin-content-docs' {
image?: string;
};
export type FileChange = {
author?: string;
/** Date can be any
* [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
*/
date?: Date | string;
};
/**
* Custom callback for parsing number prefixes from file/folder names.
*/
@ -93,9 +86,9 @@ declare module '@docusaurus/plugin-content-docs' {
*/
editLocalizedFiles: boolean;
/** Whether to display the last date the doc was updated. */
showLastUpdateTime?: boolean;
showLastUpdateTime: boolean;
/** Whether to display the author who last updated the doc. */
showLastUpdateAuthor?: boolean;
showLastUpdateAuthor: boolean;
/**
* Custom parsing logic to extract number prefixes from file names. Use
* `false` to disable this behavior and leave the docs untouched, and `true`
@ -401,7 +394,7 @@ declare module '@docusaurus/plugin-content-docs' {
/** Should this doc be accessible but hidden in production builds? */
unlisted?: boolean;
/** Allows overriding the last updated author and/or date. */
last_update?: FileChange;
last_update?: FrontMatterLastUpdate;
};
export type LastUpdateData = {