Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for path matcher function #28

Merged
merged 19 commits into from Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 24 additions & 1 deletion index.d.ts
Expand Up @@ -9,6 +9,8 @@ declare namespace findUp {
}
}

type Match = string | boolean | null | undefined;
Copy link
Collaborator Author

@sholladay sholladay Apr 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discovered that there's seemingly no way for me to tell TypeScript that the only boolean type that should be allowed is false. Annoying. Should we support returning true? It's easy to support, but I remember we discussed that the semantics may not be obvious.

Also, should I put this type on the namespace?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should type this one as:

type Match = string | typeof findUp.stop | undefined;

While JS is loose, we should try to keep the TS types strict.


Also, should I put this type on the namespace?

Yes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in general, but in this case, if we are very strict, then people can't necessarily do:

findUp((directory) => {
    return foo && bar;
});

... since foo might be false or null, etc. Is it worth losing that nice syntax?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TS users will definitely say yes. That's why they're using TS; they want strict types. It doesn't affect the majority that is using plain JS.

TS users can do:

findUp((directory) => {
    return foo ? bar : undefined;
});

It's just a tiny bit more verbose.


declare const findUp: {
/**
Find a file or directory by walking up parent directories.
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) => (Match | Promise<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) => Match, options?: findUp.Options): string | undefined;

/**
Return this in a `matcher` function to stop the search and force `findUp` to immediately return `undefined`.
*/
stop: symbol
sholladay marked this conversation as resolved.
Show resolved Hide resolved
};

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;
sholladay marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 26 additions & 0 deletions index.test-d.ts
Expand Up @@ -5,8 +5,34 @@ 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(() => false));
expectType<Promise<string | undefined>>(findUp(() => false, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(() => null));
expectType<Promise<string | undefined>>(findUp(() => null, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(() => undefined));
expectType<Promise<string | undefined>>(findUp(() => undefined, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => 'unicorn.png'));
expectType<Promise<string | undefined>>(findUp(async () => 'unicorn.png', {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => false));
expectType<Promise<string | undefined>>(findUp(async () => false, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => null));
expectType<Promise<string | undefined>>(findUp(async () => null, {cwd: ''}));
expectType<Promise<string | undefined>>(findUp(async () => undefined));
expectType<Promise<string | undefined>>(findUp(async () => undefined, {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(() => false));
expectType<string | undefined>(findUp.sync(() => false, {cwd: ''}));
expectType<string | undefined>(findUp.sync(() => null));
expectType<string | undefined>(findUp.sync(() => null, {cwd: ''}));
expectType<string | undefined>(findUp.sync(() => undefined));
expectType<string | undefined>(findUp.sync(() => undefined, {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'
sholladay marked this conversation as resolved.
Show resolved Hide resolved
})();
```


## 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