UNPKG

5.84 kBJavaScriptView Raw
1const {Watch} = require('./watch.js')
2const repl = require('repl')
3const rimraf = require('rimraf').sync
4const {stringify} = require('tap-yaml')
5const path = require('path')
6const fs = require('fs')
7const mkdirp = require('mkdirp').sync
8/* istanbul ignore next */
9const noop = () => {}
10
11// XXX useGlobal = true, because it doesn't matter, so save the cycles
12class Repl {
13 constructor (options, input, output) {
14 this.output = output || /* istanbul ignore next */ process.stdout
15 this.input = input || /* istanbul ignore next */ process.stdin
16
17 this.repl = null
18 this._cb = null
19 this.watch = new Watch(options)
20 this.watch.on('afterProcess', (...args) => this.afterProcess(...args))
21 this.watch.on('main', () => this.start(options, input, output))
22 }
23
24 start (options, input, output) {
25 this.repl = repl.start({
26 useColors: options.color,
27 input,
28 output,
29 prompt: 'TAP> ',
30 eval: (...args) => this.parseCommand(...args),
31 completer: /* istanbul ignore next */ input => this.completer(input),
32 writer: res => stringify(res),
33 })
34 this.repl.history = this.loadHistory()
35 // doens't really make sense to have all default Node.js repl commands
36 // since we're not parsing JavaScript
37 this.repl.commands = {}
38 this.repl.removeAllListeners('SIGINT')
39 this.repl.on('SIGINT', () => {
40 if (this.watch.proc) {
41 this.watch.queue.length = 0
42 rimraf(this.watch.saveFile)
43 this.watch.kill('SIGTERM')
44 } else
45 this.parseCommand('exit', null, null, noop)
46 })
47 this.repl.on('close', () => {
48 this.saveHistory()
49 this.watch.pause()
50 })
51 }
52
53 loadHistory () {
54 const dir = process.env.HOME || 'node_modules/.cache/tap'
55
56 try {
57 return fs.readFileSync(dir + '/.tap_repl_history', 'utf8')
58 .trim().split('\n')
59 } catch (_) {
60 return []
61 }
62 }
63
64 saveHistory () {
65 const dir = process.env.HOME || 'node_modules/.cache/tap'
66
67 mkdirp(dir)
68 try {
69 fs.writeFileSync(dir + '/.tap_repl_history',
70 this.repl.history.join('\n').trim())
71 } catch (e) {}
72 }
73
74 get running () {
75 return !!this.watch.proc
76 }
77
78 parseCommand (input, _, __, cb) {
79 if (this.running)
80 return cb(null, 'test in progress, please wait')
81
82 input = input.trimLeft().split(' ')
83 const cmd = input.shift().trim()
84 const arg = input.join(' ').trim()
85
86 switch (cmd) {
87 case 'r':
88 return this.run(arg, cb)
89 case 'u':
90 return this.update(arg, cb)
91 case 'n':
92 return this.changed(cb)
93 case 'p':
94 return this.pauseResume(cb)
95 case 'c':
96 return this.coverageReport(arg, cb)
97 case 'exit':
98 return this.exit(cb)
99 case 'clear':
100 return this.clear(cb)
101 case 'cls':
102 this.repl.output.write('\u001b[2J\u001b[H')
103 return cb()
104 default:
105 return this.help(cb)
106 }
107 }
108
109 run (arg, cb) {
110 this.watch.queue.length = 0
111 rimraf(this.watch.saveFile)
112 if (arg) {
113 const tests = this.watch.positionals
114 if (tests.length && !tests.includes(arg)) {
115 tests.push(arg)
116 this.watch.args.push(arg)
117 }
118 this.watch.queue.push(arg)
119 }
120 this._cb = cb
121 this.watch.run()
122 }
123
124 afterProcess (res) {
125 if (this._cb) {
126 const cb = this._cb
127 this._cb = null
128 cb(null, res)
129 } else {
130 this.output.write(stringify(res))
131 this.repl.displayPrompt(true)
132 }
133 }
134
135 update (arg, cb) {
136 const envBefore = this.watch.env
137 this.watch.env = {
138 ...this.watch.env,
139 TAP_SNAPSHOT: '1'
140 }
141 this.run(arg, (er, res) => {
142 this.watch.env = envBefore
143 cb(er, res)
144 })
145 }
146
147 changed (cb) {
148 this.watch.args.push('--changed')
149 this.run(null, (er, res) => {
150 this.watch.args.pop()
151 cb(er, res)
152 })
153 }
154
155 pauseResume (cb) {
156 if (this.watch.watcher)
157 this.watch.pause()
158 else
159 this.watch.resume()
160 this.output.write(this.watch.watcher ? 'resumed\n' : 'paused\n')
161 cb()
162 }
163
164 coverageReport (arg, cb) {
165 const report = arg || 'text'
166 const args = this.watch.args
167 this.watch.args = [this.watch.args[0], '--coverage-report=' + report]
168 this.run(null, (er, res) => {
169 this.watch.args = args
170 cb(er, res)
171 })
172 }
173
174 clear (cb) {
175 rimraf('.nyc_output')
176 this.run(null, cb)
177 }
178
179 exit (cb) {
180 this.watch.pause()
181 this.watch.kill('SIGTERM')
182 this.repl.close()
183 }
184
185 help (cb) {
186 this.output.write(`TAP Repl Commands:
187
188r [<filename>]
189 run test suite, or the supplied filename
190
191u [<filename>]
192 update snapshots in the suite, or in the supplied filename
193
194n
195 run the suite with --changed
196
197p
198 pause/resume the file watcher
199
200c [<report style>]
201 run coverage report. Default to 'text' style.
202
203exit
204 exit the repl
205
206clear
207 delete all coverage info and re-run the test suite
208
209cls
210 clear the screen
211`)
212 cb()
213 }
214
215 filterCompletions (list, input) {
216 const hits = list.filter(l => l.startsWith(input))
217 return hits.length ? hits : list
218 }
219
220 completer (input) {
221 const cmdArg = input.trimLeft().split(' ')
222 const cmd = cmdArg.shift()
223 const arg = cmdArg.join(' ').trimLeft()
224 const commands = ['r', 'u', 'n', 'p', 'c', 'exit', 'clear', 'cls']
225 if (cmd === 'r' || cmd === 'u') {
226 const d = path.dirname(arg)
227 const dir = arg.slice(-1) === '/' ? arg : d === '.' ? '' : d + '/'
228 try {
229 const set = this.filterCompletions(
230 fs.readdirSync(dir || '.')
231 .map(f => fs.statSync(dir + f).isDirectory() ? f + '/' : f)
232 .map(f => cmd + ' ' + dir + f), input)
233 return [set, input]
234 } catch (er) {
235 return [[cmd], input]
236 }
237 } else {
238 return [this.filterCompletions(commands, input), input]
239 }
240 }
241}
242
243module.exports = {Repl}