"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupContext = exports.createEvalAwarePartialHost = exports.EvalState = exports.createRepl = exports.REPL_NAME = exports.REPL_FILENAME = exports.STDIN_NAME = exports.STDIN_FILENAME = exports.EVAL_NAME = exports.EVAL_FILENAME = void 0;
const diff_1 = require("diff");
const os_1 = require("os");
const path_1 = require("path");
const repl_1 = require("repl");
const vm_1 = require("vm");
const index_1 = require("./index");
const fs_1 = require("fs");
const console_1 = require("console");
const assert = require("assert");
const module_1 = require("module");
// Lazy-loaded.
let _processTopLevelAwait;
function getProcessTopLevelAwait() {
    if (_processTopLevelAwait === undefined) {
        ({
            processTopLevelAwait: _processTopLevelAwait,
        } = require('../dist-raw/node-repl-await'));
    }
    return _processTopLevelAwait;
}
/** @internal */
exports.EVAL_FILENAME = `[eval].ts`;
/** @internal */
exports.EVAL_NAME = `[eval]`;
/** @internal */
exports.STDIN_FILENAME = `[stdin].ts`;
/** @internal */
exports.STDIN_NAME = `[stdin]`;
/** @internal */
exports.REPL_FILENAME = '<repl>.ts';
/** @internal */
exports.REPL_NAME = '<repl>';
/**
 * Create a ts-node REPL instance.
 *
 * Pay close attention to the example below.  Today, the API requires a few lines
 * of boilerplate to correctly bind the `ReplService` to the ts-node `Service` and
 * vice-versa.
 *
 * Usage example:
 *
 *     const repl = tsNode.createRepl();
 *     const service = tsNode.create({...repl.evalAwarePartialHost});
 *     repl.setService(service);
 *     repl.start();
 */
