From 3280dd02ce125e00f2e4d02ae26fcb59e82a55a7 Mon Sep 17 00:00:00 2001 From: Ryan Vance Date: Sat, 25 Feb 2017 14:03:47 -0600 Subject: [PATCH] feat: allow provided config object to extend other configs (#779) BREAKING CHANGE: `extends` key in config file is now used for extending other config files --- .editorconfig | 7 +++ README.md | 4 ++ lib/apply-extends.js | 41 ++++++++++++++ test/fixtures/extends/circular_1.json | 4 ++ test/fixtures/extends/circular_2.json | 4 ++ test/fixtures/extends/config_1.json | 5 ++ test/fixtures/extends/config_2.json | 3 + test/fixtures/extends/packageA/package.json | 6 ++ test/fixtures/extends/packageB/package.json | 6 ++ test/yargs.js | 61 ++++++++++++++++++++- yargs.js | 10 +++- 11 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 .editorconfig create mode 100644 lib/apply-extends.js create mode 100644 test/fixtures/extends/circular_1.json create mode 100644 test/fixtures/extends/circular_2.json create mode 100644 test/fixtures/extends/config_1.json create mode 100644 test/fixtures/extends/config_2.json create mode 100644 test/fixtures/extends/packageA/package.json create mode 100644 test/fixtures/extends/packageB/package.json 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) }