@fkn/lib/fs is a path-based subset of Node’s fs, so code written against import fs from 'node:fs' runs in the browser on FKN storage once your bundler resolves fs there. This page covers the two ways to point it there, the mount() call a synchronous read needs, what that call costs, and what the subset leaves out.
Your code keeps its node:fs import and gains one call:
Synchronously reads the entire contents of a file.
@param ― path A path to a file. If a URL is provided, it must use the file: protocol.
If a file descriptor is provided, the underlying file will not be closed automatically.
@param ― options Either the encoding for the result, or an object that contains the encoding and an optional flag.
If a flag is not provided, it defaults to 'r'.
readFileSync('library/catalog.json', 'utf8') // '{"items":[]}', a string under Node's overloads
Under the vite plugin’s alias, node:fs and @fkn/lib/fs are one module. The mount() imported here therefore fills the in-memory layer, the copy in memory that the synchronous calls read: every file this device holds in OPFS, the browser’s origin private file system, and, when an account is connected, every file of this app in the account. The account is the FKN identity a person carries between sites. The types stay Node’s, from whatever @types/node your app has, so the read narrows to a string.
@fkn/vite-plugin is the shortest route. With fs on, the default, Node’s fs names resolve to @fkn/lib/fs. The configuration is one fkn() entry in your Vite plugins, see what it wires.
fkn({ fs: false }) drops the fs aliases and keeps the net, dgram and http ones. fs then resolves to an empty module and fs/promises resolves to nothing, so the option suits an app that aliases both names itself, see switch an alias off.
The same four entries go into any bundler’s alias map. The buffer shim that @fkn/lib/fs imports is yours to supply on this route, see what your bundler must supply.
The default export is typed as typeof import('fs'). The named exports carry Node’s signatures, plus available, mount, flush, remount and pull, and @fkn/fs/promises carries the twelve promise members. Neither entry carries adopt, adoptable, pending, pendingDeletes, replicating, onConflict, readFileSealed or any type.
pull(path) writes the account copy of a file, the replicated copy the account holds, into this device’s OPFS. It leaves the in-memory layer alone, so remount() follows it, see the read rule.
Import mount from @fkn/fs on this route rather than from @fkn/lib/fs. @fkn/fs 0.5.1 depends on exactly @fkn/lib 0.9.24, so an app that installs a different @fkn/lib carries two copies of the library. A mount() taken from the app’s copy then fills a layer the aliased fs never reads. That pin is also older than the 0.9.28 this site describes, so we recommend the plugin wherever your build can take it.
Node code assumes a disk under it, and a browser has none. The synchronous calls answer from the in-memory layer instead, and mount() fills it. Before it resolves, a synchronous call sees only what this tab has written: existsSync answers false, and a read throws ENOENT:
For detailed information, see the documentation of the asynchronous version of
this API:
exists
.
fs.exists() is deprecated, but fs.existsSync() is not. The callback parameter to fs.exists() accepts parameters that are inconsistent with other
Node.js callbacks. fs.existsSync() does not use a callback.
import { existsSync } from'node:fs';
if (existsSync('/etc/passwd'))
console.log('The path exists.');
@since ― v0.1.21
existsSync('cache/poster.png') // false before the mount, whatever this device or the account holds
For detailed information, see the documentation of the asynchronous version of
this API:
exists
.
fs.exists() is deprecated, but fs.existsSync() is not. The callback parameter to fs.exists() accepts parameters that are inconsistent with other
Node.js callbacks. fs.existsSync() does not use a callback.
import { existsSync } from'node:fs';
if (existsSync('/etc/passwd'))
console.log('The path exists.');
@since ― v0.1.21
existsSync('cache/poster.png') // true once the mount listed the path, even when its bytes could not be read
The last answer is not Node’s. A path the account lists but this device cannot read yet, such as a locked file, is present without its bytes. existsSync answers true and readdirSync lists it, while readFileSync, statSync, lstatSync, writeFileSync, appendFileSync and renameSync throw storage: <path> exists but could not be read, retry once its scope is available with code set to FKN_E2E_LOCKED, never ENOENT.
The classic if (existsSync(path)) readFileSync(path) pair therefore throws a code no Node program handles. Read with a fallback on ENOENT and rethrow the rest, as error codes shows.
mount() costs what the tree costs. It reads this origin’s whole OPFS root, files other libraries wrote there included, plus every file this app holds in the account, and that set stays in RAM for the life of the tab. The first call scales with that tree rather than with what your app reads, see the in-memory layer.
A synchronous write goes to memory and reports nothing about the store behind it. The bytes reach OPFS on a 250 ms timer, and flush() runs that now but catches every failure and resolves anyway. While a write is still owed, shell.busyReasons() lists unsaved files, see writes and flush().
Most current Node code imports node:fs/promises, and that form needs no mount() at all:
The optional options argument can be a string specifying an encoding, or an
object with an encoding property specifying the character encoding to use for
the filenames. If the encoding is set to 'buffer', the filenames returned
will be passed as Buffer objects.
If options.withFileTypes is set to true, the returned array will contain fs.Dirent objects.
import { readdir } from'node:fs/promises';
try {
constfiles=awaitreaddir(path);
for (constfileof files)
console.log(file);
} catch (err) {
console.error(err);
}
@since ― v10.0.0
@return ― Fulfills with an array of the names of the files in the directory excluding '.' and '..'.
Asynchronously reads the entire contents of a file.
If no encoding is specified (using options.encoding), the data is returned
as a Buffer object. Otherwise, the data will be a string.
If options is a string, then it specifies the encoding.
When the path is a directory, the behavior of fsPromises.readFile() is
platform-specific. On macOS, Linux, and Windows, the promise will be rejected
with an error. On FreeBSD, a representation of the directory's contents will be
returned.
An example of reading a package.json file located in the same directory of the
running code:
Asynchronously writes data to a file, replacing the file if it already exists. data can be a string, a buffer, an
AsyncIterable, or an
Iterable object.
The encoding option is ignored if data is a buffer.
If options is a string, then it specifies the encoding.
The mode option only affects the newly created file. See fs.open() for more details.
Any specified FileHandle has to support writing.
It is unsafe to use fsPromises.writeFile() multiple times on the same file
without waiting for the promise to be settled.
Similarly to fsPromises.readFile - fsPromises.writeFile is a convenience
method that performs multiple write calls internally to write the buffer
passed to it. For performance sensitive code consider using fs.createWriteStream() or filehandle.createWriteStream().
It is possible to use an AbortSignal to cancel an fsPromises.writeFile().
Cancelation is "best effort", and some amount of data is likely still
to be written.
Asynchronously writes data to a file, replacing the file if it already exists. data can be a string, a buffer, an
AsyncIterable, or an
Iterable object.
The encoding option is ignored if data is a buffer.
If options is a string, then it specifies the encoding.
The mode option only affects the newly created file. See fs.open() for more details.
Any specified FileHandle has to support writing.
It is unsafe to use fsPromises.writeFile() multiple times on the same file
without waiting for the promise to be settled.
Similarly to fsPromises.readFile - fsPromises.writeFile is a convenience
method that performs multiple write calls internally to write the buffer
passed to it. For performance sensitive code consider using fs.createWriteStream() or filehandle.createWriteStream().
It is possible to use an AbortSignal to cancel an fsPromises.writeFile().
Cancelation is "best effort", and some amount of data is likely still
to be written.
Asynchronously reads the entire contents of a file.
@param ― path A path to a file. If a URL is provided, it must use the file: protocol.
If a FileHandle is provided, the underlying file will not be closed automatically.
@param ― options An object that may contain an optional flag.
If a flag is not provided, it defaults to 'r'.
The optional options argument can be a string specifying an encoding, or an
object with an encoding property specifying the character encoding to use for
the filenames. If the encoding is set to 'buffer', the filenames returned
will be passed as Buffer objects.
If options.withFileTypes is set to true, the returned array will contain fs.Dirent objects.
import { readdir } from'node:fs/promises';
try {
constfiles=awaitreaddir(path);
for (constfileof files)
console.log(file);
} catch (err) {
console.error(err);
}
@since ― v10.0.0
@return ― Fulfills with an array of the names of the files in the directory excluding '.' and '..'.
readdir('library') // ['catalog.json'], plus anything else the mount listed under library
Every callback and promise form awaits mount() first and then runs the same in-memory operation, so a write through them reaches OPFS on the same timer. Only the *Sync members skip the wait.
The subset leaves out file descriptors and open, close, read, write, streams (createReadStream, createWriteStream), watch and watchFile, symlinks, copyFile, truncate, utimes, chmod, realpath, opendir, mkdtemp, fs.constants and Dirent. Node’s types declare all of these, so fs.createReadStream and the rest type-check on the default import, read undefined at run time and throw a TypeError when called. A named import of one, such as import { createReadStream } from 'node:fs', never reaches a call: the module has no such export, so the import itself is rejected.
A few members are there and differ from Node rather than being absent:
readdir accepts withFileTypes and ignores it, so you get names rather than Dirents
promises.access accepts a mode and ignores it
lstat is stat, so isSymbolicLink() is always false, and a Stats carries only the fields under stats
directories exist in memory only, so an empty directory made with mkdirSync is gone after a reload
The exact signature of every member the subset does carry is in the generated reference for fs and fs/promises. storage covers the file system underneath in full.