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

@ -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([