index.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. var ProtoList = require('proto-list')
  2. , path = require('path')
  3. , fs = require('fs')
  4. , ini = require('ini')
  5. , EE = require('events').EventEmitter
  6. , url = require('url')
  7. , http = require('http')
  8. var exports = module.exports = function () {
  9. var args = [].slice.call(arguments)
  10. , conf = new ConfigChain()
  11. while(args.length) {
  12. var a = args.shift()
  13. if(a) conf.push
  14. ( 'string' === typeof a
  15. ? json(a)
  16. : a )
  17. }
  18. return conf
  19. }
  20. //recursively find a file...
  21. var find = exports.find = function () {
  22. var rel = path.join.apply(null, [].slice.call(arguments))
  23. function find(start, rel) {
  24. var file = path.join(start, rel)
  25. try {
  26. fs.statSync(file)
  27. return file
  28. } catch (err) {
  29. if(path.dirname(start) !== start) // root
  30. return find(path.dirname(start), rel)
  31. }
  32. }
  33. return find(__dirname, rel)
  34. }
  35. var parse = exports.parse = function (content, file, type) {
  36. content = '' + content
  37. // if we don't know what it is, try json and fall back to ini
  38. // if we know what it is, then it must be that.
  39. if (!type) {
  40. try { return JSON.parse(content) }
  41. catch (er) { return ini.parse(content) }
  42. } else if (type === 'json') {
  43. if (this.emit) {
  44. try { return JSON.parse(content) }
  45. catch (er) { this.emit('error', er) }
  46. } else {
  47. return JSON.parse(content)
  48. }
  49. } else {
  50. return ini.parse(content)
  51. }
  52. }
  53. var json = exports.json = function () {
  54. var args = [].slice.call(arguments).filter(function (arg) { return arg != null })
  55. var file = path.join.apply(null, args)
  56. var content
  57. try {
  58. content = fs.readFileSync(file,'utf-8')
  59. } catch (err) {
  60. return
  61. }
  62. return parse(content, file, 'json')
  63. }
  64. var env = exports.env = function (prefix, env) {
  65. env = env || process.env
  66. var obj = {}
  67. var l = prefix.length
  68. for(var k in env) {
  69. if(k.indexOf(prefix) === 0)
  70. obj[k.substring(l)] = env[k]
  71. }
  72. return obj
  73. }
  74. exports.ConfigChain = ConfigChain
  75. function ConfigChain () {
  76. EE.apply(this)
  77. ProtoList.apply(this, arguments)
  78. this._awaiting = 0
  79. this._saving = 0
  80. this.sources = {}
  81. }
  82. // multi-inheritance-ish
  83. var extras = {
  84. constructor: { value: ConfigChain }
  85. }
  86. Object.keys(EE.prototype).forEach(function (k) {
  87. extras[k] = Object.getOwnPropertyDescriptor(EE.prototype, k)
  88. })
  89. ConfigChain.prototype = Object.create(ProtoList.prototype, extras)
  90. ConfigChain.prototype.del = function (key, where) {
  91. // if not specified where, then delete from the whole chain, scorched
  92. // earth style
  93. if (where) {
  94. var target = this.sources[where]
  95. target = target && target.data
  96. if (!target) {
  97. return this.emit('error', new Error('not found '+where))
  98. }
  99. delete target[key]
  100. } else {
  101. for (var i = 0, l = this.list.length; i < l; i ++) {
  102. delete this.list[i][key]
  103. }
  104. }
  105. return this
  106. }
  107. ConfigChain.prototype.set = function (key, value, where) {
  108. var target
  109. if (where) {
  110. target = this.sources[where]
  111. target = target && target.data
  112. if (!target) {
  113. return this.emit('error', new Error('not found '+where))
  114. }
  115. } else {
  116. target = this.list[0]
  117. if (!target) {
  118. return this.emit('error', new Error('cannot set, no confs!'))
  119. }
  120. }
  121. target[key] = value
  122. return this
  123. }
  124. ConfigChain.prototype.get = function (key, where) {
  125. if (where) {
  126. where = this.sources[where]
  127. if (where) where = where.data
  128. if (where && Object.hasOwnProperty.call(where, key)) return where[key]
  129. return undefined
  130. }
  131. return this.list[0][key]
  132. }
  133. ConfigChain.prototype.save = function (where, type, cb) {
  134. if (typeof type === 'function') cb = type, type = null
  135. var target = this.sources[where]
  136. if (!target || !(target.path || target.source) || !target.data) {
  137. // TODO: maybe save() to a url target could be a PUT or something?
  138. // would be easy to swap out with a reddis type thing, too
  139. return this.emit('error', new Error('bad save target: '+where))
  140. }
  141. if (target.source) {
  142. var pref = target.prefix || ''
  143. Object.keys(target.data).forEach(function (k) {
  144. target.source[pref + k] = target.data[k]
  145. })
  146. return this
  147. }
  148. var type = type || target.type
  149. var data = target.data
  150. if (target.type === 'json') {
  151. data = JSON.stringify(data)
  152. } else {
  153. data = ini.stringify(data)
  154. }
  155. this._saving ++
  156. fs.writeFile(target.path, data, 'utf8', function (er) {
  157. this._saving --
  158. if (er) {
  159. if (cb) return cb(er)
  160. else return this.emit('error', er)
  161. }
  162. if (this._saving === 0) {
  163. if (cb) cb()
  164. this.emit('save')
  165. }
  166. }.bind(this))
  167. return this
  168. }
  169. ConfigChain.prototype.addFile = function (file, type, name) {
  170. name = name || file
  171. var marker = {__source__:name}
  172. this.sources[name] = { path: file, type: type }
  173. this.push(marker)
  174. this._await()
  175. fs.readFile(file, 'utf8', function (er, data) {
  176. if (er) this.emit('error', er)
  177. this.addString(data, file, type, marker)
  178. }.bind(this))
  179. return this
  180. }
  181. ConfigChain.prototype.addEnv = function (prefix, env, name) {
  182. name = name || 'env'
  183. var data = exports.env(prefix, env)
  184. this.sources[name] = { data: data, source: env, prefix: prefix }
  185. return this.add(data, name)
  186. }
  187. ConfigChain.prototype.addUrl = function (req, type, name) {
  188. this._await()
  189. var href = url.format(req)
  190. name = name || href
  191. var marker = {__source__:name}
  192. this.sources[name] = { href: href, type: type }
  193. this.push(marker)
  194. http.request(req, function (res) {
  195. var c = []
  196. var ct = res.headers['content-type']
  197. if (!type) {
  198. type = ct.indexOf('json') !== -1 ? 'json'
  199. : ct.indexOf('ini') !== -1 ? 'ini'
  200. : href.match(/\.json$/) ? 'json'
  201. : href.match(/\.ini$/) ? 'ini'
  202. : null
  203. marker.type = type
  204. }
  205. res.on('data', c.push.bind(c))
  206. .on('end', function () {
  207. this.addString(Buffer.concat(c), href, type, marker)
  208. }.bind(this))
  209. .on('error', this.emit.bind(this, 'error'))
  210. }.bind(this))
  211. .on('error', this.emit.bind(this, 'error'))
  212. .end()
  213. return this
  214. }
  215. ConfigChain.prototype.addString = function (data, file, type, marker) {
  216. data = this.parse(data, file, type)
  217. this.add(data, marker)
  218. return this
  219. }
  220. ConfigChain.prototype.add = function (data, marker) {
  221. if (marker && typeof marker === 'object') {
  222. var i = this.list.indexOf(marker)
  223. if (i === -1) {
  224. return this.emit('error', new Error('bad marker'))
  225. }
  226. this.splice(i, 1, data)
  227. marker = marker.__source__
  228. this.sources[marker] = this.sources[marker] || {}
  229. this.sources[marker].data = data
  230. // we were waiting for this. maybe emit 'load'
  231. this._resolve()
  232. } else {
  233. if (typeof marker === 'string') {
  234. this.sources[marker] = this.sources[marker] || {}
  235. this.sources[marker].data = data
  236. }
  237. // trigger the load event if nothing was already going to do so.
  238. this._await()
  239. this.push(data)
  240. process.nextTick(this._resolve.bind(this))
  241. }
  242. return this
  243. }
  244. ConfigChain.prototype.parse = exports.parse
  245. ConfigChain.prototype._await = function () {
  246. this._awaiting++
  247. }
  248. ConfigChain.prototype._resolve = function () {
  249. this._awaiting--
  250. if (this._awaiting === 0) this.emit('load', this)
  251. }