Repository: yargs/yargs (js)

Issue:
Async middleware called twice
I need to refresh token on every command. As a solution i've been tried to create async middleware but encountered a problem: middleware called twice per command if i return a Promise or make async function. Simplified form:
middleware
```js 
const refreshTokenMiddleware = () => {
  console.log('middleware: start')
  return new Promise(resolve => setTimeout(resolve, 10)).then(() =>
    console.log('middleware: complete')
  )
}
```
register
```js
yargs
  .commandDir('commands')
  .middleware(refreshTokenMiddleware)
```
command handler
```js
exports.handler = async () => {
  console.log('handler:start')
  await new Promise(resolve => setTimeout(resolve, 10))
  console.log('handler:complete')
}

```
Here is the output:
```
middleware: start
middleware: complete
handler:start
middleware: start
middleware: complete
handler:complete
```

I have tried to trace down this behaviour, but without any luck (insufficient time). 

Pull request:
fix: #1335: async middleware called twice
Closes #1335

Async middleware where called:
- once for the innermost subcommand (expected)
- then once for every parent of this subcommand (unexpected)

Due to an incorrect check in the first run of middleware (before validation).

CI will probably fail as #1411 introduced a broken test in master.

Patch:
diff --git a/docs/api.md b/docs/api.md
index d7eeb9e..cd1c06a 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1147,7 +1147,7 @@ command handler, for example.
 If the arguments have not been parsed, this property is `false`.
 
 If the arguments have been parsed, this contain detailed parsed arguments. See
