watchpack.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const getWatcherManager = require("./getWatcherManager");
  7. const LinkResolver = require("./LinkResolver");
  8. const EventEmitter = require("events").EventEmitter;
  9. const globToRegExp = require("glob-to-regexp");
  10. const watchEventSource = require("./watchEventSource");
  11. const EMPTY_ARRAY = [];
  12. const EMPTY_OPTIONS = {};
  13. function addWatchersToSet(watchers, set) {
  14. for (const ww of watchers) {
  15. const w = ww.watcher;
  16. if (!set.has(w.directoryWatcher)) {
  17. set.add(w.directoryWatcher);
  18. }
  19. }
  20. }
  21. const stringToRegexp = ignored => {
  22. const source = globToRegExp(ignored, { globstar: true, extended: true })
  23. .source;
  24. const matchingStart = source.slice(0, source.length - 1) + "(?:$|\\/)";
  25. return matchingStart;
  26. };
  27. const ignoredToFunction = ignored => {
  28. if (Array.isArray(ignored)) {
  29. const regexp = new RegExp(ignored.map(i => stringToRegexp(i)).join("|"));
  30. return x => regexp.test(x.replace(/\\/g, "/"));
  31. } else if (typeof ignored === "string") {
  32. const regexp = new RegExp(stringToRegexp(ignored));
  33. return x => regexp.test(x.replace(/\\/g, "/"));
  34. } else if (ignored instanceof RegExp) {
  35. return x => ignored.test(x.replace(/\\/g, "/"));
  36. } else if (ignored instanceof Function) {
  37. return ignored;
  38. } else if (ignored) {
  39. throw new Error(`Invalid option for 'ignored': ${ignored}`);
  40. } else {
  41. return () => false;
  42. }
  43. };
  44. const normalizeOptions = options => {
  45. return {
  46. followSymlinks: !!options.followSymlinks,
  47. ignored: ignoredToFunction(options.ignored),
  48. poll: options.poll
  49. };
  50. };
  51. const normalizeCache = new WeakMap();
  52. const cachedNormalizeOptions = options => {
  53. const cacheEntry = normalizeCache.get(options);
  54. if (cacheEntry !== undefined) return cacheEntry;
  55. const normalized = normalizeOptions(options);
  56. normalizeCache.set(options, normalized);
  57. return normalized;
  58. };
  59. class WatchpackFileWatcher {
  60. constructor(watchpack, watcher, files) {
  61. this.files = Array.isArray(files) ? files : [files];
  62. this.watcher = watcher;
  63. watcher.on("initial-missing", type => {
  64. for (const file of this.files) {
  65. if (!watchpack._missing.has(file))
  66. watchpack._onRemove(file, file, type);
  67. }
  68. });
  69. watcher.on("change", (mtime, type) => {
  70. for (const file of this.files) {
  71. watchpack._onChange(file, mtime, file, type);
  72. }
  73. });
  74. watcher.on("remove", type => {
  75. for (const file of this.files) {
  76. watchpack._onRemove(file, file, type);
  77. }
  78. });
  79. }
  80. update(files) {
  81. if (!Array.isArray(files)) {
  82. if (this.files.length !== 1) {
  83. this.files = [files];
  84. } else if (this.files[0] !== files) {
  85. this.files[0] = files;
  86. }
  87. } else {
  88. this.files = files;
  89. }
  90. }
  91. close() {
  92. this.watcher.close();
  93. }
  94. }
  95. class WatchpackDirectoryWatcher {
  96. constructor(watchpack, watcher, directories) {
  97. this.directories = Array.isArray(directories) ? directories : [directories];
  98. this.watcher = watcher;
  99. watcher.on("initial-missing", type => {
  100. for (const item of this.directories) {
  101. watchpack._onRemove(item, item, type);
  102. }
  103. });
  104. watcher.on("change", (file, mtime, type) => {
  105. for (const item of this.directories) {
  106. watchpack._onChange(item, mtime, file, type);
  107. }
  108. });
  109. watcher.on("remove", type => {
  110. for (const item of this.directories) {
  111. watchpack._onRemove(item, item, type);
  112. }
  113. });
  114. }
  115. update(directories) {
  116. if (!Array.isArray(directories)) {
  117. if (this.directories.length !== 1) {
  118. this.directories = [directories];
  119. } else if (this.directories[0] !== directories) {
  120. this.directories[0] = directories;
  121. }
  122. } else {
  123. this.directories = directories;
  124. }
  125. }
  126. close() {
  127. this.watcher.close();
  128. }
  129. }
  130. class Watchpack extends EventEmitter {
  131. constructor(options) {
  132. super();
  133. if (!options) options = EMPTY_OPTIONS;
  134. this.options = options;
  135. this.aggregateTimeout =
  136. typeof options.aggregateTimeout === "number"
  137. ? options.aggregateTimeout
  138. : 200;
  139. this.watcherOptions = cachedNormalizeOptions(options);
  140. this.watcherManager = getWatcherManager(this.watcherOptions);
  141. this.fileWatchers = new Map();
  142. this.directoryWatchers = new Map();
  143. this._missing = new Set();
  144. this.startTime = undefined;
  145. this.paused = false;
  146. this.aggregatedChanges = new Set();
  147. this.aggregatedRemovals = new Set();
  148. this.aggregateTimer = undefined;
  149. this._onTimeout = this._onTimeout.bind(this);
  150. }
  151. watch(arg1, arg2, arg3) {
  152. let files, directories, missing, startTime;
  153. if (!arg2) {
  154. ({
  155. files = EMPTY_ARRAY,
  156. directories = EMPTY_ARRAY,
  157. missing = EMPTY_ARRAY,
  158. startTime
  159. } = arg1);
  160. } else {
  161. files = arg1;
  162. directories = arg2;
  163. missing = EMPTY_ARRAY;
  164. startTime = arg3;
  165. }
  166. this.paused = false;
  167. const fileWatchers = this.fileWatchers;
  168. const directoryWatchers = this.directoryWatchers;
  169. const ignored = this.watcherOptions.ignored;
  170. const filter = path => !ignored(path);
  171. const addToMap = (map, key, item) => {
  172. const list = map.get(key);
  173. if (list === undefined) {
  174. map.set(key, item);
  175. } else if (Array.isArray(list)) {
  176. list.push(item);
  177. } else {
  178. map.set(key, [list, item]);
  179. }
  180. };
  181. const fileWatchersNeeded = new Map();
  182. const directoryWatchersNeeded = new Map();
  183. const missingFiles = new Set();
  184. if (this.watcherOptions.followSymlinks) {
  185. const resolver = new LinkResolver();
  186. for (const file of files) {
  187. if (filter(file)) {
  188. for (const innerFile of resolver.resolve(file)) {
  189. if (file === innerFile || filter(innerFile)) {
  190. addToMap(fileWatchersNeeded, innerFile, file);
  191. }
  192. }
  193. }
  194. }
  195. for (const file of missing) {
  196. if (filter(file)) {
  197. for (const innerFile of resolver.resolve(file)) {
  198. if (file === innerFile || filter(innerFile)) {
  199. missingFiles.add(file);
  200. addToMap(fileWatchersNeeded, innerFile, file);
  201. }
  202. }
  203. }
  204. }
  205. for (const dir of directories) {
  206. if (filter(dir)) {
  207. let first = true;
  208. for (const innerItem of resolver.resolve(dir)) {
  209. if (filter(innerItem)) {
  210. addToMap(
  211. first ? directoryWatchersNeeded : fileWatchersNeeded,
  212. innerItem,
  213. dir
  214. );
  215. }
  216. first = false;
  217. }
  218. }
  219. }
  220. } else {
  221. for (const file of files) {
  222. if (filter(file)) {
  223. addToMap(fileWatchersNeeded, file, file);
  224. }
  225. }
  226. for (const file of missing) {
  227. if (filter(file)) {
  228. missingFiles.add(file);
  229. addToMap(fileWatchersNeeded, file, file);
  230. }
  231. }
  232. for (const dir of directories) {
  233. if (filter(dir)) {
  234. addToMap(directoryWatchersNeeded, dir, dir);
  235. }
  236. }
  237. }
  238. // Close unneeded old watchers
  239. // and update existing watchers
  240. for (const [key, w] of fileWatchers) {
  241. const needed = fileWatchersNeeded.get(key);
  242. if (needed === undefined) {
  243. w.close();
  244. fileWatchers.delete(key);
  245. } else {
  246. w.update(needed);
  247. fileWatchersNeeded.delete(key);
  248. }
  249. }
  250. for (const [key, w] of directoryWatchers) {
  251. const needed = directoryWatchersNeeded.get(key);
  252. if (needed === undefined) {
  253. w.close();
  254. directoryWatchers.delete(key);
  255. } else {
  256. w.update(needed);
  257. directoryWatchersNeeded.delete(key);
  258. }
  259. }
  260. // Create new watchers and install handlers on these watchers
  261. watchEventSource.batch(() => {
  262. for (const [key, files] of fileWatchersNeeded) {
  263. const watcher = this.watcherManager.watchFile(key, startTime);
  264. if (watcher) {
  265. fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files));
  266. }
  267. }
  268. for (const [key, directories] of directoryWatchersNeeded) {
  269. const watcher = this.watcherManager.watchDirectory(key, startTime);
  270. if (watcher) {
  271. directoryWatchers.set(
  272. key,
  273. new WatchpackDirectoryWatcher(this, watcher, directories)
  274. );
  275. }
  276. }
  277. });
  278. this._missing = missingFiles;
  279. this.startTime = startTime;
  280. }
  281. close() {
  282. this.paused = true;
  283. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  284. for (const w of this.fileWatchers.values()) w.close();
  285. for (const w of this.directoryWatchers.values()) w.close();
  286. this.fileWatchers.clear();
  287. this.directoryWatchers.clear();
  288. }
  289. pause() {
  290. this.paused = true;
  291. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  292. }
  293. getTimes() {
  294. const directoryWatchers = new Set();
  295. addWatchersToSet(this.fileWatchers.values(), directoryWatchers);
  296. addWatchersToSet(this.directoryWatchers.values(), directoryWatchers);
  297. const obj = Object.create(null);
  298. for (const w of directoryWatchers) {
  299. const times = w.getTimes();
  300. for (const file of Object.keys(times)) obj[file] = times[file];
  301. }
  302. return obj;
  303. }
  304. getTimeInfoEntries() {
  305. const map = new Map();
  306. this.collectTimeInfoEntries(map, map);
  307. return map;
  308. }
  309. collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
  310. const allWatchers = new Set();
  311. addWatchersToSet(this.fileWatchers.values(), allWatchers);
  312. addWatchersToSet(this.directoryWatchers.values(), allWatchers);
  313. const safeTime = { value: 0 };
  314. for (const w of allWatchers) {
  315. w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps, safeTime);
  316. }
  317. }
  318. getAggregated() {
  319. if (this.aggregateTimer) {
  320. clearTimeout(this.aggregateTimer);
  321. this.aggregateTimer = undefined;
  322. }
  323. const changes = this.aggregatedChanges;
  324. const removals = this.aggregatedRemovals;
  325. this.aggregatedChanges = new Set();
  326. this.aggregatedRemovals = new Set();
  327. return { changes, removals };
  328. }
  329. _onChange(item, mtime, file, type) {
  330. file = file || item;
  331. if (!this.paused) {
  332. this.emit("change", file, mtime, type);
  333. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  334. this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
  335. }
  336. this.aggregatedRemovals.delete(item);
  337. this.aggregatedChanges.add(item);
  338. }
  339. _onRemove(item, file, type) {
  340. file = file || item;
  341. if (!this.paused) {
  342. this.emit("remove", file, type);
  343. if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
  344. this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
  345. }
  346. this.aggregatedChanges.delete(item);
  347. this.aggregatedRemovals.add(item);
  348. }
  349. _onTimeout() {
  350. this.aggregateTimer = undefined;
  351. const changes = this.aggregatedChanges;
  352. const removals = this.aggregatedRemovals;
  353. this.aggregatedChanges = new Set();
  354. this.aggregatedRemovals = new Set();
  355. this.emit("aggregated", changes, removals);
  356. }
  357. }
  358. module.exports = Watchpack;