Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add @babel/helper-validator-option #12006

Merged
merged 10 commits into from Sep 24, 2020
5 changes: 4 additions & 1 deletion babel.config.js
Expand Up @@ -106,7 +106,10 @@ module.exports = function (api) {
plugins: [
// TODO: Use @babel/preset-flow when
// https://github.com/babel/babel/issues/7233 is fixed
"@babel/plugin-transform-flow-strip-types",
[
"@babel/plugin-transform-flow-strip-types",
{ allowDeclareFields: true },
],
[
"@babel/proposal-object-rest-spread",
{ useBuiltIns: true, loose: true },
Expand Down
3 changes: 1 addition & 2 deletions packages/babel-helper-compilation-targets/package.json
Expand Up @@ -22,9 +22,8 @@
],
"dependencies": {
"@babel/compat-data": "workspace:^7.10.4",
"@babel/helper-validator-option": "workspace:^7.11.4",
"browserslist": "^4.12.0",
"invariant": "^2.2.4",
"levenary": "^1.1.1",
"semver": "^5.5.0"
},
"peerDependencies": {
Expand Down
102 changes: 49 additions & 53 deletions packages/babel-helper-compilation-targets/src/index.js
@@ -1,8 +1,7 @@
// @flow

import browserslist from "browserslist";
import findSuggestion from "levenary";
import invariant from "invariant";
import { findSuggestion } from "@babel/helper-validator-option";
import browserModulesData from "@babel/compat-data/native-modules";

import {
Expand All @@ -11,9 +10,11 @@ import {
isUnreleasedVersion,
getLowestUnreleased,
} from "./utils";
import { OptionValidator } from "@babel/helper-validator-option";
import { browserNameMap } from "./targets";
import { TargetNames } from "./options";
import type { Target, Targets, InputTargets, Browsers } from "./types";
import { name as packageName } from "../package.json";
import type { Targets, InputTargets, Browsers, TargetsTuple } from "./types";

export type { Targets, InputTargets };

Expand All @@ -22,6 +23,7 @@ export { getInclusionReasons } from "./debug";
export { default as filterItems, isRequired } from "./filter-items";
export { unreleasedLabels } from "./targets";

const v = new OptionValidator(packageName);
const browserslistDefaults = browserslist.defaults;

const validBrowserslistTargets = [
Expand All @@ -39,29 +41,28 @@ function objectToBrowserslist(object: Targets): Array<string> {
}, []);
}

function validateTargetNames(targets: InputTargets): Targets {
function validateTargetNames(targets: Targets): TargetsTuple {
const validTargets = Object.keys(TargetNames);
for (const target in targets) {
if (!TargetNames[target]) {
for (const target of Object.keys(targets)) {
if (!(target in TargetNames)) {
throw new Error(
`Invalid Option: '${target}' is not a valid target
Maybe you meant to use '${findSuggestion(target, validTargets)}'?`,
v.formatMessage(`'${target}' is not a valid target
- Did you mean '${findSuggestion(target, validTargets)}'?`),
);
}
}

// $FlowIgnore
return targets;
return (targets: any);
}

export function isBrowsersQueryValid(browsers: Browsers | Targets): boolean {
return typeof browsers === "string" || Array.isArray(browsers);
}

function validateBrowsers(browsers: Browsers | void) {
invariant(
typeof browsers === "undefined" || isBrowsersQueryValid(browsers),
`Invalid Option: '${String(browsers)}' is not a valid browserslist query`,
v.invariant(
browsers === undefined || isBrowsersQueryValid(browsers),
`'${String(browsers)}' is not a valid browserslist query`,
);

return browsers;
Expand Down Expand Up @@ -110,8 +111,10 @@ function getLowestVersions(browsers: Array<string>): Targets {
}, {});
}

function outputDecimalWarning(decimalTargets: Array<Object>): void {
if (!decimalTargets?.length) {
function outputDecimalWarning(
decimalTargets: Array<{| target: string, value: string |}>,
): void {
if (!decimalTargets.length) {
return;
}

Expand All @@ -133,7 +136,9 @@ function semverifyTarget(target, value) {
return semverify(value);
} catch (error) {
throw new Error(
`Invalid Option: '${value}' is not a valid value for 'targets.${target}'.`,
v.formatMessage(
`'${value}' is not a valid value for 'targets.${target}'.`,
),
);
}
}
Expand All @@ -156,16 +161,17 @@ const targetParserMap = {
},
};

type ParsedResult = {
targets: Targets,
decimalWarnings: Array<Object>,
};
function generateTargets(inputTargets: InputTargets): Targets {
const input = { ...inputTargets };
delete input.esmodules;
delete input.browsers;
return ((input: any): Targets);
}

export default function getTargets(
inputTargets: InputTargets = {},
options: Object = {},
): Targets {
const targetOpts: Targets = {};
let { browsers } = inputTargets;

// `esmodules` as a target indicates the specific set of browsers supporting ES Modules.
Expand All @@ -180,12 +186,8 @@ export default function getTargets(
// Parse browsers target via browserslist
const browsersquery = validateBrowsers(browsers);

// Remove esmodules after being consumed to fix `hasTargets` below
const input = { ...inputTargets };
delete input.esmodules;
delete input.browsers;

let targets: Targets = validateTargetNames(input);
const input = generateTargets(inputTargets);
let targets: TargetsTuple = validateTargetNames(input);

const shouldParseBrowsers = !!browsersquery;
const hasTargets = shouldParseBrowsers || Object.keys(targets).length > 0;
Expand Down Expand Up @@ -218,34 +220,28 @@ export default function getTargets(
}

// Parse remaining targets
const parsed = (Object.keys(targets): Array<Target>).sort().reduce(
existentialism marked this conversation as resolved.
Show resolved Hide resolved
(results: ParsedResult, target: $Keys<Targets>): ParsedResult => {
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?
// $FlowIgnore - Flow doesn't like that some targetParserMap[target] might be missing
const parser = targetParserMap[target] ?? targetParserMap.__default;
const [parsedTarget, parsedValue] = parser(target, value);
const result: Targets = {};
const decimalWarnings = [];
for (const target of Object.keys(targets).sort()) {
const value = targets[target];

// Warn when specifying minor/patch as a decimal
if (typeof value === "number" && value % 1 !== 0) {
decimalWarnings.push({ target, value });
}

if (parsedValue) {
// Merge (lowest wins)
results.targets[parsedTarget] = parsedValue;
}
// Check if we have a target parser?
// $FlowIgnore - Flow doesn't like that some targetParserMap[target] might be missing
const parser = targetParserMap[target] ?? targetParserMap.__default;
const [parsedTarget, parsedValue] = parser(target, value);

return results;
},
{
targets: targetOpts,
decimalWarnings: [],
},
);
if (parsedValue) {
// Merge (lowest wins)
result[parsedTarget] = parsedValue;
}
}

outputDecimalWarning(parsed.decimalWarnings);
outputDecimalWarning(decimalWarnings);

return parsed.targets;
return result;
}
4 changes: 4 additions & 0 deletions packages/babel-helper-compilation-targets/src/types.js
Expand Up @@ -18,6 +18,10 @@ export type Targets = {
[target: Target]: string,
};

export type TargetsTuple = {|
[target: Target]: string,
|};

export type Browsers = string | Array<string>;

export type InputTargets = {
Expand Down
9 changes: 5 additions & 4 deletions packages/babel-helper-compilation-targets/src/utils.js
@@ -1,13 +1,14 @@
// @flow

import invariant from "invariant";
import semver from "semver";

import { OptionValidator } from "@babel/helper-validator-option";
import { name as packageName } from "../package.json";
import { unreleasedLabels } from "./targets";
import type { Target, Targets } from "./types";

const versionRegExp = /^(\d+|\d+.\d+)$/;

const v = new OptionValidator(packageName);

export function semverMin(first: ?string, second: string): string {
return first && semver.lt(first, second) ? first : second;
}
Expand All @@ -19,7 +20,7 @@ export function semverify(version: number | string): string {
return version;
}

invariant(
v.invariant(
typeof version === "number" ||
(typeof version === "string" && versionRegExp.test(version)),
`'${version}' is not a valid version`,
Expand Down
Expand Up @@ -57,3 +57,5 @@ Object {
"samsung": "8.2.0",
}
`;

exports[`getTargets exception throws when version is not a semver 1`] = `"@babel/helper-compilation-targets: 'seventy-two' is not a valid value for 'targets.chrome'."`;
Expand Up @@ -260,4 +260,12 @@ describe("getTargets", () => {
});
});
});

describe("exception", () => {
it("throws when version is not a semver", () => {
expect(() =>
getTargets({ chrome: "seventy-two" }),
).toThrowErrorMatchingSnapshot();
});
});
});
3 changes: 3 additions & 0 deletions packages/babel-helper-validator-option/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions packages/babel-helper-validator-option/README.md
@@ -0,0 +1,19 @@
# @babel/helper-validator-option

> Validate plugin/preset options
See our website [@babel/helper-validator-option](https://babeljs.io/docs/en/next/babel-helper-validator-option.html) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/helper-validator-option
```

or using yarn:

```sh
yarn add @babel/helper-validator-option --dev
```
16 changes: 16 additions & 0 deletions packages/babel-helper-validator-option/package.json
@@ -0,0 +1,16 @@
{
"name": "@babel/helper-validator-option",
"version": "7.11.4",
"description": "Validate plugin/preset options",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-helper-validator-option"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"exports": "./lib/index.js"
}
50 changes: 50 additions & 0 deletions packages/babel-helper-validator-option/src/find-suggestion.js
@@ -0,0 +1,50 @@
// @flow

const { min } = Math;

// a minimal leven distance implementation
// balanced maintenability with code size
// It is not blazingly fast but should be okay for Babel user case
// where it will be run for at most tens of time on strings
// that have less than 20 ASCII characters

// https://rosettacode.org/wiki/Levenshtein_distance#ES5
function levenshtein(a, b) {
let t = [],
u = [],
i,
j;
const m = a.length,
n = b.length;
if (!m) {
return n;
}
if (!n) {
return m;
}
for (j = 0; j <= n; j++) {
t[j] = j;
}
for (i = 1; i <= m; i++) {
for (u = [i], j = 1; j <= n; j++) {
u[j] =
a[i - 1] === b[j - 1] ? t[j - 1] : min(t[j - 1], t[j], u[j - 1]) + 1;
}
t = u;
}
return u[n];
}

/**
* Given a string `str` and an array of candidates `arr`,
* return the first of elements in candidates that has minimal
* Levenshtein distance with `str`.
* @export
* @param {string} str
* @param {string[]} arr
* @returns {string}
*/
export function findSuggestion(str: string, arr: string[]): string {
const distances = arr.map<number>(el => levenshtein(el, str));
return arr[distances.indexOf(min(...distances))];
}
2 changes: 2 additions & 0 deletions packages/babel-helper-validator-option/src/index.js
@@ -0,0 +1,2 @@
export { OptionValidator } from "./validator";
export { findSuggestion } from "./find-suggestion";