├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── code_of_conduct.md ├── header.png └── workflows │ └── sponsors.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .env.example ├── .gitignore ├── README.md ├── assets │ ├── animations.css │ ├── hamburger.css │ └── tailwind.css ├── components │ ├── Docs │ │ ├── Anchor.vue │ │ ├── Eyebrow.vue │ │ ├── Footer.vue │ │ ├── Header.vue │ │ ├── Logo.vue │ │ ├── ModeToggle.vue │ │ ├── Navigation.vue │ │ ├── NavigationGroup.vue │ │ ├── PageLink.vue │ │ ├── Search.vue │ │ ├── SmallPrint.vue │ │ ├── Tag.vue │ │ └── TopLevelNavItem.vue │ ├── DocumentDrivenNotFound.vue │ ├── Global │ │ ├── MobileMenu.vue │ │ ├── OgImage │ │ │ └── DocsImage.vue │ │ └── ServerSideUp.vue │ ├── Icons │ │ ├── Anchor.vue │ │ ├── Arrow.vue │ │ ├── ChatBubbleIcon.vue │ │ ├── Check.vue │ │ ├── CheckIcon.vue │ │ ├── ClipboardIcon.vue │ │ ├── EnvelopeIcon.vue │ │ ├── Logos │ │ │ ├── Chrome.vue │ │ │ ├── Edge.vue │ │ │ ├── Firefox.vue │ │ │ └── Safari.vue │ │ ├── Moon.vue │ │ ├── Resource.vue │ │ ├── Search.vue │ │ ├── Social │ │ │ ├── Discord.vue │ │ │ ├── GitHub.vue │ │ │ └── Twitter.vue │ │ ├── Sun.vue │ │ ├── UserIcon.vue │ │ └── UsersIcon.vue │ └── content │ │ ├── AppButton.vue │ │ ├── AppHeading2.vue │ │ ├── AppHeading3.vue │ │ ├── AppHeading4.vue │ │ ├── AppLink.vue │ │ ├── Code │ │ ├── ClipboardIcon.vue │ │ ├── CopyButton.vue │ │ └── PanelHeader.vue │ │ ├── CodeGroup.vue │ │ ├── CodePanel.vue │ │ ├── Column.vue │ │ ├── DiscordIcon.vue │ │ ├── DocsIcon.vue │ │ ├── GitHubIcon.vue │ │ ├── GridPattern.vue │ │ ├── Guide.vue │ │ ├── Guides.vue │ │ ├── HeartIcon.vue │ │ ├── HeroPattern.vue │ │ ├── InfoIcon.vue │ │ ├── LeadP.vue │ │ ├── MarketingBook.vue │ │ ├── MarketingBrowsers.vue │ │ ├── MarketingBuildInPublic.vue │ │ ├── MarketingFaq.vue │ │ ├── MarketingFeatureGrid.vue │ │ ├── MarketingFollowAlong.vue │ │ ├── MarketingFooter.vue │ │ ├── MarketingHeader.vue │ │ ├── MarketingHero.vue │ │ ├── NotProse.vue │ │ ├── Note.vue │ │ ├── Properties.vue │ │ ├── Property.vue │ │ ├── Resources.vue │ │ ├── Resources │ │ ├── Pattern.vue │ │ ├── Resource.vue │ │ └── ResourceIcon.vue │ │ ├── ResponsiveImage.vue │ │ ├── Row.vue │ │ ├── Search.vue │ │ ├── SearchIcon.vue │ │ ├── VideoEmbed.vue │ │ └── Warning.vue ├── composables │ └── states.ts ├── content │ ├── docs │ │ ├── 1.index.md │ │ ├── 2.getting-started │ │ │ ├── 1.installation.md │ │ │ └── 2.quick-example.md │ │ ├── 3.guide │ │ │ ├── 1.concepts.md │ │ │ ├── 2.type-safe-protocols.md │ │ │ ├── 4.examples.md │ │ │ ├── 5.security.md │ │ │ ├── 6.troubleshooting.md │ │ │ └── 7.resources.md │ │ └── 4.api │ │ │ ├── 1.send-message.md │ │ │ ├── 2.on-message.md │ │ │ ├── 3.allow-window-messaging.md │ │ │ ├── 4.set-namespace.md │ │ │ ├── 5.open-stream.md │ │ │ ├── 6.on-open-stream-channel.md │ │ │ └── 7.notes.md │ └── index.md ├── layouts │ ├── docs.vue │ └── marketing.vue ├── middleware │ └── directory.ts ├── nuxt.config.ts ├── package.json ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── images │ │ ├── docs │ │ │ └── extension-communication-map.svg │ │ ├── icons │ │ │ ├── heart.svg │ │ │ ├── square-book.svg │ │ │ ├── square-check.svg │ │ │ ├── square-globe.svg │ │ │ ├── square-heart.svg │ │ │ ├── square-lightning.svg │ │ │ └── square-target.svg │ │ ├── logos │ │ │ ├── server-side-up-logo-horizontal.svg │ │ │ └── webext-bridge-horizontal-logo.svg │ │ ├── seo │ │ │ └── og-image.png │ │ └── ui │ │ │ ├── background-pattern.svg │ │ │ ├── book-3d.svg │ │ │ ├── dan.png │ │ │ └── jay.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── server │ ├── api │ │ └── search.json.get.ts │ ├── routes │ │ └── sitemap.xml.ts │ └── tsconfig.json ├── tailwind.config.js ├── tsconfig.json ├── typography.js └── yarn.lock ├── package.json ├── pnpm-lock.yaml ├── src ├── background.ts ├── content-script.ts ├── devtools.ts ├── index.ts ├── internal │ ├── connection-args.ts │ ├── delivery-logger.ts │ ├── endpoint-fingerprint.ts │ ├── endpoint-runtime.ts │ ├── endpoint.ts │ ├── is-internal-endpoint.ts │ ├── message-port.ts │ ├── persistent-port.ts │ ├── port-message.ts │ ├── post-message.ts │ ├── stream.ts │ └── types.ts ├── options.ts ├── popup.ts ├── types.ts └── window.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@antfu" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "You found a bug in the code \U0001F914" 3 | labels: ["🧐 Bug: Needs Confirmation"] 4 | body: 5 | - type: input 6 | attributes: 7 | label: Version 8 | validations: 9 | required: true 10 | - type: textarea 11 | attributes: 12 | label: Current Behavior 13 | description: A concise description of what you're experiencing. 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Expected Behavior 19 | description: A concise description of what you expected to happen. 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Steps To Reproduce 25 | description: Steps to reproduce the behavior. 26 | placeholder: | 27 | 1. In this environment... 28 | 2. With this config... 29 | 3. Run '...' 30 | 4. See error... 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Anything else? 36 | description: | 37 | Links? References? Anything that will give us more context about the issue you are encountering! 38 | 39 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 40 | validations: 41 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Support Question 4 | url: https://github.com/serversideup/webext-bridge/discussions/75 5 | about: Get friendly support from the community on our forum. 6 | 7 | - name: ✨ Request a feature 8 | url: https://github.com/serversideup/webext-bridge/discussions/76 9 | about: Learn how to request a new feature. 10 | 11 | - name: 🤵 Get Professional Support & Customizations 12 | url: https://serversideup.net/get-help/?quick_question=webext-bridge 13 | about: Skip the line and get priority support directly from the creators of webext-bridge. -------------------------------------------------------------------------------- /.github/code_of_conduct.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /.github/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/.github/header.png -------------------------------------------------------------------------------- /.github/workflows/sponsors.yml: -------------------------------------------------------------------------------- 1 | name: Generate Sponsors README 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 15 * * 0-6 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2 12 | 13 | - name: Generate Sponsors 💖 14 | uses: JamesIves/github-sponsors-readme-action@v1 15 | with: 16 | organization: true 17 | maximum: 500 18 | fallback: '

Sponsors

