Ryan Roemer de3eb44bbb
Bugfix: Ensure status is updated correctly on compilation finishing. (#322)
- Ensure a finished compilation state (failed, error, success) isn't overwritten by the ProgressPlugin. Fixes #321 
- Add error status inferred from webpack stats.
2021-01-29 10:39:06 -08:00

311 lines
7.8 KiB
JavaScript

/* eslint-disable max-params, max-statements */
"use strict";
const most = require("most");
const webpack = require("webpack");
const io = require("socket.io-client");
const inspectpack = require("inspectpack");
const serializer = require("../utils/error-serialization");
const DEFAULT_PORT = 9838;
const DEFAULT_HOST = "127.0.0.1";
const ONE_SECOND = 1000;
const INSPECTPACK_PROBLEM_ACTIONS = ["duplicates", "versions"];
const INSPECTPACK_PROBLEM_TYPE = "problems";
function noop() {}
function getTimeMessage(timer) {
let time = Date.now() - timer;
if (time >= ONE_SECOND) {
time /= ONE_SECOND;
time = Math.round(time);
time += "s";
} else {
time += "ms";
}
return ` (${time})`;
}
// Naive camel-casing.
const camel = str => str.replace(/-([a-z])/, group => group[1].toUpperCase());
// Normalize webpack3 vs. 4 API differences.
function _webpackHook(hookType, compiler, event, callback) {
if (compiler.hooks) {
hookType = hookType || "tap";
compiler.hooks[camel(event)][hookType]("webpack-dashboard", callback);
} else {
compiler.plugin(event, callback);
}
}
const webpackHook = _webpackHook.bind(null, "tap");
const webpackAsyncHook = _webpackHook.bind(null, "tapAsync");
class DashboardPlugin {
constructor(options) {
if (typeof options === "function") {
this.handler = options;
} else {
options = options || {};
this.host = options.host || DEFAULT_HOST;
this.port = options.port || DEFAULT_PORT;
this.includeAssets = options.includeAssets || [];
this.handler = options.handler || null;
}
this.cleanup = this.cleanup.bind(this);
this.watching = false;
}
cleanup() {
if (!this.watching && this.socket) {
this.handler = null;
this.socket.close();
}
}
apply(compiler) {
let handler = this.handler;
// Reached compile "done" state.
let reachedDone = false;
// Compile has finished in "done", "error", "failed" states.
let finished = false;
let timer;
if (!handler) {
handler = noop;
const port = this.port;
const host = this.host;
this.socket = io(`http://${host}:${port}`);
this.socket.on("connect", () => {
handler = this.socket.emit.bind(this.socket, "message");
});
this.socket.once("options", args => {
this.minimal = args.minimal;
this.includeAssets = this.includeAssets.concat(args.includeAssets || []);
});
this.socket.on("error", err => {
// eslint-disable-next-line no-console
console.log(err);
});
this.socket.on("disconnect", () => {
if (!reachedDone) {
// eslint-disable-next-line no-console
console.log("Socket.io disconnected before completing build lifecycle.");
}
});
}
new webpack.ProgressPlugin((percent, msg) => {
// Skip reporting once finished.
if (finished) {
return;
}
handler([
{
type: "status",
value: "Compiling"
},
{
type: "progress",
value: percent
},
{
type: "operations",
value: msg + getTimeMessage(timer)
}
]);
}).apply(compiler);
webpackAsyncHook(compiler, "watch-run", (c, done) => {
this.watching = true;
done();
});
webpackAsyncHook(compiler, "run", (c, done) => {
this.watching = false;
done();
});
webpackHook(compiler, "compile", () => {
timer = Date.now();
finished = false;
handler([
{
type: "status",
value: "Compiling"
}
]);
});
webpackHook(compiler, "invalid", () => {
finished = true;
handler([
{
type: "status",
value: "Invalidated"
},
{
type: "progress",
value: 0
},
{
type: "operations",
value: "idle"
},
{
type: "clear"
}
]);
});
webpackHook(compiler, "failed", () => {
finished = true;
handler([
{
type: "status",
value: "Failed"
},
{
type: "operations",
value: `idle${getTimeMessage(timer)}`
}
]);
});
webpackHook(compiler, "done", stats => {
const { errors, options } = stats.compilation;
const statsOptions = (options.devServer && options.devServer.stats) ||
options.stats || { colors: true };
const status = errors.length ? "Error" : "Success";
// We only need errors/warnings for stats information for finishing up.
// This allows us to avoid sending a full stats object to the CLI which
// can cause socket.io client disconnects for large objects.
// See: https://github.com/FormidableLabs/webpack-dashboard/issues/279
const statsJsonOptions = {
all: false,
errors: true,
warnings: true
};
reachedDone = true;
finished = true;
handler([
{
type: "status",
value: status
},
{
type: "progress",
value: 1
},
{
type: "operations",
value: `idle${getTimeMessage(timer)}`
},
{
type: "stats",
value: {
errors: stats.hasErrors(),
warnings: stats.hasWarnings(),
data: stats.toJson(statsJsonOptions)
}
},
{
type: "log",
value: stats.toString(statsOptions)
}
]);
if (!this.minimal) {
this.observeMetrics(stats).subscribe({
next: message => handler([message]),
error: err => {
console.log("Error from inspectpack:", err); // eslint-disable-line no-console
this.cleanup();
},
complete: this.cleanup
});
}
});
}
observeMetrics(statsObj) {
// Get the **full** stats object here for `inspectpack` analysis.
const statsToObserve = statsObj.toJson({
source: true // Needed for webpack5+
});
// Truncate off non-included assets.
const { includeAssets } = this;
if (includeAssets.length) {
statsToObserve.assets = statsToObserve.assets.filter(({ name }) =>
includeAssets.some(pattern => {
if (typeof pattern === "string") {
return name.startsWith(pattern);
} else if (pattern instanceof RegExp) {
return pattern.test(name);
}
// Pass through bad options..
return false;
})
);
}
// Late destructure so that we can stub.
const { actions } = inspectpack;
const { serializeError } = serializer;
const getSizes = stats =>
actions("sizes", { stats })
.then(instance => instance.getData())
.then(data => ({
type: "sizes",
value: data
}))
.catch(err => ({
type: "sizes",
error: true,
value: serializeError(err)
}));
const getProblems = stats =>
Promise.all(
INSPECTPACK_PROBLEM_ACTIONS.map(action =>
actions(action, { stats }).then(instance => instance.getData())
)
)
.then(datas => ({
type: INSPECTPACK_PROBLEM_TYPE,
value: INSPECTPACK_PROBLEM_ACTIONS.reduce(
(memo, action, i) =>
Object.assign({}, memo, {
[action]: datas[i]
}),
{}
)
}))
.catch(err => ({
type: INSPECTPACK_PROBLEM_TYPE,
error: true,
value: serializeError(err)
}));
const sizesStream = most.of(statsToObserve).map(getSizes);
const problemsStream = most.of(statsToObserve).map(getProblems);
return most.mergeArray([sizesStream, problemsStream]).chain(most.fromPromise);
}
}
module.exports = DashboardPlugin;