npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

webext-message

v0.0.2

Published

Type-safe, zero-config messaging library for browser extensions.

Readme

Webext Message

Type-safe, zero-config messaging library for browser extensions. Complete solution for background, content script, and page communication.

Webext Message Webext Message Webext Message Version M. Muzammil License: MIT


🎯 Features

  • ✅ Type-Safe - Full TypeScript support with strict mode
  • ✅ Multiple Patterns - Messages, Ports, Pub-Sub, Relay
  • ✅ React Hooks - useMessage, usePort, useRelay
  • ✅ Framework Agnostic - Works with WXT, Plasmo, plain MV3
  • ✅ 100% Test Coverage - Comprehensive test suite
  • ✅ Production Ready - Enterprise-grade quality
  • ✅ Zero Config - Works out of the box
  • ✅ ESM & CommonJS - Dual build outputs
  • ✅ Source Maps - Full debugging support
  • ✅ Error Handling - Built-in error management

📦 Installation

npm

npm install webext-message

yarn

yarn add webext-message

pnpm

pnpm add webext-message

🚀 Quick Start

1. Initialize Background

// background.ts
import { initializeBackgroundMessaging, onMessage } from 'webext-message'

initializeBackgroundMessaging()

onMessage<{ query: string }, { results: string[] }>(
  async (request, response) => {
    const results = await search(request.body?.query || '')
    response.send({ results })
  }
)

2. Send from Content Script

// content.ts
import { sendToBackground } from 'webext-message'

const response = await sendToBackground<
  { query: string },
  { results: string[] }
>({
  name: 'search',
  body: { query: 'react' }
})

console.log(response.results)

3. Use in React Component

// popup.tsx
import { useMessage } from 'webext-message/hook'

export function SearchUI() {
  const { data } = useMessage<{ query: string }>(async (req, res) => {
    res.send({ query: req.body?.query })
  })

  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {data?.query}</p>
    </div>
  )
}

📚 API Overview

Main Messaging

| Function | Purpose | |----------|---------| | sendToBackground() | Send to background from content script/popup | | sendToContentScript() | Send to content script from background | | onMessage() | Listen for messages in any context | | relayMessage() | Setup relay handler for window.postMessage | | sendViaRelay() | Send via relay (30s timeout) |

Port Communication

| Function | Purpose | |----------|---------| | onPort() | Listen on named port for long-lived connections | | getPort() | Get/create port connection | | listen() | Low-level port listener |

Pub-Sub Broadcasting

| Function | Purpose | |----------|---------| | startHub() | Initialize multi-tab pub-sub hub | | broadcast() | Send to all subscribed tabs | | subscribe() | Listen for broadcasts |

React Hooks

| Hook | Purpose | |------|---------| | useMessage() | Listen for messages in component | | usePort() | Port-based communication in component | | useRelay() | Relay-based communication in component |

Setup & Utilities

| Function | Purpose | |----------|---------| | initializeBackgroundMessaging() | Initialize background routing | | getExtRuntime() | Get Chrome runtime API | | getExtTabs() | Get Chrome tabs API | | getActiveTab() | Get current active tab | | getRuntimeContext() | Detect running context |


🧭 Context Compatibility Matrix

Not every function can be called from every context. Use this as a quick lookup for which functions belong in background.ts, which belong in a content script, and which belong in a UI page (popup/options).

Format: {caller context} => {valid target context(s)}

{background}      => {"contentscript", "alltabs"}      // sendToContentScript(), broadcast()
{contentscript}    => {"background", "page"}             // sendToBackground(), relayMessage()
{popup}            => {"background"}                     // sendToBackground() (relay through background to reach a content script)
{options}          => {"background"}                     // sendToBackground() (relay through background to reach a content script)
{page}             => {"background(via relay)"}          // sendViaRelay() through a content script's relay()

Function-by-function

