From eb501f4a189d5458e1e2ca81e02ed2234e1c9fcc Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Jul 2021 14:46:45 +0200 Subject: [PATCH 1/3] fix(cli-repl): return original evaluation value to REPL MONGOSH-902 Instead of returning the ShellResult interpretation of the evaluation result back to the Node.js REPL, return the original raw value and keep the ShellResult stored separately (which still needs to happen during `eval`, as the formatting itself is a synchronous operation). This ensures that `_` will always refer to the actual value of the last evaluation result. --- packages/cli-repl/src/format-output.ts | 2 +- packages/cli-repl/src/mongosh-repl.spec.ts | 30 ++++++++++++++++++++++ packages/cli-repl/src/mongosh-repl.ts | 30 +++++++++++++--------- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/cli-repl/src/format-output.ts b/packages/cli-repl/src/format-output.ts index 1083cf7790..cc2c6f4362 100644 --- a/packages/cli-repl/src/format-output.ts +++ b/packages/cli-repl/src/format-output.ts @@ -10,7 +10,7 @@ import { HelpProperties, CollectionNamesWithTypes } from '@mongosh/shell-api'; type EvaluationResult = { value: any; - type?: string; + type?: string | null; }; type FormatOptions = { diff --git a/packages/cli-repl/src/mongosh-repl.spec.ts b/packages/cli-repl/src/mongosh-repl.spec.ts index 4fc001ee0f..6ff5689583 100644 --- a/packages/cli-repl/src/mongosh-repl.spec.ts +++ b/packages/cli-repl/src/mongosh-repl.spec.ts @@ -239,6 +239,36 @@ describe('MongoshNodeRepl', () => { } expect(output).not.to.include('!this should not run!'); }); + + it('_ returns the last result', async() => { + input.write('42\n'); + await waitEval(bus); + output = ''; + + input.write('_\n'); + await waitEval(bus); + expect(output).to.include('42'); + }); + + it('_ can be used like the last result in expressions', async() => { + input.write('({ foo: "bar", baz: "quux" });\n'); + await waitEval(bus); + output = ''; + + input.write('JSON.stringify(_)\n'); + await waitEval(bus); + expect(output).to.include('{"foo":"bar","baz":"quux"}'); + }); + + it('_error yields the last exception', async() => { + input.write('throw new Error("blah")\n'); + await waitEval(bus); + output = ''; + + input.write('_error.message\n'); + await waitEval(bus); + expect(output).to.include('blah'); + }); }); context('with terminal: true', () => { diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index ac6f25fc8c..e38fc39b0d 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -2,7 +2,7 @@ import completer from '@mongosh/autocomplete'; import { MongoshCommandFailed, MongoshInternalError, MongoshWarning } from '@mongosh/errors'; import { changeHistory } from '@mongosh/history'; import type { AutoEncryptionOptions, ServiceProvider } from '@mongosh/service-provider-core'; -import { EvaluationListener, OnLoadResult, ShellCliOptions, ShellInternalState } from '@mongosh/shell-api'; +import { EvaluationListener, OnLoadResult, ShellCliOptions, ShellInternalState, getShellApiType, toShellResult } from '@mongosh/shell-api'; import { ShellEvaluator, ShellResult } from '@mongosh/shell-evaluator'; import { CliUserConfig, ConfigProvider, CliUserConfigValidator, MongoshBus } from '@mongosh/types'; import askcharacter from 'askcharacter'; @@ -46,7 +46,7 @@ export type MongoshNodeReplOptions = { export type InitializationToken = { __initialized: 'yes' }; type MongoshRuntimeState = { - shellEvaluator: ShellEvaluator; + shellEvaluator: ShellEvaluator; internalState: ShellInternalState; repl: REPLServer; console: Console; @@ -111,6 +111,7 @@ class MongoshNodeRepl implements EvaluationListener { showStackTraces = false; loadNestingLevel = 0; redactHistory: 'keep' | 'remove' | 'remove-redact' = 'remove'; + rawValueToShellResult: WeakMap = new WeakMap(); constructor(options: MongoshNodeReplOptions) { this.input = options.input; @@ -130,7 +131,7 @@ class MongoshNodeRepl implements EvaluationListener { async initialize(serviceProvider: ServiceProvider): Promise { const internalState = new ShellInternalState(serviceProvider, this.bus, this.shellCliOptions); - const shellEvaluator = new ShellEvaluator(internalState); + const shellEvaluator = new ShellEvaluator(internalState, (value: any) => value); internalState.setEvaluationListener(this); await internalState.fetchConnectionInfo(); @@ -398,6 +399,7 @@ class MongoshNodeRepl implements EvaluationListener { this.output.write(text); } + // eslint-disable-next-line complexity async eval(originalEval: asyncRepl.OriginalEvalFunction, input: string, context: any, filename: string): Promise { if (!this.insideAutoCompleteOrGetPrompt) { this.lineByLineInput.enableBlockOnNewLine(); @@ -407,9 +409,9 @@ class MongoshNodeRepl implements EvaluationListener { let interrupted = false; try { - const shellResult = await shellEvaluator.customEval(originalEval, input, context, filename); - if (!this.insideAutoCompleteOrGetPrompt) { - return shellResult; + const rawValue = await shellEvaluator.customEval(originalEval, input, context, filename); + if (typeof rawValue === 'object' && rawValue !== null) { + this.rawValueToShellResult.set(rawValue, await toShellResult(rawValue)); } // The Node.js auto completion needs to access the raw values in order // to be able to autocomplete their properties properly. One catch is @@ -417,11 +419,11 @@ class MongoshNodeRepl implements EvaluationListener { // topology, server version, etc., so for those, we only autocomplete // own, enumerable, non-underscore-prefixed properties and instead leave // the rest to the @mongosh/autocomplete package. - if (shellResult.type === null) { - return shellResult.rawValue; + if (!this.insideAutoCompleteOrGetPrompt || getShellApiType(rawValue) === null) { + return rawValue; } return Object.fromEntries( - Object.entries(shellResult.rawValue) + Object.entries(rawValue) .filter(([key]) => !key.startsWith('_'))); } catch (err) { if (this.runtimeState().internalState.interrupted.isSet()) { @@ -525,7 +527,7 @@ class MongoshNodeRepl implements EvaluationListener { /** * Format the result to a string so it can be written to the output stream. */ - writer(result: any /* Error | ShellResult */): string { + writer(result: any): string { // This checks for error instances. // The writer gets called immediately by the internal `repl.eval` // in case of errors. @@ -540,11 +542,15 @@ class MongoshNodeRepl implements EvaluationListener { return this.formatError(output); } + return this.formatShellResult(this.rawValueToShellResult.get(result) ?? { type: null, printable: result }); + } + + formatShellResult(result: { type: null | string, printable: any }): string { return this.formatOutput({ type: result.type, value: result.printable }); } onPrint(values: ShellResult[]): void { - const joined = values.map((value) => this.writer(value)).join(' '); + const joined = values.map((value) => this.formatShellResult(value)).join(' '); this.output.write(joined + '\n'); } @@ -579,7 +585,7 @@ class MongoshNodeRepl implements EvaluationListener { throw new Error(`Unrecognized prompt type ${type}`); } - formatOutput(value: { value: any, type?: string }): string { + formatOutput(value: { value: any, type?: string | null }): string { return formatOutput(value, this.getFormatOptions()); } From becd11c90b707368960e47e5001313c8144ac3d8 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Jul 2021 15:59:12 +0200 Subject: [PATCH 2/3] fixup --- packages/cli-repl/src/mongosh-repl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index e38fc39b0d..547bc323bc 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -410,7 +410,7 @@ class MongoshNodeRepl implements EvaluationListener { try { const rawValue = await shellEvaluator.customEval(originalEval, input, context, filename); - if (typeof rawValue === 'object' && rawValue !== null) { + if ((typeof rawValue === 'object' && rawValue !== null) || typeof rawValue === 'function') { this.rawValueToShellResult.set(rawValue, await toShellResult(rawValue)); } // The Node.js auto completion needs to access the raw values in order From 8d2f5587e6c55228779f5d4c4d5ca70b299bc2d5 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Jul 2021 17:40:07 +0200 Subject: [PATCH 3/3] fixup --- packages/cli-repl/src/mongosh-repl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 547bc323bc..d70e612a6a 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -705,7 +705,7 @@ class MongoshNodeRepl implements EvaluationListener { function isErrorLike(value: any): boolean { try { - return value && ( + return value && getShellApiType(value) === null && ( (value.message !== undefined && typeof value.stack === 'string') || (value.code !== undefined && value.errmsg !== undefined) );