mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
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>
373 lines
14 KiB
JavaScript
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']);
|
|
}
|