codekingpro/portable-devtools
114k
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 // eslint-disable-next-line no-console
137 console.log(er.message)
138 i--
139 } else {
140 throw er
141 }
142 }
143 continue
144 }
145
146 if (typeof v === 'function') {
147 // XXX: remove v.length check to remove cb from functions
148 // would be a breaking change for `npm init`
149 // XXX: if cb is no longer an argument then this.#ctx should
150 // be passed in to allow arrow fns to be used and still access ctx
151 const fn = v.length ? promisify(v) : v
152 o[k] = await fn.call(this.#ctx)
153 // back up so that we process this one again.
154 // this is because it might return a prompt() call in the cb.
155 i--
156 continue
157 }
158 }
159
160 return o
161 }
162
163 async #prompt (prompt, def, tx) {
164 const res = await read({ prompt: prompt + ':', default: def }).then((r) => tx ? tx(r) : r)
165 // XXX: remove this to require throwing an error instead of
166 // returning it. would be a breaking change for `npm init`
167 if (res instanceof Error && res.notValid) {
168 throw res
169 }
170 return res
171 }
172}
173
174module.exports = PromZard.promzard
175module.exports.fromBuffer = PromZard.fromBuffer
176module.exports.PromZard = PromZard
177 