fix(theme-classic): validate options properly (#7755)

* fix(theme-classic): validate options properly

* improve normalization

* fix doc
This commit is contained in:
Joshua Chen 2022-07-11 19:24:46 +08:00 committed by GitHub
parent 636d47060e
commit cba8be01a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 56 deletions

View file

@ -7,11 +7,16 @@
import _ from 'lodash';
import {normalizeThemeConfig} from '@docusaurus/utils-validation';
import {
normalizeThemeConfig,
normalizePluginOptions,
} from '@docusaurus/utils-validation';
import theme from 'prism-react-renderer/themes/github';
import darkTheme from 'prism-react-renderer/themes/dracula';
import {ThemeConfigSchema, DEFAULT_CONFIG} from '../validateThemeConfig';
import {ThemeConfigSchema, DEFAULT_CONFIG, validateOptions} from '../options';
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
import type {ThemeConfig} from '@docusaurus/theme-common';
import type {Validate} from '@docusaurus/types';
function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
return normalizeThemeConfig(ThemeConfigSchema, {
@ -20,12 +25,10 @@ function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
});
}
function testOk(partialThemeConfig: {[key: string]: unknown}) {
expect(
testValidateThemeConfig({...DEFAULT_CONFIG, ...partialThemeConfig}),
).toEqual({
...DEFAULT_CONFIG,
...partialThemeConfig,
function testValidateOptions(options: Options) {
return validateOptions({
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
options,
});
}
@ -642,36 +645,6 @@ describe('themeConfig', () => {
});
});
describe('customCss config', () => {
it('accepts customCss undefined', () => {
testOk({
customCss: undefined,
});
});
it('accepts customCss string', () => {
testOk({
customCss: './path/to/cssFile.css',
});
});
it('accepts customCss string array', () => {
testOk({
customCss: ['./path/to/cssFile.css', './path/to/cssFile2.css'],
});
});
it('rejects customCss number', () => {
expect(() =>
testValidateThemeConfig({
customCss: 42,
}),
).toThrowErrorMatchingInlineSnapshot(
`""customCss" must be one of [array, string]"`,
);
});
});
describe('color mode config', () => {
const withDefaultValues = (colorMode?: ThemeConfig['colorMode']) =>
_.merge({}, DEFAULT_CONFIG.colorMode, colorMode);
@ -849,3 +822,51 @@ describe('themeConfig', () => {
});
});
});
describe('validateOptions', () => {
describe('customCss config', () => {
it('accepts customCss undefined', () => {
expect(
testValidateOptions({
customCss: undefined,
}),
).toEqual({
id: 'default',
customCss: [],
});
});
it('accepts customCss string', () => {
expect(
testValidateOptions({
customCss: './path/to/cssFile.css',
}),
).toEqual({
id: 'default',
customCss: ['./path/to/cssFile.css'],
});
});
it('accepts customCss string array', () => {
expect(
testValidateOptions({
customCss: ['./path/to/cssFile.css', './path/to/cssFile2.css'],
}),
).toEqual({
id: 'default',
customCss: ['./path/to/cssFile.css', './path/to/cssFile2.css'],
});
});
it('rejects customCss number', () => {
expect(() =>
testValidateOptions({
// @ts-expect-error: test
customCss: 42,
}),
).toThrowErrorMatchingInlineSnapshot(
`""customCss" must be a string or an array of strings"`,
);
});
});
});

View file

