Make output readable again: Controlling your IO streams in node.
It’s no secret that javascript builds have become complicated. The latest rewrite of a boilerplate I am writing requires commands from all of the following projects: Haul-cli, react-native-cli, lerna, jest, webpack, babel, and (due to symlink issues), rsync.
Some of the commands need to persist and watch files, some need to run once and stop, and re-run later. All the while, the developer experience is horrible. As developers, we all know that not everything works all the time, and being able to debug is critical. This applies to your build scripts, and other developers are your users. Build tools and build scripts, can become the single biggest source of pain for project maintainers, if they aren’t well setup.
Most node developers write their dev commands into the package.json scripts object. This is a great pattern because everything you need to know about developing, building, or starting a project is almost always in the same spot. Easy to find, easy to use. Dev commands are usually where most the complexity comes in. On a typical project, I typically impose at least these requirements on my dev script.
- Jest watch — Unit tests whenever a file changes.
- Project build and start (or webpack dev server)
A more recent lerna based project I have been working needs all of the following.
- Run react storybook
- Run react-native storybook
- Jest watch for web
- Jest watch for react-native
- Run webpack for react-native-web
- Run react-native packager for ios and android
- Rsync shared components to react-native-web and react-native-mobile because react-native-packager can’t handle symlinks (react native and react-native web can’t live in the same project because they both explicitly require different versions of react.)
In writing the docs for react-nativeish, it became clear that the developer experience for this boilerplate was miserable. Much of it had been written with `lerna run`, which sticks a label in front of each line of output, and outputs it all in one giant stream on your terminal. Which is great for 1, or maybe 2 commands. But was unwieldy with a more complicated setup. Something had to change.
Controlling your output
The goal is to cleanup our output, and make it readable. Which is where blessed, and shelljs come in.
Blessed is A high-level terminal interface library for node.js. see the examples for complete code
First we need to spawn our command (I use shelljs because it allows you to pass in a string as a command and not worry about a lot of details, like splitting args, into a separate array for nodes default spawn command).
// silent because by default shelljs copies output to your main
// terminal output. async because shelljs blocks by default.const {stdout, stderr} = shelljs.exec('ping www.google.com', {silent: true, async: true})
Using blessed, you can create a “Box” in your terminal, and define styles for it.
var blessed = require('blessed');
// Create a screen object.
var screen = blessed.screen({
smartCSR: true
});// Depending on your needs, blessed.box may be a better solution
// Blessed.text is a child of blessed.box, but is optimized for textvar container = blessed.text({
left: 0,
top: 0,
width: '25%',
height: '50%',
scrollable: true,
alwaysScroll: true,
label: 'upperLeft'
parent: screen,
mouse: true,
});
Now all we need to do is listen to our stdout stream, and add the content to our box.
// outputFromCommand is probably a buffer, so make sure you add it to a string before putting it in the terminal, or it will just output a number.stdout.on('data', outputFromCommand => {
// push a line into the text box
container.pushLine(data + '')
// scroll to the bottom
container.scroll(Number.MAX_VALUE)
// force a render, this might be good to debounce
screen.render()
}
Thats it, you have now taken the stdout and piped it into a box. Rinse and repeat for as many commands as you would like to run.
Enter Terminus-Maximus
While writing react-nativeish, there were just too many commands to keep messing with the dev script to make it look nice. From the ashes of that dev script rose the phoenix that is Terminus-Maximus. Terminus-Maximus is a library that allows you to create a config file for your scripts, and pipes the output into boxes, using the method described above. Along with some other bells and whistles (restart buttons, kill buttons, clear, click to fullscreen, etc…)
Hopefully it’s useful in creating easy to read, debuggable, and organized scripts. Let me know if you do!