Giter Club home page Giter Club logo

webext-bridge's Introduction

webext-bridge Header

Build Status License Support us
npm npm Discord

Important

webext-bridge just joined the Server Side Up family of open source projects. Read the announcement โ†’

Introduction

Messaging in web extensions made easy. Batteries included. Reduce headache and simplify the effort of keeping data in sync across different parts of your extension. webext-bridge is a tiny library that provides a simple and consistent API for sending and receiving messages between different parts of your web extension, such as background, content-script, devtools, popup, options, and window contexts.

Example

// Inside devtools script

import { sendMessage } from "webext-bridge/devtools";

button.addEventListener("click", async () => {
  const res = await sendMessage(
    "get-selection",
    { ignoreCasing: true },
    "content-script"
  );
  console.log(res); // > "The brown fox is alive and well"
});
// Inside content script

import { sendMessage, onMessage } from "webext-bridge/content-script";

onMessage("get-selection", async (message) => {
  const {
    sender,
    data: { ignoreCasing },
  } = message;

  console.log(sender.context, sender.tabId); // > devtools  156

  const { selection } = await sendMessage(
    "get-preferences",
    { sync: false },
    "background"
  );
  return calculateSelection(data.ignoreCasing, selection);
});
// Inside background script

import { onMessage } from "webext-bridge/background";

onMessage("get-preferences", ({ data }) => {
  const { sync } = data;

  return loadUserPreferences(sync);
});

Examples above require transpilation and/or bundling using webpack/babel/rollup

webext-bridge handles everything for you as efficiently as possible. No more chrome.runtime.sendMessage or chrome.runtime.onConnect or chrome.runtime.connect ....

Learn how to use "webext-bridge" with our book

We put together a comprehensive guide to help people build multi-platform browser extensions. The book covers everything from getting started to advanced topics like messaging, storage, and debugging. It's a great resource for anyone looking to build a browser extension. The book specifically covers how to use webext-bridge to simplify messaging in your extension.

Setup

Install

$ npm i webext-bridge

Light it up

Just import { } from 'webext-bridge/{context}' wherever you need it and use as shown in example above

