Skip to content

feat: add WebExtension debugging support via extensionPath config#2361

Draft
noorez wants to merge 2 commits into
microsoft:mainfrom
noorez:feat/webextension-extensionpath
Draft

feat: add WebExtension debugging support via extensionPath config#2361
noorez wants to merge 2 commits into
microsoft:mainfrom
noorez:feat/webextension-extensionpath

Conversation

@noorez
Copy link
Copy Markdown

@noorez noorez commented May 19, 2026

Summary

Adds an extensionPath property to Chrome/Edge launch and attach configurations, enabling debugging of unpacked browser extensions (MV2 background pages, MV3 service workers) without manual target picking or workarounds.

Closes #945


Background

Issue #945 was closed as out-of-scope citing two blockers:

  1. Target type filter — the debugger only accepted page-type CDP targets; service workers were silently skipped (noted as "easy to fix")
  2. Non-deterministic extension IDs — source URLs use chrome-extension://<id>/... where <id> changes each session with a fresh profile, making static path mapping configuration impossible (noted as the "hard" blocker)

vscode-firefox-debug solves the equivalent Firefox problem (moz-extension://<uuid>/...) with a UUID-ignoring regex. The same approach works for Chrome.


Changes

File Change
src/configuration.ts extensionPath: string | null on IChromiumBaseConfiguration (Chrome + Edge, launch + attach)
src/targets/browser/browserLauncher.ts Injects --load-extension into Chrome args; overrides getFilterForTarget to match extension targets; pins discovered extension ID post-attach
src/targets/browser/browserAttacher.ts Same target filter override in getTargetFilter
src/targets/browser/browserPathResolver.ts chrome-extension://<id>/pathextensionPath/path resolution; pinExtensionId() to lock to discovered ID
src/targets/sourcePathResolverFactory.ts Threads extensionPath through to resolver options
src/build/generate-contributions.ts extensionPath in shared Chromium base schema; adds "Chrome: Launch Extension" config snippet
src/cdp/connection.ts Unknown-session-id throwwarn+return — Chrome sends Inspector.workerScriptLoaded on the service worker session before createSession() completes, causing a noisy unhandled error
package.nls.json NLS strings for new config property and snippet

How It Works

Target filtering: When extensionPath is set, the CDP target filter accepts service_worker and page targets whose URL starts with chrome-extension://, rejecting all others (normal tabs, devtools, etc.).

Source map resolution: A UUID-ignoring regex initially matches any 32-character extension ID. Once the main target attaches, the real ID is extracted from the target URL and pinned — all subsequent chrome-extension:// source URLs are matched only against that exact origin, so other loaded extensions cannot interfere.

  1. Target type filter — the debugger only accepted page-type CDP targets; service workers were silently skipped (noted as "easy to fix")
  2. Non-deterministic extension IDs — source URLs use chrome-extension://<id>/... where <id> changes each session with a fresh profile, making static path mapping configuration impossible (noted as the "hard" blocker)

vscode-firefox-debug solves the equivalent Firefox problem (moz-extension://<uuid>/...) with a UUID-ignoring regex. The same approach works for Chrome.


Changes

File Change
src/configuration.ts extensionPath: string | null on IChromiumBaseConfiguration (Chrome + Edge, launch + attach)
src/targets/browser/browserLauncher.ts Injects --load-extension into Chrome args; overrides getFilterForTarget to match extension targets; pins discovered extension ID post-attach
src/targets/browser/browserAttacher.ts Same target filter override in getTargetFilter
src/targets/browser/browserPathResolver.ts chrome-extension://<id>/pathextensionPath/path resolution; pinExtensionId() to lock to discovered ID
src/targets/sourcePathResolverFactory.ts Threads extensionPath through to resolver options
src/build/generate-contributions.ts extensionPath in shared Chromium base schema; adds "Chrome: Launch Extension" config snippet
src/cdp/connection.ts Unknown-session-id throwwarn+return — Chrome sends Inspector.workerScriptLoaded on the service worker session before createSession() completes, causing a noisy unhandled error
package.nls.json NLS strings for new config property and snippet

How It Works

Target filtering: When extensionPath is set, the CDP target filter accepts service_worker and page targets whose URL starts with chrome-extension://, rejecting all others (normal tabs, devtools, etc.).

Source map resolution: A UUID-ignoring regex initially matches any 32-character extension ID. Once the main target attaches, the real ID is extracted from the target URL and pinned — all subsequent chrome-extension:// source URLs are matched only against that exact origin, so other loaded extensions cannot interfere.


Usage

Launch — VS Code owns Chrome, no external tooling needed:

{
  "type": "pwa-chrome",
  "request": "launch",
  "name": "Debug Extension",
  "extensionPath": "${workspaceFolder}/dist",
  "webRoot": "${workspaceFolder}",
  "sourceMapPathOverrides": {
    "webpack://<name>/./src/*": "${workspaceFolder}/src/*"
  }
}

--load-extension is injected automatically. The debugger attaches to the extension's background service worker or page without any manual target selection.

Attach — bring your own Chrome launcher (web-ext, a shell script, etc.):

{
  "type": "pwa-chrome",
  "request": "attach",
  "port": 9222,
  "name": "Debug Extension (attach)",
  "extensionPath": "${workspaceFolder}/dist",
  "webRoot": "${workspaceFolder}",
  "sourceMapPathOverrides": {
    "webpack://<name>/./src/*": "${workspaceFolder}/src/*"
  }
}

---
Not Included

- Auto-reload on file change — vscode-firefox-debug implements this via reloadDescriptor. The Chrome equivalent requires calling chrome.runtime.reload() via Runtime.evaluate then re-attaching to the new service worker target. Left as a follow-up; web-ext run --target=chromium covers this in the interim.
- Content script first-class support — content scripts run in normal page targets and are already reachable. extensionPath handles source map resolution for them as well when chrome-extension:// URLs appear in their sourcemaps.

---
Testing the Branch

git clone https://github.com/noorez/vscode-js-debug.git
cd vscode-js-debug
git checkout feat/webextension-extensionpath
npm install && npm run compile

Then open an extension workspace with the dev build loaded:

code --extensionDevelopmentPath="$(pwd)/dist" \
     --enable-proposed-api=ms-vscode.js-debug \
     /path/to/your/extension

Add a pwa-chrome launch config with "extensionPath": "${workspaceFolder}/dist" (pointing to your built extension) and press F5. The debugger should attach directly to the service worker — no manual target picking.

Community testing reports very welcome, especially on:
- MV2 background pages
- MV3 service workers
- Microsoft Edge (pwa-msedge)
- Extensions with TypeScript + sourcemaps

@noorez noorez marked this pull request as draft May 19, 2026 17:59
@noorez
Copy link
Copy Markdown
Author

noorez commented May 19, 2026

@microsoft-github-policy-service agree

@noorez noorez force-pushed the feat/webextension-extensionpath branch 3 times, most recently from 0d07209 to 1bc9a3d Compare May 19, 2026 18:23
Comment thread src/cdp/connection.ts
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

hmm...on second thought, is this too lax now? initial testing showed that a real error wasnt allowed to slip through but perhaps some more aggressive timing checks are warranted?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

refined in commit: e326e5d

@noorez noorez force-pushed the feat/webextension-extensionpath branch 2 times, most recently from e326e5d to 9ee05fd Compare May 20, 2026 18:33
noorez and others added 2 commits May 20, 2026 12:37
Adds an `extensionPath` property to Chrome/Edge launch and attach configs
that enables debugging unpacked browser extensions (MV2/MV3) without
manual target picking or workarounds.

When set:
- Launch: auto-injects --load-extension and filters CDP targets to the
  extension's service worker or background page
- Attach: same target filter, works with web-ext or any externally
  launched Chrome
- Source maps: chrome-extension://<id>/path resolves to extensionPath/path
  using a UUID-ignoring regex (same pattern as vscode-firefox-debug)

After the main target is attached, the resolved extension ID is pinned in
the path resolver so subsequent source URLs are matched only against that
exact extension origin, preventing any other loaded extensions from being
incorrectly resolved or attached to.

Also fixes a race condition where Inspector.workerScriptLoaded arrives on
the service worker's CDP session before createSession() is called, which
previously threw an unhandled error.

Adds a "Chrome: Launch Extension" configuration snippet for discoverability
in the Add Configuration menu.

Closes microsoft#945 (WebExtension debugging support)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix suppressed all messages for unregistered sessions.
A command *response* arriving on an unknown session would indicate a
real bug (we only send commands after createSession()) and should still
throw so it isn't silently swallowed. Only CDP events (object.method is
set) are safe to drop — they are fire-and-forget with no waiting caller.
@noorez noorez force-pushed the feat/webextension-extensionpath branch from 9ee05fd to b275e42 Compare May 20, 2026 18:38
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.

Add support for WebExtensions

1 participant