diff --git a/fixture/foo/bar/qux.js b/fixture/foo/bar/qux.js new file mode 100644 index 0000000..e69de29 diff --git a/index.d.ts b/index.d.ts index d3c13e1..64f4049 100644 --- a/index.d.ts +++ b/index.d.ts @@ -115,6 +115,106 @@ console.log(findUpSync(directory => { */ export function findUpSync(matcher: (directory: string) => Match, options?: Options): string | undefined; +/** +Find files or directories by walking up parent directories. + +@param name - The name of the file or directory to find. Can be multiple. +@returns All paths found (by respecting the order of `name`s) or an empty array if none could be found. + +@example +``` +// / +// └── Users +// └── sindresorhus +// ├── unicorn.png +// └── foo +// ├── unicorn.png +// └── bar +// ├── baz +// └── example.js + +// example.js +import {findUpMultiple} from 'find-up'; + +console.log(await findUpMultiple('unicorn.png')); +//=> ['/Users/sindresorhus/foo/unicorn.png', '/Users/sindresorhus/unicorn.png'] + +console.log(await findUpMultiple(['rainbow.png', 'unicorn.png'])); +//=> ['/Users/sindresorhus/foo/unicorn.png', '/Users/sindresorhus/unicorn.png'] +``` +*/ +export function findUpMultiple(name: string | readonly string[], options?: Options): Promise; + +/** +Find files or directories by walking up parent directories. + +@param matcher - Called for each directory in the search. Return a path or `findUpStop` to stop the search. +@returns All paths found or an empty array if none could be found. + +@example +``` +import path from 'node:path'; +import {findUpMultiple, pathExists} from 'find-up'; + +console.log(await findUpMultiple(async directory => { + const hasUnicorns = await pathExists(path.join(directory, 'unicorn.png')); + return hasUnicorns && directory; +}, {type: 'directory'})); +//=> ['/Users/sindresorhus/foo', '/Users/sindresorhus'] +``` +*/ +export function findUpMultiple(matcher: (directory: string) => (Match | Promise), options?: Options): Promise; + +/** +Synchronously find files or directories by walking up parent directories. + +@param name - The name of the file or directory to find. Can be multiple. +@returns All paths found (by respecting the order of `name`s) or an empty array if none could be found. + +@example +``` +// / +// └── Users +// └── sindresorhus +// ├── unicorn.png +// └── foo +// ├── unicorn.png +// └── bar +// ├── baz +// └── example.js + +// example.js +import {findUpMultipleSync} from 'find-up'; + +console.log(findUpMultipleSync('unicorn.png')); +//=> ['/Users/sindresorhus/foo/unicorn.png', '/Users/sindresorhus/unicorn.png'] + +console.log(findUpMultipleSync(['rainbow.png', 'unicorn.png'])); +//=> ['/Users/sindresorhus/foo/unicorn.png', '/Users/sindresorhus/unicorn.png'] +``` +*/ +export function findUpMultipleSync(name: string | readonly string[], options?: Options): string[]; + +/** +Synchronously find files or directories by walking up parent directories. + +@param matcher - Called for each directory in the search. Return a path or `findUpStop` to stop the search. +@returns All paths found or an empty array if none could be found. + +@example +``` +import path from 'node:path'; +import {findUpMultipleSync, pathExistsSync} from 'find-up'; + +console.log(findUpMultipleSync(directory => { + const hasUnicorns = pathExistsSync(path.join(directory, 'unicorn.png')); + return hasUnicorns && directory; +}, {type: 'directory'})); +//=> ['/Users/sindresorhus/foo', '/Users/sindresorhus'] +``` +*/ +export function findUpMultipleSync(matcher: (directory: string) => Match, options?: Options): string[]; + /** Check if a path exists. diff --git a/index.js b/index.js index 94f00bb..d2d9184 100644 --- a/index.js +++ b/index.js @@ -3,10 +3,11 @@ import {locatePath, locatePathSync} from 'locate-path'; export const findUpStop = Symbol('findUpStop'); -export async function findUp(name, options = {}) { +export async function findUpMultiple(name, options = {}) { let directory = path.resolve(options.cwd || ''); const {root} = path.parse(directory); const stopAt = path.resolve(directory, options.stopAt || root); + const limit = options.limit || Number.POSITIVE_INFINITY; const paths = [name].flat(); const runMatcher = async locateOptions => { @@ -22,31 +23,35 @@ export async function findUp(name, options = {}) { return foundPath; }; + const matches = []; // eslint-disable-next-line no-constant-condition while (true) { // eslint-disable-next-line no-await-in-loop const foundPath = await runMatcher({...options, cwd: directory}); if (foundPath === findUpStop) { - return; + break; } if (foundPath) { - return path.resolve(directory, foundPath); + matches.push(path.resolve(directory, foundPath)); } - if (directory === stopAt) { - return; + if (directory === stopAt || matches.length >= limit) { + break; } directory = path.dirname(directory); } + + return matches; } -export function findUpSync(name, options = {}) { +export function findUpMultipleSync(name, options = {}) { let directory = path.resolve(options.cwd || ''); const {root} = path.parse(directory); const stopAt = options.stopAt || root; + const limit = options.limit || Number.POSITIVE_INFINITY; const paths = [name].flat(); const runMatcher = locateOptions => { @@ -62,24 +67,37 @@ export function findUpSync(name, options = {}) { return foundPath; }; + const matches = []; // eslint-disable-next-line no-constant-condition while (true) { const foundPath = runMatcher({...options, cwd: directory}); if (foundPath === findUpStop) { - return; + break; } if (foundPath) { - return path.resolve(directory, foundPath); + matches.push(path.resolve(directory, foundPath)); } - if (directory === stopAt) { - return; + if (directory === stopAt || matches.length >= limit) { + break; } directory = path.dirname(directory); } + + return matches; +} + +export async function findUp(name, options = {}) { + const matches = await findUpMultiple(name, {...options, limit: 1}); + return matches[0]; +} + +export function findUpSync(name, options = {}) { + const matches = findUpMultipleSync(name, {...options, limit: 1}); + return matches[0]; } export { diff --git a/index.test-d.ts b/index.test-d.ts index 539323c..9e9c205 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,5 @@ import {expectType, expectError} from 'tsd'; -import {findUp, findUpSync, findUpStop, pathExists, pathExistsSync} from './index.js'; +import {findUp, findUpSync, findUpMultiple, findUpMultipleSync, findUpStop, pathExists, pathExistsSync} from './index.js'; expectType>(findUp('unicorn.png')); expectType>(findUp('unicorn.png', {cwd: ''})); @@ -52,6 +52,57 @@ expectType>(findUp(async (): Promise>(findUp(async (): Promise => findUpStop, {type: 'directory'})); expectType>(findUp(async (): Promise => findUpStop, {stopAt: 'foo'})); +expectType>(findUpMultiple('unicorn.png')); +expectType>(findUpMultiple('unicorn.png', {cwd: ''})); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'])); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'], {cwd: ''})); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'], {allowSymlinks: true})); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'], {allowSymlinks: false})); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'], {type: 'file'})); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'], {type: 'directory'})); +expectType>(findUpMultiple(['rainbow.png', 'unicorn.png'], {stopAt: 'foo'})); +expectError(findUpMultiple(['rainbow.png', 'unicorn.png'], {concurrency: 1})); + +expectType>(findUpMultiple(() => 'unicorn.png')); +expectType>(findUpMultiple(() => 'unicorn.png', {cwd: ''})); +expectType>(findUpMultiple(() => 'unicorn.png', {allowSymlinks: true})); +expectType>(findUpMultiple(() => 'unicorn.png', {allowSymlinks: false})); +expectType>(findUpMultiple(() => 'unicorn.png', {type: 'file'})); +expectType>(findUpMultiple(() => 'unicorn.png', {type: 'directory'})); +expectType>(findUpMultiple(() => 'unicorn.png', {stopAt: 'foo'})); +expectType>(findUpMultiple(() => undefined)); +expectType>(findUpMultiple(() => undefined, {cwd: ''})); +expectType>(findUpMultiple(() => undefined, {allowSymlinks: true})); +expectType>(findUpMultiple(() => undefined, {allowSymlinks: false})); +expectType>(findUpMultiple(() => undefined, {type: 'file'})); +expectType>(findUpMultiple(() => undefined, {type: 'directory'})); +expectType>(findUpMultiple(() => undefined, {stopAt: 'foo'})); +expectType>(findUpMultiple((): typeof findUpStop => findUpStop)); +expectType>(findUpMultiple((): typeof findUpStop => findUpStop, {cwd: ''})); +expectType>(findUpMultiple((): typeof findUpStop => findUpStop, {stopAt: 'foo'})); +expectType>(findUpMultiple(async () => 'unicorn.png')); +expectType>(findUpMultiple(async () => 'unicorn.png', {cwd: ''})); +expectType>(findUpMultiple(async () => 'unicorn.png', {allowSymlinks: true})); +expectType>(findUpMultiple(async () => 'unicorn.png', {allowSymlinks: false})); +expectType>(findUpMultiple(async () => 'unicorn.png', {type: 'file'})); +expectType>(findUpMultiple(async () => 'unicorn.png', {type: 'directory'})); +expectType>(findUpMultiple(async () => 'unicorn.png', {stopAt: 'foo'})); +expectType>(findUpMultiple(async () => undefined)); +expectType>(findUpMultiple(async () => undefined, {cwd: ''})); +expectType>(findUpMultiple(async () => undefined, {allowSymlinks: true})); +expectType>(findUpMultiple(async () => undefined, {allowSymlinks: false})); +expectType>(findUpMultiple(async () => undefined, {type: 'file'})); +expectType>(findUpMultiple(async () => undefined, {type: 'directory'})); +expectType>(findUpMultiple(async () => undefined, {stopAt: 'foo'})); + +expectType>(findUpMultiple(async (): Promise => findUpStop)); +expectType>(findUpMultiple(async (): Promise => findUpStop, {cwd: ''})); +expectType>(findUpMultiple(async (): Promise => findUpStop, {allowSymlinks: true})); +expectType>(findUpMultiple(async (): Promise => findUpStop, {allowSymlinks: false})); +expectType>(findUpMultiple(async (): Promise => findUpStop, {type: 'file'})); +expectType>(findUpMultiple(async (): Promise => findUpStop, {type: 'directory'})); +expectType>(findUpMultiple(async (): Promise => findUpStop, {stopAt: 'foo'})); + expectType(findUpSync('unicorn.png')); expectType(findUpSync('unicorn.png', {cwd: ''})); expectType(findUpSync(['rainbow.png', 'unicorn.png'])); @@ -82,5 +133,34 @@ expectType(findUpSync((): typeof findUpStop => findUpStop, { expectType(findUpSync((): typeof findUpStop => findUpStop, {type: 'directory'})); expectType(findUpSync((): typeof findUpStop => findUpStop, {stopAt: 'foo'})); +expectType(findUpMultipleSync('unicorn.png')); +expectType(findUpMultipleSync('unicorn.png', {cwd: ''})); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'])); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'], {cwd: ''})); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'], {allowSymlinks: true})); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'], {allowSymlinks: false})); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'], {type: 'file'})); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'], {type: 'directory'})); +expectType(findUpMultipleSync(['rainbow.png', 'unicorn.png'], {stopAt: 'foo'})); +expectType(findUpMultipleSync(() => 'unicorn.png')); +expectType(findUpMultipleSync(() => 'unicorn.png', {cwd: ''})); +expectType(findUpMultipleSync(() => 'unicorn.png', {allowSymlinks: true})); +expectType(findUpMultipleSync(() => 'unicorn.png', {allowSymlinks: false})); +expectType(findUpMultipleSync(() => 'unicorn.png', {type: 'file'})); +expectType(findUpMultipleSync(() => 'unicorn.png', {type: 'directory'})); +expectType(findUpMultipleSync(() => 'unicorn.png', {stopAt: 'foo'})); +expectType(findUpMultipleSync(() => undefined)); +expectType(findUpMultipleSync(() => undefined, {cwd: ''})); +expectType(findUpMultipleSync(() => undefined, {allowSymlinks: true})); +expectType(findUpMultipleSync(() => undefined, {allowSymlinks: false})); +expectType(findUpMultipleSync(() => undefined, {type: 'file'})); +expectType(findUpMultipleSync(() => undefined, {type: 'directory'})); +expectType(findUpMultipleSync(() => undefined, {stopAt: 'foo'})); +expectType(findUpMultipleSync((): typeof findUpStop => findUpStop)); +expectType(findUpMultipleSync((): typeof findUpStop => findUpStop, {cwd: ''})); +expectType(findUpMultipleSync((): typeof findUpStop => findUpStop, {type: 'file'})); +expectType(findUpMultipleSync((): typeof findUpStop => findUpStop, {type: 'directory'})); +expectType(findUpMultipleSync((): typeof findUpStop => findUpStop, {stopAt: 'foo'})); + expectType>(pathExists('unicorn.png')); expectType(pathExistsSync('unicorn.png')); diff --git a/readme.md b/readme.md index 797f806..e3dc9ff 100644 --- a/readme.md +++ b/readme.md @@ -51,6 +51,15 @@ 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 of the array) or `undefined` if none could be found. +### findUpMultiple(name, options?) +### findUpMultiple(matcher, options?) + +Returns a `Promise` for either an array of paths or an empty array if none could be found. + +### findUpMultiple([...name], options?) + +Returns a `Promise` for either an array of the first paths found (by respecting the order of the array) or an empty array if none could be found. + ### findUpSync(name, options?) ### findUpSync(matcher, options?) @@ -60,6 +69,15 @@ Returns a path or `undefined` if it couldn't be found. Returns the first path found (by respecting the order of the array) or `undefined` if none could be found. +### findUpMultipleSync(name, options?) +### findUpMultipleSync(matcher, options?) + +Returns an array of paths or an empty array if none could be found. + +### findUpMultipleSync([...name], options?) + +Returns an array of the first paths found (by respecting the order of the array) or an empty array if none could be found. + #### name Type: `string` diff --git a/test.js b/test.js index f9d3245..ca8b2bd 100644 --- a/test.js +++ b/test.js @@ -6,7 +6,7 @@ import {fileURLToPath} from 'node:url'; import test from 'ava'; import isPathInside from 'is-path-inside'; import tempy from 'tempy'; -import {findUp, findUpSync, findUpStop, pathExists, pathExistsSync} from './index.js'; +import {findUp, findUpSync, findUpMultiple, findUpMultipleSync, findUpStop, pathExists, pathExistsSync} from './index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -14,6 +14,8 @@ const name = { packageDirectory: 'find-up', packageJson: 'package.json', fixtureDirectory: 'fixture', + fooDirectory: 'foo', + barDirectory: 'bar', modulesDirectory: 'node_modules', baz: 'baz.js', qux: 'qux.js', @@ -28,7 +30,8 @@ const relative = { }; relative.baz = path.join(relative.fixtureDirectory, name.baz); relative.qux = path.join(relative.fixtureDirectory, name.qux); -relative.barDir = path.join(relative.fixtureDirectory, 'foo', 'bar'); +relative.barDirQux = path.join(relative.fixtureDirectory, name.fooDirectory, name.barDirectory, name.qux); +relative.barDir = path.join(relative.fixtureDirectory, name.fooDirectory, name.barDirectory); const absolute = { packageDirectory: __dirname, @@ -40,8 +43,9 @@ absolute.fixtureDirectory = path.join( ); absolute.baz = path.join(absolute.fixtureDirectory, name.baz); absolute.qux = path.join(absolute.fixtureDirectory, name.qux); -absolute.fooDir = path.join(absolute.fixtureDirectory, 'foo'); -absolute.barDir = path.join(absolute.fixtureDirectory, 'foo', 'bar'); +absolute.fooDir = path.join(absolute.fixtureDirectory, name.fooDirectory); +absolute.barDir = path.join(absolute.fixtureDirectory, name.fooDirectory, name.barDirectory); +absolute.barDirQux = path.join(absolute.fixtureDirectory, name.fooDirectory, name.barDirectory, name.qux); absolute.fileLink = path.join(absolute.fixtureDirectory, name.fileLink); absolute.directoryLink = path.join(absolute.fixtureDirectory, name.directoryLink); @@ -337,6 +341,90 @@ test('sync (absolute directory)', t => { t.is(filePath, absolute.barDir); }); +test('async multiple (child file)', async t => { + const filePaths = await findUpMultiple(name.qux, {cwd: relative.barDir}); + + t.deepEqual(filePaths, [absolute.barDirQux, absolute.qux]); +}); + +test('sync multiple (child file)', t => { + const filePaths = findUpMultipleSync(name.qux, {cwd: relative.barDir}); + + t.deepEqual(filePaths, [absolute.barDirQux, absolute.qux]); +}); + +test('async multiple (child directory)', async t => { + const foundPath = await findUpMultiple(name.fixtureDirectory, {type: 'directory'}); + + t.deepEqual(foundPath, [absolute.fixtureDirectory]); +}); + +test('sync multiple (child directory)', t => { + const foundPath = findUpMultipleSync(name.fixtureDirectory, {type: 'directory'}); + + t.deepEqual(foundPath, [absolute.fixtureDirectory]); +}); + +test('async multiple (child file, array)', async t => { + const filePaths = await findUpMultiple([name.baz, name.qux], {cwd: relative.barDir}); + + t.deepEqual(filePaths, [absolute.barDirQux, absolute.baz]); +}); + +test('sync multiple (child file, array)', t => { + const filePaths = findUpMultipleSync([name.baz, name.qux], {cwd: relative.barDir}); + + t.deepEqual(filePaths, [absolute.barDirQux, absolute.baz]); +}); + +test('async multiple (child directory, custom cwd, array)', async t => { + const foundPath = await findUpMultiple([name.fixtureDirectory, name.fooDirectory], { + cwd: absolute.barDir, + type: 'directory', + }); + + t.deepEqual(foundPath, [absolute.fooDir, absolute.fixtureDirectory]); +}); + +test('sync multiple (child directory, custom cwd, array)', t => { + const foundPath = findUpMultipleSync([name.fixtureDirectory, name.fooDirectory], { + cwd: absolute.barDir, + type: 'directory', + }); + + t.deepEqual(foundPath, [absolute.fooDir, absolute.fixtureDirectory]); +}); + +test('async multiple (child file with stopAt)', async t => { + const filePaths = await findUpMultiple(name.qux, { + cwd: relative.barDir, + stopAt: absolute.fooDir, + }); + + t.deepEqual(filePaths, [absolute.barDirQux]); +}); + +test('sync multiple (child file with stopAt)', t => { + const filePaths = findUpMultipleSync(name.qux, { + cwd: relative.barDir, + stopAt: absolute.fooDir, + }); + + t.deepEqual(filePaths, [absolute.barDirQux]); +}); + +test('async multiple (not found, child file)', async t => { + const filePaths = await findUpMultiple('somenonexistentfile.js', {cwd: relative.barDir}); + + t.deepEqual(filePaths, []); +}); + +test('sync multiple (not found, child file)', t => { + const filePaths = findUpMultipleSync('somenonexistentfile.js', {cwd: relative.barDir}); + + t.deepEqual(filePaths, []); +}); + test('async (not found, absolute file)', async t => { const filePath = await findUp(path.resolve('somenonexistentfile.js'));