Browser plugins/extensions

You can add extensions/plugins to your browser, for instance, DarkReader or uBlock. It's quite easy to write your own! πŸš€

Where to learn? 🏫

There are a few differences between Firefox and Chrome. The main difference is that in Chrome 🌏 you will use

chrome.xxx.yyy

While inside Firefox 🦊, you will use

browser.xxx.yyy

🐍 Extension Workflow 🐍

Add a local extension πŸš€

  • Edge: Go to edge://extensions/, enable Developer mode, click on Load unpacked, and select the folder with your manifest.json.

  • Chrome: Go to chrome://extensions, enable Developer mode, click on Load unpacked, and select the folder with your manifest.json.

  • Firefox: to load a packed extension (zip), use: about:addons (settings > install from file), otherwise, use about:debugging#/runtime/this-firefox. The latter are unloaded after closing the browser.

Track errors/logs πŸ›

On the page where you added your extension

  • you can see every error generated by your extension.
  • you can open the console associated with your service_worker

To debug a script loaded by your popup.html, right-click on your extension in the toolbar, and use Inspect/Inspect [...].

Reloading an extension πŸ’ͺ

You have to (manually) reload the extension if you edited

  • πŸ‘‰οΈ The Manifest
  • πŸ‘‰οΈ Any script (+ refresh the page)

But, you don't need to if you only edited an HTML file.

  • Go to your plugin page
  • Use a refresh/reload button to reload your extension
Automatically reload the extension
  • Use a (good) template
  • Try chrome-extensions-reloader (πŸ‘»)
  • Call chrome.runtime.reload() (not available in a content_script).

πŸ“¦ Templates πŸ“¦

Extension Templates can make working with extensions easier.

  • ✨ These can preconfigure React/Typescript/...
  • ✨ These can reload automatically the extension
  • ...

My "Hello, World"

console.log("Hello, World")
  • Create manifest.json
{
  "name": "Hello, World",
  "version": "0.0.1",
  "manifest_version": 3,
  "description": "Print \"Hello, World\" in the console",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["scripts/main.js"]
    }
  ]
}
  • Create scripts/main.js
console.log("Hello, World")
  • Load your extension
  • Visit a website, open the console, and you will see the log

chrome-extension-typescript-starter

GitHub (1.2k ✨, React, Typescript, Jest)

See my notes

➑️ Get started

$ git clone https://github.com/chibat/chrome-extension-typescript-starter.git my-extension
$ cd .\my-extension\
$ npm install
$ npm update
$ npm run watch

➑️ Bootstrap

To load bootstrap, download it locally, and store it inside a directory in public (ex: lib/). Then in popup.html, use

<link href="lib/css/bootstrap.min.css" rel="stylesheet">
<script src="lib/js/bootstrap.bundle.min.js"></script>

➑️ MANIFEST

For JS files in the Manifest, src/*.ts will generate a file js/*.js.

"background": {
  "service_worker": "js/background.js"
},
"content_scripts": [
  {
    "matches": ["http://localhost/:*/"],
    "js": ["js/content_script.js"]
  }
],

🎺 Manifest.json 🎺

See Manifest.json

This is a JSON file defining your extension. Version 2 is being deprecated, consider using version 3. πŸš€

{
  "manifest_version": 3
  ...
}
Metadata: description, version, name, and icons

The name, the description, and the version are up to you.

  "name": "Hello, World",
  "version": "0.0.1",
  "description": "Print \"Hello, World\" in the console",

You should provide an icon in multiple sizes: 16, 32, 48 and 128. The browser will do the resizing for missing resolutions.

  "icons": {
    "16": "icons/hello_world_16.png",
    "32": "icons/hello_world_32.png",
    "48": "icons/hello_world_48.png",
    "128": "icons/hello_world_128.png"
  }

Then, you can define your extension

  • ➑️ action: open a popup when clicking on the icon in the toolbar
  • ➑️ content_scripts: to run code on each page using the DOM
  • ➑️ background: to run code that does not need to access the DOM
  • ➑️ options_ui: a page to configure the extension

