Skip to content

longcipher/rxt

Repository files navigation

rxt - Rust Extension Toolkit

A boilerplate + thin binding library for building Chrome extensions with Leptos and Rust/WASM. Inspired by WXT's architecture philosophy but staying true to Rust's transparency principles.

🎯 Design Philosophy

rxt is NOT a framework. It's a:

  • 📐 Project template with standard Cargo workspace structure
  • 🔌 1:1 Chrome API bindings via wasm-bindgen (no opinionated wrappers)
  • 🔧 Just recipes to orchestrate existing best-in-class tools
  • 🚫 Zero magic - no hidden CLI, no code generation, no black boxes

Why rxt?

Feature rxt Traditional Frameworks
Chrome API updates Add one line in shared/chrome.rs Wait for maintainer
Build transparency Plain Justfile + Trunk + cargo Custom CLI black box
Type safety Serde-based message protocol Runtime string matching
Ecosystem Standard Leptos + full crate ecosystem Framework-specific plugins
Stability Depends only on stable Rust tools Framework churn risk

📁 Project Structure

my-extension/
├── Cargo.toml              # Workspace definition
├── Justfile                # Build orchestration (replaces custom CLI)
├── manifest.json           # Native Chrome Manifest V3 (hand-written)
├── assets/
│   ├── background-loader.js   # Minimal WASM bootstrap for service worker
│   ├── content-loader.js      # Minimal WASM bootstrap for content script
│   └── icons/                 # Extension icons
│
├── shared/                 # Thin Chrome API bindings + shared types
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs
│       ├── chrome.rs       # 1:1 Chrome API extern bindings
│       └── protocol.rs     # Typed message enums for cross-context communication
│
├── popup/                  # Leptos CSR app for extension popup
│   ├── index.html          # Trunk entry point
│   ├── Cargo.toml
│   └── src/main.rs
│
├── background/             # Service worker (no DOM access)
│   ├── Cargo.toml
│   └── src/main.rs
│
└── content/                # Injected script with Shadow DOM UI
    ├── Cargo.toml
    └── src/lib.rs

🚀 Quick Start

Prerequisites

# Add WASM target
rustup target add wasm32-unknown-unknown

# Install build tools
cargo install trunk wasm-bindgen-cli cargo-watch

# Optional: formatter and linter tools
cargo install taplo-cli cargo-machete
rustup component add rustfmt clippy --toolchain nightly

Build

# One-shot release build
just

# Development mode with auto-rebuild
just watch

Output goes to dist/ - load it as an unpacked extension in Chrome.

🏗️ Architecture Deep Dive

1. Thin Chrome Bindings (shared/chrome.rs)

Pure wasm-bindgen extern blocks that map 1:1 to Chrome APIs:

#[wasm_bindgen]
extern "C" {
    pub type Chrome;
    #[wasm_bindgen(js_name = chrome)]
    pub static CHROME: Chrome;

    #[wasm_bindgen(method, getter, js_name = runtime)]
    pub fn runtime(this: &Chrome) -> Runtime;
    // ... more APIs
}

When Chrome adds new APIs: Just add another extern block. No framework update needed.

2. Type-Safe Messaging (shared/protocol.rs)

Define message contracts once, use everywhere:

#[derive(Serialize, Deserialize)]
pub enum Message {
    GetUserData,
    SaveSettings { theme: String },
}

#[derive(Serialize, Deserialize)]
pub enum Response {
    UserData { name: String },
    Saved,
}

Send from popup/content → background:

let response: Response = send_msg(Message::GetUserData).await?;

3. Build Pipeline (Justfile)

No custom CLI. Just composing battle-tested tools:

Component Tool Why
Popup UI trunk HTML/CSS/Asset bundling + HMR
Background worker cargo + wasm-bindgen --target web ES module for service worker
Content script cargo + wasm-bindgen --target no-modules Self-contained bundle for injection

4. Loader Shims

Minimal JS glue (~5 lines each) to bootstrap WASM:

Background (assets/background-loader.js):

import init from '../background/background.js';
init(); // Load WASM and call #[wasm_bindgen(start)]

Content (assets/content-loader.js):

const content = await import(chrome.runtime.getURL('content/content.js'));
await content.default(chrome.runtime.getURL('content/content_bg.wasm'));
content.start_content_script(); // Call exported Rust function

📝 Development Workflow

Adding New Chrome APIs

Edit shared/src/chrome.rs:

// Add tabs API
#[wasm_bindgen(method, getter, js_name = tabs)]
pub fn tabs(this: &Chrome) -> Tabs;

pub type Tabs;

#[wasm_bindgen(method)]
pub fn query(this: &Tabs, query_info: &JsValue) -> Promise;

Use immediately in any crate:

use shared::chrome::CHROME;

let tabs_promise = CHROME.tabs().query(&query_obj);

Extending Message Protocol

Add variants to shared/src/protocol.rs:

pub enum Message {
    GetUserData,
    FetchUrl(String), // New!
}

pub enum Response {
    UserData { name: String },
    FetchedData(String), // New!
}

Handle in background/src/main.rs:

Message::FetchUrl(url) => {
    let data = fetch_external(&url).await?;
    Response::FetchedData(data)
}

Styling Content Script

Use inline styles or inject CSS into shadow DOM:

let style = document.create_element("style")?;
style.set_inner_html(".my-widget { color: red; }");
shadow_root.append_child(&style)?;

🔍 Comparison to Alternatives

vs WXT (Node.js)

  • ✅ Type safety at compile time (not runtime)
  • ✅ No Node.js/npm dependency hell
  • ✅ Smaller bundle sizes (WASM is compact)
  • ❌ Less mature ecosystem for Chrome extension dev

vs Plasmo (React/TS)

  • ✅ Native performance (no virtual DOM overhead)
  • ✅ Better long-term stability (fewer breaking changes)
  • ❌ No built-in React ecosystem integrations

vs Raw JS

  • ✅ Eliminates entire classes of runtime errors
  • ✅ Refactoring confidence with strong types
  • ❌ Slightly more complex build setup

🛠️ Customization

Adding Tailwind CSS

Install in popup/:

cd popup
npm init -y
npm install -D tailwindcss
npx tailwindcss init

Configure Trunk.toml:

[[hooks]]
stage = "pre_build"
command = "npx"
command_arguments = ["tailwindcss", "-i", "./input.css", "-o", "./output.css"]

Supporting Options Page

  1. Create options/ crate (copy popup/ structure)

  2. Add to workspace in Cargo.toml

  3. Add build step in Justfile:

    build-options:
        trunk build options/index.html --dist dist/options --release
  4. Reference in manifest.json:

    "options_page": "options/index.html"

📚 Learning Resources

🤝 Contributing

This is a template project. Fork it and adapt to your needs! If you add useful Chrome API bindings to shared/chrome.rs, consider sharing them back.

📄 License

MIT OR Apache-2.0 (your choice)


Philosophy: Tools should empower developers, not abstract them away from the platform. rxt gives you Rust's safety + Chrome's full API surface with zero magic in between.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages