Watching.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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.invalid) return this._done(null, compilation);
  180. if (this.compiler.hooks.shouldEmit.call(compilation) === false) {
  181. return this._done(null, compilation);
  182. }
  183. process.nextTick(() => {
  184. const logger = compilation.getLogger("webpack.Compiler");
  185. logger.time("emitAssets");
  186. this.compiler.emitAssets(compilation, err => {
  187. logger.timeEnd("emitAssets");
  188. if (err) return this._done(err, compilation);
  189. if (this.invalid) return this._done(null, compilation);
  190. logger.time("emitRecords");
  191. this.compiler.emitRecords(err => {
  192. logger.timeEnd("emitRecords");
  193. if (err) return this._done(err, compilation);
  194. if (compilation.hooks.needAdditionalPass.call()) {
  195. compilation.needAdditionalPass = true;
  196. compilation.startTime = /** @type {number} */ (
  197. this.startTime
  198. );
  199. compilation.endTime = Date.now();
  200. logger.time("done hook");
  201. const stats = new Stats(compilation);
  202. this.compiler.hooks.done.callAsync(stats, err => {
  203. logger.timeEnd("done hook");
  204. if (err) return this._done(err, compilation);
  205. this.compiler.hooks.additionalPass.callAsync(err => {
  206. if (err) return this._done(err, compilation);
  207. this.compiler.compile(onCompiled);
  208. });
  209. });
  210. return;
  211. }
  212. return this._done(null, compilation);
  213. });
  214. });
  215. });
  216. };
  217. this.compiler.compile(onCompiled);
  218. });
  219. };
  220. run();
  221. }
  222. /**
  223. * @param {Compilation} compilation the compilation
  224. * @returns {Stats} the compilation stats
  225. */
  226. _getStats(compilation) {
  227. const stats = new Stats(compilation);
  228. return stats;
  229. }
  230. /**
  231. * @param {(Error | null)=} err an optional error
  232. * @param {Compilation=} compilation the compilation
  233. * @returns {void}
  234. */
  235. _done(err, compilation) {
  236. this.running = false;
  237. const logger =
  238. /** @type {Logger} */
  239. (compilation && compilation.getLogger("webpack.Watching"));
  240. /** @type {Stats | undefined} */
  241. let stats;
  242. /**
  243. * @param {Error} err error
  244. * @param {Callback<void>[]=} cbs callbacks
  245. */
  246. const handleError = (err, cbs) => {
  247. this.compiler.hooks.failed.call(err);
  248. this.compiler.cache.beginIdle();
  249. this.compiler.idle = true;
  250. this.handler(err, /** @type {Stats} */ (stats));
  251. if (!cbs) {
  252. cbs = this.callbacks;
  253. this.callbacks = [];
  254. }
  255. for (const cb of cbs) cb(err);
  256. };
  257. if (
  258. this.invalid &&
  259. !this.suspended &&
  260. !this.blocked &&
  261. !(this._isBlocked() && (this.blocked = true))
  262. ) {
  263. if (compilation) {
  264. logger.time("storeBuildDependencies");
  265. this.compiler.cache.storeBuildDependencies(
  266. compilation.buildDependencies,
  267. err => {
  268. logger.timeEnd("storeBuildDependencies");
  269. if (err) return handleError(err);
  270. this._go();
  271. }
  272. );
  273. } else {
  274. this._go();
  275. }
  276. return;
  277. }
  278. if (compilation) {
  279. compilation.startTime = /** @type {number} */ (this.startTime);
  280. compilation.endTime = Date.now();
  281. stats = new Stats(compilation);
  282. }
  283. this.startTime = null;
  284. if (err) return handleError(err);
  285. const cbs = this.callbacks;
  286. this.callbacks = [];
  287. logger.time("done hook");
  288. this.compiler.hooks.done.callAsync(/** @type {Stats} */ (stats), err => {
  289. logger.timeEnd("done hook");
  290. if (err) return handleError(err, cbs);
  291. this.handler(null, stats);
  292. logger.time("storeBuildDependencies");
  293. this.compiler.cache.storeBuildDependencies(
  294. /** @type {Compilation} */
  295. (compilation).buildDependencies,
  296. err => {
  297. logger.timeEnd("storeBuildDependencies");
  298. if (err) return handleError(err, cbs);
  299. logger.time("beginIdle");
  300. this.compiler.cache.beginIdle();
  301. this.compiler.idle = true;
  302. logger.timeEnd("beginIdle");
  303. process.nextTick(() => {
  304. if (!this.closed) {
  305. this.watch(
  306. /** @type {Compilation} */
  307. (compilation).fileDependencies,
  308. /** @type {Compilation} */
  309. (compilation).contextDependencies,
  310. /** @type {Compilation} */
  311. (compilation).missingDependencies
  312. );
  313. }
  314. });
  315. for (const cb of cbs) cb(null);
  316. this.compiler.hooks.afterDone.call(/** @type {Stats} */ (stats));
  317. }
  318. );
  319. });
  320. }
  321. /**
  322. * @param {Iterable<string>} files watched files
  323. * @param {Iterable<string>} dirs watched directories
  324. * @param {Iterable<string>} missing watched existence entries
  325. * @returns {void}
  326. */
  327. watch(files, dirs, missing) {
  328. this.pausedWatcher = null;
  329. this.watcher =
  330. /** @type {WatchFileSystem} */
  331. (this.compiler.watchFileSystem).watch(
  332. files,
  333. dirs,
  334. missing,
  335. /** @type {number} */ (this.lastWatcherStartTime),
  336. this.watchOptions,
  337. (
  338. err,
  339. fileTimeInfoEntries,
  340. contextTimeInfoEntries,
  341. changedFiles,
  342. removedFiles
  343. ) => {
  344. if (err) {
  345. this.compiler.modifiedFiles = undefined;
  346. this.compiler.removedFiles = undefined;
  347. this.compiler.fileTimestamps = undefined;
  348. this.compiler.contextTimestamps = undefined;
  349. this.compiler.fsStartTime = undefined;
  350. return this.handler(err);
  351. }
  352. this._invalidate(
  353. fileTimeInfoEntries,
  354. contextTimeInfoEntries,
  355. changedFiles,
  356. removedFiles
  357. );
  358. this._onChange();
  359. },
  360. (fileName, changeTime) => {
  361. if (!this._invalidReported) {
  362. this._invalidReported = true;
  363. this.compiler.hooks.invalid.call(fileName, changeTime);
  364. }
  365. this._onInvalid();
  366. }
  367. );
  368. }
  369. /**
  370. * @param {Callback<void>=} callback signals when the build has completed again
  371. * @returns {void}
  372. */
  373. invalidate(callback) {
  374. if (callback) {
  375. this.callbacks.push(callback);
  376. }
  377. if (!this._invalidReported) {
  378. this._invalidReported = true;
  379. this.compiler.hooks.invalid.call(null, Date.now());
  380. }
  381. this._onChange();
  382. this._invalidate();
  383. }
  384. /**
  385. * @param {TimeInfoEntries=} fileTimeInfoEntries info for files
  386. * @param {TimeInfoEntries=} contextTimeInfoEntries info for directories
  387. * @param {ReadonlySet<string>=} changedFiles changed files
  388. * @param {ReadonlySet<string>=} removedFiles removed files
  389. * @returns {void}
  390. */
  391. _invalidate(
  392. fileTimeInfoEntries,
  393. contextTimeInfoEntries,
  394. changedFiles,
  395. removedFiles
  396. ) {
  397. if (this.suspended || (this._isBlocked() && (this.blocked = true))) {
  398. this._mergeWithCollected(changedFiles, removedFiles);
  399. return;
  400. }
  401. if (this.running) {
  402. this._mergeWithCollected(changedFiles, removedFiles);
  403. this.invalid = true;
  404. } else {
  405. this._go(
  406. fileTimeInfoEntries,
  407. contextTimeInfoEntries,
  408. changedFiles,
  409. removedFiles
  410. );
  411. }
  412. }
  413. suspend() {
  414. this.suspended = true;
  415. }
  416. resume() {
  417. if (this.suspended) {
  418. this.suspended = false;
  419. this._invalidate();
  420. }
  421. }
  422. /**
  423. * @param {Callback<void>} callback signals when the watcher is closed
  424. * @returns {void}
  425. */
  426. close(callback) {
  427. if (this._closeCallbacks) {
  428. if (callback) {
  429. this._closeCallbacks.push(callback);
  430. }
  431. return;
  432. }
  433. /**
  434. * @param {WebpackError | null} err error if any
  435. * @param {Compilation=} compilation compilation if any
  436. */
  437. const finalCallback = (err, compilation) => {
  438. this.running = false;
  439. this.compiler.running = false;
  440. this.compiler.watching = undefined;
  441. this.compiler.watchMode = false;
  442. this.compiler.modifiedFiles = undefined;
  443. this.compiler.removedFiles = undefined;
  444. this.compiler.fileTimestamps = undefined;
  445. this.compiler.contextTimestamps = undefined;
  446. this.compiler.fsStartTime = undefined;
  447. /**
  448. * @param {WebpackError | null} err error if any
  449. */
  450. const shutdown = err => {
  451. this.compiler.hooks.watchClose.call();
  452. const closeCallbacks =
  453. /** @type {Callback<void>[]} */
  454. (this._closeCallbacks);
  455. this._closeCallbacks = undefined;
  456. for (const cb of closeCallbacks) cb(err);
  457. };
  458. if (compilation) {
  459. const logger = compilation.getLogger("webpack.Watching");
  460. logger.time("storeBuildDependencies");
  461. this.compiler.cache.storeBuildDependencies(
  462. compilation.buildDependencies,
  463. err2 => {
  464. logger.timeEnd("storeBuildDependencies");
  465. shutdown(err || err2);
  466. }
  467. );
  468. } else {
  469. shutdown(err);
  470. }
  471. };
  472. this.closed = true;
  473. if (this.watcher) {
  474. this.watcher.close();
  475. this.watcher = null;
  476. }
  477. if (this.pausedWatcher) {
  478. this.pausedWatcher.close();
  479. this.pausedWatcher = null;
  480. }
  481. this._closeCallbacks = [];
  482. if (callback) {
  483. this._closeCallbacks.push(callback);
  484. }
  485. if (this.running) {
  486. this.invalid = true;
  487. this._done = finalCallback;
  488. } else {
  489. finalCallback(null);
  490. }
  491. }
  492. }
  493. module.exports = Watching;