Skip to content

feat(plugin): add rspack plugin - #107

Open
pi0x wants to merge 4 commits into
mainfrom
feat/rspack
Open

pi0x wants to merge 4 commits into
mainfrom
feat/rspack

Conversation

@pi0x

@pi0x pi0x commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Adds an rspack plugin alongside the existing rollup-compatible one, exported as unwasm/plugin/rspack.

// rspack.config.mjs
import { unwasmRspack } from "unwasm/plugin/rspack";

export default {
  plugins: [unwasmRspack({ esmImport: true })],
};

Approach

All binding generation (runtime/binding, runtime/utils, tools/parseWasm) is shared with the rollup plugin — only the bundler integration differs.

.wasm requests are redirected during normalModuleFactory.hooks.beforeResolve to a generated module served through rspack's built-in experiments.VirtualModulesPlugin. Since rspack never sees a .wasm module, the native webassembly/async handler (on by default in rspack v2) is bypassed, so no type: "javascript/auto" workaround is needed for the binary itself.

Two rspack-specific adaptations:

  • The \0-prefixed virtual ids become plain unwasm-external: / a real helpers path, since module identifiers cross into the native (Rust) side.
  • esmImport registers a module-import external to keep import() intact, then rewrites specifiers to relative paths and emits the referenced binaries in processAssets — the analogue of the rollup renderChunk pass.

Generation happens at resolve time, which does not survive rebuilds on its own: rspack replays the already-rewritten request for cached dependencies. Bindings are therefore refreshed on watchRun, and a small dependency-declaring loader (src/plugin/rspack-loader.cjs, shipped next to the bundled plugin) ties each binding to its source binary so rspack invalidates it when the .wasm changes.

Tests

test/plugin.test.ts runs the five shared scenarios against rspack (inline, esmImport incl. the Miniflare/Cloudflare eval, module, esm-integration, missing-import), plus rspack-specific tests for mixed ?module/plain imports, unresolvable imports, code splitting under rspack's default asyncChunks, and watch-mode invalidation. 60 passing; lint and types clean.

One shared assertion was loosened: esm-integration-missing-import asserted code === "MISSING_EXPORT", and rspack reports ESModulesLinkingError with no code, so it now matches the code or "was not found", plus a new check that the message names badImportName. rollup/rolldown/vite still satisfy it via the strict disjunct.

@rspack/core is added as a devDependency and an optional peer dependency; the plugin imports it as import type only and reaches builtins via compiler.rspack at runtime.

Note on the asyncChunks: false in the test config

An earlier revision of this description claimed that setting was required because rspack's async chunk loader breaks Miniflare. That is true of the Miniflare test specifically, but it was not the whole story, and the wording implied the plugin was fine under async chunks without that having been verified.

It has now been verified. The failure originally observed with default asyncChunks came from the test's constant output.filename: "index.mjs" colliding with the separate runtime chunk that library.type: "modern-module" emits — not from the plugin. With [name].mjs the build is clean and the output executes correctly, and there is now a regression test covering exactly that.

Summary by CodeRabbit

  • New Features
    • Added an rspack plugin to transform WebAssembly (.wasm) imports into generated JS bindings.
    • Added a new public ./plugin/rspack export for consuming the rspack plugin.
    • Shipped a dedicated rspack dependency loader to ensure rebuilds refresh generated bindings when wasm changes.
  • Documentation
    • Updated README with rspack setup instructions, including the required ESM output setting when esmImport is enabled.
  • Tests
    • Expanded the plugin compatibility and integration coverage for rspack, including missing-wasm, ?module vs plain import behavior, chunking expectations, and watch-mode refresh behavior.

Adds `unwasm/plugin/rspack` alongside the existing rollup-compatible
plugin, reusing the shared binding generation.

