current commit
260718.6 · repl: use inspector over vm
'use strict';

const {
  ArrayPrototypeJoin,
  ArrayPrototypeSlice,
  ObjectDefineProperty,
  PromisePrototypeThen,
  ReflectApply,
  ReflectHas,
  RegExpPrototypeExec,
  StringPrototypeSplit,
  StringPrototypeTrim,
  Symbol,
  SyntaxError,
} = primordials;

const path = require('path');
const {
  isRecoverableError,
  isObjectLiteral,
  isValidSyntax,
  kContextId,
  REPL_MODE_STRICT,
} = require('internal/repl/utils');
const {
  kDynamicImportName,
  transformModuleSyntax,
} = require('internal/repl/transform');
const { getReplInspector, prepareContext } = require('internal/repl/inspector');

let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
  debug = fn;
});

const blankRegExp = /^\s*$/;

// Matches the first stack frame that belongs to the REPL
const internalFrameRegExp =
  /^\s*at\s.*(?:node:inspector|node:internal\/repl|node:repl)[:)]/;

// Removes the REPL's internal evaluation frames from an error stack string,
// leaving only the frames that correspond to the user's code.
function trimInternalFrames(err) {
  if (typeof err?.stack !== 'string') {
    return;
  }
  const lines = StringPrototypeSplit(err.stack, '\n');
  for (let i = 0; i < lines.length; i++) {
    if (RegExpPrototypeExec(internalFrameRegExp, lines[i]) !== null) {
      err.stack = ArrayPrototypeJoin(ArrayPrototypeSlice(lines, 0, i), '\n');
      return;
    }
  }
}

// Thrown to signal the server that the input is incomplete and a continuation
// prompt should be shown. Mirrors the public `repl.Recoverable`.
class Recoverable extends SyntaxError {
  constructor(err) {
    super();
    this.err = err;
  }
}

// Mark falsy thrown values out-of-band so the evaluator callback cannot
// mistake them for successful completion.
const kThrownValue = Symbol('kThrownValue');

/**
 * Creates the evaluation function used by a {@link REPLServer}.
 *
 * The returned function matches the historical `eval` contract
 * `(code, context, file, cb)` so it is a drop-in replacement for the old
 * `vm`-based default evaluator and for any custom evaluator a user supplies.
 * @param {object} repl the REPL server instance
 * @returns {Function} the evaluation function
 */
function createReplEval(repl) {
  const inspector = getReplInspector(repl);

  // Installs the dynamic-import helper that the transform routes `import`
  // through. It mirrors the host-defined `importModuleDynamically` callback the
  // old evaluator passed to `makeContextifyScript`, so relative specifiers
  // resolve against the working directory exactly as they did before.
  function installDynamicImport(context) {
    if (ReflectHas(context, kDynamicImportName)) {
      return;
    }
    const dynamicImport = (specifier, importAttributes) => {
      let parentURL;
      try {
        const { pathToFileURL } = require('internal/url');
        // Appending `/repl` keeps relative imports from resolving against the
        // parent of `process.cwd()`.
        parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href;
      } catch {
        // Continue regardless of error.
      }
      const cascadedLoader =
        require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
      return cascadedLoader.import(
        specifier,
        parentURL,
        importAttributes ?? { __proto__: null },
        cascadedLoader.kEvaluationPhase,
      );
    };

    ObjectDefineProperty(context, kDynamicImportName, {
      __proto__: null,
      value: dynamicImport,
      enumerable: false,
      writable: false,
      configurable: false,
    });
  }

  async function inspectorEval(rawCode, context, file, cb) {
    installDynamicImport(context);
    prepareContext(context);

    // Wrap bare object literals so `{ a: 1 }` is evaluated as an expression
    // rather than a block statement.
    let code = rawCode;
    if (isObjectLiteral(code) && isValidSyntax(code)) {
      code = `(${StringPrototypeTrim(rawCode)})\n`;
    }

    // Rewrite ES module syntax into runnable script form.
    code = transformModuleSyntax(code).code;

    // Strict mode opt-in keeps parity with the classic REPL. `void 0` prevents
    // the directive itself from becoming the completion value.
    if (
      repl.replMode === REPL_MODE_STRICT &&
      RegExpPrototypeExec(blankRegExp, code) === null
    ) {
      code = `'use strict'; void 0;\n${code}`;
    }

    if (code === '\n') {
      return cb(await null);
    }

    const contextId = repl.useGlobal ? undefined : repl[kContextId];

    const evaluateParams = {
      expression: `${code}\n//# sourceURL=${file}`,
      objectGroup: inspector.objectGroup,
      silent: false,
      returnByValue: false,
      generatePreview: false,
      // Await in a second inspector command after retaining the promise's
      // objectId. See ReplInspectorChannel.evaluate().
      awaitPromise: false,
      replMode: true,
      throwOnSideEffect: false,
      ...(contextId !== undefined ? { contextId } : null),
    };

    let response;
    try {
      response = await inspector.evaluate(evaluateParams, repl.breakEvalOnSigint);
    } catch (err) {
      // Either an interruption (ERR_SCRIPT_EXECUTION_INTERRUPTED) or a
      // transport-level failure (e.g. the inspector went away). Surface it
      // rather than pretending the evaluation succeeded.
      inspector.releaseObjects();
      return err ? cb(err) : cb(kThrownValue, err);
    }

    const { result, exceptionDetails } = response;

    if (exceptionDetails) {
      let err;
      try {
        ({ value: err } = await inspector.toLiveValue(
          exceptionDetails.exception ?? result, context));
      } catch {
        err = new Recoverable(exceptionDetails.text);
      }
      inspector.releaseObjects();

      const className =
        exceptionDetails.exception?.className ?? result?.className;
      // Distinguish incomplete input (recoverable -> continuation prompt) from a
      // genuine error. `isRecoverableError` re-parses the *raw* source, so a
      // user `throw new SyntaxError(...)` is correctly treated as fatal.
      if (className === 'SyntaxError' && isRecoverableError(err, rawCode)) {
        return cb(new Recoverable(err));
      }
      if (err != null && typeof err === 'object') {
        trimInternalFrames(err);
      }
      return err ? cb(err) : cb(kThrownValue, err);
    }

    let value;
    try {
      ({ value } = await inspector.toLiveValue(result, context));
    } catch (err) {
      inspector.releaseObjects();
      return cb(err);
    }
    inspector.releaseObjects();
    return cb(null, value);
  }

  // Wrap so a single rejected promise inside the async pipeline can never leave
  // `cb` uncalled. `called`/`once` also guarantee the server's callback runs at
  // most once, preserving its `arguments.length` contract verbatim.
  return function evaluate(code, context, file, cb) {
    let called = false;
    function once() {
      if (called) return undefined;
      called = true;
      return ReflectApply(cb, this, arguments);
    }
    PromisePrototypeThen(
      inspectorEval(code, context, file, once),
      undefined,
      (err) => {
        debug('unexpected eval pipeline failure', err);
        once(err);
      },
    );
  };
}

module.exports = {
  createReplEval,
  Recoverable,
  kThrownValue,
};