Today we look at a new JavaScript proposal into statge3: the ShadowRealm API.

JavaScript runtime environment

realm, a rather abstract word, actually represents a JavaScript independent runtime environment with independent variable scopes.

For example, the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

<body>
  <iframe>
  </iframe>
  <script>
    const win = frames[0].window;
    console.assert(win.globalThis !== globalThis); // true
    console.assert(win.Array !== Array); // true
  </script>
</body>

Each iframe has a separate runtime environment, the global object of document is different from the global object of iframe, and similarly, the Array on the global object must be different as well.

ShadowRealm API

The ShadowRealm API is a new JavaScript proposal that allows a JS runtime to create multiple highly isolated JS runtime environments (realm), each realm with independent global objects and built-in objects.

ShadowRealm has the following type signature.

1
2
3
4
5
declare class ShadowRealm {
  constructor();
  evaluate(sourceText: string): PrimitiveValueOrCallable;
  importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}

Each ShadowRealm instance has its own separate runtime environment, which provides two ways for us to execute the code in the runtime environment.

  • .evaluate(): synchronizes the execution of the code string, similar to eval().
  • .importValue(): returns a Promise object that executes the code string asynchronously.

shadowRealm.evaluate()

Type signature of .evaluate().

1
evaluate(sourceText: string): PrimitiveValueOrCallable;

.evaluate() works much like eval().

1
2
3
4
const sr = new ShadowRealm();
console.assert(
  sr.evaluate(`'ab' + 'cd'`) === 'abcd'
);

But unlike eval(), the code is executed in a separate runtime environment from .evaluate().

1
2
3
4
5
6
7
globalThis.realm = 'incubator realm';

const sr = new ShadowRealm();
sr.evaluate(`globalThis.realm = 'ConardLi realm'`);
console.assert(
  sr.evaluate(`globalThis.realm`) === 'ConardLi realm'
);

If .evaluate() returns a function, this function is wrapped for ease of calling externally and then run in ShadowRealm.

1
2
3
4
5
6
7
globalThis.realm = 'incubator realm';

const sr = new ShadowRealm();
sr.evaluate(`globalThis.realm = 'ConardLi realm'`);

const wrappedFunc = sr.evaluate(`() => globalThis.realm`);
console.assert(wrappedFunc() === 'ConardLi realm');

Whenever a value is passed into ShadowRealm, it must be of primitive type or callable. Otherwise, an exception will be thrown.

1
2
> new ShadowRealm().evaluate('[]')
TypeError: value passing between realms must be callable or primitive

shadowRealm.importValue()

Type signature of .importValue()

1
importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;

You can directly import an external module that executes asynchronously and returns a Promise, as follows.

1
2
3
4
5
6
7
8
9
// main.js
const sr = new ShadowRealm();
const wrappedSum = await sr.importValue('./my-module.js', 'sum');
console.assert(wrappedSum('hi', ' ', 'folks', '!') === 'hi ConardLi!');

// my-module.js
export function sum(...values) {
  return values.reduce((prev, value) => prev + value);
}

As with .evaluate(), the values passed into ShadowRealms (both the arguments and the result of the cross-environment function call) must be raw or callable.

What can ShadowRealms be used for?

  • Run third-party code such as plug-ins in programs such as Web IDE or Web drawing applications.
  • Create a programming environment in ShadowRealms to run user code.
  • The server can run third-party code in ShadowRealms.
  • Tests can be run in ShadowRealms so that the external JS execution environment is not affected and each suite can be launched in the new environment (this helps with reusability).
  • Web crawling (extracting data from web pages) and web application testing, etc. can be run in ShadowRealms.

Comparison with other solutions

eval() and Function

ShadowRealms is much like eval() and Function, but a little better than both of them: we can create new JS runtime environments and execute code in them, which protects the external JS runtime environment from the operations executed by the code.

Web Workers

Web Worker is a more powerful isolation mechanism than ShadowRealms. The code in it runs in a separate process and communication is asynchronous.

However, when we want to do some more lightweight operations, ShadowRealms is a good choice. Its algorithm allows simultaneous computation, is more convenient, and the global data management is more free.

iframe

As we mentioned before, each iframe has its own runtime environment in which we can execute code synchronously.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<body>
  <iframe>
  </iframe>
  <script>
    globalThis.realm = 'incubator';
    const iframeRealm = frames[0].window;
    iframeRealm.globalThis.realm = 'ConardLi';
    console.log(iframeRealm.eval('globalThis.realm')); // 'ConardLi'
  </script>
</body>

Compared to ShadowRealms, it still has the following disadvantages.

  • Can only use iframe in the browser.
  • The need to add an iframe to the DOM to initialize it.
  • Each iframe environment contains the full DOM, which limits the flexibility of customization in some scenarios.
  • By default, objects are cross-environmental, which means extra work is needed to ensure code security.

The vm module on Node.js

The vm module for Node.js is similar to the ShadowRealm API, but has more features: caching the JavaScript engine, intercepting import(), and so on. But its only drawback is that it is not cross-platform and can only be used in the Node.js environment.

Usage Example: Running Tests in ShadowRealms

Here we see a small Demo for running tests in ShadowRealms. The test library collects the tests specified by test() and allows us to run them via runTests().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// test-lib.js
const testDescs = [];

export function test(description, callback) {
  testDescs.push({description, callback});
}

export function runTests() {
  const testResults = [];
  for (const testDesc of testDescs) {
    try {
      testDesc.callback();
      testResults.push(`${testDesc.description}: OK\n`);
    } catch (err) {
      testResults.push(`${testDesc.description}: ${err}\n`);
    }
  }
  return testResults.join('');
}

Use the library to specify the test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// my-test.js
import {test} from './test-lib.js';
import * as assert from './assertions.js';

test('succeeds', () => {
  assert.equal(3, 3);
});

test('fails', () => {
  assert.equal(1, 3);
});

export default true;

In the next example, we dynamically load the my-test.js module to collect and then run tests.

Alas, there is currently no way to load the module without importing anything.

That’s why there is a default export in the last line of the previous example. We use the ShadowRealm.importvalue() method to import the default export.

1
2
3
4
5
6
7
8
9
// test-runner.js
async function runTestModule(moduleSpecifier) {
  const sr = new ShadowRealm();
  await sr.importValue(moduleSpecifier, 'default');
  const runTests = await sr.importValue('./test-lib.js', 'runTests');
  const result = runTests();
  console.log(result);
}
await runTestModule('./my-test.js');

Running Web Applications in ShadowRealms

The jsdom library creates a wrapped browser environment that can be used to test Web applications, extract data from HTML, and more. It currently uses the Node.js vm module, and may be updated to use ShadowRealms in the future (the latter has the benefit of being cross-platform, while vm currently only supports Node.js).