Skip to content

Commit

Permalink
Add support for path matcher function (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: Corey Farrell <git@cfware.com>
  • Loading branch information
2 people authored and sindresorhus committed Apr 24, 2019
1 parent 80fda88 commit c0f0dd7
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 15 deletions.
25 changes: 24 additions & 1 deletion index.d.ts
Expand Up @@ -7,6 +7,8 @@ declare namespace findUp {
*/
readonly cwd?: string;
}

type Match = string | symbol | undefined;
}

declare const findUp: {
Expand Down Expand Up @@ -41,13 +43,34 @@ declare const findUp: {
*/
(name: string | string[], options?: findUp.Options): Promise<string | undefined>;

/**
Find a file or directory by walking up parent directories.
@param matcher - Called for each directory in the search. Return a path or `findUp.stop` to stop the search.
@returns The first path found or `undefined` if none could be found.
*/
(matcher: (directory: string) => (findUp.Match | Promise<findUp.Match>), options?: findUp.Options): Promise<string | undefined>;

/**
Synchronously find a file or directory by walking up parent directories.
@param name - Name of the file or directory to find. Can be multiple.
@returns The first path found (by respecting the order of `names`s) or `undefined` if none could be found.
@returns The first path found (by respecting the order of `name`s) or `undefined` if none could be found.
*/
sync(name: string | string[], options?: findUp.Options): string | undefined;

/**
Synchronously find a file or directory by walking up parent directories.
@param matcher - Called for each directory in the search. Return a path or `findUp.stop` to stop the search.
@returns The first path found or `undefined` if none could be found.
*/
sync(matcher: (directory: string) => findUp.Match, options?: findUp.Options): string | undefined;

/**
Return this in a `matcher` function to stop the search and force `findUp` to immediately return `undefined`.
*/
readonly stop: unique symbol;
};

export = findUp;
21 changes: 16 additions & 5 deletions index.js
Expand Up @@ -2,18 +2,23 @@
const path = require('path');
const locatePath = require('locate-path');

const stop = Symbol('findUp.stop');

module.exports = async (name, options = {}) => {
let directory = path.resolve(options.cwd || '');
const {root} = path.parse(directory);
const paths = [].concat(name);

// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const foundPath = await locatePath(paths, {cwd: directory});
const foundPath = await (typeof name === 'function' ? name(directory) : locatePath(paths, {cwd: directory}));

if (foundPath === stop) {
return;
}

if (foundPath) {
return path.join(directory, foundPath);
return path.resolve(directory, foundPath);
}

if (directory === root) {
Expand All @@ -31,10 +36,14 @@ module.exports.sync = (name, options = {}) => {

// eslint-disable-next-line no-constant-condition
while (true) {
const foundPath = locatePath.sync(paths, {cwd: directory});
const foundPath = typeof name === 'function' ? name(directory) : locatePath.sync(paths, {cwd: directory});

if (foundPath === stop) {
return;
}

if (foundPath) {
return path.join(directory, foundPath);
return path.resolve(directory, foundPath);
}

if (directory === root) {
Expand All @@ -44,3 +53,5 @@ module.exports.sync = (name, options = {}) => {
directory = path.dirname(directory);
}
};

module.exports.stop = stop;
20 changes: 20 additions & 0 deletions index.test-d.ts
Expand Up @@ -5,8 +5,28 @@ expectType<Promise<string | undefined>>(findUp('unicorn.png'));
expectType<Promise<string | undefined>>(findUp('unicorn.png', {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(['rainbow.png', 'unicorn.png']));
expectType<Promise<string | undefined>>(findUp(['rainbow.png', 'unicorn.png'], {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(() => 'unicorn.png'));
expectType<Promise<string | undefined>>(findUp(() => 'unicorn.png', {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(() => undefined));
expectType<Promise<string | undefined>>(findUp(() => undefined, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(() => findUp.stop));
expectType<Promise<string | undefined>>(findUp(() => findUp.stop, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => 'unicorn.png'));
expectType<Promise<string | undefined>>(findUp(async () => 'unicorn.png', {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => undefined));
expectType<Promise<string | undefined>>(findUp(async () => undefined, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => findUp.stop));
expectType<Promise<string | undefined>>(findUp(async () => findUp.stop, {cwd: ''}));

expectType<string | undefined>(findUp.sync('unicorn.png'));
expectType<string | undefined>(findUp.sync('unicorn.png', {cwd: ''}));
expectType<string | undefined>(findUp.sync(['rainbow.png', 'unicorn.png']));
expectType<string | undefined>(findUp.sync(['rainbow.png', 'unicorn.png'], {cwd: ''}));
expectType<string | undefined>(findUp.sync(() => 'unicorn.png'));
expectType<string | undefined>(findUp.sync(() => 'unicorn.png', {cwd: ''}));
expectType<string | undefined>(findUp.sync(() => undefined));
expectType<string | undefined>(findUp.sync(() => undefined, {cwd: ''}));
expectType<string | undefined>(findUp.sync(() => findUp.stop));
expectType<string | undefined>(findUp.sync(() => findUp.stop, {cwd: ''}));

expectType<Symbol>(findUp.stop);
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -44,6 +44,7 @@
},
"devDependencies": {
"ava": "^1.4.1",
"is-path-inside": "^2.1.0",
"tempy": "^0.3.0",
"tsd": "^0.7.2",
"xo": "^0.24.0"
Expand Down
34 changes: 34 additions & 0 deletions readme.md
Expand Up @@ -39,6 +39,8 @@ $ npm install find-up
`example.js`

```js
const fs = require('fs');
const path = require('path');
const findUp = require('find-up');

(async () => {
Expand All @@ -47,13 +49,22 @@ const findUp = require('find-up');

console.log(await findUp(['rainbow.png', 'unicorn.png']));
//=> '/Users/sindresorhus/unicorn.png'

const pathExists = filepath => fs.promises.access(filepath).then(_ => true).catch(_ => false);
console.log(await findUp(async (directory) => {
const hasUnicorns = await pathExists(path.join(directory, 'unicorn.png'));
return hasUnicorns && directory;
}});
//=> '/Users/sindresorhus'
})();
```
## API
### findUp(name, [options])
### findUp(matcher, [options])
Returns a `Promise` for either the path or `undefined` if it couldn't be found.
Expand All @@ -62,6 +73,7 @@ Returns a `Promise` for either the path or `undefined` if it couldn't be found.
Returns a `Promise` for either the first path found (by respecting the order) or `undefined` if none could be found.
### findUp.sync(name, [options])
### findUp.sync(matcher, [options])
Returns a path or `undefined` if it couldn't be found.
Expand All @@ -75,6 +87,14 @@ Type: `string`
Name of the file or directory to find.
#### matcher
Type: `Function`
A function that will be called with each directory until it returns a `string` with the path, which stops the search, or the root directory has been reached and nothing was found. Useful if you want to match files with certain patterns, set of permissions, or other advanced use cases.
When using async mode, the `matcher` may optionally be an async or promise-returning function that returns the path.
#### options
Type: `object`
Expand All @@ -86,6 +106,20 @@ Default: `process.cwd()`
Directory to start from.
### findUp.stop
A [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) that can be returned by a `matcher` function to stop the search and cause `findUp` to immediately return `undefined`. Useful as a performance optimization in case the current working directory is deeply nested in the filesystem.
```js
const path = require('path');
const findUp = require('find-up');

(async () => {
await findUp((directory) => {
return path.basename(directory) === 'work' ? findUp.stop : 'logo.png';
});
})();
```
## Security
Expand Down

0 comments on commit c0f0dd7

Please sign in to comment.