New approach (#50)

* Separate processes

* Socket comm

* Fixing some things, adding minimal mode
This commit is contained in:
Ken Wheeler 2016-08-17 21:15:05 -04:00 committed by GitHub
parent 8832ea3755
commit 7b6e9a1904
6 changed files with 298 additions and 138 deletions

View File

@ -18,6 +18,8 @@ That's cool, but its mostly noisy and scrolly and not super helpful. This plugin
### Use
*Note that terminal mouse events are not currently supported by the default OSX Terminal.app. While the rest of the dashboard works correctly, if you wish to scroll through the logs and modules, you may want to use an alternative such as [iTerm2](https://www.iterm2.com/index.html)*
#### Turn off errors
You need to turn off all error logging by setting your webpack config `quiet` option to true. If you use webpack-hot-middleware, that is done by setting the `log` option to a no-op. You can do something sort of like this, depending upon your setup:
@ -33,7 +35,75 @@ app.use(require('webpack-hot-middleware')(compiler, {
}));
```
#### webpack-dev-middleware
#### package.json (recommended)
After the initial release, it was decided that running a separate process and communicating via sockets would be more efficient and solves a lot of problems with stdout.
The new way to run the dashboard is to add the plugin, and then call the provided binary.
First, import the plugin and add it to your webpack config, or apply it to your compiler:
```js
// Import the plugin:
var DashboardPlugin = require('webpack-dashboard/plugin');
// If you aren't using express, add it to your webpack configs plugins section:
plugins: [
new DashboardPlugin();
]
// If you are using an express based dev server, add it with compiler.apply
compiler.apply(new DashboardPlugin());
```
Note, in the new version you don't pass the handler function to the `DashboardPlugin` constructor. Because sockets use a port, the constructor now supports passing an options object that can include a custom port (if the default is giving you problems). See how below:
```js
plugins: [
new DashboardPlugin({ port: 3001 });
]
```
The next step, is to call webpack-dashboard from your `package.json`. So if your dev server start script previously looked like:
```js
"scripts": {
"dev": "node index.js"
}
```
You would change that to:
```js
"scripts": {
"dev": "webpack-dashboard -- node index.js"
}
```
If you are using the webpack-dev-server binary, you can do something like:
```js
"scripts": {
"dev": "webpack-dashboard -- webpack-dev-server --config ./webpack.dev.js"
}
```
Again, the new version uses sockets, so if you want to use a custom port you can use the `-p` option to pass that:
```js
"scripts": {
"dev": "webpack-dashboard -p 3001 -- node index.js"
}
```
You can also pass a supported ANSI color using the `-c` flag to custom colorize your dashboard:
```js
"scripts": {
"dev": "webpack-dashboard -c magenta -- node index.js"
}
```
Now you can just run your start script like normal, except now, you are awesome. Not that you weren't before. I'm just saying. More so.
#### webpack-dev-middleware (the old way)
First, import the dashboard and webpack plugin:
@ -52,7 +122,7 @@ var dashboard = new Dashboard();
compiler.apply(new DashboardPlugin(dashboard.setData));
```
#### webpack-dev-server
#### webpack-dev-server (the old way)
If you are running the dev server without an express server, you'll have to initialize the dashboard in your `webpack.config.js`.
@ -90,7 +160,26 @@ Finally, start your server using whatever command you have set up. Either you ha
Then, sit back and pretend you're an astronaut.
Note that terminal mouse events are not currently supported by the default OSX Terminal.app. While the rest of the dashboard works correctly, if you wish to scroll through the logs and modules, you may want to use an alternative such as [iTerm2](https://www.iterm2.com/index.html)
### API
#### webpack-dashboard (CLI)
##### Options
- `-c, --color [color]` - Custom ANSI color for your dashboard
- `-m, --minimal` - Runs the dashboard in minimal mode
- `-p, --port [port]` - Custom port for socket communication
##### Arguments
`[command]` - The command you want to run, i.e. `webpack-dashboard -- node index.js`
#### Webpack plugin
#### Options
- `port` - Custom port for socket communication
- `handler` - Plugin handler method, i.e. `dashboard.setData`
*Note: you can also just pass a function in as an argument, which then becomes the handler, i.e. `new DashboardPlugin(dashboard.setData)`*
#### Credits

67
bin/webpack-dashboard.js Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env node
"use strict";
var commander = require("commander");
var spawn = require("child_process").spawn;
var Dashboard = require("../dashboard/index.js");
var net = require("net");
var JsonSocket = require("json-socket");
var program = new commander.Command("webpack-dashboard");
var pkg = require("../package.json");
program.version(pkg.version);
program.option('-c, --color [color]', 'Dashboard color');
program.option('-m, --minimal', 'Minimal mode');
program.option('-p, --port [port]', 'Socket listener port');
program.usage("[options] -- [script] [arguments]");
program.parse(process.argv);
if (!program.args.length) {
program.outputHelp();
return;
}
var command = program.args[0];
var args = program.args.slice(1);
var child = spawn(command, args, {
stdio: [null, null, null, 'ipc']
});
var dashboard = new Dashboard({
color: program.color || "green",
minimal: program.minimal || false
});
var port = program.port || 9838;
var server = net.createServer();
server.listen(port);
server.on('connection', function(socket) {
socket = new JsonSocket(socket);
socket.on('message', function(message) {
dashboard.setData(message);
});
});
server.on('error', function(err) {
console.log(err);
});
child.stdout.on('data', function (data) {
dashboard.setData([{
type: 'log',
value: data.toString("utf8")
}]);
});
child.stderr.on('data', function (data) {
dashboard.setData([{
type: 'error',
value: data.toString("utf8")
}]);
});
process.on('exit', function () {
child.kill();
});

View File

@ -6,10 +6,10 @@ var blessed = require('blessed');
var formatOutput = require('../utils/format-output.js');
var formatModules = require('../utils/format-modules.js');
var formatAssets = require('../utils/format-assets.js');
var mockConsole = require('../utils/mock-console.js');
function Dashboard(options) {
this.color = options && options.color || "green";
this.minimal = options && options.minimal || false;
this.setData = this.setData.bind(this);
this.screen = blessed.screen({
@ -22,18 +22,8 @@ function Dashboard(options) {
this.layoutLog.call(this);
this.layoutStatus.call(this);
this.layoutModules.call(this);
this.layoutAssets.call(this);
var self = this;
Object.defineProperty(global, 'console', {
value: mockConsole(function(value) {
self.setData({
type: 'log',
value: value
});
})
});
!this.minimal && this.layoutModules.call(this);
!this.minimal && this.layoutAssets.call(this);
this.screen.key(['escape', 'q', 'C-c'], function() {
process.exit(0);
@ -42,74 +32,84 @@ function Dashboard(options) {
this.screen.render();
}
Dashboard.prototype.setData = function(data) {
switch (data.type) {
case 'progress': {
var percent = parseInt(data.value * 100);
this.progressbar.setProgress(percent);
this.screen.render();
break;
}
case 'operations': {
this.operations.setContent(data.value);
this.screen.render();
break;
}
case 'status': {
var content;
Dashboard.prototype.setData = function(dataArr) {
var self = this;
switch(data.value) {
case 'Success':
content = '{green-fg}{bold}' + data.value + '{/}';
break;
case 'Failed':
content = '{red-fg}{bold}' + data.value + '{/}';
break;
default:
content = '{white-fg}{bold}' + data.value + '{/}';
dataArr.forEach(function(data) {
switch (data.type) {
case 'progress': {
var percent = parseInt(data.value * 100);
if (self.minimal) {
percent && self.progress.setContent(percent.toString() + '%');
} else {
self.progressbar.setProgress(percent);
}
break;
}
this.status.setContent(content);
this.screen.render();
break;
}
case 'stats': {
var stats = data.value;
if (stats.hasErrors()) {
this.status.setContent('{red-fg}{bold}Failed{/}');
case 'operations': {
self.operations.setContent(data.value);
break;
}
this.logText.log(formatOutput(stats));
this.moduleTable.setData(formatModules(stats));
this.assetTable.setData(formatAssets(stats));
this.screen.render();
case 'status': {
var content;
break;
switch(data.value) {
case 'Success':
content = '{green-fg}{bold}' + data.value + '{/}';
break;
case 'Failed':
content = '{red-fg}{bold}' + data.value + '{/}';
break;
default:
content = '{white-fg}{bold}' + data.value + '{/}';
}
self.status.setContent(content);
break;
}
case 'stats': {
var stats = {
hasErrors: function() {
return data.value.errors;
},
hasWarnings: function() {
return data.value.warnings;
},
toJson: function() {
return data.value.data;
}
};
if (stats.hasErrors()) {
self.status.setContent('{red-fg}{bold}Failed{/}');
}
self.logText.log(formatOutput(stats));
!self.minimal && self.moduleTable.setData(formatModules(stats));
!self.minimal && self.assetTable.setData(formatAssets(stats));
break;
}
case 'log': {
self.logText.log(data.value);
break;
}
case 'error': {
self.logText.log("{red-fg}" + data.value + "{/}");
break;
}
case 'clear': {
self.logText.setContent('');
break;
}
}
case 'log': {
this.logText.log(data.value);
});
this.screen.render();
break;
}
case 'clear': {
this.logText.setContent('');
this.screen.render();
break;
}
default: {
break;
}
}
this.screen.render();
};
Dashboard.prototype.layoutLog = function() {
this.log = blessed.box({
label: 'Log',
padding: 1,
width: '75%',
height: '42%',
width: this.minimal ? "100%" : '75%',
height: this.minimal ? "70%" : '42%',
left: '0%',
top: '0%',
border: {
@ -128,6 +128,7 @@ Dashboard.prototype.layoutLog = function() {
tags: true,
width: "100%-5",
scrollable: true,
input: true,
alwaysScroll: true,
scrollbar: {
ch: ' ',
@ -167,6 +168,7 @@ Dashboard.prototype.layoutModules = function() {
width: "100%-5",
align: "left",
pad: 1,
shrink: true,
scrollable: true,
alwaysScroll: true,
scrollbar: {
@ -226,10 +228,10 @@ Dashboard.prototype.layoutAssets = function() {
Dashboard.prototype.layoutStatus = function() {
this.wrapper = blessed.layout({
width: "25%",
height: "42%",
top: "0%",
left: "75%",
width: this.minimal ? "100%" : "25%",
height: this.minimal ? "30%" : "42%",
top: this.minimal ? "70%" : "0%",
left: this.minimal ? "0%" : "75%",
layout: "grid"
});
@ -240,8 +242,8 @@ Dashboard.prototype.layoutStatus = function() {
padding: {
left: 1,
},
width: '100%',
height: '34%',
width: this.minimal ? "34%" : '100%',
height: this.minimal ? "100%" : '34%',
valign: "middle",
border: {
type: 'line',
@ -261,8 +263,8 @@ Dashboard.prototype.layoutStatus = function() {
padding: {
left: 1,
},
width: '100%',
height: '34%',
width: this.minimal ? "34%" : '100%',
height: this.minimal ? "100%" : '34%',
valign: "middle",
border: {
type: 'line',
@ -279,9 +281,11 @@ Dashboard.prototype.layoutStatus = function() {
parent: this.wrapper,
label: 'Progress',
tags: true,
padding: 1,
width: '100%',
height: '34%',
padding: this.minimal ? {
left: 1,
} : 1,
width: this.minimal ? "33%" : '100%',
height: this.minimal ? "100%" : '34%',
valign: "middle",
border: {
type: 'line',
@ -297,6 +301,10 @@ Dashboard.prototype.layoutStatus = function() {
this.progressbar = blessed.ProgressBar({
parent: this.progress,
height: 1,
width: "90%",
top: "center",
left: "center",
hidden: this.minimal,
orientation: "horizontal",
style: {
bar: {

View File

@ -2,6 +2,7 @@
"name": "webpack-dashboard",
"version": "0.0.1",
"description": "a CLI dashboard for webpack dev server",
"bin": "bin/webpack-dashboard.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@ -27,6 +28,9 @@
},
"dependencies": {
"blessed": "^0.1.81",
"filesize": "^3.3.0"
"commander": "^2.9.0",
"filesize": "^3.3.0",
"json-socket": "^0.1.2",
"webpack": "^1.13.1"
}
}

View File

@ -2,86 +2,98 @@
"use strict";
var webpack = require("webpack");
var net = require("net");
var JsonSocket = require("json-socket");
function noop() {}
function DashboardPlugin(options) {
if (typeof options === "function") {
options = {
handler: options
};
this.handler = options;
} else {
options = options || {};
this.port = options.port || 9838;
this.handler = options.handler || null;
}
options = options || {};
this.handler = options.handler;
}
DashboardPlugin.prototype.apply = function(compiler) {
var handler = this.handler || noop;
var handler = this.handler;
if (!handler) {
handler = noop;
var port = this.port;
var host = '127.0.0.1';
var socket = new JsonSocket(new net.Socket());
socket.connect(port, host);
socket.on('connect', function() {
handler = socket.sendMessage.bind(socket);
});
}
compiler.apply(new webpack.ProgressPlugin(function (percent, msg) {
handler.call(null, {
handler.call(null, [{
type: "status",
value: "Compiling"
}, {
type: "progress",
value: percent
});
handler.call(null, {
}, {
type: "operations",
value: msg
});
}]);
}));
compiler.plugin("compile", function() {
handler.call(null, {
handler.call(null, [{
type: "status",
value: "Compiling"
});
}]);
});
compiler.plugin("invalid", function() {
handler.call(null, {
handler.call(null, [{
type: "status",
value: "Invalidated"
});
handler.call(null, {
}, {
type: "progress",
value: 0
});
handler.call(null, {
}, {
type: "operations",
value: "idle"
});
handler.call(null, {
}, {
type: "clear"
});
}]);
});
compiler.plugin("done", function(stats) {
handler.call(null, {
handler.call(null, [{
type: "status",
value: "Success"
});
handler.call(null, {
type: "stats",
value: stats
});
handler.call(null, {
}, {
type: "progress",
value: 0
});
handler.call(null, {
}, {
type: "operations",
value: "idle"
});
}, {
type: "stats",
value: {
errors: stats.hasErrors(),
warnings: stats.hasWarnings(),
data: stats.toJson()
}
}]);
});
compiler.plugin("failed", function() {
handler.call(null, {
handler.call(null, [{
type: "status",
value: "Failed"
});
handler.call(null, {
}, {
type: "operations",
value: "idle"
});
}]);
});
}

View File

@ -1,20 +0,0 @@
"use strict";
function colorizeAndConcat(handler, color) {
var args = Array.prototype.slice.call(arguments, 2);
var value = "{" + color + "-fg}";
args.forEach(function(arg) {
value += arg + " ";
});
value += "{/}\n";
handler(value);
}
module.exports = function(handler) {
return {
log: colorizeAndConcat.bind(null, handler, "white"),
info: colorizeAndConcat.bind(null, handler, "white"),
error: colorizeAndConcat.bind(null, handler, "red"),
warn: colorizeAndConcat.bind(null, handler, "yellow")
};
};