-the documentation in [yargs-parser `.detailed()`][https://github.com/yargs/yargs-parser/blob/master/README.md#requireyargs-parserdetailedargs-opts]
+the documentation in [yargs-parser `.detailed()`](https://github.com/yargs/yargs-parser/blob/master/README.md#requireyargs-parserdetailedargs-opts)
 for details of this object
 
 <a name="parserConfiguration"></a>.parserConfiguration(obj)
diff --git a/lib/apply-extends.js b/lib/apply-extends.js
index 20ded5e..643c913 100644
--- a/lib/apply-extends.js
+++ b/lib/apply-extends.js
@@ -56,7 +56,7 @@ function applyExtends (config, cwd, mergeExtends) {
 
     defaultConfig = isPath ? JSON.parse(fs.readFileSync(pathToDefault, 'utf8')) : require(config.extends)
     delete config.extends
-    defaultConfig = applyExtends(defaultConfig, path.dirname(pathToDefault))
+    defaultConfig = applyExtends(defaultConfig, path.dirname(pathToDefault), mergeExtends)
   }
 
   previouslyVisitedConfigs = []
diff --git a/lib/is-promise.js b/lib/is-promise.js
index 0516def..271d93b 100644
--- a/lib/is-promise.js
+++ b/lib/is-promise.js
@@ -1,3 +1,3 @@
 module.exports = function isPromise (maybePromise) {
-  return maybePromise instanceof Promise
+  return !!maybePromise && !!maybePromise.then && (typeof maybePromise.then === 'function')
 }
diff --git a/lib/middleware.js b/lib/middleware.js
index 9f9c196..56dab75 100644
--- a/lib/middleware.js
+++ b/lib/middleware.js
@@ -40,8 +40,7 @@ function applyMiddleware (argv, yargs, middlewares, beforeValidation) {
   const beforeValidationError = new Error('middleware cannot return a promise when applyBeforeValidation is true')
   return middlewares
     .reduce((accumulation, middleware) => {
-      if (middleware.applyBeforeValidation !== beforeValidation &&
-          !isPromise(accumulation)) {
+      if (middleware.applyBeforeValidation !== beforeValidation) {
         return accumulation
       }
 
diff --git a/lib/validation.js b/lib/validation.js
index 1a31e58..35659a3 100644
--- a/lib/validation.js
+++ b/lib/validation.js
@@ -224,43 +224,36 @@ module.exports = function validation (yargs, usage, y18n) {
     return implied
   }
 
+  function keyExists (argv, val) {
+    // convert string '1' to number 1
+    let num = Number(val)
+    val = isNaN(num) ? val : num
+
+    if (typeof val === 'number') {
+      // check length of argv._
+      val = argv._.length >= val
+    } else if (val.match(/^--no-.+/)) {
+      // check if key/value doesn't exist
+      val = val.match(/^--no-(.+)/)[1]
+      val = !argv[val]
+    } else {
+      // check if key/value exists
+      val = argv[val]
+    }
+    return val
+  }
+
   self.implications = function implications (argv) {
     const implyFail = []
 
     Object.keys(implied).forEach((key) => {
       const origKey = key
       ;(implied[key] || []).forEach((value) => {
-        let num
         let key = origKey
         const origValue = value
+        key = keyExists(argv, key)
+        value = keyExists(argv, value)
 
-        // convert string '1' to number 1
-        num = Number(key)
-        key = isNaN(num) ? key : num
-
-        if (typeof key === 'number') {
-          // check length of argv._
-          key = argv._.length >= key
-        } else if (key.match(/^--no-.+/)) {
-          // check if key doesn't exist
-          key = key.match(/^--no-(.+)/)[1]
-          key = !argv[key]
-        } else {
-          // check if key exists
-          key = argv[key]
-        }
-
-        num = Number(value)
-        value = isNaN(num) ? value : num
-
-        if (typeof value === 'number') {
-          value = argv._.length >= value
-        } else if (value.match(/^--no-.+/)) {
-          value = value.match(/^--no-(.+)/)[1]
-          value = !argv[value]
-        } else {
-          value = argv[value]
-        }
         if (key && !value) {
           implyFail.push(` ${origKey} -> ${origValue}`)
         }


Test patch:
diff --git a/test/fixtures/extends/config_deep_2.json b/test/fixtures/extends/config_deep_2.json
new file mode 100644
index 0000000..88c9825
--- /dev/null
+++ b/test/fixtures/extends/config_deep_2.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./config_deep.json",
+  "a": {
+    "b": 12,
+    "g": 1
+  }
+}
diff --git a/test/is-promise.js b/test/is-promise.js
new file mode 100644
index 0000000..40f8662
--- /dev/null
+++ b/test/is-promise.js
@@ -0,0 +1,34 @@
+'use strict'
+/* global describe, it */
+
+const { expect } = require('chai')
+const isPromise = require('../lib/is-promise')
+
+describe('isPromise', () => {
+  it('returns `false` on non promise value', () => {
+    expect(isPromise('hello there.')).to.be.equal(false)
+  })
+
+  it('returns `true` on es6 promise', () => {
+    const esPromise = new Promise(() => {})
+    expect(isPromise(esPromise)).to.be.equal(true)
+  })
+
+  it('returns `true` on some other thenable', () => {
+    const thenable = { then: () => {} }
+    expect(isPromise(thenable)).to.be.equal(true)
+  })
+
+  it('returns `false` if some falsy value is passed', () => {
+    expect(isPromise(null)).to.be.equal(false)
+  })
+
+  it('returns `false` if passed object has no `then` property', () => {
+    expect(isPromise({})).to.be.equal(false)
+  })
+
+  it('returns `false` if `then` is not a function', () => {
+    const obj = { then: 4711 }
+    expect(isPromise(obj)).to.be.equal(false)
+  })
+})
diff --git a/test/middleware.js b/test/middleware.js
index dd6638c..5ea3189 100644
--- a/test/middleware.js
+++ b/test/middleware.js
@@ -145,6 +145,36 @@ describe('middleware', () => {
         .exitProcess(false)
         .parse()
     })
+
+    it('calls an async middleware only once for nested subcommands', (done) => {
+      let callCount = 0
+      let argv = yargs('cmd subcmd')
+        .command(
+          'cmd',
+          'cmd command',
+          function (yargs) {
+            yargs.command(
+              'subcmd',
+              'subcmd command',
+              function (yargs) {}
+            )
+          }
+        )
+        .middleware((argv) => new Promise((resolve) => {
+          callCount++
+          resolve(argv)
+        }))
+        .parse()
+
+      if (!(argv instanceof Promise)) done('argv should be a Promise')
+
+      argv
+        .then(() => {
+          callCount.should.equal(1)
+          done()
+        })
+        .catch(err => done(err))
+    })
   })
 
   // see: https://github.com/yargs/yargs/issues/1281
diff --git a/test/yargs.js b/test/yargs.js
index fcdd669..795f1dc 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -1402,6 +1402,23 @@ describe('yargs dsl tests', () => {
         argv.test.yes.should.equal(1)
       })
 
+      it('deep merges multiple configs when extending when deep-merge-config=true', () => {
+        const argv = yargs()
+          .parserConfiguration({ 'deep-merge-config': true })
+          .config({
+            extends: './test/fixtures/extends/config_deep_2.json',
+            a: {
+              g: 3
+            }
+          })
+          .parse()
+
+        argv.test.yes.should.equal(1)
+        argv.a.b.should.equal(12)
+        argv.a.c.should.equal(12)
+        argv.a.g.should.equal(3)
+      })
+
       it('does not deep merge objects by default', () => {
         const argv = yargs()
           .config({