⚠️ Inside a content_scripts, most of the attributes of chrome/browser extension variable aren't available. Moreover, some properties are only available after asking for permission first using both:

  "host_permissions": [ "<all_urls>" ], // OR, add URLs here
  "permissions": ["activeTab"]

✨ Usually, the permission xxx grants access to browser.xxx.


Popup

πŸ‘‰ One of the changes from Manifest V2 to Manifest V3 is browserAction being renamed action both in the Manifest, and in the code.

It's possible to show a popup when the user clicks on the icon in the toolbar, which is called badge. You can do it inside the Manifest

  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": "icons/hello_world_16.png" // optional
  }

Or, at any time, inside the code

chrome.action.setPopup({ popup: "popup/popup.html" })
popup.html

This is an HTML page as you would create for websites.

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Hello, World</title>
</head>
<body>
<div>
    <p>Hello, World</p>
    <!-- load popup/popup.js -->
    <script src="popup.js"></script>
</div>
</body>
</html>

Note that the code inside the Popup is only executed when the popup is shown.


Execute some code

If you don't open a popup, it's also possible to register a listener, and execute some code when the user clicks on the badge.

// ➑️ When users click on the badge
// ➑️ When users use the shortcut (if any)
chrome.action.onClicked.addListener((tab) => {});

Badge

The icon inside the toolbar is called a badge. You can add a text next to it, like the number of ads blocked on the current tab.

// ➑️ Inside popup.html/popup.js/...
badge.textContent = `XXX`;
// ➑️ Otherwise,
chrome.action.setBadgeText({ text: "xxx", });
chrome.action.setBadgeText({ tabId: tab.id, text: "yyy", });
chrome.action.getBadgeText({ tabId: tab.id });

Shortcut

To add a shortcut to open the badge, add to your MANIFEST:

  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Ctrl+M",
        "mac": "Command+M"
      }
    }
  }

Content scripts

Content scripts are used if you want your extension to interact with the DOM of the loaded page, for instance, to inject some code.

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["scripts/main.js"]
    }
  ]

The attribute matches takes patterns such as https://example.com/*. ➑️ Refer to the match patterns documentation.

web_accessible_resources

If you need to access a resource stored inside the plugin folder, first, declare the resource inside the Manifest

  "web_accessible_resources": [
    {
      "matches": ["<all_urls>"],
      "resources": ["xxx"]
    }
  ]

Then, use chrome.runtime.getURL("xxx") to get a URL to it.


Additional Components

Background and Service Worker

Service workers can be stopped, and started when an event occurs. They are useful for long-running tasks, or to access the Chrome/Firefox API without having to use a popup.

  "background": {
    "service_worker": "background.js"
  },

Option Page

You can create an option page to allow user to customize the extension. We usually save these settings in the storage.sync storage.

  "options_ui": {
    "page": "options.html",
    "open_in_tab": true
  },

Chrome/Firefox API

⚠️ Reminder: most of these are only available inside a background script, or a script executed by your popup. ⚠️

🎯 Just so you know, you can use async/await instead of callbacks. You can use this trick if you're not able to use await:

(async () => {
    const [tab] = await chrome.tabs.query({/*...*/});
    // ...
})();

Tabs

chrome.tabs.query: fetch a tab, such as the active tab
// ➑️ ex: popup.js
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    const url = tabs[0].url;
    const title = tabs[0].title;
    const id = tabs[0].id
    // ...
})
chrome.tabs.query({ active: true, lastFocusedWindow: true }, function (tabs) {})
chrome.tabs.query({ url: [] }, function (tabs) {})
chrome.tabs.create/update: create/update a tab
// πŸ” "tabs"
await chrome.tabs.create({ url: "URL" })
await chrome.tabs.update(tab.id, { active: true });
await chrome.windows.update(tab.windowId, { focused: true });
chrome.tabGroups/update: group tabs
// πŸ” permission "tabGroups"
const group = await chrome.tabs.group({ tabIds });
await chrome.tabGroups.update(group, { title: "xxx" });

Scripting

insertCSS/removeCSS: manipulate the CSS
// πŸ” permission "scripting"
chrome.scripting.insertCSS({ files: ["focus-mode.css"], target: { tabId: tab.id },});
chrome.scripting.removeCSS({ files: ["focus-mode.css"], target: { tabId: tab.id },});

Download

Generate a link for a JSON in a content script
const content : any = JSON.stringify( { "message": "ok" }, null, 2);
const url = URL.createObjectURL( new Blob([content], {type: 'application/json'}) );
Generate a link for a JSON in a background script

I couldn't find anything. This works, but the indentations, the spaces, and most of the generated JSON is messed up.

const url = 'data:application/json,' + JSON.stringify(/*...*/)

The only workaround is to add a comment with //? at the start of the JSON.

const url = 'data:application/json,//?\n' + JSON.stringify(/*...*/)
// πŸ” permission "downloads"
chrome.downloads.download({ url, filename: "xxx.json" });
chrome.downloads.download({ url, filename: "...", saveAs: true });

Store/Load data

Storage - reference. See also Storage Area Explorer.

Use storage.local
// πŸ” permission "storage"
// Get
chrome.storage.local.get(["key"], function(result){
    const value = result["key"]
    // ...
})
// Set
chrome.storage.local.set( { key: value } ).then(() => {})
// Dump
chrome.storage.local.get(function(result) { console.log(result) })
// Clear
chrome.storage.local.clear()
Dynamic keys

If the key is from a variable, use the code below

// Dynamic keys
const key = "key"
const entry = {}
entry[key] = value
chrome.storage.local.set(entry).then(() => {})

Bookmarks

Bookmarks - reference.

Get/Remove/...
// get
chrome.bookmarks.getChildren("id", function(result) {})
chrome.bookmarks.get("id", function(result) {})
// remove
chrome.bookmarks.remove("id")
chrome.bookmarks.removeTree("id")
Events - created, updated, removed
function cb(id, info) {}
// { "dateAdded": 0, "id": "0", "index": 0, "parentId": "0", "title": "xxx", "url": "xxx }
chrome.bookmarks.onCreated.addListener(cb)
// { "title": "xxx", "url": "xxx" }
chrome.bookmarks.onChanged.addListener(cb)
chrome.bookmarks.onMoved.addListener(cb)
// { "index": 0, "node": { "dateAdded": 0, "id": "0", "title": "xxx", "url": "xxx" }, "parentId": "0" }
chrome.bookmarks.onRemoved.addListener(cb)

Utilities

sendMessage: send messages to content scripts

To send a message from a background script/popup

// ➑️ Sender - ex: popup.js
chrome.tabs.sendMessage(tabs[0].id,
    { /* custom data */ },
    function(response) { /* ... */}
);

Inside a content script, you can use

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    // ...
    sendResponse({ /* custom */ });
});
onInstalled: run some code after installing the extension
chrome.runtime.onInstalled.addListener(() => {});

Additional Notes For Firefox Extensions

You can get started by using the chrome-extension-typescript-starter template. Replace chrome with browser and update package.json.

-    "build": "webpack --config webpack/webpack.prod.js",
+    "build": "webpack --config webpack/webpack.prod.js && cd dist && npx web-ext build",
  "dependencies": {
     ...,
+    "web-ext": "^X.X.X"
  }
  "devDependencies": {
-    "@types/chrome": "X.X.X",
+    "@types/firefox-webext-browser": "^X.X.X",
     ...
  }

The ZIP will be built in dist/web-ext-artifacts/.

You will also have to add an ID in your extension manifest:

{
 "browser_specific_settings": {
    "gecko": {
      "id": "{aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}"
    }
  },
  ...
}

To test a non-signed extension, you must use Firefox Developper. Refer to this StackOverflow thread to set the following variables:

extensions.langpacks.signatures.required
xpinstall.signatures.required

πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.

chrome.contextMenus.create({ id: "xxx", title: "xxx", contexts: ['selection'] });
chrome.contextMenus.onClicked.addListener(function(info, tab) {
    if (info.menuItemId == "xxx") {     
        const word = info.selectionText;
        // ...    
    }
});
document.addEventListener("pageshow", xxx);