refactor: unify how validateOptions is handled (#6961)

* refactor: unify how validateOptions is handled

* fix types

* fix again
This commit is contained in:
Joshua Chen 2022-03-22 19:40:56 +08:00 committed by GitHub
parent 44107fb879
commit 6e2eb44964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 542 additions and 540 deletions

View file

@ -15,11 +15,14 @@ function testValidate(options: Options) {
describe('normalizePluginOptions', () => { describe('normalizePluginOptions', () => {
it('returns default options for undefined user options', () => { it('returns default options for undefined user options', () => {
expect(testValidate(undefined)).toEqual(DEFAULT_OPTIONS); expect(testValidate(undefined)).toEqual({
...DEFAULT_OPTIONS,
id: 'default',
});
}); });
it('returns default options for empty user options', () => { it('returns default options for empty user options', () => {
expect(testValidate(undefined)).toEqual(DEFAULT_OPTIONS); expect(testValidate({})).toEqual({...DEFAULT_OPTIONS, id: 'default'});
}); });
it('overrides one default options with valid user options', () => { it('overrides one default options with valid user options', () => {

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {LoadContext, Plugin, Props} from '@docusaurus/types'; import type {LoadContext, Plugin} from '@docusaurus/types';
import type {PluginContext, RedirectMetadata} from './types'; import type {PluginContext, RedirectMetadata} from './types';
import type {PluginOptions} from '@docusaurus/plugin-client-redirects'; import type {PluginOptions} from '@docusaurus/plugin-client-redirects';
@ -24,7 +24,7 @@ export default function pluginClientRedirectsPages(
return { return {
name: 'docusaurus-plugin-client-redirects', name: 'docusaurus-plugin-client-redirects',
async postBuild(props: Props) { async postBuild(props) {
const pluginContext: PluginContext = { const pluginContext: PluginContext = {
relativeRoutesPaths: props.routesPaths.map( relativeRoutesPaths: props.routesPaths.map(
(path) => `${addLeadingSlash(removePrefix(path, props.baseUrl))}`, (path) => `${addLeadingSlash(removePrefix(path, props.baseUrl))}`,

View file

@ -7,12 +7,10 @@
import type { import type {
PluginOptions, PluginOptions,
Options,
RedirectOption, RedirectOption,
} from '@docusaurus/plugin-client-redirects'; } from '@docusaurus/plugin-client-redirects';
import type { import type {OptionValidationContext} from '@docusaurus/types';
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import {Joi, PathnameSchema} from '@docusaurus/utils-validation'; import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
export const DEFAULT_OPTIONS: Partial<PluginOptions> = { export const DEFAULT_OPTIONS: Partial<PluginOptions> = {
@ -47,6 +45,6 @@ const UserOptionsSchema = Joi.object<PluginOptions>({
export function validateOptions({ export function validateOptions({
validate, validate,
options: userOptions, options: userOptions,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> { }: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(UserOptionsSchema, userOptions); return validate(UserOptionsSchema, userOptions);
} }

View file

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateOptions throws Error in case of invalid feed type 1`] = `"\\"feedOptions.type\\" does not match any of the allowed types"`;
exports[`validateOptions throws Error in case of invalid options 1`] = `"\\"postsPerPage\\" must be greater than or equal to 1"`;

View file

@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`blog plugin options schema throws Error in case of invalid feed type 1`] = `[ValidationError: "feedOptions.type" does not match any of the allowed types]`;
exports[`blog plugin options schema throws Error in case of invalid options 1`] = `[ValidationError: "postsPerPage" must be greater than or equal to 1]`;

View file

@ -11,7 +11,7 @@ import fs from 'fs-extra';
import {createBlogFeedFiles} from '../feed'; import {createBlogFeedFiles} from '../feed';
import type {LoadContext, I18n} from '@docusaurus/types'; import type {LoadContext, I18n} from '@docusaurus/types';
import type {BlogContentPaths} from '../types'; import type {BlogContentPaths} from '../types';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema'; import {DEFAULT_OPTIONS} from '../options';
import {generateBlogPosts} from '../blogUtils'; import {generateBlogPosts} from '../blogUtils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog'; import type {PluginOptions} from '@docusaurus/plugin-content-blog';

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {validateBlogPostFrontMatter} from '../blogFrontMatter'; import {validateBlogPostFrontMatter} from '../frontMatter';
import escapeStringRegexp from 'escape-string-regexp'; import escapeStringRegexp from 'escape-string-regexp';
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog'; import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';

View file

@ -9,9 +9,9 @@ import {jest} from '@jest/globals';
import path from 'path'; import path from 'path';
import pluginContentBlog from '../index'; import pluginContentBlog from '../index';
import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types'; import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
import {PluginOptionSchema} from '../pluginOptionSchema'; import {validateOptions} from '../options';
import type {BlogPost} from '../types'; import type {BlogPost} from '../types';
import type {Joi} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {posixPath, getFileCommitDate} from '@docusaurus/utils'; import {posixPath, getFileCommitDate} from '@docusaurus/utils';
import type { import type {
PluginOptions, PluginOptions,
@ -47,18 +47,6 @@ function getI18n(locale: string): I18n {
const DefaultI18N: I18n = getI18n('en'); const DefaultI18N: I18n = getI18n('en');
function validateAndNormalize(
schema: Joi.ObjectSchema,
options: Partial<PluginOptions>,
) {
const {value, error} = schema.validate(options);
if (error) {
throw error;
} else {
return value;
}
}
const PluginPath = 'blog'; const PluginPath = 'blog';
const BaseEditUrl = 'https://baseEditUrl.com/edit'; const BaseEditUrl = 'https://baseEditUrl.com/edit';
@ -81,11 +69,14 @@ const getPlugin = async (
generatedFilesDir, generatedFilesDir,
i18n, i18n,
} as LoadContext, } as LoadContext,
validateAndNormalize(PluginOptionSchema, { validateOptions({
path: PluginPath, validate: normalizePluginOptions,
editUrl: BaseEditUrl, options: {
...pluginOptions, path: PluginPath,
}), editUrl: BaseEditUrl,
...pluginOptions,
},
}) as PluginOptions,
); );
}; };

View file

@ -0,0 +1,160 @@
/**
* 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 {validateOptions, DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import type {Options} from '@docusaurus/plugin-content-blog';
function testValidate(options: Options) {
return validateOptions({validate: normalizePluginOptions, options});
}
// the type of remark/rehype plugins can be either function, object or array
const markdownPluginsFunctionStub = () => {};
const markdownPluginsObjectStub = {};
const defaultOptions = {...DEFAULT_OPTIONS, id: 'default'};
describe('validateOptions', () => {
it('returns default options for undefined user options', () => {
expect(testValidate(undefined)).toEqual(defaultOptions);
});
it('returns default options for empty user options', () => {
expect(testValidate({})).toEqual(defaultOptions);
});
it('accepts correctly defined user options', () => {
const userOptions = {
...defaultOptions,
feedOptions: {type: 'rss' as const, title: 'myTitle'},
path: 'not_blog',
routeBasePath: 'myBlog',
postsPerPage: 5,
include: ['api/*', 'docs/*'],
};
expect(testValidate(userOptions)).toEqual({
...userOptions,
feedOptions: {type: ['rss'], title: 'myTitle', copyright: ''},
});
});
it('accepts valid user options', () => {
const userOptions = {
...defaultOptions,
routeBasePath: 'myBlog',
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [markdownPluginsFunctionStub],
remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]],
rehypePlugins: [
markdownPluginsObjectStub,
[markdownPluginsFunctionStub, {option1: '42'}],
],
};
expect(testValidate(userOptions)).toEqual(userOptions);
});
it('throws Error in case of invalid options', () => {
expect(() =>
testValidate({
path: 'not_blog',
postsPerPage: -1,
include: ['api/*', 'docs/*'],
routeBasePath: 'not_blog',
}),
).toThrowErrorMatchingSnapshot();
});
it('throws Error in case of invalid feed type', () => {
expect(() =>
testValidate({
feedOptions: {
type: 'none',
},
}),
).toThrowErrorMatchingSnapshot();
});
it('converts all feed type to array with other feed type', () => {
expect(
testValidate({
feedOptions: {type: 'all'},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: ['rss', 'atom', 'json'], copyright: ''},
});
});
it('accepts null type and return same', () => {
expect(
testValidate({
feedOptions: {type: null},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: null},
});
});
it('contains array with rss + atom for missing feed type', () => {
expect(
testValidate({
feedOptions: {},
}),
).toEqual(defaultOptions);
});
it('has array with rss + atom, title for missing feed type', () => {
expect(
testValidate({
feedOptions: {title: 'title'},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: ['rss', 'atom'], title: 'title', copyright: ''},
});
});
it('accepts 0 sidebar count', () => {
const userOptions = {blogSidebarCount: 0};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('accepts "ALL" sidebar count', () => {
const userOptions = {blogSidebarCount: 'ALL' as const};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('rejects "abcdef" sidebar count', () => {
const userOptions = {blogSidebarCount: 'abcdef'};
expect(() => testValidate(userOptions)).toThrowErrorMatchingInlineSnapshot(
`"\\"blogSidebarCount\\" must be one of [ALL, number]"`,
);
});
it('accepts "all posts" sidebar title', () => {
const userOptions = {blogSidebarTitle: 'all posts'};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('rejects 42 sidebar title', () => {
const userOptions = {blogSidebarTitle: 42};
expect(() => testValidate(userOptions)).toThrowErrorMatchingInlineSnapshot(
`"\\"blogSidebarTitle\\" must be a string"`,
);
});
});

View file

@ -1,152 +0,0 @@
/**
* 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';
// the type of remark/rehype plugins can be either function, object or array
const markdownPluginsFunctionStub = () => {};
const markdownPluginsObjectStub = {};
describe('blog plugin options schema', () => {
it('normalizes options', () => {
const {value, error} = PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
expect(error).toBeUndefined();
});
it('accepts correctly defined user options', () => {
const userOptions = {
...DEFAULT_OPTIONS,
feedOptions: {type: 'rss', title: 'myTitle'},
path: 'not_blog',
routeBasePath: 'myBlog',
postsPerPage: 5,
include: ['api/*', 'docs/*'],
};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({
...userOptions,
feedOptions: {type: ['rss'], title: 'myTitle', copyright: ''},
});
expect(error).toBeUndefined();
});
it('accepts valid user options', async () => {
const userOptions = {
...DEFAULT_OPTIONS,
routeBasePath: 'myBlog',
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [markdownPluginsFunctionStub],
remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]],
rehypePlugins: [
markdownPluginsObjectStub,
[markdownPluginsFunctionStub, {option1: '42'}],
],
};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
expect(error).toBeUndefined();
});
it('throws Error in case of invalid options', () => {
const {error} = PluginOptionSchema.validate({
path: 'not_blog',
postsPerPage: -1,
include: ['api/*', 'docs/*'],
routeBasePath: 'not_blog',
});
expect(error).toMatchSnapshot();
});
it('throws Error in case of invalid feed type', () => {
const {error} = PluginOptionSchema.validate({
feedOptions: {
type: 'none',
},
});
expect(error).toMatchSnapshot();
});
it('converts all feed type to array with other feed type', () => {
const {value} = PluginOptionSchema.validate({
feedOptions: {type: 'all'},
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: ['rss', 'atom', 'json'], copyright: ''},
});
});
it('accepts null type and return same', () => {
const {value, error} = PluginOptionSchema.validate({
feedOptions: {type: null},
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: null},
});
expect(error).toBeUndefined();
});
it('contains array with rss + atom for missing feed type', () => {
const {value} = PluginOptionSchema.validate({
feedOptions: {},
});
expect(value).toEqual(DEFAULT_OPTIONS);
});
it('has array with rss + atom, title for missing feed type', () => {
const {value} = PluginOptionSchema.validate({
feedOptions: {title: 'title'},
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: ['rss', 'atom'], title: 'title', copyright: ''},
});
});
});
describe('blog sidebar', () => {
it('accepts 0 sidebar count', () => {
const userOptions = {blogSidebarCount: 0};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
expect(error).toBeUndefined();
});
it('accepts "ALL" sidebar count', () => {
const userOptions = {blogSidebarCount: 'ALL'};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
expect(error).toBeUndefined();
});
it('rejects "abcdef" sidebar count', () => {
const userOptions = {blogSidebarCount: 'abcdef'};
const {error} = PluginOptionSchema.validate(userOptions);
expect(error).toMatchInlineSnapshot(
`[ValidationError: "blogSidebarCount" must be one of [ALL, number]]`,
);
});
it('accepts "all posts" sidebar title', () => {
const userOptions = {blogSidebarTitle: 'all posts'};
const {value, error} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
expect(error).toBeUndefined();
});
it('rejects 42 sidebar title', () => {
const userOptions = {blogSidebarTitle: 42};
const {error} = PluginOptionSchema.validate(userOptions);
expect(error).toMatchInlineSnapshot(
`[ValidationError: "blogSidebarTitle" must be a string]`,
);
});
});

View file

@ -7,7 +7,7 @@
import type {BlogPost, BlogContent} from '../types'; import type {BlogPost, BlogContent} from '../types';
import {getTranslationFiles, translateContent} from '../translations'; import {getTranslationFiles, translateContent} from '../translations';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema'; import {DEFAULT_OPTIONS} from '../options';
import {updateTranslationFileMessages} from '@docusaurus/utils'; import {updateTranslationFileMessages} from '@docusaurus/utils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog'; import type {PluginOptions} from '@docusaurus/plugin-content-blog';

View file

@ -31,7 +31,7 @@ import {
getContentPathList, getContentPathList,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import type {LoadContext} from '@docusaurus/types'; import type {LoadContext} from '@docusaurus/types';
import {validateBlogPostFrontMatter} from './blogFrontMatter'; import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors'; import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import type { import type {

View file

@ -30,14 +30,7 @@ import type {
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
} from './types'; } from './types';
import {PluginOptionSchema} from './pluginOptionSchema'; import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
import type {
LoadContext,
Plugin,
HtmlTags,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import { import {
generateBlogPosts, generateBlogPosts,
getSourceToPermalink, getSourceToPermalink,
@ -572,10 +565,4 @@ export default async function pluginContentBlog(
}; };
} }
export function validateOptions({ export {validateOptions} from './options';
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -13,7 +13,8 @@ import {
URISchema, URISchema,
} from '@docusaurus/utils-validation'; } from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils'; import {GlobExcludeDefault} from '@docusaurus/utils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog'; import type {PluginOptions, Options} from '@docusaurus/plugin-content-blog';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: PluginOptions = { export const DEFAULT_OPTIONS: PluginOptions = {
feedOptions: {type: ['rss', 'atom'], copyright: ''}, feedOptions: {type: ['rss', 'atom'], copyright: ''},
@ -46,7 +47,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
sortPosts: 'descending', sortPosts: 'descending',
}; };
export const PluginOptionSchema = Joi.object<PluginOptions>({ const PluginOptionSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path), path: Joi.string().default(DEFAULT_OPTIONS.path),
archiveBasePath: Joi.string() archiveBasePath: Joi.string()
.default(DEFAULT_OPTIONS.archiveBasePath) .default(DEFAULT_OPTIONS.archiveBasePath)
@ -125,4 +126,15 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
sortPosts: Joi.string() sortPosts: Joi.string()
.valid('descending', 'ascending') .valid('descending', 'ascending')
.default(DEFAULT_OPTIONS.sortPosts), .default(DEFAULT_OPTIONS.sortPosts),
}); }).default(DEFAULT_OPTIONS);
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
const validatedOptions = validate(
PluginOptionSchema,
options,
) as PluginOptions;
return validatedOptions;
}

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {validateDocFrontMatter} from '../docFrontMatter'; import {validateDocFrontMatter} from '../frontMatter';
import type {DocFrontMatter} from '../types'; import type {DocFrontMatter} from '../types';
import escapeStringRegexp from 'escape-string-regexp'; import escapeStringRegexp from 'escape-string-regexp';

View file

@ -20,7 +20,7 @@ import {posixPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {sortConfig} from '@docusaurus/core/src/server/plugins'; import {sortConfig} from '@docusaurus/core/src/server/plugins';
import * as cliDocs from '../cli'; import * as cliDocs from '../cli';
import {OptionsSchema} from '../options'; import {validateOptions} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import type {LoadedVersion} from '../types'; import type {LoadedVersion} from '../types';
import type { import type {
@ -119,8 +119,11 @@ describe('sidebar', () => {
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json'); const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
sidebarPath, validate: normalizePluginOptions,
options: {
sidebarPath,
},
}), }),
); );
await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot(); await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
@ -133,8 +136,11 @@ describe('sidebar', () => {
await expect(async () => { await expect(async () => {
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
sidebarPath: 'wrong-path-sidebar.json', validate: normalizePluginOptions,
options: {
sidebarPath: 'wrong-path-sidebar.json',
},
}), }),
); );
await plugin.loadContent!(); await plugin.loadContent!();
@ -152,8 +158,11 @@ describe('sidebar', () => {
const context = await loadContext(siteDir); const context = await loadContext(siteDir);
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
sidebarPath: undefined, validate: normalizePluginOptions,
options: {
sidebarPath: undefined,
},
}), }),
); );
const result = await plugin.loadContent!(); const result = await plugin.loadContent!();
@ -167,8 +176,11 @@ describe('sidebar', () => {
const context = await loadContext(siteDir); const context = await loadContext(siteDir);
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
sidebarPath: false, validate: normalizePluginOptions,
options: {
sidebarPath: false,
},
}), }),
); );
const result = await plugin.loadContent!(); const result = await plugin.loadContent!();
@ -186,7 +198,7 @@ describe('empty/no docs website', () => {
await fs.ensureDir(path.join(siteDir, 'docs')); await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, {}), validateOptions({validate: normalizePluginOptions, options: {}}),
); );
await expect( await expect(
plugin.loadContent!(), plugin.loadContent!(),
@ -200,8 +212,11 @@ describe('empty/no docs website', () => {
await expect( await expect(
pluginContentDocs( pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: `path/does/not/exist`, validate: normalizePluginOptions,
options: {
path: 'path/does/not/exist',
},
}), }),
), ),
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
@ -217,9 +232,12 @@ describe('simple website', () => {
const sidebarPath = path.join(siteDir, 'sidebars.json'); const sidebarPath = path.join(siteDir, 'sidebars.json');
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: 'docs', validate: normalizePluginOptions,
sidebarPath, options: {
path: 'docs',
sidebarPath,
},
}), }),
); );
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
@ -328,9 +346,12 @@ describe('versioned website', () => {
const routeBasePath = 'docs'; const routeBasePath = 'docs';
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
routeBasePath, validate: normalizePluginOptions,
sidebarPath, options: {
routeBasePath,
sidebarPath,
},
}), }),
); );
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
@ -455,11 +476,14 @@ describe('versioned website (community)', () => {
const pluginId = 'community'; const pluginId = 'community';
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
id: 'community', validate: normalizePluginOptions,
path: 'community', options: {
routeBasePath, id: 'community',
sidebarPath, path: 'community',
routeBasePath,
sidebarPath,
},
}), }),
); );
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
@ -558,9 +582,12 @@ describe('site with doc label', () => {
const sidebarPath = path.join(siteDir, 'sidebars.json'); const sidebarPath = path.join(siteDir, 'sidebars.json');
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: 'docs', validate: normalizePluginOptions,
sidebarPath, options: {
path: 'docs',
sidebarPath,
},
}), }),
); );
@ -596,8 +623,11 @@ describe('site with full autogenerated sidebar', () => {
const context = await loadContext(siteDir); const context = await loadContext(siteDir);
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: 'docs', validate: normalizePluginOptions,
options: {
path: 'docs',
},
}), }),
); );
@ -648,14 +678,17 @@ describe('site with partial autogenerated sidebars', () => {
const context = await loadContext(siteDir, {}); const context = await loadContext(siteDir, {});
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: 'docs', validate: normalizePluginOptions,
sidebarPath: path.join( options: {
__dirname, path: 'docs',
'__fixtures__', sidebarPath: path.join(
'site-with-autogenerated-sidebar', __dirname,
'partialAutogeneratedSidebars.js', '__fixtures__',
), 'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars.js',
),
},
}), }),
); );
@ -701,14 +734,17 @@ describe('site with partial autogenerated sidebars 2 (fix #4638)', () => {
const context = await loadContext(siteDir, {}); const context = await loadContext(siteDir, {});
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: 'docs', validate: normalizePluginOptions,
sidebarPath: path.join( options: {
__dirname, path: 'docs',
'__fixtures__', sidebarPath: path.join(
'site-with-autogenerated-sidebar', __dirname,
'partialAutogeneratedSidebars2.js', '__fixtures__',
), 'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars2.js',
),
},
}), }),
); );
@ -735,9 +771,12 @@ describe('site with custom sidebar items generator', () => {
const context = await loadContext(siteDir); const context = await loadContext(siteDir);
const plugin = await pluginContentDocs( const plugin = await pluginContentDocs(
context, context,
normalizePluginOptions(OptionsSchema, { validateOptions({
path: 'docs', validate: normalizePluginOptions,
sidebarItemsGenerator, options: {
path: 'docs',
sidebarItemsGenerator,
},
}), }),
); );
const content = (await plugin.loadContent?.())!; const content = (await plugin.loadContent?.())!;

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {OptionsSchema, DEFAULT_OPTIONS, validateOptions} from '../options'; import {validateOptions, DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DefaultSidebarItemsGenerator} from '../sidebars/generator'; import {DefaultSidebarItemsGenerator} from '../sidebars/generator';
import { import {
@ -13,27 +13,26 @@ import {
DisabledNumberPrefixParser, DisabledNumberPrefixParser,
} from '../numberPrefix'; } from '../numberPrefix';
import {GlobExcludeDefault} from '@docusaurus/utils'; import {GlobExcludeDefault} from '@docusaurus/utils';
import type {PluginOptions} from '@docusaurus/plugin-content-docs'; import type {Options} from '@docusaurus/plugin-content-docs';
// the type of remark/rehype plugins is function // the type of remark/rehype plugins is function
const markdownPluginsFunctionStub = () => {}; const markdownPluginsFunctionStub = () => {};
const markdownPluginsObjectStub = {}; const markdownPluginsObjectStub = {};
function testValidateOptions(options: Partial<PluginOptions>) { function testValidate(options: Options) {
return validateOptions({ return validateOptions({validate: normalizePluginOptions, options});
options: {
...DEFAULT_OPTIONS,
...options,
},
validate: normalizePluginOptions,
});
} }
const defaultOptions = {
...DEFAULT_OPTIONS,
id: 'default',
// The admonitions plugin is automatically added. Not really worth testing
remarkPlugins: expect.any(Array),
};
describe('normalizeDocsPluginOptions', () => { describe('normalizeDocsPluginOptions', () => {
it('returns default options for undefined user options', async () => { it('returns default options for undefined user options', async () => {
const {value, error} = await OptionsSchema.validate({}); expect(testValidate({})).toEqual(defaultOptions);
expect(value).toEqual(DEFAULT_OPTIONS);
expect(error).toBeUndefined();
}); });
it('accepts correctly defined user options', async () => { it('accepts correctly defined user options', async () => {
@ -77,14 +76,15 @@ describe('normalizeDocsPluginOptions', () => {
sidebarCollapsible: false, sidebarCollapsible: false,
sidebarCollapsed: false, sidebarCollapsed: false,
}; };
const {value, error} = await OptionsSchema.validate(userOptions); expect(testValidate(userOptions)).toEqual({
expect(value).toEqual(userOptions); ...defaultOptions,
expect(error).toBeUndefined(); ...userOptions,
remarkPlugins: [...userOptions.remarkPlugins, expect.any(Array)],
});
}); });
it('accepts correctly defined remark and rehype plugin options', async () => { it('accepts correctly defined remark and rehype plugin options', async () => {
const userOptions = { const userOptions = {
...DEFAULT_OPTIONS,
beforeDefaultRemarkPlugins: [], beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [markdownPluginsFunctionStub], beforeDefaultRehypePlugins: [markdownPluginsFunctionStub],
remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]], remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]],
@ -93,85 +93,71 @@ describe('normalizeDocsPluginOptions', () => {
[markdownPluginsFunctionStub, {option1: '42'}], [markdownPluginsFunctionStub, {option1: '42'}],
], ],
}; };
const {value, error} = await OptionsSchema.validate(userOptions); expect(testValidate(userOptions)).toEqual({
expect(value).toEqual(userOptions); ...defaultOptions,
expect(error).toBeUndefined(); ...userOptions,
remarkPlugins: [...userOptions.remarkPlugins, expect.any(Array)],
});
}); });
it('accepts admonitions false', async () => { it('accepts admonitions false', async () => {
const admonitionsFalse = { const admonitionsFalse = {
...DEFAULT_OPTIONS,
admonitions: false, admonitions: false,
}; };
const {value, error} = OptionsSchema.validate(admonitionsFalse); expect(testValidate(admonitionsFalse)).toEqual({
expect(value).toEqual(admonitionsFalse); ...defaultOptions,
expect(error).toBeUndefined(); ...admonitionsFalse,
});
it('accepts numberPrefixParser function', () => {
function customNumberPrefixParser() {}
expect(
normalizePluginOptions(OptionsSchema, {
...DEFAULT_OPTIONS,
numberPrefixParser: customNumberPrefixParser,
}),
).toEqual({
...DEFAULT_OPTIONS,
id: 'default',
numberPrefixParser: customNumberPrefixParser,
});
});
it('accepts numberPrefixParser false', () => {
expect(
normalizePluginOptions(OptionsSchema, {
...DEFAULT_OPTIONS,
numberPrefixParser: false,
}),
).toEqual({
...DEFAULT_OPTIONS,
id: 'default',
numberPrefixParser: DisabledNumberPrefixParser,
});
});
it('accepts numberPrefixParser true', () => {
expect(
normalizePluginOptions(OptionsSchema, {
...DEFAULT_OPTIONS,
numberPrefixParser: true,
}),
).toEqual({
...DEFAULT_OPTIONS,
id: 'default',
numberPrefixParser: DefaultNumberPrefixParser,
}); });
}); });
it('rejects admonitions true', async () => { it('rejects admonitions true', async () => {
const admonitionsTrue = { const admonitionsTrue = {
...DEFAULT_OPTIONS,
admonitions: true, admonitions: true,
}; };
const {error} = OptionsSchema.validate(admonitionsTrue); expect(() =>
expect(error).toMatchInlineSnapshot( testValidate(admonitionsTrue),
`[ValidationError: "admonitions" contains an invalid value]`, ).toThrowErrorMatchingInlineSnapshot(
`"\\"admonitions\\" contains an invalid value"`,
); );
}); });
it('accepts numberPrefixParser function', () => {
function customNumberPrefixParser() {}
expect(
testValidate({numberPrefixParser: customNumberPrefixParser}),
).toEqual({
...defaultOptions,
numberPrefixParser: customNumberPrefixParser,
});
});
it('accepts numberPrefixParser false', () => {
expect(testValidate({numberPrefixParser: false})).toEqual({
...defaultOptions,
numberPrefixParser: DisabledNumberPrefixParser,
});
});
it('accepts numberPrefixParser true', () => {
expect(testValidate({numberPrefixParser: true})).toEqual({
...defaultOptions,
numberPrefixParser: DefaultNumberPrefixParser,
});
});
it('rejects invalid remark plugin options', () => { it('rejects invalid remark plugin options', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({
remarkPlugins: [[{option1: '42'}, markdownPluginsFunctionStub]], remarkPlugins: [[{option1: '42'}, markdownPluginsFunctionStub]],
}); }),
}).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"\\"remarkPlugins[0]\\" does not match any of the allowed types"`, `"\\"remarkPlugins[0]\\" does not match any of the allowed types"`,
); );
}); });
it('rejects invalid rehype plugin options', () => { it('rejects invalid rehype plugin options', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({
rehypePlugins: [ rehypePlugins: [
[ [
markdownPluginsFunctionStub, markdownPluginsFunctionStub,
@ -179,61 +165,51 @@ describe('normalizeDocsPluginOptions', () => {
markdownPluginsFunctionStub, markdownPluginsFunctionStub,
], ],
], ],
}); }),
}).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"\\"rehypePlugins[0]\\" does not match any of the allowed types"`, `"\\"rehypePlugins[0]\\" does not match any of the allowed types"`,
); );
}); });
it('rejects bad path inputs', () => { it('rejects bad path inputs', () => {
expect(() => { expect(() => testValidate({path: 2})).toThrowErrorMatchingInlineSnapshot(
normalizePluginOptions(OptionsSchema, { `"\\"path\\" must be a string"`,
path: 2, );
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
}); });
it('rejects bad include inputs', () => { it('rejects bad include inputs', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({include: '**/*.{md,mdx}'}),
include: '**/*.{md,mdx}', ).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
}); });
it('rejects bad showLastUpdateTime inputs', () => { it('rejects bad showLastUpdateTime inputs', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({showLastUpdateTime: 'true'}),
showLastUpdateTime: 'true', ).toThrowErrorMatchingInlineSnapshot(
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"showLastUpdateTime\\" must be a boolean"`, `"\\"showLastUpdateTime\\" must be a boolean"`,
); );
}); });
it('rejects bad remarkPlugins input', () => { it('rejects bad remarkPlugins input', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({remarkPlugins: 'remark-math'}),
remarkPlugins: 'remark-math', ).toThrowErrorMatchingInlineSnapshot(
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"remarkPlugins\\" must be an array"`, `"\\"remarkPlugins\\" must be an array"`,
); );
}); });
it('rejects bad lastVersion', () => { it('rejects bad lastVersion', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({lastVersion: false}),
lastVersion: false, ).toThrowErrorMatchingInlineSnapshot(
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"lastVersion\\" must be a string"`, `"\\"lastVersion\\" must be a string"`,
); );
}); });
it('rejects bad versions', () => { it('rejects bad versions', () => {
expect(() => { expect(() =>
normalizePluginOptions(OptionsSchema, { testValidate({
versions: { versions: {
current: { current: {
hey: 3, hey: 3,
@ -243,32 +219,29 @@ describe('normalizeDocsPluginOptions', () => {
label: 'world', label: 'world',
}, },
}, },
}); }),
}).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"\\"versions.current.hey\\" is not allowed"`, `"\\"versions.current.hey\\" is not allowed"`,
); );
}); });
it('handles sidebarCollapsed option inconsistencies', () => { it('handles sidebarCollapsed option inconsistencies', () => {
expect( expect(
testValidateOptions({ testValidate({
...DEFAULT_OPTIONS,
sidebarCollapsible: true, sidebarCollapsible: true,
sidebarCollapsed: undefined, sidebarCollapsed: undefined,
}).sidebarCollapsed, }).sidebarCollapsed,
).toBe(true); ).toBe(true);
expect( expect(
testValidateOptions({ testValidate({
...DEFAULT_OPTIONS,
sidebarCollapsible: false, sidebarCollapsible: false,
sidebarCollapsed: undefined, sidebarCollapsed: undefined,
}).sidebarCollapsed, }).sidebarCollapsed,
).toBe(false); ).toBe(false);
expect( expect(
testValidateOptions({ testValidate({
...DEFAULT_OPTIONS,
sidebarCollapsible: false, sidebarCollapsible: false,
sidebarCollapsed: true, sidebarCollapsed: true,
}).sidebarCollapsed, }).sidebarCollapsed,

View file

@ -34,7 +34,7 @@ import getSlug from './slug';
import {CURRENT_VERSION_NAME} from './constants'; import {CURRENT_VERSION_NAME} from './constants';
import {getDocsDirPaths} from './versions'; import {getDocsDirPaths} from './versions';
import {stripPathNumberPrefixes} from './numberPrefix'; import {stripPathNumberPrefixes} from './numberPrefix';
import {validateDocFrontMatter} from './docFrontMatter'; import {validateDocFrontMatter} from './frontMatter';
import type {SidebarsUtils} from './sidebars/utils'; import type {SidebarsUtils} from './sidebars/utils';
import {toDocNavigationLink, toNavigationLink} from './sidebars/utils'; import {toDocNavigationLink, toNavigationLink} from './sidebars/utils';
import type { import type {

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {PluginOptions} from '@docusaurus/plugin-content-docs'; import type {PluginOptions, Options} from '@docusaurus/plugin-content-docs';
import { import {
Joi, Joi,
RemarkPluginsSchema, RemarkPluginsSchema,
@ -15,10 +15,7 @@ import {
} from '@docusaurus/utils-validation'; } from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils'; import {GlobExcludeDefault} from '@docusaurus/utils';
import type { import type {OptionValidationContext} from '@docusaurus/types';
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import {DefaultSidebarItemsGenerator} from './sidebars/generator'; import {DefaultSidebarItemsGenerator} from './sidebars/generator';
@ -70,7 +67,7 @@ const VersionsOptionsSchema = Joi.object()
.pattern(Joi.string().required(), VersionOptionsSchema) .pattern(Joi.string().required(), VersionOptionsSchema)
.default(DEFAULT_OPTIONS.versions); .default(DEFAULT_OPTIONS.versions);
export const OptionsSchema = Joi.object({ const OptionsSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path), path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: Joi.alternatives().try(URISchema, Joi.function()), editUrl: Joi.alternatives().try(URISchema, Joi.function()),
editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion), editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion),
@ -80,6 +77,7 @@ export const OptionsSchema = Joi.object({
// .allow('') "" // .allow('') ""
.default(DEFAULT_OPTIONS.routeBasePath), .default(DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath), tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath),
// @ts-expect-error: deprecated
homePageId: Joi.any().forbidden().messages({ homePageId: Joi.any().forbidden().messages({
'any.unknown': 'any.unknown':
'The docs plugin option homePageId is not supported anymore. To make a doc the "home", please add "slug: /" in its front matter. See: https://docusaurus.io/docs/next/docs-introduction#home-page-docs', 'The docs plugin option homePageId is not supported anymore. To make a doc the "home", please add "slug: /" in its front matter. See: https://docusaurus.io/docs/next/docs-introduction#home-page-docs',
@ -146,7 +144,7 @@ export const OptionsSchema = Joi.object({
export function validateOptions({ export function validateOptions({
validate, validate,
options: userOptions, options: userOptions,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> { }: OptionValidationContext<Options, PluginOptions>): PluginOptions {
let options = userOptions; let options = userOptions;
if (options.sidebarCollapsible === false) { if (options.sidebarCollapsible === false) {
@ -168,7 +166,7 @@ export function validateOptions({
} }
} }
const normalizedOptions = validate(OptionsSchema, options); const normalizedOptions = validate(OptionsSchema, options) as PluginOptions;
if (normalizedOptions.admonitions) { if (normalizedOptions.admonitions) {
normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([ normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([

View file

@ -9,7 +9,8 @@ import path from 'path';
import {loadContext} from '@docusaurus/core/lib/server'; import {loadContext} from '@docusaurus/core/lib/server';
import pluginContentPages from '../index'; import pluginContentPages from '../index';
import {PluginOptionSchema} from '../pluginOptionSchema'; import {validateOptions} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
describe('docusaurus-plugin-content-pages', () => { describe('docusaurus-plugin-content-pages', () => {
it('loads simple pages', async () => { it('loads simple pages', async () => {
@ -17,9 +18,12 @@ describe('docusaurus-plugin-content-pages', () => {
const context = await loadContext(siteDir); const context = await loadContext(siteDir);
const plugin = await pluginContentPages( const plugin = await pluginContentPages(
context, context,
PluginOptionSchema.validate({ validateOptions({
path: 'src/pages', validate: normalizePluginOptions,
}).value, options: {
path: 'src/pages',
},
}),
); );
const pagesMetadata = await plugin.loadContent!(); const pagesMetadata = await plugin.loadContent!();
@ -37,9 +41,12 @@ describe('docusaurus-plugin-content-pages', () => {
currentLocale: 'fr', currentLocale: 'fr',
}, },
}, },
PluginOptionSchema.validate({ validateOptions({
path: 'src/pages', validate: normalizePluginOptions,
}).value, options: {
path: 'src/pages',
},
}),
); );
const pagesMetadata = await plugin.loadContent!(); const pagesMetadata = await plugin.loadContent!();

View file

@ -5,31 +5,29 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema'; import {validateOptions, DEFAULT_OPTIONS} from '../options';
import type {PluginOptions} from '@docusaurus/plugin-content-pages'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import type {Options} from '@docusaurus/plugin-content-pages';
function normalizePluginOptions( function testValidate(options: Options) {
options: Partial<PluginOptions>, return validateOptions({validate: normalizePluginOptions, options});
): PluginOptions {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
} }
const defaultOptions = {
...DEFAULT_OPTIONS,
id: 'default',
};
describe('normalizePagesPluginOptions', () => { describe('normalizePagesPluginOptions', () => {
it('returns default options for undefined user options', () => { it('returns default options for undefined user options', () => {
const value = normalizePluginOptions({}); expect(testValidate({})).toEqual(defaultOptions);
expect(value).toEqual(DEFAULT_OPTIONS);
}); });
it('fills in default options for partially defined user options', () => { it('fills in default options for partially defined user options', () => {
const value = normalizePluginOptions({path: 'src/pages'}); expect(testValidate({path: 'src/foo'})).toEqual({
expect(value).toEqual(DEFAULT_OPTIONS); ...defaultOptions,
path: 'src/foo',
});
}); });
it('accepts correctly defined user options', () => { it('accepts correctly defined user options', () => {
@ -39,13 +37,15 @@ describe('normalizePagesPluginOptions', () => {
include: ['**/*.{js,jsx,ts,tsx}'], include: ['**/*.{js,jsx,ts,tsx}'],
exclude: ['**/$*/'], exclude: ['**/$*/'],
}; };
const value = normalizePluginOptions(userOptions); expect(testValidate(userOptions)).toEqual({
expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions}); ...defaultOptions,
...userOptions,
});
}); });
it('rejects bad path inputs', () => { it('rejects bad path inputs', () => {
expect(() => { expect(() => {
normalizePluginOptions({ testValidate({
// @ts-expect-error: bad attribute // @ts-expect-error: bad attribute
path: 42, path: 42,
}); });

View file

@ -21,15 +21,9 @@ import {
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
parseMarkdownString, parseMarkdownString,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import type { import type {LoadContext, Plugin} from '@docusaurus/types';
LoadContext,
Plugin,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import {PluginOptionSchema} from './pluginOptionSchema'; import {validatePageFrontMatter} from './frontMatter';
import {validatePageFrontMatter} from './pageFrontMatter';
import type {LoadedContent, PagesContentPaths} from './types'; import type {LoadedContent, PagesContentPaths} from './types';
import type {PluginOptions, Metadata} from '@docusaurus/plugin-content-pages'; import type {PluginOptions, Metadata} from '@docusaurus/plugin-content-pages';
@ -176,7 +170,7 @@ export default async function pluginContentPages(
); );
}, },
configureWebpack(_config, isServer, {getJSLoader}) { configureWebpack(config, isServer, {getJSLoader}) {
const { const {
rehypePlugins, rehypePlugins,
remarkPlugins, remarkPlugins,
@ -241,10 +235,4 @@ export default async function pluginContentPages(
}; };
} }
export function validateOptions({ export {validateOptions} from './options';
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {PluginOptions} from '@docusaurus/plugin-content-pages'; import type {PluginOptions, Options} from '@docusaurus/plugin-content-pages';
import { import {
Joi, Joi,
RemarkPluginsSchema, RemarkPluginsSchema,
@ -13,6 +13,7 @@ import {
AdmonitionsSchema, AdmonitionsSchema,
} from '@docusaurus/utils-validation'; } from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils'; import {GlobExcludeDefault} from '@docusaurus/utils';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: PluginOptions = { export const DEFAULT_OPTIONS: PluginOptions = {
path: 'src/pages', // Path to data on filesystem, relative to site dir. path: 'src/pages', // Path to data on filesystem, relative to site dir.
@ -27,7 +28,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
admonitions: {}, admonitions: {},
}; };
export const PluginOptionSchema = Joi.object({ const PluginOptionSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path), path: Joi.string().default(DEFAULT_OPTIONS.path),
routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath), routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
@ -43,3 +44,11 @@ export const PluginOptionSchema = Joi.object({
), ),
admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions), admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
}); });
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -10,11 +10,10 @@ import type {
LoadContext, LoadContext,
Plugin, Plugin,
OptionValidationContext, OptionValidationContext,
ValidationResult,
ThemeConfig, ThemeConfig,
ThemeConfigValidationContext, ThemeConfigValidationContext,
} from '@docusaurus/types'; } from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-google-analytics'; import type {PluginOptions, Options} from '@docusaurus/plugin-google-analytics';
export default function pluginGoogleAnalytics( export default function pluginGoogleAnalytics(
context: LoadContext, context: LoadContext,
@ -74,13 +73,13 @@ const pluginOptionsSchema = Joi.object<PluginOptions>({
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> { }: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(pluginOptionsSchema, options); return validate(pluginOptionsSchema, options);
} }
export function validateThemeConfig({ export function validateThemeConfig({
themeConfig, themeConfig,
}: ThemeConfigValidationContext<ThemeConfig>): ValidationResult<ThemeConfig> { }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
if ('googleAnalytics' in themeConfig) { if ('googleAnalytics' in themeConfig) {
throw new Error( throw new Error(
'The "googleAnalytics" field in themeConfig should now be specified as option for plugin-google-analytics. More information at https://github.com/facebook/docusaurus/pull/5832.', 'The "googleAnalytics" field in themeConfig should now be specified as option for plugin-google-analytics. More information at https://github.com/facebook/docusaurus/pull/5832.',

View file

@ -10,11 +10,10 @@ import type {
LoadContext, LoadContext,
Plugin, Plugin,
OptionValidationContext, OptionValidationContext,
ValidationResult,
ThemeConfig, ThemeConfig,
ThemeConfigValidationContext, ThemeConfigValidationContext,
} from '@docusaurus/types'; } from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-google-gtag'; import type {PluginOptions, Options} from '@docusaurus/plugin-google-gtag';
export default function pluginGoogleGtag( export default function pluginGoogleGtag(
context: LoadContext, context: LoadContext,
@ -88,13 +87,13 @@ const pluginOptionsSchema = Joi.object<PluginOptions>({
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> { }: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(pluginOptionsSchema, options); return validate(pluginOptionsSchema, options);
} }
export function validateThemeConfig({ export function validateThemeConfig({
themeConfig, themeConfig,
}: ThemeConfigValidationContext<ThemeConfig>): ValidationResult<ThemeConfig> { }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
if ('gtag' in themeConfig) { if ('gtag' in themeConfig) {
throw new Error( throw new Error(
'The "gtag" field in themeConfig should now be specified as option for plugin-google-gtag. More information at https://github.com/facebook/docusaurus/pull/5832.', 'The "gtag" field in themeConfig should now be specified as option for plugin-google-gtag. More information at https://github.com/facebook/docusaurus/pull/5832.',

View file

@ -9,7 +9,6 @@ import type {
LoadContext, LoadContext,
Plugin, Plugin,
OptionValidationContext, OptionValidationContext,
ValidationResult,
} from '@docusaurus/types'; } from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-ideal-image'; import type {PluginOptions} from '@docusaurus/plugin-ideal-image';
import {Joi} from '@docusaurus/utils-validation'; import {Joi} from '@docusaurus/utils-validation';
@ -79,7 +78,7 @@ export default function pluginIdealImage(
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> { }: OptionValidationContext<PluginOptions, PluginOptions>): PluginOptions {
const pluginOptionsSchema = Joi.object({ const pluginOptionsSchema = Joi.object({
disableInDev: Joi.boolean().default(true), disableInDev: Joi.boolean().default(true),
}).unknown(); }).unknown();

View file

@ -6,11 +6,7 @@
*/ */
import {Joi} from '@docusaurus/utils-validation'; import {Joi} from '@docusaurus/utils-validation';
import type { import type {OptionValidationContext} from '@docusaurus/types';
ThemeConfig,
ValidationResult,
OptionValidationContext,
} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-pwa'; import type {PluginOptions} from '@docusaurus/plugin-pwa';
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
@ -27,7 +23,7 @@ const DEFAULT_OPTIONS = {
reloadPopup: '@theme/PwaReloadPopup', reloadPopup: '@theme/PwaReloadPopup',
}; };
export const Schema = Joi.object({ const Schema = Joi.object({
debug: Joi.bool().default(DEFAULT_OPTIONS.debug), debug: Joi.bool().default(DEFAULT_OPTIONS.debug),
offlineModeActivationStrategies: Joi.array() offlineModeActivationStrategies: Joi.array()
.items( .items(
@ -61,6 +57,6 @@ export const Schema = Joi.object({
export function validateOptions({ export function validateOptions({
validate, validate,
options, options,
}: OptionValidationContext<PluginOptions>): ValidationResult<ThemeConfig> { }: OptionValidationContext<PluginOptions, PluginOptions>): PluginOptions {
return validate(Schema, options); return validate(Schema, options);
} }

View file

@ -0,0 +1,52 @@
/**
* 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 {validateOptions, DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import type {Options} from '@docusaurus/plugin-sitemap';
function testValidate(options: Options) {
return validateOptions({validate: normalizePluginOptions, options});
}
const defaultOptions = {
...DEFAULT_OPTIONS,
id: 'default',
};
describe('validateOptions', () => {
it('returns default values for empty user options', () => {
expect(testValidate({})).toEqual(defaultOptions);
});
it('accepts correctly defined user options', () => {
const userOptions = {
changefreq: 'yearly',
priority: 0.9,
};
expect(testValidate(userOptions)).toEqual({
...defaultOptions,
...userOptions,
});
});
it('rejects out-of-range priority inputs', () => {
expect(() =>
testValidate({priority: 2}),
).toThrowErrorMatchingInlineSnapshot(
`"\\"priority\\" must be less than or equal to 1"`,
);
});
it('rejects bad changefreq inputs', () => {
expect(() =>
testValidate({changefreq: 'annually'}),
).toThrowErrorMatchingInlineSnapshot(
`"\\"changefreq\\" must be one of [daily, monthly, always, hourly, weekly, yearly, never]"`,
);
});
});

View file

@ -1,55 +0,0 @@
/**
* 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';
function normalizePluginOptions(options) {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
}
describe('normalizeSitemapPluginOptions', () => {
it('returns default values for empty user options', () => {
const {value} = PluginOptionSchema.validate({});
expect(value).toEqual(DEFAULT_OPTIONS);
});
it('accepts correctly defined user options', () => {
const userOptions = {
changefreq: 'yearly',
priority: 0.9,
};
const {value} = PluginOptionSchema.validate(userOptions);
expect(value).toEqual(userOptions);
});
it('rejects out-of-range priority inputs', () => {
expect(() => {
normalizePluginOptions({
priority: 2,
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"priority\\" must be less than or equal to 1"`,
);
});
it('rejects bad changefreq inputs', () => {
expect(() => {
normalizePluginOptions({
changefreq: 'annually',
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"changefreq\\" must be one of [daily, monthly, always, hourly, weekly, yearly, never]"`,
);
});
});

View file

@ -9,14 +9,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import type {Options} from '@docusaurus/plugin-sitemap'; import type {Options} from '@docusaurus/plugin-sitemap';
import createSitemap from './createSitemap'; import createSitemap from './createSitemap';
import type { import type {LoadContext, Plugin} from '@docusaurus/types';
LoadContext,
Props,
OptionValidationContext,
ValidationResult,
Plugin,
} from '@docusaurus/types';
import {PluginOptionSchema} from './pluginOptionSchema';
export default function pluginSitemap( export default function pluginSitemap(
context: LoadContext, context: LoadContext,
@ -25,7 +18,7 @@ export default function pluginSitemap(
return { return {
name: 'docusaurus-plugin-sitemap', name: 'docusaurus-plugin-sitemap',
async postBuild({siteConfig, routesPaths, outDir}: Props) { async postBuild({siteConfig, routesPaths, outDir}) {
if (siteConfig.noIndex) { if (siteConfig.noIndex) {
return; return;
} }
@ -47,10 +40,4 @@ export default function pluginSitemap(
}; };
} }
export function validateOptions({ export {validateOptions} from './options';
validate,
options,
}: OptionValidationContext<Options>): ValidationResult<Options> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -8,13 +8,14 @@
import {Joi} from '@docusaurus/utils-validation'; import {Joi} from '@docusaurus/utils-validation';
import {EnumChangefreq} from 'sitemap'; import {EnumChangefreq} from 'sitemap';
import type {Options} from '@docusaurus/plugin-sitemap'; import type {Options} from '@docusaurus/plugin-sitemap';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: Required<Options> = { export const DEFAULT_OPTIONS: Options = {
changefreq: EnumChangefreq.WEEKLY, changefreq: EnumChangefreq.WEEKLY,
priority: 0.5, priority: 0.5,
}; };
export const PluginOptionSchema = Joi.object({ const PluginOptionSchema = Joi.object({
cacheTime: Joi.forbidden().messages({ cacheTime: Joi.forbidden().messages({
'any.unknown': 'any.unknown':
'Option `cacheTime` in sitemap config is deprecated. Please remove it.', 'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
@ -28,3 +29,11 @@ export const PluginOptionSchema = Joi.object({
'Please use the new Docusaurus global trailingSlash config instead, and the sitemaps plugin will use it.', 'Please use the new Docusaurus global trailingSlash config instead, and the sitemaps plugin will use it.',
}), }),
}); });
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, Options>): Options {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -8,6 +8,7 @@
import type {EnumChangefreq} from 'sitemap'; import type {EnumChangefreq} from 'sitemap';
export type Options = { export type Options = {
id?: string;
/** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */ /** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */
changefreq?: EnumChangefreq; changefreq?: EnumChangefreq;
/** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */ /** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {LoadContext, Plugin, PostCssOptions} from '@docusaurus/types'; import type {LoadContext, Plugin} from '@docusaurus/types';
import type {ThemeConfig} from '@docusaurus/theme-common'; import type {ThemeConfig} from '@docusaurus/theme-common';
import {getTranslationFiles, translateThemeConfig} from './translations'; import {getTranslationFiles, translateThemeConfig} from './translations';
import {createRequire} from 'module'; import {createRequire} from 'module';
@ -169,7 +169,7 @@ export default function docusaurusThemeClassic(
}; };
}, },
configurePostCss(postCssOptions: PostCssOptions) { configurePostCss(postCssOptions) {
if (direction === 'rtl') { if (direction === 'rtl') {
const resolvedInfimaFile = require.resolve(getInfimaCSSFile(direction)); const resolvedInfimaFile = require.resolve(getInfimaCSSFile(direction));
const plugin: PostCssPlugin = { const plugin: PostCssPlugin = {

View file

@ -6,7 +6,10 @@
*/ */
import {Joi, URISchema} from '@docusaurus/utils-validation'; import {Joi, URISchema} from '@docusaurus/utils-validation';
import type {ThemeConfig, Validate, ValidationResult} from '@docusaurus/types'; import type {
ThemeConfig,
ThemeConfigValidationContext,
} from '@docusaurus/types';
const DEFAULT_DOCS_CONFIG = { const DEFAULT_DOCS_CONFIG = {
versionPersistence: 'localStorage', versionPersistence: 'localStorage',
@ -373,9 +376,6 @@ export const ThemeConfigSchema = Joi.object({
export function validateThemeConfig({ export function validateThemeConfig({
validate, validate,
themeConfig, themeConfig,
}: { }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
return validate(ThemeConfigSchema, themeConfig); return validate(ThemeConfigSchema, themeConfig);
} }

View file

@ -6,7 +6,10 @@
*/ */
import {Joi} from '@docusaurus/utils-validation'; import {Joi} from '@docusaurus/utils-validation';
import type {ThemeConfig, Validate, ValidationResult} from '@docusaurus/types'; import type {
ThemeConfig,
ThemeConfigValidationContext,
} from '@docusaurus/types';
export const DEFAULT_CONFIG = { export const DEFAULT_CONFIG = {
playgroundPosition: 'bottom', playgroundPosition: 'bottom',
@ -25,9 +28,6 @@ export const Schema = Joi.object({
export function validateThemeConfig({ export function validateThemeConfig({
validate, validate,
themeConfig, themeConfig,
}: { }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
return validate(Schema, themeConfig); return validate(Schema, themeConfig);
} }

View file

@ -6,7 +6,10 @@
*/ */
import {Joi} from '@docusaurus/utils-validation'; import {Joi} from '@docusaurus/utils-validation';
import type {ThemeConfig, Validate, ValidationResult} from '@docusaurus/types'; import type {
ThemeConfig,
ThemeConfigValidationContext,
} from '@docusaurus/types';
export const DEFAULT_CONFIG = { export const DEFAULT_CONFIG = {
// enabled by default, as it makes sense in most cases // enabled by default, as it makes sense in most cases
@ -45,9 +48,6 @@ export const Schema = Joi.object({
export function validateThemeConfig({ export function validateThemeConfig({
validate, validate,
themeConfig, themeConfig,
}: { }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
return validate(Schema, themeConfig); return validate(Schema, themeConfig);
} }

View file

@ -320,7 +320,7 @@ export type PluginModule = {
<Options, Content>(context: LoadContext, options: Options): <Options, Content>(context: LoadContext, options: Options):
| Plugin<Content> | Plugin<Content>
| Promise<Plugin<Content>>; | Promise<Plugin<Content>>;
validateOptions?: <T>(data: OptionValidationContext<T>) => T; validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T; validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
getSwizzleComponentList?: () => string[] | undefined; // TODO deprecate this one later getSwizzleComponentList?: () => string[] | undefined; // TODO deprecate this one later
@ -436,22 +436,20 @@ interface HtmlTagObject {
innerHTML?: string; innerHTML?: string;
} }
export type ValidationResult<T> = T;
export type ValidationSchema<T> = Joi.ObjectSchema<T>; export type ValidationSchema<T> = Joi.ObjectSchema<T>;
export type Validate<T> = ( export type Validate<T, U> = (
validationSchema: ValidationSchema<T>, validationSchema: ValidationSchema<U>,
options: Partial<T>, options: T,
) => ValidationResult<T>; ) => U;
export interface OptionValidationContext<T> { export type OptionValidationContext<T, U> = {
validate: Validate<T>; validate: Validate<T, U>;
options: Partial<T>; options: T;
} };
export interface ThemeConfigValidationContext<T> { export interface ThemeConfigValidationContext<T> {
validate: Validate<T>; validate: Validate<T, T>;
themeConfig: Partial<T>; themeConfig: Partial<T>;
} }

View file

@ -24,6 +24,9 @@ describe('normalizePluginOptions', () => {
options, options,
), ),
).toEqual(options); ).toEqual(options);
expect(
normalizePluginOptions(Joi.object({foo: Joi.string()}), undefined),
).toEqual({id: 'default'});
}); });
it('normalizes plugin options', () => { it('normalizes plugin options', () => {

View file

@ -20,7 +20,8 @@ export function printWarning(warning?: Joi.ValidationError): void {
export function normalizePluginOptions<T extends {id?: string}>( export function normalizePluginOptions<T extends {id?: string}>(
schema: Joi.ObjectSchema<T>, schema: Joi.ObjectSchema<T>,
options: Partial<T>, // This allows us to automatically normalize undefined to {id: 'default'}
options: Partial<T> = {},
): T { ): T {
// All plugins can be provided an "id" option (multi-instance support) // All plugins can be provided an "id" option (multi-instance support)
// we add schema validation automatically // we add schema validation automatically

View file

@ -179,7 +179,7 @@ export default async function initPlugins({
function doValidatePluginOptions( function doValidatePluginOptions(
normalizedPluginConfig: NormalizedPluginConfig, normalizedPluginConfig: NormalizedPluginConfig,
) { ): Required<PluginOptions> {
const validateOptions = getOptionValidationFunction(normalizedPluginConfig); const validateOptions = getOptionValidationFunction(normalizedPluginConfig);
if (validateOptions) { if (validateOptions) {
return validateOptions({ return validateOptions({