node/lib/internal/modules/run_main.js
Geoffrey Booth 8bc745944e
module: eliminate performance cost of detection for cjs entry
PR-URL: https://github.com/nodejs/node/pull/52093
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Richard Lau <rlau@redhat.com>
2024-03-20 15:48:05 +00:00

207 lines
7.5 KiB
JavaScript

'use strict';
const {
StringPrototypeEndsWith,
globalThis,
} = primordials;
const { getNearestParentPackageJSONType } = internalBinding('modules');
const { getOptionValue } = require('internal/options');
const { checkPackageJSONIntegrity } = require('internal/modules/package_json_reader');
const path = require('path');
const { pathToFileURL } = 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
*/
function resolveMainPath(main) {
const defaultType = getOptionValue('--experimental-default-type');
/** @type {string} */
let mainPath;
if (defaultType === 'module') {
if (getOptionValue('--preserve-symlinks-main')) { return; }
mainPath = path.resolve(main);
} else {
// Extension searching for the main entry point is supported only in legacy mode.
// 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) {
const { toRealPath } = require('internal/modules/helpers');
try {
mainPath = toRealPath(mainPath);
} catch (err) {
if (defaultType === 'module' && err?.code === 'ENOENT') {
const { decorateErrorWithCommonJSHints } = require('internal/modules/esm/resolve');
const { getCWDURL } = require('internal/util');
decorateErrorWithCommonJSHints(err, mainPath, getCWDURL());
}
throw err;
}
}
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
*/
function shouldUseESMLoader(mainPath) {
if (getOptionValue('--experimental-default-type') === 'module') { return true; }
/**
* @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, '.cjs')) { return false; }
const response = getNearestParentPackageJSONType(mainPath);
// No package.json or no `type` field.
if (response === undefined || response[0] === 'none') {
return false;
}
// TODO(@anonrig): Do not return filePath and rawContent if experimental-policy is not used.
const {
0: type,
1: filePath,
2: rawContent,
} = response;
checkPackageJSONIntegrity(filePath, rawContent);
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.forceLoadHooks();
}
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.
* This monkey-patchable code is bypassed under `--experimental-default-type=module`.
* Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`.
* When `--experimental-detect-module` is passed, 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]) {
const resolvedMain = resolveMainPath(main);
const 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.
let retryAsESM = false;
if (!useESMLoader) {
const cjsLoader = require('internal/modules/cjs/loader');
const { Module } = cjsLoader;
if (getOptionValue('--experimental-detect-module')) {
try {
// Module._load is the monkey-patchable CJS module loader.
Module._load(main, null, true);
} catch (error) {
const source = cjsLoader.entryPointSource;
const { shouldRetryAsESM } = require('internal/modules/helpers');
retryAsESM = shouldRetryAsESM(error.message, source);
// In case the entry point is a large file, such as a bundle,
// ensure no further references can prevent it being garbage-collected.
cjsLoader.entryPointSource = undefined;
if (!retryAsESM) {
const { enrichCJSError } = require('internal/modules/esm/translators');
enrichCJSError(error, source, resolvedMain);
throw error;
}
}
} else { // `--experimental-detect-module` is not passed
Module._load(main, null, true);
}
}
if (useESMLoader || retryAsESM) {
const mainPath = resolvedMain || main;
const mainURL = pathToFileURL(mainPath).href;
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 }, true);
});
}
}
module.exports = {
executeUserEntryPoint,
runEntryPointWithESMLoader,
};