QuickJSEx

Hex.pmDocs

Embedded QuickJS-NG JavaScript engine for Elixir via Rustler NIF.

Installation

Add quickjs_ex to your list of dependencies in mix.exs:

def deps do
  [
    {:quickjs_ex, "~> 0.3.0"}
  ]
end

On macOS, Linux, and Windows, precompiled NIFs are downloaded automatically for supported targets. If your platform is unsupported, or if you want to build locally, install Rust via rustup and set:

QUICKJS_EX_BUILD=true

Supported precompiled targets in 0.3.0:

Usage

{:ok, rt} = QuickJSEx.start()

{:ok, 3} = QuickJSEx.eval(rt, "1 + 2")
{:ok, "hello"} = QuickJSEx.eval(rt, "'hello'")
{:ok, %{"a" => 1}} = QuickJSEx.eval(rt, "({a: 1})")

{:ok, _} = QuickJSEx.eval(rt, "globalThis.counter = 0")
{:ok, _} = QuickJSEx.eval(rt, "counter += 1")
{:ok, 1} = QuickJSEx.eval(rt, "counter")

QuickJSEx.stop(rt)

Async JavaScript

Top-level await works in eval/2, and call/3 automatically awaits Promise-returning functions:

{:ok, rt} = QuickJSEx.start()

{:ok, values} =
  QuickJSEx.eval(rt, """
  await Promise.all([
    Promise.resolve(1),
    Promise.resolve(2),
    Promise.resolve(3)
  ])
  """)

{:ok, _} =
  QuickJSEx.eval(rt, """
  async function greet(name) {
    return `hi ${name}`;
  }
  """)

{:ok, [1, 2, 3]} = {:ok, values}
{:ok, "hi world"} = QuickJSEx.call(rt, "greet", ["world"])

Calling Functions

Use call/3 to invoke global functions with Elixir values:

{:ok, rt} = QuickJSEx.start()

{:ok, _} =
  QuickJSEx.eval(rt, """
  function sum(values) {
    return values.reduce((acc, n) => acc + n, 0);
  }
  """)

{:ok, 10} = QuickJSEx.call(rt, "sum", [[1, 2, 3, 4]])

ES Modules

Load ES modules built with Vite, esbuild, or Rollup. Named exports are promoted to globalThis, so they can be called with call/3 or accessed with eval/2:

{:ok, rt} = QuickJSEx.start()

:ok = QuickJSEx.load_module(rt, "math", """
export function add(a, b) { return a + b; }
export const PI = 3.14159;
""")

{:ok, 5} = QuickJSEx.call(rt, "add", [2, 3])
{:ok, 3.14159} = QuickJSEx.eval(rt, "PI")

eval/2 also accepts code containing export statements and promotes those exports the same way:

{:ok, rt} = QuickJSEx.start()

{:ok, _} =
  QuickJSEx.eval(rt, """
  const greeting = "hi";
  export function greet(name) { return greeting + " " + name; }
  export const PI = 3.14;
  """)

{:ok, "hi world"} = QuickJSEx.call(rt, "greet", ["world"])
{:ok, 3.14} = QuickJSEx.eval(rt, "PI")

SSR and Browser Stubs

For SSR bundles that expect browser-like globals, start a runtime with browser_stubs: true:

{:ok, rt} = QuickJSEx.start(browser_stubs: true)

:ok = QuickJSEx.load_module(rt, "server", File.read!("priv/static/server.js"))
{:ok, html} = QuickJSEx.call(rt, "render", ["MyComponent", %{count: 0}, %{}])

When enabled, the runtime installs browser-oriented stubs including:

Async render functions are automatically awaited.

Resetting a Runtime

Use reset/1 to clear global state and loaded modules without restarting the GenServer:

{:ok, rt} = QuickJSEx.start()

{:ok, _} = QuickJSEx.eval(rt, "globalThis.answer = 42")
{:ok, 42} = QuickJSEx.eval(rt, "answer")

:ok = QuickJSEx.reset(rt)
{:error, _reason} = QuickJSEx.eval(rt, "answer")

Supervision

children = [
  {QuickJSEx.Runtime, name: MyApp.JS, browser_stubs: true}
]

Supervisor.start_link(children, strategy: :one_for_one)

{:ok, 3} = QuickJSEx.eval(MyApp.JS, "1 + 2")

Architecture

Each QuickJSEx.Runtime owns a dedicated OS thread running a QuickJS-NG context. The BEAM communicates with that thread over channels, so JavaScript execution does not block BEAM schedulers.

flowchart LR
    A[BEAM Process\nGenServer] -- mpsc channel --> B[OS Thread\nQuickJS Runtime + Context]
    B -- result channel --> A

License

MIT