feat(v2): add option validation for remaining official plugins (#2970)

* feat(v2): add option validation lifecycle method

* chore(v2): add dependencies

* chore(v2): add yup dependency

* feat(v2): add option validation for plugin-content-docs

* chore(v2): add facebook copyright

* refactor(v2): remove unused variable

* chore(v2): add dependencies

* chore(v2): add copyright

* fix(v2): use strict for option validation

* feat(v2): add option validation for plugin-content-pages

* feat(v2): add schema for plugin-google-analytics and plugin-google-gtag

* feat(v2): add option validation for plugin-sitemap

* chore(v2): add dependency for yup

* fix(v2): remove strict to allow normalization

* refactor(v2): refactor validate method

* feat(v2): modify existing tests

* feat(v2): add tests for plugin normalization

* style(v2): use a more descriptive filename for schema

* feat(v2): add normalization tests

* feat(v2): add more tests for option validation

* refactor(v2): remove unused code

* refactor(v2): remove unused code

* refactor(v2): refactor methods and types

* feat(v2): replace Yup with Joi

* fix(v2): fix plugin-content-docs schema

* feat(v2): modify tests for plugin-content-docs

* fix(v2): fix a typo

* refactor(v2): improve tests and refactor code

* feat(v2): support both commonjs and ES modules

* refactor(v2): refactor validateOption method

* style(v2): fix eslint errors and typo in types

* chore(v2): remove unused yup dependency

* style(v2): standardize naming across official plugins

* chore(v2): update test snapshots

* chore(v2): remove obsolete snapshots

* chore(v2): fix a typo and check test

* feat(v2): add validation for new field

* feat(v2): add test for new field
This commit is contained in:
Teik Jun 2020-06-26 21:14:59 +08:00 committed by GitHub
parent 3213955e72
commit 0f59cd1599
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 444 additions and 169 deletions

View file

@ -12,6 +12,7 @@ import commander from 'commander';
import fs from 'fs-extra';
import pluginContentDocs from '../index';
import loadEnv from '../env';
import normalizePluginOptions from './pluginOptionSchema.test';
import {loadContext} from '@docusaurus/core/src/server/index';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
import {RouteConfig} from '@docusaurus/types';
@ -42,9 +43,12 @@ test('site with wrong sidebar file', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = pluginContentDocs(context, {
sidebarPath,
});
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
sidebarPath,
}),
);
await expect(plugin.loadContent()).rejects.toThrowErrorMatchingSnapshot();
});
@ -54,7 +58,7 @@ describe('empty/no docs website', () => {
test('no files in docs folder', async () => {
await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = pluginContentDocs(context, {});
const plugin = pluginContentDocs(context, normalizePluginOptions({}));
const content = await plugin.loadContent();
const {docsMetadata, docsSidebars} = content;
expect(docsMetadata).toMatchInlineSnapshot(`Object {}`);
@ -73,7 +77,12 @@ describe('empty/no docs website', () => {
});
test('docs folder does not exist', async () => {
const plugin = pluginContentDocs(context, {path: '/path/does/not/exist/'});
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
path: '/path/does/not/exist/',
}),
);
const content = await plugin.loadContent();
expect(content).toBeNull();
});
@ -84,11 +93,14 @@ describe('simple website', () => {
const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'sidebars.json');
const pluginPath = 'docs';
const plugin = pluginContentDocs(context, {
path: pluginPath,
sidebarPath,
homePageId: 'hello',
});
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
path: pluginPath,
sidebarPath,
homePageId: 'hello',
}),
);
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
test('extendCli - docsVersion', () => {
@ -215,11 +227,14 @@ describe('versioned website', () => {
const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'sidebars.json');
const routeBasePath = 'docs';
const plugin = pluginContentDocs(context, {
routeBasePath,
sidebarPath,
homePageId: 'hello',
});
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
routeBasePath,
sidebarPath,
homePageId: 'hello',
}),
);
const env = loadEnv(siteDir);
const {docsDir: versionedDir} = env.versioning;
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);

View file

