Skip to content

feat(extensions): add chrome.userScripts and chrome.offscreen - #187

Open
seidnerj wants to merge 6 commits into
samuelmaddock:masterfrom
seidnerj:feat/offscreen-userscripts
Open

seidnerj wants to merge 6 commits into
samuelmaddock:masterfrom
seidnerj:feat/offscreen-userscripts

Conversation

@seidnerj

@seidnerj seidnerj commented Oct 6, 2026 •

Copy link
Copy Markdown

Adds two APIs that MV3-era userscript managers depend on.

chrome.userScripts: register, update, unregister, getScripts and configureWorld. Registered scripts are injected into matching frames of tracked tabs as each frame commits a navigation, honoring matches, excludeMatches, includeGlobs, excludeGlobs, allFrames and runAt.

  • USER_SCRIPT world scripts run in the extension's isolated world through tabs.executeScript, so they can message the extension. MAIN world scripts run in the page.
  • Injection requires a matching host permission, and file sources must resolve inside the extension.
  • When an extension registers user scripts, ElectronChromeExtensions emits user-scripts-registered with the extension id and the registered script ids. Registered scripts only reach later navigations, so a host can listen for it to reload views that loaded before the extension finished registering.

chrome.offscreen: createDocument, closeDocument and hasDocument. Each extension may have one offscreen document, hosted in a hidden window. runtime.getContexts reports it as OFFSCREEN_DOCUMENT.

Known limitations

  • Messages from user scripts arrive on runtime.onMessage/onConnect; onUserScriptMessage/onUserScriptConnect are not fired.
  • configureWorld is accepted but has no effect, so CSP and messaging aren't configurable.
  • Registrations are in memory, so extensions re-register on startup, as they do in Chrome.
  • Scripts apply to navigations after registration.

Testing: new fixture extension and specs for both APIs; the full suite passes (71/71). Also checked manually with a real userscript manager (an MV2 build): its runtime registered through userScripts and a @grant none script ran in the page.

Registered user scripts are injected into matching frames of tracked tabs as each frame commits a navigation, honoring matches, excludeMatches, includeGlobs, excludeGlobs, allFrames and runAt. USER_SCRIPT world scripts run in the extension's isolated world through tabs.executeScript, so they can message the extension over runtime.onMessage/onConnect; MAIN world scripts run in the page. Injection requires a matching host permission, and file sources must resolve inside the extension.
Each extension may create one offscreen document, hosted in a hidden window that loads the extension page. runtime.getContexts reports it as an OFFSCREEN_DOCUMENT context.
…named or bundled

resolvePreloadPath falls back to __dirname, but the bundled entry lives in dist/cjs or dist/esm while the preload is built to dist/. Check both locations.
@seidnerj

Copy link
Copy Markdown
Author

Added one more commit: chrome.permissions now handles MV2 host permissions.

An MV2 extension declares host access inside permissions (for example <all_urls>), but PermissionsAPI only read host_permissions, and contains() compared origins by exact string. So Tampermonkey's contains({origins: ["https://blacklist.tampermonkey.net/"]}) returned false despite <all_urls>, and it prompted on every launch.

  • Host patterns found in permissions count as granted origins; getAll() lists them under origins.
  • contains() and request() match origins with the existing match-pattern code (<all_urls>, * scheme, *.host).
  • request() returns true without prompting when everything requested is already held, and rejects undeclared origins.
  • New spec/chrome-permissions-spec.ts with MV2 and MV3 fixtures (14 passing).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant