wxt-dev/wxt

MV3 dev dynamic content script registration drops matchOriginAsFallback

Open

#2335 opened on May 4, 2026

View on GitHub
 (1 comment) (0 reactions) (0 assignees)TypeScript (9,861 stars) (511 forks)user submission
contribution welcomegood first issue

Description

Describe the bug

In MV3 development mode, WXT dynamically registers content scripts from the background service worker instead of putting them in manifest.content_scripts.

When a content script is defined with matchOriginAsFallback: true, the production manifest preserves the option as match_origin_as_fallback: true, but the dev-mode dynamic registration drops it. As a result, dev mode behaves differently from production for about:blank / about:srcdoc iframes that depend on origin fallback.

Reproduction

Define a content script like this:

export default defineContentScript({
  matches: ["*://*/*", "file:///*"],
  allFrames: true,
  matchAboutBlank: true,
  matchOriginAsFallback: true,
  async main() {
    // ...
  },
})

Run WXT in MV3 dev mode:

wxt -b edge

Then inspect the registered content scripts from the extension service worker:

await chrome.scripting.getRegisteredContentScripts()

Actual behavior

The dynamically registered content script has matchOriginAsFallback: false:

{
  "id": "wxt:content-scripts/node-trigger.js",
  "js": ["content-scripts/node-trigger.js"],
  "matches": ["*://*/*", "file:///*"],
  "allFrames": true,
  "matchOriginAsFallback": false,
  "runAt": "document_idle",
  "world": "ISOLATED"
}

This prevents the content script from running in a TinyMCE editor iframe whose document is about:srcdoc, even though that same script works in production.

Expected behavior

The dev-mode dynamic registration should preserve matchOriginAsFallback: true:

{
  "id": "wxt:content-scripts/node-trigger.js",
  "js": ["content-scripts/node-trigger.js"],
  "matches": ["*://*/*", "file:///*"],
  "allFrames": true,
  "matchOriginAsFallback": true,
  "runAt": "document_idle",
  "world": "ISOLATED"
}

Production build already emits the expected static manifest fields:

{
  "matches": ["*://*/*", "file:///*"],
  "all_frames": true,
  "match_about_blank": true,
  "js": ["content-scripts/node-trigger.js"],
  "match_origin_as_fallback": true
}

Likely cause

In mapWxtOptionsToContentScript, WXT maps:

match_origin_as_fallback: options.matchOriginAsFallback

But mapWxtOptionsToRegisteredContentScript does not map the equivalent dynamic registration field:

function mapWxtOptionsToRegisteredContentScript(options, js, css) {
  return {
    allFrames: options.allFrames,
    excludeMatches: options.excludeMatches,
    matches: options.matches,
    runAt: options.runAt,
    js,
    css,
    world: options.world
  };
}

Adding matchOriginAsFallback: options.matchOriginAsFallback to the dynamic registration payload fixes the dev-mode behavior in my testing.

Notes

In my Edge MV3 dev testing, chrome.scripting.registerContentScripts accepted matchOriginAsFallback, and that was enough for the about:srcdoc iframe case. Passing matchAboutBlank to dynamic registration appeared to make registration fail, so this report is specifically about preserving matchOriginAsFallback for dev-mode dynamic content script registration.

Environment

  • WXT: 0.20.25
  • Browser target: edge-mv3
  • Mode: development (wxt -b edge) *** End Patch

Contributor guide