Instead of unplugin's loader-based approach, `.wasm` requests are
redirected during `normalModuleFactory.beforeResolve` to a generated
module served through rspack's built-in `VirtualModulesPlugin`. This
keeps the plugin in a single file (rspack loaders must be path strings)
and avoids the native `webassembly/async` handler entirely, since rspack
never sees a `.wasm` module.

Rspack-specific adaptations:

- `\0`-prefixed virtual ids are rewritten to plain schemes, as module
  identifiers cross into the native side.
- `esmImport` registers a `module-import` external so `import()` is
  preserved, then emits the binaries and rewrites the specifiers to
  relative paths in `processAssets` (the `renderChunk` analogue).

The shared `esm-integration-missing-import` assertion now matches on the
error message as well as the code: rspack reports an
`ESModulesLinkingError` without a `code` field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pi0x, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a81a17f-d0b5-4818-8796-cf9b0f4cbdd5

📥 Commits

Reviewing files that changed from the base of the PR and between 3373c06 and e8602f4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • package.json
  • src/plugin/rspack.ts
  • test/plugin.test.ts
📝 Walkthrough

Walkthrough

Rspack support adds a dedicated wasm transformation plugin, package exports and optional peer metadata, documentation, a dependency-tracking loader, and shared test coverage using rspack.

Changes

Rspack integration

Layer / File(s) Summary
Rspack plugin implementation
src/plugin/rspack.ts
Adds unwasmRspack and UnwasmRspackPlugin, generating virtual wasm bindings, caching binaries, emitting assets, and rewriting external imports.
Rspack package exposure and loader wiring
build.config.ts, src/plugin/rspack-loader.cjs, package.json, README.md
Builds and exports the rspack plugin, emits its dependency loader, declares optional rspack integration, and documents configuration and ESM output requirements.
Rspack builder test coverage
test/plugin.test.ts, test/fixture/*
Runs shared and rspack-specific tests, normalizes rspack errors, reads generated ESM outputs, and validates module imports, missing wasm, code splitting, and watch rebuilds.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RspackCompiler
  participant UnwasmRspackPlugin
  participant VirtualModulesPlugin
  participant WasmAssets
  RspackCompiler->>UnwasmRspackPlugin: Resolve wasm request
  UnwasmRspackPlugin->>WasmAssets: Read and register binary
  UnwasmRspackPlugin->>VirtualModulesPlugin: Write generated binding
  RspackCompiler->>UnwasmRspackPlugin: Process output assets
  UnwasmRspackPlugin->>WasmAssets: Emit wasm and rewrite imports
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an Rspack plugin.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rspack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugin/rspack.ts`:
- Around line 221-229: Update the WebAssembly parse-failure handling in
registerAsset to accept and thread opts, then replace the direct
compilation.warnings.push call with the existing warn() helper so opts.silent
suppresses the warning consistently with the ESM fallback path. Preserve the
existing warning message, cause, and moduleFallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e59ef159-8e6d-4688-af2b-d6f120fcd6b3

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0a38 and 006818a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • README.md
  • build.config.ts
  • package.json
  • src/plugin/rspack.ts
  • test/plugin.test.ts

Comment thread src/plugin/rspack.ts Outdated
pi0 and others added 2 commits July 19, 2026 22:11
# Conflicts:
#	README.md
Follow-up fixes from review of the rspack plugin.

- The parse cache was keyed on the binary's content hash alone, so a file
  imported both plainly and as `?module` in one build reused whichever
  interface was registered first. When the `?module` form won, the plain
  import generated `export const default = ...`. The requested mode is now
  part of the binding's identity.

- Editing a `.wasm` never propagated in watch mode. Bindings are virtual
  modules, and rspack replays the already-rewritten request for cached
  dependencies, so `beforeResolve` never saw the binary again. Bindings are
  now refreshed on `watchRun`, and a small dependency-declaring loader ties
  each binding to its source binary so rspack actually invalidates it.

- `resolveSync` throws instead of returning `false`, so the existing guard
  was dead code and unresolved imports surfaced as a plugin stack trace
  rather than rspack's own "Module not found". Requests carrying inline
  loader syntax are skipped for the same reason.

- Emitted binaries were tracked per compiler and never pruned, so watch
  rebuilds kept emitting binaries for imports that no longer existed. They
  are now derived from what the output actually references.

- `silent` did not suppress parse-failure warnings.

Also renames the helpers module so its filename is valid on Windows, and
adds rspack regression tests covering each of the above plus code splitting
under rspack's default `asyncChunks`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugin/rspack.ts`:
- Line 37: Update the dependency-loader path initialization around
DEPENDENCY_LOADER to avoid using import.meta.dirname on unsupported Node
runtimes, or declare the required Node version floor through the package
engines.node configuration. Ensure loading rspack-loader.cjs remains compatible
with all supported runtimes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1586ba4f-033c-46bf-8a6e-3dcc44fb1f57

📥 Commits

Reviewing files that changed from the base of the PR and between 8dd7b13 and 3373c06.

📒 Files selected for processing (6)
  • build.config.ts
  • src/plugin/rspack-loader.cjs
  • src/plugin/rspack.ts
  • test/fixture/missing-wasm.mjs
  • test/fixture/mixed-module-import.mjs
  • test/plugin.test.ts

Comment thread src/plugin/rspack.ts
* Rspack resolves loaders by path, so this ships as a plain `.cjs` file next to
* the built plugin (see `build.config.ts`) rather than being bundled into it.
*/
const DEPENDENCY_LOADER = join(import.meta.dirname, "rspack-loader.cjs");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check declared Node engines and any existing usage of import.meta.dirname
fd -t f 'package.json' -d 2 --exec sh -c 'echo "== {} =="; jq ".engines // empty" {}'
rg -nP 'import\.meta\.dirname' --type=ts

