diff --git a/packages/gatsby-plugin-cxs/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-cxs/src/__tests__/gatsby-node.js index 8494980faf068..e04d2ef2d5b19 100644 --- a/packages/gatsby-plugin-cxs/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-cxs/src/__tests__/gatsby-node.js @@ -3,13 +3,17 @@ import { testPluginOptionsSchema } from "gatsby-plugin-utils" import { pluginOptionsSchema } from "../gatsby-node" it(`should provide meaningful errors when fields are invalid`, async () => { - const expectedErrors = [`"optionA" is not allowed`] + const expectedWarnings = [`"optionA" is not allowed`] - const { errors } = await testPluginOptionsSchema(pluginOptionsSchema, { - optionA: `This options shouldn't exist`, - }) - - expect(errors).toEqual(expectedErrors) + const { warnings, isValid, hasWarnings } = await testPluginOptionsSchema( + pluginOptionsSchema, + { + optionA: `This options shouldn't exist`, + } + ) + expect(isValid).toBe(true) + expect(hasWarnings).toBe(true) + expect(warnings).toEqual(expectedWarnings) }) it.each` diff --git a/packages/gatsby-plugin-flow/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-flow/src/__tests__/gatsby-node.js index 85934bc8380de..0bbae52fea874 100644 --- a/packages/gatsby-plugin-flow/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-flow/src/__tests__/gatsby-node.js @@ -19,17 +19,18 @@ describe(`onCreateBabelConfig`, () => { describe(`pluginOptionsSchema`, () => { it(`should provide meaningful errors when fields are invalid`, async () => { - const expectedErrors = [`"optionA" is not allowed`] + const expectedWarnings = [`"optionA" is not allowed`] - const { isValid, errors } = await testPluginOptionsSchema( + const { isValid, warnings, hasWarnings } = await testPluginOptionsSchema( pluginOptionsSchema, { optionA: `This option shouldn't exist`, } ) - expect(isValid).toBe(false) - expect(errors).toEqual(expectedErrors) + expect(isValid).toBe(true) + expect(hasWarnings).toBe(true) + expect(warnings).toEqual(expectedWarnings) }) it.each` diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js index e9d1b8087969a..8c8412159bd27 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js @@ -532,12 +532,11 @@ describe(`Test plugin manifest options`, () => { describe(`pluginOptionsSchema`, () => { it(`validates options correctly`, async () => { - expect(await testPluginOptionsSchema(pluginOptionsSchema, manifestOptions)) - .toMatchInlineSnapshot(` - Object { - "errors": Array [], - "isValid": true, - } - `) + const { isValid } = await testPluginOptionsSchema( + pluginOptionsSchema, + manifestOptions + ) + + expect(isValid).toBe(true) }) }) diff --git a/packages/gatsby-plugin-sass/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-sass/src/__tests__/gatsby-node.js index 373fa2b35bb02..74a9e2554a293 100644 --- a/packages/gatsby-plugin-sass/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-sass/src/__tests__/gatsby-node.js @@ -170,10 +170,14 @@ describe(`pluginOptionsSchema`, () => { }) it(`should allow unknown options`, async () => { - const { isValid } = await testPluginOptionsSchema(pluginOptionsSchema, { - webpackImporter: `unknown option`, - }) + const { isValid, hasWarnings } = await testPluginOptionsSchema( + pluginOptionsSchema, + { + webpackImporter: `unknown option`, + } + ) expect(isValid).toBe(true) + expect(hasWarnings).toBe(true) }) }) diff --git a/packages/gatsby-plugin-sitemap/src/__tests__/options-validation.js b/packages/gatsby-plugin-sitemap/src/__tests__/options-validation.js index d83f199342f67..3d9da3b7c81ff 100644 --- a/packages/gatsby-plugin-sitemap/src/__tests__/options-validation.js +++ b/packages/gatsby-plugin-sitemap/src/__tests__/options-validation.js @@ -3,25 +3,31 @@ import { testPluginOptionsSchema, Joi } from "gatsby-plugin-utils" describe(`pluginOptionsSchema`, () => { it(`should provide meaningful errors when fields are invalid`, async () => { - const expectedErrors = [`"wrong" is not allowed`] + const expectedErrors = [`"output" must be a string`] - const { errors } = await testPluginOptionsSchema(pluginOptionsSchema, { - wrong: `test`, - }) + const { isValid, errors } = await testPluginOptionsSchema( + pluginOptionsSchema, + { + output: 123, + } + ) + expect(isValid).toBe(false) expect(errors).toEqual(expectedErrors) }) - it(`should provide error for deprecated "exclude" option`, async () => { - const expectedErrors = [ - `As of v4 the \`exclude\` option was renamed to \`excludes\``, - ] + it(`should provide warning for deprecated "exclude" option`, async () => { + const expectedWarnings = [`"exclude" is not allowed`] - const { errors } = await testPluginOptionsSchema(pluginOptionsSchema, { - exclude: [`test`], - }) + const { warnings, hasWarnings } = await testPluginOptionsSchema( + pluginOptionsSchema, + { + exclude: [`test`], + } + ) - expect(errors).toEqual(expectedErrors) + expect(hasWarnings).toBe(true) + expect(warnings).toEqual(expectedWarnings) }) it(`creates correct defaults`, async () => { @@ -48,7 +54,7 @@ describe(`pluginOptionsSchema`, () => { ${undefined} ${{}} `(`should validate the schema: $options`, async ({ options }) => { - const { isValid } = await testPluginOptionsSchema( + const { isValid, errors } = await testPluginOptionsSchema( pluginOptionsSchema, options ) diff --git a/packages/gatsby-plugin-sitemap/src/options-validation.js b/packages/gatsby-plugin-sitemap/src/options-validation.js index 4ebbdf319e081..66bdc13c87dfa 100644 --- a/packages/gatsby-plugin-sitemap/src/options-validation.js +++ b/packages/gatsby-plugin-sitemap/src/options-validation.js @@ -41,7 +41,8 @@ export const pluginOptionsSchema = ({ Joi }) => } }` ) - .external(({ query }) => { + .external(pluginOptions => { + const query = pluginOptions?.query if (query) { try { parseGraphql(query) @@ -74,9 +75,6 @@ export const pluginOptionsSchema = ({ Joi }) => enter other data types into this array for custom filtering. Doing so will require customization of the \`filterPages\` function.` ), - exclude: Joi.forbidden().messages({ - "any.unknown": `As of v4 the \`exclude\` option was renamed to \`excludes\``, - }), resolveSiteUrl: Joi.function() .default(() => resolveSiteUrl) .description( diff --git a/packages/gatsby-plugin-twitter/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-twitter/src/__tests__/gatsby-node.js index 8494980faf068..e340053e76973 100644 --- a/packages/gatsby-plugin-twitter/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-twitter/src/__tests__/gatsby-node.js @@ -3,13 +3,18 @@ import { testPluginOptionsSchema } from "gatsby-plugin-utils" import { pluginOptionsSchema } from "../gatsby-node" it(`should provide meaningful errors when fields are invalid`, async () => { - const expectedErrors = [`"optionA" is not allowed`] + const expectedWarnings = [`"optionA" is not allowed`] - const { errors } = await testPluginOptionsSchema(pluginOptionsSchema, { - optionA: `This options shouldn't exist`, - }) + const { warnings, isValid, hasWarnings } = await testPluginOptionsSchema( + pluginOptionsSchema, + { + optionA: `This options shouldn't exist`, + } + ) - expect(errors).toEqual(expectedErrors) + expect(isValid).toBe(true) + expect(hasWarnings).toBe(true) + expect(warnings).toEqual(expectedWarnings) }) it.each` diff --git a/packages/gatsby-plugin-utils/src/__tests__/index.ts b/packages/gatsby-plugin-utils/src/__tests__/index.ts index e1c2e276ace0d..7b97c015cfdb9 100644 --- a/packages/gatsby-plugin-utils/src/__tests__/index.ts +++ b/packages/gatsby-plugin-utils/src/__tests__/index.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { validateOptionsSchema, Joi } from "../" +import { testPluginOptionsSchema } from "../test-plugin-options-schema" it(`validates a basic schema`, async () => { const pluginSchema = Joi.object({ @@ -9,9 +10,9 @@ it(`validates a basic schema`, async () => { const validOptions = { str: `is a string`, } - expect(await validateOptionsSchema(pluginSchema, validOptions)).toEqual( - validOptions - ) + + const { value } = await validateOptionsSchema(pluginSchema, validOptions) + expect(value).toEqual(validOptions) const invalid = () => validateOptionsSchema(pluginSchema, { @@ -56,18 +57,38 @@ it(`does not validate async external validation rules when validateExternalRules expect(invalid).not.toThrowError() }) -it(`throws an error on unknown values`, async () => { +it(`throws an warning on unknown values`, async () => { const schema = Joi.object({ str: Joi.string(), }) - const invalid = () => - validateOptionsSchema(schema, { + const validWarnings = [`"notInSchema" is not allowed`] + + const { hasWarnings, warnings } = await testPluginOptionsSchema( + () => schema, + { str: `bla`, notInSchema: true, - }) - - expect(invalid()).rejects.toThrowErrorMatchingInlineSnapshot( - `"\\"notInSchema\\" is not allowed"` + } ) + + expect(hasWarnings).toBe(true) + expect(warnings).toEqual(validWarnings) +}) + +it(`populates default values`, async () => { + const pluginSchema = Joi.object({ + str: Joi.string(), + default: Joi.string().default(`default`), + }) + + const validOptions = { + str: `is a string`, + } + + const { value } = await validateOptionsSchema(pluginSchema, validOptions) + expect(value).toEqual({ + ...validOptions, + default: `default`, + }) }) diff --git a/packages/gatsby-plugin-utils/src/test-plugin-options-schema.ts b/packages/gatsby-plugin-utils/src/test-plugin-options-schema.ts index edd0e259b939c..83a4c8777db00 100644 --- a/packages/gatsby-plugin-utils/src/test-plugin-options-schema.ts +++ b/packages/gatsby-plugin-utils/src/test-plugin-options-schema.ts @@ -5,7 +5,9 @@ import { IPluginInfoOptions } from "./types" interface ITestPluginOptionsSchemaReturnType { errors: Array + warnings: Array isValid: boolean + hasWarnings: boolean } export async function testPluginOptionsSchema( @@ -44,11 +46,22 @@ export async function testPluginOptionsSchema( }) try { - await validateOptionsSchema(pluginSchema, pluginOptions) + const { warning } = await validateOptionsSchema(pluginSchema, pluginOptions) + + const warnings = warning?.details?.map(detail => detail.message) ?? [] + + if (warnings?.length > 0) { + return { + isValid: true, + errors: [], + hasWarnings: true, + warnings, + } + } } catch (e) { - const errors = e.details.map(detail => detail.message) - return { isValid: false, errors } + const errors = e?.details?.map(detail => detail.message) ?? [] + return { isValid: false, errors, hasWarnings: false, warnings: [] } } - return { isValid: true, errors: [] } + return { isValid: true, errors: [], hasWarnings: false, warnings: [] } } diff --git a/packages/gatsby-plugin-utils/src/utils/plugin-options-schema-joi-type.ts b/packages/gatsby-plugin-utils/src/utils/plugin-options-schema-joi-type.ts index c9811a58dc7fe..4336ca3aba599 100644 --- a/packages/gatsby-plugin-utils/src/utils/plugin-options-schema-joi-type.ts +++ b/packages/gatsby-plugin-utils/src/utils/plugin-options-schema-joi-type.ts @@ -1160,7 +1160,7 @@ interface AnySchema extends SchemaInternals { * Warnings are reported separately from errors alongside the result value via the warning key (i.e. `{ value, warning }`). * Warning are always included when calling `any.validate()`. */ - warning(code: string, context: Context): this + warning(code: string, context?: Context): this /** * Converts the type into an alternatives type where the conditions are merged into the type definition where: diff --git a/packages/gatsby-plugin-utils/src/validate.ts b/packages/gatsby-plugin-utils/src/validate.ts index 3493481b07d31..8be0e3c39fdcb 100644 --- a/packages/gatsby-plugin-utils/src/validate.ts +++ b/packages/gatsby-plugin-utils/src/validate.ts @@ -1,5 +1,5 @@ import { ValidationOptions } from "joi" -import { ObjectSchema } from "./joi" +import { ObjectSchema, Joi } from "./joi" import { IPluginInfoOptions } from "./types" const validationOptions: ValidationOptions = { @@ -10,6 +10,20 @@ const validationOptions: ValidationOptions = { interface IOptions { validateExternalRules?: boolean + returnWarnings?: boolean +} + +interface IValidateAsyncResult { + value: IPluginInfoOptions + warning: { + message: string + details: Array<{ + message: string + path: Array + type: string + context: Array> + }> + } } export async function validateOptionsSchema( @@ -17,14 +31,19 @@ export async function validateOptionsSchema( pluginOptions: IPluginInfoOptions, options: IOptions = { validateExternalRules: true, + returnWarnings: true, } -): Promise { - const { validateExternalRules } = options +): Promise { + const { validateExternalRules, returnWarnings } = options - const value = await pluginSchema.validateAsync(pluginOptions, { + const warnOnUnknownSchema = pluginSchema.pattern( + /.*/, + Joi.any().warning(`any.unknown`) + ) + + return (await warnOnUnknownSchema.validateAsync(pluginOptions, { ...validationOptions, externals: validateExternalRules, - }) - - return value + warnings: returnWarnings, + })) as Promise } diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts index 09203261f2ecc..192f457620b06 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts @@ -454,7 +454,7 @@ describe(`Load plugins`, () => { expect((reporter.warn as jest.Mock).mock.calls[0]).toMatchInlineSnapshot(` Array [ "Warning: there are unknown plugin options for \\"gatsby-plugin-google-analytics\\": doesThisExistInTheSchema - Please open an issue at ghub.io/gatsby-plugin-google-analytics if you believe this option is valid.", + Please open an issue at https://ghub.io/gatsby-plugin-google-analytics if you believe this option is valid.", ] `) expect(mockProcessExit).not.toHaveBeenCalled() diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts index b98c44ea0e6d8..a66b44559759f 100644 --- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts +++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts @@ -282,6 +282,14 @@ async function validatePluginsOptions( }), }) + // If rootDir and plugin.parentDir are the same, i.e. if this is a plugin a user configured in their gatsby-config.js (and not a sub-theme that added it), this will be "" + // Otherwise, this will contain (and show) the relative path + const configDir = + (plugin.parentDir && + rootDir && + path.relative(rootDir, plugin.parentDir)) || + null + if (!Joi.isSchema(optionsSchema) || optionsSchema.type !== `object`) { // Validate correct usage of pluginOptionsSchema reporter.warn( @@ -300,11 +308,39 @@ async function validatePluginsOptions( }) } - plugin.options = await validateOptionsSchema( + const { value, warning } = await validateOptionsSchema( optionsSchema, (plugin.options as IPluginInfoOptions) || {} ) + plugin.options = value + + // Handle unknown key warnings + const validationWarnings = warning?.details + + if (validationWarnings?.length > 0) { + reporter.warn( + stripIndent(` + Warning: there are unknown plugin options for "${plugin.resolve}"${ + configDir ? `, configured by ${configDir}` : `` + }: ${validationWarnings + .map(error => error.path.join(`.`)) + .join(`, `)} + Please open an issue at https://ghub.io/${ + plugin.resolve + } if you believe this option is valid. + `) + ) + trackCli(`UNKNOWN_PLUGIN_OPTION`, { + name: plugin.resolve, + valueString: validationWarnings + .map(error => error.path.join(`.`)) + .join(`, `), + }) + // We do not increment errors++ here as we do not want to process.exit if there are only warnings + } + + // Validate subplugins if (plugin.options?.plugins) { const { errors: subErrors, plugins: subPlugins } = await validatePluginsOptions( @@ -322,21 +358,7 @@ async function validatePluginsOptions( } } catch (error) { if (error instanceof Joi.ValidationError) { - // Show a small warning on unknown options rather than erroring - const validationWarnings = error.details.filter( - err => err.type === `object.unknown` - ) - const validationErrors = error.details.filter( - err => err.type !== `object.unknown` - ) - - // If rootDir and plugin.parentDir are the same, i.e. if this is a plugin a user configured in their gatsby-config.js (and not a sub-theme that added it), this will be "" - // Otherwise, this will contain (and show) the relative path - const configDir = - (plugin.parentDir && - rootDir && - path.relative(rootDir, plugin.parentDir)) || - null + const validationErrors = error.details if (validationErrors.length > 0) { reporter.error({ id: `11331`, @@ -348,31 +370,6 @@ async function validatePluginsOptions( }) errors++ } - - if (validationWarnings.length > 0) { - reporter.warn( - stripIndent(` - Warning: there are unknown plugin options for "${ - plugin.resolve - }"${ - configDir ? `, configured by ${configDir}` : `` - }: ${validationWarnings - .map(error => error.path.join(`.`)) - .join(`, `)} - Please open an issue at ghub.io/${ - plugin.resolve - } if you believe this option is valid. - `) - ) - trackCli(`UNKNOWN_PLUGIN_OPTION`, { - name: plugin.resolve, - valueString: validationWarnings - .map(error => error.path.join(`.`)) - .join(`, `), - }) - // We do not increment errors++ here as we do not want to process.exit if there are only warnings - } - return plugin }