current commit
260718.6 · repl: use inspector over vm
import '../common/index.mjs';
import assert from 'node:assert';
import repl from 'node:repl';
import { startNewREPLServer } from '../common/repl.js';

const testingReplPrompt = '_REPL_TESTING_PROMPT_>';

// Feed a block of source into the REPL one line at a time, awaiting each line
// so the asynchronous evaluator settles before the next line is written.
// Blank/whitespace-only and comment-only lines produce no evaluation output
// and are skipped (the previous synchronous REPL coalesced the whole block in
// a single write, so a trailing comment line evaluated to `undefined`; feeding
// line by line we simply omit it to keep output deterministic).
async function feed(run, block) {
  for (const line of block.split('\n')) {
    const trimmed = line.trim();
    if (trimmed === '' || trimmed.startsWith('//')) continue;
    await run(`${line}\n`);
  }
}

await testSloppyMode();
await testStrictMode();
await testResetContext();
await testResetContextGlobal();
await testError();

async function testSloppyMode() {
  const { replServer, output, run } = startNewREPLServer({
    prompt: testingReplPrompt,
    mode: repl.REPL_MODE_SLOPPY,
  });

  // Cannot use `let` in sloppy mode
  await feed(run, `_;          // initial value undefined
          var x = 10;  // evaluates to undefined
          _;           // still undefined
          y = 10;      // evaluates to 10
          _;           // 10 from last eval
          _ = 20;      // explicitly set to 20
          _;           // 20 from user input
          _ = 30;      // make sure we can set it twice and no prompt
          _;           // 30 from user input
          y = 40;      // make sure eval doesn't change _
          _;           // remains 30 from user input
          `);

  assertOutput(output, [
    'undefined',
    'undefined',
    'undefined',
    '10',
    '10',
    'Expression assignment to _ now disabled.',
    '20',
    '20',
    '30',
    '30',
    '40',
    '30',
  ]);

  replServer.close();
}

async function testStrictMode() {
  const { replServer, output, run } = startNewREPLServer({
    prompt: testingReplPrompt,
    mode: repl.REPL_MODE_STRICT,
  });

  await feed(run, `_;          // initial value undefined
          var x = 10;  // evaluates to undefined
          _;           // still undefined
          let _ = 20;  // use 'let' only in strict mode - evals to undefined
          _;           // 20 from user input
          _ = 30;      // make sure we can set it twice and no prompt
          _;           // 30 from user input
          var y = 40;  // make sure eval doesn't change _
          _;           // remains 30 from user input
          function f() { let _ = 50; } // undefined
          f();         // undefined
          _;           // remains 30 from user input
          `);

  assertOutput(output, [
    'undefined',
    'undefined',
    'undefined',
    'undefined',
    '20',
    '30',
    '30',
    'undefined',
    '30',
    'undefined',
    'undefined',
    '30',
  ]);

  replServer.close();
}

async function testResetContext() {
  const { replServer, output, run } = startNewREPLServer({
    prompt: testingReplPrompt,
  });

  await feed(run, `_ = 10;     // explicitly set to 10
          _;           // 10 from user input
          .clear       // Clearing context...
          _;           // remains 10
          x = 20;      // but behavior reverts to last eval
          _;           // expect 20
          `);

  assertOutput(output, [
    'Expression assignment to _ now disabled.',
    '10',
    '10',
    'Clearing context...',
    '10',
    '20',
    '20',
  ]);

  replServer.close();
}

async function testResetContextGlobal() {
  const { replServer, output, run } = startNewREPLServer({
    prompt: testingReplPrompt,
    useGlobal: true,
  });

  await feed(run, `_ = 10;     // explicitly set to 10
          _;           // 10 from user input
          .clear       // No output because useGlobal is true
          _;           // remains 10
          `);

  assertOutput(output, [
    'Expression assignment to _ now disabled.',
    '10',
    '10',
    '10',
  ]);

  replServer.close();

  // Delete globals leaked by REPL when `useGlobal` is `true`
  delete globalThis.module;
  delete globalThis.require;
}

async function testError() {
  const { replServer, output, run } = startNewREPLServer({
    prompt: testingReplPrompt,
    replMode: repl.REPL_MODE_STRICT,
    preview: false,
  });

  await feed(run, `_error;                                // initial value undefined
           throw new Error('foo');                // throws error
           _error;                                // shows error
           fs.readdirSync('/nonexistent?');       // throws error, sync
           _error.code;                           // shows error code
           _error.syscall;                        // shows error syscall
           setImmediate(() => { throw new Error('baz'); }); undefined;
                                                  // throws error, async
           `);

  const lines = output.accumulator.trim().split('\n').filter(
    (line) => !line.includes(testingReplPrompt) || line.includes('Uncaught Error')
  );
  const expectedLines = [
    'undefined',

    // The error, both from the original throw and the `_error` echo. The
    // inspector-based evaluator returns the real Error object, so both the
    // thrown report and the echo now include a stack trace rather than the
    // compact `[Error: foo]` form produced by the previous vm-based REPL.
    'Uncaught Error: foo',
    /^ +at REPL\d+:/,
    'Error: foo',
    /^ +at REPL\d+:/,

    // The sync error, with individual property echoes. The thrown report now
    // includes stack frames before the property listing.
    /^Uncaught Error: ENOENT: no such file or directory, scandir '.*nonexistent\?'/,
    /Object\.readdirSync/,
    /^ +at REPL\d+:.*\{$/,
    /^ {2}errno: -(2|4058),$/,
    "  code: 'ENOENT',",
    "  syscall: 'scandir',",
    /^ {2}path: '*'/,
    '}',
    "'ENOENT'",
    "'scandir'",

    // 'undefined' from the explicit silencer on the setImmediate line.
    'undefined',

    // The message from the asynchronously thrown error.
    /Uncaught Error: baz/,
  ];
  for (const line of lines) {
    const expected = expectedLines.shift();
    if (typeof expected === 'string')
      assert.strictEqual(line, expected);
    else
      assert.match(line, expected);
  }
  assert.strictEqual(expectedLines.length, 0);

  // Reset output, check that '_error' is the asynchronously caught error.
  // Because input is now fed one line at a time and the accumulator is reset
  // mid-stream, the first line's echo is no longer preceded by a prompt and so
  // survives the prompt filter; ignore that leading echo. The thrown error also
  // now reports a stack frame.
  output.accumulator = '';
  await feed(run, `_error.message                 // show the message
             _error = 0;                    // disable auto-assignment
             throw new Error('quux');       // new error
             _error;                        // should not see the new error
             `);

  const errorLines = output.accumulator.trim().split('\n')
    .filter((line) => !line.includes(testingReplPrompt) &&
                      !line.includes('_error.message') &&
                      !/^ +at REPL\d+:/.test(line));
  assert.deepStrictEqual(errorLines, [
    "'baz'",
    'Expression assignment to _error now disabled.',
    '0',
    'Uncaught Error: quux',
    '0',
  ]);

  replServer.close();
}

function assertOutput(output, expected) {
  const lines = output.accumulator.trim().split('\n').filter((line) => !line.includes(testingReplPrompt));
  assert.deepStrictEqual(lines, expected);
}