Introduction
Every Node project ends up doing the same filesystem chores: create the parent
dir before writing, swallow the ENOENT when deleting something that isn’t there,
write JSON with the right indent, hash a file for a cache key. node:fs makes
you spell all of that out, every time. @warlock.js/fs gives you one object that
just does the obvious right thing.
Meet fs:
import { fs } from "@warlock.js/fs";
await fs.files.putJson("dist/manifest.json", manifest);That one line serialises the object, creates dist/ if it’s missing, and writes
UTF-8 JSON. No mkdir dance, no JSON.stringify(v, null, 2), no "utf-8"
argument to remember. The fs object carries the same defaults everywhere.
The fs object is the whole story
Section titled “The fs object is the whole story”Everything hangs off five entry points. You rarely need more than these:
fs.files.*— read, write, edit, append, copy, move, hash, and inspect files.put,getJson,editJson,appendJsonLine,mergeJson, and more.fs.dirs.*— ensure, empty, remove, copy, walk, list, size, and fingerprint directories.fs.file(path)/fs.dir(path)— lazy, immutable handles when you’d rather hold an object than pass a string around.fs.hash.*— hash a string or buffer (sync) or a file or directory tree (async). SHA-256 by default.fs.exists(path)— type-agnostic existence check, file or directory.
A quick taste of each:
await fs.files.editJson("package.json", (p) => ({ ...p, version: "4.7.0" }));await fs.dirs.empty("cache"); // clear it, keep the dirconst readme = fs.file("docs/README.md"); // lazy — no IO yetfor await (const e of fs.dirs.walk("src")) { /* { path, name, type } */ }Everything with sane defaults
Section titled “Everything with sane defaults”The point of fs isn’t fewer characters — it’s that the defaults are the ones you
actually want. node:fs gives you power and no opinions; fs gives you power
and the obvious behaviour:
- Writes create parent dirs — no
mkdir({ recursive: true })first. - Deletes are ENOENT-safe — removing something that’s already gone is a no-op, not a thrown error.
- Atomic writes on request —
{ atomic: true }does temp-file + rename, so readers never see a half-written file. - JSON that reads back typed —
getJson<T>(), optional Standard Schema validation, and adefaultfor the missing-file case. - Streaming hashes —
fs.hash.file()streams SHA-256, so a 200 MB bundle doesn’t blow the heap. - Read-modify-write in one call —
edit,editJson,mergeJsondo the read → transform → write round-trip for you. - Recursive walk that’s an async iterable —
for awaitover a whole tree.
The two layers
Section titled “The two layers”Versus the usual suspects
Section titled “Versus the usual suspects”A typical Node app assembles a small graveyard of single-purpose libraries to
cover this ground. fs replaces the lot, with zero runtime dependencies
beyond Node’s standard library:
fs-extra— foroutputFile/copy/emptyDir. Nowfs.files.put,fs.files.copy,fs.dirs.empty.mkdirp— for recursivemkdir. Now automatic on every write, orfs.dirs.ensure.rimraf— for recursive, no-throw delete. Nowfs.dirs.remove/fs.files.remove.write-file-atomic— for crash-safe writes. Nowfs.files.put(path, x, { atomic: true }).hasha/md5-file— for file hashing. Nowfs.hash.file/fs.hash.dir.jsonfile— for JSON read/write helpers. Nowfs.files.getJson/putJson, with optional schema validation.
Each of those libraries is fine on its own. The stack of them — six deps, six mental models, six changelogs — is not.
- Installation — add the package to your project.
- Your first write — a five-minute walkthrough that ensures a directory, writes a JSON manifest, reads it back, and hashes it.