webext-blob-rpc
v0.2.1
Published
Type-safe RPC for browser extensions with native Blob support via MessagePort
Maintainers
Readme
webext-blob-rpc
Type-safe RPC for browser extensions with native Blob support. Uses MessagePort via a hidden iframe bridge for communication between content scripts and the extension service worker — Blobs, ArrayBuffers, and other structured-cloneable types transfer without serialization.
Why not chrome.runtime.sendMessage?
| | chrome.runtime.sendMessage | webext-blob-rpc |
|---|---|---|
| Blob / File transfer | JSON only — must base64-encode | Native structured clone |
| ArrayBuffer | Must serialize | Native transfer |
| Type safety | Manual typing | Full RemoteProxy<T> inference |
| Bidirectional | Requires separate listeners | expose + remote on both sides |
| Setup | Manual message routing | Two functions, auto-wired |
Setup
pnpm add webext-blob-rpcCopy the pre-built bridge files from node_modules/webext-blob-rpc/static/ into your extension (e.g. at the root):
cp node_modules/webext-blob-rpc/static/bridge.html your-extension/
cp node_modules/webext-blob-rpc/static/bridge.js your-extension/Declare them in your manifest.json:
{
"web_accessible_resources": [{
"resources": ["bridge.html", "bridge.js"],
"matches": ["<all_urls>"]
}]
}Usage
Define your API types once in a shared file, then import them on both sides:
// rpc.types.ts
export type BgAPI = {
fetchData: (url: string) => any;
};
export type ContentAPI = {
getPageTitle: () => string;
getSelection: () => string | undefined;
};Content script
import { expose, remote } from 'webext-blob-rpc';
import type { BgAPI, ContentAPI } from './rpc.types';
expose<ContentAPI>({
getPageTitle: () => document.title,
getSelection: () => window.getSelection()?.toString(),
});
const bg = remote<BgAPI>();
const data = await bg.fetchData('/api/user');Service worker (background)
import { expose, remote } from 'webext-blob-rpc';
import type { BgAPI, ContentAPI } from './rpc.types';
expose<BgAPI>({
fetchData: (url: string) => fetch(url).then(r => r.json()),
});
const page = remote<ContentAPI>(tabId);
const title = await page.getPageTitle();Error propagation
Errors thrown in handlers propagate to the caller:
// background
try {
await page.riskyOp();
} catch (e) {
// Error: failed
}Blob transfer
Blobs transfer natively over MessagePort — no base64 encoding or manual chunking:
// background
const blob = await page.captureCanvas(); // Blob instanceCleanup
expose() returns a dispose function:
const dispose = expose(handlers);
// Later:
dispose();API
expose(handlers): () => void
Detects the current context (content script or service worker) and sets up transport automatically. Returns a dispose function.
- Content script: creates a bridge port in the background, then registers handlers on it.
- Service worker: listens for incoming port connections and registers handlers on each.
remote<T>(): RemoteProxy<T>
Content script overload. Returns a proxy where each method call awaits the shared port before sending the RPC request.
remote<T>(tabId: number): RemoteProxy<T>
Service worker overload. Looks up the stored port for the given tab and returns a typed proxy.
detectContext(): 'service-worker' | 'content-script'
Returns the detected execution context.
Example
See example/ for a complete Gmail extension that intercepts file uploads, sends the Blob to the service worker, and counts words in .txt attachments.
pnpm build:example
# Load example/dist/ as an unpacked extension in ChromeContributing
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
- Fork the repo
- Create a feature branch (
git checkout -b my-feature) - Run
pnpm verifybefore committing - Open a pull request
Development
pnpm install
pnpm dev # Watch mode
pnpm test # Run tests
pnpm build # Production build
pnpm typecheck # Type checking
pnpm verify # Typecheck + test + buildLicense
MIT
