UNPKG

7.97 kBJavaScriptView Raw
1/* eslint-disable max-params, max-statements */
2
3"use strict";
4
5const most = require("most");
6const webpack = require("webpack");
7const io = require("socket.io-client");
8const inspectpack = require("inspectpack");
9
10const serializer = require("../utils/error-serialization");
11
12const DEFAULT_PORT = 9838;
13const DEFAULT_HOST = "127.0.0.1";
14const ONE_SECOND = 1000;
15const INSPECTPACK_PROBLEM_ACTIONS = ["duplicates", "versions"];
16const INSPECTPACK_PROBLEM_TYPE = "problems";
17
18function noop() {}
19
20function getTimeMessage(timer) {
21 let time = Date.now() - timer;
22
23 if (time >= ONE_SECOND) {
24 time /= ONE_SECOND;
25 time = Math.round(time);
26 time += "s";
27 } else {
28 time += "ms";
29 }
30
31 return ` (${time})`;
32}
33
34// Naive camel-casing.
35const camel = str => str.replace(/-([a-z])/, group => group[1].toUpperCase());
36
37// Normalize webpack3 vs. 4 API differences.
38function _webpackHook(hookType, compiler, event, callback) {
39 if (compiler.hooks) {
40 hookType = hookType || "tap";
41 compiler.hooks[camel(event)][hookType]("webpack-dashboard", callback);
42 } else {
43 compiler.plugin(event, callback);
44 }
45}
46
47const webpackHook = _webpackHook.bind(null, "tap");
48const webpackAsyncHook = _webpackHook.bind(null, "tapAsync");
49
50class DashboardPlugin {
51 constructor(options) {
52 if (typeof options === "function") {
53 this.handler = options;
54 } else {
55 options = options || {};
56 this.host = options.host || DEFAULT_HOST;
57 this.port = options.port || DEFAULT_PORT;
58 this.includeAssets = options.includeAssets || [];
59 this.handler = options.handler || null;
60 }
61
62 this.cleanup = this.cleanup.bind(this);
63 this.watching = false;
64 }
65
66 cleanup() {
67 if (!this.watching && this.socket) {
68 this.handler = null;
69 this.socket.close();
70 }
71 }
72
73 apply(compiler) {
74 let handler = this.handler;
75 // Reached compile "done" state.
76 let reachedDone = false;
77 // Compile has finished in "done", "error", "failed" states.
78 let finished = false;
79 let timer;
80
81 if (!handler) {
82 handler = noop;
83 const port = this.port;
84 const host = this.host;
85 this.socket = io(`http://${host}:${port}`);
86 this.socket.on("connect", () => {
87 handler = this.socket.emit.bind(this.socket, "message");
88 });
89 this.socket.once("options", args => {
90 this.minimal = args.minimal;
91 this.includeAssets = this.includeAssets.concat(args.includeAssets || []);
92 });
93 this.socket.on("error", err => {
94 // eslint-disable-next-line no-console
95 console.log(err);
96 });
97 this.socket.on("disconnect", () => {
98 if (!reachedDone) {
99 // eslint-disable-next-line no-console
100 console.log("Socket.io disconnected before completing build lifecycle.");
101 }
102 });
103 }
104
105 new webpack.ProgressPlugin((percent, msg) => {
106 // Skip reporting once finished.
107 if (finished) {
108 return;
109 }
110
111 handler([
112 {
113 type: "status",
114 value: "Compiling"
115 },
116 {
117 type: "progress",
118 value: percent
119 },
120 {
121 type: "operations",
122 value: msg + getTimeMessage(timer)
123 }
124 ]);
125 }).apply(compiler);
126
127 webpackAsyncHook(compiler, "watch-run", (c, done) => {
128 this.watching = true;
129 done();
130 });
131
132 webpackAsyncHook(compiler, "run", (c, done) => {
133 this.watching = false;
134 done();
135 });
136
137 webpackHook(compiler, "compile", () => {
138 timer = Date.now();
139 finished = false;
140 handler([
141 {
142 type: "status",
143 value: "Compiling"
144 }
145 ]);
146 });
147
148 webpackHook(compiler, "invalid", () => {
149 finished = true;
150 handler([
151 {
152 type: "status",
153 value: "Invalidated"
154 },
155 {
156 type: "progress",
157 value: 0
158 },
159 {
160 type: "operations",
161 value: "idle"
162 },
163 {
164 type: "clear"
165 }
166 ]);
167 });
168
169 webpackHook(compiler, "failed", () => {
170 finished = true;
171 handler([
172 {
173 type: "status",
174 value: "Failed"
175 },
176 {
177 type: "operations",
178 value: `idle${getTimeMessage(timer)}`
179 }
180 ]);
181 });
182
183 webpackHook(compiler, "done", stats => {
184 const { errors, options } = stats.compilation;
185 const statsOptions = (options.devServer && options.devServer.stats) ||
186 options.stats || { colors: true };
187 const status = errors.length ? "Error" : "Success";
188
189 // We only need errors/warnings for stats information for finishing up.
190 // This allows us to avoid sending a full stats object to the CLI which
191 // can cause socket.io client disconnects for large objects.
192 // See: https://github.com/FormidableLabs/webpack-dashboard/issues/279
193 const statsJsonOptions = {
194 all: false,
195 errors: true,
196 warnings: true
197 };
198
199 reachedDone = true;
200 finished = true;
201 handler([
202 {
203 type: "status",
204 value: status
205 },
206 {
207 type: "progress",
208 value: 1
209 },
210 {
211 type: "operations",
212 value: `idle${getTimeMessage(timer)}`
213 },
214 {
215 type: "stats",
216 value: {
217 errors: stats.hasErrors(),
218 warnings: stats.hasWarnings(),
219 data: stats.toJson(statsJsonOptions)
220 }
221 },
222 {
223 type: "log",
224 value: stats.toString(statsOptions)
225 }
226 ]);
227
228 if (!this.minimal) {
229 this.observeMetrics(stats).subscribe({
230 next: message => handler([message]),
231 error: err => {
232 console.log("Error from inspectpack:", err); // eslint-disable-line no-console
233 this.cleanup();
234 },
235 complete: this.cleanup
236 });
237 }
238 });
239 }
240
241 observeMetrics(statsObj) {
242 // Get the **full** stats object here for `inspectpack` analysis.
243 const statsToObserve = statsObj.toJson({
244 source: true // Needed for webpack5+
245 });
246
247 // Truncate off non-included assets.
248 const { includeAssets } = this;
249 if (includeAssets.length) {
250 statsToObserve.assets = statsToObserve.assets.filter(({ name }) =>
251 includeAssets.some(pattern => {
252 if (typeof pattern === "string") {
253 return name.startsWith(pattern);
254 } else if (pattern instanceof RegExp) {
255 return pattern.test(name);
256 }
257
258 // Pass through bad options..
259 return false;
260 })
261 );
262 }
263
264 // Late destructure so that we can stub.
265 const { actions } = inspectpack;
266 const { serializeError } = serializer;
267
268 const getSizes = stats =>
269 actions("sizes", { stats })
270 .then(instance => instance.getData())
271 .then(data => ({
272 type: "sizes",
273 value: data
274 }))
275 .catch(err => ({
276 type: "sizes",
277 error: true,
278 value: serializeError(err)
279 }));
280
281 const getProblems = stats =>
282 Promise.all(
283 INSPECTPACK_PROBLEM_ACTIONS.map(action =>
284 actions(action, { stats }).then(instance => instance.getData())
285 )
286 )
287 .then(datas => ({
288 type: INSPECTPACK_PROBLEM_TYPE,
289 value: INSPECTPACK_PROBLEM_ACTIONS.reduce(
290 (memo, action, i) =>
291 Object.assign({}, memo, {
292 [action]: datas[i]
293 }),
294 {}
295 )
296 }))
297 .catch(err => ({
298 type: INSPECTPACK_PROBLEM_TYPE,
299 error: true,
300 value: serializeError(err)
301 }));
302
303 const sizesStream = most.of(statsToObserve).map(getSizes);
304 const problemsStream = most.of(statsToObserve).map(getProblems);
305
306 return most.mergeArray([sizesStream, problemsStream]).chain(most.fromPromise);
307 }
308}
309
310module.exports = DashboardPlugin;