@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.
| Subpath | What you get |
|---|---|
. | NodeShapedFileSystem, the calls the handles make and what they return; FromNodeShapedOptions; NodeShapedContext |
./handles | fromNodeShaped(fs, path, { create }): the handle of a folder in fs; the handles’ classes, to extend |
./errors | domExceptionOf: 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 hasopenAsBlob, 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 aNotFoundError. - Writables. A writable writes a file next to the original,
<name>.<n>.crswap, with the firstnthat 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.crswapfiles. - 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 ofnode:fs(openSync,readSync,writeSync,statSyncand the rest ofNodeShapedSyncFileSystem), and a file’s handle hascreateSyncAccessHandle(). 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 itsfs: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.
removeEntryremoves the link, not what it leads to. - Errors. A failure with a Node
codebecomes aDOMException:ENOENT:NotFoundError;ENOTDIRandEISDIR:TypeMismatchError;ENOTEMPTY,EEXISTandEINVAL:InvalidModificationError;EBUSYandETXTBSY:NoModificationAllowedError;EACCES,EPERMandEROFS:NotAllowedError;ENOSPC,EDQUOTandEFBIG:QuotaExceededError;EIO:NotReadableError;ELOOP:InvalidStateError;ENOTSUP,EOPNOTSUPP,ENOSYSandEXDEV: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(), whatstatgives 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 ModeFileHandleawait 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.
