Repository: yargs/yargs (js)

Issue:
Strict mode doesn't know about context variables [7.x]
Discussion moved from https://github.com/yargs/yargs/issues/776.

When running code like so against the `7.x` branch:

```js
'use strict'

// Node packaged modules
const yargs = require('yargs')
const parser = yargs()
parser.command({
  command: '!ch', // config.get('cmd'),
  description: 'test desc', // config.get('description'),
  builder: yargs => {
    yargs.command('dice', '', () => {}, () => {
      console.log('dice')
    })
    yargs.command('uptime', '', () => {}, () => {
      console.log('uptime')
    })
    yargs.help()
    yargs.strict()
    yargs.version()
  }
})

parser.parse('!ch uptime', { 'test': 'test' }, (err, argv, output) => {
  if (err && !output) throw err
  if (output) {
    console.log(output)
  }
})

parser.parse('!ch dice', { 'test': 'test' }, (err, argv, output) => {
  if (err && !output) throw err
  if (output) {
    console.log(output)
  }
})
```

I get an error declaring "test" is an unknown argument:

```
kwilliams@KENLAPTOP:/mnt/c/Users/kwilliams/Documents/Projects/cheevobot$ node .
 !ch uptime

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]

Unknown argument: test
 !ch dice

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]

Unknown argument: test
```

In response to @bcoe:

```
It seems like to simplify this use-case though, we should add all keys provided in context to the list of known arguments?
```

That sounds like it should do the trick. In my use-case, the context variables are always something I know ahead of time (at least for now) and can thus account for by specifying `option()` for each of them directly, but in some cases the context might not be known ahead of time.

Pull request description:
fix: context variables are now recognized in strict() mode
strict mode now recognizes everything set in the `context` object sent to `parse()`.

fixes #791

