Skip to content

0.33 harfbuzzjs hb.wasm fails to resolve when satori is bundled (Next.js/Turbopack) #791

Description

@piotrkulpinski

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:

  1. Document the serverExternalPackages: ["harfbuzzjs"] step for Next.js in the satori README or the 0.33 release notes.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions