Watching.js 14 KB

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