283 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| 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)
 | |
| }
 |