Skip to content

Commit 567f415

Browse files
committed
Add example using MV3 userScripts API
1 parent 2ecc219 commit 567f415

13 files changed

Lines changed: 918 additions & 2 deletions

File tree

examples.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,14 +567,36 @@
567567
"name": "user-agent-rewriter"
568568
},
569569
{
570-
"description": "Illustrates how an extension can register URL-matching user scripts at runtime.",
570+
"description": "Illustrates how an extension can register URL-matching user scripts at runtime (Manifest Version 2 only).",
571571
"javascript_apis": [
572572
"userScripts.register",
573573
"runtime.onMessage",
574574
"runtime.sendMessage"
575575
],
576576
"name": "user-script-register"
577577
},
578+
{
579+
"description": "A user script manager demonstrating the userScripts API, permissions API, optional_permissions, and Manifest Version 3 (MV3).",
580+
"javascript_apis": [
581+
"userScripts.configureWorld",
582+
"userScripts.getScripts",
583+
"userScripts.register",
584+
"userScripts.resetWorldConfiguration",
585+
"userScripts.unregister",
586+
"userScripts.update",
587+
"permissions.onAdded",
588+
"permissions.onRemoved",
589+
"permissions.request",
590+
"runtime.onInstalled",
591+
"runtime.onUserScriptMessage",
592+
"runtime.openOptionsPage",
593+
"runtime.sendMessage",
594+
"storage.local",
595+
"storage.onChanged",
596+
"storage.session"
597+
],
598+
"name": "userScripts-mv3"
599+
},
578600
{
579601
"description": "Demonstrates how to use webpack to package npm modules in an extension.",
580602
"javascript_apis": ["runtime.onMessage", "runtime.sendMessage"],

user-script-register/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# User script registration
22

3-
This extension demonstrates the [`browser.userScripts.register()`](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/Register) API.
3+
This extension demonstrates the [legacy `browser.userScripts.register()`](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts_legacy/register) API, available to Manifest Version 2 extensions only.
4+
5+
> NOTE: See [userScripts-mv3](../userScripts-mv3/) for an example of the cross-browser userScripts API for Manifest Version 3.
46
57
The extension includes an [API script](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/user_scripts) (`customUserScriptAPIs.js`) that enables user scripts to make use of `browser.storage.local`.
68

userScripts-mv3/.eslintrc.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"overrides": [
3+
{
4+
"files": ["*.mjs"],
5+
"parserOptions": {
6+
"sourceType": "module"
7+
}
8+
}
9+
]
10+
}

