From 4afbc024761c364bbe0a7b6863108c9c8628165a Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Mon, 18 Dec 2017 10:54:30 -0800 Subject: [PATCH] Move descriptor merging into config chain processing. --- .../src/config/build-config-chain.js | 252 ++++++++++++++++- .../babel-core/src/config/option-manager.js | 263 ++---------------- 2 files changed, 270 insertions(+), 245 deletions(-) diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 00cdfab2d0..7a5838f91f 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -6,23 +6,53 @@ import buildDebug from "debug"; import { validate, type ValidatedOptions, + type PluginItem, type PluginList, type IgnoreList, } from "./options"; const debug = buildDebug("babel:config:config-chain"); -import { findConfigs, loadConfig, type ConfigFile } from "./loading/files"; +import { + loadPlugin, + loadPreset, + findConfigs, + loadConfig, + type ConfigFile, +} from "./loading/files"; import { makeWeakCache, makeStrongCache } from "./caching"; -export type ConfigItem = { +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", @@ -38,11 +68,29 @@ type ConfigPart = activeEnv: string | null, }; -export default function buildConfigChain( +type SimpleConfig = { + options: ValidatedOptions, + alias: string, + dirname: 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, -): Array | null { +): ConfigChain | null { const filename = opts.filename ? path.resolve(cwd, opts.filename) : null; const builder = new ConfigChainBuilder( filename ? new LoadedFile(filename) : null, @@ -63,7 +111,9 @@ export default function buildConfigChain( return null; } - return builder.configs.reverse(); + return dedupLoadedConfigs( + builder.configs.reverse().map(config => processConfig(config)), + ); } class ConfigChainBuilder { @@ -126,6 +176,198 @@ class ConfigChainBuilder { } } +/** + * 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, + }; +} + +/** + * Load and validate the given config into a set of options, plugins, and presets. + */ +const processConfig = makeWeakCache((config: SimpleConfig): LoadedConfig => { + const options = config.options; + + 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: options.passPerPreset, + }), + ); + + assertNoDuplicates(presets); + + return { 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 diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index d8010f7d56..10abc8c5ae 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -4,23 +4,18 @@ import path from "path"; import * as context from "../index"; import Plugin, { validatePluginObject } from "./plugin"; import merge from "lodash/merge"; -import buildConfigChain, { type ConfigItem } from "./build-config-chain"; +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, type ValidatedOptions, type PluginItem } from "./options"; - -import { loadPlugin, loadPreset } from "./loading/files"; - -type MergeOptions = - | ConfigItem - | { - type: "preset", - options: ValidatedOptions, - alias: string, - dirname: string, - }; +import { validate, type ValidatedOptions } from "./options"; export default function manageOptions(opts: {}): { options: Object, @@ -71,18 +66,19 @@ class OptionManager { ); presets.forEach(({ preset, pass }) => { - const loadedConfig = loadConfig(preset); this.mergeOptions( { // Call dedupDescriptors() to remove 'false' descriptors. - plugins: dedupDescriptors(loadedConfig.plugins), - presets: dedupDescriptors(loadedConfig.presets), + plugins: preset.plugins, + presets: preset.presets, }, pass, envName, ); - merge(this.optionDefaults, normalizeOptions(loadedConfig.options)); + preset.options.forEach(opts => { + merge(this.optionDefaults, normalizeOptions(opts)); + }); }); } @@ -92,19 +88,17 @@ class OptionManager { } } - mergeConfigChain(chain: $ReadOnlyArray, envName: string) { - const config = dedupLoadedConfigs(chain.map(config => loadConfig(config))); - + mergeConfigChain(chain: ConfigChain, envName: string) { this.mergeOptions( { - plugins: config.plugins, - presets: config.presets, + plugins: chain.plugins, + presets: chain.presets, }, this.passes[0], envName, ); - config.options.forEach(opts => { + chain.options.forEach(opts => { merge(this.options, normalizeOptions(opts)); }); } @@ -115,7 +109,7 @@ class OptionManager { const { envName = getEnv(), cwd = "." } = args; const absoluteCwd = path.resolve(cwd); - const configChain = buildConfigChain(absoluteCwd, args, envName); + const configChain = buildRootChain(absoluteCwd, args, envName); if (!configChain) return null; try { @@ -170,15 +164,6 @@ function normalizeOptions(opts: ValidatedOptions): ValidatedOptions { return options; } -type BasicDescriptor = { - name: string | void, - value: {} | Function, - options: {} | void | false, - dirname: string, - alias: string, - ownPass?: boolean, -}; - type LoadedDescriptor = { value: {}, options: {}, @@ -186,139 +171,6 @@ type LoadedDescriptor = { alias: string, }; -type LoadedConfig = { - options: ValidatedOptions, - plugins: Array, - presets: Array, -}; - -/** - * Load and validate the given config into a set of options, plugins, and presets. - */ -const loadConfig = makeWeakCache((config: MergeOptions): LoadedConfig => { - const options = config.options; - - 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: options.passPerPreset, - }), - ); - - assertNoDuplicates(presets); - - return { 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, -): { - plugins: Array, - presets: Array, - options: Array, -} { - 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; - }, []); -} - /** * Load a generic plugin/preset from the given descriptor loaded from the config object. */ @@ -437,12 +289,14 @@ const instantiatePlugin = makeWeakCache( const loadPresetDescriptor = ( descriptor: BasicDescriptor, envName: string, -): MergeOptions => { - return instantiatePreset(loadDescriptor(descriptor, { envName })); +): ConfigChain => { + return buildPresetChain( + instantiatePreset(loadDescriptor(descriptor, { envName })), + ); }; const instantiatePreset = makeWeakCache( - ({ value, dirname, alias }: LoadedDescriptor): MergeOptions => { + ({ value, dirname, alias }: LoadedDescriptor): PresetInstance => { return { type: "preset", options: validate("preset", value), @@ -452,77 +306,6 @@ const instantiatePreset = makeWeakCache( }, ); -/** - * 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 chain(a, b) { const fns = [a, b].filter(Boolean); if (fns.length <= 1) return fns[0];