diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 3b07263..bbfaa3f 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -54,6 +54,8 @@ jobs: - name: Run tests run: npm test + env: + TEMP: ${{ runner.temp }} - name: Coveralls uses: coverallsapp/github-action@v1.1.2 diff --git a/index.js b/index.js index fdaed62..4c657b1 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,7 @@ function walkdir() { var ee = new EventEmitter(); - var queue = fastq(readdir, 1); + var queue = fastq(process, 1); queue.drain = function () { ee.emit('end'); }; @@ -52,20 +52,39 @@ function walkdir() { ee.end = function () { queue.kill(); }; - ee.walk = function (filepath) { - queue.push(filepath); - }; + ee.walk = walk; + ee.exists = exists; function isDefined(value) { return typeof value !== 'undefined'; } - function queuePush(value) { - queue.push(value); + function walk(path) { + queue.push({ action: 'walk', path: path }); + } + + function exists(path) { + queue.push({ action: 'exists', path: path }); } - function readdir(filepath, cb) { - fs.readdir(filepath, readdirOpts, onReaddir); + function process(data, cb) { + if (data.action === 'walk') { + fs.readdir(data.path, readdirOpts, onReaddir); + } else { + fs.stat(data.path, onStat); + } + + 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) { @@ -77,14 +96,14 @@ function walkdir() { return cb(err); } - dirs.filter(isDefined).forEach(queuePush); + dirs.filter(isDefined).forEach(walk); cb(); }); } function processDirents(dirent, key, cb) { - var nextpath = path.join(filepath, dirent.name); + var nextpath = path.join(data.path, dirent.name); ee.emit('path', nextpath, dirent); if (dirent.isDirectory()) { @@ -138,6 +157,10 @@ function validateGlobs(globs) { } } +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'); @@ -251,7 +274,19 @@ function globStream(globs, opt) { walker.on('path', onPath); walker.once('end', onEnd); walker.once('error', onError); - walker.walk(ourOpt.cwd); + 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(); diff --git a/test/index.js b/test/index.js index c87c057..5726155 100644 --- a/test/index.js +++ b/test/index.js @@ -6,6 +6,7 @@ var sinon = require('sinon'); // Need to wrap this to cause walker to emit an error var fs = require('fs'); +var os = require('os'); var globStream = require('../'); @@ -370,6 +371,11 @@ function suite(moduleName) { }); it('emits all objects (unordered) when given multiple absolute paths and no cwd', function (done) { + var testFile = path.join(os.tmpdir(), "glob-stream-test.txt"); + fs.writeFileSync(testFile, "test"); + + var tmp = deWindows(os.tmpdir()); + var expected = [ { cwd: cwd, @@ -387,24 +393,73 @@ function suite(moduleName) { base: dir + '/fixtures/whatsgoingon', path: dir + '/fixtures/whatsgoingon/test.js', }, + { + cwd: cwd, + base: tmp, + path: tmp + '/glob-stream-test.txt', + } ]; var paths = [ dir + '/fixtures/whatsgoingon/hey/isaidhey/whatsgoingon/test.txt', dir + '/fixtures/test.coffee', dir + '/fixtures/whatsgoingon/test.js', + tmp + '/glob-stream-*.txt', ]; + console.log(paths); + function assert(pathObjs) { - expect(pathObjs.length).toEqual(3); + fs.unlinkSync(testFile, "test"); + expect(pathObjs.length).toEqual(4); expect(pathObjs).toContainEqual(expected[0]); expect(pathObjs).toContainEqual(expected[1]); expect(pathObjs).toContainEqual(expected[2]); + expect(pathObjs).toContainEqual(expected[3]); } stream.pipeline([globStream(paths), concat(assert)], done); }); + it('resolves globs when process.chdir() changes the cwd', function (done) { + var prevCwd = process.cwd(); + process.chdir('test/fixtures/stuff'); + + var expected = { + cwd: dir + '/fixtures/stuff', + base: dir + '/fixtures', + path: dir + '/fixtures/test.coffee', + }; + + function assert(pathObjs) { + process.chdir(prevCwd); + + expect(pathObjs.length).toEqual(1); + expect(pathObjs[0]).toEqual(expected); + } + + stream.pipeline([globStream(['../*.coffee']), concat(assert)], done); + }); + + // https://github.com/gulpjs/glob-stream/issues/129 + it('does not take a long time when when checking a singular glob in the project root', function (done) { + // Extremely short timeout to ensure we aren't traversing node_modules + this.timeout(20); + + var expected = { + cwd: cwd, + base: cwd, + path: cwd + '/package.json', + }; + + function assert(pathObjs) { + expect(pathObjs.length).toEqual(1); + expect(pathObjs[0]).toEqual(expected); + } + + stream.pipeline([globStream(['./package.json']), concat(assert)], done); + }); + it('removes duplicate objects from the stream using default (path) filter', function (done) { var expected = { cwd: dir,