.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / @mrmlnc / readdir-enhanced / lib / directory-reader.js
1 'use strict';
2
3 const Readable = require('stream').Readable;
4 const EventEmitter = require('events').EventEmitter;
5 const path = require('path');
6 const normalizeOptions = require('./normalize-options');
7 const stat = require('./stat');
8 const call = require('./call');
9
10 /**
11  * Asynchronously reads the contents of a directory and streams the results
12  * via a {@link stream.Readable}.
13  */
14 class DirectoryReader {
15   /**
16    * @param {string} dir - The absolute or relative directory path to read
17    * @param {object} [options] - User-specified options, if any (see {@link normalizeOptions})
18    * @param {object} internalOptions - Internal options that aren't part of the public API
19    * @class
20    */
21   constructor (dir, options, internalOptions) {
22     this.options = options = normalizeOptions(options, internalOptions);
23
24     // Indicates whether we should keep reading
25     // This is set false if stream.Readable.push() returns false.
26     this.shouldRead = true;
27
28     // The directories to read
29     // (initialized with the top-level directory)
30     this.queue = [{
31       path: dir,
32       basePath: options.basePath,
33       posixBasePath: options.posixBasePath,
34       depth: 0
35     }];
36
37     // The number of directories that are currently being processed
38     this.pending = 0;
39
40     // The data that has been read, but not yet emitted
41     this.buffer = [];
42
43     this.stream = new Readable({ objectMode: true });
44     this.stream._read = () => {
45       // Start (or resume) reading
46       this.shouldRead = true;
47
48       // If we have data in the buffer, then send the next chunk
49       if (this.buffer.length > 0) {
50         this.pushFromBuffer();
51       }
52
53       // If we have directories queued, then start processing the next one
54       if (this.queue.length > 0) {
55         if (this.options.facade.sync) {
56           while (this.queue.length > 0) {
57             this.readNextDirectory();
58           }
59         }
60         else {
61           this.readNextDirectory();
62         }
63       }
64
65       this.checkForEOF();
66     };
67   }
68
69   /**
70    * Reads the next directory in the queue
71    */
72   readNextDirectory () {
73     let facade = this.options.facade;
74     let dir = this.queue.shift();
75     this.pending++;
76
77     // Read the directory listing
78     call.safe(facade.fs.readdir, dir.path, (err, items) => {
79       if (err) {
80         // fs.readdir threw an error
81         this.emit('error', err);
82         return this.finishedReadingDirectory();
83       }
84
85       try {
86         // Process each item in the directory (simultaneously, if async)
87         facade.forEach(
88           items,
89           this.processItem.bind(this, dir),
90           this.finishedReadingDirectory.bind(this, dir)
91         );
92       }
93       catch (err2) {
94         // facade.forEach threw an error
95         // (probably because fs.readdir returned an invalid result)
96         this.emit('error', err2);
97         this.finishedReadingDirectory();
98       }
99     });
100   }
101
102   /**
103    * This method is called after all items in a directory have been processed.
104    *
105    * NOTE: This does not necessarily mean that the reader is finished, since there may still
106    * be other directories queued or pending.
107    */
108   finishedReadingDirectory () {
109     this.pending--;
110
111     if (this.shouldRead) {
112       // If we have directories queued, then start processing the next one
113       if (this.queue.length > 0 && this.options.facade.async) {
114         this.readNextDirectory();
115       }
116
117       this.checkForEOF();
118     }
119   }
120
121   /**
122    * Determines whether the reader has finished processing all items in all directories.
123    * If so, then the "end" event is fired (via {@Readable#push})
124    */
125   checkForEOF () {
126     if (this.buffer.length === 0 &&   // The stuff we've already read
127     this.pending === 0 &&             // The stuff we're currently reading
128     this.queue.length === 0) {        // The stuff we haven't read yet
129       // There's no more stuff!
130       this.stream.push(null);
131     }
132   }
133
134   /**
135    * Processes a single item in a directory.
136    *
137    * If the item is a directory, and `option.deep` is enabled, then the item will be added
138    * to the directory queue.
139    *
140    * If the item meets the filter criteria, then it will be emitted to the reader's stream.
141    *
142    * @param {object} dir - A directory object from the queue
143    * @param {string} item - The name of the item (name only, no path)
144    * @param {function} done - A callback function that is called after the item has been processed
145    */
146   processItem (dir, item, done) {
147     let stream = this.stream;
148     let options = this.options;
149
150     let itemPath = dir.basePath + item;
151     let posixPath = dir.posixBasePath + item;
152     let fullPath = path.join(dir.path, item);
153
154     // If `options.deep` is a number, and we've already recursed to the max depth,
155     // then there's no need to check fs.Stats to know if it's a directory.
156     // If `options.deep` is a function, then we'll need fs.Stats
157     let maxDepthReached = dir.depth >= options.recurseDepth;
158
159     // Do we need to call `fs.stat`?
160     let needStats =
161       !maxDepthReached ||                                 // we need the fs.Stats to know if it's a directory
162       options.stats ||                                    // the user wants fs.Stats objects returned
163       options.recurseFn ||                                // we need fs.Stats for the recurse function
164       options.filterFn ||                                 // we need fs.Stats for the filter function
165       EventEmitter.listenerCount(stream, 'file') ||       // we need the fs.Stats to know if it's a file
166       EventEmitter.listenerCount(stream, 'directory') ||  // we need the fs.Stats to know if it's a directory
167       EventEmitter.listenerCount(stream, 'symlink');      // we need the fs.Stats to know if it's a symlink
168
169     // If we don't need stats, then exit early
170     if (!needStats) {
171       if (this.filter(itemPath, posixPath)) {
172         this.pushOrBuffer({ data: itemPath });
173       }
174       return done();
175     }
176
177     // Get the fs.Stats object for this path
178     stat(options.facade.fs, fullPath, (err, stats) => {
179       if (err) {
180         // fs.stat threw an error
181         this.emit('error', err);
182         return done();
183       }
184
185       try {
186         // Add the item's path to the fs.Stats object
187         // The base of this path, and its separators are determined by the options
188         // (i.e. options.basePath and options.sep)
189         stats.path = itemPath;
190
191         // Add depth of the path to the fs.Stats object for use this in the filter function
192         stats.depth = dir.depth;
193
194         if (this.shouldRecurse(stats, posixPath, maxDepthReached)) {
195           // Add this subdirectory to the queue
196           this.queue.push({
197             path: fullPath,
198             basePath: itemPath + options.sep,
199             posixBasePath: posixPath + '/',
200             depth: dir.depth + 1,
201           });
202         }
203
204         // Determine whether this item matches the filter criteria
205         if (this.filter(stats, posixPath)) {
206           this.pushOrBuffer({
207             data: options.stats ? stats : itemPath,
208             file: stats.isFile(),
209             directory: stats.isDirectory(),
210             symlink: stats.isSymbolicLink(),
211           });
212         }
213
214         done();
215       }
216       catch (err2) {
217         // An error occurred while processing the item
218         // (probably during a user-specified function, such as options.deep, options.filter, etc.)
219         this.emit('error', err2);
220         done();
221       }
222     });
223   }
224
225   /**
226    * Pushes the given chunk of data to the stream, or adds it to the buffer,
227    * depending on the state of the stream.
228    *
229    * @param {object} chunk
230    */
231   pushOrBuffer (chunk) {
232     // Add the chunk to the buffer
233     this.buffer.push(chunk);
234
235     // If we're still reading, then immediately emit the next chunk in the buffer
236     // (which may or may not be the chunk that we just added)
237     if (this.shouldRead) {
238       this.pushFromBuffer();
239     }
240   }
241
242   /**
243    * Immediately pushes the next chunk in the buffer to the reader's stream.
244    * The "data" event will always be fired (via {@link Readable#push}).
245    * In addition, the "file", "directory", and/or "symlink" events may be fired,
246    * depending on the type of properties of the chunk.
247    */
248   pushFromBuffer () {
249     let stream = this.stream;
250     let chunk = this.buffer.shift();
251
252     // Stream the data
253     try {
254       this.shouldRead = stream.push(chunk.data);
255     }
256     catch (err) {
257       this.emit('error', err);
258     }
259
260     // Also emit specific events, based on the type of chunk
261     chunk.file && this.emit('file', chunk.data);
262     chunk.symlink && this.emit('symlink', chunk.data);
263     chunk.directory && this.emit('directory', chunk.data);
264   }
265
266   /**
267    * Determines whether the given directory meets the user-specified recursion criteria.
268    * If the user didn't specify recursion criteria, then this function will default to true.
269    *
270    * @param {fs.Stats} stats - The directory's {@link fs.Stats} object
271    * @param {string} posixPath - The item's POSIX path (used for glob matching)
272    * @param {boolean} maxDepthReached - Whether we've already crawled the user-specified depth
273    * @returns {boolean}
274    */
275   shouldRecurse (stats, posixPath, maxDepthReached) {
276     let options = this.options;
277
278     if (maxDepthReached) {
279       // We've already crawled to the maximum depth. So no more recursion.
280       return false;
281     }
282     else if (!stats.isDirectory()) {
283       // It's not a directory. So don't try to crawl it.
284       return false;
285     }
286     else if (options.recurseGlob) {
287       // Glob patterns are always tested against the POSIX path, even on Windows
288       // https://github.com/isaacs/node-glob#windows
289       return options.recurseGlob.test(posixPath);
290     }
291     else if (options.recurseRegExp) {
292       // Regular expressions are tested against the normal path
293       // (based on the OS or options.sep)
294       return options.recurseRegExp.test(stats.path);
295     }
296     else if (options.recurseFn) {
297       try {
298         // Run the user-specified recursion criteria
299         return options.recurseFn.call(null, stats);
300       }
301       catch (err) {
302         // An error occurred in the user's code.
303         // In Sync and Async modes, this will return an error.
304         // In Streaming mode, we emit an "error" event, but continue processing
305         this.emit('error', err);
306       }
307     }
308     else {
309       // No recursion function was specified, and we're within the maximum depth.
310       // So crawl this directory.
311       return true;
312     }
313   }
314
315   /**
316    * Determines whether the given item meets the user-specified filter criteria.
317    * If the user didn't specify a filter, then this function will always return true.
318    *
319    * @param {string|fs.Stats} value - Either the item's path, or the item's {@link fs.Stats} object
320    * @param {string} posixPath - The item's POSIX path (used for glob matching)
321    * @returns {boolean}
322    */
323   filter (value, posixPath) {
324     let options = this.options;
325
326     if (options.filterGlob) {
327       // Glob patterns are always tested against the POSIX path, even on Windows
328       // https://github.com/isaacs/node-glob#windows
329       return options.filterGlob.test(posixPath);
330     }
331     else if (options.filterRegExp) {
332       // Regular expressions are tested against the normal path
333       // (based on the OS or options.sep)
334       return options.filterRegExp.test(value.path || value);
335     }
336     else if (options.filterFn) {
337       try {
338         // Run the user-specified filter function
339         return options.filterFn.call(null, value);
340       }
341       catch (err) {
342         // An error occurred in the user's code.
343         // In Sync and Async modes, this will return an error.
344         // In Streaming mode, we emit an "error" event, but continue processing
345         this.emit('error', err);
346       }
347     }
348     else {
349       // No filter was specified, so match everything
350       return true;
351     }
352   }
353
354   /**
355    * Emits an event.  If one of the event listeners throws an error,
356    * then an "error" event is emitted.
357    *
358    * @param {string} eventName
359    * @param {*} data
360    */
361   emit (eventName, data) {
362     let stream = this.stream;
363
364     try {
365       stream.emit(eventName, data);
366     }
367     catch (err) {
368       if (eventName === 'error') {
369         // Don't recursively emit "error" events.
370         // If the first one fails, then just throw
371         throw err;
372       }
373       else {
374         stream.emit('error', err);
375       }
376     }
377   }
378 }
379
380 module.exports = DirectoryReader;