1 | var ansi = require('ansi-escape-sequences')
|
2 | var scrollbox = require('ansi-scrollbox')
|
3 | var pretty = require('prettier-bytes')
|
4 | var gzipSize = require('gzip-size')
|
5 | var keypress = require('keypress')
|
6 | var differ = require('ansi-diff')
|
7 | var strip = require('strip-ansi')
|
8 | var nanoraf = require('nanoraf')
|
9 | var fatalError = require('./fatal-error')
|
10 |
|
11 | var StartDelimiter = '|'
|
12 | var EndDelimiter = '|'
|
13 | var Filled = '█'
|
14 | var Empty = '░'
|
15 | var NewlineMatcher = /\n/g
|
16 |
|
17 | var VIEW_MAIN = 0
|
18 | var VIEW_LOG = 1
|
19 |
|
20 | var files = [
|
21 | 'assets',
|
22 | 'documents',
|
23 | 'scripts',
|
24 | 'styles',
|
25 | 'manifest',
|
26 | 'service-worker'
|
27 | ]
|
28 |
|
29 | module.exports = createUi
|
30 |
|
31 | function createUi (compiler, state) {
|
32 | var diff = differ()
|
33 | alternateBuffer()
|
34 |
|
35 | Object.assign(state, {
|
36 | count: compiler.metadata.count,
|
37 | files: {},
|
38 | size: 0,
|
39 | currentView: VIEW_MAIN,
|
40 | log: scrollbox({
|
41 | width: process.stdout.columns,
|
42 | height: process.stdout.rows - 2
|
43 | })
|
44 | })
|
45 |
|
46 |
|
47 | state.log.scroll(-1)
|
48 |
|
49 | var render = nanoraf(onrender, raf)
|
50 |
|
51 | var views = [
|
52 | mainView,
|
53 | logView
|
54 | ]
|
55 |
|
56 | files.forEach(function (filename) {
|
57 | state.files[filename] = {
|
58 | name: filename,
|
59 | progress: 0,
|
60 | timestamp: ' ',
|
61 | size: 0,
|
62 | status: 'pending',
|
63 | done: false
|
64 | }
|
65 | })
|
66 |
|
67 | compiler.on('error', function (topic, sub, err) {
|
68 | if (err.pretty) state.error = err.pretty
|
69 | else state.error = `${topic}:${sub} ${err.message}\n${err.stack}`
|
70 | render()
|
71 | })
|
72 |
|
73 | compiler.on('ssr', render)
|
74 |
|
75 | compiler.on('progress', function (nodeName, progress) {
|
76 | state.error = null
|
77 | state.files[nodeName].progress = progress
|
78 | render()
|
79 | })
|
80 |
|
81 | compiler.on('change', function (nodeName, edgeName, nodeState) {
|
82 | var node = nodeState[nodeName][edgeName]
|
83 | var data = {
|
84 | name: nodeName,
|
85 | progress: 100,
|
86 | timestamp: time(),
|
87 | size: 0,
|
88 | status: 'done',
|
89 | done: true
|
90 | }
|
91 | state.files[nodeName] = data
|
92 |
|
93 |
|
94 |
|
95 | if (node.buffer.length) {
|
96 | gzipSize(node.buffer)
|
97 | .then(function (size) { data.size = size })
|
98 | .catch(function (size) { data.size = node.buffer.length })
|
99 | .then(render)
|
100 | }
|
101 | render()
|
102 | })
|
103 |
|
104 | compiler.on('sse-connect', render)
|
105 | compiler.on('sse-disconnect', render)
|
106 |
|
107 | compiler.ssr.console.on('data', function (chunk) {
|
108 | state.log.content += chunk.toString()
|
109 | render()
|
110 | })
|
111 |
|
112 | process.stdout.on('resize', onresize)
|
113 |
|
114 | if (process.stdin.isTTY) {
|
115 | keypress(process.stdin)
|
116 | process.stdin.setRawMode(true)
|
117 | process.stdin.resume()
|
118 | process.stdin.on('keypress', onkeypress)
|
119 | }
|
120 |
|
121 | return render
|
122 |
|
123 | function onrender () {
|
124 | var content = views[state.currentView](state)
|
125 | process.stdout.write(diff.update(content))
|
126 | }
|
127 |
|
128 | function onresize () {
|
129 | diff.resize({ width: process.stdout.columns, height: process.stdout.rows })
|
130 | state.log.resize({ width: process.stdout.columns, height: process.stdout.rows - 2 })
|
131 | clearScreen()
|
132 | render()
|
133 | }
|
134 |
|
135 | function clearScreen () {
|
136 | diff.update('')
|
137 |
|
138 |
|
139 | process.stdout.write(ansi.erase.display(2))
|
140 | }
|
141 |
|
142 | function onkeypress (ch, key) {
|
143 | if (key && key.ctrl && key.name === 'c') {
|
144 | process.exit()
|
145 | } else if (ch === '1') {
|
146 |
|
147 | state.currentView = VIEW_MAIN
|
148 | render()
|
149 | } else if (ch === '2') {
|
150 |
|
151 | state.currentView = VIEW_LOG
|
152 | render()
|
153 | } else if (ch === '3') {
|
154 |
|
155 | render()
|
156 | } else if (state.currentView === VIEW_LOG) {
|
157 | state.log.keypress(ch, key)
|
158 | render()
|
159 | }
|
160 | }
|
161 | }
|
162 |
|
163 | function mainView (state) {
|
164 | if (state.error) {
|
165 | return '\x1b[33c' + state.error
|
166 | }
|
167 |
|
168 | var str = '\x1b[33c'
|
169 | str += header(state)
|
170 | str += '\n\n'
|
171 | str += files.reduce(function (str, filename) {
|
172 | var file = state.files[filename]
|
173 | if (!file) return ''
|
174 | var status = file.status
|
175 | var count = status === 'done' ? String(state.count[filename]) : ''
|
176 | if (status === 'done') status = clr(status, 'green')
|
177 |
|
178 |
|
179 | var name = count === '1'
|
180 | ? file.name.replace(/s$/, '')
|
181 | : file.name
|
182 |
|
183 | str += clr(padLeft(count, 3), 'yellow') + ' '
|
184 | str += padRight(clr(name, 'green'), 14)
|
185 | var size = pretty(file.size).replace(' ', '')
|
186 | str += pad(7 - size.length) + clr(size, 'magenta') + ' '
|
187 | str += clr(file.timestamp, 'cyan') + ' '
|
188 | str += progress(file.progress, 10) + ' '
|
189 | str += status
|
190 |
|
191 | return str + '\n'
|
192 | }, '') + '\n'
|
193 |
|
194 | var ssrState = 'Pending'
|
195 |
|
196 | if (state.ssr) {
|
197 | ssrState = state.ssr.success
|
198 | ? 'Success'
|
199 | : `Skipped - ${state.ssr.error.message} ${state.ssr.error.stack.split('\n')[1].trim()}`
|
200 | }
|
201 | str += 'Server Side Rendering: ' + ssrState + '\n'
|
202 |
|
203 | var totalSize = Object.keys(state.files).reduce(function (num, filename) {
|
204 | var file = state.files[filename]
|
205 | return num + file.size
|
206 | }, 0)
|
207 | var prettySize = clr(pretty(totalSize).replace(' ', ''), 'magenta')
|
208 | str += footer(state, `Total size: ${prettySize}`)
|
209 |
|
210 |
|
211 | var padLines = Math.max(process.stdout.rows - str.match(NewlineMatcher).length - 1, 0)
|
212 | str += '\n'.repeat(padLines)
|
213 |
|
214 | return str
|
215 | }
|
216 |
|
217 | function logView (state) {
|
218 | return state.log.toString() + '\n' + footer(state)
|
219 | }
|
220 |
|
221 |
|
222 | function header (state) {
|
223 | var sseStatus = state.sse > 0
|
224 | ? clr('connected', 'green')
|
225 | : state.port
|
226 | ? 'ready'
|
227 | : clr('starting', 'yellow')
|
228 |
|
229 | var httpStatus = state.port
|
230 | ? clr(clr('https://localhost:' + state.port, 'underline'), 'blue')
|
231 | : clr('starting', 'yellow')
|
232 |
|
233 | var left = `HTTP: ${httpStatus}`
|
234 | var right = `Live Reload: ${sseStatus}`
|
235 | return spaceBetween(left, right)
|
236 | }
|
237 |
|
238 |
|
239 | function footer (state, bottomRight) {
|
240 | var bottomLeft = tabBar(2, state.currentView)
|
241 |
|
242 | return bottomRight ? spaceBetween(bottomLeft, bottomRight) : bottomLeft
|
243 | }
|
244 |
|
245 | function tabBar (count, curr) {
|
246 | var str = ''
|
247 | var tmp
|
248 | for (var i = 0; i < count; i++) {
|
249 | tmp = String(i + 1)
|
250 | if (curr === i) {
|
251 | tmp = `[ ${tmp} ]`
|
252 | } else {
|
253 | tmp = clr(tmp, 'gray')
|
254 | if (i !== 0) tmp = ' ' + tmp
|
255 | if (i !== count) tmp = tmp + ' '
|
256 | }
|
257 | str += tmp
|
258 | }
|
259 | return str
|
260 | }
|
261 |
|
262 | function clr (text, color) {
|
263 | return process.stdout.isTTY ? ansi.format(text, color) : text
|
264 | }
|
265 |
|
266 | function padLeft (str, num, char) {
|
267 | str = String(str)
|
268 | var len = strip(str).length
|
269 | return pad(num - len, char) + str
|
270 | }
|
271 |
|
272 | function padRight (str, num, char) {
|
273 | str = String(str)
|
274 | var len = strip(str).length
|
275 | return str + pad(num - len, char)
|
276 | }
|
277 |
|
278 | function pad (len, char) {
|
279 | char = String(char === undefined ? ' ' : char)
|
280 | var res = ''
|
281 | while (res.length < len) res += char
|
282 | return res
|
283 | }
|
284 |
|
285 | function progress (curr, max) {
|
286 | var filledLength = Math.floor((curr / 100) * max)
|
287 | var emptyLength = max - filledLength
|
288 | var i = 1 + filledLength
|
289 | var j = i + emptyLength
|
290 |
|
291 | var str = StartDelimiter
|
292 | while (str.length < i) str += Filled
|
293 | while (str.length < j) str += Empty
|
294 | str += EndDelimiter
|
295 | return str
|
296 | }
|
297 |
|
298 | function raf (cb) {
|
299 | setTimeout(cb, 50)
|
300 | }
|
301 |
|
302 | function spaceBetween (left, right) {
|
303 | var len = process.stdout.columns - strip(left).length - strip(right).length
|
304 | var space = ''
|
305 | for (var i = 0; i < len; i++) {
|
306 | space += ' '
|
307 | }
|
308 | return left + space + right
|
309 | }
|
310 |
|
311 | function time () {
|
312 | var date = new Date()
|
313 | var hours = numPad(date.getHours())
|
314 | var minutes = numPad(date.getMinutes())
|
315 | var seconds = numPad(date.getSeconds())
|
316 | return `${hours}:${minutes}:${seconds}`
|
317 | }
|
318 |
|
319 | function numPad (num) {
|
320 | if (num < 10) num = '0' + num
|
321 | return num
|
322 | }
|
323 |
|
324 | function alternateBuffer () {
|
325 | var q = Buffer.from('q')
|
326 | var esc = Buffer.from([0x1B])
|
327 |
|
328 | process.stdout.write('\x1b[?1049h')
|
329 | process.stdout.write('\x1b[H')
|
330 | process.stdout.write('\x1b[?25l')
|
331 |
|
332 | process.on('unhandledRejection', onexit)
|
333 | process.on('uncaughtException', onexit)
|
334 | process.on('SIGTERM', onexit)
|
335 | process.on('SIGINT', onexit)
|
336 | process.on('exit', onexit)
|
337 | process.stdin.on('data', handleKey)
|
338 |
|
339 | function handleKey (buf) {
|
340 | if (buf.compare(q) === 0 || buf.compare(esc) === 0) {
|
341 | onexit()
|
342 | }
|
343 | }
|
344 |
|
345 | function onexit (statusCode) {
|
346 | process.stdout.write('\x1b[?1049l')
|
347 | process.stdout.write('\x1b[?25h')
|
348 |
|
349 | if (statusCode instanceof Error) {
|
350 | console.error(fatalError(statusCode))
|
351 | statusCode = 1
|
352 | }
|
353 |
|
354 | process.exit(statusCode)
|
355 | }
|
356 | }
|