Add @cosmwasm/cli

This commit is contained in:
Simon Warta 2020-02-06 15:06:43 +01:00
parent 5046963b17
commit b5d608af10
19 changed files with 962 additions and 5 deletions

3
NOTICE
View File

@ -5,6 +5,9 @@ and heavily modified from there on.
The code in packages/faucet was forked from https://github.com/iov-one/iov-faucet on
2020-01-29 at commit 33e2d707e7.
The code in packages/cli was forked from https://github.com/iov-one/iov-core/tree/v2.0.0-alpha.7/packages/iov-cli
on 2020-02-06.
Copyright 2018-2020 IOV SAS
Copyright 2020 Confio UO
Copyright 2020 Simon Warta

1
packages/cli/.eslintignore Symbolic link
View File

@ -0,0 +1 @@
../../.eslintignore

5
packages/cli/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
build/
dist/
docs/
selftest_userprofile_db/

50
packages/cli/README.md Normal file
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
packages/cli/bin/cosmwasm-cli Executable file
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));

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();

View File

@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages

51
packages/cli/package.json Normal file
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"
}
}

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
packages/cli/src/async.ts Normal file
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
packages/cli/src/cli.ts Normal file
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);
});
}

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");
});
});
});

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));
}

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
packages/cli/src/tsrepl.ts Normal file
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;
}
}

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2017",
"noUnusedLocals": false,
"noImplicitAny": false
}
}

3
packages/cli/tslint.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../../tslint.json"
}

105
yarn.lock
View File

@ -944,6 +944,23 @@
dependencies:
"@types/node" "*"
"@types/argparse@^1.0.34":
version "1.0.38"
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9"
integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==
"@types/babel-types@*":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==
"@types/babylon@^6.16.3":
version "6.16.5"
resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4"
integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==
dependencies:
"@types/babel-types" "*"
"@types/body-parser@*":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897"
@ -969,6 +986,11 @@
"@types/keygrip" "*"
"@types/node" "*"
"@types/diff@^3.5.1":
version "3.5.3"
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.3.tgz#7c6c3721ba454d838790100faf7957116ee7deab"
integrity sha512-YrLagYnL+tfrgM7bQ5yW34pi5cg9pmh5Gbq2Lmuuh+zh0ZjmK2fU3896PtlpJT3IDG2rdkoG30biHJepgIsMnw==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -1501,7 +1523,7 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
argparse@^1.0.7:
argparse@^1.0.10, argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
@ -1577,7 +1599,7 @@ arraybuffer.slice@~0.0.7:
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
arrify@^1.0.1:
arrify@^1.0.0, arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
@ -1621,6 +1643,11 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
ast-types@0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==
astral-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
@ -1675,6 +1702,11 @@ axios@^0.19.0:
dependencies:
follow-redirects "1.5.10"
babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@ -1918,7 +1950,7 @@ buffer-fill@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
buffer-from@^1.0.0:
buffer-from@^1.0.0, buffer-from@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
@ -2252,7 +2284,7 @@ colors@1.1.2:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
colors@^1.1.0:
colors@^1.1.0, colors@^1.3.3:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@ -2789,6 +2821,11 @@ di@^0.0.1:
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
diff@^3.1.0, diff@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@ -3219,7 +3256,7 @@ espree@^6.1.2:
acorn-jsx "^5.1.0"
eslint-visitor-keys "^1.1.0"
esprima@^4.0.0:
esprima@^4.0.0, esprima@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
@ -4856,6 +4893,15 @@ level-supports@~1.0.0:
dependencies:
xtend "^4.0.2"
leveldown@^5.0.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.4.1.tgz#83a8fdd9bb52b1ed69be2ef59822b6cdfcdb51ec"
integrity sha512-3lMPc7eU3yj5g+qF1qlALInzIYnkySIosR1AsUKFjL9D8fYbTLuENBAeDRZXIG4qeWOAyqRItOoLu2v2avWiMA==
dependencies:
abstract-leveldown "~6.2.1"
napi-macros "~2.0.0"
node-gyp-build "~4.1.0"
levelup@^4.0.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.3.2.tgz#31c5b1b29f146d1d35d692e01a6da4d28fa55ebd"
@ -5078,6 +5124,11 @@ make-dir@^2.0.0, make-dir@^2.1.0:
pify "^4.0.1"
semver "^5.6.0"
make-error@^1.1.1:
version "1.3.5"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
make-fetch-happen@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd"
@ -5461,6 +5512,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
napi-macros@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -5495,6 +5551,11 @@ node-fetch@^2.3.0, node-fetch@^2.5.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-gyp-build@~4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==
node-gyp@^5.0.2:
version "5.0.7"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-5.0.7.tgz#dd4225e735e840cf2870e4037c2ed9c28a31719e"
@ -6164,6 +6225,11 @@ prettier@^1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -6507,6 +6573,16 @@ readonly-date@^1.0.0:
resolved "https://registry.yarnpkg.com/readonly-date/-/readonly-date-1.0.0.tgz#5af785464d8c7d7c40b9d738cbde8c646f97dcd9"
integrity sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==
recast@^0.18.0:
version "0.18.5"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.5.tgz#9d5adbc07983a3c8145f3034812374a493e0fe4d"
integrity sha512-sD1WJrpLQAkXGyQZyGzTM75WJvyAd98II5CHdK3IYbt/cZlU0UzCRVU11nUFNXX9fBVEt4E9ajkMjBlUlG+Oog==
dependencies:
ast-types "0.13.2"
esprima "~4.0.0"
private "^0.1.8"
source-map "~0.6.1"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@ -7494,6 +7570,20 @@ trim-off-newlines@^1.0.0:
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
ts-node@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf"
integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==
dependencies:
arrify "^1.0.0"
buffer-from "^1.1.0"
diff "^3.1.0"
make-error "^1.1.1"
minimist "^1.2.0"
mkdirp "^0.5.1"
source-map-support "^0.5.6"
yn "^2.0.0"
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
@ -8095,3 +8185,8 @@ ylru@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
yn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=