Skip to content

Commit 0abd2c8

Browse files
committed
vfs: drop --vfs-mount, keep --vfs-load alone
--vfs-mount mounted a source without running it, and shared one ordered list of sources with --vfs-load, so neither option could say which entry it had contributed. Both the entry point and the layer reserved for it were recovered from the position of --vfs-load among the mounts, in a list NODE_OPTIONS could prepend to. Nothing needs more than one mount from the command line: a program that wants more can mount them itself through node:vfs, where it also gets the instance. Remove --vfs-mount. --vfs-load keeps its own single source, so nothing has to recover which flag contributed which mount: the one source is mounted at the layer reserved for it, and the entry point comes from there. Workers are unchanged: the source is inherited but the entry point is not, so a worker mounts it and runs its own entry, which may live inside the mount. ERR_VFS_INVALID_TARGET now names --vfs-load as the source's origin, and the startup test moves to test-vfs-load.js, with the cases that covered mounting without loading removed. Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
1 parent a2cd24b commit 0abd2c8

13 files changed

Lines changed: 153 additions & 405 deletions

File tree

‎doc/api/cli.md‎

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3795,61 +3795,18 @@ added: REPLACEME
37953795

37963796
Requires [`--experimental-vfs`][]. May be given at most once.
37973797

3798-
Mounts `source` exactly as [`--vfs-mount`][] does, and additionally runs the
3799-
entry point and all subsequent `require()`/`import` resolution against that
3800-
mount rather than the real file system. The entry point is taken from the mount
3801-
the same way `node <directory>` takes one: the mount's own `package.json`
3802-
`"main"`, or `index.js`. Any positional command-line argument is the program's
3803-
own (available from `process.argv[2]` onward), never an entry-point override.
3798+
Mounts `source` as a virtual file system ([`node:vfs`][]), and runs the entry
3799+
point and all subsequent `require()`/`import` resolution against that mount
3800+
rather than the real file system. The mount is placed at a reserved mount point
3801+
assigned by Node.js, so it never shadows real paths and no target can be
3802+
chosen. The entry point is taken from the mount the same way `node <directory>`
3803+
takes one: the mount's own `package.json` `"main"`, or `index.js`. Any
3804+
positional command-line argument is the program's own (available from
3805+
`process.argv[2]` onward), never an entry-point override.
38043806

38053807
`process.argv[1]` reports `source` rather than the reserved mount point, since
38063808
the mount point is an opaque implementation detail.
38073809

3808-
Mounting the same source twice mounts it twice, at two separate mount points.
3809-
The entry point then comes from the mount `--vfs-load` itself contributed, not
3810-
from an earlier `--vfs-mount` of the same source.
3811-
3812-
In worker threads `--vfs-load` mounts but does not load: a worker inherits the
3813-
same mounts, in the same order, and runs its own entry point.
3814-
3815-
The source `--vfs-load` names is mounted at the same reserved mount point in
3816-
every thread, whatever else that thread mounts, so a path into it stays valid
3817-
in a worker - including one created with its own `execArgv`, which does not
3818-
inherit the parent's options and has to be given `--vfs-load` again.
3819-
3820-
`--vfs-load` is not permitted in [`NODE_OPTIONS`][]: which entry point runs is
3821-
the command line's decision, and the environment must not be able to redirect
3822-
it.
3823-
3824-
```console
3825-
$ node --experimental-vfs --vfs-load=app.zip
3826-
$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip
3827-
```
3828-
3829-
### `--vfs-mount=source`
3830-
3831-
<!-- YAML
3832-
added: REPLACEME
3833-
-->
3834-
3835-
* `source` {string} A directory or an archive file to mount.
3836-
3837-
Requires [`--experimental-vfs`][]. May be repeated to mount several sources.
3838-
3839-
Mounts `source` as a virtual file system ([`node:vfs`][]). Each mount is placed
3840-
at a reserved mount point assigned by Node.js, so mounts never shadow real
3841-
paths and no target can be chosen. Mounting alone does not change the entry
3842-
point; use [`--vfs-load`][] for the source to run from.
3843-
3844-
`--vfs-mount` and [`--vfs-load`][] mount in the order they are written, so
3845-
3846-
```console
3847-
$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c
3848-
```
3849-
3850-
mounts `a`, `b` and `c` in that order and runs `b`. Mounts contributed by
3851-
[`NODE_OPTIONS`][] are mounted before the command line's.
3852-
38533810
The provider backing a source is chosen from the source itself rather than from
38543811
its file name:
38553812

