Skip to content

fix(rolldown-plugin): inline wasm/bin/text content in dev SSR#67

Open
Cyberistic wants to merge 1 commit into
alchemy-run:mainfrom
Cyberistic:fix/vite-ssr-load-hook
Open

fix(rolldown-plugin): inline wasm/bin/text content in dev SSR#67
Cyberistic wants to merge 1 commit into
alchemy-run:mainfrom
Cyberistic:fix/vite-ssr-load-hook

Conversation

@Cyberistic

@Cyberistic Cyberistic commented Jul 16, 2026

Copy link
Copy Markdown

What was broken

@distilled.cloud/cloudflare-rolldown-plugin's additional-modules plugin rewrites import "foo.wasm" (and .bin, .txt, .html, .sql) to a virtual URL like __CLOUDFLARE_MODULE__CompiledWasm__/abs/path/foo.wasm__CLOUDFLARE_MODULE__ and marks it external: true. At build time the renderChunk hook reads the file and writes it as a real asset.

In dev mode renderChunk never runs, so when Vite's Node-side SSR module runner tries to load the virtual id, the file doesn't exist on disk and Vite throws ERR_LOAD_URL with Failed to load url __CLOUDFLARE_MODULE__CompiledWasm__/.../wa-sqlite.wasm... Does the file exist?. The error propagates into the workerd module runner, h3/Nitro catches it, and the user only sees the opaque {"status":500,"unhandled":true,"message":"HTTPError"} JSON.

That's why every dev request for any module that does import "*.wasm", it fails in alchemy v2 even though the same consumer worked in alchemy v1 (where the dev path ran through workerd, not Vite's SSR module runner)

What this PR changes

Adds a load hook to additionalModulesPlugin that matches the virtual id, reads the on-disk file synchronously in the plugin (which runs in Vite's Node main process), and inlines the bytes as a base64 string literal in the returned module source. Vite's module runner then evaluates the returned source inside its new AsyncFunction(...) wrapper.

Why base64 + no import statements: the wrapper is a function body, where top-level import is illegal. The returned source has no imports, it builds the binary with Buffer.from(__b64, 'base64') and exports it as default. Three branches (CompiledWasm / Text / Data) because each consumer expects a different export shape:

type exported value
CompiledWasm Buffer (binary)
Text string (utf-8)
Data base64 string

Why this didn't happen in alchemy v1

v1 didn't ship a vite-plugin-based dev server, it spawned vite dev as a subprocess and read its wrangler.json for the worker config. The vite:react-babel plugin and the cloudflare-rolldown-plugin additional-modules chain were never loaded, so the virtual __CLOUDFLARE_MODULE__CompiledWasm__/... URL was never produced and the SSR load never tried to resolve a wasm through a virtual id. The dev server was workerd, not Vite's SSR module runner.

This PR fixes the v2 path so the same example works locally again.

The `additional-modules` plugin rewrites `import 'foo.wasm'` (and .bin,
.txt, .html, .sql) to a virtual `__CLOUDFLARE_MODULE__CompiledWasm__/<abs>
__CLOUDFLARE_MODULE__` URL so the bundler can inline the file's content
as an asset at build time.

In dev (no `renderChunk`), Vite's Node-side SSR module runner still
resolves that virtual ID and then tries to load it. The 'file' portion
is just a marker — vite's `transformRequest` runs, finds no real file
on disk, and throws `Failed to load url __CLOUDFLARE_MODULE__...`.

Fix: add a `load` hook that matches the virtual id, reads the
.on-disk .wasm / .bin / .txt file here in the plugin (Vite Node main
process), and inlines the bytes as a base64 string literal in the
returned module source.

Why base64: Vite evaluates the returned source inside
`new AsyncFunction(...)`, where top-level `import` statements are
illegal. The returned source has no imports — it builds the buffer
from the inlined string with `Buffer.from(__b64, 'base64')`.

Why CompiledWasm / Text / Data each get their own branch:
`CompiledWasm` and `Data` should produce binary buffers;
`Text` is utf-8 text. Each branch emits a default export of the
correct shape for its consumer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant