Skip to content

Migrate Chrome/Edge extension tooling to WXT#106

Merged
danielchalmers merged 9 commits into
mainfrom
migrate-to-wxt-dev-mode
Jun 8, 2026
Merged

Migrate Chrome/Edge extension tooling to WXT#106
danielchalmers merged 9 commits into
mainfrom
migrate-to-wxt-dev-mode

Conversation

@danielchalmers

@danielchalmers danielchalmers commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #104.

This PR replaces the CRX/Vite extension tooling with WXT for Chrome/Edge development. The earlier manual Chrome debug-script fix was reverted, and the repo now lets WXT own extension dev mode, manifest generation, production builds, packaging, and the VS Code F5 path.

Why

The old setup made us responsible for Chrome launch/profile orchestration. WXT documents Dev Mode as opening a browser with the extension installed, which matches the workflow we wanted without adding custom profile cleanup or process-management scripts.

Sources used:

What Changed

  • Added WXT 0.20.26 and removed the CRXJS/Vite extension build path.
  • Moved extension entrypoints into WXT's src/entrypoints structure.
  • Added wxt.config.ts as the manifest/build/dev-browser source of truth.
  • Kept MV3 background registration event-driven and testable through registerBackground().
  • Switched scripts to the WXT path: dev, build, zip, and postinstall: wxt prepare.
  • Removed Firefox-specific scripts and config because this repo only supports Chrome/Edge.
  • Updated VS Code F5 to start WXT dev mode.
  • Updated Playwright fixtures and release packaging to use WXT .output artifacts.
  • Removed stale tooling/dependency leftovers: direct Vite, CRXJS, rimraf, coverage tooling, old package/build aliases, and unused Rollup override.
  • Pruned stale config and barrels so imports point directly at storage, messaging, filtering, helper, DOM, and schedule modules.
  • Restored CI as separate PR-visible jobs: Static Analysis, Unit Tests, Extension E2E, and Package Extension.
  • Added scoped dependency overrides for patched WXT transitive dev dependencies.

Debugger Lifecycle

Closing the WXT-launched Chrome window does not stop the VS Code debug session because the session is running WXT's long-lived dev server. That appears intentional for WXT: the dev server keeps watching and WXT prints Press o + enter to reopen the browser after launch.

The launch config is now named Start WXT Dev Server instead of Run WXT Dev to make that lifecycle clearer. To stop the watcher/debug session, stop the VS Code debug session or terminate the terminal. No profile cleanup is involved.

Profile Handling

No build, dev, or validation step deletes profile data. WXT uses a persistent Windows Chromium profile under .wxt/chrome-data with chromiumProfile and keepProfileChanges. The config only ensures that directory exists before launch; it does not remove or reset it.

Issue #104 reports that VS Code F5 can reopen stale Chrome for Testing blocked-page tabs using the legacy ?url= shape even after the extension was rebuilt. The important constraint for this repo is preserving the persistent .vscode/chrome-debug-profile settings and filters; deleting Sessions, Service Worker, Code Cache, Cache, or other profile files is the wrong fix.

The fix moves F5 from a direct chrome launch to an attach workflow. A sequenced prelaunch task now stops only chrome.exe processes whose command line uses this repo debug profile, rebuilds dist, ensures Playwright Chromium exists, then starts Chrome for Testing with the rebuilt dist unpacked extension and a remote-debugging port. The Node launcher creates one fresh about:blank target with Chrome DevTools Protocol and closes other restored page targets. That removes stale runtime tabs without deleting or clearing any profile files. A postDebugTask stops the same debug-profile Chrome process after debugging.

Why this shape: VS Code documents launch.json as the saved F5 debug setup, supports preLaunchTask/postDebugTask, and describes attach as connecting to an already-running debug target: https://code.visualstudio.com/docs/debugtest/debugging-configuration. VS Code tasks support process tasks and dependsOrder sequence so stop/build/install/start runs in a deterministic order: https://code.visualstudio.com/docs/debugtest/tasks. Microsoft js-debug documents chrome attach address/port/urlFilter/timeout options: https://raw.githubusercontent.com/microsoft/vscode-js-debug/main/OPTIONS.md.