@@ -3862,6 +3819,22 @@ preloaded with [`--require`][] or [`--import`][]) are consulted first, in
38623819
reverse registration order, and may claim directories as well as files. If no
38633820
provider claims the source, Node.js exits with an error.
38643821

3822+
In worker threads `--vfs-load` mounts but does not load: a worker inherits the
3823+
mount and runs its own entry point, which may itself live in the mount.
3824+
3825+
The source is mounted at the same reserved mount point in every thread,
3826+
whatever else that thread mounts, so a path into it stays valid in a worker -
3827+
including one created with its own `execArgv`, which does not inherit the
3828+
parent's options and has to be given `--vfs-load` again.
3829+
3830+
`--vfs-load` is not permitted in [`NODE_OPTIONS`][]: which entry point runs is
3831+
the command line's decision, and the environment must not be able to redirect
3832+
it.
3833+
3834+
```console
3835+
$ node --experimental-vfs --vfs-load=app.zip
3836+
```
3837+
38653838
### `--watch`
38663839

38673840
<!-- YAML
@@ -4304,7 +4277,6 @@ one is included in the list below.
43044277
* `--use-openssl-ca`
43054278
* `--use-system-ca`
43064279
* `--v8-pool-size`
4307-
* `--vfs-mount`
43084280
* `--watch-kill-signal`
43094281
* `--watch-path`
43104282
* `--watch-preserve-output`
@@ -4830,8 +4802,6 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
48304802
[`--require`]: #-r---require-module
48314803
[`--use-env-proxy`]: #--use-env-proxy
48324804
[`--use-system-ca`]: #--use-system-ca
4833-
[`--vfs-load`]: #--vfs-loadsource
4834-
[`--vfs-mount`]: #--vfs-mountsource
48354805
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
48364806
[`Buffer`]: buffer.md#class-buffer
48374807
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html

‎doc/api/errors.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3569,7 +3569,7 @@ entry types are found.
35693569

35703570
### `ERR_VFS_INVALID_TARGET`
35713571

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

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

‎doc/api/vfs.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ added: REPLACEME
106106
* `create` {Function} Called with the resolved path and its [`fs.Stats`][].
107107
Returns the {VirtualProvider} backing the source.
108108

109-
Registers a provider that [`--vfs-mount`][] can select for a source it
109+
Registers a provider that [`--vfs-load`][] can select for a source it
110110
recognizes, so a file format Node.js has no built-in provider for can still be
111111
mounted.
112112

@@ -118,7 +118,7 @@ source, the built-in providers handle it: a directory with
118118
[`RealFSProvider`][], and a file whose bytes are a ZIP archive with
119119
[`ZipProvider`][].
120120

121-
Providers must be registered before the mounts are created. Register from a
121+
Providers must be registered before the source is mounted. Register from a
122122
module preloaded with [`--require`][] or [`--import`][]:
123123

