mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-08 22:03:01 +02:00
fix(plugins): add missing validateOptions
types (#10929)
Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
parent
04f7972f32
commit
a72a06ecb1
7 changed files with 237 additions and 165 deletions
|
@ -16,7 +16,12 @@ declare module '@docusaurus/plugin-content-blog' {
|
||||||
FrontMatterLastUpdate,
|
FrontMatterLastUpdate,
|
||||||
TagsPluginOptions,
|
TagsPluginOptions,
|
||||||
} from '@docusaurus/utils';
|
} 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 {Item as FeedItem} from 'feed';
|
||||||
import type {Overwrite} from 'utility-types';
|
import type {Overwrite} from 'utility-types';
|
||||||
|
|
||||||
|
@ -666,6 +671,10 @@ declare module '@docusaurus/plugin-content-blog' {
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Promise<Plugin<BlogContent>>;
|
): Promise<Plugin<BlogContent>>;
|
||||||
|
|
||||||
|
export function validateOptions(
|
||||||
|
args: OptionValidationContext<Options | undefined, PluginOptions>,
|
||||||
|
): PluginOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogPostPage' {
|
declare module '@theme/BlogPostPage' {
|
||||||
|
|
|
@ -20,7 +20,11 @@ declare module '@docusaurus/plugin-content-docs' {
|
||||||
TagMetadata,
|
TagMetadata,
|
||||||
TagsPluginOptions,
|
TagsPluginOptions,
|
||||||
} from '@docusaurus/utils';
|
} 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';
|
import type {Overwrite, Required} from 'utility-types';
|
||||||
|
|
||||||
export type Assets = {
|
export type Assets = {
|
||||||
|
@ -559,6 +563,10 @@ declare module '@docusaurus/plugin-content-docs' {
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Promise<Plugin<LoadedContent>>;
|
): Promise<Plugin<LoadedContent>>;
|
||||||
|
|
||||||
|
export function validateOptions(
|
||||||
|
args: OptionValidationContext<Options | undefined, PluginOptions>,
|
||||||
|
): PluginOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/DocItem' {
|
declare module '@theme/DocItem' {
|
||||||
|
|
|
@ -7,7 +7,11 @@
|
||||||
|
|
||||||
declare module '@docusaurus/plugin-content-pages' {
|
declare module '@docusaurus/plugin-content-pages' {
|
||||||
import type {MDXOptions} from '@docusaurus/mdx-loader';
|
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';
|
import type {FrontMatterLastUpdate, LastUpdateData} from '@docusaurus/utils';
|
||||||
|
|
||||||
export type Assets = {
|
export type Assets = {
|
||||||
|
@ -82,6 +86,10 @@ declare module '@docusaurus/plugin-content-pages' {
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Promise<Plugin<LoadedContent | null>>;
|
): Promise<Plugin<LoadedContent | null>>;
|
||||||
|
|
||||||
|
export function validateOptions(
|
||||||
|
args: OptionValidationContext<Options | undefined, PluginOptions>,
|
||||||
|
): PluginOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/MDXPage' {
|
declare module '@theme/MDXPage' {
|
||||||
|
|
|
@ -178,7 +178,6 @@ metastring
|
||||||
metrica
|
metrica
|
||||||
Metrika
|
Metrika
|
||||||
microdata
|
microdata
|
||||||
Microdata
|
|
||||||
Milnes
|
Milnes
|
||||||
mindmap
|
mindmap
|
||||||
Mindmap
|
Mindmap
|
||||||
|
|
|
@ -282,7 +282,7 @@ export default async function createConfigAsync() {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'./src/plugins/changelog/index.js',
|
'./src/plugins/changelog/index.ts',
|
||||||
{
|
{
|
||||||
blogTitle: 'Docusaurus changelog',
|
blogTitle: 'Docusaurus changelog',
|
||||||
blogDescription:
|
blogDescription:
|
||||||
|
|
|
@ -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;
|
|
208
website/src/plugins/changelog/index.ts
Normal file
208
website/src/plugins/changelog/index.ts
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue