watchEventSource.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const fs = require("fs");
  7. const path = require("path");
  8. const { EventEmitter } = require("events");
  9. const reducePlan = require("./reducePlan");
  10. const IS_OSX = require("os").platform() === "darwin";
  11. const IS_WIN = require("os").platform() === "win32";
  12. const SUPPORTS_RECURSIVE_WATCHING = IS_OSX || IS_WIN;
  13. // Use 20 for OSX to make `FSWatcher.close` faster
  14. // https://github.com/nodejs/node/issues/29949
  15. const watcherLimit =
  16. +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 20 : 10000);
  17. const recursiveWatcherLogging = !!process.env
  18. .WATCHPACK_RECURSIVE_WATCHER_LOGGING;
  19. let isBatch = false;
  20. let watcherCount = 0;
  21. /** @type {Map<Watcher, string>} */
  22. const pendingWatchers = new Map();
  23. /** @type {Map<string, RecursiveWatcher>} */
  24. const recursiveWatchers = new Map();
  25. /** @type {Map<string, DirectWatcher>} */
  26. const directWatchers = new Map();
  27. /** @type {Map<Watcher, RecursiveWatcher | DirectWatcher>} */
  28. const underlyingWatcher = new Map();
  29. function createEPERMError(filePath) {
  30. const error = new Error(`Operation not permitted: ${filePath}`);
  31. error.code = "EPERM";
  32. return error;
  33. }
  34. function createHandleChangeEvent(watcher, filePath, handleChangeEvent) {
  35. return (type, filename) => {
  36. // TODO: After Node.js v22, fs.watch(dir) and deleting a dir will trigger the rename change event.
  37. // Here we just ignore it and keep the same behavior as before v22
  38. // https://github.com/libuv/libuv/pull/4376
  39. if (
  40. type === "rename" &&
  41. path.isAbsolute(filename) &&
  42. path.basename(filename) === path.basename(filePath)
  43. ) {
  44. if (!IS_OSX) {
  45. // Before v22, windows will throw EPERM error
  46. watcher.emit("error", createEPERMError(filename));
  47. }
  48. // Before v22, macos nothing to do
  49. return;
  50. }
  51. handleChangeEvent(type, filename);
  52. };
  53. }
  54. class DirectWatcher {
  55. constructor(filePath) {
  56. this.filePath = filePath;
  57. this.watchers = new Set();
  58. this.watcher = undefined;
  59. try {
  60. const watcher = fs.watch(filePath);
  61. this.watcher = watcher;
  62. const handleChangeEvent = createHandleChangeEvent(
  63. watcher,
  64. filePath,
  65. (type, filename) => {
  66. for (const w of this.watchers) {
  67. w.emit("change", type, filename);
  68. }
  69. }
  70. );
  71. watcher.on("change", handleChangeEvent);
  72. watcher.on("error", error => {
  73. for (const w of this.watchers) {
  74. w.emit("error", error);
  75. }
  76. });
  77. } catch (err) {
  78. process.nextTick(() => {
  79. for (const w of this.watchers) {
  80. w.emit("error", err);
  81. }
  82. });
  83. }
  84. watcherCount++;
  85. }
  86. add(watcher) {
  87. underlyingWatcher.set(watcher, this);
  88. this.watchers.add(watcher);
  89. }
  90. remove(watcher) {
  91. this.watchers.delete(watcher);
  92. if (this.watchers.size === 0) {
  93. directWatchers.delete(this.filePath);
  94. watcherCount--;
  95. if (this.watcher) this.watcher.close();
  96. }
  97. }
  98. getWatchers() {
  99. return this.watchers;
  100. }
  101. }
  102. class RecursiveWatcher {
  103. constructor(rootPath) {
  104. this.rootPath = rootPath;
  105. /** @type {Map<Watcher, string>} */
  106. this.mapWatcherToPath = new Map();
  107. /** @type {Map<string, Set<Watcher>>} */
  108. this.mapPathToWatchers = new Map();
  109. this.watcher = undefined;
  110. try {
  111. const watcher = fs.watch(rootPath, {
  112. recursive: true
  113. });
  114. this.watcher = watcher;
  115. watcher.on("change", (type, filename) => {
  116. if (!filename) {
  117. if (recursiveWatcherLogging) {
  118. process.stderr.write(
  119. `[watchpack] dispatch ${type} event in recursive watcher (${this.rootPath}) to all watchers\n`
  120. );
  121. }
  122. for (const w of this.mapWatcherToPath.keys()) {
  123. w.emit("change", type);
  124. }
  125. } else {
  126. const dir = path.dirname(filename);
  127. const watchers = this.mapPathToWatchers.get(dir);
  128. if (recursiveWatcherLogging) {
  129. process.stderr.write(
  130. `[watchpack] dispatch ${type} event in recursive watcher (${
  131. this.rootPath
  132. }) for '${filename}' to ${
  133. watchers ? watchers.size : 0
  134. } watchers\n`
  135. );
  136. }
  137. if (watchers === undefined) return;
  138. for (const w of watchers) {
  139. w.emit("change", type, path.basename(filename));
  140. }
  141. }
  142. });
  143. watcher.on("error", error => {
  144. for (const w of this.mapWatcherToPath.keys()) {
  145. w.emit("error", error);
  146. }
  147. });
  148. } catch (err) {
  149. process.nextTick(() => {
  150. for (const w of this.mapWatcherToPath.keys()) {
  151. w.emit("error", err);
  152. }
  153. });
  154. }
  155. watcherCount++;
  156. if (recursiveWatcherLogging) {
  157. process.stderr.write(
  158. `[watchpack] created recursive watcher at ${rootPath}\n`
  159. );
  160. }
  161. }
  162. add(filePath, watcher) {
  163. underlyingWatcher.set(watcher, this);
  164. const subpath = filePath.slice(this.rootPath.length + 1) || ".";
  165. this.mapWatcherToPath.set(watcher, subpath);
  166. const set = this.mapPathToWatchers.get(subpath);
  167. if (set === undefined) {
  168. const newSet = new Set();
  169. newSet.add(watcher);
  170. this.mapPathToWatchers.set(subpath, newSet);
  171. } else {
  172. set.add(watcher);
  173. }
  174. }
  175. remove(watcher) {
  176. const subpath = this.mapWatcherToPath.get(watcher);
  177. if (!subpath) return;
  178. this.mapWatcherToPath.delete(watcher);
  179. const set = this.mapPathToWatchers.get(subpath);
  180. set.delete(watcher);
  181. if (set.size === 0) {
  182. this.mapPathToWatchers.delete(subpath);
  183. }
  184. if (this.mapWatcherToPath.size === 0) {
  185. recursiveWatchers.delete(this.rootPath);
  186. watcherCount--;
  187. if (this.watcher) this.watcher.close();
  188. if (recursiveWatcherLogging) {
  189. process.stderr.write(
  190. `[watchpack] closed recursive watcher at ${this.rootPath}\n`
  191. );
  192. }
  193. }
  194. }
  195. getWatchers() {
  196. return this.mapWatcherToPath;
  197. }
  198. }
  199. class Watcher extends EventEmitter {
  200. close() {
  201. if (pendingWatchers.has(this)) {
  202. pendingWatchers.delete(this);
  203. return;
  204. }
  205. const watcher = underlyingWatcher.get(this);
  206. watcher.remove(this);
  207. underlyingWatcher.delete(this);
  208. }
  209. }
  210. const createDirectWatcher = filePath => {
  211. const existing = directWatchers.get(filePath);
  212. if (existing !== undefined) return existing;
  213. const w = new DirectWatcher(filePath);
  214. directWatchers.set(filePath, w);
  215. return w;
  216. };
  217. const createRecursiveWatcher = rootPath => {
  218. const existing = recursiveWatchers.get(rootPath);
  219. if (existing !== undefined) return existing;
  220. const w = new RecursiveWatcher(rootPath);
  221. recursiveWatchers.set(rootPath, w);
  222. return w;
  223. };
  224. const execute = () => {
  225. /** @type {Map<string, Watcher[] | Watcher>} */
  226. const map = new Map();
  227. const addWatcher = (watcher, filePath) => {
  228. const entry = map.get(filePath);
  229. if (entry === undefined) {
  230. map.set(filePath, watcher);
  231. } else if (Array.isArray(entry)) {
  232. entry.push(watcher);
  233. } else {
  234. map.set(filePath, [entry, watcher]);
  235. }
  236. };
  237. for (const [watcher, filePath] of pendingWatchers) {
  238. addWatcher(watcher, filePath);
  239. }
  240. pendingWatchers.clear();
  241. // Fast case when we are not reaching the limit
  242. if (!SUPPORTS_RECURSIVE_WATCHING || watcherLimit - watcherCount >= map.size) {
  243. // Create watchers for all entries in the map
  244. for (const [filePath, entry] of map) {
  245. const w = createDirectWatcher(filePath);
  246. if (Array.isArray(entry)) {
  247. for (const item of entry) w.add(item);
  248. } else {
  249. w.add(entry);
  250. }
  251. }
  252. return;
  253. }
  254. // Reconsider existing watchers to improving watch plan
  255. for (const watcher of recursiveWatchers.values()) {
  256. for (const [w, subpath] of watcher.getWatchers()) {
  257. addWatcher(w, path.join(watcher.rootPath, subpath));
  258. }
  259. }
  260. for (const watcher of directWatchers.values()) {
  261. for (const w of watcher.getWatchers()) {
  262. addWatcher(w, watcher.filePath);
  263. }
  264. }
  265. // Merge map entries to keep watcher limit
  266. // Create a 10% buffer to be able to enter fast case more often
  267. const plan = reducePlan(map, watcherLimit * 0.9);
  268. // Update watchers for all entries in the map
  269. for (const [filePath, entry] of plan) {
  270. if (entry.size === 1) {
  271. for (const [watcher, filePath] of entry) {
  272. const w = createDirectWatcher(filePath);
  273. const old = underlyingWatcher.get(watcher);
  274. if (old === w) continue;
  275. w.add(watcher);
  276. if (old !== undefined) old.remove(watcher);
  277. }
  278. } else {
  279. const filePaths = new Set(entry.values());
  280. if (filePaths.size > 1) {
  281. const w = createRecursiveWatcher(filePath);
  282. for (const [watcher, watcherPath] of entry) {
  283. const old = underlyingWatcher.get(watcher);
  284. if (old === w) continue;
  285. w.add(watcherPath, watcher);
  286. if (old !== undefined) old.remove(watcher);
  287. }
  288. } else {
  289. for (const filePath of filePaths) {
  290. const w = createDirectWatcher(filePath);
  291. for (const watcher of entry.keys()) {
  292. const old = underlyingWatcher.get(watcher);
  293. if (old === w) continue;
  294. w.add(watcher);
  295. if (old !== undefined) old.remove(watcher);
  296. }
  297. }
  298. }
  299. }
  300. }
  301. };
  302. exports.watch = filePath => {
  303. const watcher = new Watcher();
  304. // Find an existing watcher
  305. const directWatcher = directWatchers.get(filePath);
  306. if (directWatcher !== undefined) {
  307. directWatcher.add(watcher);
  308. return watcher;
  309. }
  310. let current = filePath;
  311. for (;;) {
  312. const recursiveWatcher = recursiveWatchers.get(current);
  313. if (recursiveWatcher !== undefined) {
  314. recursiveWatcher.add(filePath, watcher);
  315. return watcher;
  316. }
  317. const parent = path.dirname(current);
  318. if (parent === current) break;
  319. current = parent;
  320. }
  321. // Queue up watcher for creation
  322. pendingWatchers.set(watcher, filePath);
  323. if (!isBatch) execute();
  324. return watcher;
  325. };
  326. exports.batch = fn => {
  327. isBatch = true;
  328. try {
  329. fn();
  330. } finally {
  331. isBatch = false;
  332. execute();
  333. }
  334. };
  335. exports.getNumberOfWatchers = () => {
  336. return watcherCount;
  337. };
  338. exports.createHandleChangeEvent = createHandleChangeEvent;
  339. exports.watcherLimit = watcherLimit;