diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..91c336188
--- /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 7f365552a..ef2948e33 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.
+
.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 000000000..1675fb420
--- /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.isAbsolute(pathToExtend) ? pathToExtend : path.join(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/test/fixtures/extends/circular_1.json b/test/fixtures/extends/circular_1.json
new file mode 100644
index 000000000..4cd662952
--- /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 000000000..85b7de82e
--- /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 000000000..043fb87b0
--- /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 000000000..886f419f4
--- /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 000000000..236e60f2c
--- /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 000000000..473fda728
--- /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/yargs.js b/test/yargs.js
index 6b50ae1c0..45719d2ab 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 () {
diff --git a/yargs.js b/yargs.js
index acaaa4461..17b83769d 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]', [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)
}