UNPKG

11.9 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const Stats = require("./Stats");
9
10/** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
11/** @typedef {import("./Compilation")} Compilation */
12/** @typedef {import("./Compiler")} Compiler */
13
14/**
15 * @template T
16 * @callback Callback
17 * @param {Error=} err
18 * @param {T=} result
19 */
20
21class Watching {
22 /**
23 * @param {Compiler} compiler the compiler
24 * @param {WatchOptions} watchOptions options
25 * @param {Callback<Stats>} handler completion handler
26 */
27 constructor(compiler, watchOptions, handler) {
28 this.startTime = null;
29 this.invalid = false;
30 this.handler = handler;
31 /** @type {Callback<void>[]} */
32 this.callbacks = [];
33 /** @type {Callback<void>[] | undefined} */
34 this._closeCallbacks = undefined;
35 this.closed = false;
36 this.suspended = false;
37 this.blocked = false;
38 this._isBlocked = () => false;
39 this._onChange = () => {};
40 this._onInvalid = () => {};
41 if (typeof watchOptions === "number") {
42 this.watchOptions = {
43 aggregateTimeout: watchOptions
44 };
45 } else if (watchOptions && typeof watchOptions === "object") {
46 this.watchOptions = { ...watchOptions };
47 } else {
48 this.watchOptions = {};
49 }
50 if (typeof this.watchOptions.aggregateTimeout !== "number") {
51 this.watchOptions.aggregateTimeout = 200;
52 }
53 this.compiler = compiler;
54 this.running = false;
55 this._initial = true;
56 this._invalidReported = true;
57 this._needRecords = true;
58 this.watcher = undefined;
59 this.pausedWatcher = undefined;
60 /** @type {Set<string>} */
61 this._collectedChangedFiles = undefined;
62 /** @type {Set<string>} */
63 this._collectedRemovedFiles = undefined;
64 this._done = this._done.bind(this);
65 process.nextTick(() => {
66 if (this._initial) this._invalidate();
67 });
68 }
69
70 _mergeWithCollected(changedFiles, removedFiles) {
71 if (!changedFiles) return;
72 if (!this._collectedChangedFiles) {
73 this._collectedChangedFiles = new Set(changedFiles);
74 this._collectedRemovedFiles = new Set(removedFiles);
75 } else {
76 for (const file of changedFiles) {
77 this._collectedChangedFiles.add(file);
78 this._collectedRemovedFiles.delete(file);
79 }
80 for (const file of removedFiles) {
81 this._collectedChangedFiles.delete(file);
82 this._collectedRemovedFiles.add(file);
83 }
84 }
85 }
86
87 _go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) {
88 this._initial = false;
89 if (this.startTime === null) this.startTime = Date.now();
90 this.running = true;
91 if (this.watcher) {
92 this.pausedWatcher = this.watcher;
93 this.lastWatcherStartTime = Date.now();
94 this.watcher.pause();
95 this.watcher = null;
96 } else if (!this.lastWatcherStartTime) {
97 this.lastWatcherStartTime = Date.now();
98 }
99 this.compiler.fsStartTime = Date.now();
100 this._mergeWithCollected(
101 changedFiles ||
102 (this.pausedWatcher &&
103 this.pausedWatcher.getAggregatedChanges &&
104 this.pausedWatcher.getAggregatedChanges()),
105 (this.compiler.removedFiles =
106 removedFiles ||
107 (this.pausedWatcher &&
108 this.pausedWatcher.getAggregatedRemovals &&
109 this.pausedWatcher.getAggregatedRemovals()))
110 );
111
112 this.compiler.modifiedFiles = this._collectedChangedFiles;
113 this._collectedChangedFiles = undefined;
114 this.compiler.removedFiles = this._collectedRemovedFiles;
115 this._collectedRemovedFiles = undefined;
116
117 this.compiler.fileTimestamps =
118 fileTimeInfoEntries ||
119 (this.pausedWatcher && this.pausedWatcher.getFileTimeInfoEntries());
120 this.compiler.contextTimestamps =
121 contextTimeInfoEntries ||
122 (this.pausedWatcher && this.pausedWatcher.getContextTimeInfoEntries());
123
124 const run = () => {
125 if (this.compiler.idle) {
126 return this.compiler.cache.endIdle(err => {
127 if (err) return this._done(err);
128 this.compiler.idle = false;
129 run();
130 });
131 }
132 if (this._needRecords) {
133 return this.compiler.readRecords(err => {
134 if (err) return this._done(err);
135
136 this._needRecords = false;
137 run();
138 });
139 }
140 this.invalid = false;
141 this._invalidReported = false;
142 this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
143 if (err) return this._done(err);
144 const onCompiled = (err, compilation) => {
145 if (err) return this._done(err, compilation);
146 if (this.invalid) return this._done(null, compilation);
147
148 if (this.compiler.hooks.shouldEmit.call(compilation) === false) {
149 return this._done(null, compilation);
150 }
151
152 process.nextTick(() => {
153 const logger = compilation.getLogger("webpack.Compiler");
154 logger.time("emitAssets");
155 this.compiler.emitAssets(compilation, err => {
156 logger.timeEnd("emitAssets");
157 if (err) return this._done(err, compilation);
158 if (this.invalid) return this._done(null, compilation);
159
160 logger.time("emitRecords");
161 this.compiler.emitRecords(err => {
162 logger.timeEnd("emitRecords");
163 if (err) return this._done(err, compilation);
164
165 if (compilation.hooks.needAdditionalPass.call()) {
166 compilation.needAdditionalPass = true;
167
168 compilation.startTime = this.startTime;
169 compilation.endTime = Date.now();
170 logger.time("done hook");
171 const stats = new Stats(compilation);
172 this.compiler.hooks.done.callAsync(stats, err => {
173 logger.timeEnd("done hook");
174 if (err) return this._done(err, compilation);
175
176 this.compiler.hooks.additionalPass.callAsync(err => {
177 if (err) return this._done(err, compilation);
178 this.compiler.compile(onCompiled);
179 });
180 });
181 return;
182 }
183 return this._done(null, compilation);
184 });
185 });
186 });
187 };
188 this.compiler.compile(onCompiled);
189 });
190 };
191
192 run();
193 }
194
195 /**
196 * @param {Compilation} compilation the compilation
197 * @returns {Stats} the compilation stats
198 */
199 _getStats(compilation) {
200 const stats = new Stats(compilation);
201 return stats;
202 }
203
204 /**
205 * @param {Error=} err an optional error
206 * @param {Compilation=} compilation the compilation
207 * @returns {void}
208 */
209 _done(err, compilation) {
210 this.running = false;
211
212 const logger = compilation && compilation.getLogger("webpack.Watching");
213
214 let stats = null;
215
216 const handleError = (err, cbs) => {
217 this.compiler.hooks.failed.call(err);
218 this.compiler.cache.beginIdle();
219 this.compiler.idle = true;
220 this.handler(err, stats);
221 if (!cbs) {
222 cbs = this.callbacks;
223 this.callbacks = [];
224 }
225 for (const cb of cbs) cb(err);
226 };
227
228 if (
229 this.invalid &&
230 !this.suspended &&
231 !this.blocked &&
232 !(this._isBlocked() && (this.blocked = true))
233 ) {
234 if (compilation) {
235 logger.time("storeBuildDependencies");
236 this.compiler.cache.storeBuildDependencies(
237 compilation.buildDependencies,
238 err => {
239 logger.timeEnd("storeBuildDependencies");
240 if (err) return handleError(err);
241 this._go();
242 }
243 );
244 } else {
245 this._go();
246 }
247 return;
248 }
249
250 if (compilation) {
251 compilation.startTime = this.startTime;
252 compilation.endTime = Date.now();
253 stats = new Stats(compilation);
254 }
255 this.startTime = null;
256 if (err) return handleError(err);
257
258 const cbs = this.callbacks;
259 this.callbacks = [];
260 logger.time("done hook");
261 this.compiler.hooks.done.callAsync(stats, err => {
262 logger.timeEnd("done hook");
263 if (err) return handleError(err, cbs);
264 this.handler(null, stats);
265 logger.time("storeBuildDependencies");
266 this.compiler.cache.storeBuildDependencies(
267 compilation.buildDependencies,
268 err => {
269 logger.timeEnd("storeBuildDependencies");
270 if (err) return handleError(err, cbs);
271 logger.time("beginIdle");
272 this.compiler.cache.beginIdle();
273 this.compiler.idle = true;
274 logger.timeEnd("beginIdle");
275 process.nextTick(() => {
276 if (!this.closed) {
277 this.watch(
278 compilation.fileDependencies,
279 compilation.contextDependencies,
280 compilation.missingDependencies
281 );
282 }
283 });
284 for (const cb of cbs) cb(null);
285 this.compiler.hooks.afterDone.call(stats);
286 }
287 );
288 });
289 }
290
291 /**
292 * @param {Iterable<string>} files watched files
293 * @param {Iterable<string>} dirs watched directories
294 * @param {Iterable<string>} missing watched existence entries
295 * @returns {void}
296 */
297 watch(files, dirs, missing) {
298 this.pausedWatcher = null;
299 this.watcher = this.compiler.watchFileSystem.watch(
300 files,
301 dirs,
302 missing,
303 this.lastWatcherStartTime,
304 this.watchOptions,
305 (
306 err,
307 fileTimeInfoEntries,
308 contextTimeInfoEntries,
309 changedFiles,
310 removedFiles
311 ) => {
312 if (err) {
313 this.compiler.modifiedFiles = undefined;
314 this.compiler.removedFiles = undefined;
315 this.compiler.fileTimestamps = undefined;
316 this.compiler.contextTimestamps = undefined;
317 this.compiler.fsStartTime = undefined;
318 return this.handler(err);
319 }
320 this._invalidate(
321 fileTimeInfoEntries,
322 contextTimeInfoEntries,
323 changedFiles,
324 removedFiles
325 );
326 this._onChange();
327 },
328 (fileName, changeTime) => {
329 if (!this._invalidReported) {
330 this._invalidReported = true;
331 this.compiler.hooks.invalid.call(fileName, changeTime);
332 }
333 this._onInvalid();
334 }
335 );
336 }
337
338 /**
339 * @param {Callback<void>=} callback signals when the build has completed again
340 * @returns {void}
341 */
342 invalidate(callback) {
343 if (callback) {
344 this.callbacks.push(callback);
345 }
346 if (!this._invalidReported) {
347 this._invalidReported = true;
348 this.compiler.hooks.invalid.call(null, Date.now());
349 }
350 this._onChange();
351 this._invalidate();
352 }
353
354 _invalidate(
355 fileTimeInfoEntries,
356 contextTimeInfoEntries,
357 changedFiles,
358 removedFiles
359 ) {
360 if (this.suspended || (this._isBlocked() && (this.blocked = true))) {
361 this._mergeWithCollected(changedFiles, removedFiles);
362 return;
363 }
364
365 if (this.running) {
366 this._mergeWithCollected(changedFiles, removedFiles);
367 this.invalid = true;
368 } else {
369 this._go(
370 fileTimeInfoEntries,
371 contextTimeInfoEntries,
372 changedFiles,
373 removedFiles
374 );
375 }
376 }
377
378 suspend() {
379 this.suspended = true;
380 }
381
382 resume() {
383 if (this.suspended) {
384 this.suspended = false;
385 this._invalidate();
386 }
387 }
388
389 /**
390 * @param {Callback<void>} callback signals when the watcher is closed
391 * @returns {void}
392 */
393 close(callback) {
394 if (this._closeCallbacks) {
395 if (callback) {
396 this._closeCallbacks.push(callback);
397 }
398 return;
399 }
400 const finalCallback = (err, compilation) => {
401 this.running = false;
402 this.compiler.running = false;
403 this.compiler.watching = undefined;
404 this.compiler.watchMode = false;
405 this.compiler.modifiedFiles = undefined;
406 this.compiler.removedFiles = undefined;
407 this.compiler.fileTimestamps = undefined;
408 this.compiler.contextTimestamps = undefined;
409 this.compiler.fsStartTime = undefined;
410 const shutdown = () => {
411 this.compiler.cache.shutdown(err => {
412 this.compiler.hooks.watchClose.call();
413 const closeCallbacks = this._closeCallbacks;
414 this._closeCallbacks = undefined;
415 for (const cb of closeCallbacks) cb(err);
416 });
417 };
418 if (compilation) {
419 const logger = compilation.getLogger("webpack.Watching");
420 logger.time("storeBuildDependencies");
421 this.compiler.cache.storeBuildDependencies(
422 compilation.buildDependencies,
423 err => {
424 logger.timeEnd("storeBuildDependencies");
425 shutdown();
426 }
427 );
428 } else {
429 shutdown();
430 }
431 };
432
433 this.closed = true;
434 if (this.watcher) {
435 this.watcher.close();
436 this.watcher = null;
437 }
438 if (this.pausedWatcher) {
439 this.pausedWatcher.close();
440 this.pausedWatcher = null;
441 }
442 this._closeCallbacks = [];
443 if (callback) {
444 this._closeCallbacks.push(callback);
445 }
446 if (this.running) {
447 this.invalid = true;
448 this._done = finalCallback;
449 } else {
450 finalCallback();
451 }
452 }
453}
454
455module.exports = Watching;