Files
node/lib/internal/process/execution.js
Shelley Vohr e38eede0e5 lib: load fewer builtins when bootstrapping without a snapshot
Contexts that are not deserialized from the built-in snapshot -- worker
threads, and the main context of embedders that create their own
isolate or of `node --no-node-snapshot` -- compile (with the code cache
at best) every builtin the bootstrap touches, so each eagerly required
builtin is startup time (~0.15-0.4 ms apiece). A number of them are only
required eagerly so that they end up in the snapshot, or for features
the bootstrap path never uses.

Load lazily what those paths do not need:

- is_main_thread.js: preload util, url, the ESM loader (translators,
  resolver, module_job/map, source maps, node:module, vm modules, mime,
  data_url, the TypeScript stripper), internal/blob and
  internal/dns/utils only while building a snapshot; they load on first
  use otherwise.
- fs: internal/blob (+ internal/encoding and its tables) is only used
  by fs.openAsBlob().
- internal/url: internal/data_url (+ internal/mime) is only used by the
  Buffer-returning file URL helpers.
- internal/process/execution, the CommonJS loader, esm/translators and
  esm/load: the TypeScript stripper and data: URL helpers are only
  needed for TypeScript sources / data: URLs.
- pre_execution: internal/dns/utils (+ internal/net) is only needed up
  front to validate an explicit --dns-result-order or to register the
  resolver's snapshot serializer; the default order becomes the
  variable's initializer.
- internal/worker: event_loop_utilization and error_serdes are only
  needed once a sub-worker's ELU is read or it reports an error.
- worker_threads: `locks` is defined lazily, like util's lazy exports.

Main-thread startup with the snapshot is unchanged (the same modules
are preloaded into it; the bootstrap-modules test lists are adjusted).
A bare worker compiles 95 -> 83 builtins (cold start -5%); without the
snapshot an empty CommonJS entry point compiles 76 -> 59 builtins and an
empty ES module entry point 76 -> 69.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65329
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
2026-08-22 17:07:39 +00:00

517 lines
18 KiB
JavaScript

