UNPKG

7.95 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const diff_1 = require("diff");
4const path_1 = require("path");
5const repl_1 = require("repl");
6const ts_node_1 = require("ts-node");
7const vm_1 = require("vm");
8const helpers_1 = require("./helpers");
9class TsRepl {
10 constructor(tsconfigPath, initialTypeScript, debuggingEnabled = false, installationDir) {
11 this.evalFilename = `[eval].ts`;
12 this.evalData = { input: "", output: "" };
13 this.typeScriptService = ts_node_1.register({
14 project: tsconfigPath,
15 ignoreDiagnostics: [
16 "1308",
17 ],
18 });
19 this.debuggingEnabled = debuggingEnabled;
20 this.resetToZero = this.appendTypeScriptInput("");
21 this.initialTypeScript = initialTypeScript;
22 this.evalPath = path_1.join(installationDir || process.cwd(), this.evalFilename);
23 }
24 async start() {
25 /**
26 * A wrapper around replEval used to match the method signature
27 * for "Custom Evaluation Functions"
28 * https://nodejs.org/api/repl.html#repl_custom_evaluation_functions
29 */
30 const replEvalWrapper = async (code, _context, _filename, callback) => {
31 const result = await this.replEval(code);
32 callback(result.error, result.result);
33 };
34 const repl = repl_1.start({
35 prompt: ">> ",
36 input: process.stdin,
37 output: process.stdout,
38 terminal: process.stdout.isTTY,
39 eval: replEvalWrapper,
40 useGlobal: false,
41 });
42 // Prepare context for TypeScript: TypeScript compiler expects the exports shortcut
43 // to exist in `Object.defineProperty(exports, "__esModule", { value: true });`
44 const unsafeReplContext = repl.context;
45 if (!unsafeReplContext.exports) {
46 // tslint:disable-next-line:no-object-mutation
47 unsafeReplContext.exports = unsafeReplContext.module.exports;
48 }
49 // REPL context is created with a default set of module resolution paths,
50 // like for example
51 // [ '/home/me/repl/node_modules',
52 // '/home/me/node_modules',
53 // '/home/node_modules',
54 // '/node_modules',
55 // '/home/me/.node_modules',
56 // '/home/me/.node_libraries',
57 // '/usr/lib/nodejs' ]
58 // However, this does not include the installation path of @iov/cli because
59 // REPL does not inherit module paths from the current process. Thus we override
60 // the repl paths with the current process' paths
61 // tslint:disable-next-line:no-object-mutation
62 unsafeReplContext.module.paths = module.paths;
63 // tslint:disable-next-line:no-object-mutation
64 this.context = vm_1.createContext(repl.context);
65 const reset = async () => {
66 this.resetToZero();
67 // Ensure code ends with "\n" due to implementation of replEval
68 await this.compileAndExecute(this.initialTypeScript + "\n", false);
69 };
70 await reset();
71 repl.on("reset", reset);
72 repl.defineCommand("type", {
73 help: "Check the type of a TypeScript identifier",
74 action: (identifier) => {
75 if (!identifier) {
76 repl.displayPrompt();
77 return;
78 }
79 const identifierTypeScriptCode = `${identifier}\n`;
80 const undo = this.appendTypeScriptInput(identifierTypeScriptCode);
81 const identifierFirstPosition = this.evalData.input.length - identifierTypeScriptCode.length;
82 const { name, comment } = this.typeScriptService.getTypeInfo(this.evalData.input, this.evalPath, identifierFirstPosition);
83 undo();
84 repl.outputStream.write(`${name}\n${comment ? `${comment}\n` : ""}`);
85 repl.displayPrompt();
86 },
87 });
88 return repl;
89 }
90 async compileAndExecute(tsInput, isAutocompletionRequest) {
91 if (!isAutocompletionRequest) {
92 // Expect POSIX lines (https://stackoverflow.com/a/729795)
93 if (tsInput.length > 0 && !tsInput.endsWith("\n")) {
94 throw new Error("final newline missing");
95 }
96 }
97 const undo = this.appendTypeScriptInput(tsInput);
98 let output;
99 try {
100 // lineOffset unused at the moment (https://github.com/TypeStrong/ts-node/issues/661)
101 output = this.typeScriptService.compile(this.evalData.input, this.evalPath);
102 }
103 catch (err) {
104 undo();
105 throw err;
106 }
107 // Use `diff` to check for new JavaScript to execute.
108 const changes = diff_1.diffLines(this.evalData.output, output);
109 if (isAutocompletionRequest) {
110 undo();
111 }
112 else {
113 // tslint:disable-next-line:no-object-mutation
114 this.evalData.output = output;
115 }
116 // Execute new JavaScript. This may not necessarily be at the end only because e.g. an import
117 // statement in TypeScript is compiled to no JavaScript until the imported symbol is used
118 // somewhere. This btw. leads to a different execution order of imports than in the TS source.
119 let lastResult;
120 for (const added of changes.filter((change) => change.added)) {
121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
122 lastResult = await helpers_1.executeJavaScriptAsync(added.value, this.evalFilename, this.context);
123 }
124 return lastResult;
125 }
126 /**
127 * Add user-friendly error handling around compileAndExecute
128 */
129 async replEval(code) {
130 // TODO: Figure out how to handle completion here.
131 if (code === ".scope") {
132 return {
133 result: undefined,
134 error: null,
135 };
136 }
137 const isAutocompletionRequest = !/\n$/.test(code);
138 try {
139 const result = await this.compileAndExecute(code, isAutocompletionRequest);
140 return {
141 result: result,
142 error: null,
143 };
144 }
145 catch (error) {
146 if (this.debuggingEnabled) {
147 console.info("Current REPL TypeScript program:");
148 console.info(this.evalData.input);
149 }
150 let outError;
151 if (error instanceof ts_node_1.TSError) {
152 // Support recoverable compilations using >= node 6.
153 if (repl_1.Recoverable && helpers_1.isRecoverable(error)) {
154 outError = new repl_1.Recoverable(error);
155 }
156 else {
157 console.error(error.diagnosticText);
158 outError = null;
159 }
160 }
161 else {
162 outError = error;
163 }
164 return {
165 result: undefined,
166 error: outError,
167 };
168 }
169 }
170 appendTypeScriptInput(input) {
171 const oldInput = this.evalData.input;
172 const oldOutput = this.evalData.output;
173 // Handle ASI issues with TypeScript re-evaluation.
174 if (oldInput.charAt(oldInput.length - 1) === "\n" && /^\s*[[(`]/.test(input) && !/;\s*$/.test(oldInput)) {
175 // tslint:disable-next-line:no-object-mutation
176 this.evalData.input = `${this.evalData.input.slice(0, -1)};\n`;
177 }
178 // tslint:disable-next-line:no-object-mutation
179 this.evalData.input += input;
180 const undoFunction = () => {
181 // tslint:disable-next-line:no-object-mutation
182 this.evalData.input = oldInput;
183 // tslint:disable-next-line:no-object-mutation
184 this.evalData.output = oldOutput;
185 };
186 return undoFunction;
187 }
188}
189exports.TsRepl = TsRepl;
190//# sourceMappingURL=tsrepl.js.map
\No newline at end of file