babel/packages/babel-preset-env/src/targets-parser.js
2019-06-30 11:00:43 +02:00

266 lines
7.6 KiB
JavaScript

// @flow
import browserslist from "browserslist";
import invariant from "invariant";
import semver from "semver";
import {
semverify,
isUnreleasedVersion,
getLowestUnreleased,
findSuggestion,
} from "./utils";
import browserModulesData from "../data/built-in-modules.json";
import { TargetNames } from "./options";
import type { Targets } from "./types";
const browserslistDefaults = browserslist.defaults;
const validBrowserslistTargets = [
...Object.keys(browserslist.data),
...Object.keys(browserslist.aliases),
];
const objectToBrowserslist = (object: Targets): Array<string> => {
return Object.keys(object).reduce((list, targetName) => {
if (validBrowserslistTargets.indexOf(targetName) >= 0) {
const targetVersion = object[targetName];
return list.concat(`${targetName} ${targetVersion}`);
}
return list;
}, []);
};
const validateTargetNames = (targets: Targets): void => {
const validTargets = Object.keys(TargetNames);
for (const target in targets) {
if (!TargetNames[target]) {
throw new Error(
`Invalid Option: '${target}' is not a valid target
Maybe you meant to use '${findSuggestion(validTargets, target)}'?`,
);
}
}
};
const browserNameMap = {
and_chr: "chrome",
and_ff: "firefox",
android: "android",
chrome: "chrome",
edge: "edge",
firefox: "firefox",
ie: "ie",
ie_mob: "ie",
ios_saf: "ios",
node: "node",
op_mob: "opera",
opera: "opera",
safari: "safari",
samsung: "samsung",
};
export const isBrowsersQueryValid = (
browsers: string | Array<string> | Targets,
): 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 !== TargetNames.browsers) {
queryObj[targKey] = fromTarget[targKey];
}
return queryObj;
}, fromQuery);
};
const getLowestVersions = (browsers: Array<string>): Targets => {
return browsers.reduce((all: Object, browser: string): Object => {
const [browserName, browserVersion] = browser.split(" ");
const normalizedBrowserName = browserNameMap[browserName];
if (!normalizedBrowserName) {
return all;
}
try {
// Browser version can return as "10.0-10.2"
const splitVersion = browserVersion.split("-")[0].toLowerCase();
const isSplitUnreleased = isUnreleasedVersion(splitVersion, browserName);
if (!all[normalizedBrowserName]) {
all[normalizedBrowserName] = isSplitUnreleased
? splitVersion
: semverify(splitVersion);
return all;
}
const version = all[normalizedBrowserName];
const isUnreleased = isUnreleasedVersion(version, browserName);
if (isUnreleased && isSplitUnreleased) {
all[normalizedBrowserName] = getLowestUnreleased(
version,
splitVersion,
browserName,
);
} else if (isUnreleased) {
all[normalizedBrowserName] = semverify(splitVersion);
} else if (!isUnreleased && !isSplitUnreleased) {
const parsedBrowserVersion = semverify(splitVersion);
all[normalizedBrowserName] = semverMin(version, parsedBrowserVersion);
}
} catch (e) {}
return all;
}, {});
};
const outputDecimalWarning = (decimalTargets: Array<Object>): void => {
if (!decimalTargets || !decimalTargets.length) {
return;
}
console.log("Warning, the following targets are using a decimal version:");
console.log("");
decimalTargets.forEach(({ target, value }) =>
console.log(` ${target}: ${value}`),
);
console.log("");
console.log(
"We recommend using a string for minor/patch versions to avoid numbers like 6.10",
);
console.log("getting parsed as 6.1, which can lead to unexpected behavior.");
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()
: semverifyTarget(target, value);
return [target, version];
},
// Parse `node: true` and `node: "current"` to version
node: (target, value) => {
const parsed =
value === true || value === "current"
? process.versions.node
: semverifyTarget(target, value);
return [target, parsed];
},
};
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) {
const supportsESModules = browserModulesData["es6.module"];
targets.browsers = Object.keys(supportsESModules)
.map(browser => `${browser} ${supportsESModules[browser]}`)
.join(", ");
}
// Parse browsers target via browserslist
const browsersquery = validateBrowsers(targets.browsers);
const hasTargets = Object.keys(targets).length > 0;
const shouldParseBrowsers = !!targets.browsers;
const shouldSearchForConfig =
!options.ignoreBrowserslistConfig && !hasTargets;
if (shouldParseBrowsers || shouldSearchForConfig) {
// If no targets are passed, we need to overwrite browserslist's defaults
// so that we enable all transforms (acting like the now deprecated
// preset-latest).
//
// Note, if browserslist resolves the config (ex. package.json), then usage
// of `defaults` in queries will be different since we don't want to break
// the behavior of "no targets is the same as preset-latest".
if (!hasTargets) {
browserslist.defaults = objectToBrowserslist(targets);
}
const browsers = browserslist(browsersquery, {
path: options.configPath,
mobileToDesktop: true,
});
const queryBrowsers = getLowestVersions(browsers);
targets = mergeBrowsers(queryBrowsers, targets);
// Reset browserslist defaults
browserslist.defaults = browserslistDefaults;
}
// Parse remaining targets
const parsed = Object.keys(targets)
.filter(value => value !== TargetNames.esmodules)
.sort()
.reduce(
(results: ParsedResult, target: string): ParsedResult => {
if (target !== TargetNames.browsers) {
const value = targets[target];
// Warn when specifying minor/patch as a decimal
if (typeof value === "number" && value % 1 !== 0) {
results.decimalWarnings.push({ target, value });
}
// Check if we have a target parser?
const parser = targetParserMap[target] || targetParserMap.__default;
const [parsedTarget, parsedValue] = parser(target, value);
if (parsedValue) {
// Merge (lowest wins)
results.targets[parsedTarget] = parsedValue;
}
}
return results;
},
{
targets: targetOpts,
decimalWarnings: [],
},
);
outputDecimalWarning(parsed.decimalWarnings);
return parsed.targets;
};
export default getTargets;