JS (Popcorn v0.4.0-next.2)

View Source

Popcorn OTP

Run Elixir in a browser with OTP/BEAM compiled to WebAssembly. This prerelease replaces the AtomVM runtime used by Popcorn 0.3.x.

Start with the versioned Popcorn guide. It includes a complete Mix and Vite tutorial.

Install

npm install @swmansion/popcorn@next

Add {:popcorn, "0.4.0-next.2"} to your Elixir application's dependencies. Run mix deps.get before building the JavaScript application. The bundler plugins invoke Mix locally to compile and package application and standard-library code.

Use the toolchain pinned in mise.toml. The packager checks host OTP compatibility against the selected runtime's manifest.

Configure Vite

import { defineConfig } from "vite";
import { popcorn } from "@swmansion/popcorn/vite";

export default defineConfig({
  plugins: [
    popcorn({
      rootDir: "../",
    }),
  ],
});

Set rootDir to the Mix project directory. The current Mix application is the default entrypoint. Set app to select another OTP application explicitly. Use app: null to package the base runtime without starting an application.

The npm package contains two variants:

  • core: without native crypto support.
  • crypto: includes native crypto and ASN.1 support for applications that depend on crypto, public_key, or ssl.

The plugin emits only the selected variant. The browser does not download both. Both variants share one JavaScript API and one Hex package. The plugin selects crypto when the application's dependencies or extraApps require it; otherwise it selects core. Set runtimeVariant: "core" or runtimeVariant: "crypto" to override this choice. An explicit "core" selection produces a build error if the application requires crypto.

Rollup and esbuild plugins accept the same options through @swmansion/popcorn/rollup and @swmansion/popcorn/esbuild. Use ESM output with those bundlers.

Start the runtime

import { Popcorn } from "@swmansion/popcorn";

const result = await Popcorn.init();
if (!result.ok) throw result.error;

const vm = result.data;
// Stop the runtime when the application no longer needs it.
vm.deinit();

Popcorn.init() waits for the OTP application tree to start. Use popcorn.genserver.call() to call a supervised GenServer from JavaScript. Use popcorn.onEvent() to receive events from BEAM processes.

Serve in production

Serve over HTTPS or localhost. Set these headers on the application and runtime responses:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Vite sets these headers for development and preview. Configure your production server separately. Serve .wasm as application/wasm. Serve compressed .tar.gz files with Content-Encoding: gzip for .tar requests. The package also emits uncompressed tar files and Brotli variants. Brotli uses standard effort by default. Set brotliEffort: "max" for maximum compression in release builds.

The JavaScript bridge currently requires a Content Security Policy that permits unsafe-eval. See the versioned Elixir API for interoperability details.

Summary

Types

A message value. JavaScript sends use these BEAM conversions

See JS.Popcorn.Genserver for methods.

VM shutdown notification.

Pid

An opaque BEAM process identifier received from the VM.

Error tags and their structured details. Match tags instead of message text.

A decoded BEAM message payload.

Browser VM configuration.

An operation result. Check ok before accessing data or error. Expected failures use the error branch. Invalid usage can still throw.

Thread counts for schedulers.

Error tag and details without the Error instance or stack.

Terminal dimensions in character cells, from 1 to 65,535 per dimension.

Functions

Creates a BEAM atom value.

Builds beam.emulatorArgs for scheduler counts.

Creates a BEAM tuple.

Types

AnyValue

@type type AnyValue = unknown

A message value. JavaScript sends use these BEAM conversions:

  • Strings become UTF-8 binaries. Booleans become true and false atoms.
  • Integers become integers. Other finite numbers become floats.
  • Arrays become lists.
  • Plain objects become maps with binary string keys.
  • null and undefined become the nil atom.
  • atom() and tuple() create atoms and tuples. PIDs retain their BEAM identity.

Cycles, class instances, functions, symbols, bigints, unsafe integers, and non-finite numbers cause bridge:unserializable.

GenServer

@type type GenServer = {
  call(target: string | Pid, request?: unknown, opts?: CallOpts): Promise<Result<unknown>>;
  cast(target: string | Pid, request?: unknown, opts?: {
    proxy?: string;
  }): Promise<Result<null>>;
}

See JS.Popcorn.Genserver for methods.

OtpErrorPayload

@type type OtpErrorPayload = {
  data: string;
  kind: "abort";
} | {
  data: string;
  kind: "error";
} | {
  data: number;
  kind: "exit";
}

