Skip to content

Commit d4a6312

Browse files
authored
chore(shell-api): add test and note about runtime independence MONGOSH-1975 (#2312)
1 parent 4ead612 commit d4a6312

File tree

8 files changed

+126
-18
lines changed

8 files changed

+126
-18
lines changed

package-lock.json

Lines changed: 22 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/history/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
"dependencies": {
3838
"mongodb-connection-string-url": "^3.0.1",
39-
"mongodb-redact": "^1.1.2"
39+
"mongodb-redact": "^1.1.5"
4040
},
4141
"devDependencies": {
4242
"@mongodb-js/eslint-config-mongosh": "^1.0.0",

packages/logging/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@mongosh/history": "0.0.0-dev.0",
2323
"@mongosh/types": "0.0.0-dev.0",
2424
"mongodb-log-writer": "^1.4.2",
25-
"mongodb-redact": "^1.1.2"
25+
"mongodb-redact": "^1.1.5"
2626
},
2727
"devDependencies": {
2828
"@mongodb-js/eslint-config-mongosh": "^1.0.0",

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ if (
1010
// @ts-ignore
1111
typeof TextEncoder !== 'function'
1212
) {
13-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
14-
Object.assign(Function('return this')(), textEncodingPolyfill());
13+
const global =
14+
(typeof globalThis === 'object' &&
15+
globalThis?.Object === Object &&
16+
globalThis) ||
17+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
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/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@mongosh/history": "0.0.0-dev.0",
4646
"@mongosh/i18n": "0.0.0-dev.0",
4747
"@mongosh/service-provider-core": "0.0.0-dev.0",
48-
"mongodb-redact": "^1.1.2"
48+
"mongodb-redact": "^1.1.5"
4949
},
5050
"devDependencies": {
5151
"@mongodb-js/eslint-config-mongosh": "^1.0.0",

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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
27+
// Small CJS implementation, without __dirname or __filename
28+
const cache = Object.create(null);
29+
const absolutePathRequire = (absolutePath: string) => {
30+
absolutePath = fsSync.realpathSync(absolutePath);
31+
if (cache[absolutePath]) return cache[absolutePath];
32+
const module = (cache[absolutePath] = { exports: {} });
33+
const localRequire = (specifier: string) => {
34+
if (allowedNodeBuiltins.includes(specifier)) return require(specifier);
35+
return absolutePathRequire(
36+
createRequire(absolutePath).resolve(specifier)
37+
).exports;
38+
};
39+
const source = fsSync.readFileSync(absolutePath, 'utf8');
40+
const fn = vm.runInContext(
41+
`(function(module, exports, require) {\n${source}\n})`,
42+
context,
43+
{
44+
filename: `IN_CONTEXT:${absolutePath}`,
45+
}
46+
);
47+
fn(module, module.exports, localRequire);
48+
return module;
49+
};
50+
51+
// service-provider-core injects a dummy polyfill for TextDecoder/TextEncoder
52+
absolutePathRequire(require.resolve('@mongosh/service-provider-core'));
53+
const shellApi =
54+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
55+
absolutePathRequire(entryPoint).exports as typeof import('./');
56+
57+
// Verify that `shellApi` is generally usable.
58+
const sp = { platform: 'CLI', close: sinon.spy() };
59+
const evaluationListener = { onExit: sinon.spy() };
60+
const instanceState = new shellApi.ShellInstanceState(sp as any);
61+
instanceState.setEvaluationListener(evaluationListener);
62+
expect(instanceState.initialServiceProvider).to.equal(sp);
63+
const bsonObj = instanceState.shellBson.ISODate(
64+
'2025-01-09T20:43:51+01:00'
65+
);
66+
expect(bsonObj.toISOString()).to.equal('2025-01-09T19:43:51.000Z');
67+
expect(bsonObj instanceof Date).to.equal(false);
68+
expect(Object.prototype.toString.call(bsonObj)).to.equal(
69+
Object.prototype.toString.call(new Date())
70+
);
71+
72+
try {
73+
await instanceState.shellApi.exit();
74+
expect.fail('missed exception');
75+
} catch (err: any) {
76+
expect(err.message).to.include('.onExit listener returned');
77+
expect(err.stack).to.include('IN_CONTEXT');
78+
expect(err instanceof Error).to.equal(false);
79+
expect(Object.prototype.toString.call(err)).to.equal(
80+
Object.prototype.toString.call(new Error())
81+
);
82+
}
83+
expect(sp.close).to.have.been.calledOnce;
84+
expect(evaluationListener.onExit).to.have.been.calledOnce;
85+
});
86+
});

0 commit comments

Comments
 (0)