Even if your extension doesn't need a background page or wont be sending/receiving messages in background script.
webext-bridge uses background/event context as staging area for messages, therefore it must loaded in background/event page for it to work.
(Attempting to send message from any context will fail silently if webext-bridge isn't available in background page).
See troubleshooting section for more.

Type Safe Protocols

As we are likely to use sendMessage and onMessage in different contexts, keeping the type consistent could be hard, and its easy to make mistakes. webext-bridge provide a smarter way to make the type for protocols much easier.

Create shim.d.ts file with the following content and make sure it's been included in tsconfig.json.

// shim.d.ts

import { ProtocolWithReturn } from "webext-bridge";

declare module "webext-bridge" {
  export interface ProtocolMap {
    foo: { title: string };
    // to specify the return type of the message,
    // use the `ProtocolWithReturn` type wrapper
    bar: ProtocolWithReturn<CustomDataType, CustomReturnType>;
  }
}
import { onMessage } from 'webext-bridge/content-script'

onMessage('foo', ({ data }) => {
  // type of `data` will be `{ title: string }`
  console.log(data.title)
}
import { sendMessage } from "webext-bridge/background";

const returnData = await sendMessage("bar", {
  /* ... */
});
// type of `returnData` will be `CustomReturnType` as specified

API

sendMessage(messageId: string, data: any, destination: string)

Sends a message to some other part of your extension.

Notes:

  • If there is no listener on the other side an error will be thrown where sendMessage was called.

  • Listener on the other may want to reply. Get the reply by awaiting the returned Promise

  • An error thrown in listener callback (in the destination context) will behave as usual, that is, bubble up, but the same error will also be thrown where sendMessage was called

  • If the listener receives the message but the destination disconnects (tab closure for exmaple) before responding, sendMessage will throw an error in the sender context.

messageId

Required | string

Any string that both sides of your extension agree on. Could be get-flag-count or getFlagCount, as long as it's same on receiver's onMessage listener.

data

Required | any

Any serializable value you want to pass to other side, latter can access this value by refering to data property of first argument to onMessage callback function.

destination

Required | string |

The actual identifier of other endpoint. Example: devtools or content-script or background or content-script@133 or devtools@453

content-script, window and devtools destinations can be suffixed with @<tabId> to target specific tab. Example: devtools@351, points to devtools panel inspecting tab with id 351.

For content-script, a specific frameId can be specified by appending the frameId to the suffix @<tabId>.<frameId>.

Read Behavior section to see how destinations (or endpoints) are treated.

Note: For security reasons, if you want to receive or send messages to or from window context, one of your extension's content script must call allowWindowMessaging(<namespace: string>) to unlock message routing. Also call setNamespace(<namespace: string>) in those window contexts. Use same namespace string in those two calls, so webext-bridge knows which message belongs to which extension (in case multiple extensions are using webext-bridge in one page)


onMessage(messageId: string, callback: fn)

Register one and only one listener, per messageId per context. That will be called upon sendMessage from other side.

Optionally, send a response to sender by returning any value or if async a Promise.

messageId

Required | string

Any string that both sides of your extension agree on. Could be get-flag-count or getFlagCount, as long as it's same in sender's sendMessage call.

callback

Required | fn

A callback function Bridge should call when a message is received with same messageId. The callback function will be called with one argument, a BridgeMessage which has sender, data and timestamp as its properties.

Optionally, this callback can return a value or a Promise, resolved value will sent as reply to sender.

Read security note before using this.


allowWindowMessaging(namespace: string)

Caution: Dangerous action

API available only to content scripts

Unlocks the transmission of messages to and from window (top frame of loaded page) contexts in the tab where it is called. webext-bridge by default won't transmit any payload to or from window contexts for security reasons. This method can be called from a content script (in top frame of tab), which opens a gateway for messages.

Once again, window = the top frame of any tab. That means allowing window messaging without checking origin first will let JavaScript loaded at https://evil.com talk with your extension and possibly give indirect access to things you won't want to, like history API. You're expected to ensure the safety and privacy of your extension's users.

namespace

Required | string

Can be a domain name reversed like com.github.facebook.react_devtools or any uuid. Call setNamespace in window context with same value, so that webext-bridge knows which payload belongs to which extension (in case there are other extensions using webext-bridge in a tab). Make sure namespace string is unique enough to ensure no collisions happen.


setNamespace(namespace: string)

API available to scripts in top frame of loaded remote page

Sets the namespace Bridge should use when relaying messages to and from window context. In a sense, it connects the callee context to the extension which called allowWindowMessaging(<namespace: string>) in it's content script with same namespace.

namespace

Required | string

Can be a domain name reversed like com.github.facebook.react_devtools or any uuid. Call setNamespace in window context with same value, so that webext-bridge knows which payload belongs to which extension (in case there are other extensions using webext-bridge in a tab). Make sure namespace string is unique enough to ensure no collisions happen.

Extras

The following API is built on top of sendMessage and onMessage, basically, it's just a wrapper, the routing and security rules still apply the same way.

openStream(channel: string, destination: string)

Opens a Stream between caller and destination.

Returns a Promise which resolves with Stream when the destination is ready (loaded and onOpenStreamChannel callback registered). Example below illustrates a use case for Stream

channel

Required | string

Stream(s) are strictly scoped sendMessage(s). Scopes could be different features of your extension that need to talk to the other side, and those scopes are named using a channel id.

destination

Required | string

Same as destination in sendMessage(msgId, data, destination)


onOpenStreamChannel(channel: string, callback: fn)

Registers a listener for when a Stream opens. Only one listener per channel per context

channel

Required | string

Stream(s) are strictly scoped sendMessage(s). Scopes could be different features of your extension that need to talk to the other side, and those scopes are named using a channel id.

callback

Required | fn

Callback that should be called whenever Stream is opened from the other side. Callback will be called with one argument, the Stream object, documented below.

Stream(s) can be opened by a malicious webpage(s) if your extension's content script in that tab has called allowWindowMessaging, if working with sensitive information use isInternalEndpoint(stream.info.endpoint) to check, if false call stream.close() immediately.

Stream Example
// background.js

// To-Do

Behavior

Following rules apply to destination being specified in sendMessage(msgId, data, destination) and openStream(channelId, initialData, destination)

  • Specifying devtools as destination from content-script will auto-route payload to inspecting devtools page if open and listening. If devtools are not open, message will be queued up and delivered when devtools are opened and the user switches to your extension's devtools panel.

  • Specifying content-script as destination from devtools will auto-route the message to inspected window's top content-script page if listening. If page is loading, message will be queued up and delivered when page is ready and listening.

  • If window context (which could be a script injected by content script) are source or destination of any payload, transmission must be first unlocked by calling allowWindowMessaging(<namespace: string>) inside that page's top content script, since Bridge will first deliver the payload to content-script using rules above, and latter will take over and forward accordingly. content-script <-> window messaging happens using window.postMessage API. Therefore to avoid conflicts, Bridge requires you to call setNamespace(uuidOrReverseDomain) inside the said window script (injected or remote, doesn't matter).

  • Specifying devtools or content-script or window from background will throw an error. When calling from background, destination must be suffixed with tab id. Like devtools@745 for devtools inspecting tab id 745 or content-script@351 for top content-script at tab id 351.

Serious security note

The following note only applies if and only if, you will be sending/receiving messages to/from window contexts. There's no security concern if you will be only working with content-script, background, popup, options, or devtools scope, which is the default setting.

window context(s) in tab A get unlocked the moment you call allowWindowMessaging(namespace) somewhere in your extension's content script(s) that's also loaded in tab A.

Unlike chrome.runtime.sendMessage and chrome.runtime.connect, which requires extension's manifest to specify sites allowed to talk with the extension, webext-bridge has no such measure by design, which means any webpage whether you intended or not, can do sendMessage(msgId, data, 'background') or something similar that produces same effect, as long as it uses same protocol used by webext-bridge and namespace set to same as yours.

So to be safe, if you will be interacting with window contexts, treat webext-bridge as you would treat window.postMessage API.

Before you call allowWindowMessaging, check if that page's window.location.origin is something you expect already.

As an example if you plan on having something critical, always verify the sender before responding:

// background.js

import { onMessage, isInternalEndpoint } from "webext-bridge/background";

onMessage("getUserBrowsingHistory", (message) => {
  const { data, sender } = message;
  // Respond only if request is from 'devtools', 'content-script', 'popup', 'options', or 'background' endpoint
  if (isInternalEndpoint(sender)) {
    const { range } = data;
    return getHistory(range);
  }
});

Troubleshooting

  • Doesn't work?
    If window contexts are not part of the puzzle, webext-bridge works out of the box for messaging between devtools <-> background <-> content-script(s). If even that is not working, it's likely that webext-bridge hasn't been loaded in background page of your extension, which is used by webext-bridge as a relay. If you don't need a background page for yourself, here's bare minimum to get webext-bridge going.
// background.js (requires transpiration/bundling using webpack(recommended))

import "webext-bridge/background";
// manifest.json

{
  "background": {
    "scripts": ["path/to/transpiled/background.js"]
  }
}
  • Can't send messages to window?
    Sending or receiving messages from or to window requires you to open the messaging gateway in content script(s) for that particular tab. Call allowWindowMessaging(<namespaceA: string>) in any of your content script(s) in that tab and call setNamespace(<namespaceB: string>) in the script loaded in top frame i.e the window context. Make sure that namespaceA === namespaceB. If you're doing this, read the security note above

Resources

  • Discord for friendly support from the community and the team.
  • GitHub for source code, bug reports, and project management.
  • Get Professional Help - Get video + screen-sharing help directly from the core contributors.

Contributing

As an open-source project, we strive for transparency and collaboration in our development process. We greatly appreciate any contributions members of our community can provide. Whether you're fixing bugs, proposing features, improving documentation, or spreading awareness - your involvement strengthens the project. Please review our contribution guidelines and code of conduct to understand how we work together respectfully.

Need help getting started? Join our Discord community and we'll help you out!

Our Sponsors

All of our software is free an open to the world. None of this can be brought to you without the financial backing of our sponsors.

Sponsors

Individual Supporters

alexjustesenย ย GeekDougleย ย 

About Us

We're Dan and Jay - a two person team with a passion for open source products. We created Server Side Up to help share what we learn.

Dan Pastori
Jay Rogers


Find us at:

  • ๐Ÿ“– Blog - Get the latest guides and free courses on all things web/mobile development.
  • ๐Ÿ™‹ Community - Get friendly help from our community members.
  • ๐Ÿคตโ€โ™‚๏ธ Get Professional Help - Get video + screen-sharing support from the core contributors.
  • ๐Ÿ’ป GitHub - Check out our other open source projects.
  • ๐Ÿ“ซ Newsletter - Skip the algorithms and get quality content right to your inbox.
  • ๐Ÿฅ Twitter - You can also follow Dan and Jay.
  • โค๏ธ Sponsor Us - Please consider sponsoring us so we can create more helpful resources.

Our products

If you appreciate this project, be sure to check out our other projects.

๐Ÿ“š Books

๐Ÿ› ๏ธ Software-as-a-Service

  • Bugflow: Get visual bug reports directly in GitHub, GitLab, and more.
  • SelfHost Pro: Connect Stripe or Lemonsqueezy to a private docker registry for self-hosted apps.

๐ŸŒ Open Source

webext-bridge's People

Contributors

andarist avatar antfu avatar caudurodev avatar charlie632 avatar dmaretskyi avatar gurupras avatar jaydrogers avatar jgillick avatar juicpt avatar manubo avatar noamloewenstern avatar ntnyq avatar samrum avatar tachibana-shin avatar tmkx avatar web3hackerworld avatar zikaari avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

webext-bridge's Issues

Add support for `web_accessible_resources` contexts

Usecase

You have an extension with html files added to web_accessible_resources in the manifest. These pages should be allowed to communicate with the background script and other pages, similarly to how for e.g. the popup page can communicate with the background script.

The problem

webext-bridge currently does not treat these pages from web_accessible_resources as different contexts, rather it detects them as background contexts. This prevents them to communicate with the background script, even through there is no security risk here, as the browser.runtime.sendMessage/browser.runtime.onMessage APIs are already exposed to them.

Potential solution

Create a new type of context (e.g.: web_accessible) that behaves similarly to how the content-script contexts work. There could be multiple pages added to web_accessible_resources, so this would allow you to send messages to specific pages as well, using their tabId.

Example

Send a message to a page with "web_accessible" context and the tabId 3412:

const returnData = await sendMessage('bar', { /* ... */ }, "web_accessible@3412");

Question: Extension context invalidated

Within content-script I keep alive service-worker
setInterval(() => sendMessage("WORKER_KEEP_ALIVE_MESSAGE", ''),5000);

When the extension is updated, the content-script loses connection to the background and generates an error "Extension context invalidated".

How can I fix it?

sendMessage to injected script not working

Version: 6.0.0-rc2

in content-script:
allowWindowMessaging('test')
sendMessage('hello', 'world', 'test')

in injected script(append a script element):
import { setNamespace,onMessage } from 'webext-bridge/window'
setNamespace('test')
onMessage('hello', console.log )

get error when sendMessage:
Bridge#sendMessage -> Destination must be any one of known destinations

Add timeout for sendMessage

I would find it useful to add an option for timeout waiting for response, when calling "sendMessage".
Something like this:

const resp = await sendMessage('messageID', { msgData: 'data' }, `content-script@123`, { timeout: 3000 })

[discussion] Message queuing is dangerous and unreliable

While I was once proud of this feature when I implemented it, I have developed a counter opinion that message queuing is not safe (especially now that the background service workers, that webext-bridge relies on, terminate).

Take this example:

A content script calls sendMessage to have a message delivered to devtools. Because the devtools were not open, the message gets queued in the service worker context for a later delivery. Keep in mind that sendMessage call returned a Promise that's still pending:

  • Outcome A: The service worker terminates, the queue is lost, and content script keeps waiting on Promise that is never going to fulfill.
  • Outcome B: The user opens devtools, but AFTER the user navigates to another webpage in the same tab, now the devtools is receiving the message that was sent in the "past" and is probably no longer relevant.

Perhaps we need to get rid of message queuing altogether, or perhaps the problem is that we do indefinite message queuing with no timeouts and such restraints.

I want to hear your opinions on this matter so we can reach a consensus.

Learn more about service worker termination here

No handler registered in 'background' error, but background handler still being found and invoked

So, I recently ran into a fun new issue I am having a hard time debugging. I recently switched up from using "hard-coded" message handlers to generating them dynamically at runtime. Not a single issue passing messages until that change. (Ill explain)

webex-err-image

The background onMessage handler for that messageId is correctly registered and is actually being called despite that error in the foreground code.

Error conditions

  • Only If one or more contexts are open such as options + popup (works fine if only one is open)
  • Only if an onMessage handler actually yields asynchronously such as network request or setTimeout in a promise
  • It happens if the promise is accepted or rejected.
  • The last context to open fails; if the options page is open, async popup requests will fail

I was having issues tracing with the debugger from the background side (couldn't repro), however I could trace with console messages to determine that the functions were still getting called despite the error and failure. However the sendMessage console error appears after the method is called, but before the promise resolves.

Example Source Code

import { runtime } from "webextension-polyfill";
import { onMessage, sendMessage } from 'webext-bridge/background'
import { BridgeMessage, RuntimeContext, isInternalEndpoint } from "webext-bridge";
import { JsonObject } from "type-fest";
import { cloneDeep } from "lodash";
import { debugLog } from "@vnuge/vnlib.browser";
export interface BgRuntime<T> {
    readonly state: T;
    readonly onInstalled: (callback: () => Promise<void>) => void;
    readonly onConnected: (callback: () => Promise<void>) => void;
}
export type ApiExport = {
    [key: string]: Function
};
export type IFeatureApi<T> = (bgApi?: BgRuntime<T>) => ApiExport
export type SendMessageHandler = <T extends JsonObject | JsonObject[]>(action: string, data: any) => Promise<T>
export type VarArgsFunction<T> = (...args: any[]) => T
export interface IFeatureExport<TState, TFeature extends ApiExport> {
    background: (bgApi: BgRuntime<TState>) => TFeature
    foreground: () => TFeature
}
export interface IForegroundUnwrapper {
    use: <T extends ApiExport>(api:() => IFeatureExport<any, T>) => T
}
export interface IBackgroundWrapper<TState> {
    register: <T extends ApiExport>(features: (() => IFeatureExport<TState, T>)[]) => void
}
export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper<TState> => {
    const rt = { state,
        onConnected: runtime.onConnect.addListener,
        onInstalled: runtime.onInstalled.addListener,
    }   as BgRuntime<TState>
    return{
        register: <TFeature extends ApiExport>(features:(() => IFeatureExport<TState, TFeature>)[]) => {
            for (const feature of features) {
                const f = feature().background(rt)
                for (const externFuncName in f) {
                    const func = f[externFuncName] as Function
                    const onMessageFuncName = `${feature.name}-${externFuncName}`
                    onMessage(onMessageFuncName, async (msg: BridgeMessage<any>) => {
                        try {
                            if (!isInternalEndpoint(msg.sender)) {
                                throw new Error(`Unauthorized external call to ${onMessageFuncName}`)
                            }
                            const result = func(...msg.data)
                            // ---> Foreground error is raised here if pending promise is awaited
                            const data = await result;
                            return { ...data }
                        }
                        catch (e: any) {
                            return { bridgeMessageException: JSON.stringify(e),}
                        }
                    });
  }}}}
}
export const useForegoundFeatures = (sendMessage: SendMessageHandler): IForegroundUnwrapper => {
    return{
        use: <T extends ApiExport>(feature:() => IFeatureExport<any, T>): T => {
            const api = feature().foreground()
            const proxied : T = {} as T
            for(const funcName in api){
                //Create proxy for each method
                proxied[funcName] = (async (...args:any) => {
                    const result = await sendMessage(`${feature.name}-${funcName}`, cloneDeep(args)) as any
                    if(result.bridgeMessageException){
                        const err = JSON.parse(result.bridgeMessageException)
                        if(result.errResponse){
                            err.response = JSON.parse(result.errResponse)
                        }
                        throw err
                    }
                    return result;
                }) as any
            }
            return proxied;
   }}
}
const exampleFeature = () : IFeatureExport<any, {exampleMethod:() => Promise<void>}> => ({
    foreground: () => ({
        exampleMethod: () => Promise.resolve() //stub method never actually called 
    }),
    background: (state:any) => ({
        exampleMethod: () => new Promise((resolve) => setTimeout(resolve, 0)) //actual background method called
    })
})
//In background main.ts
const { register } = useBackgroundFeatures({})
register([ exampleFeature ])

//In foreground
const { use } = useForegoundFeatures(sendMessage)
const { exampleMethod } = use(exampleFeature)
await exampleMethod() //Mapped directly to background handler

Each script, popup/options/conent-script pass the correct sendMessage function to the useForegoundFeatures method. The senMessage function uses the default context argument (tried explicitly setting 'background' and doesnt change).

Extra steps I have taken

  • Confirmed that the offending methods are correctly returning pending promises
  • Confirmed the issue still exists after a release build (not just during debugging)
  • Tried loading in FireFox on another machine, not using the debug remote-control
  • Rewrite using promises instead of async/await syntax
  • Confirmed no other messages are being handled when this happens

After debugging, it seems fairly obvious that a promise is not being properly awaited, I just need to figure out where, and why it would cause that type of exception. Big apologies if this a bonehead mistake, I have just been pulling my hair out all weekend and was hoping someone might be able to help. I have not pushed these latest changes to my repo yet (I wanted to debug first) but I can create a buggy branch if it would help seeing the entire project.

`tabId` is null when `stream.send` from background script

Version

6.0.1

Current Behavior

When a stream is started by the content script, the tabId information in the endpoint data seems to get lost when it reaches the background script. Consequently, when attempting to send a message back to the content script using stream.send(), a TypeError is thrown.

I attempted a potential fix based on issue #66, which involved commenting out two specific lines of code. However, this resulted in an infinite loop in the aboutIncomingMessage function.

I'm not entirely familiar with the downstream logic, so any assistance in resolving this issue would be greatly appreciated.

Expected Behavior

stream.send() should send the message successfully

Steps To Reproduce

// content-script
const stream = openStream('test-channel', 'background')
// background
onOpenStreamChannel('test-channel', (stream) => {
  stream.onMessage((data) => {
    stream.send(data)
  })
})

Anything else?

No response

Background stream sendMessage to any tab does not working

I found that when stream uses sendMessage, the destination parameter uses this.streamInfo.endpoint, which is initialized in contrustor
https://github.com/zikaari/webext-bridge/blob/320d2501599ed4e261801b0b65823b953633788a/src/internal/stream.ts#L57-L66
Then, after the message is sent, destination.tabId is cleared, causing this.streamInfo.endpoint.tabId to be lost.

https://github.com/zikaari/webext-bridge/blob/320d2501599ed4e261801b0b65823b953633788a/src/background.ts#L159-L161

Update README

Items for README

  • Add new logo
  • Automate sponsors
  • Link where to get help
  • Show warning things are being updated
  • Link about recent migration to Server Side Up

How can I send&receive messages from a new popped up window?

I'm using webext-bridge in a new project.

However, I can't receive a response when I use sendMessage in a window opened by the code below.

browser.windows.create({
  focused: true,
  url: `notification.html`,
  type: 'popup',
})

It is the same even if I called allowWindowMessaging in content-script and setNamespace in the created window.

Did I miss something?

Sending messages from background script or popup not working in Chrome or Firefox

Messaging sending is not working for me

Link to related repo
Firefox version: 95.0.2
Chrome version: 96.0.4664.110

Explanation

I tried to follow the docs, but the onMessage and sendMessage do no appear to be working for me in either browser.

The main error I saw when installing was this, but I am unclear from the docs how to pass a tab id.

You can see how I pass the tab ID in the background script code below.
Screen Shot 2021-12-29 at 4 16 57 PM

Code

You can also clone and check out the related repo
Manifest:

{
  "name": "webext test",
  "version": "1.0",
  "manifest_version": 2,
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content_script.js"]
    }
  ],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "browser_action": {
    "default_popup": "popup.html"
  },
  "permissions": [
    "storage", 
    "activeTab"
  ]
}

