Unverified Commit 91826ab7 authored by Renée Kooi's avatar Renée Kooi Committed by GitHub
Browse files

Console panel (#468)

<!--
Thanks for creating a Pull Request 😄 ! Before you submit, please read the following:
- Read our CONTRIBUTING.md file before submitting a patch.
- By making a contribution, you agree to our Developer Certificate of Origin.
-->

This is a 🙋 feature

<!-- Provide a general summary of the changes in the title above -->

This implements the 2nd panel for `bankai start`, showing console output from the server rendered app (don't know if there's anything else?).

SSR uses a separate Console instance, injected as a global `console` variable using [require-with-global](https://github.com/goto-bus-stop/require-with-global). That `console` writes to a stream which is saved by lib/ui.js. Pressing 2 in the `bankai start` TUI will switch to the console panel, which is an [ansi-scrollbox](https://github.com/goto-bus-stop/ansi-scrollbox) that renders the saved console output. ansi-scrollbox still needs more features and optimization (it's currently probably very slow for many thousands of lines) but it does sort of work.

## Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->
- [ ] tests pass
- [ ] tests and/or benchmarks are included
- [ ] documentation is changed or added

## Semver Changes
Minor
parent 671d6ade
......@@ -8,6 +8,7 @@ var pino = require('pino')
var localization = require('./localization')
var queue = require('./lib/queue')
var utils = require('./lib/utils')
var ServerRender = require('./ssr')
var assetsNode = require('./lib/graph-assets')
var documentNode = require('./lib/graph-document')
......@@ -51,6 +52,8 @@ function Bankai (entry, opts) {
this.queue = queue(methods) // The queue caches requests until ready.
this.graph = graph(key) // The graph manages relations between deps.
this.ssr = new ServerRender(entry)
// Detect when we're ready to allow requests to go through.
this.graph.on('change', function (nodeName, edgeName, state) {
self.emit('change', nodeName, edgeName, state)
......@@ -118,6 +121,7 @@ function Bankai (entry, opts) {
fullPaths: opts.fullPaths,
reload: Boolean(opts.reload),
log: this.log,
ssr: this.ssr,
watchers: {},
entry: entry,
opts: opts,
......@@ -228,6 +232,7 @@ Bankai.prototype.sourceMaps = function (stepName, edgeName, cb) {
Bankai.prototype.close = function () {
debug('closing all file watchers')
this.ssr.close()
this.graph.emit('close')
this.emit('close')
}
......@@ -12,7 +12,6 @@ var documentify = require('documentify')
var hyperstream = require('hstream')
var ttyError = require('./tty-error')
var ServerRender = require('../ssr')
var utils = require('./utils')
var WRITE_CONCURRENCY = 3
......@@ -21,9 +20,9 @@ module.exports = node
function node (state, createEdge) {
var entry = utils.basefile(state.metadata.entry)
var ssr = state.metadata.ssr
var self = this
var ssr = new ServerRender(entry)
if (ssr.error && ssr.error.isSsr) {
self.emit('ssr', { success: false, error: ssr.error })
} else if (ssr.error) {
......
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')
......@@ -13,6 +14,9 @@ var Filled = '█'
var Empty = ''
var NewlineMatcher = /\n/g
var VIEW_MAIN = 0
var VIEW_LOG = 1
var files = [
'assets',
'documents',
......@@ -31,11 +35,24 @@ function createUi (compiler, state) {
Object.assign(state, {
count: compiler.metadata.count,
files: {},
size: 0
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,
......@@ -87,6 +104,11 @@ function createUi (compiler, state) {
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) {
......@@ -99,32 +121,46 @@ function createUi (compiler, state) {
return render
function onrender () {
process.stdout.write(diff.update(view(state)))
var content = views[state.currentView](state)
process.stdout.write(diff.update(content))
}
function onresize () {
diff.resize({width: process.stdout.columns, height: process.stdout.rows})
diff.update('')
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') {
// TODO: Switch to the main view.
// Switch to the main view.
state.currentView = VIEW_MAIN
render()
} else if (ch === '2') {
// TODO: Switch to the log view.
// 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 view (state) {
function mainView (state) {
if (state.error) {
return '\x1b[33c' + state.error
}
......@@ -164,7 +200,12 @@ function view (state) {
}
str += 'Server Side Rendering: ' + ssrState + '\n'
str += footer(state)
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)
......@@ -173,6 +214,10 @@ function view (state) {
return str
}
function logView (state) {
return state.log.toString() + '\n' + footer(state)
}
// header
function header (state) {
var sseStatus = state.sse > 0
......@@ -191,16 +236,10 @@ function header (state) {
}
// footer
function footer (state) {
var size = Object.keys(state.files).reduce(function (num, filename) {
var file = state.files[filename]
return num + file.size
}, 0)
var bottomLeft = tabBar(1, 0)
function footer (state, bottomRight) {
var bottomLeft = tabBar(2, state.currentView)
var totalSize = clr(pretty(size).replace(' ', ''), 'magenta')
var bottomRight = `Total size: ${totalSize}`
return spaceBetween(bottomLeft, bottomRight)
return bottomRight ? spaceBetween(bottomLeft, bottomRight) : bottomLeft
}
function tabBar (count, curr) {
......
var requireWithGlobal = require('require-with-global')
var debug = require('debug')('bankai.server-render')
var Console = require('console').Console
var through = require('through2')
var assert = require('assert')
var choo = require('./choo')
......@@ -7,14 +10,15 @@ module.exports = class ServerRender {
constructor (entry) {
assert.equal(typeof entry, 'string', 'bankai/ssr/index.js: entry should be type string')
this.entry = entry
this.app = this._requireApp(this.entry)
this.appType = this._getAppType(this.app)
this.routes = this._listRoutes(this.app)
this.entry = entry
this.error = null
this.console = through()
this.consoleInstance = new Console(this.console)
this.require = requireWithGlobal()
this.reload()
this.DEFAULT_RESPONSE = {
body: '<body></body>',
title: '',
......@@ -23,6 +27,12 @@ module.exports = class ServerRender {
}
}
reload () {
this.app = this._requireApp(this.entry)
this.appType = this._getAppType(this.app)
this.routes = this._listRoutes(this.app)
}
render (route, done) {
var self = this
if (this.appType === 'choo') choo.render(this.app, route, send)
......@@ -34,6 +44,10 @@ module.exports = class ServerRender {
}
}
close () {
this.require.remove()
}
_getAppType (app) {
if (choo.is(app)) return 'choo'
else return 'default'
......@@ -41,7 +55,7 @@ module.exports = class ServerRender {
_requireApp (entry) {
try {
return freshRequire(entry)
return this._freshRequire(entry, { console: this.consoleInstance })
} catch (err) {
var failedRequire = err.message === `Cannot find module '${entry}'`
if (!failedRequire) {
......@@ -52,37 +66,36 @@ module.exports = class ServerRender {
}
}
// Clear the cache, and require the file again.
_freshRequire (file, vars) {
clearRequireAndChildren(file)
return this.require(file, vars)
}
_listRoutes (app) {
if (this.appType === 'choo') return choo.listRoutes(this.app)
return ['/']
}
}
// Clear the cache, and require the file again.
function freshRequire (file) {
clearRequireAndChildren(file)
var exports = require(file)
return exports
function clearRequireAndChildren (key) {
if (!require.cache[key]) return
function isNotNativeModulePath (file) {
return /\.node$/.test(file.id) === false
}
require.cache[key].children
.filter(isNotNativeModulePath)
.filter(isNotInNodeModules)
.forEach(function (child) {
clearRequireAndChildren(child.id)
})
function isNotInNodeModules (file) {
return /node_modules/.test(file.id) === false
}
debug('clearing require cache for %s', key)
delete require.cache[key]
function clearRequireAndChildren (key) {
if (!require.cache[key]) return
require.cache[key].children
.filter(isNotNativeModulePath)
.filter(isNotInNodeModules)
.forEach(function (child) {
clearRequireAndChildren(child.id)
})
function isNotNativeModulePath (module) {
return /\.node$/.test(module.id) === false
}
debug('clearing require cache for %s', key)
delete require.cache[key]
function isNotInNodeModules (module) {
return /node_modules/.test(module.id) === false
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment