288 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			288 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								'use strict';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const fs = require('fs');
							 | 
						||
| 
								 | 
							
								const { Readable } = require('stream');
							 | 
						||
| 
								 | 
							
								const sysPath = require('path');
							 | 
						||
| 
								 | 
							
								const { promisify } = require('util');
							 | 
						||
| 
								 | 
							
								const picomatch = require('picomatch');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const readdir = promisify(fs.readdir);
							 | 
						||
| 
								 | 
							
								const stat = promisify(fs.stat);
							 | 
						||
| 
								 | 
							
								const lstat = promisify(fs.lstat);
							 | 
						||
| 
								 | 
							
								const realpath = promisify(fs.realpath);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * @typedef {Object} EntryInfo
							 | 
						||
| 
								 | 
							
								 * @property {String} path
							 | 
						||
| 
								 | 
							
								 * @property {String} fullPath
							 | 
						||
| 
								 | 
							
								 * @property {fs.Stats=} stats
							 | 
						||
| 
								 | 
							
								 * @property {fs.Dirent=} dirent
							 | 
						||
| 
								 | 
							
								 * @property {String} basename
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const BANG = '!';
							 | 
						||
| 
								 | 
							
								const RECURSIVE_ERROR_CODE = 'READDIRP_RECURSIVE_ERROR';
							 | 
						||
| 
								 | 
							
								const NORMAL_FLOW_ERRORS = new Set(['ENOENT', 'EPERM', 'EACCES', 'ELOOP', RECURSIVE_ERROR_CODE]);
							 | 
						||
| 
								 | 
							
								const FILE_TYPE = 'files';
							 | 
						||
| 
								 | 
							
								const DIR_TYPE = 'directories';
							 | 
						||
| 
								 | 
							
								const FILE_DIR_TYPE = 'files_directories';
							 | 
						||
| 
								 | 
							
								const EVERYTHING_TYPE = 'all';
							 | 
						||
