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#
npm install @rcompat/fspnpm add @rcompat/fsyarn add @rcompat/fsbun add @rcompat/fsUsage#
FileRef#
A pathlib-like class for file and directory operations.
Creating a FileRef#
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#
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#
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#
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#
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" | nullempty() follows symbolic links. Missing paths and broken links return false.
It is also available as fs.empty(path).
Directory operations#
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#
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#
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.
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#
type Path = string | FileRef;FileInfo#
type FileInfo = {
path: string;
name: string;
extension: string;
type: "file" | "directory" | "link";
};FileRef#
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#
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#
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#
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.