babel/packages/babel-core/src/config/option-manager.js
2017-11-24 19:45:15 -08:00

385 lines
9.7 KiB
JavaScript

// @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<Array<Plugin>>,
} | null {
return new OptionManager().init(opts);
}
class OptionManager {
constructor() {
this.options = {};
this.passes = [[]];
}
options: ValidatedOptions;
passes: Array<Array<Plugin>>;
/**
* 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<Plugin>, 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<BasicDescriptor>,
presets: Array<BasicDescriptor>,
} => {
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);
}
};
}