Skip to content

Commit 53a35b9

Browse files
committed
chore(shell-api): add test and note about runtime independence MONGOSH-1975
1 parent 7b85237 commit 53a35b9

File tree

4 files changed

+103
-4
lines changed

4 files changed

+103
-4
lines changed

packages/service-provider-core/src/textencoder-polyfill.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ if (
1111
typeof TextEncoder !== 'function'
1212
) {
1313
// eslint-disable-next-line @typescript-eslint/no-implied-eval
14-
Object.assign(Function('return this')(), textEncodingPolyfill());
14+
const global =
15+
(typeof globalThis === 'object' &&
16+
globalThis?.Object === Object &&
17+
globalThis) ||
18+
Function('return this')();
19+
Object.assign(global, textEncodingPolyfill());
1520
}
1621

1722
// Basic encoder/decoder polyfill for java-shell environment (see above)

packages/shell-api/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @mongosh/shell-api
2+
3+
Provides the runtime-independent classes that make up the MongoDB Shell API,
4+
such as `Database`, `Collection`, etc., and the global objects and APIs
5+
available to shell users, such as `db`, `rs`, `sh`, `console`, `print()`, etc.

packages/shell-api/src/decorators.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,11 @@ interface Signatures {
397397
// object instead of a global list, or even more radical changes
398398
// such as removing the concept of signatures altogether.
399399
const signaturesGlobalIdentifier = '@@@mdb.signatures@@@';
400-
if (!(global as any)[signaturesGlobalIdentifier]) {
401-
(global as any)[signaturesGlobalIdentifier] = {};
400+
if (!(globalThis as any)[signaturesGlobalIdentifier]) {
401+
(globalThis as any)[signaturesGlobalIdentifier] = {};
402402
}
403403

404-
const signatures: Signatures = (global as any)[signaturesGlobalIdentifier];
404+
const signatures: Signatures = (globalThis as any)[signaturesGlobalIdentifier];
405405
signatures.Document = { type: 'Document', attributes: {} };
406406
export { signatures };
407407

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// This test verifies that shell-api only uses standard JS features.
2+
import fsSync from 'fs';
3+
import vm from 'vm';
4+
import { createRequire } from 'module';
5+
import { expect } from 'chai';
6+
import sinon from 'ts-sinon';
7+
8+
describe('Runtime independence', function () {
9+
it('Can run using exclusively JS standard features', async function () {
10+
const entryPoint = require.resolve('../');
11+
12+
const context = vm.createContext(Object.create(null), {
13+
codeGeneration: {
14+
strings: false,
15+
wasm: false,
16+
},
17+
});
18+
19+
// These are all used Node.js modules that are somewhat easily polyfill-able
20+
// for other environments, but which we should still ideally remove in the
21+
// long run (and definitely not add anything here).
22+
// Guaranteed bonusly for anyone who removes a package from this list!
23+
const allowedNodeBuiltins = ['crypto', 'util', 'events', 'path'];
24+
// Our TextDecoder/TextEncoder polyfills require this, unfortunately.
25+
context.Buffer = Buffer;
26+
// lodash used by mongodb-redact used by @mongosh/history requires this Node.js-ism.
27+
// Let's get rid of it: https://github.com/mongodb-js/devtools-shared/pull/497
28+
vm.runInContext('globalThis.global = globalThis;', context);
29+
30+
// Small CJS implementation, without __dirname or __filename
31+
const cache = Object.create(null);
32+
const absolutePathRequire = (absolutePath: string) => {
33+
absolutePath = fsSync.realpathSync(absolutePath);
34+
if (cache[absolutePath]) return cache[absolutePath];
35+
const module = (cache[absolutePath] = { exports: {} });
36+
const localRequire = (specifier: string) => {
37+
if (allowedNodeBuiltins.includes(specifier)) return require(specifier);
38+
return absolutePathRequire(
39+
createRequire(absolutePath).resolve(specifier)
40+
).exports;
41+
};
42+
const source = fsSync.readFileSync(absolutePath, 'utf8');
43+
const fn = vm.runInContext(
44+
`(function(module, exports, require) {\n${source}\n})`,
45+
context,
46+
{
47+
filename: `IN_CONTEXT:${absolutePath}`,
48+
}
49+
);
50+
fn(module, module.exports, localRequire);
51+
return module;
52+
};
53+
54+
// service-provider-core injects a dummy polyfill for TextDecoder/TextEncoder
55+
absolutePathRequire(require.resolve('@mongosh/service-provider-core'));
56+
const shellApi =
57+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
58+
absolutePathRequire(entryPoint).exports as typeof import('./');
59+
60+
// Verify that `shellApi` is generally usable.
61+
const sp = { platform: 'CLI', close: sinon.spy() };
62+
const evaluationListener = { onExit: sinon.spy() };
63+
const instanceState = new shellApi.ShellInstanceState(sp as any);
64+
instanceState.setEvaluationListener(evaluationListener);
65+
expect(instanceState.initialServiceProvider).to.equal(sp);
66+
const bsonObj = instanceState.shellBson.ISODate(
67+
'2025-01-09T20:43:51+01:00'
68+
);
69+
expect(bsonObj.toISOString()).to.equal('2025-01-09T19:43:51.000Z');
70+
expect(bsonObj instanceof Date).to.equal(false);
71+
expect(Object.prototype.toString.call(bsonObj)).to.equal(
72+
Object.prototype.toString.call(new Date())
73+
);
74+
75+
try {
76+
await instanceState.shellApi.exit();
77+
expect.fail('missed exception');
78+
} catch (err: any) {
79+
expect(err.message).to.include('.onExit listener returned');
80+
expect(err.stack).to.include('IN_CONTEXT');
81+
expect(err instanceof Error).to.equal(false);
82+
expect(Object.prototype.toString.call(err)).to.equal(
83+
Object.prototype.toString.call(new Error())
84+
);
85+
}
86+
expect(sp.close).to.have.been.calledOnce;
87+
expect(evaluationListener.onExit).to.have.been.calledOnce;
88+
});
89+
});

0 commit comments

Comments
 (0)