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', () => {
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', () => {
expect(testValidate(undefined)).toEqual(DEFAULT_OPTIONS);
expect(testValidate({})).toEqual({...DEFAULT_OPTIONS, id: 'default'});
});
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.
*/
import type {LoadContext, Plugin, Props} from '@docusaurus/types';
import type {LoadContext, Plugin} from '@docusaurus/types';
import type {PluginContext, RedirectMetadata} from './types';
import type {PluginOptions} from '@docusaurus/plugin-client-redirects';
@ -24,7 +24,7 @@ export default function pluginClientRedirectsPages(
return {
name: 'docusaurus-plugin-client-redirects',
async postBuild(props: Props) {
async postBuild(props) {
const pluginContext: PluginContext = {
relativeRoutesPaths: props.routesPaths.map(
(path) => `${addLeadingSlash(removePrefix(path, props.baseUrl))}`,

View file

@ -7,12 +7,10 @@
import type {
PluginOptions,
Options,
RedirectOption,
} from '@docusaurus/plugin-client-redirects';
import type {
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import type {OptionValidationContext} from '@docusaurus/types';
import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
export const DEFAULT_OPTIONS: Partial<PluginOptions> = {
@ -47,6 +45,6 @@ const UserOptionsSchema = Joi.object<PluginOptions>({
export function validateOptions({
validate,
options: userOptions,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
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 type {LoadContext, I18n} from '@docusaurus/types';
import type {BlogContentPaths} from '../types';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {DEFAULT_OPTIONS} from '../options';
import {generateBlogPosts} from '../blogUtils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';

View file

@ -5,7 +5,7 @@
* 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 type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';

View file

@ -9,9 +9,9 @@ import {jest} from '@jest/globals';
import path from 'path';
import pluginContentBlog from '../index';
import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
import {PluginOptionSchema} from '../pluginOptionSchema';
import {validateOptions} from '../options';
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 type {
PluginOptions,
@ -47,18 +47,6 @@ function getI18n(locale: string): I18n {
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 BaseEditUrl = 'https://baseEditUrl.com/edit';
@ -81,11 +69,14 @@ const getPlugin = async (
generatedFilesDir,
i18n,
} as LoadContext,
validateAndNormalize(PluginOptionSchema, {
path: PluginPath,
editUrl: BaseEditUrl,
...pluginOptions,
}),
validateOptions({
validate: normalizePluginOptions,
options: {
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 {getTranslationFiles, translateContent} from '../translations';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {DEFAULT_OPTIONS} from '../options';
import {updateTranslationFileMessages} from '@docusaurus/utils';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';

View file

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

View file

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

View file

@ -13,7 +13,8 @@ import {
URISchema,
} from '@docusaurus/utils-validation';
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 = {
feedOptions: {type: ['rss', 'atom'], copyright: ''},
@ -46,7 +47,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
sortPosts: 'descending',
};
export const PluginOptionSchema = Joi.object<PluginOptions>({
const PluginOptionSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path),
archiveBasePath: Joi.string()
.default(DEFAULT_OPTIONS.archiveBasePath)
@ -125,4 +126,15 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
sortPosts: Joi.string()
.valid('descending', 'ascending')
.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.
*/
import {validateDocFrontMatter} from '../docFrontMatter';
import {validateDocFrontMatter} from '../frontMatter';
import type {DocFrontMatter} from '../types';
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 * as cliDocs from '../cli';
import {OptionsSchema} from '../options';
import {validateOptions} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import type {LoadedVersion} from '../types';
import type {
@ -119,8 +119,11 @@ describe('sidebar', () => {
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath,
validateOptions({
validate: normalizePluginOptions,
options: {
sidebarPath,
},
}),
);
await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
@ -133,8 +136,11 @@ describe('sidebar', () => {
await expect(async () => {
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath: 'wrong-path-sidebar.json',
validateOptions({
validate: normalizePluginOptions,
options: {
sidebarPath: 'wrong-path-sidebar.json',
},
}),
);
await plugin.loadContent!();
@ -152,8 +158,11 @@ describe('sidebar', () => {
const context = await loadContext(siteDir);
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath: undefined,
validateOptions({
validate: normalizePluginOptions,
options: {
sidebarPath: undefined,
},
}),
);
const result = await plugin.loadContent!();
@ -167,8 +176,11 @@ describe('sidebar', () => {
const context = await loadContext(siteDir);
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath: false,
validateOptions({
validate: normalizePluginOptions,
options: {
sidebarPath: false,
},
}),
);
const result = await plugin.loadContent!();
@ -186,7 +198,7 @@ describe('empty/no docs website', () => {
await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {}),
validateOptions({validate: normalizePluginOptions, options: {}}),
);
await expect(
plugin.loadContent!(),
@ -200,8 +212,11 @@ describe('empty/no docs website', () => {
await expect(
pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: `path/does/not/exist`,
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'path/does/not/exist',
},
}),
),
).rejects.toThrowErrorMatchingInlineSnapshot(
@ -217,9 +232,12 @@ describe('simple website', () => {
const sidebarPath = path.join(siteDir, 'sidebars.json');
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarPath,
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'docs',
sidebarPath,
},
}),
);
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
@ -328,9 +346,12 @@ describe('versioned website', () => {
const routeBasePath = 'docs';
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
routeBasePath,
sidebarPath,
validateOptions({
validate: normalizePluginOptions,
options: {
routeBasePath,
sidebarPath,
},
}),
);
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
@ -455,11 +476,14 @@ describe('versioned website (community)', () => {
const pluginId = 'community';
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
id: 'community',
path: 'community',
routeBasePath,
sidebarPath,
validateOptions({
validate: normalizePluginOptions,
options: {
id: 'community',
path: 'community',
routeBasePath,
sidebarPath,
},
}),
);
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 plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarPath,
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'docs',
sidebarPath,
},
}),
);
@ -596,8 +623,11 @@ describe('site with full autogenerated sidebar', () => {
const context = await loadContext(siteDir);
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'docs',
},
}),
);
@ -648,14 +678,17 @@ describe('site with partial autogenerated sidebars', () => {
const context = await loadContext(siteDir, {});
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarPath: path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars.js',
),
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'docs',
sidebarPath: path.join(
__dirname,
'__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 plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarPath: path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars2.js',
),
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'docs',
sidebarPath: path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars2.js',
),
},
}),
);
@ -735,9 +771,12 @@ describe('site with custom sidebar items generator', () => {
const context = await loadContext(siteDir);
const plugin = await pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarItemsGenerator,
validateOptions({
validate: normalizePluginOptions,
options: {
path: 'docs',
sidebarItemsGenerator,
},
}),
);
const content = (await plugin.loadContent?.())!;

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* 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 {
Joi,
RemarkPluginsSchema,
@ -15,10 +15,7 @@ import {
} from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils';
import type {
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import type {OptionValidationContext} from '@docusaurus/types';
import logger from '@docusaurus/logger';
import admonitions from 'remark-admonitions';
import {DefaultSidebarItemsGenerator} from './sidebars/generator';
@ -70,7 +67,7 @@ const VersionsOptionsSchema = Joi.object()
.pattern(Joi.string().required(), VersionOptionsSchema)
.default(DEFAULT_OPTIONS.versions);
export const OptionsSchema = Joi.object({
const OptionsSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: Joi.alternatives().try(URISchema, Joi.function()),
editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion),
@ -80,6 +77,7 @@ export const OptionsSchema = Joi.object({
// .allow('') ""
.default(DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath),
// @ts-expect-error: deprecated
homePageId: Joi.any().forbidden().messages({
'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',
@ -146,7 +144,7 @@ export const OptionsSchema = Joi.object({
export function validateOptions({
validate,
options: userOptions,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
let options = userOptions;
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) {
normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([

View file

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

View file

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

View file

@ -21,15 +21,9 @@ import {
DEFAULT_PLUGIN_ID,
parseMarkdownString,
} from '@docusaurus/utils';
import type {
LoadContext,
Plugin,
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import type {LoadContext, Plugin} from '@docusaurus/types';
import admonitions from 'remark-admonitions';
import {PluginOptionSchema} from './pluginOptionSchema';
import {validatePageFrontMatter} from './pageFrontMatter';
import {validatePageFrontMatter} from './frontMatter';
import type {LoadedContent, PagesContentPaths} from './types';
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 {
rehypePlugins,
remarkPlugins,
@ -241,10 +235,4 @@ export default async function pluginContentPages(
};
}
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}
export {validateOptions} from './options';

View file

@ -5,7 +5,7 @@
* 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 {
Joi,
RemarkPluginsSchema,
@ -13,6 +13,7 @@ import {
AdmonitionsSchema,
} from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: PluginOptions = {
path: 'src/pages', // Path to data on filesystem, relative to site dir.
@ -27,7 +28,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
admonitions: {},
};
export const PluginOptionSchema = Joi.object({
const PluginOptionSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path),
routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
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),
});
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,
Plugin,
OptionValidationContext,
ValidationResult,
ThemeConfig,
ThemeConfigValidationContext,
} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-google-analytics';
import type {PluginOptions, Options} from '@docusaurus/plugin-google-analytics';
export default function pluginGoogleAnalytics(
context: LoadContext,
@ -74,13 +73,13 @@ const pluginOptionsSchema = Joi.object<PluginOptions>({
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(pluginOptionsSchema, options);
}
export function validateThemeConfig({
themeConfig,
}: ThemeConfigValidationContext<ThemeConfig>): ValidationResult<ThemeConfig> {
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
if ('googleAnalytics' in themeConfig) {
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.',

View file

@ -10,11 +10,10 @@ import type {
LoadContext,
Plugin,
OptionValidationContext,
ValidationResult,
ThemeConfig,
ThemeConfigValidationContext,
} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
import type {PluginOptions, Options} from '@docusaurus/plugin-google-gtag';
export default function pluginGoogleGtag(
context: LoadContext,
@ -88,13 +87,13 @@ const pluginOptionsSchema = Joi.object<PluginOptions>({
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(pluginOptionsSchema, options);
}
export function validateThemeConfig({
themeConfig,
}: ThemeConfigValidationContext<ThemeConfig>): ValidationResult<ThemeConfig> {
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
if ('gtag' in themeConfig) {
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.',

View file

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

View file

@ -6,11 +6,7 @@
*/
import {Joi} from '@docusaurus/utils-validation';
import type {
ThemeConfig,
ValidationResult,
OptionValidationContext,
} from '@docusaurus/types';
import type {OptionValidationContext} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-pwa';
const DEFAULT_OPTIONS = {
@ -27,7 +23,7 @@ const DEFAULT_OPTIONS = {
reloadPopup: '@theme/PwaReloadPopup',
};
export const Schema = Joi.object({
const Schema = Joi.object({
debug: Joi.bool().default(DEFAULT_OPTIONS.debug),
offlineModeActivationStrategies: Joi.array()
.items(
@ -61,6 +57,6 @@ export const Schema = Joi.object({
export function validateOptions({
validate,
options,
}: OptionValidationContext<PluginOptions>): ValidationResult<ThemeConfig> {
}: OptionValidationContext<PluginOptions, PluginOptions>): PluginOptions {
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 type {Options} from '@docusaurus/plugin-sitemap';
import createSitemap from './createSitemap';
import type {
LoadContext,
Props,
OptionValidationContext,
ValidationResult,
Plugin,
} from '@docusaurus/types';
import {PluginOptionSchema} from './pluginOptionSchema';
import type {LoadContext, Plugin} from '@docusaurus/types';
export default function pluginSitemap(
context: LoadContext,
@ -25,7 +18,7 @@ export default function pluginSitemap(
return {
name: 'docusaurus-plugin-sitemap',
async postBuild({siteConfig, routesPaths, outDir}: Props) {
async postBuild({siteConfig, routesPaths, outDir}) {
if (siteConfig.noIndex) {
return;
}
@ -47,10 +40,4 @@ export default function pluginSitemap(
};
}
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options>): ValidationResult<Options> {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}
export {validateOptions} from './options';

View file

@ -8,13 +8,14 @@
import {Joi} from '@docusaurus/utils-validation';
import {EnumChangefreq} from '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,
priority: 0.5,
};
export const PluginOptionSchema = Joi.object({
const PluginOptionSchema = Joi.object({
cacheTime: Joi.forbidden().messages({
'any.unknown':
'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.',
}),
});
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';
export type Options = {
id?: string;
/** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */
changefreq?: EnumChangefreq;
/** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */

View file

@ -5,7 +5,7 @@
* 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 {getTranslationFiles, translateThemeConfig} from './translations';
import {createRequire} from 'module';
@ -169,7 +169,7 @@ export default function docusaurusThemeClassic(
};
},
configurePostCss(postCssOptions: PostCssOptions) {
configurePostCss(postCssOptions) {
if (direction === 'rtl') {
const resolvedInfimaFile = require.resolve(getInfimaCSSFile(direction));
const plugin: PostCssPlugin = {

View file

@ -6,7 +6,10 @@
*/
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 = {
versionPersistence: 'localStorage',
@ -373,9 +376,6 @@ export const ThemeConfigSchema = Joi.object({
export function validateThemeConfig({
validate,
themeConfig,
}: {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
return validate(ThemeConfigSchema, themeConfig);
}

View file

@ -6,7 +6,10 @@
*/
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 = {
playgroundPosition: 'bottom',
@ -25,9 +28,6 @@ export const Schema = Joi.object({
export function validateThemeConfig({
validate,
themeConfig,
}: {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
return validate(Schema, themeConfig);
}

View file

@ -6,7 +6,10 @@
*/
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 = {
// enabled by default, as it makes sense in most cases
@ -45,9 +48,6 @@ export const Schema = Joi.object({
export function validateThemeConfig({
validate,
themeConfig,
}: {
validate: Validate<ThemeConfig>;
themeConfig: ThemeConfig;
}): ValidationResult<ThemeConfig> {
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
return validate(Schema, themeConfig);
}

View file

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

View file

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

View file

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

View file

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