The initial implementation is copied over from the [prototype], see Execute `code` *cells* for usage.
Additionally through use of the Plugin system it is possible to create custom REPLs.
import util from "util";
import { transformSync } from "@babel/core";
import presetReact from "@babel/preset-react";
import presetTypescript from "@babel/preset-typescript";
import pluginTransformModulesCommonjs from "@babel/plugin-transform-modules-commonjs";
const NoOp = () => {};
function wrapConsole(console, stdoutUpdate) {
const originalConsole = console;
let buffer = [];
const wrapConsoleMethod = (method) => {
return (...args) => {
const pretty = args.map((a) =>
typeof a === "string" ? a : util.inspect(a, { maxStringLength: 20 })
);
// pretty.unshift(`[${method}] `)
buffer.push(pretty);
stdoutUpdate(buffer.join("\n"));
originalConsole.log("vvv Wrapped console vvv", method, pretty);
return originalConsole[method](...args);
};
};
const fakeConsole = {};
["log", "debug", "error", "warn", "info"].forEach((m) => {
fakeConsole[m] = wrapConsoleMethod(m);
});
return { console: fakeConsole, buffer };
}
function isPromise(obj) {
return obj && typeof obj.then === "function";
}
export const transform = (filename, source, { type } = {}) => {
const plugins = [];
if (type === "commonjs") plugins.push(pluginTransformModulesCommonjs);
const babel = transformSync(source, {
filename: filename,
sourceMaps: false,
parserOpts: { allowReturnOutsideFunction: true },
presets: [presetReact, presetTypescript],
plugins: plugins,
});
return babel;
};
export class Repl {
constructor() {
this.executions = {};
window.onerror = (msg, url, lineNo, columnNo, err) => {
// const [, filename, line, column ] = err.stack.match(/\/([\/\w-_\.]+\.js):(\d*):(\d*)/)
if (this.executions[url]) {
console.error("Uncaught Global error", {
err,
stack:
msg +
`\n ${this.executions[url].filename}[${lineNo}:${columnNo}]`,
});
this.executions[url].err = err;
console.log(
"stopping propagation of repl execution",
this.executions[url]
);
this.executions[url].cb(err);
return true;
} else {
console.error("Uncaught Global error", {
err,
stack: msg + `\n ${url}[${lineNo}:${columnNo}]`,
});
}
};
window.addEventListener("unhandledrejection", (event) => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
}
formatStack(err, src) {
if (!err.stack) return err.toString();
const filename = this.executions[src].filename;
const source = this.executions[src].source;
const sourceLen = source.length;
const srcRegex = new RegExp(src, "g");
let stack = err.stack.replace(srcRegex, filename).split("\n");
if (stack[0] !== err.toString()) {
// Add missing error message to stack (safari)
stack.unshift(err.toString);
}
return stack
.map((line) => {
const lineNoRegex = new RegExp(`(.*${filename}:)(\d+)(.*)`);
const match = line && line.match && line.match(lineNoRegex);
if (match) {
const num = parseInt(match[2]);
// skip first and last lines (wrapper code)
if (num === 1 || num >= sourceLen) return "";
else return `${(match[1], num - 1, match[3])}`;
} else {
console.log("formatStack line match fallback", typeof line, line);
return `${line}`;
}
})
.filter((x) => x)
.join("\n");
}
injectScript(source, { config, filename, ast, stdoutUpdate }) {
const self = this;
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const execId = `litExec_${Date.now()}`;
const esm = ({ raw }, ...vals) =>
URL.createObjectURL(
new Blob([String.raw({ raw }, ...vals)], { type: "text/javascript" })
);
const wrappedConsole = wrapConsole(window.console, stdoutUpdate);
try {
if (config && config.babel) {
const babel = transform(filename, source);
console.log("[babel] transformed", babel);
source = babel.code;
}
} catch (err) {
console.error("[babel] Transpile failed", err);
reject({
err: err,
resp: null,
stdout: err.toString(),
});
}
const wrappedSrc = `(function(ast,console){/*${execId}*/let error; const cb = window['${execId}'].cb; const resp = (function(){ try {
${source}
} catch(err) { error = true; cb(err) } }).call(window['${execId}'].context.ast); if (!error) cb(null, resp);})(window['${execId}'].context.ast, window['${execId}'].context.console)`;
const src = esm`${wrappedSrc}`;
this.executions[src] = window[execId] = {
execId,
source,
filename,
stdout: wrappedConsole.buffer,
err: undefined,
resp: undefined,
cb: (err, resp) => {
console.log("pJaxCallback: ", err, resp);
let error = err || (this.executions[src] && this.executions[src].err);
if (error) {
const formattedError = this.formatStack(err, src);
this.executions[src].stdout.push(formattedError);
console.error("REPL ERR: ", this.executions[src]);
reject({
err: error,
resp: resp,
stdout: this.executions[src].stdout.join("\n"),
});
} else {
if (isPromise(resp)) {
resp.then((result) => {
this.executions[src].result = result;
const pretty =
typeof result === "string"
? result
: util.inspect(result, { depth: 2 });
this.executions[src].stdout.push(pretty);
console.log("REPL DONE: ", filename, this.executions[src]);
resolve({
err: error,
resp: result,
stdout: this.executions[src].stdout.join("\n"),
});
});
} else {
this.executions[src].resp = resp;
const pretty =
typeof resp === "string"
? resp
: util.inspect(resp, { depth: 2 });
this.executions[src].stdout.push(pretty);
console.log("REPL DONE: ", filename, this.executions[src]);
resolve({
err: error,
resp: resp,
stdout: this.executions[src].stdout.join("\n"),
});
}
}
},
context: { console: wrappedConsole.console, ast },
};
script.type = "module";
script.async = true;
script.crossorigin = "use-credentials";
script.src = src;
// script.addEventListener('load', resolve);
script.addEventListener("error", (ev) => {
console.error(
"script.onerror: " +
ev.message +
" (" +
ev.filename +
":" +
ev.lineno +
")",
ev
);
reject("Error loading script.");
});
script.addEventListener("abort", (ev) => {
console.log(
"script.onabort: " +
ev.message +
" (" +
ev.filename +
":" +
ev.lineno +
")",
ev
);
reject("Script loading aborted.");
});
document.head.appendChild(script);
});
}
exec(source, config, ast, stdoutUpdate = NoOp) {
console.log("REPL: ", config.repl);
const filename = config.filename || "untitled." + config.lang;
return this.injectScript(source, { config, filename, ast, stdoutUpdate });
}
}