From 8eee435cd6227587e20c9d1235ba4cca4b05e6fc Mon Sep 17 00:00:00 2001 From: Amin Marashi Date: Sun, 18 Mar 2018 22:54:43 +0800 Subject: [PATCH] Add RegExp support to include/exclude preset-env options (#7242) * Add support for RegExp includes/excludes * Keep the plugin order * Detect invalid modules in regexp * Add more tests for regexp * Cover builtins, and unnormalized in the RegExp tests * Remove babel-plugin- in all positions * Change babel-plugin- prefix to string * Add a test for the same module in include/exclude * Handle partial matches explicitly * Remove extra valid regexp check * Optimise validation of plugins * Optimise selecting the plugins * Fix undefined include/exclude option * Update documentation to reflect the new include matching * Fix typo * Apply reviews Use regexp.test instead of string.match (slower) Define flatten helper Do not normalize babel-plugin anywhere in the string --- packages/babel-preset-env/README.md | 14 ++- .../babel-preset-env/src/normalize-options.js | 58 +++++++----- .../test/normalize-options.spec.js | 88 ++++++++++++------- 3 files changed, 103 insertions(+), 57 deletions(-) diff --git a/packages/babel-preset-env/README.md b/packages/babel-preset-env/README.md index 3bb7ec1991..8f81aa4871 100644 --- a/packages/babel-preset-env/README.md +++ b/packages/babel-preset-env/README.md @@ -245,7 +245,7 @@ Outputs the targets/plugins used and the version specified in [plugin data versi ### `include` -`Array`, defaults to `[]`. +`Array`, defaults to `[]`. An array of plugins to always include. @@ -255,6 +255,16 @@ Valid options include any: - [Built-ins](https://github.com/babel/babel/blob/master/packages/babel-preset-env/data/built-in-features.js), such as `es6.map`, `es6.set`, or `es6.object.assign`. +Plugin names can be fully or partially specified (or using `RegExp`). + +Acceptable inputs: + +- Full name (`string`): `"es6.math.sign"` +- Partial name (`string`): `"es6.math.*"` (resolves to all plugins with `es6.math` prefix) +- `RegExp` Object: `/^transform-.*$/` or `new RegExp("^transform-modules-.*")` + +Note that the above `.` is the `RegExp` equivalent to match any character, and not the actual `'.'` character. Also note that to match any character `.*` is used in `RegExp` as opposed to `*` in `glob` format. + This option is useful if there is a bug in a native implementation, or a combination of a non-supported feature + a supported one doesn't work. For example, Node 4 supports native classes but not spread. If `super` is used with a spread argument, then the `@babel/plugin-transform-classes` transform needs to be `include`d, as it is not possible to transpile a spread with `super` otherwise. @@ -263,7 +273,7 @@ For example, Node 4 supports native classes but not spread. If `super` is used w ### `exclude` -`Array`, defaults to `[]`. +`Array`, defaults to `[]`. An array of plugins to always exclude/remove. diff --git a/packages/babel-preset-env/src/normalize-options.js b/packages/babel-preset-env/src/normalize-options.js index bbcdbd19f7..13b39a1662 100644 --- a/packages/babel-preset-env/src/normalize-options.js +++ b/packages/babel-preset-env/src/normalize-options.js @@ -15,26 +15,44 @@ const validIncludesAndExcludes = new Set([ ...defaultWebIncludes, ]); -export const validateIncludesAndExcludes = ( - opts: Array = [], - type: string, -): Array => { - invariant( - Array.isArray(opts), - `Invalid Option: The '${type}' option must be an Array of plugins/built-ins`, +const pluginToRegExp = (plugin: any): RegExp => { + if (plugin instanceof RegExp) return plugin; + try { + return new RegExp(`^${normalizePluginName(plugin)}$`); + } catch (e) { + return null; + } +}; + +const selectPlugins = (regexp: RegExp): Array => + Array.from(validIncludesAndExcludes).filter( + item => regexp instanceof RegExp && regexp.test(item), ); - const unknownOpts = opts.filter(opt => !validIncludesAndExcludes.has(opt)); +const flatten = array => [].concat(...array); + +const expandIncludesAndExcludes = ( + plugins: Array = [], + type: string, +): Array => { + if (plugins.length === 0) return plugins; + + const selectedPlugins = plugins.map(plugin => + selectPlugins(pluginToRegExp(plugin)), + ); + const invalidRegExpList = plugins.filter( + (p, i) => selectedPlugins[i].length === 0, + ); invariant( - unknownOpts.length === 0, - `Invalid Option: The plugins/built-ins '${unknownOpts.join( + invalidRegExpList.length === 0, + `Invalid Option: The plugins/built-ins '${invalidRegExpList.join( ", ", )}' passed to the '${type}' option are not valid. Please check data/[plugin-features|built-in-features].js in babel-preset-env`, ); - return opts; + return flatten(selectedPlugins); }; const validBrowserslistTargets = [ @@ -45,9 +63,6 @@ const validBrowserslistTargets = [ export const normalizePluginName = (plugin: string): string => plugin.replace(/^babel-plugin-/, ""); -export const normalizePluginNames = (plugins: Array): Array => - plugins.map(normalizePluginName); - export const checkDuplicateIncludeExcludes = ( include: Array = [], exclude: Array = [], @@ -138,20 +153,16 @@ export const validateUseBuiltInsOption = ( }; export default function normalizeOptions(opts: Options) { - if (opts.exclude) { - opts.exclude = normalizePluginNames(opts.exclude); - } + const include = expandIncludesAndExcludes(opts.include, "include"); + const exclude = expandIncludesAndExcludes(opts.exclude, "exclude"); - if (opts.include) { - opts.include = normalizePluginNames(opts.include); - } - - checkDuplicateIncludeExcludes(opts.include, opts.exclude); + checkDuplicateIncludeExcludes(include, exclude); return { configPath: validateConfigPathOption(opts.configPath), debug: opts.debug, - exclude: validateIncludesAndExcludes(opts.exclude, "exclude"), + include, + exclude, forceAllTransforms: validateBoolOption( "forceAllTransforms", opts.forceAllTransforms, @@ -160,7 +171,6 @@ export default function normalizeOptions(opts: Options) { ignoreBrowserslistConfig: validateIgnoreBrowserslistConfig( opts.ignoreBrowserslistConfig, ), - include: validateIncludesAndExcludes(opts.include, "include"), loose: validateBoolOption("loose", opts.loose, false), modules: validateModulesOption(opts.modules), shippedProposals: validateBoolOption( diff --git a/packages/babel-preset-env/test/normalize-options.spec.js b/packages/babel-preset-env/test/normalize-options.spec.js index 778ca28b9b..08c4d02175 100644 --- a/packages/babel-preset-env/test/normalize-options.spec.js +++ b/packages/babel-preset-env/test/normalize-options.spec.js @@ -5,12 +5,10 @@ const assert = require("assert"); const { checkDuplicateIncludeExcludes, - normalizePluginNames, validateBoolOption, - validateIncludesAndExcludes, validateModulesOption, + normalizePluginName, } = normalizeOptions; - describe("normalize-options", () => { describe("normalizeOptions", () => { it("should return normalized `include` and `exclude`", () => { @@ -23,6 +21,11 @@ describe("normalize-options", () => { ]); }); + it("should not normalize babel-plugin with prefix", () => { + const normalized = normalizePluginName("prefix-babel-plugin-postfix"); + assert.equal(normalized, "prefix-babel-plugin-postfix"); + }); + it("should throw if duplicate names in `include` and `exclude`", () => { const normalizeWithSameIncludes = () => { normalizeOptions.default({ @@ -34,6 +37,57 @@ describe("normalize-options", () => { }); }); + describe("RegExp include/exclude", () => { + it("should not allow invalid plugins in `include` and `exclude`", () => { + const normalizeWithNonExistingPlugin = () => { + normalizeOptions.default({ + include: ["non-existing-plugin"], + }); + }; + assert.throws(normalizeWithNonExistingPlugin, Error); + }); + + it("should expand regular expressions in `include` and `exclude`", () => { + const normalized = normalizeOptions.default({ + include: ["^[a-z]*-spread", "babel-plugin-transform-classes"], + }); + assert.deepEqual(normalized.include, [ + "transform-spread", + "transform-classes", + ]); + }); + + it("should expand regular expressions in `include` and `exclude`", () => { + const normalized = normalizeOptions.default({ + exclude: ["es6.math.log.*"], + }); + assert.deepEqual(normalized.exclude, [ + "es6.math.log1p", + "es6.math.log10", + "es6.math.log2", + ]); + }); + + it("should not allow the same modules in `include` and `exclude`", () => { + const normalizeWithNonExistingPlugin = () => { + normalizeOptions.default({ + include: ["es6.math.log2"], + exclude: ["es6.math.log.*"], + }); + }; + assert.throws(normalizeWithNonExistingPlugin, Error); + }); + + it("should not do partial match if not explicitly defined `include` and `exclude`", () => { + const normalized = normalizeOptions.default({ + include: ["es6.reflect.set-prototype-of"], + exclude: ["es6.reflect.set"], + }); + assert.deepEqual(normalized.include, ["es6.reflect.set-prototype-of"]); + assert.deepEqual(normalized.exclude, ["es6.reflect.set"]); + }); + }); + describe("validateBoolOption", () => { it("`undefined` option returns false", () => { assert(validateBoolOption("test", undefined, false) === false); @@ -71,24 +125,6 @@ describe("normalize-options", () => { }); }); - describe("normalizePluginNames", function() { - it("should drop `babel-plugin-` prefix if needed", function() { - assert.deepEqual( - normalizePluginNames([ - "babel-plugin-transform-object-super", - "transform-parameters", - ]), - ["transform-object-super", "transform-parameters"], - ); - }); - - it("should not throw if no duplicate names in both", function() { - assert.doesNotThrow(() => { - checkDuplicateIncludeExcludes(["transform-regenerator"], ["map"]); - }, Error); - }); - }); - describe("validateModulesOption", () => { it("`undefined` option returns commonjs", () => { assert(validateModulesOption() === "commonjs"); @@ -126,14 +162,4 @@ describe("normalize-options", () => { }, Error); }); }); - describe("validateIncludesAndExcludes", function() { - it("should return empty arrays if undefined", function() { - assert.deepEqual(validateIncludesAndExcludes(), []); - }); - it("should throw if not in features", function() { - assert.throws(() => { - validateIncludesAndExcludes(["asdf"]); - }, Error); - }); - }); });