Skip to content

@wtf/platform-fs-adapter

Documentation / @wtf/platform-fs-adapter

Get folder handles of the WHATWG File System Standard from any file system shaped like Node’s fs/promises: the node:fs of Node, Bun or Deno, or memfs’ fs.promises in a browser or a test. It imports nothing from Node: you pass in the file system. @wtf/platform-fs-node does that for you with Node’s own.

SubpathWhat you get
.NodeShapedFileSystem, the calls the handles make and what they return; FromNodeShapedOptions; NodeShapedContext
./handlesfromNodeShaped(fs, path, { create }): the handle of a folder in fs; the handles’ classes, to extend
./errorsdomExceptionOf: turns a failure with a Node code into the standard’s DOMException; CODES, the name of each code
import type { NodeShapedFileSystem } from '@wtf/platform-fs-adapter'
import { fromNodeShaped } from '@wtf/platform-fs-adapter/handles'
import { memfs } from 'memfs'
const { fs } = memfs()
const root = await fromNodeShaped(fs.promises as unknown as NodeShapedFileSystem, '/app', {
create: true
})
  • Snapshots. A snapshot is a File. Where the file system has openAsBlob, as Node’s does, the snapshot comes from it, is read as it streams, and fails to read once the file has been written. Otherwise it holds the bytes, read whole when you take it, as with memfs. A file removed before it is read is a NotFoundError.
  • Writables. A writable writes a file next to the original, <name>.<n>.crswap, with the first n that is free, and creates it only if it is not there, so no two writables, in any process, share one, and no name is random. It replaces the file when it closes, keeping the file’s mode, and an abort removes it. A write through a link writes the file the link leads to. A folder listing leaves out .crswap files.
  • Exclusive mode. mode: 'exclusive' holds the file with <name>.crswap, created only if it is not there, so a writable in another process fails too. A process that ended while holding it leaves the file behind; remove it, and the file is free.
  • Busy entries. An entry with an open writable, or a folder containing one, cannot be removed or moved: that is a NoModificationAllowedError.
  • Sync access. Give the file system sync, the synchronous calls of node:fs (openSync, readSync, writeSync, statSync and the rest of NodeShapedSyncFileSystem), and a file’s handle has createSyncAccessHandle(). Without it, none has. A sync access handle holds the file as an exclusive writable does, with <name>.crswap, and removes it when it closes. memfs gives them as its fs: Object.create(fs.promises, { sync: { value: fs } }).
  • Links. A link the file system has is listed as what it leads to, and a link to nothing is not listed at all. removeEntry removes the link, not what it leads to.
  • Errors. A failure with a Node code becomes a DOMException:
    • ENOENT: NotFoundError;
    • ENOTDIR and EISDIR: TypeMismatchError;
    • ENOTEMPTY, EEXIST and EINVAL: InvalidModificationError;
    • EBUSY and ETXTBSY: NoModificationAllowedError;
    • EACCES, EPERM and EROFS: NotAllowedError;
    • ENOSPC, EDQUOT and EFBIG: QuotaExceededError;
    • EIO: NotReadableError;
    • ELOOP: InvalidStateError;
    • ENOTSUP, EOPNOTSUPP, ENOSYS and EXDEV: NotSupportedError;
    • any other: UnknownError.

Extend the handles

The handles are classes you can extend, to add calls of your own to every handle of a file system: NodeShapedFileHandle and NodeShapedDirectoryHandle, which both extend NodeShapedHandle.

  • A folder makes the handle of each entry in it through one method, handleOf(path, kind). Override it in your folder’s class to make your own classes, and every entry, however deep, is one of yours.
  • NodeShapedDirectoryHandle.root(fs, path, { create }), called on your class, makes a root of your class.
  • Your class reaches what a handle is of through its protected members: context, the file system (context.fs) and the writables open of its files (context.writers); path, where the entry is now; stats(), what stat gives of it; checkFree(path), which fails of an entry a writable holds; and, of a file, openSync(), the file opened synchronously.
import type { FileSystemHandleKind } from '@wtf/platform/fs'
import type { NodeShapedFileSystem } from '@wtf/platform-fs-adapter'
import { NodeShapedDirectoryHandle, NodeShapedFileHandle } from '@wtf/platform-fs-adapter/handles'
class ModeFileHandle extends NodeShapedFileHandle {
async getMode(): Promise<number> {
return (await this.stats()).mode & 0o7777
}
}
class ModeDirectoryHandle extends NodeShapedDirectoryHandle {
protected override handleOf(path: string, kind: FileSystemHandleKind) {
return kind === 'file'
? new ModeFileHandle(this.context, path)
: new ModeDirectoryHandle(this.context, path)
}
}
declare const fs: NodeShapedFileSystem
const root = await ModeDirectoryHandle.root(fs, '/app')
const file = (await root.getFileHandle('run.sh')) as ModeFileHandle
await file.getMode()

memfs’ types promise less than it provides, so you pass it with a cast (fs.promises as unknown as NodeShapedFileSystem). The package depends on @wtf/platform-fs. Its tests run the file system suite and the Web Platform Tests of fs/ on memfs, in memory, with their known failures in test/wpt/fs.json.

Modules