From 2ada4a7bf0f71b3e52bf5fc9ca863875b7813223 Mon Sep 17 00:00:00 2001 From: Boopathi Rajaa Date: Mon, 19 Sep 2016 19:33:38 +0200 Subject: [PATCH] Minify Options + (Close #54) Extract OptionsManager to a separate file Update keepFnName and snapshots Add regexp constructors rewrite options manager Fix lint Remove deadcode --- .../__snapshots__/options-tests.js.snap | 322 ++++++++++++++++++ .../__tests__/options-tests.js | 103 ++++++ packages/babel-preset-babili/package.json | 5 +- packages/babel-preset-babili/src/index.js | 128 +++++-- .../src/options-manager.js | 299 ++++++++++++++++ 5 files changed, 833 insertions(+), 24 deletions(-) create mode 100644 packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap create mode 100644 packages/babel-preset-babili/__tests__/options-tests.js create mode 100644 packages/babel-preset-babili/src/options-manager.js diff --git a/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap b/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap new file mode 100644 index 000000000..80879ed4c --- /dev/null +++ b/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap @@ -0,0 +1,322 @@ +exports[`preset-options should handle groups - remove entire group 1`] = ` +Object { + "input": Object { + "unsafe": false, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-minify-infinity", + "babel-plugin-minify-mangle-names", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should handle individual items in a group of options 1`] = ` +Object { + "input": Object { + "mangle": false, + "unsafe": Object { + "flipComparisons": false, + }, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should handle options that are delegated to multiple other options 1`] = ` +Object { + "input": Object { + "keepFnName": false, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + Array [ + "babel-plugin-minify-dead-code-elimination", + Object { + "keepFnName": false, + }, + ], + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + Array [ + "babel-plugin-minify-mangle-names", + Object { + "keepFnName": false, + }, + ], + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should handle options that are delegated to multiple other options 2`] = ` +Object { + "input": Object { + "keepFnName": true, + "mangle": Object { + "blacklist": Array [ + "foo", + "bar", + ], + }, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + Array [ + "babel-plugin-minify-dead-code-elimination", + Object { + "keepFnName": true, + }, + ], + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + Array [ + "babel-plugin-minify-mangle-names", + Object { + "blacklist": Array [ + "foo", + "bar", + ], + "keepFnName": true, + }, + ], + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should handle options that are delegated to multiple other options 3`] = ` +Object { + "input": Object { + "keepFnName": true, + "mangle": Object { + "blacklist": Array [ + "baz", + ], + "keepFnName": false, + }, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + Array [ + "babel-plugin-minify-dead-code-elimination", + Object { + "keepFnName": true, + }, + ], + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + Array [ + "babel-plugin-minify-mangle-names", + Object { + "blacklist": Array [ + "baz", + ], + "keepFnName": false, + }, + ], + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should handle simple options 1`] = ` +Object { + "input": Object { + "deadcode": false, + "mangle": false, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should pass options to respective plugin when its an object 1`] = ` +Object { + "input": Object { + "mangle": Object { + "blacklist": Array [ + "foo", + "bar", + ], + }, + }, + "output": Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + Array [ + "babel-plugin-minify-mangle-names", + Object { + "blacklist": Array [ + "foo", + "bar", + ], + }, + ], + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", + ], +} +`; + +exports[`preset-options should return defaults with no options 1`] = ` +Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + "babel-plugin-minify-mangle-names", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", +] +`; + +exports[`preset-options should return defaults with no options 2`] = ` +Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + "babel-plugin-minify-mangle-names", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", +] +`; + +exports[`preset-options should return defaults with no options 3`] = ` +Array [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + "babel-plugin-minify-mangle-names", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-undefined", +] +`; diff --git a/packages/babel-preset-babili/__tests__/options-tests.js b/packages/babel-preset-babili/__tests__/options-tests.js new file mode 100644 index 000000000..381684bc7 --- /dev/null +++ b/packages/babel-preset-babili/__tests__/options-tests.js @@ -0,0 +1,103 @@ +jest.autoMockOff(); + +const mocks = [ + "babel-plugin-minify-constant-folding", + "babel-plugin-minify-dead-code-elimination", + "babel-plugin-minify-flip-comparisons", + "babel-plugin-transform-simplify-comparison-operators", + "babel-plugin-minify-guarded-expressions", + "babel-plugin-minify-type-constructors", + "babel-plugin-minify-infinity", + "babel-plugin-minify-mangle-names", + "babel-plugin-minify-numeric-literals", + "babel-plugin-minify-replace", + "babel-plugin-minify-simplify", + "babel-plugin-transform-member-expression-literals", + "babel-plugin-transform-property-literals", + "babel-plugin-transform-merge-sibling-variables", + "babel-plugin-transform-minify-booleans", + "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-regexp-constructors", + "babel-plugin-transform-remove-debugger", + "babel-plugin-transform-remove-console", + "babel-plugin-transform-remove-undefined", +]; + +mocks.forEach((mockName) => { + // it's called mockName for jest(babel-jest-plugin) workaround + jest.mock(mockName, () => mockName); +}); + +const preset = require("../src/index"); + +function getPlugins(opts) { + return preset({}, opts).plugins; +} + +function testOpts(opts) { + expect({ + input: opts, + output: getPlugins(opts) + }).toMatchSnapshot(); +} + +describe("preset-options", () => { + it("should be a function", () => { + expect(typeof preset).toBe("function"); + }); + + it("should return defaults with no options", () => { + expect(getPlugins()).toMatchSnapshot(); + expect(getPlugins({})).toMatchSnapshot(); + expect(getPlugins(null)).toMatchSnapshot(); + }); + + it("should handle simple options", () => { + testOpts({ + mangle: false, + deadcode: false + }); + }); + + it("should pass options to respective plugin when its an object", () => { + testOpts({ + mangle: { + blacklist: ["foo", "bar"] + } + }); + }); + + it("should handle groups - remove entire group", () => { + testOpts({ + unsafe: false + }); + }); + + it("should handle individual items in a group of options", () => { + testOpts({ + unsafe: { + flipComparisons: false + }, + mangle: false + }); + }); + + it("should handle options that are delegated to multiple other options", () => { + testOpts({ + keepFnName: false + }); + testOpts({ + keepFnName: true, + mangle: { + blacklist: ["foo", "bar"] + } + }); + testOpts({ + keepFnName: true, + mangle: { + blacklist: ["baz"], + keepFnName: false + } + }); + }); +}); diff --git a/packages/babel-preset-babili/package.json b/packages/babel-preset-babili/package.json index ea2ae1fb2..44d78b2ce 100644 --- a/packages/babel-preset-babili/package.json +++ b/packages/babel-preset-babili/package.json @@ -29,7 +29,10 @@ "babel-plugin-transform-regexp-constructors": "^0.0.1", "babel-plugin-transform-remove-undefined": "^0.0.1", "babel-plugin-transform-simplify-comparison-operators": "^6.8.0", - "babel-plugin-transform-undefined-to-void": "^6.8.0" + "babel-plugin-transform-undefined-to-void": "^6.8.0", + "babel-plugin-transform-remove-debugger": "^6.8.0", + "babel-plugin-transform-remove-console": "^6.8.0", + "lodash.isplainobject": "^4.0.6" }, "devDependencies": {} } diff --git a/packages/babel-preset-babili/src/index.js b/packages/babel-preset-babili/src/index.js index 0f5cafd98..32622df07 100644 --- a/packages/babel-preset-babili/src/index.js +++ b/packages/babel-preset-babili/src/index.js @@ -1,23 +1,105 @@ -module.exports = { - minified: true, - plugins: [ - require("babel-plugin-minify-constant-folding"), - require("babel-plugin-minify-dead-code-elimination"), - require("babel-plugin-minify-flip-comparisons"), - require("babel-plugin-minify-guarded-expressions"), - require("babel-plugin-minify-infinity"), - require("babel-plugin-minify-mangle-names"), - require("babel-plugin-minify-numeric-literals"), - require("babel-plugin-minify-replace"), - require("babel-plugin-minify-simplify"), - require("babel-plugin-minify-type-constructors"), - require("babel-plugin-transform-member-expression-literals"), - require("babel-plugin-transform-merge-sibling-variables"), - require("babel-plugin-transform-minify-booleans"), - require("babel-plugin-transform-property-literals"), - require("babel-plugin-transform-regexp-constructors"), - require("babel-plugin-transform-remove-undefined"), - require("babel-plugin-transform-simplify-comparison-operators"), - require("babel-plugin-transform-undefined-to-void"), - ], -}; +const isPlainObject = require("lodash.isplainobject"); +const {group, option, proxy, generate} = require("./options-manager"); + +// the flat plugin map +// This is to prevent dynamic requires - require('babel-plugin-' + name); +// as it suffers during bundling of this code with webpack/browserify +const PLUGINS = [ + ["evaluate", require("babel-plugin-minify-constant-folding"), true], + ["deadcode", require("babel-plugin-minify-dead-code-elimination"), true], + ["flipComparisons", require("babel-plugin-minify-flip-comparisons"), true], + ["guards", require("babel-plugin-minify-guarded-expressions"), true], + ["infinity", require("babel-plugin-minify-infinity"), true], + ["mangle", require("babel-plugin-minify-mangle-names"), true], + ["numericLiterals", require("babel-plugin-minify-numeric-literals"), true], + ["replace", require("babel-plugin-minify-replace"), true], + ["simplify", require("babel-plugin-minify-simplify"), true], + ["typeConstructors", require("babel-plugin-minify-type-constructors"), true], + ["memberExpressions", require("babel-plugin-transform-member-expression-literals"), true], + ["mergeVars", require("babel-plugin-transform-merge-sibling-variables"), true], + ["booleans", require("babel-plugin-transform-minify-booleans"), true], + ["propertyLiterals", require("babel-plugin-transform-property-literals"), true], + ["regexpConstructors", require("babel-plugin-transform-regexp-constructors"), true], + ["removeConsole", require("babel-plugin-transform-remove-console"), false], + ["removeDebugger", require("babel-plugin-transform-remove-debugger"), false], + ["removeUndefined", require("babel-plugin-transform-remove-undefined"), true], + ["simplifyComparisons", require("babel-plugin-transform-simplify-comparison-operators"), true], + ["undefinedToVoid", require("babel-plugin-transform-undefined-to-void"), true], +]; + +module.exports = preset; + +function preset(context, _opts = {}) { + const opts = isPlainObject(_opts) ? _opts : {}; + + // to track every plugin is used + let usedPlugins = new Set; + + const optionsMap = PLUGINS + .map((plugin) => option(plugin[0], plugin[1], plugin[2])) + .reduce((acc, cur) => { + Object.defineProperty(acc, cur.name, { + get() { + usedPlugins.add(cur.name); + return cur; + } + }); + return acc; + }, {}); + + const optionsTree = group( + "options", + [ + optionsMap.evaluate, + optionsMap.deadcode, + + group("unsafe", [ + optionsMap.flipComparisons, + optionsMap.simplifyComparisons, + optionsMap.guards, + optionsMap.typeConstructors, + ]), + + optionsMap.infinity, + optionsMap.mangle, + optionsMap.numericLiterals, + optionsMap.replace, + optionsMap.simplify, + + group("properties", [ + optionsMap.memberExpressions, + optionsMap.propertyLiterals, + ]), + + optionsMap.mergeVars, + optionsMap.booleans, + optionsMap.undefinedToVoid, + optionsMap.regexpConstructors, + + optionsMap.removeConsole, + optionsMap.removeDebugger, + optionsMap.removeUndefined, + + proxy("keepFnName", [ + optionsMap.mangle, + optionsMap.deadcode + ]) + ], + "some" + ); + + // verify all plugins are used + if (usedPlugins.size !== PLUGINS.length) { + const unusedPlugins = PLUGINS + .filter((plugin) => !usedPlugins.has(plugin[0])) + .map((plugin) => plugin[0]); + throw new Error("Some imported plugins unused\n" + unusedPlugins); + } + + const plugins = generate(optionsTree, opts); + + return { + minified: true, + plugins, + }; +} diff --git a/packages/babel-preset-babili/src/options-manager.js b/packages/babel-preset-babili/src/options-manager.js new file mode 100644 index 000000000..404ae20a7 --- /dev/null +++ b/packages/babel-preset-babili/src/options-manager.js @@ -0,0 +1,299 @@ +const isPlainObject = require("lodash.isplainobject"); + +/** + * Options Manager + * + * Input Options: Object + * Output: Array of plugins enabled with their options + * + * Handles multiple types of input option keys + * + * 1. boolean and object values + * { mangle: true } // should enable mangler + * { mangle: { blacklist: ["foo"] } } // should enabled mangler + * // and pass obj to mangle plugin + * + * 2. group + * { unsafe: true } // should enable all plugins under unsafe + * { unsafe: { flip: false } } // should disable flip-comparisons plugin + * // and other plugins should take their defaults + * { unsafe: { simplify: {multipass: true}}} // should pass obj to simplify + * // other plugins take defaults + * + * 3. same option passed on to multiple plugins + * { keepFnames: false } // should be passed on to mangle & dce + * // without disturbing their own options + */ + +module.exports = { + option, + proxy, + group, + generate, + resolveOptions, + generateResult, +}; + +/** + * Generate the plugin list from option tree and inputOpts + */ +function generate(optionTree, inputOpts) { + return generateResult( + resolveOptions(optionTree, inputOpts) + ); +} + +/** + * Generate plugin list from the resolvedOptionTree + * where resolvedOptionTree = for every node, node.resolved = true; + */ +function generateResult(resolvedOpts) { + const options = resolvedOpts.children; + const result = []; + + for (let i = 0; i < options.length; i++) { + const option = options[i]; + + switch (option.type) { + case "option": + if (option.resolvedValue) { + result.push(option.resolvedValue); + } + break; + case "group": + result.push(...generateResult(option)); + break; + } + } + + return result; +} + +/** + * Traverses input @param{optionTree} and adds resolvedValue + * calculated from @param{inputOpts} for each Node in the tree + */ +function resolveOptions(optionTree, inputOpts = {}) { + const options = optionTree.children; + + // a queue to resolve proxies at the end after all options groups are resolved + const proxiesToResolve = []; + + for (let i = 0; i < options.length; i++) { + const option = options[i]; + switch (option.type) { + case "option": + resolveTypeOption(option, inputOpts); + break; + + case "group": + resolveTypeGroup(option, inputOpts); + break; + + case "proxy": + if (!hop(inputOpts, option.name)) { + break; + } + proxiesToResolve.push(option); + break; + + default: + throw new TypeError("Option type not supported - " + option.type); + } + } + + // resolve proxies + for (let i = 0; i < proxiesToResolve.length; i++) { + const proxy = proxiesToResolve[i]; + for (let j = 0; j < proxy.to.length; j++) { + const option = proxy.to[j]; + switch (option.type) { + case "option": + resolveTypeProxyToOption(proxy, option, inputOpts); + break; + + case "group": + case "proxy": + throw new Error(`proxy option cannot proxy to group/proxy. ${proxy.name} proxied to ${option.name}`); + + default: + throw new Error("Unsupported option type ${option.name}"); + } + } + } + + // return the same tree after modifications + return optionTree; +} + +/** + * Resolve the type - simple option using the @param{inputOpts} + */ +function resolveTypeOption(option, inputOpts) { + option.resolved = true; + + // option does NOT exist in inputOpts + if (!hop(inputOpts, option.name)) { + // default value + option.resolvedValue = option.defaultValue ? option.resolvingValue : null; + return; + } + + // Object + // { mangle: { blacklist: ["foo", "bar"] } } + if (isPlainObject(inputOpts[option.name])) { + option.resolvedValue = [option.resolvingValue, inputOpts[option.name]]; + return; + } + + // any other truthy value, just enables the plugin + // { mangle: true } + if (inputOpts[option.name]) { + option.resolvedValue = option.resolvingValue; + return; + } + + // disabled + option.resolvedValue = null; +} + +/** + * Resolve the group using @param{inputOpts} + */ +function resolveTypeGroup(option, inputOpts) { + option.resolved = true; + + // option does NOT exist in inputOpts + if (!hop(inputOpts, option.name)) { + const newInputOpts = option + .children + .filter((opt) => opt.type !== "proxy") + .reduce((acc, cur) => { + let value; + switch (option.defaultValue) { + case "all": value = true; break; + case "some": value = cur.defaultValue; break; + case "none": value = false; break; + default: throw new Error(`Unsupported defaultValue - ${option.defaultValue} for option ${option.name}`); + } + return Object.assign({}, acc, { + [cur.name]: value, + }); + }, {}); + + // recurse + resolveOptions(option, newInputOpts); + return; + } + + // has individual options for items in group + // { unsafe: { flipComparisons: true } } + if (isPlainObject(inputOpts[option.name])) { + resolveOptions(option, inputOpts[option.name]); + return; + } + + // else + // { unsafe: } + const newInputOpts = option + .children + .filter((opt) => opt.type !== "proxy") + .reduce((acc, cur) => Object.assign({}, acc, { + // if the input is truthy, enable all, else disable all + [cur.name]: !!inputOpts[option.name] + }), {}); + resolveOptions(option, newInputOpts); +} + +/** + * Resolve proxies and update the already resolved Options + */ +function resolveTypeProxyToOption(proxy, option, inputOpts) { + if (!option.resolved) { + throw new Error("Proxies cannot be applied before the original option is resolved"); + } + + // option is disabled + if (!option.resolvedValue) { + return; + } + + // option doesn't contain any option on its own + if (option.resolvedValue === option.resolvingValue) { + option.resolvedValue = [option.resolvedValue, { + [proxy.name]: inputOpts[proxy.name] + }]; + } + + // option already has its own set of options to be passed to plugins + else if (Array.isArray(option.resolvedValue) && option.resolvedValue.length === 2) { + // proxies should not override + if (!hop(option.resolvedValue[1], proxy.name)) { + option.resolvedValue = [ + option.resolvingValue, + Object.assign({}, option.resolvedValue[1], { + [proxy.name]: inputOpts[proxy.name] + }) + ]; + } + } + + // plugin is invalid + else { + throw new Error(`Invalid resolved value for option ${option.name}`); + } +} + +// create an option of type simple option +function option(name, resolvingValue, defaultValue = true) { + assertName(name); + if (!resolvingValue) { + // as plugins are truthy values + throw new Error("Only truthy resolving values are supported"); + } + return { + type: "option", + name, + resolvingValue, + defaultValue, + }; +} + +// create an option of type proxy +function proxy(name, to) { + assertName(name); + assertArray(name, "to", to); + return { + type: "proxy", + name, + to, + }; +} + +// create an option of type - group of options +function group(name, children, defaultValue = "some") { + assertName(name); + assertArray(name, "children", children); + return { + type: "group", + name, + children: children.filter((x) => !!x), + defaultValue, + }; +} + +function hop(o, key) { + return Object.hasOwnProperty.call(o, key); +} + +function assertArray(name, prop, arr) { + if (!Array.isArray(arr)) { + throw new Error(`Expected ${prop} to be an array in option ${name}`); + } +} + +function assertName(name) { + if (!name) { + throw new Error("Invalid option name " + name); + } +}