diff --git a/packages/babel-core/src/config/full.js b/packages/babel-core/src/config/full.js index ab24a1936b..35b6dc8662 100644 --- a/packages/babel-core/src/config/full.js +++ b/packages/babel-core/src/config/full.js @@ -20,7 +20,11 @@ import { makeWeakCacheSync, type CacheConfigurator, } from "./caching"; -import { validate, type CallerMetadata } from "./validation/options"; +import { + validate, + type CallerMetadata, + checkNoUnwrappedItemOptionPairs, +} from "./validation/options"; import { validatePluginObject } from "./validation/plugins"; import makeAPI from "./helpers/config-api"; @@ -78,19 +82,48 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( pass: Array, ) { const plugins = []; - for (const descriptor of config.plugins) { + for (let i = 0; i < config.plugins.length; i++) { + const descriptor = config.plugins[i]; if (descriptor.options !== false) { - plugins.push(yield* loadPluginDescriptor(descriptor, context)); + try { + plugins.push(yield* loadPluginDescriptor(descriptor, context)); + } catch (e) { + // print special message for `plugins: ["@babel/foo", { foo: "option" }]` + if (i > 0 && e.code === "BABEL_UNKNOWN_PLUGIN_PROPERTY") { + checkNoUnwrappedItemOptionPairs( + config.plugins[i - 1], + descriptor, + "plugin", + i, + e, + ); + } + throw e; + } } } const presets = []; - for (const descriptor of config.presets) { + for (let i = 0; i < config.presets.length; i++) { + const descriptor = config.presets[i]; if (descriptor.options !== false) { - presets.push({ - preset: yield* loadPresetDescriptor(descriptor, context), - pass: descriptor.ownPass ? [] : pass, - }); + try { + presets.push({ + preset: yield* loadPresetDescriptor(descriptor, context), + pass: descriptor.ownPass ? [] : pass, + }); + } catch (e) { + if (i > 0 && e.code === "BABEL_UNKNOWN_OPTION") { + checkNoUnwrappedItemOptionPairs( + config.presets[i - 1], + descriptor, + "preset", + i, + e, + ); + } + throw e; + } } } diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index 02c51649a4..c3e7f359cd 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -27,6 +27,7 @@ import { type Validator, type OptionPath, } from "./option-assertions"; +import type { UnloadedDescriptor } from "../config-descriptors"; const ROOT_VALIDATORS: ValidatorSet = { cwd: (assertString: Validator<$PropertyType>), @@ -365,16 +366,20 @@ function throwUnknownError(loc: OptionPath) { if (removed[key]) { const { message, version = 5 } = removed[key]; - throw new ReferenceError( + throw new Error( `Using removed Babel ${version} option: ${msg(loc)} - ${message}`, ); } else { // eslint-disable-next-line max-len - const unknownOptErr = `Unknown option: ${msg( - loc, - )}. Check out https://babeljs.io/docs/en/babel-core/#options for more information about options.`; + const unknownOptErr = new Error( + `Unknown option: ${msg( + loc, + )}. Check out https://babeljs.io/docs/en/babel-core/#options for more information about options.`, + ); + // $FlowIgnore + unknownOptErr.code = "BABEL_UNKNOWN_OPTION"; - throw new ReferenceError(unknownOptErr); + throw unknownOptErr; } } @@ -439,3 +444,26 @@ function assertOverridesList(loc: OptionPath, value: mixed): OverridesList { } return (arr: any); } + +export function checkNoUnwrappedItemOptionPairs( + lastItem: UnloadedDescriptor, + thisItem: UnloadedDescriptor, + type: "plugin" | "preset", + index: number, + e: Error, +): void { + if ( + lastItem.file && + lastItem.options === undefined && + typeof thisItem.value === "object" + ) { + e.message += + `\n- Maybe you meant to use\n` + + `"${type}": [\n ["${lastItem.file.request}", ${JSON.stringify( + thisItem.value, + undefined, + 2, + )}]\n]\n` + + `To be a valid ${type}, its name and options should be wrapped in a pair of brackets`; + } +} diff --git a/packages/babel-core/src/config/validation/plugins.js b/packages/babel-core/src/config/validation/plugins.js index ea1fc51430..06778caff2 100644 --- a/packages/babel-core/src/config/validation/plugins.js +++ b/packages/babel-core/src/config/validation/plugins.js @@ -97,7 +97,14 @@ export function validatePluginObject(obj: {}): PluginObject { }; if (validator) validator(optLoc, obj[key]); - else throw new Error(`.${key} is not a valid Plugin property`); + else { + const invalidPluginPropertyError = new Error( + `.${key} is not a valid Plugin property`, + ); + // $FlowIgnore + invalidPluginPropertyError.code = "BABEL_UNKNOWN_PLUGIN_PROPERTY"; + throw invalidPluginPropertyError; + } }); return (obj: any); diff --git a/packages/babel-core/test/__snapshots__/option-manager.js.snap b/packages/babel-core/test/__snapshots__/option-manager.js.snap new file mode 100644 index 0000000000..423a2dac00 --- /dev/null +++ b/packages/babel-core/test/__snapshots__/option-manager.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`option-manager config plugin/preset flattening and overriding should throw when an option is following a preset 1`] = ` +"[BABEL] unknown: Unknown option: .useSpread. Check out https://babeljs.io/docs/en/babel-core/#options for more information about options. +- Maybe you meant to use +\\"preset\\": [ + [\\"./fixtures/option-manager/babel-preset-bar\\", { + \\"useSpread\\": true +}] +] +To be a valid preset, its name and options should be wrapped in a pair of brackets" +`; + +exports[`option-manager config plugin/preset flattening and overriding should throw when an option is provided as a plugin 1`] = ` +"[BABEL] unknown: .useSpread is not a valid Plugin property +- Maybe you meant to use +\\"plugin\\": [ + [\\"./fixtures/option-manager/babel-plugin-foo\\", { + \\"useSpread\\": true +}] +] +To be a valid plugin, its name and options should be wrapped in a pair of brackets" +`; + +exports[`option-manager config plugin/preset flattening and overriding should throw when an option is provided as a preset 1`] = ` +"[BABEL] unknown: Unknown option: .useBuiltIns. Check out https://babeljs.io/docs/en/babel-core/#options for more information about options. +- Maybe you meant to use +\\"preset\\": [ + [\\"./fixtures/option-manager/babel-preset-bar\\", { + \\"useBuiltIns\\": \\"entry\\" +}] +] +To be a valid preset, its name and options should be wrapped in a pair of brackets" +`; diff --git a/packages/babel-core/test/fixtures/option-manager/babel-plugin-foo/index.js b/packages/babel-core/test/fixtures/option-manager/babel-plugin-foo/index.js new file mode 100644 index 0000000000..17f95feabd --- /dev/null +++ b/packages/babel-core/test/fixtures/option-manager/babel-plugin-foo/index.js @@ -0,0 +1 @@ +module.exports = () => ({}); diff --git a/packages/babel-core/test/fixtures/option-manager/babel-preset-bar/index.js b/packages/babel-core/test/fixtures/option-manager/babel-preset-bar/index.js new file mode 100644 index 0000000000..17f95feabd --- /dev/null +++ b/packages/babel-core/test/fixtures/option-manager/babel-preset-bar/index.js @@ -0,0 +1 @@ +module.exports = () => ({}); diff --git a/packages/babel-core/test/option-manager.js b/packages/babel-core/test/option-manager.js index 5533d54c46..880d80f411 100644 --- a/packages/babel-core/test/option-manager.js +++ b/packages/babel-core/test/option-manager.js @@ -27,6 +27,52 @@ describe("option-manager", () => { return { plugin, calls }; } + it("should throw when an option is provided as a preset", () => { + expect(() => { + loadOptions({ + presets: [ + "./fixtures/option-manager/babel-preset-bar", + { useBuiltIns: "entry" }, + ], + }); + }).toThrowErrorMatchingSnapshot(); + }); + + it("should throw when an option is provided as a plugin", () => { + expect(() => { + loadOptions({ + plugins: [ + "./fixtures/option-manager/babel-plugin-foo", + { useSpread: true }, + ], + }); + }).toThrowErrorMatchingSnapshot(); + }); + + it("should throw when an option is following a preset", () => { + expect(() => { + loadOptions({ + presets: [ + "./fixtures/option-manager/babel-plugin-foo", + "./fixtures/option-manager/babel-preset-bar", + { useSpread: true }, + ], + }); + }).toThrowErrorMatchingSnapshot(); + }); + + it("should not throw when a preset string followed by valid preset object", () => { + const { plugin } = makePlugin("my-plugin"); + expect( + loadOptions({ + presets: [ + "@babel/env", + { plugins: [[plugin, undefined, "my-plugin"]] }, + ], + }), + ).toBeTruthy(); + }); + it("should throw if a plugin is repeated, with information about the repeated plugin", () => { const { calls, plugin } = makePlugin("my-plugin");