-
-
Notifications
You must be signed in to change notification settings - Fork 41
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
Conversation
Friendly ping @sindresorhus, just in case you didn't see this. No rush, but should be a pretty quick review. Mostly just documentation and tests. |
readme.md
Outdated
Type: `string` | ||
Type: `string` `Function` | ||
|
||
Filename of the file to find, or a custom matcher function to be called with each directory until it returns a filepath to stop the search or the root directory has been reached and nothing was found. When using a matcher function, if you want to check whether a file exists, use [`fs.access()`](https://nodejs.org/api/fs.html#fs_fs_access_path_mode_callback) - this is done automatically when `filename` is a string. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be useful for us to at least expose convenience methods to check whether the path exists in a normalized way. fs.access()
is not obvious to use and you would have to manually promisify it when using it async.
We already depend on path-exists
in locate-path
, so we could depend on it and expose it here somehow without any increase in dependencies. I think this makes sense as checking whether the path exist is the most common operation you would do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my documentation made it sound worse than it is. fs.access()
isn't really necessary. In most use cases for find-up
, it's sufficient to use fs.existsSync()
if the filesystem even needs to be checked, and that returns a simple boolean, making it much easier to use here. I've added an example of that to the Usage section instead. Hopefully, a little simpler/clearer now.
I did consider that maybe we should ignore the result of the matcher if the filepath it returns does not exist. But I think it's highly likely that the matcher will be checking the filesystem anyway. And by leaving this up to the matcher, we also enable the user to decide where the search stops in a more deterministic manner (i.e. its decision will never be overridden). I think this is a good foundation to build on top of.
As for convenience methods, are you thinking something like findUp.exists(matcher)
/ findUp.existsSync(matcher)
? Or maybe a pipeline like findUp(matcher, findUp.exists)
? My immediate reaction is this merely saves a few characters. If we were to add this, I'd prefer the latter pipeline pattern.
I believe all issues have been addressed
Ready for review again @sindresorhus. At this point my main question is: are convenience methods a blocker for this PR? If so, what sort of pattern do you want to use? |
Fixed the lint errors. So awesome to see the full test matrix: multiple Node versions, all major operating systems, running in parallel, with simple config, all on one unified platform. :D |
readme.md
Outdated
@@ -47,13 +49,27 @@ const findUp = require('find-up'); | |||
|
|||
console.log(await findUp(['rainbow.png', 'unicorn.png'])); | |||
//=> '/Users/sindresorhus/unicorn.png' | |||
|
|||
console.log(await findUp(dir => { | |||
return fs.existsSync(path.join(dir, 'unicorn.png')) && 'foo'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't mix async and sync. Here, it should have used an async method to check whether it exists. The problem is that fs.exists
is deprecated, and it's also not async/await friendly. This is why I want to expose some utility methods. To make common operation simple.
I'm thinking:
findUp.exists()
findUp.isFile()
findUp.isDirectory()
findUp.sync.exists()
findUp.sync.isFile()
findUp.sync.isDirectory()
I know this seems like a lot, but I want find-up
to just work for people with minimal mental overhead or boilerplate.
Maybe we don't need the exists
methods as I can't really think of a scenario where you wouldn't care about the type. Can you?
The methods could alternatively be passed into the callback as a sort of context
or something. I think I prefer them to be on the findUp
object though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe we should have a type
option that accepts either file
or directory
, but defaults to file
? Hmm, we should probably have that regardless, as currently find-up
could match both directories and files, while most would only want to match file. Usually it's not a problem as people use extensions, but I can imagine a scenario where a user wants to find an extension-less file and instead gets a directory of the same name somewhere else in the hierarchy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I should've used findUp.sync()
. I think I was trying to avoid that and then I realized:
- As you mentioned, async
fs.exists()
is deprecated. - At the time, converting from callbacks to promises would have been some extra noisy code to understand for any async
fs
method.
But now fs.promises
is stable and it makes the examples simpler. How about something like this?
const pathExists = filepath => fs.promises.access(filepath).then(_ => true).catch(_ => false);
console.log(await findUp(async (directory) => {
const isInstalled = await pathExists(path.join(directory, 'node_modules'));
return isInstalled && 'package.json';
}});
That could replace the example below as well.
As for the utility methods, I agree they are useful and I'd also prefer for them to be properties on the findUp
function. Let's do that. I think we should merge this PR first and then build the utilities on top of this functionality in a separate PR(s), since this is still useful with or without them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like this?
👍
I think we should merge this PR first and then build the utilities on top of this functionality in a separate PR(s), since this is still useful with or without them.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test.js
Outdated
test('async (matcher function)', async t => { | ||
const cwd = process.cwd(); | ||
|
||
t.is(await m(dir => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use the directory
wording for all these instead of dir
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
What if the user knows the directory does not exist before reaching root and want to stop? They cannot return a string as that will return that string. I think we either need to support returning a |
Really sorry for dropping the ball on this... I'm just finally able to go through my backlog of PRs to review. |
I was thinking the user could just return I've added a |
# Conflicts: # index.js # package.json # readme.md
readme.md
Outdated
@@ -47,13 +49,27 @@ const findUp = require('find-up'); | |||
|
|||
console.log(await findUp(['rainbow.png', 'unicorn.png'])); | |||
//=> '/Users/sindresorhus/unicorn.png' | |||
|
|||
console.log(await findUp(dir => { | |||
return fs.existsSync(path.join(dir, 'unicorn.png')) && 'foo'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like this?
👍
I think we should merge this PR first and then build the utilities on top of this functionality in a separate PR(s), since this is still useful with or without them.
👍
Can you update the TS definition for the |
index.d.ts
Outdated
@@ -9,6 +9,8 @@ declare namespace findUp { | |||
} | |||
} | |||
|
|||
type Match = string | boolean | null | undefined; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Co-Authored-By: sholladay <me@seth-holladay.com>
This should be ready @sindresorhus. PTAL. :) |
Co-Authored-By: sholladay <me@seth-holladay.com>
* Addition testing for edge cases * Return absolute path if found regardless of cwd * Return undefined for non-existant absolute path * Find nested path in basename of cwd * Tweak variable name
More tests from @coreyfarrell, I've fixed the TypeScript types, and CI is passing. |
I don't see that. Did you see my comment in #28 (comment) ? |
I was referring to the previous CI failure, which I fixed. Now I've pushed the more strict types based on the review comments. One note... I wasn't able to use your suggestion for ❯ npx tsd
index.test-d.ts:12:53
✖ 12:53 Type symbol is not assignable to type string | unique symbol | Promise<Match> | undefined.
✖ 13:53 Type symbol is not assignable to type string | unique symbol | Promise<Match> | undefined.
✖ 18:59 Type Promise<symbol> is not assignable to type string | unique symbol | Promise<Match> | undefined.
Type Promise<symbol> is not assignable to type Promise<Match>.
Type symbol is not assignable to type Match.
✖ 19:59 Type Promise<symbol> is not assignable to type string | unique symbol | Promise<Match> | undefined.
Type Promise<symbol> is not assignable to type Promise<Match>.
4 errors I tried just |
@BendingBender Any ideas how we can enforce that only the |
This is looking great. Thanks for your perseverance on this, @sholladay. 🙌 |
Wouldn't #33 merely involve new options/methods, making it minor? Or are you thinking that you want to default to We could make I'm fine with it either way, but I think personally I lean towards |
No, currently we support both file and directories and we just match by name so the result could any anything. I would like to default to |
This is not about |
Ah, I forgot you want to default to I guess in my mind, if we decide that the |
I think that this should do the trick. I have however no idea why we have to explicitly annotate the funtion return type. Automatic inference seems not to work here: diff --git a/index.d.ts b/index.d.ts
index 425e324..5d83ba2 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,3 +1,5 @@
+declare const stop: unique symbol;
+
declare namespace findUp {
interface Options {
/**
@@ -8,7 +10,8 @@ declare namespace findUp {
readonly cwd?: string;
}
- type Match = string | symbol | undefined;
+ type StopSymbol = typeof stop;
+ type Match = string | StopSymbol | undefined;
}
declare const findUp: {
@@ -70,7 +73,7 @@ declare const findUp: {
/**
Return this in a `matcher` function to stop the search and force `findUp` to immediately return `undefined`.
*/
- readonly stop: unique symbol;
+ readonly stop: findUp.StopSymbol;
};
export = findUp;
diff --git a/index.test-d.ts b/index.test-d.ts
index 42b1397..025601e 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -9,14 +9,14 @@ 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((): findUp.StopSymbol => findUp.stop));
+expectType<Promise<string | undefined>>(findUp((): findUp.StopSymbol => 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<Promise<string | undefined>>(findUp(async (): Promise<findUp.StopSymbol> => findUp.stop));
+expectType<Promise<string | undefined>>(findUp(async (): Promise<findUp.StopSymbol> => findUp.stop, {cwd: ''}));
expectType<string | undefined>(findUp.sync('unicorn.png'));
expectType<string | undefined>(findUp.sync('unicorn.png', {cwd: ''}));
@@ -26,7 +26,7 @@ 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<string | undefined>(findUp.sync((): findUp.StopSymbol => findUp.stop));
+expectType<string | undefined>(findUp.sync((): findUp.StopSymbol => findUp.stop, {cwd: ''}));
expectType<Symbol>(findUp.stop); |
Closes #21
Closes #36
This PR adds a new call signature with a
matcher
function, wherematcher
receives the absolute path of the current directory being searched and it returns a path to finish the search. The matched path will be resolved against the directory and returned to the caller offindUp()
. Ifmatcher
returns a falsey value, the search continues up to the next directory with the same semantics asfindUp
has always had when there are no matches.Miscellaneous notes:
findUp.stop
symbol (see Add support for path matcher function #28 (comment)) so users can return early from the search without matching a particular path (i.e.findUp
will returnundefined
whenmatcher
returns thestop
symbol).