'use strict'; var fs = require('fs'); var path = require('path'); var EventEmitter = require('events'); var fastq = require('fastq'); var anymatch = require('anymatch'); var Readable = require('streamx').Readable; var isGlob = require('is-glob'); var globParent = require('glob-parent'); var normalizePath = require('normalize-path'); var isNegatedGlob = require('is-negated-glob'); var toAbsoluteGlob = require('@gulpjs/to-absolute-glob'); var globErrMessage1 = 'File not found with singular glob: '; var globErrMessage2 = ' (if this was purposeful, use `allowEmpty` option)'; function isFound(glob) { // All globs are "found", while singular globs are only found when matched successfully // This is due to the fact that a glob can match any number of files (0..Infinity) but // a signular glob is always expected to match return isGlob(glob); } function walkdir() { var readdirOpts = { withFileTypes: true, }; var ee = new EventEmitter(); var queue = fastq(onAction, 1); queue.drain = function () { ee.emit('end'); }; queue.error(onError); function onError(err) { if (err) { ee.emit('error', err); } } ee.pause = function () { queue.pause(); }; ee.resume = function () { queue.resume(); }; ee.end = function () { queue.kill(); }; ee.walk = walk; ee.exists = exists; ee.resolve = resolve; function walk(path) { queue.push({ action: 'walk', path: path }); } function exists(path) { queue.push({ action: 'exists', path: path }); } function resolve(path) { queue.push({ action: 'resolve', path: path }); } function resolveSymlink(symlinkPath, cb) { fs.realpath(symlinkPath, function (err, realpath) { if (err) { return cb(err); } fs.lstat(realpath, function (err, stat) { if (err) { return cb(err); } if (stat.isDirectory() && !symlinkPath.startsWith(realpath + path.sep)) { walk(symlinkPath); } cb(); }) }); } function onAction(data, cb) { if (data.action === 'walk') { return fs.readdir(data.path, readdirOpts, onReaddir); } if (data.action === 'exists') { return fs.stat(data.path, onStat); } if (data.action === 'resolve') { return resolveSymlink(data.path, cb); } function onStat(err, stat) { if (err) { // Ignore errors but also don't emit the path return cb(); } // `stat` has `isDirectory()` which is what we use from Dirent ee.emit('path', data.path, stat); cb(); } function onReaddir(err, dirents) { if (err) { return cb(err); } dirents.forEach(processDirent); cb(); } function processDirent(dirent) { var nextpath = path.join(data.path, dirent.name); ee.emit('path', nextpath, dirent); if (dirent.isDirectory()) { return walk(nextpath); } if (dirent.isSymbolicLink()) { return resolve(nextpath); } } } return ee; } function validateGlobs(globs) { var hasPositiveGlob = false; globs.forEach(validateGlobs); function validateGlobs(globString, index) { if (typeof globString !== 'string') { throw new Error('Invalid glob at index ' + index); } var result = isNegatedGlob(globString); if (result.negated === false) { hasPositiveGlob = true; } } if (hasPositiveGlob === false) { throw new Error('Missing positive glob'); } } function isPositiveGlob(glob) { return !isNegatedGlob(glob).negated; } function validateOptions(opts) { if (typeof opts.cwd !== 'string') { throw new Error('The `cwd` option must be a string'); } if (typeof opts.dot !== 'boolean') { throw new Error('The `dot` option must be a boolean'); } if (typeof opts.cwdbase !== 'boolean') { throw new Error('The `cwdbase` option must be a boolean'); } if ( typeof opts.uniqueBy !== 'string' && typeof opts.uniqueBy !== 'function' ) { throw new Error('The `uniqueBy` option must be a string or function'); } if (typeof opts.allowEmpty !== 'boolean') { throw new Error('The `allowEmpty` option must be a boolean'); } if (opts.base && typeof opts.base !== 'string') { throw new Error('The `base` option must be a string if specified'); } if (!Array.isArray(opts.ignore)) { throw new Error('The `ignore` option must be a string or array'); } } function uniqueBy(comparator) { var seen = new Set(); if (typeof comparator === 'string') { return isUniqueByKey; } else { return isUniqueByFunc; } function isUnique(value) { if (seen.has(value)) { return false; } else { seen.add(value); return true; } } function isUniqueByKey(obj) { return isUnique(obj[comparator]); } function isUniqueByFunc(obj) { return isUnique(comparator(obj)); } } function globStream(globs, opt) { if (!Array.isArray(globs)) { globs = [globs]; } validateGlobs(globs); var ourOpt = Object.assign( {}, { cwd: process.cwd(), dot: false, cwdbase: false, uniqueBy: 'path', allowEmpty: false, ignore: [], }, opt ); // Normalize `ignore` to array ourOpt.ignore = typeof ourOpt.ignore === 'string' ? [ourOpt.ignore] : ourOpt.ignore; validateOptions(ourOpt); ourOpt.cwd = normalizePath(path.resolve(ourOpt.cwd), true); var base = ourOpt.base; if (ourOpt.cwdbase) { base = ourOpt.cwd; } var walker = walkdir(); var stream = new Readable({ highWaterMark: ourOpt.highWaterMark, read: read, predestroy: predestroy, }); // Remove path relativity to make globs make sense var ourGlobs = globs.map(resolveGlob); ourOpt.ignore = ourOpt.ignore.map(resolveGlob); var found = ourGlobs.map(isFound); var matcher = anymatch(ourGlobs, null, ourOpt); var isUnique = uniqueBy(ourOpt.uniqueBy); walker.on('path', onPath); walker.once('end', onEnd); walker.once('error', onError); ourGlobs.forEach(function (glob) { if (isGlob(glob)) { // We only want to walk the glob-parent directories of any positive glob // to reduce the amount of files have to check. if (isPositiveGlob(glob)) { var base = globParent(glob); walker.walk(base); } } else { // If the strig is not a glob, we just check for the existence of it. walker.exists(glob); } }); function read(cb) { walker.resume(); cb(); } function predestroy() { walker.end(); } function resolveGlob(glob) { return toAbsoluteGlob(glob, ourOpt); } function onPath(filepath, dirent) { var matchIdx = matcher(filepath, true); // If the matcher doesn't match (but it is a directory), // we want to add a trailing separator to check the match again if (matchIdx === -1 && dirent.isDirectory()) { matchIdx = matcher(filepath + path.sep, true); } if (matchIdx !== -1) { found[matchIdx] = true; // Extract base path from glob var basePath = base || globParent(ourGlobs[matchIdx]); var obj = { cwd: ourOpt.cwd, base: basePath, // We always want to normalize the path to posix-style slashes path: normalizePath(filepath, true), }; var unique = isUnique(obj); if (unique) { var drained = stream.push(obj); if (!drained) { walker.pause(); } } } } function onEnd() { var destroyed = false; found.forEach(function (matchFound, idx) { if (ourOpt.allowEmpty !== true && !matchFound) { destroyed = true; var err = new Error(globErrMessage1 + ourGlobs[idx] + globErrMessage2); return stream.destroy(err); } }); if (destroyed === false) { stream.push(null); } } function onError(err) { stream.destroy(err); } return stream; } module.exports = globStream;