Manage directories
Directories are the other half of file work: you stage them before writing,
list them to find things, and wipe them when you’re done. fs.dirs.* is the
async surface for all of it — grouped, ergonomic, and named for what you
actually want.
import { fs } from "@warlock.js/fs";
await fs.dirs.ensure("uploads");Idempotent create. That single line makes uploads/ (and every missing
parent) exist, and does nothing if it already does.
Create, empty, remove
Section titled “Create, empty, remove”Three verbs cover the whole lifecycle, and they read exactly as they behave.
await fs.dirs.ensure("dist/cache/v2"); // create (+ parents), no-op if presentawait fs.dirs.empty("cache"); // clear contents, keep the folderawait fs.dirs.remove("tmp"); // delete the whole treeensure is recursive mkdir — you rarely need it before writing a file,
since fs.files.put auto-creates the parent. Reach for it when you want an
empty directory staged up front, or to make the create step visible in a
script.
empty is the one people reinvent by hand: it removes every child but leaves
the directory itself in place — perfect for a scratch folder you keep reusing.
remove deletes recursively and is safe to call on a path that doesn’t
exist — a missing target is a no-op, not an error. So a wipe-and-recreate
needs no guards:
await fs.dirs.remove("build");await fs.dirs.ensure("build");// or, if the folder should survive: await fs.dirs.empty("build");Inspect a directory
Section titled “Inspect a directory”Ask questions before you act on them.
await fs.dirs.exists("logs"); // booleanawait fs.dirs.isEmpty("logs"); // boolean — no childrenawait fs.dirs.count("logs"); // number of immediate childrenisEmpty is the honest gate for “should I skip this?” and count gives you a
quick tally without materializing the whole listing.
List children
Section titled “List children”list returns the immediate entries; listFiles and listDirs narrow to one
type. All three yield full paths joined to the directory you passed, so
results feed straight into the next call.
const everything = await fs.dirs.list("src"); // files + subdirsconst onlyFiles = await fs.dirs.listFiles("src"); // regular filesconst onlyDirs = await fs.dirs.listDirs("src"); // subdirectoriesSingle-level by default. Pass { recursive: true } to flatten the whole tree
into one array:
const allFiles = await fs.dirs.listFiles("src", { recursive: true });That’s convenient for small trees, but it builds the entire list in memory before you see the first entry. For anything large, stream instead.
Walk a tree in constant memory
Section titled “Walk a tree in constant memory”walk is an async iterator: it yields { path, name, type } one entry at a
time, so memory stays flat no matter how deep or wide the tree is.
for await (const entry of fs.dirs.walk("src", { recursive: true })) { if (entry.type === "file" && entry.name.endsWith(".ts")) { console.log(entry.path); }}You can start processing the first match immediately instead of waiting for the
whole scan — the right default for build steps, indexers, and cleanup passes.
walk also takes { followSymlinks: true } when you want to descend through
links.
Copy and move
Section titled “Copy and move”Both mirror the whole tree and auto-create the destination’s parent.
await fs.dirs.copy("public", "dist/public"); // recursive copyawait fs.dirs.move("staging/build", "releases/current");move is EXDEV-safe: when source and destination live on different volumes
(a /tmp → /var hop on Linux, or two drives on Windows), the OS can’t rename
across filesystems, so fs.dirs.move transparently falls back to copy-then-
delete. You don’t handle EXDEV yourself.
Measure and fingerprint
Section titled “Measure and fingerprint”await fs.dirs.size("dist"); // total bytes, recursiveawait fs.dirs.hash("dist"); // stable fingerprint of the whole treesize sums every file underneath — the number you’d report as “build output
size”. hash produces a stable digest of the tree’s structure and content
(SHA-256 by default): the same files in the same shape always hash the same,
regardless of walk order. It’s the reliable way to answer “did this directory
change since last time?” without diffing entry by entry — see
Hash files for the file-level counterpart.
Prefer an object? Use a handle
Section titled “Prefer an object? Use a handle”When you’re doing several operations against one directory, fs.dir(path) hands
you a lazy Directory — no IO until you call a method, and child handles for
free.
const src = fs.dir("src");
for (const file of await src.listFiles({ recursive: true })) { if (file.extension === ".ts") { console.log(await file.size()); // File handle, per entry }}
const nested = src.dir("components", "ui"); // child Directory handlelistFiles / listDirs on a handle return File[] / Directory[], so you keep
chaining without re-joining paths by hand. See
The fs facade for the full handle surface.
Related
Section titled “Related”- Read and write files — text + JSON IO through
fs.files.*. - The fs facade — the whole
fs.*surface, including handles. - Hash files — fingerprinting for cache invalidation.
- Reference / API — full signatures, including the sync primitives.