관리-도구
편집 파일: index.js
const fs = require('fs/promises') const { runInThisContext } = require('vm') const { promisify } = require('util') const { randomBytes } = require('crypto') const { Module } = require('module') const { dirname, basename } = require('path') const { read } = require('read') const files = {} class PromZard { #file = null #backupFile = null #ctx = null #unique = randomBytes(8).toString('hex') #prompts = [] constructor (file, ctx = {}, options = {}) { this.#file = file this.#ctx = ctx this.#backupFile = options.backupFile } static async promzard (file, ctx, options) { const pz = new PromZard(file, ctx, options) return pz.load() } static async fromBuffer (buf, ctx, options) { let filename = 0 do { filename = '\0' + Math.random() } while (files[filename]) files[filename] = buf const ret = await PromZard.promzard(filename, ctx, options) delete files[filename] return ret } async load () { if (files[this.#file]) { return this.#loaded() } try { files[this.#file] = await fs.readFile(this.#file, 'utf8') } catch (er) { if (er && this.#backupFile) { this.#file = this.#backupFile this.#backupFile = null return this.load() } throw er } return this.#loaded() } async #loaded () { const mod = new Module(this.#file, module) mod.loaded = true mod.filename = this.#file mod.id = this.#file mod.paths = Module._nodeModulePaths(dirname(this.#file)) this.#ctx.prompt = this.#makePrompt() this.#ctx.__filename = this.#file this.#ctx.__dirname = dirname(this.#file) this.#ctx.__basename = basename(this.#file) this.#ctx.module = mod this.#ctx.require = (p) => mod.require(p) this.#ctx.require.resolve = (p) => Module._resolveFilename(p, mod) this.#ctx.exports = mod.exports const body = `(function(${Object.keys(this.#ctx).join(', ')}) { ${files[this.#file]}\n })` runInThisContext(body, this.#file).apply(this.#ctx, Object.values(this.#ctx)) this.#ctx.res = mod.exports return this.#walk() } #makePrompt () { return (...args) => { let p, d, t for (let i = 0; i < args.length; i++) { const a = args[i] if (typeof a === 'string') { if (p) { d = a } else { p = a } } else if (typeof a === 'function') { t = a } else if (a && typeof a === 'object') { p = a.prompt || p d = a.default || d t = a.transform || t } } try { return `${this.#unique}-${this.#prompts.length}` } finally { this.#prompts.push([p, d, t]) } } } async #walk (o = this.#ctx.res) { const keys = Object.keys(o) const len = keys.length let i = 0 while (i < len) { const k = keys[i] const v = o[k] i++ if (v && typeof v === 'object') { o[k] = await this.#walk(v) continue } if (v && typeof v === 'string' && v.startsWith(this.#unique)) { const n = +v.slice(this.#unique.length + 1) // default to the key // default to the ctx value, if there is one const [prompt = k, def = this.#ctx[k], tx] = this.#prompts[n] try { o[k] = await this.#prompt(prompt, def, tx) } catch (er) { if (er.notValid) { // eslint-disable-next-line no-console console.log(er.message) i-- } else { throw er } } continue } if (typeof v === 'function') { // XXX: remove v.length check to remove cb from functions // would be a breaking change for `npm init` // XXX: if cb is no longer an argument then this.#ctx should // be passed in to allow arrow fns to be used and still access ctx const fn = v.length ? promisify(v) : v o[k] = await fn.call(this.#ctx) // back up so that we process this one again. // this is because it might return a prompt() call in the cb. i-- continue } } return o } async #prompt (prompt, def, tx) { const res = await read({ prompt: prompt + ':', default: def }).then((r) => tx ? tx(r) : r) // XXX: remove this to require throwing an error instead of // returning it. would be a breaking change for `npm init` if (res instanceof Error && res.notValid) { throw res } return res } } module.exports = PromZard.promzard module.exports.fromBuffer = PromZard.fromBuffer module.exports.PromZard = PromZard