Popup

import { sendMessage } from 'webext-bridge'

const button = document.getElementsByTagName('button')[0]

button.addEventListener('click', async() => {
  const res = await sendMessage('from-popup', {message: 'Hello World'}, 'content-script');
  console.log('popup result: ', { res })
})

Content Script

import { sendMessage, onMessage } from 'webext-bridge'  
console.log('content script loaded');

onMessage('from-popup', async(message) => {
  console.log(message)
})

onMessage('from-background', async(message) => {
  console.log(message)
})

Background script

import 'webext-bridge';
import { sendMessage } from 'webext-bridge'

console.log('hello from background script');
const currentTab = browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0])
sendMessage('from-background', {message: 'Hello World from background script'}, {context: 'content-script', tabId: currentTab.id });

Expected result

All console log messages in the content script should appear when the extension is loaded into Firefox or Chrome.

When I click the popup button, I should see a console.log of {message: "Hello World"}

Actual result

The only console message I see is 'content script loaded'

Urgency

Not urgent. I ended up using webextension-polyfill with browser.runtime.sendMessage and browser.tabs.sendMessage

Automate website PR deployments & previews

โœ‹ Blocked by

Production website deployment

  • Trigger on merge to main
  • Deploy to CloudFlare pages
  • Deploy to serversideup.net/open-source/webext-bridge

Automated PR previews

  • Deploy to pages.dev
  • Only build a preview when something changes in /docs

Back navigation breaks communication in Safari

Hi,

We are using webext-bridge to create a cross platform extension deployed to Chrome and Safari on Mac and iOS.
We found an issue that occurs when using the back button, but only on Safari.
When navigating back, sometimes following sendMessage requests will never resolve. (Using the native chrome.runtime API does resolve correctly). We tried to use the [email protected] release instead - it seemed to improve the succes rate from 0% to about 50%, however it still has issues.

Would you like for us to provide a repository reproducing the issue? And is there any way else that we can contribute to fixing the issue?

Thanks a lot

Simplify protocol map definitions with function types

Was redirected to this repo after a discussion in a similar repo, realizing where webext-core got inspired from (your repo ;-) )
After opening an issue there and simplifying the ProtocolMap definiotion when using function-types, I would recommend doing the same here.
References to the issue & pull request & change in the other repo, which can be helpful for this repo too:
The original issue
The pull request
The change

And here's a changes I would ask and recommend to modify in types.ts:

export type GetDataType<
  K extends DataTypeKey,
  Fallback extends JsonValue = undefined,
> = K extends keyof ProtocolMap
  ? ProtocolMap[K] extends (...args: infer Args) => any
    ? Args['length'] extends 0
      ? undefined
      : Args[0]
    : ProtocolMap[K] extends ProtocolWithReturn<infer Data, any>
      ? Data
      : ProtocolMap[K]
  : Fallback;


