Files
2026-05-27 23:29:53 +01:00

3.4 KiB

Add Bun-native filesystem adapter to @suaveplan/storage-file

  • Capability: storage-file
  • Status: PROPOSED
  • Design: ./design.md
  • Spec delta: ./specs/storage-file/spec.md

Why

@suaveplan/storage-file currently ships a single NodeFsLike implementation — defaultNodeFs() in src/node-fs/node-fs.ts — bound to node:fs/promises plus the two node:fs stream constructors. Genesis is a Bun-first monorepo. Bun provides a faster, non-emulated filesystem path for the common read-and-write hot loop via Bun.file() / Bun.write() that skips the node:fs emulation layer entirely. Today every read/write inside NodeFsStorage takes the slower compatibility path even when running under Bun.

This change adds a sibling defaultBunFs() factory that satisfies the same NodeFsLike structural contract but routes readFile/writeFile through Bun's native APIs. Operations that have no Bun-specific equivalent (mkdir, readdir, rename, unlink, stat, access, open, createReadStream, createWriteStream) continue to use node:fs/promises — under Bun those calls already dispatch to Bun's native Zig implementation.

resolveConfig gains a runtime-probed default: when the caller does not inject an fs and globalThis.Bun is present, defaultBunFs() is used; otherwise defaultNodeFs() is used. Both factories remain explicitly exported so callers can wire either one directly (e.g. forcing the Node path in tests, or in mixed-runtime SSR pipelines).

What changes

  • New module src/bun-fs/bun-fs.ts exporting defaultBunFs(): NodeFsLike.
    • readFile uses Bun.file(path).bytes().
    • writeFile uses Bun.write(path, data).
    • All other methods delegate to node:fs/promises (the same calls defaultNodeFs() makes), so the surface stays identical.
  • New module src/bun-fs/bun-fs.md (sibling doc, §4 conformant).
  • New module src/bun-fs/bun-fs.test.ts (@suaveplan/testing/runner).
  • New barrel src/bun-fs/index.ts (explicit named re-export per §6a).
  • resolveConfig in src/node-fs-storage/node-fs-storage.helpers.ts gains a Bun-vs-Node default selector. Behaviour is unchanged when callers pass fs explicitly.
  • src/index.ts re-exports defaultBunFs next to defaultNodeFs.
  • package.json exports map gains ./bun-fs/bun-fs.
  • README.md Modules section gets a new row.

Migration

No consumer behaviour changes if fs is passed explicitly. Consumers that relied on the implicit default got a node:fs/promises-backed implementation before this change and continue to get one when running under Node. Under Bun they transparently get the faster path. The NodeFsLike contract, all STORAGE_FILE.* codes, the recordOp instrumentation, and the spec-conformance suite are unchanged.

To force the Node path under Bun (e.g. for parity testing):

import { defaultNodeFs, NodeFsStorage } from "@suaveplan/storage-file";

const storage = new NodeFsStorage({ rootDir, fs: defaultNodeFs() });

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/storage-file to enumerate. As of authoring, @suaveplan/storage-file has no in-repo dependents (it is consumed by the upstream @suaveplan/storage factory via the optional registry path).

  • No in-repo dependents — verified by bunx turbo ls --filter=...@suaveplan/storage-file. Storage-cdn-bunnycdn and storage-cdn-rackspace consume @suaveplan/storage, not storage-file directly; their behaviour is unaffected.