function createRepl(options = {}) {
    var _a, _b, _c, _d, _e;
    const { ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl = true } = options;
    let service = options.service;
    let nodeReplServer;
    // If `useGlobal` is not true, then REPL creates a context when started.
    // This stores a reference to it or to `global`, whichever is used, after REPL has started.
    let context;
    const state = (_a = options.state) !== null && _a !== void 0 ? _a : new EvalState((0, path_1.join)(process.cwd(), exports.REPL_FILENAME));
    const evalAwarePartialHost = createEvalAwarePartialHost(state, options.composeWithEvalAwarePartialHost);
    const stdin = (_b = options.stdin) !== null && _b !== void 0 ? _b : process.stdin;
    const stdout = (_c = options.stdout) !== null && _c !== void 0 ? _c : process.stdout;
    const stderr = (_d = options.stderr) !== null && _d !== void 0 ? _d : process.stderr;
    const _console = stdout === process.stdout && stderr === process.stderr
        ? console
        : new console_1.Console(stdout, stderr);
    const replService = {
        state: (_e = options.state) !== null && _e !== void 0 ? _e : new EvalState((0, path_1.join)(process.cwd(), exports.EVAL_FILENAME)),
        setService,
        evalCode,
        evalCodeInternal,
        nodeEval,
        evalAwarePartialHost,
        start,
        startInternal,
        stdin,
        stdout,
        stderr,
        console: _console,
    };
    return replService;
    function setService(_service) {
        service = _service;
        if (ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl) {
            service.addDiagnosticFilter({
                appliesToAllFiles: false,
                filenamesAbsolute: [state.path],
                diagnosticsIgnored: [
                    2393,
                    6133,
                    7027,
                    ...(service.shouldReplAwait ? topLevelAwaitDiagnosticCodes : []),
                ],
            });
        }
    }
    function evalCode(code) {
        const result = appendCompileAndEvalInput({
            service: service,
            state,
            input: code,
            context,
        });
        assert(result.containsTopLevelAwait === false);
        return result.value;
    }
    function evalCodeInternal(options) {
        const { code, enableTopLevelAwait, context } = options;
        return appendCompileAndEvalInput({
            service: service,
            state,
            input: code,
            enableTopLevelAwait,
            context,
        });
    }
    function nodeEval(code, context, _filename, callback) {
        // TODO: Figure out how to handle completion here.
        if (code === '.scope') {
            callback(null);
            return;
        }
        try {
            const evalResult = evalCodeInternal({
                code,
                enableTopLevelAwait: true,
                context,
            });
            if (evalResult.containsTopLevelAwait) {
                (async () => {
                    try {
                        callback(null, await evalResult.valuePromise);
                    }
                    catch (promiseError) {
                        handleError(promiseError);
                    }
                })();
            }
            else {
                callback(null, evalResult.value);
            }
        }
        catch (error) {
            handleError(error);
        }
        // Log TSErrors, check if they're recoverable, log helpful hints for certain
        // well-known errors, and invoke `callback()`
        // TODO should evalCode API get the same error-handling benefits?
        function handleError(error) {
            var _a, _b;
            // Don't show TLA hint if the user explicitly disabled repl top level await
            const canLogTopLevelAwaitHint = service.options.experimentalReplAwait !== false &&
                !service.shouldReplAwait;
            if (error instanceof index_1.TSError) {
                // Support recoverable compilations using >= node 6.
                if (repl_1.Recoverable && isRecoverable(error)) {
                    callback(new repl_1.Recoverable(error));
                    return;
                }
                else {
                    _console.error(error);
                    if (canLogTopLevelAwaitHint &&
                        error.diagnosticCodes.some((dC) => topLevelAwaitDiagnosticCodes.includes(dC))) {
                        _console.error(getTopLevelAwaitHint());
                    }
                    callback(null);
                }
            }
            else {
                let _error = error;
                if (canLogTopLevelAwaitHint &&
                    _error instanceof SyntaxError &&
                    ((_a = _error.message) === null || _a === void 0 ? void 0 : _a.includes('await is only valid'))) {
                    try {
                        // Only way I know to make our hint appear after the error
                        _error.message += `\n\n${getTopLevelAwaitHint()}`;
                        _error.stack = (_b = _error.stack) === null || _b === void 0 ? void 0 : _b.replace(/(SyntaxError:.*)/, (_, $1) => `${$1}\n\n${getTopLevelAwaitHint()}`);
                    }
                    catch { }
                }
                callback(_error);
            }
        }
        function getTopLevelAwaitHint() {
            return `Hint: REPL top-level await requires TypeScript version 3.8 or higher and target ES2018 or higher. You are using TypeScript ${service.ts.version} and target ${service.ts.ScriptTarget[service.config.options.target]}.`;
        }
    }
    // Note: `code` argument is deprecated
    function start(code) {
        startInternal({ code });
    }
    // Note: `code` argument is deprecated
    function startInternal(options) {
        const { code, forceToBeModule = true, ...optionsOverride } = options !== null && options !== void 0 ? options : {};
        // TODO assert that `service` is set; remove all `service!` non-null assertions
        // Eval incoming code before the REPL starts.
        // Note: deprecated
        if (code) {
            try {
                evalCode(`${code}\n`);
            }
            catch (err) {
                _console.error(err);
                // Note: should not be killing the process here, but this codepath is deprecated anyway
                process.exit(1);
            }
        }
        // In case the typescript compiler hasn't compiled anything yet,
        // make it run though compilation at least one time before
        // the REPL starts for a snappier user experience on startup.
        service === null || service === void 0 ? void 0 : service.compile('', state.path);
        const repl = (0, repl_1.start)({
            prompt: '> ',
            input: replService.stdin,
            output: replService.stdout,
            // Mimicking node's REPL implementation: https://github.com/nodejs/node/blob/168b22ba073ee1cbf8d0bcb4ded7ff3099335d04/lib/internal/repl.js#L28-L30
            terminal: stdout.isTTY &&
                !parseInt(index_1.env.NODE_NO_READLINE, 10),
            eval: nodeEval,
            useGlobal: true,
            ...optionsOverride,
        });
        nodeReplServer = repl;
        context = repl.context;
        // Bookmark the point where we should reset the REPL state.
        const resetEval = appendToEvalState(state, '');
        function reset() {
            resetEval();
            // Hard fix for TypeScript forcing `Object.defineProperty(exports, ...)`.
            runInContext('exports = module.exports', state.path, context);
            if (forceToBeModule) {
                state.input += 'export {};void 0;\n';
            }
            // Declare node builtins.
            // Skip the same builtins as `addBuiltinLibsToObject`:
            //   those starting with _
            //   those containing /
            //   those that already exist as globals
            // Intentionally suppress type errors in case @types/node does not declare any of them, and because
            // `declare import` is technically invalid syntax.
            // Avoid this when in transpileOnly, because third-party transpilers may not handle `declare import`.
            if (!(service === null || service === void 0 ? void 0 : service.transpileOnly)) {
                state.input += `// @ts-ignore\n${module_1.builtinModules
                    .filter((name) => !name.startsWith('_') &&
                    !name.includes('/') &&
                    !['console', 'module', 'process'].includes(name))
                    .map((name) => `declare import ${name} = require('${name}')`)
                    .join(';')}\n`;
            }
        }
        reset();
        repl.on('reset', reset);
        repl.defineCommand('type', {
            help: 'Check the type of a TypeScript identifier',
            action: function (identifier) {
                if (!identifier) {
                    repl.displayPrompt();
                    return;
                }
                const undo = appendToEvalState(state, identifier);
                const { name, comment } = service.getTypeInfo(state.input, state.path, state.input.length);
                undo();
                if (name)
                    repl.outputStream.write(`${name}\n`);
                if (comment)
                    repl.outputStream.write(`${comment}\n`);
                repl.displayPrompt();
            },
        });
        // Set up REPL history when available natively via node.js >= 11.
        if (repl.setupHistory) {
            const historyPath = index_1.env.TS_NODE_HISTORY || (0, path_1.join)((0, os_1.homedir)(), '.ts_node_repl_history');
            repl.setupHistory(historyPath, (err) => {
                if (!err)
                    return;
                _console.error(err);
                process.exit(1);
            });
        }
        return repl;
    }
}
exports.createRepl = createRepl;
/**
 * Eval state management. Stores virtual `[eval].ts` file
 */