export type GetReturnType<
  K extends DataTypeKey, 
  Fallback extends JsonValue = undefined
> = K extends keyof ProtocolMap
  ? ProtocolMap[K] extends (...args: any[]) => infer Return
    ? Return
    : ProtocolMap[K] extends ProtocolWithReturn<any, infer Return>
      ? Return
      : void
  : Fallback;

Latest version import failure related

What happened?
1686550876715

Cannot load file './background' from module 'webext-bridge'

Hi author, I recently upgraded the dependency to the latest version but it seems to be loading with an error, as shown above

Version
Latest v6.0.1

Sending a message to * should broadcast it to all listeners

Clear and concise description of the problem

I want to send a message to all contexts that listen to a message. Right now it seems I need to specify each context individually, and I would like to broadcast a message to all contexts with *

Suggested solution

sendMessage("message", newValue, '*');

Alternative

No response

Additional context

No response

Validations

Sending message from background script to devtools does not work after build

Hi
Thank you for creating this package!
I use it to build a browser devtools extension for react-query library https://github.com/nirtamir2/react-query-devtools-extension

I have an issue that happens only when I build the extension.
I send messages from the user's web page with window.postMessage to the content script.
Then I use the webext-bridge to send messages from content-script to the background page (with the tabId payload) to devtools.
It works well with vite in dev mode.
After I build the extension, sending messages from the background script (with manifest v3 service worker) to the devtools does not seem to work well.

