1 |
|
2 |
|
3 | "use strict";
|
4 |
|
5 | const most = require("most");
|
6 | const webpack = require("webpack");
|
7 | const io = require("socket.io-client");
|
8 | const inspectpack = require("inspectpack");
|
9 |
|
10 | const serializer = require("../utils/error-serialization");
|
11 |
|
12 | const DEFAULT_PORT = 9838;
|
13 | const DEFAULT_HOST = "127.0.0.1";
|
14 | const ONE_SECOND = 1000;
|
15 | const INSPECTPACK_PROBLEM_ACTIONS = ["duplicates", "versions"];
|
16 | const INSPECTPACK_PROBLEM_TYPE = "problems";
|
17 |
|
18 | function noop() {}
|
19 |
|
20 | function 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 |
|
35 | const camel = str => str.replace(/-([a-z])/, group => group[1].toUpperCase());
|
36 |
|
37 |
|
38 | function _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 |
|
47 | const webpackHook = _webpackHook.bind(null, "tap");
|
48 | const webpackAsyncHook = _webpackHook.bind(null, "tapAsync");
|
49 |
|
50 | class 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 |
|
76 | let reachedDone = false;
|
77 |
|
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 |
|
95 | console.log(err);
|
96 | });
|
97 | this.socket.on("disconnect", () => {
|
98 | if (!reachedDone) {
|
99 |
|
100 | console.log("Socket.io disconnected before completing build lifecycle.");
|
101 | }
|
102 | });
|
103 | }
|
104 |
|
105 | new webpack.ProgressPlugin((percent, msg) => {
|
106 |
|
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 |
|
190 |
|
191 |
|
192 |
|
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);
|
233 | this.cleanup();
|
234 | },
|
235 | complete: this.cleanup
|
236 | });
|
237 | }
|
238 | });
|
239 | }
|
240 |
|
241 | observeMetrics(statsObj) {
|
242 |
|
243 | const statsToObserve = statsObj.toJson({
|
244 | source: true
|
245 | });
|
246 |
|
247 |
|
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 |
|
259 | return false;
|
260 | })
|
261 | );
|
262 | }
|
263 |
|
264 |
|
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 |
|
310 | module.exports = DashboardPlugin;
|