| # quickjs-emscripten |
| |
| Javascript/Typescript bindings for QuickJS, a modern Javascript interpreter, |
| compiled to WebAssembly. |
| |
| - Safely evaluate untrusted Javascript (up to ES2020). |
| - Create and manipulate values inside the QuickJS runtime ([more][values]). |
| - Expose host functions to the QuickJS runtime ([more][functions]). |
| - Execute synchronous code that uses asynchronous functions, with [asyncify][asyncify]. |
| |
| [Github] | [NPM] | [API Documentation][api] | [Examples][tests] |
| |
| ```typescript |
| import { getQuickJS } from "quickjs-emscripten" |
| |
| async function main() { |
| const QuickJS = await getQuickJS() |
| const vm = QuickJS.newContext() |
| |
| const world = vm.newString("world") |
| vm.setProp(vm.global, "NAME", world) |
| world.dispose() |
| |
| const result = vm.evalCode(`"Hello " + NAME + "!"`) |
| if (result.error) { |
| console.log("Execution failed:", vm.dump(result.error)) |
| result.error.dispose() |
| } else { |
| console.log("Success:", vm.dump(result.value)) |
| result.value.dispose() |
| } |
| |
| vm.dispose() |
| } |
| |
| main() |
| ``` |
| |
| [github]: https://github.com/justjake/quickjs-emscripten |
| [npm]: https://www.npmjs.com/package/quickjs-emscripten |
| [api]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/modules.md |
| [tests]: https://github.com/justjake/quickjs-emscripten/blob/main/ts/quickjs.test.ts |
| [values]: #interfacing-with-the-interpreter |
| [asyncify]: #asyncify |
| [functions]: #exposing-apis |
| |
| ## Usage |
| |
| Install from `npm`: `npm install --save quickjs-emscripten` or `yarn add quickjs-emscripten`. |
| |
| The root entrypoint of this library is the `getQuickJS` function, which returns |
| a promise that resolves to a [QuickJS singleton](./doc/classes/quickjs.md) when |
| the QuickJS WASM module is ready. |
| |
| Once `getQuickJS` has been awaited at least once, you also can use the `getQuickJSSync` |
| function to directly access the singleton engine in your synchronous code. |
| |
| ### Safely evaluate Javascript code |
| |
| See [QuickJS.evalCode](https://github.com/justjake/quickjs-emscripten/blob/main/doc/classes/quickjs.md#evalcode) |
| |
| ```typescript |
| import { getQuickJS, shouldInterruptAfterDeadline } from "quickjs-emscripten" |
| |
| getQuickJS().then((QuickJS) => { |
| const result = QuickJS.evalCode("1 + 1", { |
| shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 1000), |
| memoryLimitBytes: 1024 * 1024, |
| }) |
| console.log(result) |
| }) |
| ``` |
| |
| ### Interfacing with the interpreter |
| |
| You can use [QuickJSContext](https://github.com/justjake/quickjs-emscripten/blob/main/doc/classes/QuickJSContext.md) |
| to build a scripting environment by modifying globals and exposing functions |
| into the QuickJS interpreter. |
| |
| Each `QuickJSContext` instance has its own environment -- globals, built-in |
| classes -- and actions from one context won't leak into other contexts or |
| runtimes (with one exception, see [Asyncify][asyncify]). |
| |
| Every context is created inside a |
| [QuickJSRuntime](https://github.com/justjake/quickjs-emscripten/blob/main/doc/classes/QuickJSRuntime.md). |
| A runtime represents a Javascript heap, and you can even share values between |
| contexts in the same runtime. |
| |
| ```typescript |
| const vm = QuickJS.newContext() |
| let state = 0 |
| |
| const fnHandle = vm.newFunction("nextId", () => { |
| return vm.newNumber(++state) |
| }) |
| |
| vm.setProp(vm.global, "nextId", fnHandle) |
| fnHandle.dispose() |
| |
| const nextId = vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)) |
| console.log("vm result:", vm.getNumber(nextId), "native state:", state) |
| |
| nextId.dispose() |
| vm.dispose() |
| ``` |
| |
| When you create a context from a top-level API like in the example above, |
| instead of by calling `runtime.newContext()`, a runtime is automatically created |
| for the lifetime of the context, and disposed of when you dispose the context. |
| |
| #### Runtime |
| |
| The runtime has APIs for CPU and memory limits that apply to all contexts within |
| the runtime in aggregate. You can also use the runtime to configure EcmaScript |
| module loading. |
| |
| ```typescript |
| const runtime = QuickJS.newRuntime() |
| // "Should be enough for everyone" -- attributed to B. Gates |
| runtime.setMemoryLimit(1024 * 640) |
| // Limit stack size |
| runtime.setMaxStackSize(1024 * 320) |
| // Interrupt computation after 1024 calls to the interrupt handler |
| let interruptCycles = 0 |
| runtime.setInterruptHandler(() => ++interruptCycles > 1024) |
| // Toy module system that always returns the module name |
| // as the default export |
| runtime.setModuleLoader((moduleName) => `export default '${moduleName}'`) |
| const context = runtime.newContext() |
| const ok = context.evalCode(` |
| import fooName from './foo.js' |
| globalThis.result = fooName |
| `) |
| context.unwrapResult(ok).dispose() |
| // logs "foo.js" |
| console.log(context.getProp(context.global, "result").consume(context.dump)) |
| context.dispose() |
| runtime.dispose() |
| ``` |
| |
| ### Memory Management |
| |
| Many methods in this library return handles to memory allocated inside the |
| WebAssembly heap. These types cannot be garbage-collected as usual in |
| Javascript. Instead, you must manually manage their memory by calling a |
| `.dispose()` method to free the underlying resources. Once a handle has been |
| disposed, it cannot be used anymore. Note that in the example above, we call |
| `.dispose()` on each handle once it is no longer needed. |
| |
| Calling `QuickJSContext.dispose()` will throw a RuntimeError if you've forgotten to |
| dispose any handles associated with that VM, so it's good practice to create a |
| new VM instance for each of your tests, and to call `vm.dispose()` at the end |
| of every test. |
| |
| ```typescript |
| const vm = QuickJS.newContext() |
| const numberHandle = vm.newNumber(42) |
| // Note: numberHandle not disposed, so it leaks memory. |
| vm.dispose() |
| // throws RuntimeError: abort(Assertion failed: list_empty(&rt->gc_obj_list), at: quickjs/quickjs.c,1963,JS_FreeRuntime) |
| ``` |
| |
| Here are some strategies to reduce the toil of calling `.dispose()` on each |
| handle you create: |
| |
| #### Scope |
| |
| A |
| [`Scope`](https://github.com/justjake/quickjs-emscripten/blob/main/doc/classes/scope.md#class-scope) |
| instance manages a set of disposables and calls their `.dispose()` |
| method in the reverse order in which they're added to the scope. Here's the |
| "Interfacing with the interpreter" example re-written using `Scope`: |
| |
| ```typescript |
| Scope.withScope((scope) => { |
| const vm = scope.manage(QuickJS.newContext()) |
| let state = 0 |
| |
| const fnHandle = scope.manage( |
| vm.newFunction("nextId", () => { |
| return vm.newNumber(++state) |
| }) |
| ) |
| |
| vm.setProp(vm.global, "nextId", fnHandle) |
| |
| const nextId = scope.manage(vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`))) |
| console.log("vm result:", vm.getNumber(nextId), "native state:", state) |
| |
| // When the withScope block exits, it calls scope.dispose(), which in turn calls |
| // the .dispose() methods of all the disposables managed by the scope. |
| }) |
| ``` |
| |
| You can also create `Scope` instances with `new Scope()` if you want to manage |
| calling `scope.dispose()` yourself. |
| |
| #### `Lifetime.consume(fn)` |
| |
| [`Lifetime.consume`](https://github.com/justjake/quickjs-emscripten/blob/main/doc/classes/lifetime.md#consume) |
| is sugar for the common pattern of using a handle and then |
| immediately disposing of it. `Lifetime.consume` takes a `map` function that |
| produces a result of any type. The `map` fuction is called with the handle, |
| then the handle is disposed, then the result is returned. |
| |
| Here's the "Interfacing with interpreter" example re-written using `.consume()`: |
| |
| ```typescript |
| const vm = QuickJS.newContext() |
| let state = 0 |
| |
| vm.newFunction("nextId", () => { |
| return vm.newNumber(++state) |
| }).consume((fnHandle) => vm.setProp(vm.global, "nextId", fnHandle)) |
| |
| vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)).consume((nextId) => |
| console.log("vm result:", vm.getNumber(nextId), "native state:", state) |
| ) |
| |
| vm.dispose() |
| ``` |
| |
| Generally working with `Scope` leads to more straight-forward code, but |
| `Lifetime.consume` can be handy sugar as part of a method call chain. |
| |
| ### Exposing APIs |
| |
| To add APIs inside the QuickJS environment, you'll need to create objects to |
| define the shape of your API, and add properties and functions to those objects |
| to allow code inside QuickJS to call code on the host. |
| |
| By default, no host functionality is exposed to code running inside QuickJS. |
| |
| ```typescript |
| const vm = QuickJS.newContext() |
| // `console.log` |
| const logHandle = vm.newFunction("log", (...args) => { |
| const nativeArgs = args.map(vm.dump) |
| console.log("QuickJS:", ...nativeArgs) |
| }) |
| // Partially implement `console` object |
| const consoleHandle = vm.newObject() |
| vm.setProp(consoleHandle, "log", logHandle) |
| vm.setProp(vm.global, "console", consoleHandle) |
| consoleHandle.dispose() |
| logHandle.dispose() |
| |
| vm.unwrapResult(vm.evalCode(`console.log("Hello from QuickJS!")`)).dispose() |
| ``` |
| |
| #### Promises |
| |
| To expose an asynchronous function that _returns a promise_ to callers within |
| QuickJS, your function can return the handle of a `QuickJSDeferredPromise` |
| created via `context.newPromise()`. |
| |
| When you resolve a `QuickJSDeferredPromise` -- and generally whenever async |
| behavior completes for the VM -- pending listeners inside QuickJS may not |
| execute immediately. Your code needs to explicitly call |
| `runtime.executePendingJobs()` to resume execution inside QuickJS. This API |
| gives your code maximum control to _schedule_ when QuickJS will block the host's |
| event loop by resuming execution. |
| |
| To work with QuickJS handles that contain a promise inside the environment, you |
| can convert the QuickJSHandle into a native promise using |
| `context.resolvePromise()`. Take care with this API to avoid 'deadlocks' where |
| the host awaits a guest promise, but the guest cannot make progress until the |
| host calls `runtime.executePendingJobs()`. The simplest way to avoid this kind |
| of deadlock is to always schedule `executePendingJobs` after any promise is |
| settled. |
| |
| ```typescript |
| const vm = QuickJS.newContext() |
| const fakeFileSystem = new Map([["example.txt", "Example file content"]]) |
| |
| // Function that simulates reading data asynchronously |
| const readFileHandle = vm.newFunction("readFile", (pathHandle) => { |
| const path = vm.getString(pathHandle) |
| const promise = vm.newPromise() |
| setTimeout(() => { |
| const content = fakeFileSystem.get(path) |
| promise.resolve(vm.newString(content || "")) |
| }, 100) |
| // IMPORTANT: Once you resolve an async action inside QuickJS, |
| // call runtime.executePendingJobs() to run any code that was |
| // waiting on the promise or callback. |
| promise.settled.then(vm.runtime.executePendingJobs) |
| return promise.handle |
| }) |
| readFileHandle.consume((handle) => vm.setProp(vm.global, "readFile", handle)) |
| |
| // Evaluate code that uses `readFile`, which returns a promise |
| const result = vm.evalCode(`(async () => { |
| const content = await readFile('example.txt') |
| return content.toUpperCase() |
| })()`) |
| const promiseHandle = vm.unwrapResult(result) |
| |
| // Convert the promise handle into a native promise and await it. |
| // If code like this deadlocks, make sure you are calling |
| // runtime.executePendingJobs appropriately. |
| const resolvedResult = await vm.resolvePromise(promiseHandle) |
| promiseHandle.dispose() |
| const resolvedHandle = vm.unwrapResult(resolvedResult) |
| console.log("Result:", vm.getString(resolvedHandle)) |
| resolvedHandle.dispose() |
| ``` |
| |
| #### Asyncify |
| |
| Sometimes, we want to create a function that's synchronous from the perspective |
| of QuickJS, but prefer to implement that function _asynchronously_ in your host |
| code. The most obvious use-case is for EcmaScript module loading. The underlying |
| QuickJS C library expects the module loader function to return synchronously, |
| but loading data synchronously in the browser or server is somewhere between "a |
| bad idea" and "impossible". QuickJS also doesn't expose an API to "pause" the |
| execution of a runtime, and adding such an API is tricky due to the VM's |
| implementation. |
| |
| As a work-around, we provide an alternate build of QuickJS processed by |
| Emscripten/Binaryen's [ASYNCIFY](https://emscripten.org/docs/porting/asyncify.html) |
| compiler transform. Here's how Emscripten's documentation describes Asyncify: |
| |
| > Asyncify lets synchronous C or C++ code interact with asynchronous \[host] JavaScript. This allows things like: |
| > |
| > - A synchronous call in C that yields to the event loop, which allows browser events to be handled. |
| > |
| > - A synchronous call in C that waits for an asynchronous operation in \[host] JS to complete. |
| > |
| > Asyncify automatically transforms ... code into a form that can be paused and |
| > resumed ..., so that it is asynchronous (hence the name “Asyncify”) even though |
| > \[it is written] in a normal synchronous way. |
| |
| This means we can suspend an _entire WebAssembly module_ (which could contain |
| multiple runtimes and contexts) while our host Javascript loads data |
| asynchronously, and then resume execution once the data load completes. This is |
| a very handy superpower, but it comes with a couple of major limitations: |
| |
| 1. _An asyncified WebAssembly module can only suspend to wait for a single |
| asynchronous call at a time_. You may call back into a suspended WebAssembly |
| module eg. to create a QuickJS value to return a result, but the system will |
| crash if this call tries to suspend again. Take a look at Emscripten's documentation |
| on [reentrancy](https://emscripten.org/docs/porting/asyncify.html#reentrancy). |
| |
| 2. _Asyncified code is bigger and runs slower_. The asyncified build of |
| Quickjs-emscripten library is 1M, 2x larger than the 500K of the default |
| version. There may be room for further |
| [optimization](https://emscripten.org/docs/porting/asyncify.html#optimizing) |
| Of our build in the future. |
| |
| To use asyncify features, use the following functions: |
| |
| - [newAsyncRuntime][]: create a runtime inside a new WebAssembly module. |
| - [newAsyncContext][]: create runtime and context together inside a new |
| WebAssembly module. |
| - [newQuickJSAsyncWASMModule][]: create an empty WebAssembly module. |
| |
| [newasyncruntime]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/modules.md#newasyncruntime |
| [newasynccontext]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/modules.md#newasynccontext |
| [newquickjsasyncwasmmodule]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/modules.md#newquickjsasyncwasmmodule |
| |
| These functions are asynchronous because they always create a new underlying |
| WebAssembly module so that each instance can suspend and resume independently, |
| and instantiating a WebAssembly module is an async operation. This also adds |
| substantial overhead compared to creating a runtime or context inside an |
| existing module; if you only need to wait for a single async action at a time, |
| you can create a single top-level module and create runtimes or contexts inside |
| of it. |
| |
| ##### Async module loader |
| |
| Here's an example of valuating a script that loads React asynchronously as an ES |
| module. In our example, we're loading from the filesystem for reproducibility, |
| but you can use this technique to load using `fetch`. |
| |
| ```typescript |
| const module = await newQuickJSAsyncWASMModule() |
| const runtime = module.newRuntime() |
| const path = await import("path") |
| const { promises: fs } = await import("fs") |
| |
| const importsPath = path.join(__dirname, "../examples/imports") + "/" |
| // Module loaders can return promises. |
| // Execution will suspend until the promise resolves. |
| runtime.setModuleLoader((moduleName) => { |
| const modulePath = path.join(importsPath, moduleName) |
| if (!modulePath.startsWith(importsPath)) { |
| throw new Error("out of bounds") |
| } |
| console.log("loading", moduleName, "from", modulePath) |
| return fs.readFile(modulePath, "utf-8") |
| }) |
| |
| // evalCodeAsync is required when execution may suspend. |
| const context = runtime.newContext() |
| const result = await context.evalCodeAsync(` |
| import * as React from 'esm.sh/react@17' |
| import * as ReactDOMServer from 'esm.sh/react-dom@17/server' |
| const e = React.createElement |
| globalThis.html = ReactDOMServer.renderToStaticMarkup( |
| e('div', null, e('strong', null, 'Hello world!')) |
| ) |
| `) |
| context.unwrapResult(result).dispose() |
| const html = context.getProp(context.global, "html").consume(context.getString) |
| console.log(html) // <div><strong>Hello world!</strong></div> |
| ``` |
| |
| ##### Async on host, sync in QuickJS |
| |
| Here's an example of turning an async function into a sync function inside the |
| VM. |
| |
| ```typescript |
| const context = await newAsyncContext() |
| const path = await import("path") |
| const { promises: fs } = await import("fs") |
| |
| const importsPath = path.join(__dirname, "../examples/imports") + "/" |
| const readFileHandle = context.newAsyncifiedFunction("readFile", async (pathHandle) => { |
| const pathString = path.join(importsPath, context.getString(pathHandle)) |
| if (!pathString.startsWith(importsPath)) { |
| throw new Error("out of bounds") |
| } |
| const data = await fs.readFile(pathString, "utf-8") |
| return context.newString(data) |
| }) |
| readFileHandle.consume((fn) => context.setProp(context.global, "readFile", fn)) |
| |
| // evalCodeAsync is required when execution may suspend. |
| const result = await context.evalCodeAsync(` |
| // Not a promise! Sync! vvvvvvvvvvvvvvvvvvvv |
| const data = JSON.parse(readFile('data.json')) |
| data.map(x => x.toUpperCase()).join(' ') |
| `) |
| const upperCaseData = context.unwrapResult(result).consume(context.getString) |
| console.log(upperCaseData) // 'VERY USEFUL DATA' |
| ``` |
| |
| ### Testing your code |
| |
| This library is complicated to use, so please consider automated testing your |
| implementation. We highly writing your test suite to run with both the "release" |
| build variant of quickjs-emscripten, and also the [DEBUG_SYNC] build variant. |
| The debug sync build variant has extra instrumentation code for detecting memory |
| leaks. |
| |
| The class [TestQuickJSWASMModule] exposes the memory leak detection API, although |
| this API is only accurate when using `DEBUG_SYNC` variant. |
| |
| ```typescript |
| // Define your test suite in a function, so that you can test against |
| // different module loaders. |
| function myTests(moduleLoader: () => Promise<QuickJSWASMModule>) { |
| let QuickJS: TestQuickJSWASMModule |
| beforeEach(async () => { |
| // Get a unique TestQuickJSWASMModule instance for each test. |
| const wasmModule = await moduleLoader() |
| QuickJS = new TestQuickJSWASMModule(wasmModule) |
| }) |
| afterEach(() => { |
| // Assert that the test disposed all handles. The DEBUG_SYNC build |
| // variant will show detailed traces for each leak. |
| QuickJS.assertNoMemoryAllocated() |
| }) |
| |
| it("works well", () => { |
| // TODO: write a test using QuickJS |
| const context = QuickJS.newContext() |
| context.unwrapResult(context.evalCode("1 + 1")).dispose() |
| context.dispose() |
| }) |
| } |
| |
| // Run the test suite against a matrix of module loaders. |
| describe("Check for memory leaks with QuickJS DEBUG build", () => { |
| const moduleLoader = memoizePromiseFactory(() => newQuickJSWASMModule(DEBUG_SYNC)) |
| myTests(moduleLoader) |
| }) |
| |
| describe("Realistic test with QuickJS RELEASE build", () => { |
| myTests(getQuickJS) |
| }) |
| ``` |
| |
| For more testing examples, please explore the typescript source of [quickjs-emscripten][ts] repository. |
| |
| [ts]: https://github.com/justjake/quickjs-emscripten/blob/main/ts |
| [debug_sync]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/modules.md#debug_sync |
| [testquickjswasmmodule]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/classes/TestQuickJSWASMModule.md |
| |
| ### Debugging |
| |
| - Switch to a DEBUG build variant of the WebAssembly module to see debug log messages from the C part of this library. |
| - Set `process.env.QTS_DEBUG` to see debug log messages from the Javascript part of this library. |
| |
| ### More Documentation |
| |
| [Github] | [NPM] | [API Documentation][api] | [Examples][tests] |
| |
| ## Background |
| |
| This was inspired by seeing https://github.com/maple3142/duktape-eval |
| [on Hacker News](https://news.ycombinator.com/item?id=21946565) and Figma's |
| blogposts about using building a Javascript plugin runtime: |
| |
| - [How Figma built the Figma plugin system](https://www.figma.com/blog/how-we-built-the-figma-plugin-system/): Describes the LowLevelJavascriptVm interface. |
| - [An update on plugin security](https://www.figma.com/blog/an-update-on-plugin-security/): Figma switches to QuickJS. |
| |
| ## Status & Roadmap |
| |
| **Stability**: Because the version number of this project is below `1.0.0`, |
| \*expect occasional breaking API changes. |
| |
| **Security**: This project makes every effort to be secure, but has not been |
| audited. Please use with care in production settings. |
| |
| **Roadmap**: I work on this project in my free time, for fun. Here's I'm |
| thinking comes next. Last updated 2022-03-18. |
| |
| 1. Further work on module loading APIs: |
| |
| - Create modules via Javascript, instead of source text. |
| - Scan source text for imports, for ahead of time or concurrent loading. |
| (This is possible with third-party tools, so lower priority.) |
| |
| 2. Higher-level tools for reading QuickJS values: |
| |
| - Type guard functions: `context.isArray(handle)`, `context.isPromise(handle)`, etc. |
| - Iteration utilities: `context.getIterable(handle)`, `context.iterateObjectEntries(handle)`. |
| This better supports user-level code to deserialize complex handle objects. |
| |
| 3. Higher-level tools for creating QuickJS values: |
| |
| - Devise a way to avoid needing to mess around with handles when setting up |
| the environment. |
| - Consider integrating |
| [quickjs-emscripten-sync](https://github.com/reearth/quickjs-emscripten-sync) |
| for automatic translation. |
| - Consider class-based or interface-type-based marshalling. |
| |
| 4. EcmaScript Modules / WebAssembly files / Deno support. This requires me to |
| learn a lot of new things, but should be interesting for modern browser usage. |
| |
| 5. SQLite integration. |
| |
| ## Related |
| |
| - Duktape wrapped in Wasm: https://github.com/maple3142/duktape-eval/blob/main/src/Makefile |
| - QuickJS wrapped in C++: https://github.com/ftk/quickjspp |
| |
| ## Developing |
| |
| This library is implemented in two languages: C (compiled to WASM with |
| Emscripten), and Typescript. |
| |
| ### The C parts |
| |
| The ./c directory contains C code that wraps the QuickJS C library (in ./quickjs). |
| Public functions (those starting with `QTS_`) in ./c/interface.c are |
| automatically exported to native code (via a generated header) and to |
| Typescript (via a generated FFI class). See ./generate.ts for how this works. |
| |
| The C code builds as both with `emscripten` (using `emcc`), to produce WASM (or |
| ASM.js) and with `clang`. Build outputs are checked in, so you can iterate on |
| the Javascript parts of the library without setting up the Emscripten toolchain. |
| |
| Intermediate object files from QuickJS end up in ./build/quickjs/. |
| |
| This project uses `emscripten 3.1.32`. |
| |
| - On ARM64, you should install `emscripten` on your machine. For example on macOS, `brew install emscripten`. |
| - If _the correct version of emcc_ is not in your PATH, compilation falls back to using Docker. |
| On ARM64, this is 10-50x slower than native compilation, but it's just fine on x64. |
| |
| Related NPM scripts: |
| |
| - `yarn update-quickjs` will sync the ./quickjs folder with a |
| github repo tracking the upstream QuickJS. |
| - `yarn make-debug` will rebuild C outputs into ./build/wrapper |
| - `yarn make-release` will rebuild C outputs in release mode, which is the mode |
| that should be checked into the repo. |
| |
| ### The Typescript parts |
| |
| The ./ts directory contains Typescript types and wraps the generated Emscripten |
| FFI in a more usable interface. |
| |
| You'll need `node` and `yarn`. Install dependencies with `yarn install`. |
| |
| - `yarn build` produces ./dist. |
| - `yarn test` runs the tests. |
| - `yarn test --watch` watches for changes and re-runs the tests. |
| |
| ### Yarn updates |
| |
| Just run `yarn set version from sources` to upgrade the Yarn release. |