Any idea why isn't it working?
I send message to devtools with param like devtools@1280 (devtools@${message.sender.tabId})
image

Here is the place on the devtools page I listen to messages
https://github.com/nirtamir2/react-query-devtools-extension/blob/main/extension/src/App.tsx#L34

And here is the line I send the messages from the background script
https://github.com/nirtamir2/react-query-devtools-extension/blob/main/extension/src/background.ts#L8

Thank you!

Messaging from window to devtools isn't working

Hi, I'm using code from the next branch due to #7
In the window code: sendMessage("foo", "bar", "devtools");
In the devtools code: onMessage("foo", ({ data }) => console.log(data));
I'd expect this to log to the console when the sendMessage happens

What I've observed happening is that inside background.js the portMap doesn't seem to contain devtools@1833977967 destination but rather there's only [email protected] which seems like the onConnect always adds the frameId

Is this a bug or is there a problem in my usage?

Sending message from popup to background crashed (Cannot read properties of undefined reading fingerprint)

Case:

In background script create multiple popup with url to popup extension url

const url = chrome.runtime.getURL('popup.html') + '/say-hello';

await chrome.windows.create({
    type: 'popup',
    url,
    focused: true,
    top: 100,
    left: 100,
    width: 400,
    height: 400,
  });

await chrome.windows.create({
    type: 'popup',
    url,
    focused: true,
    top: 100,
    left: 100,
    width: 400,
    height: 400,
  });

onMessage('hello', ({data}) => {
  console.log(data);
});

In the popup window there is a button, when this button clicked it will send message to background.

await sendMessage('hello', { name: 'Bob' }, 'background');

Expected result

Since there is 2 windows created, in both windows when the button is clicked, background script should print console log with the data twice.

Problem / Bug

background script only print one console log, the other one crashed when the button inside the popup window is clicked.
The last created window, indeed successfully send the message to background and print log to console. In another window, when the button is clicked webext will crash.

Error preview
Screenshot 2023-04-27 at 08 21 00

Screenshot 2023-04-27 at 08 21 13

Pratical exemple how to send message between my js and background-script.

Hello, I'm using a template that uses this package, but I'm having A LOT of difficulty communicating between the content-script and my application (in this case in vue).
I use this template: https://github.com/antfu/vitesse-webext

How would I send and receive content script messages inside my .vue files? Do I need to enable allowWindowMessaging? what would be the context of the .vue file or some .js file? because I saw that it has: devtools or content-script or background and window.
Sorry to post without reproduction, it's something I really can't even reproduce.

I am very grateful in advance, I will use it in my course conclusion work.

Messages only received after page refresh

Hi,

I'm upgrading testing-playground to manifest v3, and moving from crx-bridge to webext-bridge. I've got things almost working, but one weird thing is bugging me.

Messages only seem to be received after an initial page refresh. So when I'd open a webpage, and open devtools, messages are sent, but not received. I confirm this with:

// devtools.js
import { sendMessage } from 'webext-bridge/devtools';

console.log('devtools loaded');

setInterval(() => {
  console.log('ping');
  sendMessage('PING', 'PONG', 'content-script');
}, 1_000);
// content-script.js
import { onMessage } from 'webext-bridge/content-script';

console.log('content script loaded');

onMessage('PING', () => {
  console.log('pong');
});

The loaded statements are logged, the ping is logged, but the pong never is. When I refresh the window with devtools open, the ping starts coming in and pong is logged as well.

Maybe related, but not sure, when I'd switch the direction, and send a PING fromcontent-script to devtools, I get the error: No handler registered in 'devtools' to accept messages with id 'PING'

For context, I've also setup window messaging, but didn't seem to be relevant for this case.

Any idea what's going on?

Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.

I'm trying to post message from web to content scripts, but for some reason it keeps showing error Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.

image

Here is my content script (index.ts):

const handleDOMLoaded = async () => {
  import("./content");
};

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", handleDOMLoaded);
} else {
  handleDOMLoaded();
}

content.ts:

import { allowWindowMessaging } from "webext-bridge/content-script";

allowWindowMessaging("test");

I have no idea why it causes that, it just happens whenever I call the allowWindowMessaging function.

Build Doesn't Generate CJS

The "main" entry in package.json points to a "index.js" that does not exist. The problem is that cjs is not part of the build command. The build command in package.json should be updated to:

"build": "tsup src/index.ts --format esm,cjs --dts",

disfunction to send message from an iframe page to background and others

wrap one of the extension pages as an iframe is an new extension design pattern, which is recommended by quasar. but there are some communication problems when I use webext-bridge to this pattern.

