Skip to content

feat: add WebExtension debugging support via extensionPath config - #2361

Open
noorez wants to merge 1 commit into
microsoft:mainfrom
noorez:feat/webextension-extensionpath
Open

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

Conversation

@noorez

@noorez noorez commented May 19, 2026

Copy link
Copy Markdown

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

noorez commented May 19, 2026

Copy link
Copy Markdown
Author

@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
@noorez
noorez force-pushed the feat/webextension-extensionpath branch 4 times, most recently from b275e42 to e441bb7 Compare May 22, 2026 13:32
@noorez
noorez marked this pull request as ready for review May 25, 2026 11:36
@noorez
noorez force-pushed the feat/webextension-extensionpath branch from b4eee88 to 437150a Compare June 1, 2026 12:53
@noorez
noorez force-pushed the feat/webextension-extensionpath branch 2 times, most recently from 5ee30c9 to 65a7ce1 Compare June 11, 2026 13:08
@noorez

noorez commented Jun 11, 2026

Copy link
Copy Markdown
Author

@kyranjamie and @lonr , I noticed you two had expressed interest in the past for this? any thoughts on what I have?

@noorez
noorez marked this pull request as draft July 6, 2026 19:05
@noorez
noorez marked this pull request as ready for review July 6, 2026 19:07
@noorez
noorez force-pushed the feat/webextension-extensionpath branch from 8e75b45 to 9d0ef52 Compare July 9, 2026 13:38
@noorez
noorez force-pushed the feat/webextension-extensionpath branch from 9d0ef52 to 5558dd1 Compare July 20, 2026 20:06
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

The extension id is derived offline rather than guessed: Chrome hashes the
manifest `key` (or, absent one, the absolute path), takes the first 16
bytes and maps each nibble onto a-p. Deriving it lets the target filter
require an exact match, which matters because browsers also expose service
workers for their own component extensions, and those can be reported
before the extension under debug. This addresses the "non-deterministic
extension ID" blocker noted on microsoft#945.

An idle MV3 service worker has no CDP target at all, so there is nothing
to attach to until something starts it. When no target is present the
debugger now calls ServiceWorker.startWorker for the extension's scope,
which is what previously required the manual "start the worker in
DevTools" dance described in microsoft#1445.

Also includes four robustness fixes surfaced by extension debugging,
each of which produced spurious errors before:

- cdp/connection: Chrome can push events (e.g.
  Inspector.workerScriptLoaded) on a new service worker session before
  createSession() completes. Such events are now dropped with a warning
  rather than throwing. Command *responses* on an unknown session still
  throw, since those would indicate a real bug.
- sourceContainer: removeSource asserted when a source's reference id had
  been reassigned to a newer Source for the same URL, which happens
  routinely as service workers restart. Suppressed only for the same-URL
  case; other mismatches still log.
- serviceWorkers: replaced a leftover `debugger;` statement, hit when a
  delete arrived for an unknown registration, with a verbose log.
- resourceProvider: fetching a chrome-extension:// sourcemap threw an
  opaque "Unsupported protocol" from the HTTP client, escaping before the
  existing CDP fallback could run. Non-HTTP schemes now return a failed
  response so the browser, which understands them, can serve the resource.

Adds a "Chrome: Launch Extension" configuration snippet so the option is
discoverable from the Add Configuration menu.

Closes microsoft#945

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noorez
noorez force-pushed the feat/webextension-extensionpath branch from 5558dd1 to 6c8a7ba Compare July 27, 2026 22:41
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