diff --git a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts b/packages/docusaurus-theme-classic/src/__tests__/options.test.ts similarity index 95% rename from packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts rename to packages/docusaurus-theme-classic/src/__tests__/options.test.ts index 6be029ae904a..0e9d13b08db3 100644 --- a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.ts +++ b/packages/docusaurus-theme-classic/src/__tests__/options.test.ts @@ -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, }); } @@ -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"`, + ); + }); + }); +}); diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts index 612a5ebd6f13..b85fddbbf2c3 100644 --- a/packages/docusaurus-theme-classic/src/index.ts +++ b/packages/docusaurus-theme-classic/src/index.ts @@ -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 { 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'; diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/options.ts similarity index 93% rename from packages/docusaurus-theme-classic/src/validateThemeConfig.ts rename to packages/docusaurus-theme-classic/src/options.ts index a5a91a2dcb30..a298f10760e6 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/options.ts @@ -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({ '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 { return validate(ThemeConfigSchema, themeConfig); } + +const DEFAULT_OPTIONS = { + customCss: [], +}; + +const PluginOptionSchema = Joi.object({ + 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): PluginOptions { + const validatedOptions = validate(PluginOptionSchema, options); + return validatedOptions; +} diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 8922ffce09f7..e0f864266cf0 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -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']; diff --git a/website/docs/api/themes/theme-classic.md b/website/docs/api/themes/theme-classic.md index 900f69cfa1d7..cba54b875aeb 100644 --- a/website/docs/api/themes/theme-classic.md +++ b/website/docs/api/themes/theme-classic.md @@ -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 + +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `customCss` | string[] \| string | `[]` | Stylesheets to be imported globally as [client modules](../../advanced/client.md#client-modules). Relative paths are resolved against the site directory. | + +```mdx-code-block + +``` + +:::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'), +}; +```