mirror of
https://github.com/nodejs/node.git
synced 2026-10-29 02:35:20 +08:00
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>
517 lines
18 KiB
JavaScript
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,
|
|
};
|