Skip to content

Commit 3280dd0

Browse files
rcoy-vbcoe
authored andcommitted
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
1 parent e0fbbe5 commit 3280dd0

File tree

11 files changed

+146
-5
lines changed

11 files changed

+146
-5
lines changed

.editorconfig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
root = true
2+
3+
[*.js]
4+
end_of_line = lf
5+
indent_style = space
6+
indent_size = 2
7+
insert_final_newline = true

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,8 @@ $ node test.js
980980
'$0': 'test.js' }
981981
```
982982

983+
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.
984+
983985
<a name="conflicts"></a>.conflicts(x, y)
984986
----------------------------------------------
985987

@@ -1649,6 +1651,8 @@ as a configuration object.
16491651
`cwd` can optionally be provided, the package.json will be read
16501652
from this location.
16511653

1654+
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.
1655+
16521656
.recommendCommands()
16531657
---------------------------
16541658

lib/apply-extends.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
var fs = require('fs')
2+
var path = require('path')
3+
var assign = require('./assign')
4+
var YError = require('./yerror')
5+
6+
var previouslyVisitedConfigs = []
7+
8+
function checkForCircularExtends (path) {
9+
if (previouslyVisitedConfigs.indexOf(path) > -1) {
10+
throw new YError("Circular extended configurations: '" + path + "'.")
11+
}
12+
}
13+
14+
function getPathToDefaultConfig (cwd, pathToExtend) {
15+
return path.isAbsolute(pathToExtend) ? pathToExtend : path.join(cwd, pathToExtend)
16+
}
17+
18+
function applyExtends (config, cwd, subKey) {
19+
var defaultConfig = {}
20+
21+
if (config.hasOwnProperty('extends')) {
22+
var pathToDefault = getPathToDefaultConfig(cwd, config.extends)
23+
24+
checkForCircularExtends(pathToDefault)
25+
26+
previouslyVisitedConfigs.push(pathToDefault)
27+
delete config.extends
28+
29+
defaultConfig = JSON.parse(fs.readFileSync(pathToDefault, 'utf8'))
30+
if (subKey) {
31+
defaultConfig = defaultConfig[subKey] || {}
32+
}
33+
defaultConfig = applyExtends(defaultConfig, path.dirname(pathToDefault), subKey)
34+
}
35+
36+
previouslyVisitedConfigs = []
37+
38+
return assign(defaultConfig, config)
39+
}
40+
41+
module.exports = applyExtends

test/fixtures/extends/circular_1.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"a": 44,
3+
"extends": "./circular_2.json"
4+
}

test/fixtures/extends/circular_2.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"b": "any",
3+
"extends": "./circular_1.json"
4+
}

test/fixtures/extends/config_1.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"a": 30,
3+
"b": 22,
4+
"extends": "./config_2.json"
5+
}

test/fixtures/extends/config_2.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"z": 15
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"foo": {
3+
"a": 80,
4+
"extends": "../packageB/package.json"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"foo": {
3+
"a": 90,
4+
"b": "riffiwobbles"
5+
}
6+
}

test/yargs.js

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
/* global context, describe, it, beforeEach */
1+
/* global context, describe, it, beforeEach, afterEach */
22

33
var expect = require('chai').expect
44
var fs = require('fs')
55
var path = require('path')
66
var checkOutput = require('./helpers/utils').checkOutput
7-
var yargs = require('../')
7+
var yargs
8+
var YError = require('../lib/yerror')
89

910
require('chai').should()
1011

1112
describe('yargs dsl tests', function () {
1213
beforeEach(function () {
13-
yargs.reset()
14+
yargs = require('../')
15+
})
16+
17+
afterEach(function () {
18+
delete require.cache[require.resolve('../')]
1419
})
1520

1621
it('should use bin name for $0, eliminating path', function () {
@@ -1163,6 +1168,42 @@ describe('yargs dsl tests', function () {
11631168
argv.foo.should.equal(1)
11641169
argv.bar.should.equal(2)
11651170
})
1171+
1172+
describe('extends', function () {
1173+
it('applies default configurations when given config object', function () {
1174+
var argv = yargs
1175+
.config({
1176+
extends: './test/fixtures/extends/config_1.json',
1177+
a: 1
1178+
})
1179+
.argv
1180+
1181+
argv.a.should.equal(1)
1182+
argv.b.should.equal(22)
1183+
argv.z.should.equal(15)
1184+
})
1185+
1186+
it('protects against circular extended configurations', function () {
1187+
expect(function () {
1188+
yargs.config({extends: './test/fixtures/extends/circular_1.json'})
1189+
}).to.throw(YError)
1190+
})
1191+
1192+
it('handles aboslute paths', function () {
1193+
var absolutePath = path.join(process.cwd(), 'test', 'fixtures', 'extends', 'config_1.json')
1194+
1195+
var argv = yargs
1196+
.config({
1197+
a: 2,
1198+
extends: absolutePath
1199+
})
1200+
.argv
1201+
1202+
argv.a.should.equal(2)
1203+
argv.b.should.equal(22)
1204+
argv.z.should.equal(15)
1205+
})
1206+
})
11661207
})
11671208

11681209
describe('normalize', function () {
@@ -1419,6 +1460,20 @@ describe('yargs dsl tests', function () {
14191460

14201461
argv.foo.should.equal('a')
14211462
})
1463+
1464+
it('should apply default configurations from extended packages', function () {
1465+
var argv = yargs().pkgConf('foo', 'test/fixtures/extends/packageA').argv
1466+
1467+
argv.a.should.equal(80)
1468+
argv.b.should.equals('riffiwobbles')
1469+
})
1470+
1471+
it('should apply extended configurations from cwd when no path is given', function () {
1472+
var argv = yargs('', 'test/fixtures/extends/packageA').pkgConf('foo').argv
1473+
1474+
argv.a.should.equal(80)
1475+
argv.b.should.equals('riffiwobbles')
1476+
})
14221477
})
14231478

14241479
describe('skipValidation', function () {

0 commit comments

Comments
 (0)