• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1module.exports = promzard
2promzard.PromZard = PromZard
3
4var fs = require('fs')
5var vm = require('vm')
6var util = require('util')
7var files = {}
8var crypto = require('crypto')
9var EventEmitter = require('events').EventEmitter
10var read = require('read')
11
12var Module = require('module').Module
13var path = require('path')
14
15function promzard (file, ctx, cb) {
16  if (typeof ctx === 'function') cb = ctx, ctx = null;
17  if (!ctx) ctx = {};
18  var pz = new PromZard(file, ctx)
19  pz.on('error', cb)
20  pz.on('data', function (data) {
21    cb(null, data)
22  })
23}
24promzard.fromBuffer = function (buf, ctx, cb) {
25  var filename = 0
26  do {
27    filename = '\0' + Math.random();
28  } while (files[filename])
29  files[filename] = buf
30  var ret = promzard(filename, ctx, cb)
31  delete files[filename]
32  return ret
33}
34
35function PromZard (file, ctx) {
36  if (!(this instanceof PromZard))
37    return new PromZard(file, ctx)
38  EventEmitter.call(this)
39  this.file = file
40  this.ctx = ctx
41  this.unique = crypto.randomBytes(8).toString('hex')
42  this.load()
43}
44
45PromZard.prototype = Object.create(
46  EventEmitter.prototype,
47  { constructor: {
48      value: PromZard,
49      readable: true,
50      configurable: true,
51      writable: true,
52      enumerable: false } } )
53
54PromZard.prototype.load = function () {
55  if (files[this.file])
56    return this.loaded()
57
58  fs.readFile(this.file, 'utf8', function (er, d) {
59    if (er && this.backupFile) {
60      this.file = this.backupFile
61      delete this.backupFile
62      return this.load()
63    }
64    if (er)
65      return this.emit('error', this.error = er)
66    files[this.file] = d
67    this.loaded()
68  }.bind(this))
69}
70
71PromZard.prototype.loaded = function () {
72  this.ctx.prompt = this.makePrompt()
73  this.ctx.__filename = this.file
74  this.ctx.__dirname = path.dirname(this.file)
75  this.ctx.__basename = path.basename(this.file)
76  var mod = this.ctx.module = this.makeModule()
77  this.ctx.require = function (path) {
78    return mod.require(path)
79  }
80  this.ctx.require.resolve = function(path) {
81    return Module._resolveFilename(path, mod);
82  }
83  this.ctx.exports = mod.exports
84
85  this.script = this.wrap(files[this.file])
86  var fn = vm.runInThisContext(this.script, this.file)
87  var args = Object.keys(this.ctx).map(function (k) {
88    return this.ctx[k]
89  }.bind(this))
90  try { var res = fn.apply(this.ctx, args) }
91  catch (er) { this.emit('error', er) }
92  if (res &&
93      typeof res === 'object' &&
94      exports === mod.exports &&
95      Object.keys(exports).length === 1) {
96    this.result = res
97  } else {
98    this.result = mod.exports
99  }
100  this.walk()
101}
102
103PromZard.prototype.makeModule = function () {
104  var mod = new Module(this.file, module)
105  mod.loaded = true
106  mod.filename = this.file
107  mod.id = this.file
108  mod.paths = Module._nodeModulePaths(path.dirname(this.file))
109  return mod
110}
111
112PromZard.prototype.wrap = function (body) {
113  var s = '(function( %s ) { %s\n })'
114  var args = Object.keys(this.ctx).join(', ')
115  return util.format(s, args, body)
116}
117
118PromZard.prototype.makePrompt = function () {
119  this.prompts = []
120  return prompt.bind(this)
121  function prompt () {
122    var p, d, t
123    for (var i = 0; i < arguments.length; i++) {
124      var a = arguments[i]
125      if (typeof a === 'string' && p)
126        d = a
127      else if (typeof a === 'string')
128        p = a
129      else if (typeof a === 'function')
130        t = a
131      else if (a && typeof a === 'object') {
132        p = a.prompt || p
133        d = a.default || d
134        t = a.transform || t
135      }
136    }
137
138    try { return this.unique + '-' + this.prompts.length }
139    finally { this.prompts.push([p, d, t]) }
140  }
141}
142
143PromZard.prototype.walk = function (o, cb) {
144  o = o || this.result
145  cb = cb || function (er, res) {
146    if (er)
147      return this.emit('error', this.error = er)
148    this.result = res
149    return this.emit('data', res)
150  }
151  cb = cb.bind(this)
152  var keys = Object.keys(o)
153  var i = 0
154  var len = keys.length
155
156  L.call(this)
157  function L () {
158    if (this.error)
159      return
160    while (i < len) {
161      var k = keys[i]
162      var v = o[k]
163      i++
164
165      if (v && typeof v === 'object') {
166        return this.walk(v, function (er, res) {
167          if (er) return cb(er)
168          o[k] = res
169          L.call(this)
170        }.bind(this))
171      } else if (v &&
172                 typeof v === 'string' &&
173                 v.indexOf(this.unique) === 0) {
174        var n = +v.substr(this.unique.length + 1)
175        var prompt = this.prompts[n]
176        if (isNaN(n) || !prompt)
177          continue
178
179        // default to the key
180        if (undefined === prompt[0])
181          prompt[0] = k
182
183        // default to the ctx value, if there is one
184        if (undefined === prompt[1])
185          prompt[1] = this.ctx[k]
186
187        return this.prompt(prompt, function (er, res) {
188          if (er) {
189            if (!er.notValid) {
190              return this.emit('error', this.error = er);
191            }
192            console.log(er.message)
193            i --
194            return L.call(this)
195          }
196          o[k] = res
197          L.call(this)
198        }.bind(this))
199      } else if (typeof v === 'function') {
200        try { return v.call(this.ctx, function (er, res) {
201          if (er)
202            return this.emit('error', this.error = er)
203          o[k] = res
204          // back up so that we process this one again.
205          // this is because it might return a prompt() call in the cb.
206          i --
207          L.call(this)
208        }.bind(this)) }
209        catch (er) { this.emit('error', er) }
210      }
211    }
212    // made it to the end of the loop, maybe
213    if (i >= len)
214      return cb(null, o)
215  }
216}
217
218PromZard.prototype.prompt = function (pdt, cb) {
219  var prompt = pdt[0]
220  var def = pdt[1]
221  var tx = pdt[2]
222
223  if (tx) {
224    cb = function (cb) { return function (er, data) {
225      try {
226        var res = tx(data)
227        if (!er && res instanceof Error && !!res.notValid) {
228          return cb(res, null)
229        }
230        return cb(er, res)
231      }
232      catch (er) { this.emit('error', er) }
233    }}(cb).bind(this)
234  }
235
236  read({ prompt: prompt + ':' , default: def }, cb)
237}
238
239