Validate @babel/preset-env options (#8031)
This commit is contained in:
parent
110779e9f3
commit
3de053cc6c
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
44
packages/babel-preset-env/src/options.js
Normal file
44
packages/babel-preset-env/src/options.js
Normal 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",
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user