UNPKG

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