| Function | Callable from | Reaches | |----------|----------------|---------| | sendToBackground() | content script, popup, options | background | | sendToContentScript() | background | a specific tab's content script | | onMessage() | any context (background, content script, popup, options) | listens only — receives whatever is routed to that context | | startHub() | background | n/a (initializes the hub) | | broadcast() | background | every tab currently subscribed | | subscribe() | content script (or any context with a listener registered) | listens for broadcast() payloads | | onPort() / getPort() / listen() | background ↔ content script | long-lived port between those two contexts | | relayMessage() / relay() | content script | bridges window.postMessage from the page context | | sendViaRelay() | page context (via the content script's relay bridge) | background (through the content script) | | useMessage() / usePort() / useRelay() | any React UI (popup, options, injected panel) | same rules as their non-hook counterparts above |

Key takeaway: popup and options pages are not tabs, so they can't be targeted directly by sendToContentScript() or reached directly from a content script — route those messages through the background script (sendToBackground() → background handler → sendToContentScript(), or vice versa). The example extension's popup → content script button and content script → options/popup button both demonstrate this relay pattern.


💡 Examples

Example 1: Simple Messages

// Background
onMessage<{ name: string }, { greeting: string }>(async (req, res) => {
  res.send({ greeting: `Hello, ${req.body?.name}!` })
})

// Content Script
const { greeting } = await sendToBackground({
  name: 'greet',
  body: { name: 'Alice' }
})
// greeting = "Hello, Alice!"

Example 2: Port Communication (Streaming)

// Background
onPort('stream', async (message) => {
  console.log('Chunk:', message)
})

// Content Script
const { send } = usePort('stream')

for (const chunk of chunks) {
  send(chunk)
}

Example 3: Multi-Tab Pub-Sub

// Background
import { startHub, broadcast } from 'webext-message'

startHub()

broadcast({
  payload: { type: 'update', data: newData }
})

// Any Tab
subscribe((message) => {
  if (message.payload.type === 'update') {
    updateUI(message.payload.data)
  }
})

Example 4: Window Relay (Page ↔ Content Script)

// Content Script (setup)
import { relay } from 'webext-message/relay'

relay({ name: 'pageAPI' }, async (request) => {
  const result = await backgroundOperation()
  return { result }
})

// Page Context (send)
const { result } = await sendViaRelay({
  name: 'pageAPI',
  body: { action: 'process' }
})

Example 5: React Popup with Types

interface User {
  id: string
  name: string
  email: string
}

function UserProfile() {
  const { data: user } = useMessage<{ userId: string }, User>(
    async (request, response) => {
      const user = await fetchUser(request.body?.userId)
      response.send(user)
    }
  )

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  )
}

📋 Common Patterns

Request/Response

const response = await sendToBackground({
  name: 'getData',
  body: { id: 123 }
})

Fire & Forget

sendToBackground({
  name: 'logEvent',
  body: { event: 'click' }
}).catch(console.error)

Streaming Data

const { send } = usePort('stream')
for (const chunk of data) {
  send(chunk)
}

Broadcast Updates

broadcast({ payload: { type: 'sync', data } })

Error Handling

try {
  const result = await sendToBackground(request)
} catch (error) {
  console.error('Message failed:', error)
}

⚡ Performance

  • Minimal bundle size (~21KB gzipped)
  • Efficient tree-shaking
  • Port pooling and caching
  • Zero unnecessary dependencies
  • Optimized message routing

🛡️ Type Safety

Full generic type support:

// Fully typed send
const response = await sendToBackground<
  { userId: string },  // Request type
  { user: User }       // Response type
>({
  name: 'getUser',
  body: { userId: '123' }
})

// response type is { user: User }

🐛 Debugging

Enable debug mode:

import { getDebugger } from 'webext-message/debug'

const debugger = getDebugger()
debugger.enable()

// View events
debugger.printEvents()
debugger.printStats()

📄 License

MIT - See LICENSE for details