@ -13,7 +13,7 @@ import {getTranslationFiles, translateThemeConfig} from './translations';
import type {LoadContext, Plugin} from '@docusaurus/types';
import type {ThemeConfig} from '@docusaurus/theme-common';
import type {Plugin as PostCssPlugin} from 'postcss';
import type {Options} from '@docusaurus/theme-classic';
import type {PluginOptions} from '@docusaurus/theme-classic';
import type webpack from 'webpack';
const requireFromDocusaurusCore = createRequire(
@ -98,7 +98,7 @@ function getInfimaCSSFile(direction: string) {
export default function themeClassic(
context: LoadContext,
options: Options,
options: PluginOptions,
): Plugin<undefined> {
const {
i18n: {currentLocale, localeConfigs},
@ -109,7 +109,7 @@ export default function themeClassic(
colorMode,
prism: {additionalLanguages},
} = themeConfig;
const {customCss} = options ?? {};
const {customCss} = options;
const {direction} = localeConfigs[currentLocale]!;
return {
@ -145,13 +145,7 @@ export default function themeClassic(
'./nprogress',
];
if (customCss) {
modules.push(
...(Array.isArray(customCss) ? customCss : [customCss]).map((p) =>
path.resolve(context.siteDir, p),
),
);
}
modules.push(...customCss.map((p) => path.resolve(context.siteDir, p)));
return modules;
},
@ -211,4 +205,4 @@ ${announcementBar ? AnnouncementBarInlineJavaScript : ''}
}
export {default as getSwizzleConfig} from './getSwizzleConfig';
export {validateThemeConfig} from './validateThemeConfig';
export {validateThemeConfig, validateOptions} from './options';

View file

@ -7,8 +7,12 @@
import defaultPrismTheme from 'prism-react-renderer/themes/palenight';
import {Joi, URISchema} from '@docusaurus/utils-validation';
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
import type {ThemeConfig} from '@docusaurus/theme-common';
import type {ThemeConfigValidationContext} from '@docusaurus/types';
import type {
ThemeConfigValidationContext,
OptionValidationContext,
} from '@docusaurus/types';
const DEFAULT_DOCS_CONFIG: ThemeConfig['docs'] = {
versionPersistence: 'localStorage',
@ -296,10 +300,6 @@ const FooterLinkItemSchema = Joi.object({
// attributes like target, aria-role, data-customAttribute...)
.unknown();
const CustomCssSchema = Joi.alternatives()
.try(Joi.array().items(Joi.string().required()), Joi.string().required())
.optional();
const LogoSchema = Joi.object({
alt: Joi.string().allow(''),
src: Joi.string().required(),
@ -324,7 +324,6 @@ export const ThemeConfigSchema = Joi.object<ThemeConfig>({
'any.unknown':
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
}),
customCss: CustomCssSchema,
colorMode: ColorModeSchema,
image: Joi.string(),
docs: DocsSchema,
@ -442,3 +441,29 @@ export function validateThemeConfig({
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
return validate(ThemeConfigSchema, themeConfig);
}
const DEFAULT_OPTIONS = {
customCss: [],
};
const PluginOptionSchema = Joi.object<PluginOptions>({
customCss: Joi.alternatives()
.try(
Joi.array().items(Joi.string().required()),
Joi.alternatives().conditional(Joi.string().required(), {
then: Joi.custom((val: string) => [val]),
otherwise: Joi.forbidden().messages({
'any.unknown': '"customCss" must be a string or an array of strings',
}),
}),
)
.default(DEFAULT_OPTIONS.customCss),
});
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
const validatedOptions = validate(PluginOptionSchema, options);
return validatedOptions;
}

View file

@ -23,8 +23,12 @@
declare module '@docusaurus/theme-classic' {
import type {LoadContext, Plugin, PluginModule} from '@docusaurus/types';
export type PluginOptions = {
customCss: string[];
};
export type Options = {
customCss?: string | string[];
customCss?: string[] | string;
};
export const getSwizzleConfig: PluginModule['getSwizzleConfig'];

View file

@ -18,3 +18,44 @@ npm install --save @docusaurus/theme-classic
If you have installed `@docusaurus/preset-classic`, you don't need to install it as a dependency.
:::
## Configuration {#configuration}
Accepted fields:
```mdx-code-block
<APITable>
```
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `customCss` | <code>string[] \| string</code> | `[]` | Stylesheets to be imported globally as [client modules](../../advanced/client.md#client-modules). Relative paths are resolved against the site directory. |
```mdx-code-block
</APITable>
```
:::note
Most configuration for the theme is done in `themeConfig`, which can be found in [theme configuration](./theme-configuration.md).
:::
### Example configuration {#ex-config}
You can configure this theme through preset options or plugin options.
:::tip
Most Docusaurus users configure this plugin through the preset options.
:::
```js config-tabs
// Preset Options: theme
// Plugin Options: @docusaurus/theme-classic
const config = {
customCss: require.resolve('./src/css/custom.css'),
};
```