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
2 changes: 1 addition & 1 deletion packages/cli-repl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ CLI interface for [MongoDB Shell][mongosh], an extension to Node.js REPL with Mo
--version Show version information
--shell Run the shell after executing files
--nodb Don't connect to mongod on startup - no 'db address' [arg] expected
--norc Will not run the '.mongorc.js' file on start up
--norc Will not run the '.mongoshrc.js' file on start up
--eval [arg] Evaluate javascript
--retryWrites Automatically retry write operations upon transient network errors

Expand Down
68 changes: 68 additions & 0 deletions packages/cli-repl/src/cli-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('CliRepl', () => {
shellHomePaths: {
shellRoamingDataPath: tmpdir.path,
shellLocalDataPath: tmpdir.path,
shellRcPath: tmpdir.path,
},
onExit: (code: number) => {
exitCode = code;
Expand Down Expand Up @@ -233,6 +234,73 @@ describe('CliRepl', () => {
process.env.MONGOSH_SKIP_NODE_VERSION_CHECK = origVersionCheckEnvVar || '';
}
});

context('mongoshrc', () => {
it('loads .mongoshrc if it is present', async() => {
await fs.writeFile(path.join(tmpdir.path, '.mongoshrc.js'), 'print("hi from mongoshrc")');
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).to.include('hi from mongoshrc');
});

it('does not load .mongoshrc if --norc is passed', async() => {
await fs.writeFile(path.join(tmpdir.path, '.mongoshrc.js'), 'print("hi from mongoshrc")');
cliReplOptions.shellCliOptions.norc = true;
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).not.to.include('hi from mongoshrc');
});

it('warns if .mongorc.js is present but not .mongoshrc.js', async() => {
await fs.writeFile(path.join(tmpdir.path, '.mongorc.js'), 'print("hi from mongorc")');
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).to.include('Found ~/.mongorc.js, but not ~/.mongoshrc.js. ~/.mongorc.js will not be loaded.');
expect(output).to.include('You may want to copy or rename ~/.mongorc.js to ~/.mongoshrc.js.');
expect(output).not.to.include('hi from mongorc');
});

it('warns if .mongoshrc is present but not .mongoshrc.js', async() => {
await fs.writeFile(path.join(tmpdir.path, '.mongoshrc'), 'print("hi from misspelled")');
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).to.include('Found ~/.mongoshrc, but not ~/.mongoshrc.js.');
expect(output).not.to.include('hi from misspelled');
});

it('does not warn with --quiet if .mongorc.js is present but not .mongoshrc.js', async() => {
await fs.writeFile(path.join(tmpdir.path, '.mongorc.js'), 'print("hi from mongorc")');
cliReplOptions.shellCliOptions.quiet = true;
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).not.to.include('Found ~/.mongorc.js, but not ~/.mongoshrc.js');
expect(output).not.to.include('hi from mongorc');
});

it('does not warn with --quiet if .mongoshrc is present but not .mongoshrc.js', async() => {
await fs.writeFile(path.join(tmpdir.path, '.mongoshrc'), 'print("hi from misspelled")');
cliReplOptions.shellCliOptions.quiet = true;
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).not.to.include('Found ~/.mongoshrc, but not ~/.mongoshrc.js');
expect(output).not.to.include('hi from misspelled');
});

it('loads .mongoshrc recursively if wanted', async() => {
const rcPath = path.join(tmpdir.path, '.mongoshrc.js');
await fs.writeFile(
rcPath,
`globalThis.a = (globalThis.a + 1 || 0);
if (a === 5) {
print('reached five');
} else {
load(JSON.stringify(${rcPath})
}`);
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start('', {});
expect(output).to.include('reached five');
});
});
});

