Add @cosmwasm/cli

This commit is contained in:
Simon Warta
2020-02-06 17:36:14 +01:00
parent 5046963b17
commit b5d608af10
19 changed files with 962 additions and 5 deletions
+1
View File
@@ -0,0 +1 @@
../../.eslintignore
+5
View File
@@ -0,0 +1,5 @@
build/
dist/
docs/
selftest_userprofile_db/
+50
View File
@@ -0,0 +1,50 @@
# @cosmwasm/cli
[![npm version](https://img.shields.io/npm/v/@cosmwasm/cli.svg)](https://www.npmjs.com/package/@cosmwasm/cli)
## Installation and first run
The `cosmwasm-cli` executable is available via npm. We recommend local
installations to your demo project. If you don't have one yet, just
`mkdir cosmwasm-cli-installation && cd cosmwasm-cli-installation && yarn init --yes`.
### locally with yarn
```
$ yarn add @cosmwasm/cli --dev
$ ./node_modules/.bin/cosmwasm-cli
```
### locally with npm
```
$ npm install @cosmwasm/cli --save-dev
$ ./node_modules/.bin/cosmwasm-cli
```
### globally with yarn
```
$ yarn global add @cosmwasm/cli
$ cosmwasm-cli
```
### globally with npm
```
$ npm install -g @cosmwasm/cli
$ cosmwasm-cli
```
## Getting started
1. Install `@cosmwasm/cli` and run `cosmwasm-cli` as shown above
2. TODO: write README inspired by
https://github.com/iov-one/iov-core/blob/master/packages/iov-cli/README.md
## License
This package is part of the cosmwasm-js repository, licensed under the Apache
License 2.0 (see
[NOTICE](https://github.com/confio/cosmwasm-js/blob/master/NOTICE) and
[LICENSE](https://github.com/confio/cosmwasm-js/blob/master/LICENSE)).
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env node
const path = require("path");
// attempt to call in main file....
const cli = require(path.join(__dirname, "..", "build", "cli.js"));
cli.main(process.argv.slice(2));
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
require("source-map-support").install();
const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json");
// setup Jasmine
const Jasmine = require("jasmine");
const jasmine = new Jasmine();
jasmine.loadConfig({
spec_dir: "build",
spec_files: ["**/*.spec.js"],
helpers: [],
random: false,
seed: null,
stopSpecOnExpectationFailure: false,
});
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000;
// setup reporter
const { SpecReporter } = require("jasmine-spec-reporter");
const reporter = new SpecReporter({ ...defaultSpecReporterConfig });
// initialize and execute
jasmine.env.clearReporters();
jasmine.addReporter(reporter);
jasmine.execute();
+1
View File
@@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@iov/cli",
"version": "0.0.3",
"description": "Command line interface",
"contributors": ["IOV SAS <admin@iov.one>", "Simon Warta"],
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/confio/cosmwasm-js/tree/master/packages/cli"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
"format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"",
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && tslint -t verbose --project .",
"build": "tsc",
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
"test-node": "node jasmine-testrunner.js",
"test-bin": "yarn build-or-skip && ./bin/cosmwasm-cli --selftest",
"test": "yarn build-or-skip && yarn test-node"
},
"bin": {
"cosmwasm-cli": "bin/cosmwasm-cli"
},
"files": [
"build/",
"types/",
"tsconfig_repl.json",
"*.md",
"!*.spec.*",
"!**/testdata/"
],
"dependencies": {
"@cosmwasm/sdk": "^0.0.3",
"argparse": "^1.0.10",
"babylon": "^6.18.0",
"colors": "^1.3.3",
"diff": "^3.5.0",
"leveldown": "^5.0.0",
"recast": "^0.18.0",
"ts-node": "^7.0.0",
"typescript": "~3.7"
},
"devDependencies": {
"@types/argparse": "^1.0.34",
"@types/babylon": "^6.16.3",
"@types/diff": "^3.5.1"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { wrapInAsyncFunction } from "./async";
describe("async", () => {
it("can convert wrap code in async function", () => {
expect(wrapInAsyncFunction("")).toMatch(/\(async \(\) => {\s+}\)\(\)/);
expect(wrapInAsyncFunction(" ")).toMatch(/\(async \(\) => {\s+}\)\(\)/);
expect(wrapInAsyncFunction("\n")).toMatch(/\(async \(\) => {\s+}\)\(\)/);
expect(wrapInAsyncFunction(" \n ")).toMatch(/\(async \(\) => {\s+}\)\(\)/);
// locals become globals
expect(wrapInAsyncFunction("var a = 1;")).toMatch(/\(async \(\) => {\s+a = 1;\s+}\)\(\)/);
expect(wrapInAsyncFunction("const a = Date.now();")).toMatch(
/\(async \(\) => {\s+a = Date.now\(\);\s+}\)\(\)/,
);
// expressions
expect(wrapInAsyncFunction("1")).toMatch(/\(async \(\) => {\s+return 1;\s+}\)\(\)/);
expect(wrapInAsyncFunction("1;")).toMatch(/\(async \(\) => {\s+return 1;;\s+}\)\(\)/);
expect(wrapInAsyncFunction("a+b")).toMatch(/\(async \(\) => {\s+return a\+b;\s+}\)\(\)/);
expect(wrapInAsyncFunction("a++")).toMatch(/\(async \(\) => {\s+return a\+\+;\s+}\)\(\)/);
expect(wrapInAsyncFunction("Date.now()")).toMatch(/\(async \(\) => {\s+return Date.now\(\);\s+}\)\(\)/);
expect(wrapInAsyncFunction("(1)")).toMatch(/\(async \(\) => {\s+return \(1\);\s+}\)\(\)/);
// multiple statements
expect(wrapInAsyncFunction("var a = 1; var b = 2;")).toMatch(
/\(async \(\) => {\s+a = 1;\s+b = 2;\s+}\)\(\)/,
);
expect(wrapInAsyncFunction("var a = 1; a")).toMatch(/\(async \(\) => {\s+a = 1;\s+return a;\s+}\)\(\)/);
// comments
expect(wrapInAsyncFunction("/* abcd */")).toMatch(/\(async \(\) => {\s+\/\* abcd \*\/\s+}\)\(\)/);
expect(wrapInAsyncFunction("// abcd")).toMatch(/\(async \(\) => {\s+\/\/ abcd\s+}\)\(\)/);
});
});
+47
View File
@@ -0,0 +1,47 @@
import * as recast from "recast";
import babylon = require("babylon");
export function wrapInAsyncFunction(code: string): string {
const codeInAsyncFunction = `(async () => {
${code}
})()`;
const ast = recast.parse(codeInAsyncFunction, { parser: babylon });
const body = ast.program.body[0].expression.callee.body.body;
if (body.length !== 0) {
const last = body.pop();
if (last.type === "ExpressionStatement") {
body.push({
type: "ReturnStatement",
argument: last,
});
} else {
body.push(last);
}
}
// Remove var, let, const from variable declarations to make them available in context
// tslint:disable-next-line:no-object-mutation
ast.program.body[0].expression.callee.body.body = body.map((node: any) => {
if (node.type === "VariableDeclaration") {
return {
type: "ExpressionStatement",
expression: {
type: "SequenceExpression",
expressions: node.declarations.map((declaration: any) => ({
type: "AssignmentExpression",
operator: "=",
left: declaration.id,
right: declaration.init,
})),
},
};
} else {
return node;
}
});
return recast.print(ast).code;
}
+138
View File
@@ -0,0 +1,138 @@
import { ArgumentParser } from "argparse";
// tslint:disable-next-line:no-submodule-imports
import colors = require("colors/safe");
import { join } from "path";
import { TsRepl } from "./tsrepl";
export function main(originalArgs: readonly string[]): void {
const parser = new ArgumentParser({ description: "The CosmWasm REPL" });
parser.addArgument("--version", {
action: "storeTrue",
help: "Print version and exit",
});
const maintainerGroup = parser.addArgumentGroup({
title: "Maintainer options",
description: "Don't use those unless a maintainer tells you to.",
});
maintainerGroup.addArgument("--selftest", {
action: "storeTrue",
help: "Run a selftext and exit",
});
maintainerGroup.addArgument("--debug", {
action: "storeTrue",
help: "Enable debugging",
});
const args = parser.parseArgs([...originalArgs]);
if (args.version) {
const version = require(join(__dirname, "..", "package.json")).version;
console.info(version);
return;
}
const imports = new Map<string, readonly string[]>([
["@cosmwasm/sdk", ["types", "RestClient"]],
[
"@iov/crypto",
[
"Bip39",
"Ed25519",
"Ed25519Keypair",
"EnglishMnemonic",
"Random",
"Secp256k1",
"Sha256",
"Sha512",
"Slip10",
"Slip10Curve",
"Slip10RawIndex",
],
],
[
"@iov/encoding",
[
"Bech32",
"Encoding",
// integers
"Int53",
"Uint32",
"Uint53",
"Uint64",
],
],
[
"@iov/keycontrol",
[
"Ed25519HdWallet",
"HdPaths",
"Keyring",
"Secp256k1HdWallet",
"UserProfile",
"Wallet",
"WalletId",
"WalletImplementationIdString",
"WalletSerializationString",
],
],
]);
console.info(colors.green("Initializing session for you. Have fun!"));
console.info(colors.yellow("Available imports:"));
console.info(colors.yellow(" * http"));
console.info(colors.yellow(" * https"));
console.info(colors.yellow(" * leveldown"));
console.info(colors.yellow(" * levelup"));
console.info(colors.yellow(" * from long"));
console.info(colors.yellow(" - Long"));
for (const moduleName of imports.keys()) {
console.info(colors.yellow(` * from ${moduleName}`));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
for (const symbol of imports.get(moduleName)!) {
console.info(colors.yellow(` - ${symbol}`));
}
}
console.info(colors.yellow(" * helper functions"));
console.info(colors.yellow(" - toAscii"));
console.info(colors.yellow(" - fromHex"));
console.info(colors.yellow(" - toHex"));
let init = `
import leveldown = require('leveldown');
import levelup from "levelup";
import * as http from 'http';
import * as https from 'https';
import Long from "long";
`;
for (const moduleName of imports.keys()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
init += `import { ${imports.get(moduleName)!.join(", ")} } from "${moduleName}";\n`;
}
init += `const { toAscii, fromHex, toHex } = Encoding;\n`;
if (args.selftest) {
// execute some trival stuff and exit
init += `
const hash = new Sha512(new Uint8Array([])).digest();
const hexHash = toHex(hash);
export class NewDummyClass {};
const profile = new UserProfile();
const wallet = profile.addWallet(Ed25519HdWallet.fromMnemonic("degree tackle suggest window test behind mesh extra cover prepare oak script"));
const db = levelup(leveldown('./selftest_userprofile_db'));
await profile.storeIn(db, "secret passwd");
const profileFromDb = await UserProfile.loadFrom(db, "secret passwd");
console.info("Done testing, will exit now.");
process.exit(0);
`;
}
const tsconfigPath = join(__dirname, "..", "tsconfig_repl.json");
const installationDir = join(__dirname, "..");
new TsRepl(tsconfigPath, init, !!args.debug, installationDir).start().catch(error => {
console.error(error);
process.exit(1);
});
}
+148
View File
@@ -0,0 +1,148 @@
import { createContext } from "vm";
import { executeJavaScript, executeJavaScriptAsync } from "./helpers";
describe("Helpers", () => {
describe("executeJavaScript", () => {
it("can execute simple JavaScript", () => {
{
const context = createContext({});
expect(executeJavaScript("123", "myfile.js", context)).toEqual(123);
}
{
const context = createContext({});
expect(executeJavaScript("1+1", "myfile.js", context)).toEqual(2);
}
});
it("can execute multiple commands in one context", () => {
const context = createContext({});
expect(executeJavaScript("let a", "myfile.js", context)).toBeUndefined();
expect(executeJavaScript("a = 2", "myfile.js", context)).toEqual(2);
expect(executeJavaScript("a", "myfile.js", context)).toEqual(2);
expect(executeJavaScript("let b = 3", "myfile.js", context)).toBeUndefined();
expect(executeJavaScript("a+b", "myfile.js", context)).toEqual(5);
});
it("has no require() in sandbox context", () => {
const context = createContext({});
expect(executeJavaScript("typeof require", "myfile.js", context)).toEqual("undefined");
});
it("has no exports object in sandbox context", () => {
const context = createContext({});
expect(executeJavaScript("typeof exports", "myfile.js", context)).toEqual("undefined");
});
it("has no module object in sandbox context", () => {
const context = createContext({});
expect(executeJavaScript("typeof module", "myfile.js", context)).toEqual("undefined");
});
it("can use require when passed into sandbox context", () => {
const context = createContext({ require: require });
expect(executeJavaScript("const path = require('path')", "myfile.js", context)).toBeUndefined();
expect(executeJavaScript("path.join('.')", "myfile.js", context)).toEqual(".");
});
it("can use module when passed into sandbox context", () => {
const context = createContext({ module: module });
expect(executeJavaScript("module.exports.fooTest", "myfile.js", context)).toBeUndefined();
expect(executeJavaScript("module.exports.fooTest = 'bar'", "myfile.js", context)).toEqual("bar");
expect(executeJavaScript("module.exports.fooTest", "myfile.js", context)).toEqual("bar");
// roll back change to module.exports
// tslint:disable-next-line:no-object-mutation
module.exports.fooTest = undefined;
});
it("can use exports when passed into sandbox context", () => {
const context = createContext({ exports: {} });
expect(executeJavaScript("exports.fooTest", "myfile.js", context)).toBeUndefined();
expect(executeJavaScript("exports.fooTest = 'bar'", "myfile.js", context)).toEqual("bar");
expect(executeJavaScript("exports.fooTest", "myfile.js", context)).toEqual("bar");
});
});
describe("executeJavaScriptAsync", () => {
it("can execute top level await with brackets", async () => {
const context = createContext({});
expect(await executeJavaScriptAsync("await (1)", "myfile.js", context)).toEqual(1);
});
it("can execute top level await without brackets", async () => {
const context = createContext({});
expect(await executeJavaScriptAsync("await 1", "myfile.js", context)).toEqual(1);
});
it("can handle multiple awaits", async () => {
const context = createContext({});
expect(await executeJavaScriptAsync("await 1 + await 2", "myfile.js", context)).toEqual(3);
});
it("errors for local declaration without assignment", async () => {
// `var a` cannot be converted into an assignment because it must not override an
// existing value. Thus we cannot execute it
const context = createContext({});
await executeJavaScriptAsync("var a", "myfile.js", context)
.then(() => fail("must not resolve"))
.catch(e => expect(e).toMatch(/SyntaxError:/));
await executeJavaScriptAsync("let b", "myfile.js", context)
.then(() => fail("must not resolve"))
.catch(e => expect(e).toMatch(/SyntaxError:/));
await executeJavaScriptAsync("const c", "myfile.js", context)
.then(() => fail("must not resolve"))
.catch(e => expect(e).toMatch(/SyntaxError:/));
});
it("can execute multiple commands in one context", async () => {
const context = createContext({});
expect(await executeJavaScriptAsync("let a = 2", "myfile.js", context)).toBeUndefined();
expect(await executeJavaScriptAsync("a", "myfile.js", context)).toEqual(2);
expect(await executeJavaScriptAsync("a += 1", "myfile.js", context)).toEqual(3);
expect(await executeJavaScriptAsync("a += 7", "myfile.js", context)).toEqual(10);
expect(await executeJavaScriptAsync("a", "myfile.js", context)).toEqual(10);
expect((context as any).a).toEqual(10);
});
it("local variables are available across multiple calls in one context", async () => {
const context = createContext({});
expect(await executeJavaScriptAsync("let a = 3", "myfile.js", context)).toBeUndefined();
expect(await executeJavaScriptAsync("a", "myfile.js", context)).toEqual(3);
expect((context as any).a).toEqual(3);
});
it("works with strict mode", async () => {
const context = createContext({});
expect(await executeJavaScriptAsync('"use strict"; let a = 3', "myfile.js", context)).toBeUndefined();
expect(await executeJavaScriptAsync('"use strict"; a', "myfile.js", context)).toEqual(3);
expect((context as any).a).toEqual(3);
});
it("can reassign const", async () => {
// a side-effect of local variable assignment manipulation
const context = createContext({});
expect(await executeJavaScriptAsync("const a = 3", "myfile.js", context)).toBeUndefined();
expect((context as any).a).toEqual(3);
expect(await executeJavaScriptAsync("const a = 4", "myfile.js", context)).toBeUndefined();
expect((context as any).a).toEqual(4);
});
it("can execute timeout promise code", async () => {
const context = createContext({ setTimeout: setTimeout });
const code = "await (new Promise(resolve => setTimeout(() => resolve('job done'), 5)))";
expect(await executeJavaScriptAsync(code, "myfile.js", context)).toEqual("job done");
});
it("can execute timeout code in multiple statements", async () => {
const context = createContext({ setTimeout: setTimeout });
const code = `
const promise = new Promise(resolve => {
setTimeout(() => resolve('job done'), 5);
});
await (promise);
`;
expect(await executeJavaScriptAsync(code, "myfile.js", context)).toEqual("job done");
});
});
});
+33
View File
@@ -0,0 +1,33 @@
import { TSError } from "ts-node";
import { Context, Script } from "vm";
import { wrapInAsyncFunction } from "./async";
export function executeJavaScript(code: string, filename: string, context: Context): any {
const script = new Script(code, { filename: filename });
return script.runInContext(context);
}
export async function executeJavaScriptAsync(code: string, filename: string, context: Context): Promise<any> {
const preparedCode = code.replace(/^\s*"use strict";/, "");
// wrapped code returns a promise when executed
const wrappedCode = wrapInAsyncFunction(preparedCode);
const script = new Script(wrappedCode, { filename: filename });
const out = await script.runInContext(context);
return out;
}
export function isRecoverable(error: TSError): boolean {
const recoveryCodes: Set<number> = new Set([
1003, // "Identifier expected."
1005, // "')' expected."
1109, // "Expression expected."
1126, // "Unexpected end of text."
1160, // "Unterminated template literal."
1161, // "Unterminated regular expression literal."
2355, // "A function whose declared type is neither 'void' nor 'any' must return a value."
]);
return error.diagnosticCodes.every(code => recoveryCodes.has(code));
}
+56
View File
@@ -0,0 +1,56 @@
import { join } from "path";
import { TsRepl } from "./tsrepl";
const tsConfigPath = join(__dirname, "..", "tsconfig_repl.json");
describe("TsRepl", () => {
it("can be constructed", () => {
const noCode = new TsRepl(tsConfigPath, "");
expect(noCode).toBeTruthy();
const jsCode = new TsRepl(tsConfigPath, "const a = 'foo'");
expect(jsCode).toBeTruthy();
const tsCode = new TsRepl(tsConfigPath, "const a: string = 'foo'");
expect(tsCode).toBeTruthy();
});
it("can be started", async () => {
{
const server = await new TsRepl(tsConfigPath, "").start();
expect(server).toBeTruthy();
}
{
const server = await new TsRepl(tsConfigPath, "const a = 'foo'").start();
expect(server).toBeTruthy();
}
{
const server = await new TsRepl(tsConfigPath, "const a: string = 'foo'").start();
expect(server).toBeTruthy();
}
});
it("errors when starting with broken TypeScript", async () => {
await new TsRepl(tsConfigPath, "const a: string = 123;")
.start()
.then(() => fail("must not resolve"))
.catch(e => expect(e).toMatch(/is not assignable to type 'string'/));
await new TsRepl(tsConfigPath, "const const const;")
.start()
.then(() => fail("must not resolve"))
.catch(e => expect(e).toMatch(/Variable declaration expected./));
});
it("can be started with top level await", async () => {
{
const server = await new TsRepl(tsConfigPath, "await 1").start();
expect(server).toBeTruthy();
}
{
const server = await new TsRepl(tsConfigPath, "await 1 + await 2").start();
expect(server).toBeTruthy();
}
});
});
+239
View File
@@ -0,0 +1,239 @@
import { diffLines } from "diff";
import { join } from "path";
import { Recoverable, REPLServer, start } from "repl";
import { Register, register, TSError } from "ts-node";
import { Context, createContext } from "vm";
import { executeJavaScriptAsync, isRecoverable } from "./helpers";
interface ReplEvalResult {
readonly result: any;
readonly error: Error | null;
}
export class TsRepl {
private readonly typeScriptService: Register;
private readonly debuggingEnabled: boolean;
private readonly evalFilename = `[eval].ts`;
private readonly evalPath: string;
private readonly evalData = { input: "", output: "" };
private readonly resetToZero: () => void; // Bookmark to empty TS input
private readonly initialTypeScript: string;
// tslint:disable-next-line:readonly-keyword
private context: Context | undefined;
public constructor(
tsconfigPath: string,
initialTypeScript: string,
debuggingEnabled = false,
installationDir?: string, // required when the current working directory is not the installation path
) {
this.typeScriptService = register({
project: tsconfigPath,
ignoreDiagnostics: [
"1308", // TS1308: 'await' expression is only allowed within an async function.
],
});
this.debuggingEnabled = debuggingEnabled;
this.resetToZero = this.appendTypeScriptInput("");
this.initialTypeScript = initialTypeScript;
this.evalPath = join(installationDir || process.cwd(), this.evalFilename);
}
public async start(): Promise<REPLServer> {
/**
* A wrapper around replEval used to match the method signature
* for "Custom Evaluation Functions"
* https://nodejs.org/api/repl.html#repl_custom_evaluation_functions
*/
const replEvalWrapper = async (
code: string,
_context: any,
_filename: string,
callback: (err: Error | null, result?: any) => any,
): Promise<void> => {
const result = await this.replEval(code);
callback(result.error, result.result);
};
const repl = start({
prompt: ">> ",
input: process.stdin,
output: process.stdout,
terminal: process.stdout.isTTY,
eval: replEvalWrapper,
useGlobal: false,
});
// Prepare context for TypeScript: TypeScript compiler expects the exports shortcut
// to exist in `Object.defineProperty(exports, "__esModule", { value: true });`
const unsafeReplContext = repl.context as any;
if (!unsafeReplContext.exports) {
// tslint:disable-next-line:no-object-mutation
unsafeReplContext.exports = unsafeReplContext.module.exports;
}
// REPL context is created with a default set of module resolution paths,
// like for example
// [ '/home/me/repl/node_modules',
// '/home/me/node_modules',
// '/home/node_modules',
// '/node_modules',
// '/home/me/.node_modules',
// '/home/me/.node_libraries',
// '/usr/lib/nodejs' ]
// However, this does not include the installation path of @iov/cli because
// REPL does not inherit module paths from the current process. Thus we override
// the repl paths with the current process' paths
// tslint:disable-next-line:no-object-mutation
unsafeReplContext.module.paths = module.paths;
// tslint:disable-next-line:no-object-mutation
this.context = createContext(repl.context);
const reset = async (): Promise<void> => {
this.resetToZero();
// Ensure code ends with "\n" due to implementation of replEval
await this.compileAndExecute(this.initialTypeScript + "\n", false);
};
await reset();
repl.on("reset", reset);
repl.defineCommand("type", {
help: "Check the type of a TypeScript identifier",
action: (identifier: string) => {
if (!identifier) {
repl.displayPrompt();
return;
}
const identifierTypeScriptCode = `${identifier}\n`;
const undo = this.appendTypeScriptInput(identifierTypeScriptCode);
const identifierFirstPosition = this.evalData.input.length - identifierTypeScriptCode.length;
const { name, comment } = this.typeScriptService.getTypeInfo(
this.evalData.input,
this.evalPath,
identifierFirstPosition,
);
undo();
repl.outputStream.write(`${name}\n${comment ? `${comment}\n` : ""}`);
repl.displayPrompt();
},
});
return repl;
}
private async compileAndExecute(tsInput: string, isAutocompletionRequest: boolean): Promise<any> {
if (!isAutocompletionRequest) {
// Expect POSIX lines (https://stackoverflow.com/a/729795)
if (tsInput.length > 0 && !tsInput.endsWith("\n")) {
throw new Error("final newline missing");
}
}
const undo = this.appendTypeScriptInput(tsInput);
let output: string;
try {
// lineOffset unused at the moment (https://github.com/TypeStrong/ts-node/issues/661)
output = this.typeScriptService.compile(this.evalData.input, this.evalPath);
} catch (err) {
undo();
throw err;
}
// Use `diff` to check for new JavaScript to execute.
const changes = diffLines(this.evalData.output, output);
if (isAutocompletionRequest) {
undo();
} else {
// tslint:disable-next-line:no-object-mutation
this.evalData.output = output;
}
// Execute new JavaScript. This may not necessarily be at the end only because e.g. an import
// statement in TypeScript is compiled to no JavaScript until the imported symbol is used
// somewhere. This btw. leads to a different execution order of imports than in the TS source.
let lastResult: any;
for (const added of changes.filter(change => change.added)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
lastResult = await executeJavaScriptAsync(added.value, this.evalFilename, this.context!);
}
return lastResult;
}
/**
* Add user-friendly error handling around compileAndExecute
*/
private async replEval(code: string): Promise<ReplEvalResult> {
// TODO: Figure out how to handle completion here.
if (code === ".scope") {
return {
result: undefined,
error: null,
};
}
const isAutocompletionRequest = !/\n$/.test(code);
try {
const result = await this.compileAndExecute(code, isAutocompletionRequest);
return {
result: result,
error: null,
};
} catch (error) {
if (this.debuggingEnabled) {
console.info("Current REPL TypeScript program:");
console.info(this.evalData.input);
}
let outError: Error | null;
if (error instanceof TSError) {
// Support recoverable compilations using >= node 6.
if (Recoverable && isRecoverable(error)) {
outError = new Recoverable(error);
} else {
console.error(error.diagnosticText);
outError = null;
}
} else {
outError = error;
}
return {
result: undefined,
error: outError,
};
}
}
private appendTypeScriptInput(input: string): () => void {
const oldInput = this.evalData.input;
const oldOutput = this.evalData.output;
// Handle ASI issues with TypeScript re-evaluation.
if (oldInput.charAt(oldInput.length - 1) === "\n" && /^\s*[[(`]/.test(input) && !/;\s*$/.test(oldInput)) {
// tslint:disable-next-line:no-object-mutation
this.evalData.input = `${this.evalData.input.slice(0, -1)};\n`;
}
// tslint:disable-next-line:no-object-mutation
this.evalData.input += input;
const undoFunction = (): void => {
// tslint:disable-next-line:no-object-mutation
this.evalData.input = oldInput;
// tslint:disable-next-line:no-object-mutation
this.evalData.output = oldOutput;
};
return undoFunction;
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2017",
"noUnusedLocals": false,
"noImplicitAny": false
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tslint.json"
}