Skip to content

Node fs polyfill

@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:

app.ts
import
module "node:fs"
fs
from 'node:fs'
import {
const mount: () => Promise<void>
mount
} from '@fkn/lib/fs'
await
function mount(): Promise<void>
mount
() // the synchronous calls can see storage after this
module "node:fs"
fs
.
function writeFileSync(file: fs.PathOrFileDescriptor, data: string | NodeJS.ArrayBufferView, options?: fs.WriteFileOptions): void

Returns undefined.

The mode option only affects the newly created file. See

open

for more details.

For detailed information, see the documentation of the asynchronous version of this API:

writeFile

.

@sincev0.1.29

@paramfile filename or file descriptor

writeFileSync
('library/catalog.json',
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
({
items: never[]
items
: [] }))
module "node:fs"
fs
.
function readFileSync(path: fs.PathOrFileDescriptor, options: {
encoding: BufferEncoding;
flag?: string | undefined;
} | BufferEncoding): string (+2 overloads)

Synchronously reads the entire contents of a file.

@parampath 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.

@paramoptions 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.

@fkn/fs packages the same file system under Node’s types, for a build without the plugin. Install it beside @fkn/lib, see install:

Terminal window
npm install @fkn/fs

Then alias all four specifiers, the node: forms included, since an alias matches the specifier as written:

vite.config.ts
import {
function defineConfig(config: UserConfig): UserConfig (+5 overloads)

Type helper to make it easier to use vite.config.ts accepts a direct

UserConfig

object, or a function that returns it. The function receives a

ConfigEnv

object.

defineConfig
} from 'vite'
export default
function defineConfig(config: UserConfig): UserConfig (+5 overloads)

Type helper to make it easier to use vite.config.ts accepts a direct

UserConfig

object, or a function that returns it. The function receives a

ConfigEnv

object.

defineConfig
({
resolve?: AllResolveOptions | undefined
resolve
: {
alias?: AliasOptions | undefined
alias
: {
fs: string
fs
: '@fkn/fs',
'fs/promises': '@fkn/fs/promises',
'node:fs': '@fkn/fs',
'node:fs/promises': '@fkn/fs/promises',
},
},
})

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:

app.ts
import
module "node:fs"
fs
from 'node:fs'
import {
const mount: () => Promise<void>
mount
} from '@fkn/lib/fs'
module "node:fs"
fs
.
function existsSync(path: fs.PathLike): boolean

Returns true if the path exists, false otherwise.

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.');

@sincev0.1.21

existsSync
('cache/poster.png') // false before the mount, whatever this device or the account holds
module "node:fs"
fs
.
function readFileSync(path: fs.PathOrFileDescriptor, options?: {
encoding?: null | undefined;
flag?: string | undefined;
} | null): NonSharedBuffer (+2 overloads)

Returns the contents of the path.

For detailed information, see the documentation of the asynchronous version of this API:

readFile

.

If the encoding option is specified then this function returns a string. Otherwise it returns a buffer.

Similar to

readFile

, when the path is a directory, the behavior of fs.readFileSync() is platform-specific.

import { readFileSync } from 'node:fs';
// macOS, Linux, and Windows
readFileSync('<directory>');
// => [Error: EISDIR: illegal operation on a directory, read <directory>]
// FreeBSD
readFileSync('<directory>'); // => <data>

@sincev0.1.8

@parampath filename or file descriptor

readFileSync
('cache/poster.png') // throws ENOENT before the mount, for the same reason
await
function mount(): Promise<void>
mount
()
module "node:fs"
fs
.
function existsSync(path: fs.PathLike): boolean

Returns true if the path exists, false otherwise.

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.');

@sincev0.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.

When the broker, the connection your app holds into FKN, cannot be reached, the listing waits 8,000 ms, then 1,000 ms until a probe answers again and lists this device alone. existsSync then stays false for a file only the account holds, see listings and pending work.

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:

