Browse packages

Package · v0.35.3

fs

Standard library filesystem

Filesystem utilities for JavaScript runtimes.

What is @rcompat/fs?#

A cross-runtime module providing filesystem access with a Python pathlib-inspired API. Includes file operations, directory traversal, and project discovery. Works consistently across Node, Deno, and Bun.

File-system routing is owned by consuming frameworks rather than this package. Primate applications use the routing implementation provided by @primate/core.

Installation#

bash
npm install @rcompat/fs
bash
pnpm add @rcompat/fs
bash
yarn add @rcompat/fs
bash
bun add @rcompat/fs

Usage#

FileRef#

A pathlib-like class for file and directory operations.

Creating a FileRef#

js
import fs from "@rcompat/fs";

const file = fs.ref("./src/config.json"); // FileRef("./src/config.json")
const absolute = fs.ref("/etc/hosts");    // FileRef("/etc/hosts")

// resolve relative to cwd
const resolved = fs.resolve("./src");

Path manipulation#

js
import fs from "@rcompat/fs";

const file = fs.ref("./src/components/Button.tsx");

file.path; // "./src/components/Button.tsx"
file.name; // "Button.tsx"
file.base; // "Button"
file.extension; // ".tsx"
file.directory; // FileRef("./src/components")
file.webpath(); // "./src/components/Button.tsx" (forward slashes)

// join paths
file.directory.join("Input.tsx"); // FileRef("./src/components/Input.tsx")

Reading files#

js
import fs from "@rcompat/fs";

const file = fs.ref("./config.json");

// read as text
const text = await file.text();

// read and parse JSON
const config = await file.json();

// read as binary
const bytes = await file.bytes(); // Uint8Array
const buffer = await file.arrayBuffer(); // ArrayBuffer

// read as stream
const stream = file.stream();

// get file size
const size = await file.size();

Writing files#

js
import fs from "@rcompat/fs";

const file = fs.ref("./output.txt");

// write text (auto-adds trailing newline)
await file.write("Hello, world!");

// write JSON (pretty-printed)
await file.writeJSON({ name: "rcompat", version: "1.0.0" });

// write binary
await file.write(new Uint8Array([0x48, 0x69]));

File info#

js
import fs from "@rcompat/fs";

const file = fs.ref("./src");

await file.exists(); // true/false
await file.empty(); // true for zero-byte files and directories without entries
await file.modified(); // modification time (ms)
await file.type(); // "file" | "directory" | "link" | null

empty() follows symbolic links. Missing paths and broken links return false. It is also available as fs.empty(path).

Directory operations#

js
import fs from "@rcompat/fs";

const src = fs.ref("./src");

// list directory contents
const files = await src.files();

// list with filter (shallow, non-recursive)
const ts_files = await src.files({ filter: info => info.extension === ".ts" });

// recursive, with regex matcher
const all_ts_files = await src.files({
  recursive: true,
  filter: /\.ts$/
});

// create directory (recursive by default)
await fs.ref("./dist/assets").create();

// remove directory
await dir.remove();

// copy directory
await dir.copy(fs.ref("./backup"));

Other methods#

js
import fs from "@rcompat/fs";

const file = fs.ref("./src/utils.ts");

// generate content hash
const hash = await file.hash(); // SHA-256 by default
const sha512 = await file.hash("SHA-512");

// dynamic import
const module = await file.import();
const func = await file.import("default");

// find project root (walks up looking for package.json)
const root = await file.discover("package.json");

Project utilities#

js
import fs from "@rcompat/fs";

// find project root directory
const project_root = await fs.cwd().discover("package.json");
// FileRef("/path/to/project")

// get path to package.json
const package_json = project_root.join("package.json");
// FileRef("/path/to/project/package.json")

Streamable values#

The root API recognizes blobs, readable streams, and objects implementing the rcompat stream symbol.

js
import fs from "@rcompat/fs";

// check if a value is streamable
if (fs.isStream(value)) {
  const stream = fs.stream(value);
}

// check if named (has a name property)
if (fs.isNamedStream(value)) {
  console.log(value.name);
}

API Reference#

Path#

ts
type Path = string | FileRef;

FileInfo#

ts
type FileInfo = {
  path: string;
  name: string;
  extension: string;
  type: "file" | "directory" | "link";
};

FileRef#

ts
type Filter = (info: FileInfo) => MaybePromise<boolean>;

type ListOptions = {
  recursive?: boolean;
  filter?: RegExp | Filter;
};

interface FileRef {
  path: string;
  name: string;
  base: string;
  extension: string;
  fullExtension: string;
  directory: FileRef;
  core: string;

  // path operations
  join(...paths: Path[]): FileRef;
  webpath(): string;
  append(suffix: string): FileRef;
  bare(append?: string): FileRef;
  debase(base: Path, suffix?: string): FileRef;

  // reading
  text(): Promise<string>;
  json<T>(): Promise<T>;
  bytes(): Promise<Uint8Array>;
  arrayBuffer(): Promise<ArrayBuffer>;
  stream(): ReadableStream<Uint8Array>;
  size(): Promise<number>;

  // writing
  write(input: string | Uint8Array): Promise<void>;
  writeJSON(input: JSONValue): Promise<void>;

  // file info
  exists(): Promise<boolean>;
  type(): Promise<"file" | "directory" | "link" | null>;
  modified(): Promise<number>;
  empty(): Promise<boolean>;

  // directory operations
  list(options?: ListOptions): Promise<FileRef[]>
  files(options?: ListOptions): Promise<FileRef[]>
  dirs(options?: ListOptions): Promise<FileRef[]>
  create(options?: { recursive?: boolean }): Promise<void>;
  remove(options?: { recursive?: boolean; fail?: boolean }): Promise<void>;
  copy(to: FileRef, filter?: Filter): Promise<void>;

  // other
  hash(algorithm?: string): Promise<string>;
  import(name?: string): Promise<unknown>;
  discover(filename: string): Promise<FileRef>;
  sibling(name: string): FileRef;
  or<T>(fallback: () => T): Promise<this | T>;
  toString(): string;
}

Examples#

Build script#

js
import fs from "@rcompat/fs";

const src = fs.ref("./src");
const dist = fs.ref("./dist");

// clean dist
await dist.remove();

// copy source files
await src.copy(dist, info => info.extension !== ".test.ts");

// process each file
for (const file of await dist.list()) {
  const content = await file.text();
  const transformed = content.replaceAll("__DEV__", "false");
  await file.write(transformed);
}

Config loader#

js
import fs from "@rcompat/fs";

async function loadConfig() {
  const root = await fs.cwd().discover("package.json");
  const config_path = root.join("config.json");

  if (await config_path.exists()) return config_path.json();

  return { /* defaults */ };
}

Asset fingerprinting#

js
import fs from "@rcompat/fs";

async function fingerprint(file) {
  const hash = await file.hash();
  const name = `${file.base}.${hash}${file.extension}`;
  return file.directory.join(name);
}

const original = fs.ref("./dist/bundle.js");
const fingerprinted = await fingerprint(original);
await original.copy(fingerprinted);

Cross-Runtime Compatibility#

Runtime Supported
Node.js ✓
Deno ✓
Bun ✓

No configuration required — just import and use.

License#

MIT

Contributing#

See CONTRIBUTING.md in the repository root.