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.
Summary
Since 0.33.0 added HarfBuzz shaping (#735), satori depends on
harfbuzzjs, whose emscripten loader readshb.wasmfrom 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
next buildWhat happens
next buildcompiles, then fails while rendering routes that callsatori()(in our case the OpenGraph and badge image routes):/ROOTis Turbopack's virtual build root. The real file sits atnode_modules/harfbuzzjs/hb.wasm, next tohb.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
harfbuzzjslocates its wasm with emscripten'slocateFile, resolved against__dirname/ a path relative tohb.js. Turbopack (and webpack) trace imports statically and cannot follow that runtime path, sohb.wasmis 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:With this set,
next buildcompletes and shaping works at runtime.Ask
Either of these would help downstream users:
serverExternalPackages: ["harfbuzzjs"]step for Next.js in the satori README or the 0.33 release notes.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.