|
| 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