Spaces:
Running
Running
; | |
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; | |