watchEventSource.js 12 KB

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