userScripts-mv3/README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# userScripts-mv3
2+
3+
The extension is an example of a
4+
[user script manager](https://en.wikipedia.org/wiki/Userscript_manager). It
5+
The extension is an example of a [user script
6+
manager](https://en.wikipedia.org/wiki/Userscript_manager). It demonstrates the
7+
`userScripts` API, the `permissions` API, `optional_permissions`, and Manifest
8+
Version 3 (MV3).
9+
and Manifest Version 3 (MV3).
10+
11+
This example demonstrates these aspects of extension development:
12+
13+
- Showing an onboarding UI after installation.
14+
15+
- Designing background scripts that can restart repeatedly with minimal
16+
overhead. This is especially relevant to Manifest Version 3.
17+
18+
- Minimizing the overhead of background script startup. This is relevant because
19+
Manifest Version 3 extensions use an event-based background context.
20+
21+
- Monitoring grants for an
22+
[optional-only](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions#optional-only_permissions)
23+
permission (`"userScripts"`), and dynamically registering events and scripts
24+
based on its availability.
25+
26+
- Using the `userScripts` API to register, update, and unregister user script
27+
code.
28+
29+
- Isolating user scripts in individual execution contexts (`USER_SCRIPT`
30+
world), and conditionally exposing custom functions to user scripts.
31+
32+
33+
## What it does
34+
35+
After loading, the extension detects the new installation and opens the options
36+
page embedded in `about:addons`. On the options page:
37+
38+
1. Click "Grant access to userScripts API" to trigger a permission prompt for
39+
the "userScripts" permission.
40+
2. Click "Add new user script" to open a form where a new script can be
41+
registered.
42+
3. Input a user script, by clicking one of the "Example" buttons and input a
43+
example from the [userscript_examples](userscript_examples) directory.
44+
4. Click "Save" to trigger validation and save the script.
45+
46+
If the "userScripts" permission is granted, this schedules the execution of the
47+
registered user scripts for the websites specified in each user script.
48+
49+
See [userscript_examples](userscript_examples) for examples of user scripts and
50+
what they do.
51+
52+
If you repeat steps 2-4 for both examples and then visit https://example.com/,
53+
you should see this behavior:
54+
55+
- Show a dialog containing "This is a demo of a user script".
56+
- Insert a button with the label "Show user script info", which opens a new tab
57+
displaying the extension information.
58+
59+
# What it shows
60+
61+
Showing onboarding UI after installation:
62+
63+
- `background.js` registers the `runtime.onInstalled` listener that calls
64+
`runtime.openOptionsPage` after installation.
65+
66+
Designing background scripts that can restart repeatedly with minimal overhead:
67+
68+
- This is particularly relevant to Manifest Version 3, because in MV3
69+
background scripts are always non-persistent and can suspend on inactivity.
70+
- Using `storage.session` to store initialization status, to run expensive
71+
initialization only once per browser session.
72+
- Registering events at the top level to handle events that are triggered while
73+
the background script is asleep.
74+
- Using [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
75+
to initialize optional JavaScript modules on demand.
76+
77+
Monitoring an optional permission (`userScripts`), and dynamically registering
78+
events and scripts based on its availability:
79+
events and scripts based on its availability:
80+
81+
- The `userScripts` permission is optional and can be granted by the user from:
82+
- the options page (`options.html` + `options.mjs`).
83+
- the browser UI (where the user can also revoke the permission). See the
84+
Mozilla support article [Manage optional permissions for Firefox extensions](https://support.mozilla.org/en-US/kb/manage-optional-permissions-extensions).
85+
86+
- The `permissions.onAdded` and `permissions.onRemoved` events are used to
87+
monitor permission changes and, therefore, the availability of the
88+
`userScripts` API.
89+
90+
- When the `userScripts` API is available when `background.js` starts or
91+
`permissions.onAdded` detects that permission has been granted,
92+
initialization starts (using the `ensureUserScriptsRegistered` function in
93+
`background.js`).
94+
95+
- When the `userScripts` API is unavailable when `background.js` starts,
96+
the extension cannot use the `userScripts` API until `permissions.onAdded` is
97+
triggered. The options page stores user scripts in `storage.local` to enable
98+
the user to edit scripts even without the `userScripts` permission.
99+
100+
Using the `userScripts` API to register, update, and unregister code:
101+
102+
- The `applyUserScripts()` function in `background.js` demonstrates how to use
103+
the various `userScripts` APIs to register, update, and unregister scripts.
104+
- `userscript_manager_logic.mjs` contains logic specific to user script
105+
managers. See [`userscript_manager_logic.mjs`](userscript_manager_logic.mjs)
106+
for comments and the conversion logic from a user script string to the format
107+
expected by the `userScripts` API
108+
([RegisteredUserScript](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/RegisteredUserScript)).
109+
110+
Isolating user scripts in individual execution contexts (`USER_SCRIPT` world),
111+
and conditionally exposing custom functions to user scripts:
112+
113+
- Shows the use of `USER_SCRIPT` worlds (with distinct `worldId`) to
114+
define sandboxes for scripts to run in (see `registeredUserScript`
115+
in `userscript_manager_logic.mjs`).
116+
117+
- Shows the use of `userScripts.configureWorld()` with the `messaging` flag to
118+
enable the `runtime.sendMessage()` method in `USER_SCRIPT` worlds.
119+
120+
- Shows the use of `runtime.onUserScriptMessage` and `sender.userScriptWorldId`
121+
to detect messages and the script that sent messages.
122+
123+
- Shows how an initial script can use `runtime.sendMessage` to expose custom
124+
APIs to user scripts (see `userscript_api.js`).
125+
126+
# Feature availability
127+
128+
The `userScripts` API is available from Firefox 136. In Firefox 134 and 135, the
129+
functionality is only available if the `extensions.userScripts.mv3.enabled`
130+
preference is set to `true` at `about:config` before installing the extension.

userScripts-mv3/background.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"use strict";
2+
3+
// This background.js file is responsible for observing the availability of the
4+
// userScripts API, and registering user scripts when needed.
5+
//
6+
// - The runtime.onInstalled event is used to detect new installations, and
7+
// opens a custom extension UI where the user is asked to grant the
8+
// "userScripts" permission.
9+
//
10+
// - The permissions.onAdded and permissions.onRemoved events detect changes to
11+
// the "userScripts" permission, whether triggered from the extension UI, or
12+
// externally (e.g., through browser UI).
13+
//
14+
// - The storage.local API is used to store user scripts across extension
15+
// updates. This is necessary because the userScripts API clears any
16+
// previously registered scripts when an extension is updated.
17+
//
18+
// - The userScripts API manages script registrations with the browser. The
19+
// applyUserScripts() function in this file demonstrates the relevant aspects
20+
// to registering and updating user scripts that apply to most extensions
21+
// that manage user scripts. To keep this file reasonably small, most of the
22+
// application-specific logic is in userscript_manager_logic.mjs.
23+
24+
function isUserScriptsAPIAvailable() {
25+
return !!browser.userScripts;
26+
}
27+
var userScriptsAvailableAtStartup = isUserScriptsAPIAvailable();
28+
29+
var managerLogic; // Lazily initialized by ensureManagerLogicLoaded().
30+
async function ensureManagerLogicLoaded() {
31+
if (!managerLogic) {
32+
managerLogic = await import("./userscript_manager_logic.mjs");
33+
}
34+
}
35+
36+
browser.runtime.onInstalled.addListener(details => {
37+
if (details.reason !== "install") {
38+
// Only show the extension's onboarding logic on extension installation,
39+
// and not, e.g., on browser or extension updates.
40+
return;
41+
}
42+
if (!isUserScriptsAPIAvailable()) {
43+
// The extension needs the "userScripts" permission, but this is not
44+
// granted. Open the extension's options_ui page, which implements
45+
// onboarding logic, in options.html + options.mjs.
46+
browser.runtime.openOptionsPage();
47+
}
48+
});
49+
50+
browser.permissions.onRemoved.addListener(permissions => {
51+
if (permissions.permissions.includes("userScripts")) {
52+
// Pretend that userScripts is not available, to enable permissions.onAdded
53+
// to re-initialize when the permission is restored.
54+
userScriptsAvailableAtStartup = false;
55+
56+
// Clear the cached state, so that ensureUserScriptsRegistered() refreshes
57+
// the registered user scripts when the permission is granted again.
58+
browser.storage.session.remove("didInitScripts");
59+
60+
// Note: the "userScripts" namespace is unavailable, so we cannot and
61+
// should not try to unregister scripts.
62+
}
63+
});
64+
65+
browser.permissions.onAdded.addListener(permissions => {
66+
if (permissions.permissions.includes("userScripts")) {
67+
if (userScriptsAvailableAtStartup) {
68+
// If background.js woke up to dispatch permissions.onAdded, it has
69+
// detected the availability of the userScripts API and immediately
70+
// started initialization. Return now to avoid double-initialization.
71+
return;
72+
}
73+
browser.runtime.onUserScriptMessage.addListener(onUserScriptMessage);
74+
ensureUserScriptsRegistered();
75+
}
76+
});
77+
78+
// When the user modifies a user script in options.html + options.mjs, the
79+
// changes are stored in storage.local and this listener is triggered.
80+
browser.storage.local.onChanged.addListener(changes => {
81+
if (changes.savedScripts?.newValue && isUserScriptsAPIAvailable()) {
82+
// userScripts API is available and there are changes that can be applied.
83+
applyUserScripts(changes.savedScripts.newValue);
84+
}
85+
});
86+
87+
if (userScriptsAvailableAtStartup) {
88+
// Register listener immediately if the API is available, in case the
89+
// background.js is woken to dispatch the onUserScriptMessage event.
90+
browser.runtime.onUserScriptMessage.addListener(onUserScriptMessage);
91+
ensureUserScriptsRegistered();
92+
}
93+
94+
async function onUserScriptMessage(message, sender) {
95+
await ensureManagerLogicLoaded();
96+
return managerLogic.handleUserScriptMessage(message, sender);
97+
}
98+
99+
async function ensureUserScriptsRegistered() {
100+
let { didInitScripts } = await browser.storage.session.get("didInitScripts");
101+
if (didInitScripts) {
102+
// The scripts are initialized, e.g., by a (previous) startup of this
103+
// background script. Skip expensive initialization.
104+
return;
105+
}
106+
let { savedScripts } = await browser.storage.local.get("savedScripts");
107+
savedScripts ||= [];
108+
try {
109+
await applyUserScripts(savedScripts);
110+
} finally {
111+
// Set a flag to mark the completion of initialization, to avoid running
112+
// this logic again at the next startup of this background.js script.
113+
await browser.storage.session.set({ didInitScripts: true });
114+
}
115+
}
116+
117+
async function applyUserScripts(userScriptTexts) {
118+
await ensureManagerLogicLoaded();
119+
// Note: assumes userScriptTexts to be valid, validated by options.mjs.
120+
let scripts = userScriptTexts.map(str => managerLogic.parseUserScript(str));
121+
122+
// Registering scripts is expensive. Compare the scripts with the old scripts
123+
// to ensure that only modified scripts are updated.
124+
let oldScripts = await browser.userScripts.getScripts();
125+
126+
let {
127+
scriptIdsToRemove,
128+
scriptsToUpdate,
129+
scriptsToRegister,
130+
} = managerLogic.computeScriptDifferences(oldScripts, scripts);
131+
132+
// Now, for the changed scripts, apply the changes in this order:
133+
// 1. Unregister obsolete scripts.
134+
// 2. Reset or configure worlds.
135+
// 3. Update and/or register new scripts.
136+
// This order is significant: scripts rely on world configurations, and while
137+
// running this asynchronous script updating logic, the browser may try to
138+
// execute any of the registered scripts when a website loads in a tab or
139+
// iframe, unrelated to the extension execution.
140+
// To prevent scripts from executing with the wrong world configuration,
141+
// worlds are configured before new scripts are registered.
142+
143+
// 1. Unregister obsolete scripts.
144+
if (scriptIdsToRemove.length) {
145+
await browser.userScripts.unregister({ worldIds: scriptIdsToRemove });
146+
}
147+
148+
// 2. Reset or configure worlds.
149+
if (scripts.some(s => s.worldId)) {
150+
// When userscripts need privileged functionality, run them in a sandbox
151+
// (USER_SCRIPT world). To offer privileged functionality, we need
152+
// a communication channel between the userscript and this privileged side.
153+
// Specifying "messaging:true" exposes runtime.sendMessage() these worlds,
154+
// which upon invocation triggers the runtime.onUserScriptMessage event.
155+
//
156+
// Calling configureWorld without a specific worldId sets the default world
157+
// configuration, which is inherit by every other USER_SCRIPT world that
158+
// does not have a more specific configuration.
159+
//
160+
// Since every USER_SCRIPT world in this demo extension has the same world
161+
// configuration, we can set the default once, without needing to define
162+
// world-specific configurations.
163+
await browser.userScripts.configureWorld({ messaging: true });
164+
} else {
165+
// Reset the default world's configuration.
166+
await browser.userScripts.resetWorldConfiguration();
167+
}
168+
169+
// 3. Update and/or register new scripts.
170+
if (scriptsToUpdate.length) {
171+
await browser.userScripts.update(scriptsToUpdate);
172+
}
173+
if (scriptsToRegister.length) {
174+
await browser.userScripts.register(scriptsToRegister);
175+
}
176+
}

userScripts-mv3/manifest.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"manifest_version": 3,
3+
"name": "User Scripts Manager extension",
4+
"description": "Demonstrates the userScripts API and optional permission, in MV3.",
5+
"version": "0.1",
6+
"host_permissions": ["*://*/"],
7+
"permissions": ["storage", "unlimitedStorage"],
8+
"optional_permissions": ["userScripts"],
9+
"background": {
10+
"scripts": ["background.js"]
11+
},
12+
"options_ui": {
13+
"page": "options.html"
14+
},
15+
"browser_specific_settings": {
16+
"gecko": {
17+
"id": "user-script-manager-example@mozilla.org",
18+
"strict_min_version": "134.0"
19+
}
20+
}
21+
}

userScripts-mv3/options.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#edit_script_dialog .source_text {
2+
display: block;
3+
width: 80vw;
4+
min-height: 10em;
5+
}

0 commit comments

Comments
 (0)