Chrome/Chromium behavior behind the choice: Chromium ProcessSingleton can notify an existing browser instance for a user data dir instead of making a fresh singleton, so the task first stops only this repo profile process: https://chromium.googlesource.com/chromium/src/+/master/chrome/browser/process_singleton.h. Chromium switch source documents the launch flags used here, including new-window, disable-background-mode, and disable-restore-session-state: https://chromium.googlesource.com/chromium/src/+/284303b6ca6170692f20027c6e75a6c15a75cbae/chrome/common/chrome_switches.cc; disable-session-crashed-bubble is likewise documented in Chromium switch source: https://chromium.googlesource.com/chromium/src/+/81357b39c643fc746517fd6ce5cb2076b7ddc3f4/chrome/common/chrome_switches.cc.

The target reset uses documented Chrome DevTools Protocol HTTP endpoints exposed by --remote-debugging-port: /json/version, /json/list, /json/new, /json/activate, and /json/close. Source: https://chromedevtools.github.io/devtools-protocol/.

Computer Use validation on this machine: first, I reproduced that simple launch flags could still leave a restored old Page Blocked tab. After this change, pressing F5 in VS Code produced exactly one about:blank Chrome for Testing window. I temporarily changed src/blocked/index.html to show DEBUG-ISSUE-104, pressed F5 again from VS Code, navigated to https://example.com/asdf-issue-104-final, and confirmed the blocked page rendered that marker, showed the blocked URL and responsible asdf filter, and the live target URL was chrome-extension://.../src/blocked/index.html?blockId=... with no ?url= page target. I reverted the temporary source marker before committing.

Validation: npm exec prettier -- --check .vscode/launch.json .vscode/tasks.json eslint.config.mjs tsconfig.scripts.json scripts/launch-chrome-debug-profile.mjs; npm run build:dev; npm run lint; npm run typecheck; npm run typecheck:test; npm run test:unit; npm run test:e2e. Note: npm run format:check still reports pre-existing formatting differences in unrelated source/test files, so the commit used a targeted Prettier check for touched parseable files plus git diff --check.
The previous local debug fix made F5 work by adding bespoke Chrome launch and stop scripts around a Playwright-managed Chromium profile. That solved the immediate symptom, but it left us owning profile/process orchestration that WXT already provides for extension development. This commit follows the requested direction: revert that manual tooling path in the preceding commit, then go fully onboard with WXT for dev, build, packaging, and editor launch workflows.

Sources and reasoning:
- WXT's comparison guide documents Dev Mode as opening a browser with the extension installed: https://wxt.dev/guide/resources/compare
- WXT's CRXJS migration guide calls out removing @crxjs/vite-plugin, adding WXT scripts, adding `postinstall: wxt prepare`, moving entrypoints under `entrypoints`, and comparing the generated manifest against the old one: https://wxt.dev/guide/resources/migrate.html
- WXT's project-structure docs define `srcDir`, `entrypoints`, `.wxt`, and `.output`; using the default `.output` avoids Vite treating stale `dist` HTML as dev-server input: https://wxt.dev/guide/essentials/project-structure
- WXT's browser-startup docs explain that WXT uses Mozilla web-ext to open a browser during development and show the Windows persistent Chromium profile configuration with `chromiumProfile` plus `keepProfileChanges`: https://wxt.dev/guide/essentials/config/browser-startup
- WXT's Vite docs confirm WXT config is the supported place to customize Vite behavior, and Vite's dependency optimizer docs explain default HTML crawling and `optimizeDeps.entries`: https://wxt.dev/guide/essentials/config/vite and https://vite.dev/config/dep-optimization-options.html
- WXT's zip command is now the packaging path instead of hand-zipping `dist`: https://wxt.dev/api/cli/wxt-zip

What changed:
- Replaced the CRX/Vite extension build with WXT 0.20.26 and removed the standalone `manifest.json` and `vite.config.ts`.
- Added `wxt.config.ts` as the source of truth for manifest data, production console dropping, Vite dev dependency discovery, and WXT's persistent Chromium dev profile at `.wxt/chrome-data`.
- The profile directory is created when WXT config loads so Chrome can write its launcher logs, but no build/dev script deletes or resets profile data. The `clean` script only targets `.output` build artifacts.
- Moved popup/options/blocked UI entrypoints into `src/entrypoints`, added a WXT background entrypoint, and kept the MV3 listener registration synchronous through `registerBackground()`.
- Switched package scripts to `wxt`, `wxt build`, `wxt zip`, and Firefox variants; VS Code F5 now runs `npm run dev` through WXT instead of manually launching Chrome.
- Updated Playwright e2e fixtures to load `.output/chrome-mv3`, updated release packaging to consume WXT's generated `*-chrome.zip`, and aligned TypeScript/ESLint with WXT's generated `.wxt` types.

