1 | const {Watch} = require('./watch.js')
|
2 | const repl = require('repl')
|
3 | const rimraf = require('rimraf').sync
|
4 | const {stringify} = require('tap-yaml')
|
5 | const path = require('path')
|
6 | const fs = require('fs')
|
7 | const mkdirp = require('mkdirp').sync
|
8 |
|
9 | const noop = () => {}
|
10 |
|
11 |
|
12 | class Repl {
|
13 | constructor (options, input, output) {
|
14 | this.output = output || process.stdout
|
15 | this.input = input || 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: input => this.completer(input),
|
32 | writer: res => stringify(res),
|
33 | })
|
34 | this.repl.history = this.loadHistory()
|
35 |
|
36 |
|
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 |
|
188 | r [<filename>]
|
189 | run test suite, or the supplied filename
|
190 |
|
191 | u [<filename>]
|
192 | update snapshots in the suite, or in the supplied filename
|
193 |
|
194 | n
|
195 | run the suite with --changed
|
196 |
|
197 | p
|
198 | pause/resume the file watcher
|
199 |
|
200 | c [<report style>]
|
201 | run coverage report. Default to 'text' style.
|
202 |
|
203 | exit
|
204 | exit the repl
|
205 |
|
206 | clear
|
207 | delete all coverage info and re-run the test suite
|
208 |
|
209 | cls
|
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 |
|
243 | module.exports = {Repl}
|