Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example using MV3 userScripts API #576

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -567,14 +567,36 @@
"name": "user-agent-rewriter"
},
{
"description": "Illustrates how an extension can register URL-matching user scripts at runtime.",
"description": "Illustrates how an extension can register URL-matching user scripts at runtime (Manifest Version 2 only - deprecated).",
"javascript_apis": [
"userScripts.register",
"runtime.onMessage",
"runtime.sendMessage"
],
"name": "user-script-register"
},
{
"description": "A user script manager, demonstrating the userScripts API, permissions API, optional_permissions and Manifest Version 3 (MV3).",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"description": "A user script manager, demonstrating the userScripts API, permissions API, optional_permissions and Manifest Version 3 (MV3).",
"description": "A user script manager demonstrating the userScripts API, permissions API, optional_permissions, and Manifest Version 3 (MV3).",

"javascript_apis": [
"userScripts.configureWorld",
"userScripts.getScripts",
"userScripts.register",
"userScripts.resetWorldConfiguration",
"userScripts.unregister",
"userScripts.update",
"permissions.onAdded",
"permissions.onRemoved",
"permissions.request",
"runtime.onInstalled",
"runtime.onUserScriptMessage",
"runtime.openOptionsPage",
"runtime.sendMessage",
"storage.local",
"storage.onChanged",
"storage.session"
],
"name": "userScripts-mv3"
},
{
"description": "Demonstrates how to use webpack to package npm modules in an extension.",
"javascript_apis": ["runtime.onMessage", "runtime.sendMessage"],
Expand Down
116 changes: 116 additions & 0 deletions userScripts-mv3/README.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rob--W, if you want to publish this example before the 135, I think we should add some installation instructions to the README that mention that you need to enable extensions.userScripts.mv3.enabled in about:config.

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# userScripts-mv3

A user script manager, demonstrating the userScripts API, the permissions API,
`optional_permissions`, and Manifest Version 3 (MV3).
The extension is an example of a
[user script manager](https://en.wikipedia.org/wiki/Userscript_manager).
Comment on lines +3 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A user script manager, demonstrating the userScripts API, the permissions API,
`optional_permissions`, and Manifest Version 3 (MV3).
The extension is an example of a
[user script manager](https://en.wikipedia.org/wiki/Userscript_manager).
This is an example [user script manager](https://en.wikipedia.org/wiki/Userscript_manager) that demonstrates the userScripts API, the permissions API,`optional_permissions`, and Manifest Version 3 (MV3).


This covers the following aspects to extension development:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This covers the following aspects to extension development:
This example illustrates these aspects of extension development:

Comment on lines +3 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword to fix a grammar issue and for clarity. Currently the intro sentence is incomplete.

Suggested change
A user script manager, demonstrating the userScripts API, the permissions API,
`optional_permissions`, and Manifest Version 3 (MV3).
The extension is an example of a
[user script manager](https://en.wikipedia.org/wiki/Userscript_manager).
This covers the following aspects to extension development:
The extension is an example of a
[user script manager](https://en.wikipedia.org/wiki/Userscript_manager). It
demonstrates the userScripts API, the permissions API, `optional_permissions`,
and Manifest Version 3 (MV3).
This demo covers the following aspects to extension development:


- Showing onboarding UI after installation.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Showing onboarding UI after installation.
- Showing an onboarding UI after installation.


- Designing background scripts that can restart repeatedly with minimal
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rob--W as we seem to make the assumption that this is going to be rendered as a markdown document (e.g. use of hyperlink in the introductory paragraph) is there any need to impose line breaks?

overhead. This is especially relevant to Manifest Version 3.

- Minimizing the overhead of background script startup, which is especially
relevant because event pages .
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original statement seems incomplete, not entirely sure how to complete it, e.g. why is it irrelevant?

Suggested change
- Minimizing the overhead of background script startup, which is especially
relevant because event pages .
- Minimizing the overhead of background script startup, which is relevant to event pages.

Comment on lines +15 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword to make this comment more applicable to other browsers. It also addresses the fact that other browsers may also support service workers in the future.

Suggested change
- Minimizing the overhead of background script startup, which is especially
relevant because event pages .
- Minimizing the overhead of background script startup. This is relevant because
Manifest Version 3 extensions uses an event-based background context.

NOTE: If you accept this suggestion, you should also update this line with a similar change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correcting grammar in suggestion

Suggested change
- Minimizing the overhead of background script startup, which is especially
relevant because event pages .
- Minimizing the overhead of background script startup. This is relevant because
Manifest Version 3 extensions use an event-based background context.


- Monitoring an optional (userScripts) permission, and dynamically registering
events and scripts based on its availability.
Comment on lines +18 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Monitoring an optional (userScripts) permission, and dynamically registering
events and scripts based on its availability.
- Monitoring an optional (userScripts) permission, and dynamically registering events and scripts based on whether the permission has been granted.

Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword for clarity. The parenthetical should appear after the entire phrase that it applies to.

I tend to prefer to include the quotation marks when referring to a permission. That said, I don't know if we have an established pattern one way or the other.

Suggested change
- Monitoring an optional (userScripts) permission, and dynamically registering
events and scripts based on its availability.
- Monitoring grants for an optional permission (`"userScripts"`), and
dynamically registering events and scripts based on its availability.


- Using the `userScripts` API to register, update and unregister code.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Using the `userScripts` API to register, update and unregister code.
- Using the `userScripts` API to register, update, and unregister code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: clarify that the extension is specifically operating on user scripts rather than arbitrary code.

Suggested change
- Using the `userScripts` API to register, update and unregister code.
- Using the `userScripts` API to register, update and unregister user script
code.


- Isolating user scripts in their own execution context (`USER_SCRIPT` world),
and conditionally exposing custom functions to user scripts.
Comment on lines +23 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Isolating user scripts in their own execution context (`USER_SCRIPT` world),
and conditionally exposing custom functions to user scripts.
- Isolating user scripts in individual execution contexts (`USER_SCRIPT` world), and conditionally exposing custom functions to user scripts.



## What it does

This extension is an example of a [user script manager](https://en.wikipedia.org/wiki/Userscript_manager)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: end the sentence with a period.

Suggested change
This extension is an example of a [user script manager](https://en.wikipedia.org/wiki/Userscript_manager)
This extension is an example of a [user script manager](https://en.wikipedia.org/wiki/Userscript_manager).


Comment on lines +29 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already said this in the introduction, not sure it needs repeating

Suggested change
This extension is an example of a [user script manager](https://en.wikipedia.org/wiki/Userscript_manager)

After loading the extension, the extension detects the new installation and
opens the options page embedded in `about:addons`. On the options page:
Comment on lines +31 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
After loading the extension, the extension detects the new installation and
opens the options page embedded in `about:addons`. On the options page:
After loading the extension detects the new installation and opens the options page embedded in `about:addons`. On the options page:


1. You can click on the "Grant access to userScripts API" button to trigger a
permission prompt for the "userScripts" permission.
Comment on lines +34 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. You can click on the "Grant access to userScripts API" button to trigger a
permission prompt for the "userScripts" permission.
1. Click "Grant access to userScripts API" to trigger a permission prompt for the "userScripts" permission.

2. Click on the "Add new user script" button to open a form where a new script
can be registered.
Comment on lines +36 to +37
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. Click on the "Add new user script" button to open a form where a new script
can be registered.
2. Click "Add new user script" to open a form where a new script can be registered.

3. Input a user script. E.g. by clicking one of the two "Example" buttons to
input examples from the [userscript_examples](userscript_examples) directory.
Comment on lines +38 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. Input a user script. E.g. by clicking one of the two "Example" buttons to
input examples from the [userscript_examples](userscript_examples) directory.
3. Input a user script, by clicking one of the "Example" buttons and input a example from the [userscript_examples](userscript_examples) directory.

4. Click on the "Save" button to trigger validation and save the script.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
4. Click on the "Save" button to trigger validation and save the script.
4. Click "Save" to trigger validation and save the script.


If the "userScripts" permission was granted, this will schedule the execution
of the registered user scripts for the websites as specified by the user script.
Comment on lines +42 to +43
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If the "userScripts" permission was granted, this will schedule the execution
of the registered user scripts for the websites as specified by the user script.
If the "userScripts" permission is granted, this schedules the execution of the registered user scripts for the websites specified in each user script.


See [userscript_examples](userscript_examples) for examples of user scripts and
what they do.

If you repeat steps 2-4 for both examples, then a visit to https://example.com/
should show the following behavior:
Comment on lines +48 to +49
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you repeat steps 2-4 for both examples, then a visit to https://example.com/
should show the following behavior:
If you repeat steps 2-4 for both examples, then a visit to https://example.com/ should show this behavior:


- Show a dialog containing "This is a demo of a user script".
- Insert a button with the label "Show user script info", which opens a new tab
displaying the extension information.

# What it shows

Showing onboarding UI after installation:

- `background.js` registers the `runtime.onInstalled` listener that calls
`runtime.openOptionsPage` after installation.

Designing background scripts that can restart repeatedly with minimal overhead:

- This is especially relevant to Manifest Version 3, because background scripts
are always non-persistent event pages, which can suspend on inactivity.
Comment on lines +64 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- This is especially relevant to Manifest Version 3, because background scripts
are always non-persistent event pages, which can suspend on inactivity.
- This is particularly relevant to Manifest Version 3, because in MV3 background scripts
are always non-persistent event pages that can suspend on inactivity.

- Using `storage.session` to store initialization status, to run expensive
initialization only once per browser session.
- Registering events at the top level to handle events that were triggered
while the background script was asleep.
Comment on lines +68 to +69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Registering events at the top level to handle events that were triggered
while the background script was asleep.
- Registering events at the top level to handle events that are triggered while the background script is asleep.

- Using [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
to initialize optional JavaScript modules on demand.

Monitoring an optional (userScripts) permission, and dynamically registering
events and scripts based on its availability:

- The `userScripts` permission is optional and can be granted by the user via
the options page (`options.html` + `options.mjs`). The permission can also
be granted/revoked via browser UI, by the user, as documented at
https://support.mozilla.org/en-US/kb/manage-optional-permissions-extensions
Comment on lines +76 to +79
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The `userScripts` permission is optional and can be granted by the user via
the options page (`options.html` + `options.mjs`). The permission can also
be granted/revoked via browser UI, by the user, as documented at
https://support.mozilla.org/en-US/kb/manage-optional-permissions-extensions
- The `userScripts` permission is optional and can be granted by the user from:
- the options page (`options.html` + `options.mjs`).
- the browser UI (where the user can also revoke the permission). See the Mozilla support article [Manage optional permissions for Firefox extensions](https://support.mozilla.org/en-US/kb/manage-optional-permissions-extensions).


- The `permissions.onAdded` and `permissions.onRemoved` events are used to
monitor permission changes and the (un)availability of the `userScripts` API.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
monitor permission changes and the (un)availability of the `userScripts` API.
monitor permission changes and, therefore, the availability of the `userScripts` API.


- When the `userScripts` API is available at the startup of `background.js`,
and when the permission detected via `permissions.onAdded`, the initialization
starts (via the `ensureUserScriptsRegistered` function in `background.js`).
Comment on lines +84 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- When the `userScripts` API is available at the startup of `background.js`,
and when the permission detected via `permissions.onAdded`, the initialization
starts (via the `ensureUserScriptsRegistered` function in `background.js`).
- When the `userScripts` API is available when `background.js` starts or `permissions.onAdded` detects that permission has been granted, initialization starts (using the `ensureUserScriptsRegistered` function in `background.js`).


- When the `userScripts` API is unavailable at the startup of `background.js`,
the extension cannot use the `userScripts` API until `permissions.onAdded` is
triggered. The options page stores user scripts in `storage.local` to enable
the user to edit scripts even without the `userScripts` permission.
Comment on lines +88 to +91
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- When the `userScripts` API is unavailable at the startup of `background.js`,
the extension cannot use the `userScripts` API until `permissions.onAdded` is
triggered. The options page stores user scripts in `storage.local` to enable
the user to edit scripts even without the `userScripts` permission.
- When the `userScripts` API is unavailable when `background.js` starts,
the extension cannot use the `userScripts` API until `permissions.onAdded` is
triggered. The options page stores user scripts in `storage.local` to enable
the user to edit scripts even without the `userScripts` permission.


Using the `userScripts` API to register, update and unregister code:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Using the `userScripts` API to register, update and unregister code:
Using the `userScripts` API to register, update, and unregister code:


- The `applyUserScripts()` function in `background.js` demonstrates how one use
the various `userScripts` APIs to register, update and unregister scripts.
Comment on lines +95 to +96
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The `applyUserScripts()` function in `background.js` demonstrates how one use
the various `userScripts` APIs to register, update and unregister scripts.
- The `applyUserScripts()` function in `background.js` demonstrates how to use
the various `userScripts` APIs to register, update, and unregister scripts.

- `userscript_manager_logic.mjs` contains logic specific to user script
managers. See [userscript_manager_logic.js](userscript_manager_logic.js) for
comments and the conversion logic from a user script string to the format as
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
comments and the conversion logic from a user script string to the format as
comments and the conversion logic from a user script string to the format

expected by the userScripts API (RegisteredUserScript).

Isolating user scripts in their own execution context (`USER_SCRIPT` world),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Isolating user scripts in their own execution context (`USER_SCRIPT` world),
Isolating user scripts in individual execution contexts (`USER_SCRIPT` world),

and conditionally exposing custom functions to user scripts:

- Shows the use of multiple `USER_SCRIPT` worlds (with distinct `worldId`) to
define separate sandboxes for scripts to run in (see `registeredUserScript`
in `userscript_manager_logic.mjs`).
Comment on lines +105 to +107
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Shows the use of multiple `USER_SCRIPT` worlds (with distinct `worldId`) to
define separate sandboxes for scripts to run in (see `registeredUserScript`
in `userscript_manager_logic.mjs`).
- Shows the use of `USER_SCRIPT` worlds (with distinct `worldId`) to
define sandboxes for scripts to run in (see `registeredUserScript`
in `userscript_manager_logic.mjs`).


- Shows the use of `userScripts.configureWorld()` with the `messaging` flag to
enable the `runtime.sendMessage()` method in `USER_SCRIPT` worlds.

- Shows the use of `runtime.onUserScriptMessage` and `sender.userScriptWorldId`
to detect messages and the script that sent messages.

- Shows how an initial script can use `runtime.sendMessage` to expose custom
APIs to user scripts (see `userscript_api.js`).
176 changes: 176 additions & 0 deletions userScripts-mv3/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"use strict";

// This background.js file is responsible for observing the availability of the
// userScripts API, and registering user scripts when needed.
//
// - The runtime.onInstalled event is used to detect new installations, to open
// extension UI where the user is asked to grant the "userScripts" permission.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// extension UI where the user is asked to grant the "userScripts" permission.
// an extension UI where the user is asked to grant the "userScripts" permission.

//
// - The permissions.onAdded and permissions.onRemoved events detect changes to
// the "userScripts" permission, whether triggered from the extension UI, or
// externally (e.g. through browser UI).
Comment on lines +10 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// the "userScripts" permission, whether triggered from the extension UI, or
// externally (e.g. through browser UI).
// the "userScripts" permission, whether triggered from the extension UI or
// externally (e.g., through browser UI).

//
// - The storage.local API is used to store user scripts across extension
// updates. This is necessary, because the userScripts API clears any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// updates. This is necessary, because the userScripts API clears any
// updates. This is necessary because the userScripts API clears any

// previously registered scripts when an extension is updated.
//
// - The userScripts API manages script registrations with the browser. The
// applyUserScripts() function in this file demonstrates the relevant aspects
// to registering/updating user scripts that apply to most extensions that
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// to registering/updating user scripts that apply to most extensions that
// to registering and updating user scripts that apply to most extensions that

// manage user scripts. To keep this file reasonably small, most of the
// application-specific logic is in userscript_manager_logic.js

function isUserScriptsAPIAvailable() {
return !!browser.userScripts;
}
var userScriptsAvailableAtStartup = isUserScriptsAPIAvailable();

var managerLogic; // Lazily initialized by ensureManagerLogicLoaded().
async function ensureManagerLogicLoaded() {
if (!managerLogic) {
managerLogic = await import("./userscript_manager_logic.mjs");
}
}

browser.runtime.onInstalled.addListener(details => {
if (details.reason !== "install") {
// Only show extension's onboarding logic on extension installation, and
// not e.g. on browser update or extension updates.
Comment on lines +37 to +38
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Only show extension's onboarding logic on extension installation, and
// not e.g. on browser update or extension updates.
// Only show the extension's onboarding logic on extension installation, and
// not, e.g., on browser or extension updates.

return;
}
if (!isUserScriptsAPIAvailable()) {
// The extension needs the "userScripts" permission, but this has not been
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// The extension needs the "userScripts" permission, but this has not been
// The extension needs the "userScripts" permission, but this is not

// granted. Open the extension's options_ui page where we have implemented
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// granted. Open the extension's options_ui page where we have implemented
// granted. Open the extension's options_ui page, which implements

// onboarding logic, in options.html + options.mjs
browser.runtime.openOptionsPage();
}
});

browser.permissions.onRemoved.addListener(permissions => {
if (permissions.permissions.includes("userScripts")) {
// Pretend that userScripts was not available, so that if the permission is
// restored, that permissions.onAdded will re-initialize.
Comment on lines +51 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Pretend that userScripts was not available, so that if the permission is
// restored, that permissions.onAdded will re-initialize.
// Pretend that userScripts is not available, so that if the permission is
// restored permissions.onAdded initializes.

userScriptsAvailableAtStartup = false;

// Clear cached state, so that ensureUserScriptsRegistered() will refresh
// the registered user scripts if the permissions is granted again.
Comment on lines +55 to +56
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Clear cached state, so that ensureUserScriptsRegistered() will refresh
// the registered user scripts if the permissions is granted again.
// Clear the cached state, so that ensureUserScriptsRegistered() refreshes
// the registered user scripts when the permissions is granted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// the registered user scripts if the permissions is granted again.
// the registered user scripts if the permission is granted again.

browser.storage.session.remove("didInitScripts");

// Note: the "userScripts" namespace is unavailable, so we cannot and
// should not try to unregister scripts.
}
});

browser.permissions.onAdded.addListener(permissions => {
if (permissions.permissions.includes("userScripts")) {
if (userScriptsAvailableAtStartup) {
// If background.js woke up to dispatch permissions.onAdded, then we
// would already have detected the availability of the userScripts API
// and started initialization. Return now to avoid double-initialization.
Comment on lines +67 to +69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If background.js woke up to dispatch permissions.onAdded, then we
// would already have detected the availability of the userScripts API
// and started initialization. Return now to avoid double-initialization.
// If background.js woke up to dispatch permissions.onAdded, it
// has detected the availability of the userScripts API
// and started initialization. Return now to avoid double-initialization.

return;
}
browser.runtime.onUserScriptMessage.addListener(onUserScriptMessage);
ensureUserScriptsRegistered();
}
});

// When the user modifies a user script in options.html / options.mjs, the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// When the user modifies a user script in options.html / options.mjs, the
// When the user modifies a user script in options.html + options.mjs, the

// changes are stored in storage.local and this listener is triggered.
browser.storage.local.onChanged.addListener(changes => {
if (changes.savedScripts?.newValue && isUserScriptsAPIAvailable()) {
// userScripts API is available and there are changes that we can apply!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// userScripts API is available and there are changes that we can apply!
// userScripts API is available and there are changes that can be applied.

applyUserScripts(changes.savedScripts.newValue);
}
});

if (userScriptsAvailableAtStartup) {
// Register listener immediately if the API is available, in case the
// background.js was awakened to dispatch the onUserScriptMessage event.
Comment on lines +87 to +88
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Register listener immediately if the API is available, in case the
// background.js was awakened to dispatch the onUserScriptMessage event.
// Register listener immediately if the API is available, in case the
// background.js is woken to dispatch the onUserScriptMessage event.

browser.runtime.onUserScriptMessage.addListener(onUserScriptMessage);
ensureUserScriptsRegistered();
}

async function onUserScriptMessage(message, sender) {
await ensureManagerLogicLoaded();
return managerLogic.handleUserScriptMessage(message, sender);
}

async function ensureUserScriptsRegistered() {
let { didInitScripts } = await browser.storage.session.get("didInitScripts");
if (didInitScripts) {
// The scripts has already been initialized, e.g. at a (previous) startup
// of this background script. Skip expensive initialization.
Comment on lines +101 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// The scripts has already been initialized, e.g. at a (previous) startup
// of this background script. Skip expensive initialization.
// The scripts is initialized, e.g., by a (previous) startup
// of this background script. Skip expensive initialization.

return;
}
let { savedScripts } = await browser.storage.local.get("savedScripts");
savedScripts ||= [];
try {
await applyUserScripts(savedScripts);
} finally {
// Set a flag to mark completion of initialization, to avoid running all of
// this logic again at the next startup of this background.js script.
Comment on lines +110 to +111
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Set a flag to mark completion of initialization, to avoid running all of
// this logic again at the next startup of this background.js script.
// Set a flag to mark the completion of initialization, to avoid running
// this logic again at the next startup of this background.js script.

await browser.storage.session.set({ didInitScripts: true });
}
}

async function applyUserScripts(userScriptTexts) {
await ensureManagerLogicLoaded();
// Note: assuming userScriptTexts to be valid, validated by options.mjs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Note: assuming userScriptTexts to be valid, validated by options.mjs.
// Note: assumes userScriptTexts to be valid, validated by options.mjs.

let scripts = userScriptTexts.map(str => managerLogic.parseUserScript(str));

// Registering scripts is expensive. Compare the scripts with the old scripts
// to make sure that we only update scripts that have changed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// to make sure that we only update scripts that have changed.
// to ensure that only update scripts are changed.

let oldScripts = await browser.userScripts.getScripts();

let {
scriptsToRemove,
scriptsToUpdate,
scriptsToRegister,
} = managerLogic.computeScriptDifferences(oldScripts, scripts);

// Now we have computed the changed scripts, apply the changes in this order:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Now we have computed the changed scripts, apply the changes in this order:
// Now, for the changed scripts, apply the changes in this order:

// 1. Unregister obsolete scripts.
// 2. Reset / configure worlds.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 2. Reset / configure worlds.
// 2. Reset or configure worlds.

// 3. Update / register new scripts.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 3. Update / register new scripts.
// 3. Update or register new scripts.

// This order is significant: scripts rely on world configurations, and while
// running this asynchronous script updating logic, the browser may try to
// execute any of the registered scripts when a website loaded in a tab or
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// execute any of the registered scripts when a website loaded in a tab or
// execute any of the registered scripts when a website loads in a tab or

// iframe, unrelated to the extension execution.
// To prevent scripts from executing with the wrong world configuration,
// worlds are configured before new scripts are registered.

// 1. Unregister obsolete scripts.
if (scriptsToRemove.length) {
let worldIds = scriptsToRemove.map(s => s.id);
await browser.userScripts.unregister({ worldIds });
Comment on lines +144 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 145 currently throws the following error:

Uncaught (in promise) Error: Type error for parameter filter (Unexpected property "worldIds") for userScripts.unregister.

Channel: Nightly
Version: 135.0a1 (2024-12-30) (aarch64)
OS: macOS 15.1 (24B83)

I looked through the current version of toolkit/components/extensions/schemas/user_scripts.json and the current version of UserScriptFilter only has a ids property.

It looks like this may be the result of some refactoring that occured during development. The script appears to work as expected if we change the body of the if clause as follows:

Suggested change
let worldIds = scriptsToRemove.map(s => s.id);
await browser.userScripts.unregister({ worldIds });
await browser.userScripts.unregister({ ids: scriptsToRemove });

}

// 2. Reset / configure worlds.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 2. Reset / configure worlds.
// 2. Reset or configure worlds.

if (scripts.some(s => s.worldId)) {
// When a userscripts need privileged functionality, we run them in a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// When a userscripts need privileged functionality, we run them in a
// When a userscripts need privileged functionality, run them in a

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t that be "When a userscript needs" or "When userscripts need"?

// sandbox (USER_SCRIPT world). To offer privileged functionality, we need
// a communication channel between the userscript and this privileged side.
// Specifying "messaging:true" exposes runtime.sendMessage() these worlds,
// which upon invocation triggers the runtime.onUserScriptMessage event.
//
// Calling configureWorld without a specific worldId sets the default world
// configuration, which is inherit by every other USER_SCRIPT world that
// does not have a more specific configuration.
//
// Since every USER_SCRIPT world in this demo extension has the same world
// configuration, we can set the default once, without needing to define
// world-specific configurations.
await browser.userScripts.configureWorld({ messaging: true });
} else {
// Reset the default world's configuration.
await browser.userScripts.resetWorldConfiguration();
}

// 3. Update / register new scripts.
if (scriptsToUpdate.length) {
await browser.userScripts.update(scriptsToUpdate);
}
if (scriptsToRegister.length) {
await browser.userScripts.register(scriptsToRegister);
}
}
21 changes: 21 additions & 0 deletions userScripts-mv3/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"manifest_version": 3,
"name": "User Scripts Manager extension",
"description": "Demonstrates the userScripts API and optional permission, in MV3.",
"version": "0.1",
"host_permissions": ["*://*/"],
"permissions": ["storage", "unlimitedStorage"],
"optional_permissions": ["userScripts"],
dotproto marked this conversation as resolved.
Show resolved Hide resolved
"background": {
"scripts": ["background.js"]
},
"options_ui": {
"page": "options.html"
},
"browser_specific_settings": {
"gecko": {
"id": "user-script-manager-example@mozilla.org",
"strict_min_version": "134.0a1"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I put strict_min_version 134 because this is the first version of Firefox where the API landed, behind the extensions.userScripts.mv3.enabled preference. I may update it to 135 or 136 once we ship it by default on release.

}
}
}
5 changes: 5 additions & 0 deletions userScripts-mv3/options.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#edit_script_dialog .source_text {
display: block;
width: 80vw;
min-height: 10em;
}
32 changes: 32 additions & 0 deletions userScripts-mv3/options.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width"> <!-- mobile-friendly -->
<meta name="color-scheme" content="dark light"><!-- Dark theme support -->
<link rel="stylesheet" type="text/css" href="options.css">
</head>
<body>
This page enables you to create, edit and remove user scripts.
To run them, please allow the extension to run user scripts by clicking this button:
<button id="grant_userScripts_permission"></button>

<dialog id="edit_script_dialog">
<div>
Please input a user script and save it.<br>
<button id="sample_unprivileged">Example: Unprivileged user script</button>
<button id="sample_privileged">Example: Privileged user script</button>
</div>
<textarea class="source_text"></textarea>
<button class="save_button">Save</button>
<button class="remove_button">Remove</button>
<output class="validation_status"></output>
</dialog>

<ul id="list_of_scripts">
<li><button id="add_new">Add new user script</button></li>
</ul>

<script src="options.mjs" type="module"></script>
</body>
</html>
Loading
Loading