' 19 | token: ${{ secrets.SPONSORS_README_ACTION_PERSONAL_ACCESS_TOKEN }} 20 | marker: 'supporters' 21 | template: '{{{ login }}}  ' 22 | file: 'README.md' 23 | 24 | - name: Deploy to GitHub Pages 🚀 25 | uses: JamesIves/github-pages-deploy-action@v4 26 | with: 27 | branch: main 28 | folder: '.' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # transpiled output 61 | dist 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | tslint.json 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v6.0.0 4 | 5 | This revision is primarily focused on codebase improvements all around. The code should be now much 6 | more readable and easy to comprehend as the responsibilities have been split into smaller pieces and 7 | composed as needed by each runtime context. 8 | 9 | For end users of the library, the breaking changes aren't that "breaking", they'll just need to do a 10 | bit of import restructring. The API behaviour is mostly unchanged, with just minor exceptions. 11 | 12 | ### Breaking changes 13 | 14 | - Runtime context is no longer automatically detected by `webext-bridge`. You must import the relevant part yourself depending on the context, eg: `import Bridge from 'webext-bridge/window'` 15 | for a script that'll be running in the Window context. Learn more about the change [here](https://github.com/zikaari/crx-bridge/issues/11). 16 | - `setNamespace` is not available in any context except `window`, and `allowWindowMessaging` is not available in any context except `content-script`. 17 | - `getCurrentContext` export has been removed. 18 | - `isInternalEndpoint` returns `true` for some new contexts. In summary it'll be `true` for `background`, `content-script`, `devtools`, `popup`, and `options`. 19 | - For messages sent from `background`, message queuing feature can no longer be trusted due to manifest v3 terminating the service worker runtime after certain time. The queue of messages 20 | sent from `background` will be disposed off along with the termination of the said service worker. Queuing still works for messages sent from all other contexts. 21 | 22 | ### Fixes 23 | 24 | - Fixed an issue with messages sometimes not reaching `content-script` or `window` when being sent by some other context right after a tab had navigated forward or back. This was caused by old port's 25 | `onDisconnect` callback being called _after_ the new port's `onConnect` callback. The `onDisconnect` would then remove the port mapping preventing messages from being routed to `content-script` or `window`. 26 | - If the message recipient terminates _(tab closure for example)_ before replying to the sender, the sender will be notified about the session termination instead of it waiting indefinetly for a response 27 | that's never coming back. Now, the `sendMessage` call in the sender will reject with an error. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Server Side Up 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | NUXT_APP_BASE_URL=/open-source/webext-bridge 2 | TOP_LEVEL_DOMAIN=http://localhost:3000 3 | BASE_PATH=http://localhost:3000 -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Prepare environment 6 | Copy over the `.env.example` file to `.env` and fill in the necessary environment variables. 7 | 8 | ```bash 9 | cp .env.example .env 10 | ``` 11 | 12 | ## Setup 13 | 14 | Make sure to install the dependencies: 15 | 16 | ```bash 17 | # npm 18 | npm install 19 | 20 | # pnpm 21 | pnpm install 22 | 23 | # yarn 24 | yarn install 25 | 26 | # bun 27 | bun install 28 | ``` 29 | 30 | ## Development Server 31 | 32 | Start the development server on `http://localhost:3000`: 33 | 34 | ```bash 35 | # npm 36 | npm run dev 37 | 38 | # pnpm 39 | pnpm run dev 40 | 41 | # yarn 42 | yarn dev 43 | 44 | # bun 45 | bun run dev 46 | ``` 47 | 48 | ## Production 49 | 50 | Build the application for production: 51 | 52 | ```bash 53 | # npm 54 | npm run build 55 | 56 | # pnpm 57 | pnpm run build 58 | 59 | # yarn 60 | yarn build 61 | 62 | # bun 63 | bun run build 64 | ``` 65 | 66 | Locally preview production build: 67 | 68 | ```bash 69 | # npm 70 | npm run preview 71 | 72 | # pnpm 73 | pnpm run preview 74 | 75 | # yarn 76 | yarn preview 77 | 78 | # bun 79 | bun run preview 80 | ``` 81 | 82 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 83 | -------------------------------------------------------------------------------- /docs/assets/animations.css: -------------------------------------------------------------------------------- 1 | .slide-in-right-enter-active { 2 | -webkit-animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; 3 | animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; 4 | } 5 | 6 | .slide-in-right-leave-active { 7 | -webkit-animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both reverse; 8 | animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both reverse; 9 | } 10 | 11 | @-webkit-keyframes slide-in-right { 12 | 0% { 13 | -webkit-transform: translateX(1000px); 14 | transform: translateX(1000px); 15 | opacity: 0; 16 | } 17 | 100% { 18 | -webkit-transform: translateX(0); 19 | transform: translateX(0); 20 | opacity: 1; 21 | } 22 | } 23 | @keyframes slide-in-right { 24 | 0% { 25 | -webkit-transform: translateX(1000px); 26 | transform: translateX(1000px); 27 | opacity: 0; 28 | } 29 | 100% { 30 | -webkit-transform: translateX(0); 31 | transform: translateX(0); 32 | opacity: 1; 33 | } 34 | } 35 | 36 | .slide-in-top-enter-active { 37 | -webkit-animation: slide-in-top 0.25s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; 38 | animation: slide-in-top 0.25s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; 39 | } 40 | 41 | .slide-in-top-leave-active { 42 | -webkit-animation: slide-in-top 0.25s cubic-bezier(0.250, 0.460, 0.450, 0.940) both reverse; 43 | animation: slide-in-top 0.25s cubic-bezier(0.250, 0.460, 0.450, 0.940) both reverse; 44 | } 45 | 46 | @-webkit-keyframes slide-in-top { 47 | 0% { 48 | -webkit-transform: translateY(-1000px); 49 | transform: translateY(-1000px); 50 | opacity: 0; 51 | } 52 | 100% { 53 | -webkit-transform: translateY(0); 54 | transform: translateY(0); 55 | opacity: 1; 56 | } 57 | } 58 | @keyframes slide-in-top { 59 | 0% { 60 | -webkit-transform: translateY(-1000px); 61 | transform: translateY(-1000px); 62 | opacity: 0; 63 | } 64 | 100% { 65 | -webkit-transform: translateY(0); 66 | transform: translateY(0); 67 | opacity: 1; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/assets/hamburger.css: -------------------------------------------------------------------------------- 1 | #nav-icon span { 2 | display: block; 3 | position: absolute; 4 | height: 2px; 5 | width: 50%; 6 | background: #FFF; 7 | opacity: 1; 8 | -webkit-transform: rotate(0deg); 9 | -moz-transform: rotate(0deg); 10 | -o-transform: rotate(0deg); 11 | transform: rotate(0deg); 12 | -webkit-transition: .25s ease-in-out; 13 | -moz-transition: .25s ease-in-out; 14 | -o-transition: .25s ease-in-out; 15 | transition: .25s ease-in-out; 16 | } 17 | 18 | #nav-icon span:nth-child(even) { 19 | left: 50%; 20 | border-radius: 0 9px 9px 0; 21 | } 22 | 23 | #nav-icon span:nth-child(odd) { 24 | left:0px; 25 | border-radius: 9px 0 0 9px; 26 | } 27 | 28 | #nav-icon span:nth-child(1), #nav-icon span:nth-child(2) { 29 | top: 0px; 30 | } 31 | 32 | #nav-icon span:nth-child(3), #nav-icon span:nth-child(4) { 33 | top: 6px; 34 | } 35 | 36 | #nav-icon span:nth-child(5), #nav-icon span:nth-child(6) { 37 | top: 12px; 38 | } 39 | 40 | #nav-icon.open span:nth-child(1),#nav-icon.open span:nth-child(6) { 41 | -webkit-transform: rotate(45deg); 42 | -moz-transform: rotate(45deg); 43 | -o-transform: rotate(45deg); 44 | transform: rotate(45deg); 45 | } 46 | 47 | #nav-icon.open span:nth-child(2),#nav-icon.open span:nth-child(5) { 48 | -webkit-transform: rotate(-45deg); 49 | -moz-transform: rotate(-45deg); 50 | -o-transform: rotate(-45deg); 51 | transform: rotate(-45deg); 52 | } 53 | 54 | #nav-icon.open span:nth-child(1) { 55 | left: 3px; 56 | top: 3px; 57 | } 58 | 59 | #nav-icon.open span:nth-child(2) { 60 | left: calc(50%); 61 | top: 3px; 62 | } 63 | 64 | #nav-icon.open span:nth-child(3) { 65 | left: -50%; 66 | opacity: 0; 67 | } 68 | 69 | #nav-icon.open span:nth-child(4) { 70 | left: 100%; 71 | opacity: 0; 72 | } 73 | 74 | #nav-icon.open span:nth-child(5) { 75 | left: 3px; 76 | top: 10px; 77 | } 78 | 79 | #nav-icon.open span:nth-child(6) { 80 | left: calc(50%); 81 | top: 10px; 82 | } -------------------------------------------------------------------------------- /docs/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "animations.css"; 2 | @import "hamburger.css"; 3 | @import 'tailwindcss/base'; 4 | @import 'tailwindcss/components'; 5 | @import 'tailwindcss/utilities'; 6 | 7 | ::-webkit-scrollbar { 8 | width: 0px; 9 | height: 0px; 10 | } -------------------------------------------------------------------------------- /docs/components/Docs/Anchor.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /docs/components/Docs/Eyebrow.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /docs/components/Docs/Footer.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /docs/components/Docs/Header.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | -------------------------------------------------------------------------------- /docs/components/Docs/Logo.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Docs/ModeToggle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/Docs/Navigation.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | -------------------------------------------------------------------------------- /docs/components/Docs/NavigationGroup.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /docs/components/Docs/PageLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/Docs/Search.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/Docs/SmallPrint.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/Docs/Tag.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /docs/components/Docs/TopLevelNavItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /docs/components/DocumentDrivenNotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /docs/components/Icons/Anchor.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Arrow.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/components/Icons/ChatBubbleIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/Icons/Check.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/CheckIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /docs/components/Icons/ClipboardIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/EnvelopeIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Logos/Chrome.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Moon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Resource.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /docs/components/Icons/Search.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Social/Discord.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Social/GitHub.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Social/Twitter.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/Sun.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/UserIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/Icons/UsersIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/AppButton.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /docs/components/content/AppHeading2.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/content/AppHeading3.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/components/content/AppHeading4.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/components/content/AppLink.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /docs/components/content/Code/ClipboardIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/Code/CopyButton.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | -------------------------------------------------------------------------------- /docs/components/content/Code/PanelHeader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/components/content/CodeGroup.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /docs/components/content/CodePanel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /docs/components/content/Column.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/components/content/DiscordIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/DocsIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/GitHubIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/GridPattern.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 40 | -------------------------------------------------------------------------------- /docs/components/content/Guide.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/content/Guides.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/content/HeartIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/HeroPattern.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /docs/components/content/InfoIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/LeadP.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /docs/components/content/MarketingBook.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/MarketingBrowsers.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /docs/components/content/MarketingFaq.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 81 | -------------------------------------------------------------------------------- /docs/components/content/MarketingFeatureGrid.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/MarketingHeader.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | -------------------------------------------------------------------------------- /docs/components/content/MarketingHero.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/NotProse.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/Note.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/Properties.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/Property.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /docs/components/content/Resources.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 67 | 68 | -------------------------------------------------------------------------------- /docs/components/content/Resources/Pattern.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | -------------------------------------------------------------------------------- /docs/components/content/Resources/Resource.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /docs/components/content/Resources/ResourceIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/components/content/ResponsiveImage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/content/Row.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/SearchIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/VideoEmbed.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /docs/components/content/Warning.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/composables/states.ts: -------------------------------------------------------------------------------- 1 | export const usePreferredProgrammingLanguage = () => useState('programming-language', () => '') -------------------------------------------------------------------------------- /docs/content/docs/1.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Messaging in Web Extensions made easy. Batteries included.' 3 | head.title: 'Introduction - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Introduction 8 | 9 | When building a web extension, communication between all pieces of the extension is essential, but complicated. Every browser is slightly different and targeting where to send your message is complicated. This package provides a simple, consistent API for sending messages between the different parts of your web extension, such as `background`, `content-script`, `devtools`, `popup`, `options`, and `window` contexts. 10 | 11 | This pacakge is production ready. We know, because we use it in [Bugflow](https://bugflow.io). While building our extension, we also wrote our book, ["The Easiest Guide to Building Browser Extensions"](https://serversideup.net/building-multi-platform-browser-extensions/) which highlights this package and shows a variety of in-context use cases. 12 | 13 | This project was originally started by Neek Sandhu (@zikaari) in 2017. Unfortunately time became a constraint and in January 2024, the project was graciously transferred to Server Side Up to be maintained. We're very grateful for Neek Sandhu’s contributions and we're excited to carry the torch forward. 🤝 14 | 15 | Let's get this package installed! -------------------------------------------------------------------------------- /docs/content/docs/2.getting-started/1.installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Quickly install Webext-Bridge using the package manager of your choice.' 3 | head.title: 'Installation - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Installation 8 | 9 | You can simply install the Webext-Bridge package using the package manager you are most comfortable with. 10 | 11 | ::code-panel 12 | --- 13 | label: NPM Installation 14 | --- 15 | ```bash 16 | $ npm i webext-bridge 17 | ``` 18 | :: 19 | 20 | ::code-panel 21 | --- 22 | label: Yarn Installation 23 | --- 24 | ```bash 25 | $ yarn add webext-bridge 26 | ``` 27 | :: 28 | 29 | That's it! You are ready to start simply passing messages between the different components of your extension. 30 | 31 | Up next is a quick example for those who want to see some code. Otherwise, jump to the [concepts section](/docs/guide/concepts) to see the full feature set. -------------------------------------------------------------------------------- /docs/content/docs/2.getting-started/2.quick-example.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Check out some example code before getting started.' 3 | head.title: 'Quick Example - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Quick Example 8 | Let's quickly send a message from our popup, to our background script. 9 | 10 | ## Popup 11 | In your popup, add the following code: 12 | 13 | ::code-panel 14 | --- 15 | label: Popup 16 | --- 17 | ```javascript 18 | import { sendMessage, onMessage } from "webext-bridge/popup"; 19 | const response = await sendMessage("ACTION", { 20 | data: data 21 | }, "background"); 22 | ``` 23 | :: 24 | 25 | ## Background Service Worker 26 | Now, head over to your background service worker script and add the following code: 27 | 28 | ::code-panel 29 | --- 30 | label: Background Service Worker (background.js) 31 | --- 32 | ```javascript 33 | import { onMessage, sendMessage } from "webext-bridge/background" 34 | 35 | onMessage( "ACTION", runAction ); 36 | async function runAction( {data} ){ 37 | // process data 38 | 39 | // return data 40 | return { 41 | 42 | }; 43 | } 44 | ``` 45 | :: 46 | 47 | That's it! You are ready to send messages. 48 | 49 | ## Advantages 50 | There's a lot of advantages to using the `webext-bridge` package. `webext-bridge` handles everything for you as efficiently as possible. No more `chrome.runtime.sendMessage` or `chrome.runtime.onConnect` or `chrome.runtime.connect` 51 | 52 | First, you can specifically target where your message is being sent or handled. Notice in the popup script, we import from `webext-bridge/popup` and in the background service worker, we import from `webext-bridge/background`. Super handy to control how your messages are being processed and where. No funky scoping or messages being processed in the wrong place. 53 | 54 | Second, the code is much cleaner and easy to read since you can easily bind an action to a function. No more massive switch/case statements that you'd have using the built in messaging systems: 55 | 56 | ::code-panel 57 | --- 58 | label: Compare to built in messaging 59 | --- 60 | ```javascript 61 | browser.runtime.onMessage.addListener( ( request, sender, sendResponse ) => { 62 | switch( request.action ){ 63 | case "ACTION": 64 | runAction( request.data ).then( sendResponse ); 65 | return true; 66 | break; 67 | } 68 | } ); 69 | ``` 70 | :: 71 | 72 | Also, this is a cross platform solution. This code will work on Firefox, Chrome, Safari, and Edge! 73 | 74 | While this is a very simple example, it shows the power and flexibility of the `webext-bridge` package. We will be diving in a lot more and showing a ton of examples. 75 | 76 | ## Learn how to use "webext-bridge" with our book 77 | We put together a comprehensive guide to help people [build multi-platform browser extensions](https://serversideup.net/building-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. -------------------------------------------------------------------------------- /docs/content/docs/3.guide/1.concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Quickly understand the concepts of using the Webext-Bridge package' 3 | head.title: 'Concepts - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Concepts 8 | A browser extension consists of a lot of moving parts. You essentially have small, microservices that need to communicate with one another in a variety of ways. As your extension grows in size, it gets all the more hairy when it comes to communication. 9 | 10 | When designing [Bugflow](https://bugflow.io), we handle around 15-20 different messages from a popup, background, and content scripts heading in all different directions. With standard, built-in, browser extension messaging, you don't have direct control where the message is sent, and if you have multiple messages, it's hard to efficiently handle all of them. You end up with massive switch case statements and callback chains. 11 | 12 | The goal of the `webext-bridge` package is to make your internal extension messaging a breeze. You can scope where the message is sent, use type-safe protocols, and handle incoming messages the most efficient way possible. 13 | 14 | Let's look at how the `webext-bridge` package allows you to efficiently communicate within your extension. 15 | 16 | ## Extension Communication Contexts 17 | In your extension, you will have multiple contexts that can send and receive messages: 18 | 19 | ![Extension Messaging Diagram](/images/docs/extension-communication-map.svg) 20 | 21 | Each of these contexts allows you to send a message to, or receive an incoming message. The available contexts available within the package are: 22 | 23 | - `content-script` 24 | - `popup` 25 | - `options` 26 | - `background` 27 | - `devtools` 28 | 29 | We will go through each of these contexts, explain how to use them, and what methods are available. In your code, you will just import the module by adding `import { } from 'webext-bridge/{context}'` wherever you need it. 30 | 31 | ## Background Script 32 | The background script context is special within `webext-bridge`. Even if your extension doesn't need a background page or wont be sending/receiving messages in background script. 33 | 34 | `webext-bridge` uses background/event context as staging area for messages, therefore it **must** loaded in background/event page for it to work. 35 | 36 | (Attempting to send message from any context will fail silently if `webext-bridge` isn't available in background page). See [troubleshooting section](/docs/guide/troubleshooting) for more. -------------------------------------------------------------------------------- /docs/content/docs/3.guide/2.type-safe-protocols.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Keep consistent type with your messaging using the type safe protocols' 3 | head.title: 'Concepts - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Type Safe Protocols 8 | 9 | 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. 10 | 11 | Create `shim.d.ts` file with the following content and make sure it's been included in `tsconfig.json`. 12 | 13 | ::code-panel 14 | --- 15 | label: shim.d.ts 16 | --- 17 | ```ts 18 | import { ProtocolWithReturn } from "webext-bridge"; 19 | 20 | declare module "webext-bridge" { 21 | export interface ProtocolMap { 22 | foo: { title: string }; 23 | // to specify the return type of the message, 24 | // use the `ProtocolWithReturn` type wrapper 25 | bar: ProtocolWithReturn; 26 | } 27 | } 28 | ``` 29 | :: 30 | 31 | Now within the different parts of your extension, you can use the following: 32 | 33 | ::code-panel 34 | --- 35 | label: Content Script 36 | --- 37 | ```ts 38 | import { onMessage } from 'webext-bridge/content-script' 39 | 40 | onMessage('foo', ({ data }) => { 41 | // type of `data` will be `{ title: string }` 42 | console.log(data.title) 43 | } 44 | ``` 45 | :: 46 | 47 | ::code-panel 48 | --- 49 | label: Background Worker 50 | --- 51 | ```ts 52 | import { sendMessage } from "webext-bridge/background"; 53 | 54 | const returnData = await sendMessage("bar", { 55 | /* ... */ 56 | }); 57 | // type of `returnData` will be `CustomReturnType` as specified 58 | ``` 59 | :: -------------------------------------------------------------------------------- /docs/content/docs/3.guide/4.examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Here are a few examples of how to use the webext-bridge package within your extension.' 3 | head.title: 'Examples - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | # Examples 7 | 8 | Here are a few common examples of how to use the `webext-bridge` package to communicate within your extension. There are way more possibilities, this should give a quick overview on how it all works together. 9 | 10 | ## sendMessage() and onMessage() 11 | Of the 6 available methods, the [sendMessage()](/docs/api/send-message) and the [onMessage()](/docs/api/on-message) methods are the most commonly used. Let's quickly go over these methods so the examples make a little more sense. 12 | 13 | The [sendMessage()](/docs/api/send-message) method actually sends the message and will be the same no matter what context you are importing it. This method will accept 3 parameters: 14 | - `messageId` - ID of the message that we are sending. I usually set this to an uppercase enum or string value so it's easy to listen for and makes sense. 15 | - `data` - JSON data to send along with the message. Used for processing. 16 | - `destination` - The destination is where we are sending the message to, such as `background`, `popup`, `content-script@{tabId}`, etc. 17 | 18 | Next, we have the [`onMessage()`](/docs/api/on-message) method which listens to an incoming message targeted. The two parameters accepted by this method are: 19 | - `messageId` - ID of the message are listening to. This matches the `messageId` from the `sendMessage()` method. 20 | - `callback` - The callback function used to handle the method. 21 | 22 | There are other methods that are exposed as well, but they are all built off of these two methods. All methods are documented in the [API](/docs/api/send-message). 23 | 24 | 25 | ## Popup -> Background Script 26 | In this example, we send a message from the popup to the background script. 27 | 28 | One thing to note. In all examples, we decouple the `{data}` in the `onMessage()` handler. You don't have to do that. For the most part, it's all you really need access too. However, the `callback` method accepts a JSON object that contains a little more information and is constructed like this: 29 | 30 | ::code-panel 31 | --- 32 | label: Parameter received by the `onMessage()` callback 33 | --- 34 | ```json 35 | { 36 | "sender": { 37 | "context": "popup", // could be any other context 38 | "tabId": null, 39 | "frameId": null 40 | }, 41 | "id": "MESSAGE_ID", 42 | "data": {}, 43 | "timestamp": 1701876927787 44 | } 45 | ``` 46 | :: 47 | 48 | This can be extremely helpful when figuring out where the message came from. I've used the sender.context to determine where the message came from in order to determine the response in some extensions. 49 | 50 | ::code-panel 51 | --- 52 | label: Popup 53 | --- 54 | ```javascript 55 | import { sendMessage } from "webext-bridge/popup"; 56 | 57 | const sendToBackground = async () => { 58 | await sendMessage("RECORD_NAME", { 59 | first_name: 'John', 60 | last_name: 'Doe' 61 | }, "background"); 62 | } 63 | ``` 64 | :: 65 | 66 | ::code-panel 67 | --- 68 | label: Background 69 | --- 70 | ```javascript 71 | import { onMessage } from "webext-bridge/background"; 72 | 73 | onMessage( "RECORD_NAME", recordName ); 74 | async function recordName( {data} ){ 75 | // Do whatever processing you need here. 76 | return { 77 | // Some response here 78 | }; 79 | } 80 | ``` 81 | :: 82 | 83 | ## Popup -> Content Script 84 | 85 | This example sends a message from the background to a content script. One SUPER important piece to point out, is when messaging a content script, you need to add the `tabId` where the script is located to the end of the destination. 86 | 87 | If you aren't using the [webextension-polyfill](https://www.npmjs.com/package/webextension-polyfill) package, I'd highly recommend it. Otherwise you will have to write an individual extension for each browser you want to support. In this example, we are using the polyfill and referencing the browser through `browser.` (the alternative would be `chrome.`). 88 | 89 | ::code-panel 90 | --- 91 | label: Popup 92 | --- 93 | ```javascript 94 | import { sendMessage } from "webext-bridge/popup"; 95 | 96 | function sendToContentScript{ 97 | let tabs = await browser.tabs.query({ 98 | active: true, 99 | currentWindow: true 100 | }); 101 | 102 | const response = await sendMessage("RECORD_NAME", { 103 | first_name: 'John', 104 | last_name: 'Doe' 105 | }, "content-script@"+tabs[0].id); 106 | } 107 | ``` 108 | :: 109 | 110 | Note, we are querying the active tab in the current window to grab the `tabId`. Feel free to use whatever method you need to get the `tabId` 111 | 112 | ::code-panel 113 | --- 114 | label: Content 115 | --- 116 | ```javascript 117 | import { onMessage } from "webext-bridge/content-script"; 118 | 119 | onMessage( "RECORD_NAME", recordName ); 120 | async function recordName( {data} ){ 121 | // Do whatever processing you need here. 122 | 123 | return { 124 | // Some response here 125 | }; 126 | } 127 | ``` 128 | :: 129 | 130 | 131 | ## Content Script -> Background Script 132 | This example sends a message from the content script to the background script. 133 | 134 | ::code-panel 135 | --- 136 | label: Content 137 | --- 138 | ```javascript 139 | import { sendMessage } from "webext-bridge/content-script"; 140 | 141 | const sendToBackground = async () => { 142 | const response = await sendMessage('RECORD_NAME', { 143 | first_name: 'John', 144 | last_name: 'Doe' 145 | }, 'background'); 146 | 147 | // Handle response 148 | } 149 | ``` 150 | :: 151 | 152 | ::code-panel 153 | --- 154 | label: Background 155 | --- 156 | ```javascript 157 | import { onMessage } from "webext-bridge/background"; 158 | 159 | onMessage( "RECORD_NAME", recordName ); 160 | async function recordName( {data} ){ 161 | // Do whatever processing you need here. 162 | 163 | return { 164 | // Some response here 165 | }; 166 | } 167 | ``` 168 | :: 169 | 170 | Hope these examples help! Definitely check out the [API](/docs/api/send-message) to see all available methods and how to use them within your extension. -------------------------------------------------------------------------------- /docs/content/docs/3.guide/5.security.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Serious security note, please read.' 3 | head.title: 'Security - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | # Security 7 | 8 | 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. 9 | 10 | `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`. 11 | 12 | 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. 13 | 14 | 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. 15 | 16 | If you plan on having something critical, **always** verify the `sender` before responding: 17 | 18 | ::code-panel 19 | --- 20 | label: Verifying an endpoint before responding 21 | --- 22 | ```javascript 23 | import { onMessage, isInternalEndpoint } from "webext-bridge/background"; 24 | 25 | onMessage("getUserBrowsingHistory", (message) => { 26 | const { data, sender } = message; 27 | 28 | // Respond only if request is from 'devtools', 'content-script', 'popup', 'options', or 'background' endpoint 29 | if (isInternalEndpoint(sender)) { 30 | const { range } = data; 31 | return getHistory(range); 32 | } 33 | }); 34 | ``` 35 | :: -------------------------------------------------------------------------------- /docs/content/docs/3.guide/6.troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Running into issues?' 3 | head.title: 'Troubleshooting - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Troubleshooting 8 | With all the moving parts, it's easy to run into issues. Here's a few common mistakes. 9 | 10 | ## Doesn't work? 11 | If `window` contexts are not part of the extension, `webext-bridge` works out of the box for messaging between `devtools` <-> `background` <-> `content-script`(s). 12 | 13 | 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. 14 | 15 | First, add a `background.js` file within your extension. In the background script, add the following code: 16 | 17 | ::code-panel 18 | --- 19 | label: background.js 20 | --- 21 | ```javascript 22 | import "webext-bridge/background"; 23 | ``` 24 | :: 25 | 26 | Next, include that file within your manifest: 27 | 28 | ::code-panel 29 | --- 30 | label: manifest.json 31 | --- 32 | ```json 33 | { 34 | "background": { 35 | "scripts": ["path/to/transpiled/background.js"] 36 | } 37 | } 38 | ``` 39 | :: 40 | 41 | You now have a simple background script within your extension that the `webext-bridge` can use as a staging ground for communicating messages. 42 | 43 | ## Can't send messages to `window`? 44 | Sending or receiving messages from or to `window` requires you to open the messaging gateway in content script(s) for that particular tab. 45 | 46 | Call `allowWindowMessaging()` in any of your content script(s) in that tab and call `setNamespace()` in the script loaded in top frame i.e the `window` context. Make sure that `namespaceA === namespaceB`. 47 | 48 | If you're doing this, read the [security section](/docs/guide/security) 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/content/docs/3.guide/7.resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'For more information, check out these resources' 3 | head.title: 'Resources - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Resources 8 | 9 | The best resource we have to offer regarding building browser extensions is our book, [Building Multi-Platform Browser Extensions](https://serversideup.net/building-multi-platform-browser-extensions/). We go through the whole process and provide templates to get you up and running. All templates use `webext-bridge` and we have tutorials for quite a few common issues that come up. 10 | 11 | - **[Discord](https://serversideup.net/discord)** for friendly support from the community and the team. 12 | - **[GitHub](https://github.com/serversideup/webext-bridge)** for source code, bug reports, and project management. 13 | - **[Get Professional Help](https://serversideup.net/professional-support)** - Get video + screen-sharing help directly from the core contributors. -------------------------------------------------------------------------------- /docs/content/docs/4.api/1.send-message.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'sendMessage() API Documentation' 3 | head.title: 'sendMessage() - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # sendMessage() 8 | 9 | `sendMessage(messageId: string, data: any, destination: string)` 10 | 11 | Sends a message to some other part of your extension. 12 | 13 | - If there is no listener on the other side an error will be thrown where `sendMessage` was called. 14 | 15 | - Listener on the other may want to reply. Get the reply by `await`ing the returned `Promise` 16 | 17 | - 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 18 | 19 | - 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. 20 | 21 | ## `messageId` 22 | 23 | **Required** | `string` 24 | 25 | Any `string` that both sides of your extension agree on. Could be `get-flag-count` or `getFlagCount` or `GET_FLAG_COUNT`, as long as it's same on receiver's `onMessage` listener. 26 | 27 | ## `data` 28 | 29 | **Required** | `any` 30 | 31 | 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. 32 | 33 | ## `destination` 34 | 35 | **Required** | `string` 36 | 37 | The actual identifier of other endpoint. 38 | 39 | Example: `devtools` or `content-script` or `background` or `content-script@133` or `devtools@453` 40 | 41 | `content-script`, `window` and `devtools` destinations can be suffixed with `@` to target specific tab. Example: `devtools@351`, points to devtools panel inspecting tab with id 351. 42 | 43 | For `content-script`, a specific `frameId` can be specified by appending the `frameId` to the suffix `@.`. 44 | 45 | Read [Notes](/docs/api/notes) section to see how destinations (or endpoints) are treated. 46 | 47 | ::note 48 | 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()` to unlock message routing. 49 | 50 | Also call `setNamespace()` 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) 51 | :: -------------------------------------------------------------------------------- /docs/content/docs/4.api/2.on-message.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'onMessage() API Documentation' 3 | head.title: 'onMessage() - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # onMessage() 8 | 9 | Register one and only one listener, per messageId per context. That will be called upon `sendMessage` from other side. 10 | 11 | Optionally, send a response to sender by returning any value or if async a `Promise`. 12 | 13 | ## `messageId` 14 | 15 | **Required** | `string` 16 | 17 | Any `string` that both sides of your extension agree on. Could be `get-flag-count` or `getFlagCount` or `GET_FLAG_COUNT`, as long as it's same in sender's `sendMessage` call. 18 | 19 | ## `callback` 20 | 21 | **Required** | `fn` 22 | 23 | A callback function `webext-bridge` should call when a message is received with same `messageId`. The callback function will be called with one argument, a `message` which has `sender`, `data` and `timestamp` as its properties. 24 | 25 | Optionally, this callback can return a value or a `Promise`, resolved value will sent as reply to sender. 26 | 27 | Read [security note](/docs/guide/security) before using this. -------------------------------------------------------------------------------- /docs/content/docs/4.api/3.allow-window-messaging.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'allowWindowMessaging() API Documentation' 3 | head.title: 'allowWindowMessaging() - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # allowWindowMessaging() 8 | 9 | ::warning 10 | Caution: Dangerous action 11 | :: 12 | 13 | API available only to content scripts 14 | 15 | Unlocks the transmission of messages to and from `window` (top frame of loaded page) contexts in the tab where it is called. 16 | 17 | `webext-bridge` by default won't transmit any payload to or from `window` contexts for [security](/docs/guide/security) reasons. 18 | 19 | This method can be called from a content script (in top frame of tab), which opens a gateway for messages. 20 | 21 | 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. 22 | 23 | ## `namespace` 24 | 25 | **Required** | `string` 26 | 27 | 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. -------------------------------------------------------------------------------- /docs/content/docs/4.api/4.set-namespace.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'setNamespace() API Documentation' 3 | head.title: 'setNamespace() - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # setNamespace() 8 | API available to scripts in top frame of loaded remote page. 9 | 10 | 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()` in it's content script with same namespace. 11 | 12 | ## `namespace` 13 | 14 | **Required** | `string` 15 | 16 | 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. -------------------------------------------------------------------------------- /docs/content/docs/4.api/5.open-stream.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'openStream() API Documentation' 3 | head.title: 'openStream() - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # openStream() 8 | ::note 9 | 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. 10 | :: 11 | 12 | Opens a `Stream` between caller and destination. 13 | 14 | Returns a `Promise` which resolves with `Stream` when the destination is ready (loaded and `onOpenStreamChannel` callback registered). 15 | 16 | ## `channel` 17 | 18 | **Required** | `string` 19 | 20 | `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. 21 | 22 | ## `destination` 23 | 24 | **Required** | `string` 25 | 26 | Same as `destination` in [`sendMessage(msgId, data, destination)`](/docs/api/send-message) -------------------------------------------------------------------------------- /docs/content/docs/4.api/6.on-open-stream-channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'onOpenStreamChannel() API Documentation' 3 | head.title: 'onOpenStreamChannel() - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # onOpenStreamChannel() 8 | ::note 9 | 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. 10 | :: 11 | 12 | Registers a listener for when a `Stream` opens. Only one listener per channel per context. 13 | 14 | ## `channel` 15 | 16 | **Required** | `string` 17 | 18 | `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. 19 | 20 | ## `callback` 21 | 22 | **Required** | `fn` 23 | 24 | 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. 25 | 26 | `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. -------------------------------------------------------------------------------- /docs/content/docs/4.api/7.notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Extra notes when working with webext-bridge' 3 | head.title: 'Notes - Webext-Bridge - Server Side Up' 4 | layout: docs 5 | --- 6 | 7 | # Notes 8 | ::note 9 | Following rules apply to `destination` being specified in `sendMessage(msgId, data, destination)` and `openStream(channelId, initialData, destination)` 10 | :: 11 | 12 | - 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. 13 | 14 | - 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. 15 | 16 | - 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()` 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). 17 | 18 | - 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. -------------------------------------------------------------------------------- /docs/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: marketing 3 | --- 4 | 5 | :marketing-hero 6 | 7 | :marketing-browsers 8 | 9 | :marketing-feature-grid 10 | 11 | :marketing-book 12 | 13 | :marketing-faq 14 | 15 | :marketing-build-in-public 16 | 17 | :marketing-footer -------------------------------------------------------------------------------- /docs/layouts/docs.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | -------------------------------------------------------------------------------- /docs/layouts/marketing.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /docs/middleware/directory.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(( to, from ) => { 2 | let redirectPath = to.path.endsWith('/') ? to.path.slice(0, -1) : to.path; 3 | 4 | switch( redirectPath ){ 5 | case '/docs/getting-started': 6 | return navigateTo( redirectPath+'/these-images-vs-others', { replace: true } ); 7 | break; 8 | case '/docs/guide': 9 | return navigateTo( redirectPath+'/choosing-the-right-image', { replace: true } ); 10 | break; 11 | case '/docs/reference': 12 | return navigateTo( redirectPath+'/environment-variable-specification', { replace: true } ); 13 | break; 14 | } 15 | }) -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindTypography from '@tailwindcss/typography' 2 | 3 | // https://nuxt.com/docs/api/configuration/nuxt-config 4 | export default defineNuxtConfig({ 5 | modules: [ 6 | 'nuxt-og-image', 7 | '@nuxtjs/color-mode', 8 | '@nuxt/content', 9 | '@nuxtjs/plausible', 10 | '@nuxtjs/tailwindcss', 11 | '@vueuse/nuxt' 12 | ], 13 | 14 | content: { 15 | documentDriven: true, 16 | 17 | experimental: { 18 | search: { 19 | indexed: true 20 | } 21 | }, 22 | 23 | markdown: { 24 | tags: { 25 | h2: 'AppHeading2', 26 | h3: 'AppHeading3', 27 | h4: 'AppHeading4' 28 | } 29 | }, 30 | 31 | highlight: { 32 | // OR 33 | theme: { 34 | // Default theme (same as single string) 35 | default: 'github-dark', 36 | // Theme used if `html.dark` 37 | dark: 'github-dark', 38 | // Theme used if `html.sepia` 39 | sepia: 'monokai' 40 | }, 41 | preload: [ 42 | 'dockerfile', 43 | 'ini' 44 | ] 45 | } 46 | }, 47 | 48 | colorMode: { 49 | classSuffix: '' 50 | }, 51 | 52 | devtools: { 53 | enabled: true 54 | }, 55 | 56 | nitro: { 57 | prerender: { 58 | routes: [ 59 | '/sitemap.xml', 60 | '/api/search.json' 61 | ] 62 | } 63 | }, 64 | 65 | ogImage: { 66 | componentDirs: ['~/components/Global/OgImage'], 67 | }, 68 | 69 | plausible: { 70 | apiHost: 'https://a.521dimensions.com' 71 | }, 72 | 73 | runtimeConfig: { 74 | public: { 75 | basePath: process.env.NUXT_APP_BASE_URL || '/', 76 | domain: process.env.TOP_LEVEL_DOMAIN 77 | } 78 | }, 79 | 80 | site: { 81 | url: process.env.BASE_PATH, 82 | }, 83 | 84 | tailwindcss: { 85 | config: { 86 | plugins: [tailwindTypography] 87 | }, 88 | cssPath: '~/assets/css/tailwind.css', 89 | } 90 | }) 91 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@headlessui/vue": "^1.7.22", 14 | "@heroicons/vue": "^2.1.3", 15 | "@nuxt/content": "^2.12.1", 16 | "@nuxtjs/color-mode": "^3.4.1", 17 | "@nuxtjs/plausible": "^1.0.0", 18 | "@nuxtjs/tailwindcss": "^6.12.0", 19 | "@tailwindcss/typography": "^0.5.13", 20 | "@vueuse/core": "^10.9.0", 21 | "@vueuse/nuxt": "^10.9.0", 22 | "nuxt": "^3.11.2", 23 | "nuxt-og-image": "^3.0.0-rc.53", 24 | "nuxt-site-config": "^2.2.12", 25 | "nuxt-site-config-kit": "^2.2.12", 26 | "sitemap": "^7.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/images/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/public/images/icons/square-book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/public/images/icons/square-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/public/images/icons/square-globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/public/images/icons/square-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/public/images/icons/square-lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/public/images/icons/square-target.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/public/images/seo/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/images/seo/og-image.png -------------------------------------------------------------------------------- /docs/public/images/ui/dan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/images/ui/dan.png -------------------------------------------------------------------------------- /docs/public/images/ui/jay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/images/ui/jay.png -------------------------------------------------------------------------------- /docs/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/mstile-144x144.png -------------------------------------------------------------------------------- /docs/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/mstile-150x150.png -------------------------------------------------------------------------------- /docs/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/mstile-310x150.png -------------------------------------------------------------------------------- /docs/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/mstile-310x310.png -------------------------------------------------------------------------------- /docs/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serversideup/webext-bridge/21fecc4072fd93ba4540940bc13d253ed7cb2137/docs/public/mstile-70x70.png -------------------------------------------------------------------------------- /docs/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/server/api/search.json.get.ts: -------------------------------------------------------------------------------- 1 | import { serverQueryContent } from '#content/server' 2 | 3 | export default eventHandler((event) => { 4 | return serverQueryContent(event).where({ _type: 'markdown', navigation: { $ne: false } }).find() 5 | }) -------------------------------------------------------------------------------- /docs/server/routes/sitemap.xml.ts: -------------------------------------------------------------------------------- 1 | import { serverQueryContent } from '#content/server' 2 | import { SitemapStream, streamToPromise } from 'sitemap' 3 | export default defineEventHandler(async (event) => { 4 | // Fetch all documents 5 | const docs = await serverQueryContent(event).find() 6 | const sitemap = new SitemapStream({ 7 | hostname: 'https://serversideup.net' 8 | }) 9 | 10 | for (const doc of docs) { 11 | sitemap.write({ 12 | url: '/open-source/webext-bridge'+doc._path, 13 | changefreq: 'monthly' 14 | }) 15 | } 16 | 17 | sitemap.end() 18 | return streamToPromise(sitemap) 19 | }) -------------------------------------------------------------------------------- /docs/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: "class", 6 | theme: { 7 | fontSize: { 8 | '2xs': ['0.75rem', { lineHeight: '1.25rem' }], 9 | xs: ['0.8125rem', { lineHeight: '1.5rem' }], 10 | sm: ['0.875rem', { lineHeight: '1.5rem' }], 11 | base: ['1rem', { lineHeight: '1.75rem' }], 12 | lg: ['1.125rem', { lineHeight: '1.75rem' }], 13 | xl: ['1.25rem', { lineHeight: '1.75rem' }], 14 | '2xl': ['1.5rem', { lineHeight: '2rem' }], 15 | '3xl': ['1.875rem', { lineHeight: '2.25rem' }], 16 | '4xl': ['2.25rem', { lineHeight: '2.5rem' }], 17 | '5xl': ['3rem', { lineHeight: '1' }], 18 | '6xl': ['3.75rem', { lineHeight: '1' }], 19 | '7xl': ['4.5rem', { lineHeight: '1' }], 20 | '8xl': ['6rem', { lineHeight: '1' }], 21 | '9xl': ['8rem', { lineHeight: '1' }], 22 | }, 23 | typography: require('./typography'), 24 | extend: { 25 | boxShadow: { 26 | glow: '0 0 4px rgb(0 0 0 / 0.1)', 27 | }, 28 | colors: { 29 | link: '#3B82F6' 30 | }, 31 | fontFamily: { 32 | 'sans': ['Inter', 'sans-serif'] 33 | }, 34 | maxWidth: { 35 | lg: '33rem', 36 | '2xl': '40rem', 37 | '3xl': '50rem', 38 | '5xl': '66rem', 39 | }, 40 | opacity: { 41 | 1: '0.01', 42 | 2.5: '0.025', 43 | 7.5: '0.075', 44 | 15: '0.15', 45 | } 46 | } 47 | }, 48 | plugins: [], 49 | content: [ 50 | `/components/**/*.{vue,js,ts}`, 51 | `/layouts/**/*.vue`, 52 | `/pages/**/*.vue`, 53 | `/composables/**/*.{js,ts}`, 54 | `/plugins/**/*.{js,ts}`, 55 | `/App.{js,ts,vue}`, 56 | `/app.{js,ts,vue}`, 57 | `/Error.{js,ts,vue}`, 58 | `/error.{js,ts,vue}` 59 | ], 60 | } -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webext-bridge", 3 | "version": "6.0.1", 4 | "description": "Messaging in Web Extensions made easy. Out of the box.", 5 | "keywords": [ 6 | "chrome", 7 | "extension", 8 | "messaging", 9 | "communication", 10 | "protocol", 11 | "content", 12 | "background", 13 | "devtools", 14 | "script", 15 | "crx", 16 | "bridge" 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/zikaari/webext-bridge.git" 22 | }, 23 | "author": "Neek Sandhu ", 24 | "scripts": { 25 | "build": "tsup src/index.ts src/background.ts src/content-script.ts src/devtools.ts src/options.ts src/popup.ts src/window.ts --format esm,cjs --dts", 26 | "watch": "npm run build -- --watch", 27 | "release": "bumpp --commit --push --tag && npm run build && npm publish" 28 | }, 29 | "type": "module", 30 | "exports": { 31 | ".": { 32 | "import": "./dist/index.js", 33 | "require": "./dist/index.cjs" 34 | }, 35 | "./background": { 36 | "import": "./dist/background.js", 37 | "require": "./dist/background.cjs" 38 | }, 39 | "./content-script": { 40 | "import": "./dist/content-script.js", 41 | "require": "./dist/content-script.cjs" 42 | }, 43 | "./devtools": { 44 | "import": "./dist/devtools.js", 45 | "require": "./dist/devtools.cjs" 46 | }, 47 | "./options": { 48 | "import": "./dist/options.js", 49 | "require": "./dist/options.cjs" 50 | }, 51 | "./popup": { 52 | "import": "./dist/popup.js", 53 | "require": "./dist/popup.cjs" 54 | }, 55 | "./window": { 56 | "import": "./dist/window.js", 57 | "require": "./dist/window.cjs" 58 | } 59 | }, 60 | "typesVersions": { 61 | "*": { 62 | "*": [ 63 | "dist/index.d.ts" 64 | ], 65 | "background": [ 66 | "dist/background.d.ts" 67 | ], 68 | "content-script": [ 69 | "dist/content-script.d.ts" 70 | ], 71 | "devtools": [ 72 | "dist/devtools.d.ts" 73 | ], 74 | "options": [ 75 | "dist/options.d.ts" 76 | ], 77 | "popup": [ 78 | "dist/popup.d.ts" 79 | ], 80 | "window": [ 81 | "dist/window.d.ts" 82 | ] 83 | } 84 | }, 85 | "files": [ 86 | "README.md", 87 | "package.json", 88 | "dist/**/*" 89 | ], 90 | "bugs": { 91 | "url": "https://github.com/zikaari/webext-bridge/issues" 92 | }, 93 | "homepage": "https://github.com/zikaari/webext-bridge#readme", 94 | "dependencies": { 95 | "@types/webextension-polyfill": "^0.8.3", 96 | "nanoevents": "^6.0.2", 97 | "serialize-error": "^9.0.0", 98 | "tiny-uid": "^1.1.1", 99 | "webextension-polyfill": "^0.9.0" 100 | }, 101 | "devDependencies": { 102 | "@antfu/eslint-config": "^0.16.1", 103 | "@types/node": "^17.0.16", 104 | "@typescript-eslint/eslint-plugin": "^5.11.0", 105 | "@typescript-eslint/parser": "^5.11.0", 106 | "bumpp": "^7.1.1", 107 | "eslint": "^8.8.0", 108 | "tsup": "^5.11.13", 109 | "type-fest": "^2.11.1", 110 | "typescript": "^4.5.5" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/content-script.ts: -------------------------------------------------------------------------------- 1 | import { createEndpointRuntime } from './internal/endpoint-runtime' 2 | import { usePostMessaging } from './internal/post-message' 3 | import { createStreamWirings } from './internal/stream' 4 | import { createPersistentPort } from './internal/persistent-port' 5 | import type { InternalMessage } from './types' 6 | 7 | const win = usePostMessaging('content-script') 8 | const port = createPersistentPort() 9 | const endpointRuntime = createEndpointRuntime('content-script', (message) => { 10 | if (message.destination.context === 'window') win.postMessage(message) 11 | else port.postMessage(message) 12 | }) 13 | 14 | win.onMessage((message: InternalMessage) => { 15 | endpointRuntime.handleMessage(Object.assign({}, message, {origin: { 16 | // a message event inside `content-script` means a script inside `window` dispatched it to be forwarded 17 | // so we're making sure that the origin is not tampered (i.e script is not masquerading it's true identity) 18 | context: "window", 19 | tabId: null 20 | }})) 21 | }) 22 | 23 | port.onMessage(endpointRuntime.handleMessage) 24 | 25 | port.onFailure((message) => { 26 | if (message.origin.context === 'window') { 27 | win.postMessage({ 28 | type: 'error', 29 | transactionID: message.transactionId, 30 | }) 31 | 32 | return 33 | } 34 | 35 | endpointRuntime.endTransaction(message.transactionId) 36 | }) 37 | 38 | export function allowWindowMessaging(nsps: string): void { 39 | win.setNamespace(nsps) 40 | win.enable() 41 | } 42 | 43 | export const { sendMessage, onMessage } = endpointRuntime 44 | export const { openStream, onOpenStreamChannel } 45 | = createStreamWirings(endpointRuntime) 46 | -------------------------------------------------------------------------------- /src/devtools.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import { createEndpointRuntime } from './internal/endpoint-runtime' 3 | import { createStreamWirings } from './internal/stream' 4 | import { createPersistentPort } from './internal/persistent-port' 5 | 6 | const port = createPersistentPort(`devtools@${browser.devtools.inspectedWindow.tabId}`) 7 | const endpointRuntime = createEndpointRuntime( 8 | 'devtools', 9 | message => port.postMessage(message), 10 | ) 11 | 12 | port.onMessage(endpointRuntime.handleMessage) 13 | 14 | export const { sendMessage, onMessage } = endpointRuntime 15 | export const { openStream, onOpenStreamChannel } = createStreamWirings(endpointRuntime) 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export { isInternalEndpoint } from './internal/is-internal-endpoint' 3 | export { parseEndpoint } from './internal/endpoint' 4 | -------------------------------------------------------------------------------- /src/internal/connection-args.ts: -------------------------------------------------------------------------------- 1 | import type { EndpointFingerprint } from './endpoint-fingerprint' 2 | 3 | export interface ConnectionArgs { 4 | endpointName: string 5 | fingerprint: EndpointFingerprint 6 | } 7 | 8 | const isValidConnectionArgs = ( 9 | args: unknown, 10 | requiredKeys: (keyof ConnectionArgs)[] = ['endpointName', 'fingerprint'], 11 | ): args is ConnectionArgs => 12 | typeof args === 'object' 13 | && args !== null 14 | && requiredKeys.every(k => k in args) 15 | 16 | export const encodeConnectionArgs = (args: ConnectionArgs) => { 17 | if (!isValidConnectionArgs(args)) 18 | throw new TypeError('Invalid connection args') 19 | 20 | return JSON.stringify(args) 21 | } 22 | 23 | export const decodeConnectionArgs = (encodedArgs: string): ConnectionArgs => { 24 | try { 25 | const args = JSON.parse(encodedArgs) 26 | return isValidConnectionArgs(args) ? args : null 27 | } 28 | catch (error) { 29 | return null 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/internal/delivery-logger.ts: -------------------------------------------------------------------------------- 1 | import type { InternalMessage } from '../types' 2 | import type { EndpointFingerprint } from './endpoint-fingerprint' 3 | 4 | export interface DeliveryReceipt { 5 | message: InternalMessage 6 | to: EndpointFingerprint 7 | from: { 8 | endpointId: string 9 | fingerprint: EndpointFingerprint 10 | } 11 | } 12 | 13 | export const createDeliveryLogger = () => { 14 | let logs: ReadonlyArray = [] 15 | 16 | return { 17 | add: (...receipts: DeliveryReceipt[]) => { 18 | logs = [...logs, ...receipts] 19 | }, 20 | remove: (message: string | DeliveryReceipt[]) => { 21 | logs 22 | = typeof message === 'string' 23 | ? logs.filter(receipt => receipt.message.transactionId !== message) 24 | : logs.filter(receipt => !message.includes(receipt)) 25 | }, 26 | entries: () => logs, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/internal/endpoint-fingerprint.ts: -------------------------------------------------------------------------------- 1 | import uid from 'tiny-uid' 2 | 3 | export type EndpointFingerprint = `uid::${string}` 4 | 5 | export const createFingerprint = (): EndpointFingerprint => `uid::${uid(7)}` 6 | -------------------------------------------------------------------------------- /src/internal/endpoint-runtime.ts: -------------------------------------------------------------------------------- 1 | import type { JsonValue } from 'type-fest' 2 | import uuid from 'tiny-uid' 3 | import { serializeError } from 'serialize-error' 4 | import type { 5 | BridgeMessage, 6 | DataTypeKey, 7 | Destination, 8 | GetDataType, 9 | GetReturnType, 10 | InternalMessage, 11 | OnMessageCallback, 12 | RuntimeContext, 13 | } from '../types' 14 | import { parseEndpoint } from './endpoint' 15 | 16 | export interface EndpointRuntime { 17 | sendMessage: < 18 | ReturnType extends JsonValue, 19 | K extends DataTypeKey = DataTypeKey, 20 | >( 21 | messageID: K, 22 | data: GetDataType, 23 | destination?: Destination 24 | ) => Promise> 25 | onMessage: ( 26 | messageID: K, 27 | callback: OnMessageCallback, GetReturnType> 28 | ) => (() => void) 29 | /** 30 | * @internal 31 | */ 32 | handleMessage: (message: InternalMessage) => void 33 | endTransaction: (transactionID: string) => void 34 | } 35 | 36 | export const createEndpointRuntime = ( 37 | thisContext: RuntimeContext, 38 | routeMessage: (msg: InternalMessage) => void, 39 | localMessage?: (msg: InternalMessage) => void, 40 | ): EndpointRuntime => { 41 | const runtimeId = uuid() 42 | const openTransactions = new Map< 43 | string, 44 | { resolve: (v: unknown) => void; reject: (e: unknown) => void } 45 | >() 46 | const onMessageListeners = new Map>() 47 | 48 | const handleMessage = (message: InternalMessage) => { 49 | if ( 50 | message.destination.context === thisContext 51 | && !message.destination.frameId 52 | && !message.destination.tabId 53 | ) { 54 | localMessage?.(message) 55 | 56 | const { transactionId, messageID, messageType } = message 57 | 58 | const handleReply = () => { 59 | const transactionP = openTransactions.get(transactionId) 60 | if (transactionP) { 61 | const { err, data } = message 62 | if (err) { 63 | const dehydratedErr = err as Record 64 | const errCtr = self[dehydratedErr.name] as any 65 | const hydratedErr = new ( 66 | typeof errCtr === 'function' ? errCtr : Error 67 | )(dehydratedErr.message) 68 | 69 | // eslint-disable-next-line no-restricted-syntax 70 | for (const prop in dehydratedErr) 71 | hydratedErr[prop] = dehydratedErr[prop] 72 | 73 | transactionP.reject(hydratedErr) 74 | } 75 | else { 76 | transactionP.resolve(data) 77 | } 78 | openTransactions.delete(transactionId) 79 | } 80 | } 81 | 82 | const handleNewMessage = async() => { 83 | let reply: JsonValue | void 84 | let err: Error 85 | let noHandlerFoundError = false 86 | 87 | try { 88 | const cb = onMessageListeners.get(messageID) 89 | if (typeof cb === 'function') { 90 | // eslint-disable-next-line n/no-callback-literal 91 | reply = await cb({ 92 | sender: message.origin, 93 | id: messageID, 94 | data: message.data, 95 | timestamp: message.timestamp, 96 | } as BridgeMessage) 97 | } 98 | else { 99 | noHandlerFoundError = true 100 | throw new Error( 101 | `[webext-bridge] No handler registered in '${thisContext}' to accept messages with id '${messageID}'`, 102 | ) 103 | } 104 | } 105 | catch (error) { 106 | err = error 107 | } 108 | finally { 109 | if (err) message.err = serializeError(err) 110 | 111 | handleMessage({ 112 | ...message, 113 | messageType: 'reply', 114 | data: reply, 115 | origin: { context: thisContext, tabId: null }, 116 | destination: message.origin, 117 | hops: [], 118 | }) 119 | 120 | if (err && !noHandlerFoundError) 121 | // eslint-disable-next-line no-unsafe-finally 122 | throw reply 123 | } 124 | } 125 | 126 | switch (messageType) { 127 | case 'reply': 128 | return handleReply() 129 | case 'message': 130 | return handleNewMessage() 131 | } 132 | } 133 | 134 | message.hops.push(`${thisContext}::${runtimeId}`) 135 | 136 | return routeMessage(message) 137 | } 138 | 139 | return { 140 | handleMessage, 141 | endTransaction: (transactionID) => { 142 | const transactionP = openTransactions.get(transactionID) 143 | transactionP?.reject('Transaction was ended before it could complete') 144 | openTransactions.delete(transactionID) 145 | }, 146 | sendMessage: (messageID, data, destination = 'background') => { 147 | const endpoint 148 | = typeof destination === 'string' 149 | ? parseEndpoint(destination) 150 | : destination 151 | const errFn = 'Bridge#sendMessage ->' 152 | 153 | if (!endpoint.context) { 154 | throw new TypeError( 155 | `${errFn} Destination must be any one of known destinations`, 156 | ) 157 | } 158 | 159 | return new Promise((resolve, reject) => { 160 | const payload: InternalMessage = { 161 | messageID, 162 | data, 163 | destination: endpoint, 164 | messageType: 'message', 165 | transactionId: uuid(), 166 | origin: { context: thisContext, tabId: null }, 167 | hops: [], 168 | timestamp: Date.now(), 169 | } 170 | 171 | openTransactions.set(payload.transactionId, { resolve, reject }) 172 | 173 | try { 174 | handleMessage(payload) 175 | } 176 | catch (error) { 177 | openTransactions.delete(payload.transactionId) 178 | reject(error) 179 | } 180 | }) 181 | }, 182 | onMessage: (messageID, callback) => { 183 | onMessageListeners.set(messageID, callback) 184 | return () => onMessageListeners.delete(messageID) 185 | }, 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/internal/endpoint.ts: -------------------------------------------------------------------------------- 1 | import type { Endpoint, RuntimeContext } from '../types' 2 | 3 | const ENDPOINT_RE = /^((?:background$)|devtools|popup|options|content-script|window)(?:@(\d+)(?:\.(\d+))?)?$/ 4 | 5 | export const parseEndpoint = (endpoint: string): Endpoint => { 6 | const [, context, tabId, frameId] = endpoint.match(ENDPOINT_RE) || [] 7 | 8 | return { 9 | context: context as RuntimeContext, 10 | tabId: +tabId, 11 | frameId: frameId ? +frameId : undefined, 12 | } 13 | } 14 | 15 | export const formatEndpoint = ({ context, tabId, frameId }: Endpoint): string => { 16 | if (['background', 'popup', 'options'].includes(context)) 17 | return context 18 | 19 | return `${context}@${tabId}${frameId ? `.${frameId}` : ''}` 20 | } 21 | -------------------------------------------------------------------------------- /src/internal/is-internal-endpoint.ts: -------------------------------------------------------------------------------- 1 | import type { Endpoint, RuntimeContext } from '../types' 2 | 3 | const internalEndpoints: RuntimeContext[] = ['background', 'devtools', 'content-script', 'options', 'popup'] 4 | 5 | export const isInternalEndpoint = ({ context: ctx }: Endpoint): boolean => internalEndpoints.includes(ctx) 6 | -------------------------------------------------------------------------------- /src/internal/message-port.ts: -------------------------------------------------------------------------------- 1 | let promise: Promise 2 | 3 | /** 4 | * Returns a MessagePort for one-on-one communication 5 | * 6 | * Depending on which context's code runs first, either an incoming port from the other side 7 | * is accepted OR a port will be offered, which the other side will then accept. 8 | */ 9 | export const getMessagePort = ( 10 | thisContext: 'window' | 'content-script', 11 | namespace: string, 12 | onMessage: (e: MessageEvent) => void, 13 | ): Promise => ( 14 | promise ??= new Promise((resolve) => { 15 | const acceptMessagingPort = (event: MessageEvent) => { 16 | const { data: { cmd, scope, context }, ports } = event 17 | if (cmd === 'webext-port-offer' && scope === namespace && context !== thisContext) { 18 | window.removeEventListener('message', acceptMessagingPort) 19 | ports[0].onmessage = onMessage 20 | ports[0].postMessage('port-accepted') 21 | return resolve(ports[0]) 22 | } 23 | } 24 | 25 | const offerMessagingPort = () => { 26 | const channel = new MessageChannel() 27 | channel.port1.onmessage = (event: MessageEvent) => { 28 | if (event.data === 'port-accepted') { 29 | window.removeEventListener('message', acceptMessagingPort) 30 | return resolve(channel.port1) 31 | } 32 | 33 | onMessage?.(event) 34 | } 35 | 36 | window.postMessage({ 37 | cmd: 'webext-port-offer', 38 | scope: namespace, 39 | context: thisContext, 40 | }, '*', [channel.port2]) 41 | } 42 | 43 | window.addEventListener('message', acceptMessagingPort) 44 | 45 | // one of the contexts needs to be offset by at least 1 tick to prevent a race condition 46 | // where both of them are offering, and then also accepting the port at the same time 47 | if (thisContext === 'window') 48 | setTimeout(offerMessagingPort, 0) 49 | else 50 | offerMessagingPort() 51 | }) 52 | ) 53 | -------------------------------------------------------------------------------- /src/internal/persistent-port.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import type { Runtime } from 'webextension-polyfill' 3 | import type { InternalMessage } from '../types' 4 | import { createFingerprint } from './endpoint-fingerprint' 5 | import type { QueuedMessage } from './types' 6 | import { encodeConnectionArgs } from './connection-args' 7 | import { createDeliveryLogger } from './delivery-logger' 8 | import type { StatusMessage } from './port-message' 9 | import { PortMessage } from './port-message' 10 | 11 | /** 12 | * Manfiest V3 extensions can have their service worker terminated at any point 13 | * by the browser. That termination of service worker also terminates any messaging 14 | * porta created by other parts of the extension. This class is a wrapper around the 15 | * built-in Port object that re-instantiates the port connection everytime it gets 16 | * suspended 17 | */ 18 | export const createPersistentPort = (name = '') => { 19 | const fingerprint = createFingerprint() 20 | let port: Runtime.Port 21 | let undeliveredQueue: ReadonlyArray = [] 22 | const pendingResponses = createDeliveryLogger() 23 | const onMessageListeners = new Set< 24 | (message: InternalMessage, port: Runtime.Port) => void 25 | >() 26 | const onFailureListeners = new Set<(message: InternalMessage) => void>() 27 | 28 | const handleMessage = (msg: StatusMessage, port: Runtime.Port) => { 29 | switch (msg.status) { 30 | case 'undeliverable': 31 | if ( 32 | !undeliveredQueue.some( 33 | m => m.message.messageID === msg.message.messageID, 34 | ) 35 | ) { 36 | undeliveredQueue = [ 37 | ...undeliveredQueue, 38 | { 39 | message: msg.message, 40 | resolvedDestination: msg.resolvedDestination, 41 | }, 42 | ] 43 | } 44 | 45 | return 46 | 47 | case 'deliverable': 48 | undeliveredQueue = undeliveredQueue.reduce((acc, queuedMsg) => { 49 | if (queuedMsg.resolvedDestination === msg.deliverableTo) { 50 | PortMessage.toBackground(port, { 51 | type: 'deliver', 52 | message: queuedMsg.message, 53 | }) 54 | 55 | return acc 56 | } 57 | 58 | return [...acc, queuedMsg] 59 | }, [] as ReadonlyArray) 60 | 61 | return 62 | 63 | case 'delivered': 64 | if (msg.receipt.message.messageType === 'message') 65 | pendingResponses.add(msg.receipt) 66 | 67 | return 68 | 69 | case 'incoming': 70 | if (msg.message.messageType === 'reply') 71 | pendingResponses.remove(msg.message.messageID) 72 | 73 | onMessageListeners.forEach(cb => cb(msg.message, port)) 74 | 75 | return 76 | 77 | case 'terminated': { 78 | const rogueMsgs = pendingResponses 79 | .entries() 80 | .filter(receipt => msg.fingerprint === receipt.to) 81 | pendingResponses.remove(rogueMsgs) 82 | rogueMsgs.forEach(({ message }) => 83 | onFailureListeners.forEach(cb => cb(message)), 84 | ) 85 | } 86 | } 87 | } 88 | 89 | const connect = () => { 90 | port = browser.runtime.connect({ 91 | name: encodeConnectionArgs({ 92 | endpointName: name, 93 | fingerprint, 94 | }), 95 | }) 96 | port.onMessage.addListener(handleMessage) 97 | port.onDisconnect.addListener(connect) 98 | 99 | PortMessage.toBackground(port, { 100 | type: 'sync', 101 | pendingResponses: pendingResponses.entries(), 102 | pendingDeliveries: [ 103 | ...new Set( 104 | undeliveredQueue.map(({ resolvedDestination }) => resolvedDestination), 105 | ), 106 | ], 107 | }) 108 | } 109 | 110 | connect() 111 | 112 | return { 113 | onFailure(cb: (message: InternalMessage) => void) { 114 | onFailureListeners.add(cb) 115 | }, 116 | onMessage(cb: (message: InternalMessage) => void): void { 117 | onMessageListeners.add(cb) 118 | }, 119 | postMessage(message: any): void { 120 | PortMessage.toBackground(port, { 121 | type: 'deliver', 122 | message, 123 | }) 124 | }, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/internal/port-message.ts: -------------------------------------------------------------------------------- 1 | import type { Runtime } from 'webextension-polyfill' 2 | import type { InternalMessage } from '../types' 3 | import type { DeliveryReceipt } from './delivery-logger' 4 | import type { EndpointFingerprint } from './endpoint-fingerprint' 5 | 6 | export type StatusMessage = 7 | | { 8 | status: 'undeliverable' 9 | message: InternalMessage 10 | resolvedDestination: string 11 | } 12 | | { 13 | status: 'deliverable' 14 | deliverableTo: string 15 | } 16 | | { 17 | status: 'delivered' 18 | receipt: DeliveryReceipt 19 | } 20 | | { 21 | status: 'incoming' 22 | message: InternalMessage 23 | } 24 | | { 25 | status: 'terminated' 26 | fingerprint: EndpointFingerprint 27 | } 28 | 29 | export type RequestMessage = 30 | | { 31 | type: 'sync' 32 | pendingResponses: ReadonlyArray 33 | pendingDeliveries: ReadonlyArray 34 | } 35 | | { 36 | type: 'deliver' 37 | message: InternalMessage 38 | } 39 | 40 | export class PortMessage { 41 | static toBackground(port: Runtime.Port, message: RequestMessage) { 42 | return port.postMessage(message) 43 | } 44 | 45 | static toExtensionContext(port: Runtime.Port, message: StatusMessage) { 46 | return port.postMessage(message) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/internal/post-message.ts: -------------------------------------------------------------------------------- 1 | import type { InternalMessage } from '../types' 2 | import { getMessagePort } from './message-port' 3 | 4 | export interface EndpointWontRespondError { 5 | type: 'error' 6 | transactionID: string 7 | } 8 | 9 | export const usePostMessaging = (thisContext: 'window' | 'content-script') => { 10 | let allocatedNamespace: string 11 | let messagingEnabled = false 12 | let onMessageCallback: ( 13 | msg: InternalMessage | EndpointWontRespondError 14 | ) => void 15 | let portP: Promise 16 | 17 | return { 18 | enable: () => (messagingEnabled = true), 19 | onMessage: (cb: typeof onMessageCallback) => (onMessageCallback = cb), 20 | postMessage: async(msg: InternalMessage | EndpointWontRespondError) => { 21 | if (thisContext !== 'content-script' && thisContext !== 'window') 22 | throw new Error('Endpoint does not use postMessage') 23 | 24 | if (!messagingEnabled) 25 | throw new Error('Communication with window has not been allowed') 26 | 27 | ensureNamespaceSet(allocatedNamespace) 28 | 29 | return (await portP).postMessage(msg) 30 | }, 31 | setNamespace: (nsps: string) => { 32 | if (allocatedNamespace) 33 | throw new Error('Namespace once set cannot be changed') 34 | 35 | allocatedNamespace = nsps 36 | portP = getMessagePort(thisContext, nsps, ({ data }) => 37 | onMessageCallback?.(data), 38 | ) 39 | }, 40 | } 41 | } 42 | 43 | function ensureNamespaceSet(namespace: string) { 44 | if (typeof namespace !== 'string' || namespace.trim().length === 0) { 45 | throw new Error( 46 | 'webext-bridge uses window.postMessage to talk with other "window"(s) for message routing' 47 | + 'which is global/conflicting operation in case there are other scripts using webext-bridge. ' 48 | + 'Call Bridge#setNamespace(nsps) to isolate your app. Example: setNamespace(\'com.facebook.react-devtools\'). ' 49 | + 'Make sure to use same namespace across all your scripts whereever window.postMessage is likely to be used`', 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/internal/stream.ts: -------------------------------------------------------------------------------- 1 | import { createNanoEvents } from 'nanoevents' 2 | import uuid from 'tiny-uid' 3 | import type { Emitter } from 'nanoevents' 4 | import type { JsonValue } from 'type-fest' 5 | import type { Endpoint, HybridUnsubscriber, RuntimeContext, StreamInfo } from '../types' 6 | import type { EndpointRuntime } from './endpoint-runtime' 7 | import { parseEndpoint } from './endpoint' 8 | 9 | /** 10 | * Built on top of Bridge. Nothing much special except that Stream allows 11 | * you to create a namespaced scope under a channel name of your choice 12 | * and allows continuous e2e communication, with less possibility of 13 | * conflicting messageId's, since streams are strictly scoped. 14 | */ 15 | export class Stream { 16 | private static initDone = false 17 | private static openStreams: Map = new Map() 18 | 19 | private emitter: Emitter = createNanoEvents() 20 | private isClosed = false 21 | constructor(private endpointRuntime: EndpointRuntime, private streamInfo: StreamInfo) { 22 | if (!Stream.initDone) { 23 | endpointRuntime.onMessage<{ streamId: string; action: 'transfer' | 'close'; streamTransfer: JsonValue }, string>('__crx_bridge_stream_transfer__', (msg) => { 24 | const { streamId, streamTransfer, action } = msg.data 25 | const stream = Stream.openStreams.get(streamId) 26 | if (stream && !stream.isClosed) { 27 | if (action === 'transfer') 28 | stream.emitter.emit('message', streamTransfer) 29 | 30 | if (action === 'close') { 31 | Stream.openStreams.delete(streamId) 32 | stream.handleStreamClose() 33 | } 34 | } 35 | }) 36 | Stream.initDone = true 37 | } 38 | 39 | Stream.openStreams.set(this.streamInfo.streamId, this) 40 | } 41 | 42 | /** 43 | * Returns stream info 44 | */ 45 | public get info(): StreamInfo { 46 | return this.streamInfo 47 | } 48 | 49 | /** 50 | * Sends a message to other endpoint. 51 | * Will trigger onMessage on the other side. 52 | * 53 | * Warning: Before sending sensitive data, verify the endpoint using `stream.info.endpoint.isInternal()` 54 | * The other side could be malicious webpage speaking same language as webext-bridge 55 | * @param msg 56 | */ 57 | public send(msg?: JsonValue): void { 58 | if (this.isClosed) 59 | throw new Error('Attempting to send a message over closed stream. Use stream.onClose() to keep an eye on stream status') 60 | 61 | this.endpointRuntime.sendMessage('__crx_bridge_stream_transfer__', { 62 | streamId: this.streamInfo.streamId, 63 | streamTransfer: msg, 64 | action: 'transfer', 65 | }, this.streamInfo.endpoint) 66 | } 67 | 68 | /** 69 | * Closes the stream. 70 | * Will trigger stream.onClose() on both endpoints. 71 | * If needed again, spawn a new Stream, as this instance cannot be re-opened 72 | * @param msg 73 | */ 74 | public close(msg?: JsonValue): void { 75 | if (msg) 76 | this.send(msg) 77 | 78 | this.handleStreamClose() 79 | 80 | this.endpointRuntime.sendMessage('__crx_bridge_stream_transfer__', { 81 | streamId: this.streamInfo.streamId, 82 | streamTransfer: null, 83 | action: 'close', 84 | }, this.streamInfo.endpoint) 85 | } 86 | 87 | /** 88 | * Registers a callback to fire whenever other endpoint sends a message 89 | * @param callback 90 | */ 91 | public onMessage(callback: (msg?: T) => void): HybridUnsubscriber { 92 | return this.getDisposable('message', callback) 93 | } 94 | 95 | /** 96 | * Registers a callback to fire whenever stream.close() is called on either endpoint 97 | * @param callback 98 | */ 99 | public onClose(callback: (msg?: T) => void): HybridUnsubscriber { 100 | return this.getDisposable('closed', callback) 101 | } 102 | 103 | private handleStreamClose = () => { 104 | if (!this.isClosed) { 105 | this.isClosed = true 106 | this.emitter.emit('closed', true) 107 | this.emitter.events = {} 108 | } 109 | } 110 | 111 | private getDisposable(event: string, callback: () => void): HybridUnsubscriber { 112 | const off = this.emitter.on(event, callback) 113 | 114 | return Object.assign(off, { 115 | dispose: off, 116 | close: off, 117 | }) 118 | } 119 | } 120 | 121 | export const createStreamWirings = (endpointRuntime: EndpointRuntime) => { 122 | const openStreams = new Map() 123 | const onOpenStreamCallbacks = new Map void>() 124 | const streamyEmitter = createNanoEvents() 125 | 126 | endpointRuntime.onMessage<{ channel: string; streamId: string }, string>('__crx_bridge_stream_open__', (message) => { 127 | return new Promise((resolve) => { 128 | const { sender, data } = message 129 | const { channel } = data 130 | let watching = false 131 | let off = () => { } 132 | 133 | const readyup = () => { 134 | const callback = onOpenStreamCallbacks.get(channel) 135 | 136 | if (typeof callback === 'function') { 137 | callback(new Stream(endpointRuntime, { ...data, endpoint: sender })) 138 | if (watching) 139 | off() 140 | 141 | resolve(true) 142 | } 143 | else if (!watching) { 144 | watching = true 145 | off = streamyEmitter.on('did-change-stream-callbacks', readyup) 146 | } 147 | } 148 | 149 | readyup() 150 | }) 151 | }) 152 | 153 | async function openStream(channel: string, destination: RuntimeContext | Endpoint | string): Promise { 154 | if (openStreams.has(channel)) 155 | throw new Error('webext-bridge: A Stream is already open at this channel') 156 | 157 | const endpoint = typeof destination === 'string' ? parseEndpoint(destination) : destination 158 | 159 | const streamInfo: StreamInfo = { streamId: uuid(), channel, endpoint } 160 | const stream = new Stream(endpointRuntime, streamInfo) 161 | stream.onClose(() => openStreams.delete(channel)) 162 | await endpointRuntime.sendMessage('__crx_bridge_stream_open__', streamInfo as unknown as JsonValue, endpoint) 163 | openStreams.set(channel, stream) 164 | return stream 165 | } 166 | 167 | function onOpenStreamChannel(channel: string, callback: (stream: Stream) => void): void { 168 | if (onOpenStreamCallbacks.has(channel)) 169 | throw new Error('webext-bridge: This channel has already been claimed. Stream allows only one-on-one communication') 170 | 171 | onOpenStreamCallbacks.set(channel, callback) 172 | streamyEmitter.emit('did-change-stream-callbacks') 173 | } 174 | 175 | return { 176 | openStream, 177 | onOpenStreamChannel, 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/internal/types.ts: -------------------------------------------------------------------------------- 1 | import type { InternalMessage } from '../types' 2 | 3 | export interface QueuedMessage { 4 | resolvedDestination: string 5 | message: InternalMessage 6 | } 7 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { createEndpointRuntime } from './internal/endpoint-runtime' 2 | import { createStreamWirings } from './internal/stream' 3 | import { createPersistentPort } from './internal/persistent-port' 4 | 5 | const port = createPersistentPort('options') 6 | const endpointRuntime = createEndpointRuntime( 7 | 'options', 8 | message => port.postMessage(message), 9 | ) 10 | 11 | port.onMessage(endpointRuntime.handleMessage) 12 | 13 | export const { sendMessage, onMessage } = endpointRuntime 14 | export const { openStream, onOpenStreamChannel } = createStreamWirings(endpointRuntime) 15 | -------------------------------------------------------------------------------- /src/popup.ts: -------------------------------------------------------------------------------- 1 | import { createEndpointRuntime } from './internal/endpoint-runtime' 2 | import { createStreamWirings } from './internal/stream' 3 | import { createPersistentPort } from './internal/persistent-port' 4 | 5 | const port = createPersistentPort('popup') 6 | const endpointRuntime = createEndpointRuntime( 7 | 'popup', 8 | message => port.postMessage(message), 9 | ) 10 | 11 | port.onMessage(endpointRuntime.handleMessage) 12 | 13 | export const { sendMessage, onMessage } = endpointRuntime 14 | export const { openStream, onOpenStreamChannel } = createStreamWirings(endpointRuntime) 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { JsonValue, Jsonify } from 'type-fest' 2 | 3 | export type RuntimeContext = 4 | | 'devtools' 5 | | 'background' 6 | | 'popup' 7 | | 'options' 8 | | 'content-script' 9 | | 'window' 10 | 11 | export interface Endpoint { 12 | context: RuntimeContext 13 | tabId: number 14 | frameId?: number 15 | } 16 | 17 | export interface BridgeMessage { 18 | sender: Endpoint 19 | id: string 20 | data: T 21 | timestamp: number 22 | } 23 | 24 | export type OnMessageCallback = ( 25 | message: BridgeMessage 26 | ) => R | Promise 27 | 28 | export interface InternalMessage { 29 | origin: Endpoint 30 | destination: Endpoint 31 | transactionId: string 32 | hops: string[] 33 | messageID: string 34 | messageType: 'message' | 'reply' 35 | err?: JsonValue 36 | data?: JsonValue | void 37 | timestamp: number 38 | } 39 | 40 | export interface StreamInfo { 41 | streamId: string 42 | channel: string 43 | endpoint: Endpoint 44 | } 45 | 46 | export interface HybridUnsubscriber { 47 | (): void 48 | dispose: () => void 49 | close: () => void 50 | } 51 | 52 | export type Destination = Endpoint | RuntimeContext | string 53 | 54 | declare const ProtocolWithReturnSymbol: unique symbol 55 | 56 | export interface ProtocolWithReturn { 57 | data: Jsonify 58 | return: Jsonify 59 | /** 60 | * Type differentiator only. 61 | */ 62 | [ProtocolWithReturnSymbol]: true 63 | } 64 | 65 | /** 66 | * Extendable by user. 67 | */ 68 | export interface ProtocolMap { 69 | // foo: { id: number, name: string } 70 | // bar: ProtocolWithReturn 71 | } 72 | 73 | export type DataTypeKey = keyof ProtocolMap extends never 74 | ? string 75 | : keyof ProtocolMap 76 | 77 | export type GetDataType< 78 | K extends DataTypeKey, 79 | Fallback extends JsonValue = undefined, 80 | > = K extends keyof ProtocolMap 81 | ? ProtocolMap[K] extends (...args: infer Args) => any 82 | ? Args['length'] extends 0 83 | ? undefined 84 | : Args[0] 85 | : ProtocolMap[K] extends ProtocolWithReturn 86 | ? Data 87 | : ProtocolMap[K] 88 | : Fallback; 89 | 90 | 91 | export type GetReturnType< 92 | K extends DataTypeKey, 93 | Fallback extends JsonValue = undefined 94 | > = K extends keyof ProtocolMap 95 | ? ProtocolMap[K] extends (...args: any[]) => infer R 96 | ? R 97 | : ProtocolMap[K] extends ProtocolWithReturn 98 | ? Return 99 | : void 100 | : Fallback; 101 | -------------------------------------------------------------------------------- /src/window.ts: -------------------------------------------------------------------------------- 1 | import { createEndpointRuntime } from './internal/endpoint-runtime' 2 | import { usePostMessaging } from './internal/post-message' 3 | import { createStreamWirings } from './internal/stream' 4 | 5 | const win = usePostMessaging('window') 6 | 7 | const endpointRuntime = createEndpointRuntime('window', message => 8 | win.postMessage(message), 9 | ) 10 | 11 | win.onMessage((msg) => { 12 | if ('type' in msg && 'transactionID' in msg) 13 | endpointRuntime.endTransaction(msg.transactionID) 14 | else endpointRuntime.handleMessage(msg) 15 | }) 16 | 17 | export function setNamespace(nsps: string): void { 18 | win.setNamespace(nsps) 19 | win.enable() 20 | } 21 | 22 | export const { sendMessage, onMessage } = endpointRuntime 23 | export const { openStream, onOpenStreamChannel } 24 | = createStreamWirings(endpointRuntime) 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "outDir": "./dist", 6 | "jsx": "react", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "declarationDir": "./dist" 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "dist" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------