elmc target: wasm + web

Elm → WebAssembly with elmc

elmc lowers the same Elm sources this site uses for elm-pages into a WASM module and a thin JS host. Browser programs boot, update, and render in the host — without shipping Pebble draw/cmd/sub specials into the web graph.

At a glance

  • CompilesIR → plan → WASM + minified host
  • RunsBrowser.* apps, Html/Svg, Http, Time, Task, ports
  • This sitenpm run serve:wasm boots the SPA shell
  • Demo belowelm-3d-scene / WebGL on the WASM path

Live demo

A Pebble-like watch with the Elm tangram (square) on the dial and orbiting logo-colored spheres — rendered with ianmackenzie/elm-3d-scene over elm-explorations/webgl.

No physics solver: procedural orbits on Time.every 64 (~16 Hz) so the rest of the page stays responsive. The same scene is gated on the WASM host with npm run verify:wasm:hero.

Drag to orbit. Watch face is the Elm tangram (square) — ianmackenzie/elm-3d-scene over elm-explorations/webgl.

What the WASM build is

Reachable package code is compiled into the module; unused code can be stripped. There is no separate “port package X to WASM” step — only what the app graph needs. BackendTask route data is evaluated at compile time in Elixir; the browser loads the WASM client plus the thin host.

  • Official site build

    npm run build → elm-pages + official Elm → JS in dist/.

  • WASM parity build

    npm run build:wasm → elmc --target wasm --web → dist/wasm-web/ (app.wasm, host/boot.js, manifest).

  • Strict web apps

    Browser.* / elm/*-only apps keep wasm_strict: true so Pebble specials never enter the web dispatcher.

  • This elm-pages site

    Builds with wasm_strict: false because the combined graph still carries some Pebble-only plan ops; browser-relevant stubs stay gated empty.

What already lowers

The WASM web surface boots real Browser programs end-to-end: init/update/view, subscriptions, cmds, and page data for this elm-pages site. Pebble UI, Cmd, and Events stay out of the web graph.

  • Browser programs

    sandbox, element, document, application, and Platform.worker.

  • Virtual DOM

    Html, Svg, Keyed, Lazy, Html.map, and event handlers.

  • Browser APIs

    Events, Navigation, Dom.focus / viewport / setTitle.

  • Effects

    Task, Process.sleep, Time.now / every / here, Random, Regex.

  • Http & files

    Http expect/bytes/timeout errors; File.Download / Select; BackendTask.Http via fetch.

  • Data & decode

    Bytes, Json.Decode/Encode (structured errors), Parser, Dict/Set equality.

  • elm-pages

    Multi-route bytes on boot and navigation; incoming/outgoing ports with Sub.map / Cmd.map.

  • WebGL / Scene3d

    Host webgl_* bridges; this page’s HeroScene draws on the WASM path.

  • Not in web

    Pebble UI, Cmd, and Events — filtered from the web dispatcher.

How to build it

From elm_pebble_dev/ or the repo root:

npm run build:wasm
npm run verify:wasm
npm run verify:wasm:hero
npm run serve:wasm

# equivalent
./scripts/build-elm-pebble-dev-wasm.sh
elmc compile elm_pebble_dev \
  --out-dir dist/wasm-web \
  --target wasm --web

Artifacts

Under dist/wasm-web/:

  • wasm/app.wasm (+ optional .br)
  • host/boot.js (bundled host)
  • host/browser.html
  • elmc_wasm.manifest.json

Full matrix and boot notes: elmc/docs/WASM_WEB_BUILD.md.

WASM parity

This page’s elm-pages shell boots under elmc WASM (npm run serve:wasm). Index and multi-route page data, ports, and the HeroScene WebGL path are covered by verify:wasm / verify:wasm:hero (and optional Playwright verify:wasm:browser).

Still in motion

RC ownership and Scene3d/WebGL coverage keep widening beyond this demo’s Lambertian meshes. The demo itself stays light on purpose — procedural orbits instead of elm-physics — so docs stay snappy while the toolchain catches up.