Watching.js 15 KB

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