Skip to content

Commit

Permalink
feat: allow provided config object to extend other configs (#779)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `extends` key in config file is now used for extending other config files
  • Loading branch information
rcoy-v authored and bcoe committed Feb 26, 2017
1 parent e0fbbe5 commit 3280dd0
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 5 deletions.
7 changes: 7 additions & 0 deletions .editorconfig
@@ -0,0 +1,7 @@
root = true

[*.js]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -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)
----------------------------------------------

Expand Down Expand Up @@ -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()
---------------------------

Expand Down
41 changes: 41 additions & 0 deletions 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
4 changes: 4 additions & 0 deletions test/fixtures/extends/circular_1.json
@@ -0,0 +1,4 @@
{
"a": 44,
"extends": "./circular_2.json"
}
4 changes: 4 additions & 0 deletions test/fixtures/extends/circular_2.json
@@ -0,0 +1,4 @@
{
"b": "any",
"extends": "./circular_1.json"
}
5 changes: 5 additions & 0 deletions test/fixtures/extends/config_1.json
@@ -0,0 +1,5 @@
{
"a": 30,
"b": 22,
"extends": "./config_2.json"
}
3 changes: 3 additions & 0 deletions test/fixtures/extends/config_2.json
@@ -0,0 +1,3 @@
{
"z": 15
}
6 changes: 6 additions & 0 deletions test/fixtures/extends/packageA/package.json
@@ -0,0 +1,6 @@
{
"foo": {
"a": 80,
"extends": "../packageB/package.json"
}
}
6 changes: 6 additions & 0 deletions test/fixtures/extends/packageB/package.json
@@ -0,0 +1,6 @@
{
"foo": {
"a": 90,
"b": "riffiwobbles"
}
}
61 changes: 58 additions & 3 deletions 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 () {
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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 () {
Expand Down
10 changes: 8 additions & 2 deletions yargs.js
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -319,6 +321,7 @@ function Yargs (processArgs, cwd, parentRequire) {
;(Array.isArray(key) ? key : [key]).forEach(function (k) {
options.config[k] = parseFn || true
})

return self
}

Expand Down Expand Up @@ -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)
}

Expand Down

0 comments on commit 3280dd0

Please sign in to comment.