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