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:
Anshul Goyal 2020-06-24 23:38:16 +05:30 committed by GitHub
parent ce10646606
commit 81d855355e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 490 additions and 63 deletions

View file

@ -11,8 +11,11 @@ import {RedirectMetadata} from './types';
export const PathnameValidator = Joi.string() export const PathnameValidator = Joi.string()
.custom((val) => { .custom((val) => {
if (!isValidPathname(val)) throw new Error(); if (!isValidPathname(val)) {
else return val; throw new Error();
} else {
return val;
}
}) })
.message( .message(
'{{#label}} is not a valid pathname. Pathname should start with / and not contain any domain or query string', '{{#label}} is not a valid pathname. Pathname should start with / and not contain any domain or query string',

View file

@ -11,10 +11,14 @@
"access": "public" "access": "public"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
},
"dependencies": { "dependencies": {
"@docusaurus/mdx-loader": "^2.0.0-alpha.58", "@docusaurus/mdx-loader": "^2.0.0-alpha.58",
"@docusaurus/types": "^2.0.0-alpha.58", "@docusaurus/types": "^2.0.0-alpha.58",
"@docusaurus/utils": "^2.0.0-alpha.58", "@docusaurus/utils": "^2.0.0-alpha.58",
"@hapi/joi": "^17.1.1",
"feed": "^4.1.0", "feed": "^4.1.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"globby": "^10.0.1", "globby": "^10.0.1",

View file

@ -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]`;

View file

@ -9,6 +9,16 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import pluginContentBlog from '../index'; import pluginContentBlog from '../index';
import {DocusaurusConfig, LoadContext} from '@docusaurus/types'; 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', () => { describe('loadBlog', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
@ -26,11 +36,11 @@ describe('loadBlog', () => {
siteConfig, siteConfig,
generatedFilesDir, generatedFilesDir,
} as LoadContext, } as LoadContext,
{ validateAndNormalize(PluginOptionSchema, {
path: pluginPath, path: pluginPath,
editUrl: editUrl:
'https://github.com/facebook/docusaurus/edit/master/website-1x', 'https://github.com/facebook/docusaurus/edit/master/website-1x',
}, }),
); );
const {blogPosts} = await plugin.loadContent(); const {blogPosts} = await plugin.loadContent();

View file

@ -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']},
});
});

View file

@ -10,6 +10,7 @@ import kebabCase from 'lodash.kebabcase';
import path from 'path'; import path from 'path';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils'; import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
import {ValidationError} from '@hapi/joi';
import { import {
PluginOptions, PluginOptions,
@ -18,9 +19,9 @@ import {
BlogItemsToMetadata, BlogItemsToMetadata,
TagsModule, TagsModule,
BlogPaginated, BlogPaginated,
FeedType,
BlogPost, BlogPost,
} from './types'; } from './types';
import {PluginOptionSchema} from './validation';
import { import {
LoadContext, LoadContext,
PluginContentLoadedActions, PluginContentLoadedActions,
@ -28,56 +29,19 @@ import {
Props, Props,
Plugin, Plugin,
HtmlTags, HtmlTags,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types'; } from '@docusaurus/types';
import {Configuration, Loader} from 'webpack'; import {Configuration, Loader} from 'webpack';
import {generateBlogFeed, generateBlogPosts} from './blogUtils'; 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( export default function pluginContentBlog(
context: LoadContext, context: LoadContext,
opts: Partial<PluginOptions>, options: PluginOptions,
): Plugin<BlogContent | null> { ): Plugin<BlogContent | null, typeof PluginOptionSchema> {
const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts};
if (options.admonitions) { if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([ options.remarkPlugins = options.remarkPlugins.concat([
[admonitions, opts.admonitions || {}], [admonitions, options.admonitions],
]); ]);
} }
@ -426,7 +390,7 @@ export default function pluginContentBlog(
}, },
async postBuild({outDir}: Props) { async postBuild({outDir}: Props) {
if (!options.feedOptions) { if (!options.feedOptions?.type) {
return; return;
} }
@ -436,7 +400,7 @@ export default function pluginContentBlog(
return; return;
} }
const feedTypes = getFeedTypes(options.feedOptions?.type); const feedTypes = options.feedOptions.type;
await Promise.all( await Promise.all(
feedTypes.map(async (feedType) => { feedTypes.map(async (feedType) => {
@ -456,11 +420,10 @@ export default function pluginContentBlog(
}, },
injectHtmlTags() { injectHtmlTags() {
if (!options.feedOptions) { if (!options.feedOptions?.type) {
return {}; return {};
} }
const feedTypes = options.feedOptions.type;
const feedTypes = getFeedTypes(options.feedOptions?.type);
const { const {
siteConfig: {title}, siteConfig: {title},
baseUrl, 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;
}

View file

@ -17,7 +17,7 @@ export interface DateLink {
link: string; link: string;
} }
export type FeedType = 'rss' | 'atom' | 'all'; export type FeedType = 'rss' | 'atom';
export interface PluginOptions { export interface PluginOptions {
path: string; path: string;
@ -32,8 +32,8 @@ export interface PluginOptions {
rehypePlugins: string[]; rehypePlugins: string[];
truncateMarker: RegExp; truncateMarker: RegExp;
showReadingTime: boolean; showReadingTime: boolean;
feedOptions?: { feedOptions: {
type: FeedType; type: [FeedType];
title?: string; title?: string;
description?: string; description?: string;
copyright: string; copyright: string;

View 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),
});

View file

@ -51,7 +51,7 @@ function assertItem<K extends string>(
): asserts item is Record<K, any> { ): asserts item is Record<K, any> {
const unknownKeys = Object.keys(item).filter( const unknownKeys = Object.keys(item).filter(
// @ts-expect-error: key is always string // @ts-expect-error: key is always string
(key) => !keys.includes(key) && key !== 'type', (key) => !keys.includes(key as string) && key !== 'type',
); );
if (unknownKeys.length) { if (unknownKeys.length) {

View file

@ -8,6 +8,7 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hapi/joi": "^17.1.1",
"@mdx-js/mdx": "^1.5.8", "@mdx-js/mdx": "^1.5.8",
"@mdx-js/react": "^1.5.8", "@mdx-js/react": "^1.5.8",
"clsx": "^1.1.1", "clsx": "^1.1.1",
@ -27,5 +28,8 @@
}, },
"engines": { "engines": {
"node": ">=10.15.1" "node": ">=10.15.1"
},
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
} }
} }

View file

@ -7,6 +7,7 @@
const path = require('path'); const path = require('path');
const Module = require('module'); const Module = require('module');
const Joi = require('@hapi/joi');
const createRequire = Module.createRequire || Module.createRequireFromPath; const createRequire = Module.createRequire || Module.createRequireFromPath;
const requireFromDocusaurusCore = createRequire( const requireFromDocusaurusCore = createRequire(
@ -109,3 +110,71 @@ module.exports = function (context, options) {
}, },
}; };
}; };
const NavbarLinkSchema = Joi.object({
items: Joi.array().optional().items(Joi.link('#navbarLinkSchema')),
to: Joi.string(),
href: Joi.string().uri(),
prependBaseUrlToHref: Joi.bool().default(true),
label: Joi.string(),
position: Joi.string().equal('left', 'right').default('left'),
activeBasePath: Joi.string(),
activeBaseRegex: Joi.string(),
className: Joi.string(),
'aria-label': Joi.string(),
})
.xor('href', 'to')
.id('navbarLinkSchema');
const ThemeConfigSchema = Joi.object({
disableDarkMode: Joi.bool().default(false),
image: Joi.string(),
announcementBar: Joi.object({
id: Joi.string(),
content: Joi.string(),
backgroundColor: Joi.string().default('#fff'),
textColor: Joi.string().default('#000'),
}).optional(),
navbar: Joi.object({
hideOnScroll: Joi.bool().default(false),
links: Joi.array().items(NavbarLinkSchema),
title: Joi.string().required(),
logo: Joi.object({
alt: Joi.string(),
src: Joi.string().required(),
srcDark: Joi.string(),
href: Joi.string(),
target: Joi.string(),
}),
}),
footer: Joi.object({
style: Joi.string().equal('dark', 'light').default('light'),
logo: Joi.object({
alt: Joi.string(),
src: Joi.string(),
href: Joi.string(),
}),
copyright: Joi.string(),
links: Joi.array().items(
Joi.object({
title: Joi.string().required(),
items: Joi.array().items(
Joi.object({
to: Joi.string(),
href: Joi.string().uri(),
html: Joi.string(),
label: Joi.string(),
})
.xor('to', 'href', 'html')
.with('to', 'label')
.with('href', 'label')
.nand('html', 'label'),
),
}),
),
}),
});
module.exports.validateThemeConfig = ({validate, themeConfig}) => {
return validate(ThemeConfigSchema, themeConfig);
};

View file

@ -8,11 +8,13 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hapi/joi": "^17.1.1",
"@philpl/buble": "^0.19.7", "@philpl/buble": "^0.19.7",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"parse-numeric-range": "^0.0.2", "parse-numeric-range": "^0.0.2",
"prism-react-renderer": "^1.1.0", "prism-react-renderer": "^1.1.0",
"react-live": "^2.2.1" "react-live": "^2.2.1",
"yup": "^0.29.1"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0", "@docusaurus/core": "^2.0.0",
@ -21,5 +23,8 @@
}, },
"engines": { "engines": {
"node": ">=10.15.1" "node": ">=10.15.1"
},
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
} }
} }

View file

@ -6,6 +6,7 @@
*/ */
const path = require('path'); const path = require('path');
const Joi = require('@hapi/joi');
module.exports = function () { module.exports = function () {
return { return {
@ -29,3 +30,21 @@ module.exports = function () {
}, },
}; };
}; };
const ThemeConfigSchema = Joi.object({
prism: Joi.object({
theme: Joi.object({
plain: Joi.alternatives().try(Joi.array(), Joi.object()).required(),
styles: Joi.alternatives().try(Joi.array(), Joi.object()).required(),
}),
darkTheme: Joi.object({
plain: Joi.alternatives().try(Joi.array(), Joi.object()).required(),
styles: Joi.alternatives().try(Joi.array(), Joi.object()).required(),
}),
defaultLanguage: Joi.string(),
}),
});
module.exports.validateThemeConfig = ({validate, themeConfig}) => {
return validate(ThemeConfigSchema, themeConfig);
};

View file

@ -91,7 +91,7 @@ export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
export interface Props extends LoadContext, InjectedHtmlTags { export interface Props extends LoadContext, InjectedHtmlTags {
routesPaths: string[]; routesPaths: string[];
plugins: Plugin<unknown>[]; plugins: Plugin<any, unknown>[];
} }
export interface PluginContentLoadedActions { export interface PluginContentLoadedActions {
@ -99,9 +99,11 @@ export interface PluginContentLoadedActions {
createData(name: string, data: any): Promise<string>; createData(name: string, data: any): Promise<string>;
} }
export interface Plugin<T> { export interface Plugin<T, U = unknown> {
name: string; name: string;
loadContent?(): Promise<T>; loadContent?(): Promise<T>;
validateOptions?(): ValidationResult<U>;
validateThemeConfig?(): ValidationResult<any>;
contentLoaded?({ contentLoaded?({
content, content,
actions, actions,
@ -201,3 +203,28 @@ interface HtmlTagObject {
*/ */
innerHTML?: string; innerHTML?: string;
} }
export interface ValidationResult<T, E extends Error = Error> {
error?: E;
value: T;
}
export type Validate<T, E extends Error = Error> = (
validationSchrema: ValidationSchema<T>,
options: Partial<T>,
) => ValidationResult<T, E>;
export interface OptionValidationContext<T, E extends Error = Error> {
validate: Validate<T, E>;
options: Partial<T>;
}
export interface ThemeConfigValidationContext<T, E extends Error = Error> {
validate: Validate<T, E>;
themeConfig: Partial<T>;
}
export interface ValidationSchema<T> {
validate(options: Partial<T>, opt: object): ValidationResult<T>;
unknown(): ValidationSchema<T>;
}

View file

@ -8,9 +8,35 @@
import Module from 'module'; import Module from 'module';
import {join} from 'path'; import {join} from 'path';
import importFresh from 'import-fresh'; import importFresh from 'import-fresh';
import {LoadContext, Plugin, PluginConfig} from '@docusaurus/types'; import {
LoadContext,
Plugin,
PluginConfig,
ValidationSchema,
} from '@docusaurus/types';
import {CONFIG_FILE_NAME} from '../../constants'; import {CONFIG_FILE_NAME} from '../../constants';
function validate<T>(schema: ValidationSchema<T>, options: Partial<T>) {
const {error, value} = schema.validate(options, {
convert: false,
});
if (error) {
throw error;
}
return value;
}
function validateAndStrip<T>(schema: ValidationSchema<T>, options: Partial<T>) {
const {error, value} = schema.unknown().validate(options, {
convert: false,
});
if (error) {
throw error;
}
return value;
}
export default function initPlugins({ export default function initPlugins({
pluginConfigs, pluginConfigs,
context, context,
@ -49,9 +75,34 @@ export default function initPlugins({
const pluginModule: any = importFresh( const pluginModule: any = importFresh(
pluginRequire.resolve(pluginModuleImport), pluginRequire.resolve(pluginModuleImport),
); );
return (pluginModule.default || pluginModule)(context, pluginOptions);
const plugin = pluginModule.default || pluginModule;
// support both commonjs and ES modules
const validateOptions =
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;
if (validateOptions) {
const options = validateOptions({
validate,
options: pluginOptions,
});
pluginOptions = options;
}
// support both commonjs and ES modules
const validateThemeConfig =
pluginModule.default?.validateThemeConfig ??
pluginModule.validateThemeConfig;
if (validateThemeConfig) {
validateThemeConfig({
validate: validateAndStrip,
themeConfig: context.siteConfig.themeConfig,
});
}
return plugin(context, pluginOptions);
}) })
.filter(Boolean); .filter(Boolean);
return plugins; return plugins;
} }

View file

@ -11,6 +11,104 @@ This section is a work in progress.
Lifecycle APIs are shared by Themes and Plugins. Lifecycle APIs are shared by Themes and Plugins.
## `validateOptions({options,validate})`
Return validated and normalized options for the plugin. This method is called before the plugin is initialized.You must return options since the returned options will be passed to plugin during intialization.
### `options`
`validateOptions` is called with `options` passed to plugin for validation and normalization.
### `validate`
`validateOptions` is called with `validate` function which takes a **[Joi](https://www.npmjs.com/package/@hapi/joi)** schema and options as argument, returns validated and normalized options. `validate` will automatically handle error and validation config.
:::tip
[Joi](https://www.npmjs.com/package/@hapi/joi) is recommended for validation and normalization of options.
:::
If you don't use **[Joi](https://www.npmjs.com/package/@hapi/joi)** for validation you can throw an Error in case of invalid options and return options in case of success.
```js {8-11} title="my-plugin/src/index.js"
module.exports = function (context, options) {
return {
name: 'docusaurus-plugin',
// rest of methods
};
};
module.exports.validateOptions = ({options, validate}) => {
const validatedOptions = validate(myValidationSchema, options);
return validationOptions;
};
```
You can also use ES modules style exports.
```ts {8-11} title="my-plugin/src/index.ts"
export default function (context, options) {
return {
name: 'docusaurus-plugin',
// rest of methods
};
}
export function validateOptions({options, validate}) {
const validatedOptions = validate(myValidationSchema, options);
return validationOptions;
}
```
## `validateThemeConfig({themeConfig,validate})`
Validate `themeConfig` for the plugins and theme. This method is called before the plugin is initialized.
### `themeConfig`
`validateThemeConfig` is called with `themeConfig` provided in `docusaurus.config.js` for validation.
### `validate`
`validateThemeConfig` is called with `validate` function which takes a **[Joi](https://www.npmjs.com/package/@hapi/joi)** schema and `themeConfig` as argument, returns validated and normalized options. `validate` will automatically handle error and validation config.
:::tip
[Joi](https://www.npmjs.com/package/@hapi/joi) is recommended for validation and normalization of options.
:::
If you don't use **[Joi](https://www.npmjs.com/package/@hapi/joi)** for validation you can throw an Error in case of invalid options.
```js {8-11} title="my-theme/src/index.js"
module.exports = function (context, options) {
return {
name: 'docusaurus-plugin',
// rest of methods
};
};
module.exports.validateThemeConfig = ({themeConfig, validate}) => {
validate(myValidationSchema, options);
};
```
You can also use ES modules style exports.
```ts {8-11} title="my-theme/src/index.ts"
export default function (context, options) {
return {
name: 'docusaurus-plugin',
// rest of methods
};
}
export function validateThemeConfig({themeConfig, validate}) {
validate(myValidationSchema, options);
}
```
## `getPathsToWatch()` ## `getPathsToWatch()`
Specifies the paths to watch for plugins and themes. The paths are watched by the dev server so that the plugin lifecycles are reloaded when contents in the watched paths change. Note that the plugins and themes modules are initially called with `context` and `options` from Node, which you may use to find the necessary directory information about the site. Specifies the paths to watch for plugins and themes. The paths are watched by the dev server so that the plugin lifecycles are reloaded when contents in the watched paths change. Note that the plugins and themes modules are initially called with `context` and `options` from Node, which you may use to find the necessary directory information about the site.

View file

@ -172,6 +172,11 @@ There are two lifecycle methods that are essential to theme implementation:
- [`getThemePath()`](lifecycle-apis.md#getthemepath) - [`getThemePath()`](lifecycle-apis.md#getthemepath)
- [`getClientModules()`](lifecycle-apis.md#getclientmodules) - [`getClientModules()`](lifecycle-apis.md#getclientmodules)
These lifecycle method are not essential but recommended:
- [`validateThemeConfig({themeConfig,validate})`](lifecycle-apis.md#validatethemeconfigthemeconfigvalidate)
- [`validateOptions({options,validate})`](lifecycle-apis.md#validateoptionsoptionsvalidate)
<!-- <!--
Outline Outline

View file

@ -19119,6 +19119,19 @@ yup@^0.29.0:
synchronous-promise "^2.0.10" synchronous-promise "^2.0.10"
toposort "^2.0.2" toposort "^2.0.2"
yup@^0.29.1:
version "0.29.1"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.1.tgz#35d25aab470a0c3950f66040ba0ff4b1b6efe0d9"
integrity sha512-U7mPIbgfQWI6M3hZCJdGFrr+U0laG28FxMAKIgNvgl7OtyYuUoc4uy9qCWYHZjh49b8T7Ug8NNDdiMIEytcXrQ==
dependencies:
"@babel/runtime" "^7.9.6"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.10"
toposort "^2.0.2"
zepto@^1.2.0: zepto@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98" resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98"