diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..ed3166b --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: 'gemini-testing', + root: true +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..56e3703 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.* +test/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..38b4d1c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +language: node_js +node_js: 4 diff --git a/README.md b/README.md index 9a9eda9..f59e1a6 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# path-utils \ No newline at end of file +[![Build +Status](https://travis-ci.org/gemini-testing/glob-extra.png)](https://travis-ci.org/gemini-testing/glob-extra) + +# glob-extra + +Wrapper for utility [glob](https://github.com/isaacs/node-glob) with promises support which provides expanding of masks, dirs and files to absolute file paths. + +## Installation + +```bash +$ npm install glob-extra +``` + +## Usage + +```js +const globExtra = require('glob-extra'); +const paths = ['some/path', 'other/path/*.js', 'other/deep/path/**/*.js'] + +// options are optional +globExtra.expandPaths(paths, options) + .then((files) => { + // ['/absolute/some/path/file1.js', + // '/absolute/other/path/file2.js', + // '/absolute/other/deep/path/dir/file3.js'] + }) + .done(); +``` + +### Options + +* **formats** *{String[]}* – files formats to expand; it will expand all files by default. For example: + +```js +globExtra.expandPaths(paths, {formats: ['.txt', '.js']}) + .then((files) => { + // will expand only js ant txt files + }) + .done(); +``` diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..4527827 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,43 @@ +'use strict'; + +const q = require('q'); +const qfs = require('q-io/fs'); +const _ = require('lodash'); +const glob = require('glob'); +const utils = require('./utils'); + +const getFilesByMask = (mask) => { + return q.nfcall(glob, mask) + .then((paths) => { + return _.isEmpty(paths) + ? q.reject(new Error(`Cannot find files by mask ${mask}`)) + : paths; + }); +}; + +const listFiles = (path) => { + return qfs.listTree(path) + .then((paths) => utils.asyncFilter(paths, utils.isFile)); +}; + +const expandPath = (path, options) => { + return utils.isFile(path) + .then((isFile) => isFile ? [path] : listFiles(path)) + .then((paths) => paths.filter((path) => utils.matchesFormats(path, options.formats))) + .then((paths) => paths.map((path) => qfs.absolute(path))); +}; + +const processPaths = (paths, cb) => { + return _(paths) + .map(cb) + .thru(q.all).value() + .then(_.flatten) + .then(_.uniq); +}; + +exports.expandPaths = (paths, options) => { + options = options || {}; + + return processPaths(paths, getFilesByMask) + .then((matchedPaths) => processPaths(matchedPaths, (path) => expandPath(path, options))); +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..80e4cfc --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,19 @@ +'use strict'; + +const _ = require('lodash'); +const q = require('q'); +const qfs = require('q-io/fs'); + +exports.asyncFilter = (items, cb) => { + return _(items) + .map((item) => cb(item).then((res) => res && item)) + .thru(q.all) + .value() + .then(_.compact); +}; + +exports.matchesFormats = (path, formats) => { + return _.isEmpty(formats) || _.includes(formats, qfs.extension(path)); +}; + +exports.isFile = (path) => qfs.stat(path).then((stat) => stat.isFile()); diff --git a/package.json b/package.json new file mode 100644 index 0000000..5ca681a --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "glob-extra", + "version": "1.0.0", + "description": "Utility which provides expanding of masks, dirs and files to absolute file paths.", + "main": "lib/index.js", + "scripts": { + "lint": "eslint .", + "test-unit": "mocha test", + "test": "npm run lint && npm run test-unit" + }, + "repository": { + "type": "git", + "url": "git://github.com/gemini-testing/path-utils.git" + }, + "keywords": [ + "expand", + "paths", + "masks" + ], + "dependencies": { + "glob": "^7.0.5", + "lodash": "^4.15.0", + "q": "^1.1.2", + "q-io": "^1.13.2" + }, + "devDependencies": { + "chai": "^3.4.1", + "chai-as-promised": "^5.3.0", + "eslint": "^3.1.1", + "eslint-config-gemini-testing": "^2.2.0", + "mocha": "^2.4.5", + "proxyquire": "^1.7.3", + "sinon": "^1.17.2" + } +} diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000..40d5bd0 --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: 'gemini-testing/tests' +}; diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..39072e2 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,164 @@ +'use strict'; + +const proxyquire = require('proxyquire'); +const qfs = require('q-io/fs'); +const q = require('q'); + +describe('path-utils', () => { + const sandbox = sinon.sandbox.create(); + + let glob; + let pathUtils; + + beforeEach(() => { + sandbox.stub(qfs, 'listTree'); + sandbox.stub(qfs, 'absolute'); + sandbox.stub(qfs, 'stat').returns(q({isFile: () => true})); + + glob = sandbox.stub(); + + pathUtils = proxyquire('../lib/index', {glob}); + }); + + afterEach(() => sandbox.restore()); + + describe('masks', () => { + it('should get absolute file path from passed mask', () => { + glob.withArgs('some/deep/**/*.js').yields(null, ['some/deep/path/file.js']); + + qfs.absolute.withArgs('some/deep/path/file.js').returns('/absolute/some/deep/path/file.js'); + + return pathUtils.expandPaths(['some/deep/**/*.js']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/deep/path/file.js']); + }); + }); + + it('should throw an error if a mask does not match files', () => { + glob.withArgs('bad/mask/*.js').yields(null, []); + + return assert.isRejected(pathUtils.expandPaths(['bad/mask/*.js']), /Cannot find files by mask bad\/mask\/\*\.js/); + }); + + it('should get absolute file path from passed mask according to formats option', () => { + glob.withArgs('some/path/*.*').yields(null, ['some/path/file.js', 'some/path/file.txt']); + + qfs.absolute + .withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/*.*'], {formats: ['.js']}) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + + it('should get uniq absolute file path from passed masks', () => { + glob.withArgs('some/path/*.js').yields(null, ['some/path/file.js']); + + qfs.absolute.withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/*.js', 'some/path/*.js']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + }); + + describe('directories', () => { + beforeEach(() => { + qfs.stat.withArgs('some/path/').returns(q({isFile: () => false})); + }); + + it('should get absolute paths for all files from passed dir', () => { + glob.withArgs('some/path/').yields(null, ['some/path/']); + + qfs.listTree.withArgs('some/path/').returns(q(['some/path/first.js', 'some/path/second.txt'])); + + qfs.absolute + .withArgs('some/path/first.js').returns('/absolute/some/path/first.js') + .withArgs('some/path/second.txt').returns('/absolute/some/path/second.txt'); + + return pathUtils.expandPaths(['some/path/']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/first.js', '/absolute/some/path/second.txt']); + }); + }); + + it('should get absolute file paths according to formats option', () => { + glob.withArgs('some/path/').yields(null, ['some/path/']); + + qfs.listTree.withArgs('some/path/').returns(q(['some/path/first.js', 'some/path/second.txt'])); + + qfs.absolute.withArgs('some/path/first.js').returns('/absolute/some/path/first.js'); + + return pathUtils.expandPaths(['some/path/'], {formats: ['.js']}) + .then((absolutePaths) => assert.deepEqual(absolutePaths, ['/absolute/some/path/first.js'])); + }); + + it('should get uniq absolute file path from passed dirs', () => { + glob.withArgs('some/path/').yields(null, ['some/path/']); + + qfs.listTree.withArgs('some/path/').returns(q(['some/path/file.js'])); + + qfs.absolute.withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/', 'some/path/']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + + it('should get only file paths from dir tree', () => { + glob.withArgs('some/path/').yields(null, ['some/path/']); + + qfs.stat.withArgs('some/path/dir').returns(q({isFile: () => false})); + + qfs.listTree.withArgs('some/path/').returns(q(['some/path/file.js', 'some/path/dir'])); + + qfs.absolute.withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + }); + + describe('files', () => { + it('should get absolute file path from passed file path', () => { + glob.withArgs('some/path/file.js').yields(null, ['some/path/file.js']); + + qfs.absolute.withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/file.js']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + + it('should filter files according to formats option', () => { + glob + .withArgs('some/path/file.js').yields(null, ['some/path/file.js']) + .withArgs('some/path/file.txt').yields(null, ['some/path/file.txt']); + + qfs.absolute + .withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/file.js', 'some/path/file.txt'], {formats: ['.js']}) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + + it('should get uniq absolute file path', () => { + glob.withArgs('some/path/file.js').yields(null, ['some/path/file.js']); + + qfs.absolute.withArgs('some/path/file.js').returns('/absolute/some/path/file.js'); + + return pathUtils.expandPaths(['some/path/file.js', 'some/path/file.js']) + .then((absolutePaths) => { + assert.deepEqual(absolutePaths, ['/absolute/some/path/file.js']); + }); + }); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..1f3f43d --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--require ./test/setup diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..dee0812 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,9 @@ +'use strict'; + +const chai = require('chai'); + +global.sinon = require('sinon'); +global.assert = chai.assert; + +chai.use(require('chai-as-promised')); +sinon.assert.expose(chai.assert, {prefix: ''}); diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..3e8eaca --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,48 @@ +'use strict'; + +const q = require('q'); +const qfs = require('q-io/fs'); +const sinon = require('sinon'); +const utils = require('../lib/utils'); + +describe('utils', () => { + describe('asyncFilter', () => { + it('should filter array using async function', () => { + const isPositive = (number) => q.delay(1).then(() => q(number > 0)); + + return assert.becomes(utils.asyncFilter([-1, 0, 1], isPositive), [1]); + }); + }); + + describe('matchesFormats', () => { + it('should return `true` if the formats option contain passed file format', () => { + assert.isTrue(utils.matchesFormats('some/path/file.js', ['.js'])); + }); + + it('should return `false` if the formats option does not contain passed file format', () => { + assert.isFalse(utils.matchesFormats('some/path/file.js', ['.txt'])); + }); + }); + + describe('isFile', () => { + const sandbox = sinon.sandbox.create(); + + beforeEach(() => { + sandbox.stub(qfs, 'stat'); + }); + + afterEach(() => sandbox.restore()); + + it('should return `true` if the passed path is file', () => { + qfs.stat.returns(q({isFile: () => true})); + + return assert.becomes(utils.isFile('some/path/file.js'), true); + }); + + it('should return `false` if the passed path is dir', () => { + qfs.stat.returns(q({isFile: () => false})); + + return assert.becomes(utils.isFile('some/path/dir'), false); + }); + }); +});