diff --git a/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts b/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts index d8da9403d3..16d67f1df0 100644 --- a/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts +++ b/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts @@ -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', diff --git a/packages/docusaurus-plugin-content-blog/package.json b/packages/docusaurus-plugin-content-blog/package.json index 438be5766b..e5d0912c56 100644 --- a/packages/docusaurus-plugin-content-blog/package.json +++ b/packages/docusaurus-plugin-content-blog/package.json @@ -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", diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/validation.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/validation.test.ts.snap new file mode 100644 index 0000000000..13f5cee4b5 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/validation.test.ts.snap @@ -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]`; diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index 44a361660b..98e5dfb67c 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -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(); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/validation.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/validation.test.ts new file mode 100644 index 0000000000..7e3085cdec --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/validation.test.ts @@ -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']}, + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 0d9537e8a6..96c0a9cb57 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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: //, // 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, -): Plugin { - const options: PluginOptions = {...DEFAULT_OPTIONS, ...opts}; - + options: PluginOptions, +): Plugin { 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): ValidationResult< + PluginOptions, + ValidationError +> { + const validatedOptions = validate(PluginOptionSchema, options); + return validatedOptions; +} diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index a556b98385..80e7a13cd8 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -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; diff --git a/packages/docusaurus-plugin-content-blog/src/validation.ts b/packages/docusaurus-plugin-content-blog/src/validation.ts new file mode 100644 index 0000000000..523864a976 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/validation.ts @@ -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: //, + 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), +}); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts index 13542accf7..7afb576e55 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts @@ -51,7 +51,7 @@ function assertItem( ): asserts item is Record { 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) { diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index adc547a420..16d12753b8 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -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", @@ -27,5 +28,8 @@ }, "engines": { "node": ">=10.15.1" + }, + "devDependencies": { + "@types/hapi__joi": "^17.1.2" } } diff --git a/packages/docusaurus-theme-classic/src/index.js b/packages/docusaurus-theme-classic/src/index.js index cbe3e4c914..fa62991c27 100644 --- a/packages/docusaurus-theme-classic/src/index.js +++ b/packages/docusaurus-theme-classic/src/index.js @@ -7,6 +7,7 @@ const path = require('path'); const Module = require('module'); +const Joi = require('@hapi/joi'); const createRequire = Module.createRequire || Module.createRequireFromPath; 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); +}; diff --git a/packages/docusaurus-theme-live-codeblock/package.json b/packages/docusaurus-theme-live-codeblock/package.json index b1a983ac74..5ea8d383ab 100644 --- a/packages/docusaurus-theme-live-codeblock/package.json +++ b/packages/docusaurus-theme-live-codeblock/package.json @@ -8,11 +8,13 @@ }, "license": "MIT", "dependencies": { + "@hapi/joi": "^17.1.1", "@philpl/buble": "^0.19.7", "clsx": "^1.1.1", "parse-numeric-range": "^0.0.2", "prism-react-renderer": "^1.1.0", - "react-live": "^2.2.1" + "react-live": "^2.2.1", + "yup": "^0.29.1" }, "peerDependencies": { "@docusaurus/core": "^2.0.0", @@ -21,5 +23,8 @@ }, "engines": { "node": ">=10.15.1" + }, + "devDependencies": { + "@types/hapi__joi": "^17.1.2" } } diff --git a/packages/docusaurus-theme-live-codeblock/src/index.js b/packages/docusaurus-theme-live-codeblock/src/index.js index fb41c91241..b922baf8c7 100644 --- a/packages/docusaurus-theme-live-codeblock/src/index.js +++ b/packages/docusaurus-theme-live-codeblock/src/index.js @@ -6,6 +6,7 @@ */ const path = require('path'); +const Joi = require('@hapi/joi'); module.exports = function () { 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); +}; diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index b9d29faad2..1a2365914c 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -91,7 +91,7 @@ export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[]; export interface Props extends LoadContext, InjectedHtmlTags { routesPaths: string[]; - plugins: Plugin[]; + plugins: Plugin[]; } export interface PluginContentLoadedActions { @@ -99,9 +99,11 @@ export interface PluginContentLoadedActions { createData(name: string, data: any): Promise; } -export interface Plugin { +export interface Plugin { name: string; loadContent?(): Promise; + validateOptions?(): ValidationResult; + validateThemeConfig?(): ValidationResult; contentLoaded?({ content, actions, @@ -201,3 +203,28 @@ interface HtmlTagObject { */ innerHTML?: string; } + +export interface ValidationResult { + error?: E; + value: T; +} + +export type Validate = ( + validationSchrema: ValidationSchema, + options: Partial, +) => ValidationResult; + +export interface OptionValidationContext { + validate: Validate; + options: Partial; +} + +export interface ThemeConfigValidationContext { + validate: Validate; + themeConfig: Partial; +} + +export interface ValidationSchema { + validate(options: Partial, opt: object): ValidationResult; + unknown(): ValidationSchema; +} diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts index 2c28902af0..0cde6e9f16 100644 --- a/packages/docusaurus/src/server/plugins/init.ts +++ b/packages/docusaurus/src/server/plugins/init.ts @@ -8,9 +8,35 @@ import Module from 'module'; import {join} from 'path'; 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'; +function validate(schema: ValidationSchema, options: Partial) { + const {error, value} = schema.validate(options, { + convert: false, + }); + if (error) { + throw error; + } + return value; +} + +function validateAndStrip(schema: ValidationSchema, options: Partial) { + const {error, value} = schema.unknown().validate(options, { + convert: false, + }); + + if (error) { + throw error; + } + return value; +} + export default function initPlugins({ pluginConfigs, context, @@ -49,9 +75,34 @@ export default function initPlugins({ const pluginModule: any = importFresh( 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); - return plugins; } diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index 6c093155f0..14f42c598a 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -11,6 +11,104 @@ This section is a work in progress. 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()` 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. diff --git a/website/docs/using-themes.md b/website/docs/using-themes.md index 56e706ab9f..5f66df46ca 100644 --- a/website/docs/using-themes.md +++ b/website/docs/using-themes.md @@ -172,6 +172,11 @@ There are two lifecycle methods that are essential to theme implementation: - [`getThemePath()`](lifecycle-apis.md#getthemepath) - [`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) +