Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3633,6 +3633,78 @@ added: v0.1.3

Print node's version.

### `--vfs-load=source`

<!-- YAML
added: REPLACEME
-->

* `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 <directory>` 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`

<!-- YAML
added: REPLACEME
-->

* `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`

<!-- YAML
Expand Down Expand Up @@ -4066,6 +4138,7 @@ one is included in the list below.
* `--use-openssl-ca`
* `--use-system-ca`
* `--v8-pool-size`
* `--vfs-mount`
* `--watch-kill-signal`
* `--watch-path`
* `--watch-preserve-output`
Expand Down Expand Up @@ -4579,6 +4652,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--env-file-if-exists`]: #--env-file-if-existsfile
[`--env-file`]: #--env-filefile
[`--experimental-sea-config`]: single-executable-applications.md#1-generating-single-executable-preparation-blobs
[`--experimental-vfs`]: #--experimental-vfs
[`--heap-prof-dir`]: #--heap-prof-dir
[`--import`]: #--importmodule
[`--no-require-module`]: #--no-require-module
Expand All @@ -4590,6 +4664,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--require`]: #-r---require-module
[`--use-env-proxy`]: #--use-env-proxy
[`--use-system-ca`]: #--use-system-ca
[`--vfs-load`]: #--vfs-loadsource
[`--vfs-mount`]: #--vfs-mountsource
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
[`Buffer`]: buffer.md#class-buffer
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
Expand All @@ -4598,8 +4674,10 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`NODE_OPTIONS`]: #node_optionsoptions
[`NODE_USE_ENV_PROXY=1`]: #node_use_env_proxy1
[`NO_COLOR`]: https://no-color.org
[`RealFSProvider`]: vfs.md#class-realfsprovider
[`Web Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
[`YoungGenerationSizeFromSemiSpaceSize`]: https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.3.129/src/heap/heap.cc#328
[`ZipProvider`]: vfs.md#class-zipprovider
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder
Expand Down
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -3530,6 +3530,13 @@ An attempt was made to use something that was already closed.
While using the Performance Timing API (`perf_hooks`), no valid performance
entry types are found.

<a id="ERR_VFS_INVALID_TARGET"></a>

### `ERR_VFS_INVALID_TARGET`

A `--vfs-mount` source does not exist, is neither a regular file nor a
directory, or is a source no provider claims.

<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a>

