From f4a24a38ca44270d8e2748baf6645350e959f9ce Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 20 Dec 2017 16:03:57 -0800 Subject: [PATCH] Allow config objects to use test/include/exclude to limit application to specific files. --- .../babel-core/src/config/config-chain.js | 56 ++- .../config/validation/option-assertions.js | 29 +- .../src/config/validation/options.js | 21 + packages/babel-core/test/config-chain.js | 414 ++++++++++++++++++ 4 files changed, 514 insertions(+), 6 deletions(-) diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js index 6924fa0bd3..f7a65b1c19 100644 --- a/packages/babel-core/src/config/config-chain.js +++ b/packages/babel-core/src/config/config-chain.js @@ -7,6 +7,7 @@ import { validate, type ValidatedOptions, type IgnoreList, + type ConfigApplicableTest, } from "./validation/options"; const debug = buildDebug("babel:config:config-chain"); @@ -204,11 +205,13 @@ function makeChainWalker< const flattenedConfigs = []; const rootOpts = root(input); - flattenedConfigs.push(rootOpts); + if (configIsApplicable(rootOpts, dirname, context)) { + flattenedConfigs.push(rootOpts); - const envOpts = env(input, context.envName); - if (envOpts) { - flattenedConfigs.push(envOpts); + const envOpts = env(input, context.envName); + if (envOpts && configIsApplicable(envOpts, dirname, context)) { + flattenedConfigs.push(envOpts); + } } // Process 'ignore' and 'only' before 'extends' items are processed so @@ -355,6 +358,42 @@ function dedupDescriptors( }, []); } +function configIsApplicable( + { options }: OptionsAndDescriptors, + dirname: string, + context: ConfigContext, +): boolean { + return ( + (options.test === undefined || + configFieldIsApplicable(context, options.test, dirname)) && + (options.include === undefined || + configFieldIsApplicable(context, options.include, dirname)) && + (options.exclude === undefined || + !configFieldIsApplicable(context, options.exclude, dirname)) + ); +} + +function configFieldIsApplicable( + context: ConfigContext, + test: ConfigApplicableTest, + dirname: string, +): boolean { + if (context.filename === null) { + throw new Error( + `Configuration contains explicit test/include/exclude checks, but no filename was passed to Babel`, + ); + } + // $FlowIgnore - Flow refinements aren't quite smart enough for this :( + const ctx: ConfigContextNamed = context; + + const patterns = Array.isArray(test) ? test : [test]; + + // Disabling negation here because it's a bit buggy from + // https://github.com/babel/babel/issues/6907 and it's not clear that it is + // needed since users can use 'exclude' alongside 'test'/'include'. + return matchesPatterns(ctx, patterns, dirname, false /* allowNegation */); +} + /** * Tests if a filename should be ignored based on "ignore" and "only" options. */ @@ -403,6 +442,7 @@ function matchesPatterns( context: ConfigContextNamed, patterns: IgnoreList, dirname: string, + allowNegation?: boolean = true, ): boolean { const res = []; const strings = []; @@ -424,13 +464,19 @@ function matchesPatterns( const absolutePatterns = strings.map(pattern => { // Preserve the "!" prefix so that micromatch can use it for negation. const negate = pattern[0] === "!"; + if (negate && !allowNegation) { + throw new Error(`Negation of file paths is not supported.`); + } if (negate) pattern = pattern.slice(1); return (negate ? "!" : "") + path.resolve(dirname, pattern); }); if ( - micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0 + micromatch(possibleDirs, absolutePatterns, { + nocase: true, + nonegate: !allowNegation, + }).length > 0 ) { return true; } diff --git a/packages/babel-core/src/config/validation/option-assertions.js b/packages/babel-core/src/config/validation/option-assertions.js index facdbbf345..312a1bfddc 100644 --- a/packages/babel-core/src/config/validation/option-assertions.js +++ b/packages/babel-core/src/config/validation/option-assertions.js @@ -6,6 +6,7 @@ import type { PluginList, PluginItem, PluginTarget, + ConfigApplicableTest, SourceMapsOption, SourceTypeOption, CompactOption, @@ -122,12 +123,38 @@ function assertIgnoreItem( !(value instanceof RegExp) ) { throw new Error( - `.${key}[${index}] must be an array of string/Funtion/RegExp values, or or undefined`, + `.${key}[${index}] must be an array of string/Funtion/RegExp values, or undefined`, ); } return value; } +export function assertConfigApplicableTest( + key: string, + value: mixed, +): ConfigApplicableTest | void { + if (Array.isArray(value)) { + value.forEach((item, i) => { + if (!checkValidTest(item)) { + throw new Error(`.${key}[${i}] must be a string/Function/RegExp.`); + } + }); + } else if (!checkValidTest(value)) { + throw new Error( + `.${key} must be a string/Function/RegExp, or an array of those`, + ); + } + return (value: any); +} + +function checkValidTest(value: mixed): boolean { + return ( + typeof value === "string" || + typeof value === "function" || + value instanceof RegExp + ); +} + export function assertPluginList(key: string, value: mixed): PluginList | void { const arr = assertArray(key, value); if (arr) { diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index ff5ba9602f..21def26551 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -8,6 +8,7 @@ import { assertInputSourceMap, assertIgnoreList, assertPluginList, + assertConfigApplicableTest, assertFunction, assertSourceMaps, assertCompact, @@ -44,6 +45,19 @@ const NONPRESET_VALIDATORS: ValidatorSet = { $PropertyType, >), only: (assertIgnoreList: Validator<$PropertyType>), + + // We could limit these to 'overrides' blocks, but it's not clear why we'd + // bother, when the ability to limit a config to a specific set of files + // is a fairly general useful feature. + test: (assertConfigApplicableTest: Validator< + $PropertyType, + >), + include: (assertConfigApplicableTest: Validator< + $PropertyType, + >), + exclude: (assertConfigApplicableTest: Validator< + $PropertyType, + >), }; const COMMON_VALIDATORS: ValidatorSet = { @@ -143,6 +157,11 @@ export type ValidatedOptions = { ignore?: IgnoreList, only?: IgnoreList, + // Generally verify if a given config object should be applied to the given file. + test?: ConfigApplicableTest, + include?: ConfigApplicableTest, + exclude?: ConfigApplicableTest, + presets?: PluginList, plugins?: PluginList, passPerPreset?: boolean, @@ -196,6 +215,8 @@ export type PluginItem = | [PluginTarget, PluginOptions, string]; export type PluginList = $ReadOnlyArray; +export type ConfigApplicableTest = IgnoreItem | Array; + export type SourceMapsOption = boolean | "inline" | "both"; export type SourceTypeOption = "module" | "script" | "unambiguous"; export type CompactOption = boolean | "auto"; diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index 651639ac36..867d946818 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -8,6 +8,420 @@ function fixture(...args) { } describe("buildConfigChain", function() { + describe("test", () => { + describe("single", () => { + it("should process matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: fixture("nonexistant-fake"), + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: new RegExp(fixture("nonexistant-fake")), + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: p => p.indexOf(fixture("nonexistant-fake")) === 0, + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: fixture("nonexistant-fake-unknown"), + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: new RegExp(fixture("nonexistant-unknown")), + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: p => p.indexOf(fixture("nonexistant-unknown")) === 0, + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + }); + + describe("array", () => { + it("should process matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: [fixture("nonexistant-fake")], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: [new RegExp(fixture("nonexistant-fake"))], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: [p => p.indexOf(fixture("nonexistant-fake")) === 0], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: [fixture("nonexistant-fake-unknown")], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: [new RegExp(fixture("nonexistant-unknown"))], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + test: [p => p.indexOf(fixture("nonexistant-unknown")) === 0], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + }); + }); + + describe("include", () => { + describe("single", () => { + it("should process matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: fixture("nonexistant-fake"), + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: new RegExp(fixture("nonexistant-fake")), + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: p => p.indexOf(fixture("nonexistant-fake")) === 0, + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: fixture("nonexistant-fake-unknown"), + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: new RegExp(fixture("nonexistant-unknown")), + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: p => p.indexOf(fixture("nonexistant-unknown")) === 0, + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + }); + + describe("array", () => { + it("should process matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: [fixture("nonexistant-fake")], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: [new RegExp(fixture("nonexistant-fake"))], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: [p => p.indexOf(fixture("nonexistant-fake")) === 0], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: [fixture("nonexistant-fake-unknown")], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: [new RegExp(fixture("nonexistant-unknown"))], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + include: [p => p.indexOf(fixture("nonexistant-unknown")) === 0], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + }); + }); + + describe("exclude", () => { + describe("single", () => { + it("should process matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: fixture("nonexistant-fake"), + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: new RegExp(fixture("nonexistant-fake")), + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: p => p.indexOf(fixture("nonexistant-fake")) === 0, + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: fixture("nonexistant-fake-unknown"), + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: new RegExp(fixture("nonexistant-unknown")), + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: p => p.indexOf(fixture("nonexistant-unknown")) === 0, + comments: true, + }); + + assert.equal(opts.comments, true); + }); + }); + + describe("array", () => { + it("should process matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: [fixture("nonexistant-fake")], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: [new RegExp(fixture("nonexistant-fake"))], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: [p => p.indexOf(fixture("nonexistant-fake")) === 0], + comments: true, + }); + + assert.equal(opts.comments, undefined); + }); + + it("should process non-matching string values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: [fixture("nonexistant-fake-unknown")], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching RegExp values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: [new RegExp(fixture("nonexistant-unknown"))], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + + it("should process non-matching function values", () => { + const opts = loadOptions({ + filename: fixture("nonexistant-fake", "src.js"), + babelrc: false, + exclude: [p => p.indexOf(fixture("nonexistant-unknown")) === 0], + comments: true, + }); + + assert.equal(opts.comments, true); + }); + }); + }); + describe("ignore", () => { it("should ignore files that match", () => { const opts = loadOptions({