From 2d55ca474eb8f29bb88b41af2a3a273b73aa75a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Mon, 19 Aug 2024 18:30:52 +0200 Subject: [PATCH] fix(types): fix type of PluginModule (#10420) --- packages/docusaurus-types/src/plugin.d.ts | 3 +- .../configValidation.test.ts.snap | 278 ++++----- .../server/__tests__/configValidation.test.ts | 530 ++++++++++-------- 3 files changed, 445 insertions(+), 366 deletions(-) diff --git a/packages/docusaurus-types/src/plugin.d.ts b/packages/docusaurus-types/src/plugin.d.ts index 33ce5bb32b..249c90e6c2 100644 --- a/packages/docusaurus-types/src/plugin.d.ts +++ b/packages/docusaurus-types/src/plugin.d.ts @@ -191,9 +191,8 @@ export type LoadedPlugin = InitializedPlugin & { export type PluginModule = { (context: LoadContext, options: unknown): | Plugin - | Promise> | null - | Promise; + | Promise | null>; validateOptions?: (data: OptionValidationContext) => U; validateThemeConfig?: (data: ThemeConfigValidationContext) => T; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap index 6dca123a95..d298a46eb8 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap @@ -1,143 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`normalizeConfig should throw error if plugins is not a string and it's not an array #1 for the input of: [123] 1`] = ` -" => Bad Docusaurus plugin value plugins[0]. -Example valid plugin config: -{ - plugins: [ - ["@docusaurus/plugin-content-docs",options], - "./myPlugin", - ["./myPlugin",{someOption: 42}], - function myPlugin() { }, - [function myPlugin() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = ` -" => Bad Docusaurus plugin value plugins[0]. -Example valid plugin config: -{ - plugins: [ - ["@docusaurus/plugin-content-docs",options], - "./myPlugin", - ["./myPlugin",{someOption: 42}], - function myPlugin() { }, - [function myPlugin() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = ` -" => Bad Docusaurus plugin value plugins[0]. -Example valid plugin config: -{ - plugins: [ - ["@docusaurus/plugin-content-docs",options], - "./myPlugin", - ["./myPlugin",{someOption: 42}], - function myPlugin() { }, - [function myPlugin() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = ` -" => Bad Docusaurus plugin value plugins[0]. -Example valid plugin config: -{ - plugins: [ - ["@docusaurus/plugin-content-docs",options], - "./myPlugin", - ["./myPlugin",{someOption: 42}], - function myPlugin() { }, - [function myPlugin() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if plugins is not array for the input of: {} 1`] = ` -""plugins" must be an array -" -`; - -exports[`normalizeConfig should throw error if themes is not a string and it's not an array #1 for the input of: [123] 1`] = ` -" => Bad Docusaurus theme value themes[0]. -Example valid theme config: -{ - themes: [ - ["@docusaurus/theme-classic",options], - "./myTheme", - ["./myTheme",{someOption: 42}], - function myTheme() { }, - [function myTheme() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if themes is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = ` -" => Bad Docusaurus theme value themes[0]. -Example valid theme config: -{ - themes: [ - ["@docusaurus/theme-classic",options], - "./myTheme", - ["./myTheme",{someOption: 42}], - function myTheme() { }, - [function myTheme() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if themes is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = ` -" => Bad Docusaurus theme value themes[0]. -Example valid theme config: -{ - themes: [ - ["@docusaurus/theme-classic",options], - "./myTheme", - ["./myTheme",{someOption: 42}], - function myTheme() { }, - [function myTheme() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if themes is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = ` -" => Bad Docusaurus theme value themes[0]. -Example valid theme config: -{ - themes: [ - ["@docusaurus/theme-classic",options], - "./myTheme", - ["./myTheme",{someOption: 42}], - function myTheme() { }, - [function myTheme() { },options] - ], -}; - -" -`; - -exports[`normalizeConfig should throw error if themes is not array for the input of: {} 1`] = ` -""themes" must be an array -" -`; - exports[`normalizeConfig throws error for required fields 1`] = ` ""baseUrl" is required "title" is required @@ -157,7 +19,145 @@ If you still want these fields to be in your configuration, put them in the "cus See https://docusaurus.io/docs/api/docusaurus-config/#customfields" `; -exports[`normalizeConfig throws for "error" reporting severity 1`] = ` +exports[`onBrokenLinks throws for "error" reporting severity 1`] = ` ""onBrokenLinks" must be one of [ignore, log, warn, throw] " `; + +exports[`plugins should throw error if plugins is not a string and it's not an array #1 for the input of: [123] 1`] = ` +" => Bad Docusaurus plugin value plugins[0]. +Example valid plugin config: +{ + plugins: [ + ["@docusaurus/plugin-content-docs",options], + "./myPlugin", + ["./myPlugin",{someOption: 42}], + function myPlugin() { }, + [function myPlugin() { },options] + ], +}; + +" +`; + +exports[`plugins should throw error if plugins is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = ` +" => Bad Docusaurus plugin value plugins[0]. +Example valid plugin config: +{ + plugins: [ + ["@docusaurus/plugin-content-docs",options], + "./myPlugin", + ["./myPlugin",{someOption: 42}], + function myPlugin() { }, + [function myPlugin() { },options] + ], +}; + +" +`; + +exports[`plugins should throw error if plugins is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = ` +" => Bad Docusaurus plugin value plugins[0]. +Example valid plugin config: +{ + plugins: [ + ["@docusaurus/plugin-content-docs",options], + "./myPlugin", + ["./myPlugin",{someOption: 42}], + function myPlugin() { }, + [function myPlugin() { },options] + ], +}; + +" +`; + +exports[`plugins should throw error if plugins is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = ` +" => Bad Docusaurus plugin value plugins[0]. +Example valid plugin config: +{ + plugins: [ + ["@docusaurus/plugin-content-docs",options], + "./myPlugin", + ["./myPlugin",{someOption: 42}], + function myPlugin() { }, + [function myPlugin() { },options] + ], +}; + +" +`; + +exports[`plugins should throw error if plugins is not array for the input of: {} 1`] = ` +""plugins" must be an array +" +`; + +exports[`themes should throw error if themes is not a string and it's not an array #1 for the input of: [123] 1`] = ` +" => Bad Docusaurus theme value themes[0]. +Example valid theme config: +{ + themes: [ + ["@docusaurus/theme-classic",options], + "./myTheme", + ["./myTheme",{someOption: 42}], + function myTheme() { }, + [function myTheme() { },options] + ], +}; + +" +`; + +exports[`themes should throw error if themes is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = ` +" => Bad Docusaurus theme value themes[0]. +Example valid theme config: +{ + themes: [ + ["@docusaurus/theme-classic",options], + "./myTheme", + ["./myTheme",{someOption: 42}], + function myTheme() { }, + [function myTheme() { },options] + ], +}; + +" +`; + +exports[`themes should throw error if themes is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = ` +" => Bad Docusaurus theme value themes[0]. +Example valid theme config: +{ + themes: [ + ["@docusaurus/theme-classic",options], + "./myTheme", + ["./myTheme",{someOption: 42}], + function myTheme() { }, + [function myTheme() { },options] + ], +}; + +" +`; + +exports[`themes should throw error if themes is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = ` +" => Bad Docusaurus theme value themes[0]. +Example valid theme config: +{ + themes: [ + ["@docusaurus/theme-classic",options], + "./myTheme", + ["./myTheme",{someOption: 42}], + function myTheme() { }, + [function myTheme() { },options] + ], +}; + +" +`; + +exports[`themes should throw error if themes is not array for the input of: {} 1`] = ` +""themes" must be an array +" +`; diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 3fb607deba..b6d210a3ba 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -12,7 +12,7 @@ import { validateConfig, } from '../configValidation'; import type {StorageConfig} from '@docusaurus/types/src/config'; -import type {Config, DocusaurusConfig} from '@docusaurus/types'; +import type {Config, DocusaurusConfig, PluginConfig} from '@docusaurus/types'; import type {DeepPartial} from 'utility-types'; const baseConfig = { @@ -114,6 +114,34 @@ describe('normalizeConfig', () => { }).toThrowErrorMatchingSnapshot(); }); + it('throws error for required fields', () => { + expect(() => + validateConfig( + { + invalidField: true, + presets: {}, + stylesheets: {}, + themes: {}, + scripts: {}, + }, + 'docusaurus.config.js', + ), + ).toThrowErrorMatchingSnapshot(); + }); +}); + +describe('config warning and error', () => { + function getWarning(config: unknown) { + return ConfigSchema.validate(config).warning; + } + + it('baseConfig has no warning', () => { + const warning = getWarning(baseConfig); + expect(warning).toBeUndefined(); + }); +}); + +describe('url', () => { it('throws for non-string URLs', () => { expect(() => normalizeConfig({ @@ -180,195 +208,19 @@ describe('normalizeConfig', () => { ).toBe('/noSlash/foo/'); }); - it.each([ - ['should throw error if plugins is not array', {}], - [ - "should throw error if plugins is not a string and it's not an array #1", - [123], - ], - [ - 'should throw error if plugins is not an array of [string, object][] #1', - [['example/path', 'wrong parameter here']], - ], - [ - 'should throw error if plugins is not an array of [string, object][] #2', - [[{}, 'example/path']], - ], - [ - 'should throw error if plugins is not an array of [string, object][] #3', - [[{}, {}]], - ], - ])(`%s for the input of: %p`, (_message, plugins) => { - expect(() => { - normalizeConfig({ - // @ts-expect-error: test - plugins, - }); - }).toThrowErrorMatchingSnapshot(); - }); - - it.each([ - ['should throw error if themes is not array', {}], - [ - "should throw error if themes is not a string and it's not an array #1", - [123], - ], - [ - 'should throw error if themes is not an array of [string, object][] #1', - [['example/path', 'wrong parameter here']], - ], - [ - 'should throw error if themes is not an array of [string, object][] #2', - [[{}, 'example/path']], - ], - [ - 'should throw error if themes is not an array of [string, object][] #3', - [[{}, {}]], - ], - ])(`%s for the input of: %p`, (_message, themes) => { - expect(() => { - normalizeConfig({ - // @ts-expect-error: test - themes, - }); - }).toThrowErrorMatchingSnapshot(); - }); - - it.each([ - ['should accept [string] for plugins', ['plain/string']], - [ - 'should accept string[] for plugins', - ['plain/string', 'another/plain/string/path'], - ], - [ - 'should accept [string, object] for plugins', - [['plain/string', {it: 'should work'}]], - ], - [ - 'should accept [string, object][] for plugins', - [ - ['plain/string', {it: 'should work'}], - ['this/should/work', {too: 'yes'}], - ], - ], - [ - 'should accept ([string, object]|string)[] for plugins', - [ - 'plain/string', - ['plain', {it: 'should work'}], - ['this/should/work', {too: 'yes'}], - ], - ], - ['should accept function for plugin', [function plugin() {}]], - [ - 'should accept [function, object] for plugin', - [[() => {}, {it: 'should work'}]], - ], - ['should accept false/null for plugin', [false as const, null, 'classic']], - ])(`%s for the input of: %p`, (_message, plugins) => { - expect(() => { - normalizeConfig({ - plugins, - } as Config); - }).not.toThrow(); - }); - - it.each([ - ['should accept [string] for themes', ['plain/string']], - [ - 'should accept string[] for themes', - ['plain/string', 'another/plain/string/path'], - ], - [ - 'should accept [string, object] for themes', - [['plain/string', {it: 'should work'}]], - ], - [ - 'should accept [string, object][] for themes', - [ - ['plain/string', {it: 'should work'}], - ['this/should/work', {too: 'yes'}], - ], - ], - [ - 'should accept ([string, object]|string)[] for themes', - [ - 'plain/string', - ['plain', {it: 'should work'}], - ['this/should/work', {too: 'yes'}], - ], - ], - ['should accept function for theme', [function theme() {}]], - [ - 'should accept [function, object] for theme', - [[function theme() {}, {it: 'should work'}]], - ], - ['should accept false/null for themes', [false, null, 'classic']], - ])(`%s for the input of: %p`, (_message, themes) => { - expect(() => { - normalizeConfig({ - themes, - } as Config); - }).not.toThrow(); - }); - - it('throws error if themes is not array', () => { - expect(() => { - normalizeConfig({ - // @ts-expect-error: test - themes: {}, - }); - }).toThrowErrorMatchingInlineSnapshot(` - ""themes" must be an array - " - `); - }); - - it('throws error if presets is not array', () => { - expect(() => { - normalizeConfig({ - // @ts-expect-error: test - presets: {}, - }); - }).toThrowErrorMatchingInlineSnapshot(` - ""presets" must be an array - " - `); - }); - - it('throws error if presets looks invalid', () => { - expect(() => { - normalizeConfig({ - // @ts-expect-error: test - presets: [() => {}], - }); - }).toThrowErrorMatchingInlineSnapshot(` - ""presets[0]" does not look like a valid preset config. A preset config entry should be one of: - - A tuple of [presetName, options], like \`["classic", { blog: false }]\`, or - - A simple string, like \`"classic"\` - " - `); - }); - - it('accepts presets as false / null', () => { - expect(() => { - normalizeConfig({ - presets: [false, null, 'classic'], - }); - }).not.toThrow(); - }); - - it("throws error if scripts doesn't have src", () => { - expect(() => { - normalizeConfig({ - scripts: ['https://some.com', {}], - }); - }).toThrowErrorMatchingInlineSnapshot(` - ""scripts[1]" is invalid. A script must be a plain string (the src), or an object with at least a "src" property. - " - `); + it('site url fails validation when using subpath', () => { + const {error} = ConfigSchema.validate({ + ...baseConfig, + url: 'https://mysite.com/someSubpath', + }); + expect(error).toBeDefined(); + expect(error?.message).toBe( + 'The url is not supposed to contain a sub-path like "/someSubpath". Please use the baseUrl field for sub-paths.', + ); }); +}); +describe('headTags', () => { it('accepts headTags with tagName and attributes', () => { expect(() => { normalizeConfig({ @@ -436,7 +288,9 @@ describe('normalizeConfig', () => { " `); }); +}); +describe('css', () => { it("throws error if css doesn't have href", () => { expect(() => { normalizeConfig({ @@ -447,22 +301,22 @@ describe('normalizeConfig', () => { " `); }); +}); - it('throws error for required fields', () => { - expect(() => - validateConfig( - { - invalidField: true, - presets: {}, - stylesheets: {}, - themes: {}, - scripts: {}, - }, - 'docusaurus.config.js', - ), - ).toThrowErrorMatchingSnapshot(); +describe('scripts', () => { + it("throws error if scripts doesn't have src", () => { + expect(() => { + normalizeConfig({ + scripts: ['https://some.com', {}], + }); + }).toThrowErrorMatchingInlineSnapshot(` + ""scripts[1]" is invalid. A script must be a plain string (the src), or an object with at least a "src" property. + " + `); }); +}); +describe('onBrokenLinks', () => { it('throws for "error" reporting severity', () => { expect(() => validateConfig( @@ -478,28 +332,6 @@ describe('normalizeConfig', () => { }); }); -describe('config warning and error', () => { - function getWarning(config: unknown) { - return ConfigSchema.validate(config).warning; - } - - it('baseConfig has no warning', () => { - const warning = getWarning(baseConfig); - expect(warning).toBeUndefined(); - }); - - it('site url fails validation when using subpath', () => { - const {error} = ConfigSchema.validate({ - ...baseConfig, - url: 'https://mysite.com/someSubpath', - }); - expect(error).toBeDefined(); - expect(error?.message).toBe( - 'The url is not supposed to contain a sub-path like "/someSubpath". Please use the baseUrl field for sub-paths.', - ); - }); -}); - describe('markdown', () => { it('accepts undefined object', () => { expect( @@ -597,8 +429,14 @@ describe('markdown', () => { }); it('throw for bad markdown format', () => { - expect(() => normalizeConfig({markdown: {format: null}})) - .toThrowErrorMatchingInlineSnapshot(` + expect(() => + normalizeConfig({ + markdown: { + // @ts-expect-error: bad value + format: null, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` ""markdown.format" must be one of [mdx, md, detect] "markdown.format" must be a string " @@ -617,6 +455,7 @@ describe('markdown', () => { it('throw for null object', () => { expect(() => { normalizeConfig({ + // @ts-expect-error: bad value markdown: null, }); }).toThrowErrorMatchingInlineSnapshot(` @@ -626,6 +465,245 @@ describe('markdown', () => { }); }); +describe('plugins', () => { + // Only here to verify typing + function ensurePlugins(plugins: PluginConfig[]): PluginConfig[] { + return plugins; + } + + it.each([ + ['should throw error if plugins is not array', {}], + [ + "should throw error if plugins is not a string and it's not an array #1", + [123], + ], + [ + 'should throw error if plugins is not an array of [string, object][] #1', + [['example/path', 'wrong parameter here']], + ], + [ + 'should throw error if plugins is not an array of [string, object][] #2', + [[{}, 'example/path']], + ], + [ + 'should throw error if plugins is not an array of [string, object][] #3', + [[{}, {}]], + ], + ])(`%s for the input of: %p`, (_message, plugins) => { + expect(() => { + normalizeConfig({ + // @ts-expect-error: test + plugins, + }); + }).toThrowErrorMatchingSnapshot(); + }); + + it.each([ + ['should accept [string] for plugins', ensurePlugins(['plain/string'])], + [ + 'should accept string[] for plugins', + ensurePlugins(['plain/string', 'another/plain/string/path']), + ], + [ + 'should accept [string, object] for plugins', + ensurePlugins([['plain/string', {it: 'should work'}]]), + ], + [ + 'should accept [string, object][] for plugins', + ensurePlugins([ + ['plain/string', {it: 'should work'}], + ['this/should/work', {too: 'yes'}], + ]), + ], + [ + 'should accept ([string, object]|string)[] for plugins', + ensurePlugins([ + 'plain/string', + ['plain', {it: 'should work'}], + ['this/should/work', {too: 'yes'}], + ]), + ], + [ + 'should accept function returning null', + ensurePlugins([ + function plugin() { + return null; + }, + ]), + ], + [ + 'should accept function returning plugin', + ensurePlugins([ + function plugin() { + return {name: 'plugin'}; + }, + ]), + ], + [ + 'should accept function returning plugin or null', + ensurePlugins([ + function plugin() { + return Math.random() > 0.5 ? null : {name: 'plugin'}; + }, + ]), + ], + [ + 'should accept async function returning null', + ensurePlugins([ + async function plugin() { + return null; + }, + ]), + ], + [ + 'should accept async function returning plugin', + ensurePlugins([ + async function plugin() { + return {name: 'plugin'}; + }, + ]), + ], + [ + 'should accept function returning plugin or null', + ensurePlugins([ + async function plugin() { + return Math.random() > 0.5 ? null : {name: 'plugin'}; + }, + ]), + ], + [ + 'should accept [function, object] for plugin', + [[() => {}, {it: 'should work'}]], + ], + [ + 'should accept false/null for plugin', + ensurePlugins([false as const, null, 'classic']), + ], + ])(`%s for the input of: %p`, (_message, plugins) => { + expect(() => { + normalizeConfig({ + plugins, + } as Config); + }).not.toThrow(); + }); +}); + +describe('themes', () => { + it.each([ + ['should throw error if themes is not array', {}], + [ + "should throw error if themes is not a string and it's not an array #1", + [123], + ], + [ + 'should throw error if themes is not an array of [string, object][] #1', + [['example/path', 'wrong parameter here']], + ], + [ + 'should throw error if themes is not an array of [string, object][] #2', + [[{}, 'example/path']], + ], + [ + 'should throw error if themes is not an array of [string, object][] #3', + [[{}, {}]], + ], + ])(`%s for the input of: %p`, (_message, themes) => { + expect(() => { + normalizeConfig({ + // @ts-expect-error: test + themes, + }); + }).toThrowErrorMatchingSnapshot(); + }); + + it.each([ + ['should accept [string] for themes', ['plain/string']], + [ + 'should accept string[] for themes', + ['plain/string', 'another/plain/string/path'], + ], + [ + 'should accept [string, object] for themes', + [['plain/string', {it: 'should work'}]], + ], + [ + 'should accept [string, object][] for themes', + [ + ['plain/string', {it: 'should work'}], + ['this/should/work', {too: 'yes'}], + ], + ], + [ + 'should accept ([string, object]|string)[] for themes', + [ + 'plain/string', + ['plain', {it: 'should work'}], + ['this/should/work', {too: 'yes'}], + ], + ], + ['should accept function for theme', [function theme() {}]], + [ + 'should accept [function, object] for theme', + [[function theme() {}, {it: 'should work'}]], + ], + ['should accept false/null for themes', [false, null, 'classic']], + ])(`%s for the input of: %p`, (_message, themes) => { + expect(() => { + normalizeConfig({ + themes, + } as Config); + }).not.toThrow(); + }); + + it('throws error if themes is not array', () => { + expect(() => { + normalizeConfig({ + // @ts-expect-error: test + themes: {}, + }); + }).toThrowErrorMatchingInlineSnapshot(` + ""themes" must be an array + " + `); + }); +}); + +describe('presets', () => { + it('throws error if presets is not array', () => { + expect(() => { + normalizeConfig({ + // @ts-expect-error: test + presets: {}, + }); + }).toThrowErrorMatchingInlineSnapshot(` + ""presets" must be an array + " + `); + }); + + it('throws error if presets looks invalid', () => { + expect(() => { + normalizeConfig({ + // @ts-expect-error: test + presets: [() => {}], + }); + }).toThrowErrorMatchingInlineSnapshot(` + ""presets[0]" does not look like a valid preset config. A preset config entry should be one of: + - A tuple of [presetName, options], like \`["classic", { blog: false }]\`, or + - A simple string, like \`"classic"\` + " + `); + }); + + it('accepts presets as false / null', () => { + expect(() => { + normalizeConfig({ + presets: [false, null, 'classic'], + }); + }).not.toThrow(); + }); +}); + describe('future', () => { it('accepts future - undefined', () => { expect( @@ -737,6 +815,7 @@ describe('future', () => { }); it('rejects router - null', () => { + // @ts-expect-error: bad value const router: DocusaurusConfig['future']['experimental_router'] = null; expect(() => normalizeConfig({ @@ -976,6 +1055,7 @@ describe('future', () => { }); it('rejects namespace - null', () => { + // @ts-expect-error: bad value const storage: Partial = {namespace: null}; expect(() => normalizeConfig({