for example, I use the options/index.html as the iframe page, (https://github.com/antfu/vitesse-webext as the base template)
so, in contentScripts/index.ts

  const iFrame = document.createElement('iframe')
  const defaultFrameHeight = '200px'

  /**
   * Set the height of our iFrame housing our BEX
   * @param height
   */
  const setIFrameHeight = (height) => {
    iFrame.height = height
  }

  /**
   * Reset the iFrame to its default height e.g The height of the top bar.
   */
  const resetIFrameHeight = () => {
    setIFrameHeight(defaultFrameHeight)
  }

  /**
   * The code below will get everything going. Initialize the iFrame with defaults and add it to the page.
   * @type {string}
   */
  iFrame.id = 'bex-app-iframe'
  iFrame.width = '100%'
  resetIFrameHeight()

  // Assign some styling so it looks seamless
  Object.assign(iFrame.style, {
    position: 'fixed',
    top: '0',
    right: '0',
    bottom: '0',
    left: '0',
    border: '0',
    // scrolling: 'no',
    allowfullscreen: 'true',
    zIndex: '9999999', // Make sure it's on top
    overflow: 'visible',
  })

  iFrame.src = browser.runtime.getURL('dist/options/index.html')
  document.body.prepend(iFrame)

then, in options/index.html, I add an button of which the handler function is test to start the sendmessage test

async function test(){
  console.group('webext-bridge')
  const result = await sendMessage('test', 'ping')
  console.log(result)
  console.groupEnd('webext-bridge')
}

in background.ts

onMessage('test', async (payload) => {
  console.log(payload)
  return 'pong'
})

send I open an new page and clicked the test button in the iframe some times, the console show as the follow:
image

console of background
(nothing!)

it proved that it's failed to send messages to background.

that I open the options/index.html directly through the menu on popup icon. and replay the test again.

this time, it's successful.

console of options/index.html

image

console of background

image

@antfu @zikaari How can I do to enable the communication from an iframe to background with webext-bridge?

Bundling issues

This is probably not an issue with the library itself, but maybe the issues described below can be solved at the library layer?

I currently use webext-bridge through vitesse-webext and I ran into some issues that took a few hours to get to the bottom of.

At its core, the issue is that I have a manifest that looks like this:

{
  "content_scripts": [
    {
      "all_frames": true,
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "./dist/contentScripts/index.global.js",
        "./dist/contentScripts/amazon-video/index.js"
      ]
    },
    {
      "all_frames": true,
      "run_at": "document_start",
      "matches": [
        "*://*.twoseven.xyz/**"
      ],
      "js": [
        "./dist/contentScripts/twoseven/index.js"
      ]
    },
    {
      "all_frames": true,
      "matches": [
        "http://*.netflix.com/**",
        "https://*.netflix.com/**"
      ],
      "run_at": "document_end",
      "js": [
        "./dist/contentScripts/netflix/index.js"
      ]
    }
  ]
}

All JS files mentioned above make use of webext-bridge and because I have a) a global bundle and b) per-website bundles, this causes webext-bridge to be present in two separate bundles. As a result, I was observing that I was running into scenarios where messages that were being sent by the background script would make it to the wrong bundle, which had no open transactions and thus led to the messages being dropped.

Any elegant way to solve this at the library level? Or do I have to come up with some different bundling strategies to solve this?

Project unmaintained - looking for maintainers

While this project has served its purpose well for many users, I regret to inform you that due to constraints on my time and resources, I am no longer able to support or maintain this project. Consequently, existing and potential issues may remain unresolved.

I am reaching out to the vibrant webext-bridge community in the hopes of finding dedicated maintainers who possess a strong understanding of Chrome extension APIs and the intricate interactions within the project. Your commitment will be crucial in addressing both past and future issues, ensuring the continued success of this project.

If you are passionate about webext-bridge and have the expertise to contribute, please express your interest by reacting with a ๐Ÿš€ to this message. I am actively looking to add collaborators who can invest their time and skills to keep this project thriving.

Stay tuned for more details on how you can become a collaborator and contribute to the longevity of webext-bridge.

Thank you for your understanding and continued support.

Exception `TypeError: Cannot read properties of undefined (reading 'port')` thrown when ending session

Hi, I am using the next-version of this package. Each time I reload the browser page, i.e. the session is ended, an exception is raised in the background script:

Error in event handler: TypeError: Cannot read properties of undefined (reading 'port')
    at Object.aboutSessionEnded (<URL>)
    .
    .
    .

The exception is thrown in aboutSessionEnded. Both variables conn and fingerprint are undefined, which results in a true evaluation of the condition and then it tries to read conn.port, which causes the error:

aboutSessionEnded: (endedSessionFingerprint) => {
        const conn = connMap.get(endpoint);
        if ((conn == null ? void 0 : conn.fingerprint) === fingerprint) {
          PortMessage.toExtensionContext(conn.port, {
            status: "terminated",
            fingerprint: endedSessionFingerprint
          });
        }
        return nextChain(notifications);
      }

The rogue message is a message sent from the background and its rogueMessage.from.fingerprint is undefined.

Not sure if I am doing something wrong or how to fix this. Anybody else having the same issue? Any help would be highly appreciated, thanks.

Declared type gets transformed to void

Tested with vitesse-webext template. Not sure if the issue comes from the template or this repo.

When any Manifest.___ interface is declared as a return type, typescript changes it with a void type inside onMessage and sendMessage functions.

// shim.d.ts
import { ProtocolWithReturn } from 'webext-bridge'
import { Manifest } from 'webextension-polyfill'

declare module 'webext-bridge' {
  export interface ProtocolMap {
    'get-manifest': ProtocolWithReturn<null, Manifest.WebExtensionManifest>
  }
}
// In background script
onMessage('get-manifest', () => {
  return browser.runtime.getManifest() // Which return Manifest.WebExtensionManifest
})
/* IDE: Argument of type '() => Manifest.WebExtensionManifest' is not assignable to parameter of type 'OnMessageCallback<null, void>'.
  Type 'WebExtensionManifest' is not assignable to type 'void | Promise<void>'.
    Type 'WebExtensionManifest' is missing the following properties from type 'Promise<void>': then, catch, finally, [Symbol.toStringTag]*/
// In option page
const manifest = await sendMessage('get-manifest', null, 'background');
// IDE: manifest: void