VM shutdown notification.

An exit carries a status code, including zero for a normal exit.

Pid

@type type Pid = {
  readonly [pidBrand]: true;
}

An opaque BEAM process identifier received from the VM.

Valid only with the Popcorn instance and boot that produced it.

PopcornErrors

@type type PopcornErrors = {
  "beam:missing-boot-script": {
    url: string;
  };
  "beam:missing-manifest": {
    url: string;
  };
  "beam:missing-tarball": {
    all: string[];
    name: string;
  };
  "bridge:invalid-target": EmptyData;
  "bridge:listener-not-found": {
    targetName: string;
  };
  "bridge:not-started": EmptyData;
  "bridge:unserializable": UnserializableData;
  "genserver:exit": {
    reason: string;
  };
  "genserver:noproc": {
    target: string;
  };
  "genserver:unserializable": EmptyData;
  "internal:check": {
    detail?: string;
  };
  "internal:unreachable": EmptyData;
  "runtime:eval-unavailable": EmptyData;
  "stdio:overflow": {
    attemptedBytes: number;
    capacityBytes: number;
  };
  "timeout:call": {
    timeoutMs: number;
  };
  "timeout:init": {
    timeoutMs: number;
  };
  "timeout:send": {
    timeoutMs: number;
  };
  "vm:exited": VmExitedData;
  "worker:load": {
    message: string;
  };
}

Error tags and their structured details. Match tags instead of message text.

PopcornEvent

@type type PopcornEvent = AnyValue

A decoded BEAM message payload.

Includes restored PID handles and tracked JavaScript values.

PopcornOpts

@type type PopcornOpts<Output extends TtyOutput = "text"> = {
  beam?: Pick<BeamBootOptions, "emulatorArgs" | "extraArgs" | "env"> & {
    otpAssetsRoot?: string;
  };
  onError?: (event: OtpErrorPayload) => void;
  onStderr?: (chunk: OutputChunk<Output>) => void;
  onStdout?: (chunk: OutputChunk<Output>) => void;
  timeoutsMs?: {
    appStartup?: number;
    boot?: number;
    send?: number;
  };
  tty?: {
    output?: Output;
    size?: TtySize;
  };
  workerUrl?: string | URL;
}

Browser VM configuration.

Properties

  • onError?: (event: OtpErrorPayload) => void - Receives VM errors and exits before shutdown.

Defaults to console output.

  • onStderr?: (chunk: OutputChunk<Output>) => void - Receives stderr.

Defaults to console.error. When tty.output is "bytes", we pass an ArrayBuffer as an argument and string otherwise

  • onStdout?: (chunk: OutputChunk<Output>) => void - Receives stdout.

Defaults to console.log. When tty.output is "bytes", we pass an ArrayBuffer as an argument and string otherwise

  • workerUrl?: string | URL - Module worker URL.

Defaults to the worker included with the package.

Result

@type type Result<T, E extends Tag = Tag> = {
  data: T;
  ok: true;
} | {
  error: PopcornError<E>;
  ok: false;
}

An operation result. Check ok before accessing data or error. Expected failures use the error branch. Invalid usage can still throw.

SchedulerOptions

@type type SchedulerOptions = {
  base: number;
  dirtyCpu: number;
  dirtyIo: number;
}

Thread counts for schedulers.

Each count must be positive.

Properties

  • base: number - Regular schedulers.
  • dirtyCpu: number - Dirty CPU schedulers.
  • dirtyIo: number - Dirty I/O schedulers.

SerializedError

@type type SerializedError<T extends Tag = Tag> = {
  [K in T]: {
    data: PopcornErrors[K];
    t: K;
  };
}[T]

Error tag and details without the Error instance or stack.

TtySize

@type type TtySize = {
  columns: number;
  rows: number;
}

Terminal dimensions in character cells, from 1 to 65,535 per dimension.

Functions

atom(name)

@spec atom(name: string): AtomTerm

Creates a BEAM atom value.

The atom must already exist in the receiving VM. Plain strings encode as binaries.

schedulers(opts)

@spec schedulers(opts: SchedulerOptions): string[]

Builds beam.emulatorArgs for scheduler counts.

Defaults to one scheduler of each type.

tuple(first, second, ...rest)

(variadic)
@spec tuple(first: unknown, second: unknown, ...rest: unknown[]): TupleTerm

Creates a BEAM tuple.

Plain arrays encode as lists.