mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-20 20:46:58 +02:00
feat(v2): option and config validation life cycle method for official plugins (#2943)
* add validation for blog plugin * fix wrong default component * fix test and add yup to package.json * remove console.log * add validation for classic theme and code block theme * add yup to packages * remove console.log * fix build * fix logo required * replaced yup with joi * fix test * remove hapi from docusuars core * replace joi with @hapi/joi * fix eslint * fix remark plugin type * change remark plugin validation to match documentation * move schema to it's own file * allow unknown only on outer theme object * fix type for schema type * fix yarn.lock * support both commonjs and ES modules * add docs for new lifecycle method
This commit is contained in:
parent
ce10646606
commit
81d855355e
18 changed files with 490 additions and 63 deletions
|
@ -0,0 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`throw Error in case of invalid feedtype 1`] = `[ValidationError: "feedOptions.type" does not match any of the allowed types]`;
|
||||
|
||||
exports[`throw Error in case of invalid options 1`] = `[ValidationError: "postsPerPage" must be larger than or equal to 1]`;
|
|
@ -9,6 +9,16 @@ import fs from 'fs-extra';
|
|||
import path from 'path';
|
||||
import pluginContentBlog from '../index';
|
||||
import {DocusaurusConfig, LoadContext} from '@docusaurus/types';
|
||||
import {PluginOptionSchema} from '../validation';
|
||||
|
||||
function validateAndNormalize(schema, options) {
|
||||
const {value, error} = schema.validate(options);
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
describe('loadBlog', () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
|
@ -26,11 +36,11 @@ describe('loadBlog', () => {
|
|||
siteConfig,
|
||||
generatedFilesDir,
|
||||
} as LoadContext,
|
||||
{
|
||||
validateAndNormalize(PluginOptionSchema, {
|
||||
path: pluginPath,
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website-1x',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const {blogPosts} = await plugin.loadContent();
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* 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 {PluginOptionSchema, DefaultOptions} from '../validation';
|
||||
|
||||
test('normalize options', () => {
|
||||
const {value} = PluginOptionSchema.validate({});
|
||||
expect(value).toEqual(DefaultOptions);
|
||||
});
|
||||
|
||||
test('validate options', () => {
|
||||
const {value} = PluginOptionSchema.validate({
|
||||
path: 'not_blog',
|
||||
postsPerPage: 5,
|
||||
include: ['api/*', 'docs/*'],
|
||||
routeBasePath: 'not_blog',
|
||||
});
|
||||
expect(value).toEqual({
|
||||
...DefaultOptions,
|
||||
postsPerPage: 5,
|
||||
include: ['api/*', 'docs/*'],
|
||||
routeBasePath: 'not_blog',
|
||||
path: 'not_blog',
|
||||
});
|
||||
});
|
||||
|
||||
test('throw Error in case of invalid options', () => {
|
||||
const {error} = PluginOptionSchema.validate({
|
||||
path: 'not_blog',
|
||||
postsPerPage: -1,
|
||||
include: ['api/*', 'docs/*'],
|
||||
routeBasePath: 'not_blog',
|
||||
});
|
||||
|
||||
expect(error).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('throw Error in case of invalid feedtype', () => {
|
||||
const {error} = PluginOptionSchema.validate({
|
||||
feedOptions: {
|
||||
type: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
expect(error).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('convert all feed type to array with other feed type', () => {
|
||||
const {value} = PluginOptionSchema.validate({
|
||||
feedOptions: {type: 'all'},
|
||||
});
|
||||
expect(value).toEqual({
|
||||
...DefaultOptions,
|
||||
feedOptions: {type: ['rss', 'atom']},
|
||||
});
|
||||
});
|
|
@ -10,6 +10,7 @@ import kebabCase from 'lodash.kebabcase';
|
|||
import path from 'path';
|
||||
import admonitions from 'remark-admonitions';
|
||||
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
|
||||
import {ValidationError} from '@hapi/joi';
|
||||
|
||||
import {
|
||||
PluginOptions,
|
||||
|
@ -18,9 +19,9 @@ import {
|
|||
BlogItemsToMetadata,
|
||||
TagsModule,
|
||||
BlogPaginated,
|
||||
FeedType,
|
||||
BlogPost,
|
||||
} from './types';
|
||||
import {PluginOptionSchema} from './validation';
|
||||
import {
|
||||
LoadContext,
|
||||
PluginContentLoadedActions,
|
||||
|
@ -28,56 +29,19 @@ import {
|
|||
Props,
|
||||
Plugin,
|
||||
HtmlTags,
|
||||
OptionValidationContext,
|
||||
ValidationResult,
|
||||
} from '@docusaurus/types';
|
||||
import {Configuration, Loader} from 'webpack';
|
||||
import {generateBlogFeed, generateBlogPosts} from './blogUtils';
|
||||
|
||||
const DEFAULT_OPTIONS: PluginOptions = {
|
||||
path: 'blog', // Path to data on filesystem, relative to site dir.
|
||||
routeBasePath: 'blog', // URL Route.
|
||||
include: ['*.md', '*.mdx'], // Extensions to include.
|
||||
postsPerPage: 10, // How many posts per page.
|
||||
blogListComponent: '@theme/BlogListPage',
|
||||
blogPostComponent: '@theme/BlogPostPage',
|
||||
blogTagsListComponent: '@theme/BlogTagsListPage',
|
||||
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
|
||||
showReadingTime: true,
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
editUrl: undefined,
|
||||
truncateMarker: /<!--\s*(truncate)\s*-->/, // Regex.
|
||||
admonitions: {},
|
||||
};
|
||||
|
||||
function assertFeedTypes(val: any): asserts val is FeedType {
|
||||
if (typeof val !== 'string' && !['rss', 'atom', 'all'].includes(val)) {
|
||||
throw new Error(
|
||||
`Invalid feedOptions type: ${val}. It must be either 'rss', 'atom', or 'all'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getFeedTypes = (type?: FeedType) => {
|
||||
assertFeedTypes(type);
|
||||
let feedTypes: ('rss' | 'atom')[] = [];
|
||||
|
||||
if (type === 'all') {
|
||||
feedTypes = ['rss', 'atom'];
|
||||
} else {
|
||||
feedTypes.push(type);
|
||||
}
|
||||
return feedTypes;
|
||||
};
|
||||
|
||||
export default function pluginContentBlog(
|
||||
context: LoadContext,
|
||||
opts: Partial<PluginOptions>,
|
||||
): Plugin<BlogContent | null> {
|
||||
const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts};
|
||||
|
||||
options: PluginOptions,
|
||||
): Plugin<BlogContent | null, typeof PluginOptionSchema> {
|
||||
if (options.admonitions) {
|
||||
options.remarkPlugins = options.remarkPlugins.concat([
|
||||
[admonitions, opts.admonitions || {}],
|
||||
[admonitions, options.admonitions],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -426,7 +390,7 @@ export default function pluginContentBlog(
|
|||
},
|
||||
|
||||
async postBuild({outDir}: Props) {
|
||||
if (!options.feedOptions) {
|
||||
if (!options.feedOptions?.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -436,7 +400,7 @@ export default function pluginContentBlog(
|
|||
return;
|
||||
}
|
||||
|
||||
const feedTypes = getFeedTypes(options.feedOptions?.type);
|
||||
const feedTypes = options.feedOptions.type;
|
||||
|
||||
await Promise.all(
|
||||
feedTypes.map(async (feedType) => {
|
||||
|
@ -456,11 +420,10 @@ export default function pluginContentBlog(
|
|||
},
|
||||
|
||||
injectHtmlTags() {
|
||||
if (!options.feedOptions) {
|
||||
if (!options.feedOptions?.type) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const feedTypes = getFeedTypes(options.feedOptions?.type);
|
||||
const feedTypes = options.feedOptions.type;
|
||||
const {
|
||||
siteConfig: {title},
|
||||
baseUrl,
|
||||
|
@ -509,3 +472,14 @@ export default function pluginContentBlog(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function validateOptions({
|
||||
validate,
|
||||
options,
|
||||
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
|
||||
PluginOptions,
|
||||
ValidationError
|
||||
> {
|
||||
const validatedOptions = validate(PluginOptionSchema, options);
|
||||
return validatedOptions;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface DateLink {
|
|||
link: string;
|
||||
}
|
||||
|
||||
export type FeedType = 'rss' | 'atom' | 'all';
|
||||
export type FeedType = 'rss' | 'atom';
|
||||
|
||||
export interface PluginOptions {
|
||||
path: string;
|
||||
|
@ -32,8 +32,8 @@ export interface PluginOptions {
|
|||
rehypePlugins: string[];
|
||||
truncateMarker: RegExp;
|
||||
showReadingTime: boolean;
|
||||
feedOptions?: {
|
||||
type: FeedType;
|
||||
feedOptions: {
|
||||
type: [FeedType];
|
||||
title?: string;
|
||||
description?: string;
|
||||
copyright: string;
|
||||
|
|
80
packages/docusaurus-plugin-content-blog/src/validation.ts
Normal file
80
packages/docusaurus-plugin-content-blog/src/validation.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* 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 * as Joi from '@hapi/joi';
|
||||
|
||||
export const DefaultOptions = {
|
||||
feedOptions: {},
|
||||
beforeDefaultRehypePlugins: [],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
admonitions: {},
|
||||
truncateMarker: /<!--\s*(truncate)\s*-->/,
|
||||
rehypePlugins: [],
|
||||
remarkPlugins: [],
|
||||
showReadingTime: true,
|
||||
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
|
||||
blogTagsListComponent: '@theme/BlogTagsListPage',
|
||||
blogPostComponent: '@theme/BlogPostPage',
|
||||
blogListComponent: '@theme/BlogListPage',
|
||||
postsPerPage: 10,
|
||||
include: ['*.md', '*.mdx'],
|
||||
routeBasePath: 'blog',
|
||||
path: 'blog',
|
||||
};
|
||||
|
||||
export const PluginOptionSchema = Joi.object({
|
||||
path: Joi.string().default(DefaultOptions.path),
|
||||
routeBasePath: Joi.string().default(DefaultOptions.routeBasePath),
|
||||
include: Joi.array().items(Joi.string()).default(DefaultOptions.include),
|
||||
postsPerPage: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.default(DefaultOptions.postsPerPage),
|
||||
blogListComponent: Joi.string().default(DefaultOptions.blogListComponent),
|
||||
blogPostComponent: Joi.string().default(DefaultOptions.blogPostComponent),
|
||||
blogTagsListComponent: Joi.string().default(
|
||||
DefaultOptions.blogTagsListComponent,
|
||||
),
|
||||
blogTagsPostsComponent: Joi.string().default(
|
||||
DefaultOptions.blogTagsPostsComponent,
|
||||
),
|
||||
showReadingTime: Joi.bool().default(DefaultOptions.showReadingTime),
|
||||
remarkPlugins: Joi.array()
|
||||
.items(
|
||||
Joi.alternatives().try(
|
||||
Joi.function(),
|
||||
Joi.array()
|
||||
.items(Joi.function().required(), Joi.object().required())
|
||||
.length(2),
|
||||
),
|
||||
)
|
||||
.default(DefaultOptions.remarkPlugins),
|
||||
rehypePlugins: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default(DefaultOptions.rehypePlugins),
|
||||
editUrl: Joi.string().uri(),
|
||||
truncateMarker: Joi.object().default(DefaultOptions.truncateMarker),
|
||||
admonitions: Joi.object().default(DefaultOptions.admonitions),
|
||||
beforeDefaultRemarkPlugins: Joi.array()
|
||||
.items(Joi.object())
|
||||
.default(DefaultOptions.beforeDefaultRemarkPlugins),
|
||||
beforeDefaultRehypePlugins: Joi.array()
|
||||
.items(Joi.object())
|
||||
.default(DefaultOptions.beforeDefaultRehypePlugins),
|
||||
feedOptions: Joi.object({
|
||||
type: Joi.alternatives().conditional(
|
||||
Joi.string().equal('all', 'rss', 'atom'),
|
||||
{
|
||||
then: Joi.custom((val) => (val === 'all' ? ['rss', 'atom'] : [val])),
|
||||
},
|
||||
),
|
||||
title: Joi.string(),
|
||||
description: Joi.string(),
|
||||
copyright: Joi.string(),
|
||||
language: Joi.string(),
|
||||
}).default(DefaultOptions.feedOptions),
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue