@ 261105.16
current
'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,
};