diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 78e97794cf..c5f738cc11 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -4,6 +4,12 @@ import { getEnv } from "./helpers/environment"; import path from "path"; import micromatch from "micromatch"; import buildDebug from "debug"; +import { + validate, + type ValidatedOptions, + type PluginList, + type IgnoreList, +} from "./options"; const debug = buildDebug("babel:config:config-chain"); @@ -11,19 +17,19 @@ import { findConfigs, loadConfig, type ConfigFile } from "./loading/files"; import { makeWeakCache, makeStrongCache } from "./caching"; -type ConfigItem = { - type: "options" | "arguments", - options: {}, - dirname: string, +export type ConfigItem = { + type: "arguments" | "env" | "file", + options: ValidatedOptions, alias: string, + dirname: string, }; type ConfigPart = | { part: "config", config: ConfigItem, - ignore: ?Array, - only: ?Array, + ignore: ?IgnoreList, + only: ?IgnoreList, activeEnv: string | null, } | { @@ -33,11 +39,9 @@ type ConfigPart = activeEnv: string | null, }; -export default function buildConfigChain(opts: {}): Array | null { - if (typeof opts.filename !== "string" && opts.filename != null) { - throw new Error(".filename must be a string, null, or undefined"); - } - +export default function buildConfigChain( + opts: ValidatedOptions, +): Array | null { const filename = opts.filename ? path.resolve(opts.filename) : null; const builder = new ConfigChainBuilder( filename ? new LoadedFile(filename) : null, @@ -70,7 +74,11 @@ class ConfigChainBuilder { this.file = file; } - mergeConfigArguments(opts: {}, dirname: string, envKey: string) { + mergeConfigArguments( + opts: ValidatedOptions, + dirname: string, + envKey: string, + ) { flattenArgumentsOptionsParts(opts, dirname, envKey).forEach(part => this._processConfigPart(part, envKey), ); @@ -117,43 +125,26 @@ class ConfigChainBuilder { * object identity preserved between calls so that they can be used for caching. */ function flattenArgumentsOptionsParts( - opts: {}, + opts: ValidatedOptions, dirname: string, envKey: string, ): Array { + const { + env, + plugins, + presets, + passPerPreset, + extends: extendsPath, + ...options + } = opts; + const raw = []; - - const env = typeof opts.env === "object" ? opts.env : null; - const plugins = Array.isArray(opts.plugins) ? opts.plugins : null; - const presets = Array.isArray(opts.presets) ? opts.presets : null; - const passPerPreset = - typeof opts.passPerPreset === "boolean" ? opts.passPerPreset : false; - if (env) { raw.push(...flattenArgumentsEnvOptionsParts(env)(dirname)(envKey)); } - const innerOpts = Object.assign({}, opts); - // If the env, plugins, and presets values on the object aren't arrays or - // objects, leave them in the base opts so that normal options validation - // will throw errors on them later. - if (env) delete innerOpts.env; - if (plugins) delete innerOpts.plugins; - if (presets) { - delete innerOpts.presets; - delete innerOpts.passPerPreset; - } - delete innerOpts.extends; - - if (Object.keys(innerOpts).length > 0) { - raw.push( - ...flattenOptionsParts({ - type: "arguments", - options: innerOpts, - alias: "base", - dirname, - }), - ); + if (Object.keys(options).length > 0) { + raw.push(...flattenOptionsParts(buildArgumentsItem(options, dirname))); } if (plugins) { @@ -161,14 +152,14 @@ function flattenArgumentsOptionsParts( } if (presets) { raw.push( - ...flattenArgumentsPresetsOptionsParts(presets)(passPerPreset)(dirname), + ...flattenArgumentsPresetsOptionsParts(presets)(!!passPerPreset)(dirname), ); } - if (opts.extends != null) { + if (extendsPath != null) { raw.push( ...flattenOptionsParts( - buildArgumentsItem({ extends: opts.extends }, dirname), + buildArgumentsItem({ extends: extendsPath }, dirname), ), ); } @@ -181,7 +172,7 @@ function flattenArgumentsOptionsParts( * the object identity of the 'env' object. */ const flattenArgumentsEnvOptionsParts = makeWeakCache((env: {}) => { - const options = { env }; + const options: ValidatedOptions = { env }; return makeStrongCache((dirname: string) => flattenOptionsPartsLookup(buildArgumentsItem(options, dirname)), @@ -193,8 +184,8 @@ const flattenArgumentsEnvOptionsParts = makeWeakCache((env: {}) => { * the object identity of the 'plugins' object. */ const flattenArgumentsPluginsOptionsParts = makeWeakCache( - (plugins: Array) => { - const options = { plugins }; + (plugins: PluginList) => { + const options: ValidatedOptions = { plugins }; return makeStrongCache((dirname: string) => flattenOptionsParts(buildArgumentsItem(options, dirname)), @@ -207,8 +198,8 @@ const flattenArgumentsPluginsOptionsParts = makeWeakCache( * the object identity of the 'presets' object. */ const flattenArgumentsPresetsOptionsParts = makeWeakCache( - (presets: Array) => - makeStrongCache((passPerPreset: ?boolean) => { + (presets: PluginList) => + makeStrongCache((passPerPreset: boolean) => { // The concept of passPerPreset is integrally tied to the preset list // so unfortunately we need to copy both values here, adding an extra // layer of caching functions. @@ -220,7 +211,10 @@ const flattenArgumentsPresetsOptionsParts = makeWeakCache( }), ); -function buildArgumentsItem(options: {}, dirname: string): ConfigItem { +function buildArgumentsItem( + options: ValidatedOptions, + dirname: string, +): ConfigItem { return { type: "arguments", options, @@ -236,8 +230,8 @@ function buildArgumentsItem(options: {}, dirname: string): ConfigItem { */ const flattenFileOptionsParts = makeWeakCache((file: ConfigFile) => { return flattenOptionsPartsLookup({ - type: "options", - options: file.options, + type: "file", + options: validate("file", file.options), alias: file.filepath, dirname: file.dirname, }); @@ -278,74 +272,37 @@ function flattenOptionsParts( config: ConfigItem, activeEnv: string | null = null, ): Array { - const { type, options: rawOpts, alias, dirname } = config; - - if (rawOpts.ignore != null && !Array.isArray(rawOpts.ignore)) { - throw new Error( - `.ignore should be an array, ${JSON.stringify(rawOpts.ignore)} given`, - ); - } - if (rawOpts.only != null && !Array.isArray(rawOpts.only)) { - throw new Error( - `.only should be an array, ${JSON.stringify(rawOpts.only)} given`, - ); - } - const ignore = rawOpts.ignore || null; - const only = rawOpts.only || null; + const { options: rawOpts, alias, dirname } = config; const parts = []; - if ( - rawOpts.env != null && - (typeof rawOpts.env !== "object" || Array.isArray(rawOpts.env)) - ) { - throw new Error(".env block must be an object, null, or undefined"); + if (rawOpts.env) { + for (const envKey of Object.keys(rawOpts.env)) { + if (rawOpts.env[envKey]) { + parts.push( + ...flattenOptionsParts( + { + type: "env", + options: rawOpts.env[envKey], + alias: alias + `.env.${envKey}`, + dirname, + }, + envKey, + ), + ); + } + } } - const rawEnv = rawOpts.env || {}; - - Object.keys(rawEnv).forEach(envKey => { - const envOpts = rawEnv[envKey]; - - if (envOpts !== undefined && activeEnv !== null && activeEnv !== envKey) { - throw new Error(`Unreachable .env[${envKey}] block detected`); - } - - if ( - envOpts != null && - (typeof envOpts !== "object" || Array.isArray(envOpts)) - ) { - throw new Error(".env[...] block must be an object, null, or undefined"); - } - - if (envOpts) { - parts.push( - ...flattenOptionsParts( - { - type, - options: envOpts, - alias: alias + `.env.${envKey}`, - dirname, - }, - envKey, - ), - ); - } - }); - parts.push({ part: "config", config, - ignore, - only, + ignore: rawOpts.ignore, + only: rawOpts.only, activeEnv, }); if (rawOpts.extends != null) { - if (typeof rawOpts.extends !== "string") { - throw new Error(".extends must be a string"); - } - parts.push({ part: "extends", path: rawOpts.extends, @@ -372,8 +329,8 @@ class LoadedFile { * Tests if a filename should be ignored based on "ignore" and "only" options. */ shouldIgnore( - ignore: ?Array, - only: ?Array, + ignore: ?IgnoreList, + only: ?IgnoreList, dirname: string, ): boolean { if (ignore) { @@ -407,7 +364,7 @@ class LoadedFile { * Returns result of calling function with filename if pattern is a function. * Otherwise returns result of matching pattern Regex with filename. */ - _matchesPatterns(patterns: Array, dirname: string): boolean { + _matchesPatterns(patterns: IgnoreList, dirname: string): boolean { const res = []; const strings = []; const fns = []; @@ -415,12 +372,7 @@ class LoadedFile { patterns.forEach(pattern => { if (typeof pattern === "string") strings.push(pattern); else if (typeof pattern === "function") fns.push(pattern); - else if (pattern instanceof RegExp) res.push(pattern); - else { - throw new Error( - "Patterns must be a string, function, or regular expression", - ); - } + else res.push(pattern); }); const filename = this.filename; diff --git a/packages/babel-core/src/config/caching.js b/packages/babel-core/src/config/caching.js index 052e2803c3..bd12bc31ac 100644 --- a/packages/babel-core/src/config/caching.js +++ b/packages/babel-core/src/config/caching.js @@ -31,7 +31,7 @@ export function makeStrongCache( * configures its caching behavior. Cached values are stored weakly and the function argument must be * an object type. */ -export function makeWeakCache, ResultT>( +export function makeWeakCache | $ReadOnlyArray<*>, ResultT>( handler: (ArgT, CacheConfigurator) => ResultT, autoPermacache?: boolean, ): ArgT => ResultT { diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index 2676d24047..8e458cdf7d 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -16,7 +16,7 @@ export type PluginPasses = Array; * Standard API for loading Babel configuration data. Not for public consumption. */ export default function loadConfig(opts: mixed): ResolvedConfig | null { - if (opts != null && typeof opts !== "object") { + if (opts != null && (typeof opts !== "object" || Array.isArray(opts))) { throw new Error("Babel options must be an object, null, or undefined"); } diff --git a/packages/babel-core/src/config/option-assertions.js b/packages/babel-core/src/config/option-assertions.js new file mode 100644 index 0000000000..12aa4460c6 --- /dev/null +++ b/packages/babel-core/src/config/option-assertions.js @@ -0,0 +1,174 @@ +// @flow + +import type { + IgnoreList, + IgnoreItem, + PluginList, + PluginItem, + PluginTarget, + SourceMapsOption, + SourceTypeOption, + CompactOption, + RootInputSourceMapOption, +} from "./options"; + +export function assertSourceMaps(key: string, value: mixed): ?SourceMapsOption { + if ( + value != null && + typeof value !== "boolean" && + value !== "inline" && + value !== "both" + ) { + throw new Error( + `.${key} must be a boolean, "inline", "both", null, or undefined`, + ); + } + return value; +} + +export function assertCompact(key: string, value: mixed): ?CompactOption { + if (value != null && typeof value !== "boolean" && value !== "auto") { + throw new Error(`.${key} must be a boolean, "auto", null, or undefined`); + } + return value; +} + +export function assertSourceType(key: string, value: mixed): ?SourceTypeOption { + if (value != null && value !== "module" && value !== "script") { + throw new Error(`.${key} must be "module", "script", null, or undefined`); + } + return value; +} + +export function assertInputSourceMap( + key: string, + value: mixed, +): ?RootInputSourceMapOption { + if ( + value != null && + typeof value !== "boolean" && + typeof value !== "object" + ) { + throw new Error( + ".inputSourceMap must be a boolean, object, null, or undefined", + ); + } + return value; +} + +export function assertString(key: string, value: mixed): ?string { + if (value != null && typeof value !== "string") { + throw new Error(`.${key} must be a string, null, or undefined`); + } + return value; +} + +export function assertFunction(key: string, value: mixed): ?Function { + if (value != null && typeof value !== "function") { + throw new Error(`.${key} must be a function, null, or undefined`); + } + return value; +} + +export function assertBoolean(key: string, value: mixed): ?boolean { + if (value != null && typeof value !== "boolean") { + throw new Error(`.${key} must be a boolean, null, or undefined`); + } + return value; +} + +export function assertObject(key: string, value: mixed): ?{} { + if (value != null && (typeof value !== "object" || Array.isArray(value))) { + throw new Error(`.${key} must be an object, null, or undefined`); + } + return value; +} + +export function assertIgnoreList(key: string, value: mixed): ?IgnoreList { + const arr = assertArray(key, value); + if (arr) { + arr.forEach((item, i) => assertIgnoreItem(key, i, item)); + } + return (arr: any); +} +function assertIgnoreItem( + key: string, + index: number, + value: mixed, +): IgnoreItem { + if ( + typeof value !== "string" && + typeof value !== "function" && + !(value instanceof RegExp) + ) { + throw new Error( + `.${key}[${index}] must be an array of string/Funtion/RegExp values, or null, or undefined`, + ); + } + return value; +} + +export function assertPluginList(key: string, value: mixed): ?PluginList { + const arr = assertArray(key, value); + if (arr) { + // Loop instead of using `.map` in order to preserve object identity + // for plugin array for use during config chain processing. + arr.forEach((item, i) => assertPluginItem(key, i, item)); + } + return (arr: any); +} +function assertPluginItem( + key: string, + index: number, + value: mixed, +): PluginItem { + if (Array.isArray(value)) { + if (value.length === 0) { + throw new Error(`.${key}[${index}] must include an object`); + } + if (value.length > 2) { + throw new Error(`.${key}[${index}] may only be a two-tuple`); + } + + assertPluginTarget(key, index, true, value[0]); + + if (value.length === 2) { + const opts = value[1]; + if (opts != null && (typeof opts !== "object" || Array.isArray(opts))) { + throw new Error( + `.${key}[${index}][1] must be an object, null, or undefined`, + ); + } + } + } else { + assertPluginTarget(key, index, false, value); + } + + return (value: any); +} +function assertPluginTarget( + key: string, + index: number, + inArray: boolean, + value: mixed, +): PluginTarget { + if ( + (typeof value !== "object" || !value) && + typeof value !== "string" && + typeof value !== "function" + ) { + throw new Error( + `.${key}[${index}]${inArray + ? `[0]` + : ""} must be a string, object, function`, + ); + } + return value; +} + +function assertArray(key: string, value: mixed): ?$ReadOnlyArray { + if (value != null && !Array.isArray(value)) { + throw new Error(`.${key} must be an array, null, or undefined`); + } + return value; +} diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index ae63a9ba92..8a4537a272 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -4,13 +4,13 @@ import * as context from "../index"; import Plugin from "./plugin"; import defaults from "lodash/defaults"; import merge from "lodash/merge"; -import removed from "./removed"; -import buildConfigChain from "./build-config-chain"; +import buildConfigChain, { type ConfigItem } from "./build-config-chain"; import path from "path"; import traverse from "@babel/traverse"; import clone from "lodash/clone"; import { makeWeakCache } from "./caching"; import { getEnv } from "./helpers/environment"; +import { validate, type ValidatedOptions, type PluginItem } from "./options"; import { loadPlugin, @@ -19,50 +19,14 @@ import { loadGenerator, } from "./loading/files"; -type MergeOptions = { - +type: "arguments" | "options" | "preset", - options: {}, - alias: string, - dirname: string, -}; - -const optionNames = new Set([ - "filename", - "filenameRelative", - "inputSourceMap", - "env", - "retainLines", - "highlightCode", - "presets", - "plugins", - "ignore", - "only", - "code", - "ast", - "extends", - "comments", - "shouldPrintComment", - "wrapPluginVisitorMethod", - "compact", - "minified", - "sourceMaps", - "sourceMapTarget", - "sourceFileName", - "sourceRoot", - "babelrc", - "sourceType", - "auxiliaryCommentBefore", - "auxiliaryCommentAfter", - "getModuleId", - "moduleRoot", - "moduleIds", - "moduleId", - "passPerPreset", - // Deprecate top level parserOpts - "parserOpts", - // Deprecate top level generatorOpts - "generatorOpts", -]); +type MergeOptions = + | ConfigItem + | { + type: "preset", + options: ValidatedOptions, + alias: string, + dirname: string, + }; const ALLOWED_PLUGIN_KEYS = new Set([ "name", @@ -82,11 +46,11 @@ export default function manageOptions(opts: {}): { class OptionManager { constructor() { - this.options = createInitialOptions(); + this.options = {}; this.passes = [[]]; } - options: Object; + options: ValidatedOptions; passes: Array>; /** @@ -108,12 +72,6 @@ class OptionManager { loadPresetDescriptor(descriptor), ); - if ( - config.options.passPerPreset != null && - typeof config.options.passPerPreset !== "boolean" - ) { - throw new Error(".passPerPreset must be a boolean or undefined"); - } const passPerPreset = config.options.passPerPreset; pass = pass || this.passes[0]; @@ -142,12 +100,22 @@ class OptionManager { delete options.env; delete options.plugins; delete options.presets; + delete options.passPerPreset; + + // "sourceMap" is just aliased to sourceMap, so copy it over as + // we merge the options together. + if (options.sourceMap) { + options.sourceMaps = options.sourceMap; + delete options.sourceMap; + } merge(this.options, options); } - init(opts: {}) { - const configChain = buildConfigChain(opts); + init(inputOpts: {}) { + const args = validate("arguments", inputOpts); + + const configChain = buildConfigChain(args); if (!configChain) return null; try { @@ -158,15 +126,13 @@ class OptionManager { // There are a few case where thrown errors will try to annotate themselves multiple times, so // to keep things simple we just bail out if re-wrapping the message. if (!/^\[BABEL\]/.test(e.message)) { - const filename = - typeof opts.filename === "string" ? opts.filename : null; - e.message = `[BABEL] ${filename || "unknown"}: ${e.message}`; + e.message = `[BABEL] ${args.filename || "unknown"}: ${e.message}`; } throw e; } - opts = this.options; + const opts: Object = merge(createInitialOptions(), this.options); // Tack the passes onto the object itself so that, if this object is passed back to Babel a second time, // it will be in the right structure to not change behavior. @@ -175,6 +141,7 @@ class OptionManager { .slice(1) .filter(plugins => plugins.length > 0) .map(plugins => ({ plugins })); + opts.passPerPreset = opts.presets.length > 0; if (opts.inputSourceMap) { opts.sourceMaps = true; @@ -231,20 +198,13 @@ type LoadedDescriptor = { /** * Load and validate the given config into a set of options, plugins, and presets. */ -const loadConfig = makeWeakCache((config): { +const loadConfig = makeWeakCache((config: MergeOptions): { options: {}, plugins: Array, presets: Array, } => { const options = normalizeOptions(config); - if ( - config.options.plugins != null && - !Array.isArray(config.options.plugins) - ) { - throw new Error(".plugins should be an array, null, or undefined"); - } - const plugins = (config.options.plugins || []).map((plugin, index) => { const { filepath, value, options } = normalizePair( plugin, @@ -260,13 +220,6 @@ const loadConfig = makeWeakCache((config): { }; }); - if ( - config.options.presets != null && - !Array.isArray(config.options.presets) - ) { - throw new Error(".presets should be an array, null, or undefined"); - } - const presets = (config.options.presets || []).map((preset, index) => { const { filepath, value, options } = normalizePair( preset, @@ -405,7 +358,7 @@ const instantiatePreset = makeWeakCache( ({ value, dirname, alias }: LoadedDescriptor): MergeOptions => { return { type: "preset", - options: value, + options: validate("preset", value), alias, dirname, }; @@ -416,72 +369,12 @@ const instantiatePreset = makeWeakCache( * Validate and return the options object for the config. */ function normalizeOptions(config) { - const alias = config.alias || "foreign"; - const type = config.type; - - // - if (typeof config.options !== "object" || Array.isArray(config.options)) { - throw new TypeError(`Invalid options type for ${alias}`); - } - // const options = Object.assign({}, config.options); - if (type !== "arguments") { - if (options.filename !== undefined) { - throw new Error(`${alias}.filename is only allowed as a root argument`); - } - - if (options.babelrc !== undefined) { - throw new Error(`${alias}.babelrc is only allowed as a root argument`); - } - } - - if (type === "preset") { - if (options.only !== undefined) { - throw new Error(`${alias}.only is not supported in a preset`); - } - if (options.ignore !== undefined) { - throw new Error(`${alias}.ignore is not supported in a preset`); - } - if (options.extends !== undefined) { - throw new Error(`${alias}.extends is not supported in a preset`); - } - if (options.env !== undefined) { - throw new Error(`${alias}.env is not supported in a preset`); - } - } - - if (options.sourceMap !== undefined) { - if (options.sourceMaps !== undefined) { - throw new Error(`Both ${alias}.sourceMap and .sourceMaps have been set`); - } - - options.sourceMaps = options.sourceMap; - delete options.sourceMap; - } - - for (const key in options) { - // check for an unknown option - if (!optionNames.has(key)) { - if (removed[key]) { - const { message, version = 5 } = removed[key]; - - throw new ReferenceError( - `Using removed Babel ${version} option: ${alias}.${key} - ${message}`, - ); - } else { - // eslint-disable-next-line max-len - const unknownOptErr = `Unknown option: ${alias}.${key}. Check out http://babeljs.io/docs/usage/options/ for more information about options.`; - - throw new ReferenceError(unknownOptErr); - } - } - } - if (options.parserOpts && typeof options.parserOpts.parser === "string") { options.parserOpts = Object.assign({}, options.parserOpts); - options.parserOpts.parser = loadParser( + (options.parserOpts: any).parser = loadParser( options.parserOpts.parser, config.dirname, ).value; @@ -492,16 +385,12 @@ function normalizeOptions(config) { typeof options.generatorOpts.generator === "string" ) { options.generatorOpts = Object.assign({}, options.generatorOpts); - options.generatorOpts.generator = loadGenerator( + (options.generatorOpts: any).generator = loadGenerator( options.generatorOpts.generator, config.dirname, ).value; } - delete options.passPerPreset; - delete options.plugins; - delete options.presets; - return options; } @@ -509,7 +398,7 @@ function normalizeOptions(config) { * Given a plugin/preset item, resolve it into a standard format. */ function normalizePair( - pair: mixed, + pair: PluginItem, resolver, dirname, ): { @@ -519,14 +408,8 @@ function normalizePair( } { let options; let value = pair; - if (Array.isArray(pair)) { - if (pair.length > 2) { - throw new Error( - `Unexpected extra options ${JSON.stringify(pair.slice(2))}.`, - ); - } - - [value, options] = pair; + if (Array.isArray(value)) { + [value, options] = value; } let filepath = null; diff --git a/packages/babel-core/src/config/options.js b/packages/babel-core/src/config/options.js new file mode 100644 index 0000000000..75a91388f9 --- /dev/null +++ b/packages/babel-core/src/config/options.js @@ -0,0 +1,255 @@ +// @flow + +import removed from "./removed"; +import { + assertString, + assertBoolean, + assertObject, + assertInputSourceMap, + assertIgnoreList, + assertPluginList, + assertFunction, + assertSourceMaps, + assertCompact, + assertSourceType, +} from "./option-assertions"; + +type ValidatorSet = { + [string]: Validator, +}; + +type Validator = (string, mixed) => T; + +const ROOT_VALIDATORS: ValidatorSet = { + filename: (assertString: Validator< + $PropertyType, + >), + filenameRelative: (assertString: Validator< + $PropertyType, + >), + babelrc: (assertBoolean: Validator< + $PropertyType, + >), + code: (assertBoolean: Validator<$PropertyType>), + ast: (assertBoolean: Validator<$PropertyType>), +}; + +const NONPRESET_VALIDATORS: ValidatorSet = { + extends: (assertString: Validator< + $PropertyType, + >), + env: (assertEnvSet: Validator<$PropertyType>), + ignore: (assertIgnoreList: Validator< + $PropertyType, + >), + only: (assertIgnoreList: Validator<$PropertyType>), +}; + +const COMMON_VALIDATORS: ValidatorSet = { + // TODO: Should 'inputSourceMap' be moved to be a root-only option? + // We may want a boolean-only version to be a common option, with the + // object only allowed as a root config argument. + inputSourceMap: (assertInputSourceMap: Validator< + $PropertyType, + >), + presets: (assertPluginList: Validator< + $PropertyType, + >), + plugins: (assertPluginList: Validator< + $PropertyType, + >), + passPerPreset: (assertBoolean: Validator< + $PropertyType, + >), + retainLines: (assertBoolean: Validator< + $PropertyType, + >), + comments: (assertBoolean: Validator< + $PropertyType, + >), + shouldPrintComment: (assertFunction: Validator< + $PropertyType, + >), + compact: (assertCompact: Validator< + $PropertyType, + >), + minified: (assertBoolean: Validator< + $PropertyType, + >), + auxiliaryCommentBefore: (assertString: Validator< + $PropertyType, + >), + auxiliaryCommentAfter: (assertString: Validator< + $PropertyType, + >), + sourceType: (assertSourceType: Validator< + $PropertyType, + >), + wrapPluginVisitorMethod: (assertFunction: Validator< + $PropertyType, + >), + highlightCode: (assertBoolean: Validator< + $PropertyType, + >), + sourceMaps: (assertSourceMaps: Validator< + $PropertyType, + >), + sourceMap: (assertSourceMaps: Validator< + $PropertyType, + >), + sourceMapTarget: (assertString: Validator< + $PropertyType, + >), + sourceFileName: (assertString: Validator< + $PropertyType, + >), + sourceRoot: (assertString: Validator< + $PropertyType, + >), + getModuleId: (assertFunction: Validator< + $PropertyType, + >), + moduleRoot: (assertString: Validator< + $PropertyType, + >), + moduleIds: (assertBoolean: Validator< + $PropertyType, + >), + moduleId: (assertString: Validator< + $PropertyType, + >), + parserOpts: (assertObject: Validator< + $PropertyType, + >), + generatorOpts: (assertObject: Validator< + $PropertyType, + >), +}; +export type ValidatedOptions = { + filename?: ?string, + filenameRelative?: ?string, + babelrc?: ?boolean, + code?: ?boolean, + ast?: ?boolean, + inputSourceMap?: ?RootInputSourceMapOption, + + extends?: ?string, + env?: ?EnvSet, + ignore?: ?IgnoreList, + only?: ?IgnoreList, + + presets?: ?PluginList, + plugins?: ?PluginList, + passPerPreset?: ?boolean, + + // Options for @babel/generator + retainLines?: ?boolean, + comments?: ?boolean, + shouldPrintComment?: ?Function, + compact?: ?CompactOption, + minified?: ?boolean, + auxiliaryCommentBefore?: ?string, + auxiliaryCommentAfter?: ?string, + + // Parser + sourceType?: ?SourceTypeOption, + + wrapPluginVisitorMethod?: ?Function, + highlightCode?: ?boolean, + + // Sourcemap generation options. + sourceMaps?: ?SourceMapsOption, + sourceMap?: ?SourceMapsOption, + sourceMapTarget?: ?string, + sourceFileName?: ?string, + sourceRoot?: ?string, + + // AMD/UMD/SystemJS module naming options. + getModuleId?: ?Function, + moduleRoot?: ?string, + moduleIds?: ?boolean, + moduleId?: ?string, + + // Deprecate top level parserOpts + parserOpts?: ?{}, + // Deprecate top level generatorOpts + generatorOpts?: ?{}, +}; + +export type EnvSet = { + [string]: ?T, +}; +export type IgnoreItem = string | Function | RegExp; +export type IgnoreList = $ReadOnlyArray; + +export type PluginTarget = string | {} | Function; +export type PluginItem = PluginTarget | [PluginTarget, {} | void]; +export type PluginList = $ReadOnlyArray; + +export type SourceMapsOption = boolean | "inline" | "both"; +export type SourceTypeOption = "module" | "script"; +export type CompactOption = boolean | "auto"; +export type RootInputSourceMapOption = {} | boolean; + +export type OptionsType = "arguments" | "file" | "env" | "preset"; + +export function validate(type: OptionsType, opts: {}): ValidatedOptions { + assertNoDuplicateSourcemap(opts); + + Object.keys(opts).forEach(key => { + if (type === "preset" && NONPRESET_VALIDATORS[key]) { + throw new Error(`.${key} is not allowed in preset options`); + } + if (type !== "arguments" && ROOT_VALIDATORS[key]) { + throw new Error(`.${key} is only allowed in root programmatic options`); + } + + const validator = + COMMON_VALIDATORS[key] || + NONPRESET_VALIDATORS[key] || + ROOT_VALIDATORS[key]; + + if (validator) validator(key, opts[key]); + else throw buildUnknownError(key); + }); + + return (opts: any); +} + +function buildUnknownError(key: string) { + if (removed[key]) { + const { message, version = 5 } = removed[key]; + + throw new ReferenceError( + `Using removed Babel ${version} option: .${key} - ${message}`, + ); + } else { + // eslint-disable-next-line max-len + const unknownOptErr = `Unknown option: .${key}. Check out http://babeljs.io/docs/usage/options/ for more information about options.`; + + throw new ReferenceError(unknownOptErr); + } +} + +function has(obj: {}, key: string) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +function assertNoDuplicateSourcemap(opts: {}): void { + if (has(opts, "sourceMap") && has(opts, "sourceMaps")) { + throw new Error(".sourceMap is an alias for .sourceMaps, cannot use both"); + } +} + +function assertEnvSet(key: string, value: mixed): ?EnvSet { + const obj = assertObject(key, value); + if (obj) { + // Validate but don't copy the .env object in order to preserve + // object identity for use during config chain processing. + for (const key of Object.keys(obj)) { + const env = assertObject(key, obj[key]); + if (env) validate("env", env); + } + } + return (obj: any); +} diff --git a/packages/babel-core/test/api.js b/packages/babel-core/test/api.js index e66d88d6cf..cad0308862 100644 --- a/packages/babel-core/test/api.js +++ b/packages/babel-core/test/api.js @@ -145,7 +145,7 @@ describe("api", function() { babel.transform("", { plugins: [__dirname + "/../../babel-plugin-syntax-jsx", false], }); - }, /Error: \[BABEL\] unknown: Unexpected falsy value: false/); + }, /.plugins\[1\] must be a string, object, function/); }); it("options merge backwards", function() { diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index e004a8aaa2..097112cb07 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -372,7 +372,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { plugins: ["extended"], }, @@ -380,7 +380,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { extends: "./extended.babelrc.json", plugins: ["root"], @@ -389,7 +389,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -416,7 +416,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -424,7 +424,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { plugins: ["dir2"], }, @@ -451,7 +451,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { plugins: ["extended"], }, @@ -459,7 +459,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { extends: "./extended.babelrc.json", plugins: ["root"], @@ -468,7 +468,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -495,7 +495,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -503,7 +503,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { env: { bar: { @@ -540,7 +540,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -548,7 +548,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { env: { bar: { @@ -564,7 +564,7 @@ describe("buildConfigChain", function() { dirname: fixture("env"), }, { - type: "options", + type: "env", options: { plugins: ["env-foo"], }, @@ -594,7 +594,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -602,7 +602,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { env: { bar: { @@ -618,7 +618,7 @@ describe("buildConfigChain", function() { dirname: fixture("env"), }, { - type: "options", + type: "env", options: { plugins: ["env-bar"], }, @@ -647,7 +647,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { plugins: ["pkg-plugin"], }, @@ -655,7 +655,7 @@ describe("buildConfigChain", function() { dirname: fixture("pkg"), }, { - type: "options", + type: "file", options: { ignore: ["pkg-ignore"], }, @@ -682,7 +682,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -690,7 +690,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { plugins: ["foo", "bar"], }, @@ -717,7 +717,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -725,7 +725,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { compact: true, }, @@ -752,7 +752,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -760,7 +760,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { plugins: ["foo", "bar"], }, @@ -786,7 +786,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -794,7 +794,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { plugins: ["extended"], }, @@ -802,7 +802,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { extends: "../extended.babelrc.json", plugins: ["foo", "bar"], @@ -833,7 +833,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -841,7 +841,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { plugins: ["json"], }, @@ -869,7 +869,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -877,7 +877,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { ignore: ["*", "!src.js"], }, @@ -910,7 +910,7 @@ describe("buildConfigChain", function() { const expected = [ { - type: "options", + type: "file", options: { ignore: ["root-ignore"], }, @@ -918,7 +918,7 @@ describe("buildConfigChain", function() { dirname: fixture(), }, { - type: "options", + type: "file", options: { ignore: ["*", "!folder"], }, diff --git a/packages/babel-core/test/option-manager.js b/packages/babel-core/test/option-manager.js index 7e233a2a67..f6429615f5 100644 --- a/packages/babel-core/test/option-manager.js +++ b/packages/babel-core/test/option-manager.js @@ -17,7 +17,7 @@ describe("option-manager", () => { manageOptions({ randomOption: true, }); - }, /Unknown option: base.randomOption/); + }, /Unknown option: .randomOption/); }); it("throws for removed babel 5 options", () => { @@ -29,7 +29,7 @@ describe("option-manager", () => { }); }, // eslint-disable-next-line max-len - /Using removed Babel 5 option: base.auxiliaryComment - Use `auxiliaryCommentBefore` or `auxiliaryCommentAfter`/, + /Using removed Babel 5 option: .auxiliaryComment - Use `auxiliaryCommentBefore` or `auxiliaryCommentAfter`/, ); }); @@ -47,7 +47,7 @@ describe("option-manager", () => { describe("source type", function() { it("should set module for .mjs extension", () => { const config = manageOptions({ - sourceType: "program", + sourceType: "script", filename: "foo.mjs", });