index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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 globToRegExp = require("glob-to-regexp");
  8. const LinkResolver = require("./LinkResolver");
  9. const getWatcherManager = require("./getWatcherManager");
  10. const watchEventSource = require("./watchEventSource");
  11. /** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */
  12. /** @typedef {import("./DirectoryWatcher")} DirectoryWatcher */
  13. /** @typedef {import("./DirectoryWatcher").DirectoryWatcherEvents} DirectoryWatcherEvents */
  14. /** @typedef {import("./DirectoryWatcher").FileWatcherEvents} FileWatcherEvents */
  15. // eslint-disable-next-line jsdoc/reject-any-type
  16. /** @typedef {Record<string, (...args: any[]) => any>} EventMap */
  17. /**
  18. * @template {EventMap} T
  19. * @typedef {import("./DirectoryWatcher").Watcher<T>} Watcher
  20. */
  21. /** @typedef {(item: string) => boolean} IgnoredFunction */
  22. /** @typedef {string[] | RegExp | string | IgnoredFunction} Ignored */
  23. /**
  24. * @typedef {object} WatcherOptions
  25. * @property {boolean=} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false
  26. * @property {Ignored=} ignored ignore some files from watching (glob pattern or regexp)
  27. * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false
  28. */
  29. /** @typedef {WatcherOptions & { aggregateTimeout?: number }} WatchOptions */
  30. /**
  31. * @typedef {object} NormalizedWatchOptions
  32. * @property {boolean} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false
  33. * @property {IgnoredFunction} ignored ignore some files from watching (glob pattern or regexp)
  34. * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false
  35. */
  36. /** @typedef {`scan (${string})` | "change" | "rename" | `watch ${string}` | `directory-removed ${string}`} EventType */
  37. /** @typedef {{ safeTime: number, timestamp: number, accuracy: number }} Entry */
  38. /** @typedef {{ safeTime: number }} OnlySafeTimeEntry */
  39. // eslint-disable-next-line jsdoc/ts-no-empty-object-type
  40. /** @typedef {{}} ExistenceOnlyTimeEntry */
  41. /** @typedef {Map<string, Entry | OnlySafeTimeEntry | ExistenceOnlyTimeEntry | null>} TimeInfoEntries */
  42. /** @typedef {Set<string>} Changes */
  43. /** @typedef {Set<string>} Removals */
  44. /** @typedef {{ changes: Changes, removals: Removals }} Aggregated */
  45. /** @typedef {{ files?: Iterable<string>, directories?: Iterable<string>, missing?: Iterable<string>, startTime?: number }} WatchMethodOptions */
  46. /** @typedef {Record<string, number>} Times */
  47. /**
  48. * @param {MapIterator<WatchpackFileWatcher> | MapIterator<WatchpackDirectoryWatcher>} watchers watchers
  49. * @param {Set<DirectoryWatcher>} set set
  50. */
  51. function addWatchersToSet(watchers, set) {
  52. for (const ww of watchers) {
  53. const w = ww.watcher;
  54. if (!set.has(w.directoryWatcher)) {
  55. set.add(w.directoryWatcher);
  56. }
  57. }
  58. }
  59. /**
  60. * @param {string} ignored ignored
  61. * @returns {string | undefined} resolved global to regexp
  62. */
  63. const stringToRegexp = (ignored) => {
  64. if (ignored.length === 0) {
  65. return;
  66. }
  67. const { source } = globToRegExp(ignored, { globstar: true, extended: true });
  68. return `${source.slice(0, -1)}(?:$|\\/)`;
  69. };
  70. /**
  71. * @param {Ignored=} ignored ignored
  72. * @returns {(item: string) => boolean} ignored to function
  73. */
  74. const ignoredToFunction = (ignored) => {
  75. if (Array.isArray(ignored)) {
  76. const stringRegexps = ignored.map((i) => stringToRegexp(i)).filter(Boolean);
  77. if (stringRegexps.length === 0) {
  78. return () => false;
  79. }
  80. const regexp = new RegExp(stringRegexps.join("|"));
  81. return (item) => regexp.test(item.replace(/\\/g, "/"));
  82. } else if (typeof ignored === "string") {
  83. const stringRegexp = stringToRegexp(ignored);
  84. if (!stringRegexp) {
  85. return () => false;
  86. }
  87. const regexp = new RegExp(stringRegexp);
  88. return (item) => regexp.test(item.replace(/\\/g, "/"));
  89. } else if (ignored instanceof RegExp) {
  90. return (item) => ignored.test(item.replace(/\\/g, "/"));
  91. } else if (typeof ignored === "function") {
  92. return ignored;
  93. } else if (ignored) {
  94. throw new Error(`Invalid option for 'ignored': ${ignored}`);
  95. } else {
  96. return () => false;
  97. }
  98. };
  99. /**
  100. * @param {WatchOptions} options options
  101. * @returns {NormalizedWatchOptions} normalized options
  102. */
  103. const normalizeOptions = (options) => ({
  104. followSymlinks: Boolean(options.followSymlinks),
  105. ignored: ignoredToFunction(options.ignored),
  106. poll: options.poll,
  107. });
  108. const normalizeCache = new WeakMap();
  109. /**
  110. * @param {WatchOptions} options options
  111. * @returns {NormalizedWatchOptions} normalized options
  112. */
  113. const cachedNormalizeOptions = (options) => {
  114. const cacheEntry = normalizeCache.get(options);
  115. if (cacheEntry !== undefined) return cacheEntry;
  116. const normalized = normalizeOptions(options);
  117. normalizeCache.set(options, normalized);
  118. return normalized;
  119. };
  120. class WatchpackFileWatcher {
  121. /**
  122. * @param {Watchpack} watchpack watchpack
  123. * @param {Watcher<FileWatcherEvents>} watcher watcher
  124. * @param {string | string[]} files files
  125. */
  126. constructor(watchpack, watcher, files) {
  127. /** @type {string[]} */
  128. this.files = Array.isArray(files) ? files : [files];
  129. this.watcher = watcher;
  130. watcher.on("initial-missing", (type) => {
  131. for (const file of this.files) {
  132. if (!watchpack._missing.has(file)) {
  133. watchpack._onRemove(file, file, type);
  134. }
  135. }
  136. });
  137. watcher.on("change", (mtime, type, _initial) => {
  138. for (const file of this.files) {
  139. watchpack._onChange(file, mtime, file, type);
  140. }
  141. });
  142. watcher.on("remove", (type) => {
  143. for (const file of this.files) {
  144. watchpack._onRemove(file, file, type);
  145. }
  146. });
  147. }
  148. /**
  149. * @param {string | string[]} files files
  150. */
  151. update(files) {
  152. if (!Array.isArray(files)) {
  153. if (this.files.length !== 1) {
  154. this.files = [files];
  155. } else if (this.files[0] !== files) {
  156. this.files[0] = files;
  157. }
  158. } else {
  159. this.files = files;
  160. }
  161. }
  162. close() {
  163. this.watcher.close();
  164. }
  165. }
  166. class WatchpackDirectoryWatcher {
  167. /**
  168. * @param {Watchpack} watchpack watchpack
  169. * @param {Watcher<DirectoryWatcherEvents>} watcher watcher
  170. * @param {string} directories directories
  171. */
  172. constructor(watchpack, watcher, directories) {
  173. /** @type {string[]} */
  174. this.directories = Array.isArray(directories) ? directories : [directories];
  175. this.watcher = watcher;
  176. watcher.on("initial-missing", (type) => {
  177. for (const item of this.directories) {
  178. watchpack._onRemove(item, item, type);
  179. }
  180. });
  181. watcher.on("change", (file, mtime, type, _initial) => {
  182. for (const item of this.directories) {
  183. watchpack._onChange(item, mtime, file, type);
  184. }
  185. });
  186. watcher.on("remove", (type) => {
  187. for (const item of this.directories) {
  188. watchpack._onRemove(item, item, type);
  189. }
  190. });
  191. }
  192. /**
  193. * @param {string | string[]} directories directories
  194. */
  195. update(directories) {
  196. if (!Array.isArray(directories)) {
  197. if (this.directories.length !== 1) {
  198. this.directories = [directories];
  199. } else if (this.directories[0] !== directories) {
  200. this.directories[0] = directories;
  201. }
  202. } else {
  203. this.directories = directories;
  204. }
  205. }
  206. close() {
  207. this.watcher.close();
  208. }
  209. }
  210. /**
  211. * @typedef {object} WatchpackEvents
  212. * @property {(file: string, mtime: number, type: EventType) => void} change change event
  213. * @property {(file: string, type: EventType) => void} remove remove event
  214. * @property {(changes: Changes, removals: Removals) => void} aggregated aggregated event
  215. */
  216. /**
  217. * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters<WatchpackEvents[K]> }>}
  218. */
  219. class Watchpack extends EventEmitter {
  220. /**
  221. * @param {WatchOptions=} options options
  222. */
  223. constructor(options = {}) {
  224. super();
  225. if (!options) options = {};
  226. /** @type {WatchOptions} */
  227. this.options = options;
  228. this.aggregateTimeout =
  229. typeof options.aggregateTimeout === "number"
  230. ? options.aggregateTimeout
  231. : 200;
  232. /** @type {NormalizedWatchOptions} */
  233. this.watcherOptions = cachedNormalizeOptions(options);
  234. /** @type {WatcherManager} */
  235. this.watcherManager = getWatcherManager(this.watcherOptions);
  236. /** @type {Map<string, WatchpackFileWatcher>} */
  237. this.fileWatchers = new Map();
  238. /** @type {Map<string, WatchpackDirectoryWatcher>} */
  239. this.directoryWatchers = new Map();
  240. /** @type {Set<string>} */
  241. this._missing = new Set();
  242. this.startTime = undefined;
  243. this.paused = false;
  244. /** @type {Changes} */
  245. this.aggregatedChanges = new Set();
  246. /** @type {Removals} */
  247. this.aggregatedRemovals = new Set();
  248. /** @type {undefined | NodeJS.Timeout} */
  249. this.aggregateTimer = undefined;
  250. this._onTimeout = this._onTimeout.bind(this);
  251. }
  252. /**
  253. * @overload
  254. * @param {Iterable<string>} arg1 files
  255. * @param {Iterable<string>} arg2 directories
  256. * @param {number=} arg3 startTime
  257. * @returns {void}
  258. */
  259. /**
  260. * @overload
  261. * @param {WatchMethodOptions} arg1 watch options
  262. * @returns {void}
  263. */
  264. /**
  265. * @param {Iterable<string> | WatchMethodOptions} arg1 files
  266. * @param {Iterable<string>=} arg2 directories
  267. * @param {number=} arg3 startTime
  268. * @returns {void}
  269. */
  270. watch(arg1, arg2, arg3) {
  271. /** @type {Iterable<string> | undefined} */
  272. let files;
  273. /** @type {Iterable<string> | undefined} */
  274. let directories;
  275. /** @type {Iterable<string> | undefined} */
  276. let missing;
  277. /** @type {number | undefined} */
  278. let startTime;
  279. if (!arg2) {
  280. ({
  281. files = [],
  282. directories = [],
  283. missing = [],
  284. startTime,
  285. } = /** @type {WatchMethodOptions} */ (arg1));
  286. } else {
  287. files = /** @type {Iterable<string>} */ (arg1);
  288. directories = /** @type {Iterable<string>} */ (arg2);
  289. missing = [];
  290. startTime = /** @type {number} */ (arg3);
  291. }
  292. this.paused = false;
  293. const { fileWatchers, directoryWatchers } = this;
  294. const { ignored } = this.watcherOptions;
  295. /**
  296. * @param {string} path path
  297. * @returns {boolean} true when need to filter, otherwise false
  298. */
  299. const filter = (path) => !ignored(path);
  300. /**
  301. * @template K, V
  302. * @param {Map<K, V | V[]>} map map
  303. * @param {K} key key
  304. * @param {V} item item
  305. */
  306. const addToMap = (map, key, item) => {
  307. const list = map.get(key);
  308. if (list === undefined) {
  309. map.set(key, item);
  310. } else if (Array.isArray(list)) {
  311. list.push(item);
  312. } else {
  313. map.set(key, [list, item]);
  314. }
  315. };
  316. const fileWatchersNeeded = new Map();
  317. const directoryWatchersNeeded = new Map();
  318. /** @type {Set<string>} */
  319. const missingFiles = new Set();
  320. if (this.watcherOptions.followSymlinks) {
  321. const resolver = new LinkResolver();
  322. for (const file of files) {
  323. if (filter(file)) {
  324. for (const innerFile of resolver.resolve(file)) {
  325. if (file === innerFile || filter(innerFile)) {
  326. addToMap(fileWatchersNeeded, innerFile, file);
  327. }
  328. }
  329. }
  330. }
  331. for (const file of missing) {
  332. if (filter(file)) {
  333. for (const innerFile of resolver.resolve(file)) {
  334. if (file === innerFile || filter(innerFile)) {
  335. missingFiles.add(file);
  336. addToMap(fileWatchersNeeded, innerFile, file);
  337. }
  338. }
  339. }
  340. }
  341. for (const dir of directories) {
  342. if (filter(dir)) {
  343. let first = true;
  344. for (const innerItem of resolver.resolve(dir)) {
  345. if (filter(innerItem)) {
  346. addToMap(
  347. first ? directoryWatchersNeeded : fileWatchersNeeded,
  348. innerItem,
  349. dir,
  350. );
  351. }
  352. first = false;
  353. }
  354. }
  355. }
  356. } else {
  357. for (const file of files) {
  358. if (filter(file)) {
  359. addToMap(fileWatchersNeeded, file, file);
  360. }
  361. }
  362. for (const file of missing) {
  363. if (filter(file)) {
  364. missingFiles.add(file);
  365. addToMap(fileWatchersNeeded, file, file);
  366. }
  367. }
  368. for (const dir of directories) {
  369. if (filter(dir)) {
  370. addToMap(directoryWatchersNeeded, dir, dir);
  371. }
  372. }
  373. }
  374. // Close unneeded old watchers
  375. // and update existing watchers
  376. for (const [key, w] of fileWatchers) {
  377. const needed = fileWatchersNeeded.get(key);
  378. if (needed === undefined) {
  379. w.close();
  380. fileWatchers.delete(key);
  381. } else {
  382. w.update(needed);
  383. fileWatchersNeeded.delete(key);
  384. }
  385. }
  386. for (const [key, w] of directoryWatchers) {
  387. const needed = directoryWatchersNeeded.get(key);
  388. if (needed === undefined) {
  389. w.close();
  390. directoryWatchers.delete(key);
  391. } else {
  392. w.update(needed);
  393. directoryWatchersNeeded.delete(key);
  394. }
  395. }
  396. // Create new watchers and install handlers on these watchers
  397. watchEventSource.batch(() => {
  398. for (const [key, files] of fileWatchersNeeded) {
  399. const watcher = this.watcherManager.watchFile(key, startTime);
  400. if (watcher) {
  401. fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files));
  402. }
  403. }
  404. for (const [key, directories] of directoryWatchersNeeded) {
  405. const watcher = this.watcherManager.watchDirectory(key, startTime);
  406. if (watcher) {
  407. directoryWatchers.set(
  408. key,
  409. new WatchpackDirectoryWatcher(this, watcher, directories),
  410. );
  411. }
  412. }
  413. });
  414. this._missing = missingFiles;
  415. this.startTime = startTime;
  416. }
  417. close() {
  418. this.paused = true;
  419. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  420. for (const w of this.fileWatchers.values()) w.close();
  421. for (const w of this.directoryWatchers.values()) w.close();
  422. this.fileWatchers.clear();
  423. this.directoryWatchers.clear();
  424. }
  425. pause() {
  426. this.paused = true;
  427. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  428. }
  429. /**
  430. * @returns {Record<string, number>} times
  431. */
  432. getTimes() {
  433. /** @type {Set<DirectoryWatcher>} */
  434. const directoryWatchers = new Set();
  435. addWatchersToSet(this.fileWatchers.values(), directoryWatchers);
  436. addWatchersToSet(this.directoryWatchers.values(), directoryWatchers);
  437. /** @type {Record<string, number>} */
  438. const obj = Object.create(null);
  439. for (const w of directoryWatchers) {
  440. const times = w.getTimes();
  441. for (const file of Object.keys(times)) obj[file] = times[file];
  442. }
  443. return obj;
  444. }
  445. /**
  446. * @returns {TimeInfoEntries} time info entries
  447. */
  448. getTimeInfoEntries() {
  449. /** @type {TimeInfoEntries} */
  450. const map = new Map();
  451. this.collectTimeInfoEntries(map, map);
  452. return map;
  453. }
  454. /**
  455. * @param {TimeInfoEntries} fileTimestamps file timestamps
  456. * @param {TimeInfoEntries} directoryTimestamps directory timestamps
  457. */
  458. collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
  459. /** @type {Set<DirectoryWatcher>} */
  460. const allWatchers = new Set();
  461. addWatchersToSet(this.fileWatchers.values(), allWatchers);
  462. addWatchersToSet(this.directoryWatchers.values(), allWatchers);
  463. for (const w of allWatchers) {
  464. w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps);
  465. }
  466. }
  467. /**
  468. * @returns {Aggregated} aggregated info
  469. */
  470. getAggregated() {
  471. if (this.aggregateTimer) {
  472. clearTimeout(this.aggregateTimer);
  473. this.aggregateTimer = undefined;
  474. }
  475. const changes = this.aggregatedChanges;
  476. const removals = this.aggregatedRemovals;
  477. this.aggregatedChanges = new Set();
  478. this.aggregatedRemovals = new Set();
  479. return { changes, removals };
  480. }
  481. /**
  482. * @param {string} item item
  483. * @param {number} mtime mtime
  484. * @param {string} file file
  485. * @param {EventType} type type
  486. */
  487. _onChange(item, mtime, file, type) {
  488. file = file || item;
  489. if (!this.paused) {
  490. this.emit("change", file, mtime, type);
  491. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  492. this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
  493. }
  494. this.aggregatedRemovals.delete(item);
  495. this.aggregatedChanges.add(item);
  496. }
  497. /**
  498. * @param {string} item item
  499. * @param {string} file file
  500. * @param {EventType} type type
  501. */
  502. _onRemove(item, file, type) {
  503. file = file || item;
  504. if (!this.paused) {
  505. this.emit("remove", file, type);
  506. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  507. this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
  508. }
  509. this.aggregatedChanges.delete(item);
  510. this.aggregatedRemovals.add(item);
  511. }
  512. _onTimeout() {
  513. this.aggregateTimer = undefined;
  514. const changes = this.aggregatedChanges;
  515. const removals = this.aggregatedRemovals;
  516. this.aggregatedChanges = new Set();
  517. this.aggregatedRemovals = new Set();
  518. this.emit("aggregated", changes, removals);
  519. }
  520. }
  521. module.exports = Watchpack;