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 afe7fe519b..a3056261cf 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 @@ -16,7 +16,12 @@ declare module '@docusaurus/plugin-content-blog' { FrontMatterLastUpdate, TagsPluginOptions, } from '@docusaurus/utils'; - import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types'; + import type { + DocusaurusConfig, + Plugin, + LoadContext, + OptionValidationContext, + } from '@docusaurus/types'; import type {Item as FeedItem} from 'feed'; import type {Overwrite} from 'utility-types'; @@ -666,6 +671,10 @@ declare module '@docusaurus/plugin-content-blog' { context: LoadContext, options: PluginOptions, ): Promise>; + + export function validateOptions( + args: OptionValidationContext, + ): PluginOptions; } declare module '@theme/BlogPostPage' { 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 e11340aa5b..10f4a84a3b 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 @@ -20,7 +20,11 @@ declare module '@docusaurus/plugin-content-docs' { TagMetadata, TagsPluginOptions, } from '@docusaurus/utils'; - import type {Plugin, LoadContext} from '@docusaurus/types'; + import type { + Plugin, + LoadContext, + OptionValidationContext, + } from '@docusaurus/types'; import type {Overwrite, Required} from 'utility-types'; export type Assets = { @@ -559,6 +563,10 @@ declare module '@docusaurus/plugin-content-docs' { context: LoadContext, options: PluginOptions, ): Promise>; + + export function validateOptions( + args: OptionValidationContext, + ): PluginOptions; } declare module '@theme/DocItem' { diff --git a/packages/docusaurus-plugin-content-pages/src/plugin-content-pages.d.ts b/packages/docusaurus-plugin-content-pages/src/plugin-content-pages.d.ts index e66659f8ea..d9045fd3f4 100644 --- a/packages/docusaurus-plugin-content-pages/src/plugin-content-pages.d.ts +++ b/packages/docusaurus-plugin-content-pages/src/plugin-content-pages.d.ts @@ -7,7 +7,11 @@ declare module '@docusaurus/plugin-content-pages' { import type {MDXOptions} from '@docusaurus/mdx-loader'; - import type {LoadContext, Plugin} from '@docusaurus/types'; + import type { + LoadContext, + Plugin, + OptionValidationContext, + } from '@docusaurus/types'; import type {FrontMatterLastUpdate, LastUpdateData} from '@docusaurus/utils'; export type Assets = { @@ -82,6 +86,10 @@ declare module '@docusaurus/plugin-content-pages' { context: LoadContext, options: PluginOptions, ): Promise>; + + export function validateOptions( + args: OptionValidationContext, + ): PluginOptions; } declare module '@theme/MDXPage' { diff --git a/project-words.txt b/project-words.txt index 5a0ef4e675..5c0cd0765f 100644 --- a/project-words.txt +++ b/project-words.txt @@ -178,7 +178,6 @@ metastring metrica Metrika microdata -Microdata Milnes mindmap Mindmap diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 8aa35595d9..a1ce845153 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -282,7 +282,7 @@ export default async function createConfigAsync() { }, ], [ - './src/plugins/changelog/index.js', + './src/plugins/changelog/index.ts', { blogTitle: 'Docusaurus changelog', blogDescription: diff --git a/website/src/plugins/changelog/index.js b/website/src/plugins/changelog/index.js deleted file mode 100644 index 2636bc9b9c..0000000000 --- a/website/src/plugins/changelog/index.js +++ /dev/null @@ -1,160 +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 path from 'path'; -import fs from 'fs-extra'; -import pluginContentBlog from '@docusaurus/plugin-content-blog'; -import {aliasedSitePath, docuHash, normalizeUrl} from '@docusaurus/utils'; - -/** - * Multiple versions may be published on the same day, causing the order to be - * the reverse. Therefore, our publish time has a "fake hour" to order them. - */ -const publishTimes = new Set(); -/** - * @type {Record} - */ -const authorsMap = {}; - -/** - * @param {string} section - */ -function processSection(section) { - const title = section - .match(/\n## .*/)?.[0] - .trim() - .replace('## ', ''); - if (!title) { - return null; - } - const content = section - .replace(/\n## .*/, '') - .trim() - .replace('running_woman', 'running'); - - let authors = content.match(/## Committers: \d.*/s); - if (authors) { - authors = authors[0] - .match(/- .*/g) - .map( - (line) => - line.match( - /- (?:(?.*?) \()?\[@(?.*)\]\((?.*?)\)\)?/, - ).groups, - ) - .map((author) => ({ - ...author, - name: author.name ?? author.alias, - imageURL: `https://github.com/${author.alias}.png`, - })) - .sort((a, b) => a.url.localeCompare(b.url)); - - authors.forEach((author) => { - authorsMap[author.alias] = author; - }); - } - let hour = 20; - const date = title.match(/ \((?.*)\)/)?.groups.date; - while (publishTimes.has(`${date}T${hour}:00`)) { - hour -= 1; - } - publishTimes.add(`${date}T${hour}:00`); - - return { - title: title.replace(/ \(.*\)/, ''), - content: `--- -mdx: - format: md -date: ${`${date}T${hour}:00`}${ - authors - ? ` -authors: -${authors.map((author) => ` - '${author.alias}'`).join('\n')}` - : '' - } ---- - -# ${title.replace(/ \(.*\)/, '')} - - - -${content.replace(/####/g, '##')}`, - }; -} - -/** - * @param {import('@docusaurus/types').LoadContext} context - * @returns {import('@docusaurus/types').Plugin} - */ -export default async function ChangelogPlugin(context, options) { - const generateDir = path.join(context.siteDir, 'changelog/source'); - const blogPlugin = await pluginContentBlog.default(context, { - ...options, - path: generateDir, - id: 'changelog', - blogListComponent: '@theme/ChangelogList', - blogPostComponent: '@theme/ChangelogPage', - }); - const changelogPath = path.join(__dirname, '../../../../CHANGELOG.md'); - return { - ...blogPlugin, - name: 'changelog-plugin', - async loadContent() { - const fileContent = await fs.readFile(changelogPath, 'utf-8'); - const sections = fileContent - .split(/(?=\n## )/) - .map(processSection) - .filter(Boolean); - await Promise.all( - sections.map((section) => - fs.outputFile( - path.join(generateDir, `${section.title}.md`), - section.content, - ), - ), - ); - const authorsPath = path.join(generateDir, 'authors.json'); - await fs.outputFile(authorsPath, JSON.stringify(authorsMap, null, 2)); - const content = await blogPlugin.loadContent(); - content.blogPosts.forEach((post, index) => { - const pageIndex = Math.floor(index / options.postsPerPage); - post.metadata.listPageLink = normalizeUrl([ - context.baseUrl, - options.routeBasePath, - pageIndex === 0 ? '/' : `/page/${pageIndex + 1}`, - ]); - }); - return content; - }, - configureWebpack(...args) { - const config = blogPlugin.configureWebpack(...args); - const pluginDataDirRoot = path.join( - context.generatedFilesDir, - 'changelog-plugin', - 'default', - ); - // Redirect the metadata path to our folder - const mdxLoader = config.module.rules[0].use[0]; - mdxLoader.options.metadataPath = (mdxPath) => { - // Note that metadataPath must be the same/in-sync as - // the path from createData for each MDX. - const aliasedPath = aliasedSitePath(mdxPath, context.siteDir); - return path.join(pluginDataDirRoot, `${docuHash(aliasedPath)}.json`); - }; - return config; - }, - getThemePath() { - return './theme'; - }, - getPathsToWatch() { - // Don't watch the generated dir - return [changelogPath]; - }, - }; -} - -export const {validateOptions} = pluginContentBlog; diff --git a/website/src/plugins/changelog/index.ts b/website/src/plugins/changelog/index.ts new file mode 100644 index 0000000000..46f5f08efb --- /dev/null +++ b/website/src/plugins/changelog/index.ts @@ -0,0 +1,208 @@ +/** + * 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 path from 'path'; +import fs from 'fs-extra'; +import pluginContentBlog from '@docusaurus/plugin-content-blog'; +import {aliasedSitePath, docuHash, normalizeUrl} from '@docusaurus/utils'; + +export {validateOptions} from '@docusaurus/plugin-content-blog'; + +/** + * Multiple versions may be published on the same day, causing the order to be + * the reverse. Therefore, our publish time has a "fake hour" to order them. + */ +// TODO may leak small amount of memory in multi-locale builds +const publishTimes = new Set(); + +type Author = {name: string; url: string; alias: string; imageURL: string}; + +type AuthorsMap = Record; + +type ChangelogEntry = { + title: string; + content: string; + authors: Author[]; +}; + +function parseAuthor(committerLine: string): Author { + const groups = committerLine.match( + /- (?:(?.*?) \()?\[@(?.*)\]\((?.*?)\)\)?/, + )!.groups as {name: string; alias: string; url: string}; + + return { + ...groups, + name: groups.name ?? groups.alias, + imageURL: `https://github.com/${groups.alias}.png`, + }; +} + +function parseAuthors(content: string): Author[] { + const committersContent = content.match(/## Committers: \d.*/s)?.[0]; + if (!committersContent) { + return []; + } + const committersLines = committersContent.match(/- .*/g)!; + + const authors = committersLines + .map(parseAuthor) + .sort((a, b) => a.url.localeCompare(b.url)); + + return authors; +} + +function createAuthorsMap(changelogEntries: ChangelogEntry[]): AuthorsMap { + const allAuthors = changelogEntries.flatMap((entry) => entry.authors); + const authorsMap: AuthorsMap = {}; + allAuthors?.forEach((author) => { + authorsMap[author.alias] = author; + }); + return authorsMap; +} + +function toChangelogEntry(sectionContent: string): ChangelogEntry | null { + const title = sectionContent + .match(/\n## .*/)?.[0] + .trim() + .replace('## ', ''); + if (!title) { + return null; + } + const content = sectionContent + .replace(/\n## .*/, '') + .trim() + .replace('running_woman', 'running'); + + const authors = parseAuthors(content); + + let hour = 20; + const date = title.match(/ \((?.*)\)/)?.groups!.date; + while (publishTimes.has(`${date}T${hour}:00`)) { + hour -= 1; + } + publishTimes.add(`${date}T${hour}:00`); + + return { + authors, + title: title.replace(/ \(.*\)/, ''), + content: `--- +mdx: + format: md +date: ${`${date}T${hour}:00`}${ + authors.length > 0 + ? ` +authors: +${authors.map((author) => ` - '${author.alias}'`).join('\n')}` + : '' + } +--- + +# ${title.replace(/ \(.*\)/, '')} + + + +${content.replace(/####/g, '##')}`, + }; +} + +function toChangelogEntries(fileContent: string): ChangelogEntry[] { + return fileContent + .split(/(?=\n## )/) + .map(toChangelogEntry) + .filter((s): s is ChangelogEntry => s !== null); +} + +async function createBlogFiles( + generateDir: string, + changelogEntries: ChangelogEntry[], +) { + await Promise.all( + changelogEntries.map((changelogEntry) => + fs.outputFile( + path.join(generateDir, `${changelogEntry.title}.md`), + changelogEntry.content, + ), + ), + ); + + await fs.outputFile( + path.join(generateDir, 'authors.json'), + JSON.stringify(createAuthorsMap(changelogEntries), null, 2), + ); +} + +const ChangelogPlugin: typeof pluginContentBlog = + async function ChangelogPlugin(context, options) { + const generateDir = path.join(context.siteDir, 'changelog/source'); + const blogPlugin = await pluginContentBlog(context, { + ...options, + path: generateDir, + id: 'changelog', + blogListComponent: '@theme/ChangelogList', + blogPostComponent: '@theme/ChangelogPage', + }); + const changelogPath = path.join(__dirname, '../../../../CHANGELOG.md'); + return { + ...blogPlugin, + name: 'changelog-plugin', + + async loadContent() { + const fileContent = await fs.readFile(changelogPath, 'utf-8'); + const changelogEntries = toChangelogEntries(fileContent); + + // We have to create intermediate files here + // Unfortunately Docusaurus doesn't have yet any concept of virtual file + await createBlogFiles(generateDir, changelogEntries); + + // Read the files we actually just wrote + const content = (await blogPlugin.loadContent?.())!; + + content.blogPosts.forEach((post, index) => { + const pageIndex = Math.floor( + index / (options.postsPerPage as number), + ); + // @ts-expect-error: TODO Docusaurus use interface declaration merging + post.metadata.listPageLink = normalizeUrl([ + context.baseUrl, + options.routeBasePath, + pageIndex === 0 ? '/' : `/page/${pageIndex + 1}`, + ]); + }); + return content; + }, + + configureWebpack(...args) { + const config = blogPlugin.configureWebpack?.(...args); + const pluginDataDirRoot = path.join( + context.generatedFilesDir, + 'changelog-plugin', + 'default', + ); + // Redirect the metadata path to our folder + // @ts-expect-error: unsafe but works + const mdxLoader = config.module.rules[0].use[0]; + mdxLoader.options.metadataPath = (mdxPath: string) => { + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + const aliasedPath = aliasedSitePath(mdxPath, context.siteDir); + return path.join(pluginDataDirRoot, `${docuHash(aliasedPath)}.json`); + }; + return config; + }, + + getThemePath() { + return './theme'; + }, + + getPathsToWatch() { + // Don't watch the generated dir + return [changelogPath]; + }, + }; + }; + +export default ChangelogPlugin;