123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- 'use strict';
-
- const fs = require('fs');
- const { Readable } = require('stream');
- const sysPath = require('path');
- const picomatch = require('picomatch');
- const { promisify } = require('util');
- const [readdir, stat, lstat] = [promisify(fs.readdir), promisify(fs.stat), promisify(fs.lstat)];
- const supportsDirent = 'Dirent' in fs;
-
- /**
- * @typedef {Object} EntryInfo
- * @property {String} path
- * @property {String} fullPath
- * @property {fs.Stats=} stats
- * @property {fs.Dirent=} dirent
- * @property {String} basename
- */
-
- const isWindows = process.platform === 'win32';
- const supportsBigint = typeof BigInt === 'function';
- const BANG = '!';
- const NORMAL_FLOW_ERRORS = new Set(['ENOENT', 'EPERM', 'EACCES', 'ELOOP']);
- const STAT_OPTIONS_SUPPORT_LENGTH = 3;
- const FILE_TYPE = 'files';
- const DIR_TYPE = 'directories';
- const FILE_DIR_TYPE = 'files_directories';
- const EVERYTHING_TYPE = 'all';
- const FILE_TYPES = new Set([FILE_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE]);
- const DIR_TYPES = new Set([DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE]);
- const ALL_TYPES = [FILE_TYPE, DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE];
-
- const isNormalFlowError = errorCode => NORMAL_FLOW_ERRORS.has(errorCode);
-
- const checkBasename = f => f(entry.basename);
-
- 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));
- } else {
- return entry => !negative.some(f => f(entry.basename));
- }
- } else {
- return entry => positive.some(f => f(entry.basename));
- }
- }
- };
-
- class ExploringDir {
- constructor(path, depth) {
- this.path = path;
- this.depth = depth;
- }
- }
-
- class ReaddirpStream extends Readable {
- static get defaultOptions() {
- return {
- root: '.',
- fileFilter: path => true,
- directoryFilter: path => true,
- type: 'files',
- lstat: false,
- depth: 2147483648,
- alwaysStat: false
- };
- }
-
- constructor(options = {}) {
- super({ objectMode: true, highWaterMark: 1, autoDestroy: true });
- const opts = Object.assign({}, ReaddirpStream.defaultOptions, options);
- const { root } = opts;
-
- this._fileFilter = normalizeFilter(opts.fileFilter);
- this._directoryFilter = normalizeFilter(opts.directoryFilter);
- this._statMethod = opts.lstat ? lstat : stat;
- this._statOpts = { bigint: isWindows };
- this._maxDepth = opts.depth;
- this._entryType = opts.type;
- this._root = sysPath.resolve(root);
- this._isDirent = !opts.alwaysStat && supportsDirent;
- this._statsProp = this._isDirent ? 'dirent' : 'stats';
- this._readdir_options = { encoding: 'utf8', withFileTypes: this._isDirent };
-
- // Launch stream with one parent, the root dir.
- /** @type Array<[string, number]> */
- this.parents = [new ExploringDir(root, 0)];
- this.filesToRead = 0;
- }
-
- async _read() {
- do {
- // If the stream was destroyed, we must not proceed.
- if (this.destroyed) return;
-
- const parent = this.parents.pop();
- if (!parent) {
- // ...we have files to process; but not directories.
- // hence, parent is undefined; and we cannot execute fs.readdir().
- // The files are being processed anywhere.
- break;
- }
- await this._exploreDirectory(parent);
- } while (!this.isPaused() && !this._isQueueEmpty());
-
- this._endStreamIfQueueIsEmpty();
- }
-
- async _exploreDirectory(parent) {
- /** @type Array<fs.Dirent|string> */
- let files = [];
-
- // To prevent race conditions, we increase counter while awaiting readdir.
- this.filesToRead++;
- try {
- files = await readdir(parent.path, this._readdir_options);
- } catch (error) {
- if (isNormalFlowError(error.code)) {
- this._handleError(error);
- } else {
- this._handleFatalError(error);
- }
- }
- this.filesToRead--;
-
- // If the stream was destroyed, after readdir is completed
- if (this.destroyed) return;
-
- this.filesToRead += files.length;
-
- const entries = await Promise.all(files.map(dirent => this._formatEntry(dirent, parent)));
-
- if (this.destroyed) return;
-
- for (let i = 0; i < entries.length; i++) {
- const entry = entries[i];
- this.filesToRead--;
- if (!entry) {
- continue;
- }
- if (this._isDirAndMatchesFilter(entry)) {
- this._pushNewParentIfLessThanMaxDepth(entry.fullPath, parent.depth + 1);
- this._emitPushIfUserWantsDir(entry);
- } else if (this._isFileAndMatchesFilter(entry)) {
- this._emitPushIfUserWantsFile(entry);
- }
- }
- }
-
- _isStatOptionsSupported() {
- return this._statMethod.length === STAT_OPTIONS_SUPPORT_LENGTH;
- }
-
- _stat(fullPath) {
- if (isWindows && this._isStatOptionsSupported()) {
- return this._statMethod(fullPath, this._statOpts);
- } else {
- return this._statMethod(fullPath);
- }
- }
-
- async _formatEntry(dirent, parent) {
- const basename = this._isDirent ? dirent.name : dirent;
- const fullPath = sysPath.resolve(sysPath.join(parent.path, basename));
-
- let stats;
- if (this._isDirent) {
- stats = dirent;
- } else {
- try {
- stats = await this._stat(fullPath);
- } catch (error) {
- if (isNormalFlowError(error.code)) {
- this._handleError(error);
- } else {
- this._handleFatalError(error);
- }
- return;
- }
- }
- const path = sysPath.relative(this._root, fullPath);
-
- /** @type {EntryInfo} */
- const entry = { path, fullPath, basename, [this._statsProp]: stats };
-
- return entry;
- }
-
- _isQueueEmpty() {
- return this.parents.length === 0 && this.filesToRead === 0 && this.readable;
- }
-
- _endStreamIfQueueIsEmpty() {
- if (this._isQueueEmpty()) {
- this.push(null);
- }
- }
-
- _pushNewParentIfLessThanMaxDepth(parentPath, depth) {
- if (depth <= this._maxDepth) {
- this.parents.push(new ExploringDir(parentPath, depth));
- return true;
- } else {
- return false;
- }
- }
-
- _isDirAndMatchesFilter(entry) {
- return entry[this._statsProp].isDirectory() && this._directoryFilter(entry);
- }
-
- _isFileAndMatchesFilter(entry) {
- const stats = entry[this._statsProp];
- const isFileType =
- (this._entryType === EVERYTHING_TYPE && !stats.isDirectory()) ||
- (stats.isFile() || stats.isSymbolicLink());
- return isFileType && this._fileFilter(entry);
- }
-
- _emitPushIfUserWantsDir(entry) {
- if (DIR_TYPES.has(this._entryType)) {
- // TODO: Understand why this happens.
- const fn = () => {
- this.push(entry);
- };
- if (this._isDirent) setImmediate(fn);
- else fn();
- }
- }
-
- _emitPushIfUserWantsFile(entry) {
- if (FILE_TYPES.has(this._entryType)) {
- this.push(entry);
- }
- }
-
- _handleError(error) {
- if (!this.destroyed) {
- this.emit('warn', error);
- }
- }
-
- _handleFatalError(error) {
- this.destroy(error);
- }
- }
-
- /**
- * @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 == null || typeof root === 'undefined') {
- throw new Error('readdirp: root argument is required. Usage: readdirp(root, options)');
- } else if (typeof root !== 'string') {
- throw new Error(`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;
|