Source: dialects/jtree/runAppCode.js

const vm = require('vm');
const fs = require('fs-extra');
const path = require('path');
const Utils = require('../../Utils.js');

/**
 * Runs the code of a jtree app (a .jtt or .js file), or of one of a folder app's stage files.
 *
 * The code is compiled as the body of a function whose parameters are the names in scope
 * (`app`, and `stage` for a stage file), plus Utils, fs, path and require, which apps have
 * always been able to use. Compiling it with its file's name puts that name and the line in
 * every error from it, now or later from the functions it defines (playerStart, ...).
 *
 * An error thrown while compiling or running the code gets `jtreePosition`: {file, line,
 * column} of where it happened, in this file or in a stage file the code added.
 *
 * @param {string} code
 * @param {string} filename The file the code is from.
 * @param {Object} scope Names the code can use, e.g. {app}; `this` is scope.app.
 */
function runAppCode(code, filename, scope) {
    const names = Object.assign({ Utils, fs, path, require }, scope);
    const keys = Object.keys(names);
    let fn;
    try {
        fn = vm.compileFunction(code, keys, { filename });
    } catch (err) {
        throw withPosition(err, filename);
    }
    try {
        fn.apply(scope.app, keys.map((k) => names[k]));
    } catch (err) {
        throw withPosition(err, filename);
    }
}

function withPosition(err, filename) {
    if (err != null && typeof err === 'object' && err.jtreePosition === undefined) {
        err.jtreePosition = errorPosition(err, filename);
    }
    return err;
}

/**
 * Where err happened in filename, as {file, line, column} (line and column as strings,
 * from 1), from its stack; null if the stack does not say.
 * - A SyntaxError's stack starts "<filename>:<line>", then the line, then a caret under the column.
 * - Other errors have a frame "<filename>:<line>:<column>".
 */
function errorPosition(err, filename) {
    const lines = String(err && err.stack).split('\n');
    if (err instanceof SyntaxError && lines[0] && lines[0].startsWith(filename + ':')) {
        const caret = (lines[2] || '').indexOf('^');
        return {
            file: filename,
            line: lines[0].substring(filename.length + 1),
            column: caret >= 0 ? String(caret + 1) : 'unknown',
        };
    }
    for (const line of lines) {
        const at = line.indexOf(filename + ':');
        if (at >= 0) {
            const m = /^(\d+):(\d+)/.exec(line.substring(at + filename.length + 1));
            if (m) {
                return { file: filename, line: m[1], column: m[2] };
            }
        }
    }
    return null;
}

module.exports = { runAppCode, errorPosition };