From dccf75127723172d804fb891405cce1e8a22dfe4 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) --- .../configValidation.test.ts.snap | 68 +-- .../server/__tests__/configValidation.test.ts | 530 ++++++++++-------- 2 files changed, 339 insertions(+), 259 deletions(-) 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 6dca123a95076..d298a46eb83d9 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap @@ -1,6 +1,30 @@ // 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`] = ` +exports[`normalizeConfig throws error for required fields 1`] = ` +""baseUrl" is required +"title" is required +"url" is required +"themes" must be an array +"presets" must be an array +"scripts" must be an array +"stylesheets" must be an array +These field(s) ("invalidField",) are not recognized in docusaurus.config.js. +If you still want these fields to be in your configuration, put them in the "customFields" field. +See https://docusaurus.io/docs/api/docusaurus-config/#customfields" +`; + +exports[`normalizeConfig throws error for unknown field 1`] = ` +"These field(s) ("invalid",) are not recognized in docusaurus.config.js. +If you still want these fields to be in your configuration, put them in the "customFields" field. +See https://docusaurus.io/docs/api/docusaurus-config/#customfields" +`; + +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: { @@ -16,7 +40,7 @@ Example valid plugin config: " `; -exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = ` +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: { @@ -32,7 +56,7 @@ Example valid plugin config: " `; -exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = ` +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: { @@ -48,7 +72,7 @@ Example valid plugin config: " `; -exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = ` +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: { @@ -64,12 +88,12 @@ Example valid plugin config: " `; -exports[`normalizeConfig should throw error if plugins is not array for the input of: {} 1`] = ` +exports[`plugins 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`] = ` +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: { @@ -85,7 +109,7 @@ Example valid theme config: " `; -exports[`normalizeConfig should throw error if themes is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = ` +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: { @@ -101,7 +125,7 @@ Example valid theme config: " `; -exports[`normalizeConfig should throw error if themes is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = ` +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: { @@ -117,7 +141,7 @@ Example valid theme config: " `; -exports[`normalizeConfig should throw error if themes is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = ` +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: { @@ -133,31 +157,7 @@ Example valid theme config: " `; -exports[`normalizeConfig should throw error if themes is not array for the input of: {} 1`] = ` +exports[`themes 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 -"url" is required -"themes" must be an array -"presets" must be an array -"scripts" must be an array -"stylesheets" must be an array -These field(s) ("invalidField",) are not recognized in docusaurus.config.js. -If you still want these fields to be in your configuration, put them in the "customFields" field. -See https://docusaurus.io/docs/api/docusaurus-config/#customfields" -`; - -exports[`normalizeConfig throws error for unknown field 1`] = ` -"These field(s) ("invalid",) are not recognized in docusaurus.config.js. -If you still want these fields to be in your configuration, put them in the "customFields" field. -See https://docusaurus.io/docs/api/docusaurus-config/#customfields" -`; - -exports[`normalizeConfig throws for "error" reporting severity 1`] = ` -""onBrokenLinks" must be one of [ignore, log, warn, throw] -" -`; diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 3fb607deba47f..b6d210a3ba66d 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({