提交学习笔记专用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

526 lines
14 KiB

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