diff --git a/.eslintrc.js b/.eslintrc.js index c6cea664f8dd..90f7db3d0c00 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -91,7 +91,7 @@ module.exports = { 'no-constant-binary-expression': ERROR, 'no-continue': OFF, 'no-control-regex': WARNING, - 'no-else-return': [WARNING, {allowElseIf: true}], + 'no-else-return': OFF, 'no-empty': [WARNING, {allowEmptyCatch: true}], 'no-lonely-if': WARNING, 'no-nested-ternary': WARNING, diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/author.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/author.md new file mode 100644 index 000000000000..53dd2c41e934 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/author.md @@ -0,0 +1,9 @@ +--- +title: Author +slug: author +author: ozaki +last_update: + author: seb +--- + +author diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/both.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/both.md new file mode 100644 index 000000000000..4171ca894fb2 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/both.md @@ -0,0 +1,11 @@ +--- +title: Both +slug: both +date: 2020-01-01 +last_update: + date: 2021-01-01 + author: seb +author: ozaki +--- + +last update date diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/lastUpdateDate.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/lastUpdateDate.md new file mode 100644 index 000000000000..81e8a1bb8267 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/lastUpdateDate.md @@ -0,0 +1,9 @@ +--- +title: Last update date +slug: lastUpdateDate +date: 2020-01-01 +last_update: + date: 2021-01-01 +--- + +last update date diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/nothing.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/nothing.md new file mode 100644 index 000000000000..1f78115cf7b7 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-last-update/blog/nothing.md @@ -0,0 +1,6 @@ +--- +title: Nothing +slug: nothing +--- + +nothing diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap index 441e7e6ef590..983054187fee 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap @@ -137,6 +137,8 @@ exports[`blog plugin process blog posts load content 2`] = ` "title": "Another Simple Slug", }, "hasTruncateMarker": false, + "lastUpdatedAt": undefined, + "lastUpdatedBy": undefined, "nextItem": { "permalink": "/blog/another/tags", "title": "Another With Tag", @@ -172,6 +174,8 @@ exports[`blog plugin process blog posts load content 2`] = ` "title": "Another With Tag", }, "hasTruncateMarker": false, + "lastUpdatedAt": undefined, + "lastUpdatedBy": undefined, "nextItem": { "permalink": "/blog/another/tags2", "title": "Another With Tag", @@ -215,6 +219,8 @@ exports[`blog plugin process blog posts load content 2`] = ` "title": "Another With Tag", }, "hasTruncateMarker": false, + "lastUpdatedAt": undefined, + "lastUpdatedBy": undefined, "permalink": "/blog/another/tags2", "prevItem": { "permalink": "/blog/another/tags", diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index 73ecb63a2567..89009b57b575 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -8,7 +8,12 @@ import {jest} from '@jest/globals'; import path from 'path'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; -import {posixPath, getFileCommitDate} from '@docusaurus/utils'; +import { + posixPath, + getFileCommitDate, + GIT_FALLBACK_LAST_UPDATE_DATE, + GIT_FALLBACK_LAST_UPDATE_AUTHOR, +} from '@docusaurus/utils'; import pluginContentBlog from '../index'; import {validateOptions} from '../options'; import type { @@ -510,7 +515,7 @@ describe('blog plugin', () => { { postsPerPage: 1, processBlogPosts: async ({blogPosts}) => - blogPosts.filter((blog) => blog.metadata.tags[0].label === 'tag1'), + blogPosts.filter((blog) => blog.metadata.tags[0]?.label === 'tag1'), }, DefaultI18N, ); @@ -526,3 +531,137 @@ describe('blog plugin', () => { expect(blogPosts).toMatchSnapshot(); }); }); + +describe('last update', () => { + const siteDir = path.join( + __dirname, + '__fixtures__', + 'website-blog-with-last-update', + ); + + const lastUpdateFor = (date: string) => new Date(date).getTime() / 1000; + + it('author and time', async () => { + const plugin = await getPlugin( + siteDir, + { + showLastUpdateAuthor: true, + showLastUpdateTime: true, + }, + DefaultI18N, + ); + const {blogPosts} = (await plugin.loadContent!())!; + + expect(blogPosts[0]?.metadata.lastUpdatedBy).toBe('seb'); + expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe( + GIT_FALLBACK_LAST_UPDATE_DATE, + ); + + expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe( + GIT_FALLBACK_LAST_UPDATE_AUTHOR, + ); + expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe( + GIT_FALLBACK_LAST_UPDATE_DATE, + ); + + expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb'); + expect(blogPosts[2]?.metadata.lastUpdatedAt).toBe( + lastUpdateFor('2021-01-01'), + ); + + expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe( + GIT_FALLBACK_LAST_UPDATE_AUTHOR, + ); + expect(blogPosts[3]?.metadata.lastUpdatedAt).toBe( + lastUpdateFor('2021-01-01'), + ); + }); + + it('time only', async () => { + const plugin = await getPlugin( + siteDir, + { + showLastUpdateAuthor: false, + showLastUpdateTime: true, + }, + DefaultI18N, + ); + const {blogPosts} = (await plugin.loadContent!())!; + + expect(blogPosts[0]?.metadata.title).toBe('Author'); + expect(blogPosts[0]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe( + GIT_FALLBACK_LAST_UPDATE_DATE, + ); + + expect(blogPosts[1]?.metadata.title).toBe('Nothing'); + expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe( + GIT_FALLBACK_LAST_UPDATE_DATE, + ); + + expect(blogPosts[2]?.metadata.title).toBe('Both'); + expect(blogPosts[2]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[2]?.metadata.lastUpdatedAt).toBe( + lastUpdateFor('2021-01-01'), + ); + + expect(blogPosts[3]?.metadata.title).toBe('Last update date'); + expect(blogPosts[3]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[3]?.metadata.lastUpdatedAt).toBe( + lastUpdateFor('2021-01-01'), + ); + }); + + it('author only', async () => { + const plugin = await getPlugin( + siteDir, + { + showLastUpdateAuthor: true, + showLastUpdateTime: false, + }, + DefaultI18N, + ); + const {blogPosts} = (await plugin.loadContent!())!; + + expect(blogPosts[0]?.metadata.lastUpdatedBy).toBe('seb'); + expect(blogPosts[0]?.metadata.lastUpdatedAt).toBeUndefined(); + + expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe( + GIT_FALLBACK_LAST_UPDATE_AUTHOR, + ); + expect(blogPosts[1]?.metadata.lastUpdatedAt).toBeUndefined(); + + expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb'); + expect(blogPosts[2]?.metadata.lastUpdatedAt).toBeUndefined(); + + expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe( + GIT_FALLBACK_LAST_UPDATE_AUTHOR, + ); + expect(blogPosts[3]?.metadata.lastUpdatedAt).toBeUndefined(); + }); + + it('none', async () => { + const plugin = await getPlugin( + siteDir, + { + showLastUpdateAuthor: false, + showLastUpdateTime: false, + }, + DefaultI18N, + ); + const {blogPosts} = (await plugin.loadContent!())!; + + expect(blogPosts[0]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[0]?.metadata.lastUpdatedAt).toBeUndefined(); + + expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[1]?.metadata.lastUpdatedAt).toBeUndefined(); + + expect(blogPosts[2]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[2]?.metadata.lastUpdatedAt).toBeUndefined(); + + expect(blogPosts[3]?.metadata.lastUpdatedBy).toBeUndefined(); + expect(blogPosts[3]?.metadata.lastUpdatedAt).toBeUndefined(); + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index fd544db14f41..ff8b5b892915 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -26,6 +26,7 @@ import { getContentPathList, isUnlisted, isDraft, + readLastUpdateData, } from '@docusaurus/utils'; import {validateBlogPostFrontMatter} from './frontMatter'; import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors'; @@ -231,6 +232,12 @@ async function processBlogSourceFile( const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir); + const lastUpdate = await readLastUpdateData( + blogSourceAbsolute, + options, + frontMatter.last_update, + ); + const draft = isDraft({frontMatter}); const unlisted = isUnlisted({frontMatter}); @@ -337,6 +344,8 @@ async function processBlogSourceFile( authors, frontMatter, unlisted, + lastUpdatedAt: lastUpdate.lastUpdatedAt, + lastUpdatedBy: lastUpdate.lastUpdatedBy, }, content, }; diff --git a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts index 73b4d37d25ee..3c3f0f8883ee 100644 --- a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts @@ -4,14 +4,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - import { + ContentVisibilitySchema, + FrontMatterLastUpdateSchema, + FrontMatterTOCHeadingLevels, + FrontMatterTagsSchema, JoiFrontMatter as Joi, // Custom instance for front matter URISchema, validateFrontMatter, - FrontMatterTagsSchema, - FrontMatterTOCHeadingLevels, - ContentVisibilitySchema, } from '@docusaurus/utils-validation'; import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog'; @@ -69,6 +69,7 @@ const BlogFrontMatterSchema = Joi.object({ hide_table_of_contents: Joi.boolean(), ...FrontMatterTOCHeadingLevels, + last_update: FrontMatterLastUpdateSchema, }) .messages({ 'deprecate.error': diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index 62e2a36c94d9..86dcbbd4be4f 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -51,6 +51,8 @@ export const DEFAULT_OPTIONS: PluginOptions = { authorsMapPath: 'authors.yml', readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), sortPosts: 'descending', + showLastUpdateTime: false, + showLastUpdateAuthor: false, processBlogPosts: async () => undefined, }; @@ -135,6 +137,10 @@ const PluginOptionSchema = Joi.object({ sortPosts: Joi.string() .valid('descending', 'ascending') .default(DEFAULT_OPTIONS.sortPosts), + showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime), + showLastUpdateAuthor: Joi.bool().default( + DEFAULT_OPTIONS.showLastUpdateAuthor, + ), processBlogPosts: Joi.function() .optional() .default(() => DEFAULT_OPTIONS.processBlogPosts), 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 898ed8b2e92f..a1f580466ebe 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 @@ -10,7 +10,12 @@ declare module '@docusaurus/plugin-content-blog' { import type {LoadedMDXContent} from '@docusaurus/mdx-loader'; import type {MDXOptions} from '@docusaurus/mdx-loader'; - import type {FrontMatterTag, Tag} from '@docusaurus/utils'; + import type { + FrontMatterTag, + Tag, + LastUpdateData, + FrontMatterLastUpdate, + } from '@docusaurus/utils'; import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types'; import type {Item as FeedItem} from 'feed'; import type {Overwrite} from 'utility-types'; @@ -156,6 +161,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the toc_min_heading_level?: number; /** Maximum TOC heading level. Must be between 2 and 6. */ toc_max_heading_level?: number; + /** Allows overriding the last updated author and/or date. */ + last_update?: FrontMatterLastUpdate; }; export type BlogPostFrontMatterAuthor = Author & { @@ -180,7 +187,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the | BlogPostFrontMatterAuthor | (string | BlogPostFrontMatterAuthor)[]; - export type BlogPostMetadata = { + export type BlogPostMetadata = LastUpdateData & { /** Path to the Markdown source, with `@site` alias. */ readonly source: string; /** @@ -426,6 +433,10 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the readingTime: ReadingTimeFunctionOption; /** Governs the direction of blog post sorting. */ sortPosts: 'ascending' | 'descending'; + /** Whether to display the last date the doc was updated. */ + showLastUpdateTime: boolean; + /** Whether to display the author who last updated the doc. */ + showLastUpdateAuthor: boolean; /** An optional function which can be used to transform blog posts * (filter, modify, delete, etc...). */ diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts index 8589a707681b..2219ad86af55 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts @@ -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 :('}}, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts deleted file mode 100644 index 53f2827f2dc7..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 158f949ef370..7c94593fa0cf 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -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 { - 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, diff --git a/packages/docusaurus-plugin-content-docs/src/frontMatter.ts b/packages/docusaurus-plugin-content-docs/src/frontMatter.ts index 1cffac35f32c..de315cddab9c 100644 --- a/packages/docusaurus-plugin-content-docs/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/frontMatter.ts @@ -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({ +export const DocFrontMatterSchema = Joi.object({ 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({ 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); diff --git a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts deleted file mode 100644 index 2a0fefd5b6fa..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index dc0db6e40368..38606e51168b 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -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 = { diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 6afed49abcfc..39e7195893b4 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -676,6 +676,16 @@ declare module '@theme/DocVersionSuggestions' { export default function DocVersionSuggestions(): JSX.Element; } +declare module '@theme/EditMetaRow' { + export interface Props { + readonly className: string; + readonly editUrl: string | null | undefined; + readonly lastUpdatedAt: number | undefined; + readonly lastUpdatedBy: string | undefined; + } + export default function EditMetaRow(props: Props): JSX.Element; +} + declare module '@theme/EditThisPage' { export interface Props { readonly editUrl: string; diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/index.tsx index 314faacf2d98..22020c37e380 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/index.tsx @@ -8,15 +8,21 @@ import React from 'react'; import clsx from 'clsx'; import {useBlogPost} from '@docusaurus/theme-common/internal'; -import EditThisPage from '@theme/EditThisPage'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import EditMetaRow from '@theme/EditMetaRow'; import TagsListInline from '@theme/TagsListInline'; import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink'; -import styles from './styles.module.css'; - export default function BlogPostItemFooter(): JSX.Element | null { const {metadata, isBlogPostPage} = useBlogPost(); - const {tags, title, editUrl, hasTruncateMarker} = metadata; + const { + tags, + title, + editUrl, + hasTruncateMarker, + lastUpdatedBy, + lastUpdatedAt, + } = metadata; // A post is truncated if it's in the "list view" and it has a truncate marker const truncatedPost = !isBlogPostPage && hasTruncateMarker; @@ -29,32 +35,56 @@ export default function BlogPostItemFooter(): JSX.Element | null { return null; } - return ( -
- {tagsExists && ( -
- -
- )} - - {isBlogPostPage && editUrl && ( -
- -
- )} - - {truncatedPost && ( -
- -
- )} -
- ); + // BlogPost footer - details view + if (isBlogPostPage) { + const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + + return ( +
+ {tagsExists && ( +
+
+ +
+
+ )} + {canDisplayEditMetaRow && ( + + )} +
+ ); + } + // BlogPost footer - list view + else { + return ( +
+ {tagsExists && ( +
+ +
+ )} + {truncatedPost && ( +
+ +
+ )} +
+ ); + } } diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/styles.module.css b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/styles.module.css deleted file mode 100644 index f9272fb53b69..000000000000 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/styles.module.css +++ /dev/null @@ -1,10 +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. - */ - -.blogPostFooterDetailsFull { - flex-direction: column; -} diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/Footer/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItem/Footer/index.tsx index c59b85b3d49d..3757d1cac2ed 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/Footer/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/Footer/index.tsx @@ -8,53 +8,10 @@ import React from 'react'; import clsx from 'clsx'; import {ThemeClassNames} from '@docusaurus/theme-common'; -import {useDoc, type DocContextValue} from '@docusaurus/theme-common/internal'; -import LastUpdated from '@theme/LastUpdated'; -import EditThisPage from '@theme/EditThisPage'; -import TagsListInline, { - type Props as TagsListInlineProps, -} from '@theme/TagsListInline'; +import {useDoc} from '@docusaurus/theme-common/internal'; +import TagsListInline from '@theme/TagsListInline'; -import styles from './styles.module.css'; - -function TagsRow(props: TagsListInlineProps) { - return ( -
-
- -
-
- ); -} - -type EditMetaRowProps = Pick< - DocContextValue['metadata'], - 'editUrl' | 'lastUpdatedAt' | 'lastUpdatedBy' ->; -function EditMetaRow({ - editUrl, - lastUpdatedAt, - lastUpdatedBy, -}: EditMetaRowProps) { - return ( -
-
{editUrl && }
- -
- {(lastUpdatedAt || lastUpdatedBy) && ( - - )} -
-
- ); -} +import EditMetaRow from '@theme/EditMetaRow'; export default function DocItemFooter(): JSX.Element | null { const {metadata} = useDoc(); @@ -72,9 +29,23 @@ export default function DocItemFooter(): JSX.Element | null { return (