Update options to not rely on background page access

This commit is contained in:
Nathan Bierema 2024-08-11 13:53:38 -04:00
parent a7f6dbc0a2
commit 4b43b513d3
6 changed files with 101 additions and 115 deletions

View File

@ -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();
});
});

View File

@ -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<S, A extends Action<string>> {
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<S, A extends Action<string>> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage
| SingleMessage;
type BackgroundStoreResponse = { readonly options: Options };
// Receive messages from content scripts
function messaging<S, A extends Action<string>>(
request: BackgroundStoreMessage<S, A>,
sender: chrome.runtime.MessageSender,
sendResponse?: (response?: BackgroundStoreResponse) => void,
) {
let tabId = getId(sender);
if (!tabId) return;
@ -498,10 +488,8 @@ function messaging<S, A extends Action<string>>(
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') {

View File

@ -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<S, A extends Action<string>> {
readonly type: 'IMPORT_STATE';
@ -112,6 +122,7 @@ export type ListenerMessage<S, A extends Action<string>> =
| ActionAction
| ExportAction
| UpdateAction
| OptionsAction
| ImportStateDispatchAction<S, A>;
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<S, A extends Action<string>>(
) {
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<S, A extends Action<string>>(
tryCatch(send, message);
}
prefetchOptions();
window.addEventListener('message', handleMessages, false);
setInterval(() => {

View File

@ -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 = <K extends keyof Options>(name: K, value: Options[K]) => {
syncOptions.save(name, value);
};
const renderOptions = (options: Options) => {
const root = createRoot(document.getElementById('root')!);
root.render(<OptionsComponent options={options} saveOption={saveOption} />);
};
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(<OptionsComponent options={options} saveOption={saveOption} />);
};
subscribeToOptions(renderOptions);
getOptions((options) => {
renderOptions(options);
});

View File

@ -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) =>
<K extends keyof Options>(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 = <K extends keyof Options>(
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: <K extends keyof Options>(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,
};
}

View File

@ -432,6 +432,13 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
serializeAction,
);
}
return;
case 'OPTIONS':
window.devToolsOptions = Object.assign(
window.devToolsOptions || {},
message.options,
);
return;
}
}