@waynevanson/vite-plugin-cargo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +124 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +140 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Wayne Van Son
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# vite-plugin-cargo
|
|
2
|
+
|
|
3
|
+
A Vite plugin that seamlessly integrates Rust crates into your frontend project by compiling them to WebAssembly via `cargo` and `wasm-bindgen`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero-Config Compiling**: Automatically detects the closest `Cargo.toml`.
|
|
8
|
+
- **WASM-Bindgen Integration**: Generates the necessary JS glue code automatically.
|
|
9
|
+
- **TypeScript Support**: Automatically generates and syncs `.d.ts` files for your Rust exports.
|
|
10
|
+
- **HMR Support**: Works with Vite's dev server.
|
|
11
|
+
- **Release Optimization**: Automatically uses `--release` builds during `vite build`.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
You must have the following installed on your system:
|
|
16
|
+
|
|
17
|
+
1. [Rust and Cargo](https://rustup.rs/)
|
|
18
|
+
2. `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown`
|
|
19
|
+
3. [`wasm-bindgen-cli`](<https://www.google.com/search?q=%5Bhttps://github.com/rustwasm/wasm-bindgen%5D(https://github.com/rustwasm/wasm-bindgen)>): `cargo install -f wasm-bindgen-cli`
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install vite-plugin-cargo --save-dev
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### 1. Configure Vite
|
|
31
|
+
|
|
32
|
+
Add the plugin to your `vite.config.ts`. You must specify which files should be treated as Rust entrypoints using a glob pattern.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { defineConfig } from "vite";
|
|
36
|
+
import { cargo } from "vite-plugin-cargo";
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
plugins: [
|
|
40
|
+
cargo({
|
|
41
|
+
includes: ["src/lib.rs"], // Files to treat as Cargo entrypoints
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Prepare your Rust code
|
|
48
|
+
|
|
49
|
+
Ensure your Rust crate is configured as a `cdylib`.
|
|
50
|
+
|
|
51
|
+
**Cargo.toml**
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[package]
|
|
55
|
+
name = "my-rust-lib"
|
|
56
|
+
version = "0.1.0"
|
|
57
|
+
edition = "2021"
|
|
58
|
+
|
|
59
|
+
[lib]
|
|
60
|
+
crate-type = ["cdylib"]
|
|
61
|
+
|
|
62
|
+
[dependencies]
|
|
63
|
+
wasm-bindgen = "0.2"
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**src/lib.rs**
|
|
68
|
+
|
|
69
|
+
```rust
|
|
70
|
+
use wasm_bindgen::prelude::*;
|
|
71
|
+
|
|
72
|
+
#[wasm_bindgen]
|
|
73
|
+
pub fn greet(name: &str) -> String {
|
|
74
|
+
format!("Hello, {}!", name)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Import in JS/TS
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { greet } from "./src/lib.rs";
|
|
83
|
+
|
|
84
|
+
console.log(greet("Vite"));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuration Options
|
|
88
|
+
|
|
89
|
+
### Base Configuration
|
|
90
|
+
|
|
91
|
+
| Option | Type | Description |
|
|
92
|
+
| :------------- | :-------- | :----------------------------------------------- |
|
|
93
|
+
| `includes` | `string | string[]` Glob pattern of Rust files to process. |
|
|
94
|
+
| `browserOnly` | `boolean` | (Optional) Passes `--browser` to `wasm-bindgen`. |
|
|
95
|
+
| `noTypescript` | `boolean` | (Optional) Disables `.d.ts` generation. |
|
|
96
|
+
|
|
97
|
+
### Rust Features
|
|
98
|
+
|
|
99
|
+
Additionally, one of the following configurations can be used with the base.
|
|
100
|
+
|
|
101
|
+
| Option | Type | Description |
|
|
102
|
+
| :------------------ | :--------- | :------------------------------------------- |
|
|
103
|
+
| `features` | `string[]` | (Optional) List of Cargo features to enable. |
|
|
104
|
+
| `noDefaultFeatures` | `boolean` | (Optional) Disable default Cargo features. |
|
|
105
|
+
|
|
106
|
+
| Option | Type | Description |
|
|
107
|
+
| :------------ | :-------- | :------------------------------------ |
|
|
108
|
+
| `allFeatures` | `boolean` | (Optional) Enable all Cargo features. |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
Transformation pipeline:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
`.rs` -> `.wasm` + `.js` + `.d.ts`
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
1. **Detection**: The plugin matches files via the `includes` glob.
|
|
121
|
+
2. **Metadata**: It runs `cargo metadata` to find the correct `cdylib` target.
|
|
122
|
+
3. **Compilation**: Runs `cargo build --target wasm32-unknown-unknown`.
|
|
123
|
+
4. **Binding**: Runs `wasm-bindgen` on the resulting `.wasm` file to a local cache in `node_modules/.cache`.
|
|
124
|
+
5. **Resolution**: Injects the generated JavaScript glue code into your Vite bundle.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
export type VitePluginCargoOptions = {
|
|
4
|
+
includes: picomatch.Glob;
|
|
5
|
+
browserOnly?: boolean;
|
|
6
|
+
noTypescript?: boolean;
|
|
7
|
+
} & ({
|
|
8
|
+
noDefaultFeatures?: boolean;
|
|
9
|
+
features?: Array<string>;
|
|
10
|
+
} | {
|
|
11
|
+
allFeatures: true;
|
|
12
|
+
});
|
|
13
|
+
export declare function cargo(pluginOptions: VitePluginCargoOptions): Plugin<never>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import picomatch from "picomatch";
|
|
5
|
+
import * as v from "valibot";
|
|
6
|
+
// todo: Replace all schema with only parsing what we need to searching.
|
|
7
|
+
const MetadataSchema = (options) => v.nonNullish(v.pipe(v.object({
|
|
8
|
+
packages: v.array(v.pipe(v.object({
|
|
9
|
+
manifest_path: v.string(),
|
|
10
|
+
targets: v.array(v.object({
|
|
11
|
+
kind: v.array(v.string()),
|
|
12
|
+
name: v.string(),
|
|
13
|
+
src_path: v.string(),
|
|
14
|
+
})),
|
|
15
|
+
}), v.transform((package_) => package_.targets.flatMap((target) => target.kind.flatMap((kind) => ({
|
|
16
|
+
manifest_path: package_.manifest_path,
|
|
17
|
+
...target,
|
|
18
|
+
kind,
|
|
19
|
+
})))))),
|
|
20
|
+
}), v.transform((packages) => packages.packages.flat()), v.findItem((metadata) => v.is(v.object({
|
|
21
|
+
manifest_path: v.literal(options.project),
|
|
22
|
+
name: v.string(),
|
|
23
|
+
src_path: v.literal(options.id),
|
|
24
|
+
kind: v.literal("cdylib"),
|
|
25
|
+
}), metadata))));
|
|
26
|
+
function getClosestCargoProject(id) {
|
|
27
|
+
return execFileSync("cargo", ["locate-project", "--message-format=plain"], {
|
|
28
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
cwd: path.dirname(id),
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
// todo: what to do there's multiple libraries for the same file?
|
|
34
|
+
// Maybe add config force user to resolve this.
|
|
35
|
+
function ensureRustLibraryMetadata(options) {
|
|
36
|
+
// validate if the file is the entrypoint to a cdylib target as rust lib
|
|
37
|
+
const metacontent = execFileSync("cargo", ["metadata", "--no-deps", "--format-version=1"], {
|
|
38
|
+
cwd: path.dirname(options.id),
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
}).trim();
|
|
41
|
+
// find the right library from our file
|
|
42
|
+
const metadata = v.parse(MetadataSchema(options), JSON.parse(metacontent));
|
|
43
|
+
return metadata;
|
|
44
|
+
}
|
|
45
|
+
function compileLibrary(options, isServe) {
|
|
46
|
+
// create `.wasm` from `.rs`
|
|
47
|
+
const ndjson = execFileSync("cargo", [
|
|
48
|
+
"build",
|
|
49
|
+
"--lib",
|
|
50
|
+
"--target=wasm32-unknown-unknown",
|
|
51
|
+
"--message-format=json",
|
|
52
|
+
"--color=never",
|
|
53
|
+
"--quiet",
|
|
54
|
+
isServe || "--release",
|
|
55
|
+
].filter((a) => typeof a === "string"), {
|
|
56
|
+
cwd: path.dirname(options.id),
|
|
57
|
+
encoding: "utf-8",
|
|
58
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
59
|
+
});
|
|
60
|
+
const json = ndjson
|
|
61
|
+
.trim()
|
|
62
|
+
.split("\n")
|
|
63
|
+
.map((json) => JSON.parse(json));
|
|
64
|
+
// find the `.wasm` file
|
|
65
|
+
const filename = json
|
|
66
|
+
.filter((a) => a?.reason === "compiler-artifact")
|
|
67
|
+
.filter((a) => a?.manifest_path === options.project)[0]?.filenames?.[0];
|
|
68
|
+
return filename;
|
|
69
|
+
}
|
|
70
|
+
// 1. Plugin for Rust -> WASM -> WASM + ESM
|
|
71
|
+
// Dependencies:
|
|
72
|
+
// 1. `cargo`
|
|
73
|
+
// 2. `wasm_bindgen`
|
|
74
|
+
export function cargo(pluginOptions) {
|
|
75
|
+
const matches = picomatch(pluginOptions.includes, { contains: true });
|
|
76
|
+
const typescript = !(pluginOptions?.noTypescript ?? false);
|
|
77
|
+
let isServe = false;
|
|
78
|
+
const libraries = new Map();
|
|
79
|
+
return {
|
|
80
|
+
name: "vite-plugin-cargo",
|
|
81
|
+
configResolved(config) {
|
|
82
|
+
isServe = config.command === "serve";
|
|
83
|
+
},
|
|
84
|
+
async resolveId(source, importer) {
|
|
85
|
+
// check if this import came from one of our entrypoints
|
|
86
|
+
const entry = libraries
|
|
87
|
+
.values()
|
|
88
|
+
.find((library) => library.id === importer);
|
|
89
|
+
if (entry === undefined) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// ensure source is relative to wasm_bindgen output dir
|
|
93
|
+
return path.resolve(entry.outDir, source);
|
|
94
|
+
},
|
|
95
|
+
async transform(_code, id) {
|
|
96
|
+
if (!matches(id)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const project = getClosestCargoProject(id);
|
|
100
|
+
const options = { id, project };
|
|
101
|
+
const metadata = ensureRustLibraryMetadata(options);
|
|
102
|
+
const hash = createHash("sha256")
|
|
103
|
+
.update(`${project}:${id}`)
|
|
104
|
+
.digest("hex");
|
|
105
|
+
const outDir = path.resolve(`node_modules/.cache/vite-plugin-cargo/${hash}`);
|
|
106
|
+
libraries.set(hash, { id, outDir, project });
|
|
107
|
+
const wasm = compileLibrary(options, isServe);
|
|
108
|
+
// create `.js` from `.wasm`
|
|
109
|
+
//
|
|
110
|
+
// `.js` and `.wasm` files are created in outDir,
|
|
111
|
+
// and added to dependency graph from imports in the `.js` entrypoint.
|
|
112
|
+
execFileSync("wasm-bindgen", [
|
|
113
|
+
"--target=bundler",
|
|
114
|
+
`--out-dir=${outDir}`,
|
|
115
|
+
isServe && `--debug`,
|
|
116
|
+
typescript || `--no-typescript`,
|
|
117
|
+
pluginOptions.browserOnly && `--browser`,
|
|
118
|
+
wasm,
|
|
119
|
+
].filter((a) => typeof a === "string"));
|
|
120
|
+
const entrypoint = path.resolve(outDir, `${metadata.name}.js`);
|
|
121
|
+
const content = await this.fs.readFile(entrypoint, { encoding: "utf8" });
|
|
122
|
+
// `.d.ts` from `wasm_bindgen` aren't read I believe, so we emit them ourselves.
|
|
123
|
+
// todo: only emit the files we load. Currently we assume they're all loaded.
|
|
124
|
+
if (typescript) {
|
|
125
|
+
// add `.d.ts` to files
|
|
126
|
+
const filenames = await this.fs.readdir(outDir);
|
|
127
|
+
await Promise.all(filenames
|
|
128
|
+
.filter((filename) => filename.endsWith(".d.ts"))
|
|
129
|
+
.map(async (filename) => await this.load({ id: path.resolve(outDir, filename) })));
|
|
130
|
+
// copy <name>.d.ts to the <id>.d.ts so user gets type definitions
|
|
131
|
+
const source = path.join(outDir, `${metadata.name}.d.ts`);
|
|
132
|
+
const target = `${id}.d.ts`;
|
|
133
|
+
await this.fs.copyFile(source, target);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
code: content,
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waynevanson/vite-plugin-cargo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Import wasm_bingen compatible Rust to JavaScript via Vite",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/waynevanson/vite-plugin-cargo"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org/"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"vite-plugin"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"prepublishOnly": "tsc"
|
|
24
|
+
},
|
|
25
|
+
"author": "Wayne Van Son",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"packageManager": "pnpm@10.27.0",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@biomejs/biome": "^2.3.13",
|
|
30
|
+
"@types/node": "^24.10.9",
|
|
31
|
+
"@types/picomatch": "^4.0.2",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vite": "^7.3.1",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"picomatch": "^4.0.3",
|
|
38
|
+
"valibot": "^1.2.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"vite": "^7.3.1"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"vite": {
|
|
45
|
+
"optional": false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|