From ed7ec03a6de722f38e52109e37bef301aeff17e9 Mon Sep 17 00:00:00 2001 From: Philipp Dunkel Date: Sat, 1 Aug 2026 20:38:52 +0200 Subject: [PATCH] vfs: add --vfs-load to run modules from a mount Builds on --vfs-mount to make a mount a real execution environment, resolving require()/import inside it. Adds --vfs-load, which runs the entry point (process.argv[1]) and all subsequent require()/import resolution against the last --vfs-mount rather than the real file system. process.argv[1] becomes that mount's root, as if `node ` had been run, so the mount's own package.json "main"/index.js selects the entry point and positional arguments are the program's own. A self-mounting shebang (`#!/usr/bin/env -S node --vfs-load --vfs-mount`) therefore makes an archive directly executable. --vfs-load requires at least one --vfs-mount, and workers inherit it alongside the mounts. Making resolution VFS-aware requires four of the CJS/ESM loader's resolution primitives - package.json reading, nearest-parent/scope lookup, legacy main resolution, and extensionless-file format sniffing - to stop bypassing the public fs module and call it instead, since that bypass is what let them ignore a mount. Each defers to the real native binding unchanged for anything outside an active mount. A lean mount registry (internal/vfs/registry) is extracted from internal/vfs/setup so the resolution fast path can answer "is this path under a mount?" without loading the heavier setup/provider machinery. Provider selection - and, for a --vfs-load entry, its resolution - is deferred until after -r and --import preload modules have run, so a provider registered from either backs the mount. Native addons are supported: a directory-backed mount dlopens the real underlying file directly (RealFSProvider.toRealPath); an archive-backed mount extracts the addon to a content-hashed temp file first, scoped per pid so processes don't clobber each other's extractions. Signed-off-by: Philipp Dunkel --- doc/api/cli.md | 69 ++++- doc/api/vfs.md | 24 +- doc/node.1 | 45 ++- lib/internal/modules/cjs/loader.js | 8 +- lib/internal/modules/esm/get_format.js | 5 +- lib/internal/modules/esm/resolve.js | 8 +- lib/internal/modules/package_json_reader.js | 20 +- lib/internal/modules/run_main.js | 29 +- lib/internal/modules/vfs_addons.js | 106 +++++++ lib/internal/modules/vfs_resolution.js | 291 ++++++++++++++++++ lib/internal/process/pre_execution.js | 186 +++++++---- lib/internal/vfs/file_system.js | 12 + lib/internal/vfs/providers/real.js | 12 + lib/internal/vfs/registry.js | 67 ++++ lib/internal/vfs/setup.js | 42 +-- lib/internal/worker.js | 7 +- src/node.cc | 7 +- src/node_options.cc | 18 +- src/node_options.h | 1 + test/parallel/test-bootstrap-modules.js | 6 + test/parallel/test-vfs-cli-flag-addons.js | 80 +++++ .../test-vfs-cli-flag-custom-provider.js | 201 ++++++++++++ test/parallel/test-vfs-cli-flag.js | 212 +++++++++++++ test/parallel/test-vfs-mount-require.js | 73 +++++ 24 files changed, 1392 insertions(+), 137 deletions(-) create mode 100644 lib/internal/modules/vfs_addons.js create mode 100644 lib/internal/modules/vfs_resolution.js create mode 100644 lib/internal/vfs/registry.js create mode 100644 test/parallel/test-vfs-cli-flag-addons.js create mode 100644 test/parallel/test-vfs-cli-flag-custom-provider.js create mode 100644 test/parallel/test-vfs-cli-flag.js create mode 100644 test/parallel/test-vfs-mount-require.js diff --git a/doc/api/cli.md b/doc/api/cli.md index fa7db08d71dd..074f6ea6df90 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1497,8 +1497,8 @@ added: v26.4.0 > Stability: 1 - Experimental Enable the experimental [`node:vfs`][] module. This flag also gates the -[`--vfs-mount`][] startup flag, which is only allowed when `--experimental-vfs` -is set. +[`--vfs-mount`][] and [`--vfs-load`][] startup flags, which are only allowed +when `--experimental-vfs` is set. ### `--experimental-vm-modules` @@ -3564,6 +3564,38 @@ added: v0.1.3 Print node's version. +### `--vfs-load` + + + +Requires [`--experimental-vfs`][] and at least one [`--vfs-mount`][]. + +Runs the entry point (`process.argv[1]`) and all subsequent +`require()`/`import` resolution against the **last** [`--vfs-mount`][] rather +than the real file system. `process.argv[1]` becomes that mount's root, as if +`node ` had been run: the mount's own `package.json` `"main"` (or +`index.js`) selects the entry point, and any positional command-line argument +is the program's own (available from `process.argv[2]` onward), never an +entry-point override. + +Module resolution under the loaded mount is fully sandboxed: `package.json` +lookups, `node_modules`-style resolution, and legacy `main` resolution never +fall back to the real file system once they would step outside the mount. + +Combined with a self-mounting shebang this makes an archive directly +executable. The kernel appends the script's own path as the trailing argument, +which the final `--vfs-mount` consumes as its source, so the archive mounts +itself and runs (the ZIP is located by its trailing record, so the shebang +prefix is ignored): + +```console +$ (printf '#!/usr/bin/env -S node --vfs-load --vfs-mount\n'; cat app.zip) > app +$ chmod +x app +$ ./app arg1 arg2 # runs the archive's index.js with ['arg1', 'arg2'] +``` + ### `--vfs-mount=source[=target]`