NodeWatchFileSystem.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const util = require("util");
  7. const memoize = require("../util/memoize");
  8. // watchpack is only reachable from `watch`, so non-watch builds never load it
  9. const getWatchpack = memoize(() => require("watchpack"));
  10. /** @typedef {InstanceType<import("watchpack")>} Watchpack */
  11. /** @import { TimeInfoEntries, WatchOptions } from "watchpack" */
  12. /**
  13. * @import {
  14. * InputFileSystem,
  15. * WatchMethod,
  16. * Changes,
  17. * Removals
  18. * } from "../util/fs"
  19. */
  20. // Watchpack pools DirectoryWatchers per options object identity; interning by
  21. // value lets a MultiCompiler's children share one pool instead of one scan each.
  22. /** @type {Map<string, WatchOptions>} */
  23. const canonicalWatchOptions = new Map();
  24. /** @type {WeakMap<RegExp, string>} */
  25. const regExpKeys = new WeakMap();
  26. let nextRegExpKey = 0;
  27. // A RegExp keys by reference, so two configs spelling the same one inline do
  28. // not share a pool.
  29. /**
  30. * @param {WatchOptions["ignored"]} ignored the `ignored` option
  31. * @returns {string} key describing it, or "" when it cannot be keyed
  32. */
  33. const ignoredKey = (ignored) => {
  34. if (ignored === undefined || ignored === null) return "-";
  35. if (typeof ignored === "string") return `s${ignored}`;
  36. if (Array.isArray(ignored)) return `a${JSON.stringify(ignored)}`;
  37. if (!(ignored instanceof RegExp)) return "";
  38. let key = regExpKeys.get(ignored);
  39. if (key === undefined) {
  40. key = `r${nextRegExpKey++}`;
  41. regExpKeys.set(ignored, key);
  42. }
  43. return key;
  44. };
  45. // Snapshot rather than keep the caller's object: mutating it later would hand
  46. // the next caller options it never asked for.
  47. /**
  48. * @param {WatchOptions} options watch options
  49. * @returns {WatchOptions} a shared object equal to `options`, or `options`
  50. */
  51. const getCanonicalWatchOptions = (options) => {
  52. const ignored = ignoredKey(options.ignored);
  53. if (ignored === "") return options;
  54. const key = `${options.aggregateTimeout}|${Boolean(
  55. options.followSymlinks
  56. )}|${options.poll}|${ignored}`;
  57. const canonical = canonicalWatchOptions.get(key);
  58. if (canonical !== undefined) return canonical;
  59. /** @type {WatchOptions} */
  60. const snapshot = {
  61. aggregateTimeout: options.aggregateTimeout,
  62. followSymlinks: options.followSymlinks,
  63. ignored: Array.isArray(options.ignored)
  64. ? [...options.ignored]
  65. : options.ignored,
  66. poll: options.poll
  67. };
  68. canonicalWatchOptions.set(key, snapshot);
  69. return snapshot;
  70. };
  71. class NodeWatchFileSystem {
  72. /**
  73. * Creates an instance of NodeWatchFileSystem.
  74. * @param {InputFileSystem} inputFileSystem input filesystem
  75. */
  76. constructor(inputFileSystem) {
  77. /** @type {InputFileSystem} */
  78. this.inputFileSystem = inputFileSystem;
  79. /** @type {WatchOptions} */
  80. this.watcherOptions = {
  81. aggregateTimeout: 0
  82. };
  83. // `watch` replaces this with a watcher built from its own options
  84. /** @type {Watchpack | null} */
  85. this.watcher = null;
  86. }
  87. /** @type {WatchMethod} */
  88. watch(
  89. files,
  90. directories,
  91. missing,
  92. startTime,
  93. options,
  94. callback,
  95. callbackUndelayed
  96. ) {
  97. if (!files || typeof files[Symbol.iterator] !== "function") {
  98. throw new Error("Invalid arguments: 'files'");
  99. }
  100. if (!directories || typeof directories[Symbol.iterator] !== "function") {
  101. throw new Error("Invalid arguments: 'directories'");
  102. }
  103. if (!missing || typeof missing[Symbol.iterator] !== "function") {
  104. throw new Error("Invalid arguments: 'missing'");
  105. }
  106. if (typeof callback !== "function") {
  107. throw new Error("Invalid arguments: 'callback'");
  108. }
  109. if (typeof startTime !== "number" && startTime) {
  110. throw new Error("Invalid arguments: 'startTime'");
  111. }
  112. if (typeof options !== "object") {
  113. throw new Error("Invalid arguments: 'options'");
  114. }
  115. if (typeof callbackUndelayed !== "function" && callbackUndelayed) {
  116. throw new Error("Invalid arguments: 'callbackUndelayed'");
  117. }
  118. const oldWatcher = this.watcher;
  119. this.watcher = new (getWatchpack())(getCanonicalWatchOptions(options));
  120. if (callbackUndelayed) {
  121. this.watcher.once("change", callbackUndelayed);
  122. }
  123. const fetchTimeInfo = () => {
  124. /** @type {TimeInfoEntries} */
  125. const fileTimeInfoEntries = new Map();
  126. /** @type {TimeInfoEntries} */
  127. const contextTimeInfoEntries = new Map();
  128. if (this.watcher) {
  129. this.watcher.collectTimeInfoEntries(
  130. fileTimeInfoEntries,
  131. contextTimeInfoEntries
  132. );
  133. }
  134. return { fileTimeInfoEntries, contextTimeInfoEntries };
  135. };
  136. const directoriesSet =
  137. directories instanceof Set ? directories : new Set(directories);
  138. // Watchpack reports a watched directory (a context dependency) in
  139. // `changes` whenever its contents change, alongside the individual
  140. // file events. The default `fs.purge(dir)` matches cache keys by
  141. // prefix, so it would wipe the stat cache of every file inside the
  142. // directory even though only file-level events actually invalidate
  143. // file stats. For directories we explicitly watch, purge only the
  144. // directory's own entry (`{ exact: true }`, enhanced-resolve >=
  145. // 5.22.0); file-level events in the same aggregated batch still
  146. // handle file stats and the parent readdir invalidation.
  147. /**
  148. * @param {Changes | null | undefined} changes changes set
  149. * @param {Removals | null | undefined} removals removals set
  150. */
  151. const purgeChanges = (changes, removals) => {
  152. const fs = this.inputFileSystem;
  153. if (!fs || !fs.purge) return;
  154. if (changes) {
  155. for (const item of changes) {
  156. if (directoriesSet.has(item)) {
  157. fs.purge(item, { exact: true });
  158. } else {
  159. fs.purge(item);
  160. }
  161. }
  162. }
  163. if (removals) {
  164. for (const item of removals) {
  165. fs.purge(item);
  166. }
  167. }
  168. };
  169. this.watcher.once(
  170. "aggregated",
  171. /**
  172. * Handles the callback logic for this hook.
  173. * @param {Changes} changes changes
  174. * @param {Removals} removals removals
  175. */
  176. (changes, removals) => {
  177. // pause emitting events (avoids clearing aggregated changes and removals on timeout)
  178. /** @type {Watchpack} */
  179. (this.watcher).pause();
  180. purgeChanges(changes, removals);
  181. const { fileTimeInfoEntries, contextTimeInfoEntries } = fetchTimeInfo();
  182. callback(
  183. null,
  184. fileTimeInfoEntries,
  185. contextTimeInfoEntries,
  186. changes,
  187. removals
  188. );
  189. }
  190. );
  191. this.watcher.watch({ files, directories, missing, startTime });
  192. if (oldWatcher) {
  193. oldWatcher.close();
  194. }
  195. return {
  196. close: () => {
  197. if (this.watcher) {
  198. this.watcher.close();
  199. this.watcher = null;
  200. }
  201. },
  202. pause: () => {
  203. if (this.watcher) {
  204. this.watcher.pause();
  205. }
  206. },
  207. getAggregatedRemovals: util.deprecate(
  208. () => {
  209. const items = this.watcher && this.watcher.aggregatedRemovals;
  210. const fs = this.inputFileSystem;
  211. if (items && fs && fs.purge) {
  212. for (const item of items) {
  213. fs.purge(item);
  214. }
  215. }
  216. return items;
  217. },
  218. "Watcher.getAggregatedRemovals is deprecated in favor of Watcher.getInfo since that's more performant.",
  219. "DEP_WEBPACK_WATCHER_GET_AGGREGATED_REMOVALS"
  220. ),
  221. getAggregatedChanges: util.deprecate(
  222. () => {
  223. const items = this.watcher && this.watcher.aggregatedChanges;
  224. const fs = this.inputFileSystem;
  225. if (items && fs && fs.purge) {
  226. for (const item of items) {
  227. fs.purge(item);
  228. }
  229. }
  230. return items;
  231. },
  232. "Watcher.getAggregatedChanges is deprecated in favor of Watcher.getInfo since that's more performant.",
  233. "DEP_WEBPACK_WATCHER_GET_AGGREGATED_CHANGES"
  234. ),
  235. getFileTimeInfoEntries: util.deprecate(
  236. () => fetchTimeInfo().fileTimeInfoEntries,
  237. "Watcher.getFileTimeInfoEntries is deprecated in favor of Watcher.getInfo since that's more performant.",
  238. "DEP_WEBPACK_WATCHER_FILE_TIME_INFO_ENTRIES"
  239. ),
  240. getContextTimeInfoEntries: util.deprecate(
  241. () => fetchTimeInfo().contextTimeInfoEntries,
  242. "Watcher.getContextTimeInfoEntries is deprecated in favor of Watcher.getInfo since that's more performant.",
  243. "DEP_WEBPACK_WATCHER_CONTEXT_TIME_INFO_ENTRIES"
  244. ),
  245. getInfo: () => {
  246. const removals = this.watcher && this.watcher.aggregatedRemovals;
  247. const changes = this.watcher && this.watcher.aggregatedChanges;
  248. purgeChanges(changes, removals);
  249. const { fileTimeInfoEntries, contextTimeInfoEntries } = fetchTimeInfo();
  250. return {
  251. changes,
  252. removals,
  253. fileTimeInfoEntries,
  254. contextTimeInfoEntries
  255. };
  256. }
  257. };
  258. }
  259. }
  260. module.exports = NodeWatchFileSystem;