1const fs = require('fs/promises') 2const { runInThisContext } = require('vm') 3const { promisify } = require('util') 4const { randomBytes } = require('crypto') 5const { Module } = require('module') 6const { dirname, basename } = require('path') 7const read = require('read') 8 9const files = {} 10 11class PromZard { 12 #file = null 13 #backupFile = null 14 #ctx = null 15 #unique = randomBytes(8).toString('hex') 16 #prompts = [] 17 18 constructor (file, ctx = {}, options = {}) { 19 this.#file = file 20 this.#ctx = ctx 21 this.#backupFile = options.backupFile 22 } 23 24 static async promzard (file, ctx, options) { 25 const pz = new PromZard(file, ctx, options) 26 return pz.load() 27 } 28 29 static async fromBuffer (buf, ctx, options) { 30 let filename = 0 31 do { 32 filename = '\0' + Math.random() 33 } while (files[filename]) 34 files[filename] = buf 35 const ret = await PromZard.promzard(filename, ctx, options) 36 delete files[filename] 37 return ret 38 } 39 40 async load () { 41 if (files[this.#file]) { 42 return this.#loaded() 43 } 44 45 try { 46 files[this.#file] = await fs.readFile(this.#file, 'utf8') 47 } catch (er) { 48 if (er && this.#backupFile) { 49 this.#file = this.#backupFile 50 this.#backupFile = null 51 return this.load() 52 } 53 throw er 54 } 55 56 return this.#loaded() 57 } 58 59 async #loaded () { 60 const mod = new Module(this.#file, module) 61 mod.loaded = true 62 mod.filename = this.#file 63 mod.id = this.#file 64 mod.paths = Module._nodeModulePaths(dirname(this.#file)) 65 66 this.#ctx.prompt = this.#makePrompt() 67 this.#ctx.__filename = this.#file 68 this.#ctx.__dirname = dirname(this.#file) 69 this.#ctx.__basename = basename(this.#file) 70 this.#ctx.module = mod 71 this.#ctx.require = (p) => mod.require(p) 72 this.#ctx.require.resolve = (p) => Module._resolveFilename(p, mod) 73 this.#ctx.exports = mod.exports 74 75 const body = `(function(${Object.keys(this.#ctx).join(', ')}) { ${files[this.#file]}\n })` 76 runInThisContext(body, this.#file).apply(this.#ctx, Object.values(this.#ctx)) 77 this.#ctx.res = mod.exports 78 79 return this.#walk() 80 } 81 82 #makePrompt () { 83 return (...args) => { 84 let p, d, t 85 for (let i = 0; i < args.length; i++) { 86 const a = args[i] 87 if (typeof a === 'string') { 88 if (p) { 89 d = a 90 } else { 91 p = a 92 } 93 } else if (typeof a === 'function') { 94 t = a 95 } else if (a && typeof a === 'object') { 96 p = a.prompt || p 97 d = a.default || d 98 t = a.transform || t 99 } 100 } 101 try { 102 return `${this.#unique}-${this.#prompts.length}` 103 } finally { 104 this.#prompts.push([p, d, t]) 105 } 106 } 107 } 108 109 async #walk (o = this.#ctx.res) { 110 const keys = Object.keys(o) 111 112 const len = keys.length 113 let i = 0 114 115 while (i < len) { 116 const k = keys[i] 117 const v = o[k] 118 i++ 119 120 if (v && typeof v === 'object') { 121 o[k] = await this.#walk(v) 122 continue 123 } 124 125 if (v && typeof v === 'string' && v.startsWith(this.#unique)) { 126 const n = +v.slice(this.#unique.length + 1) 127 128 // default to the key 129 // default to the ctx value, if there is one 130 const [prompt = k, def = this.#ctx[k], tx] = this.#prompts[n] 131 132 try { 133 o[k] = await this.#prompt(prompt, def, tx) 134 } catch (er) { 135 if (er.notValid) { 136 console.log(er.message) 137 i-- 138 } else { 139 throw er 140 } 141 } 142 continue 143 } 144 145 if (typeof v === 'function') { 146 // XXX: remove v.length check to remove cb from functions 147 // would be a breaking change for `npm init` 148 // XXX: if cb is no longer an argument then this.#ctx should 149 // be passed in to allow arrow fns to be used and still access ctx 150 const fn = v.length ? promisify(v) : v 151 o[k] = await fn.call(this.#ctx) 152 // back up so that we process this one again. 153 // this is because it might return a prompt() call in the cb. 154 i-- 155 continue 156 } 157 } 158 159 return o 160 } 161 162 async #prompt (prompt, def, tx) { 163 const res = await read({ prompt: prompt + ':', default: def }).then((r) => tx ? tx(r) : r) 164 // XXX: remove this to require throwing an error instead of 165 // returning it. would be a breaking change for `npm init` 166 if (res instanceof Error && res.notValid) { 167 throw res 168 } 169 return res 170 } 171} 172 173module.exports = PromZard.promzard 174module.exports.fromBuffer = PromZard.fromBuffer 175module.exports.PromZard = PromZard 176