diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index a30480b84025..6f060d8e22ae 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -23,6 +23,7 @@ import { isUnlisted, isDraft, readLastUpdateData, + readCreationData, normalizeTags, aliasedSitePathToRelativePath, } from '@docusaurus/utils'; @@ -260,6 +261,13 @@ async function processBlogSourceFile( vcs, ); + const creation = await readCreationData( + blogSourceAbsolute, + options, + frontMatter.created_at, + vcs, + ); + const draft = isDraft({frontMatter}); const unlisted = isUnlisted({frontMatter}); @@ -379,6 +387,8 @@ async function processBlogSourceFile( unlisted, lastUpdatedAt: lastUpdate.lastUpdatedAt, lastUpdatedBy: lastUpdate.lastUpdatedBy, + createdAt: creation.createdAt, + createdBy: creation.createdBy, }, content, }; diff --git a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts index 9a6002776734..22cfb6e93492 100644 --- a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts @@ -9,6 +9,7 @@ import { ContentVisibilitySchema, FrontMatterLastUpdateSchema, + FrontMatterCreationSchema, FrontMatterTOCHeadingLevels, FrontMatterTagsSchema, JoiFrontMatter as Joi, // Custom instance for front matter @@ -76,6 +77,7 @@ const BlogFrontMatterSchema = Joi.object({ ...FrontMatterTOCHeadingLevels, last_update: FrontMatterLastUpdateSchema, + created_at: FrontMatterCreationSchema, }) .messages({ 'deprecate.error': diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index b808e144c805..d0687bb0d9c2 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -69,6 +69,8 @@ export const DEFAULT_OPTIONS: PluginOptions = { sortPosts: 'descending', showLastUpdateTime: false, showLastUpdateAuthor: false, + showCreatedTime: false, + showCreatedBy: false, processBlogPosts: async () => undefined, tags: undefined, authorsBasePath: 'authors', @@ -227,6 +229,8 @@ const PluginOptionSchema = Joi.object({ showLastUpdateAuthor: Joi.bool().default( DEFAULT_OPTIONS.showLastUpdateAuthor, ), + showCreatedTime: Joi.bool().default(DEFAULT_OPTIONS.showCreatedTime), + showCreatedBy: Joi.bool().default(DEFAULT_OPTIONS.showCreatedBy), 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 3f47050be68b..0095ceb8853f 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 @@ -12,7 +12,9 @@ declare module '@docusaurus/plugin-content-blog' { FrontMatterTag, TagMetadata, LastUpdateData, + CreationData, FrontMatterLastUpdate, + FrontMatterCreation, TagsPluginOptions, } from '@docusaurus/utils'; import type { @@ -236,6 +238,8 @@ declare module '@docusaurus/plugin-content-blog' { toc_max_heading_level?: number; /** Allows overriding the last updated author and/or date. */ last_update?: FrontMatterLastUpdate; + /** Allows overriding the creation author and/or date. */ + created_at?: FrontMatterCreation; }; export type BlogPostFrontMatterAuthor = AuthorAttributes & { @@ -260,61 +264,62 @@ declare module '@docusaurus/plugin-content-blog' { | BlogPostFrontMatterAuthor | (string | BlogPostFrontMatterAuthor)[]; - export type BlogPostMetadata = LastUpdateData & { - /** Path to the Markdown source, with `@site` alias. */ - readonly source: string; - /** - * Used to generate the page h1 heading, tab title, and pagination title. - */ - readonly title: string; - /** - * The publish date of the post. On client side, this will be serialized - * into a string. - */ - readonly date: Date; - /** Full link including base URL. */ - readonly permalink: string; - /** - * Description used in the meta. Could be an empty string (empty content) - */ - readonly description: string; - /** - * Absolute URL to the editing page of the post. Undefined if the post - * shouldn't be edited. - */ - readonly editUrl?: string; - /** - * Reading time in minutes calculated based on word count. - */ - readonly readingTime?: number; - /** - * Whether the truncate marker exists in the post's content. - */ - readonly hasTruncateMarker: boolean; - /** - * Used in pagination. Generated after the other metadata, so not readonly. - * Content is just a subset of another post's metadata. - */ - nextItem?: {readonly title: string; readonly permalink: string}; - /** - * Used in pagination. Generated after the other metadata, so not readonly. - * Content is just a subset of another post's metadata. - */ - prevItem?: {readonly title: string; readonly permalink: string}; - /** - * Author metadata, normalized. Should be used in joint with - * `assets.authorsImageUrls` on client side. - */ - readonly authors: Author[]; - /** Front matter, as-is. */ - readonly frontMatter: BlogPostFrontMatter & {[key: string]: unknown}; - /** Tags, normalized. */ - readonly tags: TagMetadata[]; - /** - * Marks the post as unlisted and visibly hides it unless directly accessed. - */ - readonly unlisted: boolean; - }; + export type BlogPostMetadata = LastUpdateData & + CreationData & { + /** Path to the Markdown source, with `@site` alias. */ + readonly source: string; + /** + * Used to generate the page h1 heading, tab title, and pagination title. + */ + readonly title: string; + /** + * The publish date of the post. On client side, this will be serialized + * into a string. + */ + readonly date: Date; + /** Full link including base URL. */ + readonly permalink: string; + /** + * Description used in the meta. Could be an empty string (empty content) + */ + readonly description: string; + /** + * Absolute URL to the editing page of the post. Undefined if the post + * shouldn't be edited. + */ + readonly editUrl?: string; + /** + * Reading time in minutes calculated based on word count. + */ + readonly readingTime?: number; + /** + * Whether the truncate marker exists in the post's content. + */ + readonly hasTruncateMarker: boolean; + /** + * Used in pagination. Generated after the other metadata, so not readonly. + * Content is just a subset of another post's metadata. + */ + nextItem?: {readonly title: string; readonly permalink: string}; + /** + * Used in pagination. Generated after the other metadata, so not readonly. + * Content is just a subset of another post's metadata. + */ + prevItem?: {readonly title: string; readonly permalink: string}; + /** + * Author metadata, normalized. Should be used in joint with + * `assets.authorsImageUrls` on client side. + */ + readonly authors: Author[]; + /** Front matter, as-is. */ + readonly frontMatter: BlogPostFrontMatter & {[key: string]: unknown}; + /** Tags, normalized. */ + readonly tags: TagMetadata[]; + /** + * Marks the post as unlisted and visibly hides it unless directly accessed. + */ + readonly unlisted: boolean; + }; /** * @returns The edit URL that's directly plugged into metadata. */ @@ -524,6 +529,10 @@ declare module '@docusaurus/plugin-content-blog' { showLastUpdateTime: boolean; /** Whether to display the author who last updated the blog post. */ showLastUpdateAuthor: boolean; + /** Whether to display the creation date of the blog post. */ + showCreatedTime: boolean; + /** Whether to display the author who created the blog post. */ + showCreatedBy: 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/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 06704fbff1dd..8e2d6a80261a 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -20,6 +20,7 @@ import { isUnlisted, isDraft, readLastUpdateData, + readCreationData, normalizeTags, } from '@docusaurus/utils'; import {validateDocFrontMatter} from './frontMatter'; @@ -63,7 +64,12 @@ export async function readVersionDocs( versionMetadata: VersionMetadata, options: Pick< PluginOptions, - 'include' | 'exclude' | 'showLastUpdateAuthor' | 'showLastUpdateTime' + | 'include' + | 'exclude' + | 'showLastUpdateAuthor' + | 'showLastUpdateTime' + | 'showCreatedTime' + | 'showCreatedBy' >, ): Promise { const sources = await Globby(options.include, { @@ -120,6 +126,7 @@ async function doProcessDocMetadata({ // but allow to disable this behavior with front matter parse_number_prefixes: parseNumberPrefixes = true, last_update: lastUpdateFrontMatter, + created_at: creationFrontMatter, } = frontMatter; const lastUpdate = await readLastUpdateData( @@ -129,6 +136,13 @@ async function doProcessDocMetadata({ vcs, ); + const creation = await readCreationData( + filePath, + options, + creationFrontMatter, + vcs, + ); + // E.g. api/plugins/myDoc -> myDoc; myDoc -> myDoc const sourceFileNameWithoutExtension = path.basename( source, @@ -240,6 +254,8 @@ async function doProcessDocMetadata({ version: versionMetadata.versionName, lastUpdatedBy: lastUpdate.lastUpdatedBy, lastUpdatedAt: lastUpdate.lastUpdatedAt, + createdBy: creation.createdBy, + createdAt: creation.createdAt, sidebarPosition, frontMatter, }; diff --git a/packages/docusaurus-plugin-content-docs/src/frontMatter.ts b/packages/docusaurus-plugin-content-docs/src/frontMatter.ts index 091b7aef9cf2..6188efe34853 100644 --- a/packages/docusaurus-plugin-content-docs/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/frontMatter.ts @@ -14,6 +14,7 @@ import { validateFrontMatter, ContentVisibilitySchema, FrontMatterLastUpdateSchema, + FrontMatterCreationSchema, } from '@docusaurus/utils-validation'; import type {DocFrontMatter} from '@docusaurus/plugin-content-docs'; @@ -46,6 +47,7 @@ export const DocFrontMatterSchema = Joi.object({ pagination_prev: Joi.string().allow(null), ...FrontMatterTOCHeadingLevels, last_update: FrontMatterLastUpdateSchema, + created_at: FrontMatterCreationSchema, }) .unknown() .concat(ContentVisibilitySchema); diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index 4812e5dc4ad6..3580e54eca09 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -46,6 +46,8 @@ export const DEFAULT_OPTIONS: Omit = { beforeDefaultRehypePlugins: [], showLastUpdateTime: false, showLastUpdateAuthor: false, + showCreatedTime: false, + showCreatedBy: false, admonitions: true, includeCurrentVersion: true, disableVersioning: false, @@ -137,6 +139,8 @@ const OptionsSchema = Joi.object({ showLastUpdateAuthor: Joi.bool().default( DEFAULT_OPTIONS.showLastUpdateAuthor, ), + showCreatedTime: Joi.bool().default(DEFAULT_OPTIONS.showCreatedTime), + showCreatedBy: Joi.bool().default(DEFAULT_OPTIONS.showCreatedBy), includeCurrentVersion: Joi.bool().default( DEFAULT_OPTIONS.includeCurrentVersion, ), 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 f89b3f63169e..9d1621051681 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,7 +16,9 @@ declare module '@docusaurus/plugin-content-docs' { TagsListItem, TagModule, FrontMatterLastUpdate, + FrontMatterCreation, LastUpdateData, + CreationData, TagMetadata, TagsPluginOptions, } from '@docusaurus/utils'; @@ -95,6 +97,10 @@ declare module '@docusaurus/plugin-content-docs' { showLastUpdateTime: boolean; /** Whether to display the author who last updated the doc. */ showLastUpdateAuthor: boolean; + /** Whether to display the creation date of the doc. */ + showCreatedTime: boolean; + /** Whether to display the author who created the doc. */ + showCreatedBy: boolean; /** * Custom parsing logic to extract number prefixes from file names. Use * `false` to disable this behavior and leave the docs untouched, and `true` @@ -409,60 +415,63 @@ declare module '@docusaurus/plugin-content-docs' { unlisted?: boolean; /** Allows overriding the last updated author and/or date. */ last_update?: FrontMatterLastUpdate; + /** Allows overriding the creation author and/or date. */ + created_at?: FrontMatterCreation; }; - export type DocMetadataBase = LastUpdateData & { - /** - * The document id. - * Multiple documents can have the same id, when in different versions. - */ - id: string; - /** The name of the version this doc belongs to. */ - version: string; - /** - * Used to generate the page h1 heading, tab title, and pagination title. - */ - title: string; - /** - * Description used in the meta. Could be an empty string (empty content) - */ - description: string; - /** Path to the Markdown source, with `@site` alias. */ - source: string; - /** - * Posix path relative to the content path. Can be `"."`. - * e.g. "folder/subfolder/subsubfolder" - */ - sourceDirName: string; - /** `permalink` without base URL or version path. */ - slug: string; - /** Full URL to this doc, with base URL and version path. */ - permalink: string; - /** - * Draft docs will be excluded for production environment. - */ - draft: boolean; - /** - * Unlisted docs are accessible when directly visible, but will be hidden - * from the sidebar and pagination in production. - */ - unlisted: boolean; - /** - * Position in an autogenerated sidebar slice, acquired through front matter - * or number prefix. - */ - sidebarPosition?: number; - /** - * Acquired from the options; can be customized with front matter. - * `custom_edit_url` will always lead to it being null, but you should treat - * `undefined` and `null` as equivalent. - */ - editUrl?: string | null; - /** Tags, normalized. */ - tags: TagMetadata[]; - /** Front matter, as-is. */ - frontMatter: DocFrontMatter & {[key: string]: unknown}; - }; + export type DocMetadataBase = LastUpdateData & + CreationData & { + /** + * The document id. + * Multiple documents can have the same id, when in different versions. + */ + id: string; + /** The name of the version this doc belongs to. */ + version: string; + /** + * Used to generate the page h1 heading, tab title, and pagination title. + */ + title: string; + /** + * Description used in the meta. Could be an empty string (empty content) + */ + description: string; + /** Path to the Markdown source, with `@site` alias. */ + source: string; + /** + * Posix path relative to the content path. Can be `"."`. + * e.g. "folder/subfolder/subsubfolder" + */ + sourceDirName: string; + /** `permalink` without base URL or version path. */ + slug: string; + /** Full URL to this doc, with base URL and version path. */ + permalink: string; + /** + * Draft docs will be excluded for production environment. + */ + draft: boolean; + /** + * Unlisted docs are accessible when directly visible, but will be hidden + * from the sidebar and pagination in production. + */ + unlisted: boolean; + /** + * Position in an autogenerated sidebar slice, acquired through front matter + * or number prefix. + */ + sidebarPosition?: number; + /** + * Acquired from the options; can be customized with front matter. + * `custom_edit_url` will always lead to it being null, but you should treat + * `undefined` and `null` as equivalent. + */ + editUrl?: string | null; + /** Tags, normalized. */ + tags: TagMetadata[]; + /** Front matter, as-is. */ + frontMatter: DocFrontMatter & {[key: string]: unknown}; + }; export type DocMetadata = DocMetadataBase & PropNavigation & { diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index cde7d8831241..434ef5b30658 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -35,6 +35,7 @@ function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata { return { sourceFilePath: aliasedSitePathToRelativePath(docMeta.source), lastUpdatedAt: docMeta.lastUpdatedAt, + createdAt: docMeta.createdAt, }; } diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index d1d80a5289a3..456f313b8d33 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -920,6 +920,8 @@ declare module '@theme/EditMetaRow' { readonly editUrl: string | null | undefined; readonly lastUpdatedAt: number | null | undefined; readonly lastUpdatedBy: string | null | undefined; + readonly createdAt?: number | null | undefined; + readonly createdBy?: string | null | undefined; } export default function EditMetaRow(props: Props): ReactNode; } @@ -1092,6 +1094,8 @@ declare module '@theme/LastUpdated' { export interface Props { readonly lastUpdatedAt?: number | null; readonly lastUpdatedBy?: string | null; + readonly createdAt?: number | null; + readonly createdBy?: string | null; } export default function LastUpdated(props: Props): ReactNode; 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 1646ecfdc83e..daa62a9556ce 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Footer/index.tsx @@ -22,6 +22,8 @@ export default function BlogPostItemFooter(): ReactNode { hasTruncateMarker, lastUpdatedBy, lastUpdatedAt, + createdBy, + createdAt, } = metadata; // A post is truncated if it's in the "list view" and it has a truncate marker @@ -37,7 +39,13 @@ export default function BlogPostItemFooter(): ReactNode { // BlogPost footer - details view if (isBlogPostPage) { - const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + const canDisplayEditMetaRow = !!( + editUrl || + lastUpdatedAt || + lastUpdatedBy || + createdAt || + createdBy + ); return (
@@ -62,6 +70,8 @@ export default function BlogPostItemFooter(): ReactNode { editUrl={editUrl} lastUpdatedAt={lastUpdatedAt} lastUpdatedBy={lastUpdatedBy} + createdAt={createdAt} + createdBy={createdBy} /> )}
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 e497c85c0cee..3f773b7d7757 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/Footer/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/Footer/index.tsx @@ -15,10 +15,17 @@ import EditMetaRow from '@theme/EditMetaRow'; export default function DocItemFooter(): ReactNode { const {metadata} = useDoc(); - const {editUrl, lastUpdatedAt, lastUpdatedBy, tags} = metadata; + const {editUrl, lastUpdatedAt, lastUpdatedBy, createdAt, createdBy, tags} = + metadata; const canDisplayTagsRow = tags.length > 0; - const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + const canDisplayEditMetaRow = !!( + editUrl || + lastUpdatedAt || + lastUpdatedBy || + createdAt || + createdBy + ); const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow; @@ -49,6 +56,8 @@ export default function DocItemFooter(): ReactNode { editUrl={editUrl} lastUpdatedAt={lastUpdatedAt} lastUpdatedBy={lastUpdatedBy} + createdAt={createdAt} + createdBy={createdBy} /> )} diff --git a/packages/docusaurus-theme-classic/src/theme/EditMetaRow/index.tsx b/packages/docusaurus-theme-classic/src/theme/EditMetaRow/index.tsx index d12ef51abfa2..0b9053b2d77f 100644 --- a/packages/docusaurus-theme-classic/src/theme/EditMetaRow/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/EditMetaRow/index.tsx @@ -17,6 +17,8 @@ export default function EditMetaRow({ editUrl, lastUpdatedAt, lastUpdatedBy, + createdAt, + createdBy, }: Props): ReactNode { return (
@@ -24,10 +26,12 @@ export default function EditMetaRow({ {editUrl && }
- {(lastUpdatedAt || lastUpdatedBy) && ( + {(lastUpdatedAt || lastUpdatedBy || createdAt || createdBy) && ( )}
diff --git a/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx b/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx index b454863587be..86ff55fef3d6 100644 --- a/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx @@ -65,9 +65,35 @@ function LastUpdatedByUser({ export default function LastUpdated({ lastUpdatedAt, lastUpdatedBy, + createdAt, + createdBy, }: Props): ReactNode { return ( + {createdAt || createdBy ? ( + + + ) : ( + '' + ), + byUser: createdBy ? ( + + ) : ( + '' + ), + }}> + {'Created{atDate}{byUser}'} + + {(lastUpdatedAt || lastUpdatedBy) && ( + · + )} + + ) : null} ({ + author: Joi.string(), + date: Joi.alternatives().try(Joi.date().raw()), +}) + .or('author', 'date') + .messages({ + 'object.missing': FrontMatterCreationErrorMessage, + 'object.base': FrontMatterCreationErrorMessage, + }); diff --git a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts index 67b57f8deaa1..c93eb81060f0 100644 --- a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts @@ -6,10 +6,13 @@ */ import {describe, expect, it} from 'vitest'; -import {readLastUpdateData} from '../lastUpdateUtils'; +import {readLastUpdateData, readCreationData} from '../lastUpdateUtils'; import {TEST_VCS} from '../vcs/vcs'; -import type {FrontMatterLastUpdate} from '../lastUpdateUtils'; +import type { + FrontMatterLastUpdate, + FrontMatterCreation, +} from '../lastUpdateUtils'; describe('readLastUpdateData', () => { const testDate = '2021-01-01'; @@ -181,3 +184,119 @@ describe('readLastUpdateData', () => { expect(lastUpdatedAt).toBeUndefined(); }); }); + +describe('readCreationData', () => { + const testDate = '2021-01-01'; + const testTimestamp = new Date(testDate).getTime(); + const testAuthor = 'ozaki'; + + async function readData( + filePath: string, + options: Parameters[1], + creationFrontMatter: Parameters[2], + ) { + return readCreationData(filePath, options, creationFrontMatter, TEST_VCS); + } + + describe('on untracked Git file', () => { + function readUntrackedFile( + creationFrontMatter: FrontMatterCreation | undefined, + ) { + return readData( + TEST_VCS.UNTRACKED_FILE_PATH, + {showCreatedBy: true, showCreatedTime: true}, + creationFrontMatter, + ); + } + + it('reads null at/by from Git', async () => { + const {createdAt, createdBy} = await readUntrackedFile({}); + expect(createdAt).toBeNull(); + expect(createdBy).toBeNull(); + }); + + it('reads null at from Git and author from front matter', async () => { + const {createdAt, createdBy} = await readUntrackedFile({ + author: testAuthor, + }); + expect(createdAt).toBeNull(); + expect(createdBy).toEqual(testAuthor); + }); + + it('reads null by from Git and date from front matter', async () => { + const {createdAt, createdBy} = await readUntrackedFile({ + date: testDate, + }); + expect(createdBy).toBeNull(); + expect(createdAt).toEqual(testTimestamp); + }); + }); + + it('read creation time with creation author', async () => { + const {createdAt, createdBy} = await readData( + '', + {showCreatedBy: true, showCreatedTime: true}, + {date: testDate}, + ); + expect(createdAt).toEqual(testTimestamp); + expect(createdBy).toBe(TEST_VCS.CREATION_INFO.author); + }); + + it('read creation author with creation time', async () => { + const {createdAt, createdBy} = await readData( + '', + {showCreatedBy: true, showCreatedTime: true}, + {author: testAuthor}, + ); + expect(createdBy).toEqual(testAuthor); + expect(createdAt).toBe(TEST_VCS.CREATION_INFO.timestamp); + }); + + it('read creation all from front matter', async () => { + const {createdAt, createdBy} = await readData( + '', + {showCreatedBy: true, showCreatedTime: true}, + {author: testAuthor, date: testDate}, + ); + expect(createdBy).toEqual(testAuthor); + expect(createdAt).toEqual(testTimestamp); + }); + + it('read creation default show none', async () => { + const creation = await readData( + '', + {showCreatedBy: false, showCreatedTime: false}, + {}, + ); + expect(creation).toEqual({}); + }); + + it('read creation author show none', async () => { + const creation = await readData( + '', + {showCreatedBy: false, showCreatedTime: false}, + {author: testAuthor}, + ); + expect(creation).toEqual({}); + }); + + it('read creation time show author', async () => { + const {createdAt, createdBy} = await readData( + '', + {showCreatedBy: true, showCreatedTime: false}, + {date: testDate}, + ); + expect(createdBy).toBe(TEST_VCS.CREATION_INFO.author); + expect(createdAt).toBeUndefined(); + }); + + it('read creation author show time', async () => { + const {createdAt, createdBy} = await readData( + '', + {showCreatedBy: false, showCreatedTime: true}, + {date: testDate}, + ); + expect(createdBy).toBeUndefined(); + expect(createdAt).toEqual(testTimestamp); + }); +}); diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index ed3106af890b..cf30c0bd5084 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -118,8 +118,11 @@ export {flattenRoutes} from './routeUtils'; export { readLastUpdateData, + readCreationData, type LastUpdateData, + type CreationData, type FrontMatterLastUpdate, + type FrontMatterCreation, } from './lastUpdateUtils'; export {VcsPresetNames, getVcsPreset, TEST_VCS} from './vcs/vcs'; diff --git a/packages/docusaurus-utils/src/lastUpdateUtils.ts b/packages/docusaurus-utils/src/lastUpdateUtils.ts index 0f1a44e86887..9f09283857fe 100644 --- a/packages/docusaurus-utils/src/lastUpdateUtils.ts +++ b/packages/docusaurus-utils/src/lastUpdateUtils.ts @@ -25,11 +25,28 @@ export type LastUpdateData = { lastUpdatedBy: string | undefined | null; }; +export type CreationData = { + /** + * A timestamp in **milliseconds**, usually read from `git log` + * `undefined`: not read + * `null`: no value to read (usual for untracked files) + */ + createdAt: number | undefined | null; + /** + * The author's name, usually coming from `git log` + * `undefined`: not read + * `null`: no value to read (usual for untracked files) + */ + createdBy: string | undefined | null; +}; + type LastUpdateOptions = Pick< PluginOptions, 'showLastUpdateAuthor' | 'showLastUpdateTime' >; +type CreationOptions = Pick; + export type FrontMatterLastUpdate = { author?: string; /** @@ -39,6 +56,15 @@ export type FrontMatterLastUpdate = { date?: Date | string; }; +export type FrontMatterCreation = { + 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; +}; + // TODO Docusaurus v4: refactor/rename, make it clear this fn is only // for Markdown files with front matter shared by content plugin export async function readLastUpdateData( @@ -101,3 +127,54 @@ export async function readLastUpdateData( lastUpdatedAt, }; } + +export async function readCreationData( + filePath: string, + options: CreationOptions, + creationFrontMatter: FrontMatterCreation | undefined, + vcsParam: Pick, +): Promise { + const vcs = vcsParam ?? getVcsPreset('default-v1'); + + const {showCreatedTime, showCreatedBy} = options; + + if (!showCreatedBy && !showCreatedTime) { + return {createdBy: undefined, createdAt: undefined}; + } + + const frontMatterAuthor = creationFrontMatter?.author; + const frontMatterTimestamp = creationFrontMatter?.date + ? new Date(creationFrontMatter.date).getTime() + : undefined; + + const getCreationMemoized = _.memoize(() => + vcs.getFileCreationInfo(filePath), + ); + const getCreatedBy = () => + getCreationMemoized().then((info) => { + if (info === null) { + return null; + } + return info?.author; + }); + const getCreatedAt = () => + getCreationMemoized().then((info) => { + if (info === null) { + return null; + } + return info?.timestamp; + }); + + const createdBy = showCreatedBy + ? (frontMatterAuthor ?? (await getCreatedBy())) + : undefined; + + const createdAt = showCreatedTime + ? (frontMatterTimestamp ?? (await getCreatedAt())) + : undefined; + + return { + createdBy, + createdAt, + }; +}