JS (Popcorn v0.4.0-next.2)
View SourcePopcorn 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-corpVite 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.
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
@type type AnyValue = unknown
A message value. JavaScript sends use these BEAM conversions:
- Strings become UTF-8 binaries. Booleans become
trueandfalseatoms. - Integers become integers. Other finite numbers become floats.
- Arrays become lists.
- Plain objects become maps with binary string keys.
nullandundefinedbecome thenilatom.atom()andtuple()create atoms and tuples. PIDs retain their BEAM identity.
Cycles, class instances, functions, symbols, bigints, unsafe integers, and non-finite numbers cause bridge:unserializable.
@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.
@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.
@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.
@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.
@type type PopcornEvent = AnyValue
A decoded BEAM message payload.
Includes restored PID handles and tracked JavaScript values.
@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.
@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.
@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.
@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.
@type type TtySize = {
columns: number;
rows: number;
}
Terminal dimensions in character cells, from 1 to 65,535 per dimension.
Functions
@spec atom(name: string): AtomTerm
Creates a BEAM atom value.
The atom must already exist in the receiving VM. Plain strings encode as binaries.
@spec schedulers(opts: SchedulerOptions): string[]
Builds beam.emulatorArgs for scheduler counts.
Defaults to one scheduler of each type.
@spec tuple(first: unknown, second: unknown, ...rest: unknown[]): TupleTerm
Creates a BEAM tuple.
Plain arrays encode as lists.