AphrodyBun GitHub

Runtime docs · Bundler · WebAssembly

WebAssembly from Rust

Build Rust crates into WebAssembly ES modules with bun wasm build, bun build --target=wasm and bun:wasm

Bun builds a Rust crate into a WebAssembly package that you import like any ES module. It runs cargo build for a wasm32 target, then wasm-bindgen, wasm-opt and writes the .js, .d.ts, .wasm and package.json files. It replaces wasm-pack.

bun wasm build ./crates/hello --outdir ./pkg
index.ts
import init, { greet } from "./pkg/hello.js";

await init();
console.log(greet("Bun"));

bun build --target=wasm is the same command:

bun build --target=wasm ./crates/hello --outdir ./pkg

Requirements

  • A Rust toolchain with the target installed: rustup target add wasm32-unknown-unknown (or wasm32-wasip1, wasm32-wasip2 for WASI).
  • wasm-bindgen and wasm-opt are used from PATH when present. Otherwise Bun installs them once into $BUN_INSTALL/tools: wasm-bindgen-cli with cargo, at the version the crate's Cargo.lock pins, and wasm-opt (binaryen) with bun x.

Import a crate directly

The bun:wasm plugin builds a crate when a module imports its Cargo.toml or one of its .rs files. The package is built into the crate's Cargo target directory and reused while the artifact is unchanged.

preload.ts
import { plugin } from "bun:wasm";

Bun.plugin(plugin());
bunfig.toml
preload = ["./preload.ts"]
index.ts
import init, { greet } from "./crates/hello/src/lib.rs";

await init();
console.log(greet("Bun"));

The same plugin works with Bun.build:

build.ts
import { plugin } from "bun:wasm";

await Bun.build({
  entrypoints: ["./index.ts"],
  outdir: "./dist",
  plugins: [plugin()],
});

Build from JavaScript

build.ts
import { build, optimize } from "bun:wasm";

const result = await build({
  crate: "./crates/hello", // a directory, its Cargo.toml or a .rs file in it
  outdir: "./pkg",
  target: "web",
  optimize: "Oz",
  maxBytes: 200_000,
});
console.log(result.js, result.bytes, result.rawBytes);

// run wasm-opt on any module; a larger result is discarded
await optimize("./module.wasm", { level: "O3" });

outdir is replaced only when the build succeeds, so a failed build keeps the last usable package.

Cargo workspaces and prebuilt artifacts

package selects a member of a Cargo workspace, like cargo build -p. artifact packages a .wasm that cargo already built, for example on a build server, without running cargo.

bun wasm build --package hello --outdir ./pkg
bun wasm build --package hello --artifact ./target/wasm32-unknown-unknown/release/hello.wasm --outdir ./pkg

cargo replaces the command that runs cargo metadata and cargo build, for example a wrapper script or cross. In code it is a path or an array of words; on the command line, quote words that contain spaces. Bun does not run rustup target add for a custom cargo.

bun wasm build --package hello --cargo=./scripts/cargo.sh --outdir ./pkg

WASI

--wasi=p1 builds for wasm32-wasip1. The generated module's default export instantiates the module with node:wasi:

index.ts
import init from "./pkg/tool.js";

const { exports } = await init({ args: ["tool"], env: {}, preopens: { "/": "." } });

--wasi=p2 builds a wasm32-wasip2 component and transpiles it to JavaScript with jco, run through bun x. wasm-opt does not read components, so p2 output is not optimized.

Flags

FlagDescription
-o, --outdir=<dir>Output package. Defaults to pkg in the crate
-p, --package=<name>A package of the Cargo workspace
--artifact=<file>Package a .wasm cargo already built
--cargo=<cmd>Run <cmd> instead of cargo
--target=<t>wasm-bindgen target: web (default), bundler, nodejs, deno, no-modules
--wasi=<p1|p2>Build for WASI preview 1 or preview 2
--triple=<triple>Any Rust target triple
--devDebug profile, no wasm-opt
--profile=<name>Cargo profile. Defaults to release
--opt-level=<level>wasm-opt level: Oz (default), Os, O0 to O4
--no-optSkip wasm-opt
--name=<name>Base name of the generated files
--features=<a,b>Cargo features
--no-default-featuresDisable the crate's default features
--lockedPass --locked to cargo
--no-typescriptDo not emit .d.ts files
--no-packDo not write package.json
--max-bytes=<n>Fail when the optimized .wasm is larger
-- <cargo args>Passed to cargo build

bun wasm opt <in.wasm> runs wasm-opt (-Oz by default) on a module.

If package.json has a wasm script, bun wasm runs that script. Inside the script, bun wasm is the command.