I've been using the quickjs-emscripten package a bit recently. It's a Web Assembly compiled wrapper around Fabrice Bellard's QuickJS, a small & modern JavaScript engine.
This allows me to execute sandboxed JavaScript in an environment that is completely controlled!
For example, here's some code showing how arbitrary JavaScript code can be evaluated safely. It isolates the code so it
doesn't have access to the outside environment with the exception of a special location.search
global variable, which
reflects the browser's query string as a readonly value.
It also tracks reads to that query string in an accessLog
:
safeEval.ts
import { getQuickJS } from 'quickjs-emscripten'; async function safeEval(code: string) { const QuickJS = await getQuickJS(); const runtimeStart = Date.now(); const runtime = QuickJS.newRuntime({ interruptHandler: () => { return Date.now() - runtimeStart > 100; }, }); const ctx = runtime.newContext(); const accessLog: string[] = []; const locationHandle = ctx.newObject(); ctx.setProp(ctx.global, 'location', locationHandle); ctx.defineProp(locationHandle, 'search', { configurable: false, enumerable: true, get: () => { accessLog.push('search'); return ctx.newString(window.location.search); }, }); const result = ctx.evalCode(code, 'untrusted.js'); if (result.error) { result.error.dispose(); locationHandle.dispose(); ctx.dispose(); runtime.dispose(); throw new Error('Error executing code'); } const evaluationResult = ctx.dump(result.unwrap()); result.dispose(); locationHandle.dispose(); ctx.dispose(); runtime.dispose(); return { evaluationResult, accessLog }; }
While this works great, it's a bit awkward to use because it requires explicit disposal of "handle" objects that reference values inside of the QuickJS engine.
See all those .dispose()
calls? If one is missed, it causes a memory leak.
And to make matters worse, sometimes you need to exit early, or catch exceptions, or do all sorts of things which make
it complicated to keep track of all of the objects that need to be .dispose()
d.
No need to fret, Explicit Resource Management makes this much easier!
Explicit Resource Management is an ECMAScript language
proposal that introduces a new keyword using
, which is similar to const
, but is used to define resources that need
to be disposed when they leave scope.
It means that the above code could be rewritten as:
safeEval.ts
import { getQuickJS } from 'quickjs-emscripten'; async function safeEval(code: string) { const QuickJS = await getQuickJS(); const runtimeStart = Date.now(); using runtime = QuickJS.newRuntime({ interruptHandler: () => { return Date.now() - runtimeStart > 100; }, }); using ctx = runtime.newContext(); const accessLog: string[] = []; using locationHandle = ctx.newObject(); ctx.setProp(ctx.global, 'location', locationHandle); ctx.defineProp(locationHandle, 'search', { configurable: false, enumerable: true, get: () => { accessLog.push('search'); return ctx.newString(window.location.search); }, }); using result = ctx.evalCode(code, 'untrusted.js'); if (result.error) { throw new Error('Error executing code'); } const evaluationResult = ctx.dump(result.unwrap()); return { evaluationResult, accessLog }; }
See that? No more .dispose()
calls!
Regardless of how scope is left (here, variables may go out of scope when an exception is thrown or we return), the objects are automatically disposed when execution leaves the syntactic scope.
And because this feature has reached Stage 3, it's both very likely to be coming soon to the standard language and can be used today if you build your JavaScript.
I use TypeScript
, prettier
, eslint
, esbuild
, vite
, and neovim
to develop and build my code. And all of them fully
support this proposal and esbuild can produce code that performs disposal correctly in JS engines that haven't yet
implemented the functionality.
Here's how to get them working:
TypeScript:
You must use TypeScript 5.2 or above.
Your tsconfig.json
must have:
compilerOptions.lib
containing "ES2019"
and "esnext.disposable"
compilerOptions.target
set to something lower than "esnext"
, I use "ES2019"
since 5 years old is a reasonable
target for my projects.My tsconfig.json
looks like this:
tsconfig.json
{ "compilerOptions": { "lib": ["ES2019", "esnext.disposable", "DOM"], "target": "ES2019", }, }
prettier:
Supported out of the box in prettier version 3.0.3 or later.
eslint:
I use typescript-eslint/parser
, which got support in typescript-eslint version
7.13.1.
esbuild:
Implemented in version 0.18.17, bugfixes in version 0.18.19.
You must specify a target of something lower than esnext
. Same with
TypeScript, I use es2019
.
vite:
Since vite uses esbuild under the hood to bundle, you'll need to set a target of es2019
(or whatever works best for
you) when building:
vite.config.js
import { defineConfig } from 'vite'; export default defineConfig({ base: './', build: { assetsDir: './', target: 'es2019', }, esbuild: { target: 'es2019', }, });
vim/neovim:
To get the using
keyword working correctly, you need to tell vim/neovim that using
is a keyword.
There's probably a better way to do this, but what I ended up doing was adding two files (~/.config/nvim/after/syntax/javascript.vim
and ~/.config/nvim/after/syntax/typescript.vim
) with the same contents:
javascript.vim
syn keyword esNextKeyword using
hi def link esNextKeyword Keyword
This tells vim that after loading the syntax files for javascript/typescript (and javascriptreact/typescriptreact),
it should treat using
as a new keyword in a class named esNextKeyword
. And link that new class to the Keyword
definition.
Note: Stage 3 is not a guarantee that it will ship as-is in browsers and other JavaScript engines.
I personally think it will ship, since it will lead to standardizing a well-known pattern (RAII) found in other languages.
The nice thing about this proposal is that it's just enough of a change to allow for new patterns to emerge.
Say you've got a class which owns and manages a set of resources. The problem is now that class instances needs to dispose of its resources when it's no longer used.
Now you can just add a Symbol.dispose
method on your class, and it can be disposed in the same way as the resources it
manages.
This pattern looks like this:
class ResourceManager implements Disposable { resource1: SomeResource; resource2: SomeOtherResource; constructor() { this.resource1 = allocateSomeResource(); this.resource2 = allocateSomeOtherResource(); } [Symbol.dispose]() { this.resource1[Symbol.dispose](); this.resource2[Symbol.dispose](); } } function doWork() { using manager = new ResourceManager(); return; }
This lets us do RAII in JavaScript easily, which can help avoid resource leaks and better standardize the structure of code that manages resources.