| 
								 | 
							
								const ALL_TYPES = [FILE_TYPE, DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const isNormalFlowError = error => NORMAL_FLOW_ERRORS.has(error.code);
							 | 
						||
| 
								 | 
							
								const [maj, min] = process.versions.node.split('.').slice(0, 2).map(n => Number.parseInt(n, 10));
							 | 
						||
| 
								 | 
							
								const wantBigintFsStats = process.platform === 'win32' && (maj > 10 || (maj === 10 && min >= 5));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const normalizeFilter = filter => {
							 | 
						||
| 
								 | 
							
								  if (filter === undefined) return;
							 | 
						||
| 
								 | 
							
								  if (typeof filter === 'function') return filter;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (typeof filter === 'string') {
							 | 
						||
| 
								 | 
							
								    const glob = picomatch(filter.trim());
							 | 
						||
| 
								 | 
							
								    return entry => glob(entry.basename);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (Array.isArray(filter)) {
							 | 
						||
| 
								 | 
							
								    const positive = [];
							 | 
						||
| 
								 | 
							
								    const negative = [];
							 | 
						||
| 
								 | 
							
								    for (const item of filter) {
							 | 
						||
| 
								 | 
							
								      const trimmed = item.trim();
							 | 
						||
| 
								 | 
							
								      if (trimmed.charAt(0) === BANG) {
							 | 
						||
| 
								 | 
							
								        negative.push(picomatch(trimmed.slice(1)));
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        positive.push(picomatch(trimmed));
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (negative.length > 0) {
							 | 
						||
| 
								 | 
							
								      if (positive.length > 0) {
							 | 
						||
| 
								 | 
							
								        return entry =>
							 | 
						||
| 
								 | 
							
								          positive.some(f => f(entry.basename)) && !negative.some(f => f(entry.basename));
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      return entry => !negative.some(f => f(entry.basename));
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return entry => positive.some(f => f(entry.basename));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ReaddirpStream extends Readable {
							 | 
						||
| 
								 | 
							
								  static get defaultOptions() {
							 | 
						||
| 
								 | 
							
								    return {
							 | 
						||
| 
								 | 
							
								      root: '.',
							 | 
						||
| 
								 | 
							
								      /* eslint-disable no-unused-vars */
							 | 
						||
| 
								 | 
							
								      fileFilter: (path) => true,
							 | 
						||
| 
								 | 
							
								      directoryFilter: (path) => true,
							 | 
						||
| 
								 | 
							
								      /* eslint-enable no-unused-vars */
							 | 
						||
| 
								 | 
							
								      type: FILE_TYPE,
							 | 
						||
| 
								 | 
							
								      lstat: false,
							 | 
						||
| 
								 | 
							
								      depth: 2147483648,
							 | 
						||
| 
								 | 
							
								      alwaysStat: false
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  constructor(options = {}) {
							 | 
						||
| 
								 | 
							
								    super({
							 | 
						||
| 
								 | 
							
								      objectMode: true,
							 | 
						||
| 
								 | 
							
								      autoDestroy: true,
							 | 
						||
| 
								 | 
							
								      highWaterMark: options.highWaterMark || 4096
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    const opts = { ...ReaddirpStream.defaultOptions, ...options };
							 | 
						||
| 
								 | 
							
								    const { root, type } = opts;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this._fileFilter = normalizeFilter(opts.fileFilter);
							 | 
						||
| 
								 | 
							
								    this._directoryFilter = normalizeFilter(opts.directoryFilter);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const statMethod = opts.lstat ? lstat : stat;
							 | 
						||
| 
								 | 
							
								    // Use bigint stats if it's windows and stat() supports options (node 10+).
							 | 
						||
| 
								 | 
							
								    if (wantBigintFsStats) {
							 | 
						||
| 
								 | 
							
								      this._stat = path => statMethod(path, { bigint: true });
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      this._stat = statMethod;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this._maxDepth = opts.depth;
							 | 
						||
| 
								 | 
							
								    this._wantsDir = [DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type);
							 | 
						||
| 
								 | 
							
								    this._wantsFile = [FILE_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type);
							 | 
						||
| 
								 | 
							
								    this._wantsEverything = type === EVERYTHING_TYPE;
							 | 
						||
| 
								 | 
							
								    this._root = sysPath.resolve(root);
							 | 
						||
| 
								 | 
							
								    this._isDirent = ('Dirent' in fs) && !opts.alwaysStat;
							 | 
						||
| 
								 | 
							
								    this._statsProp = this._isDirent ? 'dirent' : 'stats';
							 | 
						||
| 
								 | 
							
								    this._rdOptions = { encoding: 'utf8', withFileTypes: this._isDirent };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Launch stream with one parent, the root dir.
							 | 
						||
| 
								 | 
							
								    this.parents = [this._exploreDir(root, 1)];
							 | 
						||
| 
								 | 
							
								    this.reading = false;
							 | 
						||
| 
								 | 
							
								    this.parent = undefined;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  async _read(batch) {
							 | 
						||
| 
								 | 
							
								    if (this.reading) return;
							 | 
						||
| 
								 | 
							
								    this.reading = true;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								      while (!this.destroyed && batch > 0) {
							 | 
						||
| 
								 | 
							
								        const { path, depth, files = [] } = this.parent || {};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (files.length > 0) {
							 | 
						||
| 
								 | 
							
								          const slice = files.splice(0, batch).map(dirent => this._formatEntry(dirent, path));
							 | 
						||
| 
								 | 
							
								          for (const entry of await Promise.all(slice)) {
							 | 
						||
| 
								 | 
							
								            if (this.destroyed) return;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            const entryType = await this._getEntryType(entry);
							 | 
						||
| 
								 | 
							
								            if (entryType === 'directory' && this._directoryFilter(entry)) {
							 | 
						||
| 
								 | 
							
								              if (depth <= this._maxDepth) {
							 | 
						||
| 
								 | 
							
								                this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
							 | 
						||
| 
								 | 
							
								              }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								              if (this._wantsDir) {
							 | 
						||
| 
								 | 
							
								                this.push(entry);
							 | 
						||
| 
								 | 
							
								                batch--;
							 | 
						||
| 
								 | 
							
								              }
							 | 
						||
| 
								 | 
							
								            } else if ((entryType === 'file' || this._includeAsFile(entry)) && this._fileFilter(entry)) {
							 | 
						||
| 
								 | 
							
								              if (this._wantsFile) {
							 | 
						||
| 
								 | 
							
								                this.push(entry);
							 | 
						||
| 
								 | 
							
								                batch--;
							 | 
						||
| 
								 | 
							
								              }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								          const parent = this.parents.pop();
							 | 
						||
| 
								 | 
							
								          if (!parent) {
							 | 
						||
| 
								 | 
							
								            this.push(null);
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								          this.parent = await parent;
							 | 
						||
| 
								 | 
							
								          if (this.destroyed) return;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } catch (error) {
							 | 
						||
| 
								 | 
							
								      this.destroy(error);
							 | 
						||
| 
								 | 
							
								    } finally {
							 | 
						||
| 
								 | 
							
								      this.reading = false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  async _exploreDir(path, depth) {
							 | 
						||
| 
								 | 
							
								    let files;
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								      files = await readdir(path, this._rdOptions);
							 | 
						||
| 
								 | 
							
								    } catch (error) {
							 | 
						||
| 
								 | 
							
								      this._onError(error);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return { files, depth, path };
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  async _formatEntry(dirent, path) {
							 | 
						||
| 
								 | 
							
								    let entry;
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								      const basename = this._isDirent ? dirent.name : dirent;
							 | 
						||
| 
								 | 
							
								      const fullPath = sysPath.resolve(sysPath.join(path, basename));
							 | 
						||
| 
								 | 
							
								      entry = { path: sysPath.relative(this._root, fullPath), fullPath, basename };
							 | 
						||
| 
								 | 
							
								      entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
							 | 
						||
| 
								 | 
							
								    } catch (err) {
							 | 
						||
| 
								 | 
							
								      this._onError(err);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return entry;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  _onError(err) {
							 | 
						||
| 
								 | 
							
								    if (isNormalFlowError(err) && !this.destroyed) {
							 | 
						||
| 
								 | 
							
								      this.emit('warn', err);
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      this.destroy(err);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  async _getEntryType(entry) {
							 | 
						||
| 
								 | 
							
								    // entry may be undefined, because a warning or an error were emitted
							 | 
						||
| 
								 | 
							
								    // and the statsProp is undefined
							 | 
						||
| 
								 | 
							
								    const stats = entry && entry[this._statsProp];
							 | 
						||
| 
								 | 
							
								    if (!stats) {
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (stats.isFile()) {
							 | 
						||
| 
								 | 
							
								      return 'file';
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (stats.isDirectory()) {
							 | 
						||
| 
								 | 
							
								      return 'directory';
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (stats && stats.isSymbolicLink()) {
							 | 
						||
| 
								 | 
							
								      const full = entry.fullPath;
							 | 
						||
| 
								 | 
							
								      try {
							 | 
						||
| 
								 | 
							
								        const entryRealPath = await realpath(full);
							 | 
						||
| 
								 | 
							
								        const entryRealPathStats = await lstat(entryRealPath);
							 | 
						||
| 
								 | 
							
								        if (entryRealPathStats.isFile()) {
							 | 
						||
| 
								 | 
							
								          return 'file';
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if (entryRealPathStats.isDirectory()) {
							 | 
						||
| 
								 | 
							
								          const len = entryRealPath.length;
							 | 
						||
| 
								 | 
							
								          if (full.startsWith(entryRealPath) && full.substr(len, 1) === sysPath.sep) {
							 | 
						||
| 
								 | 
							
								            const recursiveError = new Error(
							 | 
						||
| 
								 | 
							
								              `Circular symlink detected: "${full}" points to "${entryRealPath}"`
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								            recursiveError.code = RECURSIVE_ERROR_CODE;
							 | 
						||
| 
								 | 
							
								            return this._onError(recursiveError);
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								          return 'directory';
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      } catch (error) {
							 | 
						||
| 
								 | 
							
								        this._onError(error);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  _includeAsFile(entry) {
							 | 
						||
| 
								 | 
							
								    const stats = entry && entry[this._statsProp];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return stats && this._wantsEverything && !stats.isDirectory();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * @typedef {Object} ReaddirpArguments
							 | 
						||
| 
								 | 
							
								 * @property {Function=} fileFilter
							 | 
						||
| 
								 | 
							
								 * @property {Function=} directoryFilter
							 | 
						||
| 
								 | 
							
								 * @property {String=} type
							 | 
						||
| 
								 | 
							
								 * @property {Number=} depth
							 | 
						||
| 
								 | 
							
								 * @property {String=} root
							 | 
						||
| 
								 | 
							
								 * @property {Boolean=} lstat
							 | 
						||
| 
								 | 
							
								 * @property {Boolean=} bigint
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Main function which ends up calling readdirRec and reads all files and directories in given root recursively.
							 | 
						||
| 
								 | 
							
								 * @param {String} root Root directory
							 | 
						||
| 
								 | 
							
								 * @param {ReaddirpArguments=} options Options to specify root (start directory), filters and recursion depth
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								const readdirp = (root, options = {}) => {
							 | 
						||
| 
								 | 
							
								  let type = options.entryType || options.type;
							 | 
						||
| 
								 | 
							
								  if (type === 'both') type = FILE_DIR_TYPE; // backwards-compatibility
							 | 
						||
| 
								 | 
							
								  if (type) options.type = type;
							 | 
						||
| 
								 | 
							
								  if (!root) {
							 | 
						||
| 
								 | 
							
								    throw new Error('readdirp: root argument is required. Usage: readdirp(root, options)');
							 | 
						||
| 
								 | 
							
								  } else if (typeof root !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TypeError('readdirp: root argument must be a string. Usage: readdirp(root, options)');
							 | 
						||
| 
								 | 
							
								  } else if (type && !ALL_TYPES.includes(type)) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(', ')}`);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  options.root = root;
							 | 
						||
| 
								 | 
							
								  return new ReaddirpStream(options);
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const readdirpPromise = (root, options = {}) => {
							 | 
						||
| 
								 | 
							
								  return new Promise((resolve, reject) => {
							 | 
						||
| 
								 | 
							
								    const files = [];
							 | 
						||
| 
								 | 
							
								    readdirp(root, options)
							 | 
						||
| 
								 | 
							
								      .on('data', entry => files.push(entry))
							 | 
						||
| 
								 | 
							
								      .on('end', () => resolve(files))
							 | 
						||
| 
								 | 
							
								      .on('error', error => reject(error));
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								readdirp.promise = readdirpPromise;
							 | 
						||
| 
								 | 
							
								readdirp.ReaddirpStream = ReaddirpStream;
							 | 
						||
| 
								 | 
							
								readdirp.default = readdirp;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = readdirp;
							 |