The fs object
Everything in this package hangs off one object: fs. Import it once, and
files, directories, hashing, and existence checks are all one short call
away.
import { fs } from "@warlock.js/fs";
await fs.files.put("out/log.txt", "hello\n");That’s the whole idea. You don’t hunt for a function named
putFileAsync — you reach for fs, then say what you’re touching. This
page is the map: five namespaces, one naming rule, and a couple of things
fs deliberately does not do.
The shape of fs
Section titled “The shape of fs”fs groups its surface by what you’re acting on, so the call reads like
a sentence.
fs.files.* — everything you do to a file. Read, write, JSON, append,
edit-in-place, hash, stat, copy, move.
await fs.files.getJson<Config>("config.json");fs.dirs.* — everything you do to a directory. Ensure, empty, remove,
list, walk, size, hash the whole tree.
await fs.dirs.ensure("uploads/images");fs.file(path) and fs.dir(path) — a handle: a small object bound to
one path, with the same methods hanging off it. Handy when you’ll touch
the same path several times.
const pkg = fs.file("package.json");await pkg.editJson((p) => ({ ...p, version: "4.7.0" }));fs.hash.* — hashing. Strings and buffers hash in memory; files and
directories stream from disk.
fs.hash.string("hello"); // syncawait fs.hash.file("bundle.js"); // asyncfs.exists(path) — a type-agnostic existence check. True whether the path
is a file or a directory.
if (await fs.exists("cache")) { /* something is there */ }Each namespace has its own page. Jump wherever your task lives:
- Files —
fs.files.*in depth. - Directories —
fs.dirs.*in depth. - Lazy handles —
fs.file()/fs.dir(). - Hashing —
fs.hash.*.
Async-first — the one naming rule
Section titled “Async-first — the one naming rule”Every method on fs returns a Promise. There is no fs.files.getSync,
no Async suffix to remember — the facade is async, full stop, so you
just await it.
const text = await fs.files.get("config.toml"); // Promise<string>Under the hood, though, this package ships two layers of every operation:
a bare synchronous primitive (getFile, putFile, ensureDirectory, …)
and an async one (getFileAsync, putFileAsync, …). The facade is built
on the async layer and hides the suffix entirely.
You only meet that layer when you genuinely can’t await — a code
generator, a config loader at startup, a one-shot CLI script. There, drop
to the bare synchronous primitives:
import { getFile, putJsonFile } from "@warlock.js/fs";
const raw = getFile("config.toml"); // blocking, no awaitputJsonFile("out.json", { ok: true });That’s the whole rule: fs.* is async; the bare * primitives are the
sync escape hatch. The *Async primitives exist too, but the facade is
the friendlier way to reach them — reach for fs first.
”I want to…” → use
Section titled “”I want to…” → use”| I want to… | Use |
|---|---|
| Read a text file | fs.files.get(path) |
| Read + parse JSON (typed) | fs.files.getJson<T>(path) |
| Write a file (create parents) | fs.files.put(path, content) |
| Write JSON | fs.files.putJson(path, value) |
| Patch a JSON object | fs.files.mergeJson(path, partial) |
| Transform a file in place | fs.files.edit(path, fn) |
| Make a directory (mkdir -p) | fs.dirs.ensure(path) |
| Empty a directory, keep it | fs.dirs.empty(path) |
| Delete a directory tree | fs.dirs.remove(path) |
| List / walk a tree | fs.dirs.list(path) / fs.dirs.walk(path) |
| Hash a string or buffer | fs.hash.string(s) / fs.hash.buffer(b) |
| Hash a file or a whole tree | fs.hash.file(path) / fs.hash.dir(path) |
| Check if anything is there | fs.exists(path) |
| Hold one path and reuse it | fs.file(path) / fs.dir(path) |
| Read/write synchronously | bare getFile / putFile primitives |
Options ride along the last argument
Section titled “Options ride along the last argument”Most calls take an options object as their final argument. Same handful of names show up everywhere, so once you learn them once you know them everywhere.
await fs.files.put("cache/data.json", "{}", { atomic: true, // write to a temp file, then rename onto target overwrite: false, // fail instead of clobbering an existing file ensureDir: true, // create parent directories first});atomic— readers see the old content or the complete new content, never a half-written file. See Atomic vs non-atomic.overwrite— setfalsefor create-if-missing semantics (that’s whatfs.files.createdoes for you).ensureDir— create the parent path before writing.recursive— onfs.dirs.list/walk, descend into subdirectories.schema+default— onfs.files.getJson, validate against a Standard Schema and fall back when the file is missing.
What fs will not do for you
Section titled “What fs will not do for you”- Atomic vs non-atomic — when to flip
{ atomic: true }on a write. - The fs facade — the full facade surface, including lazy handles and read-modify-write helpers.
- Reference / API — every export with signatures.