UNPKG

10.5 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 this._done = this._done.bind(this);
61 process.nextTick(() => {
62 if (this._initial) this._invalidate();
63 });
64 }
65
66 _go() {
67 this._initial = false;
68 this.startTime = Date.now();
69 this.running = true;
70 const run = () => {
71 if (this.compiler.idle) {
72 return this.compiler.cache.endIdle(err => {
73 if (err) return this._done(err);
74 this.compiler.idle = false;
75 run();
76 });
77 }
78 if (this._needRecords) {
79 return this.compiler.readRecords(err => {
80 if (err) return this._done(err);
81
82 this._needRecords = false;
83 run();
84 });
85 }
86 this.invalid = false;
87 this._invalidReported = false;
88 this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
89 if (err) return this._done(err);
90 const onCompiled = (err, compilation) => {
91 if (err) return this._done(err, compilation);
92 if (this.invalid) return this._done(null, compilation);
93
94 if (this.compiler.hooks.shouldEmit.call(compilation) === false) {
95 return this._done(null, compilation);
96 }
97
98 process.nextTick(() => {
99 const logger = compilation.getLogger("webpack.Compiler");
100 logger.time("emitAssets");
101 this.compiler.emitAssets(compilation, err => {
102 logger.timeEnd("emitAssets");
103 if (err) return this._done(err, compilation);
104 if (this.invalid) return this._done(null, compilation);
105
106 logger.time("emitRecords");
107 this.compiler.emitRecords(err => {
108 logger.timeEnd("emitRecords");
109 if (err) return this._done(err, compilation);
110
111 if (compilation.hooks.needAdditionalPass.call()) {
112 compilation.needAdditionalPass = true;
113
114 compilation.startTime = this.startTime;
115 compilation.endTime = Date.now();
116 logger.time("done hook");
117 const stats = new Stats(compilation);
118 this.compiler.hooks.done.callAsync(stats, err => {
119 logger.timeEnd("done hook");
120 if (err) return this._done(err, compilation);
121
122 this.compiler.hooks.additionalPass.callAsync(err => {
123 if (err) return this._done(err, compilation);
124 this.compiler.compile(onCompiled);
125 });
126 });
127 return;
128 }
129 return this._done(null, compilation);
130 });
131 });
132 });
133 };
134 this.compiler.compile(onCompiled);
135 });
136 };
137
138 run();
139 }
140
141 /**
142 * @param {Compilation} compilation the compilation
143 * @returns {Stats} the compilation stats
144 */
145 _getStats(compilation) {
146 const stats = new Stats(compilation);
147 return stats;
148 }
149
150 /**
151 * @param {Error=} err an optional error
152 * @param {Compilation=} compilation the compilation
153 * @returns {void}
154 */
155 _done(err, compilation) {
156 this.running = false;
157
158 const logger = compilation && compilation.getLogger("webpack.Watching");
159
160 let stats = null;
161
162 const handleError = err => {
163 this.compiler.hooks.failed.call(err);
164 this.compiler.cache.beginIdle();
165 this.compiler.idle = true;
166 this.handler(err, stats);
167 for (const cb of this.callbacks) cb();
168 this.callbacks.length = 0;
169 };
170
171 if (
172 this.invalid &&
173 !this.suspended &&
174 !this.blocked &&
175 !(this._isBlocked() && (this.blocked = true))
176 ) {
177 if (compilation) {
178 logger.time("storeBuildDependencies");
179 this.compiler.cache.storeBuildDependencies(
180 compilation.buildDependencies,
181 err => {
182 logger.timeEnd("storeBuildDependencies");
183 if (err) return handleError(err);
184 this._go();
185 }
186 );
187 } else {
188 this._go();
189 }
190 return;
191 }
192
193 if (compilation) {
194 compilation.startTime = this.startTime;
195 compilation.endTime = Date.now();
196 stats = new Stats(compilation);
197 }
198 if (err) return handleError(err);
199
200 logger.time("done hook");
201 this.compiler.hooks.done.callAsync(stats, err => {
202 logger.timeEnd("done hook");
203 if (err) return handleError(err);
204 this.handler(null, stats);
205 logger.time("storeBuildDependencies");
206 this.compiler.cache.storeBuildDependencies(
207 compilation.buildDependencies,
208 err => {
209 logger.timeEnd("storeBuildDependencies");
210 if (err) return handleError(err);
211 logger.time("beginIdle");
212 this.compiler.cache.beginIdle();
213 this.compiler.idle = true;
214 logger.timeEnd("beginIdle");
215 process.nextTick(() => {
216 if (!this.closed) {
217 this.watch(
218 compilation.fileDependencies,
219 compilation.contextDependencies,
220 compilation.missingDependencies
221 );
222 }
223 });
224 for (const cb of this.callbacks) cb();
225 this.callbacks.length = 0;
226 this.compiler.hooks.afterDone.call(stats);
227 }
228 );
229 });
230 }
231
232 /**
233 * @param {Iterable<string>} files watched files
234 * @param {Iterable<string>} dirs watched directories
235 * @param {Iterable<string>} missing watched existence entries
236 * @returns {void}
237 */
238 watch(files, dirs, missing) {
239 this.pausedWatcher = null;
240 this.watcher = this.compiler.watchFileSystem.watch(
241 files,
242 dirs,
243 missing,
244 this.startTime,
245 this.watchOptions,
246 (
247 err,
248 fileTimeInfoEntries,
249 contextTimeInfoEntries,
250 changedFiles,
251 removedFiles
252 ) => {
253 this.pausedWatcher = this.watcher;
254 this.watcher = null;
255 if (err) {
256 this.compiler.modifiedFiles = undefined;
257 this.compiler.removedFiles = undefined;
258 this.compiler.fileTimestamps = undefined;
259 this.compiler.contextTimestamps = undefined;
260 return this.handler(err);
261 }
262 this.compiler.fileTimestamps = fileTimeInfoEntries;
263 this.compiler.contextTimestamps = contextTimeInfoEntries;
264 this.compiler.removedFiles = removedFiles;
265 this.compiler.modifiedFiles = changedFiles;
266 if (this.watcher) {
267 this.pausedWatcher = this.watcher;
268 this.watcher.pause();
269 this.watcher = null;
270 }
271 this._invalidate();
272 this._onChange();
273 },
274 (fileName, changeTime) => {
275 if (!this._invalidReported) {
276 this._invalidReported = true;
277 this.compiler.hooks.invalid.call(fileName, changeTime);
278 }
279 this._onInvalid();
280 }
281 );
282 }
283
284 /**
285 * @param {Callback<void>=} callback signals when the build has completed again
286 * @returns {void}
287 */
288 invalidate(callback) {
289 if (callback) {
290 this.callbacks.push(callback);
291 }
292 if (!this._invalidReported) {
293 this._invalidReported = true;
294 this.compiler.hooks.invalid.call(null, Date.now());
295 }
296 this._invalidate();
297 }
298
299 _invalidate() {
300 if (this.suspended) return;
301 if (this._isBlocked()) {
302 this.blocked = true;
303 return;
304 }
305 if (this.watcher) {
306 this.compiler.modifiedFiles =
307 this.watcher.getAggregatedChanges &&
308 this.watcher.getAggregatedChanges();
309 this.compiler.removedFiles =
310 this.watcher.getAggregatedRemovals &&
311 this.watcher.getAggregatedRemovals();
312 this.compiler.fileTimestamps = this.watcher.getFileTimeInfoEntries();
313 this.compiler.contextTimestamps = this.watcher.getContextTimeInfoEntries();
314 this.pausedWatcher = this.watcher;
315 this.watcher.pause();
316 this.watcher = null;
317 }
318
319 if (this.running) {
320 this.invalid = true;
321 } else {
322 this._go();
323 }
324 }
325
326 suspend() {
327 this.suspended = true;
328 }
329
330 resume() {
331 if (this.suspended) {
332 this.suspended = false;
333 this._invalidate();
334 }
335 }
336
337 _checkUnblocked() {
338 if (this.blocked && !this._isBlocked()) {
339 this.blocked = false;
340 this._needWatcherInfo = true;
341 this._invalidate();
342 }
343 }
344
345 /**
346 * @param {Callback<void>} callback signals when the watcher is closed
347 * @returns {void}
348 */
349 close(callback) {
350 if (this._closeCallbacks) {
351 if (callback) {
352 this._closeCallbacks.push(callback);
353 }
354 return;
355 }
356 const finalCallback = (err, compilation) => {
357 this.running = false;
358 this.compiler.running = false;
359 this.compiler.watching = undefined;
360 this.compiler.watchMode = false;
361 this.compiler.modifiedFiles = undefined;
362 this.compiler.removedFiles = undefined;
363 this.compiler.fileTimestamps = undefined;
364 this.compiler.contextTimestamps = undefined;
365 const shutdown = () => {
366 this.compiler.cache.shutdown(err => {
367 this.compiler.hooks.watchClose.call();
368 const closeCallbacks = this._closeCallbacks;
369 this._closeCallbacks = undefined;
370 for (const cb of closeCallbacks) cb(err);
371 });
372 };
373 if (compilation) {
374 const logger = compilation.getLogger("webpack.Watching");
375 logger.time("storeBuildDependencies");
376 this.compiler.cache.storeBuildDependencies(
377 compilation.buildDependencies,
378 err => {
379 logger.timeEnd("storeBuildDependencies");
380 shutdown();
381 }
382 );
383 } else {
384 shutdown();
385 }
386 };
387
388 this.closed = true;
389 if (this.watcher) {
390 this.watcher.close();
391 this.watcher = null;
392 }
393 if (this.pausedWatcher) {
394 this.pausedWatcher.close();
395 this.pausedWatcher = null;
396 }
397 this._closeCallbacks = [];
398 if (callback) {
399 this._closeCallbacks.push(callback);
400 }
401 if (this.running) {
402 this.invalid = true;
403 this._done = finalCallback;
404 } else {
405 finalCallback();
406 }
407 }
408}
409
410module.exports = Watching;