/* 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 */ /** @typedef {import("./util/fs").TimeInfoEntries} TimeInfoEntries */ /** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem */ /** @typedef {import("./util/fs").Watcher} Watcher */ /** * @template T * @callback Callback * @param {Error | null} err * @param {T=} result */ class Watching { /** * @param {Compiler} compiler the compiler * @param {WatchOptions} watchOptions options * @param {Callback} handler completion handler */ constructor(compiler, watchOptions, handler) { this.startTime = null; this.invalid = false; this.handler = handler; /** @type {Callback[]} */ this.callbacks = []; /** @type {Callback[] | undefined} */ this._closeCallbacks = undefined; this.closed = false; this.suspended = false; this.blocked = false; this._isBlocked = () => false; this._onChange = () => {}; this._onInvalid = () => {}; if (typeof watchOptions === "number") { /** @type {WatchOptions} */ this.watchOptions = { aggregateTimeout: watchOptions }; } else if (watchOptions && typeof watchOptions === "object") { /** @type {WatchOptions} */ this.watchOptions = { ...watchOptions }; } else { /** @type {WatchOptions} */ 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 | undefined} */ this._collectedChangedFiles = undefined; /** @type {Set | undefined} */ this._collectedRemovedFiles = undefined; this._done = this._done.bind(this); process.nextTick(() => { if (this._initial) this._invalidate(); }); } /** * @param {ReadonlySet=} changedFiles changed files * @param {ReadonlySet=} 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} */ (this._collectedRemovedFiles).delete(file); } for (const file of /** @type {ReadonlySet} */ (removedFiles)) { this._collectedChangedFiles.delete(file); /** @type {Set} */ (this._collectedRemovedFiles).add(file); } } } /** * @param {TimeInfoEntries=} fileTimeInfoEntries info for files * @param {TimeInfoEntries=} contextTimeInfoEntries info for directories * @param {ReadonlySet=} changedFiles changed files * @param {ReadonlySet=} 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); /** * @param {Error | null} err error * @param {Compilation=} _compilation compilation * @returns {void} */ const onCompiled = (err, _compilation) => { if (err) return this._done(err, _compilation); const compilation = /** @type {Compilation} */ (_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 = /** @type {number} */ ( 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 = /** @type {Logger} */ (compilation && compilation.getLogger("webpack.Watching")); /** @type {Stats | undefined} */ let stats; /** * @param {Error} err error * @param {Callback[]=} 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) { logger.time("storeBuildDependencies"); this.compiler.cache.storeBuildDependencies( compilation.buildDependencies, err => { logger.timeEnd("storeBuildDependencies"); if (err) return handleError(err); this._go(); } ); } else { this._go(); } return; } if (compilation) { compilation.startTime = /** @type {number} */ (this.startTime); compilation.endTime = Date.now(); stats = new Stats(compilation); } this.startTime = null; if (err) return handleError(err); const cbs = this.callbacks; this.callbacks = []; logger.time("done hook"); this.compiler.hooks.done.callAsync(/** @type {Stats} */ (stats), err => { logger.timeEnd("done hook"); if (err) return handleError(err, cbs); this.handler(null, stats); logger.time("storeBuildDependencies"); this.compiler.cache.storeBuildDependencies( /** @type {Compilation} */ (compilation).buildDependencies, err => { logger.timeEnd("storeBuildDependencies"); if (err) return handleError(err, cbs); logger.time("beginIdle"); this.compiler.cache.beginIdle(); this.compiler.idle = true; 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} files watched files * @param {Iterable} dirs watched directories * @param {Iterable} missing watched existence entries * @returns {void} */ watch(files, dirs, missing) { this.pausedWatcher = null; this.watcher = /** @type {WatchFileSystem} */ (this.compiler.watchFileSystem).watch( files, dirs, missing, /** @type {number} */ (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=} 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 {TimeInfoEntries=} fileTimeInfoEntries info for files * @param {TimeInfoEntries=} contextTimeInfoEntries info for directories * @param {ReadonlySet=} changedFiles changed files * @param {ReadonlySet=} 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} 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[]} */ (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(null); } } } module.exports = Watching;