Memory filesystem
Keep files in memory with memoryFileSystem for tests and examples, simulate other backends, and make a read-only view of any backend.
memoryFileSystem() from @better-fs-tools/fs keeps files in memory. It is a full WritableFileSystem, so the read tool and the write tools both work with it. It needs no Node module, and has no dependencies. Use it in tests, in examples, and when your files are already in memory.
npm install @better-fs-tools/fs @better-fs-tools/read @better-fs-tools/writeimport { memoryFileSystem, readOnlyFileSystem } from "@better-fs-tools/fs";
import { createReadTool, textOf } from "@better-fs-tools/read";
import { createWriteTool } from "@better-fs-tools/write";
// Seed files and folders, then change them from the test.
const fs = memoryFileSystem({
files: { "/src/index.ts": "export const a = 1;\n" },
directories: ["/src/empty"],
denyRoots: ["/secrets"],
});
fs.setFile("/secrets/key.pem", "-----BEGIN KEY-----\n");
const read = createReadTool({ fs });
const write = createWriteTool({ fs });
console.log(textOf(await write({ path: "/src/index.ts", content: "export const a = 2;\n" })));
console.log(new TextDecoder().decode(fs.peek("/src/index.ts")?.bytes)); // "export const a = 2;\n"
console.log(textOf(await read({ path: "/secrets/key.pem" })));Updated /src/index.ts (+1 -1 lines).
export const a = 2;
[read:dangerous-path] /secrets/key.pem is refused by policy (/secrets). Choose another path.Paths are absolute POSIX paths. The backend has no allowed roots, so every absolute path is inside. It has no symbolic links. A relative path resolves against /.
Options
| Option | Default | Meaning |
|---|---|---|
files | None | Seed files, keyed by absolute path. A value is a string or a Uint8Array. Parent folders are created |
directories | None | Seed folders. Use it for empty folders |
denyRoots | None | Refused as DANGEROUS_PATH |
id | "memory" | Appears in result.file.backend and in versions |
identity | "required" | "none" reports no identity, as a backend without inodes does |
maxBufferedBytes | 16 MiB | A larger file gives TOO_LARGE, for reads and writes |
chunkBytes | 64 KiB | The size of each chunk that bytes() yields |
streaming | true | false yields the whole file as one chunk, as a buffered backend does |
list | true | false removes list(), so the read tool makes no suggestions and no folder listings |
stage | true | false removes stage(), so apply_patch writes each file in turn |
remove | true | false removes remove(), so a patch cannot delete or move a file |
writeCapabilities | { compareAndSwap: true } | compareAndSwap: false makes the write tools also check the file just before the write |
readOnly | false | true makes every write give read-only |
faults | None | A function that can fail a write, a stage, a publish, or a remove |
The backend always checks the precondition and writes in one synchronous step, even with compareAndSwap: false. A new file gets mode 0o644. A replace keeps the old mode.
Test helpers
The result has helpers that change the files directly, with no tool and no check:
| Helper | Does |
|---|---|
setFile(path, contents, { mode? }) | Sets the bytes and gives the file a new version. Creates parent folders. Keeps the mode of an existing file, else 0o644 |
deleteFile(path) | Removes the file |
makeDirectory(path) | Adds a folder |
setMimeType(path, value) | Sets the MIME type that open() reports, or null |
peek(path) | Returns { bytes, mode, version }, or null when the file does not exist. Use it in assertions |
Use setFile() to change a file after the model read it. The next edit or write then sees a new version.
Simulate other backends
The options let a test act like a weaker backend. A backend that refuses every write:
const locked = memoryFileSystem({ files: { "/notes.txt": "one\n" }, readOnly: true });
const lockedWrite = createWriteTool({ fs: locked });
console.log(textOf(await lockedWrite({ path: "/notes.txt", content: "two\n" })));[write:read-only] /notes.txt is on a read-only filesystem, so it cannot be changed.faults gets the operation and the resolved path, after every other check passed. It returns a MutationError to fail that operation, or null to let it run. A full disk:
const full = memoryFileSystem({
faults: (operation) => (operation === "write" ? { reason: "no-space" } : null),
});
const fullWrite = createWriteTool({ fs: full });
console.log(textOf(await fullWrite({ path: "/notes.txt", content: "two\n" })));[write:no-space] There is no space left to write /notes.txt.To act like a buffered backend such as just-bash, set streaming: false. To act like a backend with no stage(), as every backend except Node and memory, set stage: false.
A read-only view
readOnlyFileSystem(fs) works over every backend, not only the memory backend. It keeps open, list, id, capabilities, paths, and the root settings of the backend. It drops writeCapabilities and every write method. A read tool works as before. A write tool throws when you create it:
try {
// @ts-expect-error A read-only view is not a WritableFileSystem.
createWriteTool({ fs: readOnlyFileSystem(fs) });
} catch (error) {
console.log(String(error));
}TypeError: fs has no write methods: a write tool needs a WritableFileSystemisWritableFileSystem(fs) from @better-fs-tools/fs returns true when fs has writeCapabilities, stat, and write.
The type of the view is ReadOnlyFileSystem<typeof fs>, so TypeScript also refuses it where a WritableFileSystem is needed.