Skip to content

Commit

Permalink
First release.
Browse files Browse the repository at this point in the history
  • Loading branch information
DPassarelli committed Aug 21, 2016
0 parents commit 731cedd
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
*.sublime-*
node_modules/
62 changes: 62 additions & 0 deletions README.md
@@ -0,0 +1,62 @@
# then-recursively-search

A Promise-based Node module that recursively searches for a file, moving up the directory tree until found.

Adheres to the `standard` coding style (click below for more information):

[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard#javascript-standard-style)


## Why

I needed to start my GitHub/NPM publishing journey somewhere, and this seemed like a good starting point. I wanted it to be [FIRST](https://addyosmani.com/first/).


## How to use it

### Install

```
npm install then-recursively-search --save
```


### API

#### `fn(startIn, filename)`

* @param {String} `startIn` - The directory to begin searching in.
* @param {String} `filename` - The name (including extension) of the file to search for.
* @return {Promise} - Fulfilled with the complete path to the file (if found); otherwise, rejected.


#### Example

```
const findFile = require('then-recursively-search')
findFile(__dirname, 'package.json')
.then(function (pathToFile) {
...
})
```


## Test

Couldn't be easier...

```
npm test
```


## License

**ISC**

Copyright (c) 2016, [David Passarelli](mailto:dpassarelli@camelotcg.com)

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
48 changes: 48 additions & 0 deletions index.js
@@ -0,0 +1,48 @@
'use strict'

const Promise = require('bluebird')
const debug = require('debug')('then-recursively-search')
const fs = Promise.promisifyAll(require('fs'))
const path = require('path')

module.exports = _findFile

/**
* Recursively search a directory tree for the specified file.
*
* @param {String} startIn The directory to begin searching in.
*
* @param {String} filename The name (including extension) of the file to search for.
*
* @return {Promise} Fulfilled with the complete path to the file (if found); otherwise, rejected.
*/
function _findFile (startIn, filename) {
debug('searching for "%s" in "%s"', filename, startIn)

return Promise.try(function () {
if (startIn == null) {
throw new Error('The required parameter "startIn" is missing.')
}

if (filename == null) {
throw new Error('The required parameter "filename" is missing.')
}

return fs.readdirAsync(startIn)
.then(function (contents) {
const parentDir = path.dirname(startIn)

if (~contents.indexOf(filename)) {
debug('found!')
return path.join(startIn, filename)
}

if (parentDir === startIn) {
debug('not found')
throw new Error('File not found.')
}

return _findFile(parentDir, filename)
})
})
}
48 changes: 48 additions & 0 deletions package.json
@@ -0,0 +1,48 @@
{
"name": "then-recursively-search",
"version": "1.0.0",
"description": "A Promise-based utility that recursively searches for a file, moving up the directory tree until found.",
"main": "index.js",
"scripts": {
"test": "mocha",
"lint": "standard"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dpassarelli/node-then-recursively-search.git"
},
"keywords": [
"file",
"search",
"recursive",
"promise"
],
"author": "David Passarelli <dpassarelli@camelotcg.com>",
"license": "ISC",
"bugs": {
"url": "https://github.com/dpassarelli/node-then-recursively-search/issues"
},
"homepage": "https://github.com/dpassarelli/node-then-recursively-search#readme",
"devDependencies": {
"chai": "^3.5.0",
"chai-as-promised": "^5.3.0",
"mocha": "^3.0.2",
"mock-fs": "^3.11.0",
"standard": "^7.1.2"
},
"dependencies": {
"bluebird": "^3.4.1",
"debug": "^2.2.0"
},
"engines": {
"node": ">=0.12.x"
},
"standard": {
"ignore": [
"build/"
],
"global": [
"expect"
]
}
}
4 changes: 4 additions & 0 deletions test/common.js
@@ -0,0 +1,4 @@
const chai = require('chai')
global.expect = chai.expect

chai.use(require('chai-as-promised'))
3 changes: 3 additions & 0 deletions test/mocha.opts
@@ -0,0 +1,3 @@
--reporter spec
--ui bdd
--require test/common.js
70 changes: 70 additions & 0 deletions test/test-fulfillment.js
@@ -0,0 +1,70 @@
/* eslint-env mocha */
const mockFS = require('mock-fs')
const path = require('path')

/**
* Code under test.
* @type {Function}
*/
const T = require('../index.js')

describe('fulfillment', function () {
before(function () {
mockFS({
one: {
'one.txt': 'test file',
'two.txt': 'test file',
two: {
'two.txt': 'test file',
three: {
'three.txt': 'test file',
four: {}
}
}
}
})
})

after(function () {
mockFS.restore()
})

it('should be fulfilled if "filename" exists in "startIn"', function () {
const startIn = path.join(process.cwd(), './one/two/three')
const filename = 'three.txt'

/**
* The expected fulfillment value.
* @type {String}
*/
const expected = path.join(process.cwd(), './one/two/three/three.txt')

return expect(T(startIn, filename)).to.be.eventually.fulfilled.with.string(expected)
})

it('should be fulfilled if "filename" exists in directory more than one level above "startIn"', function () {
const startIn = path.join(process.cwd(), './one/two/three')
const filename = 'one.txt'

/**
* The expected fulfillment value.
* @type {String}
*/
const expected = path.join(process.cwd(), './one/one.txt')

return expect(T(startIn, filename)).to.be.eventually.fulfilled.with.string(expected)
})

it('should be fulfilled with the first match, if "filename" exists in more than one parent directory', function () {
const startIn = path.join(process.cwd(), './one/two/three')
const filename = 'two.txt'

/**
* The expected fulfillment value.
* @type {String}
*/
const expected = path.join(process.cwd(), './one/two/two.txt')

return expect(T(startIn, filename)).to.be.eventually.fulfilled.with.string(expected)
})
})
82 changes: 82 additions & 0 deletions test/test-rejections.js
@@ -0,0 +1,82 @@
/* eslint-env mocha */
const mockFS = require('mock-fs')
const path = require('path')

/**
* Code under test.
* @type {Function}
*/
const T = require('../index.js')

/**
* The error message thrown when the first parameter is missing.
* @type {String}
*/
const ERR_MISSING_DIR = 'The required parameter "startIn" is missing.'

/**
* The error message thrown when the second parameter is missing.
* @type {String}
*/
const ERR_MISSING_FILE = 'The required parameter "filename" is missing.'

/**
* The error message thrown when the file cannot be found (in any directory).
* @type {String}
*/
const ERR_404 = 'File not found.'

describe('rejections', function () {
before(function () {
mockFS({
one: {
'one.txt': 'test file',
'two.txt': 'test file',
two: {
'two.txt': 'test file',
three: {
'three.txt': 'test file',
four: {}
}
}
}
})
})

after(function () {
mockFS.restore()
})

it('should be rejected if no parameter values are provided', function () {
return expect(T()).to.be.eventually.rejected.with.property('message', ERR_MISSING_DIR)
})

it('should be rejected if the "startIn" parameter is provided, but not "filename"', function () {
return expect(T('C:/')).to.be.eventually.rejected.with.property('message', ERR_MISSING_FILE)
})

it('should be rejected if the value for "startIn" is not a string', function () {
return expect(T(123, 'test.txt')).to.be.eventually.rejected.with.instanceof(TypeError).and.property('message').match(/path must be a string/i)
})

it('should be rejected if the file does not exist anywhere in the specified directory (or any parent directories)', function () {
const startIn = path.join(process.cwd(), './one/two/three/four')
const filename = 'dne.txt'

return expect(T(startIn, filename)).to.be.eventually.rejected.with.property('message', ERR_404)
})

it('should be rejected if the directory specified by "startIn" does not exist', function () {
const startIn = path.join(process.cwd(), './one/two/three/four/five')
const filename = 'dne.txt'

return expect(T(startIn, filename)).to.be.eventually.rejected.with.property('code', 'ENOENT')
})

it('should be rejected if "filename" exists, but with a different case', function () {
const startIn = path.join(process.cwd(), './one/two/three')
const filename = 'ONE.txt'

return expect(T(startIn, filename)).to.be.eventually.rejected.with.property('message', ERR_404)
})
})

0 comments on commit 731cedd

Please sign in to comment.