Skip to content

Commit

Permalink
path-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
rostik404 committed Aug 19, 2016
1 parent 2461227 commit 36a7e69
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
extends: 'gemini-testing',
root: true
};
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.*
test/
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
language: node_js
node_js: 4
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
# path-utils
[![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();
```
43 changes: 43 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -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)));
};
19 changes: 19 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -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());
35 changes: 35 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions test/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: 'gemini-testing/tests'
};
164 changes: 164 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
@@ -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']);
});
});
});
});
1 change: 1 addition & 0 deletions test/mocha.opts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require ./test/setup
9 changes: 9 additions & 0 deletions test/setup.js
Original file line number Diff line number Diff line change
@@ -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: ''});
48 changes: 48 additions & 0 deletions test/utils.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 36a7e69

Please sign in to comment.