@ -0,0 +1,83 @@
/**
* 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, DEFAULT_OPTIONS} from '../pluginOptionSchema';
export default function normalizePluginOptions(options) {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
}
describe('normalizeDocsPluginOptions', () => {
test('should return default options for undefined user options', async () => {
const {value} = await PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
});
test('should accept correctly defined user options', async () => {
const userOptions = {
path: 'my-docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'my-docs', // URL Route.
homePageId: 'home', // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages.
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
rehypePlugins: [],
showLastUpdateTime: true,
showLastUpdateAuthor: true,
admonitions: {},
excludeNextVersionDocs: true,
};
const {value} = await PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
});
test('should reject bad path inputs', () => {
expect(() => {
normalizePluginOptions({
path: 2,
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
});
test('should reject bad include inputs', () => {
expect(() => {
normalizePluginOptions({
include: '**/*.{md,mdx}',
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
});
test('should reject bad showLastUpdateTime inputs', () => {
expect(() => {
normalizePluginOptions({
showLastUpdateTime: 'true',
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"showLastUpdateTime\\" must be a boolean"`,
);
});
test('should reject bad remarkPlugins input', () => {
expect(() => {
normalizePluginOptions({
remarkPlugins: 'remark-math',
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"remarkPlugins\\" must be an array"`,
);
});
});

View file

@ -24,7 +24,7 @@ describe('loadSidebars', () => {
expect(result).toMatchSnapshot();
});
test('sidebars shortand and longform lead to exact same sidebar', async () => {
test('sidebars shorthand and longform lead to exact same sidebar', async () => {
const sidebarPath1 = path.join(fixtureDir, 'sidebars-category.js');
const sidebarPath2 = path.join(
fixtureDir,

View file

@ -18,7 +18,13 @@ import {
objectWithKeySorted,
aliasedSitePath,
} from '@docusaurus/utils';
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
import {
LoadContext,
Plugin,
RouteConfig,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import createOrder from './order';
import loadSidebars from './sidebars';
@ -47,24 +53,8 @@ import {
import {Configuration} from 'webpack';
import {docsVersion} from './version';
import {VERSIONS_JSON_FILE} from './constants';
const REVERSED_DOCS_HOME_PAGE_ID = '_index';
const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
homePageId: REVERSED_DOCS_HOME_PAGE_ID, // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages.
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
rehypePlugins: [],
showLastUpdateTime: false,
showLastUpdateAuthor: false,
admonitions: {},
excludeNextVersionDocs: false,
};
import {PluginOptionSchema} from './pluginOptionSchema';
import {ValidationError} from '@hapi/joi';
function getFirstDocLinkOfSidebar(
sidebarItems: DocsSidebarItem[],
@ -84,9 +74,8 @@ function getFirstDocLinkOfSidebar(
export default function pluginContentDocs(
context: LoadContext,
opts: Partial<PluginOptions>,
): Plugin<LoadedContent | null> {
const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts};
options: PluginOptions,
): Plugin<LoadedContent | null, typeof PluginOptionSchema> {
const homePageDocsRoutePath =
options.routeBasePath === '' ? '/' : options.routeBasePath;
@ -551,3 +540,14 @@ Available document ids=
},
};
}
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
PluginOptions,
ValidationError
> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -0,0 +1,54 @@
/**
* 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';
import {PluginOptions} from './types';
const REVERSED_DOCS_HOME_PAGE_ID = '_index';
export const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
homePageId: REVERSED_DOCS_HOME_PAGE_ID, // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages.
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
remarkPlugins: [],
rehypePlugins: [],
showLastUpdateTime: false,
showLastUpdateAuthor: false,
admonitions: {},
excludeNextVersionDocs: false,
};
export const PluginOptionSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: Joi.string().uri(),
routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
homePageId: Joi.string().default(DEFAULT_OPTIONS.homePageId),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
sidebarPath: Joi.string().default(DEFAULT_OPTIONS.sidebarPath),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
remarkPlugins: Joi.array()
.items(
Joi.array().items(Joi.function(), Joi.object()).length(2),
Joi.function(),
)
.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: Joi.array()
.items(Joi.string())
.default(DEFAULT_OPTIONS.rehypePlugins),
showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime),
showLastUpdateAuthor: Joi.bool().default(
DEFAULT_OPTIONS.showLastUpdateAuthor,
),
admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions),
excludeNextVersionDocs: Joi.bool().default(
DEFAULT_OPTIONS.excludeNextVersionDocs,
),
});