diff --git a/packages/babel-core/src/config/validation/option-assertions.js b/packages/babel-core/src/config/validation/option-assertions.js index 7c76f97937..a00f430b8c 100644 --- a/packages/babel-core/src/config/validation/option-assertions.js +++ b/packages/babel-core/src/config/validation/option-assertions.js @@ -13,16 +13,54 @@ import type { SourceTypeOption, CompactOption, RootInputSourceMapOption, + NestingPath, } from "./options"; export type ValidatorSet = { [string]: Validator, }; -export type Validator = (string, mixed) => T; +export type Validator = (OptionPath, mixed) => T; + +export function msg(loc: NestingPath | GeneralPath) { + switch (loc.type) { + case "root": + return ``; + case "env": + return `${msg(loc.parent)}.env["${loc.name}"]`; + case "overrides": + return `${msg(loc.parent)}.overrides[${loc.index}]`; + case "option": + return `${msg(loc.parent)}.${loc.name}`; + case "access": + return `${msg(loc.parent)}[${JSON.stringify(loc.name)}]`; + default: + throw new Error(`Assertion failure: Unknown type ${loc.type}`); + } +} + +export function access(loc: GeneralPath, name: string | number): AccessPath { + return { + type: "access", + name, + parent: loc, + }; +} + +export type OptionPath = $ReadOnly<{ + type: "option", + name: string, + parent: NestingPath, +}>; +type AccessPath = $ReadOnly<{ + type: "access", + name: string | number, + parent: GeneralPath, +}>; +type GeneralPath = OptionPath | AccessPath; export function assertSourceMaps( - key: string, + loc: OptionPath, value: mixed, ): SourceMapsOption | void { if ( @@ -32,21 +70,24 @@ export function assertSourceMaps( value !== "both" ) { throw new Error( - `.${key} must be a boolean, "inline", "both", or undefined`, + `${msg(loc)} must be a boolean, "inline", "both", or undefined`, ); } return value; } -export function assertCompact(key: string, value: mixed): CompactOption | void { +export function assertCompact( + loc: OptionPath, + value: mixed, +): CompactOption | void { if (value !== undefined && typeof value !== "boolean" && value !== "auto") { - throw new Error(`.${key} must be a boolean, "auto", or undefined`); + throw new Error(`${msg(loc)} must be a boolean, "auto", or undefined`); } return value; } export function assertSourceType( - key: string, + loc: OptionPath, value: mixed, ): SourceTypeOption | void { if ( @@ -56,14 +97,14 @@ export function assertSourceType( value !== "unambiguous" ) { throw new Error( - `.${key} must be "module", "script", "unambiguous", or undefined`, + `${msg(loc)} must be "module", "script", "unambiguous", or undefined`, ); } return value; } export function assertInputSourceMap( - key: string, + loc: OptionPath, value: mixed, ): RootInputSourceMapOption | void { if ( @@ -71,75 +112,82 @@ export function assertInputSourceMap( typeof value !== "boolean" && (typeof value !== "object" || !value) ) { - throw new Error(".inputSourceMap must be a boolean, object, or undefined"); + throw new Error(`${msg(loc)} must be a boolean, object, or undefined`); } return value; } -export function assertString(key: string, value: mixed): string | void { +export function assertString(loc: GeneralPath, value: mixed): string | void { if (value !== undefined && typeof value !== "string") { - throw new Error(`.${key} must be a string, or undefined`); + throw new Error(`${msg(loc)} must be a string, or undefined`); } return value; } -export function assertFunction(key: string, value: mixed): Function | void { +export function assertFunction( + loc: GeneralPath, + value: mixed, +): Function | void { if (value !== undefined && typeof value !== "function") { - throw new Error(`.${key} must be a function, or undefined`); + throw new Error(`${msg(loc)} must be a function, or undefined`); } return value; } -export function assertBoolean(key: string, value: mixed): boolean | void { +export function assertBoolean(loc: GeneralPath, value: mixed): boolean | void { if (value !== undefined && typeof value !== "boolean") { - throw new Error(`.${key} must be a boolean, or undefined`); + throw new Error(`${msg(loc)} must be a boolean, or undefined`); } return value; } -export function assertObject(key: string, value: mixed): {} | void { +export function assertObject(loc: GeneralPath, value: mixed): {} | void { if ( value !== undefined && (typeof value !== "object" || Array.isArray(value) || !value) ) { - throw new Error(`.${key} must be an object, or undefined`); + throw new Error(`${msg(loc)} must be an object, or undefined`); } return value; } -export function assertArray(key: string, value: mixed): ?$ReadOnlyArray { +export function assertArray( + loc: GeneralPath, + value: mixed, +): ?$ReadOnlyArray { if (value != null && !Array.isArray(value)) { - throw new Error(`.${key} must be an array, or undefined`); + throw new Error(`${msg(loc)} must be an array, or undefined`); } return value; } -export function assertIgnoreList(key: string, value: mixed): IgnoreList | void { - const arr = assertArray(key, value); +export function assertIgnoreList( + loc: OptionPath, + value: mixed, +): IgnoreList | void { + const arr = assertArray(loc, value); if (arr) { - arr.forEach((item, i) => assertIgnoreItem(key, i, item)); + arr.forEach((item, i) => assertIgnoreItem(access(loc, i), item)); } return (arr: any); } -function assertIgnoreItem( - key: string, - index: number, - value: mixed, -): IgnoreItem { +function assertIgnoreItem(loc: GeneralPath, 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 undefined`, + `${msg( + loc, + )} must be an array of string/Funtion/RegExp values, or undefined`, ); } return value; } export function assertConfigApplicableTest( - key: string, + loc: OptionPath, value: mixed, ): ConfigApplicableTest | void { if (value === undefined) return value; @@ -147,12 +195,14 @@ export function assertConfigApplicableTest( if (Array.isArray(value)) { value.forEach((item, i) => { if (!checkValidTest(item)) { - throw new Error(`.${key}[${i}] must be a string/Function/RegExp.`); + throw new Error( + `${msg(access(loc, i))} must be a string/Function/RegExp.`, + ); } }); } else if (!checkValidTest(value)) { throw new Error( - `.${key} must be a string/Function/RegExp, or an array of those`, + `${msg(loc)} must be a string/Function/RegExp, or an array of those`, ); } return (value: any); @@ -167,7 +217,7 @@ function checkValidTest(value: mixed): boolean { } export function assertConfigFileSearch( - key: string, + loc: OptionPath, value: mixed, ): ConfigFileSearch | void { if ( @@ -176,7 +226,7 @@ export function assertConfigFileSearch( typeof value !== "string" ) { throw new Error( - `.${key} must be a undefined, a boolean, a string, ` + + `${msg(loc)} must be a undefined, a boolean, a string, ` + `got ${JSON.stringify(value)}`, ); } @@ -185,7 +235,7 @@ export function assertConfigFileSearch( } export function assertBabelrcSearch( - key: string, + loc: OptionPath, value: mixed, ): BabelrcSearch | void { if (value === undefined || typeof value === "boolean") return value; @@ -193,44 +243,43 @@ export function assertBabelrcSearch( if (Array.isArray(value)) { value.forEach((item, i) => { if (!checkValidTest(item)) { - throw new Error(`.${key}[${i}] must be a string/Function/RegExp.`); + throw new Error( + `${msg(access(loc, i))} must be a string/Function/RegExp.`, + ); } }); } else if (!checkValidTest(value)) { throw new Error( - `.${key} must be a undefined, a boolean, a string/Function/RegExp ` + + `${msg(loc)} must be a undefined, a boolean, a string/Function/RegExp ` + `or an array of those, got ${JSON.stringify(value)}`, ); } return (value: any); } -export function assertPluginList(key: string, value: mixed): PluginList | void { - const arr = assertArray(key, value); +export function assertPluginList( + loc: OptionPath, + value: mixed, +): PluginList | void { + const arr = assertArray(loc, 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)); + arr.forEach((item, i) => assertPluginItem(access(loc, i), item)); } return (arr: any); } -function assertPluginItem( - key: string, - index: number, - value: mixed, -): PluginItem { +function assertPluginItem(loc: GeneralPath, value: mixed): PluginItem { if (Array.isArray(value)) { if (value.length === 0) { - throw new Error(`.${key}[${index}] must include an object`); + throw new Error(`${msg(loc)} must include an object`); } if (value.length > 3) { - throw new Error( - `.${key}[${index}] may only be a two-tuple or three-tuple`, - ); + throw new Error(`${msg(loc)} may only be a two-tuple or three-tuple`); } - assertPluginTarget(key, index, true, value[0]); + assertPluginTarget(access(loc, 0), value[0]); if (value.length > 1) { const opts = value[1]; @@ -240,38 +289,31 @@ function assertPluginItem( (typeof opts !== "object" || Array.isArray(opts)) ) { throw new Error( - `.${key}[${index}][1] must be an object, false, or undefined`, + `${msg(access(loc, 1))} must be an object, false, or undefined`, ); } } if (value.length === 3) { const name = value[2]; if (name !== undefined && typeof name !== "string") { - throw new Error(`.${key}[${index}][2] must be a string, or undefined`); + throw new Error( + `${msg(access(loc, 2))} must be a string, or undefined`, + ); } } } else { - assertPluginTarget(key, index, false, value); + assertPluginTarget(loc, value); } return (value: any); } -function assertPluginTarget( - key: string, - index: number, - inArray: boolean, - value: mixed, -): PluginTarget { +function assertPluginTarget(loc: GeneralPath, 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`, - ); + throw new Error(`${msg(loc)} must be a string, object, function`); } return value; } diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index 5b4585be02..085f0d3608 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -5,6 +5,8 @@ import Plugin from "../plugin"; import removed from "./removed"; import { + msg, + access, assertString, assertBoolean, assertObject, @@ -21,6 +23,7 @@ import { assertSourceType, type ValidatorSet, type Validator, + type OptionPath, } from "./option-assertions"; const ROOT_VALIDATORS: ValidatorSet = { @@ -248,24 +251,62 @@ export type SourceTypeOption = "module" | "script" | "unambiguous"; export type CompactOption = boolean | "auto"; export type RootInputSourceMapOption = {} | boolean; -export type OptionsType = +export type OptionsSource = | "arguments" - | "env" - | "preset" - | "override" | "configfile" | "babelrcfile" - | "extendsfile"; + | "extendsfile" + | "preset"; + +type RootPath = $ReadOnly<{ + type: "root", + source: OptionsSource, +}>; +type OverridesPath = $ReadOnly<{ + type: "overrides", + index: number, + parent: RootPath, +}>; +type EnvPath = $ReadOnly<{ + type: "env", + name: string, + parent: RootPath | OverridesPath, +}>; +export type NestingPath = RootPath | OverridesPath | EnvPath; + +function getSource(loc: NestingPath): OptionsSource { + return loc.type === "root" ? loc.source : getSource(loc.parent); +} + +export function validate(type: OptionsSource, opts: {}): ValidatedOptions { + return validateNested( + { + type: "root", + source: type, + }, + opts, + ); +} + +function validateNested(loc: NestingPath, opts: {}) { + const type = getSource(loc); -export function validate(type: OptionsType, opts: {}): ValidatedOptions { assertNoDuplicateSourcemap(opts); Object.keys(opts).forEach(key => { + const optLoc = { + type: "option", + name: key, + parent: loc, + }; + if (type === "preset" && NONPRESET_VALIDATORS[key]) { - throw new Error(`.${key} is not allowed in preset options`); + throw new Error(`${msg(optLoc)} is not allowed in preset options`); } if (type !== "arguments" && ROOT_VALIDATORS[key]) { - throw new Error(`.${key} is only allowed in root programmatic options`); + throw new Error( + `${msg(optLoc)} is only allowed in root programmatic options`, + ); } if ( type !== "arguments" && @@ -274,48 +315,47 @@ export function validate(type: OptionsType, opts: {}): ValidatedOptions { ) { if (type === "babelrcfile" || type === "extendsfile") { throw new Error( - `.${key} is not allowed in .babelrc or "extend"ed files, only in root programmatic options, ` + + `${msg( + optLoc, + )} is not allowed in .babelrc or "extends"ed files, only in root programmatic options, ` + `or babel.config.js/config file options`, ); } throw new Error( - `.${key} is only allowed in root programmatic options, or babel.config.js/config file options`, + `${msg( + optLoc, + )} is only allowed in root programmatic options, or babel.config.js/config file options`, ); } - if (type === "env" && key === "env") { - throw new Error(`.${key} is not allowed inside another env block`); - } - if (type === "env" && key === "overrides") { - throw new Error(`.${key} is not allowed inside an env block`); - } - if (type === "override" && key === "overrides") { - throw new Error(`.${key} is not allowed inside an overrides block`); - } const validator = COMMON_VALIDATORS[key] || NONPRESET_VALIDATORS[key] || BABELRC_VALIDATORS[key] || - ROOT_VALIDATORS[key]; + ROOT_VALIDATORS[key] || + throwUnknownError; - if (validator) validator(key, opts[key]); - else throw buildUnknownError(key); + validator(optLoc, opts[key]); }); return (opts: any); } -function buildUnknownError(key: string) { +function throwUnknownError(loc: OptionPath) { + const key = loc.name; + if (removed[key]) { const { message, version = 5 } = removed[key]; throw new ReferenceError( - `Using removed Babel ${version} option: .${key} - ${message}`, + `Using removed Babel ${version} option: ${msg(loc)} - ${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.`; + const unknownOptErr = `Unknown option: ${msg( + loc, + )}. Check out http://babeljs.io/docs/usage/options/ for more information about options.`; throw new ReferenceError(unknownOptErr); } @@ -331,27 +371,53 @@ function assertNoDuplicateSourcemap(opts: {}): void { } } -function assertEnvSet(key: string, value: mixed): EnvSet { - const obj = assertObject(key, value); +function assertEnvSet(loc: OptionPath, value: mixed): EnvSet { + if (loc.parent.type === "env") { + throw new Error(`${msg(loc)} is not allowed inside of another .env block`); + } + const parent: RootPath | OverridesPath = loc.parent; + + const obj = assertObject(loc, 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); + for (const envName of Object.keys(obj)) { + const env = assertObject(access(loc, envName), obj[envName]); + if (!env) continue; + + const envLoc = { + type: "env", + name: envName, + parent, + }; + validateNested(envLoc, env); } } return (obj: any); } -function assertOverridesList(key: string, value: mixed): OverridesList { - const arr = assertArray(key, value); +function assertOverridesList(loc: OptionPath, value: mixed): OverridesList { + if (loc.parent.type === "env") { + throw new Error(`${msg(loc)} is not allowed inside an .env block`); + } + if (loc.parent.type === "overrides") { + throw new Error(`${msg(loc)} is not allowed inside an .overrides block`); + } + const parent: RootPath = loc.parent; + + const arr = assertArray(loc, value); if (arr) { for (const [index, item] of arr.entries()) { - const env = assertObject(`${index}`, item); - if (!env) throw new Error(`.${key}[${index}] must be an object`); + const objLoc = access(loc, index); + const env = assertObject(objLoc, item); + if (!env) throw new Error(`${msg(objLoc)} must be an object`); - validate("override", env); + const overridesLoc = { + type: "overrides", + index, + parent, + }; + validateNested(overridesLoc, env); } } return (arr: any);