Skip to content

Commit

Permalink
fix(theme-classic): validate options properly (#7755)
Browse files Browse the repository at this point in the history
* fix(theme-classic): validate options properly

* improve normalization

* fix doc
  • Loading branch information
Josh-Cena committed Jul 11, 2022
1 parent 636d470 commit cba8be0
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 56 deletions.
Expand Up @@ -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, {
Expand All @@ -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,
});
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"`,
);
});
});
});
16 changes: 5 additions & 11 deletions packages/docusaurus-theme-classic/src/index.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -98,7 +98,7 @@ function getInfimaCSSFile(direction: string) {

export default function themeClassic(
context: LoadContext,
options: Options,
options: PluginOptions,
): Plugin<undefined> {
const {
i18n: {currentLocale, localeConfigs},
Expand All @@ -109,7 +109,7 @@ export default function themeClassic(
colorMode,
prism: {additionalLanguages},
} = themeConfig;
const {customCss} = options ?? {};
const {customCss} = options;
const {direction} = localeConfigs[currentLocale]!;

return {
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -211,4 +205,4 @@ ${announcementBar ? AnnouncementBarInlineJavaScript : ''}
}

export {default as getSwizzleConfig} from './getSwizzleConfig';
export {validateThemeConfig} from './validateThemeConfig';
export {validateThemeConfig, validateOptions} from './options';
Expand Up @@ -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',
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
6 changes: 5 additions & 1 deletion packages/docusaurus-theme-classic/src/theme-classic.d.ts
Expand Up @@ -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'];
Expand Down
41 changes: 41 additions & 0 deletions website/docs/api/themes/theme-classic.md
Expand Up @@ -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'),
};
```

0 comments on commit cba8be0

Please sign in to comment.