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

 
 
 

Contributors

Languages