UNPKG

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