Messaging from background to popup stuck

Whenever my popup is closed, and I use sendMessage it is stuck and waiting for the popup to open

I can't find any fix on my side to check if the tab is contactable or not, any solution ? Is it a bug ?

TS usability issues

At the moment sendMesage and onMessage accept just anything - we can extend ProtocolMap to get some stricter types but it's still super easy to make a spelling mistake and there are no helpful completion suggestions from TS:
TS playground

import { sendMessage } from 'webext-bridge' // types: 5.0.4

declare module 'webext-bridge' {
  export interface ProtocolMap {
    CURRENT_COUNT: { count: number };
  }
}


sendMessage('CURRENT_COUNT', { count: 100 })
// @ts-expect-error, it doesn't error though
sendMessage('CURENT_COUNT', { count: 100 })
// no autocomplete suggestions
sendMessage('')

The problem is in the K extends DataTypeKey | string constraint. Specifically, the | string part makes this unsafe.

Would you be willing to accept a PR that would implement a stricter behavior for this? I can think of 3 strategies here:

  1. Remove | string and always require extending the ProtocolMap
  2. detect if the ProtocolMap has been extended and adjust the behavior based on that
  3. expose a type-level "strict" flag that could be enabled by extending FeatureFlags interface that would be exposed by the library

Popup has no tabId so can't message between it and background

Sending a message from popup to background doesn't seem to work for me.

I am using this package in a Vue project.

//popup.ts

import { createApp } from 'vue'
import App from './Popup.vue'
import '../styles'
import { sendMessage } from 'webext-bridge/popup'

(async () => {
    const resBG = await sendMessage('test-message', { text: "Popup" }, 'background')
    console.log(`[popup] Got ${ resBG }`);
    
    const resCS = await sendMessage('test-message', { text: "Popup" }, 'content-script')
    console.log(`[popup] Got ${ resCS }`);
})()

const app = createApp(App)
app.mount('#app')

Background receives this message but throws an error:

Uncaught (in promise) TypeError: When sending messages from background page, use @tabId syntax to target specific tab

Background looks like this:

import { onMessage } from 'webext-bridge/background'
onMessage('test-message', async (message) => {
  const { data, sender } = message
  console.log(`[background] Got test message from ${ sender.context }@${ sender.tabId }. Data: ${ JSON.stringify(data) } `);
  return `Hello`
  
})

that console message shows:

[background] Got test message from popup@NaN. Data: [object Object]

Content script does not receive anything from the popup.

Is it possible to communicate with the popup in this way or have I got it wrong?

Thanks

Autocomplete & Error alert extending ProtocolMap as shown in docs in Type Safe Protocols

According to the docs at type-safe-protocols, the way to enable autocomplete and ensure corresponding messages are registered for listeners (and alert ts-error when misused) is to add a shim.d.ts and override the ProtocolMap with custom keys definitions.
But, trying to do so, does the following:
If I actually set a key in a custom ProtocolMap in a custom shim.d.ts file, then:

  • I still can (by accident) call onMessage on any string, even though was not defined in my custom ProtocolMap. Of course, the 'data' passed is of type 'any'.
  • If I pass a key-string which was defined in my custom ProtocolMap - then the TS does pickup the data passed correctly, as according to the data described in the corresponding key in ProtocolMap. Meaning, the shim.d.ts is recognized.

The desired behavior is to have a TS type error, when trying to call sendMessage('NonExistingKey').

I've cloned your repo, and overriding the ProtocolMap where it's defined, and then the TS does show error when trying to call a non-existing key.
The non-error only accurres when install the package in another project, and trying to customly override the ProtocolMap in an external custom shim.d.ts as the docs recommends.

I've tested this functionality with the most up-to-date version (webext-bridge@next == version 6 beta), in a new project here in order to reproduce the issue using a custom shim.d.ts (I've also updated all the other packages-version, trying to see if the latest TS is the issue - which is not the case...)

Seems that the issue is something to do when installing the package, tthe actual types.d.ts of the package is hardcoded to use the "key" ts-type as a "string", and not a dynamically type check (via "K extends DataTypeKey" etc.) as in the original source code of the 'webext-bridge' package over here.
Here is an image of the type when importing the functions:

image


(And on another note, when will the 'next' version be published as the 'latest'?)

High cpu and ram usage

ๅ›พ็‰‡

I don't know what's going on internally, but when I remove all code that references webext-bridge, cpu and ram usage will normal
ๅ›พ็‰‡

I use firefox, the following is a profile all threads of process for 5 seconds, hope to help you
https://share.firefox.dev/3oMZ9Z9

Create landing page & docs site

Image

๐ŸŽจ Design Assets

https://www.figma.com/file/FGtJTt9r2mmEQKCRJwTNd9/webext-bridge-Designs?type=design&node-id=15120-421067&mode=design&t=pHsj203pK0G23q8j-11

Hero

Special notes for the hero section:

Navigation

  • Link to Discord
  • Link to GitHub
  • Use Nuxt search

Docs

Hero

  • Link blue badge to book

Professionally Supported

Image

  • Link to the book
  • Professional support: Maybe link somewhere in the docs, that eventually links to our professional support calendar?
  • Community: Link to our GitHub Discussions

FAQs

Image

  • Feel free to create content that answers the rest of the questions

Building in Public

Image

  • Connect our ConvertKit

Footer

  • Link everything like our other projects

Mobile

  • Ensure text sizes scale on mobile
  • Create mobile menus similar to our other projects
  • Scale things accordingly to the Figma document

Listening to onConnect/onDisconnect events

After the fix: #18
Now its re-connecting automatically. Is there any way to listen to these connect/disconnect events? I would like to save some state of the extension once the connection is lost.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.