Spaces:
Sleeping
Sleeping
| # 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. | |