packages/sqrl-cli/src/repl/SqrlRepl.ts (171 lines of code) (raw):
/**
* Copyright 2018 Twitter, Inc.
* Licensed under the Apache License, Version 2.0
* http://www.apache.org/licenses/LICENSE-2.0
*/
// tslint:disable:no-console
// tslint:disable:no-submodule-imports (@TODO)
import * as repl from "repl";
import { SqrlTest } from "sqrl/lib/testing/SqrlTest";
import { parseRepl } from "sqrl/lib/parser/SqrlParse";
import SqrlAst from "sqrl/lib/ast/SqrlAst";
import * as expandTilde from "expand-tilde";
import { existsSync, readFileSync, appendFileSync } from "fs";
import { EventEmitter } from "eventemitter3";
import {
SqrlCompileError,
SqrlObject,
Context,
Instance,
isValidFeatureName,
StatementAst,
Ast,
} from "sqrl";
import chalk from "chalk";
import { SlotMissingCallbackError } from "sqrl/lib/execute/SqrlExecutionState";
import { Readable, Writable } from "stream";
import Semaphore from "sqrl/lib/jslib/Semaphore";
import { invariant } from "sqrl-common";
import { spanToShell } from "../spanToShell";
import { renderFunctionsHelp } from "../renderFunctionsHelp";
type EventTypes = "exit";
export class SqrlRepl extends EventEmitter<EventTypes> {
private traceFactory: () => Context;
private server: repl.REPLServer | null = null;
private historyPath: string = expandTilde("~/.sqrl_repl_history");
private stdin: Readable;
private stdout: Writable;
private busy = new Semaphore();
constructor(
private instance: Instance,
private test: SqrlTest,
options: {
traceFactory: () => Context;
stdin?: Readable;
stdout?: Writable;
}
) {
super();
this.traceFactory = options.traceFactory;
this.stdin = options.stdin || process.stdin;
this.stdout = options.stdout || process.stdout;
}
async repl(ctx: Context, input: string): Promise<any> {
let returnFeature = null;
if (isValidFeatureName(input.trim())) {
returnFeature = input.trim();
} else {
const ast = parseRepl(input, {
customFunctions: this.instance._instance.customFunctions,
});
const statements = ast.statements;
if (!statements.length) {
return;
}
// If the last statement is an expression, save its result in SqrlReplOutput
let last: Ast = statements[statements.length - 1];
// If it's a call, make sure it's to a statement otherwise treat as an expression
if (last.type === "call") {
if (
this.instance._instance.has(last.func) &&
!this.instance._instance.isStatement(last.func)
) {
last = {
type: "expr",
location: last.location,
expr: last,
};
}
}
if (last.type === "let") {
returnFeature = last.feature;
} else if (last.type === "expr") {
statements.pop();
const { expr, location } = last;
statements.push(
SqrlAst.letStatement("SqrlReplOutput", expr, {
description: null,
location,
})
);
returnFeature = "SqrlReplOutput";
}
// @NOTE: Only the last statement could be an Expr and that is handled
// above. Newer TypeScript version may be able to handle this constraint
// without a cast.
await this.test.runStatements(ctx, statements as StatementAst[]);
}
if (returnFeature) {
const state = await this.test.executeStatements(ctx, []);
return state.fetchByName(returnFeature);
}
}
private printHelp() {
console.log(renderFunctionsHelp(this.instance));
}
private async eval(cmd, context, filename) {
this.busy.increment();
const ctx = this.traceFactory();
try {
appendFileSync(this.historyPath, cmd);
if (cmd.trim() === "help") {
this.printHelp();
return;
}
const result = await this.repl(ctx, cmd);
if (result instanceof SqrlObject) {
console.log(spanToShell(result.render()).trimRight());
return result.getBasicValue();
}
return result;
} catch (e) {
if (this.isRecoverableError(e)) {
throw new repl.Recoverable(e);
} else if (e instanceof SqrlCompileError) {
console.log(chalk.red(e.message));
return;
} else if (e instanceof SlotMissingCallbackError) {
console.log(
chalk.red("Required feature was not defined: ") +
chalk.red.bold(e.slotName)
);
} else {
throw e;
}
} finally {
this.busy.decrement();
}
}
isRecoverableError(error) {
return / but end of input found\.$/.test(error.message);
}
readHistory() {
if (existsSync(this.historyPath)) {
return readFileSync(this.historyPath, { encoding: "utf-8" })
.split("\n")
.reverse()
.filter((line) => line.trim());
} else {
return [];
}
}
on(event: "exit", callback: () => void);
on(event, callback) {
invariant(event === "exit", "Only exit event is supported");
super.on("exit", () => {
this.busy.waitForZero().then(callback);
});
}
start() {
this.server = repl.start({
input: this.stdin,
output: this.stdout,
prompt: "sqrl> ",
ignoreUndefined: true,
eval: (cmd, context, filename, callback) => {
this.eval(cmd, context, filename).then(
(rv) => callback(null, rv),
(err) => callback(err, null)
);
},
});
this.server.on("exit", () => {
this.server = null;
this.emit("exit");
});
// Internal api to push history
if ((this.server as any).history) {
this.readHistory().map((line) => (this.server as any).history.push(line));
}
}
stop() {
this.server.close();
}
}