Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/cli-repl/src/cli-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
13 changes: 11 additions & 2 deletions packages/cli-repl/src/cli-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -89,7 +90,7 @@ class CliRepl {
terminal: process.env.MONGOSH_FORCE_TERMINAL ? true : undefined,
},
bus: this.bus,
configProvider: this
ioProvider: this
});
}

Expand Down Expand Up @@ -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);
}
Expand Down
14 changes: 7 additions & 7 deletions packages/cli-repl/src/mongosh-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -24,7 +24,7 @@ describe('MongoshNodeRepl', () => {
let outputStream: Duplex;
let output = '';
let bus: EventEmitter;
let configProvider: MongoshConfigProvider;
let ioProvider: MongoshIOProvider;
let sp: StubbedInstance<ServiceProvider>;
let serviceProvider: ServiceProvider;
let config: Record<string, any>;
Expand All @@ -38,13 +38,13 @@ describe('MongoshNodeRepl', () => {
bus = new EventEmitter();

config = {};
const cp = stubInterface<MongoshConfigProvider>();
const cp = stubInterface<MongoshIOProvider>();
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<ServiceProvider>();
sp.bsonLibrary = bson;
Expand All @@ -64,7 +64,7 @@ describe('MongoshNodeRepl', () => {
input: input,
output: outputStream,
bus: bus,
configProvider: configProvider
ioProvider: ioProvider
};
mongoshRepl = new MongoshNodeRepl(mongoshReplOptions);
});
Expand Down Expand Up @@ -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() => {
Expand Down
43 changes: 34 additions & 9 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,18 +25,19 @@ export type MongoshCliOptions = ShellCliOptions & {
redactInfo?: boolean;
};

export type MongoshConfigProvider = {
export type MongoshIOProvider = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a non-important random observation: this type is getting to be a bit of a jack of all trades, did we consider having multiple interfaces that ties together only the methods that are naturally coupled?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so... we could definitely split it up, but I feel like this might be one of those things where it's okay to wait until we have a reason to do so (e.g. if we ever make MongoshRepl a public API, which seems possible)

No strong feelings though :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, me neither, keeping it like that is super fine!

getHistoryFilePath(): string;
getConfig<K extends keyof UserConfig>(key: K): Promise<UserConfig[K]>;
setConfig<K extends keyof UserConfig>(key: K, value: UserConfig[K]): Promise<void>;
exit(code: number): Promise<never>;
readFileUTF8(filename: string): Promise<{ contents: string, absolutePath: string }>;
};

export type MongoshNodeReplOptions = {
input: Readable;
output: Writable;
bus: MongoshBus;
configProvider: MongoshConfigProvider;
ioProvider: MongoshIOProvider;
shellCliOptions?: Partial<MongoshCliOptions>;
nodeReplOptions?: Partial<ReplOptions>;
};
Expand Down Expand Up @@ -63,7 +65,7 @@ class MongoshNodeRepl implements EvaluationListener {
bus: MongoshBus;
nodeReplOptions: Partial<ReplOptions>;
shellCliOptions: Partial<MongoshCliOptions>;
configProvider: MongoshConfigProvider;
ioProvider: MongoshIOProvider;
onClearCommand?: EvaluationListener['onClearCommand'];
insideAutoComplete: boolean;

Expand All @@ -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;
}
Expand Down Expand Up @@ -170,7 +172,7 @@ class MongoshNodeRepl implements EvaluationListener {
// https://github.com/nodejs/node/issues/36773
(repl as Mutable<typeof repl>).line = '';

const historyFile = this.configProvider.getHistoryFilePath();
const historyFile = this.ioProvider.getHistoryFilePath();
const { redactInfo } = this.shellCliOptions;
try {
await promisify(repl.setupHistory).call(repl, historyFile);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -316,6 +318,29 @@ class MongoshNodeRepl implements EvaluationListener {
}
}

async onLoad(filename: string): Promise<void> {
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.
*/
Expand Down Expand Up @@ -344,7 +369,7 @@ class MongoshNodeRepl implements EvaluationListener {
}

async toggleTelemetry(enabled: boolean): Promise<string> {
await this.configProvider.setConfig('enableTelemetry', enabled);
await this.ioProvider.setConfig('enableTelemetry', enabled);

if (enabled) {
return i18n.__('cli-repl.cli-repl.enabledTelemetry');
Expand Down Expand Up @@ -410,7 +435,7 @@ class MongoshNodeRepl implements EvaluationListener {

async onExit(): Promise<never> {
await this.close();
return this.configProvider.exit(0);
return this.ioProvider.exit(0);
}

private async getShellPrompt(internalState: ShellInternalState): Promise<string> {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-repl/test/fixtures/load/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable */
let variableFromA = 'yes from A';
print('Hi!')
3 changes: 3 additions & 0 deletions packages/cli-repl/test/fixtures/load/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable */
load(__dirname + '/a.js');
let variableFromB = variableFromA + ' from B';
4 changes: 4 additions & 0 deletions packages/cli-repl/test/fixtures/load/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable */
const start = new Date().getTime();
sleep(100);
const diff = new Date().getTime() - start;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { WorkerRuntime } from './index';
import { RuntimeEvaluationListener } from '@mongosh/browser-runtime-core';

export class ChildProcessEvaluationListener {
exposedListener: Exposed<Required<RuntimeEvaluationListener>>;
exposedListener: Exposed<Required<Omit<RuntimeEvaluationListener, 'onLoad'>>>;

constructor(workerRuntime: WorkerRuntime, childProcess: ChildProcess) {
this.exposedListener = exposeAll(
Expand Down
7 changes: 7 additions & 0 deletions packages/shell-api/src/shell-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down
17 changes: 11 additions & 6 deletions packages/shell-api/src/shell-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <filename> as an alternative.',
CommonErrors.NotImplemented
);
@returnsPromise
async load(filename: string): Promise<true> {
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
Expand Down
5 changes: 5 additions & 0 deletions packages/shell-api/src/shell-internal-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface EvaluationListener {
* Called when exit/quit is entered in the shell.
*/
onExit?: () => Promise<never>;

/**
* Called when load() is used in the shell.
*/
onLoad?: (filename: string) => Promise<void>;
}

/**
Expand Down