Manage directories
Everything you need to move, list, and clean up files and folders. Same
naming convention as the rest of the package — *Async returns a Promise,
the bare name is synchronous.
Ensure a directory exists
Section titled “Ensure a directory exists”import { ensureDirectoryAsync, ensureDirectory } from "@warlock.js/fs";
await ensureDirectoryAsync("./dist/cache/v2");// creates ./dist, ./dist/cache, ./dist/cache/v2 if any are missing// no-op if they're all already thereIdempotent. Calls mkdir({ recursive: true }) under the hood. You usually
don’t need this before writing a file — putFileAsync, putJsonFileAsync,
atomicWriteAsync, and copyFileAsync all auto-create the immediate
parent of their target. Reach for ensureDirectoryAsync when:
- You’re about to copy several files into a directory and want the
directory itself created up front (
copyDirectoryAsyncalso creates the destination, but the explicit call documents intent). - You’re staging an empty directory for another tool to write into.
- You want the create step to be visible in a multi-stage script.
List children
Section titled “List children”Three variants, all returning full paths joined to the directory you passed in — not bare entry names. This means the results feed straight into the next call:
import { listAsync, listFilesAsync, listDirectoriesAsync } from "@warlock.js/fs";
const everything = await listAsync("./src"); // files + subdirsconst onlyFiles = await listFilesAsync("./src"); // regular files onlyconst onlyDirs = await listDirectoriesAsync("./src"); // directories onlySo this just works:
import { listFilesAsync, getFileAsync } from "@warlock.js/fs";
for (const file of await listFilesAsync("./src/components")) { // file = "./src/components/Button.tsx" — full path const source = await getFileAsync(file); // ...}Non-recursive by design. All three list only the immediate children. If you need to walk a tree, recurse yourself:
import { listAsync, directoryExistsAsync } from "@warlock.js/fs";
async function walk(dir: string): Promise<string[]> { const entries = await listAsync(dir); const results: string[] = [];
for (const entry of entries) { if (await directoryExistsAsync(entry)) { results.push(...(await walk(entry))); } else { results.push(entry); } }
return results;}
const allFiles = await walk("./src");Why no built-in walk? Because every project’s needs differ — extensions
to include, dot-files to skip, depth caps, parallel limits. A hand-rolled
walker takes ten lines and matches your project exactly.
import { copyFileAsync, copyDirectoryAsync } from "@warlock.js/fs";
// File — destination's parent dir is auto-createdawait copyFileAsync("./dist/bundle.js", "./snapshot/v2/bundle.js");
// Directory — fully recursive, preserves the treeawait copyDirectoryAsync("./public", "./dist/public");copyDirectoryAsync uses Node’s cp with recursive: true. Existing
files at the destination are overwritten. Symlinks are preserved (not
dereferenced).
Rename / move
Section titled “Rename / move”import { renameFileAsync } from "@warlock.js/fs";
await renameFileAsync("./tmp/foo.txt", "./final/foo.txt");await renameFileAsync("./old-name-dir", "./new-name-dir"); // works for dirs toorenameFileAsync is a thin wrapper around node:fs/promises’s rename.
It does not auto-create the destination’s parent directory — if
./final doesn’t exist, the call fails. Pair with ensureDirectoryAsync
if you can’t be sure:
import { ensureDirectoryAsync, renameFileAsync } from "@warlock.js/fs";import path from "node:path";
const destination = "./final/foo.txt";await ensureDirectoryAsync(path.dirname(destination));await renameFileAsync("./tmp/foo.txt", destination);Cross-device gotcha. If source and destination are on different
mounts / volumes (e.g. /tmp and /var on Linux, or moving between two
drives on Windows), rename fails with EXDEV. The OS can’t atomically
move bytes across filesystems. The workaround is copy + delete:
import { copyFileAsync, unlinkAsync } from "@warlock.js/fs";
await copyFileAsync("/tmp/foo.txt", "/var/lib/foo.txt");await unlinkAsync("/tmp/foo.txt");Delete
Section titled “Delete”import { unlinkAsync, removeDirectoryAsync } from "@warlock.js/fs";
await unlinkAsync("./obsolete.txt"); // single fileawait removeDirectoryAsync("./dist"); // recursive forceBoth are ENOENT-safe — calling them on a path that doesn’t exist is a
no-op, not an error. Other errors (EACCES permission denied, EBUSY
file in use) still throw.
This means you can write reset-style code without any guards:
import { removeDirectoryAsync, ensureDirectoryAsync } from "@warlock.js/fs";
// Wipe and recreate, regardless of whether ./tmp existed beforeawait removeDirectoryAsync("./tmp");await ensureDirectoryAsync("./tmp");Picking a delete shape
Section titled “Picking a delete shape”| You want to… | Use |
|---|---|
| Drop one file | unlinkAsync(path) |
| Drop a whole tree | removeDirectoryAsync(path) |
| Drop everything in a folder, keep the folder | Iterate, delete each child appropriately |
For the third case:
import { listAsync, fileExistsAsync, unlinkAsync, removeDirectoryAsync,} from "@warlock.js/fs";
for (const entry of await listAsync("./tmp")) { if (await fileExistsAsync(entry)) { await unlinkAsync(entry); } else { await removeDirectoryAsync(entry); }}Common shapes
Section titled “Common shapes”Snapshot a build output
Section titled “Snapshot a build output”import { copyDirectoryAsync } from "@warlock.js/fs";
const target = `./snapshots/${Date.now()}`;await copyDirectoryAsync("./dist", target);// copyDirectoryAsync creates ./snapshots/<timestamp>/ if missingMove artifacts from a staging directory
Section titled “Move artifacts from a staging directory”import { listFilesAsync, renameFileAsync, ensureDirectoryAsync } from "@warlock.js/fs";import path from "node:path";
await ensureDirectoryAsync("./final");for (const staged of await listFilesAsync("./staging")) { const target = path.join("./final", path.basename(staged)); await renameFileAsync(staged, target);}(Works only if ./staging and ./final are on the same mount.)
Related
Section titled “Related”- Read and write files — text + JSON IO.
- Write atomically — for files concurrent readers see.
- Reference / API — full signatures.