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/lib/apply-extends.js b/lib/apply-extends.js new file mode 100644 index 000000000..d91211b2a --- /dev/null +++ b/lib/apply-extends.js @@ -0,0 +1,37 @@ +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 applyExtends (config, cwd, subKey) { + var defaultConfig = {} + + if (config.hasOwnProperty('extends')) { + var pathToDefault = path.join(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 5d5771df8..85a8b6921 100644 --- a/test/yargs.js +++ b/test/yargs.js @@ -5,6 +5,7 @@ var fs = require('fs') var path = require('path') var checkOutput = require('./helpers/utils').checkOutput var yargs = require('../') +var YError = require('../lib/yerror') require('chai').should() @@ -1155,6 +1156,27 @@ 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) + }) + }) }) describe('normalize', function () { @@ -1411,6 +1433,13 @@ 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') + }) }) describe('skipValidation', function () { diff --git a/yargs.js b/yargs.js index f39150db3..42add466d 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 } @@ -473,7 +476,7 @@ function Yargs (processArgs, cwd, parentRequire) { // 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, key) options.configObjects = (options.configObjects || []).concat(conf) }