watchpack.js 10 KB

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