Files
Geoffrey Booth c89ec290f0 module: synchronously load most ES modules
Signed-off-by: Geoffrey Booth <webadmin@geoffreybooth.com>
PR-URL: https://github.com/nodejs/node/pull/62530
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-10-06 17:40:45 +00:00

215 lines
8.8 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`
* @returns {Promise|undefined} A promise when the entry point is still being evaluated
* asynchronously because its graph contains top-level await, otherwise `undefined`.
*/
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);
// When no async loader hooks or --import are registered, load the entry point
// directly. The graph is loaded and linked synchronously, and evaluated
// synchronously too unless it contains top-level await.
if (getOptionValue('--experimental-loader').length === 0 &&
getOptionValue('--import').length === 0) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const job = cascadedLoader.importForEntryPoint(mainURL.href);
if (job === undefined) {
// Evaluated synchronously; no promise was created at any point.
return;
}
// Top-level await: evaluate through the usual asynchronous entry point path,
// reusing the job that was already resolved, linked and instantiated above.
return runEntryPointWithESMLoader(async () => {
// Begin evaluation inside a microtask, as `import()` does by awaiting the
// module job before running it, so that a top-level await in the entry point
// resumes in the same microtask drain instead of unwinding to the main stack
// and letting the `process.nextTick` queue run first.
await null;
// Await rather than returning the job's promise: resolving a promise *with* a
// thenable performs a `then` lookup on it, which user code can observe.
await job.run(true);
});
}
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,
};