Skip to content

Commit cba8be0

Browse files
authoredJul 11, 2022
fix(theme-classic): validate options properly (#7755)
* fix(theme-classic): validate options properly * improve normalization * fix doc
1 parent 636d470 commit cba8be0

File tree

5 files changed

+141
-56
lines changed

5 files changed

+141
-56
lines changed
 

‎packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts ‎packages/docusaurus-theme-classic/src/__tests__/options.test.ts

+59-38
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77

88
import _ from 'lodash';
99

10-
import {normalizeThemeConfig} from '@docusaurus/utils-validation';
10+
import {
11+
normalizeThemeConfig,
12+
normalizePluginOptions,
13+
} from '@docusaurus/utils-validation';
1114
import theme from 'prism-react-renderer/themes/github';
1215
import darkTheme from 'prism-react-renderer/themes/dracula';
13-
import {ThemeConfigSchema, DEFAULT_CONFIG} from '../validateThemeConfig';
16+
import {ThemeConfigSchema, DEFAULT_CONFIG, validateOptions} from '../options';
17+
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
1418
import type {ThemeConfig} from '@docusaurus/theme-common';
19+
import type {Validate} from '@docusaurus/types';
1520

1621
function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
1722
return normalizeThemeConfig(ThemeConfigSchema, {
@@ -20,12 +25,10 @@ function testValidateThemeConfig(partialThemeConfig: {[key: string]: unknown}) {
2025
});
2126
}
2227

23-
function testOk(partialThemeConfig: {[key: string]: unknown}) {
24-
expect(
25-
testValidateThemeConfig({...DEFAULT_CONFIG, ...partialThemeConfig}),
26-
).toEqual({
27-
...DEFAULT_CONFIG,
28-
...partialThemeConfig,
28+
function testValidateOptions(options: Options) {
29+
return validateOptions({
30+
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
31+
options,
2932
});
3033
}
3134

@@ -642,36 +645,6 @@ describe('themeConfig', () => {
642645
});
643646
});
644647

645-
describe('customCss config', () => {
646-
it('accepts customCss undefined', () => {
647-
testOk({
648-
customCss: undefined,
649-
});
650-
});
651-
652-
it('accepts customCss string', () => {
653-
testOk({
654-
customCss: './path/to/cssFile.css',
655-
});
656-
});
657-
658-
it('accepts customCss string array', () => {
659-
testOk({
660-
customCss: ['./path/to/cssFile.css', './path/to/cssFile2.css'],
661-
});
662-
});
663-
664-
it('rejects customCss number', () => {
665-
expect(() =>
666-
testValidateThemeConfig({
667-
customCss: 42,
668-
}),
669-
).toThrowErrorMatchingInlineSnapshot(
670-
`""customCss" must be one of [array, string]"`,
671-
);
672-
});
673-
});
674-
675648
describe('color mode config', () => {
676649
const withDefaultValues = (colorMode?: ThemeConfig['colorMode']) =>
677650
_.merge({}, DEFAULT_CONFIG.colorMode, colorMode);
@@ -849,3 +822,51 @@ describe('themeConfig', () => {
849822
});
850823
});
851824
});
825+
826+
describe('validateOptions', () => {
827+
describe('customCss config', () => {
828+
it('accepts customCss undefined', () => {
829+
expect(
830+
testValidateOptions({
831+
customCss: undefined,
832+
}),
833+
).toEqual({
834+
id: 'default',
835+
customCss: [],
836+
});
837+
});
838+
839+
it('accepts customCss string', () => {
840+
expect(
841+
testValidateOptions({
842+
customCss: './path/to/cssFile.css',
843+
}),
844+
).toEqual({
845+
id: 'default',
846+
customCss: ['./path/to/cssFile.css'],
847+
});
848+
});
849+
850+
it('accepts customCss string array', () => {
851+
expect(
852+
testValidateOptions({
853+
customCss: ['./path/to/cssFile.css', './path/to/cssFile2.css'],
854+
}),
855+
).toEqual({
856+
id: 'default',
857+
customCss: ['./path/to/cssFile.css', './path/to/cssFile2.css'],
858+
});
859+
});
860+
861+
it('rejects customCss number', () => {
862+
expect(() =>
863+
testValidateOptions({
864+
// @ts-expect-error: test
865+
customCss: 42,
866+
}),
867+
).toThrowErrorMatchingInlineSnapshot(
868+
`""customCss" must be a string or an array of strings"`,
869+
);
870+
});
871+
});
872+
});

‎packages/docusaurus-theme-classic/src/index.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {getTranslationFiles, translateThemeConfig} from './translations';
1313
import type {LoadContext, Plugin} from '@docusaurus/types';
1414
import type {ThemeConfig} from '@docusaurus/theme-common';
1515
import type {Plugin as PostCssPlugin} from 'postcss';
16-
import type {Options} from '@docusaurus/theme-classic';
16+
import type {PluginOptions} from '@docusaurus/theme-classic';
1717
import type webpack from 'webpack';
1818