Validation performed:
- `npm run check` passed: Prettier, ESLint, app typecheck, test typecheck, 157 unit tests, production WXT build, and 40 Playwright e2e tests.
- `npm run zip` produced `.output/teichos-0.0.0-chrome.zip`.
- WXT dev mode was launched on this Windows machine; its log reported `Opened browser`, and Computer Use found and activated the WXT-launched `about:blank - Google Chrome` desktop window.
- Chrome's profile metadata showed Teichos installed from `F:\source\repos\PageBlock\.output\chrome-mv3-dev`, with the MV3 service worker started and active permissions matching the WXT development manifest.
- A temporary marker was added to `src/entrypoints/blocked/index.html` while `npm run dev` was running; WXT logged the source change and reloaded extension pages, `.output/chrome-mv3-dev/blocked.html` contained the marker, then the marker was removed and the generated output no longer contained it. This confirms edits in the repo are reflected in the actual WXT debug extension path on this machine.
The first WXT migration fixed the extension development workflow but left some old repo shape behind. The PR CI exposed the biggest mismatch: the Playwright job still waited for a `dist` artifact from the build job, while WXT writes extension builds to `.output`. Rather than keep a build-artifact handoff for generated files, this commit makes CI match the local validation path directly.

CI is now one job in the Playwright container. It installs once and runs `npm run check`, which already performs formatting, linting, both TypeScript checks, unit tests, the WXT production build, and Playwright e2e tests. This removes the broken `dist` artifact download, the separate build upload, and the repeated dependency installs across four jobs.

The repo cleanup removes direct tooling we no longer own or use:
- Firefox WXT scripts are gone; this project supports Chrome/Edge only.
- `build:dev`, `playwright:install`, `test:coverage`, `clean`, and the `package` alias are gone.
- Direct `vite`, `rimraf`, and `@vitest/coverage-v8` dependencies are gone. Vite still exists transitively through WXT/Vitest, but it is no longer a top-level repo dependency.
- The old Rollup override is gone because Rollup is no longer in the installed dependency graph.
- VS Code tasks are reduced to Check, Build, and Test E2E, and generated WXT output/profile folders are hidden in Explorer without deleting them.
- TypeScript excludes now include WXT's `.output` and `.wxt` directories explicitly.

I also ran dependency cleanup after pruning. `npm audit fix` safely updated `brace-expansion`, and the remaining WXT/web-ext-run audit path was resolved with scoped overrides for patched `tmp` and `uuid` versions. That keeps WXT at 0.20.26 instead of using npm audit's suggested force path, which would downgrade WXT to 0.3.2.

Validation:
- `npm audit --audit-level=moderate` reports 0 vulnerabilities.
- `npm run check` passes, including all 157 unit tests and 40 Playwright e2e tests.
- `npm run zip` produces `.output/teichos-0.0.0-chrome.zip`.
- `npm run dev` smoke-tested WXT's browser runner: WXT built the dev extension and logged `Opened browser`; the launched WXT node/chrome processes were then stopped without removing profile data.
The simplified CI now runs the whole local check path in a Playwright container. That exposed an existing timing-sensitive lifecycle test under the default CI worker count: one extension-runtime/storage transition test intermittently missed the expected blocked-page redirect with two workers. These browser tests exercise a shared extension service worker and chrome.storage behavior, so running them serially is the conservative configuration for repeatable CI. Local validation passes with all 40 e2e tests using one worker.
The WXT migration made the extension tooling much simpler, but a few leftover conveniences were still hiding old assumptions: a duplicate unit-test script, a Vitest alias/globals setup that the code no longer used, and catch-all shared barrels that made filtering and API ownership look broader than it is.

