@theholocron/fs-utils
v1.16.0
Published
Walk a directory tree upward to find the nearest match or project root.
Maintainers
Readme
@theholocron/fs-utils
Walk a directory tree upward to find the nearest match or project root.
Installation
pnpm add @theholocron/fs-utilsUsage
import {
findExecutable,
findExecutableSync,
findRoot,
findUpward,
hasGitEntry,
isWorkspaceRoot,
walkFilesSync,
} from "@theholocron/fs-utils";
// nearest ancestor holding a marker file or directory
const repoRoot = await findRoot(process.cwd(), ".git");
const monorepoRoot = await findRoot(process.cwd(), ["pnpm-workspace.yaml", "turbo.json"]);
// find the nearest ancestor holding a config file, stopping at the repo root
const configDir = await findUpward(
process.cwd(),
async (dir) => ((await fileExists(join(dir, "app.config.json"))) ? dir : undefined),
hasGitEntry
);
// a plain boundary check, usable as `findUpward`'s `stop` or on its own
await hasGitEntry("/path/to/dir"); // true if a .git entry (file or folder) exists there
// decide whether a pnpm command needs -w at this directory
await isWorkspaceRoot(repoRoot ?? process.cwd());findUpward(startDir, check, stop?)
Walks upward from startDir — itself first, then its parent, and so on —
calling check(dir) at each directory and returning the first result
that isn't undefined. When given, stop(dir) is checked only after
check has already come back empty for that directory (so it's still
fully checked before stopping) — returning true ends the walk there
with no result, instead of continuing to the parent. Without stop,
walks all the way to the filesystem root. Always terminates: directory
nesting is finite.
findRoot(startDir, marker, options?) / findRootSync(...)
The nearest ancestor of startDir — itself first — that contains marker: a
file or directory name, or a list of names where any one counts. Resolves to
undefined when nothing is found. findRootSync is the synchronous twin.
Each marker must be a bare name — an empty string, ., .. or anything with a
path separator throws a TypeError, as does any filesystem error other than
"not there" (e.g. EACCES). A relative startDir is resolved against the
working directory up front, so results are always absolute.
options.fallback: "start"— return the start directory instead ofundefinedwhen nothing is found (a soft-skip for callers that treat the start dir as the root anyway; always absolute).options.realpath: true— resolve symlinks ofstartDirbefore walking, so a symlinked start walks the physical tree instead of the link's own ancestors. A start path that doesn't exist counts as not found.
// a git root that never throws and never returns undefined
const root = findRootSync(cwd, ".git", { fallback: "start", realpath: true });findExecutable(bin, options?) / findExecutableSync(...)
The first runnable file named bin on options.path (default process.env.PATH),
or undefined — a which. Directories and files without the exec bit don't
count. bin must be a bare command name (an empty string, ., .. or anything
with a path separator throws a TypeError), as does any filesystem error other
than "not there" / "not permitted".
options.path— directories to search, delimited likePATHon the target platform (:on POSIX,;on Windows). Empty entries are skipped; relative ones are resolved against the working directory when the call starts, so results are always absolute.options.pathExt/options.platform— on Windows there is no exec bit:binis tried with eachPATHEXTextension (default.COM;.EXE;.BAT;.CMD) unless it already ends in one. An extension containing a path separator throws aTypeError.
// a project-local binary first, then the real PATH
const tsc = findExecutableSync("tsc", { path: join(cwd, "node_modules", ".bin") }) ?? findExecutableSync("tsc");walkFilesSync(dir, options?)
Every regular file under dir, as absolute paths in a stable (name-sorted,
depth-first) order. options.skip names the directories not entered, at any
depth (default .git and node_modules; it replaces the default); a file
with that name is still listed. Symlinks found while walking are neither
followed nor listed, so a link back up the tree can't loop; dir itself is
followed if it is a symlink. A subdirectory that can't be read is skipped; dir
itself must be readable — its error propagates. The tree is assumed not to
change mid-walk.
const sources = walkFilesSync(repoRoot, { skip: [".git", "node_modules", "dist"] });hasGitEntry(dir)
Returns true when dir has a .git entry — a directory in a normal
checkout, a file in a worktree/submodule (existence is all that matters).
A common stop boundary for findUpward: treat the nearest .git as
the project root, searched but not walked past.
isWorkspaceRoot(dir)
Returns true when dir has a pnpm-workspace.yaml — the file that marks
a pnpm workspace root. Several pnpm CLI commands (pnpm add among them)
behave differently at a workspace root than in a plain package, so a caller
shelling out to pnpm can check this before deciding which flags to pass.
Documentation
Check out The Holocron Archive for more information.
