// @flow 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 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, }; export default function manageOptions(opts: {}): { options: Object, passes: Array>, } | null { return new OptionManager().init(opts); } class OptionManager { constructor() { this.options = {}; this.passes = [[]]; } options: ValidatedOptions; passes: Array>; /** * This is called when we want to merge the input `opts` into the * base options. * * - `alias` is used to output pretty traces back to the original source. * - `loc` is used to point to the original config. * - `dirname` is used to resolve plugins relative to it. */ mergeOptions(config: MergeOptions, pass: Array, envName: string) { const result = loadConfig(config); const plugins = result.plugins.map(descriptor => loadPluginDescriptor(descriptor, envName), ); const presets = result.presets.map(descriptor => ({ pass: descriptor.ownPass ? [] : pass, preset: loadPresetDescriptor(descriptor, envName), })); // 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. this.passes.splice( 1, 0, ...presets.map(o => o.pass).filter(p => p !== pass), ); presets.forEach(({ preset, pass }) => { this.mergeOptions(preset, pass, envName); }); } // resolve plugins if (plugins.length > 0) { pass.unshift(...plugins); } const options = Object.assign({}, result.options); delete options.extends; delete options.env; delete options.plugins; delete options.presets; delete options.passPerPreset; // "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; } merge(this.options, options); } init(inputOpts: {}) { const args = validate("arguments", inputOpts); const { envName = getEnv(), cwd = "." } = args; const absoluteCwd = path.resolve(cwd); const configChain = buildConfigChain(absoluteCwd, args, envName); if (!configChain) return null; try { for (const config of configChain) { this.mergeOptions(config, this.passes[0], envName); } } 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 = this.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 = this.passes[0]; opts.presets = this.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: this.passes, }; } } type BasicDescriptor = { value: {} | Function, options: {} | void, dirname: string, alias: string, ownPass?: boolean, }; type LoadedDescriptor = { value: {}, options: {}, dirname: string, alias: string, }; /** * Load and validate the given config into a set of options, plugins, and presets. */ const loadConfig = makeWeakCache((config: MergeOptions): { options: {}, plugins: Array, presets: Array, } => { const options = config.options; const plugins = (config.options.plugins || []).map((plugin, index) => createDescriptor(plugin, loadPlugin, config.dirname, { index, alias: config.alias, }), ); const presets = (config.options.presets || []).map((preset, index) => createDescriptor(preset, loadPreset, config.dirname, { index, alias: config.alias, ownPass: config.options.passPerPreset, }), ); return { options, plugins, presets }; }); /** * 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 => { 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 = { 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, ): MergeOptions => { return instantiatePreset(loadDescriptor(descriptor, { envName })); }; const instantiatePreset = makeWeakCache( ({ value, dirname, alias }: LoadedDescriptor): MergeOptions => { return { type: "preset", options: validate("preset", value), alias, dirname, }; }, ); /** * 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 options; let value = pair; if (Array.isArray(value)) { [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 { 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]; return function(...args) { for (const fn of fns) { fn.apply(this, args); } }; }