Watching.js 14 KB

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