4.7 KiB
bun-fs — Bun-native NodeFsLike adapter
(no submodules)
Purpose
bun-fs is a sibling of node-fs that satisfies the
same structural NodeFsLike contract consumed by NodeFsStorage. Where
defaultNodeFs() wires every method to node:fs/promises and the two
node:fs stream constructors, defaultBunFs() overrides only the two
methods that have a measurably faster Bun-native path — readFile (routed
through Bun.file(path).bytes()) and writeFile (routed through
Bun.write(path, data)). Every other method delegates to the Node base
because under Bun those calls already dispatch to Bun's native (Zig)
implementation transparently via the Node-compat layer, leaving no perf on
the table.
This module exists so Suaveplan — which targets Bun as its first
runtime — does not pay the Node-compat reshaping cost on the hot read/write
loop. It is additive: no behaviour changes for callers that inject a custom
fs, and the Node adapter remains the explicit choice when running outside
Bun or when parity testing under Bun.
Features
defaultBunFs(): NodeFsLike— Bun-native adapter. Throws a plainErrorif called outside a Bun runtime.pickDefaultFs(): NodeFsLike— runtime-probed selector. ReturnsdefaultBunFs()whenglobalThis.Bunis defined, otherwisedefaultNodeFs(). The probe runs on every call (never cached) so tests can switch adapters deterministically.- Returns a fresh object per call so callers can selectively override any single method without mutating shared state.
- Zero runtime branching inside individual methods — composition over conditionals.
Usage: Basic Example
import { NodeFsStorage, defaultBunFs } from "@suaveplan/storage-file";
const storage = new NodeFsStorage({
rootDir: "/var/data",
fs: defaultBunFs(),
});
await storage.set("hello", new TextEncoder().encode("world"));
const bytes = await storage.get("hello");
console.log(new TextDecoder().decode(bytes!)); // "world"
Usage: Advanced Example
Combine runtime detection with a custom override — useful when you want
the Bun fast path everywhere except a tracing wrapper around writeFile:
import {
NodeFsStorage,
pickDefaultFs,
type NodeFsLike,
} from "@suaveplan/storage-file";
function tracedFs(): NodeFsLike {
const base = pickDefaultFs();
return {
...base,
async writeFile(path, data) {
const start = performance.now();
try {
await base.writeFile(path, data);
} finally {
console.info("writeFile", path, performance.now() - start);
}
},
};
}
const storage = new NodeFsStorage({
rootDir: "/var/data",
fs: tracedFs(),
});
When you construct NodeFsStorage without passing fs, resolveConfig
calls pickDefaultFs() for you — Bun-first, with Node fallback.
API Reference
defaultBunFs(): NodeFsLike
- Returns a fresh
NodeFsLikewhosereadFileis implemented asBun.file(path).bytes()and whosewriteFileis implemented asBun.write(path, data). Every other method (mkdir,readdir,rename,unlink,stat,access,open,createReadStream,createWriteStream) delegates to thedefaultNodeFs()base. - Throws a plain
Errorwith the messagedefaultBunFs() requires a Bun runtime (globalThis.Bun is undefined)if the probe finds no Bun global. The intended entry point for adapter selection ispickDefaultFs, which performs the probe first.
pickDefaultFs(): NodeFsLike
- Returns
defaultBunFs()whenglobalThis.Bunis an object, otherwisedefaultNodeFs(). Consumed byresolveConfiginnode-fs-storage.helpers.tsto wire the defaultfsforNodeFsStorage.
Implementation Notes
- Streams are NOT routed through
Bun.file(path).stream().BunFile.stream()returns a WebReadableStream, butNodeFsLike.createReadStreamis typed as a NodeReadable. Translating between the two introduces a copy that erases the speedup, so the Nodenode:fsconstructor is used unchanged — it already dispatches to Bun's native impl under Bun. - The detection probe uses
typeof globalThis.Bun === "object"rather than"undefined" !== typeof Bun. The former works without TypeScript ambient globals and is robust to module-scope mocking via(globalThis as { Bun?: unknown }).Bun = undefined. Bun.writeaccepts bothUint8Arrayandstring, matchingNodeFsLike.writeFile's union type. No buffer copying is performed by the adapter.- The adapter has zero
@suaveplan/*runtime dependencies. It is internally consumed bynode-fs-storage.helpers.resolveConfig, which is the only module that should callpickDefaultFsdirectly.