From 00f41f9a389e9accfcad90fe825d2c8777136a8e Mon Sep 17 00:00:00 2001 From: Nightt <87569709+nightt5879@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:47:42 +0800 Subject: [PATCH 1/2] Expose background load errors in options --- rollup.config.js | 1 + source/background-loader.js | 89 +++++++++++++++++++++++++++++++++++++ source/background.ts | 4 ++ source/manifest.json | 4 +- source/options.css | 7 +++ source/options.html | 9 ++-- source/options.tsx | 36 ++++++++++++++- 7 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 source/background-loader.js diff --git a/rollup.config.js b/rollup.config.js index 5b7898b0d148..12043ae600e5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -119,6 +119,7 @@ const rollup = { targets: [ {src: './source/manifest.json', dest: 'distribution'}, {src: './source/*.+(html|png)', dest: 'distribution/assets'}, + {src: './source/background-loader.js', dest: 'distribution/assets'}, {src: './source/options-preflight.js', dest: 'distribution/assets'}, ], }), diff --git a/source/background-loader.js b/source/background-loader.js new file mode 100644 index 000000000000..ecf7e364b381 --- /dev/null +++ b/source/background-loader.js @@ -0,0 +1,89 @@ +const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors'; +const storage = chrome.storage.session ?? chrome.storage.local; + +async function storageGet(key) { + return new Promise((resolve, reject) => { + storage.get(key, result => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(result); + } + }); + }); +} + +async function storageRemove(key) { + return new Promise((resolve, reject) => { + storage.remove(key, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + }); +} + +async function storageSet(value) { + return new Promise((resolve, reject) => { + storage.set(value, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + }); +} + +function serializeError(error) { + const serialized = { + message: error instanceof Error ? error.message : String(error), + }; + + if (error instanceof Error && error.stack) { + serialized.stack = error.stack; + } + + return serialized; +} + +async function storeBackgroundPageLoadError(error) { + const serialized = serializeError(error); + const storedErrors = await storageGet(backgroundPageLoadErrorsKey); + const errors = storedErrors[backgroundPageLoadErrorsKey] ?? []; + + if (errors.some(storedError => storedError.message === serialized.message && storedError.stack === serialized.stack)) { + return; + } + + await storageSet({ + [backgroundPageLoadErrorsKey]: [ + ...errors, + serialized, + ], + }); +} + +function backgroundPageLoadErrorListener(event) { + storeBackgroundPageLoadError(event.error ?? event.message); +} + +globalThis.addEventListener('error', backgroundPageLoadErrorListener); +Object.defineProperty(globalThis, 'removeBackgroundPageLoadErrorListener', { + configurable: true, + value() { + globalThis.removeEventListener('error', backgroundPageLoadErrorListener); + }, +}); + +await storageRemove(backgroundPageLoadErrorsKey); + +try { + // eslint-disable-next-line import-x/extensions -- The loader is copied to `distribution/assets`, where the built file is `background.js`. + await import('./background.js'); +} catch (error) { + await storeBackgroundPageLoadError(error); + throw error; +} diff --git a/source/background.ts b/source/background.ts index e05c04e7cebd..b0f32ce299ca 100644 --- a/source/background.ts +++ b/source/background.ts @@ -115,3 +115,7 @@ chrome.runtime.onInstalled.addListener(async () => { // Call after the reset above just in case we nuked Safari's base permissions await showWelcomePage(); }); + +(globalThis as typeof globalThis & { + removeBackgroundPageLoadErrorListener?: () => void; +}).removeBackgroundPageLoadErrorListener?.(); diff --git a/source/manifest.json b/source/manifest.json index 614ddbe83c80..f31433a88a6b 100644 --- a/source/manifest.json +++ b/source/manifest.json @@ -37,10 +37,10 @@ }, "options_page": "assets/options.html", "background": { - "service_worker": "assets/background.js", + "service_worker": "assets/background-loader.js", "type": "module", "scripts": [ - "assets/background.js" + "assets/background-loader.js" ] }, "content_scripts": [ diff --git a/source/options.css b/source/options.css index 5c6a0e0d542f..ed9d16e7a708 100644 --- a/source/options.css +++ b/source/options.css @@ -239,3 +239,10 @@ summary:hover { border: 1px solid var(--rgh-red); background: color-mix(in srgb, var(--rgh-red) 10%, transparent); } + +.js-background-fail-error { + overflow: auto; + white-space: pre-wrap; + margin: 1em 0 0; + font-size: 12px; +} diff --git a/source/options.html b/source/options.html index e3d91c6274a8..f5e789557079 100644 --- a/source/options.html +++ b/source/options.html @@ -24,9 +24,12 @@ - +
🔑 Personal token diff --git a/source/options.tsx b/source/options.tsx index 7f524471e366..3ba17ff2f41c 100644 --- a/source/options.tsx +++ b/source/options.tsx @@ -11,6 +11,7 @@ import {messageRuntime} from 'webext-msg'; import {startFeatureIdentification} from './helpers/bisect.js'; import clearCacheHandler from './helpers/clear-cache-handler.js'; +import delay from './helpers/delay.js'; import {doesBrowserActionOpenOptions} from './helpers/feature-utils.js'; import {brokenFeatures, styleHotfixes} from './helpers/hotfix.js'; import isDevelopmentVersion from './helpers/is-development-version.js'; @@ -22,6 +23,23 @@ import initTokenValidation from './options/token-validation.js'; let syncedForm: SyncedForm | undefined; const {version} = chrome.runtime.getManifest(); +const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors'; +const backgroundPageLoadErrorsStorage = chrome.storage.session ?? chrome.storage.local; +const backgroundPageValidationTimeout = 3000; + +type BackgroundPageLoadError = { + message: string; + stack?: string; +}; + +async function getBackgroundPageLoadErrors(): Promise { + const storedErrors: Record = await backgroundPageLoadErrorsStorage.get(backgroundPageLoadErrorsKey); + return storedErrors[backgroundPageLoadErrorsKey] ?? []; +} + +function formatBackgroundPageLoadError({message, stack}: BackgroundPageLoadError): string { + return stack ?? message; +} async function findFeatureHandler(this: HTMLButtonElement): Promise { // TODO: Add support for GHE @@ -105,9 +123,23 @@ async function fetchHotfixes(event: MouseEvent): Promise { } async function validateBackgroundPage(): Promise { - if (await messageRuntime({ping: true}) !== 'pong') { - $('.js-background-fail-banner').hidden = false; + try { + if (await Promise.race([ + messageRuntime({ping: true}), + delay(backgroundPageValidationTimeout).then(() => undefined), + ]) === 'pong') { + return; + } + } catch {} + + const errors = await getBackgroundPageLoadErrors(); + if (errors.length > 0) { + const errorField = $('.js-background-fail-error'); + errorField.textContent = errors.map(error => formatBackgroundPageLoadError(error)).join('\n\n'); + errorField.hidden = false; } + + $('.js-background-fail-banner').hidden = false; } async function generateDom(): Promise { From f793aa90147b7aa0ca1b9359ff330809c451ac47 Mon Sep 17 00:00:00 2001 From: Nightt <87569709+nightt5879@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:00:04 +0800 Subject: [PATCH 2/2] Simplify background load error storage --- source/background-loader.js | 86 ++++--------------------------------- source/background.ts | 4 +- source/options.tsx | 44 ++++--------------- 3 files changed, 17 insertions(+), 117 deletions(-) diff --git a/source/background-loader.js b/source/background-loader.js index ecf7e364b381..890c45a6285b 100644 --- a/source/background-loader.js +++ b/source/background-loader.js @@ -1,89 +1,19 @@ -const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors'; -const storage = chrome.storage.session ?? chrome.storage.local; - -async function storageGet(key) { - return new Promise((resolve, reject) => { - storage.get(key, result => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(result); - } - }); - }); -} - -async function storageRemove(key) { - return new Promise((resolve, reject) => { - storage.remove(key, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); -} - -async function storageSet(value) { - return new Promise((resolve, reject) => { - storage.set(value, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); -} - -function serializeError(error) { - const serialized = { - message: error instanceof Error ? error.message : String(error), - }; - - if (error instanceof Error && error.stack) { - serialized.stack = error.stack; - } - - return serialized; +function storeBackgroundLoadError(error) { + localStorage.backgroundLoadErrors ??= ''; + localStorage.backgroundLoadErrors += `${error?.stack ?? error?.message ?? String(error)}\n\n`; } -async function storeBackgroundPageLoadError(error) { - const serialized = serializeError(error); - const storedErrors = await storageGet(backgroundPageLoadErrorsKey); - const errors = storedErrors[backgroundPageLoadErrorsKey] ?? []; - - if (errors.some(storedError => storedError.message === serialized.message && storedError.stack === serialized.stack)) { - return; - } - - await storageSet({ - [backgroundPageLoadErrorsKey]: [ - ...errors, - serialized, - ], - }); +function backgroundLoadErrorListener(event) { + storeBackgroundLoadError(event.error ?? event.message); } -function backgroundPageLoadErrorListener(event) { - storeBackgroundPageLoadError(event.error ?? event.message); -} - -globalThis.addEventListener('error', backgroundPageLoadErrorListener); -Object.defineProperty(globalThis, 'removeBackgroundPageLoadErrorListener', { - configurable: true, - value() { - globalThis.removeEventListener('error', backgroundPageLoadErrorListener); - }, -}); - -await storageRemove(backgroundPageLoadErrorsKey); +globalThis.addEventListener('error', backgroundLoadErrorListener); try { // eslint-disable-next-line import-x/extensions -- The loader is copied to `distribution/assets`, where the built file is `background.js`. await import('./background.js'); + globalThis.removeEventListener('error', backgroundLoadErrorListener); } catch (error) { - await storeBackgroundPageLoadError(error); + storeBackgroundLoadError(error); throw error; } diff --git a/source/background.ts b/source/background.ts index b0f32ce299ca..25bc462f6b3c 100644 --- a/source/background.ts +++ b/source/background.ts @@ -116,6 +116,4 @@ chrome.runtime.onInstalled.addListener(async () => { await showWelcomePage(); }); -(globalThis as typeof globalThis & { - removeBackgroundPageLoadErrorListener?: () => void; -}).removeBackgroundPageLoadErrorListener?.(); +delete localStorage.backgroundLoadErrors; diff --git a/source/options.tsx b/source/options.tsx index 3ba17ff2f41c..29dd3a7f8629 100644 --- a/source/options.tsx +++ b/source/options.tsx @@ -7,11 +7,8 @@ import {isChrome, isFirefox} from 'webext-detect'; import type {SyncedForm} from 'webext-options-sync-per-domain'; import 'webext-bugs/target-blank'; -import {messageRuntime} from 'webext-msg'; - import {startFeatureIdentification} from './helpers/bisect.js'; import clearCacheHandler from './helpers/clear-cache-handler.js'; -import delay from './helpers/delay.js'; import {doesBrowserActionOpenOptions} from './helpers/feature-utils.js'; import {brokenFeatures, styleHotfixes} from './helpers/hotfix.js'; import isDevelopmentVersion from './helpers/is-development-version.js'; @@ -23,23 +20,6 @@ import initTokenValidation from './options/token-validation.js'; let syncedForm: SyncedForm | undefined; const {version} = chrome.runtime.getManifest(); -const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors'; -const backgroundPageLoadErrorsStorage = chrome.storage.session ?? chrome.storage.local; -const backgroundPageValidationTimeout = 3000; - -type BackgroundPageLoadError = { - message: string; - stack?: string; -}; - -async function getBackgroundPageLoadErrors(): Promise { - const storedErrors: Record = await backgroundPageLoadErrorsStorage.get(backgroundPageLoadErrorsKey); - return storedErrors[backgroundPageLoadErrorsKey] ?? []; -} - -function formatBackgroundPageLoadError({message, stack}: BackgroundPageLoadError): string { - return stack ?? message; -} async function findFeatureHandler(this: HTMLButtonElement): Promise { // TODO: Add support for GHE @@ -122,23 +102,15 @@ async function fetchHotfixes(event: MouseEvent): Promise { } } -async function validateBackgroundPage(): Promise { - try { - if (await Promise.race([ - messageRuntime({ping: true}), - delay(backgroundPageValidationTimeout).then(() => undefined), - ]) === 'pong') { - return; - } - } catch {} - - const errors = await getBackgroundPageLoadErrors(); - if (errors.length > 0) { - const errorField = $('.js-background-fail-error'); - errorField.textContent = errors.map(error => formatBackgroundPageLoadError(error)).join('\n\n'); - errorField.hidden = false; +function validateBackgroundPage(): void { + const backgroundLoadErrors = localStorage.backgroundLoadErrors?.trim(); + if (!backgroundLoadErrors) { + return; } + const errorField = $('.js-background-fail-error'); + errorField.textContent = backgroundLoadErrors; + errorField.hidden = false; $('.js-background-fail-banner').hidden = false; } @@ -170,7 +142,7 @@ async function generateDom(): Promise { // Show stored CSS hotfixes void showStoredCssHotfixes(); - void validateBackgroundPage(); + validateBackgroundPage(); } function addEventListeners(): void {