### `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`
Expand Down
62 changes: 62 additions & 0 deletions doc/api/vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,65 @@ const memoryVfs = vfs.create();
const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/vfs-root'));
```

## `vfs.registerProvider(entry)`

<!-- YAML
added: REPLACEME
-->

* `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`

<!-- YAML
Expand Down Expand Up @@ -623,6 +682,9 @@ fields use synthetic but stable values:
[ES modules resolution algorithm]: esm.md#resolution-algorithm
[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
[Single Executable Application]: single-executable-applications.md
[`--import`]: cli.md#--importmodule
[`--require`]: cli.md#-r---require-module
[`--vfs-mount`]: cli.md#--vfs-mountsource
[`MemoryProvider`]: #class-memoryprovider
[`RealFSProvider`]: #class-realfsprovider
[`VirtualFileSystem`]: #class-virtualfilesystem
Expand Down
59 changes: 59 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,63 @@ amount of CPUs, but it may diverge in environments such as VMs or containers.
.It Fl v , Fl -version
Print node's version.
.
.It Fl -vfs-load Ns = Ns Ar source
.Bl -bullet
.It
\fBsource\fR \fB<string>\fR A directory or an archive file to mount and run.
.El
Requires \fB--experimental-vfs\fR. May be given at most once.
Mounts \fBsource\fR exactly as \fB--vfs-mount\fR does, and additionally runs the
entry point and all subsequent \fBrequire()\fR/\fBimport\fR resolution against that
mount rather than the real file system. The entry point is taken from the mount
the same way \fBnode <directory>\fR takes one: the mount's own \fBpackage.json\fR
\fB"main"\fR, or \fBindex.js\fR. Any positional command-line argument is the program's
own (available from \fBprocess.argv[2]\fR onward), never an entry-point override.
\fBprocess.argv[1]\fR reports \fBsource\fR 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 \fB--vfs-load\fR itself contributed, not
from an earlier \fB--vfs-mount\fR of the same source.
In worker threads \fB--vfs-load\fR mounts but does not load: a worker inherits the
same mounts, in the same order, and runs its own entry point.
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
the command line's decision, and the environment must not be able to redirect
it.
.Bd -literal
$ node --experimental-vfs --vfs-load=app.zip
$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip
.Ed
.
.It Fl -vfs-mount Ns = Ns Ar source
.Bl -bullet
.It
\fBsource\fR \fB<string>\fR A directory or an archive file to mount.
.El
Requires \fB--experimental-vfs\fR. May be repeated to mount several sources.
Mounts \fBsource\fR as a virtual file system (\fBnode:vfs\fR). 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 \fB--vfs-load\fR for the source to run from.
\fB--vfs-mount\fR and \fB--vfs-load\fR mount in the order they are written, so
.Bd -literal
$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c
.Ed
mounts \fBa\fR, \fBb\fR and \fBc\fR in that order and runs \fBb\fR. Mounts contributed by
\fBNODE_OPTIONS\fR are mounted before the command line's.
The provider backing a source is chosen from the source itself rather than from
its file name:
.Bl -bullet
.It
A directory is mounted with a \fBRealFSProvider\fR rooted there.
.It
A file whose bytes are a ZIP archive is mounted with a \fBZipProvider\fR, so
an archive can carry any name.
.El
Providers registered with \fBvfs.registerProvider()\fR (typically from a module
preloaded with \fB--require\fR or \fB--import\fR) 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.
.
.It Fl -watch
Starts Node.js in watch mode.
When in watch mode, changes in the watched files cause the Node.js process to
Expand Down Expand Up @@ -2284,6 +2341,8 @@ one is included in the list below.
.It
\fB--v8-pool-size\fR
.It
\fB--vfs-mount\fR
.It
\fB--watch-kill-signal\fR
.It
\fB--watch-path\fR
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,8 @@ E('ERR_USE_AFTER_CLOSE', '%s was closed', Error);
// This should probably be a `TypeError`.
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
'At least one valid performance entry type is required', Error);
E('ERR_VFS_INVALID_TARGET',
'%s is not a valid --vfs-mount source: must be an existing file or directory', Error);
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
'A dynamic import callback was not specified.', TypeError);
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG',
Expand Down
8 changes: 8 additions & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
const {
prepareWorkerThreadExecution,
initializeModuleLoaders,
finishVfsMounts,
markBootstrapComplete,
} = require('internal/process/pre_execution');

Expand Down Expand Up @@ -144,6 +145,13 @@ port.on('message', (message) => {
// initializeAsyncLoaderHooksOnLoaderHookWorker() which needs to run preloads
// after the asynchronous loader hooks are registered.
initializeModuleLoaders({ shouldSpawnLoaderHookWorker: true, shouldPreloadModules: true });
// Re-mount inherited --vfs-mount sources so their reserved paths (which a
// worker filename may point into) resolve in this thread too. With
// --import, mounting is deferred to after that loop in run_main, matching
// the main thread; finishVfsMounts() is idempotent so it runs once.
if (getOptionValue('--import').length === 0) {
finishVfsMounts();
}
}

if (!hasStdin)
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ async function asyncRunEntryPointWithESMLoader(callback) {
} else {
cascadedLoader.waitForAsyncLoaderHookInitialization();
}
// Any --import above may have registered a VFS provider, so mount now (no-op
// if prepareExecution already mounted in the no-import case).
require('internal/process/pre_execution').finishVfsMounts();
await callback(cascadedLoader);
} catch (err) {
if (hasUncaughtExceptionCaptureCallback()) {
Expand Down Expand Up @@ -138,6 +141,19 @@ function runEntryPointWithESMLoader(callback) {
* @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
*/
function executeUserEntryPoint(main = process.argv[1]) {
if (getOptionValue('[vfs_load_set]')) {
// The entry is a directory mount whose reserved root only exists after
// finishVfsMounts() runs (inside runEntryPointWithESMLoader, once --import
// preloads have had a chance to registerProvider()). Load it through the CJS
// main loader so the reserved directory resolves to its index the same way
// any require() of a directory does, sidestepping ESM directory-import.
runEntryPointWithESMLoader(() => {
const { getVfsLoadRoot } = require('internal/process/pre_execution');
const { wrapModuleLoad } = require('internal/modules/cjs/loader');
return wrapModuleLoad(getVfsLoadRoot(), null, true);
});
return;
}
let useESMLoader;
let resolvedMain;
if (getOptionValue('--entry-url')) {
Expand Down
Loading
Loading