Skip to content

Commit

Permalink
feat(v2): option and config validation life cycle method for official…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
anshulrgoyal authored Jun 24, 2020
1 parent ce10646 commit 81d8553
Show file tree
Hide file tree
Showing 18 changed files with 490 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import {RedirectMetadata} from './types';

export const PathnameValidator = Joi.string()
.custom((val) => {
if (!isValidPathname(val)) throw new Error();
else return val;
if (!isValidPathname(val)) {
throw new Error();
} else {
return val;
}
})
.message(
'{{#label}} is not a valid pathname. Pathname should start with / and not contain any domain or query string',
Expand Down
4 changes: 4 additions & 0 deletions packages/docusaurus-plugin-content-blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
"access": "public"
},
"license": "MIT",
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
},
"dependencies": {
"@docusaurus/mdx-loader": "^2.0.0-alpha.58",
"@docusaurus/types": "^2.0.0-alpha.58",
"@docusaurus/utils": "^2.0.0-alpha.58",
"@hapi/joi": "^17.1.1",
"feed": "^4.1.0",
"fs-extra": "^8.1.0",
"globby": "^10.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -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]`;
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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']},
});
});
70 changes: 22 additions & 48 deletions packages/docusaurus-plugin-content-blog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,66 +19,29 @@ import {
BlogItemsToMetadata,
TagsModule,
BlogPaginated,
FeedType,
BlogPost,
} from './types';
import {PluginOptionSchema} from './validation';
import {
LoadContext,
PluginContentLoadedActions,
ConfigureWebpackUtils,
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],
]);
}

Expand Down Expand Up @@ -426,7 +390,7 @@ export default function pluginContentBlog(
},

async postBuild({outDir}: Props) {
if (!options.feedOptions) {
if (!options.feedOptions?.type) {
return;
}

Expand All @@ -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) => {
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
6 changes: 3 additions & 3 deletions packages/docusaurus-plugin-content-blog/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/validation.ts
Original file line number Diff line number Diff line change
@@ -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),
});
2 changes: 1 addition & 1 deletion packages/docusaurus-plugin-content-docs/src/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function assertItem<K extends string>(
): asserts item is Record<K, any> {
const unknownKeys = Object.keys(item).filter(
// @ts-expect-error: key is always string
(key) => !keys.includes(key) && key !== 'type',
(key) => !keys.includes(key as string) && key !== 'type',
);

if (unknownKeys.length) {
Expand Down
4 changes: 4 additions & 0 deletions packages/docusaurus-theme-classic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"license": "MIT",
"dependencies": {
"@hapi/joi": "^17.1.1",
"@mdx-js/mdx": "^1.5.8",
"@mdx-js/react": "^1.5.8",
"clsx": "^1.1.1",
Expand All @@ -27,5 +28,8 @@
},
"engines": {
"node": ">=10.15.1"
},
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
}
}
Loading

0 comments on commit 81d8553

Please sign in to comment.