From fef5c7e52315e8858ab4d1e7beea709a593df603 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Fri, 2 Mar 2018 16:46:04 -0800 Subject: [PATCH] Expose the partial Babel config for people to load and mutate. --- packages/babel-core/README.md | 72 ++++ .../babel-core/src/config/config-chain.js | 27 +- .../src/config/config-descriptors.js | 52 ++- packages/babel-core/src/config/full.js | 300 ++++++++++++++++ packages/babel-core/src/config/index.js | 337 +----------------- packages/babel-core/src/config/item.js | 120 +++++++ packages/babel-core/src/config/partial.js | 118 ++++++ packages/babel-core/src/config/util.js | 30 ++ .../src/config/validation/options.js | 6 +- packages/babel-core/src/index.js | 15 +- 10 files changed, 722 insertions(+), 355 deletions(-) create mode 100644 packages/babel-core/src/config/full.js create mode 100644 packages/babel-core/src/config/item.js create mode 100644 packages/babel-core/src/config/partial.js create mode 100644 packages/babel-core/src/config/util.js diff --git a/packages/babel-core/README.md b/packages/babel-core/README.md index 24262bec47..810f84238e 100644 --- a/packages/babel-core/README.md +++ b/packages/babel-core/README.md @@ -123,6 +123,78 @@ const parsedAst = babylon.parse(sourceCode, { allowReturnOutsideFunction: true } const { code, map, ast } = babel.transformFromAstSync(parsedAst, sourceCode, options); ``` +## babel.parse(code: string, [options?](#options): Object) + +Given some code, parse it using Babel's standard behavior. Referenced presets and +plugins will be loaded such that optional syntax plugins are automatically +enabled. + + +## Advanced APIs + +Many systems that wrap Babel like to automatically inject plugins and presets, +or override options. To accomplish this goal, Babel exposes several functions +that aid in loading the configuration part-way without transforming. + +### babel.loadOptions([options?](#options): Object) + +Resolve Babel's options fully, resulting in an options object where: + +* `opts.plugins` is a full list of `Plugin` instances. +* `opts.presets` is empty and all presets are flattened into `opts`. +* It can be safely passed back to Babel. Fields like `babelrc` have been set to + false so that later calls to Babel will not make a second attempt to load + config files. + +`Plugin` instances aren't meant to be manipulated directly, but often +callers will serialize this `opts` to JSON to use it as a cache key representing +the options Babel has received. Caching on this isn't 100% guaranteed to +invalidate properly, but it is the best we have at the moment. + + +### babel.loadPartialConfig([options?](#options): Object): PartialConfig + +To allow systems to easily manipulate and validate a user's config, this function +resolves the plugins and presets and proceeds no further. The expectation is +that callers will take the config's `.options`, manipulate it as then see fit +and pass it back to Babel again. + +* `babelrc: string | void` - The path of the `.babelrc` file, if there was one. +* `babelignore: string | void` - The path of the `.babelignore` file, if there was one. +* `options: ValidatedOptions` - The partially resolved options, which can be manipulated and passed back to Babel again. + * `plugins: Array` - See below. + * `presets: Array` - See below. + * It can be safely passed back to Babel. Fields like `babelrc` have been set + to false so that later calls to Babel will not make a second attempt to + load config files. + +[`ConfigItem`](#configitem-type) instances expose properties to introspect the values, but each +item should be treated as immutable. If changes are desired, the item should be +removed from the list and replaced with either a normal Babel config value, or +with a replacement item created by `babel.createConfigItem`. See that +function for information about `ConfigItem` fields. + + +### babel.createConfigItem(value: string | {} | Function, options?: {}, { dirname?: string, name?: string, type?: "preset" | "plugin" }): ConfigItem + +Allows build tooling to create and cache config items up front. If this function +is called multiple times for a given plugin, Babel will call the plugin's function itself +multiple times. If you have a clear set of expected plugins and presets to +inject, pre-constructing the config items would be recommended. + + +### `ConfigItem` type + +Each `ConfigItem` exposes all of the information Babel knows. The fields are: + +* `value: {} | Function` - The resolved value of the plugin. +* `options: {} | void` - The options object passed to the plugin. +* `dirname: string` - The path that the options are relative to. +* `name: string | void` - The name that the user gave the plugin instance, e.g. `plugins: [ ['env', {}, 'my-env'] ]` +* `file: Object | void` - Information about the plugin's file, if Babel knows it. + * `request: string` - The file that the user requested, e.g. `"@babel/env"` + * `resolved: string` - The full path of the resolved file, e.g. `"/tmp/node_modules/@babel/preset-env/lib/index.js"` + ## Options diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js index 81adcdff79..cf0e3eaf01 100644 --- a/packages/babel-core/src/config/config-chain.js +++ b/packages/babel-core/src/config/config-chain.js @@ -12,7 +12,12 @@ import { const debug = buildDebug("babel:config:config-chain"); -import { findRelativeConfig, loadConfig, type ConfigFile } from "./files"; +import { + findRelativeConfig, + loadConfig, + type ConfigFile, + type IgnoreFile, +} from "./files"; import { makeWeakCache, makeStrongCache } from "./caching"; @@ -99,13 +104,18 @@ const loadPresetOverridesEnvDescriptors = makeWeakCache( ), ); +export type RootConfigChain = ConfigChain & { + babelrc: ConfigFile | void, + ignore: IgnoreFile | void, +}; + /** * Build a config chain for Babel's full root configuration. */ export function buildRootChain( opts: ValidatedOptions, context: ConfigContext, -): ConfigChain | null { +): RootConfigChain | null { const programmaticChain = loadProgrammaticChain( { options: opts, @@ -115,19 +125,24 @@ export function buildRootChain( ); if (!programmaticChain) return null; + let ignore, babelrc; + const fileChain = emptyChain(); // resolve all .babelrc files if (opts.babelrc !== false && context.filename !== null) { const filename = context.filename; - const { ignore, config } = findRelativeConfig(filename, context.envName); + ({ ignore, config: babelrc } = findRelativeConfig( + filename, + context.envName, + )); if (ignore && shouldIgnore(context, ignore.ignore, null, ignore.dirname)) { return null; } - if (config) { - const result = loadFileChain(config, context); + if (babelrc) { + const result = loadFileChain(babelrc, context); if (!result) return null; mergeChain(fileChain, result); @@ -145,6 +160,8 @@ export function buildRootChain( plugins: dedupDescriptors(chain.plugins), presets: dedupDescriptors(chain.presets), options: chain.options.map(o => normalizeOptions(o)), + ignore: ignore || undefined, + babelrc: babelrc || undefined, }; } diff --git a/packages/babel-core/src/config/config-descriptors.js b/packages/babel-core/src/config/config-descriptors.js index a7ea427179..d5bf311149 100644 --- a/packages/babel-core/src/config/config-descriptors.js +++ b/packages/babel-core/src/config/config-descriptors.js @@ -2,6 +2,8 @@ import { loadPlugin, loadPreset } from "./files"; +import { getItemDescriptor } from "./item"; + import { makeWeakCache, makeStrongCache, @@ -33,6 +35,10 @@ export type UnloadedDescriptor = { dirname: string, alias: string, ownPass?: boolean, + file?: { + request: string, + resolved: string, + } | void, }; export type ValidatedFile = { @@ -152,16 +158,11 @@ function createDescriptors( ownPass?: boolean, ): Array { const descriptors = items.map((item, index) => - createDescriptor( - item, - type === "plugin" ? loadPlugin : loadPreset, - dirname, - { - index, - alias, - ownPass: !!ownPass, - }, - ), + createDescriptor(item, dirname, { + type, + alias: `${alias}$${index}`, + ownPass: !!ownPass, + }), ); assertNoDuplicates(descriptors); @@ -172,20 +173,24 @@ function createDescriptors( /** * Given a plugin/preset item, resolve it into a standard format. */ -function createDescriptor( +export function createDescriptor( pair: PluginItem, - resolver, - dirname, + dirname: string, { - index, + type, alias, ownPass, }: { - index: number, + type?: "plugin" | "preset", alias: string, ownPass?: boolean, }, ): UnloadedDescriptor { + const desc = getItemDescriptor(pair); + if (desc) { + return desc; + } + let name; let options; let value = pair; @@ -198,9 +203,23 @@ function createDescriptor( } } + let file = undefined; let filepath = null; if (typeof value === "string") { + if (typeof type !== "string") { + throw new Error( + "To resolve a string-based item, the type of item must be given", + ); + } + const resolver = type === "plugin" ? loadPlugin : loadPreset; + const request = value; + ({ filepath, value } = resolver(value, dirname)); + + file = { + request, + resolved: filepath, + }; } if (!value) { @@ -232,11 +251,12 @@ function createDescriptor( return { name, - alias: filepath || `${alias}$${index}`, + alias: filepath || alias, value, options, dirname, ownPass, + file, }; } diff --git a/packages/babel-core/src/config/full.js b/packages/babel-core/src/config/full.js new file mode 100644 index 0000000000..a65a2e9ff0 --- /dev/null +++ b/packages/babel-core/src/config/full.js @@ -0,0 +1,300 @@ +// @flow + +import { mergeOptions } from "./util"; +import * as context from "../index"; +import Plugin from "./plugin"; +import { getItemDescriptor } from "./item"; +import { + buildPresetChain, + type ConfigContext, + type ConfigChain, + type PresetInstance, +} from "./config-chain"; +import type { UnloadedDescriptor } from "./config-descriptors"; +import traverse from "@babel/traverse"; +import { makeWeakCache, type CacheConfigurator } from "./caching"; +import { validate } from "./validation/options"; +import { validatePluginObject } from "./validation/plugins"; +import makeAPI from "./helpers/config-api"; + +import loadPrivatePartialConfig from "./partial"; + +type LoadedDescriptor = { + value: {}, + options: {}, + dirname: string, + alias: string, +}; + +export type { InputOptions } from "./validation/options"; + +export type ResolvedConfig = { + options: Object, + passes: PluginPasses, +}; + +export type { Plugin }; +export type PluginPassList = Array; +export type PluginPasses = Array; + +// Context not including filename since it is used in places that cannot +// process 'ignore'/'only' and other filename-based logic. +type SimpleContext = { + envName: string, +}; + +export default function loadFullConfig( + inputOpts: mixed, +): ResolvedConfig | null { + const result = loadPrivatePartialConfig(inputOpts); + if (!result) { + return null; + } + const { options, context } = result; + + const optionDefaults = {}; + const passes = [[]]; + try { + const { plugins, presets } = options; + + if (!plugins || !presets) { + throw new Error("Assertion failure - plugins and presets exist"); + } + + const ignored = (function recurseDescriptors( + config: { + plugins: Array, + presets: Array, + }, + pass: Array, + ) { + const plugins = config.plugins.map(descriptor => { + return loadPluginDescriptor(descriptor, context); + }); + const presets = config.presets.map(descriptor => { + return { + preset: loadPresetDescriptor(descriptor, context), + 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) { + if (!preset) return true; + + const ignored = recurseDescriptors( + { + plugins: preset.plugins, + presets: preset.presets, + }, + pass, + ); + if (ignored) return true; + + preset.options.forEach(opts => { + mergeOptions(optionDefaults, opts); + }); + } + } + + // resolve plugins + if (plugins.length > 0) { + pass.unshift(...plugins); + } + })( + { + plugins: plugins.map(item => { + const desc = getItemDescriptor(item); + if (!desc) { + throw new Error("Assertion failure - must be config item"); + } + + return desc; + }), + presets: presets.map(item => { + const desc = getItemDescriptor(item); + if (!desc) { + throw new Error("Assertion failure - must be config item"); + } + + return desc; + }), + }, + passes[0], + ); + + if (ignored) return null; + } 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] ${context.filename || "unknown"}: ${e.message}`; + } + + throw e; + } + + const opts: Object = optionDefaults; + mergeOptions(opts, options); + + opts.plugins = passes[0]; + opts.presets = passes + .slice(1) + .filter(plugins => plugins.length > 0) + .map(plugins => ({ plugins })); + opts.passPerPreset = opts.presets.length > 0; + + 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, + ): 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), makeAPI(cache)); + + 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, + context: SimpleContext, +): 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, context), context); +} + +const instantiatePlugin = makeWeakCache( + ( + { value, options, dirname, alias }: LoadedDescriptor, + cache: CacheConfigurator, + ): Plugin => { + const pluginObj = validatePluginObject(value); + + const plugin = Object.assign({}, pluginObj); + if (plugin.visitor) { + plugin.visitor = traverse.explode(Object.assign({}, 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), + ); + + 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, + context: ConfigContext, +): ConfigChain | null => { + return buildPresetChain( + instantiatePreset(loadDescriptor(descriptor, context)), + context, + ); +}; + +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/index.js b/packages/babel-core/src/config/index.js index 5ee1c04818..734855c7bb 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -1,329 +1,26 @@ // @flow -import path from "path"; -import * as context from "../index"; -import Plugin from "./plugin"; -import { - buildRootChain, - buildPresetChain, - type ConfigContext, - type ConfigChain, - type PresetInstance, -} from "./config-chain"; -import type { UnloadedDescriptor } from "./config-descriptors"; -import traverse from "@babel/traverse"; -import { makeWeakCache, type CacheConfigurator } from "./caching"; -import { getEnv } from "./helpers/environment"; -import { validate, type ValidatedOptions } from "./validation/options"; -import { validatePluginObject } from "./validation/plugins"; -import makeAPI from "./helpers/config-api"; +import loadFullConfig from "./full"; +export type { + ResolvedConfig, + InputOptions, + PluginPasses, + Plugin, +} from "./full"; -type LoadedDescriptor = { - value: {}, - options: {}, - dirname: string, - alias: string, -}; +export { loadFullConfig as default }; +export { loadPartialConfig } from "./partial"; +export type { PartialConfig } from "./partial"; -export type { InputOptions } from "./validation/options"; +export function loadOptions(opts: {}): Object | null { + const config = loadFullConfig(opts); -export type ResolvedConfig = { - options: Object, - passes: PluginPasses, -}; - -export type { Plugin }; -export type PluginPassList = Array; -export type PluginPasses = Array; - -// Context not including filename since it is used in places that cannot -// process 'ignore'/'only' and other filename-based logic. -type SimpleContext = { - envName: string, -}; - -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"); - } - - const args = inputOpts ? validate("arguments", inputOpts) : {}; - - const { envName = getEnv(), cwd = "." } = args; - const absoluteCwd = path.resolve(cwd); - - const context: ConfigContext = { - filename: args.filename ? path.resolve(cwd, args.filename) : null, - cwd: absoluteCwd, - envName, - }; - - const configChain = buildRootChain(args, context); - if (!configChain) return null; - - const optionDefaults = {}; - const options = {}; - const passes = [[]]; - try { - const ignored = (function recurseDescriptors( - config: { - plugins: Array, - presets: Array, - }, - pass: Array, - ) { - const plugins = config.plugins.map(descriptor => - loadPluginDescriptor(descriptor, context), - ); - const presets = config.presets.map(descriptor => { - return { - preset: loadPresetDescriptor(descriptor, context), - 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) { - if (!preset) return true; - - const ignored = recurseDescriptors( - { - plugins: preset.plugins, - presets: preset.presets, - }, - pass, - ); - if (ignored) return true; - - preset.options.forEach(opts => { - mergeOptions(optionDefaults, opts); - }); - } - } - - // resolve plugins - if (plugins.length > 0) { - pass.unshift(...plugins); - } - })( - { - plugins: configChain.plugins, - presets: configChain.presets, - }, - passes[0], - ); - - if (ignored) return null; - - configChain.options.forEach(opts => { - mergeOptions(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 = optionDefaults; - mergeOptions(opts, 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, - }; + return config ? config.options : null; } -function mergeOptions( - target: ValidatedOptions, - source: ValidatedOptions, -): void { - for (const k of Object.keys(source)) { - if (k === "parserOpts" && source.parserOpts) { - const parserOpts = source.parserOpts; - const targetObj = (target.parserOpts = target.parserOpts || {}); - mergeDefaultFields(targetObj, parserOpts); - } else if (k === "generatorOpts" && source.generatorOpts) { - const generatorOpts = source.generatorOpts; - const targetObj = (target.generatorOpts = target.generatorOpts || {}); - mergeDefaultFields(targetObj, generatorOpts); - } else { - const val = source[k]; - if (val !== undefined) target[k] = (val: any); - } +// For easier backward-compatibility, provide an API like the one we exposed in Babel 6. +export class OptionManager { + init(opts: {}) { + return loadOptions(opts); } } - -function mergeDefaultFields(target: T, source: T) { - for (const k of Object.keys(source)) { - const val = source[k]; - if (val !== undefined) target[k] = (val: any); - } -} - -/** - * Load a generic plugin/preset from the given descriptor loaded from the config object. - */ -const loadDescriptor = makeWeakCache( - ( - { value, options, dirname, alias }: UnloadedDescriptor, - cache: CacheConfigurator, - ): 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), makeAPI(cache)); - - 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, - context: SimpleContext, -): 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, context), context); -} - -const instantiatePlugin = makeWeakCache( - ( - { value, options, dirname, alias }: LoadedDescriptor, - cache: CacheConfigurator, - ): Plugin => { - const pluginObj = validatePluginObject(value); - - const plugin = Object.assign({}, pluginObj); - if (plugin.visitor) { - plugin.visitor = traverse.explode(Object.assign({}, 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), - ); - - 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, - context: ConfigContext, -): ConfigChain | null => { - return buildPresetChain( - instantiatePreset(loadDescriptor(descriptor, context)), - context, - ); -}; - -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/item.js b/packages/babel-core/src/config/item.js new file mode 100644 index 0000000000..78fcc50926 --- /dev/null +++ b/packages/babel-core/src/config/item.js @@ -0,0 +1,120 @@ +// @flow + +import path from "path"; +import { + createDescriptor, + type UnloadedDescriptor, +} from "./config-descriptors"; + +export function createItemFromDescriptor(desc: UnloadedDescriptor): ConfigItem { + return new ConfigItem(desc); +} + +export function createConfigItem( + value: string | {} | Function, + options?: {} | void, + { + dirname = ".", + name, + type, + }: { + dirname?: string, + name?: string, + type?: "preset" | "plugin", + } = {}, +): ConfigItem { + const descriptor = createDescriptor( + [value, options, name], + path.resolve(dirname), + { + type, + alias: "programmatic item", + }, + ); + + return createItemFromDescriptor(descriptor); +} + +export function getItemDescriptor(item: mixed): UnloadedDescriptor | void { + if (item instanceof ConfigItem) { + return item._descriptor; + } + + return undefined; +} + +/** + * A public representation of a plugin/preset that will _eventually_ be load. + * Users can use this to interact with the results of a loaded Babel + * configuration. + */ +export class ConfigItem { + _descriptor: UnloadedDescriptor; + + constructor(descriptor: UnloadedDescriptor) { + this._descriptor = descriptor; + + // Make people less likely to stumble onto this if they are exploring + // programmatically. + enumerable(this, "_descriptor", false); + } + + /** + * The resolved value of the item itself. + */ + get value(): {} | Function { + return this._descriptor.value; + } + + /** + * The options, if any, that were passed to the item. + * Mutating this will lead to undefined behavior. If you need + */ + get options(): {} | void { + const options = this._descriptor.options; + if (options === false) { + throw new Error("Assertion failure - unexpected false options"); + } + + return options; + } + + /** + * The directory that the options for this item are relative to. + */ + get dirname(): string { + return this._descriptor.dirname; + } + + /** + * Get the name of the plugin, if the user gave it one. + */ + get name(): string | void { + return this._descriptor.name; + } + + get file(): { + request: string, + resolved: string, + } | void { + const file = this._descriptor.file; + if (!file) return undefined; + + return { + request: file.request, + resolved: file.resolved, + }; + } +} + +// Make these slightly easier for people to find if they are exploring the +// API programmatically. +enumerable(ConfigItem.prototype, "value", true); +enumerable(ConfigItem.prototype, "options", true); +enumerable(ConfigItem.prototype, "dirname", true); +enumerable(ConfigItem.prototype, "name", true); +enumerable(ConfigItem.prototype, "file", true); + +function enumerable(obj: {}, prop: string, enumerable: boolean) { + Object.defineProperty(obj, prop, { enumerable }); +} diff --git a/packages/babel-core/src/config/partial.js b/packages/babel-core/src/config/partial.js new file mode 100644 index 0000000000..877e924075 --- /dev/null +++ b/packages/babel-core/src/config/partial.js @@ -0,0 +1,118 @@ +// @flow + +import path from "path"; +import Plugin from "./plugin"; +import { mergeOptions } from "./util"; +import { createItemFromDescriptor } from "./item"; +import { buildRootChain, type ConfigContext } from "./config-chain"; +import { getEnv } from "./helpers/environment"; +import { validate, type ValidatedOptions } from "./validation/options"; + +import type { ConfigFile, IgnoreFile } from "./files"; + +export default function loadPrivatePartialConfig( + inputOpts: mixed, +): { + options: ValidatedOptions, + context: ConfigContext, + ignore: IgnoreFile | void, + babelrc: ConfigFile | void, +} | null { + if ( + inputOpts != null && + (typeof inputOpts !== "object" || Array.isArray(inputOpts)) + ) { + throw new Error("Babel options must be an object, null, or undefined"); + } + + const args = inputOpts ? validate("arguments", inputOpts) : {}; + + const { envName = getEnv(), cwd = "." } = args; + const absoluteCwd = path.resolve(cwd); + + const context: ConfigContext = { + filename: args.filename ? path.resolve(cwd, args.filename) : null, + cwd: absoluteCwd, + envName, + }; + + const configChain = buildRootChain(args, context); + if (!configChain) return null; + + const options = {}; + configChain.options.forEach(opts => { + mergeOptions(options, opts); + }); + + // 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. + options.babelrc = false; + options.envName = envName; + options.cwd = absoluteCwd; + options.passPerPreset = false; + + options.plugins = configChain.plugins.map(descriptor => + createItemFromDescriptor(descriptor), + ); + options.presets = configChain.presets.map(descriptor => + createItemFromDescriptor(descriptor), + ); + + return { + options, + context, + ignore: configChain.ignore, + babelrc: configChain.babelrc, + }; +} + +export function loadPartialConfig(inputOpts: mixed): PartialConfig | null { + const result = loadPrivatePartialConfig(inputOpts); + if (!result) return null; + + const { options, babelrc, ignore } = result; + + (options.plugins || []).forEach(item => { + if (item.value instanceof Plugin) { + throw new Error( + "Passing cached plugin instances is not supported in " + + "babel.loadPartialConfig()", + ); + } + }); + + return new PartialConfig( + options, + babelrc ? babelrc.filepath : undefined, + ignore ? ignore.filepath : undefined, + ); +} + +export type { PartialConfig }; + +class PartialConfig { + _options: ValidatedOptions; + _babelrc: string | void; + _babelignore: string | void; + + constructor( + options: ValidatedOptions, + babelrc: string | void, + ignore: string | void, + ) { + this._options = options; + this._babelignore = ignore; + this._babelrc = babelrc; + } + + get babelignore(): string | void { + return this._babelignore; + } + get babelrc(): string | void { + return this._babelrc; + } + get options(): ValidatedOptions { + return this._options; + } +} diff --git a/packages/babel-core/src/config/util.js b/packages/babel-core/src/config/util.js new file mode 100644 index 0000000000..f64975216d --- /dev/null +++ b/packages/babel-core/src/config/util.js @@ -0,0 +1,30 @@ +// @flow + +import type { ValidatedOptions } from "./validation/options"; + +export function mergeOptions( + target: ValidatedOptions, + source: ValidatedOptions, +): void { + for (const k of Object.keys(source)) { + if (k === "parserOpts" && source.parserOpts) { + const parserOpts = source.parserOpts; + const targetObj = (target.parserOpts = target.parserOpts || {}); + mergeDefaultFields(targetObj, parserOpts); + } else if (k === "generatorOpts" && source.generatorOpts) { + const generatorOpts = source.generatorOpts; + const targetObj = (target.generatorOpts = target.generatorOpts || {}); + mergeDefaultFields(targetObj, generatorOpts); + } else { + const val = source[k]; + if (val !== undefined) target[k] = (val: any); + } + } +} + +function mergeDefaultFields(target: T, source: T) { + for (const k of Object.keys(source)) { + const val = source[k]; + if (val !== undefined) target[k] = (val: any); + } +} diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index 96b51f73ef..bd7a4ffe66 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -1,5 +1,8 @@ // @flow +import { ConfigItem } from "../item"; +import Plugin from "../plugin"; + import removed from "./removed"; import { assertString, @@ -214,10 +217,11 @@ export type IgnoreList = $ReadOnlyArray; export type PluginOptions = {} | void | false; export type PluginTarget = string | {} | Function; export type PluginItem = + | ConfigItem | Plugin | PluginTarget | [PluginTarget, PluginOptions] - | [PluginTarget, PluginOptions, string]; + | [PluginTarget, PluginOptions, string | void]; export type PluginList = $ReadOnlyArray; export type OverridesList = Array; diff --git a/packages/babel-core/src/index.js b/packages/babel-core/src/index.js index 7b30f15dbb..d6a3e80cac 100644 --- a/packages/babel-core/src/index.js +++ b/packages/babel-core/src/index.js @@ -13,20 +13,9 @@ export * as types from "@babel/types"; export { default as traverse } from "@babel/traverse"; export { default as template } from "@babel/template"; -import loadConfig from "./config"; +export { loadPartialConfig, loadOptions, OptionManager } from "./config"; -export function loadOptions(opts: {}): Object | null { - const config = loadConfig(opts); - - return config ? config.options : null; -} - -// For easier backward-compatibility, provide an API like the one we exposed in Babel 6. -export class OptionManager { - init(opts: {}) { - return loadOptions(opts); - } -} +export { createConfigItem } from "./config/item"; export function Plugin(alias: string) { throw new Error(