mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Mounting a virtual file system requires calling vfs.mount() from inside the program, so a program cannot itself be served from one: something already running has to mount the VFS first. Add two startup flags. --vfs-mount=<source> mounts a directory or an archive as a virtual file system, and may be repeated. --vfs-load=<source> mounts that source the same way and additionally runs the entry point and all subsequent require()/import resolution against it rather than against the real file system, so an application can be run straight out of a directory or a ZIP archive: node --experimental-vfs --vfs-load=my-app.zip Both options append to one list, so mounts happen in the order written: node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c mounts a, b and c in that order and runs b. Mounting the same source twice mounts it twice, at two mount points, and the entry point comes from the mount --vfs-load contributed rather than the earlier one. The provider backing a source is chosen from the source itself rather than from its name: a directory is served by RealFSProvider and a file whose bytes are a ZIP archive by ZipProvider, so an archive can carry any extension. vfs.registerProvider() registers a provider for formats there is no built-in for; selection is deferred until -r and --import preloads have run, so a preloaded module can register one before its source is claimed. The entry point comes from the mount the way `node <directory>` takes one, from package.json "main" or index.js. Nothing is consumed as an entry point argument, so every positional reaches the program unchanged from argv[2] on, and argv[1] reports the named source rather than the reserved mount point, which is an opaque implementation detail. --vfs-load may be given at most once, and is not permitted in NODE_OPTIONS: which entry point runs is the command line's decision, and NODE_OPTIONS is parsed first, so an environment variable could otherwise redirect any invocation. --vfs-mount is permitted there. Workers inherit the same mounts in the same order but not the loading: a worker mounts what the parent mounted and runs its own entry point. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: https://github.com/nodejs/node/pull/65748 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
188 lines
7.2 KiB
JavaScript
188 lines
7.2 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
StringPrototypeEndsWith,
|
|
globalThis,
|
|
} = primordials;
|
|
|
|
const { getOptionValue } = require('internal/options');
|
|
// Through loaderMethods rather than the `modules` binding directly: a mounted
|
|
// VFS overrides these, and a worker entry point can live inside one.
|
|
const { loaderMethods } = require('internal/modules/helpers');
|
|
const path = require('path');
|
|
const { pathToFileURL, URL } = require('internal/url');
|
|
const { kEmptyObject, getCWDURL } = require('internal/util');
|
|
const {
|
|
hasUncaughtExceptionCaptureCallback,
|
|
} = require('internal/process/execution');
|
|
const {
|
|
triggerUncaughtException,
|
|
} = internalBinding('errors');
|
|
const {
|
|
privateSymbols: {
|
|
entry_point_promise_private_symbol,
|
|
},
|
|
} = internalBinding('util');
|
|
/**
|
|
* Get the absolute path to the main entry point.
|
|
* @param {string} main - Entry point path
|
|
* @returns {string|undefined}
|
|
*/
|
|
function resolveMainPath(main) {
|
|
/** @type {string} */
|
|
let mainPath;
|
|
// Extension searching for the main entry point is supported for backward compatibility.
|
|
// Module._findPath is monkey-patchable here.
|
|
const { Module } = require('internal/modules/cjs/loader');
|
|
mainPath = Module._findPath(path.resolve(main), null, true);
|
|
if (!mainPath) { return; }
|
|
|
|
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
|
if (!preserveSymlinksMain) {
|
|
mainPath = loaderMethods.realpathSync(mainPath);
|
|
}
|
|
|
|
return mainPath;
|
|
}
|
|
|
|
/**
|
|
* Determine whether the main entry point should be loaded through the ESM Loader.
|
|
* @param {string} mainPath - Absolute path to the main entry point
|
|
* @returns {boolean}
|
|
*/
|
|
function shouldUseESMLoader(mainPath) {
|
|
/**
|
|
* @type {string[]} userLoaders A list of custom loaders registered by the user
|
|
* (or an empty list when none have been registered).
|
|
*/
|
|
const userLoaders = getOptionValue('--experimental-loader');
|
|
/**
|
|
* @type {string[]} userImports A list of preloaded modules registered by the user
|
|
* (or an empty list when none have been registered).
|
|
*/
|
|
const userImports = getOptionValue('--import');
|
|
if (userLoaders.length > 0 || userImports.length > 0) { return true; }
|
|
|
|
// Determine the module format of the entry point.
|
|
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
|
|
if (mainPath && StringPrototypeEndsWith(mainPath, '.wasm')) { return true; }
|
|
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
|
|
|
|
if (getOptionValue('--strip-types')) {
|
|
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cts')) { return false; }
|
|
// This will likely change in the future to start with commonjs loader by default
|
|
if (mainPath && StringPrototypeEndsWith(mainPath, '.mts')) { return true; }
|
|
}
|
|
|
|
const type = loaderMethods.getNearestParentPackageJSONType(mainPath);
|
|
|
|
// No package.json or no `type` field.
|
|
if (type === undefined || type === 'none') {
|
|
return false;
|
|
}
|
|
|
|
return type === 'module';
|
|
}
|
|
|
|
/**
|
|
* @param {function(ModuleLoader):ModuleWrap|undefined} callback
|
|
*/
|
|
async function asyncRunEntryPointWithESMLoader(callback) {
|
|
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
|
try {
|
|
const userImports = getOptionValue('--import');
|
|
if (userImports.length > 0) {
|
|
const parentURL = getCWDURL().href;
|
|
for (let i = 0; i < userImports.length; i++) {
|
|
await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
|
|
}
|
|
} 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()) {
|
|
process._fatalException(err);
|
|
return;
|
|
}
|
|
triggerUncaughtException(
|
|
err,
|
|
true, /* fromPromise */
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This initializes the ESM loader and runs --import (if any) before executing the
|
|
* callback to run the entry point.
|
|
* If the callback intends to evaluate a ESM module as entry point, it should return
|
|
* the corresponding ModuleWrap so that stalled TLA can be checked a process exit.
|
|
* @param {function(ModuleLoader):ModuleWrap|undefined} callback
|
|
* @returns {Promise}
|
|
*/
|
|
function runEntryPointWithESMLoader(callback) {
|
|
const promise = asyncRunEntryPointWithESMLoader(callback);
|
|
// Register the promise - if by the time the event loop finishes running, this is
|
|
// still unsettled, we'll search the graph from the entry point module and print
|
|
// the location of any unsettled top-level await found.
|
|
globalThis[entry_point_promise_private_symbol] = promise;
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Parse the CLI main entry point string and run it.
|
|
* For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed
|
|
* by `require('module')`) even when the entry point is ESM.
|
|
* Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`.
|
|
* Because of module detection, this function will attempt to run ambiguous (no explicit extension, no
|
|
* `package.json` type field) entry points as CommonJS first; under certain conditions, it will retry running as ESM.
|
|
* @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')) {
|
|
useESMLoader = true;
|
|
} else {
|
|
resolvedMain = resolveMainPath(main);
|
|
useESMLoader = shouldUseESMLoader(resolvedMain);
|
|
}
|
|
// Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
|
|
// try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
|
|
if (!useESMLoader) {
|
|
const cjsLoader = require('internal/modules/cjs/loader');
|
|
const { wrapModuleLoad } = cjsLoader;
|
|
wrapModuleLoad(main, null, true);
|
|
} else {
|
|
const mainPath = resolvedMain || main;
|
|
const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);
|
|
|
|
runEntryPointWithESMLoader((cascadedLoader) => {
|
|
// Note that if the graph contains unsettled TLA, this may never resolve
|
|
// even after the event loop stops running.
|
|
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
executeUserEntryPoint,
|
|
runEntryPointWithESMLoader,
|
|
};
|