watchEventSource.js 11 KB

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