Skip to content

Commit

Permalink
refactor: add @babel/helper-validator-option (#12006)
Browse files Browse the repository at this point in the history
* refactor: add @babel/helper-validator-option

* refactor: simplify validateTopLevelOptions

* perf: the recursive version is not practically fast

* Update packages/babel-helper-validator-option/README.md

Co-authored-by: Brian Ng <bng412@gmail.com>

* Update packages/babel-helper-validator-option/src/validator.js

* fix: incorrect type annotation

* refactor: use babel/helper-option-validator in babel/compat-data

* chore: fix flow types error

* Address review comments

* address review comments

Co-authored-by: Brian Ng <bng412@gmail.com>
  • Loading branch information
JLHwung and existentialism committed Sep 24, 2020
1 parent 0d32e3f commit f2da186
Show file tree
Hide file tree
Showing 19 changed files with 375 additions and 197 deletions.
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(
(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";

0 comments on commit f2da186

Please sign in to comment.