class EvalState {
    constructor(path) {
        this.path = path;
        /** @internal */
        this.input = '';
        /** @internal */
        this.output = '';
        /** @internal */
        this.version = 0;
        /** @internal */
        this.lines = 0;
    }
}
exports.EvalState = EvalState;
function createEvalAwarePartialHost(state, composeWith) {
    function readFile(path) {
        if (path === state.path)
            return state.input;
        if (composeWith === null || composeWith === void 0 ? void 0 : composeWith.readFile)
            return composeWith.readFile(path);
        try {
            return (0, fs_1.readFileSync)(path, 'utf8');
        }
        catch (err) {
            /* Ignore. */
        }
    }
    function fileExists(path) {
        if (path === state.path)
            return true;
        if (composeWith === null || composeWith === void 0 ? void 0 : composeWith.fileExists)
            return composeWith.fileExists(path);
        try {
            const stats = (0, fs_1.statSync)(path);
            return stats.isFile() || stats.isFIFO();
        }
        catch (err) {
            return false;
        }
    }
    return { readFile, fileExists };
}
exports.createEvalAwarePartialHost = createEvalAwarePartialHost;
const sourcemapCommentRe = /\/\/# ?sourceMappingURL=\S+[\s\r\n]*$/;
/**
 * Evaluate the code snippet.
 *
 * Append it to virtual .ts file, compile, handle compiler errors, compute a diff of the JS, and eval any code that
 * appears as "added" in the diff.
 */
