No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. module.exports = readdirGlob;
  2. const fs = require('fs');
  3. const { EventEmitter } = require('events');
  4. const { Minimatch } = require('minimatch');
  5. const { resolve } = require('path');
  6. function readdir(dir, strict) {
  7. return new Promise((resolve, reject) => {
  8. fs.readdir(dir, {withFileTypes: true} ,(err, files) => {
  9. if(err) {
  10. switch (err.code) {
  11. case 'ENOTDIR': // Not a directory
  12. if(strict) {
  13. reject(err);
  14. } else {
  15. resolve([]);
  16. }
  17. break;
  18. case 'ENOTSUP': // Operation not supported
  19. case 'ENOENT': // No such file or directory
  20. case 'ENAMETOOLONG': // Filename too long
  21. case 'UNKNOWN':
  22. resolve([]);
  23. break;
  24. case 'ELOOP': // Too many levels of symbolic links
  25. default:
  26. reject(err);
  27. break;
  28. }
  29. } else {
  30. resolve(files);
  31. }
  32. });
  33. });
  34. }
  35. function stat(file, followSyslinks) {
  36. return new Promise((resolve, reject) => {
  37. const statFunc = followSyslinks ? fs.stat : fs.lstat;
  38. statFunc(file, (err, stats) => {
  39. if(err) {
  40. switch (err.code) {
  41. case 'ENOENT':
  42. if(followSyslinks) {
  43. // Fallback to lstat to handle broken links as files
  44. resolve(stat(file, false));
  45. } else {
  46. resolve(null);
  47. }
  48. break;
  49. default:
  50. resolve(null);
  51. break;
  52. }
  53. } else {
  54. resolve(stats);
  55. }
  56. });
  57. });
  58. }
  59. async function* exploreWalkAsync(dir, path, followSyslinks, useStat, shouldSkip, strict) {
  60. let files = await readdir(path + dir, strict);
  61. for(const file of files) {
  62. let name = file.name;
  63. if(name === undefined) {
  64. // undefined file.name means the `withFileTypes` options is not supported by node
  65. // we have to call the stat function to know if file is directory or not.
  66. name = file;
  67. useStat = true;
  68. }
  69. const filename = dir + '/' + name;
  70. const relative = filename.slice(1); // Remove the leading /
  71. const absolute = path + '/' + relative;
  72. let stats = null;
  73. if(useStat || followSyslinks) {
  74. stats = await stat(absolute, followSyslinks);
  75. }
  76. if(!stats && file.name !== undefined) {
  77. stats = file;
  78. }
  79. if(stats === null) {
  80. stats = { isDirectory: () => false };
  81. }
  82. if(stats.isDirectory()) {
  83. if(!shouldSkip(relative)) {
  84. yield {relative, absolute, stats};
  85. yield* exploreWalkAsync(filename, path, followSyslinks, useStat, shouldSkip, false);
  86. }
  87. } else {
  88. yield {relative, absolute, stats};
  89. }
  90. }
  91. }
  92. async function* explore(path, followSyslinks, useStat, shouldSkip) {
  93. yield* exploreWalkAsync('', path, followSyslinks, useStat, shouldSkip, true);
  94. }
  95. function readOptions(options) {
  96. return {
  97. pattern: options.pattern,
  98. dot: !!options.dot,
  99. noglobstar: !!options.noglobstar,
  100. matchBase: !!options.matchBase,
  101. nocase: !!options.nocase,
  102. ignore: options.ignore,
  103. skip: options.skip,
  104. follow: !!options.follow,
  105. stat: !!options.stat,
  106. nodir: !!options.nodir,
  107. mark: !!options.mark,
  108. silent: !!options.silent,
  109. absolute: !!options.absolute
  110. };
  111. }
  112. class ReaddirGlob extends EventEmitter {
  113. constructor(cwd, options, cb) {
  114. super();
  115. if(typeof options === 'function') {
  116. cb = options;
  117. options = null;
  118. }
  119. this.options = readOptions(options || {});
  120. this.matchers = [];
  121. if(this.options.pattern) {
  122. const matchers = Array.isArray(this.options.pattern) ? this.options.pattern : [this.options.pattern];
  123. this.matchers = matchers.map( m =>
  124. new Minimatch(m, {
  125. dot: this.options.dot,
  126. noglobstar:this.options.noglobstar,
  127. matchBase:this.options.matchBase,
  128. nocase:this.options.nocase
  129. })
  130. );
  131. }
  132. this.ignoreMatchers = [];
  133. if(this.options.ignore) {
  134. const ignorePatterns = Array.isArray(this.options.ignore) ? this.options.ignore : [this.options.ignore];
  135. this.ignoreMatchers = ignorePatterns.map( ignore =>
  136. new Minimatch(ignore, {dot: true})
  137. );
  138. }
  139. this.skipMatchers = [];
  140. if(this.options.skip) {
  141. const skipPatterns = Array.isArray(this.options.skip) ? this.options.skip : [this.options.skip];
  142. this.skipMatchers = skipPatterns.map( skip =>
  143. new Minimatch(skip, {dot: true})
  144. );
  145. }
  146. this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat, this._shouldSkipDirectory.bind(this));
  147. this.paused = false;
  148. this.inactive = false;
  149. this.aborted = false;
  150. if(cb) {
  151. this._matches = [];
  152. this.on('match', match => this._matches.push(this.options.absolute ? match.absolute : match.relative));
  153. this.on('error', err => cb(err));
  154. this.on('end', () => cb(null, this._matches));
  155. }
  156. setTimeout( () => this._next(), 0);
  157. }
  158. _shouldSkipDirectory(relative) {
  159. //console.log(relative, this.skipMatchers.some(m => m.match(relative)));
  160. return this.skipMatchers.some(m => m.match(relative));
  161. }
  162. _fileMatches(relative, isDirectory) {
  163. const file = relative + (isDirectory ? '/' : '');
  164. return (this.matchers.length === 0 || this.matchers.some(m => m.match(file)))
  165. && !this.ignoreMatchers.some(m => m.match(file))
  166. && (!this.options.nodir || !isDirectory);
  167. }
  168. _next() {
  169. if(!this.paused && !this.aborted) {
  170. this.iterator.next()
  171. .then((obj)=> {
  172. if(!obj.done) {
  173. const isDirectory = obj.value.stats.isDirectory();
  174. if(this._fileMatches(obj.value.relative, isDirectory )) {
  175. let relative = obj.value.relative;
  176. let absolute = obj.value.absolute;
  177. if(this.options.mark && isDirectory) {
  178. relative += '/';
  179. absolute += '/';
  180. }
  181. if(this.options.stat) {
  182. this.emit('match', {relative, absolute, stat:obj.value.stats});
  183. } else {
  184. this.emit('match', {relative, absolute});
  185. }
  186. }
  187. this._next(this.iterator);
  188. } else {
  189. this.emit('end');
  190. }
  191. })
  192. .catch((err) => {
  193. this.abort();
  194. this.emit('error', err);
  195. if(!err.code && !this.options.silent) {
  196. console.error(err);
  197. }
  198. });
  199. } else {
  200. this.inactive = true;
  201. }
  202. }
  203. abort() {
  204. this.aborted = true;
  205. }
  206. pause() {
  207. this.paused = true;
  208. }
  209. resume() {
  210. this.paused = false;
  211. if(this.inactive) {
  212. this.inactive = false;
  213. this._next();
  214. }
  215. }
  216. }
  217. function readdirGlob(pattern, options, cb) {
  218. return new ReaddirGlob(pattern, options, cb);
  219. }
  220. readdirGlob.ReaddirGlob = ReaddirGlob;