UNPKG

bankai

Version:

The easiest way to compile JavaScript, HTML and CSS

357 lines (298 loc) 8.74 kB
var ansi = require('ansi-escape-sequences') var scrollbox = require('ansi-scrollbox') var pretty = require('prettier-bytes') var gzipSize = require('gzip-size') var keypress = require('keypress') var differ = require('ansi-diff') var strip = require('strip-ansi') var nanoraf = require('nanoraf') var fatalError = require('./fatal-error') var StartDelimiter = '|' var EndDelimiter = '|' var Filled = '█' var Empty = '░' var NewlineMatcher = /\n/g var VIEW_MAIN = 0 var VIEW_LOG = 1 var files = [ 'assets', 'documents', 'scripts', 'styles', 'manifest', 'service-worker' ] module.exports = createUi function createUi (compiler, state) { var diff = differ() alternateBuffer() Object.assign(state, { count: compiler.metadata.count, files: {}, size: 0, currentView: VIEW_MAIN, log: scrollbox({ width: process.stdout.columns, height: process.stdout.rows - 2 }) }) // tail by default state.log.scroll(-1) var render = nanoraf(onrender, raf) var views = [ mainView, logView ] files.forEach(function (filename) { state.files[filename] = { name: filename, progress: 0, timestamp: ' ', size: 0, status: 'pending', done: false } }) compiler.on('error', function (topic, sub, err) { if (err.pretty) state.error = err.pretty else state.error = `${topic}:${sub} ${err.message}\n${err.stack}` render() }) compiler.on('ssr', render) compiler.on('progress', function (nodeName, progress) { state.error = null state.files[nodeName].progress = progress render() }) compiler.on('change', function (nodeName, edgeName, nodeState) { var node = nodeState[nodeName][edgeName] var data = { name: nodeName, progress: 100, timestamp: time(), size: 0, status: 'done', done: true } state.files[nodeName] = data // Only calculate the gzip size if there's a buffer. Apparently zipping // an empty file means it'll pop out with a 20B base size. if (node.buffer.length) { gzipSize(node.buffer) .then(function (size) { data.size = size }) .catch(function (size) { data.size = node.buffer.length }) .then(render) } render() }) compiler.on('sse-connect', render) compiler.on('sse-disconnect', render) compiler.ssr.console.on('data', function (chunk) { state.log.content += chunk.toString() render() }) process.stdout.on('resize', onresize) if (process.stdin.isTTY) { keypress(process.stdin) process.stdin.setRawMode(true) process.stdin.resume() process.stdin.on('keypress', onkeypress) } return render function onrender () { var content = views[state.currentView](state) process.stdout.write(diff.update(content)) } function onresize () { diff.resize({ width: process.stdout.columns, height: process.stdout.rows }) state.log.resize({ width: process.stdout.columns, height: process.stdout.rows - 2 }) clearScreen() render() } function clearScreen () { diff.update('') // Ensure it's _completely_ cleared so that nothing lingers between views. // Some views (*cough* log *cough*) don't use ansi-diff so we can't just rely on that. process.stdout.write(ansi.erase.display(2)) } function onkeypress (ch, key) { if (key && key.ctrl && key.name === 'c') { process.exit() } else if (ch === '1') { // Switch to the main view. state.currentView = VIEW_MAIN render() } else if (ch === '2') { // Switch to the main view. state.currentView = VIEW_LOG render() } else if (ch === '3') { // TODO: Switch to the stats view. render() } else if (state.currentView === VIEW_LOG) { state.log.keypress(ch, key) render() } } } function mainView (state) { if (state.error) { return '\x1b[33c' + state.error } var str = '\x1b[33c' str += header(state) str += '\n\n' str += files.reduce(function (str, filename) { var file = state.files[filename] if (!file) return '' var status = file.status var count = status === 'done' ? String(state.count[filename]) : '' if (status === 'done') status = clr(status, 'green') // Make it so singular words aren't pluralized. var name = count === '1' ? file.name.replace(/s$/, '') : file.name str += clr(padLeft(count, 3), 'yellow') + ' ' str += padRight(clr(name, 'green'), 14) var size = pretty(file.size).replace(' ', '') str += pad(7 - size.length) + clr(size, 'magenta') + ' ' str += clr(file.timestamp, 'cyan') + ' ' str += progress(file.progress, 10) + ' ' str += status return str + '\n' }, '') + '\n' var ssrState = 'Pending' if (state.ssr) { ssrState = state.ssr.success ? 'Success' : `Skipped - ${state.ssr.error.message} ${state.ssr.error.stack.split('\n')[1].trim()}` } str += 'Server Side Rendering: ' + ssrState + '\n' var totalSize = Object.keys(state.files).reduce(function (num, filename) { var file = state.files[filename] return num + file.size }, 0) var prettySize = clr(pretty(totalSize).replace(' ', ''), 'magenta') str += footer(state, `Total size: ${prettySize}`) // pad string with newlines to ensure old rendered lines are cleared var padLines = Math.max(process.stdout.rows - str.match(NewlineMatcher).length - 1, 0) str += '\n'.repeat(padLines) return str } function logView (state) { return state.log.toString() + '\n' + footer(state) } // header function header (state) { var sseStatus = state.sse > 0 ? clr('connected', 'green') : state.port ? 'ready' : clr('starting', 'yellow') var httpStatus = state.port ? clr(clr('https://localhost:' + state.port, 'underline'), 'blue') : clr('starting', 'yellow') var left = `HTTP: ${httpStatus}` var right = `Live Reload: ${sseStatus}` return spaceBetween(left, right) } // footer function footer (state, bottomRight) { var bottomLeft = tabBar(2, state.currentView) return bottomRight ? spaceBetween(bottomLeft, bottomRight) : bottomLeft } function tabBar (count, curr) { var str = '' var tmp for (var i = 0; i < count; i++) { tmp = String(i + 1) if (curr === i) { tmp = `[ ${tmp} ]` } else { tmp = clr(tmp, 'gray') if (i !== 0) tmp = ' ' + tmp if (i !== count) tmp = tmp + ' ' } str += tmp } return str } function clr (text, color) { return process.stdout.isTTY ? ansi.format(text, color) : text } function padLeft (str, num, char) { str = String(str) var len = strip(str).length return pad(num - len, char) + str } function padRight (str, num, char) { str = String(str) var len = strip(str).length return str + pad(num - len, char) } function pad (len, char) { char = String(char === undefined ? ' ' : char) var res = '' while (res.length < len) res += char return res } function progress (curr, max) { var filledLength = Math.floor((curr / 100) * max) var emptyLength = max - filledLength var i = 1 + filledLength var j = i + emptyLength var str = StartDelimiter while (str.length < i) str += Filled while (str.length < j) str += Empty str += EndDelimiter return str } function raf (cb) { setTimeout(cb, 50) } function spaceBetween (left, right) { var len = process.stdout.columns - strip(left).length - strip(right).length var space = '' for (var i = 0; i < len; i++) { space += ' ' } return left + space + right } function time () { var date = new Date() var hours = numPad(date.getHours()) var minutes = numPad(date.getMinutes()) var seconds = numPad(date.getSeconds()) return `${hours}:${minutes}:${seconds}` } function numPad (num) { if (num < 10) num = '0' + num return num } function alternateBuffer () { var q = Buffer.from('q') var esc = Buffer.from([0x1B]) process.stdout.write('\x1b[?1049h') // Enter alternate buffer. process.stdout.write('\x1b[H') // Reset screen to top. process.stdout.write('\x1b[?25l') // Hide cursor process.on('unhandledRejection', onexit) process.on('uncaughtException', onexit) process.on('SIGTERM', onexit) process.on('SIGINT', onexit) process.on('exit', onexit) process.stdin.on('data', handleKey) function handleKey (buf) { if (buf.compare(q) === 0 || buf.compare(esc) === 0) { onexit() } } function onexit (statusCode) { process.stdout.write('\x1b[?1049l') // Enter to main buffer. process.stdout.write('\x1b[?25h') // Restore cursor if (statusCode instanceof Error) { console.error(fatalError(statusCode)) statusCode = 1 } process.exit(statusCode) } }