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.
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.
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
- All you need is the nearest package.json directory: pkg-dir wraps exactly that case, and escalade or empathic do the general walk with a smaller footprint
- You are on CommonJS or Node.js under 20: v8 is ESM-only with engines set to Node 20+, so require() users are stuck on find-up@5, which is fine but frozen
- You are really searching downward through a tree for patterns: findDown only goes one level deep by default and takes exact names, so glob work belongs to fast-glob
- You are dependency-averse: the core walk is a short readable loop over path.dirname, and some teams reasonably inline those ten lines rather than adopt another sindresorhus micro-package chain (it pulls locate-path and unicorn-magic)
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
| Package | Registry | Pick it when |
|---|---|---|
| pkg-dir | npm | You specifically want the root directory of the nearest package.json; same author, purpose-built. |
| escalade | npm | You want the same upward walk in a tiny single-file package with sync and async forms and no dependencies. |
| empathic | npm | You want a modern zero-dependency kit of find-up-style path utilities from the escalade author. |