Files
Matteo Collina 49ad95045a vfs: unflag
Enable node:vfs by default. --no-experimental-vfs disables node:vfs
exposure and --vfs-load.

Assisted-by: pi
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/66318
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
2026-10-15 07:53:03 +00:00

373 lines
14 KiB
JavaScript

'use strict';
// Covers --vfs-load: running a mounted directory's entry point with require()
// resolving inside the mount, a provider registered by either a -r (CJS) or an
// --import (ESM) preload backing a non-directory source, a ZIP archive claimed
// by the built-in provider, and a worker reaching the mount, whether it
// inherits the options or is given them itself.
//
// Native addon loading from a mount is not exercised here (it needs a compiled
// .node), only the startup wiring around it.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const { spawnSync } = require('child_process');
tmpdir.refresh();
let id = 0;
function fixture(name) { return path.join(tmpdir.path, `${id++}-${name}`); }
function run(args) {
return spawnSync(process.execPath, args, { encoding: 'utf8' });
}
// Node.js can be built without NODE_OPTIONS support, in which case the
// environment cannot carry a flag at all and there is nothing to assert.
const hasNodeOptions = !process.config.variables.node_without_node_options;
// NODE_OPTIONS is tokenized with shell-like quoting, so a path holding a space
// or a quote - as the checkout directory does on some CI machines - has to be
// quoted and escaped rather than interpolated raw.
function envArg(flag, value) {
return `"${flag}=${value.replace(/[\\"]/g, '\\$&')}"`;
}
// A directory source: the entry point runs and require() resolves inside it.
{
const dir = fixture('app');
fs.mkdirSync(path.join(dir, 'lib'), { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'),
"console.log(require('./lib/greet')());\n");
fs.writeFileSync(path.join(dir, 'lib', 'greet.js'),
"module.exports = () => 'hello from inside the mount';\n");
const res = run([`--vfs-load=${dir}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /hello from inside the mount/);
}
// --no-experimental-vfs disables --vfs-load.
{
const dir = fixture('disabled');
fs.mkdirSync(dir, { recursive: true });
const res = run(['--no-experimental-vfs', `--vfs-load=${dir}`]);
assert.notStrictEqual(res.status, 0);
assert.match(res.stderr, /--vfs-load requires node:vfs to be enabled/);
}
// A provider registered by a -r (CommonJS) preload backs a custom file format.
{
const providerModule = fixture('provider.js');
fs.writeFileSync(providerModule, `
'use strict';
const fs = require('fs');
const vfs = require('node:vfs');
const MAGIC = Buffer.from('CUSTOMFMT');
vfs.registerProvider({
name: 'customfmt',
canHandle(p, stats) {
if (!stats.isFile()) return false;
const fd = fs.openSync(p, 'r');
try {
const buf = Buffer.alloc(MAGIC.length);
fs.readSync(fd, buf, 0, MAGIC.length, 0);
return buf.equals(MAGIC);
} finally { fs.closeSync(fd); }
},
create(p) {
const body = fs.readFileSync(p).subarray(MAGIC.length).toString('utf8');
const provider = new vfs.MemoryProvider();
provider.writeFileSync('/index.js', body);
return provider;
},
});
`);
const target = fixture('app.customfmt');
fs.writeFileSync(target, Buffer.concat([
Buffer.from('CUSTOMFMT'),
Buffer.from("console.log('hello from custom provider');"),
]));
const res = run(['-r', providerModule, `--vfs-load=${target}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /hello from custom provider/);
}
// A provider registered by an --import (ES module) preload: this only works
// because mounting is deferred until after the --import loop has run.
{
const providerModule = fixture('provider.mjs');
fs.writeFileSync(providerModule, `
import fs from 'node:fs';
import { registerProvider, MemoryProvider } from 'node:vfs';
const MAGIC = Buffer.from('ESMFMT');
registerProvider({
name: 'esmfmt',
canHandle(p, stats) {
if (!stats.isFile()) return false;
return fs.readFileSync(p).subarray(0, MAGIC.length).equals(MAGIC);
},
create(p) {
const body = fs.readFileSync(p).subarray(MAGIC.length).toString('utf8');
const provider = new MemoryProvider();
provider.writeFileSync('/index.js', body);
return provider;
},
});
`);
const target = fixture('app.esmfmt');
fs.writeFileSync(target, Buffer.concat([
Buffer.from('ESMFMT'),
Buffer.from("console.log('hello from ESM-imported provider');"),
]));
const res = run([
'--import', pathToFileURL(providerModule).href,
`--vfs-load=${target}`,
]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /hello from ESM-imported provider/);
}
// A ZIP archive is claimed by the built-in provider (detected by opening it,
// not by extension).
{
const zlib = require('zlib');
const zipPath = fixture('app.zip');
const entry = zlib.ZipEntry.createSync(
'index.js', Buffer.from("console.log('hello from zip archive');"));
const chunks = [];
for (const chunk of zlib.createZipArchiveSync([entry])) chunks.push(chunk);
fs.writeFileSync(zipPath, Buffer.concat(chunks));
const res = run([`--vfs-load=${zipPath}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /hello from zip archive/);
}
// A worker inherits the mount, so a worker script that lives inside it
// (addressed here via the entry's own __dirname) resolves and runs.
{
const dir = fixture('worker-app');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), `
'use strict';
const path = require('path');
const { Worker } = require('worker_threads');
const w = new Worker(path.join(__dirname, 'worker.js'));
w.on('message', (m) => { console.log(m); process.exit(0); });
w.on('error', (e) => { console.error(e); process.exit(1); });
`);
fs.writeFileSync(path.join(dir, 'worker.js'), `
'use strict';
require('worker_threads').parentPort.postMessage('hello from worker in mount');
`);
const res = run([`--vfs-load=${dir}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /hello from worker in mount/);
}
// The same, for a worker whose nearest package.json inside the mount says
// "module": the entry point's type is read from the mount rather than from
// the real file system above the reserved mount point.
{
const dir = fixture('worker-esm-app');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'package.json'),
'{"type":"module","main":"index.js"}\n');
fs.writeFileSync(path.join(dir, 'index.js'), `
import path from 'node:path';
import { Worker } from 'node:worker_threads';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const w = new Worker(path.join(here, 'worker.js'));
w.on('message', (m) => { console.log(m); process.exit(0); });
w.on('error', (e) => { console.error(e); process.exit(1); });
`);
// No extension hint and no CJS wrapper: this only parses if the worker is
// loaded as ESM, which takes reading "type" out of the mount's package.json.
fs.writeFileSync(path.join(dir, 'worker.js'), `
import { parentPort } from 'node:worker_threads';
parentPort.postMessage('hello from esm worker in mount');
`);
const res = run([`--vfs-load=${dir}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /hello from esm worker in mount/);
}
// A worker created with its own execArgv does not inherit the parent's options,
// so it has to be given the mount itself. Because the loaded source is always
// the first file system a thread mounts, it lands at the same reserved mount
// point in both threads, and a worker path the parent built still resolves.
{
const dir = fixture('worker-execargv-app');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), `
'use strict';
const path = require('path');
const { Worker } = require('worker_threads');
const w = new Worker(path.join(__dirname, 'worker.js'), {
execArgv: ['--vfs-load=' + process.argv[1]],
});
w.on('message', (m) => { console.log(m); process.exit(0); });
w.on('error', (e) => { console.error(e); process.exit(1); });
`);
fs.writeFileSync(path.join(dir, 'worker.js'), `
'use strict';
require('worker_threads').parentPort.postMessage('worker ran from ' + __dirname);
`);
// A preload that mounts a file system of its own runs before the --vfs-load
// source is mounted, and still does not move it.
const preload = fixture('mounting-preload.js');
fs.writeFileSync(preload, `
'use strict';
require('node:vfs').create().mount();
`);
const seen = new Set();
for (const args of [[`--vfs-load=${dir}`], ['-r', preload, `--vfs-load=${dir}`]]) {
const res = run(args);
assert.strictEqual(res.status, 0, res.stderr);
const [, dirname] = /worker ran from (\S+)/.exec(res.stdout);
seen.add(dirname);
}
// The worker resolved a path the main thread built, in both runs, and that
// path did not move between them.
assert.strictEqual(seen.size, 1, [...seen].join());
}
// Without that flag the worker has no mount to load from, so a script in the
// mount cannot be its entry point.
{
const dir = fixture('worker-execargv-missing');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), `
'use strict';
const path = require('path');
const { Worker } = require('worker_threads');
const w = new Worker(path.join(__dirname, 'worker.js'), { execArgv: [] });
w.on('message', (m) => { console.log('ran:' + m); process.exit(0); });
w.on('error', (e) => { console.log('failed:' + e.code); process.exit(0); });
`);
fs.writeFileSync(path.join(dir, 'worker.js'), `
'use strict';
require('worker_threads').parentPort.postMessage('unexpected');
`);
const res = run([`--vfs-load=${dir}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /failed:/);
assert.doesNotMatch(res.stdout, /ran:/);
}
// --vfs-load names the source it loads, so it always takes a value.
{
const res = run(['--vfs-load']);
assert.notStrictEqual(res.status, 0);
assert.match(res.stderr, /--vfs-load requires an argument/);
}
// The value may also be given as a separate argument.
{
const dir = fixture('spaced-value');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), "console.log('ran:spaced');\n");
const res = run(['--vfs-load', dir]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /ran:spaced/);
}
// A source whose path holds spaces or quotes is mounted as given. Windows
// forbids `"` in a file name, so only the spaces and the `$` are exercised
// there.
{
const oddName = common.isWindows ? `${id++}-od d $x` : `${id++}-od d "q" $x`;
const dir = path.join(tmpdir.path, oddName);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), 'console.log("ran:odd");\n');
const res = run([`--vfs-load=${dir}`]);
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /ran:odd/);
}
// --vfs-load may only be given once: a second one would otherwise quietly
// replace the first.
{
const dirs = {};
for (const name of ['once-a', 'once-b']) {
dirs[name] = fixture(name);
fs.mkdirSync(dirs[name], { recursive: true });
fs.writeFileSync(path.join(dirs[name], 'index.js'),
`console.log('ran:${name}');\n`);
}
const twice = run([`--vfs-load=${dirs['once-a']}`,
`--vfs-load=${dirs['once-b']}`]);
assert.notStrictEqual(twice.status, 0);
assert.match(twice.stderr, /--vfs-load may only be given once/);
}
// --vfs-load picks the entry point, so it is refused in NODE_OPTIONS: the
// environment must not be able to redirect what a `node <args>` run executes.
// Everything but the flag under test is passed on the command line, so a build
// that ignores NODE_OPTIONS cannot make this pass for the wrong reason.
if (hasNodeOptions) {
const dir = fixture('env-refused');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), 'console.log("ran");\n');
// On its own, and alongside a --vfs-load the command line legitimately gave:
// the environment is refused either way rather than merged.
for (const args of [[], [`--vfs-load=${dir}`]]) {
const res = spawnSync(process.execPath, args, {
encoding: 'utf8',
env: { ...process.env, NODE_OPTIONS: envArg('--vfs-load', dir) },
});
assert.notStrictEqual(res.status, 0);
assert.match(res.stderr, /--vfs-load.* is not allowed in NODE_OPTIONS/);
}
}
// The old --experimental-vfs option remains accepted in NODE_OPTIONS. Its
// positive form no longer gates --vfs-load, and its negated form disables it.
if (hasNodeOptions) {
const dir = fixture('env-flag-cli-load');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'), 'console.log("ran");\n');
const res = spawnSync(process.execPath, [`--vfs-load=${dir}`], {
encoding: 'utf8',
env: { ...process.env, NODE_OPTIONS: '--experimental-vfs' },
});
assert.strictEqual(res.status, 0, res.stderr);
assert.match(res.stdout, /ran/);
const disabled = spawnSync(process.execPath, [`--vfs-load=${dir}`], {
encoding: 'utf8',
env: { ...process.env, NODE_OPTIONS: '--no-experimental-vfs' },
});
assert.notStrictEqual(disabled.status, 0);
assert.match(disabled.stderr, /--vfs-load requires node:vfs to be enabled/);
}
// Under --vfs-load the entry point comes from the mount, so no positional
// argument is consumed as one: every positional reaches the program verbatim
// from argv[2] onward, and argv[1] reports the mounted source.
{
const dir = fixture('argv-app');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'index.js'),
'console.log(JSON.stringify(process.argv.slice(1)));\n');
for (const extra of [[], ['alpha'], ['alpha', 'beta']]) {
const res = run([`--vfs-load=${dir}`, ...extra]);
assert.strictEqual(res.status, 0, res.stderr);
assert.deepStrictEqual(JSON.parse(res.stdout), [dir, ...extra]);
}
// A path-like argument must not be resolved against the real file system the
// way a genuine entry-point argument would be.
const res = run([`--vfs-load=${dir}`, './not/an/entry.js']);
assert.strictEqual(res.status, 0, res.stderr);
assert.deepStrictEqual(JSON.parse(res.stdout), [dir, './not/an/entry.js']);
}