Repository: unjs/unwasm

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== root package.json =="
cat -n package.json | sed -n '1,220p'

echo
echo "== examples/package.json =="
cat -n examples/package.json | sed -n '1,220p'

echo
echo "== references to Node version/support =="
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' -e 'engines\.node|node version|Node\.js|Nodejs|import\.meta\.dirname' .

Repository: unjs/unwasm

Length of output: 3180


Add a Node engine floor import.meta.dirname requires Node 20.11+/21.2+, but package.json doesn’t declare an engines.node constraint. Add one or use a supported path resolver here to avoid load-time failures on older runtimes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugin/rspack.ts` at line 37, Update the dependency-loader path
initialization around DEPENDENCY_LOADER to avoid using import.meta.dirname on
unsupported Node runtimes, or declare the required Node version floor through
the package engines.node configuration. Ensure loading rspack-loader.cjs remains
compatible with all supported runtimes.

Source: Linters/SAST tools

…ache

Follow-up fixes from review of the rspack plugin:

- Move the `esmImport` specifier rewrite from `PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE`
  to `PROCESS_ASSETS_STAGE_OPTIMIZE`, ahead of minification and source map
  generation. The rewrite shortens the line it sits on, so running it after
  `DEV_TOOLING` left the emitted `.map` describing the longer
  `unwasm-external:` text and every mapping to the right of the import
  pointing at the wrong original column. Use `ReplaceSource` so the chunk's
  mappings survive the edit.
- Prune bindings whose module has left the graph. They were kept for the
  compiler's lifetime, regenerated on every rebuild and holding their binary
  in the watch set. Skipped on failed builds, where the graph is incomplete.
- Match JS output assets with a regex, so `.cjs` chunks and names carrying a
  query (`[name].js?[contenthash]`) are rewritten too.
- Treat `?module&foo` and `?foo&module` as module requests, matching the
  Rollup plugin's `?module` handling.
- Escape `EXTERNAL_PREFIX` when building its `RegExp`.

Tests: regression guards for the source map and pruning fixes, both verified
to fail without their fix. Watch test now rejects on build errors instead of
timing out, and derives fixture hashes rather than hard-coding them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants