Next.js Bundling a wasm Package Breaks Only in Production
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
An npm package that loads WASM breaks its internal relative path resolution once bundled by Next.js, so it must be externalised with serverExternalPackages and loaded by plain require.
Conclusion
An npm package that loads a wasm file breaks when included in the standard Next.js bundle. If the package resolves its wasm by relative path internally, bundling changes what that path is relative to, and it fails at runtime with a type error.
The fix is to exclude the package from bundling with serverExternalPackages and to explicitly include the wasm itself in the deployment with outputFileTracingIncludes — both, not one.
Symptom
- Requests issuing a document PDF return 502 in production only
- The same failure reproduces locally under
next build && next start(production build mode) - The log shows this type error
TypeError: The "path" argument must be of type string … Received type number
A type error where a string path should be passed. Reviewing the calling code, the value being passed is a string.
Cause
PDF generation uses HarfBuzz (subset-font) to build a font reduced to just the characters used, avoiding dropped CJK glyphs. When subset-font loads its .wasm file, it resolves it relative to its own module.
Next.js builds pack dependency code into a single runtime bundle by default. Once bundled, a module’s physical location changes, and the base for the relative path to the wasm shifts. The package believes it is passing a string path, but the value the bundler rewrote is no longer a string at runtime, and it surfaces as a path type error.
This breakage is caused by the bundling step itself. outputFileTracingIncludes and serverExternalPackages both exist only in Next.js’s build and deployment packaging, so simply running the code never touches them.
The fix
Both settings, together. One alone does not fix it.
1. Exclude from the bundle
// next.config.ts
const nextConfig: NextConfig = {
serverExternalPackages: ["subset-font", "harfbuzzjs", "fontverter"],
};
Now those packages skip webpack/turbopack bundling and are loaded at runtime from node_modules by plain require. Their internal relative path resolution stays as it is in development.
2. Include in the deployment
const nextConfig: NextConfig = {
outputFileTracingIncludes: {
"/**": ["./src/assets/fonts/**", "./node_modules/harfbuzzjs/**"],
},
};
Next.js’s deployment packaging traces file dependencies from static imports in the code. Runtime dynamic reads like fs.readFileSync are not traced, so unless font files and the wasm are included explicitly, the files simply do not exist inside the deployed function.
Specifying only serverExternalPackages stops the package code being bundled, but does not guarantee the wasm it depends on ends up in the deployment package. Only both together satisfy path resolution in the code and the existence of the file.
Why it went unnoticed
Both settings presuppose that a bundling and tracing step exists. outputFileTracingIncludes is part of the mechanism that decides the deployment set by tracing; serverExternalPackages is the mechanism for taking a package out of that bundling. This is a bug you only hit by going through that step, so running the code as usual never reveals it.
The relevant place in next.config.ts now carries a comment with the symptom (the type error text) and the cause (wasm path resolution breaking) — where the next person adding a wasm-requiring package will read it before hitting the same wall.
よくある質問
Q1What error appeared in production?
A runtime TypeError: 'path' argument must be string ... Received type number. Requests generating a document PDF failed with it, surfacing to the user as a 502.
Q2Why are both outputFileTracingIncludes and serverExternalPackages needed?
Different jobs. outputFileTracingIncludes includes files read at runtime with fs.readFileSync (the wasm), which tracing misses. serverExternalPackages keeps the loading JS out of the bundle so its relative paths still resolve. With one, either the file is missing or the path is broken.
Q3Why use HarfBuzz (wasm) to subset the font at all?
The PDF library's built-in subsetting (embedFont with subset: true) has a known bug dropping CJK glyphs. So it runs two passes: render with the full font, collect the characters used, build a reduced TTF with HarfBuzz (subset-font), re-embed. HarfBuzz is wasm, which is how this hit bundling.
Q4Does this apply to other npm packages using wasm?
Yes. Any package that loads a wasm file by require or dynamic import and resolves it by relative path breaks the same way. serverExternalPackages exists precisely to keep such packages out of the bundle.
この記事の根拠
- TypeScriptファイル 12〜20行目コミット f2a4b20
- TypeScriptファイル 108〜126行目
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。