From b5d608af10098cbb0f3276188b65553d5dbf9342 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 15:06:43 +0100 Subject: [PATCH 1/9] Add @cosmwasm/cli --- NOTICE | 3 + packages/cli/.eslintignore | 1 + packages/cli/.gitignore | 5 + packages/cli/README.md | 50 ++++++ packages/cli/bin/cosmwasm-cli | 6 + packages/cli/jasmine-testrunner.js | 26 ++++ packages/cli/nonces/README.txt | 1 + packages/cli/package.json | 51 ++++++ packages/cli/src/async.spec.ts | 34 ++++ packages/cli/src/async.ts | 47 ++++++ packages/cli/src/cli.ts | 138 +++++++++++++++++ packages/cli/src/helpers.spec.ts | 148 ++++++++++++++++++ packages/cli/src/helpers.ts | 33 ++++ packages/cli/src/tsrepl.spec.ts | 56 +++++++ packages/cli/src/tsrepl.ts | 239 +++++++++++++++++++++++++++++ packages/cli/tsconfig.json | 11 ++ packages/cli/tsconfig_repl.json | 10 ++ packages/cli/tslint.json | 3 + yarn.lock | 105 ++++++++++++- 19 files changed, 962 insertions(+), 5 deletions(-) create mode 120000 packages/cli/.eslintignore create mode 100644 packages/cli/.gitignore create mode 100644 packages/cli/README.md create mode 100755 packages/cli/bin/cosmwasm-cli create mode 100755 packages/cli/jasmine-testrunner.js create mode 100644 packages/cli/nonces/README.txt create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/async.spec.ts create mode 100644 packages/cli/src/async.ts create mode 100644 packages/cli/src/cli.ts create mode 100644 packages/cli/src/helpers.spec.ts create mode 100644 packages/cli/src/helpers.ts create mode 100644 packages/cli/src/tsrepl.spec.ts create mode 100644 packages/cli/src/tsrepl.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsconfig_repl.json create mode 100644 packages/cli/tslint.json diff --git a/NOTICE b/NOTICE index 216009de..bfe81681 100644 --- a/NOTICE +++ b/NOTICE @@ -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 diff --git a/packages/cli/.eslintignore b/packages/cli/.eslintignore new file mode 120000 index 00000000..86039baf --- /dev/null +++ b/packages/cli/.eslintignore @@ -0,0 +1 @@ +../../.eslintignore \ No newline at end of file diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 00000000..2f7cb8a7 --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,5 @@ +build/ +dist/ +docs/ + +selftest_userprofile_db/ diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..474ba32c --- /dev/null +++ b/packages/cli/README.md @@ -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)). diff --git a/packages/cli/bin/cosmwasm-cli b/packages/cli/bin/cosmwasm-cli new file mode 100755 index 00000000..6ff19d7f --- /dev/null +++ b/packages/cli/bin/cosmwasm-cli @@ -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)); diff --git a/packages/cli/jasmine-testrunner.js b/packages/cli/jasmine-testrunner.js new file mode 100755 index 00000000..9fada59b --- /dev/null +++ b/packages/cli/jasmine-testrunner.js @@ -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(); diff --git a/packages/cli/nonces/README.txt b/packages/cli/nonces/README.txt new file mode 100644 index 00000000..092fe732 --- /dev/null +++ b/packages/cli/nonces/README.txt @@ -0,0 +1 @@ +Directory used to trigger lerna package updates for all packages diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..e4cc5bdd --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,51 @@ +{ + "name": "@iov/cli", + "version": "0.0.3", + "description": "Command line interface", + "contributors": ["IOV SAS ", "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" + } +} diff --git a/packages/cli/src/async.spec.ts b/packages/cli/src/async.spec.ts new file mode 100644 index 00000000..f972526d --- /dev/null +++ b/packages/cli/src/async.spec.ts @@ -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+}\)\(\)/); + }); +}); diff --git a/packages/cli/src/async.ts b/packages/cli/src/async.ts new file mode 100644 index 00000000..290b2f4b --- /dev/null +++ b/packages/cli/src/async.ts @@ -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; +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 00000000..9782a670 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -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([ + ["@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); + }); +} diff --git a/packages/cli/src/helpers.spec.ts b/packages/cli/src/helpers.spec.ts new file mode 100644 index 00000000..a3e4bf9b --- /dev/null +++ b/packages/cli/src/helpers.spec.ts @@ -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"); + }); + }); +}); diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts new file mode 100644 index 00000000..d0c59d4d --- /dev/null +++ b/packages/cli/src/helpers.ts @@ -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 { + 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 = 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)); +} diff --git a/packages/cli/src/tsrepl.spec.ts b/packages/cli/src/tsrepl.spec.ts new file mode 100644 index 00000000..23540372 --- /dev/null +++ b/packages/cli/src/tsrepl.spec.ts @@ -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(); + } + }); +}); diff --git a/packages/cli/src/tsrepl.ts b/packages/cli/src/tsrepl.ts new file mode 100644 index 00000000..4e66fe9d --- /dev/null +++ b/packages/cli/src/tsrepl.ts @@ -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 { + /** + * 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 => { + 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 => { + 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 { + 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 { + // 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; + } +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..df66add1 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/cli/tsconfig_repl.json b/packages/cli/tsconfig_repl.json new file mode 100644 index 00000000..4c72f447 --- /dev/null +++ b/packages/cli/tsconfig_repl.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "es2017", + "noUnusedLocals": false, + "noImplicitAny": false + } +} diff --git a/packages/cli/tslint.json b/packages/cli/tslint.json new file mode 100644 index 00000000..0946f209 --- /dev/null +++ b/packages/cli/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tslint.json" +} diff --git a/yarn.lock b/yarn.lock index 6eec216f..62011ff7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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= From 84a97ad65575a69ba49e1875916c72edfa5f95da Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 17:45:01 +0100 Subject: [PATCH 2/9] Add missing dependencies --- packages/cli/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/package.json b/packages/cli/package.json index e4cc5bdd..d4e78f73 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,9 @@ ], "dependencies": { "@cosmwasm/sdk": "^0.0.3", + "@iov/crypto": "^2.0.0-alpha.7", + "@iov/encoding": "^2.0.0-alpha.7", + "@iov/keycontrol": "^2.0.0-alpha.7", "argparse": "^1.0.10", "babylon": "^6.18.0", "colors": "^1.3.3", From 6e5ce2d8f754c1124e307af29ea4738856de4b38 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 15:16:51 +0100 Subject: [PATCH 3/9] Upgrade diff --- packages/cli/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index d4e78f73..0fa37348 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,7 +40,7 @@ "argparse": "^1.0.10", "babylon": "^6.18.0", "colors": "^1.3.3", - "diff": "^3.5.0", + "diff": "^4", "leveldown": "^5.0.0", "recast": "^0.18.0", "ts-node": "^7.0.0", diff --git a/yarn.lock b/yarn.lock index 62011ff7..dc1d1206 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2821,12 +2821,12 @@ 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: +diff@^3.1.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: +diff@^4, diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== From 6c6177eec4910bc06096a4e1b87c05475288c261 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 15:19:08 +0100 Subject: [PATCH 4/9] Upgrade ts-node --- packages/cli/package.json | 2 +- yarn.lock | 39 ++++++++++++++++++--------------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0fa37348..29c06126 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,7 +43,7 @@ "diff": "^4", "leveldown": "^5.0.0", "recast": "^0.18.0", - "ts-node": "^7.0.0", + "ts-node": "^8", "typescript": "~3.7" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index dc1d1206..12ca8412 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1523,6 +1523,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1599,7 +1604,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.0, arrify@^1.0.1: +arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= @@ -1950,7 +1955,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.1.0: +buffer-from@^1.0.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== @@ -2821,11 +2826,6 @@ di@^0.0.1: resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= -diff@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4, diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -7570,19 +7570,16 @@ 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== +ts-node@^8: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" + integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg== dependencies: - arrify "^1.0.0" - buffer-from "^1.1.0" - diff "^3.1.0" + arg "^4.1.0" + diff "^4.0.1" make-error "^1.1.1" - minimist "^1.2.0" - mkdirp "^0.5.1" source-map-support "^0.5.6" - yn "^2.0.0" + yn "3.1.1" tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" @@ -8186,7 +8183,7 @@ ylru@^1.2.0: 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= +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From e9907026ed729bd8019411c55dc7929e0901fc1f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 17:45:52 +0100 Subject: [PATCH 5/9] Add sleep to selftest --- packages/cli/package.json | 1 + packages/cli/src/cli.ts | 7 ++++++- yarn.lock | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 29c06126..e1648ccc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ "@iov/crypto": "^2.0.0-alpha.7", "@iov/encoding": "^2.0.0-alpha.7", "@iov/keycontrol": "^2.0.0-alpha.7", + "@iov/utils": "^2.0.0-alpha.7", "argparse": "^1.0.10", "babylon": "^6.18.0", "colors": "^1.3.3", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 9782a670..5aba2625 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -76,6 +76,7 @@ export function main(originalArgs: readonly string[]): void { "WalletSerializationString", ], ], + ["@iov/utils", ["sleep"]], ]); console.info(colors.green("Initializing session for you. Have fun!")); @@ -109,11 +110,15 @@ export function main(originalArgs: readonly string[]): void { // 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`; + // helper functions + init += ` + const { toAscii, fromHex, toHex } = Encoding; + `; if (args.selftest) { // execute some trival stuff and exit init += ` + await sleep(123); const hash = new Sha512(new Uint8Array([])).digest(); const hexHash = toHex(hash); export class NewDummyClass {}; diff --git a/yarn.lock b/yarn.lock index 12ca8412..b93ac1be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -157,6 +157,11 @@ dependencies: xstream "^11.10.0" +"@iov/utils@^2.0.0-alpha.7": + version "2.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/@iov/utils/-/utils-2.0.0-alpha.7.tgz#af9aebcb9e221e53cf62c5511f007c65d95f5c0c" + integrity sha512-myWATqmlCFZ/jSrsBQlyn+6D2ViAscZVAqBAw38kbIfv3TiKTjWb2RqhlNDnlVgt3uD0ZtQFyY9hkRMCwfIk+w== + "@koa/cors@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.0.0.tgz#df021b4df2dadf1e2b04d7c8ddf93ba2d42519cb" From e3d8da119b7765b56184c8b65b463c211d021862 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 17:46:11 +0100 Subject: [PATCH 6/9] Rename script to "yarn selftest" --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index e1648ccc..575934a3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,8 +17,8 @@ "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && tslint -t verbose --project .", "build": "tsc", "build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build", + "selftest": "yarn build-or-skip && ./bin/cosmwasm-cli --selftest", "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": { From ec961ca34f37b709eb68dc0a9b63602fad45d36a Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 15:27:32 +0100 Subject: [PATCH 7/9] Use modern import style for babylon --- packages/cli/src/async.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/async.ts b/packages/cli/src/async.ts index 290b2f4b..c9d80a74 100644 --- a/packages/cli/src/async.ts +++ b/packages/cli/src/async.ts @@ -1,7 +1,6 @@ +import * as babylon from "babylon"; import * as recast from "recast"; -import babylon = require("babylon"); - export function wrapInAsyncFunction(code: string): string { const codeInAsyncFunction = `(async () => { ${code} From cf083e3fb06291346173a1b89bc2e3dc61b0e7b2 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 15:44:36 +0100 Subject: [PATCH 8/9] Run CLI selftest in CI --- .circleci/config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0227d097..b12f1baa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,6 +88,12 @@ jobs: COSMOS_ENABLED: 1 SKIP_BUILD: 1 command: yarn test + - run: + name: Run CLI selftest + working_directory: packages/cli + environment: + SKIP_BUILD: 1 + command: yarn selftest - run: command: ./scripts/cosm/stop.sh lint: From bc87f8e87adc070c77855421e7970215c19919d7 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 6 Feb 2020 17:46:40 +0100 Subject: [PATCH 9/9] Add some code snippets --- .eslintignore | 1 + packages/cli/README.md | 38 +++++++++++++++++++++++++-- packages/cli/examples/local_faucet.ts | 21 +++++++++++++++ packages/cli/package.json | 1 + packages/cli/src/cli.ts | 34 +++++++++++++++++++++++- 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 packages/cli/examples/local_faucet.ts diff --git a/.eslintignore b/.eslintignore index f373a53f..6942c0df 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,5 +4,6 @@ build/ custom_types/ dist/ docs/ +examples/ generated/ types/ diff --git a/packages/cli/README.md b/packages/cli/README.md index 474ba32c..40da3cca 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -39,8 +39,42 @@ $ 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 +2. Start a local wasmd blockchain +3. Start with `./bin/cosmwasm-cli --init examples/local_faucet.ts` +4. Play around as in the following example code + +```ts +// Get account information +const account = (await client.authAccounts(faucetAddress)).result.value; + +// Craft a send transaction +const emptyAddress = Bech32.encode("cosmos", Random.getBytes(20)); +const memo = "My first contract on chain"; +const sendTokensMsg: types.MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: faucetAddress, + to_address: emptyAddress, + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, +}; + +const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account) as SignableBytes; +const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); +const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); +const signedTx: types.StdTx = { + msg: [sendTokensMsg], + fee: defaultFee, + memo: memo, + signatures: [signature], + } +const postResult = await client.postTx(marshalTx(signedTx)); +``` ## License diff --git a/packages/cli/examples/local_faucet.ts b/packages/cli/examples/local_faucet.ts new file mode 100644 index 00000000..2b6c6522 --- /dev/null +++ b/packages/cli/examples/local_faucet.ts @@ -0,0 +1,21 @@ +const defaultHttpUrl = "http://localhost:1317"; +const defaultNetworkId = "testing"; +const defaultFee: types.StdFee = { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "890000", +}; + +const faucetMnemonic = + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone"; +const faucetPath = HdPaths.cosmos(0); +const faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"; + +const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); +const signer = await wallet.createIdentity("unused_value" as ChainId, faucetPath); + +const client = new RestClient(defaultHttpUrl); diff --git a/packages/cli/package.json b/packages/cli/package.json index 575934a3..826a6326 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,7 @@ ], "dependencies": { "@cosmwasm/sdk": "^0.0.3", + "@iov/bcp": "^2.0.0-alpha.7", "@iov/crypto": "^2.0.0-alpha.7", "@iov/encoding": "^2.0.0-alpha.7", "@iov/keycontrol": "^2.0.0-alpha.7", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 5aba2625..57ecd587 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,6 +1,7 @@ import { ArgumentParser } from "argparse"; // tslint:disable-next-line:no-submodule-imports import colors = require("colors/safe"); +import * as fs from "fs"; import { join } from "path"; import { TsRepl } from "./tsrepl"; @@ -11,6 +12,10 @@ export function main(originalArgs: readonly string[]): void { action: "storeTrue", help: "Print version and exit", }); + parser.addArgument("--init", { + metavar: "FILEPATH", + help: "Read initial TypeScript code from file", + }); const maintainerGroup = parser.addArgumentGroup({ title: "Maintainer options", @@ -33,7 +38,30 @@ export function main(originalArgs: readonly string[]): void { } const imports = new Map([ - ["@cosmwasm/sdk", ["types", "RestClient"]], + ["@cosmwasm/sdk", ["encodeSecp256k1Signature", "makeSignBytes", "marshalTx", "types", "RestClient"]], + [ + "@iov/bcp", + [ + "Address", + "Algorithm", + "ChainId", + "Nonce", + "PrehashType", + "PubkeyBytes", + "SendTransaction", + "SignableBytes", + "TokenTicker", + "TransactionId", + // block info + "BlockInfoPending", + "BlockInfoSucceeded", + "BlockInfoFailed", + "BlockInfo", + "isBlockInfoPending", + "isBlockInfoSucceeded", + "isBlockInfoFailed", + ], + ], [ "@iov/crypto", [ @@ -134,6 +162,10 @@ export function main(originalArgs: readonly string[]): void { `; } + if (args.init) { + init += fs.readFileSync(args.init, "utf8") + "\n"; + } + const tsconfigPath = join(__dirname, "..", "tsconfig_repl.json"); const installationDir = join(__dirname, ".."); new TsRepl(tsconfigPath, init, !!args.debug, installationDir).start().catch(error => {