Repository navigation
Track: drop the @roberts_lando/vfs polyfill in favour of native node:vfs #306
Description
Summary
prelude/sea-vfs-setup.js depends on the @roberts_lando/vfs polyfill to give SEA binaries a /snapshot filesystem. Node.js has since grown its own node:vfs module. This issue tracks what has to be true before we can drop the polyfill and use core.
Status: not possible today. Nothing here is a request for work on our side yet — it is a checklist to re-check on each Node.js release.
Where the dependency lives
Only the SEA path uses the package. Classic mode has its own VIRTUAL_FILESYSTEM implementation in prelude/bootstrap.js and is unaffected.
prelude/sea-vfs-setup.js already prefers core and falls back:
try { vfsModule = require('node:vfs'); }
catch (_) { vfsModule = require('@roberts_lando/vfs'); }What landed in Node.js
| Module | node:vfs, added v26.4.0 |
| Stability | 1 — Experimental, requires --experimental-vfs |
| Backported | No. latest-v22.x, latest-v24.x, latest-v25.x have no vfs.html |
| Still moving | mount() / mountPointURL / ZipProvider added v26.9.0; registerProvider and --vfs-mount / --vfs-load added v26.10.0 |
Docs: https://nodejs.org/api/vfs.html — original PR: nodejs/node#61478 — follow-ups: nodejs/node#62328
Blockers
1. Version floor and flag. node:vfs exists only on Node 26.4+, and only with --experimental-vfs. pkg targets node18 through node26. Dropping the polyfill orphans every target below 26, so the realistic first step is dual-path, not removal.
2. No caller-chosen mount point. We mount at /snapshot:
virtualFs.mount(SNAPSHOT_PREFIX, { overlay: true }); // prelude/sea-vfs-setup.jsCore's mount() takes no arguments. It returns a reserved path it picks (/dev/null/vfs/0), and the docs say the scheme "is subject to change and users should not manually construct them". /snapshot is pkg's public contract: __filename, error messages, user code that hardcodes it, and parity with classic mode.
3. No overlay / fall-through. We pass { overlay: true } so a path not in the VFS falls through to the real filesystem. Core is explicitly the opposite — "if a path under a mount point does not exist in the VFS, resolution fails with ENOENT without consulting the disk", and "there is no search order or fallback between the two".
4. Provider contract gaps. SEAProvider extends MemoryProvider and overrides internalModuleStat() — the module-resolution fast path, roughly 30K calls per startup on a large project, returning 0/1/-2 straight from the manifest with no stat allocation. Core's VirtualProvider has no equivalent hook. We also monkey-patch shouldHandle() and resolvePath() on the prototype for Windows path conversion; neither method exists on core's class.
5. Missing fs surface. Core has a wider API than the polyfill (rmSync, truncateSync, linkSync, chmodSync, mkdtempSync, opendirSync, createWriteStream, fd writes, promises.open), so that direction is fine. But nothing in core replaces the pkg-specific extensions above.
6. The SEA-level alternative does not fit either. Core's own "useVfs": true SEA mode cannot be combined with "useCodeCache": true, which we forward from the user's seaConfig (lib/sea.ts). Native addons still have to be written to a temp file. And it assumes core's asset layout, not our manifest + single-blob archive with per-file zstd.
Latent bug to fix now
The require('node:vfs') above is a bare try/catch. --experimental-vfs is allowed in NODE_OPTIONS, and our node-26 pkg-fetch patch does not strip NODE_OPTIONS (only debug options). So on a node26 binary:
NODE_OPTIONS=--experimental-vfs ./my-app
takes the core branch, and mount('/snapshot', { overlay: true }) silently ignores both arguments and mounts at /dev/null/vfs/0. Every /snapshot path then fails. The probe needs to be a capability check (does mount() accept a prefix, is overlay honoured), not a bare require.
Criteria to drop the polyfill
-
node:vfsis stable (no--experimental-vfs), or pkg can reliably force the flag on in its own binaries - Mount at a caller-chosen prefix, or a documented stable prefix pkg can adopt as its
/snapshotreplacement - Overlay / fall-through to the real filesystem, or a confirmed design that does not need it
- A module-resolution stat fast path equivalent to
internalModuleStat(), or benchmarks showingstatSync()is fast enough -
useVfs+useCodeCachetogether, if we go the core-SEA route instead of our own provider - Node 26 is LTS and is pkg's minimum target — otherwise the polyfill stays for older targets and this becomes a dual path, not a removal
Related
- Upstream pkg-fetch patches to Node.js core to eliminate custom binaries #231 — upstreaming pkg-fetch patches to Node.js core
Activity