mountx logomountx

Auto

One mount() that picks a transport, kept off the root export so importing mountx for the driver types does not drag two protocol stacks in with it.

import { mount, probeTransports, liveMounts, unmountAll } from "mountx/auto";

#mount(driver, mountpoint, options?)

function mount(
  driver: FsDriver,
  mountpoint: string,
  options?: AutoMountOptions,
): Promise<AutoMount>;

Serves driver at mountpoint over whatever this host can mount, and resolves once the mountpoint is usable. The result is the transport's own mount object with a transport property defined on it — tagged, not wrapped — so await using, unmount() and every transport-specific member work exactly as they do when the transport is imported directly.

await using mounted = await mount(createMemoryDriver(), "/mnt/point");

Throws when the probe finds nothing usable, with a message naming what each transport is missing.

#AutoMountOptions

Deliberately not the union of both transports' option types: they have same-named options with genuinely different shapes (onError hands FUSE a request and NFS an RPC call), and a merged type would either lie about that or collapse to something unusable.

So the top level is the options that mean the same thing in both, and anything transport-specific goes in fuse: {…} or nfs: {…} — applied after the shared ones, and therefore winning.

interface AutoMountOptions {
  /** "auto" (default), or a name — which skips the probe entirely. */
  transport?: "auto" | "fuse" | "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;
  /** NFS-only options. Applied after the shared ones. */
  nfs?: MountNfsOptions;
}

A block for the transport you did not get is simply ignored — which is what makes one call portable.

See MountOptions and MountNfsOptions for the two nested blocks.

#AutoMount

type AutoMount =
  (Mount & { readonly transport: "fuse" }) | (NfsMount & { readonly transport: "nfs" });

A discriminated union. Narrowing on the tag reaches the full transport type, with no 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 {
  mounted.server; // NfsServer
  mounted.port; // number
}

Both 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 }
  nfs: TransportProbe;
  reason: string | undefined; // when nothing can mount, naming both
}

Cheap enough to call before deciding whether to offer a mount at all, and specific enough to print. It reads facts in order of cheapness — the platform, then /dev/fuse, then (only when unprivileged) the fusermount3 helper and the native addon — and it loads no protocol codec on either branch.

The preference order is ["fuse", "nfs"] on Linux and ["nfs", "fuse"] elsewhere. Linux is the only host where both can work, so it is the only host whose order decides anything.

Note

platform exists to be overridden in tests — it is how the darwin and win32 answers are checked from any host. Leave it alone otherwise.

#liveMounts() / unmountAll()

function liveMounts(): Promise<AutoMount[]>;
function unmountAll(): Promise<unknown[]>;

Every live mount in this process, on both transports, tagged; and unmount them all — never rejects, returning whatever went wrong. Only transports that were actually used are asked, so neither loads anything that was not already loaded.

#Re-exported types

Transport, TransportProbe, AutoProbe, AutoMountOptions, AutoMount, plus Mount/MountOptions from the FUSE transport and NfsMount/MountNfsOptions from the NFS one — so a caller needs one import either way.

#Three things auto deliberately does not do

  • No fallback after a failure. The probe decides once, from host facts. If the chosen transport then fails to mount, that error is what you get — silently mounting the other one would hand back a filesystem with different semantics than the error you never saw.
  • No probing when you name a transport. transport: "fuse" calls the FUSE 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 transport property defined on it. Narrowing on that tag reaches session, fd and the notifyInval* pair for FUSE, or server and port for NFS — and await using works either way.

And one it does do quietly: it loads nothing it does not use. Both transports arrive through await import(), so choosing one never pulls in the other's codec.

#Next

  • FUSE — what a Linux host gets, in detail.
  • NFSv3 — what macOS gets, and serving without mounting at all.
  • Mounting — the lifecycle and the unmount rules, in context.
mountx logo

mountx  Write a filesystem in JavaScript, mount it as a real folder.