fix(plugins): add missing validateOptions types (#10929)

Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
Sébastien Lorber 2025-02-13 15:29:51 +01:00 committed by GitHub
parent 04f7972f32
commit a72a06ecb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 237 additions and 165 deletions

View file

@ -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<Plugin<BlogContent>>;
export function validateOptions(
args: OptionValidationContext<Options | undefined, PluginOptions>,
): PluginOptions;
}
declare module '@theme/BlogPostPage' {

View file

@ -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<Plugin<LoadedContent>>;
export function validateOptions(
args: OptionValidationContext<Options | undefined, PluginOptions>,
): PluginOptions;
}
declare module '@theme/DocItem' {

View file

@ -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<Plugin<LoadedContent | null>>;
export function validateOptions(
args: OptionValidationContext<Options | undefined, PluginOptions>,
): PluginOptions;
}
declare module '@theme/MDXPage' {

View file

@ -178,7 +178,6 @@ metastring
metrica
Metrika
microdata
Microdata
Milnes
mindmap
Mindmap

View file

@ -282,7 +282,7 @@ export default async function createConfigAsync() {
},
],
[
'./src/plugins/changelog/index.js',
'./src/plugins/changelog/index.ts',
{
blogTitle: 'Docusaurus changelog',
blogDescription:

View file

@ -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<string, {name: string, url: string,alias: string, imageURL: string}>}
*/
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(
/- (?:(?<name>.*?) \()?\[@(?<alias>.*)\]\((?<url>.*?)\)\)?/,
).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(/ \((?<date>.*)\)/)?.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(/ \(.*\)/, '')}
<!-- truncate -->
${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;

View file

@ -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<string>();
type Author = {name: string; url: string; alias: string; imageURL: string};
type AuthorsMap = Record<string, Author>;
type ChangelogEntry = {
title: string;
content: string;
authors: Author[];
};
function parseAuthor(committerLine: string): Author {
const groups = committerLine.match(
/- (?:(?<name>.*?) \()?\[@(?<alias>.*)\]\((?<url>.*?)\)\)?/,
)!.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(/ \((?<date>.*)\)/)?.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(/ \(.*\)/, '')}
<!-- truncate -->
${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;