unplugin-rust-wasm-pack
v0.1.0
Published
Unplugin that runs wasm-pack on virtual Cargo.toml imports for Vite, Rollup, Rolldown, and Webpack
Maintainers
Readme
unplugin-rust-wasm-pack
Import Rust/WASM crates in Vite, Rollup, Rolldown, and Webpack via virtual Cargo.toml imports. The plugin runs wasm-pack automatically, watches Rust sources in dev, generates TypeScript shims, and warns when required Vite peer plugins are missing.
Table of contents
- How it works
- Prerequisites
- Installation
- Quick start (Vite)
- Rust crate setup
- Import patterns
- TypeScript
- Required peer plugins (Vite)
- Configuration
- SSR
- Rollup / Rolldown
- Webpack
- Monorepo / crates outside project root
- Caching and rebuilds
- API reference
- Troubleshooting
- Examples
How it works
- You import a crate like any other module:
import { greet } from './rust/my-crate/Cargo.toml'. - The plugin intercepts
Cargo.tomlspecifiers, resolves the crate root, and runswasm-pack buildwhen sources or options change. - The virtual import is redirected to wasm-pack's generated JS glue in
pkg/(client) orpkg-node/(SSR). - A
Cargo.toml.d.tsshim is written next to each crate so TypeScript sees wasm-bindgen's exports. - In dev, Rust file changes trigger a rebuild and Vite HMR invalidation (when enabled).
app.ts
└── import './rust/my-crate/Cargo.toml'
└── wasm-pack → rust/my-crate/pkg/my_crate.js + my_crate_bg.wasmPrerequisites
Installation
pnpm add -D unplugin-rust-wasm-packFor Vite with the default bundler target, also install the peer plugins:
pnpm add -D vite-plugin-wasm vite-plugin-top-level-awaitQuick start (Vite)
// vite.config.ts
import { defineConfig } from 'vite'
import RustWasmPack from 'unplugin-rust-wasm-pack/vite'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
export default defineConfig({
plugins: [
RustWasmPack(),
wasm(),
topLevelAwait(),
],
})// app.ts
import { greet } from './rust/my-crate/Cargo.toml'
console.log(greet('world'))Rust crate setup
Each WASM crate needs a [lib] section with crate-type = ["cdylib"] and typically wasm-bindgen:
[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}Place the crate anywhere your bundler can resolve — e.g. rust/my-crate/ next to src/.
Import patterns
Static import — simplest; wasm is built on first resolve:
import { greet } from './rust/my-crate/Cargo.toml'Dynamic import — code-split or defer loading (recommended for client-only crates in SSR apps):
if (!import.meta.env.SSR) {
const { double } = await import('./rust/client-fx/Cargo.toml')
}Web Workers — import Cargo.toml inside the worker entry; add wasm + top-level-await to config.worker.plugins in Vite.
Multiple crates — each Cargo.toml path is a separate virtual module. See examples/vite-multi-crate.
TypeScript
On first import, the plugin writes Cargo.toml.d.ts next to each crate. That shim re-exports wasm-pack's generated types from pkg/, so imports like import { greet } from './rust/my-crate/Cargo.toml' are fully typed.
Disable shim generation with typesShim: false if you maintain your own declarations or import wasm-pack output directly.
Add a tsconfig.json that includes your source and generated shims:
{
"compilerOptions": {
"moduleResolution": "bundler",
"strict": true,
"noEmit": true
},
"include": ["src", "rust/**/*.d.ts"]
}The shim is regenerated when wasm-pack rebuilds. You can gitignore **/Cargo.toml.d.ts if you prefer — it is recreated on the next dev/build.
Import option types from the package:
import type { Options, CrateConfig, CrateRuntime } from 'unplugin-rust-wasm-pack'Required peer plugins (Vite + bundler target)
When target is bundler (default), Vite needs these plugins to load .wasm and top-level await:
| Package | Purpose |
|---------|---------|
| vite-plugin-wasm | Import .wasm files |
| vite-plugin-top-level-await | Top-level await in glue code |
The plugin warns at build time if either is missing. Disable with checkPeers: false.
Workers: If you use config.worker.plugins, add wasm + top-level-await there too.
Configuration
import RustWasmPack from 'unplugin-rust-wasm-pack/vite'
RustWasmPack({
crates: [{ root: './rust/my-crate', outDir: 'pkg', runtime: 'both' }],
target: 'bundler',
browser: false,
devProfile: 'dev',
buildProfile: 'release',
profile: undefined,
noOpt: false,
weakRefs: false,
referenceTypes: false,
features: [],
noDefaultFeatures: false,
outDir: 'pkg',
ssrOutDir: 'pkg-node',
defaultRuntime: 'client',
wasmPackArgs: [],
cacheDir: 'node_modules/.cache/unplugin-rust-wasm-pack',
hmr: true,
checkPeers: true,
typesShim: true,
})Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| crates | CrateConfig[] | — | Allowlist of crate roots with per-crate overrides. Omit to accept any resolved Cargo.toml. |
| target | 'bundler' \| 'web' \| 'nodejs' | 'bundler' | wasm-pack --target for client builds. SSR always uses nodejs. |
| browser | boolean | false | Pass --browser to wasm-bindgen (wasm-pack build -- --browser). |
| devProfile | 'dev' \| 'profiling' | 'dev' | Dev build profile: --dev or --profiling. |
| buildProfile | 'release' \| 'dev' | 'release' | Production build profile: --release or --dev. |
| profile | string | — | Custom Cargo profile (wasm-pack build --profile <name>). Overrides devProfile / buildProfile. |
| noOpt | boolean | false | Skip wasm-opt (wasm-pack build --no-opt). Faster builds; larger output. |
| weakRefs | boolean | false | Enable JS weak references (--weak-refs). |
| referenceTypes | boolean | false | Enable WebAssembly reference types (--reference-types). |
| features | string \| string[] | — | Cargo features (wasm-pack build -- --features <list>). |
| noDefaultFeatures | boolean | false | Disable default Cargo features (-- --no-default-features). |
| outDir | string | 'pkg' | Default client output dir, relative to each crate root. |
| ssrOutDir | string | 'pkg-node' | Default SSR (nodejs) output dir, relative to each crate root. |
| defaultRuntime | 'client' \| 'server' \| 'both' | 'client' | Default runtime for crates without a crates entry. |
| wasmPackArgs | string[] | [] | Extra args appended after plugin-managed flags (e.g. ['--out-name', 'custom']). For uncommon cargo flags, prefer features / profile options first. |
| cacheDir | string | node_modules/.cache/unplugin-rust-wasm-pack | Build-cache metadata directory. |
| hmr | boolean | true | Invalidate Vite modules when Rust rebuilds (Vite only). |
| checkPeers | boolean | true | Warn when Vite peer plugins are missing. |
| typesShim | boolean | true | Write Cargo.toml.d.ts type shims next to each crate. |
Per-crate config (CrateConfig)
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| root | string | — | Path to the crate directory (folder containing Cargo.toml). |
| outDir | string | plugin outDir | Client wasm-pack output, relative to root. |
| ssrOutDir | string | plugin ssrOutDir | SSR wasm-pack output, relative to root. |
| runtime | 'client' \| 'server' \| 'both' | plugin defaultRuntime | Where this crate's WASM may run. See SSR. |
| devProfile | 'dev' \| 'profiling' | plugin devProfile | Per-crate dev profile override. |
| buildProfile | 'release' \| 'dev' | plugin buildProfile | Per-crate production profile override. |
| profile | string | plugin profile | Per-crate custom Cargo profile. |
| noOpt | boolean | plugin noOpt | Per-crate wasm-opt skip. |
| weakRefs | boolean | plugin weakRefs | Per-crate weak references. |
| referenceTypes | boolean | plugin referenceTypes | Per-crate reference types. |
| features | string \| string[] | plugin features | Per-crate Cargo features. |
| noDefaultFeatures | boolean | plugin noDefaultFeatures | Per-crate no-default-features. |
| wasmPackArgs | string[] | [] | Extra args appended after global wasmPackArgs. |
Dev builds use the resolved dev profile (--dev or --profiling); production uses --release or --dev per buildProfile, unless profile is set. Builds write to a staging directory first, then atomically replace pkg/ to avoid partial output.
Examples:
RustWasmPack({
noOpt: true,
devProfile: 'profiling',
features: 'console_error_panic_hook',
crates: [
{ root: './rust/heavy', features: 'simd', profile: 'release-with-debug' },
],
})SSR
Each crate can run on the client, server, or both via the runtime option:
| runtime | Client bundle | SSR server | Use case |
|-----------|---------------|------------|----------|
| 'client' (default) | wasm-pack bundler target | empty stub | Browser-only work (canvas, WebGL, lazy UI) |
| 'server' | empty stub | wasm-pack nodejs target | SEO, auth, data prep — never shipped to browser |
| 'both' | bundler target | nodejs target | Shared logic (formatting, parsing) on server and client |
RustWasmPack({
crates: [
{ root: './rust/shared', runtime: 'both' },
{ root: './rust/client-fx', runtime: 'client' },
{ root: './rust/server-meta', runtime: 'server' },
],
})Server builds emit nodejs glue to pkg-node/ (override with ssrOutDir). During vite build --ssr, nodejs glue and .wasm files are copied to dist/server/wasm/<crate-name>/.
Client-only crates resolve to export {} on the server — use dynamic import if you want to defer loading:
if (!import.meta.env.SSR) {
const { double } = await import('./rust/client-fx/Cargo.toml')
}See examples/vite-ssr for all three modes in one app.
Rollup / Rolldown
import RustWasmPack from 'unplugin-rust-wasm-pack/rollup'
// or
import RustWasmPack from 'unplugin-rust-wasm-pack/rolldown'No Vite peer plugins are required. Rollup 4+ has native wasm support; otherwise ensure your setup handles .wasm imports.
When target is 'bundler', the plugin still warns if wasm-loading plugins are missing from the Rollup plugin list.
Webpack
import RustWasmPack from 'unplugin-rust-wasm-pack/webpack'
export default {
mode: 'production',
experiments: { asyncWebAssembly: true },
module: {
rules: [{ test: /\.wasm$/, type: 'webassembly/async' }],
},
plugins: [RustWasmPack()],
}Webpack 5 requires experiments.asyncWebAssembly: true for wasm-pack bundler target glue. The plugin warns at compile time if it is missing (checkPeers: true).
No Vite peer plugins are required. Rust file changes trigger rebuilds via webpack watch / dev server.
Monorepo / crates outside project root
Crates outside the Vite root are supported. The plugin uses fs.watch on the crate directory so changes are picked up even when Vite's watcher does not cover them.
Use crates to pin roots explicitly:
RustWasmPack({
crates: [{ root: '../crates/compute' }],
})Caching and rebuilds
- Build metadata is stored under
cacheDir(defaultnode_modules/.cache/unplugin-rust-wasm-pack). - wasm-pack runs only when crate sources,
Cargo.toml, or relevant plugin options change. - Concurrent imports for the same crate share a single in-flight build.
- Delete
cacheDirandpkg/if you suspect stale output.
API reference
Package exports
| Import path | Default export | Named exports |
|-------------|----------------|---------------|
| unplugin-rust-wasm-pack | unplugin instance | rustWasmPack, vite, rollup, rolldown, webpack, types |
| unplugin-rust-wasm-pack/vite | Vite plugin | same as main |
| unplugin-rust-wasm-pack/rollup | Rollup plugin | same as main |
| unplugin-rust-wasm-pack/rolldown | Rolldown plugin | same as main |
| unplugin-rust-wasm-pack/webpack | Webpack plugin | same as main |
Public types
All types are exported from every entry path. Full JSDoc is available in your editor and in dist/*.d.ts.
| Type | Description |
|------|-------------|
| Options | Plugin configuration |
| CrateConfig | Per-crate overrides inside Options.crates |
| CrateRuntime | 'client' \| 'server' \| 'both' |
| WasmPackTarget | 'bundler' \| 'web' \| 'nodejs' |
| DevProfile | 'dev' \| 'profiling' |
| BuildProfile | 'release' \| 'dev' |
Troubleshooting
| Issue | Fix |
|-------|-----|
| cdylib error | Add [lib] crate-type = ["cdylib"] to Cargo.toml |
| Stale output | Delete pkg/, pkg-node/, and node_modules/.cache/unplugin-rust-wasm-pack |
| .wasm import fails in Vite | Add vite-plugin-wasm and vite-plugin-top-level-await |
| wasm-pack not found | cargo install wasm-pack |
| HMR not updating | ABI-breaking Rust changes may need a full page reload |
| Types missing in IDE | Ensure rust/**/*.d.ts is in tsconfig include; trigger a build so shims are generated |
| Crate not resolved | Check the import path ends with Cargo.toml; add the root to crates if using an allowlist |
Examples
| Example | What it tests | Run |
|---------|---------------|-----|
| vite-basic | Minimal Vite + Cargo.toml import | cd examples/vite-basic && pnpm dev |
| vite-worker | WASM in a dedicated Web Worker | cd examples/vite-worker && pnpm dev |
| vite-shared-worker | WASM in a SharedWorker | cd examples/vite-shared-worker && pnpm dev |
| vite-lazy | Dynamic import() of Cargo.toml | cd examples/vite-lazy && pnpm dev |
| vite-ssr | Client / server / both WASM runtimes | cd examples/vite-ssr && pnpm dev |
| vite-monorepo | Crate outside Vite root | cd examples/vite-monorepo && pnpm dev |
| vite-multi-crate | Two Cargo.toml imports in one app | cd examples/vite-multi-crate && pnpm dev |
| rollup-basic | Rollup (no Vite peers) | cd examples/rollup-basic && pnpm build |
| webpack-basic | Webpack 5 + asyncWebAssembly | cd examples/webpack-basic && pnpm build |
Build all examples (requires Rust + wasm-pack):
pnpm examples:buildLicense
MIT
