mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 18:58:36 +02:00
fix(theme-classic): validate options properly (#7755)
* fix(theme-classic): validate options properly * improve normalization * fix doc
This commit is contained in:
parent
636d47060e
commit
cba8be01a3
5 changed files with 141 additions and 56 deletions
|
@ -7,11 +7,16 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
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 theme from 'prism-react-renderer/themes/github';
|
||||||
import darkTheme from 'prism-react-renderer/themes/dracula';
|
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 {ThemeConfig} from '@docusaurus/theme-common';
|
||||||
|
import type {Validate} from '@docusaurus/types';
|
||||||
|
|
||||||
function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
|
function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
|
||||||
return normalizeThemeConfig(ThemeConfigSchema, {
|
return normalizeThemeConfig(ThemeConfigSchema, {
|
||||||
|
@ -20,12 +25,10 @@ function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function testOk(partialThemeConfig: {[key: string]: unknown}) {
|
function testValidateOptions(options: Options) {
|
||||||
expect(
|
return validateOptions({
|
||||||
testValidateThemeConfig({...DEFAULT_CONFIG, ...partialThemeConfig}),
|
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
|
||||||
).toEqual({
|
options,
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
...partialThemeConfig,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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', () => {
|
describe('color mode config', () => {
|
||||||
const withDefaultValues = (colorMode?: ThemeConfig['colorMode']) =>
|
const withDefaultValues = (colorMode?: ThemeConfig['colorMode']) =>
|
||||||
_.merge({}, DEFAULT_CONFIG.colorMode, 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"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,7 +13,7 @@ import {getTranslationFiles, translateThemeConfig} from './translations';
|
||||||
import type {LoadContext, Plugin} 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 type {Plugin as PostCssPlugin} from 'postcss';
|
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';
|
import type webpack from 'webpack';
|
||||||
|
|
||||||
const requireFromDocusaurusCore = createRequire(
|
const requireFromDocusaurusCore = createRequire(
|
||||||
|
@ -98,7 +98,7 @@ function getInfimaCSSFile(direction: string) {
|
||||||
|
|
||||||
export default function themeClassic(
|
export default function themeClassic(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: Options,
|
options: PluginOptions,
|
||||||
): Plugin<undefined> {
|
): Plugin<undefined> {
|
||||||
const {
|
const {
|
||||||
i18n: {currentLocale, localeConfigs},
|
i18n: {currentLocale, localeConfigs},
|
||||||
|
@ -109,7 +109,7 @@ export default function themeClassic(
|
||||||
colorMode,
|
colorMode,
|
||||||
prism: {additionalLanguages},
|
prism: {additionalLanguages},
|
||||||
} = themeConfig;
|
} = themeConfig;
|
||||||
const {customCss} = options ?? {};
|
const {customCss} = options;
|
||||||
const {direction} = localeConfigs[currentLocale]!;
|
const {direction} = localeConfigs[currentLocale]!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -145,13 +145,7 @@ export default function themeClassic(
|
||||||
'./nprogress',
|
'./nprogress',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (customCss) {
|
modules.push(...customCss.map((p) => path.resolve(context.siteDir, p)));
|
||||||
modules.push(
|
|
||||||
...(Array.isArray(customCss) ? customCss : [customCss]).map((p) =>
|
|
||||||
path.resolve(context.siteDir, p),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
},
|
},
|
||||||
|
@ -211,4 +205,4 @@ ${announcementBar ? AnnouncementBarInlineJavaScript : ''}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {default as getSwizzleConfig} from './getSwizzleConfig';
|
export {default as getSwizzleConfig} from './getSwizzleConfig';
|
||||||
export {validateThemeConfig} from './validateThemeConfig';
|
export {validateThemeConfig, validateOptions} from './options';
|
||||||
|
|
|
@ -7,8 +7,12 @@
|
||||||
|
|
||||||
import defaultPrismTheme from 'prism-react-renderer/themes/palenight';
|
import defaultPrismTheme from 'prism-react-renderer/themes/palenight';
|
||||||
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
||||||
|
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
|
||||||
import type {ThemeConfig} from '@docusaurus/theme-common';
|
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'] = {
|
const DEFAULT_DOCS_CONFIG: ThemeConfig['docs'] = {
|
||||||
versionPersistence: 'localStorage',
|
versionPersistence: 'localStorage',
|
||||||
|
@ -296,10 +300,6 @@ const FooterLinkItemSchema = Joi.object({
|
||||||
// attributes like target, aria-role, data-customAttribute...)
|
// attributes like target, aria-role, data-customAttribute...)
|
||||||
.unknown();
|
.unknown();
|
||||||
|
|
||||||
const CustomCssSchema = Joi.alternatives()
|
|
||||||
.try(Joi.array().items(Joi.string().required()), Joi.string().required())
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
const LogoSchema = Joi.object({
|
const LogoSchema = Joi.object({
|
||||||
alt: Joi.string().allow(''),
|
alt: Joi.string().allow(''),
|
||||||
src: Joi.string().required(),
|
src: Joi.string().required(),
|
||||||
|
@ -324,7 +324,6 @@ export const ThemeConfigSchema = Joi.object<ThemeConfig>({
|
||||||
'any.unknown':
|
'any.unknown':
|
||||||
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
|
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
|
||||||
}),
|
}),
|
||||||
customCss: CustomCssSchema,
|
|
||||||
colorMode: ColorModeSchema,
|
colorMode: ColorModeSchema,
|
||||||
image: Joi.string(),
|
image: Joi.string(),
|
||||||
docs: DocsSchema,
|
docs: DocsSchema,
|
||||||
|
@ -442,3 +441,29 @@ export function validateThemeConfig({
|
||||||
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
||||||
return validate(ThemeConfigSchema, 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;
|
||||||
|
}
|
|
@ -23,8 +23,12 @@
|
||||||
declare module '@docusaurus/theme-classic' {
|
declare module '@docusaurus/theme-classic' {
|
||||||
import type {LoadContext, Plugin, PluginModule} from '@docusaurus/types';
|
import type {LoadContext, Plugin, PluginModule} from '@docusaurus/types';
|
||||||
|
|
||||||
|
export type PluginOptions = {
|
||||||
|
customCss: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
customCss?: string | string[];
|
customCss?: string[] | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSwizzleConfig: PluginModule['getSwizzleConfig'];
|
export const getSwizzleConfig: PluginModule['getSwizzleConfig'];
|
||||||
|
|
|
@ -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.
|
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'),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
Loading…
Add table
Reference in a new issue