This removes the duplicate test:unit script and points check:fast at the canonical test script. It also trims vitest.config.ts down to the settings the repo actively uses, drops the unused @/* test alias and Vitest global types, and makes test/setup.ts import beforeEach explicitly so the test project no longer depends on implicit globals.

The shared/api and shared/utils root barrels are removed, along with the two legacy filtering shim files under shared/utils. Consumers now import directly from storage, messaging, runtime, tabs, helpers, DOM helpers, filtering patterns, filtering schedules, or the filtering engine. That keeps module boundaries visible in the import graph instead of relying on convenience re-exports.

Evidence and reasoning: rg found no @/ imports, no remaining test:unit consumer after check:fast was updated, and no remaining root shared/api or shared/utils imports after the direct-import rewrite. The coverage config was stale because the coverage dependency was already gone and no committed script invokes coverage. WXT's local CLI help exposes dev, build, and zip as the supported tooling surface, so this commit keeps the repo on those WXT commands instead of adding scripts.

Validation: npm run check passed, including format, lint, app typecheck, test typecheck, 17 Vitest files with 157 tests, WXT production build, and 40 Playwright extension e2e tests. npm run zip passed and produced the chrome zip through WXT. npm audit --audit-level=moderate found 0 vulnerabilities. A live WXT dev smoke ran npm run dev -- --browser chrome --port 3000, confirmed WXT built .output/chrome-mv3-dev and reported Opened browser, then stopped the dev process tree after that success signal. No profile cleanup or profile deletion was used.
@danielchalmers danielchalmers changed the title Migrate extension tooling to WXT Migrate Chrome/Edge extension tooling to WXT Jun 5, 2026
The WXT migration collapsed CI down to one npm run check job named Checks. That kept the workflow lean, but it made the PR status too opaque: reviewers could no longer tell whether static analysis, unit coverage, extension e2e, or packaging was the failing surface without opening the job log.

This keeps the WXT-oriented cleanup while restoring useful PR-level check names. Static Analysis runs formatting, lint, and both TypeScript projects. Unit Tests runs the Vitest suite through the canonical npm run test script. Extension E2E runs npm run test:e2e in the Playwright container and uploads Playwright artifacts on failures. Package Extension runs npm run zip and uploads WXT's generated Chrome zip from .output.

The old dist artifact handoff is intentionally not restored. WXT owns the output path now, and Playwright already builds the extension it tests. The package job validates release packaging separately without coupling e2e to a stale dist directory.

The VS Code launch configuration is also renamed from Run WXT Dev to Start WXT Dev Server. WXT dev mode is a long-running watcher with browser reopen support; closing the browser does not mean the dev server/debug session is finished, so the launch label should describe the lifecycle more accurately.

Validation: npm run check passed, including 17 Vitest files with 157 tests and 40 Playwright extension e2e tests. npm run zip passed and produced .output/teichos-0.0.0-chrome.zip. npm audit --audit-level=moderate reported 0 vulnerabilities. git diff --check passed.
Package Extension was failing after the split-CI follow-up even though npm run zip succeeded. The log showed WXT created .output/teichos-0.0.0-chrome.zip, then actions/upload-artifact@v7 reported no files for .output/*-chrome.zip.

The issue is the artifact path lives under the hidden .output directory, while upload-artifact defaults include-hidden-files to false. This keeps the precise zip path and enables hidden-file inclusion only for that explicit artifact upload.

Validation: npm run zip passed, npx prettier --check .github/workflows/ci.yml .vscode/launch.json passed, and git diff --check passed.
After splitting CI, the Extension E2E job exposed two intermittent option-page failures around whitelist creation. The trace showed the exception row was created, but the optional Name field stayed empty, so the UI displayed the required pattern instead of the test's description label.

The underlying race is in the tests: option modals focus their primary pattern/name field on requestAnimationFrame. The test was immediately filling the optional description field after the modal became visible, so CI could occasionally let the modal autofocus steal focus while the fill was in progress. The extension behavior is unchanged.

This waits for each modal's intended autofocus target before filling values in the shared helpers and the direct edit flows that hit the same path. That aligns the tests with the UI lifecycle instead of broadening timeouts or adding retries.

Validation: npm run build plus the two previously failing Playwright tests passed. npm run check passed, including 17 Vitest files with 157 tests and all 40 Playwright extension e2e tests. git diff --check passed.
@danielchalmers danielchalmers merged commit 8713ff9 into main Jun 8, 2026
4 checks passed
@danielchalmers danielchalmers deleted the migrate-to-wxt-dev-mode branch June 8, 2026 18:00
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.

F5 debugging can run stale extension code and still opens legacy blocked-page URLs

1 participant