The helpers
A quick tour of everything in the box, grouped by what they do rather than which file they live in. If you’ve read Your first write, this is the next step — same vocabulary, full surface.
Reading text and JSON
Section titled “Reading text and JSON”import { getFileAsync, getFile, getJsonFileAsync, getJsonFile } from "@warlock.js/fs";
const text = await getFileAsync("./config.toml"); // string (UTF-8)const sync = getFile("./config.toml"); // string
const data = await getJsonFileAsync<MyConfig>("./config.json"); // Tconst dataSync = getJsonFile<MyConfig>("./config.json"); // TThrows on missing files (ENOENT) and on malformed JSON. Don’t try/catch
ENOENT as control flow — use the existence checks below.
Writing text and JSON
Section titled “Writing text and JSON”import { putFileAsync, putFile, putJsonFileAsync, putJsonFile } from "@warlock.js/fs";
await putFileAsync("./out/log.txt", "hello\n");await putJsonFileAsync("./out/state.json", { ok: true });Both variants:
- Create parent directories recursively.
- Overwrite existing files (last writer wins).
- JSON variants pretty-print at 2-space indent.
For files other processes read concurrently, use atomic writes instead.
Atomic writes
Section titled “Atomic writes”import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs";
await atomicWriteAsync("./config.toml", configString);await atomicWriteAsync("./binary.bin", Buffer.from([0x01, 0x02])); // accepts Buffer tooawait atomicWriteJsonAsync("./manifest.json", { version: "1.0.0" });Writes to a uniquely-named temp file in the same directory, then renames onto the target. Readers see the old content or the complete new content, never anything in between. There’s no sync variant — atomic writes are always async, because they’re worth the await.
Full mechanics in Atomic vs non-atomic and Write atomically.
Directories
Section titled “Directories”import { ensureDirectoryAsync, ensureDirectory, removeDirectoryAsync, removeDirectory,} from "@warlock.js/fs";
await ensureDirectoryAsync("./dist/cache/v2"); // mkdir -p, idempotentawait removeDirectoryAsync("./dist"); // rm -rf, ENOENT-safeensureDirectoryAsync is a no-op if the directory already exists.
removeDirectoryAsync is a no-op if the target doesn’t exist.
Listing children
Section titled “Listing children”import { listAsync, listFilesAsync, listDirectoriesAsync } from "@warlock.js/fs";
await listAsync("./src"); // every immediate child, full pathsawait listFilesAsync("./src"); // only regular filesawait listDirectoriesAsync("./src"); // only subdirectoriesAll three return full paths joined to the directory you passed, not bare entry names — feed them straight into the next call. Non-recursive by design; if you need a deep walk, recurse yourself (there’s a snippet in Manage directories).
Copying and renaming
Section titled “Copying and renaming”import { copyFileAsync, copyDirectoryAsync, renameFileAsync,} from "@warlock.js/fs";
await copyFileAsync("./src.txt", "./dst/copy.txt"); // parent dirs auto-createdawait copyDirectoryAsync("./public", "./dist/public"); // recursiveawait renameFileAsync("./tmp/foo", "./final/foo"); // works for files and dirsCross-mount renames may fail with EXDEV — for cross-device moves, copy
then delete.
Deleting
Section titled “Deleting”import { unlinkAsync, removeDirectoryAsync } from "@warlock.js/fs";
await unlinkAsync("./obsolete.txt"); // single file, ENOENT-safeawait removeDirectoryAsync("./dist"); // recursive + force, ENOENT-safeBoth swallow “not found” errors. Other errors (EACCES, EBUSY) still
throw — if you’re catching, you’re catching a real problem.
Existence checks
Section titled “Existence checks”Three variants, pick the strictest one that answers your question:
import { pathExistsAsync, fileExistsAsync, directoryExistsAsync } from "@warlock.js/fs";
await pathExistsAsync("./anything"); // true if file OR directoryawait fileExistsAsync("./config.toml"); // true only if a regular fileawait directoryExistsAsync("./dist"); // true only if a directoryUse these to gate creation, not as a try/catch replacement for read errors (though they do read more cleanly than that pattern). Sync variants exist under the bare names.
Metadata
Section titled “Metadata”import { lastModifiedAsync, statsAsync } from "@warlock.js/fs";
const mtime = await lastModifiedAsync("./bundle.js"); // Dateconst all = await statsAsync("./bundle.js"); // fs.StatslastModified is sugar around stat().mtime. Reach for stats when you
need size, mode bits, or any of the other fs.Stats fields.
Hashing
Section titled “Hashing”import { hashFileAsync, hashString, hashBuffer, hashFileSmallAsync } from "@warlock.js/fs";
await hashFileAsync("./bundle.js"); // streaming, SHA-256, hexhashString("hello world"); // in-memory stringhashBuffer(Buffer.from([1, 2, 3])); // in-memory bytesawait hashFileSmallAsync("./tiny.svg"); // one-shot read, < ~1 MBAll four accept a second arg picking the algorithm:
"sha256" | "sha1" | "md5" | "sha512". Default is sha256. Full
walkthrough in Hash files.
Sync vs async — when to pick which
Section titled “Sync vs async — when to pick which”| Context | Use |
|---|---|
| Server / app runtime | *Async always — keep the event loop free |
| CLI scripts | Either — blocking sync is usually fine, and reads cleaner |
| Code generators, build scripts | Sync is fine — it’s a one-shot process |
| Config loaders at startup | Sync — there’s nothing else to do yet |
When in doubt: async. The cost is one await keyword; the benefit is
that you never accidentally block a server’s request handler.
- Atomic vs non-atomic — picking between
putFileAsyncandatomicWriteAsync. - Reference / API — full export list with signatures.