124124
```cjs
@@ -758,7 +758,7 @@ fields use synthetic but stable values:
758758
[Single Executable Application]: single-executable-applications.md
759759
[`--import`]: cli.md#--importmodule
760760
[`--require`]: cli.md#-r---require-module
761-
[`--vfs-mount`]: cli.md#--vfs-mountsource
761+
[`--vfs-load`]: cli.md#--vfs-loadsource
762762
[`MemoryProvider`]: #class-memoryprovider
763763
[`RealFSProvider`]: #class-realfsprovider
764764
[`VirtualFileSystem`]: #class-virtualfilesystem

‎doc/node.1‎

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,47 +1888,16 @@ Print node's version.
18881888
\fBsource\fR \fB{string}\fR A directory or an archive file to mount and run.
18891889
.El
18901890
Requires \fB--experimental-vfs\fR. May be given at most once.
1891-
Mounts \fBsource\fR exactly as \fB--vfs-mount\fR does, and additionally runs the
1892-
entry point and all subsequent \fBrequire()\fR/\fBimport\fR resolution against that
1893-
mount rather than the real file system. The entry point is taken from the mount
1894-
the same way \fBnode <directory>\fR takes one: the mount's own \fBpackage.json\fR
1895-
\fB"main"\fR, or \fBindex.js\fR. Any positional command-line argument is the program's
1896-
own (available from \fBprocess.argv[2]\fR onward), never an entry-point override.
1891+
Mounts \fBsource\fR as a virtual file system (\fBnode:vfs\fR), and runs the entry
1892+
point and all subsequent \fBrequire()\fR/\fBimport\fR resolution against that mount
1893+
rather than the real file system. The mount is placed at a reserved mount point
1894+
assigned by Node.js, so it never shadows real paths and no target can be
1895+
chosen. The entry point is taken from the mount the same way \fBnode <directory>\fR
1896+
takes one: the mount's own \fBpackage.json\fR \fB"main"\fR, or \fBindex.js\fR. Any
1897+
positional command-line argument is the program's own (available from
1898+
\fBprocess.argv[2]\fR onward), never an entry-point override.
18971899
\fBprocess.argv[1]\fR reports \fBsource\fR rather than the reserved mount point, since
18981900
the mount point is an opaque implementation detail.
1899-
Mounting the same source twice mounts it twice, at two separate mount points.
1900-
The entry point then comes from the mount \fB--vfs-load\fR itself contributed, not
1901-
from an earlier \fB--vfs-mount\fR of the same source.
1902-
In worker threads \fB--vfs-load\fR mounts but does not load: a worker inherits the
1903-
same mounts, in the same order, and runs its own entry point.
1904-
The source \fB--vfs-load\fR names is mounted at the same reserved mount point in
1905-
every thread, whatever else that thread mounts, so a path into it stays valid
1906-
in a worker - including one created with its own \fBexecArgv\fR, which does not
1907-
inherit the parent's options and has to be given \fB--vfs-load\fR again.
1908-
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
1909-
the command line's decision, and the environment must not be able to redirect
1910-
it.
1911-
.Bd -literal
1912-
$ node --experimental-vfs --vfs-load=app.zip
1913-
$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip
1914-
.Ed
1915-
.
1916-
.It Fl -vfs-mount Ns = Ns Ar source
1917-
.Bl -bullet
1918-
.It
1919-
\fBsource\fR \fB{string}\fR A directory or an archive file to mount.
1920-
.El
1921-
Requires \fB--experimental-vfs\fR. May be repeated to mount several sources.
1922-
Mounts \fBsource\fR as a virtual file system (\fBnode:vfs\fR). Each mount is placed
1923-
at a reserved mount point assigned by Node.js, so mounts never shadow real
1924-
paths and no target can be chosen. Mounting alone does not change the entry
1925-
point; use \fB--vfs-load\fR for the source to run from.
1926-
\fB--vfs-mount\fR and \fB--vfs-load\fR mount in the order they are written, so
1927-
.Bd -literal
1928-
$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c
1929-
.Ed
1930-
mounts \fBa\fR, \fBb\fR and \fBc\fR in that order and runs \fBb\fR. Mounts contributed by
1931-
\fBNODE_OPTIONS\fR are mounted before the command line's.
19321901
The provider backing a source is chosen from the source itself rather than from
19331902
its file name:
19341903
.Bl -bullet
@@ -1942,6 +1911,18 @@ Providers registered with \fBvfs.registerProvider()\fR (typically from a module
19421911
preloaded with \fB--require\fR or \fB--import\fR) are consulted first, in
19431912
reverse registration order, and may claim directories as well as files. If no
19441913
provider claims the source, Node.js exits with an error.
1914+
In worker threads \fB--vfs-load\fR mounts but does not load: a worker inherits the
1915+
mount and runs its own entry point, which may itself live in the mount.
1916+
The source is mounted at the same reserved mount point in every thread,
1917+
whatever else that thread mounts, so a path into it stays valid in a worker -
1918+
including one created with its own \fBexecArgv\fR, which does not inherit the
1919+
parent's options and has to be given \fB--vfs-load\fR again.
1920+
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
1921+
the command line's decision, and the environment must not be able to redirect
1922+
it.
1923+
.Bd -literal
1924+
$ node --experimental-vfs --vfs-load=app.zip
1925+
.Ed
19451926
.
19461927
.It Fl -watch
19471928
Starts Node.js in watch mode.
@@ -2434,8 +2415,6 @@ one is included in the list below.
24342415
.It
24352416
\fB--v8-pool-size\fR
24362417
.It
2437-
\fB--vfs-mount\fR
2438-
.It
24392418
\fB--watch-kill-signal\fR
24402419
.It
24412420
\fB--watch-path\fR

‎lib/internal/errors.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1962,7 +1962,7 @@ E('ERR_USE_AFTER_CLOSE', '%s was closed', Error);
19621962
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
19631963
'At least one valid performance entry type is required', Error);
19641964
E('ERR_VFS_INVALID_TARGET',
1965-
'%s is not a valid --vfs-mount source: must be an existing file or directory', Error);
1965+
'%s is not a valid --vfs-load source: must be an existing file or directory', Error);
19661966
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
19671967
'A dynamic import callback was not specified.', TypeError);
19681968
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG',

‎lib/internal/main/worker_thread.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ port.on('message', (message) => {
145145
// initializeAsyncLoaderHooksOnLoaderHookWorker() which needs to run preloads
146146
// after the asynchronous loader hooks are registered.
147147
initializeModuleLoaders({ shouldSpawnLoaderHookWorker: true, shouldPreloadModules: true });
148-
// Re-mount inherited --vfs-mount sources so their reserved paths (which a
149-
// worker filename may point into) resolve in this thread too. With
148+
// Re-mount the inherited --vfs-load source so its reserved path (which a
149+
// worker filename may point into) resolves in this thread too. With
150150
// --import, mounting is deferred to after that loop in run_main, matching
151151
// the main thread; finishVfsMounts() is idempotent so it runs once.
152152
if (getOptionValue('--import').length === 0) {

‎lib/internal/process/pre_execution.js‎

Lines changed: 41 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ const {
1414
ObjectDefineProperty,
1515
ObjectFreeze,
1616
String,
17-
StringPrototypeIndexOf,
18-
StringPrototypeSlice,
1917
globalThis,
2018
} = primordials;
2119

@@ -216,98 +214,58 @@ function setupVmModules() {
216214
let vfsMounted = false;
217215
let vfsLoadRoot;
218216

219-
// --vfs-mount and --vfs-load append to one list, so `mounts` is already in the
220-
// order the command line gave, and the entry point comes from whichever of them
221-
// --vfs-load contributed. The parser stores plain strings and cannot record
222-
// which flag produced an entry, so its position is recovered from execArgv -
223-
// the command line's own node options, in order. NODE_OPTIONS may add mounts
224-
// but not a --vfs-load, so anything it contributed sits ahead of these.
225-
// Returns -1 when no --vfs-load was given.
226-
//
227-
// A worker does not run the --vfs-load entry (see node_worker.cc), but it must
228-
// still recognize which source that entry is, so as to give it the reserved
229-
// layer its mount point has on every other thread.
230-
function getVfsLoadIndex(mountCount) {
231-
const execArgv = process.execArgv;
232-
let seen = 0;
233-
let found = -1;
234-
for (let i = 0; i < execArgv.length; i++) {
235-
const arg = execArgv[i];
236-
let name = arg;
237-
const eq = StringPrototypeIndexOf(arg, '=');
238-
let spaced = false;
239-
if (eq !== -1) {
240-
name = StringPrototypeSlice(arg, 0, eq);
241-
} else {
242-
// `--vfs-mount value`: the value is the next argument, so skip it rather
243-
// than counting it as a flag of its own.
244-
spaced = true;
245-
}
246-
if (name !== '--vfs-mount' && name !== '--vfs-load') continue;
247-
if (name === '--vfs-load') found = seen;
248-
seen++;
249-
if (spaced) i++;
250-
}
251-
if (found === -1) return -1;
252-
// Mounts from NODE_OPTIONS are parsed first and so precede the command
253-
// line's; `seen` counts only the latter.
254-
return mountCount - seen + found;
255-
}
256-
257-
// Mounts every --vfs-mount source. Called from prepareExecution() when there is
217+
// Mounts the --vfs-load source. Called from prepareExecution() when there is
258218
// no --import, and otherwise from run_main after the --import loop has run; the
259-
// guard makes the second call a no-op so a provider registered by either a -r or
260-
// an --import preload is available before its source's provider is chosen.
219+
// guard makes the second call a no-op so a provider registered by either a -r
220+
// or an --import preload is available before the source's provider is chosen.
261221
function finishVfsMounts() {
262222
if (vfsMounted) return;
263223
vfsMounted = true;
264224

265-
const entries = getOptionValue('--vfs-mount');
266-
if (entries.length === 0) return;
267-
emitExperimentalWarning('--vfs-mount');
225+
const source = getOptionValue('--vfs-load');
226+
if (source === '') return;
227+
emitExperimentalWarning('--vfs-load');
268228

269229
const fs = require('fs');
270230
const path = require('path');
271231
const { selectProvider } = require('internal/vfs/provider_registry');
272232
const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
273233

274-
const loadIndex = getVfsLoadIndex(entries.length);
275-
// [vfs_load_set] is forced off in workers (see node_worker.cc), so a worker
276-
// mounts every source, the --vfs-load one at its reserved layer, and runs its
277-
// own entry rather than the mount's.
278-
const loads = getOptionValue('[vfs_load_set]');
279-
for (let i = 0; i < entries.length; i++) {
280-
const resolvedSource = path.resolve(entries[i]);
281-
let stats;
282-
try {
283-
stats = fs.statSync(resolvedSource);
284-
} catch {
285-
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
286-
}
287-
if (!stats.isDirectory() && !stats.isFile()) {
288-
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
289-
}
290-
const provider = selectProvider(resolvedSource, stats);
291-
if (provider === null) {
292-
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
293-
}
294-
const vfs = new VirtualFileSystem(provider, {
295-
__proto__: null,
296-
emitExperimentalWarning: false,
297-
[kLoadLayer]: i === loadIndex,
298-
});
299-
const mountPoint = vfs.mount();
300-
// The mount --vfs-load contributed is what the entry is require()d from;
301-
// process.argv[1] names the real source instead, since the reserved mount
302-
// point is an opaque implementation detail.
303-
//
304-
// The source is spliced in rather than assigned over argv[1]: the entry
305-
// comes from the mount, so nothing was consumed as an entry point and the
306-
// first positional argument is the program's own. Overwriting would drop it.
307-
if (i === loadIndex && loads) {
308-
vfsLoadRoot = mountPoint;
309-
ArrayPrototypeSplice(process.argv, 1, 0, resolvedSource);
310-
}
234+
const resolvedSource = path.resolve(source);
235+
let stats;
236+
try {
237+
stats = fs.statSync(resolvedSource);
238+
} catch {
239+
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
240+
}
241+
if (!stats.isDirectory() && !stats.isFile()) {
242+
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
243+
}
244+
const provider = selectProvider(resolvedSource, stats);
245+
if (provider === null) {
246+
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
247+
}
248+
// The source is mounted at the layer reserved for it, so it is at the same
249+
// reserved path in every thread.
250+
const vfs = new VirtualFileSystem(provider, {
251+
__proto__: null,
252+
emitExperimentalWarning: false,
253+
[kLoadLayer]: true,
254+
});
255+
const mountPoint = vfs.mount();
256+
// A worker inherits the source but not [vfs_load_set] (see node_worker.cc):
257+
// it mounts the same source, and so reaches it at the same reserved path,
258+
// but runs its own entry point.
259+
//
260+
// On the main thread the entry is require()d from the mount point, while
261+
// process.argv[1] reports the source, since the reserved mount point is an
262+
// opaque implementation detail. The source is spliced in rather than
263+
// assigned over argv[1]: the entry comes from the mount, so nothing was
264+
// consumed as an entry point and the first positional argument is the
265+
// program's own. Overwriting would drop it.
266+
if (getOptionValue('[vfs_load_set]')) {
267+
vfsLoadRoot = mountPoint;
268+
ArrayPrototypeSplice(process.argv, 1, 0, resolvedSource);
311269
}
312270
}
313271

0 commit comments

Comments
 (0)