diff --git a/doc/api/cli.md b/doc/api/cli.md index 9434e19d3b73..074f6ea6df90 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1496,7 +1496,9 @@ added: v26.4.0 > Stability: 1 - Experimental -Enable the experimental [`node:vfs`][] module. +Enable the experimental [`node:vfs`][] module. This flag also gates the +[`--vfs-mount`][] and [`--vfs-load`][] startup flags, which are only allowed +when `--experimental-vfs` is set. ### `--experimental-vm-modules` @@ -3562,6 +3564,89 @@ 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]` + + + +* `source` {string} A directory or an archive file to mount. +* `target` {string} Where to mount it. **Default:** `source`'s own resolved + path. + +Requires [`--experimental-vfs`][]. May be repeated to mount several sources. + +Mounts `source` as a virtual file system ([`node:vfs`][]) at `target` (or at +`source`'s own path when no `target` is given). Paths under `target` then +resolve against the mount - both `require()`/`import` and the running +program's own [`node:fs`][] calls - while every other path uses the real file +system unchanged. Mounting alone does **not** change the entry point; pass +[`--vfs-load`][] to also run from a mount. + +* If `source` is a directory, it's mounted with a [`RealFSProvider`][] rooted + there. The files are already real, so mounting doesn't change what bytes are + read - it adds path containment, rejecting resolution that would escape the + root via `..`. +* If `source` is a file, a provider is chosen for it by **content**, not by + file extension, so an archive can carry any name. Providers registered with + [`vfs.registerProvider()`][] (typically from a module preloaded with + [`--require`][] or [`--import`][]) are tried first, in reverse registration + order and for directories as well as files; 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 the read-only [`ZipProvider`][] + ([`zlib.ZipFile`][]; a `.zip` name is accepted without reading, as a fast + path). A source no provider claims fails with `ERR_VFS_INVALID_TARGET`. + +Provider selection is deferred until after [`--require`][] and [`--import`][] +preload modules have run, so a preloaded module can register a custom provider +that backs the mount. + +This only affects the paths under a mount. The running program's own +[`node:fs`][] calls to other paths work normally against the real file system, +the same as any other [`node:vfs`][] mount. + +Native addons (`.node` files) are supported: from a directory-backed mount +they're loaded directly from their real underlying path; from an +archive-backed mount, the addon's bytes are extracted to a content-hashed file +under the OS temporary directory before being loaded, and that file is +best-effort removed when the process exits. + +A [`Worker`][] created from a process started with `--vfs-mount` inherits the +same mounts unless its own `execArgv` explicitly supplies its own +`--vfs-mount`. + ### `--watch` + +* `entry` {Object} + * `name` {string} A short identifier for the provider, used in diagnostics. + * `canHandle` {Function} `(resolvedPath, stats) => boolean`. Returns `true` + if this provider should back `resolvedPath`. `stats` is the + `fs.statSync()` result, so a provider can claim directories, files, or + both. Prefer inspecting the stats and (for archives) the contents - for + example, sniffing a magic-number signature - over trusting the file + extension, so an archive can carry any name. + * `create` {Function} `(resolvedPath, stats) => VirtualProvider`. Returns the + provider that backs `resolvedPath`. Only ever called after `canHandle` + returned `true` for the same path. + +Registers a provider that the [`--vfs-mount`][] startup flag can select for a +mount source it recognizes. This is the extension point for supporting archive +formats beyond the built-in ZIP, or for wrapping the built-in directory and +ZIP providers: a module that implements, say, a 7-Zip provider registers it +here — typically from a module preloaded with [`--require`][] or [`--import`][], +so it is in place before `--vfs-mount` selects a provider (selection is +deferred until after both kinds of preload have run): + +```console +$ node --experimental-vfs -r @me/my-7z-provider --vfs-load --vfs-mount app.7z +``` + +```cjs +// @me/my-7z-provider (the preloaded module) +const vfs = require('node:vfs'); +const { SevenZipProvider } = require('./provider'); + +vfs.registerProvider({ + name: '7z', + // Recognize by the 7-Zip signature, not the file name. + canHandle(resolvedPath, stats) { + if (!stats.isFile()) return false; + const fd = require('fs').openSync(resolvedPath, 'r'); + try { + const magic = Buffer.alloc(6); + require('fs').readSync(fd, magic, 0, 6, 0); + return magic.equals(Buffer.from([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])); + } finally { + require('fs').closeSync(fd); + } + }, + create(resolvedPath) { return new SevenZipProvider(resolvedPath); }, +}); +``` + +Selection rules for a `--vfs-mount` source: + +* Registered providers are consulted first, in reverse registration order (the + most recently registered wins), so a custom provider always takes precedence + over the built-ins — even for a source they would otherwise handle. This lets + a provider back, wrap, or vet any mount, including a directory (for example, + a provider that wraps [`RealFSProvider`][] to record every read, or one that + verifies a signature before allowing use). +* If no registered provider claims the source, the built-ins handle it: a + directory with [`RealFSProvider`][], and a file whose bytes are a ZIP archive + with the built-in ZIP provider. A `.zip` name is accepted without reading the + file, as a fast path; any other name is recognized by locating the archive's + end-of-central-directory record. +* If no provider claims the source, `--vfs-mount` fails with + `ERR_VFS_INVALID_TARGET`. + +Registration is process-wide and affects only how the [`--vfs-mount`][] flag +chooses a provider; it does not change how [`vfs.create()`][] or +`new ZipProvider()` behave when a provider is passed explicitly. + ## Class: `VirtualFileSystem`