Take top-level config source into consideration when processing nested env/overrides. (#8493)

This commit is contained in:
Logan Smyth 2018-08-19 21:46:09 -07:00 committed by GitHub
parent ef68114d67
commit c2a2e24965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 206 additions and 98 deletions

View File

@ -13,16 +13,54 @@ import type {
SourceTypeOption, SourceTypeOption,
CompactOption, CompactOption,
RootInputSourceMapOption, RootInputSourceMapOption,
NestingPath,
} from "./options"; } from "./options";
export type ValidatorSet = { export type ValidatorSet = {
[string]: Validator<any>, [string]: Validator<any>,
}; };
export type Validator<T> = (string, mixed) => T; export type Validator<T> = (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( export function assertSourceMaps(
key: string, loc: OptionPath,
value: mixed, value: mixed,
): SourceMapsOption | void { ): SourceMapsOption | void {
if ( if (
@ -32,21 +70,24 @@ export function assertSourceMaps(
value !== "both" value !== "both"
) { ) {
throw new Error( throw new Error(
`.${key} must be a boolean, "inline", "both", or undefined`, `${msg(loc)} must be a boolean, "inline", "both", or undefined`,
); );
} }
return value; 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") { 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; return value;
} }
export function assertSourceType( export function assertSourceType(
key: string, loc: OptionPath,
value: mixed, value: mixed,
): SourceTypeOption | void { ): SourceTypeOption | void {
if ( if (
@ -56,14 +97,14 @@ export function assertSourceType(
value !== "unambiguous" value !== "unambiguous"
) { ) {
throw new Error( throw new Error(
`.${key} must be "module", "script", "unambiguous", or undefined`, `${msg(loc)} must be "module", "script", "unambiguous", or undefined`,
); );
} }
return value; return value;
} }
export function assertInputSourceMap( export function assertInputSourceMap(
key: string, loc: OptionPath,
value: mixed, value: mixed,
): RootInputSourceMapOption | void { ): RootInputSourceMapOption | void {
if ( if (
@ -71,75 +112,82 @@ export function assertInputSourceMap(
typeof value !== "boolean" && typeof value !== "boolean" &&
(typeof value !== "object" || !value) (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; 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") { 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; 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") { 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; 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") { 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; return value;
} }
export function assertObject(key: string, value: mixed): {} | void { export function assertObject(loc: GeneralPath, value: mixed): {} | void {
if ( if (
value !== undefined && value !== undefined &&
(typeof value !== "object" || Array.isArray(value) || !value) (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; return value;
} }
export function assertArray(key: string, value: mixed): ?$ReadOnlyArray<mixed> { export function assertArray(
loc: GeneralPath,
value: mixed,
): ?$ReadOnlyArray<mixed> {
if (value != null && !Array.isArray(value)) { 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; return value;
} }
export function assertIgnoreList(key: string, value: mixed): IgnoreList | void { export function assertIgnoreList(
const arr = assertArray(key, value); loc: OptionPath,
value: mixed,
): IgnoreList | void {
const arr = assertArray(loc, value);
if (arr) { if (arr) {
arr.forEach((item, i) => assertIgnoreItem(key, i, item)); arr.forEach((item, i) => assertIgnoreItem(access(loc, i), item));
} }
return (arr: any); return (arr: any);
} }
function assertIgnoreItem( function assertIgnoreItem(loc: GeneralPath, value: mixed): IgnoreItem {
key: string,
index: number,
value: mixed,
): IgnoreItem {
if ( if (
typeof value !== "string" && typeof value !== "string" &&
typeof value !== "function" && typeof value !== "function" &&
!(value instanceof RegExp) !(value instanceof RegExp)
) { ) {
throw new Error( 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; return value;
} }
export function assertConfigApplicableTest( export function assertConfigApplicableTest(
key: string, loc: OptionPath,
value: mixed, value: mixed,
): ConfigApplicableTest | void { ): ConfigApplicableTest | void {
if (value === undefined) return value; if (value === undefined) return value;
@ -147,12 +195,14 @@ export function assertConfigApplicableTest(
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((item, i) => { value.forEach((item, i) => {
if (!checkValidTest(item)) { 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)) { } else if (!checkValidTest(value)) {
throw new Error( 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); return (value: any);
@ -167,7 +217,7 @@ function checkValidTest(value: mixed): boolean {
} }
export function assertConfigFileSearch( export function assertConfigFileSearch(
key: string, loc: OptionPath,
value: mixed, value: mixed,
): ConfigFileSearch | void { ): ConfigFileSearch | void {
if ( if (
@ -176,7 +226,7 @@ export function assertConfigFileSearch(
typeof value !== "string" typeof value !== "string"
) { ) {
throw new Error( 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)}`, `got ${JSON.stringify(value)}`,
); );
} }
@ -185,7 +235,7 @@ export function assertConfigFileSearch(
} }
export function assertBabelrcSearch( export function assertBabelrcSearch(
key: string, loc: OptionPath,
value: mixed, value: mixed,
): BabelrcSearch | void { ): BabelrcSearch | void {
if (value === undefined || typeof value === "boolean") return value; if (value === undefined || typeof value === "boolean") return value;
@ -193,44 +243,43 @@ export function assertBabelrcSearch(
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((item, i) => { value.forEach((item, i) => {
if (!checkValidTest(item)) { 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)) { } else if (!checkValidTest(value)) {
throw new Error( 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)}`, `or an array of those, got ${JSON.stringify(value)}`,
); );
} }
return (value: any); return (value: any);
} }
export function assertPluginList(key: string, value: mixed): PluginList | void { export function assertPluginList(
const arr = assertArray(key, value); loc: OptionPath,
value: mixed,
): PluginList | void {
const arr = assertArray(loc, value);
if (arr) { if (arr) {
// Loop instead of using `.map` in order to preserve object identity // Loop instead of using `.map` in order to preserve object identity
// for plugin array for use during config chain processing. // 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); return (arr: any);
} }
function assertPluginItem( function assertPluginItem(loc: GeneralPath, value: mixed): PluginItem {
key: string,
index: number,
value: mixed,
): PluginItem {
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length === 0) { 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) { if (value.length > 3) {
throw new Error( throw new Error(`${msg(loc)} may only be a two-tuple or three-tuple`);
`.${key}[${index}] 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) { if (value.length > 1) {
const opts = value[1]; const opts = value[1];
@ -240,38 +289,31 @@ function assertPluginItem(
(typeof opts !== "object" || Array.isArray(opts)) (typeof opts !== "object" || Array.isArray(opts))
) { ) {
throw new Error( 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) { if (value.length === 3) {
const name = value[2]; const name = value[2];
if (name !== undefined && typeof name !== "string") { 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 { } else {
assertPluginTarget(key, index, false, value); assertPluginTarget(loc, value);
} }
return (value: any); return (value: any);
} }
function assertPluginTarget( function assertPluginTarget(loc: GeneralPath, value: mixed): PluginTarget {
key: string,
index: number,
inArray: boolean,
value: mixed,
): PluginTarget {
if ( if (
(typeof value !== "object" || !value) && (typeof value !== "object" || !value) &&
typeof value !== "string" && typeof value !== "string" &&
typeof value !== "function" typeof value !== "function"
) { ) {
throw new Error( throw new Error(`${msg(loc)} must be a string, object, function`);
`.${key}[${index}]${
inArray ? `[0]` : ""
} must be a string, object, function`,
);
} }
return value; return value;
} }

View File

@ -5,6 +5,8 @@ import Plugin from "../plugin";
import removed from "./removed"; import removed from "./removed";
import { import {
msg,
access,
assertString, assertString,
assertBoolean, assertBoolean,
assertObject, assertObject,
@ -21,6 +23,7 @@ import {
assertSourceType, assertSourceType,
type ValidatorSet, type ValidatorSet,
type Validator, type Validator,
type OptionPath,
} from "./option-assertions"; } from "./option-assertions";
const ROOT_VALIDATORS: ValidatorSet = { const ROOT_VALIDATORS: ValidatorSet = {
@ -248,24 +251,62 @@ export type SourceTypeOption = "module" | "script" | "unambiguous";
export type CompactOption = boolean | "auto"; export type CompactOption = boolean | "auto";
export type RootInputSourceMapOption = {} | boolean; export type RootInputSourceMapOption = {} | boolean;
export type OptionsType = export type OptionsSource =
| "arguments" | "arguments"
| "env"
| "preset"
| "override"
| "configfile" | "configfile"
| "babelrcfile" | "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); assertNoDuplicateSourcemap(opts);
Object.keys(opts).forEach(key => { Object.keys(opts).forEach(key => {
const optLoc = {
type: "option",
name: key,
parent: loc,
};
if (type === "preset" && NONPRESET_VALIDATORS[key]) { 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]) { 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 ( if (
type !== "arguments" && type !== "arguments" &&
@ -274,48 +315,47 @@ export function validate(type: OptionsType, opts: {}): ValidatedOptions {
) { ) {
if (type === "babelrcfile" || type === "extendsfile") { if (type === "babelrcfile" || type === "extendsfile") {
throw new Error( 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`, `or babel.config.js/config file options`,
); );
} }
throw new Error( 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 = const validator =
COMMON_VALIDATORS[key] || COMMON_VALIDATORS[key] ||
NONPRESET_VALIDATORS[key] || NONPRESET_VALIDATORS[key] ||
BABELRC_VALIDATORS[key] || BABELRC_VALIDATORS[key] ||
ROOT_VALIDATORS[key]; ROOT_VALIDATORS[key] ||
throwUnknownError;
if (validator) validator(key, opts[key]); validator(optLoc, opts[key]);
else throw buildUnknownError(key);
}); });
return (opts: any); return (opts: any);
} }
function buildUnknownError(key: string) { function throwUnknownError(loc: OptionPath) {
const key = loc.name;
if (removed[key]) { if (removed[key]) {
const { message, version = 5 } = removed[key]; const { message, version = 5 } = removed[key];
throw new ReferenceError( throw new ReferenceError(
`Using removed Babel ${version} option: .${key} - ${message}`, `Using removed Babel ${version} option: ${msg(loc)} - ${message}`,
); );
} else { } else {
// eslint-disable-next-line max-len // 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); throw new ReferenceError(unknownOptErr);
} }
@ -331,27 +371,53 @@ function assertNoDuplicateSourcemap(opts: {}): void {
} }
} }
function assertEnvSet(key: string, value: mixed): EnvSet<ValidatedOptions> { function assertEnvSet(loc: OptionPath, value: mixed): EnvSet<ValidatedOptions> {
const obj = assertObject(key, value); 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) { if (obj) {
// Validate but don't copy the .env object in order to preserve // Validate but don't copy the .env object in order to preserve
// object identity for use during config chain processing. // object identity for use during config chain processing.
for (const key of Object.keys(obj)) { for (const envName of Object.keys(obj)) {
const env = assertObject(key, obj[key]); const env = assertObject(access(loc, envName), obj[envName]);
if (env) validate("env", env); if (!env) continue;
const envLoc = {
type: "env",
name: envName,
parent,
};
validateNested(envLoc, env);
} }
} }
return (obj: any); return (obj: any);
} }
function assertOverridesList(key: string, value: mixed): OverridesList { function assertOverridesList(loc: OptionPath, value: mixed): OverridesList {
const arr = assertArray(key, value); 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) { if (arr) {
for (const [index, item] of arr.entries()) { for (const [index, item] of arr.entries()) {
const env = assertObject(`${index}`, item); const objLoc = access(loc, index);
if (!env) throw new Error(`.${key}[${index}] must be an object`); 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); return (arr: any);