1919
const requireFromDocusaurusCore = createRequire(
@@ -98,7 +98,7 @@ function getInfimaCSSFile(direction: string) {
9898

9999
export default function themeClassic(
100100
context: LoadContext,
101-
options: Options,
101+
options: PluginOptions,
102102
): Plugin<undefined> {
103103
const {
104104
i18n: {currentLocale, localeConfigs},
@@ -109,7 +109,7 @@ export default function themeClassic(
109109
colorMode,
110110
prism: {additionalLanguages},
111111
} = themeConfig;
112-
const {customCss} = options ?? {};
112+
const {customCss} = options;
113113
const {direction} = localeConfigs[currentLocale]!;
114114

115115
return {
@@ -145,13 +145,7 @@ export default function themeClassic(
145145
'./nprogress',
146146
];
147147

148-
if (customCss) {
149-
modules.push(
150-
...(Array.isArray(customCss) ? customCss : [customCss]).map((p) =>
151-
path.resolve(context.siteDir, p),
152-
),
153-
);
154-
}
148+
modules.push(...customCss.map((p) => path.resolve(context.siteDir, p)));
155149

156150
return modules;
157151
},
@@ -211,4 +205,4 @@ ${announcementBar ? AnnouncementBarInlineJavaScript : ''}
211205
}
212206

213207
export {default as getSwizzleConfig} from './getSwizzleConfig';
214-
export {validateThemeConfig} from './validateThemeConfig';
208+
export {validateThemeConfig, validateOptions} from './options';

‎packages/docusaurus-theme-classic/src/validateThemeConfig.ts ‎packages/docusaurus-theme-classic/src/options.ts

+31-6
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77

88
import defaultPrismTheme from 'prism-react-renderer/themes/palenight';
99
import {Joi, URISchema} from '@docusaurus/utils-validation';
10+
import type {Options, PluginOptions} from '@docusaurus/theme-classic';
1011
import type {ThemeConfig} from '@docusaurus/theme-common';
11-
import type {ThemeConfigValidationContext} from '@docusaurus/types';
12+
import type {
13+
ThemeConfigValidationContext,
14+
OptionValidationContext,
15+
} from '@docusaurus/types';
1216

1317
const DEFAULT_DOCS_CONFIG: ThemeConfig['docs'] = {
1418
versionPersistence: 'localStorage',
@@ -296,10 +300,6 @@ const FooterLinkItemSchema = Joi.object({
296300
// attributes like target, aria-role, data-customAttribute...)
297301
.unknown();
298302

299-
const CustomCssSchema = Joi.alternatives()
300-
.try(Joi.array().items(Joi.string().required()), Joi.string().required())
301-
.optional();
302-
303303
const LogoSchema = Joi.object({
304304
alt: Joi.string().allow(''),
305305
src: Joi.string().required(),
@@ -324,7 +324,6 @@ export const ThemeConfigSchema = Joi.object<ThemeConfig>({
324324
'any.unknown':
325325
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
326326
}),
327-
customCss: CustomCssSchema,
328327
colorMode: ColorModeSchema,
329328
image: Joi.string(),
330329
docs: DocsSchema,
@@ -442,3 +441,29 @@ export function validateThemeConfig({
442441
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
443442
return validate(ThemeConfigSchema, themeConfig);
444443
}
444+
445+
const DEFAULT_OPTIONS = {
446+
customCss: [],
447+
};
448+
449+
const PluginOptionSchema = Joi.object<PluginOptions>({
450+
customCss: Joi.alternatives()
451+
.try(
452+
Joi.array().items(Joi.string().required()),
453+
Joi.alternatives().conditional(Joi.string().required(), {
454+
then: Joi.custom((val: string) => [val]),
455+
otherwise: Joi.forbidden().messages({
456+
'any.unknown': '"customCss" must be a string or an array of strings',
457+
}),
458+
}),
459+
)
460+
.default(DEFAULT_OPTIONS.customCss),
461+
});
462+
463+
export function validateOptions({
464+
validate,
465+
options,
466+
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
467+
const validatedOptions = validate(PluginOptionSchema, options);
468+
return validatedOptions;
469+
}

‎packages/docusaurus-theme-classic/src/theme-classic.d.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@
2323
declare module '@docusaurus/theme-classic' {
2424
import type {LoadContext, Plugin, PluginModule} from '@docusaurus/types';
2525

26+
export type PluginOptions = {
27+
customCss: string[];
28+
};
29+
2630
export type Options = {
27-
customCss?: string | string[];
31+
customCss?: string[] | string;
2832
};
2933

3034
export const getSwizzleConfig: PluginModule['getSwizzleConfig'];

‎website/docs/api/themes/theme-classic.md

+41
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,44 @@ npm install --save @docusaurus/theme-classic
1818
If you have installed `@docusaurus/preset-classic`, you don't need to install it as a dependency.
1919

2020
:::
21+
22+
## Configuration {#configuration}
23+
24+
Accepted fields:
25+
26+
```mdx-code-block
27+
<APITable>
28+
```
29+
30+
| Option | Type | Default | Description |
31+
| --- | --- | --- | --- |
32+
| `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. |
33+
34+
```mdx-code-block
35+
</APITable>
36+
```
37+
38+
:::note
39+
40+
Most configuration for the theme is done in `themeConfig`, which can be found in [theme configuration](./theme-configuration.md).
41+
42+
:::
43+
44+
### Example configuration {#ex-config}
45+
46+
You can configure this theme through preset options or plugin options.
47+
48+
:::tip
49+
50+
Most Docusaurus users configure this plugin through the preset options.
51+
52+
:::
53+
54+
```js config-tabs
55+
// Preset Options: theme
56+
// Plugin Options: @docusaurus/theme-classic
57+
58+
const config = {
59+
customCss: require.resolve('./src/css/custom.css'),
60+
};
61+
```

0 commit comments

Comments
 (0)
Please sign in to comment.