Repository: istanbuljs/nyc
Pull request #388 (2016-09-13 02:30:07)

feat: refactored config to fix precedence of config vs. args
see #379 

* refactors config to simplify logic and fix order that args vs. config is applied.
* should improve performance since yargs is only applied once now.

@mourner I will be landing your performance fixes to lib-instrument along with these other fixes, in the next nyc release.

reviewers: @addaleax, @JaKXz, @mourner

mourner: @bcoe the config refactor looks good, the only thing that doesn't fit usual JS conventions is the `Config` object, which is both a factory and a namespace, although typically capitalized names in JS are classes which are instantiated with `new`. For clarity, I'd turn the factory into another function (`exports.makeConfig`), and make the namespace lowercase (or maybe rename to `config-util`?).

Patch:
diff --git a/bin/nyc.js b/bin/nyc.js
index 1f74661..4b0bff0 100755
--- a/bin/nyc.js
+++ b/bin/nyc.js
@@ -1,5 +1,5 @@
 #!/usr/bin/env node
-var arrify = require('arrify')
+var configUtil = require('../lib/config-util')
 var foreground = require('foreground-child')
 var NYC
 try {
@@ -10,68 +10,44 @@ try {
 var processArgs = require('../lib/process-args')
 
 var sw = require('spawn-wrap')
-var testExclude = require('test-exclude')
 var wrapper = require.resolve('./wrap.js')
-var Yargs = require('yargs/yargs')
 
-var yargs = decorateYargs(buildYargs())
-var argv = yargs.parse(processArgs.hideInstrumenteeArgs())
+// parse configuration and command-line arguments;
+// we keep these values in a few different forms,
+// used in the various execution contexts of nyc:
+// reporting, instrumenting subprocesses, etc.
+var yargs = configUtil.decorateYargs(configUtil.buildYargs())
+var instrumenterArgs = processArgs.hideInstrumenteeArgs()
+var argv = yargs.parse(instrumenterArgs)
+var config = configUtil.loadConfig(instrumenterArgs)
 
 if (argv._[0] === 'report') {
   // run a report.
   process.env.NYC_CWD = process.cwd()
 
-  report(argv)
+  report(config)
 } else if (argv._[0] === 'check-coverage') {
-  checkCoverage(argv)
+  checkCoverage(config)
 } else if (argv._[0] === 'instrument') {
   // look in lib/commands/instrument.js for logic.
 } else if (argv._.length) {
-  // wrap subprocesses and execute argv[1]
-  argv.require = arrify(argv.require)
-  argv.extension = arrify(argv.extension)
-  argv.exclude = arrify(argv.exclude)
-  argv.include = arrify(argv.include)
-
   // if instrument is set to false,
   // enable a noop instrumenter.
-  if (!argv.instrument) argv.instrumenter = './lib/instrumenters/noop'
-  else argv.instrumenter = './lib/instrumenters/istanbul'
+  if (!config.instrument) config.instrumenter = './lib/instrumenters/noop'
+  else config.instrumenter = './lib/instrumenters/istanbul'
 
-  var nyc = (new NYC({
-    require: argv.require,
-    include: argv.include,
-    exclude: argv.exclude,
-    sourceMap: !!argv.sourceMap,
-    instrumenter: argv.instrumenter,
-    hookRunInContext: argv.hookRunInContext,
-    showProcessTree: argv.showProcessTree
-  }))
+  var nyc = (new NYC(config))
   nyc.reset()
 
-  if (argv.all) nyc.addAllFiles()
+  if (config.all) nyc.addAllFiles()
 
   var env = {
-    NYC_CWD: process.cwd(),
     NYC_CACHE: argv.cache ? 'enable' : 'disable',
-    NYC_SOURCE_MAP: argv.sourceMap ? 'enable' : 'disable',
-    NYC_INSTRUMENTER: argv.instrumenter,
-    NYC_HOOK_RUN_IN_CONTEXT: argv.hookRunInContext ? 'enable' : 'disable',
-    NYC_SHOW_PROCESS_TREE: argv.showProcessTree ? 'enable' : 'disable',
+    NYC_CONFIG: JSON.stringify(config),
+    NYC_CWD: process.cwd(),
     NYC_ROOT_ID: nyc.rootId,
-    BABEL_DISABLE_CACHE: 1
-  }
-  if (argv.require.length) {
-    env.NYC_REQUIRE = argv.require.join(',')
-  }
-  if (argv.extension.length) {
-    env.NYC_EXTENSION = argv.extension.join(',')
-  }
-  if (argv.exclude.length) {
-    env.NYC_EXCLUDE = argv.exclude.join(':')
-  }
-  if (argv.include.length) {
-    env.NYC_INCLUDE = argv.include.join(':')
+    BABEL_DISABLE_CACHE: 1,
+    NYC_INSTRUMENTER: config.instrumenter
   }
   sw([wrapper], env)
 
@@ -82,17 +58,17 @@ if (argv._[0] === 'report') {
   foreground(processArgs.hideInstrumenterArgs(
     // use the same argv descrption, but don't exit
     // for flags like --help.
-    buildYargs().parse(process.argv.slice(2))
+    configUtil.buildYargs().parse(process.argv.slice(2))
   ), function (done) {
     var mainChildExitCode = process.exitCode
 
-    if (argv.checkCoverage) {
-      checkCoverage(argv)
+    if (config.checkCoverage) {
+      checkCoverage(config)
       process.exitCode = process.exitCode || mainChildExitCode
-      if (!argv.silent) report(argv)
+      if (!config.silent) report(config)
       return done()
     } else {
-      if (!argv.silent) report(argv)
+      if (!config.silent) report(config)
       return done()
     }
   })
@@ -104,12 +80,7 @@ if (argv._[0] === 'report') {
 function report (argv) {
   process.env.NYC_CWD = process.cwd()
 
-  var nyc = new NYC({
-    reporter: argv.reporter,
-    reportDir: argv.reportDir,
-    tempDirectory: argv.tempDirectory,
-    showProcessTree: argv.showProcessTree
-  })
+  var nyc = new NYC(argv)
   nyc.report()
 }
 
@@ -123,158 +94,3 @@ function checkCoverage (argv, cb) {
     statements: argv.statements
   })
 }
-
-function buildYargs () {
-  return Yargs([])
-    .usage('$0 [command] [options]\n\nrun your tests with the nyc bin to instrument them with coverage')
-    .command('report', 'run coverage report for .nyc_output', function (yargs) {
-      return yargs
-        .usage('$0 report [options]')
-        .option('reporter', {
-          alias: 'r',
-          describe: 'coverage reporter(s) to use',
-          default: 'text'
-        })
-        .option('report-dir', {
-          describe: 'directory to output coverage reports in',
-          default: 'coverage'
-        })
-        .option('temp-directory', {
-          describe: 'directory from which coverage JSON files are read',
-          default: './.nyc_output'
-        })
-        .option('show-process-tree', {
-          describe: 'display the tree of spawned processes',
-          default: false,
-          type: 'boolean'
-        })
-        .example('$0 report --reporter=lcov', 'output an HTML lcov report to ./coverage')
-    })
-    .command('check-coverage', 'check whether coverage is within thresholds provided', function (yargs) {
-      return yargs
-        .usage('$0 check-coverage [options]')
-        .option('branches', {
-          default: 0,
-          description: 'what % of branches must be covered?'
-        })
-        .option('functions', {
-          default: 0,
-          description: 'what % of functions must be covered?'
-        })
-        .option('lines', {
-          default: 90,
-          description: 'what % of lines must be covered?'
-        })
-        .option('statements', {
-          default: 0,
-          description: 'what % of statements must be covered?'
-        })
-        .example('$0 check-coverage --lines 95', "check whether the JSON in nyc's output folder meets the thresholds provided")
-    })
-    .option('reporter', {
-      alias: 'r',
-      describe: 'coverage reporter(s) to use',
-      default: 'text'
-    })
-    .option('report-dir', {
-      describe: 'directory to output coverage reports in',
-      default: 'coverage'
-    })
-    .option('silent', {
-      alias: 's',
-      default: false,
-      type: 'boolean',
-      describe: "don't output a report after tests finish running"
-    })
-    .option('all', {
-      alias: 'a',
-      default: false,
-      type: 'boolean',
-      describe: 'whether or not to instrument all files of the project (not just the ones touched by your test suite)'
-    })
-    .option('exclude', {
-      alias: 'x',
-      default: testExclude.defaultExclude,
-      describe: 'a list of specific files and directories that should be excluded from coverage, glob patterns are supported, node_modules is always excluded'
-    })
-    .option('include', {
-      alias: 'n',
-      default: [],
-      describe: 'a list of specific files that should be covered, glob patterns are supported'
-    })
-    .option('require', {
-      alias: 'i',
-      default: [],
-      describe: 'a list of additional modules that nyc should attempt to require in its subprocess, e.g., babel-register, babel-polyfill.'
-    })
-    .option('cache', {
-      alias: 'c',
-      default: false,
-      type: 'boolean',
-      describe: 'cache instrumentation results for improved performance'
-    })
-    .option('extension', {
-      alias: 'e',
-      default: [],
-      describe: 'a list of extensions that nyc should handle in addition to .js'
-    })
-    .option('check-coverage', {
-      type: 'boolean',
-      default: false,
-      describe: 'check whether coverage is within thresholds provided'
-    })
-    .option('branches', {
-      default: 0,
-      description: 'what % of branches must be covered?'
-    })
-    .option('functions', {
-      default: 0,
-      description: 'what % of functions must be covered?'
-    })
-    .option('lines', {
-      default: 90,
-      description: 'what % of lines must be covered?'
-    })
-    .option('statements', {
-      default: 0,
-      description: 'what % of statements must be covered?'
-    })
-    .option('source-map', {
-      default: true,
-      type: 'boolean',
-      description: 'should nyc detect and handle source maps?'
-    })
-    .option('instrument', {
-      default: true,
-      type: 'boolean',
-      description: 'should nyc handle instrumentation?'
-    })
-    .option('hook-run-in-context', {
-      default: true,
-      type: 'boolean',
-      description: 'should nyc wrap vm.runInThisContext?'
-    })
-    .option('show-process-tree', {
-      describe: 'display the tree of spawned processes',
-      default: false,
-      type: 'boolean'
-    })
-    .pkgConf('nyc', process.cwd())
-    .example('$0 npm test', 'instrument your tests with coverage')
-    .example('$0 --require babel-core/register npm test', 'instrument your tests with coverage and babel')
-    .example('$0 report --reporter=text-lcov', 'output lcov report after running your tests')
-    .epilog('visit https://git.io/voHar for list of available reporters')
-    .boolean('help')
-    .boolean('h')
-    .boolean('version')
-}
-
-// decorate yargs with all the actions
-// that would make it exit: help, version, command.
-function decorateYargs (yargs) {
-  return yargs
-    .help('h')
-    .alias('h', 'help')
-    .version()
-    .command(require('../lib/commands/instrument'))
-}
diff --git a/bin/wrap.js b/bin/wrap.js
index c539253..d64aceb 100644
--- a/bin/wrap.js
+++ b/bin/wrap.js
@@ -9,20 +9,14 @@ try {
 var parentPid = process.env.NYC_PARENT_PID || '0'
 process.env.NYC_PARENT_PID = process.pid
 
-;(new NYC({
-  require: process.env.NYC_REQUIRE ? process.env.NYC_REQUIRE.split(',') : [],
-  extension: process.env.NYC_EXTENSION ? process.env.NYC_EXTENSION.split(',') : [],
-  exclude: process.env.NYC_EXCLUDE ? process.env.NYC_EXCLUDE.split(':') : [],
-  include: process.env.NYC_INCLUDE ? process.env.NYC_INCLUDE.split(':') : [],
-  enableCache: process.env.NYC_CACHE === 'enable',
-  sourceMap: process.env.NYC_SOURCE_MAP === 'enable',
-  instrumenter: process.env.NYC_INSTRUMENTER,
-  hookRunInContext: process.env.NYC_HOOK_RUN_IN_CONTEXT === 'enable',
-  showProcessTree: process.env.NYC_SHOW_PROCESS_TREE === 'enable',
-  _processInfo: {
-    ppid: parentPid,
-    root: process.env.NYC_ROOT_ID
-  }
-})).wrap()
+var config = {}
+if (process.env.NYC_CONFIG) config = JSON.parse(process.env.NYC_CONFIG)
+config.enableCache = process.env.NYC_CACHE === 'enable'
+config._processInfo = {
+  ppid: parentPid,
+  root: process.env.NYC_ROOT_ID
+}
+
+;(new NYC(config)).wrap()
 
 sw.runMain()
diff --git a/index.js b/index.js
index 1517a55..d64c378 100755
--- a/index.js
+++ b/index.js
@@ -1,4 +1,5 @@
 /* global __coverage__ */
+var arrify = require('arrify')
 var fs = require('fs')
 var glob = require('glob')
 var libCoverage = require('istanbul-lib-coverage')
@@ -13,14 +14,11 @@ var path = require('path')
 var rimraf = require('rimraf')
 var onExit = require('signal-exit')
 var resolveFrom = require('resolve-from')
-var arrify = require('arrify')
 var convertSourceMap = require('convert-source-map')
 var md5hex = require('md5-hex')
 var findCacheDir = require('find-cache-dir')
 var js = require('default-require-extensions/js')
-var pkgUp = require('pkg-up')
 var testExclude = require('test-exclude')
-var yargs = require('yargs')
 
 var ProcessInfo
 try {
@@ -34,17 +32,16 @@ if (/index\.covered\.js$/.test(__filename)) {
   require('./lib/self-coverage-helper')
 }
 
-function NYC (opts) {
-  var config = this._loadConfig(opts || {})
+function NYC (config) {
+  config = config || {}
 
-  this._istanbul = config.istanbul
   this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js')
   this._tempDirectory = config.tempDirectory || './.nyc_output'
   this._instrumenterLib = require(config.instrumenter || './lib/instrumenters/istanbul')
-  this._reportDir = config.reportDir
-  this._sourceMap = config.sourceMap
-  this._showProcessTree = config.showProcessTree
-  this.cwd = config.cwd
+  this._reportDir = config.reportDir || 'coverage'
+  this._sourceMap = typeof config.sourceMap === 'boolean' ? config.sourceMap : true
+  this._showProcessTree = config.showProcessTree || false
+  this.cwd = config.cwd || process.cwd()
 
   this.reporter = arrify(config.reporter || 'text')
 
@@ -80,26 +77,10 @@ function NYC (opts) {
   this.loadedMaps = null
   this.fakeRequire = null
 
-  this.processInfo = new ProcessInfo(opts && opts._processInfo)
+  this.processInfo = new ProcessInfo(config && config._processInfo)
   this.rootId = this.processInfo.root || this.generateUniqueID()
 }
 
-NYC.prototype._loadConfig = function (opts) {
-  var cwd = opts.cwd || process.env.NYC_CWD || process.cwd()
-  var pkgPath = pkgUp.sync(cwd)
-
-  if (pkgPath) {
-    cwd = path.dirname(pkgPath)
-  }
-
-  opts.cwd = cwd
-
-  return yargs([])
-    .pkgConf('nyc', cwd)
-    .default(opts)
-    .argv
-}
-
 NYC.prototype._createTransform = function (ext) {
   var _this = this
   return cachingTransform({
diff --git a/lib/config-util.js b/lib/config-util.js
new file mode 100644
index 0000000..b137d59
--- /dev/null
+++ b/lib/config-util.js
@@ -0,0 +1,196 @@
+var arrify = require('arrify')
+var path = require('path')
+var pkgUp = require('pkg-up')
+var testExclude = require('test-exclude')
+var Yargs = require('yargs/yargs')
+
+var Config = {}
+
+// load config from a cascade of sources:
+// * command line arguments.
+// * package.json.
+// * .nycrc (coming soon)
+Config.loadConfig = function (argv, cwd) {
+  cwd = cwd || process.env.NYC_CWD || process.cwd()
+  var pkgPath = pkgUp.sync(cwd)
+
+  if (pkgPath) {
+    cwd = path.dirname(pkgPath)
+  }
+
+  var config = Config.buildYargs(cwd)
+    .default({
+      cwd: cwd
+    })
+    .parse(argv || [])
+
+  // post-hoc, we convert several of the
+  // configuration settings to arrays, providing
+  // a consistent contract to index.js.
+  config.require = arrify(config.require)
+  config.extension = arrify(config.extension)
+  config.exclude = arrify(config.exclude)
+  config.include = arrify(config.include)
+  config.cwd = cwd
+
+  return config
+}
+
+// build a yargs object, omitting any settings
+// that would cause the application to exit early.
+Config.buildYargs = function (cwd) {
+  return Yargs([])
+    .usage('$0 [command] [options]\n\nrun your tests with the nyc bin to instrument them with coverage')
+    .command('report', 'run coverage report for .nyc_output', function (yargs) {
+      return yargs
+        .usage('$0 report [options]')
+        .option('reporter', {
+          alias: 'r',
+          describe: 'coverage reporter(s) to use',
+          default: 'text'
+        })
+        .option('report-dir', {
+          describe: 'directory to output coverage reports in',
+          default: 'coverage'
+        })
+        .option('temp-directory', {
+          describe: 'directory from which coverage JSON files are read',
+          default: './.nyc_output'
+        })
+        .option('show-process-tree', {
+          describe: 'display the tree of spawned processes',
+          default: false,
+          type: 'boolean'
+        })
+        .example('$0 report --reporter=lcov', 'output an HTML lcov report to ./coverage')
+    })
+    .command('check-coverage', 'check whether coverage is within thresholds provided', function (yargs) {
+      return yargs
+        .usage('$0 check-coverage [options]')
+        .option('branches', {
+          default: 0,
+          description: 'what % of branches must be covered?'
+        })
+        .option('functions', {
+          default: 0,
+          description: 'what % of functions must be covered?'
+        })
+        .option('lines', {
+          default: 90,
+          description: 'what % of lines must be covered?'
+        })
+        .option('statements', {
+          default: 0,
+          description: 'what % of statements must be covered?'
+        })
+        .example('$0 check-coverage --lines 95', "check whether the JSON in nyc's output folder meets the thresholds provided")
+    })
+    .option('reporter', {
+      alias: 'r',
+      describe: 'coverage reporter(s) to use',
+      default: 'text'
+    })
+    .option('report-dir', {
+      describe: 'directory to output coverage reports in',
+      default: 'coverage'
+    })
+    .option('silent', {
+      alias: 's',
+      default: false,
+      type: 'boolean',
+      describe: "don't output a report after tests finish running"
+    })
+    .option('all', {
+      alias: 'a',
+      default: false,
+      type: 'boolean',
+      describe: 'whether or not to instrument all files of the project (not just the ones touched by your test suite)'
+    })
+    .option('exclude', {
+      alias: 'x',
+      default: testExclude.defaultExclude,
+      describe: 'a list of specific files and directories that should be excluded from coverage, glob patterns are supported, node_modules is always excluded'
+    })
+    .option('include', {
+      alias: 'n',
+      default: [],
+      describe: 'a list of specific files that should be covered, glob patterns are supported'
+    })
+    .option('require', {
+      alias: 'i',
+      default: [],
+      describe: 'a list of additional modules that nyc should attempt to require in its subprocess, e.g., babel-register, babel-polyfill.'
+    })
+    .option('cache', {
+      alias: 'c',
+      default: false,
+      type: 'boolean',
+      describe: 'cache instrumentation results for improved performance'
+    })
+    .option('extension', {
+      alias: 'e',
+      default: [],
+      describe: 'a list of extensions that nyc should handle in addition to .js'
+    })
+    .option('check-coverage', {
+      type: 'boolean',
+      default: false,
+      describe: 'check whether coverage is within thresholds provided'
+    })
+    .option('branches', {
+      default: 0,
+      description: 'what % of branches must be covered?'
+    })
+    .option('functions', {
+      default: 0,
+      description: 'what % of functions must be covered?'
+    })
+    .option('lines', {
+      default: 90,
+      description: 'what % of lines must be covered?'
+    })
+    .option('statements', {
+      default: 0,
+      description: 'what % of statements must be covered?'
+    })
+    .option('source-map', {
+      default: true,
+      type: 'boolean',
+      description: 'should nyc detect and handle source maps?'
+    })
+    .option('instrument', {
+      default: true,
+      type: 'boolean',
+      description: 'should nyc handle instrumentation?'
+    })
+    .option('hook-run-in-context', {
+      default: true,
+      type: 'boolean',
+      description: 'should nyc wrap vm.runInThisContext?'
+    })
+    .option('show-process-tree', {
+      describe: 'display the tree of spawned processes',
+      default: false,
+      type: 'boolean'
+    })
+    .pkgConf('nyc', cwd || process.cwd())
+    .example('$0 npm test', 'instrument your tests with coverage')
+    .example('$0 --require babel-core/register npm test', 'instrument your tests with coverage and babel')
+    .example('$0 report --reporter=text-lcov', 'output lcov report after running your tests')
+    .epilog('visit https://git.io/voHar for list of available reporters')
+    .boolean('help')
+    .boolean('h')
+    .boolean('version')
+}
+
+// decorate yargs with all the actions
+// that would make it exit: help, version, command.
+Config.decorateYargs = function (yargs) {
+  return yargs
+    .help('h')
+    .alias('h', 'help')
+    .version()
+    .command(require('../lib/commands/instrument'))
+}
+
+module.exports = Config


Test patch:
diff --git a/test/fixtures/cli/package.json b/test/fixtures/cli/package.json
index 352055c..d67ca66 100644
--- a/test/fixtures/cli/package.json
+++ b/test/fixtures/cli/package.json
@@ -1,3 +1,6 @@
 {
-  "private": true
+  "private": true,
+  "nyc": {
+    "reporter": ["text"]
+  }
 }
diff --git a/test/src/nyc-bin.js b/test/src/nyc-bin.js
index e6fe04b..4f99895 100644
--- a/test/src/nyc-bin.js
+++ b/test/src/nyc-bin.js
@@ -222,44 +222,67 @@ describe('the nyc cli', function () {
     })
   })
 
-  it('passes configuration via environment variables', function (done) {
-    var args = [
-      bin,
-      '--silent',
-      '--require=mkdirp',
-      '--include=env.js',
-      '--exclude=batman.js',
-      '--extension=.js',
-      '--cache=true',
-      '--source-map=true',
-      process.execPath,
-      './env.js'
-    ]
-    var expected = {
-      NYC_REQUIRE: 'mkdirp',
-      NYC_INCLUDE: 'env.js',
-      NYC_EXCLUDE: 'batman.js',
-      NYC_EXTENSION: '.js',
-      NYC_CACHE: 'enable',
-      NYC_SOURCE_MAP: 'enable',
-      NYC_INSTRUMENTER: './lib/instrumenters/istanbul'
-    }
+  describe('configuration', function () {
+    it('passes configuration via environment variables', function (done) {
+      var args = [
+        bin,
+        '--silent',
+        '--require=mkdirp',
+        '--include=env.js',
+        '--exclude=batman.js',
+        '--extension=.js',
+        '--cache=true',
+        '--source-map=true',
+        process.execPath,
+        './env.js'
+      ]
+      var expected = {
+        instrumenter: './lib/instrumenters/istanbul',
+        silent: true,
+        cache: true,
+        sourceMap: true
+      }
 
-    var proc = spawn(process.execPath, args, {
-      cwd: fixturesCLI,
-      env: env
-    })
+      var proc = spawn(process.execPath, args, {
+        cwd: fixturesCLI,
+        env: env
+      })
 
-    var stdout = ''
-    proc.stdout.on('data', function (chunk) {
-      stdout += chunk
+      var stdout = ''
+      proc.stdout.on('data', function (chunk) {
+        stdout += chunk
+      })
+
+      proc.on('close', function (code) {
+        code.should.equal(0)
+        var env = JSON.parse(stdout)
+        var config = JSON.parse(env.NYC_CONFIG, null, 2)
+        config.should.include(expected)
+        config.include.should.include('env.js')
+        config.exclude.should.include('batman.js')
+        config.extension.should.include('.js')
+        done()
+      })
     })
 
-    proc.on('close', function (code) {
-      code.should.equal(0)
-      var env = JSON.parse(stdout)
-      env.should.include(expected)
-      done()
+    it('allows package.json configuration to be overridden with command line args', function (done) {
+      var args = [bin, '--reporter=text-lcov', process.execPath, './half-covered.js']
+
+      var proc = spawn(process.execPath, args, {
+        cwd: fixturesCLI,
+        env: env
+      })
+
+      var stdout = ''
+      proc.stdout.on('data', function (chunk) {
+        stdout += chunk
+      })
+
+      proc.on('close', function (code) {
+        code.should.equal(0)
+        stdout.should.match(/SF:.*half-covered\.js/)
+        done()
+      })
     })
   })
 
@@ -273,8 +296,10 @@ describe('the nyc cli', function () {
       './env.js'
     ]
     var expected = {
-      NYC_SOURCE_MAP: 'disable',
-      NYC_INSTRUMENTER: './lib/instrumenters/noop'
+      silent: true,
+      instrument: false,
+      sourceMap: false,
+      instrumenter: './lib/instrumenters/noop'
     }
 
     var proc = spawn(process.execPath, args, {
@@ -290,7 +315,8 @@ describe('the nyc cli', function () {
     proc.on('close', function (code) {
       code.should.equal(0)
       var env = JSON.parse(stdout)
-      env.should.include(expected)
+      var config = JSON.parse(env.NYC_CONFIG, null, 2)
+      config.should.include(expected)
       done()
     })
   })
diff --git a/test/src/nyc-test.js b/test/src/nyc-test.js
index 2659979..9657d6a 100644
--- a/test/src/nyc-test.js
+++ b/test/src/nyc-test.js
@@ -3,6 +3,7 @@
 require('source-map-support').install()
 var _ = require('lodash')
 var ap = require('any-path')
+var configUtil = require('../../lib/config-util')
 var fs = require('fs')
 var enableCache = false
 var _NYC
@@ -44,51 +45,46 @@ require('tap').mochaGlobals()
 describe('nyc', function () {
   describe('cwd', function () {
     it('sets cwd to process.cwd() if no environment variable is set', function () {
-      var nyc = new NYC()
+      var nyc = new NYC(configUtil.loadConfig())
 
       nyc.cwd.should.eql(process.cwd())
     })
 
     it('uses NYC_CWD environment variable for cwd if it is set', function () {
       process.env.NYC_CWD = path.resolve(__dirname, '../fixtures')
-
-      var nyc = new NYC()
+      var nyc = new NYC(configUtil.loadConfig())
 
       nyc.cwd.should.equal(path.resolve(__dirname, '../fixtures'))
     })
 
     it('will look upwards for package.json from cwd', function () {
-      var nyc = new NYC({cwd: __dirname})
+      var nyc = new NYC(configUtil.loadConfig([], __dirname))
       nyc.cwd.should.eql(path.join(__dirname, '../..'))
     })
   })
 
   describe('config', function () {
     it("loads 'exclude' patterns from package.json#nyc", function () {
-      var nyc = new NYC({
-        cwd: path.resolve(__dirname, '../fixtures')
-      })
+      var nyc = new NYC(configUtil.loadConfig([], path.resolve(__dirname, '../fixtures')))
 
       nyc.exclude.exclude.length.should.eql(4)
     })
 
     it("loads 'extension' patterns from package.json#nyc", function () {
-      var nyc = new NYC({
-        cwd: path.resolve(__dirname, '../fixtures/conf-multiple-extensions')
-      })
+      var nyc = new NYC(configUtil.loadConfig([], path.resolve(__dirname, '../fixtures/conf-multiple-extensions')))
 
       nyc.extensions.length.should.eql(3)
     })
 
     it("ignores 'include' option if it's falsy or []", function () {
-      var nyc1 = new NYC({
-        cwd: path.resolve(__dirname, '../fixtures/conf-empty')
-      })
+      var nyc1 = new NYC(configUtil.loadConfig(
+        [],
+        path.resolve(__dirname, '../fixtures/conf-empty')
+      ))
 
       nyc1.exclude.include.should.equal(false)
 
       var nyc2 = new NYC({
-        cwd: path.resolve(__dirname, '../fixtures/conf-empty'),
         include: []
       })
 
@@ -96,18 +92,13 @@ describe('nyc', function () {
     })
 
     it("ignores 'exclude' option if it's falsy", function () {
-      var nyc1 = new NYC({
-        cwd: path.resolve(__dirname, '../fixtures/conf-empty')
-      })
+      var nyc1 = new NYC(configUtil.loadConfig([], path.resolve(__dirname, '../fixtures/conf-empty')))
 
       nyc1.exclude.exclude.length.should.eql(7)
     })
 
     it("allows for empty 'exclude'", function () {
-      var nyc2 = new NYC({
-        cwd: path.resolve(__dirname, '../fixtures/conf-empty'),
-        exclude: []
-      })
+      var nyc2 = new NYC({exclude: []})
 
       nyc2.exclude.exclude.length.should.eql(0)
     })
@@ -115,16 +106,13 @@ describe('nyc', function () {
 
   describe('shouldInstrumentFile', function () {
     it('should exclude appropriately with defaults', function () {
-      var nyc = new NYC({
-        cwd: '/cwd/',
-        exclude: [
-          '**/node_modules/**',
-          'test/**',
-          'test{,-*}.js',
-          '**/*.test.js',
-          '**/__tests__/**'
-        ]
-      })
+      var nyc = new NYC(configUtil.loadConfig([
+        '--exclude=**/node_modules/**',
+        '--exclude=test/**',
+        '--exclude=test{,-*}.js',
+        '--exclude=**/*.test.js',
+        '--exclude=**/__tests__/**'
+      ], '/cwd'))
 
       // nyc always excludes "node_modules/**"
       nyc.exclude.shouldInstrument('/cwd/foo', 'foo').should.equal(true)
@@ -141,9 +129,7 @@ describe('nyc', function () {
     })
 
     it('should exclude appropriately with config.exclude', function () {
-      var nyc = new NYC({
-        cwd: fixtures
-      })
+      var nyc = new NYC(configUtil.loadConfig([], fixtures))
 
       // fixtures/package.json configures excludes: "blarg", "blerg"
       nyc.exclude.shouldInstrument('blarg', 'blarg').should.equal(false)
@@ -154,43 +140,31 @@ describe('nyc', function () {
     })
 
     it('should exclude outside of the current working directory', function () {
-      var nyc = new NYC({
-        cwd: '/cwd/foo/'
-      })
+      var nyc = new NYC(configUtil.loadConfig([], '/cwd/foo/'))
       nyc.exclude.shouldInstrument('/cwd/bar', '../bar').should.equal(false)
     })
 
     it('should not exclude if the current working directory is inside node_modules', function () {
-      var nyc = new NYC({
-        cwd: '/cwd/node_modules/foo/'
-      })
+      var nyc = new NYC(configUtil.loadConfig([], '/cwd/node_modules/foo/'))
       nyc.exclude.shouldInstrument('/cwd/node_modules/foo/bar', './bar').should.equal(true)
       nyc.exclude.shouldInstrument('/cwd/node_modules/foo/bar', '.\\bar').should.equal(true)
     })
 
     it('allows files to be explicitly included, rather than excluded', function () {
-      var nyc = new NYC({
-        cwd: '/cwd/',
-        include: 'foo.js'
-      })
+      var nyc = new NYC(configUtil.loadConfig(['--include=foo.js'], '/cwd/'))
 
       nyc.exclude.shouldInstrument('/cwd/foo.js', 'foo.js').should.equal(true)
       nyc.exclude.shouldInstrument('/cwd/index.js', 'index.js').should.equal(false)
     })
 
     it('exclude overrides include', function () {
-      var nyc = new NYC({
-        cwd: '/cwd/',
-        include: [
-          'foo.js',
-          'test.js'
-        ],
-        exclude: [
-          '**/node_modules/**',
-          'test/**',
-          'test{,-*}.js'
-        ]
-      })
+      var nyc = new NYC(configUtil.loadConfig([
+        '--include=foo.js',
+        '--include=test.js',
+        '--exclude=**/node_modules/**',
+        '--exclude=test/**',
+        '--exclude=test{,-*}.js'
+      ], '/cwd/'))
 
       nyc.exclude.shouldInstrument('/cwd/foo.js', 'foo.js').should.equal(true)
       nyc.exclude.shouldInstrument('/cwd/test.js', 'test.js').should.equal(false)
@@ -199,9 +173,7 @@ describe('nyc', function () {
 
   describe('wrap', function () {
     it('wraps modules with coverage counters when they are required', function () {
-      var nyc = new NYC({
-        cwd: process.cwd()
-      })
+      var nyc = new NYC(configUtil.loadConfig())
       nyc.reset()
       nyc.wrap()
 
@@ -218,9 +190,7 @@ describe('nyc', function () {
         // the `require` call to istanbul is deferred, loaded here so it doesn't mess with the hooks callCount
         require('istanbul-lib-instrument')
 
-        var nyc = new NYC({
-          cwd: process.cwd()
-        })
+        var nyc = new NYC(configUtil.loadConfig())
         nyc.reset()
         nyc.wrap()
 
@@ -237,9 +207,9 @@ describe('nyc', function () {
 
     describe('compile handlers for custom extensions are assigned', function () {
       it('assigns a function to custom extensions', function () {
-        var nyc = new NYC({
-          cwd: path.resolve(__dirname, '../fixtures/conf-multiple-extensions')
-        })
+        var nyc = new NYC(configUtil.loadConfig([],
+          path.resolve(__dirname, '../fixtures/conf-multiple-extensions')
+        ))
         nyc.reset()
         nyc.wrap()
 
@@ -254,9 +224,9 @@ describe('nyc', function () {
         // the `require` call to istanbul is deferred, loaded here so it doesn't mess with the hooks callCount
         require('istanbul-lib-instrument')
 
-        var nyc = new NYC({
-          cwd: path.resolve(__dirname, '../fixtures/conf-multiple-extensions')
-        })
+        var nyc = new NYC(configUtil.loadConfig([],
+          path.resolve(__dirname, '../fixtures/conf-multiple-extensions')
+        ))
 
         sinon.spy(nyc, '_handleJs')
 
@@ -272,9 +242,7 @@ describe('nyc', function () {
     })
 
     function testSignal (signal, done) {
-      var nyc = (new NYC({
-        cwd: fixtures
-      }))
+      var nyc = (new NYC(configUtil.loadConfig([], fixtures)))
 
       var proc = spawn(process.execPath, [bin, './' + signal + '.js'], {
         cwd: fixtures,
@@ -302,9 +270,7 @@ describe('nyc', function () {
     })
 
     it('does not output coverage for files that have not been included, by default', function (done) {
-      var nyc = (new NYC({
-        cwd: process.cwd()
-      }))
+      var nyc = (new NYC(configUtil.loadConfig([], process.cwd())))
       nyc.wrap()
       nyc.reset()
 
@@ -318,12 +284,9 @@ describe('nyc', function () {
 
   describe('report', function () {
     it('allows coverage report to be output in an alternative directory', function (done) {
-      var reporters = ['lcov']
-      var nyc = new NYC({
-        cwd: process.cwd(),
-        reporter: reporters,
-        reportDir: './alternative-report'
-      })
+      var nyc = new NYC(configUtil.loadConfig(
+        ['--report-dir=./alternative-report', '--reporter=lcov']
+      ))
       nyc.reset()
 
       var proc = spawn(process.execPath, ['./test/fixtures/child-1.js'], {
@@ -343,9 +306,7 @@ describe('nyc', function () {
 
   describe('addAllFiles', function () {
     it('outputs an empty coverage report for all files that are not excluded', function (done) {
-      var nyc = new NYC({
-        cwd: fixtures
-      })
+      var nyc = new NYC(configUtil.loadConfig([], fixtures))
       nyc.reset()
       nyc.addAllFiles()
 
@@ -363,9 +324,7 @@ describe('nyc', function () {
 
     it('outputs an empty coverage report for multiple configured extensions', function (done) {
       var cwd = path.resolve(fixtures, './conf-multiple-extensions')
-      var nyc = new NYC({
-        cwd: cwd
-      })
+      var nyc = new NYC(configUtil.loadConfig([], cwd))
       nyc.reset()
       nyc.addAllFiles()
 
@@ -390,9 +349,7 @@ describe('nyc', function () {
     })
 
     it('tracks coverage appropriately once the file is required', function (done) {
-      var nyc = (new NYC({
-        cwd: fixtures
-      }))
+      var nyc = (new NYC(configUtil.loadConfig([], fixtures)))
       nyc.reset()
       nyc.wrap()
 
@@ -420,10 +377,7 @@ describe('nyc', function () {
         'utf-8'
       )
 
-      var nyc = (new NYC({
-        cwd: fixtures,
-        require: './test/fixtures/transpile-hook'
-      }))
+      var nyc = (new NYC(configUtil.loadConfig(['--require', './test/fixtures/transpile-hook'], fixtures)))
 
       nyc.reset()
       nyc.addAllFiles()
@@ -449,11 +403,10 @@ describe('nyc', function () {
       'utf-8'
     )
 
-    var nyc = (new NYC({
-      cwd: fixtures,
-      require: './test/fixtures/transpile-hook',
-      extension: ['.whatever']
-    }))
+    var nyc = (new NYC(configUtil.loadConfig([
+      '--require=./test/fixtures/transpile-hook',
+      '--extension=.whatever'
+    ], fixtures)))
 
     nyc.reset()
     nyc.addAllFiles()
@@ -473,7 +426,7 @@ describe('nyc', function () {
 
   describe('cache', function () {
     it('handles collisions', function (done) {
-      var nyc = new NYC({cwd: fixtures})
+      var nyc = new NYC(configUtil.loadConfig([], fixtures))
       nyc.clearCache()
 
       var args = [bin, process.execPath, './cache-collision-runner.js']