UNPKG

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