Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the web extension build and background runtime to Manifest V3 by transforming the generated manifest per-platform and replacing the MV2 AngularJS background page bootstrap with an MV3-compatible background entry that uses a manual DI container and lightweight AngularJS service shims.
Changes:
- Transform generated
manifest.jsonto MV3 for Chromium and Firefox, and stop copyingbackground.html. - Replace AngularJS background module bootstrap with MV3 background entry points + a manual DI container.
- Add AngularJS service shims and a background-friendly alert notification hook.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| webpack/firefox.config.js | Adds MV3 manifest transform + Firefox-specific settings; removes background.html copy. |
| webpack/chromium.config.js | Adds MV3 manifest transform for Chromium; removes background.html copy. |
| src/modules/webext/webext-background/webext-background.service.ts | Removes AngularJS usage for install handling; switches bookmark service abstraction; removes angular.isUndefined usage. |
| src/modules/webext/webext-background/background-container.ts | New manual DI container for MV3 background context. |
| src/modules/webext/webext-background/angular-shims.ts | New $q/$http/$timeout/$interval/$injector + angular global shim for service-worker execution. |
| src/modules/webext/shared/webext-platform/webext-platform.service.ts | Removes AngularJS dependency; supports browser.action in MV3; background-context messaging shortcut. |
| src/modules/webext/firefox/firefox-background/firefox-background.module.ts | New MV3 Firefox background entry point using the manual DI container. |
| src/modules/webext/chromium/chromium-background/chromium-background.module.ts | New MV3 Chromium service-worker entry point using the manual DI container. |
| src/modules/shared/alert/alert.service.ts | Adds onAlertChanged callback to support background notifications on alerts. |
Comments suppressed due to low confidence (2)
src/modules/webext/chromium/chromium-background/chromium-background.module.ts:40
- MV3 service workers can be started for events like runtime.onMessage without firing onStartup; with the current gating, init() may never run in those wake-ups. Consider initializing (or ensuring a one-time init) at service-worker startup so required state (e.g., icon/title refresh, update checks, enabling sync) is consistently set whenever the worker is launched.
// Register event handlers synchronously (required for MV3 service workers)
let startupInitiated = false;
browser.runtime.onInstalled.addListener((details) => {
if (startupInitiated) return;
startupInitiated = true;
backgroundSvc.onInstall(details.reason);
});
browser.runtime.onStartup.addListener(() => {
if (startupInitiated) return;
startupInitiated = true;
backgroundSvc.init();
});
src/modules/webext/firefox/firefox-background/firefox-background.module.ts:40
- MV3 background scripts/service workers may be woken by events other than onStartup/onInstalled (e.g., runtime.onMessage, alarms). With the current startupInitiated gating, init() may not run on those wake-ups. Consider triggering a one-time init at script startup so the background is always initialized whenever the worker starts.
// Register event handlers synchronously (required for MV3 background scripts)
let startupInitiated = false;
browser.runtime.onInstalled.addListener((details) => {
if (startupInitiated) return;
startupInitiated = true;
backgroundSvc.onInstall(details.reason);
});
browser.runtime.onStartup.addListener(() => {
if (startupInitiated) return;
startupInitiated = true;
backgroundSvc.init();
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Set up angular shim before any service code runs | ||
| setupAngularShim(); | ||
|
|
There was a problem hiding this comment.
setupAngularShim() is called in the module body, but all imports (including services that may reference angular or import the real AngularJS package) are evaluated before this runs. If the intent is to rely on the shim in the MV3 background context, move shim setup to a side-effect import that executes before other modules, or ensure background dependencies do not import angular at all.
This issue also appears on line 27 of the same file.
| // Set up angular shim before any service code runs | ||
| setupAngularShim(); | ||
|
|
There was a problem hiding this comment.
setupAngularShim() runs after module imports have already been evaluated. If any imported service relies on a global angular shim (or if importing AngularJS would fail in a service worker), this is too late. Consider making the shim a side-effect import executed before any other background dependencies, or remove/alias all import angular from 'angular' usage for the MV3 background bundle.
This issue also appears on line 27 of the same file.
| get: (name: string): any => { | ||
| const svc = services.get(name); | ||
| if (!svc) { | ||
| throw new Error(`Service '${name}' not registered in background injector`); | ||
| } | ||
| return svc; | ||
| }, |
There was a problem hiding this comment.
$injectorFactory.get() treats any falsy registered value (e.g., 0/false/"") as “not registered” because it checks if (!svc). This should check services.has(name) instead, otherwise valid registrations can be incorrectly rejected.
| $timeout.cancel = (promise: any): boolean => { | ||
| if (promise && promise.$$timeoutId != null) { | ||
| clearTimeout(promise.$$timeoutId); | ||
| return true; | ||
| } | ||
| return false; | ||
| }; |
There was a problem hiding this comment.
$timeout.cancel() clears the timer but leaves the returned promise permanently pending. AngularJS $timeout typically rejects/cancels the promise so callers can unblock; consider rejecting/settling the promise on cancel (or returning a cancellable promise implementation) to avoid dangling awaits and resource leaks.
| // 11. ApiXbrowsersyncService (depends on $injector, $http, $q, NetworkService, StoreService, UtilityService) | ||
| const apiSvc = new ApiXbrowsersyncService(injector, $http, $q, networkSvc, storeSvc, utilitySvc); | ||
| injector.register('ApiXbrowsersyncService', apiSvc); |
There was a problem hiding this comment.
ApiXbrowsersyncService is instantiated in the MV3 background container, but it imports DOMPurify/marked and uses DOMParser for formatting service messages. Those DOM-dependent dependencies can be problematic in a service worker context. Consider moving the markdown/DOM sanitization logic into a UI-only helper (or lazy-loading it inside formatServiceInfo) so the background can safely use the API client without DOM requirements.
| export const createBackgroundContainer = (config: BackgroundPlatformConfig): BackgroundContainer => { | ||
| // Create AngularJS shims | ||
| const $q = $qFactory(); | ||
| const $timeout = $timeoutFactory(); | ||
| const $interval = $intervalFactory(); | ||
| const $http = $httpFactory(); | ||
| const $log = $logFactory(); | ||
| const $rootScope = $rootScopeFactory(); | ||
| const $location = $locationFactory(); |
There was a problem hiding this comment.
This container aims to run background code without AngularJS, but many of the services it instantiates (and the platform BookmarkService implementations it receives) still import angular from 'angular', which will pull AngularJS into the MV3 background bundle and bypass the shim approach. Consider aliasing angular to a lightweight shim for the background build, or refactoring those services to avoid importing AngularJS (and ensuring shim setup happens before any usage).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (alert) { | ||
| backgroundSvc.displayAlert(alert); |
There was a problem hiding this comment.
alertSvc.onAlertChanged calls backgroundSvc.displayAlert(alert), but WebExtBackgroundService.displayAlert() uses DOMParser to strip HTML. In an MV3 service worker context there may be no DOM/DOMParser, which would cause a runtime crash the first time an alert is raised. Consider making displayAlert resilient to missing DOM APIs (e.g., avoid DOMParser or guard it) before wiring it into the background container.
| if (alert) { | |
| backgroundSvc.displayAlert(alert); | |
| if (!alert) { | |
| return; | |
| } | |
| // In MV3 service worker contexts, DOM APIs like DOMParser may not exist. | |
| // Guard the call to displayAlert to avoid runtime crashes when DOMParser is unavailable. | |
| if (typeof DOMParser !== 'undefined') { | |
| backgroundSvc.displayAlert(alert); | |
| } else { | |
| // Fall back to logging if we cannot safely display the alert. | |
| logSvc.warn('Unable to display alert: DOMParser is not available in this context.', alert); |
| export const createBackgroundContainer = (config: BackgroundPlatformConfig): BackgroundContainer => { | ||
| // Create AngularJS shims | ||
| const $q = $qFactory(); | ||
| const $timeout = $timeoutFactory(); | ||
| const $interval = $intervalFactory(); | ||
| const $http = $httpFactory(); |
There was a problem hiding this comment.
createBackgroundContainer() introduces a new manual DI/bootstrapping path for MV3 background execution, but there are no accompanying unit tests validating the wiring (e.g., that required shim services are registered and that the constructed WebExtBackgroundService can handle a basic message/alert flow). Since the repo already has Jest coverage for other webext services, adding a focused spec for this container would help prevent regressions in the MV3 migration.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/modules/webext/shared/webext-platform/webext-platform.service.ts
Outdated
Show resolved
Hide resolved
| const webExtTransformResult = webExtTransform(buffer); | ||
| const manifest = JSON.parse(webExtTransformResult); | ||
|
|
||
| // Upgrade to Manifest V3 | ||
| manifest.manifest_version = 3; | ||
|
|
||
| // browser_action -> action | ||
| manifest.action = manifest.browser_action; | ||
| delete manifest.browser_action; | ||
|
|
||
| // background page -> service worker | ||
| manifest.background = { | ||
| service_worker: 'assets/background.js' | ||
| }; | ||
|
|
||
| // content_security_policy string -> object | ||
| manifest.content_security_policy = { | ||
| extension_pages: manifest.content_security_policy | ||
| }; | ||
|
|
||
| // Split optional_permissions: host patterns go to optional_host_permissions (MV3) | ||
| if (manifest.optional_permissions) { | ||
| const hostPatterns = manifest.optional_permissions.filter((p) => /^https?:/.test(p)); | ||
| const nonHostPerms = manifest.optional_permissions.filter((p) => !/^https?:/.test(p)); | ||
| manifest.optional_host_permissions = hostPatterns; | ||
| if (nonHostPerms.length > 0) { | ||
| manifest.optional_permissions = nonHostPerms; | ||
| } else { | ||
| delete manifest.optional_permissions; | ||
| } | ||
| } | ||
|
|
||
| // _execute_browser_action -> _execute_action | ||
| if (manifest.commands && manifest.commands._execute_browser_action) { | ||
| manifest.commands._execute_action = manifest.commands._execute_browser_action; | ||
| delete manifest.commands._execute_browser_action; | ||
| } | ||
|
|
||
| return JSON.stringify(manifest, null, 2); |
There was a problem hiding this comment.
This transforms the manifest to MV3 for Chromium, but the codebase still uses MV2-only APIs such as browser.tabs.executeScript (see WebExtPlatformService.getPageMetadata) and the base manifest doesn’t include the MV3 scripting permission. As-is, the Chromium MV3 build is likely to fail at runtime when injecting content scripts. Consider either keeping Chromium on MV2 for now or updating the manifest (permissions) and code to use browser.scripting.executeScript / equivalent MV3 APIs.
| const webExtTransformResult = webExtTransform(buffer); | |
| const manifest = JSON.parse(webExtTransformResult); | |
| // Upgrade to Manifest V3 | |
| manifest.manifest_version = 3; | |
| // browser_action -> action | |
| manifest.action = manifest.browser_action; | |
| delete manifest.browser_action; | |
| // background page -> service worker | |
| manifest.background = { | |
| service_worker: 'assets/background.js' | |
| }; | |
| // content_security_policy string -> object | |
| manifest.content_security_policy = { | |
| extension_pages: manifest.content_security_policy | |
| }; | |
| // Split optional_permissions: host patterns go to optional_host_permissions (MV3) | |
| if (manifest.optional_permissions) { | |
| const hostPatterns = manifest.optional_permissions.filter((p) => /^https?:/.test(p)); | |
| const nonHostPerms = manifest.optional_permissions.filter((p) => !/^https?:/.test(p)); | |
| manifest.optional_host_permissions = hostPatterns; | |
| if (nonHostPerms.length > 0) { | |
| manifest.optional_permissions = nonHostPerms; | |
| } else { | |
| delete manifest.optional_permissions; | |
| } | |
| } | |
| // _execute_browser_action -> _execute_action | |
| if (manifest.commands && manifest.commands._execute_browser_action) { | |
| manifest.commands._execute_action = manifest.commands._execute_browser_action; | |
| delete manifest.commands._execute_browser_action; | |
| } | |
| return JSON.stringify(manifest, null, 2); | |
| // Delegate to the original WebExt transform without forcing MV3-specific changes. | |
| return webExtTransform(buffer); |
src/modules/webext/webext-background/webext-background.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 6. UtilityService (depends on $exceptionHandler, $http, $injector, $location, $q, $rootScope, LogService, NetworkService, StoreService) | ||
| const utilitySvc = new UtilityService( | ||
| $exceptionHandler, | ||
| $http, | ||
| injector, | ||
| $location, | ||
| $q, | ||
| $rootScope, |
There was a problem hiding this comment.
createBackgroundContainer instantiates UtilityService, WebExtBackgroundService, and platform/bookmark services in an MV3 worker context, but several code paths they use rely on window/document (e.g., UtilityService.getUniqueishId() uses window.crypto, UtilityService.isBraveBrowser() uses window.navigator, and UtilityService.parseUrl() uses document.createElement). In a Chromium MV3 service worker these globals are undefined and will throw during startup/notifications/telemetry. Refactor the shared utilities to use globalThis/crypto/navigator/URL with DOM guards, or provide a background-only shim/alias for the DOM-dependent parts.
| let messageToDisplay = alert.message; | ||
| if (urlInAlert && typeof DOMParser !== 'undefined') { | ||
| messageToDisplay = | ||
| new DOMParser().parseFromString(`<span>${alert.message}</span>`, 'text/xml').firstElementChild.textContent ?? | ||
| alert.message; | ||
| } |
There was a problem hiding this comment.
displayAlert is intended to strip HTML tags from alert.message, but the new logic only does so when a URL is present and DOMParser exists. In MV3 worker contexts (no DOMParser) this will display raw markup in notifications, and even in DOM contexts messages without a URL won’t be stripped. Consider always stripping tags using a DOM-free approach (e.g., the existing regex-based tag stripping utility) and only using DOMParser as an optional refinement.
| let messageToDisplay = alert.message; | |
| if (urlInAlert && typeof DOMParser !== 'undefined') { | |
| messageToDisplay = | |
| new DOMParser().parseFromString(`<span>${alert.message}</span>`, 'text/xml').firstElementChild.textContent ?? | |
| alert.message; | |
| } | |
| let messageToDisplay = alert.message ?? ''; | |
| // Always perform a DOM-free tag stripping so this works in MV3 worker contexts | |
| messageToDisplay = messageToDisplay.replace(/<\/?[^>]+>/g, ''); | |
| // Optionally refine using DOMParser when available (e.g. to handle entities better) | |
| if (typeof DOMParser !== 'undefined') { | |
| try { | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(`<span>${alert.message}</span>`, 'text/html'); | |
| const textContent = (doc.body && doc.body.textContent) || doc.firstElementChild?.textContent; | |
| if (typeof textContent === 'string' && textContent.length > 0) { | |
| messageToDisplay = textContent; | |
| } | |
| } catch { | |
| // Fallback to the regex-stripped messageToDisplay on any parsing error | |
| } | |
| } |
| // --- $q shim: wraps native Promise to satisfy ng.IQService interface --- | ||
|
|
||
| interface Deferred<T> { | ||
| promise: Promise<T>; | ||
| resolve: (value?: T | PromiseLike<T>) => void; | ||
| reject: (reason?: any) => void; | ||
| } | ||
|
|
||
| const $qFactory = (): ng.IQService => { | ||
| const $q: any = <T>( | ||
| resolver: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void | ||
| ): Promise<T> => { | ||
| return new Promise<T>(resolver); | ||
| }; | ||
|
|
||
| $q.defer = <T>(): Deferred<T> => { | ||
| let resolve: any; | ||
| let reject: any; | ||
| const promise = new Promise<T>((res, rej) => { | ||
| resolve = res; | ||
| reject = rej; | ||
| }); | ||
| return { promise, resolve, reject }; | ||
| }; | ||
|
|
||
| $q.resolve = <T>(value?: T | PromiseLike<T>): Promise<T> => Promise.resolve(value); | ||
| $q.reject = (reason?: any): Promise<never> => Promise.reject(reason); | ||
| $q.when = <T>(value?: T | PromiseLike<T>): Promise<T> => Promise.resolve(value); | ||
| $q.all = (promises: any): Promise<any> => { | ||
| if (Array.isArray(promises)) { | ||
| return Promise.all(promises); | ||
| } | ||
| // Handle object of promises | ||
| const keys = Object.keys(promises); | ||
| return Promise.all(keys.map((k) => promises[k])).then((values) => { | ||
| const result: any = {}; | ||
| keys.forEach((k, i) => { | ||
| result[k] = values[i]; | ||
| }); | ||
| return result; | ||
| }); | ||
| }; | ||
|
|
||
| return $q as ng.IQService; | ||
| }; | ||
|
|
There was a problem hiding this comment.
The new AngularJS shims ($q, $http, $timeout, $interval, injector, and setupAngularShim) are now foundational for MV3 background execution, but there are no focused unit tests validating their behavior (timeouts/cancellation semantics, $http error shape, $q.all object/array handling, etc.). Adding tests for these shims would help prevent subtle worker-only regressions that won’t be caught by existing DOM-based tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -317,33 +342,33 @@ export abstract class WebExtPlatformService implements PlatformService { | |||
| // Set a delay if finished syncing to prevent flickering when executing many syncs | |||
| if (currentTitle.indexOf(syncingTitle) > 0 && newTitle.indexOf(syncedTitle)) { | |||
There was a problem hiding this comment.
In refreshNativeInterface, the condition newTitle.indexOf(syncedTitle) is using the raw indexOf number as a boolean. This is incorrect because -1 is truthy (so the delay branch can trigger even when syncedTitle is not present), and 0 is falsy (so it won’t trigger if syncedTitle is at the start). Compare the result explicitly (e.g., > -1) or use .includes(...) to ensure the flicker-prevention delay only applies when the title actually contains the synced suffix.
| if (currentTitle.indexOf(syncingTitle) > 0 && newTitle.indexOf(syncedTitle)) { | |
| if (currentTitle.includes(syncingTitle) && newTitle.includes(syncedTitle)) { |
| onInstall(reason?: string): void { | ||
| // Check if fresh install needed | ||
| const details = angular.element(event.currentTarget as Element).data('details'); | ||
| (details?.reason === 'install' ? this.installExtension() : this.$q.resolve()).then(() => this.init()); | ||
| (reason === 'install' ? this.installExtension() : this.$q.resolve()).then(() => this.init()); | ||
| } |
There was a problem hiding this comment.
onInstall now takes a reason?: string, but there is still at least one call site that passes an event object (e.g. WebExtBackgroundComponent.install(event) calls backgroundSvc.onInstall(event)). With the new implementation, this will skip the install path because the object will never equal 'install'. Consider keeping backward compatibility (accept either the old event shape or a string and normalize), or update/remove the remaining MV2 background component/module usage accordingly.
No description provided.