diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index f4a8fdd935..bef5b7f874 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -143,6 +143,36 @@ describe('CliRepl', () => { } expect.fail('expected error'); }); + + context('loading JS files from disk', () => { + it('allows loading a file from the disk', async() => { + const filenameA = path.resolve(__dirname, '..', 'test', 'fixtures', 'load', 'a.js'); + input.write(`load(${JSON.stringify(filenameA)})\n`); + await waitEval(cliRepl.bus); + input.write('variableFromA\n'); + await waitEval(cliRepl.bus); + expect(output).to.include('yes from A'); + }); + + it('allows nested loading', async() => { + const filenameB = path.resolve(__dirname, '..', 'test', 'fixtures', 'load', 'b.js'); + input.write(`load(${JSON.stringify(filenameB)})\n`); + await waitEval(cliRepl.bus); + input.write('variableFromA + " " + variableFromB\n'); + await waitEval(cliRepl.bus); + expect(output).to.include('yes from A yes from A from B'); + }); + + it('allows async operations', async() => { + const filenameC = path.resolve(__dirname, '..', 'test', 'fixtures', 'load', 'c.js'); + input.write(`load(${JSON.stringify(filenameC)})\n`); + await waitEval(cliRepl.bus); + output = ''; + input.write('diff >= 50\n'); + await waitEval(cliRepl.bus); + expect(output).to.include('true'); + }); + }); }); context('during startup', () => { diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index 22b8ea7d2e..113876ac2f 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -16,7 +16,8 @@ import MongoshNodeRepl, { MongoshNodeReplOptions } from './mongosh-repl'; import setupLoggerAndTelemetry from './setup-logger-and-telemetry'; import { MongoshBus, UserConfig } from '@mongosh/types'; import { once } from 'events'; -import { createWriteStream } from 'fs'; +import { createWriteStream, promises as fs } from 'fs'; +import path from 'path'; import { promisify } from 'util'; /** @@ -89,7 +90,7 @@ class CliRepl { terminal: process.env.MONGOSH_FORCE_TERMINAL ? true : undefined, }, bus: this.bus, - configProvider: this + ioProvider: this }); } @@ -286,6 +287,14 @@ class CliRepl { throw error; } + async readFileUTF8(filename: string): Promise<{ contents: string, absolutePath: string }> { + const resolved = path.resolve(filename); + return { + contents: await fs.readFile(resolved, 'utf8'), + absolutePath: resolved + }; + } + clr(text: string, style: StyleDefinition): string { return this.mongoshRepl.clr(text, style); } diff --git a/packages/cli-repl/src/mongosh-repl.spec.ts b/packages/cli-repl/src/mongosh-repl.spec.ts index b92f243c36..22612d7bb1 100644 --- a/packages/cli-repl/src/mongosh-repl.spec.ts +++ b/packages/cli-repl/src/mongosh-repl.spec.ts @@ -8,7 +8,7 @@ import { Duplex, PassThrough } from 'stream'; import { StubbedInstance, stubInterface } from 'ts-sinon'; import { promisify } from 'util'; import { expect, fakeTTYProps, tick, useTmpdir, waitEval } from '../test/repl-helpers'; -import MongoshNodeRepl, { MongoshConfigProvider, MongoshNodeReplOptions } from './mongosh-repl'; +import MongoshNodeRepl, { MongoshIOProvider, MongoshNodeReplOptions } from './mongosh-repl'; import stripAnsi from 'strip-ansi'; const delay = promisify(setTimeout); @@ -24,7 +24,7 @@ describe('MongoshNodeRepl', () => { let outputStream: Duplex; let output = ''; let bus: EventEmitter; - let configProvider: MongoshConfigProvider; + let ioProvider: MongoshIOProvider; let sp: StubbedInstance; let serviceProvider: ServiceProvider; let config: Record; @@ -38,13 +38,13 @@ describe('MongoshNodeRepl', () => { bus = new EventEmitter(); config = {}; - const cp = stubInterface(); + const cp = stubInterface(); cp.getHistoryFilePath.returns(path.join(tmpdir.path, 'history')); cp.getConfig.callsFake(async(key: string) => config[key]); cp.setConfig.callsFake(async(key: string, value: any) => { config[key] = value; }); cp.exit.callsFake(((code) => bus.emit('test-exit-event', code)) as any); - configProvider = cp; + ioProvider = cp; sp = stubInterface(); sp.bsonLibrary = bson; @@ -64,7 +64,7 @@ describe('MongoshNodeRepl', () => { input: input, output: outputStream, bus: bus, - configProvider: configProvider + ioProvider: ioProvider }; mongoshRepl = new MongoshNodeRepl(mongoshReplOptions); }); @@ -146,10 +146,10 @@ describe('MongoshNodeRepl', () => { it('forwards telemetry config requests', async() => { input.write('disableTelemetry()\n'); await waitEval(bus); - expect(configProvider.setConfig).to.have.been.calledWith('enableTelemetry', false); + expect(ioProvider.setConfig).to.have.been.calledWith('enableTelemetry', false); input.write('enableTelemetry()\n'); await waitEval(bus); - expect(configProvider.setConfig).to.have.been.calledWith('enableTelemetry', true); + expect(ioProvider.setConfig).to.have.been.calledWith('enableTelemetry', true); }); it('makes .clear just display the prompt again', async() => { diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index a72daa958b..a5f50d7679 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -9,6 +9,7 @@ import type { MongoshBus, UserConfig } from '@mongosh/types'; import askpassword from 'askpassword'; import { Console } from 'console'; import { once } from 'events'; +import path from 'path'; import prettyRepl from 'pretty-repl'; import { ReplOptions, REPLServer } from 'repl'; import type { Readable, Writable } from 'stream'; @@ -24,18 +25,19 @@ export type MongoshCliOptions = ShellCliOptions & { redactInfo?: boolean; }; -export type MongoshConfigProvider = { +export type MongoshIOProvider = { getHistoryFilePath(): string; getConfig(key: K): Promise; setConfig(key: K, value: UserConfig[K]): Promise; exit(code: number): Promise; + readFileUTF8(filename: string): Promise<{ contents: string, absolutePath: string }>; }; export type MongoshNodeReplOptions = { input: Readable; output: Writable; bus: MongoshBus; - configProvider: MongoshConfigProvider; + ioProvider: MongoshIOProvider; shellCliOptions?: Partial; nodeReplOptions?: Partial; }; @@ -63,7 +65,7 @@ class MongoshNodeRepl implements EvaluationListener { bus: MongoshBus; nodeReplOptions: Partial; shellCliOptions: Partial; - configProvider: MongoshConfigProvider; + ioProvider: MongoshIOProvider; onClearCommand?: EvaluationListener['onClearCommand']; insideAutoComplete: boolean; @@ -74,7 +76,7 @@ class MongoshNodeRepl implements EvaluationListener { this.bus = options.bus; this.nodeReplOptions = options.nodeReplOptions || {}; this.shellCliOptions = options.shellCliOptions || {}; - this.configProvider = options.configProvider; + this.ioProvider = options.ioProvider; this.insideAutoComplete = false; this._runtimeState = null; } @@ -170,7 +172,7 @@ class MongoshNodeRepl implements EvaluationListener { // https://github.com/nodejs/node/issues/36773 (repl as Mutable).line = ''; - const historyFile = this.configProvider.getHistoryFilePath(); + const historyFile = this.ioProvider.getHistoryFilePath(); const { redactInfo } = this.shellCliOptions; try { await promisify(repl.setupHistory).call(repl, historyFile); @@ -245,9 +247,9 @@ class MongoshNodeRepl implements EvaluationListener { text += `Using MongoDB: ${mongodVersion}\n`; text += `${this.clr('Using Mongosh Beta', ['bold', 'yellow'])}: ${version}\n`; text += `${MONGOSH_WIKI}\n`; - if (!await this.configProvider.getConfig('disableGreetingMessage')) { + if (!await this.ioProvider.getConfig('disableGreetingMessage')) { text += `${TELEMETRY_GREETING_MESSAGE}\n`; - await this.configProvider.setConfig('disableGreetingMessage', true); + await this.ioProvider.setConfig('disableGreetingMessage', true); } this.output.write(text); } @@ -316,6 +318,29 @@ class MongoshNodeRepl implements EvaluationListener { } } + async onLoad(filename: string): Promise { + const repl = this.runtimeState().repl; + const { + contents, + absolutePath + } = await this.ioProvider.readFileUTF8(filename); + + const previousFilename = repl.context.__filename; + repl.context.__filename = absolutePath; + repl.context.__dirname = path.dirname(absolutePath); + try { + await promisify(repl.eval.bind(repl))(contents, repl.context, filename); + } finally { + if (previousFilename) { + repl.context.__filename = previousFilename; + repl.context.__dirname = path.dirname(previousFilename); + } else { + delete repl.context.__filename; + delete repl.context.__dirname; + } + } + } + /** * Format the result to a string so it can be written to the output stream. */ @@ -344,7 +369,7 @@ class MongoshNodeRepl implements EvaluationListener { } async toggleTelemetry(enabled: boolean): Promise { - await this.configProvider.setConfig('enableTelemetry', enabled); + await this.ioProvider.setConfig('enableTelemetry', enabled); if (enabled) { return i18n.__('cli-repl.cli-repl.enabledTelemetry'); @@ -410,7 +435,7 @@ class MongoshNodeRepl implements EvaluationListener { async onExit(): Promise { await this.close(); - return this.configProvider.exit(0); + return this.ioProvider.exit(0); } private async getShellPrompt(internalState: ShellInternalState): Promise { diff --git a/packages/cli-repl/test/fixtures/load/a.js b/packages/cli-repl/test/fixtures/load/a.js new file mode 100644 index 0000000000..4ced1d63b1 --- /dev/null +++ b/packages/cli-repl/test/fixtures/load/a.js @@ -0,0 +1,3 @@ +/* eslint-disable */ +let variableFromA = 'yes from A'; +print('Hi!') diff --git a/packages/cli-repl/test/fixtures/load/b.js b/packages/cli-repl/test/fixtures/load/b.js new file mode 100644 index 0000000000..f20a1c2b03 --- /dev/null +++ b/packages/cli-repl/test/fixtures/load/b.js @@ -0,0 +1,3 @@ +/* eslint-disable */ +load(__dirname + '/a.js'); +let variableFromB = variableFromA + ' from B'; diff --git a/packages/cli-repl/test/fixtures/load/c.js b/packages/cli-repl/test/fixtures/load/c.js new file mode 100644 index 0000000000..352f094a91 --- /dev/null +++ b/packages/cli-repl/test/fixtures/load/c.js @@ -0,0 +1,4 @@ +/* eslint-disable */ +const start = new Date().getTime(); +sleep(100); +const diff = new Date().getTime() - start; diff --git a/packages/node-runtime-worker-thread/src/child-process-evaluation-listener.ts b/packages/node-runtime-worker-thread/src/child-process-evaluation-listener.ts index c09d2b461c..ebb687208d 100644 --- a/packages/node-runtime-worker-thread/src/child-process-evaluation-listener.ts +++ b/packages/node-runtime-worker-thread/src/child-process-evaluation-listener.ts @@ -4,7 +4,7 @@ import type { WorkerRuntime } from './index'; import { RuntimeEvaluationListener } from '@mongosh/browser-runtime-core'; export class ChildProcessEvaluationListener { - exposedListener: Exposed>; + exposedListener: Exposed>>; constructor(workerRuntime: WorkerRuntime, childProcess: ChildProcess) { this.exposedListener = exposeAll( diff --git a/packages/shell-api/src/shell-api.spec.ts b/packages/shell-api/src/shell-api.spec.ts index 6964cbe3d2..4de56d7a9f 100644 --- a/packages/shell-api/src/shell-api.spec.ts +++ b/packages/shell-api/src/shell-api.spec.ts @@ -566,6 +566,13 @@ describe('ShellApi', () => { expect(evaluationListener.onClearCommand).to.have.been.calledWith(); }); }); + describe('load', () => { + it('asks the evaluation listener to load a file', async() => { + evaluationListener.onLoad.resolves(); + await internalState.context.load('abc.js'); + expect(evaluationListener.onLoad).to.have.been.calledWith('abc.js'); + }); + }); for (const cmd of ['print', 'printjson']) { // eslint-disable-next-line no-loop-func describe(cmd, () => { diff --git a/packages/shell-api/src/shell-api.ts b/packages/shell-api/src/shell-api.ts index b766ba681a..6df21d5593 100644 --- a/packages/shell-api/src/shell-api.ts +++ b/packages/shell-api/src/shell-api.ts @@ -100,12 +100,17 @@ export default class ShellApi extends ShellApiClass { return version; } - load(): void { - throw new MongoshUnimplementedError( - 'load is not currently implemented. If you are running mongosh from the CLI ' + - 'then you can use .load as an alternative.', - CommonErrors.NotImplemented - ); + @returnsPromise + async load(filename: string): Promise { + assertArgsDefined(filename); + if (!this.internalState.evaluationListener.onLoad) { + throw new MongoshUnimplementedError( + 'load is not currently implemented for this platform', + CommonErrors.NotImplemented + ); + } + await this.internalState.evaluationListener.onLoad(filename); + return true; } @returnsPromise diff --git a/packages/shell-api/src/shell-internal-state.ts b/packages/shell-api/src/shell-internal-state.ts index 990cbb354e..c7ef94a1bc 100644 --- a/packages/shell-api/src/shell-internal-state.ts +++ b/packages/shell-api/src/shell-internal-state.ts @@ -62,6 +62,11 @@ export interface EvaluationListener { * Called when exit/quit is entered in the shell. */ onExit?: () => Promise; + + /** + * Called when load() is used in the shell. + */ + onLoad?: (filename: string) => Promise; } /**