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