Patch:
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..91c3361
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*.js]
+end_of_line = lf
+indent_style = space
+indent_size = 2
+insert_final_newline = true
diff --git a/README.md b/README.md
index 7f36555..ef2948e 100644
--- a/README.md
+++ b/README.md
@@ -980,6 +980,8 @@ $ node test.js
   '$0': 'test.js' }
 ```
 
+Note that a configuration object may extend from a JSON file using the `"extends"` property. When doing so, the `"extends"` value should be a path (relative or absolute) to the extended JSON file.
+
 <a name="conflicts"></a>.conflicts(x, y)
 ----------------------------------------------
 
@@ -1649,6 +1651,8 @@ as a configuration object.
 `cwd` can optionally be provided, the package.json will be read
 from this location.
 
+Note that a configuration stanza in package.json may extend from an identically keyed stanza in another package.json file using the `"extends"` property. When doing so, the `"extends"` value should be a path (relative or absolute) to the extended package.json file.
+
 .recommendCommands()
 ---------------------------
 
diff --git a/lib/apply-extends.js b/lib/apply-extends.js
new file mode 100644
index 0000000..ec751b0
--- /dev/null
+++ b/lib/apply-extends.js
@@ -0,0 +1,41 @@
+var fs = require('fs')
+var path = require('path')
+var assign = require('./assign')
+var YError = require('./yerror')
+
+var previouslyVisitedConfigs = []
+
+function checkForCircularExtends (path) {
+  if (previouslyVisitedConfigs.indexOf(path) > -1) {
+    throw new YError("Circular extended configurations: '" + path + "'.")
+  }
+}
+
+function getPathToDefaultConfig (cwd, pathToExtend) {
+  return path.resolve(cwd, pathToExtend)
+}
+
+function applyExtends (config, cwd, subKey) {
+  var defaultConfig = {}
+
+  if (config.hasOwnProperty('extends')) {
+    var pathToDefault = getPathToDefaultConfig(cwd, config.extends)
+
+    checkForCircularExtends(pathToDefault)
+
+    previouslyVisitedConfigs.push(pathToDefault)
+    delete config.extends
+
+    defaultConfig = JSON.parse(fs.readFileSync(pathToDefault, 'utf8'))
+    if (subKey) {
+      defaultConfig = defaultConfig[subKey] || {}
+    }
+    defaultConfig = applyExtends(defaultConfig, path.dirname(pathToDefault), subKey)
+  }
+
+  previouslyVisitedConfigs = []
+
+  return assign(defaultConfig, config)
+}
+
+module.exports = applyExtends
diff --git a/lib/validation.js b/lib/validation.js
index cc0ea65..3d3dbf5 100644
--- a/lib/validation.js
+++ b/lib/validation.js
@@ -135,6 +135,7 @@ module.exports = function (yargs, usage, y18n) {
         !descriptions.hasOwnProperty(key) &&
         !demandedOptions.hasOwnProperty(key) &&
         !positionalMap.hasOwnProperty(key) &&
+        !yargs._getParseContext().hasOwnProperty(key) &&
         !aliasLookup.hasOwnProperty(key)) {
         unknown.push(key)
       }
diff --git a/yargs.js b/yargs.js
index acaaa44..d1ffd31 100644
--- a/yargs.js
+++ b/yargs.js
@@ -9,6 +9,7 @@ const Validation = require('./lib/validation')
 const Y18n = require('y18n')
 const objFilter = require('./lib/obj-filter')
 const setBlocking = require('set-blocking')
+const applyExtends = require('./lib/apply-extends')
 const YError = require('./lib/yerror')
 
 var exports = module.exports = Yargs
@@ -304,6 +305,7 @@ function Yargs (processArgs, cwd, parentRequire) {
     argsert('[object|string] [string|function] [function]', [key, msg, parseFn], arguments.length)
     // allow a config object to be provided directly.
     if (typeof key === 'object') {
+      key = applyExtends(key, cwd)
       options.configObjects = (options.configObjects || []).concat(key)
       return self
     }
@@ -319,6 +321,7 @@ function Yargs (processArgs, cwd, parentRequire) {
     ;(Array.isArray(key) ? key : [key]).forEach(function (k) {
       options.config[k] = parseFn || true
     })
+
     return self
   }
 
@@ -469,11 +472,14 @@ function Yargs (processArgs, cwd, parentRequire) {
   self.pkgConf = function (key, path) {
     argsert('<string> [string]', [key, path], arguments.length)
     var conf = null
-    var obj = pkgUp(path)
+    // prefer cwd to require-main-filename in this method
+    // since we're looking for e.g. "nyc" config in nyc consumer
+    // rather than "yargs" config in nyc (where nyc is the main filename)
+    var obj = pkgUp(path || cwd)
 
     // If an object exists in the key, add it to options.configObjects
     if (obj[key] && typeof obj[key] === 'object') {
-      conf = obj[key]
+      conf = applyExtends(obj[key], path || cwd, key)
       options.configObjects = (options.configObjects || []).concat(conf)
     }
 
@@ -531,6 +537,10 @@ function Yargs (processArgs, cwd, parentRequire) {
     return parsed
   }
 
+  self._getParseContext = function () {
+    return parseContext || {}
+  }
+
   self._hasParseCallback = function () {
     return !!parseFn
   }


Test patch:
diff --git a/test/fixtures/extends/circular_1.json b/test/fixtures/extends/circular_1.json
new file mode 100644
index 0000000..4cd6629
--- /dev/null
+++ b/test/fixtures/extends/circular_1.json
@@ -0,0 +1,4 @@
+{
+  "a": 44,
+  "extends": "./circular_2.json"
+}
\ No newline at end of file
diff --git a/test/fixtures/extends/circular_2.json b/test/fixtures/extends/circular_2.json
new file mode 100644
index 0000000..85b7de8
--- /dev/null
+++ b/test/fixtures/extends/circular_2.json
@@ -0,0 +1,4 @@
+{
+  "b": "any",
+  "extends": "./circular_1.json"
+}
\ No newline at end of file
diff --git a/test/fixtures/extends/config_1.json b/test/fixtures/extends/config_1.json
new file mode 100644
index 0000000..043fb87
--- /dev/null
+++ b/test/fixtures/extends/config_1.json
@@ -0,0 +1,5 @@
+{
+  "a": 30,
+  "b": 22,
+  "extends": "./config_2.json"
+}
\ No newline at end of file
diff --git a/test/fixtures/extends/config_2.json b/test/fixtures/extends/config_2.json
new file mode 100644
index 0000000..886f419
--- /dev/null
+++ b/test/fixtures/extends/config_2.json
@@ -0,0 +1,3 @@
+{
+  "z": 15
+}
\ No newline at end of file
diff --git a/test/fixtures/extends/packageA/package.json b/test/fixtures/extends/packageA/package.json
new file mode 100644
index 0000000..236e60f
--- /dev/null
+++ b/test/fixtures/extends/packageA/package.json
@@ -0,0 +1,6 @@
+{
+  "foo": {
+    "a": 80,
+    "extends": "../packageB/package.json"
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/extends/packageB/package.json b/test/fixtures/extends/packageB/package.json
new file mode 100644
index 0000000..473fda7
--- /dev/null
+++ b/test/fixtures/extends/packageB/package.json
@@ -0,0 +1,6 @@
+{
+  "foo": {
+    "a": 90,
+    "b": "riffiwobbles"
+  }
+}
\ No newline at end of file
diff --git a/test/validation.js b/test/validation.js
index a831dea..53c30bb 100644
--- a/test/validation.js
+++ b/test/validation.js
@@ -233,6 +233,43 @@ describe('validation tests', function () {
       argv._[0].should.equal('koala')
     })
 
+    // addresses: https://github.com/yargs/yargs/issues/791
+    it('should recognize context variables in strict mode', function (done) {
+      yargs()
+        .command('foo <y>')
+        .strict()
+        .parse('foo 99', {x: 33}, function (err, argv, output) {
+          expect(err).to.equal(null)
+          expect(output).to.equal('')
+          argv.y.should.equal(99)
+          argv.x.should.equal(33)
+          argv._.should.include('foo')
+          return done()
+        })
+    })
+
+    // addresses: https://github.com/yargs/yargs/issues/791
+    it('should recognize context variables in strict mode, when running sub-commands', function (done) {
+      yargs()
+        .command('request', 'request command', function (yargs) {
+          yargs
+            .command('get', 'sub-command')
+            .option('y', {
+              describe: 'y inner option'
+            })
+        })
+        .strict()
+        .parse('request get --y=22', {x: 33}, function (err, argv, output) {
+          expect(err).to.equal(null)
+          expect(output).to.equal('')
+          argv.y.should.equal(22)
+          argv.x.should.equal(33)
+          argv._.should.include('request')
+          argv._.should.include('get')
+          return done()
+        })
+    })
+
     it('fails when a required argument is missing', function (done) {
       yargs('-w 10 marsupial')
         .demand(1, ['w', 'b'])
diff --git a/test/yargs.js b/test/yargs.js
index 6b50ae1..45719d2 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -1,16 +1,21 @@
-/* global context, describe, it, beforeEach */
+/* global context, describe, it, beforeEach, afterEach */
 
 var expect = require('chai').expect
 var fs = require('fs')
 var path = require('path')
 var checkOutput = require('./helpers/utils').checkOutput
-var yargs = require('../')
+var yargs
+var YError = require('../lib/yerror')
 
 require('chai').should()
 
 describe('yargs dsl tests', function () {
   beforeEach(function () {
-    yargs.reset()
+    yargs = require('../')
+  })
+
+  afterEach(function () {
+    delete require.cache[require.resolve('../')]
   })
 
   it('should use bin name for $0, eliminating path', function () {
@@ -1163,6 +1168,42 @@ describe('yargs dsl tests', function () {
       argv.foo.should.equal(1)
       argv.bar.should.equal(2)
     })
+
+    describe('extends', function () {
+      it('applies default configurations when given config object', function () {
+        var argv = yargs
+          .config({
+            extends: './test/fixtures/extends/config_1.json',
+            a: 1
+          })
+          .argv
+
+        argv.a.should.equal(1)
+        argv.b.should.equal(22)
+        argv.z.should.equal(15)
+      })
+
+      it('protects against circular extended configurations', function () {
+        expect(function () {
+          yargs.config({extends: './test/fixtures/extends/circular_1.json'})
+        }).to.throw(YError)
+      })
+
+      it('handles aboslute paths', function () {
+        var absolutePath = path.join(process.cwd(), 'test', 'fixtures', 'extends', 'config_1.json')
+
+        var argv = yargs
+          .config({
+            a: 2,
+            extends: absolutePath
+          })
+          .argv
+
+        argv.a.should.equal(2)
+        argv.b.should.equal(22)
+        argv.z.should.equal(15)
+      })
+    })
   })
 
   describe('normalize', function () {
@@ -1419,6 +1460,20 @@ describe('yargs dsl tests', function () {
 
       argv.foo.should.equal('a')
     })
+
+    it('should apply default configurations from extended packages', function () {
+      var argv = yargs().pkgConf('foo', 'test/fixtures/extends/packageA').argv
+
+      argv.a.should.equal(80)
+      argv.b.should.equals('riffiwobbles')
+    })
+
+    it('should apply extended configurations from cwd when no path is given', function () {
+      var argv = yargs('', 'test/fixtures/extends/packageA').pkgConf('foo').argv
+
+      argv.a.should.equal(80)
+      argv.b.should.equals('riffiwobbles')
+    })
   })
 
   describe('skipValidation', function () {