mrkeyoor.com_
Wed 05 Aug 19:53 UTC
npmUtilsupdated 05 Aug 2026

find-up

find-up answers one question CLI tools ask constantly: starting from this directory, where is the nearest package.json, .git, lockfile, or config file in any parent directory? You give it a name (or an array of names, or a matcher function) and it walks up the tree until it finds a match or hits the filesystem root, returning the absolute path or undefined. It has async and sync variants, a multiple-results form, a stopAt boundary, file/directory/both matching, and as of v8 a findDown counterpart that searches descendant directories instead. This is the lookup mechanism inside a large slice of npm's config-loading tooling.

Verdict

The standard way for Node CLIs to locate the nearest config or project root, and at 314M weekly downloads it is already in your tree somewhere. Use it when you need matchers, multiple results, or findDown; if you only ever look for package.json, pkg-dir or ten lines of your own code do the job.

API stability4/5The findUp signature has been steady for years and v8 only added findDown, but frequent ESM and Node-requirement major bumps mean version churn in dependency trees even when the API barely moved.
Docs4/5The README documents every export, option, and the matcher contract with worked examples including the monorepo case; there is no separate site, which is proportionate to the scope.
Maintenance4/5One open item across issues and PRs and a September 2025 push; it is a finished tool from a prolific maintainer, so quiet periods reflect completeness rather than abandonment, though bus factor is one.
Ecosystem5/5Roughly 314M weekly downloads as the config-resolution step inside countless CLIs, and it composes with the same author's locate-path and path-exists family.

Use it if

  • You are building a CLI that must behave correctly from any subdirectory of a project, which means resolving the nearest package.json, tsconfig, lockfile, or custom config upward from process.cwd()
  • You need monorepo-aware logic, like walking up to the workspace root by matching pnpm-workspace.yaml or combining a findUp matcher with findDown to inspect each ancestor's children
  • Your matching is more than a filename: the matcher function form lets you check file contents, permissions, or multiple conditions per directory, and findUpStop lets you bail out early
  • You want both async and sync forms with identical semantics, because config resolution in module init code is often forced to be synchronous
Skip it if

Setup reality

npm install find-up is trivial, two small dependencies, no build step. The real cost is packaging: v8 is ESM-only and requires Node.js 20+, so a CommonJS codebase either stays on find-up@5 or deals with dynamic import(). The API has grown to a dozen exports (findUp, findUpSync, findUpMultiple, findDown, and their variants), and matcher functions have their own rules: return a path to accept, undefined to keep walking, or the findUpStop symbol to abort. Note the major-version churn: this package has had several majors that were mostly Node/ESM requirement bumps, so npm audit dedupe views of your tree may show multiple copies.

Patterns

Find the nearest config file walking upfind-nearest-config

import {findUp} from 'find-up';

const configPath = await findUp('myapp.config.json');

if (!configPath) {
  throw new Error('No myapp.config.json found in this or any parent directory');
}

Returns the absolute path or undefined; it never throws for a missing file. The search starts at process.cwd() unless you pass cwd.

Accept several config filenames in priority orderfirst-of-several-names

import {findUp} from 'find-up';

const path = await findUp(['.myapprc.json', '.myapprc', 'myapp.config.js']);

Order in the array is priority order within each directory, and each directory is fully checked before moving up a level.

Find a directory such as .gitfind-directory

import {findUp} from 'find-up';

const gitDir = await findUp('.git', {type: 'both'});
const nodeModules = await findUp('node_modules', {type: 'directory'});

The default type is 'file', so directory lookups silently miss unless you set type. Use 'both' for .git, which is a file in submodules and worktrees.

Synchronous lookup for module-init codesync-lookup

import {findUpSync} from 'find-up';

const pkgPath = findUpSync('package.json');

Same options as the async form. Fine for CLI startup; avoid on a server's request path since it does blocking stat calls up the tree.

Match with custom logic per directorymatcher-function

import path from 'node:path';
import {pathExists} from 'path-exists';
import {findUp} from 'find-up';

const projectDir = await findUp(async directory => {
  const hasPkg = await pathExists(path.join(directory, 'package.json'));
  const notHome = directory !== process.env.HOME;
  return hasPkg && notHome && directory;
}, {type: 'directory'});

The matcher receives each directory on the way up. Return a path to accept it, undefined to continue, or findUpStop to abort the whole search.

Stop the search at a boundary directorystop-at-boundary

import {findUp} from 'find-up';

const config = await findUp('.eslintrc.json', {
  cwd: '/repos/app/packages/web/src',
  stopAt: '/repos/app',
});

Without stopAt the walk continues to the filesystem root, which can pick up stray configs in a user's home directory, a classic tooling bug.

Collect every match up the treecollect-all-matches

import {findUpMultiple} from 'find-up';

const allEnvFiles = await findUpMultiple('.env', {limit: 5});
// nearest first: ['/a/b/c/.env', '/a/b/.env', '/a/.env']

Results are ordered nearest to farthest, which is the natural cascade order for layered config. limit caps how many are returned.

Search downward into child directoriesfind-down-descendants

import {findDown} from 'find-up';

const example = await findDown('example.js', {
  cwd: '/repos/app',
  depth: 2,
});

findDown arrived in v8 and defaults to depth 1 (direct children only). It takes exact names, not globs; deep pattern searches want fast-glob instead.

Find the monorepo root via findUp plus findDownmonorepo-root-detection

import {findUp, findDown} from 'find-up';

const workspaceRoot = await findUp(async directory => {
  const marker = await findDown('pnpm-workspace.yaml', {cwd: directory, depth: 1});
  return marker && directory;
}, {type: 'directory'});

This is the README's own recipe shape: walk up, and at each ancestor peek one level down for a workspace marker file.

Abort the walk early with findUpStopabort-early-with-stop-symbol

import path from 'node:path';
import {findUp, findUpStop} from 'find-up';

const result = await findUp(directory => {
  if (path.basename(directory) === 'work') {
    return findUpStop;
  }
  return 'package.json';
});

Returning findUpStop makes findUp resolve undefined immediately, a performance escape hatch when cwd can be very deeply nested.

Alternatives

PackageRegistryPick it when
pkg-dirnpmYou specifically want the root directory of the nearest package.json; same author, purpose-built.
escaladenpmYou want the same upward walk in a tiny single-file package with sync and async forms and no dependencies.
empathicnpmYou want a modern zero-dependency kit of find-up-style path utilities from the escalade author.