mirror of https://github.com/nodejs/node.git
476 lines
17 KiB
JavaScript
476 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
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 { pathToFileURL } = require('internal/url');
|
|
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
|
|
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
|
|
|
|
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');
|
|
const { emitExperimentalWarning } = require('internal/util');
|
|
// shouldAbortOnUncaughtToggle is a typed array for faster
|
|
// communication with JS.
|
|
const { shouldAbortOnUncaughtToggle } = internalBinding('util');
|
|
|
|
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('--experimental-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,
|
|
reportFlag: false,
|
|
};
|
|
|
|
function setUncaughtExceptionCaptureCallback(fn) {
|
|
if (fn === null) {
|
|
exceptionHandlerState.captureFn = fn;
|
|
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;
|
|
}
|
|
|
|
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);
|
|
if (exceptionHandlerState.captureFn !== null) {
|
|
exceptionHandlerState.captureFn(er);
|
|
} else if (!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, name, false);
|
|
// 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);
|
|
// Emit the experimental warning after the code was successfully evaluated.
|
|
emitExperimentalWarning('Type Stripping');
|
|
} 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, url, false);
|
|
// 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);
|
|
// Emit the experimental warning after the code was successfully compiled.
|
|
emitExperimentalWarning('Type Stripping');
|
|
} 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, getEvalModuleUrl());
|
|
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, getEvalModuleUrl());
|
|
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) {
|
|
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
|
return cascadedLoader.import(specifier, baseUrl, importAttributes);
|
|
}
|
|
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');
|
|
|
|
process.on('exit', () => {
|
|
log(result);
|
|
});
|
|
}
|
|
if (origModule !== undefined)
|
|
globalThis.module = origModule;
|
|
}
|
|
|
|
module.exports = {
|
|
parseAndEvalCommonjsTypeScript,
|
|
parseAndEvalModuleTypeScript,
|
|
readStdin,
|
|
tryGetCwd,
|
|
evalModuleEntryPoint,
|
|
evalTypeScript,
|
|
evalScript,
|
|
onGlobalUncaughtException: createOnGlobalUncaughtException(),
|
|
setUncaughtExceptionCaptureCallback,
|
|
hasUncaughtExceptionCaptureCallback,
|
|
};
|