From b2f1d01965653750615f631af75b624f6b00847a Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Tue, 19 Dec 2017 15:35:34 -0800 Subject: [PATCH 1/7] Add type annotations to utility file. --- .../src/transformation/util/missing-plugin-helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-core/src/transformation/util/missing-plugin-helper.js b/packages/babel-core/src/transformation/util/missing-plugin-helper.js index 0cc73a4a46..0d029e71eb 100644 --- a/packages/babel-core/src/transformation/util/missing-plugin-helper.js +++ b/packages/babel-core/src/transformation/util/missing-plugin-helper.js @@ -208,8 +208,8 @@ to enable [parsing|transformation]. */ export default function generateMissingPluginMessage( missingPluginName: string, - loc, - codeFrame, + loc: { line: number, column: number }, + codeFrame: string, ): string { let helpMessage = `Support for the experimental syntax '${missingPluginName}' isn't currently enabled ` + From 1178799f071d38f4b760a6899784ed53c2553322 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Tue, 19 Dec 2017 15:36:54 -0800 Subject: [PATCH 2/7] Remove the LoadedFile class. --- .../src/config/build-config-chain.js | 237 +++++++++--------- 1 file changed, 121 insertions(+), 116 deletions(-) diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 2e3858d263..39c3bb2088 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -74,6 +74,17 @@ type SimpleConfig = { dirname: string, }; +type ConfigContext = { + filename: string | null, + cwd: string, + envName: string, +}; + +type ConfigContextNamed = { + ...ConfigContext, + filename: string, +}; + export const buildPresetChain = makeWeakCache( (preset: PresetInstance): ConfigChain => { const loaded = processConfig(preset); @@ -91,18 +102,20 @@ export function buildRootChain( opts: ValidatedOptions, envName: string, ): ConfigChain | null { - const filename = opts.filename ? path.resolve(cwd, opts.filename) : null; - const builder = new ConfigChainBuilder( - filename ? new LoadedFile(filename) : null, - ); + const context = { + filename: opts.filename ? path.resolve(cwd, opts.filename) : null, + cwd, + envName, + }; + const builder = new ConfigChainBuilder(context); try { - builder.mergeConfigArguments(opts, cwd, envName); + builder.mergeConfigArguments(opts); // resolve all .babelrc files - if (opts.babelrc !== false && filename) { - findConfigs(path.dirname(filename), envName).forEach(configFile => - builder.mergeConfigFile(configFile, envName), + if (opts.babelrc !== false && context.filename !== null) { + findConfigs(path.dirname(context.filename), envName).forEach(configFile => + builder.mergeConfigFile(configFile), ); } } catch (e) { @@ -117,25 +130,23 @@ export function buildRootChain( } class ConfigChainBuilder { - file: LoadedFile | null; + context: ConfigContext; configs: Array = []; seenFiles: Set = new Set(); - constructor(file: LoadedFile | null) { - this.file = file; + constructor(context: ConfigContext) { + this.context = context; } - mergeConfigArguments( - opts: ValidatedOptions, - dirname: string, - envKey: string, - ) { - flattenArgumentsOptionsParts(opts, dirname, envKey).forEach(part => - this._processConfigPart(part, envKey), - ); + mergeConfigArguments(opts: ValidatedOptions) { + flattenArgumentsOptionsParts( + opts, + this.context.cwd, + this.context.envName, + ).forEach(part => this._processConfigPart(part)); } - mergeConfigFile(file: ConfigFile, envName: string) { + mergeConfigFile(file: ConfigFile) { if (this.seenFiles.has(file)) { throw new Error( `Cycle detected in Babel configuration file through "${ @@ -144,22 +155,19 @@ class ConfigChainBuilder { ); } - const parts = flattenFileOptionsParts(file)(envName); + const parts = flattenFileOptionsParts(file)(this.context.envName); this.seenFiles.add(file); - parts.forEach(part => this._processConfigPart(part, envName)); + parts.forEach(part => this._processConfigPart(part)); this.seenFiles.delete(file); } - _processConfigPart(part: ConfigPart, envName: string) { + _processConfigPart(part: ConfigPart) { if (part.part === "config") { const { ignore, only } = part; // Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files. - if ( - this.file && - this.file.shouldIgnore(ignore, only, part.config.dirname) - ) { + if (shouldIgnore(this.context, ignore, only, part.config.dirname)) { // TODO(logan): This is a really gross way to bail out. Avoid this in rewrite. throw Object.assign((new Error("This file has been ignored."): any), { code: "BABEL_IGNORED_FILE", @@ -169,8 +177,7 @@ class ConfigChainBuilder { this.configs.push(part.config); } else { this.mergeConfigFile( - loadConfig(part.path, part.dirname, envName), - envName, + loadConfig(part.path, part.dirname, this.context.envName), ); } } @@ -585,103 +592,101 @@ function flattenOptionsParts( } /** - * Track a given file and expose function to check if it should be ignored. + * Tests if a filename should be ignored based on "ignore" and "only" options. */ -class LoadedFile { - filename: string; - possibleDirs: null | Array = null; +function shouldIgnore( + context: ConfigContext, + ignore: ?IgnoreList, + only: ?IgnoreList, + dirname: string, +): boolean { + if (context.filename === null) return false; + // $FlowIgnore - Flow refinements aren't quite smart enough for this :( + const ctx: ConfigContextNamed = context; - constructor(filename) { - this.filename = filename; + if (ignore) { + if (matchesPatterns(ctx, ignore, dirname)) { + debug( + "Ignored %o because it matched one of %O from %o", + context.filename, + ignore, + dirname, + ); + return true; + } } - /** - * Tests if a filename should be ignored based on "ignore" and "only" options. - */ - shouldIgnore( - ignore: ?IgnoreList, - only: ?IgnoreList, - dirname: string, - ): boolean { - if (ignore) { - if (this._matchesPatterns(ignore, dirname)) { - debug( - "Ignored %o because it matched one of %O from %o", - this.filename, - ignore, - dirname, - ); - return true; - } + if (only) { + if (!matchesPatterns(ctx, only, dirname)) { + debug( + "Ignored %o because it failed to match one of %O from %o", + context.filename, + only, + dirname, + ); + return true; } - - if (only) { - if (!this._matchesPatterns(only, dirname)) { - debug( - "Ignored %o because it failed to match one of %O from %o", - this.filename, - only, - dirname, - ); - return true; - } - } - - return false; } - /** - * Returns result of calling function with filename if pattern is a function. - * Otherwise returns result of matching pattern Regex with filename. - */ - _matchesPatterns(patterns: IgnoreList, dirname: string): boolean { - const res = []; - const strings = []; - const fns = []; + return false; +} - patterns.forEach(pattern => { - if (typeof pattern === "string") strings.push(pattern); - else if (typeof pattern === "function") fns.push(pattern); - else res.push(pattern); +/** + * Returns result of calling function with filename if pattern is a function. + * Otherwise returns result of matching pattern Regex with filename. + */ +function matchesPatterns( + context: ConfigContextNamed, + patterns: IgnoreList, + dirname: string, +): boolean { + const res = []; + const strings = []; + const fns = []; + + patterns.forEach(pattern => { + if (typeof pattern === "string") strings.push(pattern); + else if (typeof pattern === "function") fns.push(pattern); + else res.push(pattern); + }); + + const filename = context.filename; + if (res.some(re => re.test(context.filename))) return true; + if (fns.some(fn => fn(filename))) return true; + + if (strings.length > 0) { + const possibleDirs = getPossibleDirs(context); + + const absolutePatterns = strings.map(pattern => { + // Preserve the "!" prefix so that micromatch can use it for negation. + const negate = pattern[0] === "!"; + if (negate) pattern = pattern.slice(1); + + return (negate ? "!" : "") + path.resolve(dirname, pattern); }); - const filename = this.filename; - if (res.some(re => re.test(filename))) return true; - if (fns.some(fn => fn(filename))) return true; - - if (strings.length > 0) { - let possibleDirs = this.possibleDirs; - // Lazy-init so we don't initialize this for files that have no glob patterns. - if (!possibleDirs) { - possibleDirs = this.possibleDirs = []; - - possibleDirs.push(filename); - - let current = filename; - while (true) { - const previous = current; - current = path.dirname(current); - if (previous === current) break; - - possibleDirs.push(current); - } - } - - const absolutePatterns = strings.map(pattern => { - // Preserve the "!" prefix so that micromatch can use it for negation. - const negate = pattern[0] === "!"; - if (negate) pattern = pattern.slice(1); - - return (negate ? "!" : "") + path.resolve(dirname, pattern); - }); - - if ( - micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0 - ) { - return true; - } + if ( + micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0 + ) { + return true; } - - return false; } + + return false; } + +const getPossibleDirs = makeWeakCache((context: ConfigContextNamed) => { + let current = context.filename; + if (current === null) return []; + + const possibleDirs = [current]; + while (true) { + const previous = current; + current = path.dirname(current); + if (previous === current) break; + + possibleDirs.push(current); + } + + return possibleDirs; +}); From de634437627969a455c3442c5c46580cd3f061f9 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Tue, 19 Dec 2017 20:21:13 -0800 Subject: [PATCH 3/7] Split babelrc and babelignore searching into two functions. --- .../src/config/build-config-chain.js | 22 +++- .../src/config/loading/files/configuration.js | 105 ++++++++++-------- .../src/config/loading/files/index-browser.js | 17 ++- 3 files changed, 89 insertions(+), 55 deletions(-) diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 39c3bb2088..452490ac48 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -16,7 +16,8 @@ const debug = buildDebug("babel:config:config-chain"); import { loadPlugin, loadPreset, - findConfigs, + findBabelrc, + findBabelignore, loadConfig, type ConfigFile, } from "./loading/files"; @@ -114,9 +115,22 @@ export function buildRootChain( // resolve all .babelrc files if (opts.babelrc !== false && context.filename !== null) { - findConfigs(path.dirname(context.filename), envName).forEach(configFile => - builder.mergeConfigFile(configFile), - ); + const filename = context.filename; + const babelrcFile = findBabelrc(filename, context.envName); + if (babelrcFile) builder.mergeConfigFile(babelrcFile); + + const babelignoreFile = findBabelignore(filename); + if ( + babelignoreFile && + shouldIgnore( + context, + babelignoreFile.ignore, + null, + babelignoreFile.dirname, + ) + ) { + return null; + } } } catch (e) { if (e.code !== "BABEL_IGNORED_FILE") throw e; diff --git a/packages/babel-core/src/config/loading/files/configuration.js b/packages/babel-core/src/config/loading/files/configuration.js index a8caa01cdc..061f4ce3d7 100644 --- a/packages/babel-core/src/config/loading/files/configuration.js +++ b/packages/babel-core/src/config/loading/files/configuration.js @@ -15,68 +15,75 @@ export type ConfigFile = { options: {}, }; +export type IgnoreFile = { + filepath: string, + dirname: string, + ignore: Array, +}; + const BABELRC_FILENAME = ".babelrc"; const BABELRC_JS_FILENAME = ".babelrc.js"; const PACKAGE_FILENAME = "package.json"; const BABELIGNORE_FILENAME = ".babelignore"; -export function findConfigs( - dirname: string, +export function findBabelrc( + filepath: string, envName: string, -): Array { - let foundConfig = false; - let foundIgnore = false; - - const confs = []; - +): ConfigFile | null { + const dirname = path.dirname(filepath); let loc = dirname; while (true) { - if (!foundIgnore) { - const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME); - const ignore = readIgnoreConfig(ignoreLoc); + const conf = [ + BABELRC_FILENAME, + BABELRC_JS_FILENAME, + PACKAGE_FILENAME, + ].reduce((previousConfig: ConfigFile | null, name) => { + const filepath = path.join(loc, name); + const config = readConfig(filepath, envName); - if (ignore) { - debug("Found ignore %o from %o.", ignore.filepath, dirname); - confs.push(ignore); - foundIgnore = true; + if (config && previousConfig) { + throw new Error( + `Multiple configuration files found. Please remove one:\n` + + ` - ${path.basename(previousConfig.filepath)}\n` + + ` - ${name}\n` + + `from ${loc}`, + ); } + + return config || previousConfig; + }, null); + + if (conf) { + debug("Found configuration %o from %o.", conf.filepath, dirname); + return conf; } - if (!foundConfig) { - const conf = [ - BABELRC_FILENAME, - BABELRC_JS_FILENAME, - PACKAGE_FILENAME, - ].reduce((previousConfig: ConfigFile | null, name) => { - const filepath = path.join(loc, name); - const config = readConfig(filepath, envName); - - if (config && previousConfig) { - throw new Error( - `Multiple configuration files found. Please remove one:\n- ${path.basename( - previousConfig.filepath, - )}\n- ${name}\nfrom ${loc}`, - ); - } - - return config || previousConfig; - }, null); - - if (conf) { - debug("Found configuration %o from %o.", conf.filepath, dirname); - confs.push(conf); - foundConfig = true; - } - } - - if (foundIgnore && foundConfig) break; - - if (loc === path.dirname(loc)) break; - - loc = path.dirname(loc); + const nextLoc = path.dirname(loc); + if (loc === nextLoc) break; + loc = nextLoc; } - return confs; + return null; +} + +export function findBabelignore(filepath: string): IgnoreFile | null { + const dirname = path.dirname(filepath); + let loc = dirname; + while (true) { + const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME); + const ignore = readIgnoreConfig(ignoreLoc); + + if (ignore) { + debug("Found ignore %o from %o.", ignore.filepath, dirname); + return ignore; + } + + const nextLoc = path.dirname(loc); + if (loc === nextLoc) break; + loc = nextLoc; + } + + return null; } export function loadConfig( @@ -224,7 +231,7 @@ const readIgnoreConfig = makeStaticFileCache((filepath, content) => { return { filepath, dirname: path.dirname(filepath), - options: { ignore }, + ignore, }; }); diff --git a/packages/babel-core/src/config/loading/files/index-browser.js b/packages/babel-core/src/config/loading/files/index-browser.js index 1d0b249225..bcd6b439a0 100644 --- a/packages/babel-core/src/config/loading/files/index-browser.js +++ b/packages/babel-core/src/config/loading/files/index-browser.js @@ -6,9 +6,22 @@ export type ConfigFile = { options: {}, }; +export type IgnoreFile = { + filepath: string, + dirname: string, + ignore: Array, +}; + +export function findBabelrc( + filepath: string, + envName: string, // eslint-disable-line no-unused-vars +): ConfigFile | null { + return null; +} + // eslint-disable-next-line no-unused-vars -export function findConfigs(dirname: string): Array { - return []; +export function findBabelignore(filepath: string): IgnoreFile | null { + return null; } export function loadConfig(name: string, dirname: string): ConfigFile { From 43e7d1d2cc55fd0277cf6cc7c81af912403746ce Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 20 Dec 2017 00:41:40 -0800 Subject: [PATCH 4/7] Use an object instead of a 2-tuple. --- packages/babel-core/src/config/caching.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/babel-core/src/config/caching.js b/packages/babel-core/src/config/caching.js index 47414fbc68..b9703560b5 100644 --- a/packages/babel-core/src/config/caching.js +++ b/packages/babel-core/src/config/caching.js @@ -14,9 +14,10 @@ type SimpleCacheConfiguratorObj = { invalidate: (handler: () => T) => T, }; -type CacheEntry = Array< - [ResultT, (SideChannel) => boolean], ->; +type CacheEntry = Array<{ + value: ResultT, + valid: SideChannel => boolean, +}>; export type { CacheConfigurator }; @@ -64,7 +65,7 @@ function makeCachedFunction< ); if (cachedValue) { - for (const [value, valid] of cachedValue) { + for (const { value, valid } of cachedValue) { if (valid(data)) return value; } } @@ -79,18 +80,18 @@ function makeCachedFunction< switch (cache.mode()) { case "forever": - cachedValue = [[value, () => true]]; + cachedValue = [{ value, valid: () => true }]; callCache.set(arg, cachedValue); break; case "invalidate": - cachedValue = [[value, cache.validator()]]; + cachedValue = [{ value, valid: cache.validator() }]; callCache.set(arg, cachedValue); break; case "valid": if (cachedValue) { - cachedValue.push([value, cache.validator()]); + cachedValue.push({ value, valid: cache.validator() }); } else { - cachedValue = [[value, cache.validator()]]; + cachedValue = [{ value, valid: cache.validator() }]; callCache.set(arg, cachedValue); } } From f9825394a7b4867b4c806559fcbee6ee8f42a5b8 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Thu, 21 Dec 2017 13:56:47 -0800 Subject: [PATCH 5/7] Remove unnecessary folder nesting. --- packages/babel-core/package.json | 2 +- packages/babel-core/src/config/build-config-chain.js | 2 +- .../babel-core/src/config/{loading => }/files/configuration.js | 2 +- .../babel-core/src/config/{loading => }/files/index-browser.js | 0 packages/babel-core/src/config/{loading => }/files/index.js | 0 packages/babel-core/src/config/{loading => }/files/plugins.js | 0 packages/babel-core/src/index.js | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename packages/babel-core/src/config/{loading => }/files/configuration.js (99%) rename packages/babel-core/src/config/{loading => }/files/index-browser.js (100%) rename packages/babel-core/src/config/{loading => }/files/index.js (100%) rename packages/babel-core/src/config/{loading => }/files/plugins.js (100%) diff --git a/packages/babel-core/package.json b/packages/babel-core/package.json index 1ae2704777..6a2054ebf3 100644 --- a/packages/babel-core/package.json +++ b/packages/babel-core/package.json @@ -23,7 +23,7 @@ "compiler" ], "browser": { - "./lib/config/loading/files/index.js": "./lib/config/loading/files/index-browser.js", + "./lib/config/files/index.js": "./lib/config/files/index-browser.js", "./lib/transform-file.js": "./lib/transform-file-browser.js", "./lib/transform-file-sync.js": "./lib/transform-file-sync-browser.js" }, diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 452490ac48..9f8f17e574 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -20,7 +20,7 @@ import { findBabelignore, loadConfig, type ConfigFile, -} from "./loading/files"; +} from "./files"; import { makeWeakCache, makeStrongCache } from "./caching"; diff --git a/packages/babel-core/src/config/loading/files/configuration.js b/packages/babel-core/src/config/files/configuration.js similarity index 99% rename from packages/babel-core/src/config/loading/files/configuration.js rename to packages/babel-core/src/config/files/configuration.js index 061f4ce3d7..5617f3e473 100644 --- a/packages/babel-core/src/config/loading/files/configuration.js +++ b/packages/babel-core/src/config/files/configuration.js @@ -5,7 +5,7 @@ import path from "path"; import fs from "fs"; import json5 from "json5"; import resolve from "resolve"; -import { makeStrongCache, type CacheConfigurator } from "../../caching"; +import { makeStrongCache, type CacheConfigurator } from "../caching"; const debug = buildDebug("babel:config:loading:files:configuration"); diff --git a/packages/babel-core/src/config/loading/files/index-browser.js b/packages/babel-core/src/config/files/index-browser.js similarity index 100% rename from packages/babel-core/src/config/loading/files/index-browser.js rename to packages/babel-core/src/config/files/index-browser.js diff --git a/packages/babel-core/src/config/loading/files/index.js b/packages/babel-core/src/config/files/index.js similarity index 100% rename from packages/babel-core/src/config/loading/files/index.js rename to packages/babel-core/src/config/files/index.js diff --git a/packages/babel-core/src/config/loading/files/plugins.js b/packages/babel-core/src/config/files/plugins.js similarity index 100% rename from packages/babel-core/src/config/loading/files/plugins.js rename to packages/babel-core/src/config/files/plugins.js diff --git a/packages/babel-core/src/index.js b/packages/babel-core/src/index.js index f2a968e887..dbf2cff91b 100644 --- a/packages/babel-core/src/index.js +++ b/packages/babel-core/src/index.js @@ -4,7 +4,7 @@ export { default as File } from "./transformation/file/file"; export { default as buildExternalHelpers, } from "./tools/build-external-helpers"; -export { resolvePlugin, resolvePreset } from "./config/loading/files"; +export { resolvePlugin, resolvePreset } from "./config/files"; export { version } from "../package.json"; export { getEnv } from "./config/helpers/environment"; From 7b861796cf01abf33614383bea1d79ecefe6df3d Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Thu, 21 Dec 2017 14:36:54 -0800 Subject: [PATCH 6/7] Centralize validation logic in common folder. --- .../src/config/build-config-chain.js | 2 +- packages/babel-core/src/config/index.js | 2 +- .../babel-core/src/config/option-manager.js | 5 +- packages/babel-core/src/config/plugin.js | 96 +------------------ .../{ => validation}/option-assertions.js | 0 .../src/config/{ => validation}/options.js | 0 .../src/config/validation/plugins.js | 95 ++++++++++++++++++ .../src/config/{ => validation}/removed.js | 0 8 files changed, 101 insertions(+), 99 deletions(-) rename packages/babel-core/src/config/{ => validation}/option-assertions.js (100%) rename packages/babel-core/src/config/{ => validation}/options.js (100%) create mode 100644 packages/babel-core/src/config/validation/plugins.js rename packages/babel-core/src/config/{ => validation}/removed.js (100%) diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 9f8f17e574..53d4e88fa3 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -9,7 +9,7 @@ import { type PluginItem, type PluginList, type IgnoreList, -} from "./options"; +} from "./validation/options"; const debug = buildDebug("babel:config:config-chain"); diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index e988ed02da..0bdd1184bb 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -3,7 +3,7 @@ import type Plugin from "./plugin"; import manageOptions from "./option-manager"; -export type { InputOptions } from "./options"; +export type { InputOptions } from "./validation/options"; export type ResolvedConfig = { options: Object, diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index c25c037905..37e54aa879 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -2,7 +2,7 @@ import path from "path"; import * as context from "../index"; -import Plugin, { validatePluginObject } from "./plugin"; +import Plugin from "./plugin"; import merge from "lodash/merge"; import { buildRootChain, @@ -15,7 +15,8 @@ import traverse from "@babel/traverse"; import clone from "lodash/clone"; import { makeWeakCache, type CacheConfigurator } from "./caching"; import { getEnv } from "./helpers/environment"; -import { validate } from "./options"; +import { validate } from "./validation/options"; +import { validatePluginObject } from "./validation/plugins"; export default function manageOptions(inputOpts: {}): { options: Object, diff --git a/packages/babel-core/src/config/plugin.js b/packages/babel-core/src/config/plugin.js index 97b1364777..6da0fe6360 100644 --- a/packages/babel-core/src/config/plugin.js +++ b/packages/babel-core/src/config/plugin.js @@ -1,100 +1,6 @@ // @flow -import { - assertString, - assertFunction, - assertObject, - type ValidatorSet, - type Validator, -} from "./option-assertions"; - -// Note: The casts here are just meant to be static assertions to make sure -// that the assertion functions actually assert that the value's type matches -// the declared types. -const VALIDATORS: ValidatorSet = { - name: (assertString: Validator<$PropertyType>), - manipulateOptions: (assertFunction: Validator< - $PropertyType, - >), - pre: (assertFunction: Validator<$PropertyType>), - post: (assertFunction: Validator<$PropertyType>), - inherits: (assertFunction: Validator< - $PropertyType, - >), - visitor: (assertVisitorMap: Validator< - $PropertyType, - >), - - parserOverride: (assertFunction: Validator< - $PropertyType, - >), - generatorOverride: (assertFunction: Validator< - $PropertyType, - >), -}; - -function assertVisitorMap(key: string, value: mixed): VisitorMap { - const obj = assertObject(key, value); - if (obj) { - Object.keys(obj).forEach(prop => assertVisitorHandler(prop, obj[prop])); - - if (obj.enter || obj.exit) { - throw new Error( - `.${key} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`, - ); - } - } - return (obj: any); -} - -function assertVisitorHandler( - key: string, - value: mixed, -): VisitorHandler | void { - if (value && typeof value === "object") { - Object.keys(value).forEach(handler => { - if (handler !== "enter" && handler !== "exit") { - throw new Error( - `.visitor["${key}"] may only have .enter and/or .exit handlers.`, - ); - } - }); - } else if (typeof value !== "function") { - throw new Error(`.visitor["${key}"] must be a function`); - } - - return (value: any); -} - -type VisitorHandler = Function | { enter?: Function, exit?: Function }; -export type VisitorMap = { - [string]: VisitorHandler, -}; - -export type PluginObject = { - name?: string, - manipulateOptions?: Function, - - pre?: Function, - post?: Function, - - inherits?: Function, - visitor?: VisitorMap, - - parserOverride?: Function, - generatorOverride?: Function, -}; - -export function validatePluginObject(obj: {}): PluginObject { - Object.keys(obj).forEach(key => { - const validator = VALIDATORS[key]; - - if (validator) validator(key, obj[key]); - else throw new Error(`.${key} is not a valid Plugin property`); - }); - - return (obj: any); -} +import type { PluginObject } from "./validation/plugins"; export default class Plugin { key: ?string; diff --git a/packages/babel-core/src/config/option-assertions.js b/packages/babel-core/src/config/validation/option-assertions.js similarity index 100% rename from packages/babel-core/src/config/option-assertions.js rename to packages/babel-core/src/config/validation/option-assertions.js diff --git a/packages/babel-core/src/config/options.js b/packages/babel-core/src/config/validation/options.js similarity index 100% rename from packages/babel-core/src/config/options.js rename to packages/babel-core/src/config/validation/options.js diff --git a/packages/babel-core/src/config/validation/plugins.js b/packages/babel-core/src/config/validation/plugins.js new file mode 100644 index 0000000000..7800de965e --- /dev/null +++ b/packages/babel-core/src/config/validation/plugins.js @@ -0,0 +1,95 @@ +import { + assertString, + assertFunction, + assertObject, + type ValidatorSet, + type Validator, +} from "./option-assertions"; + +// Note: The casts here are just meant to be static assertions to make sure +// that the assertion functions actually assert that the value's type matches +// the declared types. +const VALIDATORS: ValidatorSet = { + name: (assertString: Validator<$PropertyType>), + manipulateOptions: (assertFunction: Validator< + $PropertyType, + >), + pre: (assertFunction: Validator<$PropertyType>), + post: (assertFunction: Validator<$PropertyType>), + inherits: (assertFunction: Validator< + $PropertyType, + >), + visitor: (assertVisitorMap: Validator< + $PropertyType, + >), + + parserOverride: (assertFunction: Validator< + $PropertyType, + >), + generatorOverride: (assertFunction: Validator< + $PropertyType, + >), +}; + +function assertVisitorMap(key: string, value: mixed): VisitorMap { + const obj = assertObject(key, value); + if (obj) { + Object.keys(obj).forEach(prop => assertVisitorHandler(prop, obj[prop])); + + if (obj.enter || obj.exit) { + throw new Error( + `.${key} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`, + ); + } + } + return (obj: any); +} + +function assertVisitorHandler( + key: string, + value: mixed, +): VisitorHandler | void { + if (value && typeof value === "object") { + Object.keys(value).forEach(handler => { + if (handler !== "enter" && handler !== "exit") { + throw new Error( + `.visitor["${key}"] may only have .enter and/or .exit handlers.`, + ); + } + }); + } else if (typeof value !== "function") { + throw new Error(`.visitor["${key}"] must be a function`); + } + + return (value: any); +} + +type VisitorHandler = Function | { enter?: Function, exit?: Function }; +export type VisitorMap = { + [string]: VisitorHandler, +}; + +export type PluginObject = { + name?: string, + manipulateOptions?: Function, + + pre?: Function, + post?: Function, + + inherits?: Function, + visitor?: VisitorMap, + + parserOverride?: Function, + generatorOverride?: Function, +}; + +export function validatePluginObject(obj: {}): PluginObject { + Object.keys(obj).forEach(key => { + const validator = VALIDATORS[key]; + + if (validator) validator(key, obj[key]); + else throw new Error(`.${key} is not a valid Plugin property`); + }); + + return (obj: any); +} diff --git a/packages/babel-core/src/config/removed.js b/packages/babel-core/src/config/validation/removed.js similarity index 100% rename from packages/babel-core/src/config/removed.js rename to packages/babel-core/src/config/validation/removed.js From 758fd0369cd568f9344db465a1a0f76dfd90e2c1 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Tue, 19 Dec 2017 18:10:51 -0800 Subject: [PATCH 7/7] Rewrite config chain loading to build chain recursively to keep caching readable. --- .../src/config/build-config-chain.js | 706 ------------------ .../babel-core/src/config/config-chain.js | 452 +++++++++++ .../src/config/config-descriptors.js | 272 +++++++ packages/babel-core/src/config/index.js | 282 ++++++- .../babel-core/src/config/option-manager.js | 278 ------- packages/babel-core/test/config-chain.js | 49 +- 6 files changed, 1039 insertions(+), 1000 deletions(-) delete mode 100644 packages/babel-core/src/config/build-config-chain.js create mode 100644 packages/babel-core/src/config/config-chain.js create mode 100644 packages/babel-core/src/config/config-descriptors.js delete mode 100644 packages/babel-core/src/config/option-manager.js diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js deleted file mode 100644 index 53d4e88fa3..0000000000 --- a/packages/babel-core/src/config/build-config-chain.js +++ /dev/null @@ -1,706 +0,0 @@ -// @flow - -import path from "path"; -import micromatch from "micromatch"; -import buildDebug from "debug"; -import { - validate, - type ValidatedOptions, - type PluginItem, - type PluginList, - type IgnoreList, -} from "./validation/options"; - -const debug = buildDebug("babel:config:config-chain"); - -import { - loadPlugin, - loadPreset, - findBabelrc, - findBabelignore, - loadConfig, - type ConfigFile, -} from "./files"; - -import { makeWeakCache, makeStrongCache } from "./caching"; - -type ConfigItem = { - type: "arguments" | "env" | "file", - options: ValidatedOptions, - alias: string, - dirname: string, -}; - -export type ConfigChain = { - plugins: Array, - presets: Array, - options: Array, -}; - -export type BasicDescriptor = { - name: string | void, - value: {} | Function, - options: {} | void | false, - dirname: string, - alias: string, - ownPass?: boolean, -}; - -export type PresetInstance = SimpleConfig; - -type LoadedConfig = { - options: ValidatedOptions, - plugins: Array, - presets: Array, -}; - -type ConfigPart = - | { - part: "config", - config: ConfigItem, - ignore: ?IgnoreList, - only: ?IgnoreList, - activeEnv: string | null, - } - | { - part: "extends", - path: string, - dirname: string, - activeEnv: string | null, - }; - -type SimpleConfig = { - options: ValidatedOptions, - alias: string, - dirname: string, -}; - -type ConfigContext = { - filename: string | null, - cwd: string, - envName: string, -}; - -type ConfigContextNamed = { - ...ConfigContext, - filename: string, -}; - -export const buildPresetChain = makeWeakCache( - (preset: PresetInstance): ConfigChain => { - const loaded = processConfig(preset); - - return { - plugins: loaded.plugins, - presets: loaded.presets, - options: [loaded.options], - }; - }, -); - -export function buildRootChain( - cwd: string, - opts: ValidatedOptions, - envName: string, -): ConfigChain | null { - const context = { - filename: opts.filename ? path.resolve(cwd, opts.filename) : null, - cwd, - envName, - }; - const builder = new ConfigChainBuilder(context); - - try { - builder.mergeConfigArguments(opts); - - // resolve all .babelrc files - if (opts.babelrc !== false && context.filename !== null) { - const filename = context.filename; - const babelrcFile = findBabelrc(filename, context.envName); - if (babelrcFile) builder.mergeConfigFile(babelrcFile); - - const babelignoreFile = findBabelignore(filename); - if ( - babelignoreFile && - shouldIgnore( - context, - babelignoreFile.ignore, - null, - babelignoreFile.dirname, - ) - ) { - return null; - } - } - } catch (e) { - if (e.code !== "BABEL_IGNORED_FILE") throw e; - - return null; - } - - return dedupLoadedConfigs( - builder.configs.reverse().map(config => processConfig(config)), - ); -} - -class ConfigChainBuilder { - context: ConfigContext; - configs: Array = []; - seenFiles: Set = new Set(); - - constructor(context: ConfigContext) { - this.context = context; - } - - mergeConfigArguments(opts: ValidatedOptions) { - flattenArgumentsOptionsParts( - opts, - this.context.cwd, - this.context.envName, - ).forEach(part => this._processConfigPart(part)); - } - - mergeConfigFile(file: ConfigFile) { - if (this.seenFiles.has(file)) { - throw new Error( - `Cycle detected in Babel configuration file through "${ - file.filepath - }".`, - ); - } - - const parts = flattenFileOptionsParts(file)(this.context.envName); - - this.seenFiles.add(file); - parts.forEach(part => this._processConfigPart(part)); - this.seenFiles.delete(file); - } - - _processConfigPart(part: ConfigPart) { - if (part.part === "config") { - const { ignore, only } = part; - - // Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files. - if (shouldIgnore(this.context, ignore, only, part.config.dirname)) { - // TODO(logan): This is a really gross way to bail out. Avoid this in rewrite. - throw Object.assign((new Error("This file has been ignored."): any), { - code: "BABEL_IGNORED_FILE", - }); - } - - this.configs.push(part.config); - } else { - this.mergeConfigFile( - loadConfig(part.path, part.dirname, this.context.envName), - ); - } - } -} - -/** - * Given a plugin/preset item, resolve it into a standard format. - */ -function createDescriptor( - pair: PluginItem, - resolver, - dirname, - { - index, - alias, - ownPass, - }: { - index: number, - alias: string, - ownPass?: boolean, - }, -): BasicDescriptor { - let name; - let options; - let value = pair; - if (Array.isArray(value)) { - if (value.length === 3) { - // $FlowIgnore - Flow doesn't like the multiple tuple types. - [value, options, name] = value; - } else { - [value, options] = value; - } - } - - let filepath = null; - if (typeof value === "string") { - ({ filepath, value } = resolver(value, dirname)); - } - - if (!value) { - throw new Error(`Unexpected falsy value: ${String(value)}`); - } - - if (typeof value === "object" && value.__esModule) { - if (value.default) { - value = value.default; - } else { - throw new Error("Must export a default export when using ES6 modules."); - } - } - - if (typeof value !== "object" && typeof value !== "function") { - throw new Error( - `Unsupported format: ${typeof value}. Expected an object or a function.`, - ); - } - - if (filepath !== null && typeof value === "object" && value) { - // We allow object values for plugins/presets nested directly within a - // config object, because it can be useful to define them in nested - // configuration contexts. - throw new Error( - "Plugin/Preset files are not allowed to export objects, only functions.", - ); - } - - return { - name, - alias: filepath || `${alias}$${index}`, - value, - options, - dirname, - ownPass, - }; -} - -function normalizeOptions(opts: ValidatedOptions): ValidatedOptions { - const options = Object.assign({}, opts); - delete options.extends; - delete options.env; - delete options.plugins; - delete options.presets; - delete options.passPerPreset; - delete options.ignore; - delete options.only; - - // "sourceMap" is just aliased to sourceMap, so copy it over as - // we merge the options together. - if (options.sourceMap) { - options.sourceMaps = options.sourceMap; - delete options.sourceMap; - } - return options; -} - -/** - * Load and validate the given config into a set of options, plugins, and presets. - */ -const processConfig = makeWeakCache((config: SimpleConfig): LoadedConfig => { - const plugins = (config.options.plugins || []).map((plugin, index) => - createDescriptor(plugin, loadPlugin, config.dirname, { - index, - alias: config.alias, - }), - ); - - assertNoDuplicates(plugins); - - const presets = (config.options.presets || []).map((preset, index) => - createDescriptor(preset, loadPreset, config.dirname, { - index, - alias: config.alias, - ownPass: config.options.passPerPreset, - }), - ); - - assertNoDuplicates(presets); - - return { - options: normalizeOptions(config.options), - plugins, - presets, - }; -}); - -function assertNoDuplicates(items: Array): void { - const map = new Map(); - - for (const item of items) { - if (typeof item.value !== "function") continue; - - let nameMap = map.get(item.value); - if (!nameMap) { - nameMap = new Set(); - map.set(item.value, nameMap); - } - - if (nameMap.has(item.name)) { - throw new Error( - [ - `Duplicate plugin/preset detected.`, - `If you'd like to use two separate instances of a plugin,`, - `they neen separate names, e.g.`, - ``, - ` plugins: [`, - ` ['some-plugin', {}],`, - ` ['some-plugin', {}, 'some unique name'],`, - ` ]`, - ].join("\n"), - ); - } - - nameMap.add(item.name); - } -} - -function dedupLoadedConfigs(items: Array): ConfigChain { - const options = []; - const plugins = []; - const presets = []; - - for (const item of items) { - plugins.push(...item.plugins); - presets.push(...item.presets); - options.push(item.options); - } - - return { - options, - plugins: dedupDescriptors(plugins), - presets: dedupDescriptors(presets), - }; -} - -function dedupDescriptors( - items: Array, -): Array { - const map: Map< - Function, - Map, - > = new Map(); - - const descriptors = []; - - for (const item of items) { - if (typeof item.value === "function") { - const fnKey = item.value; - let nameMap = map.get(fnKey); - if (!nameMap) { - nameMap = new Map(); - map.set(fnKey, nameMap); - } - let desc = nameMap.get(item.name); - if (!desc) { - desc = { value: null }; - descriptors.push(desc); - - // Treat passPerPreset presets as unique, skipping them - // in the merge processing steps. - if (!item.ownPass) nameMap.set(item.name, desc); - } - - if (item.options === false) { - desc.value = null; - } else { - desc.value = item; - } - } else { - descriptors.push({ value: item }); - } - } - - return descriptors.reduce((acc, desc) => { - if (desc.value) acc.push(desc.value); - return acc; - }, []); -} - -/** - * Given the root config object passed to Babel, split it into the separate - * config parts. The resulting config objects in the 'ConfigPart' have their - * object identity preserved between calls so that they can be used for caching. - */ -function flattenArgumentsOptionsParts( - opts: ValidatedOptions, - dirname: string, - envName: string, -): Array { - const { - env, - plugins, - presets, - passPerPreset, - extends: extendsPath, - ...options - } = opts; - - const raw = []; - if (env) { - raw.push(...flattenArgumentsEnvOptionsParts(env)(dirname)(envName)); - } - - if (Object.keys(options).length > 0) { - raw.push(...flattenOptionsParts(buildArgumentsItem(options, dirname))); - } - - if (plugins) { - raw.push(...flattenArgumentsPluginsOptionsParts(plugins)(dirname)); - } - if (presets) { - raw.push( - ...flattenArgumentsPresetsOptionsParts(presets)(!!passPerPreset)(dirname), - ); - } - - if (extendsPath != null) { - raw.push( - ...flattenOptionsParts( - buildArgumentsItem({ extends: extendsPath }, dirname), - ), - ); - } - - return raw; -} - -/** - * For the top-level 'options' object, we cache the env list based on - * the object identity of the 'env' object. - */ -const flattenArgumentsEnvOptionsParts = makeWeakCache((env: {}) => { - const options: ValidatedOptions = { env }; - - return makeStrongCache((dirname: string) => - flattenOptionsPartsLookup(buildArgumentsItem(options, dirname)), - ); -}); - -/** - * For the top-level 'options' object, we cache the plugin list based on - * the object identity of the 'plugins' object. - */ -const flattenArgumentsPluginsOptionsParts = makeWeakCache( - (plugins: PluginList) => { - const options: ValidatedOptions = { plugins }; - - return makeStrongCache((dirname: string) => - flattenOptionsParts(buildArgumentsItem(options, dirname)), - ); - }, -); - -/** - * For the top-level 'options' object, we cache the preset list based on - * the object identity of the 'presets' object. - */ -const flattenArgumentsPresetsOptionsParts = makeWeakCache( - (presets: PluginList) => - makeStrongCache((passPerPreset: boolean) => { - // The concept of passPerPreset is integrally tied to the preset list - // so unfortunately we need to copy both values here, adding an extra - // layer of caching functions. - const options = { presets, passPerPreset }; - - return makeStrongCache((dirname: string) => - flattenOptionsParts(buildArgumentsItem(options, dirname)), - ); - }), -); - -function buildArgumentsItem( - options: ValidatedOptions, - dirname: string, -): ConfigItem { - return { - type: "arguments", - options, - alias: "base", - dirname, - }; -} - -/** - * Given a config from a specific file, return a list of ConfigPart objects - * with object identity preserved for all 'config' part objects for use - * with caching later in config processing. - */ -const flattenFileOptionsParts = makeWeakCache((file: ConfigFile) => { - return flattenOptionsPartsLookup({ - type: "file", - options: validate("file", file.options), - alias: file.filepath, - dirname: file.dirname, - }); -}); - -/** - * Given a config, create a function that will return the config parts for - * the environment passed as the first argument. - */ -function flattenOptionsPartsLookup( - config: ConfigItem, -): (string | null) => Array { - const parts = flattenOptionsParts(config); - - const def = parts.filter(part => part.activeEnv === null); - const lookup = new Map(); - - parts.forEach(part => { - if (part.activeEnv !== null) lookup.set(part.activeEnv, []); - }); - - for (const [activeEnv, values] of lookup) { - parts.forEach(part => { - if (part.activeEnv === null || part.activeEnv === activeEnv) { - values.push(part); - } - }); - } - - return envName => lookup.get(envName) || def; -} - -/** - * Given a generic config object, flatten it into its various parts so that - * then can be cached and processed later. - */ -function flattenOptionsParts( - config: ConfigItem, - activeEnv: string | null = null, -): Array { - const { options: rawOpts, alias, dirname } = config; - - const parts = []; - - if (rawOpts.env) { - for (const envKey of Object.keys(rawOpts.env)) { - if (rawOpts.env[envKey]) { - parts.push( - ...flattenOptionsParts( - { - type: "env", - options: rawOpts.env[envKey], - alias: alias + `.env.${envKey}`, - dirname, - }, - envKey, - ), - ); - } - } - } - - parts.push({ - part: "config", - config, - ignore: rawOpts.ignore, - only: rawOpts.only, - activeEnv, - }); - - if (rawOpts.extends != null) { - parts.push({ - part: "extends", - path: rawOpts.extends, - dirname, - activeEnv, - }); - } - - return parts; -} - -/** - * Tests if a filename should be ignored based on "ignore" and "only" options. - */ -function shouldIgnore( - context: ConfigContext, - ignore: ?IgnoreList, - only: ?IgnoreList, - dirname: string, -): boolean { - if (context.filename === null) return false; - // $FlowIgnore - Flow refinements aren't quite smart enough for this :( - const ctx: ConfigContextNamed = context; - - if (ignore) { - if (matchesPatterns(ctx, ignore, dirname)) { - debug( - "Ignored %o because it matched one of %O from %o", - context.filename, - ignore, - dirname, - ); - return true; - } - } - - if (only) { - if (!matchesPatterns(ctx, only, dirname)) { - debug( - "Ignored %o because it failed to match one of %O from %o", - context.filename, - only, - dirname, - ); - return true; - } - } - - return false; -} - -/** - * Returns result of calling function with filename if pattern is a function. - * Otherwise returns result of matching pattern Regex with filename. - */ -function matchesPatterns( - context: ConfigContextNamed, - patterns: IgnoreList, - dirname: string, -): boolean { - const res = []; - const strings = []; - const fns = []; - - patterns.forEach(pattern => { - if (typeof pattern === "string") strings.push(pattern); - else if (typeof pattern === "function") fns.push(pattern); - else res.push(pattern); - }); - - const filename = context.filename; - if (res.some(re => re.test(context.filename))) return true; - if (fns.some(fn => fn(filename))) return true; - - if (strings.length > 0) { - const possibleDirs = getPossibleDirs(context); - - const absolutePatterns = strings.map(pattern => { - // Preserve the "!" prefix so that micromatch can use it for negation. - const negate = pattern[0] === "!"; - if (negate) pattern = pattern.slice(1); - - return (negate ? "!" : "") + path.resolve(dirname, pattern); - }); - - if ( - micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0 - ) { - return true; - } - } - - return false; -} - -const getPossibleDirs = makeWeakCache((context: ConfigContextNamed) => { - let current = context.filename; - if (current === null) return []; - - const possibleDirs = [current]; - while (true) { - const previous = current; - current = path.dirname(current); - if (previous === current) break; - - possibleDirs.push(current); - } - - return possibleDirs; -}); diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js new file mode 100644 index 0000000000..5ecbde756f --- /dev/null +++ b/packages/babel-core/src/config/config-chain.js @@ -0,0 +1,452 @@ +// @flow + +import path from "path"; +import micromatch from "micromatch"; +import buildDebug from "debug"; +import { + validate, + type ValidatedOptions, + type IgnoreList, +} from "./validation/options"; + +const debug = buildDebug("babel:config:config-chain"); + +import { + findBabelrc, + findBabelignore, + loadConfig, + type ConfigFile, +} from "./files"; + +import { makeWeakCache, makeStrongCache } from "./caching"; + +import { + createCachedDescriptors, + createUncachedDescriptors, + type UnloadedDescriptor, + type OptionsAndDescriptors, + type ValidatedFile, +} from "./config-descriptors"; + +export type ConfigChain = { + plugins: Array, + presets: Array, + options: Array, +}; + +export type PresetInstance = { + options: ValidatedOptions, + alias: string, + dirname: string, +}; + +type ConfigContext = { + filename: string | null, + cwd: string, + envName: string, +}; + +type ConfigContextNamed = { + ...ConfigContext, + filename: string, +}; + +/** + * Build a config chain for a given preset. + */ +export const buildPresetChain = makeWeakCache( + ({ dirname, options, alias }: PresetInstance): ConfigChain => { + const result = createUncachedDescriptors(dirname, options, alias); + const { plugins, presets } = result; + return { + plugins: plugins(), + presets: presets(), + options: [normalizeOptions(result.options)], + }; + }, +); + +/** + * Build a config chain for Babel's full root configuration. + */ +export function buildRootChain( + cwd: string, + opts: ValidatedOptions, + envName: string, +): ConfigChain | null { + const context = { + filename: opts.filename ? path.resolve(cwd, opts.filename) : null, + cwd, + envName, + }; + + const programmaticChain = loadProgrammaticChain( + { + options: opts, + dirname: context.cwd, + }, + context, + ); + if (!programmaticChain) return null; + + const fileChain = emptyChain(); + // resolve all .babelrc files + if (opts.babelrc !== false && context.filename !== null) { + const filename = context.filename; + const babelrcFile = findBabelrc(filename, context.envName); + if (babelrcFile) { + const result = loadFileChain(babelrcFile, context); + if (!result) return null; + + mergeChain(fileChain, result); + } + + const babelignoreFile = findBabelignore(filename); + if ( + babelignoreFile && + shouldIgnore( + context, + babelignoreFile.ignore, + null, + babelignoreFile.dirname, + ) + ) { + return null; + } + } + + // Insert file chain in front so programmatic options have priority + // over configuration file chain items. + const chain = mergeChain( + mergeChain(emptyChain(), fileChain), + programmaticChain, + ); + + return { + plugins: dedupDescriptors(chain.plugins), + presets: dedupDescriptors(chain.presets), + options: chain.options.map(o => normalizeOptions(o)), + }; +} + +/** + * Build a config chain for just the programmatic options passed into Babel. + */ +const loadProgrammaticChain = makeChainWalker({ + init: arg => arg, + root: input => buildRootDescriptors(input, "base", createCachedDescriptors), + env: (input, envName) => + buildEnvDescriptors(input, "base", createCachedDescriptors, envName), +}); + +/** + * Build a config chain for a given file. + */ +const loadFileChain = makeChainWalker({ + init: input => validateFile(input), + root: file => loadFileDescriptors(file), + env: (file, envName) => loadFileEnvDescriptors(file)(envName), +}); +const validateFile = makeWeakCache((file: ConfigFile): ValidatedFile => ({ + filepath: file.filepath, + dirname: file.dirname, + options: validate("file", file.options), +})); +const loadFileDescriptors = makeWeakCache((file: ValidatedFile) => + buildRootDescriptors(file, file.filepath, createUncachedDescriptors), +); +const loadFileEnvDescriptors = makeWeakCache((file: ValidatedFile) => + makeStrongCache((envName: string) => + buildEnvDescriptors( + file, + file.filepath, + createUncachedDescriptors, + envName, + ), + ), +); + +function buildRootDescriptors({ dirname, options }, alias, descriptors) { + return descriptors(dirname, options, alias); +} + +function buildEnvDescriptors( + { dirname, options }, + alias, + descriptors, + envName, +) { + const opts = options.env && options.env[envName]; + return opts ? descriptors(dirname, opts, `${alias}.env["${envName}"]`) : null; +} + +function makeChainWalker< + ArgT, + InnerT: { options: ValidatedOptions, dirname: string }, +>({ + init, + root, + env, +}: { + init: ArgT => InnerT, + root: InnerT => OptionsAndDescriptors, + env: (InnerT, string) => OptionsAndDescriptors | null, +}): (ArgT, ConfigContext, Set | void) => ConfigChain | null { + return (arg, context, files = new Set()) => { + const input = init(arg); + + const { dirname } = input; + + const flattenedConfigs = []; + + const rootOpts = root(input); + flattenedConfigs.push(rootOpts); + + const envOpts = env(input, context.envName); + if (envOpts) { + flattenedConfigs.push(envOpts); + } + + // Process 'ignore' and 'only' before 'extends' items are processed so + // that we don't do extra work loading extended configs if a file is + // ignored. + if ( + flattenedConfigs.some(({ options: { ignore, only } }) => + shouldIgnore(context, ignore, only, dirname), + ) + ) { + return null; + } + + const chain = emptyChain(); + + for (const op of flattenedConfigs) { + if (!mergeExtendsChain(chain, op.options, dirname, context, files)) { + return null; + } + + mergeChainOpts(chain, op); + } + return chain; + }; +} + +function mergeExtendsChain( + chain: ConfigChain, + opts: ValidatedOptions, + dirname: string, + context: ConfigContext, + files: Set, +): boolean { + if (opts.extends === undefined) return true; + + const file = loadConfig(opts.extends, dirname, context.envName); + + if (files.has(file)) { + throw new Error( + `Configuration cycle detected loading ${file.filepath}.\n` + + `File already loaded following the config chain:\n` + + Array.from(files, file => ` - ${file.filepath}`).join("\n"), + ); + } + + files.add(file); + const fileChain = loadFileChain(file, context, files); + files.delete(file); + + if (!fileChain) return false; + + mergeChain(chain, fileChain); + + return true; +} + +function mergeChain(target: ConfigChain, source: ConfigChain): ConfigChain { + target.options.push(...source.options); + target.plugins.push(...source.plugins); + target.presets.push(...source.presets); + + return target; +} + +function mergeChainOpts( + target: ConfigChain, + { options, plugins, presets }: OptionsAndDescriptors, +): ConfigChain { + target.options.push(options); + target.plugins.push(...plugins()); + target.presets.push(...presets()); + + return target; +} + +function emptyChain(): ConfigChain { + return { + options: [], + presets: [], + plugins: [], + }; +} + +function normalizeOptions(opts: ValidatedOptions): ValidatedOptions { + const options = Object.assign({}, opts); + delete options.extends; + delete options.env; + delete options.plugins; + delete options.presets; + delete options.passPerPreset; + delete options.ignore; + delete options.only; + + // "sourceMap" is just aliased to sourceMap, so copy it over as + // we merge the options together. + if (options.sourceMap) { + options.sourceMaps = options.sourceMap; + delete options.sourceMap; + } + return options; +} + +function dedupDescriptors( + items: Array, +): Array { + const map: Map< + Function, + Map, + > = new Map(); + + const descriptors = []; + + for (const item of items) { + if (typeof item.value === "function") { + const fnKey = item.value; + let nameMap = map.get(fnKey); + if (!nameMap) { + nameMap = new Map(); + map.set(fnKey, nameMap); + } + let desc = nameMap.get(item.name); + if (!desc) { + desc = { value: null }; + descriptors.push(desc); + + // Treat passPerPreset presets as unique, skipping them + // in the merge processing steps. + if (!item.ownPass) nameMap.set(item.name, desc); + } + + if (item.options === false) { + desc.value = null; + } else { + desc.value = item; + } + } else { + descriptors.push({ value: item }); + } + } + + return descriptors.reduce((acc, desc) => { + if (desc.value) acc.push(desc.value); + return acc; + }, []); +} + +/** + * Tests if a filename should be ignored based on "ignore" and "only" options. + */ +function shouldIgnore( + context: ConfigContext, + ignore: ?IgnoreList, + only: ?IgnoreList, + dirname: string, +): boolean { + if (context.filename === null) return false; + // $FlowIgnore - Flow refinements aren't quite smart enough for this :( + const ctx: ConfigContextNamed = context; + + if (ignore) { + if (matchesPatterns(ctx, ignore, dirname)) { + debug( + "Ignored %o because it matched one of %O from %o", + context.filename, + ignore, + dirname, + ); + return true; + } + } + + if (only) { + if (!matchesPatterns(ctx, only, dirname)) { + debug( + "Ignored %o because it failed to match one of %O from %o", + context.filename, + only, + dirname, + ); + return true; + } + } + + return false; +} + +/** + * Returns result of calling function with filename if pattern is a function. + * Otherwise returns result of matching pattern Regex with filename. + */ +function matchesPatterns( + context: ConfigContextNamed, + patterns: IgnoreList, + dirname: string, +): boolean { + const res = []; + const strings = []; + const fns = []; + + patterns.forEach(pattern => { + if (typeof pattern === "string") strings.push(pattern); + else if (typeof pattern === "function") fns.push(pattern); + else res.push(pattern); + }); + + const filename = context.filename; + if (res.some(re => re.test(context.filename))) return true; + if (fns.some(fn => fn(filename))) return true; + + if (strings.length > 0) { + const possibleDirs = getPossibleDirs(context); + + const absolutePatterns = strings.map(pattern => { + // Preserve the "!" prefix so that micromatch can use it for negation. + const negate = pattern[0] === "!"; + if (negate) pattern = pattern.slice(1); + + return (negate ? "!" : "") + path.resolve(dirname, pattern); + }); + + if ( + micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0 + ) { + return true; + } + } + + return false; +} + +const getPossibleDirs = makeWeakCache((context: ConfigContextNamed) => { + let current = context.filename; + if (current === null) return []; + + const possibleDirs = [current]; + while (true) { + const previous = current; + current = path.dirname(current); + if (previous === current) break; + + possibleDirs.push(current); + } + + return possibleDirs; +}); diff --git a/packages/babel-core/src/config/config-descriptors.js b/packages/babel-core/src/config/config-descriptors.js new file mode 100644 index 0000000000..6b31c640d2 --- /dev/null +++ b/packages/babel-core/src/config/config-descriptors.js @@ -0,0 +1,272 @@ +// @flow + +import { loadPlugin, loadPreset } from "./files"; + +import { + makeWeakCache, + makeStrongCache, + type CacheConfigurator, +} from "./caching"; + +import type { + ValidatedOptions, + PluginList, + PluginItem, +} from "./validation/options"; + +// Represents a config object and functions to lazily load the descriptors +// for the plugins and presets so we don't load the plugins/presets unless +// the options object actually ends up being applicable. +export type OptionsAndDescriptors = { + options: ValidatedOptions, + plugins: () => Array, + presets: () => Array, +}; + +// Represents a plugin or presets at a given location in a config object. +// At this point these have been resolved to a specific object or function, +// but have not yet been executed to call functions with options. +export type UnloadedDescriptor = { + name: string | void, + value: {} | Function, + options: {} | void | false, + dirname: string, + alias: string, + ownPass?: boolean, +}; + +export type ValidatedFile = { + filepath: string, + dirname: string, + options: ValidatedOptions, +}; + +/** + * Create a set of descriptors from a given options object, preserving + * descriptor identity based on the identity of the plugin/preset arrays + * themselves. + */ +export function createCachedDescriptors( + dirname: string, + options: ValidatedOptions, + alias: string, +): OptionsAndDescriptors { + const { plugins, presets, passPerPreset } = options; + return { + options, + plugins: plugins + ? () => createCachedPluginDescriptors(plugins, dirname)(alias) + : () => [], + presets: presets + ? () => + createCachedPresetDescriptors(presets, dirname)(alias)( + !!passPerPreset, + ) + : () => [], + }; +} + +/** + * Create a set of descriptors from a given options object, with consistent + * identity for the descriptors, but not caching based on any specific identity. + */ +export function createUncachedDescriptors( + dirname: string, + options: ValidatedOptions, + alias: string, +): OptionsAndDescriptors { + // The returned result here is cached to represent a config object in + // memory, so we build and memoize the descriptors to ensure the same + // values are returned consistently. + let plugins; + let presets; + + return { + options, + plugins: () => { + if (!plugins) { + plugins = createPluginDescriptors( + options.plugins || [], + dirname, + alias, + ); + } + return plugins; + }, + presets: () => { + if (!presets) { + presets = createPresetDescriptors( + options.presets || [], + dirname, + alias, + !!options.passPerPreset, + ); + } + return presets; + }, + }; +} + +const createCachedPresetDescriptors = makeWeakCache( + (items: PluginList, cache: CacheConfigurator) => { + const dirname = cache.using(dir => dir); + return makeStrongCache((alias: string) => + makeStrongCache((passPerPreset: boolean) => + createPresetDescriptors(items, dirname, alias, passPerPreset), + ), + ); + }, +); + +const createCachedPluginDescriptors = makeWeakCache( + (items: PluginList, cache: CacheConfigurator) => { + const dirname = cache.using(dir => dir); + return makeStrongCache((alias: string) => + createPluginDescriptors(items, dirname, alias), + ); + }, +); + +function createPresetDescriptors( + items: PluginList, + dirname: string, + alias: string, + passPerPreset: boolean, +): Array { + return createDescriptors("preset", items, dirname, alias, passPerPreset); +} + +function createPluginDescriptors( + items: PluginList, + dirname: string, + alias: string, +): Array { + return createDescriptors("plugin", items, dirname, alias); +} + +function createDescriptors( + type: "plugin" | "preset", + items: PluginList, + dirname: string, + alias: string, + ownPass?: boolean, +): Array { + const descriptors = items.map((item, index) => + createDescriptor( + item, + type === "plugin" ? loadPlugin : loadPreset, + dirname, + { + index, + alias, + ownPass: !!ownPass, + }, + ), + ); + + assertNoDuplicates(descriptors); + + return descriptors; +} + +/** + * Given a plugin/preset item, resolve it into a standard format. + */ +function createDescriptor( + pair: PluginItem, + resolver, + dirname, + { + index, + alias, + ownPass, + }: { + index: number, + alias: string, + ownPass?: boolean, + }, +): UnloadedDescriptor { + let name; + let options; + let value = pair; + if (Array.isArray(value)) { + if (value.length === 3) { + // $FlowIgnore - Flow doesn't like the multiple tuple types. + [value, options, name] = value; + } else { + [value, options] = value; + } + } + + let filepath = null; + if (typeof value === "string") { + ({ filepath, value } = resolver(value, dirname)); + } + + if (!value) { + throw new Error(`Unexpected falsy value: ${String(value)}`); + } + + if (typeof value === "object" && value.__esModule) { + if (value.default) { + value = value.default; + } else { + throw new Error("Must export a default export when using ES6 modules."); + } + } + + if (typeof value !== "object" && typeof value !== "function") { + throw new Error( + `Unsupported format: ${typeof value}. Expected an object or a function.`, + ); + } + + if (filepath !== null && typeof value === "object" && value) { + // We allow object values for plugins/presets nested directly within a + // config object, because it can be useful to define them in nested + // configuration contexts. + throw new Error( + "Plugin/Preset files are not allowed to export objects, only functions.", + ); + } + + return { + name, + alias: filepath || `${alias}$${index}`, + value, + options, + dirname, + ownPass, + }; +} + +function assertNoDuplicates(items: Array): void { + const map = new Map(); + + for (const item of items) { + if (typeof item.value !== "function") continue; + + let nameMap = map.get(item.value); + if (!nameMap) { + nameMap = new Set(); + map.set(item.value, nameMap); + } + + if (nameMap.has(item.name)) { + throw new Error( + [ + `Duplicate plugin/preset detected.`, + `If you'd like to use two separate instances of a plugin,`, + `they neen separate names, e.g.`, + ``, + ` plugins: [`, + ` ['some-plugin', {}],`, + ` ['some-plugin', {}, 'some unique name'],`, + ` ]`, + ].join("\n"), + ); + } + + nameMap.add(item.name); + } +} diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index 0bdd1184bb..33ab644a8b 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -1,7 +1,29 @@ // @flow -import type Plugin from "./plugin"; -import manageOptions from "./option-manager"; +import path from "path"; +import * as context from "../index"; +import Plugin from "./plugin"; +import merge from "lodash/merge"; +import { + buildRootChain, + buildPresetChain, + type ConfigChain, + type PresetInstance, +} from "./config-chain"; +import type { UnloadedDescriptor } from "./config-descriptors"; +import traverse from "@babel/traverse"; +import clone from "lodash/clone"; +import { makeWeakCache, type CacheConfigurator } from "./caching"; +import { getEnv } from "./helpers/environment"; +import { validate } from "./validation/options"; +import { validatePluginObject } from "./validation/plugins"; + +type LoadedDescriptor = { + value: {}, + options: {}, + dirname: string, + alias: string, +}; export type { InputOptions } from "./validation/options"; @@ -14,13 +36,257 @@ export type { Plugin }; export type PluginPassList = Array; export type PluginPasses = Array; -/** - * Standard API for loading Babel configuration data. Not for public consumption. - */ -export default function loadConfig(opts: mixed): ResolvedConfig | null { - if (opts != null && (typeof opts !== "object" || Array.isArray(opts))) { +export default function loadConfig(inputOpts: mixed): ResolvedConfig | null { + if ( + inputOpts != null && + (typeof inputOpts !== "object" || Array.isArray(inputOpts)) + ) { throw new Error("Babel options must be an object, null, or undefined"); } - return manageOptions(opts || {}); + const args = inputOpts ? validate("arguments", inputOpts) : {}; + + const { envName = getEnv(), cwd = "." } = args; + const absoluteCwd = path.resolve(cwd); + + const configChain = buildRootChain(absoluteCwd, args, envName); + if (!configChain) return null; + + const optionDefaults = {}; + const options = {}; + const passes = [[]]; + try { + (function recurseDescriptors( + config: { + plugins: Array, + presets: Array, + }, + pass: Array, + envName: string, + ) { + const plugins = config.plugins.map(descriptor => + loadPluginDescriptor(descriptor, envName), + ); + const presets = config.presets.map(descriptor => { + return { + preset: loadPresetDescriptor(descriptor, envName), + pass: descriptor.ownPass ? [] : pass, + }; + }); + + // resolve presets + if (presets.length > 0) { + // The passes are created in the same order as the preset list, but are inserted before any + // existing additional passes. + passes.splice( + 1, + 0, + ...presets.map(o => o.pass).filter(p => p !== pass), + ); + + for (const { preset, pass } of presets) { + recurseDescriptors( + { + plugins: preset.plugins, + presets: preset.presets, + }, + pass, + envName, + ); + + preset.options.forEach(opts => { + merge(optionDefaults, opts); + }); + } + } + + // resolve plugins + if (plugins.length > 0) { + pass.unshift(...plugins); + } + })( + { + plugins: configChain.plugins, + presets: configChain.presets, + }, + passes[0], + envName, + ); + + configChain.options.forEach(opts => { + merge(options, opts); + }); + } catch (e) { + // There are a few case where thrown errors will try to annotate themselves multiple times, so + // to keep things simple we just bail out if re-wrapping the message. + if (!/^\[BABEL\]/.test(e.message)) { + e.message = `[BABEL] ${args.filename || "unknown"}: ${e.message}`; + } + + throw e; + } + + const opts: Object = merge(optionDefaults, options); + + // Tack the passes onto the object itself so that, if this object is passed back to Babel a second time, + // it will be in the right structure to not change behavior. + opts.babelrc = false; + opts.plugins = passes[0]; + opts.presets = passes + .slice(1) + .filter(plugins => plugins.length > 0) + .map(plugins => ({ plugins })); + opts.passPerPreset = opts.presets.length > 0; + opts.envName = envName; + opts.cwd = absoluteCwd; + + return { + options: opts, + passes: passes, + }; +} + +/** + * Load a generic plugin/preset from the given descriptor loaded from the config object. + */ +const loadDescriptor = makeWeakCache( + ( + { value, options, dirname, alias }: UnloadedDescriptor, + cache: CacheConfigurator<{ envName: string }>, + ): LoadedDescriptor => { + // Disabled presets should already have been filtered out + if (options === false) throw new Error("Assertion failure"); + + options = options || {}; + + let item = value; + if (typeof value === "function") { + const api = Object.assign(Object.create(context), { + cache: cache.simple(), + env: () => cache.using(data => data.envName), + async: () => false, + }); + + try { + item = value(api, options, dirname); + } catch (e) { + if (alias) { + e.message += ` (While processing: ${JSON.stringify(alias)})`; + } + throw e; + } + } + + if (!item || typeof item !== "object") { + throw new Error("Plugin/Preset did not return an object."); + } + + if (typeof item.then === "function") { + throw new Error( + `You appear to be using an async plugin, ` + + `which your current version of Babel does not support.` + + `If you're using a published plugin, ` + + `you may need to upgrade your @babel/core version.`, + ); + } + + return { value: item, options, dirname, alias }; + }, +); + +/** + * Instantiate a plugin for the given descriptor, returning the plugin/options pair. + */ +function loadPluginDescriptor( + descriptor: UnloadedDescriptor, + envName: string, +): Plugin { + if (descriptor.value instanceof Plugin) { + if (descriptor.options) { + throw new Error( + "Passed options to an existing Plugin instance will not work.", + ); + } + + return descriptor.value; + } + + return instantiatePlugin(loadDescriptor(descriptor, { envName }), { + envName, + }); +} + +const instantiatePlugin = makeWeakCache( + ( + { value, options, dirname, alias }: LoadedDescriptor, + cache: CacheConfigurator<{ envName: string }>, + ): Plugin => { + const pluginObj = validatePluginObject(value); + + const plugin = Object.assign({}, pluginObj); + if (plugin.visitor) { + plugin.visitor = traverse.explode(clone(plugin.visitor)); + } + + if (plugin.inherits) { + const inheritsDescriptor = { + name: undefined, + alias: `${alias}$inherits`, + value: plugin.inherits, + options, + dirname, + }; + + // If the inherited plugin changes, reinstantiate this plugin. + const inherits = cache.invalidate(data => + loadPluginDescriptor(inheritsDescriptor, data.envName), + ); + + plugin.pre = chain(inherits.pre, plugin.pre); + plugin.post = chain(inherits.post, plugin.post); + plugin.manipulateOptions = chain( + inherits.manipulateOptions, + plugin.manipulateOptions, + ); + plugin.visitor = traverse.visitors.merge([ + inherits.visitor || {}, + plugin.visitor || {}, + ]); + } + + return new Plugin(plugin, options, alias); + }, +); + +/** + * Generate a config object that will act as the root of a new nested config. + */ +const loadPresetDescriptor = ( + descriptor: UnloadedDescriptor, + envName: string, +): ConfigChain => { + return buildPresetChain( + instantiatePreset(loadDescriptor(descriptor, { envName })), + ); +}; + +const instantiatePreset = makeWeakCache( + ({ value, dirname, alias }: LoadedDescriptor): PresetInstance => { + return { + options: validate("preset", value), + alias, + dirname, + }; + }, +); + +function chain(a, b) { + const fns = [a, b].filter(Boolean); + if (fns.length <= 1) return fns[0]; + + return function(...args) { + for (const fn of fns) { + fn.apply(this, args); + } + }; } diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js deleted file mode 100644 index 37e54aa879..0000000000 --- a/packages/babel-core/src/config/option-manager.js +++ /dev/null @@ -1,278 +0,0 @@ -// @flow - -import path from "path"; -import * as context from "../index"; -import Plugin from "./plugin"; -import merge from "lodash/merge"; -import { - buildRootChain, - buildPresetChain, - type ConfigChain, - type BasicDescriptor, - type PresetInstance, -} from "./build-config-chain"; -import traverse from "@babel/traverse"; -import clone from "lodash/clone"; -import { makeWeakCache, type CacheConfigurator } from "./caching"; -import { getEnv } from "./helpers/environment"; -import { validate } from "./validation/options"; -import { validatePluginObject } from "./validation/plugins"; - -export default function manageOptions(inputOpts: {}): { - options: Object, - passes: Array>, -} | null { - const args = validate("arguments", inputOpts); - - const { envName = getEnv(), cwd = "." } = args; - const absoluteCwd = path.resolve(cwd); - - const configChain = buildRootChain(absoluteCwd, args, envName); - if (!configChain) return null; - - const optionDefaults = {}; - const options = {}; - const passes = [[]]; - try { - (function recurseDescriptors( - config: { - plugins: Array, - presets: Array, - }, - pass: Array, - envName: string, - ) { - const plugins = config.plugins.map(descriptor => - loadPluginDescriptor(descriptor, envName), - ); - const presets = config.presets.map(descriptor => { - return { - preset: loadPresetDescriptor(descriptor, envName), - pass: descriptor.ownPass ? [] : pass, - }; - }); - - // resolve presets - if (presets.length > 0) { - // The passes are created in the same order as the preset list, but are inserted before any - // existing additional passes. - passes.splice( - 1, - 0, - ...presets.map(o => o.pass).filter(p => p !== pass), - ); - - for (const { preset, pass } of presets) { - recurseDescriptors( - { - plugins: preset.plugins, - presets: preset.presets, - }, - pass, - envName, - ); - - preset.options.forEach(opts => { - merge(optionDefaults, opts); - }); - } - } - - // resolve plugins - if (plugins.length > 0) { - pass.unshift(...plugins); - } - })( - { - plugins: configChain.plugins, - presets: configChain.presets, - }, - passes[0], - envName, - ); - - configChain.options.forEach(opts => { - merge(options, opts); - }); - } catch (e) { - // There are a few case where thrown errors will try to annotate themselves multiple times, so - // to keep things simple we just bail out if re-wrapping the message. - if (!/^\[BABEL\]/.test(e.message)) { - e.message = `[BABEL] ${args.filename || "unknown"}: ${e.message}`; - } - - throw e; - } - - const opts: Object = merge(optionDefaults, options); - - // Tack the passes onto the object itself so that, if this object is passed back to Babel a second time, - // it will be in the right structure to not change behavior. - opts.babelrc = false; - opts.plugins = passes[0]; - opts.presets = passes - .slice(1) - .filter(plugins => plugins.length > 0) - .map(plugins => ({ plugins })); - opts.passPerPreset = opts.presets.length > 0; - opts.envName = envName; - opts.cwd = absoluteCwd; - - return { - options: opts, - passes: passes, - }; -} - -type LoadedDescriptor = { - value: {}, - options: {}, - dirname: string, - alias: string, -}; - -/** - * Load a generic plugin/preset from the given descriptor loaded from the config object. - */ -const loadDescriptor = makeWeakCache( - ( - { value, options, dirname, alias }: BasicDescriptor, - cache: CacheConfigurator<{ envName: string }>, - ): LoadedDescriptor => { - // Disabled presets should already have been filtered out - if (options === false) throw new Error("Assertion failure"); - - options = options || {}; - - let item = value; - if (typeof value === "function") { - const api = Object.assign(Object.create(context), { - cache: cache.simple(), - env: () => cache.using(data => data.envName), - async: () => false, - }); - - try { - item = value(api, options, dirname); - } catch (e) { - if (alias) { - e.message += ` (While processing: ${JSON.stringify(alias)})`; - } - throw e; - } - } - - if (!item || typeof item !== "object") { - throw new Error("Plugin/Preset did not return an object."); - } - - if (typeof item.then === "function") { - throw new Error( - `You appear to be using an async plugin, ` + - `which your current version of Babel does not support.` + - `If you're using a published plugin, ` + - `you may need to upgrade your @babel/core version.`, - ); - } - - return { value: item, options, dirname, alias }; - }, -); - -/** - * Instantiate a plugin for the given descriptor, returning the plugin/options pair. - */ -function loadPluginDescriptor( - descriptor: BasicDescriptor, - envName: string, -): Plugin { - if (descriptor.value instanceof Plugin) { - if (descriptor.options) { - throw new Error( - "Passed options to an existing Plugin instance will not work.", - ); - } - - return descriptor.value; - } - - return instantiatePlugin(loadDescriptor(descriptor, { envName }), { - envName, - }); -} - -const instantiatePlugin = makeWeakCache( - ( - { value, options, dirname, alias }: LoadedDescriptor, - cache: CacheConfigurator<{ envName: string }>, - ): Plugin => { - const pluginObj = validatePluginObject(value); - - const plugin = Object.assign({}, pluginObj); - if (plugin.visitor) { - plugin.visitor = traverse.explode(clone(plugin.visitor)); - } - - if (plugin.inherits) { - const inheritsDescriptor = { - name: undefined, - alias: `${alias}$inherits`, - value: plugin.inherits, - options, - dirname, - }; - - // If the inherited plugin changes, reinstantiate this plugin. - const inherits = cache.invalidate(data => - loadPluginDescriptor(inheritsDescriptor, data.envName), - ); - - plugin.pre = chain(inherits.pre, plugin.pre); - plugin.post = chain(inherits.post, plugin.post); - plugin.manipulateOptions = chain( - inherits.manipulateOptions, - plugin.manipulateOptions, - ); - plugin.visitor = traverse.visitors.merge([ - inherits.visitor || {}, - plugin.visitor || {}, - ]); - } - - return new Plugin(plugin, options, alias); - }, -); - -/** - * Generate a config object that will act as the root of a new nested config. - */ -const loadPresetDescriptor = ( - descriptor: BasicDescriptor, - envName: string, -): ConfigChain => { - return buildPresetChain( - instantiatePreset(loadDescriptor(descriptor, { envName })), - ); -}; - -const instantiatePreset = makeWeakCache( - ({ value, dirname, alias }: LoadedDescriptor): PresetInstance => { - return { - type: "preset", - options: validate("preset", value), - alias, - dirname, - }; - }, -); - -function chain(a, b) { - const fns = [a, b].filter(Boolean); - if (fns.length <= 1) return fns[0]; - - return function(...args) { - for (const fn of fns) { - fn.apply(this, args); - } - }; -} diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index f36ea98e4f..651639ac36 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -194,17 +194,50 @@ describe("buildConfigChain", function() { assert.notEqual(opts1.plugins[0], opts2.plugins[1]); }); - it("should cache the env options by identity", () => { - const env = { - foo: { - plugins: plugins1, + it("should cache the env plugins by identity", () => { + const plugins = [() => ({})]; + + const opts1 = loadOptions({ + envName: "foo", + env: { + foo: { + plugins, + }, }, - }; + }); + const opts2 = loadOptions({ + envName: "foo", + env: { + foo: { + plugins, + }, + }, + }); - const opts1 = loadOptions({ envName: "foo", env }); + assert.equal(opts1.plugins.length, 1); + assert.equal(opts2.plugins.length, 1); + assert.equal(opts1.plugins[0], opts2.plugins[0]); + }); - env.foo.plugins = plugins2; - const opts2 = loadOptions({ envName: "foo", env }); + it("should cache the env presets by identity", () => { + const presets = [() => ({ plugins: [() => ({})] })]; + + const opts1 = loadOptions({ + envName: "foo", + env: { + foo: { + presets, + }, + }, + }); + const opts2 = loadOptions({ + envName: "foo", + env: { + foo: { + presets, + }, + }, + }); assert.equal(opts1.plugins.length, 1); assert.equal(opts2.plugins.length, 1);