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? π«
- Chrome extensions + API reference + Examples
- Firefox extensions + Examples (π»)
- Microsoft Edge extensions (π»)
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/
, enableDeveloper mode
, click onLoad unpacked
, and select the folder with yourmanifest.json
. -
Chrome: Go to
chrome://extensions
, enableDeveloper mode
, click onLoad unpacked
, and select the folder with yourmanifest.json
. -
Firefox: to load a packed extension (zip), use:
about:addons
(settings > install from file), otherwise, useabout: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 πΊ
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
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);