repl.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.setupContext = exports.createEvalAwarePartialHost = exports.EvalState = exports.createRepl = exports.REPL_NAME = exports.REPL_FILENAME = exports.STDIN_NAME = exports.STDIN_FILENAME = exports.EVAL_NAME = exports.EVAL_FILENAME = void 0;
  4. const os_1 = require("os");
  5. const path_1 = require("path");
  6. const repl_1 = require("repl");
  7. const vm_1 = require("vm");
  8. const index_1 = require("./index");
  9. const fs_1 = require("fs");
  10. const console_1 = require("console");
  11. const assert = require("assert");
  12. const module_1 = require("module");
  13. // Lazy-loaded.
  14. let _processTopLevelAwait;
  15. function getProcessTopLevelAwait() {
  16. if (_processTopLevelAwait === undefined) {
  17. ({
  18. processTopLevelAwait: _processTopLevelAwait,
  19. } = require('../dist-raw/node-internal-repl-await'));
  20. }
  21. return _processTopLevelAwait;
  22. }
  23. let diff;
  24. function getDiffLines() {
  25. if (diff === undefined) {
  26. diff = require('diff');
  27. }
  28. return diff.diffLines;
  29. }
  30. /** @internal */
  31. exports.EVAL_FILENAME = `[eval].ts`;
  32. /** @internal */
  33. exports.EVAL_NAME = `[eval]`;
  34. /** @internal */
  35. exports.STDIN_FILENAME = `[stdin].ts`;
  36. /** @internal */
  37. exports.STDIN_NAME = `[stdin]`;
  38. /** @internal */
  39. exports.REPL_FILENAME = '<repl>.ts';
  40. /** @internal */
  41. exports.REPL_NAME = '<repl>';
  42. /**
  43. * Create a ts-node REPL instance.
  44. *
  45. * Pay close attention to the example below. Today, the API requires a few lines
  46. * of boilerplate to correctly bind the `ReplService` to the ts-node `Service` and
  47. * vice-versa.
  48. *
  49. * Usage example:
  50. *
  51. * const repl = tsNode.createRepl();
  52. * const service = tsNode.create({...repl.evalAwarePartialHost});
  53. * repl.setService(service);
  54. * repl.start();
  55. *
  56. * @category REPL
  57. */
  58. function createRepl(options = {}) {
  59. var _a, _b, _c, _d, _e;
  60. const { ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl = true } = options;
  61. let service = options.service;
  62. let nodeReplServer;
  63. // If `useGlobal` is not true, then REPL creates a context when started.
  64. // This stores a reference to it or to `global`, whichever is used, after REPL has started.
  65. let context;
  66. const state = (_a = options.state) !== null && _a !== void 0 ? _a : new EvalState((0, path_1.join)(process.cwd(), exports.REPL_FILENAME));
  67. const evalAwarePartialHost = createEvalAwarePartialHost(state, options.composeWithEvalAwarePartialHost);
  68. const stdin = (_b = options.stdin) !== null && _b !== void 0 ? _b : process.stdin;
  69. const stdout = (_c = options.stdout) !== null && _c !== void 0 ? _c : process.stdout;
  70. const stderr = (_d = options.stderr) !== null && _d !== void 0 ? _d : process.stderr;
  71. const _console = stdout === process.stdout && stderr === process.stderr
  72. ? console
  73. : new console_1.Console(stdout, stderr);
  74. const replService = {
  75. state: (_e = options.state) !== null && _e !== void 0 ? _e : new EvalState((0, path_1.join)(process.cwd(), exports.EVAL_FILENAME)),
  76. setService,
  77. evalCode,
  78. evalCodeInternal,
  79. nodeEval,
  80. evalAwarePartialHost,
  81. start,
  82. startInternal,
  83. stdin,
  84. stdout,
  85. stderr,
  86. console: _console,
  87. };
  88. return replService;
  89. function setService(_service) {
  90. service = _service;
  91. if (ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl) {
  92. service.addDiagnosticFilter({
  93. appliesToAllFiles: false,
  94. filenamesAbsolute: [state.path],
  95. diagnosticsIgnored: [
  96. 2393,
  97. 6133,
  98. 7027,
  99. ...(service.shouldReplAwait ? topLevelAwaitDiagnosticCodes : []),
  100. ],
  101. });
  102. }
  103. }
  104. function evalCode(code) {
  105. const result = appendCompileAndEvalInput({
  106. service: service,
  107. state,
  108. input: code,
  109. context,
  110. overrideIsCompletion: false,
  111. });
  112. assert(result.containsTopLevelAwait === false);
  113. return result.value;
  114. }
  115. function evalCodeInternal(options) {
  116. const { code, enableTopLevelAwait, context } = options;
  117. return appendCompileAndEvalInput({
  118. service: service,
  119. state,
  120. input: code,
  121. enableTopLevelAwait,
  122. context,
  123. });
  124. }
  125. function nodeEval(code, context, _filename, callback) {
  126. // TODO: Figure out how to handle completion here.
  127. if (code === '.scope') {
  128. callback(null);
  129. return;
  130. }
  131. try {
  132. const evalResult = evalCodeInternal({
  133. code,
  134. enableTopLevelAwait: true,
  135. context,
  136. });
  137. if (evalResult.containsTopLevelAwait) {
  138. (async () => {
  139. try {
  140. callback(null, await evalResult.valuePromise);
  141. }
  142. catch (promiseError) {
  143. handleError(promiseError);
  144. }
  145. })();
  146. }
  147. else {
  148. callback(null, evalResult.value);
  149. }
  150. }
  151. catch (error) {
  152. handleError(error);
  153. }
  154. // Log TSErrors, check if they're recoverable, log helpful hints for certain
  155. // well-known errors, and invoke `callback()`
  156. // TODO should evalCode API get the same error-handling benefits?
  157. function handleError(error) {
  158. var _a, _b;
  159. // Don't show TLA hint if the user explicitly disabled repl top level await
  160. const canLogTopLevelAwaitHint = service.options.experimentalReplAwait !== false &&
  161. !service.shouldReplAwait;
  162. if (error instanceof index_1.TSError) {
  163. // Support recoverable compilations using >= node 6.
  164. if (repl_1.Recoverable && isRecoverable(error)) {
  165. callback(new repl_1.Recoverable(error));
  166. return;
  167. }
  168. else {
  169. _console.error(error);
  170. if (canLogTopLevelAwaitHint &&
  171. error.diagnosticCodes.some((dC) => topLevelAwaitDiagnosticCodes.includes(dC))) {
  172. _console.error(getTopLevelAwaitHint());
  173. }
  174. callback(null);
  175. }
  176. }
  177. else {
  178. let _error = error;
  179. if (canLogTopLevelAwaitHint &&
  180. _error instanceof SyntaxError &&
  181. ((_a = _error.message) === null || _a === void 0 ? void 0 : _a.includes('await is only valid'))) {
  182. try {
  183. // Only way I know to make our hint appear after the error
  184. _error.message += `\n\n${getTopLevelAwaitHint()}`;
  185. _error.stack = (_b = _error.stack) === null || _b === void 0 ? void 0 : _b.replace(/(SyntaxError:.*)/, (_, $1) => `${$1}\n\n${getTopLevelAwaitHint()}`);
  186. }
  187. catch { }
  188. }
  189. callback(_error);
  190. }
  191. }
  192. function getTopLevelAwaitHint() {
  193. return `Hint: REPL top-level await requires TypeScript version 3.8 or higher and target ES2018 or higher. You are using TypeScript ${service.ts.version} and target ${service.ts.ScriptTarget[service.config.options.target]}.`;
  194. }
  195. }
  196. // Note: `code` argument is deprecated
  197. function start(code) {
  198. startInternal({ code });
  199. }
  200. // Note: `code` argument is deprecated
  201. function startInternal(options) {
  202. const { code, forceToBeModule = true, ...optionsOverride } = options !== null && options !== void 0 ? options : {};
  203. // TODO assert that `service` is set; remove all `service!` non-null assertions
  204. // Eval incoming code before the REPL starts.
  205. // Note: deprecated
  206. if (code) {
  207. try {
  208. evalCode(`${code}\n`);
  209. }
  210. catch (err) {
  211. _console.error(err);
  212. // Note: should not be killing the process here, but this codepath is deprecated anyway
  213. process.exit(1);
  214. }
  215. }
  216. // In case the typescript compiler hasn't compiled anything yet,
  217. // make it run though compilation at least one time before
  218. // the REPL starts for a snappier user experience on startup.
  219. service === null || service === void 0 ? void 0 : service.compile('', state.path);
  220. const repl = (0, repl_1.start)({
  221. prompt: '> ',
  222. input: replService.stdin,
  223. output: replService.stdout,
  224. // Mimicking node's REPL implementation: https://github.com/nodejs/node/blob/168b22ba073ee1cbf8d0bcb4ded7ff3099335d04/lib/internal/repl.js#L28-L30
  225. terminal: stdout.isTTY &&
  226. !parseInt(index_1.env.NODE_NO_READLINE, 10),
  227. eval: nodeEval,
  228. useGlobal: true,
  229. ...optionsOverride,
  230. });
  231. nodeReplServer = repl;
  232. context = repl.context;
  233. // Bookmark the point where we should reset the REPL state.
  234. const resetEval = appendToEvalState(state, '');
  235. function reset() {
  236. resetEval();
  237. // Hard fix for TypeScript forcing `Object.defineProperty(exports, ...)`.
  238. runInContext('exports = module.exports', state.path, context);
  239. if (forceToBeModule) {
  240. state.input += 'export {};void 0;\n';
  241. }
  242. // Declare node builtins.
  243. // Skip the same builtins as `addBuiltinLibsToObject`:
  244. // those starting with _
  245. // those containing /
  246. // those that already exist as globals
  247. // Intentionally suppress type errors in case @types/node does not declare any of them, and because
  248. // `declare import` is technically invalid syntax.
  249. // Avoid this when in transpileOnly, because third-party transpilers may not handle `declare import`.
  250. if (!(service === null || service === void 0 ? void 0 : service.transpileOnly)) {
  251. state.input += `// @ts-ignore\n${module_1.builtinModules
  252. .filter((name) => !name.startsWith('_') &&
  253. !name.includes('/') &&
  254. !['console', 'module', 'process'].includes(name))
  255. .map((name) => `declare import ${name} = require('${name}')`)
  256. .join(';')}\n`;
  257. }
  258. }
  259. reset();
  260. repl.on('reset', reset);
  261. repl.defineCommand('type', {
  262. help: 'Check the type of a TypeScript identifier',
  263. action: function (identifier) {
  264. if (!identifier) {
  265. repl.displayPrompt();
  266. return;
  267. }
  268. const undo = appendToEvalState(state, identifier);
  269. const { name, comment } = service.getTypeInfo(state.input, state.path, state.input.length);
  270. undo();
  271. if (name)
  272. repl.outputStream.write(`${name}\n`);
  273. if (comment)
  274. repl.outputStream.write(`${comment}\n`);
  275. repl.displayPrompt();
  276. },
  277. });
  278. // Set up REPL history when available natively via node.js >= 11.
  279. if (repl.setupHistory) {
  280. const historyPath = index_1.env.TS_NODE_HISTORY || (0, path_1.join)((0, os_1.homedir)(), '.ts_node_repl_history');
  281. repl.setupHistory(historyPath, (err) => {
  282. if (!err)
  283. return;
  284. _console.error(err);
  285. process.exit(1);
  286. });
  287. }
  288. return repl;
  289. }
  290. }
  291. exports.createRepl = createRepl;
  292. /**
  293. * Eval state management. Stores virtual `[eval].ts` file
  294. */
  295. class EvalState {
  296. constructor(path) {
  297. this.path = path;
  298. /** @internal */
  299. this.input = '';
  300. /** @internal */
  301. this.output = '';
  302. /** @internal */
  303. this.version = 0;
  304. /** @internal */
  305. this.lines = 0;
  306. }
  307. }
  308. exports.EvalState = EvalState;
  309. function createEvalAwarePartialHost(state, composeWith) {
  310. function readFile(path) {
  311. if (path === state.path)
  312. return state.input;
  313. if (composeWith === null || composeWith === void 0 ? void 0 : composeWith.readFile)
  314. return composeWith.readFile(path);
  315. try {
  316. return (0, fs_1.readFileSync)(path, 'utf8');
  317. }
  318. catch (err) {
  319. /* Ignore. */
  320. }
  321. }
  322. function fileExists(path) {
  323. if (path === state.path)
  324. return true;
  325. if (composeWith === null || composeWith === void 0 ? void 0 : composeWith.fileExists)
  326. return composeWith.fileExists(path);
  327. try {
  328. const stats = (0, fs_1.statSync)(path);
  329. return stats.isFile() || stats.isFIFO();
  330. }
  331. catch (err) {
  332. return false;
  333. }
  334. }
  335. return { readFile, fileExists };
  336. }
  337. exports.createEvalAwarePartialHost = createEvalAwarePartialHost;
  338. const sourcemapCommentRe = /\/\/# ?sourceMappingURL=\S+[\s\r\n]*$/;
  339. /**
  340. * Evaluate the code snippet.
  341. *
  342. * Append it to virtual .ts file, compile, handle compiler errors, compute a diff of the JS, and eval any code that
  343. * appears as "added" in the diff.
  344. */
  345. function appendCompileAndEvalInput(options) {
  346. const { service, state, wrappedErr, enableTopLevelAwait = false, context, overrideIsCompletion, } = options;
  347. let { input } = options;
  348. // It's confusing for `{ a: 1 }` to be interpreted as a block statement
  349. // rather than an object literal. So, we first try to wrap it in
  350. // parentheses, so that it will be interpreted as an expression.
  351. // Based on https://github.com/nodejs/node/blob/c2e6822153bad023ab7ebd30a6117dcc049e475c/lib/repl.js#L413-L422
  352. let wrappedCmd = false;
  353. if (!wrappedErr && /^\s*{/.test(input) && !/;\s*$/.test(input)) {
  354. input = `(${input.trim()})\n`;
  355. wrappedCmd = true;
  356. }
  357. const lines = state.lines;
  358. const isCompletion = overrideIsCompletion !== null && overrideIsCompletion !== void 0 ? overrideIsCompletion : !/\n$/.test(input);
  359. const undo = appendToEvalState(state, input);
  360. let output;
  361. // Based on https://github.com/nodejs/node/blob/92573721c7cff104ccb82b6ed3e8aa69c4b27510/lib/repl.js#L457-L461
  362. function adjustUseStrict(code) {
  363. // "void 0" keeps the repl from returning "use strict" as the result
  364. // value for statements and declarations that don't return a value.
  365. return code.replace(/^"use strict";/, '"use strict"; void 0;');
  366. }
  367. try {
  368. output = service.compile(state.input, state.path, -lines);
  369. }
  370. catch (err) {
  371. undo();
  372. if (wrappedCmd) {
  373. if (err instanceof index_1.TSError && err.diagnosticCodes[0] === 2339) {
  374. // Ensure consistent and more sane behavior between { a: 1 }['b'] and ({ a: 1 }['b'])
  375. throw err;
  376. }
  377. // Unwrap and try again
  378. return appendCompileAndEvalInput({
  379. ...options,
  380. wrappedErr: err,
  381. });
  382. }
  383. if (wrappedErr)
  384. throw wrappedErr;
  385. throw err;
  386. }
  387. output = adjustUseStrict(output);
  388. // Note: REPL does not respect sourcemaps!
  389. // To properly do that, we'd need to prefix the code we eval -- which comes
  390. // from `diffLines` -- with newlines so that it's at the proper line numbers.
  391. // Then we'd need to ensure each bit of eval-ed code, if there are multiples,
  392. // has the sourcemap appended to it.
  393. // We might also need to integrate with our sourcemap hooks' cache; I'm not sure.
  394. const outputWithoutSourcemapComment = output.replace(sourcemapCommentRe, '');
  395. const oldOutputWithoutSourcemapComment = state.output.replace(sourcemapCommentRe, '');
  396. // Use `diff` to check for new JavaScript to execute.
  397. const changes = getDiffLines()(oldOutputWithoutSourcemapComment, outputWithoutSourcemapComment);
  398. if (isCompletion) {
  399. undo();
  400. }
  401. else {
  402. state.output = output;
  403. // Insert a semicolon to make sure that the code doesn't interact with the next line,
  404. // for example to prevent `2\n+ 2` from producing 4.
  405. // This is safe since the output will not change since we can only get here with successful inputs,
  406. // and adding a semicolon to the end of a successful input won't ever change the output.
  407. state.input = state.input.replace(/([^\n\s])([\n\s]*)$/, (all, lastChar, whitespace) => {
  408. if (lastChar !== ';')
  409. return `${lastChar};${whitespace}`;
  410. return all;
  411. });
  412. }
  413. let commands = [];
  414. let containsTopLevelAwait = false;
  415. // Build a list of "commands": bits of JS code in the diff that must be executed.
  416. for (const change of changes) {
  417. if (change.added) {
  418. if (enableTopLevelAwait &&
  419. service.shouldReplAwait &&
  420. change.value.indexOf('await') > -1) {
  421. const processTopLevelAwait = getProcessTopLevelAwait();
  422. // Newline prevents comments to mess with wrapper
  423. const wrappedResult = processTopLevelAwait(change.value + '\n');
  424. if (wrappedResult !== null) {
  425. containsTopLevelAwait = true;
  426. commands.push({
  427. mustAwait: true,
  428. execCommand: () => runInContext(wrappedResult, state.path, context),
  429. });
  430. continue;
  431. }
  432. }
  433. commands.push({
  434. execCommand: () => runInContext(change.value, state.path, context),
  435. });
  436. }
  437. }
  438. // Execute all commands asynchronously if necessary, returning the result or a
  439. // promise of the result.
  440. if (containsTopLevelAwait) {
  441. return {
  442. containsTopLevelAwait,
  443. valuePromise: (async () => {
  444. let value;
  445. for (const command of commands) {
  446. const r = command.execCommand();
  447. value = command.mustAwait ? await r : r;
  448. }
  449. return value;
  450. })(),
  451. };
  452. }
  453. else {
  454. return {
  455. containsTopLevelAwait: false,
  456. value: commands.reduce((_, c) => c.execCommand(), undefined),
  457. };
  458. }
  459. }
  460. /**
  461. * Low-level execution of JS code in context
  462. */
  463. function runInContext(code, filename, context) {
  464. const script = new vm_1.Script(code, { filename });
  465. if (context === undefined || context === global) {
  466. return script.runInThisContext();
  467. }
  468. else {
  469. return script.runInContext(context);
  470. }
  471. }
  472. /**
  473. * Append to the eval instance and return an undo function.
  474. */
  475. function appendToEvalState(state, input) {
  476. const undoInput = state.input;
  477. const undoVersion = state.version;
  478. const undoOutput = state.output;
  479. const undoLines = state.lines;
  480. state.input += input;
  481. state.lines += lineCount(input);
  482. state.version++;
  483. return function () {
  484. state.input = undoInput;
  485. state.output = undoOutput;
  486. state.version = undoVersion;
  487. state.lines = undoLines;
  488. };
  489. }
  490. /**
  491. * Count the number of lines.
  492. */
  493. function lineCount(value) {
  494. let count = 0;
  495. for (const char of value) {
  496. if (char === '\n') {
  497. count++;
  498. }
  499. }
  500. return count;
  501. }
  502. /**
  503. * TS diagnostic codes which are recoverable, meaning that the user likely entered an incomplete line of code
  504. * and should be prompted for the next. For example, starting a multi-line for() loop and not finishing it.
  505. * null value means code is always recoverable. `Set` means code is only recoverable when occurring alongside at least one
  506. * of the other codes.
  507. */
  508. const RECOVERY_CODES = new Map([
  509. [1003, null],
  510. [1005, null],
  511. [1109, null],
  512. [1126, null],
  513. [
  514. 1136,
  515. new Set([1005]), // happens when typing out an object literal or block scope across multiple lines: '{ foo: 123,'
  516. ],
  517. [1160, null],
  518. [1161, null],
  519. [2355, null],
  520. [2391, null],
  521. [
  522. 7010,
  523. new Set([1005]), // happens when fn signature spread across multiple lines: 'function a(\nb: any\n) {'
  524. ],
  525. ]);
  526. /**
  527. * Diagnostic codes raised when using top-level await.
  528. * These are suppressed when top-level await is enabled.
  529. * When it is *not* enabled, these trigger a helpful hint about enabling top-level await.
  530. */
  531. const topLevelAwaitDiagnosticCodes = [
  532. 1375,
  533. 1378,
  534. 1431,
  535. 1432, // Top-level 'for await' loops are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.
  536. ];
  537. /**
  538. * Check if a function can recover gracefully.
  539. */
  540. function isRecoverable(error) {
  541. return error.diagnosticCodes.every((code) => {
  542. const deps = RECOVERY_CODES.get(code);
  543. return (deps === null ||
  544. (deps && error.diagnosticCodes.some((code) => deps.has(code))));
  545. });
  546. }
  547. /**
  548. * @internal
  549. * Set properties on `context` before eval-ing [stdin] or [eval] input.
  550. */
  551. function setupContext(context, module, filenameAndDirname) {
  552. if (filenameAndDirname) {
  553. context.__dirname = '.';
  554. context.__filename = `[${filenameAndDirname}]`;
  555. }
  556. context.module = module;
  557. context.exports = module.exports;
  558. context.require = module.require.bind(module);
  559. }
  560. exports.setupContext = setupContext;
  561. //# sourceMappingURL=repl.js.map