Skip to content

Manifest v3 update#528

Draft
natpicone wants to merge 7 commits intomasterfrom
manifest-upd
Draft

Manifest v3 update#528
natpicone wants to merge 7 commits intomasterfrom
manifest-upd

Conversation

@natpicone
Copy link
Collaborator

No description provided.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.json to MV3 for Chromium and Firefox, and stop copying background.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.

Comment on lines +13 to +15
// Set up angular shim before any service code runs
setupAngularShim();

Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to 15
// Set up angular shim before any service code runs
setupAngularShim();

Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +236
get: (name: string): any => {
const svc = services.get(name);
if (!svc) {
throw new Error(`Service '${name}' not registered in background injector`);
}
return svc;
},
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

$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.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +76
$timeout.cancel = (promise: any): boolean => {
if (promise && promise.$$timeoutId != null) {
clearTimeout(promise.$$timeoutId);
return true;
}
return false;
};
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

$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.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +129
// 11. ApiXbrowsersyncService (depends on $injector, $http, $q, NetworkService, StoreService, UtilityService)
const apiSvc = new ApiXbrowsersyncService(injector, $http, $q, networkSvc, storeSvc, utilitySvc);
injector.register('ApiXbrowsersyncService', apiSvc);
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +58
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();
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +242 to +243
if (alert) {
backgroundSvc.displayAlert(alert);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +55
export const createBackgroundContainer = (config: BackgroundPlatformConfig): BackgroundContainer => {
// Create AngularJS shims
const $q = $qFactory();
const $timeout = $timeoutFactory();
const $interval = $intervalFactory();
const $http = $httpFactory();
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +12 to +50
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);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +97 to +104
// 6. UtilityService (depends on $exceptionHandler, $http, $injector, $location, $q, $rootScope, LogService, NetworkService, StoreService)
const utilitySvc = new UtilityService(
$exceptionHandler,
$http,
injector,
$location,
$q,
$rootScope,
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +201
let messageToDisplay = alert.message;
if (urlInAlert && typeof DOMParser !== 'undefined') {
messageToDisplay =
new DOMParser().parseFromString(`<span>${alert.message}</span>`, 'text/xml').firstElementChild.textContent ??
alert.message;
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +55
// --- $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;
};

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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)) {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
if (currentTitle.indexOf(syncingTitle) > 0 && newTitle.indexOf(syncedTitle)) {
if (currentTitle.includes(syncingTitle) && newTitle.includes(syncedTitle)) {

Copilot uses AI. Check for mistakes.
Comment on lines +331 to 334
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());
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.

2 participants