'use strict';
const {
ArrayPrototypePush,
RegExpPrototypeExec,
StringPrototypeIndexOf,
StringPrototypeSlice,
Symbol,
globalThis,
} = primordials;
const path = require('path');
const {
codes: {
ERR_EVAL_ESM_CANNOT_PRINT,
ERR_INVALID_ARG_TYPE,
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
},
} = require('internal/errors');
const { validateFunction } = require('internal/validators');
const { pathToFileURL } = require('internal/url');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const {
kSourcePhase,
kEvaluationPhase,
} = internalBinding('module_wrap');
function stripTypeScriptModuleTypes(source, filename) {
return require('internal/modules/typescript').stripTypeScriptModuleTypes(source, filename);
}
const {
executionAsyncId,
clearDefaultTriggerAsyncId,
clearAsyncIdStack,
hasAsyncIdStack,
afterHooksExist,
emitAfter,
popAsyncContext,
} = require('internal/async_hooks');
const { containsModuleSyntax } = internalBinding('contextify');
const { getOptionValue } = require('internal/options');
const {
makeContextifyScript, runScriptInThisContext,
} = require('internal/vm');
// shouldAbortOnUncaughtToggle is a typed array for faster
// communication with JS.
const { shouldAbortOnUncaughtToggle } = internalBinding('util');
const kEvalTag = '[eval]';
function tryGetCwd() {
try {
return process.cwd();
} catch {
// getcwd(3) can fail if the current working directory has been deleted.
// Fall back to the directory name of the (absolute) executable path.
// It's not really correct but what are the alternatives?
return path.dirname(process.execPath);
}
}
let evalIndex = 0;
function getEvalModuleUrl() {
return `${pathToFileURL(process.cwd())}/[eval${++evalIndex}]`;
}
/**
* Evaluate an ESM entry point and return the promise that gets fulfilled after
* it finishes evaluation.
* @param {string} source Source code the ESM
* @param {boolean} print Whether the result should be printed.
* @returns {Promise}
*/
function evalModuleEntryPoint(source, print) {
if (print) {
throw new ERR_EVAL_ESM_CANNOT_PRINT();
}
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
return require('internal/modules/run_main').runEntryPointWithESMLoader(
(loader) => loader.eval(source, getEvalModuleUrl(), true),
);
}
function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
const origModule = globalThis.module; // Set e.g. when called from the REPL.
const module = createModule(name);
const baseUrl = pathToFileURL(module.filename).href;
if (shouldUseModuleEntryPoint(name, body)) {
return getOptionValue('--strip-types') ?
evalTypeScriptModuleEntryPoint(body, print) :
evalModuleEntryPoint(body, print);
}
const evalFunction = () => runScriptInContext(name,
body,
breakFirstLine,
print,
module,
baseUrl,
undefined,
origModule);
if (shouldLoadESM) {
return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction);
}
evalFunction();
}
const exceptionHandlerState = {
captureFn: null, // Primary callback (for domain's exclusive use)
auxiliaryCallbacks: [], // Auxiliary callbacks (for REPL, etc.) - always called
reportFlag: false,
};
function setUncaughtExceptionCaptureCallback(fn) {
if (fn === null) {
exceptionHandlerState.captureFn = fn;
if (exceptionHandlerState.auxiliaryCallbacks.length === 0) {
shouldAbortOnUncaughtToggle[0] = 1;
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
}
return;
}
if (typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('fn', ['Function', 'null'], fn);
}
if (exceptionHandlerState.captureFn !== null) {
throw new ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET();
}
exceptionHandlerState.captureFn = fn;
shouldAbortOnUncaughtToggle[0] = 0;
exceptionHandlerState.reportFlag =
process.report.reportOnUncaughtException === true;
process.report.reportOnUncaughtException = false;
}
// Add an auxiliary callback that coexists with the primary callback.
// Auxiliary callbacks are called first; if any returns true, the error is handled.
// Otherwise, the primary callback (if set) is called.
function addUncaughtExceptionCaptureCallback(fn) {
validateFunction(fn, 'fn');
if (exceptionHandlerState.auxiliaryCallbacks.length === 0 &&
exceptionHandlerState.captureFn === null) {
exceptionHandlerState.reportFlag =
process.report.reportOnUncaughtException === true;
process.report.reportOnUncaughtException = false;
shouldAbortOnUncaughtToggle[0] = 0;
}
ArrayPrototypePush(exceptionHandlerState.auxiliaryCallbacks, fn);
}
function hasUncaughtExceptionCaptureCallback() {
return exceptionHandlerState.captureFn !== null;
}
function noop() {}
// XXX(joyeecheung): for some reason this cannot be defined at the top-level
// and exported to be written to process._fatalException, it has to be
// returned as an *anonymous function* wrapped inside a factory function,
// otherwise it breaks the test-timers.setInterval async hooks test -
// this may indicate that node::errors::TriggerUncaughtException() should
// fix up the callback scope before calling into process._fatalException,
// or this function should take extra care of the async hooks before it
// schedules a setImmediate.
function createOnGlobalUncaughtException() {
// The C++ land node::errors::TriggerUncaughtException() will
// exit the process if it returns false, and continue execution if it
// returns true (which indicates that the exception is handled by the user).
return (er, fromPromise) => {
// It's possible that defaultTriggerAsyncId was set for a constructor
// call that threw and was never cleared. So clear it now.
clearDefaultTriggerAsyncId();
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
process.emit('uncaughtExceptionMonitor', er, type);
// Primary callback (e.g., domain) has priority and always handles the exception
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else {
// If no primary callback, try auxiliary callbacks (e.g., REPL)
// They must return true to indicate handling
let handled = false;
for (let i = exceptionHandlerState.auxiliaryCallbacks.length - 1; i >= 0; i--) {
if (exceptionHandlerState.auxiliaryCallbacks[i](er) === true) {
handled = true;
break;
}
}
if (!handled && !process.emit('uncaughtException', er, type)) {
// If someone handled it, then great. Otherwise, die in C++ land
// since that means that we'll exit the process, emit the 'exit' event.
try {
if (!process._exiting) {
process._exiting = true;
process.exitCode = kGenericUserError;
process.emit('exit', kGenericUserError);
}
} catch {
// Nothing to be done about it at this point.
}
return false;
}
}
// If we handled an error, then make sure any ticks get processed
// by ensuring that the next Immediate cycle isn't empty.
require('timers').setImmediate(noop);
// Emit the after() hooks now that the exception has been handled.
if (afterHooksExist()) {
do {
const asyncId = executionAsyncId();
if (asyncId === 0)
popAsyncContext(0);
else
emitAfter(asyncId);
} while (hasAsyncIdStack());
}
// And completely empty the id stack, including anything that may be
// cached on the native side.
clearAsyncIdStack();
return true;
};
}
function readStdin(callback) {
process.stdin.setEncoding('utf8');
let code = '';
process.stdin.on('data', (d) => {
code += d;
});
process.stdin.on('end', () => {
callback(code);
});
}
/**
* Adds the TS message to the error stack.
*
* At the 3rd line of the stack, the message is added.
* @param {string} originalStack The stack to decorate
* @param {string} newMessage the message to add to the error stack
* @returns {void}
*/
function decorateCJSErrorWithTSMessage(originalStack, newMessage) {
let index;
for (let i = 0; i < 3; i++) {
index = StringPrototypeIndexOf(originalStack, '\n', index + 1);
}
return StringPrototypeSlice(originalStack, 0, index) +
'\n' + newMessage +
StringPrototypeSlice(originalStack, index);
}
/**
*
* Wrapper of evalScript
*
* This function wraps the evaluation of the source code in a try-catch block.
* If the source code fails to be evaluated, it will retry evaluating the source code
* with the TypeScript parser.
*
* If the source code fails to be evaluated with the TypeScript parser,
* it will rethrow the original error, adding the TypeScript error message to the stack.
*
* This way we don't change the behavior of the code, but we provide a better error message
* in case of a typescript error.
* @param {string} name The name of the file
* @param {string} source The source code to evaluate
* @param {boolean} breakFirstLine Whether to break on the first line
* @param {boolean} print If the result should be printed
* @param {boolean} shouldLoadESM If the code should be loaded as an ESM module
* @returns {void}
*/
function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
const origModule = globalThis.module; // Set e.g. when called from the REPL.
const module = createModule(name);
const baseUrl = pathToFileURL(module.filename).href;
if (shouldUseModuleEntryPoint(name, source)) {
return evalTypeScriptModuleEntryPoint(source, print);
}
let compiledScript;
// This variable can be modified if the source code is stripped.
let sourceToRun = source;
try {
compiledScript = compileScript(name, source, baseUrl);
} catch (originalError) {
try {
sourceToRun = stripTypeScriptModuleTypes(source, kEvalTag);
// Retry the CJS/ESM syntax detection after stripping the types.
if (shouldUseModuleEntryPoint(name, sourceToRun)) {
return evalTypeScriptModuleEntryPoint(source, print);
}
// If the ContextifiedScript was successfully created, execute it.
// outside the try-catch block to avoid catching runtime errors.
compiledScript = compileScript(name, sourceToRun, baseUrl);
} catch (tsError) {
// If it's invalid or unsupported TypeScript syntax, rethrow the original error
// with the TypeScript error message added to the stack.
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' || tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message);
throw originalError;
}
throw tsError;
}
}
const evalFunction = () => runScriptInContext(name,
sourceToRun,
breakFirstLine,
print,
module,
baseUrl,
compiledScript,
origModule);
if (shouldLoadESM) {
return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction);
}
evalFunction();
}
/**
* Wrapper of evalModuleEntryPoint
*
* This function wraps the compilation of the source code in a try-catch block.
* If the source code fails to be compiled, it will retry transpiling the source code
* with the TypeScript parser.
* @param {string} source The source code to evaluate
* @param {boolean} print If the result should be printed
* @returns {Promise} The module evaluation promise
*/
function evalTypeScriptModuleEntryPoint(source, print) {
if (print) {
throw new ERR_EVAL_ESM_CANNOT_PRINT();
}
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
return require('internal/modules/run_main').runEntryPointWithESMLoader(
async (loader) => {
const url = getEvalModuleUrl();
let moduleWrap;
try {
// Compile the module to check for syntax errors.
moduleWrap = loader.createModuleWrap(source, url);
} catch (originalError) {
try {
const strippedSource = stripTypeScriptModuleTypes(source, kEvalTag);
// If the moduleWrap was successfully created, execute the module job.
// outside the try-catch block to avoid catching runtime errors.
moduleWrap = loader.createModuleWrap(strippedSource, url);
} catch (tsError) {
// If it's invalid or unsupported TypeScript syntax, rethrow the original error
// with the TypeScript error message added to the stack.
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' ||
tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
originalError.stack = `${tsError.message}\n\n${originalError.stack}`;
throw originalError;
}
throw tsError;
}
}
// If the moduleWrap was successfully created either with by just compiling
// or after transpilation, execute the module job.
return loader.executeModuleJob(url, moduleWrap, true);
},
);
};
/**
*
* Function used to shortcut when `--input-type=module-typescript` is set.
* @param {string} source
* @param {boolean} print
*/
function parseAndEvalModuleTypeScript(source, print) {
// We know its a TypeScript module, we can safely emit the experimental warning.
const strippedSource = stripTypeScriptModuleTypes(source, kEvalTag);
evalModuleEntryPoint(strippedSource, print);
}
/**
* Function used to shortcut when `--input-type=commonjs-typescript` is set
* @param {string} name The name of the file
* @param {string} source The source code to evaluate
* @param {boolean} breakFirstLine Whether to break on the first line
* @param {boolean} print If the result should be printed
* @param {boolean} shouldLoadESM If the code should be loaded as an ESM module
* @returns {void}
*/
function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
// We know its a TypeScript module, we can safely emit the experimental warning.
const strippedSource = stripTypeScriptModuleTypes(source, kEvalTag);
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
}
/**
*
* @param {string} name - The filename of the script.
* @param {string} body - The code of the script.
* @param {string} baseUrl Path of the parent importing the module.
* @returns {ContextifyScript} The created contextify script.
*/
function compileScript(name, body, baseUrl) {
const hostDefinedOptionId = Symbol(name);
async function importModuleDynamically(specifier, _, importAttributes, phase) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, baseUrl, importAttributes,
phase === 'source' ? kSourcePhase : kEvaluationPhase);
}
return makeContextifyScript(
body, // code
name, // filename,
0, // lineOffset
0, // columnOffset,
undefined, // cachedData
false, // produceCachedData
undefined, // parsingContext
hostDefinedOptionId, // hostDefinedOptionId
importModuleDynamically, // importModuleDynamically
);
}
/**
* @param {string} name - The filename of the script.
* @param {string} body - The code of the script.
* @returns {boolean} Whether the module entry point should be evaluated as a module.
*/
function shouldUseModuleEntryPoint(name, body) {
return getOptionValue('--experimental-detect-module') &&
getOptionValue('--input-type') === '' &&
containsModuleSyntax(body, name, null, 'no CJS variables');
}
/**
*
* @param {string} name - The filename of the script.
* @returns {import('internal/modules/esm/loader').CJSModule} The created module.
*/
function createModule(name) {
const CJSModule = require('internal/modules/cjs/loader').Module;
const cwd = tryGetCwd();
const module = new CJSModule(name);
module.filename = path.join(cwd, name);
module.paths = CJSModule._nodeModulePaths(cwd);
return module;
}
/**
*
* @param {string} name - The filename of the script.
* @param {string} body - The code of the script.
* @param {boolean} breakFirstLine Whether to break on the first line
* @param {boolean} print If the result should be printed
* @param {import('internal/modules/esm/loader').CJSModule} module The module
* @param {string} baseUrl Path of the parent importing the module.
* @param {object} compiledScript The compiled script.
* @param {any} origModule The original module.
* @returns {void}
*/
function runScriptInContext(name, body, breakFirstLine, print, module, baseUrl, compiledScript, origModule) {
// Create wrapper for cache entry
const script = `
globalThis.module = module;
globalThis.exports = exports;
globalThis.__dirname = __dirname;
globalThis.require = require;
return (main) => main();
`;
globalThis.__filename = name;
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const result = module._compile(script, `${name}-wrapper`)(() => {
// If the script was already compiled, use it.
return runScriptInThisContext(
compiledScript ?? compileScript(name, body, baseUrl),
true, !!breakFirstLine);
});
if (print) {
const { log } = require('internal/console/global');
const printResult = () => log(result);
process.on('exit', printResult);
process.once('beforeExit', () => {
printResult();
process.off('exit', printResult);
});
}
if (origModule !== undefined)
globalThis.module = origModule;
}
module.exports = {
parseAndEvalCommonjsTypeScript,
parseAndEvalModuleTypeScript,
readStdin,
tryGetCwd,
evalModuleEntryPoint,
evalTypeScript,
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
addUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
};