0.33 harfbuzzjs hb.wasm fails to resolve when satori is bundled (Next.js/Turbopack)
Summary
Since 0.33.0 added HarfBuzz shaping (#735), satori depends on harfbuzzjs, whose emscripten loader reads hb.wasm from a path next to its own file. When a bundler processes satori's server code, that path is rewritten and the wasm is never emitted, so shaping fails at build and at runtime.
Environment
- satori 0.33.3
- harfbuzzjs 0.10.0
- Next.js 16.3.2 with Turbopack
- Node 22, production
next build
What happens
next build compiles, then fails while rendering routes that call satori() (in our case the OpenGraph and badge image routes):
failed to asynchronously prepare wasm: Error: ENOENT: no such file or directory,
open '/ROOT/node_modules/harfbuzzjs/hb.wasm'
Aborted(Error: ENOENT: no such file or directory, open '/ROOT/node_modules/harfbuzzjs/hb.wasm')
RuntimeError: Aborted(Error: ENOENT ... /ROOT/node_modules/harfbuzzjs/hb.wasm)./ROOT is Turbopack's virtual build root. The real file sits at node_modules/harfbuzzjs/hb.wasm, next to hb.js, but the bundled code looks for it under the virtual root, where nothing was copied. Pinning satori back to 0.32.x (before the HarfBuzz change) removes the error.
Cause
harfbuzzjs locates its wasm with emscripten's locateFile, resolved against __dirname / a path relative to hb.js. Turbopack (and webpack) trace imports statically and cannot follow that runtime path, so hb.wasm is not traced into the build output.
Workaround
Mark the package external so the bundler leaves it in node_modules, where the loader finds the wasm:
// next.config.ts
const nextConfig = {
serverExternalPackages: ["harfbuzzjs"],
}With this set, next build completes and shaping works at runtime.
Ask
Either of these would help downstream users:
- Document the
serverExternalPackages: ["harfbuzzjs"]step for Next.js in the satori README or the 0.33 release notes. - Reference the wasm in a form bundlers can trace (for example
new URL("hb.wasm", import.meta.url)) so no manual config is needed.
Glad to open a docs PR if option 1 is the preferred path.
Source: vercel/satori