362 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 |