From 0c4f4fc79b0ecd855d71f782b7f725e1c75473aa Mon Sep 17 00:00:00 2001 From: Philipp Dunkel Date: Thu, 3 Sep 2026 11:43:29 +0200 Subject: [PATCH] vfs: add --vfs-mount and --vfs-load startup flags Mounting a virtual file system requires calling vfs.mount() from inside the program, so a program cannot itself be served from one: something already running has to mount the VFS first. Add two startup flags. --vfs-mount= mounts a directory or an archive as a virtual file system, and may be repeated. --vfs-load= mounts that source the same way and additionally runs the entry point and all subsequent require()/import resolution against it rather than against the real file system, so an application can be run straight out of a directory or a ZIP archive: node --experimental-vfs --vfs-load=my-app.zip Both options append to one list, so mounts happen in the order written: node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c mounts a, b and c in that order and runs b. Mounting the same source twice mounts it twice, at two mount points, and the entry point comes from the mount --vfs-load contributed rather than the earlier one. The provider backing a source is chosen from the source itself rather than from its name: a directory is served by RealFSProvider and a file whose bytes are a ZIP archive by ZipProvider, so an archive can carry any extension. vfs.registerProvider() registers a provider for formats there is no built-in for; selection is deferred until -r and --import preloads have run, so a preloaded module can register one before its source is claimed. The entry point comes from the mount the way `node ` takes one, from package.json "main" or index.js. Nothing is consumed as an entry point argument, so every positional reaches the program unchanged from argv[2] on, and argv[1] reports the named source rather than the reserved mount point, which is an opaque implementation detail. --vfs-load may be given at most once, and is not permitted in NODE_OPTIONS: which entry point runs is the command line's decision, and NODE_OPTIONS is parsed first, so an environment variable could otherwise redirect any invocation. --vfs-mount is permitted there. Workers inherit the same mounts in the same order but not the loading: a worker mounts what the parent mounted and runs its own entry point. Signed-off-by: Philipp Dunkel --- doc/api/cli.md | 78 +++++ doc/api/errors.md | 7 + doc/api/vfs.md | 62 ++++ doc/node.1 | 59 ++++ lib/internal/errors.js | 2 + lib/internal/main/worker_thread.js | 8 + lib/internal/modules/run_main.js | 16 ++ lib/internal/process/pre_execution.js | 117 +++++++- lib/internal/vfs/provider_registry.js | 95 +++++++ lib/vfs.js | 2 + src/node.cc | 36 ++- src/node_options.cc | 20 ++ src/node_options.h | 2 + src/node_worker.cc | 5 + test/parallel/test-vfs-mount-load.js | 392 ++++++++++++++++++++++++++ 15 files changed, 899 insertions(+), 2 deletions(-) create mode 100644 lib/internal/vfs/provider_registry.js create mode 100644 test/parallel/test-vfs-mount-load.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 59945be330a2..4a1f2d0ccda1 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -3633,6 +3633,78 @@ added: v0.1.3 Print node's version. +### `--vfs-load=source` + + + +* `source` {string} A directory or an archive file to mount and run. + +Requires [`--experimental-vfs`][]. May be given at most once. + +Mounts `source` exactly as [`--vfs-mount`][] does, and additionally runs the +entry point and all subsequent `require()`/`import` resolution against that +mount rather than the real file system. The entry point is taken from the mount +the same way `node ` takes one: the mount's own `package.json` +`"main"`, or `index.js`. Any positional command-line argument is the program's +own (available from `process.argv[2]` onward), never an entry-point override. + +`process.argv[1]` reports `source` rather than the reserved mount point, since +the mount point is an opaque implementation detail. + +Mounting the same source twice mounts it twice, at two separate mount points. +The entry point then comes from the mount `--vfs-load` itself contributed, not +from an earlier `--vfs-mount` of the same source. + +In worker threads `--vfs-load` mounts but does not load: a worker inherits the +same mounts, in the same order, and runs its own entry point. + +`--vfs-load` is not permitted in [`NODE_OPTIONS`][]: which entry point runs is +the command line's decision, and the environment must not be able to redirect +it. + +```console +$ node --experimental-vfs --vfs-load=app.zip +$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip +``` + +### `--vfs-mount=source` + + + +* `source` {string} A directory or an archive file to mount. + +Requires [`--experimental-vfs`][]. May be repeated to mount several sources. + +Mounts `source` as a virtual file system ([`node:vfs`][]). Each mount is placed +at a reserved mount point assigned by Node.js, so mounts never shadow real +paths and no target can be chosen. Mounting alone does not change the entry +point; use [`--vfs-load`][] for the source to run from. + +`--vfs-mount` and [`--vfs-load`][] mount in the order they are written, so + +```console +$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c +``` + +mounts `a`, `b` and `c` in that order and runs `b`. Mounts contributed by +[`NODE_OPTIONS`][] are mounted before the command line's. + +The provider backing a source is chosen from the source itself rather than from +its file name: + +* A directory is mounted with a [`RealFSProvider`][] rooted there. +* A file whose bytes are a ZIP archive is mounted with a [`ZipProvider`][], so + an archive can carry any name. + +Providers registered with `vfs.registerProvider()` (typically from a module +preloaded with [`--require`][] or [`--import`][]) are consulted first, in +reverse registration order, and may claim directories as well as files. If no +provider claims the source, Node.js exits with an error. + ### `--watch` + +* `entry` {Object} + * `name` {string} A short identifier, used in diagnostics. + * `canHandle` {Function} Called with the resolved path and its + [`fs.Stats`][]. Returns `true` if this provider should back the source. + * `create` {Function} Called with the resolved path and its [`fs.Stats`][]. + Returns the {VirtualProvider} backing the source. + +Registers a provider that [`--vfs-mount`][] can select for a source it +recognizes, so a file format Node.js has no built-in provider for can still be +mounted. + +A source is claimed by the first provider whose `canHandle()` returns `true`. +Registered providers are consulted before the built-in ones, newest +registration first, and are offered directories as well as files, so a +registered provider can back, wrap, or vet any source. If none claims the +source, the built-in providers handle it: a directory with +[`RealFSProvider`][], and a file whose bytes are a ZIP archive with +[`ZipProvider`][]. + +Providers must be registered before the mounts are created. Register from a +module preloaded with [`--require`][] or [`--import`][]: + +```cjs +// provider.js, preloaded with --require +const fs = require('node:fs'); +const vfs = require('node:vfs'); + +const MAGIC = Buffer.from('CUSTOMFMT'); + +vfs.registerProvider({ + name: 'customfmt', + canHandle(path, stats) { + if (!stats.isFile()) return false; + const head = Buffer.alloc(MAGIC.length); + const fd = fs.openSync(path, 'r'); + try { + fs.readSync(fd, head, 0, MAGIC.length, 0); + } finally { + fs.closeSync(fd); + } + return head.equals(MAGIC); + }, + create(path) { + return new MyCustomProvider(path); + }, +}); +``` + +```console +$ node --experimental-vfs --require ./provider.js \ + --vfs-load archive.customfmt +``` + ## Class: `VirtualFileSystem`