You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
283 lines
7.0 KiB
283 lines
7.0 KiB
4 years ago
|
var ProtoList = require('proto-list')
|
||
|
, path = require('path')
|
||
|
, fs = require('fs')
|
||
|
, ini = require('ini')
|
||
|
, EE = require('events').EventEmitter
|
||
|
, url = require('url')
|
||
|
, http = require('http')
|
||
|
|
||
|
var exports = module.exports = function () {
|
||
|
var args = [].slice.call(arguments)
|
||
|
, conf = new ConfigChain()
|
||
|
|
||
|
while(args.length) {
|
||
|
var a = args.shift()
|
||
|
if(a) conf.push
|
||
|
( 'string' === typeof a
|
||
|
? json(a)
|
||
|
: a )
|
||
|
}
|
||
|
|
||
|
return conf
|
||
|
}
|
||
|
|
||
|
//recursively find a file...
|
||
|
|
||
|
var find = exports.find = function () {
|
||
|
var rel = path.join.apply(null, [].slice.call(arguments))
|
||
|
|
||
|
function find(start, rel) {
|
||
|
var file = path.join(start, rel)
|
||
|
try {
|
||
|
fs.statSync(file)
|
||
|
return file
|
||
|
} catch (err) {
|
||
|
if(path.dirname(start) !== start) // root
|
||
|
return find(path.dirname(start), rel)
|
||
|
}
|
||
|
}
|
||
|
return find(__dirname, rel)
|
||
|
}
|
||
|
|
||
|
var parse = exports.parse = function (content, file, type) {
|
||
|
content = '' + content
|
||
|
// if we don't know what it is, try json and fall back to ini
|
||
|
// if we know what it is, then it must be that.
|
||
|
if (!type) {
|
||
|
try { return JSON.parse(content) }
|
||
|
catch (er) { return ini.parse(content) }
|
||
|
} else if (type === 'json') {
|
||
|
if (this.emit) {
|
||
|
try { return JSON.parse(content) }
|
||
|
catch (er) { this.emit('error', er) }
|
||
|
} else {
|
||
|
return JSON.parse(content)
|
||
|
}
|
||
|
} else {
|
||
|
return ini.parse(content)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var json = exports.json = function () {
|
||
|
var args = [].slice.call(arguments).filter(function (arg) { return arg != null })
|
||
|
var file = path.join.apply(null, args)
|
||
|
var content
|
||
|
try {
|
||
|
content = fs.readFileSync(file,'utf-8')
|
||
|
} catch (err) {
|
||
|
return
|
||
|
}
|
||
|
return parse(content, file, 'json')
|
||
|
}
|
||
|
|
||
|
var env = exports.env = function (prefix, env) {
|
||
|
env = env || process.env
|
||
|
var obj = {}
|
||
|
var l = prefix.length
|
||
|
for(var k in env) {
|
||
|
if(k.indexOf(prefix) === 0)
|
||
|
obj[k.substring(l)] = env[k]
|
||
|
}
|
||
|
|
||
|
return obj
|
||
|
}
|
||
|
|
||
|
exports.ConfigChain = ConfigChain
|
||
|
function ConfigChain () {
|
||
|
EE.apply(this)
|
||
|
ProtoList.apply(this, arguments)
|
||
|
this._awaiting = 0
|
||
|
this._saving = 0
|
||
|
this.sources = {}
|
||
|
}
|
||
|
|
||
|
// multi-inheritance-ish
|
||
|
var extras = {
|
||
|
constructor: { value: ConfigChain }
|
||
|
}
|
||
|
Object.keys(EE.prototype).forEach(function (k) {
|
||
|
extras[k] = Object.getOwnPropertyDescriptor(EE.prototype, k)
|
||
|
})
|
||
|
ConfigChain.prototype = Object.create(ProtoList.prototype, extras)
|
||
|
|
||
|
ConfigChain.prototype.del = function (key, where) {
|
||
|
// if not specified where, then delete from the whole chain, scorched
|
||
|
// earth style
|
||
|
if (where) {
|
||
|
var target = this.sources[where]
|
||
|
target = target && target.data
|
||
|
if (!target) {
|
||
|
return this.emit('error', new Error('not found '+where))
|
||
|
}
|
||
|
delete target[key]
|
||
|
} else {
|
||
|
for (var i = 0, l = this.list.length; i < l; i ++) {
|
||
|
delete this.list[i][key]
|
||
|
}
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.set = function (key, value, where) {
|
||
|
var target
|
||
|
|
||
|
if (where) {
|
||
|
target = this.sources[where]
|
||
|
target = target && target.data
|
||
|
if (!target) {
|
||
|
return this.emit('error', new Error('not found '+where))
|
||
|
}
|
||
|
} else {
|
||
|
target = this.list[0]
|
||
|
if (!target) {
|
||
|
return this.emit('error', new Error('cannot set, no confs!'))
|
||
|
}
|
||
|
}
|
||
|
target[key] = value
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.get = function (key, where) {
|
||
|
if (where) {
|
||
|
where = this.sources[where]
|
||
|
if (where) where = where.data
|
||
|
if (where && Object.hasOwnProperty.call(where, key)) return where[key]
|
||
|
return undefined
|
||
|
}
|
||
|
return this.list[0][key]
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.save = function (where, type, cb) {
|
||
|
if (typeof type === 'function') cb = type, type = null
|
||
|
var target = this.sources[where]
|
||
|
if (!target || !(target.path || target.source) || !target.data) {
|
||
|
// TODO: maybe save() to a url target could be a PUT or something?
|
||
|
// would be easy to swap out with a reddis type thing, too
|
||
|
return this.emit('error', new Error('bad save target: '+where))
|
||
|
}
|
||
|
|
||
|
if (target.source) {
|
||
|
var pref = target.prefix || ''
|
||
|
Object.keys(target.data).forEach(function (k) {
|
||
|
target.source[pref + k] = target.data[k]
|
||
|
})
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
var type = type || target.type
|
||
|
var data = target.data
|
||
|
if (target.type === 'json') {
|
||
|
data = JSON.stringify(data)
|
||
|
} else {
|
||
|
data = ini.stringify(data)
|
||
|
}
|
||
|
|
||
|
this._saving ++
|
||
|
fs.writeFile(target.path, data, 'utf8', function (er) {
|
||
|
this._saving --
|
||
|
if (er) {
|
||
|
if (cb) return cb(er)
|
||
|
else return this.emit('error', er)
|
||
|
}
|
||
|
if (this._saving === 0) {
|
||
|
if (cb) cb()
|
||
|
this.emit('save')
|
||
|
}
|
||
|
}.bind(this))
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.addFile = function (file, type, name) {
|
||
|
name = name || file
|
||
|
var marker = {__source__:name}
|
||
|
this.sources[name] = { path: file, type: type }
|
||
|
this.push(marker)
|
||
|
this._await()
|
||
|
fs.readFile(file, 'utf8', function (er, data) {
|
||
|
if (er) this.emit('error', er)
|
||
|
this.addString(data, file, type, marker)
|
||
|
}.bind(this))
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.addEnv = function (prefix, env, name) {
|
||
|
name = name || 'env'
|
||
|
var data = exports.env(prefix, env)
|
||
|
this.sources[name] = { data: data, source: env, prefix: prefix }
|
||
|
return this.add(data, name)
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.addUrl = function (req, type, name) {
|
||
|
this._await()
|
||
|
var href = url.format(req)
|
||
|
name = name || href
|
||
|
var marker = {__source__:name}
|
||
|
this.sources[name] = { href: href, type: type }
|
||
|
this.push(marker)
|
||
|
http.request(req, function (res) {
|
||
|
var c = []
|
||
|
var ct = res.headers['content-type']
|
||
|
if (!type) {
|
||
|
type = ct.indexOf('json') !== -1 ? 'json'
|
||
|
: ct.indexOf('ini') !== -1 ? 'ini'
|
||
|
: href.match(/\.json$/) ? 'json'
|
||
|
: href.match(/\.ini$/) ? 'ini'
|
||
|
: null
|
||
|
marker.type = type
|
||
|
}
|
||
|
|
||
|
res.on('data', c.push.bind(c))
|
||
|
.on('end', function () {
|
||
|
this.addString(Buffer.concat(c), href, type, marker)
|
||
|
}.bind(this))
|
||
|
.on('error', this.emit.bind(this, 'error'))
|
||
|
|
||
|
}.bind(this))
|
||
|
.on('error', this.emit.bind(this, 'error'))
|
||
|
.end()
|
||
|
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.addString = function (data, file, type, marker) {
|
||
|
data = this.parse(data, file, type)
|
||
|
this.add(data, marker)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.add = function (data, marker) {
|
||
|
if (marker && typeof marker === 'object') {
|
||
|
var i = this.list.indexOf(marker)
|
||
|
if (i === -1) {
|
||
|
return this.emit('error', new Error('bad marker'))
|
||
|
}
|
||
|
this.splice(i, 1, data)
|
||
|
marker = marker.__source__
|
||
|
this.sources[marker] = this.sources[marker] || {}
|
||
|
this.sources[marker].data = data
|
||
|
// we were waiting for this. maybe emit 'load'
|
||
|
this._resolve()
|
||
|
} else {
|
||
|
if (typeof marker === 'string') {
|
||
|
this.sources[marker] = this.sources[marker] || {}
|
||
|
this.sources[marker].data = data
|
||
|
}
|
||
|
// trigger the load event if nothing was already going to do so.
|
||
|
this._await()
|
||
|
this.push(data)
|
||
|
process.nextTick(this._resolve.bind(this))
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype.parse = exports.parse
|
||
|
|
||
|
ConfigChain.prototype._await = function () {
|
||
|
this._awaiting++
|
||
|
}
|
||
|
|
||
|
ConfigChain.prototype._resolve = function () {
|
||
|
this._awaiting--
|
||
|
if (this._awaiting === 0) this.emit('load', this)
|
||
|
}
|