You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			517 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			517 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | /* | ||
|  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | ||
|  | 	Author Tobias Koppers @sokra | ||
|  | */ | ||
|  | 
 | ||
|  | "use strict"; | ||
|  | 
 | ||
|  | const Stats = require("./Stats"); | ||
|  | 
 | ||
|  | /** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */ | ||
|  | /** @typedef {import("./Compilation")} Compilation */ | ||
|  | /** @typedef {import("./Compiler")} Compiler */ | ||
|  | /** @typedef {import("./FileSystemInfo").FileSystemInfoEntry} FileSystemInfoEntry */ | ||
|  | /** @typedef {import("./WebpackError")} WebpackError */ | ||
|  | /** @typedef {import("./logging/Logger").Logger} Logger */ | ||
|  | 
 | ||
|  | /** | ||
|  |  * @template T | ||
|  |  * @callback Callback | ||
|  |  * @param {(Error | null)=} err | ||
|  |  * @param {T=} result | ||
|  |  */ | ||
|  | 
 | ||
|  | class Watching { | ||
|  | 	/** | ||
|  | 	 * @param {Compiler} compiler the compiler | ||
|  | 	 * @param {WatchOptions} watchOptions options | ||
|  | 	 * @param {Callback<Stats>} handler completion handler | ||
|  | 	 */ | ||
|  | 	constructor(compiler, watchOptions, handler) { | ||
|  | 		this.startTime = null; | ||
|  | 		this.invalid = false; | ||
|  | 		this.handler = handler; | ||
|  | 		/** @type {Callback<void>[]} */ | ||
|  | 		this.callbacks = []; | ||
|  | 		/** @type {Callback<void>[] | undefined} */ | ||
|  | 		this._closeCallbacks = undefined; | ||
|  | 		this.closed = false; | ||
|  | 		this.suspended = false; | ||
|  | 		this.blocked = false; | ||
|  | 		this._isBlocked = () => false; | ||
|  | 		this._onChange = () => {}; | ||
|  | 		this._onInvalid = () => {}; | ||
|  | 		if (typeof watchOptions === "number") { | ||
|  | 			this.watchOptions = { | ||
|  | 				aggregateTimeout: watchOptions | ||
|  | 			}; | ||
|  | 		} else if (watchOptions && typeof watchOptions === "object") { | ||
|  | 			this.watchOptions = { ...watchOptions }; | ||
|  | 		} else { | ||
|  | 			this.watchOptions = {}; | ||
|  | 		} | ||
|  | 		if (typeof this.watchOptions.aggregateTimeout !== "number") { | ||
|  | 			this.watchOptions.aggregateTimeout = 20; | ||
|  | 		} | ||
|  | 		this.compiler = compiler; | ||
|  | 		this.running = false; | ||
|  | 		this._initial = true; | ||
|  | 		this._invalidReported = true; | ||
|  | 		this._needRecords = true; | ||
|  | 		this.watcher = undefined; | ||
|  | 		this.pausedWatcher = undefined; | ||
|  | 		/** @type {Set<string> | undefined} */ | ||
|  | 		this._collectedChangedFiles = undefined; | ||
|  | 		/** @type {Set<string> | undefined} */ | ||
|  | 		this._collectedRemovedFiles = undefined; | ||
|  | 		this._done = this._done.bind(this); | ||
|  | 		process.nextTick(() => { | ||
|  | 			if (this._initial) this._invalidate(); | ||
|  | 		}); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {ReadonlySet<string>=} changedFiles changed files | ||
|  | 	 * @param {ReadonlySet<string>=} removedFiles removed files | ||
|  | 	 */ | ||
|  | 	_mergeWithCollected(changedFiles, removedFiles) { | ||
|  | 		if (!changedFiles) return; | ||
|  | 		if (!this._collectedChangedFiles) { | ||
|  | 			this._collectedChangedFiles = new Set(changedFiles); | ||
|  | 			this._collectedRemovedFiles = new Set(removedFiles); | ||
|  | 		} else { | ||
|  | 			for (const file of changedFiles) { | ||
|  | 				this._collectedChangedFiles.add(file); | ||
|  | 				/** @type {Set<string>} */ | ||
|  | 				(this._collectedRemovedFiles).delete(file); | ||
|  | 			} | ||
|  | 			for (const file of /** @type {ReadonlySet<string>} */ (removedFiles)) { | ||
|  | 				this._collectedChangedFiles.delete(file); | ||
|  | 				/** @type {Set<string>} */ | ||
|  | 				(this._collectedRemovedFiles).add(file); | ||
|  | 			} | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {ReadonlyMap<string, FileSystemInfoEntry | "ignore">=} fileTimeInfoEntries info for files | ||
|  | 	 * @param {ReadonlyMap<string, FileSystemInfoEntry | "ignore">=} contextTimeInfoEntries info for directories | ||
|  | 	 * @param {ReadonlySet<string>=} changedFiles changed files | ||
|  | 	 * @param {ReadonlySet<string>=} removedFiles removed files | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	_go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) { | ||
|  | 		this._initial = false; | ||
|  | 		if (this.startTime === null) this.startTime = Date.now(); | ||
|  | 		this.running = true; | ||
|  | 		if (this.watcher) { | ||
|  | 			this.pausedWatcher = this.watcher; | ||
|  | 			this.lastWatcherStartTime = Date.now(); | ||
|  | 			this.watcher.pause(); | ||
|  | 			this.watcher = null; | ||
|  | 		} else if (!this.lastWatcherStartTime) { | ||
|  | 			this.lastWatcherStartTime = Date.now(); | ||
|  | 		} | ||
|  | 		this.compiler.fsStartTime = Date.now(); | ||
|  | 		if ( | ||
|  | 			changedFiles && | ||
|  | 			removedFiles && | ||
|  | 			fileTimeInfoEntries && | ||
|  | 			contextTimeInfoEntries | ||
|  | 		) { | ||
|  | 			this._mergeWithCollected(changedFiles, removedFiles); | ||
|  | 			this.compiler.fileTimestamps = fileTimeInfoEntries; | ||
|  | 			this.compiler.contextTimestamps = contextTimeInfoEntries; | ||
|  | 		} else if (this.pausedWatcher) { | ||
|  | 			if (this.pausedWatcher.getInfo) { | ||
|  | 				const { | ||
|  | 					changes, | ||
|  | 					removals, | ||
|  | 					fileTimeInfoEntries, | ||
|  | 					contextTimeInfoEntries | ||
|  | 				} = this.pausedWatcher.getInfo(); | ||
|  | 				this._mergeWithCollected(changes, removals); | ||
|  | 				this.compiler.fileTimestamps = fileTimeInfoEntries; | ||
|  | 				this.compiler.contextTimestamps = contextTimeInfoEntries; | ||
|  | 			} else { | ||
|  | 				this._mergeWithCollected( | ||
|  | 					this.pausedWatcher.getAggregatedChanges && | ||
|  | 						this.pausedWatcher.getAggregatedChanges(), | ||
|  | 					this.pausedWatcher.getAggregatedRemovals && | ||
|  | 						this.pausedWatcher.getAggregatedRemovals() | ||
|  | 				); | ||
|  | 				this.compiler.fileTimestamps = | ||
|  | 					this.pausedWatcher.getFileTimeInfoEntries(); | ||
|  | 				this.compiler.contextTimestamps = | ||
|  | 					this.pausedWatcher.getContextTimeInfoEntries(); | ||
|  | 			} | ||
|  | 		} | ||
|  | 		this.compiler.modifiedFiles = this._collectedChangedFiles; | ||
|  | 		this._collectedChangedFiles = undefined; | ||
|  | 		this.compiler.removedFiles = this._collectedRemovedFiles; | ||
|  | 		this._collectedRemovedFiles = undefined; | ||
|  | 
 | ||
|  | 		const run = () => { | ||
|  | 			if (this.compiler.idle) { | ||
|  | 				return this.compiler.cache.endIdle(err => { | ||
|  | 					if (err) return this._done(err); | ||
|  | 					this.compiler.idle = false; | ||
|  | 					run(); | ||
|  | 				}); | ||
|  | 			} | ||
|  | 			if (this._needRecords) { | ||
|  | 				return this.compiler.readRecords(err => { | ||
|  | 					if (err) return this._done(err); | ||
|  | 
 | ||
|  | 					this._needRecords = false; | ||
|  | 					run(); | ||
|  | 				}); | ||
|  | 			} | ||
|  | 			this.invalid = false; | ||
|  | 			this._invalidReported = false; | ||
|  | 			this.compiler.hooks.watchRun.callAsync(this.compiler, err => { | ||
|  | 				if (err) return this._done(err); | ||
|  | 				const onCompiled = (err, compilation) => { | ||
|  | 					if (err) return this._done(err, compilation); | ||
|  | 					if (this.invalid) return this._done(null, compilation); | ||
|  | 
 | ||
|  | 					if (this.compiler.hooks.shouldEmit.call(compilation) === false) { | ||
|  | 						return this._done(null, compilation); | ||
|  | 					} | ||
|  | 
 | ||
|  | 					process.nextTick(() => { | ||
|  | 						const logger = compilation.getLogger("webpack.Compiler"); | ||
|  | 						logger.time("emitAssets"); | ||
|  | 						this.compiler.emitAssets(compilation, err => { | ||
|  | 							logger.timeEnd("emitAssets"); | ||
|  | 							if (err) return this._done(err, compilation); | ||
|  | 							if (this.invalid) return this._done(null, compilation); | ||
|  | 
 | ||
|  | 							logger.time("emitRecords"); | ||
|  | 							this.compiler.emitRecords(err => { | ||
|  | 								logger.timeEnd("emitRecords"); | ||
|  | 								if (err) return this._done(err, compilation); | ||
|  | 
 | ||
|  | 								if (compilation.hooks.needAdditionalPass.call()) { | ||
|  | 									compilation.needAdditionalPass = true; | ||
|  | 
 | ||
|  | 									compilation.startTime = this.startTime; | ||
|  | 									compilation.endTime = Date.now(); | ||
|  | 									logger.time("done hook"); | ||
|  | 									const stats = new Stats(compilation); | ||
|  | 									this.compiler.hooks.done.callAsync(stats, err => { | ||
|  | 										logger.timeEnd("done hook"); | ||
|  | 										if (err) return this._done(err, compilation); | ||
|  | 
 | ||
|  | 										this.compiler.hooks.additionalPass.callAsync(err => { | ||
|  | 											if (err) return this._done(err, compilation); | ||
|  | 											this.compiler.compile(onCompiled); | ||
|  | 										}); | ||
|  | 									}); | ||
|  | 									return; | ||
|  | 								} | ||
|  | 								return this._done(null, compilation); | ||
|  | 							}); | ||
|  | 						}); | ||
|  | 					}); | ||
|  | 				}; | ||
|  | 				this.compiler.compile(onCompiled); | ||
|  | 			}); | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		run(); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {Compilation} compilation the compilation | ||
|  | 	 * @returns {Stats} the compilation stats | ||
|  | 	 */ | ||
|  | 	_getStats(compilation) { | ||
|  | 		const stats = new Stats(compilation); | ||
|  | 		return stats; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {(Error | null)=} err an optional error | ||
|  | 	 * @param {Compilation=} compilation the compilation | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	_done(err, compilation) { | ||
|  | 		this.running = false; | ||
|  | 
 | ||
|  | 		const logger = compilation && compilation.getLogger("webpack.Watching"); | ||
|  | 
 | ||
|  | 		/** @type {Stats | null} */ | ||
|  | 		let stats = null; | ||
|  | 
 | ||
|  | 		/** | ||
|  | 		 * @param {Error} err error | ||
|  | 		 * @param {Callback<void>[]=} cbs callbacks | ||
|  | 		 */ | ||
|  | 		const handleError = (err, cbs) => { | ||
|  | 			this.compiler.hooks.failed.call(err); | ||
|  | 			this.compiler.cache.beginIdle(); | ||
|  | 			this.compiler.idle = true; | ||
|  | 			this.handler(err, /** @type {Stats} */ (stats)); | ||
|  | 			if (!cbs) { | ||
|  | 				cbs = this.callbacks; | ||
|  | 				this.callbacks = []; | ||
|  | 			} | ||
|  | 			for (const cb of cbs) cb(err); | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		if ( | ||
|  | 			this.invalid && | ||
|  | 			!this.suspended && | ||
|  | 			!this.blocked && | ||
|  | 			!(this._isBlocked() && (this.blocked = true)) | ||
|  | 		) { | ||
|  | 			if (compilation) { | ||
|  | 				/** @type {Logger} */ | ||
|  | 				(logger).time("storeBuildDependencies"); | ||
|  | 				this.compiler.cache.storeBuildDependencies( | ||
|  | 					compilation.buildDependencies, | ||
|  | 					err => { | ||
|  | 						/** @type {Logger} */ | ||
|  | 						(logger).timeEnd("storeBuildDependencies"); | ||
|  | 						if (err) return handleError(err); | ||
|  | 						this._go(); | ||
|  | 					} | ||
|  | 				); | ||
|  | 			} else { | ||
|  | 				this._go(); | ||
|  | 			} | ||
|  | 			return; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if (compilation) { | ||
|  | 			compilation.startTime = this.startTime; | ||
|  | 			compilation.endTime = Date.now(); | ||
|  | 			stats = new Stats(compilation); | ||
|  | 		} | ||
|  | 		this.startTime = null; | ||
|  | 		if (err) return handleError(err); | ||
|  | 
 | ||
|  | 		const cbs = this.callbacks; | ||
|  | 		this.callbacks = []; | ||
|  | 		/** @type {Logger} */ | ||
|  | 		(logger).time("done hook"); | ||
|  | 		this.compiler.hooks.done.callAsync(/** @type {Stats} */ (stats), err => { | ||
|  | 			/** @type {Logger} */ | ||
|  | 			(logger).timeEnd("done hook"); | ||
|  | 			if (err) return handleError(err, cbs); | ||
|  | 			this.handler(null, /** @type {Stats} */ (stats)); | ||
|  | 			/** @type {Logger} */ | ||
|  | 			(logger).time("storeBuildDependencies"); | ||
|  | 			this.compiler.cache.storeBuildDependencies( | ||
|  | 				/** @type {Compilation} */ | ||
|  | 				(compilation).buildDependencies, | ||
|  | 				err => { | ||
|  | 					/** @type {Logger} */ | ||
|  | 					(logger).timeEnd("storeBuildDependencies"); | ||
|  | 					if (err) return handleError(err, cbs); | ||
|  | 					/** @type {Logger} */ | ||
|  | 					(logger).time("beginIdle"); | ||
|  | 					this.compiler.cache.beginIdle(); | ||
|  | 					this.compiler.idle = true; | ||
|  | 					/** @type {Logger} */ | ||
|  | 					(logger).timeEnd("beginIdle"); | ||
|  | 					process.nextTick(() => { | ||
|  | 						if (!this.closed) { | ||
|  | 							this.watch( | ||
|  | 								/** @type {Compilation} */ | ||
|  | 								(compilation).fileDependencies, | ||
|  | 								/** @type {Compilation} */ | ||
|  | 								(compilation).contextDependencies, | ||
|  | 								/** @type {Compilation} */ | ||
|  | 								(compilation).missingDependencies | ||
|  | 							); | ||
|  | 						} | ||
|  | 					}); | ||
|  | 					for (const cb of cbs) cb(null); | ||
|  | 					this.compiler.hooks.afterDone.call(/** @type {Stats} */ (stats)); | ||
|  | 				} | ||
|  | 			); | ||
|  | 		}); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {Iterable<string>} files watched files | ||
|  | 	 * @param {Iterable<string>} dirs watched directories | ||
|  | 	 * @param {Iterable<string>} missing watched existence entries | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	watch(files, dirs, missing) { | ||
|  | 		this.pausedWatcher = null; | ||
|  | 		this.watcher = this.compiler.watchFileSystem.watch( | ||
|  | 			files, | ||
|  | 			dirs, | ||
|  | 			missing, | ||
|  | 			this.lastWatcherStartTime, | ||
|  | 			this.watchOptions, | ||
|  | 			( | ||
|  | 				err, | ||
|  | 				fileTimeInfoEntries, | ||
|  | 				contextTimeInfoEntries, | ||
|  | 				changedFiles, | ||
|  | 				removedFiles | ||
|  | 			) => { | ||
|  | 				if (err) { | ||
|  | 					this.compiler.modifiedFiles = undefined; | ||
|  | 					this.compiler.removedFiles = undefined; | ||
|  | 					this.compiler.fileTimestamps = undefined; | ||
|  | 					this.compiler.contextTimestamps = undefined; | ||
|  | 					this.compiler.fsStartTime = undefined; | ||
|  | 					return this.handler(err); | ||
|  | 				} | ||
|  | 				this._invalidate( | ||
|  | 					fileTimeInfoEntries, | ||
|  | 					contextTimeInfoEntries, | ||
|  | 					changedFiles, | ||
|  | 					removedFiles | ||
|  | 				); | ||
|  | 				this._onChange(); | ||
|  | 			}, | ||
|  | 			(fileName, changeTime) => { | ||
|  | 				if (!this._invalidReported) { | ||
|  | 					this._invalidReported = true; | ||
|  | 					this.compiler.hooks.invalid.call(fileName, changeTime); | ||
|  | 				} | ||
|  | 				this._onInvalid(); | ||
|  | 			} | ||
|  | 		); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {Callback<void>=} callback signals when the build has completed again | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	invalidate(callback) { | ||
|  | 		if (callback) { | ||
|  | 			this.callbacks.push(callback); | ||
|  | 		} | ||
|  | 		if (!this._invalidReported) { | ||
|  | 			this._invalidReported = true; | ||
|  | 			this.compiler.hooks.invalid.call(null, Date.now()); | ||
|  | 		} | ||
|  | 		this._onChange(); | ||
|  | 		this._invalidate(); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {ReadonlyMap<string, FileSystemInfoEntry | "ignore">=} fileTimeInfoEntries info for files | ||
|  | 	 * @param {ReadonlyMap<string, FileSystemInfoEntry | "ignore">=} contextTimeInfoEntries info for directories | ||
|  | 	 * @param {ReadonlySet<string>=} changedFiles changed files | ||
|  | 	 * @param {ReadonlySet<string>=} removedFiles removed files | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	_invalidate( | ||
|  | 		fileTimeInfoEntries, | ||
|  | 		contextTimeInfoEntries, | ||
|  | 		changedFiles, | ||
|  | 		removedFiles | ||
|  | 	) { | ||
|  | 		if (this.suspended || (this._isBlocked() && (this.blocked = true))) { | ||
|  | 			this._mergeWithCollected(changedFiles, removedFiles); | ||
|  | 			return; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if (this.running) { | ||
|  | 			this._mergeWithCollected(changedFiles, removedFiles); | ||
|  | 			this.invalid = true; | ||
|  | 		} else { | ||
|  | 			this._go( | ||
|  | 				fileTimeInfoEntries, | ||
|  | 				contextTimeInfoEntries, | ||
|  | 				changedFiles, | ||
|  | 				removedFiles | ||
|  | 			); | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	suspend() { | ||
|  | 		this.suspended = true; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	resume() { | ||
|  | 		if (this.suspended) { | ||
|  | 			this.suspended = false; | ||
|  | 			this._invalidate(); | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {Callback<void>} callback signals when the watcher is closed | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	close(callback) { | ||
|  | 		if (this._closeCallbacks) { | ||
|  | 			if (callback) { | ||
|  | 				this._closeCallbacks.push(callback); | ||
|  | 			} | ||
|  | 			return; | ||
|  | 		} | ||
|  | 		/** | ||
|  | 		 * @param {(WebpackError | null)=} err error if any | ||
|  | 		 * @param {Compilation=} compilation compilation if any | ||
|  | 		 */ | ||
|  | 		const finalCallback = (err, compilation) => { | ||
|  | 			this.running = false; | ||
|  | 			this.compiler.running = false; | ||
|  | 			this.compiler.watching = undefined; | ||
|  | 			this.compiler.watchMode = false; | ||
|  | 			this.compiler.modifiedFiles = undefined; | ||
|  | 			this.compiler.removedFiles = undefined; | ||
|  | 			this.compiler.fileTimestamps = undefined; | ||
|  | 			this.compiler.contextTimestamps = undefined; | ||
|  | 			this.compiler.fsStartTime = undefined; | ||
|  | 			/** | ||
|  | 			 * @param {(WebpackError | null)=} err error if any | ||
|  | 			 */ | ||
|  | 			const shutdown = err => { | ||
|  | 				this.compiler.hooks.watchClose.call(); | ||
|  | 				const closeCallbacks = | ||
|  | 					/** @type {Callback<void>[]} */ | ||
|  | 					(this._closeCallbacks); | ||
|  | 				this._closeCallbacks = undefined; | ||
|  | 				for (const cb of closeCallbacks) cb(err); | ||
|  | 			}; | ||
|  | 			if (compilation) { | ||
|  | 				const logger = compilation.getLogger("webpack.Watching"); | ||
|  | 				logger.time("storeBuildDependencies"); | ||
|  | 				this.compiler.cache.storeBuildDependencies( | ||
|  | 					compilation.buildDependencies, | ||
|  | 					err2 => { | ||
|  | 						logger.timeEnd("storeBuildDependencies"); | ||
|  | 						shutdown(err || err2); | ||
|  | 					} | ||
|  | 				); | ||
|  | 			} else { | ||
|  | 				shutdown(err); | ||
|  | 			} | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		this.closed = true; | ||
|  | 		if (this.watcher) { | ||
|  | 			this.watcher.close(); | ||
|  | 			this.watcher = null; | ||
|  | 		} | ||
|  | 		if (this.pausedWatcher) { | ||
|  | 			this.pausedWatcher.close(); | ||
|  | 			this.pausedWatcher = null; | ||
|  | 		} | ||
|  | 		this._closeCallbacks = []; | ||
|  | 		if (callback) { | ||
|  | 			this._closeCallbacks.push(callback); | ||
|  | 		} | ||
|  | 		if (this.running) { | ||
|  | 			this.invalid = true; | ||
|  | 			this._done = finalCallback; | ||
|  | 		} else { | ||
|  | 			finalCallback(); | ||
|  | 		} | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = Watching; |