app.ts
import {
function readdir(path: PathLike, options?: (ObjectEncodingOptions & {
withFileTypes?: false | undefined;
recursive?: boolean | undefined;
}) | BufferEncoding | null): Promise<string[]> (+4 overloads)

Reads the contents of a directory.

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 {
const files = await readdir(path);
for (const file of files)
console.log(file);
} catch (err) {
console.error(err);
}

@sincev10.0.0

@returnFulfills with an array of the names of the files in the directory excluding '.' and '..'.

readdir
,
function readFile(path: PathLike | FileHandle, options?: ({
encoding?: null | undefined;
flag?: OpenMode | undefined;
} & EventEmitter<T extends EventMap<T> = any>.Abortable) | null): Promise<NonSharedBuffer> (+2 overloads)

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:

import { readFile } from 'node:fs/promises';
try {
const filePath = new URL('./package.json', import.meta.url);
const contents = await readFile(filePath, { encoding: 'utf8' });
console.log(contents);
} catch (err) {
console.error(err.message);
}

It is possible to abort an ongoing readFile using an AbortSignal. If a request is aborted the promise returned is rejected with an AbortError:

import { readFile } from 'node:fs/promises';
try {
const controller = new AbortController();
const { signal } = controller;
const promise = readFile(fileName, { signal });
// Abort the request before the promise settles.
controller.abort();
await promise;
} catch (err) {
// When a request is aborted - err is an AbortError
console.error(err);
}

Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.readFile performs.

Any specified FileHandle has to support reading.

@sincev10.0.0

@parampath filename or FileHandle

@returnFulfills with the contents of the file.

readFile
,
function writeFile(file: PathLike | FileHandle, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: (ObjectEncodingOptions & {
mode?: Mode | undefined;
flag?: OpenMode | undefined;
flush?: boolean | undefined;
} & EventEmitter<T extends EventMap<T> = any>.Abortable) | BufferEncoding | null): Promise<void>

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.

import { writeFile } from 'node:fs/promises';
import { Buffer } from 'node:buffer';
try {
const controller = new AbortController();
const { signal } = controller;
const data = new Uint8Array(Buffer.from('Hello Node.js'));
const promise = writeFile('message.txt', data, { signal });
// Abort the request before the promise settles.
controller.abort();
await promise;
} catch (err) {
// When a request is aborted - err is an AbortError
console.error(err);
}

Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.writeFile performs.

@sincev10.0.0

@paramfile filename or FileHandle

@returnFulfills with undefined upon success.

writeFile
} from 'node:fs/promises'
await
function writeFile(file: PathLike | FileHandle, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: (ObjectEncodingOptions & {
mode?: Mode | undefined;
flag?: OpenMode | undefined;
flush?: boolean | undefined;
} & EventEmitter<T extends EventMap<T> = any>.Abortable) | BufferEncoding | null): Promise<void>

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.

import { writeFile } from 'node:fs/promises';
import { Buffer } from 'node:buffer';
try {
const controller = new AbortController();
const { signal } = controller;
const data = new Uint8Array(Buffer.from('Hello Node.js'));
const promise = writeFile('message.txt', data, { signal });
// Abort the request before the promise settles.
controller.abort();
await promise;
} catch (err) {
// When a request is aborted - err is an AbortError
console.error(err);
}

Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.writeFile performs.

@sincev10.0.0

@paramfile filename or FileHandle

@returnFulfills with undefined upon success.

writeFile
('library/catalog.json',
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
({
items: never[]
items
: [] })) // mounts first, then writes
await
function readFile(path: PathLike | FileHandle, options: ({
encoding: BufferEncoding;
flag?: OpenMode | undefined;
} & EventEmitter<T extends EventMap<T> = any>.Abortable) | BufferEncoding): Promise<string> (+2 overloads)

Asynchronously reads the entire contents of a file.

@parampath 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.

@paramoptions An object that may contain an optional flag. If a flag is not provided, it defaults to 'r'.

readFile
('library/catalog.json', 'utf8') // '{"items":[]}'
await
function readdir(path: PathLike, options?: (ObjectEncodingOptions & {
withFileTypes?: false | undefined;
recursive?: boolean | undefined;
}) | BufferEncoding | null): Promise<string[]> (+4 overloads)

Reads the contents of a directory.

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 {
const files = await readdir(path);
for (const file of files)
console.log(file);
} catch (err) {
console.error(err);
}

@sincev10.0.0

@returnFulfills 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 common whole-file operations, in the forms @fkn/lib/fs has for them:

OperationForms
readFile, writeFile, appendFilesync, callback, promise
stat, readdir, mkdir, rm, unlink, renamesync, callback, promise
existssync, callback
lstat, rmdirsync, promise
accesspromise

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.