| '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; |