[PR #644] [MERGED] fix: use dynamic WASM imports in og-font-patch for Node.js compat #739

Closed
opened 2026-05-06 13:09:53 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/644
Author: @NathanDrake2406
Created: 3/22/2026
Status: Merged
Merged: 3/23/2026
Merged by: @james-elicx

Base: mainHead: fix/og-wasm-node-compat


📝 Commits (3)

  • 5669b0f fix: use dynamic WASM imports in og-font-patch for Node.js compat
  • c44121b fix: move resvg new URL() inside catch handler for workerd compat
  • 3f96f21 refactor: address review — remove dead atob branch, async readFile, dedup tests

📊 Changes

2 files changed (+266 additions, -26 deletions)

View changed files

📝 packages/vinext/src/index.ts (+72 -26)
tests/og-font-patch.test.ts (+194 -0)

📄 Description

Summary

Fixes #642

vinext start crashes with ERR_MODULE_NOT_FOUND: Cannot find package 'a' when the app uses ImageResponse from next/og. The root cause: the vinext:og-font-patch plugin injects static import X from "./X.wasm?module" statements — a workerd-only convention that Node.js can't handle. Node tries to resolve the WASM binary's emscripten import section (module "a") as an npm package.

Fix: Replace all static WASM imports with dynamic import() calls that try the ?module path (for workerd) and fall back to compiling from bytes (for Node.js). This produces a single build output that runs on both runtimes.

WASM module Size Strategy
Yoga ~70KB Dynamic import + inline base64 fallback
Resvg ~1.3MB Dynamic import + new URL() disk-read fallback

How it works

  • On workerd (Cloudflare Workers): import("./yoga.wasm?module") succeeds → returns a pre-compiled WebAssembly.Module → used directly
  • On Node.js (vinext start): The dynamic import fails → .catch() fires → yoga compiles from inline base64 bytes, resvg reads .wasm from disk via readFileSync + WebAssembly.compile

The key insight is that import() is lazy — failures are catchable at runtime, unlike static import which crashes at module parse time.

Test plan

  • 14 new unit tests in tests/og-font-patch.test.ts
  • Verify no static WASM imports in transform output (critical invariant)
  • Verify yoga dynamic import + base64 fallback
  • Verify resvg dynamic import + disk-read fallback
  • Verify yoga.wasm still written to disk for workerd builds
  • Existing OG inline, shims, and app-router tests pass (1035 tests)

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/644 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 3/22/2026 **Status:** ✅ Merged **Merged:** 3/23/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/og-wasm-node-compat` --- ### 📝 Commits (3) - [`5669b0f`](https://github.com/cloudflare/vinext/commit/5669b0f6e2ce85e44a29f54cc6415470e5446bce) fix: use dynamic WASM imports in og-font-patch for Node.js compat - [`c44121b`](https://github.com/cloudflare/vinext/commit/c44121bd1cdcb96ab193bd0c6da8f61ddacc02b7) fix: move resvg new URL() inside catch handler for workerd compat - [`3f96f21`](https://github.com/cloudflare/vinext/commit/3f96f217b77e9161426a9d7b26c451ad2e36e94d) refactor: address review — remove dead atob branch, async readFile, dedup tests ### 📊 Changes **2 files changed** (+266 additions, -26 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/index.ts` (+72 -26) ➕ `tests/og-font-patch.test.ts` (+194 -0) </details> ### 📄 Description ## Summary Fixes #642 `vinext start` crashes with `ERR_MODULE_NOT_FOUND: Cannot find package 'a'` when the app uses `ImageResponse` from `next/og`. The root cause: the `vinext:og-font-patch` plugin injects static `import X from "./X.wasm?module"` statements — a workerd-only convention that Node.js can't handle. Node tries to resolve the WASM binary's emscripten import section (module `"a"`) as an npm package. **Fix:** Replace all static WASM imports with dynamic `import()` calls that try the `?module` path (for workerd) and fall back to compiling from bytes (for Node.js). This produces a single build output that runs on **both** runtimes. | WASM module | Size | Strategy | |-------------|------|----------| | Yoga | ~70KB | Dynamic import + inline base64 fallback | | Resvg | ~1.3MB | Dynamic import + `new URL()` disk-read fallback | ### How it works - **On workerd (Cloudflare Workers):** `import("./yoga.wasm?module")` succeeds → returns a pre-compiled `WebAssembly.Module` → used directly - **On Node.js (`vinext start`):** The dynamic import fails → `.catch()` fires → yoga compiles from inline base64 bytes, resvg reads `.wasm` from disk via `readFileSync` + `WebAssembly.compile` The key insight is that `import()` is lazy — failures are catchable at runtime, unlike static `import` which crashes at module parse time. ## Test plan - [x] 14 new unit tests in `tests/og-font-patch.test.ts` - [x] Verify no static WASM imports in transform output (critical invariant) - [x] Verify yoga dynamic import + base64 fallback - [x] Verify resvg dynamic import + disk-read fallback - [x] Verify yoga.wasm still written to disk for workerd builds - [x] Existing OG inline, shims, and app-router tests pass (1035 tests) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:09:53 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#739
No description provided.