Add @cosmwasm/cli
This commit is contained in:
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.eslintignore
|
||||
@@ -0,0 +1,5 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
|
||||
selftest_userprofile_db/
|
||||
@@ -0,0 +1,50 @@
|
||||
# @cosmwasm/cli
|
||||
|
||||
[](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)).
|
||||
Executable
+6
@@ -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));
|
||||
Executable
+26
@@ -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();
|
||||
@@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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+}\)\(\)/);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2017",
|
||||
"noUnusedLocals": false,
|
||||
"noImplicitAny": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tslint.json"
|
||||
}
|
||||
Reference in New Issue
Block a user