Auto
This subpath provides one mount() function that selects a transport. It is separate from the root export, so an import of driver types does not load three protocol stacks.
import { mount, probeTransports, liveMounts, unmountAll } from "mountx/auto";#mount(driver, mountpoint, options?)
function mount(
driver: FsDriver,
mountpoint: string,
options?: AutoMountOptions,
): Promise<AutoMount>;This function serves driver at mountpoint with a usable host transport.
It resolves only when the mount point is usable.
The result is the transport's mount object with an added transport property.
Auto adds a tag but does not wrap the object. Therefore, await using, unmount(), and all transport-specific members work as they do with a direct import.
await using mounted = await mount(createMemoryDriver(), "/mnt/point");
mounted.transport; // "fuse" | "9p" | "nfs"If no transport is usable, the function throws an error that lists the missing requirement for each transport.
#AutoMountOptions
This type is intentionally not a union of the three transport option types. Some transport options have the same name but different shapes. For example, onError receives a FUSE request, an NFS RPC call, or a 9P message header. A merged type would be inaccurate or unusable.
Put options with shared meanings at the top level. Put transport-specific options in fuse: {…}, "9p": {…}, or nfs: {…}. mountx applies transport-specific values after shared values, so they take priority.
interface AutoMountOptions {
/** "auto" (default), or a name — which skips the probe entirely. */
transport?: "auto" | "fuse" | "9p" | "nfs";
/** Mount read-only. */
readOnly?: boolean;
/** Unmount on SIGINT/SIGTERM. Default true. */
signals?: boolean;
/** Milliseconds an unmount may spend before it is forced. Default 10_000. */
unmountTimeout?: number;
/** Report the driver's own ino values instead of synthesising them. */
useDriverIno?: boolean;
/** Called for errors raised while answering a request. */
onError?: (error: unknown) => void;
/** Called for transport-level failures, and for a forced teardown. */
onTransportError?: (error: unknown) => void;
/** FUSE-only options. Applied after the shared ones. */
fuse?: MountOptions;
/** 9P-only options. Applied after the shared ones. Quoted: `9p` is not an identifier. */
"9p"?: MountP9Options;
/** NFS-only options. Applied after the shared ones. */
nfs?: MountNfsOptions;
}Auto ignores option blocks for transports that it did not select. This lets one call work on different hosts.
See MountOptions, MountP9Options and MountNfsOptions for the three nested blocks.
#AutoMount
type AutoMount =
| (Mount & { readonly transport: "fuse" })
| (P9Mount & { readonly transport: "9p" })
| (NfsMount & { readonly transport: "nfs" });This is a discriminated union. Check the tag to access the complete transport type without a cast:
if (mounted.transport === "fuse") {
mounted.session; // FuseSession
mounted.fd; // the /dev/fuse descriptor
mounted.closed; // Promise<void>, never rejects
mounted.notifyInvalInode(2n);
mounted.notifyInvalEntry(1n, "hello.txt");
} else if (mounted.transport === "9p") {
mounted.server; // P9Server
mounted.connection; // P9Connection — the kernel's, and the only one this mount cares about
mounted.trans; // "unix" | "tcp"
} else {
mounted.server; // NfsServer
mounted.port; // number
}All three share mountpoint, source, active, unmount() and [Symbol.asyncDispose].
#probeTransports(platform?)
function probeTransports(platform?: NodeJS.Platform): Promise<AutoProbe>;interface AutoProbe {
platform: NodeJS.Platform;
chosen: Transport | undefined; // what mount() would use
preference: readonly Transport[]; // the list `chosen` was picked from
fuse: TransportProbe; // { usable: boolean; reason: string | undefined }
"9p": TransportProbe; // quoted: `9p` is not an identifier
nfs: TransportProbe;
reason: string | undefined; // when nothing can mount, naming all three
}Call this small probe before you offer a mount.
Its result contains enough detail to show to a user.
It runs all three transport probes and checks the least expensive facts first. It starts with the platform and then checks FUSE. The FUSE check reads /dev/fuse. For an unprivileged process, it also checks fusermount3 and the native addon.
Next, p9ClientProbe() checks root access and 9p in /proc/filesystems. Finally, nfsClientProbe() checks the client binaries and root access on Linux.
Every one of them reads node:fs and nothing else, so it loads no protocol codec on any branch.
The preference order is ["fuse", "9p", "nfs"] on Linux and ["nfs", "fuse", "9p"] elsewhere.
Linux is the only host where more than one transport can work. Therefore, the order matters only on Linux. On other platforms, only NFS can be usable. Other kernels have no v9fs client, and macFUSE uses a protocol that mountx does not implement.
Every transport appears in every preference list regardless of platform: a name missing from it would read as one that was never considered.
Note
Tests can override platform to check Darwin and Windows results from any host. Leave it alone otherwise.
#The 9P module refusal
probeTransports() does not simply forward p9ClientProbe().usable for the "9p" entry.
In one case, the probe reports usable 9P but auto refuses it. The host is Linux, the process has root, and /proc/filesystems lists 9p. However, the host has no 9pnet_fd module and no module tree that can load it. 9pnet_fd registers trans=unix, trans=tcp, and trans=fd. This case can occur in a virtio-only guest or a container with an empty /lib/modules.
p9ModuleRefusal() refines only an already-usable probe. On a non-root or non-Linux host, the probe has already refused 9P.
p9ClientProbe() does not refuse when 9pnet_fd is missing because the result is ambiguous. A kernel with the transport built in looks like a kernel that cannot provide it. Also, mount(8) can run modprobe as root before mounting.
That is the right call for a named -t 9p, where the alternative is refusing a mount that would have worked.
That behavior is not safe for auto because auto does not fall back. It selects one transport. An incorrect 9P choice would fail on a host where NFS could mount.
Therefore, auto rules out 9P only when no module is loaded and no module tree can load one. It changes the usable flag for the "9p" entry, not its place in preference. Every platform keeps all transports in that list.
A direct mountx --transport 9p or transport: "9p" request can still try the mount. If it fails, it returns a more specific error.
The pure, exported seam for this is p9ModuleRefusal(probe: P9ClientProbe): string | undefined.
#liveMounts() / unmountAll()
function liveMounts(): Promise<AutoMount[]>;
function unmountAll(): Promise<unknown[]>;liveMounts() returns every live mount for transports that mountx/auto loaded.
Each item has a transport tag. unmountAll() unmounts them and never rejects.
It returns all failures in an array.
These functions ask only transports that mount() used. They do not load an unused transport. They also skip a transport module that failed to load.
Caution
These are not filtered to the mounts mount() returned, and they cannot be. Each transport has one process-wide registry of live mounts. Its mount() function adds entries, whether mountx/auto or your code called it.
Assume that you import mountx/fuse directly and create a mount. After mountx/auto also uses FUSE, liveMounts() returns the direct mount. unmountAll() also unmounts it.
Do not rely on either one to leave a mount alone.
They cannot see a transport that mountx/auto never loaded. Auto loads only transports that mount() used.
Mount with mountx/9p directly while mountx/auto only ever chose FUSE, and that 9P mount is invisible to both; tear it down with unmountAll9p().
Closing this gap would require each transport to register itself in a shared module during import. That registration is a load-order side effect. A bundler can remove it because the package declares "sideEffects": false. The documented gap is safer than a registry that can silently disappear from a bundle.
#Re-exported types
This subpath exports Transport, TransportProbe, AutoProbe, AutoMountOptions, and AutoMount. It also exports Mount and MountOptions from FUSE. The 9P types are P9Mount and MountP9Options. The NFS types are NfsMount and MountNfsOptions. A caller needs only one import.
#Three things auto deliberately does not do
- No fallback after a failure. The probe decides once, from host facts. If the selected transport fails to mount,
autoreturns that error. A silent fallback could return a filesystem with different behavior. - No probing when you name a transport.
transport: "fuse"(or"9p", or"nfs") calls that transport directly, whose own errors are more specific than anything the chooser could say. - No wrapping. The result is the transport's own mount object with a
transportproperty defined on it. The tag provides access to transport-specific members. FUSE hassession,fd, and thenotifyInval*pair. 9P hasserver,connection, andtrans. NFS hasserverandport.await usingworks for every type.
Auto also follows one loading rule: it does not load unused transports. Every transport arrives through await import(), so choosing one never pulls in either other codec.
#Why FUSE, then 9P, then NFS
FUSE is preferred wherever it works, for two reasons that do not overlap.
For an unprivileged Linux process, FUSE is the only usable mount transport. 9P and NFS both need root.
As root, FUSE communicates directly with the kernel through /dev/fuse. There is no socket or RPC layer between the virtual file system (VFS) and the process. FUSE also has the notify_inval_inode and notify_inval_entry invalidation channels. The other mount transports do not have them. A driver can report changes instead of waiting for cached data to expire.
It is also the transport this project has run pjdfstest against.
9P comes next and NFS last, which is a different argument: both need root, so what separates them is semantics. 9P is stateful, while NFSv3 is not. A file deleted while open stays readable instead of returning ESTALE. close() and fsync() reach the driver, which a handle-buffering driver needs. Requests do not need a handle-table lookup.
See the transport comparison.
macOS uses NFS without root because it is the only supported mount client there. macFUSE is a third-party kernel extension with a different protocol. BSD kernels do not have v9fs.