function appendCompileAndEvalInput(options) {
    const { service, state, input, enableTopLevelAwait = false, context, } = options;
    const lines = state.lines;
    const isCompletion = !/\n$/.test(input);
    const undo = appendToEvalState(state, input);
    let output;
    // Based on https://github.com/nodejs/node/blob/92573721c7cff104ccb82b6ed3e8aa69c4b27510/lib/repl.js#L457-L461
    function adjustUseStrict(code) {
        // "void 0" keeps the repl from returning "use strict" as the result
        // value for statements and declarations that don't return a value.
        return code.replace(/^"use strict";/, '"use strict"; void 0;');
    }
    try {
        output = service.compile(state.input, state.path, -lines);
    }
    catch (err) {
        undo();
        throw err;
    }
    output = adjustUseStrict(output);
    // Note: REPL does not respect sourcemaps!
    // To properly do that, we'd need to prefix the code we eval -- which comes
    // from `diffLines` -- with newlines so that it's at the proper line numbers.
    // Then we'd need to ensure each bit of eval-ed code, if there are multiples,
    // has the sourcemap appended to it.
    // We might also need to integrate with our sourcemap hooks' cache; I'm not sure.
    const outputWithoutSourcemapComment = output.replace(sourcemapCommentRe, '');
    const oldOutputWithoutSourcemapComment = state.output.replace(sourcemapCommentRe, '');
    // Use `diff` to check for new JavaScript to execute.
    const changes = (0, diff_1.diffLines)(oldOutputWithoutSourcemapComment, outputWithoutSourcemapComment);
    if (isCompletion) {
        undo();
    }
    else {
        state.output = output;
        // Insert a semicolon to make sure that the code doesn't interact with the next line,
        // for example to prevent `2\n+ 2` from producing 4.
        // This is safe since the output will not change since we can only get here with successful inputs,
        // and adding a semicolon to the end of a successful input won't ever change the output.
        state.input = state.input.replace(/([^\n\s])([\n\s]*)$/, (all, lastChar, whitespace) => {
            if (lastChar !== ';')
                return `${lastChar};${whitespace}`;
            return all;
        });
    }
    let commands = [];
    let containsTopLevelAwait = false;
    // Build a list of "commands": bits of JS code in the diff that must be executed.
    for (const change of changes) {
        if (change.added) {
            if (enableTopLevelAwait &&
                service.shouldReplAwait &&
                change.value.indexOf('await') > -1) {
                const processTopLevelAwait = getProcessTopLevelAwait();
                // Newline prevents comments to mess with wrapper
                const wrappedResult = processTopLevelAwait(change.value + '\n');
                if (wrappedResult !== null) {
                    containsTopLevelAwait = true;
                    commands.push({
                        mustAwait: true,
                        execCommand: () => runInContext(wrappedResult, state.path, context),
                    });
                    continue;
                }
            }
            commands.push({
                execCommand: () => runInContext(change.value, state.path, context),
            });
        }
    }
    // Execute all commands asynchronously if necessary, returning the result or a
    // promise of the result.
    if (containsTopLevelAwait) {
        return {
            containsTopLevelAwait,
            valuePromise: (async () => {
                let value;
                for (const command of commands) {
                    const r = command.execCommand();
                    value = command.mustAwait ? await r : r;
                }
                return value;
            })(),
        };
    }
    else {
        return {
            containsTopLevelAwait: false,
            value: commands.reduce((_, c) => c.execCommand(), undefined),
        };
    }
}
/**
 * Low-level execution of JS code in context
 */
function runInContext(code, filename, context) {
    const script = new vm_1.Script(code, { filename });
    if (context === undefined || context === global) {
        return script.runInThisContext();
    }
    else {
        return script.runInContext(context);
    }
}
/**
 * Append to the eval instance and return an undo function.
 */
function appendToEvalState(state, input) {
    const undoInput = state.input;
    const undoVersion = state.version;
    const undoOutput = state.output;
    const undoLines = state.lines;
    state.input += input;
    state.lines += lineCount(input);
    state.version++;
    return function () {
        state.input = undoInput;
        state.output = undoOutput;
        state.version = undoVersion;
        state.lines = undoLines;
    };
}
/**
 * Count the number of lines.
 */
function lineCount(value) {
    let count = 0;
    for (const char of value) {
        if (char === '\n') {
            count++;
        }
    }
    return count;
}
/**
 * TS diagnostic codes which are recoverable, meaning that the user likely entered and incomplete line of code
 * and should be prompted for the next.  For example, starting a multi-line for() loop and not finishing it.
 */
const RECOVERY_CODES = new Set([
    1003,
    1005,
    1109,
    1126,
    1160,
    1161,
    2355, // "A function whose declared type is neither 'void' nor 'any' must return a value."
]);
/**
 * Diagnostic codes raised when using top-level await.
 * These are suppressed when top-level await is enabled.
 * When it is *not* enabled, these trigger a helpful hint about enabling top-level await.
 */
const topLevelAwaitDiagnosticCodes = [
    1375,
    1378,
    1431,
    1432, // Top-level 'for await' loops are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.
];
/**
 * Check if a function can recover gracefully.
 */
function isRecoverable(error) {
    return error.diagnosticCodes.every((code) => RECOVERY_CODES.has(code));
}
/**
 * @internal
 * Set properties on `context` before eval-ing [stdin] or [eval] input.
 */
function setupContext(context, module, filenameAndDirname) {
    if (filenameAndDirname) {
        context.__dirname = '.';
        context.__filename = `[${filenameAndDirname}]`;
    }
    context.module = module;
    context.exports = module.exports;
    context.require = module.require.bind(module);
}
exports.setupContext = setupContext;
//# sourceMappingURL=repl.js.map