You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
287 lines
8.7 KiB
287 lines
8.7 KiB
'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;
|
|
|