Watching.js 14 KB

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