Watching.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Stats = require("./Stats");
  7. /** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
  8. /** @typedef {import("./Compilation")} Compilation */
  9. /** @typedef {import("./Compiler")} Compiler */
  10. /** @typedef {import("./Compiler").ErrorCallback} ErrorCallback */
  11. /** @typedef {import("./WebpackError")} WebpackError */
  12. /** @typedef {import("./logging/Logger").Logger} Logger */
  13. /** @typedef {import("./util/fs").TimeInfoEntries} TimeInfoEntries */
  14. /** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem */
  15. /** @typedef {import("./util/fs").Watcher} Watcher */
  16. /**
  17. * Defines the callback type used by this module.
  18. * @template T
  19. * @template [R=void]
  20. * @typedef {import("./webpack").Callback<T, R>} Callback
  21. */
  22. /** @typedef {Set<string>} CollectedFiles */
  23. class Watching {
  24. /**
  25. * Creates an instance of Watching.
  26. * @param {Compiler} compiler the compiler
  27. * @param {WatchOptions} watchOptions options
  28. * @param {Callback<Stats>} handler completion handler
  29. */
  30. constructor(compiler, watchOptions, handler) {
  31. /** @type {null | number} */
  32. this.startTime = null;
  33. this.invalid = false;
  34. /** @type {Callback<Stats>} */
  35. this.handler = handler;
  36. /** @type {ErrorCallback[]} */
  37. this.callbacks = [];
  38. /** @type {ErrorCallback[] | undefined} */
  39. this._closeCallbacks = undefined;
  40. this.closed = false;
  41. this.suspended = false;
  42. this.blocked = false;
  43. this._isBlocked = () => false;
  44. this._onChange = () => {};
  45. this._onInvalid = () => {};
  46. if (typeof watchOptions === "number") {
  47. /** @type {WatchOptions} */
  48. this.watchOptions = {
  49. aggregateTimeout: watchOptions
  50. };
  51. } else if (watchOptions && typeof watchOptions === "object") {
  52. /** @type {WatchOptions} */
  53. this.watchOptions = { ...watchOptions };
  54. } else {
  55. /** @type {WatchOptions} */
  56. this.watchOptions = {};
  57. }
  58. if (typeof this.watchOptions.aggregateTimeout !== "number") {
  59. this.watchOptions.aggregateTimeout = 20;
  60. }
  61. this.compiler = compiler;
  62. this.running = false;
  63. this._initial = true;
  64. this._invalidReported = true;
  65. this._needRecords = true;
  66. /** @type {undefined | null | Watcher} */
  67. this.watcher = undefined;
  68. /** @type {undefined | null | Watcher} */
  69. this.pausedWatcher = undefined;
  70. /** @type {CollectedFiles | undefined} */
  71. this._collectedChangedFiles = undefined;
  72. /** @type {CollectedFiles | undefined} */
  73. this._collectedRemovedFiles = undefined;
  74. this._done = this._done.bind(this);
  75. process.nextTick(() => {
  76. if (this._initial) this._invalidate();
  77. });
  78. }
  79. /**
  80. * Merge with collected.
  81. * @param {ReadonlySet<string> | undefined | null} changedFiles changed files
  82. * @param {ReadonlySet<string> | undefined | null} removedFiles removed files
  83. */
  84. _mergeWithCollected(changedFiles, removedFiles) {
  85. if (!changedFiles) return;
  86. if (!this._collectedChangedFiles) {
  87. this._collectedChangedFiles = new Set(changedFiles);
  88. this._collectedRemovedFiles = new Set(removedFiles);
  89. } else {
  90. for (const file of changedFiles) {
  91. this._collectedChangedFiles.add(file);
  92. /** @type {CollectedFiles} */
  93. (this._collectedRemovedFiles).delete(file);
  94. }
  95. for (const file of /** @type {ReadonlySet<string>} */ (removedFiles)) {
  96. this._collectedChangedFiles.delete(file);
  97. /** @type {CollectedFiles} */
  98. (this._collectedRemovedFiles).add(file);
  99. }
  100. }
  101. }
  102. /**
  103. * Processes the provided file time info entries.
  104. * @param {TimeInfoEntries=} fileTimeInfoEntries info for files
  105. * @param {TimeInfoEntries=} contextTimeInfoEntries info for directories
  106. * @param {ReadonlySet<string>=} changedFiles changed files
  107. * @param {ReadonlySet<string>=} removedFiles removed files
  108. * @returns {void}
  109. */
  110. _go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) {
  111. this._initial = false;
  112. if (this.startTime === null) this.startTime = Date.now();
  113. this.running = true;
  114. if (this.watcher) {
  115. this.pausedWatcher = this.watcher;
  116. this.lastWatcherStartTime = Date.now();
  117. this.watcher.pause();
  118. this.watcher = null;
  119. } else if (!this.lastWatcherStartTime) {
  120. this.lastWatcherStartTime = Date.now();
  121. }
  122. this.compiler.fsStartTime = Date.now();
  123. if (
  124. changedFiles &&
  125. removedFiles &&
  126. fileTimeInfoEntries &&
  127. contextTimeInfoEntries
  128. ) {
  129. this._mergeWithCollected(changedFiles, removedFiles);
  130. this.compiler.fileTimestamps = fileTimeInfoEntries;
  131. this.compiler.contextTimestamps = contextTimeInfoEntries;
  132. } else if (this.pausedWatcher) {
  133. if (this.pausedWatcher.getInfo) {
  134. const {
  135. changes,
  136. removals,
  137. fileTimeInfoEntries,
  138. contextTimeInfoEntries
  139. } = this.pausedWatcher.getInfo();
  140. this._mergeWithCollected(changes, removals);
  141. this.compiler.fileTimestamps = fileTimeInfoEntries;
  142. this.compiler.contextTimestamps = contextTimeInfoEntries;
  143. } else {
  144. this._mergeWithCollected(
  145. this.pausedWatcher.getAggregatedChanges &&
  146. this.pausedWatcher.getAggregatedChanges(),
  147. this.pausedWatcher.getAggregatedRemovals &&
  148. this.pausedWatcher.getAggregatedRemovals()
  149. );
  150. this.compiler.fileTimestamps =
  151. this.pausedWatcher.getFileTimeInfoEntries();
  152. this.compiler.contextTimestamps =
  153. this.pausedWatcher.getContextTimeInfoEntries();
  154. }
  155. }
  156. this.compiler.modifiedFiles = this._collectedChangedFiles;
  157. this._collectedChangedFiles = undefined;
  158. this.compiler.removedFiles = this._collectedRemovedFiles;
  159. this._collectedRemovedFiles = undefined;
  160. const run = () => {
  161. if (this.compiler.idle) {
  162. return this.compiler.cache.endIdle((err) => {
  163. if (err) return this._done(err);
  164. this.compiler.idle = false;
  165. run();
  166. });
  167. }
  168. if (this._needRecords) {
  169. return this.compiler.readRecords((err) => {
  170. if (err) return this._done(err);
  171. this._needRecords = false;
  172. run();
  173. });
  174. }
  175. this.invalid = false;
  176. this._invalidReported = false;
  177. this.compiler.hooks.watchRun.callAsync(this.compiler, (err) => {
  178. if (err) return this._done(err);
  179. /**
  180. * Processes the provided err.
  181. * @param {Error | null} err error
  182. * @param {Compilation=} _compilation compilation
  183. * @returns {void}
  184. */
  185. const onCompiled = (err, _compilation) => {
  186. if (err) return this._done(err, _compilation);
  187. const compilation = /** @type {Compilation} */ (_compilation);
  188. if (this.compiler.hooks.shouldEmit.call(compilation) === false) {
  189. return this._done(null, compilation);
  190. }
  191. process.nextTick(() => {
  192. const logger = compilation.getLogger("webpack.Compiler");
  193. logger.time("emitAssets");
  194. this.compiler.emitAssets(compilation, (err) => {
  195. logger.timeEnd("emitAssets");
  196. if (err) return this._done(err, compilation);
  197. if (this.invalid) return this._done(null, compilation);
  198. logger.time("emitRecords");
  199. this.compiler.emitRecords((err) => {
  200. logger.timeEnd("emitRecords");
  201. if (err) return this._done(err, compilation);
  202. if (compilation.hooks.needAdditionalPass.call()) {
  203. compilation.needAdditionalPass = true;
  204. compilation.startTime = /** @type {number} */ (
  205. this.startTime
  206. );
  207. compilation.endTime = Date.now();
  208. logger.time("done hook");
  209. const stats = new Stats(compilation);
  210. this.compiler.hooks.done.callAsync(stats, (err) => {
  211. logger.timeEnd("done hook");
  212. if (err) return this._done(err, compilation);
  213. this.compiler.hooks.additionalPass.callAsync((err) => {
  214. if (err) return this._done(err, compilation);
  215. this.compiler.compile(onCompiled);
  216. });
  217. });
  218. return;
  219. }
  220. return this._done(null, compilation);
  221. });
  222. });
  223. });
  224. };
  225. this.compiler.compile(onCompiled);
  226. });
  227. };
  228. run();
  229. }
  230. /**
  231. * Returns the compilation stats.
  232. * @param {Compilation} compilation the compilation
  233. * @returns {Stats} the compilation stats
  234. */
  235. _getStats(compilation) {
  236. const stats = new Stats(compilation);
  237. return stats;
  238. }
  239. /**
  240. * Processes the provided err.
  241. * @param {(Error | null)=} err an optional error
  242. * @param {Compilation=} compilation the compilation
  243. * @returns {void}
  244. */
  245. _done(err, compilation) {
  246. this.running = false;
  247. const logger =
  248. /** @type {Logger} */
  249. (compilation && compilation.getLogger("webpack.Watching"));
  250. /** @type {Stats | undefined} */
  251. let stats;
  252. /**
  253. * Processes the provided err.
  254. * @param {Error} err error
  255. * @param {ErrorCallback[]=} cbs callbacks
  256. */
  257. const handleError = (err, cbs) => {
  258. this.compiler.hooks.failed.call(err);
  259. this.compiler.cache.beginIdle();
  260. this.compiler.idle = true;
  261. this.handler(err, /** @type {Stats} */ (stats));
  262. if (!cbs) {
  263. cbs = this.callbacks;
  264. this.callbacks = [];
  265. }
  266. for (const cb of cbs) cb(err);
  267. };
  268. if (
  269. this.invalid &&
  270. !this.suspended &&
  271. !this.blocked &&
  272. !(this._isBlocked() && (this.blocked = true))
  273. ) {
  274. if (compilation) {
  275. logger.time("storeBuildDependencies");
  276. this.compiler.cache.storeBuildDependencies(
  277. compilation.buildDependencies,
  278. (err) => {
  279. logger.timeEnd("storeBuildDependencies");
  280. if (err) return handleError(err);
  281. this._go();
  282. }
  283. );
  284. } else {
  285. this._go();
  286. }
  287. return;
  288. }
  289. if (compilation) {
  290. compilation.startTime = /** @type {number} */ (this.startTime);
  291. compilation.endTime = Date.now();
  292. stats = new Stats(compilation);
  293. }
  294. this.startTime = null;
  295. if (err) return handleError(err);
  296. const cbs = this.callbacks;
  297. this.callbacks = [];
  298. logger.time("done hook");
  299. this.compiler.hooks.done.callAsync(/** @type {Stats} */ (stats), (err) => {
  300. logger.timeEnd("done hook");
  301. if (err) return handleError(err, cbs);
  302. this.handler(null, stats);
  303. logger.time("storeBuildDependencies");
  304. this.compiler.cache.storeBuildDependencies(
  305. /** @type {Compilation} */
  306. (compilation).buildDependencies,
  307. (err) => {
  308. logger.timeEnd("storeBuildDependencies");
  309. if (err) return handleError(err, cbs);
  310. logger.time("beginIdle");
  311. this.compiler.cache.beginIdle();
  312. this.compiler.idle = true;
  313. logger.timeEnd("beginIdle");
  314. process.nextTick(() => {
  315. if (!this.closed) {
  316. this.watch(
  317. /** @type {Compilation} */
  318. (compilation).fileDependencies,
  319. /** @type {Compilation} */
  320. (compilation).contextDependencies,
  321. /** @type {Compilation} */
  322. (compilation).missingDependencies
  323. );
  324. }
  325. });
  326. for (const cb of cbs) cb(null);
  327. this.compiler.hooks.afterDone.call(/** @type {Stats} */ (stats));
  328. }
  329. );
  330. });
  331. }
  332. /**
  333. * Processes the provided file.
  334. * @param {Iterable<string>} files watched files
  335. * @param {Iterable<string>} dirs watched directories
  336. * @param {Iterable<string>} missing watched existence entries
  337. * @returns {void}
  338. */
  339. watch(files, dirs, missing) {
  340. this.pausedWatcher = null;
  341. this.watcher =
  342. /** @type {WatchFileSystem} */
  343. (this.compiler.watchFileSystem).watch(
  344. files,
  345. dirs,
  346. missing,
  347. /** @type {number} */ (this.lastWatcherStartTime),
  348. this.watchOptions,
  349. (
  350. err,
  351. fileTimeInfoEntries,
  352. contextTimeInfoEntries,
  353. changedFiles,
  354. removedFiles
  355. ) => {
  356. if (err) {
  357. this.compiler.modifiedFiles = undefined;
  358. this.compiler.removedFiles = undefined;
  359. this.compiler.fileTimestamps = undefined;
  360. this.compiler.contextTimestamps = undefined;
  361. this.compiler.fsStartTime = undefined;
  362. return this.handler(err);
  363. }
  364. this._invalidate(
  365. fileTimeInfoEntries,
  366. contextTimeInfoEntries,
  367. changedFiles,
  368. removedFiles
  369. );
  370. this._onChange();
  371. },
  372. (fileName, changeTime) => {
  373. if (!this._invalidReported) {
  374. this._invalidReported = true;
  375. this.compiler.hooks.invalid.call(fileName, changeTime);
  376. }
  377. this._onInvalid();
  378. }
  379. );
  380. }
  381. /**
  382. * Processes the provided error callback.
  383. * @param {ErrorCallback=} callback signals when the build has completed again
  384. * @returns {void}
  385. */
  386. invalidate(callback) {
  387. if (callback) {
  388. this.callbacks.push(callback);
  389. }
  390. if (!this._invalidReported) {
  391. this._invalidReported = true;
  392. this.compiler.hooks.invalid.call(null, Date.now());
  393. }
  394. this._onChange();
  395. this._invalidate();
  396. }
  397. /**
  398. * Processes the provided file time info entries.
  399. * @param {TimeInfoEntries=} fileTimeInfoEntries info for files
  400. * @param {TimeInfoEntries=} contextTimeInfoEntries info for directories
  401. * @param {ReadonlySet<string>=} changedFiles changed files
  402. * @param {ReadonlySet<string>=} removedFiles removed files
  403. * @returns {void}
  404. */
  405. _invalidate(
  406. fileTimeInfoEntries,
  407. contextTimeInfoEntries,
  408. changedFiles,
  409. removedFiles
  410. ) {
  411. if (this.suspended || (this._isBlocked() && (this.blocked = true))) {
  412. this._mergeWithCollected(changedFiles, removedFiles);
  413. return;
  414. }
  415. if (this.running) {
  416. this._mergeWithCollected(changedFiles, removedFiles);
  417. this.invalid = true;
  418. } else {
  419. this._go(
  420. fileTimeInfoEntries,
  421. contextTimeInfoEntries,
  422. changedFiles,
  423. removedFiles
  424. );
  425. }
  426. }
  427. suspend() {
  428. this.suspended = true;
  429. }
  430. resume() {
  431. if (this.suspended) {
  432. this.suspended = false;
  433. this._invalidate();
  434. }
  435. }
  436. /**
  437. * Processes the provided error callback.
  438. * @param {ErrorCallback} callback signals when the watcher is closed
  439. * @returns {void}
  440. */
  441. close(callback) {
  442. if (this._closeCallbacks) {
  443. if (callback) {
  444. this._closeCallbacks.push(callback);
  445. }
  446. return;
  447. }
  448. /**
  449. * Processes the provided err.
  450. * @param {WebpackError | null} err error if any
  451. * @param {Compilation=} compilation compilation if any
  452. */
  453. const finalCallback = (err, compilation) => {
  454. this.running = false;
  455. this.compiler.running = false;
  456. this.compiler.watching = undefined;
  457. this.compiler.watchMode = false;
  458. this.compiler.modifiedFiles = undefined;
  459. this.compiler.removedFiles = undefined;
  460. this.compiler.fileTimestamps = undefined;
  461. this.compiler.contextTimestamps = undefined;
  462. this.compiler.fsStartTime = undefined;
  463. /**
  464. * Processes the provided err.
  465. * @param {WebpackError | null} err error if any
  466. */
  467. const shutdown = (err) => {
  468. this.compiler.hooks.watchClose.call();
  469. const closeCallbacks =
  470. /** @type {ErrorCallback[]} */
  471. (this._closeCallbacks);
  472. this._closeCallbacks = undefined;
  473. for (const cb of closeCallbacks) cb(err);
  474. };
  475. if (compilation) {
  476. const logger = compilation.getLogger("webpack.Watching");
  477. logger.time("storeBuildDependencies");
  478. this.compiler.cache.storeBuildDependencies(
  479. compilation.buildDependencies,
  480. (err2) => {
  481. logger.timeEnd("storeBuildDependencies");
  482. shutdown(err || err2);
  483. }
  484. );
  485. } else {
  486. shutdown(err);
  487. }
  488. };
  489. this.closed = true;
  490. if (this.watcher) {
  491. this.watcher.close();
  492. this.watcher = null;
  493. }
  494. if (this.pausedWatcher) {
  495. this.pausedWatcher.close();
  496. this.pausedWatcher = null;
  497. }
  498. this._closeCallbacks = [];
  499. if (callback) {
  500. this._closeCallbacks.push(callback);
  501. }
  502. if (this.running) {
  503. this.invalid = true;
  504. this._done = finalCallback;
  505. } else {
  506. finalCallback(null);
  507. }
  508. }
  509. }
  510. module.exports = Watching;