Watching.js 14 KB

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