From ded4ca48cd4f93f079a7edaf0045074a0e297467 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Mon, 3 Apr 2017 14:38:50 -0700 Subject: [PATCH 01/11] Move .inherits handling to OptionManager. --- .../babel-core/src/config/option-manager.js | 64 +++++++++++--- packages/babel-core/src/config/plugin.js | 86 ++----------------- 2 files changed, 60 insertions(+), 90 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 0f9b58281213..a004a1a308d7 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -7,6 +7,8 @@ import merge from "./helpers/merge"; import removed from "./removed"; import buildConfigChain from "./build-config-chain"; import path from "path"; +import traverse from "babel-traverse"; +import clone from "lodash/clone"; import { loadPlugin, loadPreset, loadParser, loadGenerator } from "./loading/files"; @@ -74,6 +76,15 @@ const optionNames = new Set([ "generatorOpts", ]); +const ALLOWED_PLUGIN_KEYS = new Set([ + "name", + "manipulateOptions", + "pre", + "post", + "visitor", + "inherits", +]); + export default class OptionManager { constructor() { this.options = OptionManager.createBareOptions(); @@ -99,16 +110,51 @@ export default class OptionManager { obj = fn; } - if (typeof obj === "object") { - const plugin = new Plugin(obj, alias); - OptionManager.memoisedPlugins.push({ - container: fn, - plugin: plugin, - }); - return plugin; - } else { + if (typeof obj !== "object") { throw new TypeError(messages.get("pluginNotObject", loc, i, typeof obj) + loc + i); } + Object.keys(obj).forEach((key) => { + if (!ALLOWED_PLUGIN_KEYS.has(key)) { + throw new Error(messages.get("pluginInvalidProperty", loc, i, key)); + } + }); + if (obj.visitor && (obj.visitor.enter || obj.visitor.exit)) { + throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + + "Please target individual nodes."); + } + + obj = Object.assign({}, obj, { + visitor: clone(obj.visitor || {}), + }); + + traverse.explode(obj.visitor); + + if (obj.inherits) { + const inherited = OptionManager.normalisePlugin(obj.inherits, loc, "inherits"); + + obj.pre = OptionManager.chain(inherited.pre, obj.pre); + obj.post = OptionManager.chain(inherited.post, obj.post); + obj.manipulateOptions = OptionManager.chain(inherited.manipulateOptions, obj.manipulateOptions); + obj.visitor = traverse.visitors.merge([inherited.visitor, obj.visitor]); + } + + const plugin = new Plugin(obj, alias); + OptionManager.memoisedPlugins.push({ + container: fn, + plugin: plugin, + }); + return plugin; + } + + static 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); + } + }; } static createBareOptions() { @@ -137,8 +183,6 @@ export default class OptionManager { } } - plugin.init(loc, i); - return plugin; } diff --git a/packages/babel-core/src/config/plugin.js b/packages/babel-core/src/config/plugin.js index fb637d7ceea2..5db1e1dd00f5 100644 --- a/packages/babel-core/src/config/plugin.js +++ b/packages/babel-core/src/config/plugin.js @@ -1,90 +1,16 @@ -import OptionManager from "./option-manager"; -import * as messages from "babel-messages"; -import traverse from "babel-traverse"; -import clone from "lodash/clone"; - -const GLOBAL_VISITOR_PROPS = ["enter", "exit"]; - export default class Plugin { constructor(plugin: Object, key?: string) { - this.initialized = false; - this.raw = Object.assign({}, plugin); - this.key = this.take("name") || key; + this.key = plugin.name || key; - this.manipulateOptions = this.take("manipulateOptions"); - this.post = this.take("post"); - this.pre = this.take("pre"); - this.visitor = this.normaliseVisitor(clone(this.take("visitor")) || {}); + this.manipulateOptions = plugin.manipulateOptions; + this.post = plugin.post; + this.pre = plugin.pre; + this.visitor = plugin.visitor; } - initialized: boolean; - raw: Object; + key: ?string; manipulateOptions: ?Function; post: ?Function; pre: ?Function; visitor: Object; - - take(key) { - const val = this.raw[key]; - delete this.raw[key]; - return val; - } - - chain(target, key) { - if (!target[key]) return this[key]; - if (!this[key]) return target[key]; - - const fns: Array = [target[key], this[key]]; - - return function (...args) { - let val; - for (const fn of fns) { - if (fn) { - const ret = fn.apply(this, args); - if (ret != null) val = ret; - } - } - return val; - }; - } - - maybeInherit(loc: string) { - let inherits = this.take("inherits"); - if (!inherits) return; - - inherits = OptionManager.normalisePlugin(inherits, loc, "inherits"); - - this.manipulateOptions = this.chain(inherits, "manipulateOptions"); - this.post = this.chain(inherits, "post"); - this.pre = this.chain(inherits, "pre"); - this.visitor = traverse.visitors.merge([inherits.visitor, this.visitor]); - } - - /** - * We lazy initialise parts of a plugin that rely on contextual information such as - * position on disk and how it was specified. - */ - - init(loc: string, i: number) { - if (this.initialized) return; - this.initialized = true; - - this.maybeInherit(loc); - - for (const key in this.raw) { - throw new Error(messages.get("pluginInvalidProperty", loc, i, key)); - } - } - - normaliseVisitor(visitor: Object): Object { - for (const key of GLOBAL_VISITOR_PROPS) { - if (visitor[key]) { - throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + - "Please target individual nodes."); - } - } - - traverse.explode(visitor); - return visitor; - } } From 86fc4fbc4fa4c0a24f0908ec335a06fba9d9b3cb Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Mon, 3 Apr 2017 14:47:07 -0700 Subject: [PATCH 02/11] Avoid using OptionManager statics --- packages/babel-core/test/option-manager.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/babel-core/test/option-manager.js b/packages/babel-core/test/option-manager.js index a2cfd86faf84..fac8342c90c2 100644 --- a/packages/babel-core/test/option-manager.js +++ b/packages/babel-core/test/option-manager.js @@ -3,13 +3,15 @@ import OptionManager from "../lib/config/option-manager"; import path from "path"; describe("option-manager", () => { - describe("memoisePluginContainer", () => { - it("throws for babel 5 plugin", () => { - return assert.throws( - () => OptionManager.memoisePluginContainer(({ Plugin }) => new Plugin("object-assign", {})), - /Babel 5 plugin is being run with Babel 6/ - ); - }); + it("throws for babel 5 plugin", () => { + return assert.throws(() => { + const opt = new OptionManager(); + opt.init({ + plugins: [ + ({ Plugin }) => new Plugin("object-assign", {}), + ], + }); + }, /Babel 5 plugin is being run with Babel 6/); }); describe("mergeOptions", () => { From 258e82ca13a627a7392f46b7b4714b48c67b5398 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Mon, 3 Apr 2017 14:51:52 -0700 Subject: [PATCH 03/11] Expose a function rather than a class from option-manager. --- packages/babel-core/src/config/index.js | 4 ++-- .../babel-core/src/config/option-manager.js | 6 +++++- packages/babel-core/test/option-manager.js | 20 +++++++------------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index 3c8f49a1577f..238bc81cdfda 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -1,5 +1,5 @@ import type Plugin from "./plugin"; -import OptionManager from "./option-manager"; +import manageOptions from "./option-manager"; export type ResolvedConfig = { options: Object, @@ -10,7 +10,7 @@ export type ResolvedConfig = { * Standard API for loading Babel configuration data. Not for public consumption. */ export default function loadConfig(opts: Object): ResolvedConfig|null { - const mergedOpts = new OptionManager().init(opts); + const mergedOpts = manageOptions(opts); if (!mergedOpts) return null; let passes = []; diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index a004a1a308d7..67c68fdb7509 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -85,7 +85,11 @@ const ALLOWED_PLUGIN_KEYS = new Set([ "inherits", ]); -export default class OptionManager { +export default function manageOptions(opts?: Object) { + return new OptionManager().init(opts); +} + +class OptionManager { constructor() { this.options = OptionManager.createBareOptions(); } diff --git a/packages/babel-core/test/option-manager.js b/packages/babel-core/test/option-manager.js index fac8342c90c2..b0bbd18feab8 100644 --- a/packages/babel-core/test/option-manager.js +++ b/packages/babel-core/test/option-manager.js @@ -1,12 +1,11 @@ import assert from "assert"; -import OptionManager from "../lib/config/option-manager"; +import manageOptions from "../lib/config/option-manager"; import path from "path"; describe("option-manager", () => { it("throws for babel 5 plugin", () => { return assert.throws(() => { - const opt = new OptionManager(); - opt.init({ + manageOptions({ plugins: [ ({ Plugin }) => new Plugin("object-assign", {}), ], @@ -18,8 +17,7 @@ describe("option-manager", () => { it("throws for removed babel 5 options", () => { return assert.throws( () => { - const opt = new OptionManager(); - opt.init({ + manageOptions({ "randomOption": true, }); }, @@ -30,8 +28,7 @@ describe("option-manager", () => { it("throws for removed babel 5 options", () => { return assert.throws( () => { - const opt = new OptionManager(); - opt.init({ + manageOptions({ "auxiliaryComment": true, "blacklist": true, }); @@ -44,8 +41,7 @@ describe("option-manager", () => { it("throws for resolved but erroring preset", () => { return assert.throws( () => { - const opt = new OptionManager(); - opt.init({ + manageOptions({ "presets": [path.join(__dirname, "fixtures/option-manager/not-a-preset")], }); }, @@ -57,8 +53,7 @@ describe("option-manager", () => { describe("presets", function () { function presetTest(name) { it(name, function () { - const opt = new OptionManager(); - const options = opt.init({ + const options = manageOptions({ "presets": [path.join(__dirname, "fixtures/option-manager/presets", name)], }); @@ -69,8 +64,7 @@ describe("option-manager", () => { function presetThrowsTest(name, msg) { it(name, function () { - const opt = new OptionManager(); - assert.throws(() => opt.init({ + assert.throws(() => manageOptions({ "presets": [path.join(__dirname, "fixtures/option-manager/presets", name)], }), msg); }); From ac5c13c1c810ea2b17f2eb6744578a7b2ec5014d Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 15:08:49 -0700 Subject: [PATCH 04/11] Move OptionManager statics to be module functions to the lack of sideeffects clearer. --- .../babel-core/src/config/option-manager.js | 381 +++++++++--------- 1 file changed, 190 insertions(+), 191 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 67c68fdb7509..53cee648426c 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -91,133 +91,11 @@ export default function manageOptions(opts?: Object) { class OptionManager { constructor() { - this.options = OptionManager.createBareOptions(); + this.options = createBareOptions(); } options: Object; - static memoisedPlugins: Array<{ - container: Function; - plugin: Plugin; - }>; - - static memoisePluginContainer(fn, loc, i, alias) { - for (const cache of (OptionManager.memoisedPlugins: Array)) { - if (cache.container === fn) return cache.plugin; - } - - let obj: ?PluginObject; - - if (typeof fn === "function") { - obj = fn(context); - } else { - obj = fn; - } - - if (typeof obj !== "object") { - throw new TypeError(messages.get("pluginNotObject", loc, i, typeof obj) + loc + i); - } - Object.keys(obj).forEach((key) => { - if (!ALLOWED_PLUGIN_KEYS.has(key)) { - throw new Error(messages.get("pluginInvalidProperty", loc, i, key)); - } - }); - if (obj.visitor && (obj.visitor.enter || obj.visitor.exit)) { - throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + - "Please target individual nodes."); - } - - obj = Object.assign({}, obj, { - visitor: clone(obj.visitor || {}), - }); - - traverse.explode(obj.visitor); - - if (obj.inherits) { - const inherited = OptionManager.normalisePlugin(obj.inherits, loc, "inherits"); - - obj.pre = OptionManager.chain(inherited.pre, obj.pre); - obj.post = OptionManager.chain(inherited.post, obj.post); - obj.manipulateOptions = OptionManager.chain(inherited.manipulateOptions, obj.manipulateOptions); - obj.visitor = traverse.visitors.merge([inherited.visitor, obj.visitor]); - } - - const plugin = new Plugin(obj, alias); - OptionManager.memoisedPlugins.push({ - container: fn, - plugin: plugin, - }); - return plugin; - } - - static 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); - } - }; - } - - static createBareOptions() { - return { - sourceType: "module", - babelrc: true, - filename: "unknown", - code: true, - metadata: true, - ast: true, - comments: true, - compact: "auto", - highlightCode: true, - }; - } - - static normalisePlugin(plugin, loc, i, alias) { - plugin = plugin.__esModule ? plugin.default : plugin; - - if (!(plugin instanceof Plugin)) { - // allow plugin containers to be specified so they don't have to manually require - if (typeof plugin === "function" || typeof plugin === "object") { - plugin = OptionManager.memoisePluginContainer(plugin, loc, i, alias); - } else { - throw new TypeError(messages.get("pluginNotFunction", loc, i, typeof plugin)); - } - } - - return plugin; - } - - static normalisePlugins(loc, dirname, plugins) { - return plugins.map(function (val, i) { - let plugin, options; - - if (!val) { - throw new TypeError("Falsy value found in plugins"); - } - - // destructure plugins - if (Array.isArray(val)) { - [plugin, options] = val; - } else { - plugin = val; - } - - const alias = typeof plugin === "string" ? plugin : `${loc}$${i}`; - - // allow plugins to be specified as strings - if (typeof plugin === "string") { - plugin = loadPlugin(plugin, dirname).plugin; - } - - plugin = OptionManager.normalisePlugin(plugin, loc, i, alias); - - return [plugin, options]; - }); - } - /** * This is called when we want to merge the input `opts` into the * base options (passed as the `extendingOpts`: at top-level it's the @@ -307,14 +185,14 @@ class OptionManager { if (opts.plugins) { if (!Array.isArray(rawOpts.plugins)) throw new Error(`${alias}.plugins should be an array`); - opts.plugins = OptionManager.normalisePlugins(loc, dirname, opts.plugins); + opts.plugins = normalisePlugins(loc, dirname, opts.plugins); } // resolve presets if (opts.presets) { if (!Array.isArray(rawOpts.presets)) throw new Error(`${alias}.presets should be an array`); - opts.presets = this.resolvePresets(opts.presets, dirname, (preset, presetLoc) => { + opts.presets = resolvePresets(opts.presets, dirname, (preset, presetLoc) => { this.mergeOptions({ type: "preset", options: preset, @@ -342,71 +220,6 @@ class OptionManager { } } - /** - * Resolves presets options which can be either direct object data, - * or a module name to require. - */ - resolvePresets(presets: Array, dirname: string, onResolve?) { - return presets.map((preset) => { - let options; - if (Array.isArray(preset)) { - if (preset.length > 2) { - throw new Error(`Unexpected extra options ${JSON.stringify(preset.slice(2))} passed to preset.`); - } - - [preset, options] = preset; - } - - let presetLoc; - try { - if (typeof preset === "string") { - ({ - filepath: presetLoc, - preset, - } = loadPreset(preset, dirname)); - } - const resolvedPreset = this.loadPreset(preset, options, { dirname }); - - if (onResolve) onResolve(resolvedPreset, presetLoc); - - return resolvedPreset; - } catch (e) { - if (presetLoc) { - e.message += ` (While processing preset: ${JSON.stringify(presetLoc)})`; - } - throw e; - } - }); - } - - /** - * Tries to load one preset. The input is either the module name of the preset, - * a function, or an object - */ - loadPreset(preset, options, meta) { - let presetFactory = preset; - - if (typeof presetFactory === "object" && presetFactory.__esModule) { - if (presetFactory.default) { - presetFactory = presetFactory.default; - } else { - throw new Error("Preset must export a default export when using ES6 modules."); - } - } - - // Allow simple object exports - if (typeof presetFactory === "object") { - return presetFactory; - } - - if (typeof presetFactory !== "function") { - // eslint-disable-next-line max-len - throw new Error(`Unsupported preset format: ${typeof presetFactory}. Expected preset to return a function.`); - } - - return presetFactory(context, options, meta); - } - init(opts: Object = {}): Object { const configChain = buildConfigChain(opts); if (!configChain) return null; @@ -458,4 +271,190 @@ class OptionManager { } } -OptionManager.memoisedPlugins = []; +/** + * Resolves presets options which can be either direct object data, + * or a module name to require. + */ +function resolvePresets(presets: Array, dirname: string, onResolve?) { + return presets.map((preset) => { + let options; + if (Array.isArray(preset)) { + if (preset.length > 2) { + throw new Error(`Unexpected extra options ${JSON.stringify(preset.slice(2))} passed to preset.`); + } + + [preset, options] = preset; + } + + let presetLoc; + try { + if (typeof preset === "string") { + ({ + filepath: presetLoc, + preset, + } = loadPreset(preset, dirname)); + } + const resolvedPreset = loadPresetObject(preset, options, { dirname }); + + if (onResolve) onResolve(resolvedPreset, presetLoc); + + return resolvedPreset; + } catch (e) { + if (presetLoc) { + e.message += ` (While processing preset: ${JSON.stringify(presetLoc)})`; + } + throw e; + } + }); +} + +/** + * Tries to load one preset. The input is either the module name of the preset, + * a function, or an object + */ +function loadPresetObject(preset, options, meta) { + let presetFactory = preset; + + if (typeof presetFactory === "object" && presetFactory.__esModule) { + if (presetFactory.default) { + presetFactory = presetFactory.default; + } else { + throw new Error("Preset must export a default export when using ES6 modules."); + } + } + + // Allow simple object exports + if (typeof presetFactory === "object") { + return presetFactory; + } + + if (typeof presetFactory !== "function") { + // eslint-disable-next-line max-len + throw new Error(`Unsupported preset format: ${typeof presetFactory}. Expected preset to return a function.`); + } + + return presetFactory(context, options, meta); +} + + +const memoisedPlugins: Array<{ + container: Function; + plugin: Plugin; +}> = []; + +function memoisePluginContainer(fn, loc, i, alias) { + for (const cache of (memoisedPlugins: Array)) { + if (cache.container === fn) return cache.plugin; + } + + let obj: ?PluginObject; + + if (typeof fn === "function") { + obj = fn(context); + } else { + obj = fn; + } + + if (typeof obj !== "object") { + throw new TypeError(messages.get("pluginNotObject", loc, i, typeof obj) + loc + i); + } + Object.keys(obj).forEach((key) => { + if (!ALLOWED_PLUGIN_KEYS.has(key)) { + throw new Error(messages.get("pluginInvalidProperty", loc, i, key)); + } + }); + if (obj.visitor && (obj.visitor.enter || obj.visitor.exit)) { + throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + + "Please target individual nodes."); + } + + obj = Object.assign({}, obj, { + visitor: clone(obj.visitor || {}), + }); + + traverse.explode(obj.visitor); + + if (obj.inherits) { + const inherited = normalisePlugin(obj.inherits, loc, "inherits"); + + obj.pre = chain(inherited.pre, obj.pre); + obj.post = chain(inherited.post, obj.post); + obj.manipulateOptions = chain(inherited.manipulateOptions, obj.manipulateOptions); + obj.visitor = traverse.visitors.merge([inherited.visitor, obj.visitor]); + } + + const plugin = new Plugin(obj, alias); + memoisedPlugins.push({ + container: fn, + plugin: plugin, + }); + return plugin; +} + +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); + } + }; +} + +function createBareOptions() { + return { + sourceType: "module", + babelrc: true, + filename: "unknown", + code: true, + metadata: true, + ast: true, + comments: true, + compact: "auto", + highlightCode: true, + }; +} + +function normalisePlugin(plugin, loc, i, alias) { + plugin = plugin.__esModule ? plugin.default : plugin; + + if (!(plugin instanceof Plugin)) { + // allow plugin containers to be specified so they don't have to manually require + if (typeof plugin === "function" || typeof plugin === "object") { + plugin = memoisePluginContainer(plugin, loc, i, alias); + } else { + throw new TypeError(messages.get("pluginNotFunction", loc, i, typeof plugin)); + } + } + + return plugin; +} + +function normalisePlugins(loc, dirname, plugins) { + return plugins.map(function (val, i) { + let plugin, options; + + if (!val) { + throw new TypeError("Falsy value found in plugins"); + } + + // destructure plugins + if (Array.isArray(val)) { + [plugin, options] = val; + } else { + plugin = val; + } + + const alias = typeof plugin === "string" ? plugin : `${loc}$${i}`; + + // allow plugins to be specified as strings + if (typeof plugin === "string") { + plugin = loadPlugin(plugin, dirname).plugin; + } + + plugin = normalisePlugin(plugin, loc, i, alias); + + return [plugin, options]; + }); +} From 1fecf286c6db4dcd972a874bbf3d1cea0c502b94 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 15:24:34 -0700 Subject: [PATCH 05/11] Avoid the onResolve callback. --- packages/babel-core/src/config/option-manager.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 53cee648426c..585c9db281f9 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -115,7 +115,6 @@ class OptionManager { dirname, }: MergeOptions) { alias = alias || "foreign"; - if (!rawOpts) return; // if (typeof rawOpts !== "object" || Array.isArray(rawOpts)) { @@ -129,10 +128,6 @@ class OptionManager { } }); - // - dirname = dirname || process.cwd(); - loc = loc || alias; - if (type !== "arguments") { if (opts.filename !== undefined) { throw new Error(`${alias}.filename is only allowed as a root argument`); @@ -192,7 +187,7 @@ class OptionManager { if (opts.presets) { if (!Array.isArray(rawOpts.presets)) throw new Error(`${alias}.presets should be an array`); - opts.presets = resolvePresets(opts.presets, dirname, (preset, presetLoc) => { + opts.presets = resolvePresets(opts.presets, dirname).map(([preset, presetLoc]) => { this.mergeOptions({ type: "preset", options: preset, @@ -203,6 +198,8 @@ class OptionManager { loc: presetLoc, dirname: dirname, }); + + return preset; }); // If not passPerPreset, the plugins have all been merged into the parent config so the presets @@ -275,7 +272,7 @@ class OptionManager { * Resolves presets options which can be either direct object data, * or a module name to require. */ -function resolvePresets(presets: Array, dirname: string, onResolve?) { +function resolvePresets(presets: Array, dirname: string) { return presets.map((preset) => { let options; if (Array.isArray(preset)) { @@ -296,9 +293,7 @@ function resolvePresets(presets: Array, dirname: string, onReso } const resolvedPreset = loadPresetObject(preset, options, { dirname }); - if (onResolve) onResolve(resolvedPreset, presetLoc); - - return resolvedPreset; + return [ resolvedPreset, presetLoc ]; } catch (e) { if (presetLoc) { e.message += ` (While processing preset: ${JSON.stringify(presetLoc)})`; From 75847306fd10075713fe917e31d057d8e407f897 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 15:59:53 -0700 Subject: [PATCH 06/11] Expose passes as a first-class output of OptionManager. --- packages/babel-core/src/config/index.js | 18 +---- .../babel-core/src/config/option-manager.js | 70 +++++++++++-------- packages/babel-core/test/option-manager.js | 4 +- 3 files changed, 44 insertions(+), 48 deletions(-) diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index 238bc81cdfda..a37b7bc55bfa 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -10,21 +10,5 @@ export type ResolvedConfig = { * Standard API for loading Babel configuration data. Not for public consumption. */ export default function loadConfig(opts: Object): ResolvedConfig|null { - const mergedOpts = manageOptions(opts); - if (!mergedOpts) return null; - - let passes = []; - if (mergedOpts.plugins) { - passes.push(mergedOpts.plugins); - } - - // With "passPerPreset" enabled there may still be presets in the options. - if (mergedOpts.presets) { - passes = passes.concat(mergedOpts.presets.map((preset) => preset.plugins).filter(Boolean)); - } - - return { - options: mergedOpts, - passes, - }; + return manageOptions(opts); } diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 585c9db281f9..ba313cb1fb91 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -92,14 +92,15 @@ export default function manageOptions(opts?: Object) { class OptionManager { constructor() { this.options = createBareOptions(); + this.passes = [[]]; } options: Object; + passes: Array>; /** * This is called when we want to merge the input `opts` into the - * base options (passed as the `extendingOpts`: at top-level it's the - * main options, at presets level it's presets options). + * base options. * * - `alias` is used to output pretty traces back to the original source. * - `loc` is used to point to the original config. @@ -109,13 +110,14 @@ class OptionManager { mergeOptions({ type, options: rawOpts, - extending: extendingOpts, alias, loc, dirname, - }: MergeOptions) { + }: MergeOptions, pass?: Array) { alias = alias || "foreign"; + if (!pass) pass = this.passes[0]; + // if (typeof rawOpts !== "object" || Array.isArray(rawOpts)) { throw new TypeError(`Invalid options type for ${alias}`); @@ -176,45 +178,43 @@ class OptionManager { opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, dirname).generator; } - // resolve plugins - if (opts.plugins) { - if (!Array.isArray(rawOpts.plugins)) throw new Error(`${alias}.plugins should be an array`); - - opts.plugins = normalisePlugins(loc, dirname, opts.plugins); - } - // resolve presets if (opts.presets) { if (!Array.isArray(rawOpts.presets)) throw new Error(`${alias}.presets should be an array`); - opts.presets = resolvePresets(opts.presets, dirname).map(([preset, presetLoc]) => { + const presets = resolvePresets(opts.presets, dirname); + + let presetPasses = null; + if (opts.passPerPreset) { + presetPasses = presets.map(() => []); + // 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, ...presetPasses); + } + + presets.forEach(([preset, presetLoc], i) => { this.mergeOptions({ type: "preset", options: preset, - - // For `passPerPreset` we merge child options back into the preset object instead of the root. - extending: opts.passPerPreset ? preset : null, alias: presetLoc, loc: presetLoc, dirname: dirname, - }); - - return preset; + }, presetPasses ? presetPasses[i] : pass); }); - - // If not passPerPreset, the plugins have all been merged into the parent config so the presets - // list is not needed. - if (!opts.passPerPreset) delete opts.presets; } - // Merge them into current extending options in case of top-level - // options. In case of presets, just re-assign options which are got - // normalized during the `mergeOptions`. - if (rawOpts === extendingOpts) { - Object.assign(extendingOpts, opts); - } else { - merge(extendingOpts || this.options, opts); + // resolve plugins + if (opts.plugins) { + if (!Array.isArray(rawOpts.plugins)) throw new Error(`${alias}.plugins should be an array`); + + pass.unshift(...normalisePlugins(loc, dirname, opts.plugins)); } + + delete opts.passPerPreset; + delete opts.plugins; + delete opts.presets; + + merge(this.options, opts); } init(opts: Object = {}): Object { @@ -237,6 +237,13 @@ class OptionManager { opts = 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.plugins = this.passes[0]; + opts.presets = this.passes.slice(1) + .filter((plugins) => plugins.length > 0) + .map((plugins) => ({ plugins })); + if (opts.inputSourceMap) { opts.sourceMaps = true; } @@ -264,7 +271,10 @@ class OptionManager { sourceMapTarget: basenameRelative, }); - return opts; + return { + options: opts, + passes: this.passes, + }; } } diff --git a/packages/babel-core/test/option-manager.js b/packages/babel-core/test/option-manager.js index b0bbd18feab8..d9caef2cdf0c 100644 --- a/packages/babel-core/test/option-manager.js +++ b/packages/babel-core/test/option-manager.js @@ -53,12 +53,14 @@ describe("option-manager", () => { describe("presets", function () { function presetTest(name) { it(name, function () { - const options = manageOptions({ + const { options, passes } = manageOptions({ "presets": [path.join(__dirname, "fixtures/option-manager/presets", name)], }); assert.equal(true, Array.isArray(options.plugins)); assert.equal(1, options.plugins.length); + assert.equal(1, passes.length); + assert.equal(1, passes[0].length); }); } From adb903fd4b62134c2371f6cc08de68f604ec344f Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 16:20:07 -0700 Subject: [PATCH 07/11] Standardlize plugin/preset result object. --- .../src/config/loading/files/index-browser.js | 8 ++++---- .../src/config/loading/files/plugins.js | 16 ++++++++-------- packages/babel-core/src/config/option-manager.js | 10 ++++++---- 3 files changed, 18 insertions(+), 16 deletions(-) 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 5653c1816038..b07fe3b2ded9 100644 --- a/packages/babel-core/src/config/loading/files/index-browser.js +++ b/packages/babel-core/src/config/loading/files/index-browser.js @@ -25,18 +25,18 @@ export function resolvePreset(name: string, dirname: string): string|null { return null; } -export function loadPlugin(name: string, dirname: string): { filepath: string, plugin: mixed } { +export function loadPlugin(name: string, dirname: string): { filepath: string, value: mixed } { throw new Error(`Cannot load plugin ${name} relative to ${dirname} in a browser`); } -export function loadPreset(name: string, dirname: string): { filepath: string, preset: mixed } { +export function loadPreset(name: string, dirname: string): { filepath: string, value: mixed } { throw new Error(`Cannot load preset ${name} relative to ${dirname} in a browser`); } -export function loadParser(name: string, dirname: string): { filepath: string, parser: Function } { +export function loadParser(name: string, dirname: string): { filepath: string, value: Function } { throw new Error(`Cannot load parser ${name} relative to ${dirname} in a browser`); } -export function loadGenerator(name: string, dirname: string): { filepath: string, generator: Function } { +export function loadGenerator(name: string, dirname: string): { filepath: string, value: Function } { throw new Error(`Cannot load generator ${name} relative to ${dirname} in a browser`); } diff --git a/packages/babel-core/src/config/loading/files/plugins.js b/packages/babel-core/src/config/loading/files/plugins.js index 539683a63518..e6704d5fe06f 100644 --- a/packages/babel-core/src/config/loading/files/plugins.js +++ b/packages/babel-core/src/config/loading/files/plugins.js @@ -26,27 +26,27 @@ export function resolvePreset(presetName: string, dirname: string): string|null return resolveFromPossibleNames(possibleNames, dirname); } -export function loadPlugin(name: string, dirname: string): { filepath: string, plugin: mixed } { +export function loadPlugin(name: string, dirname: string): { filepath: string, value: mixed } { const filepath = resolvePlugin(name, dirname); if (!filepath) throw new Error(`Plugin ${name} not found relative to ${dirname}`); return { filepath, - plugin: requireModule(filepath), + value: requireModule(filepath), }; } -export function loadPreset(name: string, dirname: string): { filepath: string, preset: mixed } { +export function loadPreset(name: string, dirname: string): { filepath: string, value: mixed } { const filepath = resolvePreset(name, dirname); if (!filepath) throw new Error(`Preset ${name} not found relative to ${dirname}`); return { filepath, - preset: requireModule(filepath), + value: requireModule(filepath), }; } -export function loadParser(name: string, dirname: string): { filepath: string, parser: Function } { +export function loadParser(name: string, dirname: string): { filepath: string, value: Function } { const filepath = resolveQuiet(name, dirname); if (!filepath) throw new Error(`Parser ${name} not found relative to ${dirname}`); @@ -61,11 +61,11 @@ export function loadParser(name: string, dirname: string): { filepath: string, p return { filepath, - parser: mod.parse, + value: mod.parse, }; } -export function loadGenerator(name: string, dirname: string): { filepath: string, generator: Function } { +export function loadGenerator(name: string, dirname: string): { filepath: string, value: Function } { const filepath = resolveQuiet(name, dirname); if (!filepath) throw new Error(`Generator ${name} not found relative to ${dirname}`); @@ -80,7 +80,7 @@ export function loadGenerator(name: string, dirname: string): { filepath: string return { filepath, - generator: mod.print, + value: mod.print, }; } diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index ba313cb1fb91..6ac59f75a9e2 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -171,13 +171,15 @@ class OptionManager { } if (opts.parserOpts && typeof opts.parserOpts.parser === "string") { - opts.parserOpts.parser = loadParser(opts.parserOpts.parser, dirname).parser; + opts.parserOpts.parser = loadParser(opts.parserOpts.parser, dirname).value; } if (opts.generatorOpts && typeof opts.generatorOpts.generator === "string") { - opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, dirname).generator; + opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, dirname).value; } + // const plugins = + // resolve presets if (opts.presets) { if (!Array.isArray(rawOpts.presets)) throw new Error(`${alias}.presets should be an array`); @@ -298,7 +300,7 @@ function resolvePresets(presets: Array, dirname: string) { if (typeof preset === "string") { ({ filepath: presetLoc, - preset, + value: preset, } = loadPreset(preset, dirname)); } const resolvedPreset = loadPresetObject(preset, options, { dirname }); @@ -455,7 +457,7 @@ function normalisePlugins(loc, dirname, plugins) { // allow plugins to be specified as strings if (typeof plugin === "string") { - plugin = loadPlugin(plugin, dirname).plugin; + plugin = loadPlugin(plugin, dirname).value; } plugin = normalisePlugin(plugin, loc, i, alias); From e13320ef7390c99992415330291abda93ed7d0df Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 17:11:07 -0700 Subject: [PATCH 08/11] Separate plugin/preset loading and plugin/preset evaluation. --- .../babel-core/src/config/option-manager.js | 157 ++++++++++-------- 1 file changed, 92 insertions(+), 65 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 6ac59f75a9e2..bb49ec2aca6b 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -2,7 +2,6 @@ import * as context from "../index"; import Plugin from "./plugin"; import * as messages from "babel-messages"; import defaults from "lodash/defaults"; -import cloneDeepWith from "lodash/cloneDeepWith"; import merge from "./helpers/merge"; import removed from "./removed"; import buildConfigChain from "./build-config-chain"; @@ -124,11 +123,7 @@ class OptionManager { } // - const opts = cloneDeepWith(rawOpts, (val) => { - if (val instanceof Plugin) { - return val; - } - }); + const opts = Object.assign({}, rawOpts); if (type !== "arguments") { if (opts.filename !== undefined) { @@ -171,30 +166,43 @@ class OptionManager { } if (opts.parserOpts && typeof opts.parserOpts.parser === "string") { + opts.parserOpts = Object.assign({}, opts.parserOpts); opts.parserOpts.parser = loadParser(opts.parserOpts.parser, dirname).value; } if (opts.generatorOpts && typeof opts.generatorOpts.generator === "string") { + opts.generatorOpts = Object.assign({}, opts.generatorOpts); opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, dirname).value; } - // const plugins = + if (rawOpts.presets && !Array.isArray(rawOpts.presets)) { + throw new Error(`${alias}.presets should be an array`); + } + if (rawOpts.plugins && !Array.isArray(rawOpts.plugins)) { + throw new Error(`${alias}.plugins should be an array`); + } - // resolve presets - if (opts.presets) { - if (!Array.isArray(rawOpts.presets)) throw new Error(`${alias}.presets should be an array`); + delete opts.passPerPreset; + delete opts.plugins; + delete opts.presets; + + const passPerPreset = rawOpts.passPerPreset; + const plugins = normalizePlugins(rawOpts.plugins, dirname); + const presets = normalizePresets(rawOpts.presets, dirname); - const presets = resolvePresets(opts.presets, dirname); + // resolve presets + if (presets.length > 0) { + const presetsObjects = resolvePresets(presets, dirname); let presetPasses = null; - if (opts.passPerPreset) { - presetPasses = presets.map(() => []); + if (passPerPreset) { + presetPasses = presetsObjects.map(() => []); // 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, ...presetPasses); } - presets.forEach(([preset, presetLoc], i) => { + presetsObjects.forEach(([preset, presetLoc], i) => { this.mergeOptions({ type: "preset", options: preset, @@ -206,16 +214,12 @@ class OptionManager { } // resolve plugins - if (opts.plugins) { - if (!Array.isArray(rawOpts.plugins)) throw new Error(`${alias}.plugins should be an array`); - - pass.unshift(...normalisePlugins(loc, dirname, opts.plugins)); + if (plugins.length > 0) { + pass.unshift(...plugins.map(function ({ filepath, plugin, options }, i) { + return [ normalisePlugin(plugin, loc, i, filepath || `${loc}$${i}`), options ]; + })); } - delete opts.passPerPreset; - delete opts.plugins; - delete opts.presets; - merge(this.options, opts); } @@ -280,11 +284,43 @@ class OptionManager { } } -/** - * Resolves presets options which can be either direct object data, - * or a module name to require. - */ -function resolvePresets(presets: Array, dirname: string) { +function normalizePlugins(plugins, dirname) { + if (!plugins) return []; + + return plugins.map((plugin) => { + + let options; + if (Array.isArray(plugin)) { + if (plugin.length > 2) { + throw new Error(`Unexpected extra options ${JSON.stringify(plugin.slice(2))} passed to plugin.`); + } + + [plugin, options] = plugin; + } + + if (!plugin) { + throw new TypeError("Falsy value found in plugins"); + } + + let filepath = null; + if (typeof plugin === "string") { + ({ + filepath, + value: plugin, + } = loadPlugin(plugin, dirname)); + } + + return { + filepath, + plugin, + options, + }; + }); +} + +function normalizePresets(presets, dirname) { + if (!presets) return []; + return presets.map((preset) => { let options; if (Array.isArray(preset)) { @@ -295,20 +331,39 @@ function resolvePresets(presets: Array, dirname: string) { [preset, options] = preset; } - let presetLoc; + if (!preset) { + throw new TypeError("Falsy value found in presets"); + } + + let filepath = null; + if (typeof preset === "string") { + ({ + filepath, + value: preset, + } = loadPreset(preset, dirname)); + } + + return { + filepath, + preset, + options, + }; + }); +} + +/** + * Resolves presets options which can be either direct object data, + * or a module name to require. + */ +function resolvePresets(presets: Array, dirname: string) { + return presets.map(({ filepath, preset, options }) => { try { - if (typeof preset === "string") { - ({ - filepath: presetLoc, - value: preset, - } = loadPreset(preset, dirname)); - } const resolvedPreset = loadPresetObject(preset, options, { dirname }); - return [ resolvedPreset, presetLoc ]; + return [ resolvedPreset, filepath ]; } catch (e) { - if (presetLoc) { - e.message += ` (While processing preset: ${JSON.stringify(presetLoc)})`; + if (filepath) { + e.message += ` (While processing preset: ${JSON.stringify(filepath)})`; } throw e; } @@ -437,31 +492,3 @@ function normalisePlugin(plugin, loc, i, alias) { return plugin; } - -function normalisePlugins(loc, dirname, plugins) { - return plugins.map(function (val, i) { - let plugin, options; - - if (!val) { - throw new TypeError("Falsy value found in plugins"); - } - - // destructure plugins - if (Array.isArray(val)) { - [plugin, options] = val; - } else { - plugin = val; - } - - const alias = typeof plugin === "string" ? plugin : `${loc}$${i}`; - - // allow plugins to be specified as strings - if (typeof plugin === "string") { - plugin = loadPlugin(plugin, dirname).value; - } - - plugin = normalisePlugin(plugin, loc, i, alias); - - return [plugin, options]; - }); -} From d16991054e34404e84f98aa97d1f18235a73b1e3 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 17:28:25 -0700 Subject: [PATCH 09/11] Pass the config through mergeOptions. --- .../babel-core/src/config/option-manager.js | 92 ++++++++----------- 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index bb49ec2aca6b..c20994125e4b 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -27,7 +27,6 @@ type PluginObject = { type MergeOptions = { type: "arguments"|"options"|"preset", options?: Object, - extending?: Object, alias: string, loc?: string, dirname?: string @@ -106,24 +105,17 @@ class OptionManager { * - `dirname` is used to resolve plugins relative to it. */ - mergeOptions({ - type, - options: rawOpts, - alias, - loc, - dirname, - }: MergeOptions, pass?: Array) { - alias = alias || "foreign"; - - if (!pass) pass = this.passes[0]; + mergeOptions(config: MergeOptions, pass?: Array) { + const alias = config.alias || "foreign"; + const type = config.type; // - if (typeof rawOpts !== "object" || Array.isArray(rawOpts)) { + if (typeof config.options !== "object" || Array.isArray(config.options)) { throw new TypeError(`Invalid options type for ${alias}`); } // - const opts = Object.assign({}, rawOpts); + const opts = Object.assign({}, config.options); if (type !== "arguments") { if (opts.filename !== undefined) { @@ -167,18 +159,18 @@ class OptionManager { if (opts.parserOpts && typeof opts.parserOpts.parser === "string") { opts.parserOpts = Object.assign({}, opts.parserOpts); - opts.parserOpts.parser = loadParser(opts.parserOpts.parser, dirname).value; + opts.parserOpts.parser = loadParser(opts.parserOpts.parser, config.dirname).value; } if (opts.generatorOpts && typeof opts.generatorOpts.generator === "string") { opts.generatorOpts = Object.assign({}, opts.generatorOpts); - opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, dirname).value; + opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, config.dirname).value; } - if (rawOpts.presets && !Array.isArray(rawOpts.presets)) { + if (config.options.presets && !Array.isArray(config.options.presets)) { throw new Error(`${alias}.presets should be an array`); } - if (rawOpts.plugins && !Array.isArray(rawOpts.plugins)) { + if (config.options.plugins && !Array.isArray(config.options.plugins)) { throw new Error(`${alias}.plugins should be an array`); } @@ -186,29 +178,36 @@ class OptionManager { delete opts.plugins; delete opts.presets; - const passPerPreset = rawOpts.passPerPreset; - const plugins = normalizePlugins(rawOpts.plugins, dirname); - const presets = normalizePresets(rawOpts.presets, dirname); + const passPerPreset = config.options.passPerPreset; + const plugins = normalizePlugins(config); + const presets = normalizePresets(config); + pass = pass || this.passes[0]; // resolve presets if (presets.length > 0) { - const presetsObjects = resolvePresets(presets, dirname); - let presetPasses = null; if (passPerPreset) { - presetPasses = presetsObjects.map(() => []); + presetPasses = presets.map(() => []); // 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, ...presetPasses); } - presetsObjects.forEach(([preset, presetLoc], i) => { + presets.forEach(({ filepath, preset, options }, i) => { + let resolvedPreset; + try { + resolvedPreset = loadPresetObject(preset, options, { dirname: config.dirname }); + } catch (e) { + if (filepath) e.message += ` (While processing preset: ${JSON.stringify(filepath)})`; + throw e; + } + this.mergeOptions({ type: "preset", - options: preset, - alias: presetLoc, - loc: presetLoc, - dirname: dirname, + options: resolvedPreset, + alias: filepath, + loc: filepath, + dirname: config.dirname, }, presetPasses ? presetPasses[i] : pass); }); } @@ -216,7 +215,7 @@ class OptionManager { // resolve plugins if (plugins.length > 0) { pass.unshift(...plugins.map(function ({ filepath, plugin, options }, i) { - return [ normalisePlugin(plugin, loc, i, filepath || `${loc}$${i}`), options ]; + return [ normalisePlugin(plugin, config.loc, i, filepath || `${config.loc}$${i}`), options ]; })); } @@ -284,10 +283,10 @@ class OptionManager { } } -function normalizePlugins(plugins, dirname) { - if (!plugins) return []; +function normalizePlugins(config) { + if (!config.options.plugins) return []; - return plugins.map((plugin) => { + return config.options.plugins.map((plugin) => { let options; if (Array.isArray(plugin)) { @@ -307,7 +306,7 @@ function normalizePlugins(plugins, dirname) { ({ filepath, value: plugin, - } = loadPlugin(plugin, dirname)); + } = loadPlugin(plugin, config.dirname)); } return { @@ -318,10 +317,10 @@ function normalizePlugins(plugins, dirname) { }); } -function normalizePresets(presets, dirname) { - if (!presets) return []; +function normalizePresets(config) { + if (!config.options.presets) return []; - return presets.map((preset) => { + return config.options.presets.map((preset) => { let options; if (Array.isArray(preset)) { if (preset.length > 2) { @@ -340,7 +339,7 @@ function normalizePresets(presets, dirname) { ({ filepath, value: preset, - } = loadPreset(preset, dirname)); + } = loadPreset(preset, config.dirname)); } return { @@ -351,25 +350,6 @@ function normalizePresets(presets, dirname) { }); } -/** - * Resolves presets options which can be either direct object data, - * or a module name to require. - */ -function resolvePresets(presets: Array, dirname: string) { - return presets.map(({ filepath, preset, options }) => { - try { - const resolvedPreset = loadPresetObject(preset, options, { dirname }); - - return [ resolvedPreset, filepath ]; - } catch (e) { - if (filepath) { - e.message += ` (While processing preset: ${JSON.stringify(filepath)})`; - } - throw e; - } - }); -} - /** * Tries to load one preset. The input is either the module name of the preset, * a function, or an object From d7e10a3adc80c3a6bb407cc51674d3550af32254 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 5 Apr 2017 18:18:18 -0700 Subject: [PATCH 10/11] Misc refactoring of OptionManager. --- .../babel-core/src/config/option-manager.js | 431 +++++++++--------- packages/babel-core/test/api.js | 2 +- packages/babel-core/test/option-manager.js | 8 +- packages/babel-messages/src/index.js | 2 +- 4 files changed, 213 insertions(+), 230 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index c20994125e4b..f1f8838a0c61 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -11,19 +11,6 @@ import clone from "lodash/clone"; import { loadPlugin, loadPreset, loadParser, loadGenerator } from "./loading/files"; -type PluginObject = { - pre?: Function; - post?: Function; - manipulateOptions?: Function; - - visitor: ?{ - [key: string]: Function | { - enter?: Function | Array; - exit?: Function | Array; - } - }; -}; - type MergeOptions = { type: "arguments"|"options"|"preset", options?: Object, @@ -106,81 +93,12 @@ class OptionManager { */ mergeOptions(config: MergeOptions, pass?: Array) { - const alias = config.alias || "foreign"; - const type = config.type; - - // - if (typeof config.options !== "object" || Array.isArray(config.options)) { - throw new TypeError(`Invalid options type for ${alias}`); - } - - // - const opts = Object.assign({}, config.options); - - if (type !== "arguments") { - if (opts.filename !== undefined) { - throw new Error(`${alias}.filename is only allowed as a root argument`); - } - - if (opts.babelrc !== undefined) { - throw new Error(`${alias}.babelrc is only allowed as a root argument`); - } - } - - if (type === "preset") { - if (opts.only !== undefined) throw new Error(`${alias}.only is not supported in a preset`); - if (opts.ignore !== undefined) throw new Error(`${alias}.ignore is not supported in a preset`); - if (opts.extends !== undefined) throw new Error(`${alias}.extends is not supported in a preset`); - if (opts.env !== undefined) throw new Error(`${alias}.env is not supported in a preset`); - } - - if (opts.sourceMap !== undefined) { - if (opts.sourceMaps !== undefined) { - throw new Error(`Both ${alias}.sourceMap and .sourceMaps have been set`); - } - - opts.sourceMaps = opts.sourceMap; - delete opts.sourceMap; - } - - for (const key in opts) { - // check for an unknown option - if (!optionNames.has(key)) { - if (removed[key]) { - throw new ReferenceError(`Using removed Babel 5 option: ${alias}.${key} - ${removed[key].message}`); - } else { - // eslint-disable-next-line max-len - const unknownOptErr = `Unknown option: ${alias}.${key}. Check out http://babeljs.io/docs/usage/options/ for more information about options.`; - - throw new ReferenceError(unknownOptErr); - } - } - } + const result = loadConfig(config); - if (opts.parserOpts && typeof opts.parserOpts.parser === "string") { - opts.parserOpts = Object.assign({}, opts.parserOpts); - opts.parserOpts.parser = loadParser(opts.parserOpts.parser, config.dirname).value; - } - - if (opts.generatorOpts && typeof opts.generatorOpts.generator === "string") { - opts.generatorOpts = Object.assign({}, opts.generatorOpts); - opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, config.dirname).value; - } - - if (config.options.presets && !Array.isArray(config.options.presets)) { - throw new Error(`${alias}.presets should be an array`); - } - if (config.options.plugins && !Array.isArray(config.options.plugins)) { - throw new Error(`${alias}.plugins should be an array`); - } - - delete opts.passPerPreset; - delete opts.plugins; - delete opts.presets; + const plugins = result.plugins.map((descriptor) => loadPluginDescriptor(descriptor)); + const presets = result.presets.map((descriptor) => loadPresetDescriptor(descriptor)); const passPerPreset = config.options.passPerPreset; - const plugins = normalizePlugins(config); - const presets = normalizePresets(config); pass = pass || this.passes[0]; // resolve presets @@ -193,33 +111,17 @@ class OptionManager { this.passes.splice(1, 0, ...presetPasses); } - presets.forEach(({ filepath, preset, options }, i) => { - let resolvedPreset; - try { - resolvedPreset = loadPresetObject(preset, options, { dirname: config.dirname }); - } catch (e) { - if (filepath) e.message += ` (While processing preset: ${JSON.stringify(filepath)})`; - throw e; - } - - this.mergeOptions({ - type: "preset", - options: resolvedPreset, - alias: filepath, - loc: filepath, - dirname: config.dirname, - }, presetPasses ? presetPasses[i] : pass); + presets.forEach((presetConfig, i) => { + this.mergeOptions(presetConfig, presetPasses ? presetPasses[i] : pass); }); } // resolve plugins if (plugins.length > 0) { - pass.unshift(...plugins.map(function ({ filepath, plugin, options }, i) { - return [ normalisePlugin(plugin, config.loc, i, filepath || `${config.loc}$${i}`), options ]; - })); + pass.unshift(...plugins); } - merge(this.options, opts); + merge(this.options, result.options); } init(opts: Object = {}): Object { @@ -283,154 +185,250 @@ class OptionManager { } } -function normalizePlugins(config) { - if (!config.options.plugins) return []; - - return config.options.plugins.map((plugin) => { - - let options; - if (Array.isArray(plugin)) { - if (plugin.length > 2) { - throw new Error(`Unexpected extra options ${JSON.stringify(plugin.slice(2))} passed to plugin.`); - } - - [plugin, options] = plugin; - } +/** + * Load and validate the given config into a set of options, plugins, and presets. + */ +function loadConfig(config) { + const options = normalizeOptions(config); - if (!plugin) { - throw new TypeError("Falsy value found in plugins"); - } + const plugins = (config.options.plugins || []).map((plugin, index) => { + const { filepath, value, options } = normalizePair(plugin, loadPlugin, config.dirname); - let filepath = null; - if (typeof plugin === "string") { - ({ - filepath, - value: plugin, - } = loadPlugin(plugin, config.dirname)); - } + return { + alias: filepath || `${config.loc}$${index}`, + loc: filepath || config.loc, + value, + options, + dirname: config.dirname, + }; + }); + const presets = (config.options.presets || []).map((preset, index) => { + const { filepath, value, options } = normalizePair(preset, loadPreset, config.dirname); return { - filepath, - plugin, + alias: filepath || `${config.loc}$${index}`, + loc: filepath || config.loc, + value, options, + dirname: config.dirname, }; }); + + return { options, plugins, presets }; } -function normalizePresets(config) { - if (!config.options.presets) return []; +/** + * Load a generic plugin/preset from the given descriptor loaded from the config object. + */ +function loadDescriptor(descriptor, skipOptions) { + if (typeof descriptor.value !== "function") return { value: descriptor.value, descriptor }; + + const { value, options } = descriptor; + let item; + try { + if (skipOptions) { + item = value(context); + } else { + item = value(context, options, { dirname: descriptor.dirname }); + } + } catch (e) { + if (descriptor.alias) e.message += ` (While processing: ${JSON.stringify(descriptor.alias)})`; + throw e; + } + + if (!item || typeof item !== "object") { + throw new Error("Plugin/Preset did not return an object."); + } - return config.options.presets.map((preset) => { - let options; - if (Array.isArray(preset)) { - if (preset.length > 2) { - throw new Error(`Unexpected extra options ${JSON.stringify(preset.slice(2))} passed to preset.`); - } + return { value: item, descriptor }; +} - [preset, options] = preset; - } +/** + * Instantiate a plugin for the given descriptor, returning the plugin/options pair. + */ +const PLUGIN_CACHE = new WeakMap(); +function loadPluginDescriptor(descriptor) { + if (descriptor.value instanceof Plugin) return [ descriptor.value, descriptor.options ]; + + let result = PLUGIN_CACHE.get(descriptor.value); + if (!result) { + result = instantiatePlugin(loadDescriptor(descriptor, true /* skipOptions */)); + PLUGIN_CACHE.set(descriptor.value, result); + } - if (!preset) { - throw new TypeError("Falsy value found in presets"); - } + return [ result, descriptor.options]; +} - let filepath = null; - if (typeof preset === "string") { - ({ - filepath, - value: preset, - } = loadPreset(preset, config.dirname)); +function instantiatePlugin({ value: pluginObject, descriptor }) { + Object.keys(pluginObject).forEach((key) => { + if (!ALLOWED_PLUGIN_KEYS.has(key)) { + throw new Error(messages.get("pluginInvalidProperty", descriptor.alias, key)); } + }); + if (pluginObject.visitor && (pluginObject.visitor.enter || pluginObject.visitor.exit)) { + throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + + "Please target individual nodes."); + } - return { - filepath, - preset, - options, - }; + const plugin = Object.assign({}, pluginObject, { + visitor: clone(pluginObject.visitor || {}), }); + + traverse.explode(plugin.visitor); + + let inheritsDescriptor; + let inherits; + if (plugin.inherits) { + inheritsDescriptor = { + alias: `${descriptor.loc}$inherits`, + loc: descriptor.loc, + value: plugin.inherits, + options: descriptor.options, + }; + + inherits = loadPluginDescriptor(inheritsDescriptor)[0]; + + 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, descriptor.alias); +} + +/** + * Generate a config object that will act as the root of a new nested config. + */ +function loadPresetDescriptor(descriptor) { + return { + type: "preset", + options: loadDescriptor(descriptor).value, + alias: descriptor.alias, + loc: descriptor.loc, + dirname: descriptor.dirname, + }; } /** - * Tries to load one preset. The input is either the module name of the preset, - * a function, or an object + * Validate and return the options object for the config. */ -function loadPresetObject(preset, options, meta) { - let presetFactory = preset; +function normalizeOptions(config) { + const alias = config.alias || "foreign"; + const type = config.type; - if (typeof presetFactory === "object" && presetFactory.__esModule) { - if (presetFactory.default) { - presetFactory = presetFactory.default; - } else { - throw new Error("Preset must export a default export when using ES6 modules."); - } + // + if (typeof config.options !== "object" || Array.isArray(config.options)) { + throw new TypeError(`Invalid options type for ${alias}`); } - // Allow simple object exports - if (typeof presetFactory === "object") { - return presetFactory; + // + const options = Object.assign({}, config.options); + + if (type !== "arguments") { + if (options.filename !== undefined) { + throw new Error(`${alias}.filename is only allowed as a root argument`); + } + + if (options.babelrc !== undefined) { + throw new Error(`${alias}.babelrc is only allowed as a root argument`); + } } - if (typeof presetFactory !== "function") { - // eslint-disable-next-line max-len - throw new Error(`Unsupported preset format: ${typeof presetFactory}. Expected preset to return a function.`); + if (type === "preset") { + if (options.only !== undefined) throw new Error(`${alias}.only is not supported in a preset`); + if (options.ignore !== undefined) throw new Error(`${alias}.ignore is not supported in a preset`); + if (options.extends !== undefined) throw new Error(`${alias}.extends is not supported in a preset`); + if (options.env !== undefined) throw new Error(`${alias}.env is not supported in a preset`); } - return presetFactory(context, options, meta); -} + if (options.sourceMap !== undefined) { + if (options.sourceMaps !== undefined) { + throw new Error(`Both ${alias}.sourceMap and .sourceMaps have been set`); + } + options.sourceMaps = options.sourceMap; + delete options.sourceMap; + } -const memoisedPlugins: Array<{ - container: Function; - plugin: Plugin; -}> = []; + for (const key in options) { + // check for an unknown option + if (!optionNames.has(key)) { + if (removed[key]) { + throw new ReferenceError(`Using removed Babel 5 option: ${alias}.${key} - ${removed[key].message}`); + } else { + // eslint-disable-next-line max-len + const unknownOptErr = `Unknown option: ${alias}.${key}. Check out http://babeljs.io/docs/usage/options/ for more information about options.`; -function memoisePluginContainer(fn, loc, i, alias) { - for (const cache of (memoisedPlugins: Array)) { - if (cache.container === fn) return cache.plugin; + throw new ReferenceError(unknownOptErr); + } + } } - let obj: ?PluginObject; + if (options.parserOpts && typeof options.parserOpts.parser === "string") { + options.parserOpts = Object.assign({}, options.parserOpts); + options.parserOpts.parser = loadParser(options.parserOpts.parser, config.dirname).value; + } - if (typeof fn === "function") { - obj = fn(context); - } else { - obj = fn; + if (options.generatorOpts && typeof options.generatorOpts.generator === "string") { + options.generatorOpts = Object.assign({}, options.generatorOpts); + options.generatorOpts.generator = loadGenerator(options.generatorOpts.generator, config.dirname).value; } - if (typeof obj !== "object") { - throw new TypeError(messages.get("pluginNotObject", loc, i, typeof obj) + loc + i); + if (config.options.presets && !Array.isArray(config.options.presets)) { + throw new Error(`${alias}.presets should be an array`); } - Object.keys(obj).forEach((key) => { - if (!ALLOWED_PLUGIN_KEYS.has(key)) { - throw new Error(messages.get("pluginInvalidProperty", loc, i, key)); + if (config.options.plugins && !Array.isArray(config.options.plugins)) { + throw new Error(`${alias}.plugins should be an array`); + } + + delete options.passPerPreset; + delete options.plugins; + delete options.presets; + + return options; +} + +/** + * Given a plugin/preset item, resolve it into a standard format. + */ +function normalizePair(pair, resolver, dirname) { + let options; + let value = pair; + if (Array.isArray(value)) { + if (value.length > 2) { + throw new Error(`Unexpected extra options ${JSON.stringify(value.slice(2))}.`); } - }); - if (obj.visitor && (obj.visitor.enter || obj.visitor.exit)) { - throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + - "Please target individual nodes."); + + [value, options] = value; } - obj = Object.assign({}, obj, { - visitor: clone(obj.visitor || {}), - }); + let filepath = null; + if (typeof value === "string") { + ({ + filepath, + value, + } = resolver(value, dirname)); + } - traverse.explode(obj.visitor); + 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 (obj.inherits) { - const inherited = normalisePlugin(obj.inherits, loc, "inherits"); + if (!value) { + throw new Error(`Unexpected falsy value: ${value}`); + } - obj.pre = chain(inherited.pre, obj.pre); - obj.post = chain(inherited.post, obj.post); - obj.manipulateOptions = chain(inherited.manipulateOptions, obj.manipulateOptions); - obj.visitor = traverse.visitors.merge([inherited.visitor, obj.visitor]); + const type = typeof value; + if (type !== "object" && type !== "function") { + throw new Error(`Unsupported format: ${type}. Expected an object or a function.`); } - const plugin = new Plugin(obj, alias); - memoisedPlugins.push({ - container: fn, - plugin: plugin, - }); - return plugin; + return { filepath, value, options }; } function chain(a, b) { @@ -457,18 +455,3 @@ function createBareOptions() { highlightCode: true, }; } - -function normalisePlugin(plugin, loc, i, alias) { - plugin = plugin.__esModule ? plugin.default : plugin; - - if (!(plugin instanceof Plugin)) { - // allow plugin containers to be specified so they don't have to manually require - if (typeof plugin === "function" || typeof plugin === "object") { - plugin = memoisePluginContainer(plugin, loc, i, alias); - } else { - throw new TypeError(messages.get("pluginNotFunction", loc, i, typeof plugin)); - } - } - - return plugin; -} diff --git a/packages/babel-core/test/api.js b/packages/babel-core/test/api.js index 2f7a07819210..e86f2a54af1e 100644 --- a/packages/babel-core/test/api.js +++ b/packages/babel-core/test/api.js @@ -146,7 +146,7 @@ describe("api", function () { plugins: [__dirname + "/../../babel-plugin-syntax-jsx", false], }); }, - /TypeError: \[BABEL\] unknown: Falsy value found in plugins/ + /Error: \[BABEL\] unknown: Unexpected falsy value: false/ ); }); diff --git a/packages/babel-core/test/option-manager.js b/packages/babel-core/test/option-manager.js index d9caef2cdf0c..cf2fcaa48417 100644 --- a/packages/babel-core/test/option-manager.js +++ b/packages/babel-core/test/option-manager.js @@ -45,7 +45,7 @@ describe("option-manager", () => { "presets": [path.join(__dirname, "fixtures/option-manager/not-a-preset")], }); }, - /While processing preset: .*option-manager(?:\/|\\\\)not-a-preset\.js/ + /While processing: .*option-manager(?:\/|\\\\)not-a-preset\.js/ ); }); }); @@ -77,8 +77,8 @@ describe("option-manager", () => { presetTest("es2015_default_function"); presetTest("es2015_default_object"); - presetThrowsTest("es2015_named", /Preset must export a default export when using ES6 modules/); - presetThrowsTest("es2015_invalid", /Unsupported preset format: string/); - presetThrowsTest("es5_invalid", /Unsupported preset format: string/); + presetThrowsTest("es2015_named", /Must export a default export when using ES6 modules/); + presetThrowsTest("es2015_invalid", /Unsupported format: string/); + presetThrowsTest("es5_invalid", /Unsupported format: string/); }); }); diff --git a/packages/babel-messages/src/index.js b/packages/babel-messages/src/index.js index aefd7d46a08d..53b559d437f5 100644 --- a/packages/babel-messages/src/index.js +++ b/packages/babel-messages/src/index.js @@ -40,7 +40,7 @@ export const MESSAGES = { pluginNotObject: "Plugin $2 specified in $1 was expected to return an object when invoked but returned $3", pluginNotFunction: "Plugin $2 specified in $1 was expected to return a function but returned $3", pluginUnknown: "Unknown plugin $1 specified in $2 at $3, attempted to resolve relative to $4", - pluginInvalidProperty: "Plugin $2 specified in $1 provided an invalid property of $3", + pluginInvalidProperty: "Plugin $1 provided an invalid property of $3", }; /** From 248c2409980b3020df44457b14c32e9b68fd85df Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Thu, 6 Apr 2017 23:09:31 -0700 Subject: [PATCH 11/11] Use clearer function name. --- packages/babel-core/src/config/option-manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index f1f8838a0c61..caf3ead14555 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -76,7 +76,7 @@ export default function manageOptions(opts?: Object) { class OptionManager { constructor() { - this.options = createBareOptions(); + this.options = createInitialOptions(); this.passes = [[]]; } @@ -442,7 +442,7 @@ function chain(a, b) { }; } -function createBareOptions() { +function createInitialOptions() { return { sourceType: "module", babelrc: true,