Validate @babel/preset-env options (#8031)

This commit is contained in:
Serhii Nanovskyi 2018-06-10 02:38:14 +03:00 committed by Brian Ng
parent 110779e9f3
commit 3de053cc6c
10 changed files with 226 additions and 46 deletions

View File

@ -166,3 +166,9 @@ declare module "convert-source-map" {
generateMapFileComment(path: string, options?: ?{ multiline: boolean }): string,
};
}
declare module "js-levenshtein" {
declare module.exports: {
(string, string): number,
};
}

View File

@ -49,6 +49,7 @@
"@babel/plugin-transform-unicode-regex": "7.0.0-beta.49",
"browserslist": "^3.0.0",
"invariant": "^2.2.2",
"js-levenshtein": "^1.1.3",
"semver": "^5.3.0"
},
"peerDependencies": {

View File

@ -5,9 +5,23 @@ import browserslist from "browserslist";
import builtInsList from "../data/built-ins.json";
import { defaultWebIncludes } from "./default-includes";
import moduleTransformations from "./module-transformations";
import { getValues, findSuggestion } from "./utils";
import pluginsList from "../data/plugins.json";
import { TopLevelOptions, ModulesOption, UseBuiltInsOption } from "./options";
import type { Targets, Options, ModuleOption, BuiltInsOption } from "./types";
const validateTopLevelOptions = (options: Options) => {
for (const option in options) {
if (!TopLevelOptions[option]) {
const validOptions = getValues(TopLevelOptions);
throw new Error(
`Invalid Option: ${option} is not a valid top-level option.
Maybe you meant to use '${findSuggestion(validOptions, option)}'?`,
);
}
}
};
const validIncludesAndExcludes = new Set([
...Object.keys(pluginsList),
...Object.keys(moduleTransformations).map(m => moduleTransformations[m]),
@ -108,17 +122,17 @@ export const validateIgnoreBrowserslistConfig = (
ignoreBrowserslistConfig: boolean,
) =>
validateBoolOption(
"ignoreBrowserslistConfig",
TopLevelOptions.ignoreBrowserslistConfig,
ignoreBrowserslistConfig,
false,
);
export const validateModulesOption = (
modulesOpt: ModuleOption = "commonjs",
modulesOpt: ModuleOption = ModulesOption.commonjs,
) => {
invariant(
modulesOpt === false ||
Object.keys(moduleTransformations).indexOf(modulesOpt) > -1,
ModulesOption[modulesOpt] ||
ModulesOption[modulesOpt] === ModulesOption.false,
`Invalid Option: The 'modules' option must be either 'false' to indicate no modules, or a
module type which can be be one of: 'commonjs' (default), 'amd', 'umd', 'systemjs'.`,
);
@ -140,7 +154,8 @@ export const validateUseBuiltInsOption = (
builtInsOpt: BuiltInsOption = false,
): BuiltInsOption => {
invariant(
builtInsOpt === "usage" || builtInsOpt === false || builtInsOpt === "entry",
UseBuiltInsOption[builtInsOpt] ||
UseBuiltInsOption[builtInsOpt] === UseBuiltInsOption.false,
`Invalid Option: The 'useBuiltIns' option must be either
'false' (default) to indicate no polyfill,
'"entry"' to indicate replacing the entry polyfill, or
@ -151,32 +166,40 @@ export const validateUseBuiltInsOption = (
};
export default function normalizeOptions(opts: Options) {
const include = expandIncludesAndExcludes(opts.include, "include");
const exclude = expandIncludesAndExcludes(opts.exclude, "exclude");
validateTopLevelOptions(opts);
const include = expandIncludesAndExcludes(
opts.include,
TopLevelOptions.include,
);
const exclude = expandIncludesAndExcludes(
opts.exclude,
TopLevelOptions.exclude,
);
checkDuplicateIncludeExcludes(include, exclude);
return {
configPath: validateConfigPathOption(opts.configPath),
debug: opts.debug,
debug: validateBoolOption(TopLevelOptions.debug, opts.debug, false),
include,
exclude,
forceAllTransforms: validateBoolOption(
"forceAllTransforms",
TopLevelOptions.forceAllTransforms,
opts.forceAllTransforms,
false,
),
ignoreBrowserslistConfig: validateIgnoreBrowserslistConfig(
opts.ignoreBrowserslistConfig,
),
loose: validateBoolOption("loose", opts.loose, false),
loose: validateBoolOption(TopLevelOptions.loose, opts.loose, false),
modules: validateModulesOption(opts.modules),
shippedProposals: validateBoolOption(
"shippedProposals",
TopLevelOptions.shippedProposals,
opts.shippedProposals,
false,
),
spec: validateBoolOption("loose", opts.spec, false),
spec: validateBoolOption(TopLevelOptions.spec, opts.spec, false),
targets: {
...opts.targets,
},

View File

@ -0,0 +1,44 @@
export const TopLevelOptions = {
configPath: "configPath",
debug: "debug",
exclude: "exclude",
forceAllTransforms: "forceAllTransforms",
ignoreBrowserslistConfig: "ignoreBrowserslistConfig",
include: "include",
loose: "loose",
modules: "modules",
shippedProposals: "shippedProposals",
spec: "spec",
targets: "targets",
useBuiltIns: "useBuiltIns",
};
export const ModulesOption = {
false: false,
amd: "amd",
commonjs: "commonjs",
cjs: "cjs",
systemjs: "systemjs",
umd: "umd",
};
export const UseBuiltInsOption = {
false: false,
entry: "entry",
usage: "usage",
};
export const TargetNames = {
esmodules: "esmodules",
node: "node",
browsers: "browsers",
chrome: "chrome",
opera: "opera",
edge: "edge",
firefox: "firefox",
safari: "safari",
ie: "ie",
ios: "ios",
android: "android",
electron: "electron",
};

View File

@ -1,12 +1,32 @@
// @flow
import browserslist from "browserslist";
import invariant from "invariant";
import semver from "semver";
import { semverify, isUnreleasedVersion, getLowestUnreleased } from "./utils";
import {
semverify,
isUnreleasedVersion,
getLowestUnreleased,
getValues,
findSuggestion,
} from "./utils";
import { objectToBrowserslist } from "./normalize-options";
import browserModulesData from "../data/built-in-modules.json";
import { TargetNames } from "./options";
import type { Targets } from "./types";
const validateTargetNames = (validTargets, targets) => {
for (const target in targets) {
if (!TargetNames[target]) {
const validOptions = getValues(TargetNames);
throw new Error(
`Invalid Option: '${target}' is not a valid target
Maybe you meant to use '${findSuggestion(validOptions, target)}'?`,
);
}
}
};
const browserNameMap = {
android: "android",
chrome: "chrome",
@ -21,13 +41,21 @@ const browserNameMap = {
const isBrowsersQueryValid = (browsers: string | Array<string>): boolean =>
typeof browsers === "string" || Array.isArray(browsers);
const validateBrowsers = browsers => {
invariant(
typeof browsers === "undefined" || isBrowsersQueryValid(browsers),
`Invalid Option: '${browsers}' is not a valid browserslist query`,
);
return browsers;
};
export const semverMin = (first: ?string, second: string): string => {
return first && semver.lt(first, second) ? first : second;
};
const mergeBrowsers = (fromQuery: Targets, fromTarget: Targets) => {
return Object.keys(fromTarget).reduce((queryObj, targKey) => {
if (targKey !== "browsers") {
if (targKey !== TargetNames.browsers) {
queryObj[targKey] = fromTarget[targKey];
}
return queryObj;
@ -85,11 +113,21 @@ const outputDecimalWarning = (decimalTargets: Array<Object>): void => {
console.log("");
};
const semverifyTarget = (target, value) => {
try {
return semverify(value);
} catch (error) {
throw new Error(
`Invalid Option: '${value}' is not a valid value for 'targets.${target}'.`,
);
}
};
const targetParserMap = {
__default: (target, value) => {
const version = isUnreleasedVersion(value, target)
? value.toLowerCase()
: semverify(value);
: semverifyTarget(target, value);
return [target, version];
},
@ -98,8 +136,7 @@ const targetParserMap = {
const parsed =
value === true || value === "current"
? process.versions.node
: semverify(value);
: semverifyTarget(target, value);
return [target, parsed];
},
};
@ -108,9 +145,12 @@ type ParsedResult = {
targets: Targets,
decimalWarnings: Array<Object>,
};
const getTargets = (targets: Object = {}, options: Object = {}): Targets => {
const targetOpts: Targets = {};
validateTargetNames(targets);
// `esmodules` as a target indicates the specific set of browsers supporting ES Modules.
// These values OVERRIDE the `browsers` field.
if (targets.esmodules) {
@ -119,23 +159,24 @@ const getTargets = (targets: Object = {}, options: Object = {}): Targets => {
.map(browser => `${browser} ${supportsESModules[browser]}`)
.join(", ");
}
// Parse browsers target via browserslist;
const queryIsValid = isBrowsersQueryValid(targets.browsers);
const browsersquery = queryIsValid ? targets.browsers : null;
if (queryIsValid || !options.ignoreBrowserslistConfig) {
// Parse browsers target via browserslist
const browsersquery = validateBrowsers(targets.browsers);
if (!options.ignoreBrowserslistConfig) {
browserslist.defaults = objectToBrowserslist(targets);
const browsers = browserslist(browsersquery, { path: options.configPath });
const queryBrowsers = getLowestVersions(browsers);
targets = mergeBrowsers(queryBrowsers, targets);
}
// Parse remaining targets
const parsed = Object.keys(targets)
.filter(value => value !== "esmodules")
.filter(value => value !== TargetNames.esmodules)
.sort()
.reduce(
(results: ParsedResult, target: string): ParsedResult => {
if (target !== "browsers") {
if (target !== TargetNames.browsers) {
const value = targets[target];
// Warn when specifying minor/patch as a decimal

View File

@ -1,15 +1,17 @@
//@flow
import { TargetNames, ModulesOption, UseBuiltInsOption } from "./options";
// Targets
export type Target = string;
export type Target = $Keys<typeof TargetNames>;
export type Targets = {
[target: string]: Target,
[target: Target]: string,
};
// Options
// Use explicit modules to prevent typo errors.
export type ModuleOption = false | "amd" | "commonjs" | "systemjs" | "umd";
export type BuiltInsOption = false | "entry" | "usage";
export type ModuleOption = $Values<typeof ModulesOption>;
export type BuiltInsOption = $Values<typeof UseBuiltInsOption>;
export type Options = {
configPath: string,

View File

@ -1,26 +1,51 @@
// @flow
import invariant from "invariant";
import semver from "semver";
import levenshtein from "js-levenshtein";
import { addSideEffect } from "@babel/helper-module-imports";
import unreleasedLabels from "../data/unreleased-labels";
import { semverMin } from "./targets-parser";
import type { Targets } from "./types";
const versionRegExp = /^(\d+|\d+.\d+)$/;
// Convert version to a semver value.
// 2.5 -> 2.5.0; 1 -> 1.0.0;
export const semverify = (version: string | number): string => {
if (typeof version === "string" && semver.valid(version)) {
const isString = typeof version === "string";
if (isString && semver.valid(version)) {
return version;
}
const split = version.toString().split(".");
invariant(
typeof version === "number" || (isString && versionRegExp.test(version)),
`'${version}' is not a valid version`,
);
const split = version.toString().split(".");
while (split.length < 3) {
split.push("0");
}
return split.join(".");
};
export const getValues = (object: Object): Array<any> =>
Object.keys(object).map(key => object[key]);
export const findSuggestion = (options: Array<string>, option: string) => {
let levenshteinValue = Infinity;
return options.reduce((suggestion, validOption) => {
const value = levenshtein(validOption, option);
if (value < levenshteinValue) {
levenshteinValue = value;
return validOption;
}
return suggestion;
}, undefined);
};
export const prettifyVersion = (version: string): string => {
if (typeof version !== "string") {
return version;

View File

@ -36,6 +36,15 @@ describe("normalize-options", () => {
});
});
describe("Config format validation", () => {
it("should throw if top-level option not found", () => {
const unknownTopLevelOption = () => {
normalizeOptions({ unknown: "option" });
};
expect(unknownTopLevelOption).toThrow();
});
});
describe("RegExp include/exclude", () => {
it("should not allow invalid plugins in `include` and `exclude`", () => {
const normalizeWithNonExistingPlugin = () => {

View File

@ -19,6 +19,35 @@ describe("getTargets", () => {
});
});
describe("validation", () => {
it("throws on invalid target name", () => {
const invalidTargetName = () => {
getTargets({
unknown: "unknown",
});
};
expect(invalidTargetName).toThrow();
});
it("throws on invalid browsers target", () => {
const invalidBrowsersTarget = () => {
getTargets({
browsers: 59,
});
};
expect(invalidBrowsersTarget).toThrow();
});
it("throws on invalid target version", () => {
const invalidTargetVersion = () => {
getTargets({
chrome: "unknown",
});
};
expect(invalidTargetVersion).toThrow();
});
});
describe("browser", () => {
it("merges browser key targets", () => {
expect(
@ -55,21 +84,6 @@ describe("getTargets", () => {
safari: "tp",
});
});
it("ignores invalid", () => {
expect(
getTargets({
browsers: 59,
chrome: "49",
firefox: "55",
ie: "11",
}),
).toEqual({
chrome: "49.0.0",
firefox: "55.0.0",
ie: "11.0.0",
});
});
});
describe("esmodules", () => {

View File

@ -2,7 +2,7 @@
const utils = require("../lib/utils");
const { prettifyTargets, prettifyVersion, semverify } = utils;
const { prettifyTargets, prettifyVersion, semverify, findSuggestion } = utils;
describe("utils", () => {
describe("semverify", () => {
@ -13,6 +13,13 @@ describe("utils", () => {
expect(semverify(1)).toBe("1.0.0");
expect(semverify(1.2)).toBe("1.2.0");
});
it("throws", () => {
const invalidSemver = () => {
semverify("invalid");
};
expect(invalidSemver).toThrow();
});
});
describe("prettifyVersion", () => {
@ -43,4 +50,12 @@ describe("utils", () => {
});
});
});
describe("findSuggestion", () => {
it("returns", () => {
const options = ["one", "two", "three"];
expect(findSuggestion(options, "onr")).toEqual("one");
expect(findSuggestion(options, "tree")).toEqual("three");
});
});
});