diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index c34b4a54..85732367 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -1,7 +1,7 @@ import configureStore from './store/backgroundStore'; import openDevToolsWindow, { DevToolsPosition } from './openWindow'; import { createMenu, removeMenu } from './contextMenus'; -import createSyncOptions from '../options/syncOptions'; +import { getOptions } from '../options/syncOptions'; // Expose the extension's store globally to access it from the windows // via chrome.runtime.getBackgroundPage @@ -16,7 +16,7 @@ chrome.commands.onCommand.addListener((shortcut) => { chrome.runtime.onInstalled.addListener(() => { chrome.action.disable(); - createSyncOptions().get((option) => { + getOptions((option) => { if (option.showContextMenus) createMenu(); }); }); diff --git a/extension/src/background/store/apiMiddleware.ts b/extension/src/background/store/apiMiddleware.ts index 12764e6e..930efc4e 100644 --- a/extension/src/background/store/apiMiddleware.ts +++ b/extension/src/background/store/apiMiddleware.ts @@ -11,10 +11,7 @@ import { TOGGLE_PERSIST, UPDATE_STATE, } from '@redux-devtools/app'; -import createSyncOptions, { - Options, - OptionsMessage, -} from '../../options/syncOptions'; +import type { Options, OptionsMessage } from '../../options/syncOptions'; import openDevToolsWindow, { DevToolsPosition } from '../openWindow'; import { getReport } from '../logging'; import { Action, Dispatch, Middleware } from 'redux'; @@ -51,6 +48,11 @@ interface StopAction extends TabMessageBase { readonly id?: never; } +interface OptionsAction { + readonly type: 'OPTIONS'; + readonly options: Options; +} + interface DispatchAction extends TabMessageBase { readonly type: 'DISPATCH'; readonly action: AppDispatchAction; @@ -196,7 +198,7 @@ interface SplitUpdateStateAction> { export type TabMessage = | StartAction | StopAction - | OptionsMessage + | OptionsAction | DispatchAction | ImportAction | ActionAction @@ -414,14 +416,11 @@ function toContentScript(messageBody: ToContentScriptMessage) { } function toAllTabs(msg: TabMessage) { - const tabs = connections.tab; - Object.keys(tabs).forEach((id) => { - tabs[id].postMessage(msg); - }); + for (const tabPort of Object.values(connections.tab)) { + tabPort.postMessage(msg); + } } -const syncOptions = createSyncOptions(toAllTabs); - function monitorInstances(shouldMonitor: boolean, id?: string) { if (!id && isMonitored === shouldMonitor) return; const action = { @@ -463,26 +462,17 @@ interface OpenOptionsMessage { readonly type: 'OPEN_OPTIONS'; } -interface GetOptionsMessage { - readonly type: 'GET_OPTIONS'; -} - -export type SingleMessage = - | OpenMessage - | OpenOptionsMessage - | GetOptionsMessage; +export type SingleMessage = OpenMessage | OpenOptionsMessage | OptionsMessage; type BackgroundStoreMessage> = | PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance | SplitMessage | SingleMessage; -type BackgroundStoreResponse = { readonly options: Options }; // Receive messages from content scripts function messaging>( request: BackgroundStoreMessage, sender: chrome.runtime.MessageSender, - sendResponse?: (response?: BackgroundStoreResponse) => void, ) { let tabId = getId(sender); if (!tabId) return; @@ -498,10 +488,8 @@ function messaging>( chrome.runtime.openOptionsPage(); return; } - if (request.type === 'GET_OPTIONS') { - syncOptions.get((options) => { - sendResponse!({ options }); - }); + if (request.type === 'OPTIONS') { + toAllTabs({ type: 'OPTIONS', options: request.options }); return; } if (request.type === 'GET_REPORT') { diff --git a/extension/src/contentScript/index.ts b/extension/src/contentScript/index.ts index dd0e96ff..9da069b4 100644 --- a/extension/src/contentScript/index.ts +++ b/extension/src/contentScript/index.ts @@ -1,8 +1,10 @@ import '../chromeApiMock'; import { - injectOptions, - getOptionsFromBg, + getOptions, isAllowed, + Options, + prefetchOptions, + prepareOptionsForPage, } from '../options/syncOptions'; import type { TabMessage } from '../background/store/apiMiddleware'; import type { @@ -84,6 +86,13 @@ interface UpdateAction { readonly source: typeof source; } +interface OptionsAction { + readonly type: 'OPTIONS'; + readonly options: Options; + readonly id: undefined; + readonly source: typeof source; +} + export type ContentScriptToPageScriptMessage = | StartAction | StopAction @@ -91,7 +100,8 @@ export type ContentScriptToPageScriptMessage = | ImportAction | ActionAction | ExportAction - | UpdateAction; + | UpdateAction + | OptionsAction; interface ImportStatePayload> { readonly type: 'IMPORT_STATE'; @@ -112,6 +122,7 @@ export type ListenerMessage> = | ActionAction | ExportAction | UpdateAction + | OptionsAction | ImportStateDispatchAction; function postToPageScript(message: ContentScriptToPageScriptMessage) { @@ -156,8 +167,13 @@ function connect() { source, }); } - } else if ('options' in message) { - injectOptions(message.options); + } else if (message.type === 'OPTIONS') { + postToPageScript({ + type: message.type, + options: prepareOptionsForPage(message.options), + id: undefined, + source, + }); } else { postToPageScript({ type: message.type, @@ -289,7 +305,14 @@ function send>( ) { if (!connected) connect(); if (message.type === 'INIT_INSTANCE') { - getOptionsFromBg(); + getOptions((options) => { + postToPageScript({ + type: 'OPTIONS', + options: prepareOptionsForPage(options), + id: undefined, + source, + }); + }); postToBackground({ name: 'INIT_INSTANCE', instanceId: message.instanceId }); } else { postToBackground({ name: 'RELAY', message }); @@ -317,6 +340,8 @@ function handleMessages>( tryCatch(send, message); } +prefetchOptions(); + window.addEventListener('message', handleMessages, false); setInterval(() => { diff --git a/extension/src/options/index.tsx b/extension/src/options/index.tsx index 2e0fd7a4..29f9efb0 100644 --- a/extension/src/options/index.tsx +++ b/extension/src/options/index.tsx @@ -2,22 +2,25 @@ import '../chromeApiMock'; import React from 'react'; import { createRoot } from 'react-dom/client'; import OptionsComponent from './Options'; -import { Options } from './syncOptions'; +import { + getOptions, + Options, + OptionsMessage, + saveOption, + subscribeToOptions, +} from './syncOptions'; -chrome.runtime.getBackgroundPage((background) => { - const syncOptions = background!.syncOptions; - - const saveOption = (name: K, value: Options[K]) => { - syncOptions.save(name, value); - }; - - const renderOptions = (options: Options) => { - const root = createRoot(document.getElementById('root')!); - root.render(); - }; - - syncOptions.subscribe(renderOptions); - syncOptions.get((options) => { - renderOptions(options); - }); +subscribeToOptions((options) => { + const message: OptionsMessage = { type: 'OPTIONS', options }; + chrome.runtime.sendMessage(message); +}); + +const renderOptions = (options: Options) => { + const root = createRoot(document.getElementById('root')!); + root.render(); +}; + +subscribeToOptions(renderOptions); +getOptions((options) => { + renderOptions(options); }); diff --git a/extension/src/options/syncOptions.ts b/extension/src/options/syncOptions.ts index 17324ffc..fe1cae6f 100644 --- a/extension/src/options/syncOptions.ts +++ b/extension/src/options/syncOptions.ts @@ -38,21 +38,22 @@ let options: Options | undefined; let subscribers: ((options: Options) => void)[] = []; export interface OptionsMessage { + readonly type: 'OPTIONS'; readonly options: Options; } -type ToAllTabs = (msg: OptionsMessage) => void; - -const save = - (toAllTabs: ToAllTabs | undefined) => - (key: K, value: Options[K]) => { - let obj: { [K1 in keyof Options]?: Options[K1] } = {}; - obj[key] = value; - chrome.storage.sync.set(obj); - options![key] = value; - toAllTabs!({ options: options! }); - subscribers.forEach((s) => s(options!)); - }; +export const saveOption = ( + key: K, + value: Options[K], +) => { + let obj: { [K1 in keyof Options]?: Options[K1] } = {}; + obj[key] = value; + chrome.storage.sync.set(obj); + options![key] = value; + for (const subscriber of subscribers) { + subscriber(options!); + } +}; const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({ ...oldOptions, @@ -71,7 +72,7 @@ const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({ : oldOptions.filter, }); -const get = (callback: (options: Options) => void) => { +export const getOptions = (callback: (options: Options) => void) => { if (options) callback(options); else { chrome.storage.sync.get( @@ -98,67 +99,29 @@ const get = (callback: (options: Options) => void) => { } }; -const subscribe = (callback: (options: Options) => void) => { +export const prefetchOptions = () => getOptions(() => {}); + +export const subscribeToOptions = (callback: (options: Options) => void) => { subscribers = subscribers.concat(callback); }; const toReg = (str: string) => str !== '' ? str.split('\n').filter(Boolean).join('|') : null; -export const injectOptions = (newOptions: Options) => { - if (!newOptions) return; - - options = { - ...newOptions, - allowlist: - newOptions.filter !== FilterState.DO_NOT_FILTER - ? toReg(newOptions.allowlist)! - : newOptions.allowlist, - denylist: - newOptions.filter !== FilterState.DO_NOT_FILTER - ? toReg(newOptions.denylist)! - : newOptions.denylist, - }; - let s = document.createElement('script'); - s.type = 'text/javascript'; - s.appendChild( - document.createTextNode( - 'window.devToolsOptions = Object.assign(window.devToolsOptions||{},' + - JSON.stringify(options) + - ');', - ), - ); - (document.head || document.documentElement).appendChild(s); - s.parentNode!.removeChild(s); -}; - -export const getOptionsFromBg = () => { - /* chrome.runtime.sendMessage({ type: 'GET_OPTIONS' }, response => { - if (response && response.options) injectOptions(response.options); - }); -*/ - get((newOptions) => { - injectOptions(newOptions); - }); // Legacy -}; +export const prepareOptionsForPage = (options: Options): Options => ({ + ...options, + allowlist: + options.filter !== FilterState.DO_NOT_FILTER + ? toReg(options.allowlist)! + : options.allowlist, + denylist: + options.filter !== FilterState.DO_NOT_FILTER + ? toReg(options.denylist)! + : options.denylist, +}); export const isAllowed = (localOptions = options) => !localOptions || localOptions.inject || !localOptions.urls || location.href.match(toReg(localOptions.urls)!); - -export interface SyncOptions { - readonly save: (key: K, value: Options[K]) => void; - readonly get: (callback: (options: Options) => void) => void; - readonly subscribe: (callback: (options: Options) => void) => void; -} - -export default function createSyncOptions(toAllTabs?: ToAllTabs): SyncOptions { - if (toAllTabs && !options) get(() => {}); // Initialize - return { - save: save(toAllTabs), - get: get, - subscribe: subscribe, - }; -} diff --git a/extension/src/pageScript/index.ts b/extension/src/pageScript/index.ts index 313a09ef..98880391 100644 --- a/extension/src/pageScript/index.ts +++ b/extension/src/pageScript/index.ts @@ -432,6 +432,13 @@ function __REDUX_DEVTOOLS_EXTENSION__>( serializeAction, ); } + return; + case 'OPTIONS': + window.devToolsOptions = Object.assign( + window.devToolsOptions || {}, + message.options, + ); + return; } }