verifyAutocompletion({
Expand Down
55 changes: 54 additions & 1 deletion packages/cli-repl/src/cli-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,60 @@ class CliRepl {
}

const initialServiceProvider = await this.connect(driverUri, driverOptions);
await this.mongoshRepl.start(initialServiceProvider);
const initialized = await this.mongoshRepl.initialize(initialServiceProvider);
await this.loadRcFiles();
await this.mongoshRepl.startRepl(initialized);
}

async loadRcFiles(): Promise<void> {
if (this.cliOptions.norc) {
return;
}
const legacyPath = this.shellHomeDirectory.rcPath('.mongorc.js');
const mongoshrcPath = this.shellHomeDirectory.rcPath('.mongoshrc.js');
const mongoshrcMisspelledPath = this.shellHomeDirectory.rcPath('.mongoshrc');

let hasMongoshRc = false;
try {
await fs.stat(mongoshrcPath);
hasMongoshRc = true;
} catch { /* file not present */ }
if (hasMongoshRc) {
try {
await this.mongoshRepl.loadExternalFile(mongoshrcPath);
} catch (err) {
this.output.write(this.mongoshRepl.writer(err) + '\n');
}
return;
}

if (this.cliOptions.quiet) {
return;
}

let hasLegacyRc = false;
try {
await fs.stat(legacyPath);
hasLegacyRc = true;
} catch { /* file not present */ }
if (hasLegacyRc) {
const msg =
'Warning: Found ~/.mongorc.js, but not ~/.mongoshrc.js. ~/.mongorc.js will not be loaded.\n' +
' You may want to copy or rename ~/.mongorc.js to ~/.mongoshrc.js.\n';
this.output.write(this.clr(msg, ['bold', 'yellow']));
return;
}

let hasMisspelledFilename = false;
try {
await fs.stat(mongoshrcMisspelledPath);
hasMisspelledFilename = true;
} catch { /* file not present */ }
if (hasMisspelledFilename) {
const msg =
'Warning: Found ~/.mongoshrc, but not ~/.mongoshrc.js. Did you forget to add .js?\n';
this.output.write(this.clr(msg, ['bold', 'yellow']));
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-repl/src/config-directory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ describe('home directory management', () => {
base = path.resolve(__dirname, '..', '..', '..', 'tmp', 'test', `${Date.now()}`, `${Math.random()}`);
shellHomeDirectory = new ShellHomeDirectory({
shellRoamingDataPath: base,
shellLocalDataPath: base
shellLocalDataPath: base,
shellRcPath: base
});
manager = new ConfigManager(shellHomeDirectory);
manager.on('error', onError = sinon.spy());
Expand Down
8 changes: 7 additions & 1 deletion packages/cli-repl/src/config-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EventEmitter } from 'events';
export type ShellHomePaths = {
shellRoamingDataPath: string;
shellLocalDataPath: string;
shellRcPath: string;
};

export class ShellHomeDirectory {
Expand All @@ -31,6 +32,10 @@ export class ShellHomeDirectory {
localPath(subpath: string): string {
return path.join(this.paths.shellLocalDataPath, subpath);
}

rcPath(subpath: string): string {
return path.join(this.paths.shellRcPath, subpath);
}
}

export class ConfigManager<Config> extends EventEmitter {
Expand Down Expand Up @@ -118,6 +123,7 @@ export function getStoragePaths(): ShellHomePaths {
shellRoamingDataPath ??= homedir;
return {
shellLocalDataPath,
shellRoamingDataPath
shellRoamingDataPath,
shellRcPath: os.homedir()
};
}
1 change: 1 addition & 0 deletions packages/cli-repl/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const USAGE = `
--port [arg] ${i18n.__('cli-repl.args.port')}
--version ${i18n.__('cli-repl.args.version')}
--nodb ${i18n.__('cli-repl.args.nodb')}
--norc ${i18n.__('cli-repl.args.norc')}
--retryWrites ${i18n.__('cli-repl.args.retryWrites')}

${clr(i18n.__('cli-repl.args.authenticationOptions'), ['bold', 'yellow'])}
Expand Down
31 changes: 18 additions & 13 deletions packages/cli-repl/src/mongosh-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ describe('MongoshNodeRepl', () => {

context('with default options', () => {
beforeEach(async() => {
await mongoshRepl.start(serviceProvider);
const initialized = await mongoshRepl.initialize(serviceProvider);
await mongoshRepl.startRepl(initialized);
});

it('shows a nice message to say hello', () => {
Expand Down Expand Up @@ -223,7 +224,8 @@ describe('MongoshNodeRepl', () => {
...mongoshReplOptions,
nodeReplOptions: { terminal: true }
});
await mongoshRepl.start(serviceProvider);
const initialized = await mongoshRepl.initialize(serviceProvider);
await mongoshRepl.startRepl(initialized);
});

it('provides an editor action', async() => {
Expand Down Expand Up @@ -424,7 +426,8 @@ describe('MongoshNodeRepl', () => {
Object.assign(outputStream, fakeTTYProps);
Object.assign(input, fakeTTYProps);
mongoshRepl = new MongoshNodeRepl(mongoshReplOptions);
await mongoshRepl.start(serviceProvider);
const initialized = await mongoshRepl.initialize(serviceProvider);
await mongoshRepl.startRepl(initialized);
expect(mongoshRepl.getFormatOptions().colors).to.equal(true);
});

Expand Down Expand Up @@ -515,15 +518,15 @@ describe('MongoshNodeRepl', () => {
});

it('warns about the unavailable history file support', async() => {
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);
expect(output).to.include('Error processing history file');
});
});

context('when the config says to skip the telemetry greeting message', () => {
beforeEach(async() => {
config.disableGreetingMessage = true;
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);
});

it('skips telemetry intro', () => {
Expand All @@ -538,7 +541,7 @@ describe('MongoshNodeRepl', () => {
nodb: true
};
mongoshRepl = new MongoshNodeRepl(mongoshReplOptions);
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);
});

it('does not show warnings', () => {
Expand All @@ -555,7 +558,7 @@ describe('MongoshNodeRepl', () => {
sp.runCommandWithCheck.withArgs(ADMIN_DB, {
getLog: 'startupWarnings'
}, {}).resolves({ ok: 1, log: logLines });
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);

expect(output).to.contain('The server generated these startup warnings when booting');
logLines.forEach(l => {
Expand All @@ -567,7 +570,7 @@ describe('MongoshNodeRepl', () => {
sp.runCommandWithCheck.withArgs(ADMIN_DB, {
getLog: 'startupWarnings'
}, {}).resolves({ ok: 1, log: ['Not JSON'] });
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);

expect(output).to.contain('The server generated these startup warnings when booting');
expect(output).to.contain('Unexpected log line format: Not JSON');
Expand All @@ -578,7 +581,7 @@ describe('MongoshNodeRepl', () => {
sp.runCommandWithCheck.withArgs(ADMIN_DB, {
getLog: 'startupWarnings'
}, {}).resolves({ ok: 1, log: [] });
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);

expect(output).to.not.contain('The server generated these startup warnings when booting');
expect(error).to.be.null;
Expand All @@ -590,7 +593,7 @@ describe('MongoshNodeRepl', () => {
sp.runCommandWithCheck.withArgs(ADMIN_DB, {
getLog: 'startupWarnings'
}, {}).rejects(expectedError);
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);

expect(output).to.not.contain('The server generated these startup warnings when booting');
expect(output).to.not.contain('Error');
Expand All @@ -602,7 +605,7 @@ describe('MongoshNodeRepl', () => {
sp.runCommandWithCheck.withArgs(ADMIN_DB, {
getLog: 'startupWarnings'
}, {}).resolves(undefined);
await mongoshRepl.start(serviceProvider);
await mongoshRepl.initialize(serviceProvider);

expect(output).to.not.contain('The server generated these startup warnings when booting');
expect(output).to.not.contain('Error');
Expand All @@ -624,12 +627,14 @@ describe('MongoshNodeRepl', () => {
}
});

await mongoshRepl.start(serviceProvider);
const initialized = await mongoshRepl.initialize(serviceProvider);
await mongoshRepl.startRepl(initialized);
expect(output).to.contain('Enterprise > ');
});

it('defaults if an error occurs', async() => {
await mongoshRepl.start(serviceProvider);
const initialized = await mongoshRepl.initialize(serviceProvider);
await mongoshRepl.startRepl(initialized);
expect(output).to.contain('> ');
mongoshRepl.runtimeState().internalState.getDefaultPrompt = () => { throw new Error('no prompt'); };

Expand Down
19 changes: 17 additions & 2 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export type MongoshNodeReplOptions = {
nodeReplOptions?: Partial<ReplOptions>;
};

// Used to make sure start() can only be called after initialize().
export type InitializationToken = { __initialized: 'yes' };

type MongoshRuntimeState = {
shellEvaluator: ShellEvaluator;
internalState: ShellInternalState;
Expand Down Expand Up @@ -81,7 +84,7 @@ class MongoshNodeRepl implements EvaluationListener {
this._runtimeState = null;
}

async start(serviceProvider: ServiceProvider): Promise<void> {
async initialize(serviceProvider: ServiceProvider): Promise<InitializationToken> {
const internalState = new ShellInternalState(serviceProvider, this.bus, this.shellCliOptions);
const shellEvaluator = new ShellEvaluator(internalState);
internalState.setEvaluationListener(this);
Expand All @@ -95,7 +98,7 @@ class MongoshNodeRepl implements EvaluationListener {
start: prettyRepl.start,
input: this.lineByLineInput as unknown as Readable,
output: this.output,
prompt: await this.getShellPrompt(internalState),
prompt: '',
writer: this.writer.bind(this),
breakEvalOnSigint: true,
preview: false,
Expand Down Expand Up @@ -233,9 +236,17 @@ class MongoshNodeRepl implements EvaluationListener {
});

internalState.setCtx(repl.context);
return { __initialized: 'yes' };
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async startRepl(_initializationToken: InitializationToken): Promise<void> {
const { repl, internalState } = this.runtimeState();
// Only start reading from the input *after* we set up everything, including
// internalState.setCtx().
this.lineByLineInput.start();
repl.setPrompt(await this.getShellPrompt(internalState));
repl.displayPrompt();
}

/**
Expand Down Expand Up @@ -334,6 +345,10 @@ class MongoshNodeRepl implements EvaluationListener {
};
}

async loadExternalFile(filename: string): Promise<void> {
await this.runtimeState().internalState.shellApi.load(filename);
}

/**
* Format the result to a string so it can be written to the output stream.
*/
Expand Down
Loading