API reference
The complete public surface of @warlock.js/fs. It’s organized the way you
should reach for it: the fs facade is the main event —
the async, ergonomic surface you’ll use for almost everything. The
low-level primitives sit underneath it
as the synchronous escape hatch and the building blocks the facade delegates to.
The fs facade
Section titled “The fs facade”import { fs } from "@warlock.js/fs";fs is a single async object. Four groups hang off it — fs.files.* for
files, fs.dirs.* for directories, fs.file(path) / fs.dir(path) for lazy
handles, and fs.hash.* / fs.exists() for the odds and ends. Every method is
async by design; synchronous callers drop to the
primitives.
fs.files.* and fs.dirs.* do not sandbox paths. For a folder you do not
control, read through fs.confined(root).
fs.files.* — file operations
Section titled “fs.files.* — file operations”Read:
get(path: string, options?: ReadOptions): Promise<string | Buffer> // string; Buffer when { encoding: null }getJson<T>(path: string, options?: ReadJsonOptions<T>): Promise<T> // { schema } validates, { default } on ENOENTWrite:
put(path: string, content: string | Buffer, options?: WriteOptions): Promise<void>putJson(path: string, value: unknown, options?: WriteJsonOptions): Promise<void>create(path: string, content: string | Buffer, options?: WriteOptions): Promise<void> // put with { overwrite: false }createJson(path: string, value: unknown, options?: WriteJsonOptions): Promise<void> // putJson with { overwrite: false }Append / prepend:
append(path: string, content: string): Promise<void>prepend(path: string, content: string): Promise<void>appendLine(path: string, line: string): Promise<void> // newline-terminatedappendJsonLine(path: string, value: unknown): Promise<void> // NDJSON — one JSON record per lineTransform in place:
edit(path: string, editor: (content: string) => string | Promise<string>): Promise<void>editJson<T>(path: string, editor: (value: T) => T | Promise<T>): Promise<void>mergeJson<T>(path: string, partial: Partial<T>, options?: MergeJsonOptions): Promise<void> // shallow; { deep } to recurseensureJson<T>(path: string, fallback: T): Promise<T> // read, or create-and-return the fallbackExistence / shape / lifecycle:
exists(path: string): Promise<boolean>isEmpty(path: string): Promise<boolean>size(path: string): Promise<number>ensure(path: string): Promise<void> // create-if-missing — never truncatestouch(path: string): Promise<void>remove(path: string): Promise<void> // ENOENT-safeMetadata:
stats(path: string): Promise<FileStats>lastModified(path: string): Promise<Date>hash(path: string, algorithm?: HashAlgorithm): Promise<string>checksumMatches(path: string, expected: string, algorithm?: HashAlgorithm): Promise<boolean>Move / copy / stream:
copy(from: string, to: string, options?: CopyOptions): Promise<void>move(from: string, to: string, options?: MoveOptions): Promise<void> // EXDEV-safe: falls back to copy+deletereadLines(path: string): AsyncIterable<string> // for await (const line of fs.files.readLines(p))fs.dirs.* — directory operations
Section titled “fs.dirs.* — directory operations”Lifecycle:
ensure(path: string): Promise<void> // recursive, idempotentremove(path: string): Promise<void> // recursive deleteempty(path: string): Promise<void> // clear contents, keep the directoryExistence / shape:
exists(path: string): Promise<boolean>isEmpty(path: string): Promise<boolean>count(path: string): Promise<number> // immediate childrensize(path: string): Promise<number> // total bytes, recursiveMove / copy / metadata:
copy(from: string, to: string, options?: CopyOptions): Promise<void>move(from: string, to: string, options?: MoveOptions): Promise<void> // EXDEV-safemoveNoReplace(from, to, options?): Promise<MoveDirectoryNoReplaceResult> // = moveDirectoryNoReplaceAsyncstats(path: string): Promise<FileStats>hash(path: string, algorithm?: HashAlgorithm): Promise<string> // stable tree fingerprintmoveNoReplace claims a destination among cooperating writers. It is not an atomic no-replace rename against arbitrary processes: an empty placeholder swapped before its first identity read or after its final check can be silently replaced. See directory move limits before using it with concurrent filesystem writers.
Listing / walking:
list(path: string, options?: ListOptions): Promise<string[]> // files + subdirectorieslistFiles(path: string, options?: ListOptions): Promise<string[]>listDirs(path: string, options?: ListOptions): Promise<string[]>walk(path: string, options?: WalkOptions): AsyncIterable<WalkEntry> // { path, name, type }fs.file(path): File
Section titled “fs.file(path): File”A lazy, immutable handle to a file path. No IO runs in the constructor, and
mutating methods (copy / copyTo / move / moveTo / rename) return a
new handle rather than changing this one.
const file = fs.file("cache/report.json");const renamed = await file.rename("report.v2.json"); // `file` still points at report.jsonProperties:
file.path: stringfile.name: string // "report.json"file.basename: string // "report"file.extension: string // ".json"file.parent(): DirectoryMethods mirror fs.files.* (path-bound):
get(options?) / getJson<T>(options?)put(content, options?) / putJson(value, options?)append(content) / prepend(content) / appendJsonLine(value)edit(editor) / editJson(editor) / mergeJson(partial, options?)exists() / isEmpty() / ensure() / touch() / remove()stats() / size() / lastModified() / hash(algorithm?)readLines(): AsyncIterable<string>copy(to: string): Promise<File> copyTo(dir: string): Promise<File>move(to: string): Promise<File> moveTo(dir: string): Promise<File>rename(name: string): Promise<File>fs.dir(path): Directory
Section titled “fs.dir(path): Directory”A lazy, immutable handle to a directory path. file(...segments) and
dir(...segments) compose child handles without touching disk.
const uploads = fs.dir("storage/uploads");const avatar = uploads.dir("users").file("42", "avatar.png"); // storage/uploads/users/42/avatar.pngProperties:
dir.path: stringdir.name: stringdir.parent(): Directorydir.file(...segments: string[]): Filedir.dir(...segments: string[]): DirectoryMethods mirror fs.dirs.* (path-bound):
ensure() / remove() / empty()exists() / isEmpty() / count()stats() / size() / hash(algorithm?)list(options?) / listFiles(options?): Promise<File[]> listDirs(options?): Promise<Directory[]>walk(options?): AsyncIterable<WalkEntry>copy(to: string): Promise<Directory> move(to: string): Promise<Directory>fs.confined(root, options?): ConfinedReader
Section titled “fs.confined(root, options?): ConfinedReader”A reader that cannot leave root — see Read a folder confined.
Also exported as createConfinedReader(root, options?). The root is resolved
with realpath once, synchronously, when the reader is created.
type ConfinedReaderOptions = { followLinks?: boolean; // default false — refuse every link allowSymlinkedRoot?: boolean; // default false maxFileBytes?: number; // default 16 MiB (CONFINED_DEFAULT_MAX_FILE_BYTES) maxEntries?: number; // default 100_000 (CONFINED_DEFAULT_MAX_ENTRIES) maxDepth?: number; // default 64 (CONFINED_DEFAULT_MAX_DEPTH)};
reader.root: string // canonical rootreader.readText(path, { encoding?, maxFileBytes? }): Promise<string>reader.readBuffer(path, { maxFileBytes? }): Promise<Buffer>reader.readJson<T>(path, { schema?, maxFileBytes? }): Promise<T>reader.stat(path): Promise<ConfinedStats> // { path, name, type: "file" | "directory" | "other", size, lastModified }reader.exists(path): Promise<boolean> // false only for ENOENT / ENOTDIRreader.list(path?, { maxEntries? }): Promise<ConfinedEntry[]>reader.walk(path?, { maxEntries?, maxDepth? }): AsyncGenerator<ConfinedEntry>
type ConfinedEntry = { path: string; // root-relative, "/"-separated name: string; type: "file" | "directory" | "symlink" | "other"; depth: number;};
class FsPathEscapeError extends Error { type: "path-escape"; path: string; root: string; reason: "absolute" | "parent-traversal" | "invalid" | "link-escape";}class FsLinkRefusedError extends Error { type: "link-refused"; path: string; root: string; linkPath: string; reason: "symlink" | "root-symlink" | "identity-changed";}class FsLimitExceededError extends Error { type: "limit-exceeded"; path: string; limit: "maxFileBytes" | "maxEntries" | "maxDepth"; max: number; actual: number;}
isPathInside(child: string, root: string): boolean // segment-wise containment of two absolute pathsfs.exists · fs.hash.*
Section titled “fs.exists · fs.hash.*”fs.exists(path: string): Promise<boolean> // type-agnostic — file OR directory
fs.hash.string(content: string, algorithm?): string // sync — pure, in-memoryfs.hash.buffer(bytes: Buffer | Uint8Array, algorithm?): string // syncfs.hash.file(path: string, algorithm?): Promise<string> // async — reads from diskfs.hash.dir(path: string, algorithm?): Promise<string> // async — stable tree fingerprintAll hashing defaults to sha256.
Option & result types
Section titled “Option & result types”Every option bag is a plain object, exported from the package root.
type ReadOptions = { encoding?: BufferEncoding | null }; // encoding: null → Buffer
type ReadJsonOptions<T = unknown> = { schema?: StandardSchemaV1<T>; // validate the parsed value default?: T; // returned on ENOENT instead of throwing};
type WriteOptions = { encoding?: BufferEncoding; // string writes; default "utf-8" atomic?: boolean; // temp file + rename ensureDir?: boolean; // create parents; default true overwrite?: boolean; // false → throw if target exists; default true};
type WriteJsonOptions = WriteOptions & { indent?: number }; // default 2type MergeJsonOptions = WriteJsonOptions & { deep?: boolean }; // shallow by default
type CopyOptions = { overwrite?: boolean; // default true errorOnExist?: boolean; // default false dereference?: boolean; // follow symlinks; default false};
type MoveOptions = { overwrite?: boolean; ensureDir?: boolean }; // ensureDir default true
type ListOptions = { recursive?: boolean };type WalkOptions = { recursive?: boolean; followSymlinks?: boolean };type WalkEntry = { path: string; name: string; type: "file" | "directory"; // discriminated by `type`, never `kind`};
type FileStats = { path: string; name: string; size: number; type: "file" | "directory"; lastModified: Date; raw: import("node:fs").Stats; // escape hatch to the raw stats};Schema validation on JSON reads is validator-agnostic — @warlock.js/fs ships
with zero dependencies, so it speaks the neutral
Standard Schema contract instead of importing any
validator.
interface StandardSchemaV1<Output = unknown> { readonly "~standard": {/* version, vendor, validate */};}
class JsonSchemaValidationError extends Error { readonly path: string; readonly issues: ReadonlyArray<StandardSchemaIssue>;}Any Standard-Schema-compliant validator — @warlock.js/seal, zod, valibot —
can be passed as getJson’s schema. On failure it throws
JsonSchemaValidationError carrying the raw issues.
Low-level primitives (sync + *Async)
Section titled “Low-level primitives (sync + *Async)”The flat functions the facade is built on. Reach for these when you need a
synchronous call (the facade is async-only) or want the thinnest possible
wrapper over node:fs. Each comes in a bare (sync) and *Async flavor.
Read / write / JSON
Section titled “Read / write / JSON”getFileAsync(path: string): Promise<string> getFile(path: string): stringgetJsonFileAsync<T>(path: string): Promise<T> getJsonFile<T>(path: string): TputFileAsync(filePath: string, content: string): Promise<void> putFile(filePath, content): voidputJsonFileAsync(filePath: string, value: unknown): Promise<void> putJsonFile(filePath, value): voidput* create missing parents, overwrite existing files, and are text-only
— for binary or crash-safe writes use the atomic writers.
Atomic write (async only)
Section titled “Atomic write (async only)”atomicWriteAsync(filePath: string, content: string | Buffer): Promise<void> // temp file + renameatomicWriteJsonAsync(filePath: string, value: unknown): Promise<void> // pretty-printed, 2-spaceDirectories
Section titled “Directories”ensureDirectoryAsync(path: string): Promise<void> ensureDirectory(path: string): void // recursive, idempotentremoveDirectoryAsync(path: string): Promise<void> removeDirectory(path: string): void // recursive, ENOENT-safeListing
Section titled “Listing”listAsync(dir: string): Promise<string[]> list(dir: string): string[] // files + subdirslistFilesAsync(dir: string): Promise<string[]> listFiles(dir: string): string[]listDirectoriesAsync(dir: string): Promise<string[]> listDirectories(dir: string): string[]All return full paths joined to the directory.
Copy / rename
Section titled “Copy / rename”copyFileAsync(source, destination): Promise<void> copyFile(source, destination): void // creates dest parentcopyDirectoryAsync(source, destination): Promise<void> copyDirectory(source, destination): void // recursiverenameFileAsync(from, to): Promise<void> renameFile(from, to): void // no auto-parent; EXDEV on cross-mountDelete
Section titled “Delete”unlinkAsync(path: string): Promise<void> unlink(path: string): void // single file, ENOENT-safe(Recursive deletes live on removeDirectory* above.)
lastModifiedAsync(path: string): Promise<Date> lastModified(path: string): Date // mtimestatsAsync(path: string): Promise<import("node:fs").Stats> stats(path): import("node:fs").StatsExistence
Section titled “Existence”pathExistsAsync(path: string): Promise<boolean> pathExists(path: string): boolean // file OR directoryfileExistsAsync(path: string): Promise<boolean> fileExists(path: string): boolean // file only (follows symlinks)directoryExistsAsync(path: string): Promise<boolean> directoryExists(path: string): boolean // directory onlyHashing
Section titled “Hashing”type HashAlgorithm = "sha256" | "sha1" | "md5" | "sha512"; // default "sha256"
hashFileAsync(path: string, algorithm?: HashAlgorithm): Promise<string> // streaming — constant memoryhashFileSmallAsync(path: string, algorithm?: HashAlgorithm): Promise<string> // one-shot; small files onlyhashFile(path: string, algorithm?: HashAlgorithm): string // sync; small files onlyhashString(content: string, algorithm?: HashAlgorithm): string // in-memoryhashBuffer(content: Buffer | Uint8Array, algorithm?: HashAlgorithm): string // in-memorySee also
Section titled “See also”- The fs facade — why
fs.*is the way in. - Read and write files — the IO walkthrough.
- Write atomically — the atomic-write deep dive.
- Manage directories — directory operations end to end.
- Hash files — hashing strings, buffers, files, and trees.
- Read a folder confined —
fs.confinedand its guarantees.
Source: @warlock.js/fs/src/
Multi-file transactions
Section titled “Multi-file transactions”See Multi-file transactions for options, errors, replay, recovery and durability limits.
fs.transaction(root, async (tx) => { /* await tx.read/write/remove */ }, options?)fs.openTransactions(root, { durability?, lockTimeoutMs? }) // Promise<TransactionRoot>fs.inspectTransactions(root) // Promise<TransactionInspection>, read-onlyfs.read(path, { root, consistent: true, attempts?, retryDelayMs? }) // Promise<string>
// TransactionRootstore.transaction(callback, options?)store.inspect()store.read(path, { consistent?, attempts?, retryDelayMs? })store.readMany(paths, { consistent?, attempts?, retryDelayMs? })
// FileTransactiontx.operationId; tx.epoch;tx.read(path) // Promise<string | null>tx.readBuffer(path) // Promise<Buffer | null>tx.hash(path) // Promise<string | null>, SHA-256 of staged viewtx.write(path, stringOrBuffer, { expectedHash?: string | null })tx.remove(path, { expectedHash?: string | null })Flat async exports: transactionAsync, openTransactionRootAsync,
inspectTransactionsAsync, readTransactionFileAsync. The optional third
argument of atomicWriteAsync is { durability?: "file" | "full" }.