diff --git a/packages/babel-core/src/config/option-assertions.js b/packages/babel-core/src/config/option-assertions.js index 01c65e4e14..bc0ce7fae6 100644 --- a/packages/babel-core/src/config/option-assertions.js +++ b/packages/babel-core/src/config/option-assertions.js @@ -12,6 +12,12 @@ import type { RootInputSourceMapOption, } from "./options"; +export type ValidatorSet = { + [string]: Validator, +}; + +export type Validator = (string, mixed) => T; + export function assertSourceMaps( key: string, value: mixed, diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 0c4cd84ffa..20cab68f1c 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -1,7 +1,7 @@ // @flow import * as context from "../index"; -import Plugin from "./plugin"; +import Plugin, { validatePluginObject } from "./plugin"; import defaults from "lodash/defaults"; import merge from "lodash/merge"; import buildConfigChain, { type ConfigItem } from "./build-config-chain"; @@ -28,15 +28,6 @@ type MergeOptions = dirname: string, }; -const ALLOWED_PLUGIN_KEYS = new Set([ - "name", - "manipulateOptions", - "pre", - "post", - "visitor", - "inherits", -]); - export default function manageOptions(opts: {}): { options: Object, passes: Array>, @@ -275,37 +266,16 @@ function loadPluginDescriptor(descriptor: BasicDescriptor): Plugin { } const instantiatePlugin = makeWeakCache( - ( - { value: pluginObj, options, dirname, alias }: LoadedDescriptor, - cache, - ): Plugin => { - Object.keys(pluginObj).forEach(key => { - if (!ALLOWED_PLUGIN_KEYS.has(key)) { - throw new Error( - `Plugin ${alias} provided an invalid property of ${key}`, - ); - } - }); - if ( - pluginObj.visitor && - (pluginObj.visitor.enter || pluginObj.visitor.exit) - ) { - throw new Error( - "Plugins aren't allowed to specify catch-all enter/exit handlers. " + - "Please target individual nodes.", - ); + ({ value, options, dirname, alias }: LoadedDescriptor, cache): Plugin => { + const pluginObj = validatePluginObject(value); + + const plugin = Object.assign({}, pluginObj); + if (plugin.visitor) { + plugin.visitor = traverse.explode(clone(plugin.visitor)); } - const plugin = Object.assign({}, pluginObj, { - visitor: clone(pluginObj.visitor || {}), - }); - - traverse.explode(plugin.visitor); - - let inheritsDescriptor; - let inherits; if (plugin.inherits) { - inheritsDescriptor = { + const inheritsDescriptor = { alias: `${alias}$inherits`, value: plugin.inherits, options, @@ -313,7 +283,7 @@ const instantiatePlugin = makeWeakCache( }; // If the inherited plugin changes, reinstantiate this plugin. - inherits = cache.invalidate(() => + const inherits = cache.invalidate(() => loadPluginDescriptor(inheritsDescriptor), ); @@ -324,8 +294,8 @@ const instantiatePlugin = makeWeakCache( plugin.manipulateOptions, ); plugin.visitor = traverse.visitors.merge([ - inherits.visitor, - plugin.visitor, + inherits.visitor || {}, + plugin.visitor || {}, ]); } diff --git a/packages/babel-core/src/config/options.js b/packages/babel-core/src/config/options.js index 8720dfe267..e47babff92 100644 --- a/packages/babel-core/src/config/options.js +++ b/packages/babel-core/src/config/options.js @@ -12,14 +12,10 @@ import { assertSourceMaps, assertCompact, assertSourceType, + type ValidatorSet, + type Validator, } from "./option-assertions"; -type ValidatorSet = { - [string]: Validator, -}; - -type Validator = (string, mixed) => T; - const ROOT_VALIDATORS: ValidatorSet = { filename: (assertString: Validator< $PropertyType, diff --git a/packages/babel-core/src/config/plugin.js b/packages/babel-core/src/config/plugin.js index 615a5f5ece..edc251d0ce 100644 --- a/packages/babel-core/src/config/plugin.js +++ b/packages/babel-core/src/config/plugin.js @@ -1,43 +1,104 @@ // @flow -// $FlowIssue recursion? +import { + assertString, + assertFunction, + assertObject, + type ValidatorSet, + type Validator, +} from "./option-assertions"; + +const VALIDATORS: ValidatorSet = { + name: (assertString: Validator<$PropertyType>), + manipulateOptions: (assertFunction: Validator< + $PropertyType, + >), + pre: (assertFunction: Validator<$PropertyType>), + post: (assertFunction: Validator<$PropertyType>), + inherits: (assertFunction: Validator< + $PropertyType, + >), + visitor: (assertVisitorMap: Validator< + $PropertyType, + >), +}; + +function assertVisitorMap(key: string, value: mixed): VisitorMap { + const obj = assertObject(key, value); + if (obj) { + Object.keys(obj).forEach(prop => assertVisitorHandler(prop, obj[prop])); + + if (obj.enter || obj.exit) { + throw new Error( + `.${key} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`, + ); + } + } + return (obj: any); +} + +function assertVisitorHandler( + key: string, + value: mixed, +): VisitorHandler | void { + if (value && typeof value === "object") { + Object.keys(value).forEach(handler => { + if (handler !== "enter" && handler !== "exit") { + throw new Error( + `.visitor["${key}"] may only have .enter and/or .exit handlers.`, + ); + } + }); + } else if (typeof value !== "function") { + throw new Error(`.visitor["${key}"] must be a function`); + } + + return (value: any); +} + +type VisitorHandler = Function | { enter?: Function, exit?: Function }; +export type VisitorMap = { + [string]: VisitorHandler, +}; + +export type PluginObject = { + name?: string, + manipulateOptions?: Function, + + pre?: Function, + post?: Function, + + inherits?: Function, + visitor?: VisitorMap, +}; + +export function validatePluginObject(obj: {}): PluginObject { + Object.keys(obj).forEach(key => { + const validator = VALIDATORS[key]; + + if (validator) validator(key, obj[key]); + else throw new Error(`.${key} is not a valid Plugin property`); + }); + + return (obj: any); +} + export default class Plugin { key: ?string; - manipulateOptions: ?Function; - post: ?Function; - pre: ?Function; - visitor: ?{}; + manipulateOptions: Function | void; + post: Function | void; + pre: Function | void; + visitor: {}; options: {}; - constructor(plugin: {}, options: {}, key?: string) { - if (plugin.name != null && typeof plugin.name !== "string") { - throw new Error("Plugin .name must be a string, null, or undefined"); - } - if ( - plugin.manipulateOptions != null && - typeof plugin.manipulateOptions !== "function" - ) { - throw new Error( - "Plugin .manipulateOptions must be a function, null, or undefined", - ); - } - if (plugin.post != null && typeof plugin.post !== "function") { - throw new Error("Plugin .post must be a function, null, or undefined"); - } - if (plugin.pre != null && typeof plugin.pre !== "function") { - throw new Error("Plugin .pre must be a function, null, or undefined"); - } - if (plugin.visitor != null && typeof plugin.visitor !== "object") { - throw new Error("Plugin .visitor must be an object, null, or undefined"); - } - + constructor(plugin: PluginObject, options: {}, key?: string) { this.key = plugin.name || key; this.manipulateOptions = plugin.manipulateOptions; this.post = plugin.post; this.pre = plugin.pre; - this.visitor = plugin.visitor; + this.visitor = plugin.visitor || {}; this.options = options; } }