From dd7dc6764af456d10b2764b3f1520cd3adf8a913 Mon Sep 17 00:00:00 2001 From: Boopathi Rajaa Date: Fri, 11 Aug 2017 14:18:33 +0200 Subject: [PATCH] Node API and CLI (#507) * Node API and CLI update Add yargs-parser Add key not exist case in options parser Remove deadcode Cleanup options parser Improve filtering input CLI options * file handling in cli cli options and fs add out-file, out-dir and accept alias add small validation for cli set up cli tests and help option add help options and fix stdin fix test cases Add tests for cli file/dir and help option add eslintrc for tests ignore fixtures in test suite update package deps small changes for stdin * Async fs Fix up Simplify Fix lint handle default case Fix some tests * move to spawnSync from exec to capture stderr, stdout * Move tests to async Format jest snapshots Fix yarn deps * Add node api tests Remove outFile and outDir from options passed to babili Update snapshot * use stdin stream and api test Update package.json --- .babelrc | 13 +- .eslintrc.js | 2 +- package.json | 9 +- .../__tests__/__snapshots__/cli-tests.js.snap | 31 +++ .../__snapshots__/node-api-tests.js.snap | 5 + packages/babili/__tests__/cli-tests.js | 103 +++++++ packages/babili/__tests__/fixtures/.eslintrc | 6 + .../__tests__/fixtures/out-dir/a/foo.js | 1 + .../babili/__tests__/fixtures/out-file/foo.js | 5 + packages/babili/__tests__/node-api-tests.js | 26 ++ packages/babili/bin/babili.js | 2 +- packages/babili/package.json | 13 +- packages/babili/src/cli.js | 252 ++++++++++++++++++ packages/babili/src/fs.js | 144 ++++++++++ packages/babili/src/index.js | 39 ++- packages/babili/src/options-parser.js | 65 +++++ yarn.lock | 99 +++---- 17 files changed, 732 insertions(+), 83 deletions(-) create mode 100644 packages/babili/__tests__/__snapshots__/cli-tests.js.snap create mode 100644 packages/babili/__tests__/__snapshots__/node-api-tests.js.snap create mode 100644 packages/babili/__tests__/cli-tests.js create mode 100644 packages/babili/__tests__/fixtures/.eslintrc create mode 100644 packages/babili/__tests__/fixtures/out-dir/a/foo.js create mode 100644 packages/babili/__tests__/fixtures/out-file/foo.js create mode 100644 packages/babili/__tests__/node-api-tests.js create mode 100644 packages/babili/src/cli.js create mode 100644 packages/babili/src/fs.js create mode 100644 packages/babili/src/options-parser.js diff --git a/.babelrc b/.babelrc index 6e6df8faf..47125d57a 100644 --- a/.babelrc +++ b/.babelrc @@ -1,9 +1,14 @@ { "presets": [ - ["env", { - "targets": { - "node": 4 + [ + "env", + { + "targets": { + "node": 4 + }, + "include": ["transform-async-to-generator"], + "exclude": ["transform-regenerator"] } - }] + ] ] } diff --git a/.eslintrc.js b/.eslintrc.js index 71efe17ac..ba766ca76 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,7 @@ const OFF = "off"; module.exports = { extends: "eslint:recommended", parserOptions: { - ecmaVersion: 7, + ecmaVersion: 2017, sourceType: "module" }, env: { diff --git a/package.json b/package.json index cd0eaadca..402d44f8c 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,13 @@ ".*": "/node_modules/babel-jest" }, "testEnvironment": "node", + "testPathIgnorePatterns": [ + "/node_modules/", + "/fixtures/" + ], "roots": [ "packages" ], - "transformIgnorePatterns": [ - "/node_modules/" - ], "coverageDirectory": "./coverage/" }, "devDependencies": { @@ -51,6 +52,7 @@ "codecov": "^2.3.0", "commander": "^2.11.0", "eslint": "^4.4.0", + "fs-readdir-recursive": "^1.0.0", "google-closure-compiler-js": "^20170626.0.0", "gulp": "github:gulpjs/gulp#4.0", "gulp-babel": "^6.1.2", @@ -62,6 +64,7 @@ "markdown-table": "^1.1.1", "prettier": "^1.5.3", "request": "^2.81.0", + "rimraf": "^2.6.1", "through2": "^2.0.3", "uglify-js": "^3.0.27" }, diff --git a/packages/babili/__tests__/__snapshots__/cli-tests.js.snap b/packages/babili/__tests__/__snapshots__/cli-tests.js.snap new file mode 100644 index 000000000..075c88c8c --- /dev/null +++ b/packages/babili/__tests__/__snapshots__/cli-tests.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Babili CLI input dir + outdir 1`] = `"let foo=10;"`; + +exports[`Babili CLI input file + outDir 1`] = `"function foo(){const a=x(1),b=y(2);return z(a,b)}"`; + +exports[`Babili CLI input file + outFile 1`] = `"function foo(){const a=x(1),b=y(2);return z(a,b)}"`; + +exports[`Babili CLI input file + stdout 1`] = ` +Object { + "stderr": "", + "stdout": "function foo(){const a=x(1),b=y(2);return z(a,b)}", +} +`; + +exports[`Babili CLI should throw on all invalid options 1`] = ` +Object { + "code": 1, + "stderr": "Error: Invalid Options passed: foo,bar +", +} +`; + +exports[`Babili CLI stdin + outFile 1`] = `"function foo(){const a=x(1),b=y(2);return z(a,b)}"`; + +exports[`Babili CLI stdin + stdout 1`] = ` +Object { + "stderr": "", + "stdout": "function a(){const a=x(1),b=y(2);return z(a,b)}", +} +`; diff --git a/packages/babili/__tests__/__snapshots__/node-api-tests.js.snap b/packages/babili/__tests__/__snapshots__/node-api-tests.js.snap new file mode 100644 index 000000000..02bcca18a --- /dev/null +++ b/packages/babili/__tests__/__snapshots__/node-api-tests.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Babili Node API override default babili options 1`] = `"function foo(){const bar=x(1),baz=y(2);return z(bar,baz)}"`; + +exports[`Babili Node API simple usage 1`] = `"function foo(){const a=x(1),b=y(2);return z(a,b)}"`; diff --git a/packages/babili/__tests__/cli-tests.js b/packages/babili/__tests__/cli-tests.js new file mode 100644 index 000000000..31bdb48d9 --- /dev/null +++ b/packages/babili/__tests__/cli-tests.js @@ -0,0 +1,103 @@ +jest.autoMockOff(); + +const { spawn } = require("child_process"); +const path = require("path"); +const fs = require("fs"); +const promisify = require("util.promisify"); +const rimraf = require("rimraf"); +const babiliCli = require.resolve("../bin/babili"); + +const readFileAsync = promisify(fs.readFile); +const readFile = file => readFileAsync(file).then(out => out.toString()); +const unlink = promisify(rimraf); + +function runCli(args = [], stdin) { + return new Promise((resolve, reject) => { + const child = spawn(babiliCli, args, { + stdio: [stdin ? "pipe" : "inherit", "pipe", "pipe"], + shell: true + }); + + if (stdin) { + child.stdin.end(stdin); + } + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", data => (stdout += data)); + child.stderr.on("data", data => (stderr += data)); + child.on( + "close", + code => + code === 0 ? resolve({ stdout, stderr }) : reject({ code, stderr }) + ); + }); +} + +let tempSource = ` +function foo() { + const bar = x(1); + const baz = y(2); + return z(bar, baz); +} +`; + +const sampleInputFile = path.join(__dirname, "fixtures/out-file/foo.js"); +const sampleInputDir = path.join(__dirname, "fixtures/out-dir/a"); + +const tempOutFile = path.join(__dirname, "fixtures/out-file/foo.min.js"); +const tempOutDir = path.join(__dirname, "fixtures/out-dir/min"); +const tempOutDirFile = path.join(__dirname, "fixtures/out-dir/min/foo.js"); + +describe("Babili CLI", () => { + afterEach(async () => { + await unlink(tempOutDir); + await unlink(tempOutFile); + }); + + it("should show help for --help", () => { + return expect(runCli(["--help"])).resolves.toBeDefined(); + }); + + it("should show version for --version", () => { + const { version } = require("../package"); + return expect( + runCli(["--version"]).then(({ stdout }) => stdout.trim()) + ).resolves.toBe(version); + }); + + it("should throw on all invalid options", () => { + return expect(runCli(["--foo", "--bar"])).rejects.toMatchSnapshot(); + }); + + it("stdin + stdout", () => { + return expect( + runCli(["--mangle.topLevel"], tempSource) + ).resolves.toMatchSnapshot(); + }); + + it("stdin + outFile", async () => { + await runCli(["--out-file", tempOutFile], tempSource); + expect(await readFile(tempOutFile)).toMatchSnapshot(); + }); + + it("input file + stdout", async () => { + return expect(runCli([sampleInputFile])).resolves.toMatchSnapshot(); + }); + + it("input file + outFile", async () => { + await runCli([sampleInputFile, "--out-file", tempOutFile]); + expect(await readFile(tempOutFile)).toMatchSnapshot(); + }); + + it("input file + outDir", async () => { + await runCli([sampleInputFile, "--out-dir", tempOutDir]); + expect(await readFile(tempOutDirFile)).toMatchSnapshot(); + }); + + it("input dir + outdir", async () => { + await runCli([sampleInputDir, "--out-dir", tempOutDir]); + expect(await readFile(tempOutDirFile)).toMatchSnapshot(); + }); +}); diff --git a/packages/babili/__tests__/fixtures/.eslintrc b/packages/babili/__tests__/fixtures/.eslintrc new file mode 100644 index 000000000..35aae7b37 --- /dev/null +++ b/packages/babili/__tests__/fixtures/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-unused-vars": "off", + "no-undef": "off" + } +} diff --git a/packages/babili/__tests__/fixtures/out-dir/a/foo.js b/packages/babili/__tests__/fixtures/out-dir/a/foo.js new file mode 100644 index 000000000..1fda16226 --- /dev/null +++ b/packages/babili/__tests__/fixtures/out-dir/a/foo.js @@ -0,0 +1 @@ +let foo = 10; diff --git a/packages/babili/__tests__/fixtures/out-file/foo.js b/packages/babili/__tests__/fixtures/out-file/foo.js new file mode 100644 index 000000000..8232e690b --- /dev/null +++ b/packages/babili/__tests__/fixtures/out-file/foo.js @@ -0,0 +1,5 @@ +function foo() { + const bar = x(1); + const baz = y(2); + return z(bar, baz); +} diff --git a/packages/babili/__tests__/node-api-tests.js b/packages/babili/__tests__/node-api-tests.js new file mode 100644 index 000000000..857471fd5 --- /dev/null +++ b/packages/babili/__tests__/node-api-tests.js @@ -0,0 +1,26 @@ +jest.autoMockOff(); + +const babili = require("../src/index"); + +const sampleInput = ` +function foo() { + const bar = x(1); + const baz = y(2); + return z(bar, baz); +} +`; + +describe("Babili Node API", () => { + it("simple usage", () => { + expect(babili(sampleInput).code).toMatchSnapshot(); + }); + + it("throw on invalid options", () => { + expect(() => babili(sampleInput, { foo: false, bar: true }).code).toThrow(); + }); + + it("override default babili options", () => { + const babiliOpts = { mangle: false }; + expect(babili(sampleInput, babiliOpts).code).toMatchSnapshot(); + }); +}); diff --git a/packages/babili/bin/babili.js b/packages/babili/bin/babili.js index 74638147f..b465f9fa3 100755 --- a/packages/babili/bin/babili.js +++ b/packages/babili/bin/babili.js @@ -1,3 +1,3 @@ #!/usr/bin/env node -require("../lib/index"); +require("../lib/cli"); diff --git a/packages/babili/package.json b/packages/babili/package.json index 3696697a0..ba45b4c73 100644 --- a/packages/babili/package.json +++ b/packages/babili/package.json @@ -11,11 +11,14 @@ "bin": { "babili": "./bin/babili.js" }, - "keywords": [ - "babel-preset" - ], + "keywords": ["babel-preset"], "dependencies": { - "babel-cli": "^6.24.1", - "babel-preset-babili": "^0.1.4" + "babel-core": "^6.24.1", + "babel-preset-babili": "^0.1.4", + "fs-readdir-recursive": "^1.0.0", + "mkdirp": "^0.5.1", + "output-file-sync": "^2.0.0", + "util.promisify": "^1.0.0", + "yargs-parser": "^5.0.0" } } diff --git a/packages/babili/src/cli.js b/packages/babili/src/cli.js new file mode 100644 index 000000000..50cb747f8 --- /dev/null +++ b/packages/babili/src/cli.js @@ -0,0 +1,252 @@ +const yargsParser = require("yargs-parser"); +const optionsParser = require("./options-parser"); +const { version } = require("../package.json"); +const { handleStdin, handleFile, handleArgs, isFile } = require("./fs"); + +const plugins = [ + "booleans", + "builtIns", + "consecutiveAdds", + "deadcode", + "evaluate", + "flipComparisons", + "guards", + "infinity", + "mangle", + "memberExpressions", + "mergeVars", + "numericLiterals", + "propertyLiterals", + "regexpConstructors", + "removeConsole", + "removeDebugger", + "removeUndefined", + "replace", + "simplify", + "simplifyComparisons", + "typeConstructors", + "undefinedToVoid" +]; + +const proxies = ["keepFnName", "keepClassName"]; + +const dceBooleanOpts = [ + "deadcode.keepFnName", + "deadcode.keepFnArgs", + "deadcode.keepClassName" +]; + +const mangleBooleanOpts = [ + "mangle.eval", + "mangle.keepFnName", + "mangle.topLevel", + "mangle.keepClassName" +]; + +const mangleArrayOpts = ["mangle.blacklist"]; + +const typeConsOpts = [ + "typeConstructors.array", + "typeConstructors.boolean", + "typeConstructors.number", + "typeConstructors.object", + "typeConstructors.string" +]; + +const cliBooleanOpts = ["help", "version"]; +const cliOpts = ["out-file", "out-dir"]; +const alias = { + outFile: "o", + outDir: "d", + version: "V" +}; + +function aliasArr(obj) { + const r = Object.keys(obj).reduce((acc, val) => { + return acc.concat(val, obj[val]); + }, []); + return r; +} + +function printHelpInfo({ exitCode = 0 } = {}) { + const msg = ` + Usage: babili index.js [options] + + Options: + --out-file, -o Output to a specific file + --out-dir, -d Output to a specific directory + --mangle Context and scope aware variable renaming + --simplify Simplifies code for minification by reducing statements into + expressions + --booleans Transform boolean literals into !0 for true and !1 for false + --builtIns Minify standard built-in objects + --consecutiveAdds Inlines consecutive property assignments, array pushes, etc. + --deadcode Inlines bindings and tries to evaluate expressions. + --evaluate Tries to evaluate expressions and inline the result. Deals + with numbers and strings + --flipComparisons Optimize code for repetition-based compression algorithms + such as gzip. + --infinity Minify Infinity to 1/0 + --memberExpressions Convert valid member expression property literals into plain + identifiers + --mergeVars Merge sibling variables into single variable declaration + --numericLiterals Shortening of numeric literals via scientific notation + --propertyLiterals Transform valid identifier property key literals into identifiers + --regexpConstructors Change RegExp constructors into literals + --removeConsole Removes all console.* calls + --removeDebugger Removes all debugger statements + --removeUndefined Removes rval's for variable assignments, return arguments from + functions that evaluate to undefined + --replace Replaces matching nodes in the tree with a given replacement node + --simplifyComparisons Convert === and !== to == and != if their types are inferred + to be the same + --typeConstructors Minify constructors to equivalent version + --undefinedToVoid Transforms undefined into void 0 + --version, -V Prints the current version number + `; + log(msg, exitCode); +} + +function log(msg, exitCode = 0) { + process.stdout.write(msg + "\n"); + process.exit(exitCode); +} + +function error(err) { + process.stderr.write(err + "\n"); + process.exit(1); +} + +function getArgv(args) { + const presetOpts = [...plugins, ...proxies]; + + const booleanOpts = [ + ...presetOpts, + ...dceBooleanOpts, + ...mangleBooleanOpts, + ...typeConsOpts, + ...cliBooleanOpts + ]; + + const booleanDefaults = booleanOpts.reduce( + (acc, cur) => + Object.assign(acc, { + [cur]: void 0 + }), + {} + ); + + const arrayOpts = [...mangleArrayOpts]; + + const arrayDefaults = arrayOpts.reduce( + (acc, cur) => + Object.assign(acc, { + [cur]: [] + }), + {} + ); + + return yargsParser(args, { + boolean: booleanOpts, + array: mangleArrayOpts, + default: Object.assign({}, arrayDefaults, booleanDefaults), + alias, + configuration: { + "dot-notation": false + } + }); +} + +function getBabiliOpts(argv) { + const inputOpts = Object.keys(argv) + .filter(key => { + if (Array.isArray(argv[key])) return argv[key].length > 0; + return argv[key] !== void 0; + }) + .reduce((acc, cur) => Object.assign(acc, { [cur]: argv[cur] }), {}); + + const invalidOpts = validate(inputOpts); + + if (invalidOpts.length > 0) { + throw new Error("Invalid Options passed: " + invalidOpts.join(",")); + } + + const options = optionsParser(inputOpts); + + // delete unncessary options to babili preset + delete options["_"]; + delete options.d; + delete options["out-dir"]; + delete options.o; + delete options["out-file"]; + delete options.outFile; + delete options.outDir; + + return options; +} + +function validate(opts) { + const allOpts = [ + ...plugins, + ...proxies, + ...dceBooleanOpts, + ...mangleBooleanOpts, + ...typeConsOpts, + ...mangleArrayOpts, + ...cliBooleanOpts, + ...cliOpts, + ...aliasArr(alias) + ]; + + return Object.keys(opts).filter( + opt => opt !== "_" && allOpts.indexOf(opt) === -1 + ); +} + +function runStdin(argv, options) { + if (argv._.length > 0) { + throw new Error("Reading input from STDIN. Cannot take file params"); + } + + return handleStdin(argv.outFile, options); +} + +function runFile(argv, options) { + const file = argv._[0]; + + // prefer outFile + if (argv.outFile) { + return handleFile(file, argv.outFile, options); + } else if (argv.outDir) { + return handleArgs([file], argv.outDir, options); + } else { + // prints to STDOUT + return handleFile(file, void 0, options); + } +} + +function runArgs(argv, options) { + return handleArgs(argv._, argv.outDir, options); +} + +async function run(args) { + const argv = getArgv(args); + + // early exits + if (argv.help) printHelpInfo(); + if (argv.V) log(version); + + const options = getBabiliOpts(argv); + + if (!process.stdin.isTTY) { + return runStdin(argv, options); + } else if (argv._.length <= 0) { + return printHelpInfo({ exitCode: 1 }); + } else if (argv._.length === 1 && (await isFile(argv._[0]))) { + return runFile(argv, options); + } else { + return runArgs(argv, options); + } +} + +run(process.argv.slice(2)).catch(e => error(e)); diff --git a/packages/babili/src/fs.js b/packages/babili/src/fs.js new file mode 100644 index 000000000..b6c9a5593 --- /dev/null +++ b/packages/babili/src/fs.js @@ -0,0 +1,144 @@ +const fs = require("fs"); +const path = require("path"); +const readdir = require("fs-readdir-recursive"); +const promisify = require("util.promisify"); +const mkdirp = promisify(require("mkdirp")); + +const babili = require("./"); +const EXTENSIONS = [".js", ".mjs"]; + +const readFileAsync = promisify(fs.readFile); +const writeFileAsync = promisify(fs.writeFile); +const lstat = promisify(fs.lstat); + +// set defaults +const readFile = file => readFileAsync(file, { encoding: "utf-8" }); +const writeFile = (file, data) => + writeFileAsync(file, data, { encoding: "utf-8" }); + +function isJsFile(file) { + return EXTENSIONS.some(ext => path.basename(file, ext) !== file); +} + +async function isDir(p) { + try { + return (await lstat(p)).isDirectory(); + } catch (e) { + return false; + } +} + +async function isFile(p) { + try { + return (await lstat(p)).isFile(); + } catch (e) { + return false; + } +} + +// the async keyword simply exists to denote we are returning a promise +// even though we don't use await inside it +async function readStdin() { + let code = ""; + const stdin = process.stdin; + return new Promise(resolve => { + stdin.setEncoding("utf8"); + stdin.on("readable", () => { + const chunk = process.stdin.read(); + if (chunk !== null) code += chunk; + }); + stdin.on("end", () => { + resolve(code); + }); + }); +} + +async function handleStdin(outputFilename, options) { + const { code } = babili(await readStdin(), options); + if (outputFilename) { + await writeFile(outputFilename, code); + } else { + process.stdout.write(code); + } +} + +async function handleFile(filename, outputFilename, options) { + const { code } = babili(await readFile(filename), options); + if (outputFilename) { + await writeFile(outputFilename, code); + } else { + process.stdout.write(code); + } +} + +async function handleFiles(files, outputDir, options) { + if (!outputDir) { + throw new TypeError(`outputDir is falsy. Got "${outputDir}"`); + } + + return Promise.all( + files.map(file => { + const outputFilename = path.join(outputDir, path.basename(file)); + return mkdirp(path.dirname(outputFilename)).then(() => + handleFile(file, outputFilename, options) + ); + }) + ); +} + +async function handleDir(dir, outputDir, options) { + if (!outputDir) { + throw new TypeError(`outputDir is falsy`); + } + + // relative paths + const files = readdir(dir); + + return Promise.all( + files.filter(file => isJsFile(file)).map(file => { + const outputFilename = path.join(outputDir, file); + const inputFilename = path.join(dir, file); + + return mkdirp(path.dirname(outputFilename)).then(() => + handleFile(inputFilename, outputFilename, options) + ); + }) + ); +} + +async function handleArgs(args, outputDir, options) { + const files = []; + const dirs = []; + + if (!Array.isArray(args)) { + throw new TypeError(`Expected Array. Got ${JSON.stringify(args)}`); + } + + for (const arg of args) { + if (await isFile(arg)) { + files.push(arg); + } else if (await isDir(arg)) { + dirs.push(arg); + } else { + throw new TypeError(`Input "${arg}" is neither a file nor a directory.`); + } + } + + return Promise.all([ + handleFiles(files, outputDir, options), + ...dirs.map(dir => handleDir(dir, outputDir, options)) + ]); +} + +module.exports = { + handleFile, + handleStdin, + handleFiles, + handleDir, + handleArgs, + isFile, + isDir, + isJsFile, + readFile, + writeFile +}; diff --git a/packages/babili/src/index.js b/packages/babili/src/index.js index a7e030e5b..d72f11226 100644 --- a/packages/babili/src/index.js +++ b/packages/babili/src/index.js @@ -1,15 +1,30 @@ -import child from "child_process"; +const babelCore = require("babel-core"); +const babelPresetBabili = require("babel-preset-babili"); -const args = [ - require.resolve("babel-cli/bin/babel.js"), - ...process.argv.slice(2), - `--presets=${require.resolve("babel-preset-babili")}`, - "--no-babelrc" -]; +module.exports = function babili( + input, + // Minify options passed to babiliPreset + // defaults are handled in preset + options = {}, + // overrides and other options + { + minified = true, + inputSourceMap = null, + sourceMaps = false, -const opts = { - stdio: "inherit", - env: process.env -}; + // to override the default babelCore used + babel = babelCore, -child.spawn(process.execPath, args, opts); + // to override the default babiliPreset used + babiliPreset = babelPresetBabili + } = {} +) { + return babel.transform(input, { + babelrc: false, + presets: [[babiliPreset, options]], + comments: false, + inputSourceMap, + sourceMaps, + minified + }); +}; diff --git a/packages/babili/src/options-parser.js b/packages/babili/src/options-parser.js new file mode 100644 index 000000000..df86c9d23 --- /dev/null +++ b/packages/babili/src/options-parser.js @@ -0,0 +1,65 @@ +"use strict"; + +const DELIMITTER = "."; + +module.exports = function parseOpts(argv) { + return dotsToObject(argv); +}; + +/** + * Converts and Object of the form - {key: value} to deep object + * following rules of babili preset options + * + * A preset option can be `true` | `object` which enables the particular plugin + * `false` disables the plugin + * + * @param input - An Object with dot-notation keys + */ +function dotsToObject(input) { + const dots = Object.keys(input).map(key => [ + ...key.split(DELIMITTER), + input[key] + ]); + + // sort to ensure dot notation occurs after parent key + dots.sort((a, b) => { + if (a.length === b.length) { + return a[0] > b[0]; + } + return a.length > b.length; + }); + + const obj = {}; + + for (const parts of dots) { + add(obj, ...parts); + } + + // make object + function add(o, first, ...rest) { + if (rest.length < 1) { + // something went wrong + throw new Error("Option Parse Error"); + } else if (rest.length === 1) { + // there is only a key and a value + // for example: mangle: true + o[first] = rest[0]; + } else { + // create the current path and recurse if the plugin is enabled + if (!hop(o, first) || o[first] === true) { + // if the plugin is enabled + o[first] = {}; + } + if (o[first] !== false) { + // if the plugin is NOT disabled then recurse + add(o[first], ...rest); + } + } + } + + return obj; +} + +function hop(o, key) { + return Object.prototype.hasOwnProperty.call(o, key); +} diff --git a/yarn.lock b/yarn.lock index 3c6362e9c..7fb2cf1bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -271,27 +271,6 @@ aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -babel-cli@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" - dependencies: - babel-core "^6.24.1" - babel-polyfill "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - commander "^2.8.1" - convert-source-map "^1.1.0" - fs-readdir-recursive "^1.0.0" - glob "^7.0.0" - lodash "^4.2.0" - output-file-sync "^1.1.0" - path-is-absolute "^1.0.0" - slash "^1.0.0" - source-map "^0.5.0" - v8flags "^2.0.10" - optionalDependencies: - chokidar "^1.6.1" - babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" @@ -692,14 +671,6 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-polyfill@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" - dependencies: - babel-runtime "^6.22.0" - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - babel-preset-env@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4" @@ -892,10 +863,10 @@ browser-resolve@^1.11.2: resolve "1.1.7" browserslist@^2.1.2: - version "2.3.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.3.0.tgz#b2aa76415c71643fe2368f6243b43bbbb4211752" + version "2.3.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.3.1.tgz#39500a2090330b2a090120ea6c7fc78b6e091c5e" dependencies: - caniuse-lite "^1.0.30000710" + caniuse-lite "^1.0.30000712" electron-to-chromium "^1.3.17" bser@1.0.2: @@ -969,9 +940,9 @@ camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" -caniuse-lite@^1.0.30000710: - version "1.0.30000710" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000710.tgz#1c249bf7c6a61161c9b10906e3ad9fa5b6761af1" +caniuse-lite@^1.0.30000712: + version "1.0.30000713" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000713.tgz#33957ecb4a2154a5d40a60d13d8bf1cfa0881a8a" caseless@~0.12.0: version "0.12.0" @@ -995,14 +966,14 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: supports-color "^2.0.0" chalk@^2.0.0, chalk@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" dependencies: ansi-styles "^3.1.0" escape-string-regexp "^1.0.5" supports-color "^4.0.0" -chokidar@^1.4.3, chokidar@^1.6.1: +chokidar@^1.4.3: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1153,7 +1124,7 @@ command-join@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/command-join/-/command-join-2.0.0.tgz#52e8b984f4872d952ff1bdc8b98397d27c7144cf" -commander@^2.11.0, commander@^2.8.1, commander@~2.11.0: +commander@^2.11.0, commander@~2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -1355,8 +1326,8 @@ copy-props@^1.4.1: is-plain-object "^2.0.1" core-js@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + version "2.5.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -1579,7 +1550,7 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.4.3: +es-abstract@^1.4.3, es-abstract@^1.5.1: version "1.8.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.0.tgz#3b00385e85729932beffa9163bbea1234e932914" dependencies: @@ -1651,8 +1622,8 @@ eslint-scope@^3.7.1: estraverse "^4.1.1" eslint@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.4.0.tgz#a3e153e704b64f78290ef03592494eaba228d3bc" + version "4.4.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.4.1.tgz#99cd7eafcffca2ff99a5c8f5f2a474d6364b4bd3" dependencies: ajv "^5.2.0" babel-code-frame "^6.22.0" @@ -2151,7 +2122,7 @@ glob@^5.0.3, glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2221,7 +2192,7 @@ google-closure-compiler@20150901.x: version "20150901.0.0" resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20150901.0.0.tgz#3d01c6cade65790a9bfb4e30b2158b7635acbade" -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2611,7 +2582,7 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-obj@^1.0.0: +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -3001,8 +2972,8 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" jschardet@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e" + version "1.5.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" jsdom@^9.12.0: version "9.12.0" @@ -3559,8 +3530,8 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" node-fetch@^1.7.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5" + version "1.7.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7" dependencies: encoding "^0.1.11" is-stream "^1.0.1" @@ -3672,6 +3643,13 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -3758,13 +3736,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -output-file-sync@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" +output-file-sync@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-2.0.0.tgz#5d348a1a1eaed1ad168648a01a2d6d13078ce987" dependencies: - graceful-fs "^4.1.4" + graceful-fs "^4.1.11" + is-plain-obj "^1.1.0" mkdirp "^0.5.1" - object-assign "^4.1.0" p-finally@^1.0.0: version "1.0.0" @@ -4772,6 +4750,13 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -4780,7 +4765,7 @@ uuid@^3.0.0, uuid@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -v8flags@^2.0.10, v8flags@^2.0.9: +v8flags@^2.0.9: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" dependencies: