├── .yarnrc.yml ├── tests ├── src │ ├── bar.js │ ├── foo │ │ ├── package.json │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── foo.js │ └── bar │ │ ├── src │ │ └── lib.rs │ │ └── Cargo.toml ├── .gitignore ├── Cargo.toml ├── dist │ └── index.html └── rollup.config.js ├── example ├── .gitignore ├── rust-toolchain.toml ├── dist │ └── index.html ├── src │ └── lib.rs ├── Cargo.toml ├── package.json └── rollup.config.js ├── .gitignore ├── src ├── wasm-opt.js ├── typescript.js ├── wasm-bindgen.js ├── cargo.js ├── utils.js └── index.js ├── package.json └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /tests/src/bar.js: -------------------------------------------------------------------------------- 1 | import "./bar/Cargo.toml"; 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | /target 4 | /dist/js 5 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | Cargo.lock 4 | yarn.lock 5 | /target 6 | /dist/js 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Cargo.lock 3 | yarn.lock 4 | package-lock.json 5 | yarn-error.log 6 | /typescript 7 | /example-multi 8 | .DS_Store 9 | .yarn 10 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/bar", 4 | "src/foo", 5 | ] 6 | resolver = "2" 7 | 8 | [workspace.package] 9 | edition = "2018" 10 | -------------------------------------------------------------------------------- /example/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-11-20" 3 | components = [ "rust-std", "rust-src", "rustfmt", "clippy" ] 4 | targets = [ "wasm32-unknown-unknown" ] 5 | -------------------------------------------------------------------------------- /tests/src/foo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "foo", 4 | "author": "You ", 5 | "version": "0.1.0", 6 | "devDependencies": { 7 | "nop": "^1.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/src/foo.js: -------------------------------------------------------------------------------- 1 | import * as foo1 from "./foo/Cargo.toml?custom"; 2 | import * as foo2 from "./foo/Cargo.toml?custom"; 3 | 4 | await foo1.init({ module: foo1.module }); 5 | await foo2.init({ module: foo2.module }); 6 | -------------------------------------------------------------------------------- /example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::console; 3 | 4 | 5 | #[wasm_bindgen(start)] 6 | pub fn main_js() { 7 | console_error_panic_hook::set_once(); 8 | 9 | console::log_1(&JsValue::from("Hello world!")); 10 | } 11 | -------------------------------------------------------------------------------- /tests/src/bar/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::console; 3 | 4 | 5 | #[wasm_bindgen(start)] 6 | pub fn main_js() { 7 | console_error_panic_hook::set_once(); 8 | 9 | console::log_1(&JsValue::from("Bar!")); 10 | } 11 | -------------------------------------------------------------------------------- /tests/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | authors = ["You "] 5 | edition = "2018" 6 | categories = ["wasm"] 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2.58" 10 | console_error_panic_hook = "0.1.6" 11 | 12 | [dependencies.web-sys] 13 | version = "0.3.35" 14 | features = [ 15 | "console", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/src/bar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bar" 3 | version = "0.1.0" 4 | authors = ["You "] 5 | edition.workspace = true 6 | categories = ["wasm"] 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2.58" 10 | console_error_panic_hook = "0.1.6" 11 | 12 | [dependencies.web-sys] 13 | version = "0.3.35" 14 | features = [ 15 | "console", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/src/foo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "0.1.0" 4 | authors = ["You "] 5 | edition.workspace = true 6 | categories = ["wasm"] 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2.58" 10 | console_error_panic_hook = "0.1.6" 11 | 12 | [dependencies.web-sys] 13 | version = "0.3.35" 14 | features = [ 15 | "console", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/src/foo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::console; 3 | 4 | 5 | #[wasm_bindgen(inline_js = "export function foo() { return 5; }")] 6 | extern "C" { 7 | fn foo() -> u32; 8 | } 9 | 10 | 11 | #[wasm_bindgen(module = "nop")] 12 | extern "C" { 13 | #[wasm_bindgen(js_name = default)] 14 | fn nop(); 15 | } 16 | 17 | 18 | #[wasm_bindgen(start)] 19 | pub fn main_js() { 20 | console_error_panic_hook::set_once(); 21 | 22 | console::log_1(&JsValue::from(format!("Foo! {:?} {}", nop(), foo()))); 23 | } 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "name": "example", 5 | "author": "You ", 6 | "version": "0.1.0", 7 | "scripts": { 8 | "build": "rimraf dist/js && rollup --config", 9 | "watch": "rimraf dist/js && rollup --config --watch" 10 | }, 11 | "devDependencies": { 12 | "@wasm-tool/rollup-plugin-rust": "portal:..", 13 | "binaryen": "^125.0.0", 14 | "rimraf": "^6.1.2", 15 | "rollup": "^4.53.3", 16 | "rollup-plugin-livereload": "^2.0.5", 17 | "rollup-plugin-serve": "^3.0.0", 18 | "rollup-plugin-terser": "^7.0.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/rollup.config.js: -------------------------------------------------------------------------------- 1 | import rust from "../src/index.js"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | 5 | export default { 6 | input: { 7 | foo: "./src/foo.js", 8 | bar: "./src/bar.js", 9 | qux: "./src/foo/Cargo.toml", 10 | }, 11 | output: { 12 | dir: "dist/js", 13 | format: "es", 14 | sourcemap: true, 15 | }, 16 | plugins: [ 17 | nodeResolve(), 18 | 19 | commonjs(), 20 | 21 | rust({ 22 | extraArgs: { 23 | wasmBindgen: ["--debug", "--keep-debug"], 24 | }, 25 | verbose: true, 26 | }), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /example/rollup.config.js: -------------------------------------------------------------------------------- 1 | import rust from "@wasm-tool/rollup-plugin-rust"; 2 | import serve from "rollup-plugin-serve"; 3 | import livereload from "rollup-plugin-livereload"; 4 | import { terser } from "rollup-plugin-terser"; 5 | 6 | const is_watch = !!process.env.ROLLUP_WATCH; 7 | 8 | export default { 9 | input: { 10 | example: "Cargo.toml", 11 | }, 12 | output: { 13 | dir: "dist/js", 14 | format: "es", 15 | sourcemap: true, 16 | }, 17 | plugins: [ 18 | rust(), 19 | 20 | is_watch && serve({ 21 | contentBase: "dist", 22 | open: true, 23 | }), 24 | 25 | is_watch && livereload("dist"), 26 | 27 | !is_watch && terser(), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /src/wasm-opt.js: -------------------------------------------------------------------------------- 1 | import * as $path from "node:path"; 2 | import { getEnv, debug, spawn, mv } from "./utils.js"; 3 | 4 | 5 | // Replace with @webassemblyjs/wasm-opt ? 6 | export async function run({ dir, input, output, extraArgs, verbose }) { 7 | const isWindows = (process.platform === "win32"); 8 | 9 | // Needed to make wasm-opt work on Windows 10 | const bin = getEnv("WASM_OPT_BIN", (isWindows ? "wasm-opt.cmd" : "wasm-opt")); 11 | 12 | const args = [input, "--output", output].concat(extraArgs); 13 | 14 | if (verbose) { 15 | debug(`Running ${bin} ${args.join(" ")}`); 16 | } 17 | 18 | try { 19 | await spawn(bin, args, { cwd: dir, shell: isWindows, stdio: "inherit" }); 20 | 21 | } catch (e) { 22 | return e; 23 | } 24 | 25 | await mv($path.join(dir, output), $path.join(dir, input)); 26 | 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wasm-tool/rollup-plugin-rust", 3 | "author": "Pauan ", 4 | "description": "Rollup plugin for bundling and importing Rust crates.", 5 | "version": "3.1.4", 6 | "license": "MIT", 7 | "repository": "github:wasm-tool/rollup-plugin-rust", 8 | "homepage": "https://github.com/wasm-tool/rollup-plugin-rust#readme", 9 | "bugs": "https://github.com/wasm-tool/rollup-plugin-rust/issues", 10 | "type": "module", 11 | "main": "src/index.js", 12 | "scripts": { 13 | "test:foo": "cd tests/src/foo && yarn install", 14 | "test": "yarn test:foo && cd tests && rimraf dist/js && rollup --config", 15 | "test:watch": "yarn test:foo && cd tests && rimraf dist/js && rollup --config --watch", 16 | "test:serve": "live-server tests/dist" 17 | }, 18 | "directories": { 19 | "example": "example" 20 | }, 21 | "keywords": [ 22 | "rollup-plugin", 23 | "vite-plugin", 24 | "rust-wasm", 25 | "wasm", 26 | "rust", 27 | "rollup", 28 | "plugin", 29 | "webassembly", 30 | "wasm-bindgen", 31 | "wasm-pack" 32 | ], 33 | "dependencies": { 34 | "@iarna/toml": "^2.2.5", 35 | "@rollup/pluginutils": "^5.3.0", 36 | "chalk": "^5.6.2", 37 | "glob": "^13.0.0", 38 | "node-fetch": "^3.3.2", 39 | "rimraf": "^6.1.2", 40 | "tar": "^7.5.2" 41 | }, 42 | "devDependencies": { 43 | "@rollup/plugin-commonjs": "^29.0.0", 44 | "@rollup/plugin-node-resolve": "^16.0.3", 45 | "binaryen": "^125.0.0", 46 | "live-server": "^1.2.2", 47 | "rollup": "^4.53.3" 48 | }, 49 | "peerDependencies": { 50 | "binaryen": "*" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/typescript.js: -------------------------------------------------------------------------------- 1 | import * as $path from "node:path"; 2 | import { writeString, readString, mkdir } from "./utils.js"; 3 | 4 | 5 | function trim(s) { 6 | return s.replace(/\n\n\n+/g, "\n\n").replace(/\n\n+\}/g, "\n}").trim(); 7 | } 8 | 9 | 10 | function parse(declaration) { 11 | declaration = declaration.replace(/export type InitInput = [\s\S]*/g, ""); 12 | return trim(declaration); 13 | } 14 | 15 | 16 | export async function writeCustom(name, typescriptDir, inline, synchronous) { 17 | const outPath = $path.join(typescriptDir, name + "_custom.d.ts"); 18 | 19 | let output; 20 | 21 | if (synchronous) { 22 | output = `export type Module = BufferSource | WebAssembly.Module; 23 | 24 | export type InitOutput = typeof import("./${name}"); 25 | 26 | export interface InitOptions { 27 | module: Module; 28 | 29 | memory?: WebAssembly.Memory; 30 | } 31 | 32 | export const module: Uint8Array; 33 | 34 | export function init(options: InitOptions): InitOutput; 35 | `; 36 | 37 | } else { 38 | output = `export type Module = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 39 | 40 | export type InitOutput = typeof import("./${name}"); 41 | 42 | export interface InitOptions { 43 | module: Module | Promise; 44 | 45 | memory?: WebAssembly.Memory; 46 | } 47 | 48 | export const module: ${inline ? "Uint8Array" : "URL"}; 49 | 50 | export function init(options: InitOptions): Promise; 51 | `; 52 | } 53 | 54 | await writeString(outPath, output); 55 | } 56 | 57 | 58 | export async function write(name, typescriptDir, outDir) { 59 | const realPath = $path.join(outDir, "index.d.ts"); 60 | 61 | const [declaration] = await Promise.all([ 62 | readString(realPath), 63 | 64 | mkdir(typescriptDir), 65 | ]); 66 | 67 | await writeString($path.join(typescriptDir, name + ".d.ts"), parse(declaration)); 68 | } 69 | -------------------------------------------------------------------------------- /src/wasm-bindgen.js: -------------------------------------------------------------------------------- 1 | import * as $path from "node:path"; 2 | import * as $tar from "tar"; 3 | import $fetch from "node-fetch"; 4 | import { getVersion } from "./cargo.js"; 5 | import { exec, mkdir, getCacheDir, tar, exists, spawn, info, debug, getEnv } from "./utils.js"; 6 | 7 | 8 | const WASM_BINDGEN_CACHE = {}; 9 | 10 | 11 | function getName(version) { 12 | switch (process.platform) { 13 | case "win32": 14 | return `wasm-bindgen-${version}-x86_64-pc-windows-msvc`; 15 | case "darwin": 16 | switch (process.arch) { 17 | case "arm64": 18 | return `wasm-bindgen-${version}-aarch64-apple-darwin`; 19 | default: 20 | return `wasm-bindgen-${version}-x86_64-apple-darwin`; 21 | } 22 | default: 23 | switch (process.arch) { 24 | case "arm64": 25 | return `wasm-bindgen-${version}-aarch64-unknown-linux-gnu`; 26 | default: 27 | return `wasm-bindgen-${version}-x86_64-unknown-linux-musl`; 28 | } 29 | } 30 | } 31 | 32 | 33 | function getUrl(version, name) { 34 | return `https://github.com/rustwasm/wasm-bindgen/releases/download/${version}/${name}.tar.gz`; 35 | } 36 | 37 | 38 | function getPath(dir) { 39 | if (process.platform === "win32") { 40 | return $path.join(dir, "wasm-bindgen.exe"); 41 | } else { 42 | return $path.join(dir, "wasm-bindgen"); 43 | } 44 | } 45 | 46 | 47 | async function fetchBin(dir, version, name, path) { 48 | await mkdir(dir); 49 | 50 | if (!(await exists(path))) { 51 | info(`Downloading wasm-bindgen version ${version}`); 52 | 53 | const response = await $fetch(getUrl(version, name)); 54 | 55 | if (!response.ok) { 56 | throw new Error(`Could not download wasm-bindgen: ${response.statusText}`); 57 | } 58 | 59 | await tar(response.body, { 60 | cwd: dir, 61 | }); 62 | } 63 | } 64 | 65 | 66 | export async function download(dir, verbose) { 67 | const version = await getVersion(dir, "wasm-bindgen"); 68 | const name = getName(version); 69 | 70 | const cache = getCacheDir("rollup-plugin-rust"); 71 | 72 | const path = getPath($path.join(cache, name)); 73 | 74 | if (verbose) { 75 | debug(`Searching for wasm-bindgen at ${path}`); 76 | } 77 | 78 | let promise = WASM_BINDGEN_CACHE[path]; 79 | 80 | if (promise == null) { 81 | promise = WASM_BINDGEN_CACHE[path] = fetchBin(cache, version, name, path); 82 | } 83 | 84 | await promise; 85 | 86 | return path; 87 | } 88 | 89 | 90 | export async function run({ bin, dir, wasmPath, outDir, typescript, extraArgs, verbose }) { 91 | // TODO what about --debug --no-demangle --keep-debug ? 92 | let args = [ 93 | "--out-dir", outDir, 94 | "--out-name", "index", 95 | "--target", "web", 96 | "--omit-default-module-path", 97 | ]; 98 | 99 | if (!typescript) { 100 | args.push("--no-typescript"); 101 | } 102 | 103 | args.push(wasmPath); 104 | 105 | args = args.concat(extraArgs); 106 | 107 | if (verbose) { 108 | debug(`Running wasm-bindgen ${args.join(" ")}`); 109 | } 110 | 111 | await spawn(bin, args, { cwd: dir, stdio: "inherit" }); 112 | } 113 | -------------------------------------------------------------------------------- /src/cargo.js: -------------------------------------------------------------------------------- 1 | import { getEnv, exec, debug, spawn, Lock } from "./utils.js"; 2 | 3 | 4 | const GLOBAL_LOCK = new Lock(); 5 | 6 | 7 | export class Nightly { 8 | constructor(year, month, day) { 9 | this.year = year; 10 | this.month = month; 11 | this.day = day; 12 | } 13 | 14 | greaterThan(other) { 15 | if (this.year > other.year) { 16 | return true; 17 | 18 | } else if (this.year === other.year) { 19 | if (this.month > other.month) { 20 | return true; 21 | 22 | } else if (this.month === other.month) { 23 | return this.day > other.day; 24 | 25 | } else { 26 | return false; 27 | } 28 | 29 | } else { 30 | return false; 31 | } 32 | } 33 | 34 | supportsImmediateAbort() { 35 | return this.greaterThan(new Nightly(2025, 10, 4)); 36 | } 37 | } 38 | 39 | 40 | export async function getNightly(dir) { 41 | const bin = getEnv("CARGO_BIN", "cargo"); 42 | 43 | // TODO make this faster somehow ? 44 | const version = await exec(`${bin} --version`, { cwd: dir }); 45 | 46 | const a = /\-nightly \([^ ]+ ([0-9]+)\-([0-9]+)\-([0-9]+)\)/.exec(version); 47 | 48 | if (a) { 49 | return new Nightly(+a[1], +a[2], +a[3]); 50 | 51 | } else { 52 | return null; 53 | } 54 | } 55 | 56 | 57 | export async function getTargetDir(dir) { 58 | const bin = getEnv("CARGO_BIN", "cargo"); 59 | 60 | // TODO make this faster somehow ? 61 | const metadata = await exec(`${bin} metadata --format-version 1 --no-deps --color never`, { cwd: dir }); 62 | 63 | return JSON.parse(metadata)["target_directory"]; 64 | } 65 | 66 | 67 | export async function getVersion(dir, name) { 68 | const bin = getEnv("CARGO_BIN", "cargo"); 69 | const spec = await exec(`${bin} pkgid ${name}`, { cwd: dir }); 70 | 71 | const version = /([\d\.]+)[\r\n]*$/.exec(spec); 72 | 73 | if (version) { 74 | return version[1]; 75 | 76 | } else { 77 | throw new Error(`Could not determine ${name} version`); 78 | } 79 | } 80 | 81 | 82 | export async function run({ dir, verbose, cargoArgs, rustcArgs, release, optimize, nightly, atomics, strip }) { 83 | const cargoBin = getEnv("CARGO_BIN", "cargo"); 84 | 85 | let args = [ 86 | "rustc", 87 | "--lib", 88 | "--target", "wasm32-unknown-unknown", 89 | "--crate-type", "cdylib", // Needed for wasm-bindgen to work 90 | ]; 91 | 92 | let rustflags = []; 93 | 94 | if (atomics) { 95 | rustflags.push( 96 | "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", 97 | "-C", "link-args=--shared-memory", 98 | "-C", "link-args=--import-memory", 99 | 100 | "-C", "link-args=--export=__wasm_init_tls", 101 | "-C", "link-args=--export=__tls_size", 102 | "-C", "link-args=--export=__tls_align", 103 | "-C", "link-args=--export=__tls_base", 104 | ); 105 | 106 | args.push("-Z", "build-std=panic_abort,core,std,alloc,proc_macro"); 107 | } 108 | 109 | // https://doc.rust-lang.org/cargo/reference/profiles.html#release 110 | if (release) { 111 | args.push("--release"); 112 | 113 | if (nightly) { 114 | if (strip.location.get()) { 115 | rustflags.push("-Z", "location-detail=none"); 116 | } 117 | 118 | if (strip.formatDebug.get()) { 119 | rustflags.push("-Z", "fmt-debug=none"); 120 | } 121 | } 122 | 123 | if (optimize) { 124 | // Wasm doesn't support unwind, so we abort instead 125 | if (nightly && nightly.supportsImmediateAbort()) { 126 | // Reduces file size by removing panic strings 127 | args.push("--config"); 128 | args.push("profile.release.panic=\"immediate-abort\""); 129 | 130 | } else { 131 | args.push("--config"); 132 | args.push("profile.release.panic=\"abort\""); 133 | } 134 | 135 | // Improves runtime performance and file size 136 | args.push("--config"); 137 | args.push("profile.release.lto=true"); 138 | 139 | // Improves runtime performance 140 | args.push("--config"); 141 | args.push("profile.release.codegen-units=1"); 142 | 143 | // Reduces file size 144 | args.push("--config"); 145 | args.push("profile.release.strip=true"); 146 | 147 | // Reduces file size by removing panic strings 148 | if (nightly) { 149 | if (nightly.supportsImmediateAbort()) { 150 | args.push("-Z", "panic-immediate-abort"); 151 | } 152 | 153 | args.push("-Z", "build-std=panic_abort,core,std,alloc,proc_macro"); 154 | args.push("-Z", "build-std-features=optimize_for_size"); 155 | } 156 | } 157 | 158 | // https://doc.rust-lang.org/cargo/reference/profiles.html#dev 159 | } else { 160 | if (optimize) { 161 | // Wasm doesn't support unwind 162 | args.push("--config"); 163 | args.push("profile.dev.panic=\"abort\""); 164 | 165 | args.push("--config"); 166 | args.push("profile.dev.lto=\"off\""); 167 | 168 | // Speeds up compilation 169 | // https://github.com/MoonZoon/MoonZoon/issues/170 170 | args.push("--config"); 171 | args.push("profile.dev.debug=false"); 172 | } 173 | } 174 | 175 | rustflags = rustflags.concat(rustcArgs); 176 | 177 | if (rustflags.length > 0) { 178 | args.push("--config", "build.rustflags=" + JSON.stringify(rustflags)); 179 | } 180 | 181 | args = args.concat(cargoArgs); 182 | 183 | await GLOBAL_LOCK.withLock(async () => { 184 | if (verbose) { 185 | debug(`Running cargo ${args.join(" ")}`); 186 | } 187 | 188 | try { 189 | await spawn(cargoBin, args, { cwd: dir, stdio: "inherit" }); 190 | 191 | } catch (e) { 192 | if (verbose) { 193 | throw e; 194 | 195 | } else { 196 | const e = new Error("Rust compilation failed"); 197 | e.stack = null; 198 | throw e; 199 | } 200 | } 201 | }); 202 | } 203 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import * as $path from "node:path"; 2 | import * as $stream from "node:stream"; 3 | import * as $fs from "node:fs"; 4 | import * as $os from "node:os"; 5 | import * as $child from "node:child_process"; 6 | 7 | import * as $glob from "glob"; 8 | import * as $rimraf from "rimraf"; 9 | import * as $tar from "tar"; 10 | import $chalk from "chalk"; 11 | 12 | 13 | export function getCacheDir(name) { 14 | switch (process.platform) { 15 | case "win32": 16 | const localAppData = process.env.LOCALAPPDATA || $path.join($os.homedir(), "AppData", "Local"); 17 | return $path.join(localAppData, name, "Cache"); 18 | 19 | case "darwin": 20 | return $path.join($os.homedir(), "Library", "Caches", name); 21 | 22 | // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 23 | default: 24 | const cacheDir = process.env.XDG_CACHE_HOME || $path.join($os.homedir(), ".cache"); 25 | return $path.join(cacheDir, name); 26 | } 27 | } 28 | 29 | 30 | export function posixPath(path) { 31 | return path.replace(/\\/g, $path.posix.sep); 32 | } 33 | 34 | 35 | export function debug(s) { 36 | console.debug($chalk.blue("> " + s + "\n")); 37 | } 38 | 39 | 40 | export function info(s) { 41 | console.info($chalk.yellow(s)); 42 | } 43 | 44 | 45 | export function isObject(value) { 46 | return Object.prototype.toString.call(value) === "[object Object]"; 47 | } 48 | 49 | 50 | export function eachObject(object, f) { 51 | Object.keys(object).forEach((key) => { 52 | f(key, object[key]); 53 | }); 54 | } 55 | 56 | 57 | export function copyObject(object, f) { 58 | const output = {}; 59 | 60 | eachObject(object, (key, value) => { 61 | if (isObject(value)) { 62 | output[key] = copyObject(value, f); 63 | 64 | } else { 65 | output[key] = f(value); 66 | } 67 | }); 68 | 69 | return output; 70 | } 71 | 72 | 73 | export function glob(pattern, cwd) { 74 | return $glob.glob(pattern, { 75 | cwd: cwd, 76 | strict: true, 77 | absolute: true, 78 | nodir: true 79 | }); 80 | } 81 | 82 | 83 | export function rm(path) { 84 | return $rimraf.rimraf(path, { glob: false }); 85 | } 86 | 87 | 88 | export function mv(from, to) { 89 | return new Promise((resolve, reject) => { 90 | $fs.rename(from, to, (err) => { 91 | if (err) { 92 | reject(err); 93 | } else { 94 | resolve(); 95 | } 96 | }); 97 | }); 98 | } 99 | 100 | 101 | export function mkdir(path) { 102 | return new Promise((resolve, reject) => { 103 | $fs.mkdir(path, { recursive: true }, (err) => { 104 | if (err) { 105 | reject(err); 106 | } else { 107 | resolve(); 108 | } 109 | }); 110 | }); 111 | } 112 | 113 | 114 | export function exists(path) { 115 | return new Promise((resolve, reject) => { 116 | $fs.access(path, (err) => { 117 | if (err) { 118 | resolve(false); 119 | } else { 120 | resolve(true); 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | 127 | export function read(path) { 128 | return new Promise(function (resolve, reject) { 129 | $fs.readFile(path, function (err, file) { 130 | if (err) { 131 | reject(err); 132 | 133 | } else { 134 | resolve(file); 135 | } 136 | }); 137 | }); 138 | } 139 | 140 | 141 | export function readString(path) { 142 | return new Promise(function (resolve, reject) { 143 | $fs.readFile(path, { encoding: "utf8" }, function (err, file) { 144 | if (err) { 145 | reject(err); 146 | 147 | } else { 148 | resolve(file); 149 | } 150 | }); 151 | }); 152 | } 153 | 154 | 155 | export function writeString(path, value) { 156 | return new Promise(function (resolve, reject) { 157 | $fs.writeFile(path, value, { encoding: "utf8" }, function (err) { 158 | if (err) { 159 | reject(err); 160 | 161 | } else { 162 | resolve(); 163 | } 164 | }); 165 | }); 166 | } 167 | 168 | 169 | export function getEnv(name, fallback) { 170 | const value = process.env[name]; 171 | 172 | if (value == null) { 173 | return fallback; 174 | 175 | } else { 176 | return value; 177 | } 178 | } 179 | 180 | 181 | export function exec(cmd, options) { 182 | return new Promise((resolve, reject) => { 183 | $child.exec(cmd, options, (err, stdout, stderr) => { 184 | if (err) { 185 | reject(err); 186 | 187 | } else if (stderr.length > 0) { 188 | reject(new Error(stderr)); 189 | 190 | } else { 191 | resolve(stdout); 192 | } 193 | }); 194 | }); 195 | } 196 | 197 | 198 | export function spawn(command, args, options) { 199 | return wait($child.spawn(command, args, options)); 200 | } 201 | 202 | 203 | export function wait(p) { 204 | return new Promise((resolve, reject) => { 205 | p.on("close", (code) => { 206 | if (code === 0) { 207 | resolve(); 208 | 209 | } else { 210 | reject(new Error("Command `" + p.spawnargs.join(" ") + "` failed with error code: " + code)); 211 | } 212 | }); 213 | 214 | p.on("error", reject); 215 | }); 216 | } 217 | 218 | 219 | export function tar(stream, options) { 220 | return new Promise((resolve, reject) => { 221 | $stream.pipeline( 222 | stream, 223 | $tar.x({ 224 | cwd: options.cwd, 225 | strict: true, 226 | }, options.files), 227 | (err) => { 228 | if (err) { 229 | reject(err); 230 | } else { 231 | resolve(); 232 | } 233 | }, 234 | ); 235 | }); 236 | } 237 | 238 | 239 | export class Lock { 240 | constructor() { 241 | this.locked = false; 242 | this.pending = []; 243 | } 244 | 245 | async withLock(f) { 246 | await this.lock(); 247 | 248 | try { 249 | return await f(); 250 | 251 | } finally { 252 | this.unlock(); 253 | } 254 | } 255 | 256 | async lock() { 257 | if (this.locked) { 258 | await new Promise((resolve, reject) => { 259 | this.pending.push(resolve); 260 | }); 261 | 262 | if (this.locked) { 263 | throw new Error("Invalid lock state"); 264 | } 265 | } 266 | 267 | this.locked = true; 268 | } 269 | 270 | unlock() { 271 | this.locked = false; 272 | 273 | if (this.pending.length !== 0) { 274 | const resolve = this.pending.shift(); 275 | // Wake up pending task 276 | resolve(); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rollup-plugin-rust 2 | 3 | Rollup plugin for bundling and importing Rust crates. 4 | 5 | This plugin internally uses [`wasm-bindgen`](https://rustwasm.github.io/docs/wasm-bindgen/). 6 | 7 | `wasm-bindgen` is automatically installed, you do not need to install it separately. 8 | 9 | 10 | ## Installation 11 | 12 | First, make sure that [rustup](https://rustup.rs/) is installed. 13 | 14 | If you are on Windows, then you also need to install the [Visual Studio build tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16) (make sure to enable the "C++ build tools" option). 15 | 16 | Lastly, run this: 17 | 18 | ```sh 19 | yarn add --dev @wasm-tool/rollup-plugin-rust binaryen 20 | ``` 21 | 22 | Or if you're using npm you can use this instead: 23 | 24 | ```sh 25 | npm install --save-dev @wasm-tool/rollup-plugin-rust binaryen 26 | ``` 27 | 28 | 29 | ## Usage 30 | 31 | Add the plugin to your `rollup.config.js`, and now you can use `Cargo.toml` files as entries: 32 | 33 | ```js 34 | import rust from "@wasm-tool/rollup-plugin-rust"; 35 | 36 | export default { 37 | format: "es", 38 | input: { 39 | foo: "Cargo.toml", 40 | }, 41 | plugins: [ 42 | rust(), 43 | ], 44 | }; 45 | ``` 46 | 47 | You can import as many different `Cargo.toml` files as you want, each one will be compiled separately. 48 | 49 | See the [example folder](/example) for a simple working example. First run `yarn install`, and then `yarn watch` for development. Use `yarn build` to build for production. 50 | 51 | 52 | ### Importing `Cargo.toml` within `.js` 53 | 54 | It is also possible to import a `Cargo.toml` file inside of a `.js` file, like this: 55 | 56 | ```js 57 | import { foo, bar } from "./path/to/Cargo.toml"; 58 | 59 | // Use functions which were exported from Rust... 60 | ``` 61 | 62 | ---- 63 | 64 | ## Extra Tips 65 | 66 | 67 | ### Nightly 68 | 69 | It is recommended to use the [nightly toolchain](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) because it significantly reduces the size of the `.wasm` file. 70 | 71 | You can use nightly by creating a `rust-toolchain.toml` file in your project directory: 72 | 73 | ```toml 74 | [toolchain] 75 | channel = "nightly-2025-11-20" 76 | components = [ "rust-std", "rust-src", "rustfmt", "clippy" ] 77 | targets = [ "wasm32-unknown-unknown" ] 78 | ``` 79 | 80 | You can change the `channel` to upgrade to the latest nightly version (or downgrade to a past nightly version). 81 | 82 | After changing the `rust-toolchain.toml` file, you might need to run `rustup show` in order to download the correct Rust version. 83 | 84 | 85 | ### Workspaces 86 | 87 | When compiling multiple crates it is highly recommended to use a [workspace](https://doc.rust-lang.org/cargo/reference/manifest.html#the-workspace-section) to improve compile times. 88 | 89 | Create a `Cargo.toml` file in the root of your project which lists out the sub-crates that are a part of the workspace: 90 | 91 | ```toml 92 | [workspace] 93 | members = [ 94 | "src/foo", 95 | "src/bar", 96 | ] 97 | ``` 98 | 99 | 100 | ### Optimizing for size 101 | 102 | By default the Rust compiler optimizes for maximum runtime performance, but this comes at the cost of a bigger file size. 103 | 104 | On the web it is desirable to have a small file size, because a smaller `.wasm` file will download faster. 105 | 106 | This plugin automatically optimizes for smaller file size, but you can reduce the size even further by adding this into your `Cargo.toml`: 107 | 108 | ```toml 109 | [profile.release] 110 | opt-level = "z" 111 | ``` 112 | 113 | You can also try `opt-level = "s"` which in some cases might produce a smaller file size. 114 | 115 | If you're using workspaces, make sure to add that into your *workspace* `Cargo.toml`, not the sub-crates. 116 | 117 | 118 | ### Usage with Vite 119 | 120 | This plugin works out of the box with Vite, however Vite has SSR, which means that it runs your code on both the server and browser. 121 | 122 | This can cause errors when loading Wasm files, so you need to disable SSR when loading the Wasm: 123 | 124 | ```js 125 | async function loadWasm() { 126 | // This code will only run in the browser 127 | if (!import.meta.env.SSR) { 128 | const { foo, bar } = await import("./path/to/Cargo.toml"); 129 | 130 | // Use functions which were exported from Rust... 131 | } 132 | } 133 | ``` 134 | 135 | 136 | ### Customizing the Wasm loading 137 | 138 | For very advanced use cases, you might want to manually initialize the `.wasm` code. 139 | 140 | If you add `?custom` when importing a `Cargo.toml` file, it will give you: 141 | 142 | * `module` which is the `URL` of the `.wasm` file, or a `Uint8Array` if using `inlineWasm: true`. 143 | 144 | * `init` which is a function that initializes the Wasm and returns a Promise. The `init` function accepts these options: 145 | 146 | * `module` which is a `URL` or `Uint8Array` or `WebAssembly.Module` for the `.wasm` code. 147 | * `memory` which is a `WebAssembly.Memory` that will be used as the memory for the Wasm. 148 | 149 | ```js 150 | import { module, init } from "./path/to/Cargo.toml?custom"; 151 | 152 | async function loadWasm() { 153 | const { foo, bar } = await init({ 154 | // The URL or Uint8Array which will be initialized. 155 | module: module, 156 | 157 | // The WebAssembly.Memory which will be used for the Wasm. 158 | // 159 | // If this is undefined then it will automatically create a new memory. 160 | // 161 | // This is useful for doing multi-threading with multiple Workers sharing the same SharedArrayBuffer. 162 | memory: undefined, 163 | }); 164 | 165 | // Use functions which were exported from Rust... 166 | } 167 | ``` 168 | 169 | ---- 170 | 171 | ## Build options 172 | 173 | The default options are good for most use cases, so you generally shouldn't need to change them. 174 | 175 | These are the default options: 176 | 177 | ```js 178 | rust({ 179 | // Whether the code will be run in Node.js or not. 180 | // 181 | // This is needed because Node.js does not support `fetch`. 182 | nodejs: false, 183 | 184 | // Whether to inline the `.wasm` file into the `.js` file. 185 | // 186 | // This is slower and it increases the file size by ~33%, 187 | // but it does not require a separate `.wasm` file. 188 | inlineWasm: false, 189 | 190 | // Whether to display extra compilation information in the console. 191 | verbose: false, 192 | 193 | extraArgs: { 194 | // Extra arguments passed to `cargo`. 195 | cargo: [], 196 | 197 | // Extra arguments passed to `rustc`, this is equivalent to `RUSTFLAGS`. 198 | rustc: [], 199 | 200 | // Extra arguments passed to `wasm-bindgen`. 201 | wasmBindgen: [], 202 | 203 | // Extra arguments passed to `wasm-opt`. 204 | wasmOpt: ["-O", "--enable-threads", "--enable-bulk-memory", "--enable-bulk-memory-opt"], 205 | }, 206 | 207 | optimize: { 208 | // Whether to build in release mode. 209 | // 210 | // In watch mode this defaults to false. 211 | release: true, 212 | 213 | // Whether to run wasm-opt. 214 | // 215 | // In watch mode this defaults to false. 216 | wasmOpt: true, 217 | 218 | // Whether to use optimized rustc settings. 219 | // 220 | // This slows down compilation but significantly reduces the file size. 221 | // 222 | // If you use the nightly toolchain, this will reduce the file size even more. 223 | rustc: true, 224 | 225 | // These options default to false in watch mode. 226 | strip: { 227 | // Removes location information, resulting in lower file size but worse stace traces. 228 | // Currently this only works on nightly. 229 | location: true, 230 | 231 | // Removes debug formatting from strings, such as `format!("{:?}", ...)` 232 | // Currently this only works on nightly. 233 | formatDebug: true, 234 | }, 235 | }, 236 | 237 | // Which files it should watch in watch mode. This is relative to the Cargo.toml file. 238 | // Supports all of the glob syntax: https://www.npmjs.com/package/glob 239 | watchPatterns: ["src/**"], 240 | 241 | // These options should not be relied upon, they can change or disappear in future versions. 242 | experimental: { 243 | // Compiles with atomics and enables multi-threading. 244 | // Currently this only works on nightly. 245 | atomics: false, 246 | 247 | // Whether the Wasm will be initialized synchronously or not. 248 | // 249 | // In the browser you can only use synchronous loading inside of Workers. 250 | // 251 | // This requires `inlineWasm: true`. 252 | synchronous: false, 253 | 254 | // Creates a `.d.ts` file for each `Cargo.toml` crate and places them 255 | // into this directory. 256 | // 257 | // This is useful for libraries which want to export TypeScript types. 258 | typescriptDeclarationDir: null, 259 | }, 260 | }) 261 | ``` 262 | 263 | 264 | ### Environment variables 265 | 266 | You can use the following environment variables to customize some aspects of this plugin: 267 | 268 | * `CARGO_BIN` is the path to the `cargo` executable. 269 | * `WASM_BINDGEN_BIN` is the path to the `wasm-bindgen` executable. 270 | * `WASM_OPT_BIN` is the path to the `wasm-opt` executable. 271 | 272 | If not specified, they will use a good default value, so you shouldn't need to change them, this is for advanced uses only. 273 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as $path from "node:path"; 2 | import * as $toml from "@iarna/toml"; 3 | import { createFilter } from "@rollup/pluginutils"; 4 | import { glob, rm, read, readString, debug, getEnv, isObject, eachObject, copyObject } from "./utils.js"; 5 | import * as $wasmBindgen from "./wasm-bindgen.js"; 6 | import * as $cargo from "./cargo.js"; 7 | import * as $wasmOpt from "./wasm-opt.js"; 8 | import * as $typescript from "./typescript.js"; 9 | 10 | 11 | const PREFIX = "./.__rollup-plugin-rust__"; 12 | const INLINE_ID = "\0__rollup-plugin-rust-inlineWasm__"; 13 | 14 | 15 | function stripPath(path) { 16 | return path.replace(/\?[^\?]*$/, ""); 17 | } 18 | 19 | 20 | class Option { 21 | constructor(value) { 22 | this.value = value; 23 | this.isDefault = true; 24 | } 25 | 26 | get() { 27 | return this.value; 28 | } 29 | 30 | getOr(fallback) { 31 | if (this.isDefault) { 32 | return fallback; 33 | 34 | } else { 35 | return this.value; 36 | } 37 | } 38 | 39 | set(value) { 40 | this.value = value; 41 | this.isDefault = false; 42 | } 43 | } 44 | 45 | 46 | class State { 47 | constructor() { 48 | // Whether the plugin is running in Vite or not 49 | this.vite = false; 50 | 51 | // Whether we're in watch mode or not 52 | this.watch = false; 53 | 54 | // Whether the options have been processed or not 55 | this.processed = false; 56 | 57 | this.fileIds = new Set(); 58 | 59 | this.defaults = { 60 | watchPatterns: ["src/**"], 61 | 62 | inlineWasm: false, 63 | 64 | verbose: false, 65 | 66 | nodejs: false, 67 | 68 | optimize: { 69 | release: true, 70 | 71 | wasmOpt: true, 72 | 73 | rustc: true, 74 | 75 | strip: { 76 | location: true, 77 | 78 | formatDebug: true, 79 | }, 80 | }, 81 | 82 | extraArgs: { 83 | cargo: [], 84 | 85 | rustc: [], 86 | 87 | wasmBindgen: [], 88 | 89 | // TODO figure out better optimization options ? 90 | wasmOpt: ["-O", "--enable-threads", "--enable-bulk-memory", "--enable-bulk-memory-opt"], 91 | }, 92 | 93 | experimental: { 94 | atomics: false, 95 | 96 | synchronous: false, 97 | 98 | typescriptDeclarationDir: null, 99 | }, 100 | }; 101 | 102 | // Make a copy of the default settings 103 | this.options = copyObject(this.defaults, (value) => new Option(value)); 104 | 105 | this.deprecations = { 106 | debug: (cx, value) => { 107 | cx.warn("The `debug` option has been changed to `optimize.release`"); 108 | this.options.optimize.release.set(!value); 109 | }, 110 | 111 | cargoArgs: (cx, value) => { 112 | cx.warn("The `cargoArgs` option has been changed to `extraArgs.cargo`"); 113 | this.options.extraArgs.cargo.set(value); 114 | }, 115 | 116 | wasmBindgenArgs: (cx, value) => { 117 | cx.warn("The `wasmBindgenArgs` option has been changed to `extraArgs.wasmBindgen`"); 118 | this.options.extraArgs.wasmBindgen.set(value); 119 | }, 120 | 121 | wasmOptArgs: (cx, value) => { 122 | cx.warn("The `wasmOptArgs` option has been changed to `extraArgs.wasmOpt`"); 123 | this.options.extraArgs.wasmOpt.set(value); 124 | }, 125 | 126 | serverPath: (cx, value) => { 127 | cx.warn("The `serverPath` option is deprecated and no longer works"); 128 | }, 129 | 130 | importHook: (cx, value) => { 131 | cx.warn("The `importHook` option is deprecated and no longer works"); 132 | }, 133 | 134 | experimental: { 135 | directExports: (cx, value) => { 136 | cx.warn("The `experimental.directExports` option is deprecated and no longer works"); 137 | }, 138 | }, 139 | }; 140 | 141 | this.cache = { 142 | nightly: {}, 143 | targetDir: {}, 144 | wasmBindgen: {}, 145 | build: {}, 146 | }; 147 | } 148 | 149 | 150 | reset() { 151 | this.fileIds.clear(); 152 | 153 | this.cache.nightly = {}; 154 | this.cache.targetDir = {}; 155 | this.cache.wasmBindgen = {}; 156 | this.cache.build = {}; 157 | } 158 | 159 | 160 | processOptions(cx, oldOptions) { 161 | if (!this.processed) { 162 | this.processed = true; 163 | 164 | // Overwrite the default settings with the user-provided settings 165 | this.setOptions(cx, [], oldOptions, this.options, this.defaults, this.deprecations); 166 | } 167 | } 168 | 169 | setOptions(cx, path, oldOptions, options, defaults, deprecations) { 170 | if (oldOptions != null) { 171 | if (isObject(oldOptions)) { 172 | eachObject(oldOptions, (key, value) => { 173 | const newPath = path.concat([key]); 174 | 175 | // If the option is deprecated, call the function 176 | if (deprecations != null && key in deprecations) { 177 | const deprecation = deprecations[key]; 178 | 179 | if (isObject(deprecation)) { 180 | this.setOptions(cx, newPath, value, options?.[key], defaults?.[key], deprecation); 181 | 182 | } else { 183 | deprecation(cx, value); 184 | } 185 | 186 | // If the option has a default, apply it 187 | } else if (defaults != null && key in defaults) { 188 | const def = defaults[key]; 189 | 190 | if (isObject(def)) { 191 | this.setOptions(cx, newPath, value, options?.[key], def, deprecations?.[key]); 192 | 193 | } else if (value != null) { 194 | if (options[key].isDefault) { 195 | options[key].set(value); 196 | } 197 | } 198 | 199 | // The option doesn't exist 200 | } else { 201 | throw new Error(`The \`${newPath.join(".")}\` option does not exist`); 202 | } 203 | }); 204 | 205 | } else if (path.length > 0) { 206 | throw new Error(`The \`${path.join(".")}\` option must be an object`); 207 | 208 | } else { 209 | throw new Error(`Options must be an object`); 210 | } 211 | } 212 | } 213 | 214 | 215 | async watchFiles(cx, dir) { 216 | if (this.watch) { 217 | const matches = await Promise.all(this.options.watchPatterns.get().map((pattern) => glob(pattern, dir))); 218 | 219 | // TODO deduplicate matches ? 220 | matches.forEach(function (files) { 221 | files.forEach(function (file) { 222 | cx.addWatchFile(file); 223 | }); 224 | }); 225 | } 226 | } 227 | 228 | 229 | async getNightly(dir) { 230 | let nightly = this.cache.nightly[dir]; 231 | 232 | if (nightly == null) { 233 | nightly = this.cache.nightly[dir] = $cargo.getNightly(dir); 234 | } 235 | 236 | return await nightly; 237 | } 238 | 239 | 240 | async getTargetDir(dir) { 241 | let targetDir = this.cache.targetDir[dir]; 242 | 243 | if (targetDir == null) { 244 | targetDir = this.cache.targetDir[dir] = $cargo.getTargetDir(dir); 245 | } 246 | 247 | return await targetDir; 248 | } 249 | 250 | 251 | async getWasmBindgen(dir) { 252 | let bin = getEnv("WASM_BINDGEN_BIN", null); 253 | 254 | if (bin == null) { 255 | bin = this.cache.wasmBindgen[dir]; 256 | 257 | if (bin == null) { 258 | bin = this.cache.wasmBindgen[dir] = $wasmBindgen.download(dir, this.options.verbose.get()); 259 | } 260 | 261 | return await bin; 262 | 263 | } else { 264 | return bin; 265 | } 266 | } 267 | 268 | 269 | async loadWasm(outDir) { 270 | const wasmPath = $path.join(outDir, "index_bg.wasm"); 271 | 272 | if (this.options.verbose.get()) { 273 | debug(`Looking for wasm at ${wasmPath}`); 274 | } 275 | 276 | return await read(wasmPath); 277 | } 278 | 279 | 280 | async compileTypescript(name, outDir) { 281 | if (this.options.experimental.typescriptDeclarationDir.get() != null) { 282 | await $typescript.write( 283 | name, 284 | this.options.experimental.typescriptDeclarationDir.get(), 285 | outDir, 286 | ); 287 | } 288 | } 289 | 290 | 291 | async compileTypescriptCustom(name, isCustom) { 292 | if (isCustom && this.options.experimental.typescriptDeclarationDir.get() != null) { 293 | await $typescript.writeCustom( 294 | name, 295 | this.options.experimental.typescriptDeclarationDir.get(), 296 | this.options.inlineWasm.get(), 297 | this.options.experimental.synchronous.get(), 298 | ); 299 | } 300 | } 301 | 302 | 303 | async wasmOpt(cx, outDir) { 304 | if (this.options.optimize.wasmOpt.getOr(!this.watch)) { 305 | const result = await $wasmOpt.run({ 306 | dir: outDir, 307 | input: "index_bg.wasm", 308 | output: "wasm_opt.wasm", 309 | extraArgs: this.options.extraArgs.wasmOpt.get(), 310 | verbose: this.options.verbose.get(), 311 | }); 312 | 313 | if (result !== null) { 314 | cx.warn("wasm-opt failed: " + result.message); 315 | } 316 | } 317 | } 318 | 319 | 320 | compileInlineWasm(build) { 321 | const wasmString = JSON.stringify(build.wasm.toString("base64")); 322 | 323 | const code = ` 324 | const base64codes = [62,0,0,0,63,52,53,54,55,56,57,58,59,60,61,0,0,0,0,0,0,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,0,0,0,0,0,0,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51]; 325 | 326 | function getBase64Code(charCode) { 327 | return base64codes[charCode - 43]; 328 | } 329 | 330 | function base64Decode(str) { 331 | let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0; 332 | let n = str.length; 333 | let result = new Uint8Array(3 * (n / 4)); 334 | let buffer; 335 | 336 | for (let i = 0, j = 0; i < n; i += 4, j += 3) { 337 | buffer = 338 | getBase64Code(str.charCodeAt(i)) << 18 | 339 | getBase64Code(str.charCodeAt(i + 1)) << 12 | 340 | getBase64Code(str.charCodeAt(i + 2)) << 6 | 341 | getBase64Code(str.charCodeAt(i + 3)); 342 | result[j] = buffer >> 16; 343 | result[j + 1] = (buffer >> 8) & 0xFF; 344 | result[j + 2] = buffer & 0xFF; 345 | } 346 | 347 | return result.subarray(0, result.length - missingOctets); 348 | } 349 | 350 | export default base64Decode(${wasmString}); 351 | `; 352 | 353 | return { 354 | code, 355 | map: { mappings: '' }, 356 | moduleSideEffects: false, 357 | }; 358 | } 359 | 360 | 361 | compileJsInline(build, isCustom) { 362 | let mainCode; 363 | let sideEffects; 364 | 365 | if (this.options.experimental.synchronous.get()) { 366 | if (isCustom) { 367 | sideEffects = false; 368 | 369 | mainCode = `export { module }; 370 | 371 | export function init(options) { 372 | exports.initSync({ 373 | module: options.module, 374 | memory: options.memory, 375 | }); 376 | return exports; 377 | }` 378 | 379 | } else { 380 | sideEffects = true; 381 | 382 | mainCode = ` 383 | exports.initSync({ module }); 384 | export * from ${build.importPath}; 385 | `; 386 | } 387 | 388 | } else { 389 | if (isCustom) { 390 | sideEffects = false; 391 | 392 | mainCode = `export { module }; 393 | 394 | export async function init(options) { 395 | await exports.default({ 396 | module_or_path: await options.module, 397 | memory: options.memory, 398 | }); 399 | return exports; 400 | }`; 401 | 402 | } else { 403 | sideEffects = true; 404 | 405 | mainCode = ` 406 | await exports.default({ module_or_path: module }); 407 | export * from ${build.importPath}; 408 | `; 409 | } 410 | } 411 | 412 | 413 | const wasmString = JSON.stringify(build.wasm.toString("base64")); 414 | 415 | const code = ` 416 | import * as exports from ${build.importPath}; 417 | 418 | import module from "${INLINE_ID}"; 419 | 420 | ${mainCode} 421 | `; 422 | 423 | return { 424 | code, 425 | map: { mappings: '' }, 426 | moduleSideEffects: sideEffects, 427 | meta: { 428 | "rollup-plugin-rust": { root: false, realPath: build.realPath } 429 | }, 430 | }; 431 | } 432 | 433 | 434 | compileJsNormal(build, isCustom) { 435 | let wasmPath = `import.meta.ROLLUP_FILE_URL_${build.fileId}`; 436 | 437 | let prelude; 438 | 439 | if (this.options.nodejs.get()) { 440 | prelude = `function loadFile(url) { 441 | return new Promise((resolve, reject) => { 442 | require("node:fs").readFile(url, (err, data) => { 443 | if (err) { 444 | reject(err); 445 | 446 | } else { 447 | resolve(data); 448 | } 449 | }); 450 | }); 451 | } 452 | 453 | const module = loadFile(${wasmPath});`; 454 | 455 | } else { 456 | prelude = `const module = ${wasmPath};`; 457 | } 458 | 459 | 460 | let mainCode; 461 | let sideEffects; 462 | 463 | if (this.options.experimental.synchronous.get()) { 464 | throw new Error("synchronous option can only be used with inlineWasm: true"); 465 | 466 | } else { 467 | if (isCustom) { 468 | sideEffects = false; 469 | 470 | mainCode = `export { module }; 471 | 472 | export async function init(options) { 473 | await exports.default({ 474 | module_or_path: await options.module, 475 | memory: options.memory, 476 | }); 477 | return exports; 478 | }`; 479 | 480 | } else { 481 | sideEffects = true; 482 | 483 | mainCode = ` 484 | await exports.default({ module_or_path: module }); 485 | export * from ${build.importPath}; 486 | `; 487 | } 488 | } 489 | 490 | return { 491 | code: ` 492 | import * as exports from ${build.importPath}; 493 | 494 | ${prelude} 495 | ${mainCode} 496 | `, 497 | map: { mappings: '' }, 498 | moduleSideEffects: sideEffects, 499 | meta: { 500 | "rollup-plugin-rust": { root: false, realPath: build.realPath } 501 | }, 502 | }; 503 | } 504 | 505 | 506 | compileJs(build, isCustom) { 507 | if (this.options.inlineWasm.get()) { 508 | return this.compileJsInline(build, isCustom); 509 | 510 | } else { 511 | return this.compileJsNormal(build, isCustom); 512 | } 513 | } 514 | 515 | 516 | async getInfo(dir, id) { 517 | const [targetDir, source] = await Promise.all([ 518 | this.getTargetDir(dir), 519 | readString(id), 520 | ]); 521 | 522 | const toml = $toml.parse(source); 523 | 524 | // TODO make this faster somehow 525 | // TODO does it need to do more transformations on the name ? 526 | const name = toml.package.name.replace(/\-/g, "_"); 527 | 528 | const wasmPath = $path.resolve($path.join( 529 | targetDir, 530 | "wasm32-unknown-unknown", 531 | (this.options.optimize.release.getOr(!this.watch) ? "release" : "debug"), 532 | name + ".wasm" 533 | )); 534 | 535 | const outDir = $path.resolve($path.join(targetDir, "rollup-plugin-rust", name)); 536 | 537 | if (this.options.verbose.get()) { 538 | debug(`Using target directory ${targetDir}`); 539 | debug(`Using rustc output ${wasmPath}`); 540 | debug(`Using output directory ${outDir}`); 541 | } 542 | 543 | await rm(outDir); 544 | 545 | return { name, wasmPath, outDir }; 546 | } 547 | 548 | 549 | async buildCargo(dir) { 550 | const nightly = await this.getNightly(dir); 551 | 552 | await $cargo.run({ 553 | dir, 554 | nightly, 555 | verbose: this.options.verbose.get(), 556 | cargoArgs: this.options.extraArgs.cargo.get(), 557 | rustcArgs: this.options.extraArgs.rustc.get(), 558 | release: this.options.optimize.release.getOr(!this.watch), 559 | optimize: this.options.optimize.rustc.get(), 560 | strip: this.options.optimize.strip, 561 | atomics: this.options.experimental.atomics.get(), 562 | }); 563 | } 564 | 565 | 566 | async buildWasm(cx, dir, bin, name, wasmPath, outDir) { 567 | await $wasmBindgen.run({ 568 | bin, 569 | dir, 570 | wasmPath, 571 | outDir, 572 | typescript: this.options.experimental.typescriptDeclarationDir.get() != null, 573 | extraArgs: this.options.extraArgs.wasmBindgen.get(), 574 | verbose: this.options.verbose.get(), 575 | }); 576 | 577 | const [wasm] = await Promise.all([ 578 | this.wasmOpt(cx, outDir).then(() => { 579 | return this.loadWasm(outDir); 580 | }), 581 | 582 | this.compileTypescript(name, outDir), 583 | ]); 584 | 585 | let fileId; 586 | 587 | if (!this.options.inlineWasm.get()) { 588 | fileId = cx.emitFile({ 589 | type: "asset", 590 | source: wasm, 591 | name: name + ".wasm" 592 | }); 593 | 594 | this.fileIds.add(fileId); 595 | } 596 | 597 | const realPath = $path.join(outDir, "index.js"); 598 | 599 | // This returns a fake file path, this ensures that the directory is the 600 | // same as the Cargo.toml file, which is necessary in order to make npm 601 | // package imports work correctly. 602 | const importPath = `"${PREFIX}${name}/index.js"`; 603 | 604 | return { name, outDir, importPath, realPath, wasm, fileId }; 605 | } 606 | 607 | 608 | async build(cx, dir, id) { 609 | if (this.options.verbose.get()) { 610 | debug(`Compiling ${id}`); 611 | } 612 | 613 | await this.buildCargo(dir); 614 | 615 | const [bin, { name, wasmPath, outDir }] = await Promise.all([ 616 | this.getWasmBindgen(dir), 617 | this.getInfo(dir, id), 618 | ]); 619 | 620 | return await this.buildWasm(cx, dir, bin, name, wasmPath, outDir); 621 | } 622 | 623 | 624 | async load(cx, oldId) { 625 | try { 626 | const id = stripPath(oldId); 627 | 628 | let promise = this.cache.build[id]; 629 | 630 | if (promise == null) { 631 | const dir = $path.dirname(id); 632 | 633 | promise = this.cache.build[id] = Promise.all([ 634 | this.build(cx, dir, id), 635 | this.watchFiles(cx, dir), 636 | ]); 637 | } 638 | 639 | const [build] = await promise; 640 | 641 | if (oldId.endsWith("?inline")) { 642 | return this.compileInlineWasm(build); 643 | 644 | } else { 645 | const isCustom = oldId.endsWith("?custom"); 646 | 647 | const [result] = await Promise.all([ 648 | this.compileJs(build, isCustom), 649 | this.compileTypescriptCustom(build.name, isCustom), 650 | ]); 651 | 652 | return result; 653 | } 654 | 655 | } catch (e) { 656 | if (!this.options.verbose.get()) { 657 | e.stack = null; 658 | } 659 | 660 | throw e; 661 | } 662 | } 663 | } 664 | 665 | 666 | export default function rust(options = {}) { 667 | // TODO should the filter affect the watching ? 668 | // TODO should the filter affect the Rust compilation ? 669 | const filter = createFilter(options.include, options.exclude); 670 | 671 | const state = new State(); 672 | 673 | return { 674 | name: "rust", 675 | 676 | // Vite-specific hook 677 | configResolved(config) { 678 | state.vite = true; 679 | 680 | if (config.command !== "build") { 681 | // We have to force inlineWasm during dev because Vite doesn't support emitFile 682 | // https://github.com/vitejs/vite/issues/7029 683 | state.options.inlineWasm.set(true); 684 | } 685 | }, 686 | 687 | buildStart(rollup) { 688 | state.reset(); 689 | 690 | state.processOptions(this, options); 691 | 692 | state.watch = this.meta.watchMode || rollup.watch; 693 | }, 694 | 695 | // This is only compatible with Rollup 2.78.0 and higher 696 | resolveId: { 697 | order: "pre", 698 | handler(id, importer, info) { 699 | if (id === INLINE_ID) { 700 | return { 701 | id: stripPath(importer) + "?inline", 702 | meta: { 703 | "rollup-plugin-rust": { root: true } 704 | } 705 | }; 706 | 707 | } else { 708 | const name = $path.basename(id); 709 | 710 | const normal = (name === "Cargo.toml"); 711 | const custom = (name === "Cargo.toml?custom"); 712 | 713 | if ((normal || custom) && filter(id)) { 714 | const path = (importer ? $path.resolve($path.dirname(importer), id) : $path.resolve(id)); 715 | 716 | return { 717 | id: path, 718 | moduleSideEffects: !custom, 719 | meta: { 720 | "rollup-plugin-rust": { root: true } 721 | } 722 | }; 723 | 724 | // Rewrites the fake file paths to real file paths. 725 | } else if (importer && id[0] === ".") { 726 | const info = this.getModuleInfo(importer); 727 | 728 | if (info && info.meta) { 729 | const meta = info.meta["rollup-plugin-rust"]; 730 | 731 | if (meta && !meta.root) { 732 | // TODO maybe use resolve ? 733 | const path = $path.join($path.dirname(importer), id); 734 | 735 | const realPath = (id.startsWith(PREFIX) 736 | ? meta.realPath 737 | : $path.join($path.dirname(meta.realPath), id)); 738 | 739 | return { 740 | id: path, 741 | meta: { 742 | "rollup-plugin-rust": { 743 | root: false, 744 | realPath, 745 | } 746 | } 747 | }; 748 | } 749 | } 750 | } 751 | } 752 | 753 | return null; 754 | }, 755 | }, 756 | 757 | load(id, loadState) { 758 | const info = this.getModuleInfo(id); 759 | 760 | if (info && info.meta) { 761 | const meta = info.meta["rollup-plugin-rust"]; 762 | 763 | if (meta) { 764 | if (meta.root) { 765 | // This causes Vite to load a noop module during SSR 766 | if (state.vite && loadState && loadState.ssr) { 767 | return { 768 | code: `export {};`, 769 | map: { mappings: '' }, 770 | moduleSideEffects: false, 771 | }; 772 | 773 | // This compiles the Cargo.toml 774 | } else { 775 | return state.load(this, id); 776 | } 777 | 778 | } else { 779 | if (state.options.verbose.get()) { 780 | debug(`Loading file ${meta.realPath}`); 781 | } 782 | 783 | // This maps the fake path to a real path on disk and loads it 784 | return readString(meta.realPath); 785 | } 786 | } 787 | } 788 | 789 | return null; 790 | }, 791 | 792 | resolveFileUrl(info) { 793 | if (state.fileIds.has(info.referenceId)) { 794 | return `new URL(${JSON.stringify(info.relativePath)}, import.meta.url)`; 795 | 796 | } else { 797 | return null; 798 | } 799 | }, 800 | }; 801 | }; 802 | --------------------------------------------------------------------------------