Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v2): option and config validation life cycle method for official plugins #2943

Merged
merged 25 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
slorber marked this conversation as resolved.
Show resolved Hide resolved

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']},
});
slorber marked this conversation as resolved.
Show resolved Hide resolved
});
66 changes: 18 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,28 @@ import {
BlogItemsToMetadata,
TagsModule,
BlogPaginated,
FeedType,
BlogPost,
} from './types';
import {PluginOptionSchema} from './validation';
import {
LoadContext,
PluginContentLoadedActions,
ConfigureWebpackUtils,
Props,
Plugin,
HtmlTags,
OptionValidationContext,
} 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 +389,7 @@ export default function pluginContentBlog(
},

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

Expand All @@ -436,7 +399,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 +419,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 +471,11 @@ export default function pluginContentBlog(
},
};
}

pluginContentBlog.validateOptions = ({
validate,
options,
}: OptionValidationContext<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: [],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, are these options a typo?

That looks like, please confirm so that I can remove in my current pr

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah no it seems used in practice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied it from docs.https://v2.docusaurus.io/docs/using-plugins/#docusaurusplugin-content-blog

        beforeDefaultRemarkPlugins: [],
        beforeDefaultRehypePlugins: [],
        feedOptions: {
          type: '', // required. 'rss' | 'feed' | 'all'
          title: '', // default to siteConfig.title
          description: '', // default to  `${siteConfig.title} Blog`
          copyright: '',
          language: undefined, // possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
        },

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah no it seems used in practice

Sorry I didn't understand what u mean.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind I thought it was not used but it is

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])),
anshulrgoyal marked this conversation as resolved.
Show resolved Hide resolved
},
),
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