### What? Adds a CI job that checks Turbopack compiles for `wasm32-wasip1-threads`, so wasm portability regressions are caught rather than rediscovered. ### Why? Everything under `#[cfg(target_family = "wasm")]` is invisible to host builds **and** to host clippy — it is only checked when you deliberately build for the target. Four separate defects in this stack were caught only that way (a wrong build-script condition, two bad imports, and a value that compiled but was wrong). Without a gate, the next one lands unnoticed. This also re-enables coverage that had been off for ~2 years: the old `test-next-napi-bindings-wasi` job was disabled with `if: false` pending napi-rs/napi-rs#2009, which closed in April 2024. ### How? Modelled on `rust-check` via `build_reusable.yml` (`needsRust`, `skipInstallBuild`, `skipNativeBuild`), so it does not pay for a JS build. Beyond `rustup target add` it needs two things: - **a WASI C toolchain**, because `lzzzz` (LZ4, via `turbo-persistence`) and `zstd-sys` have C build scripts. The SDK build is selected from `$RUNNER_ARCH` — `build_reusable.yml` defaults to an arm64 runner, and an x86_64 clang fails there with `Exec format error` — with a pinned sha256 per arch, unpacked under `$RUNNER_TEMP` so the workspace stays clean. - **emnapi**, because `next-napi-bindings`' build script calls `napi_build::setup()`, whose wasi path panics without `EMNAPI_LINK_DIR` — so it is required even for `cargo check`. It is installed into a scratch directory rather than the root `package.json`, because this job runs with `skipInstallBuild` and therefore never runs `pnpm install`. Two things worth recording, both of which cost a CI round trip to find: - pnpm is invoked from the repo root with `--dir`, not by `cd`-ing into the scratch directory: corepack resolves the pnpm version from the nearest `package.json`, and outside the repo it picks the latest pnpm (11.x), which cannot run on the pinned Node 20 (`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`). - the job is added to `tests-pass`, whose `needs:` list is what actually blocks a PR — a job that runs but is absent from that list looks like coverage while blocking nothing. The `emnapi@2.0.0-alpha.4` pin is deliberate and is the one fragility here: the archive must define `emnapi_create_env` / `emnapi_delete_env`, which exists only in emnapi v2, still a prerelease. Move to the stable release once it ships. <!-- fleet b6d0486f-97c7-42a7-bdaf-3490774cdec3 --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
4.1 KiB
Building Turbopack for WebAssembly
Turbopack's napi bindings (crates/next-napi-bindings) can be compiled for
wasm32-wasip1-threads. This is the target that would let Turbopack run where no native binding
exists — unusual CPU architectures and operating systems we do not publish artifacts for.
This is distinct from the
@next/swc-wasm-*packages, which are built withwasm-packfromcrates/wasmforwasm32-unknown-unknownand contain SWC only, not Turbopack.
Prerequisites
Beyond the usual Rust toolchain, the target needs a WASI clang and sysroot (several dependencies have
C build scripts) and emnapi (napi's build script links against it). scripts/setup-wasi-env.sh
provisions both.
Building on Linux
The setup script supports Linux systems directly. Source it, then build as usual:
source scripts/setup-wasi-env.sh
cargo check -p next-napi-bindings --target wasm32-wasip1-threads
It must be sourced from Bash because it uses BASH_SOURCE; alternative shells such as zsh should
invoke Bash explicitly:
bash -c 'source scripts/setup-wasi-env.sh && cargo check -p next-napi-bindings --target wasm32-wasip1-threads'
The script exports environment variables into the calling shell, which a subprocess cannot do. Running it directly prints an error and does nothing.
The script downloads the wasi-sdk matching your host architecture, verifies it against a pinned
sha256, installs emnapi, and exports the cross-compilation variables (WASI_SDK_PATH,
EMNAPI_LINK_DIR, and the *_wasm32_wasip1_threads compiler variables). Downloads are cached under
~/.cache/next-wasi-toolchain, so re-sourcing in a new shell is fast. Set WASI_SETUP_CACHE_DIR to
move the cache.
CI sources the same script, so local and CI builds cannot drift apart.
Building with Docker
On macOS, Windows, or any other Docker-capable host, use the Linux builder image:
docker build -t next-wasi-builder -f scripts/wasi-builder.Dockerfile .
docker run --rm -it \
-v "$PWD:/workspace" \
-v next-wasi-cache:/root/.cache/next-wasi-toolchain \
-w /workspace \
next-wasi-builder
Inside the container, use the same commands as CI:
source scripts/setup-wasi-env.sh
cargo check -p next-napi-bindings --target wasm32-wasip1-threads
The named volume preserves the wasi-sdk and emnapi downloads between runs. Mount a second volume at
/workspace/target if you also want to preserve Rust build artifacts without writing them to the host
checkout.
Running tests
Wasm test binaries cannot be run directly: turbo-tasks gathers its task registries at link time and
needs the embedder to supply an env.read_custom_section import. scripts/wasi-test-host/ is a Node
host that provides it, along with WASI preview1 and thread spawning.
Sourcing the setup script points Cargo's runner at that host, so tests work with the normal command:
source scripts/setup-wasi-env.sh
cargo test -p turbo-tasks --lib --target wasm32-wasip1-threads
Some tests are skipped on wasm; each carries a reason describing the specific platform limitation (no unwinding, no mmap, and so on).
Linting
Use --lib --tests rather than --all-targets when running clippy for wasm. --all-targets includes
benchmark targets, which depend on criterion (and so rayon) and cannot build for WASI:
cargo clippy -p turbo-tasks --lib --tests --target wasm32-wasip1-threads -- -D warnings
Known limitations
- emnapi v2 is a prerelease.
napi-buildneeds theemnapi_create_env/emnapi_delete_envexports, which exist only in v2, so the script pinsemnapi@2.0.0-alpha.4. Move to the stable release once it ships. - A full
napi buildis not wired up yet, for the same reason — CI currently compiles and runs tests rather than producing a publishable artifact. - The JS side cannot load the artifact yet. It needs a loader that supplies
env.read_custom_sectionand runs the module's initialization, which does not exist. - Some features are unavailable on wasm and report an error when configured: SWC wasm plugins, and anything requiring the child-process pool.