From a3df1df2eb32e489f8d3580e1cda7431320eb3b6 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Fri, 16 Jul 2021 19:15:26 -0400 Subject: [PATCH] stash --- extension/src/app/api/filters.ts | 38 ++- extension/src/app/api/generateInstanceId.js | 5 - extension/src/app/api/generateInstanceId.ts | 5 + .../api/{importState.js => importState.ts} | 0 extension/src/app/api/{index.js => index.ts} | 63 +++-- .../api/{notifyErrors.js => notifyErrors.ts} | 12 +- .../app/api/{openWindow.js => openWindow.ts} | 4 +- .../src/app/middlewares/{api.js => api.ts} | 105 ++++++-- .../app/service/{Monitor.js => Monitor.ts} | 27 +- extension/src/app/stores/backgroundStore.ts | 2 +- extension/src/app/stores/createStore.js | 5 - extension/src/app/stores/createStore.ts | 15 ++ .../{enhancerStore.js => enhancerStore.ts} | 19 +- .../src/browser/extension/background/index.ts | 13 +- .../background/{logging.js => logging.ts} | 0 .../{chromeAPIMock.js => chromeAPIMock.ts} | 0 .../{contentScript.js => contentScript.ts} | 54 +++- .../inject/{pageScript.js => pageScript.ts} | 253 ++++++++++++------ .../src/browser/extension/options/index.tsx | 2 +- .../browser/extension/options/syncOptions.ts | 14 +- .../src/browser/extension/window/index.tsx | 3 +- 21 files changed, 470 insertions(+), 169 deletions(-) delete mode 100644 extension/src/app/api/generateInstanceId.js create mode 100644 extension/src/app/api/generateInstanceId.ts rename extension/src/app/api/{importState.js => importState.ts} (100%) rename extension/src/app/api/{index.js => index.ts} (85%) rename extension/src/app/api/{notifyErrors.js => notifyErrors.ts} (75%) rename extension/src/app/api/{openWindow.js => openWindow.ts} (51%) rename extension/src/app/middlewares/{api.js => api.ts} (73%) rename extension/src/app/service/{Monitor.js => Monitor.ts} (70%) delete mode 100644 extension/src/app/stores/createStore.js create mode 100644 extension/src/app/stores/createStore.ts rename extension/src/app/stores/{enhancerStore.js => enhancerStore.ts} (64%) rename extension/src/browser/extension/background/{logging.js => logging.ts} (100%) rename extension/src/browser/extension/{chromeAPIMock.js => chromeAPIMock.ts} (100%) rename extension/src/browser/extension/inject/{contentScript.js => contentScript.ts} (71%) rename extension/src/browser/extension/inject/{pageScript.js => pageScript.ts} (67%) diff --git a/extension/src/app/api/filters.ts b/extension/src/app/api/filters.ts index e072437d..4f13a367 100644 --- a/extension/src/app/api/filters.ts +++ b/extension/src/app/api/filters.ts @@ -1,4 +1,7 @@ import mapValues from 'lodash/mapValues'; +import { Config } from '../../browser/extension/inject/pageScript'; +import { Action } from 'redux'; +import { LiftedState, PerformAction } from '@redux-devtools/instrument'; export type FilterStateValue = | 'DO_NOT_FILTER' @@ -11,13 +14,22 @@ export const FilterState: { [K in FilterStateValue]: FilterStateValue } = { WHITELIST_SPECIFIC: 'WHITELIST_SPECIFIC', }; -export function getLocalFilter(config) { +function isArray(arg: unknown): arg is readonly unknown[] { + return Array.isArray(arg); +} + +interface LocalFilter { + readonly whitelist: string | undefined; + readonly blacklist: string | undefined; +} + +export function getLocalFilter(config: Config): LocalFilter | undefined { if (config.actionsBlacklist || config.actionsWhitelist) { return { - whitelist: Array.isArray(config.actionsWhitelist) + whitelist: isArray(config.actionsWhitelist) ? config.actionsWhitelist.join('|') : config.actionsWhitelist, - blacklist: Array.isArray(config.actionsBlacklist) + blacklist: isArray(config.actionsBlacklist) ? config.actionsBlacklist.join('|') : config.actionsBlacklist, }; @@ -125,13 +137,17 @@ export function filterState( }; } -export function startingFrom( - sendingActionId, - state, - localFilter, - stateSanitizer, - actionSanitizer, - predicate +export function startingFrom>( + sendingActionId: number, + state: LiftedState, + localFilter: LocalFilter | undefined, + stateSanitizer: ((state: S, index: number) => S) | undefined, + actionSanitizer: + | (>(action: A, id: number) => A) + | undefined, + predicate: + | (>(state: S, action: A) => boolean) + | undefined ) { const stagedActionIds = state.stagedActionIds; if (sendingActionId <= stagedActionIds[1]) return state; @@ -142,7 +158,7 @@ export function startingFrom( const filteredStagedActionIds = shouldFilter ? [0] : stagedActionIds; const actionsById = state.actionsById; const computedStates = state.computedStates; - const newActionsById = {}; + const newActionsById: { [key: number]: PerformAction } = {}; const newComputedStates = []; let key; let currAction; diff --git a/extension/src/app/api/generateInstanceId.js b/extension/src/app/api/generateInstanceId.js deleted file mode 100644 index 4471d9cb..00000000 --- a/extension/src/app/api/generateInstanceId.js +++ /dev/null @@ -1,5 +0,0 @@ -let id = 0; - -export default function generateId(instanceId) { - return instanceId || ++id; -} diff --git a/extension/src/app/api/generateInstanceId.ts b/extension/src/app/api/generateInstanceId.ts new file mode 100644 index 00000000..ae121568 --- /dev/null +++ b/extension/src/app/api/generateInstanceId.ts @@ -0,0 +1,5 @@ +let id = 0; + +export default function generateId(instanceId: number | undefined) { + return instanceId || ++id; +} diff --git a/extension/src/app/api/importState.js b/extension/src/app/api/importState.ts similarity index 100% rename from extension/src/app/api/importState.js rename to extension/src/app/api/importState.ts diff --git a/extension/src/app/api/index.js b/extension/src/app/api/index.ts similarity index 85% rename from extension/src/app/api/index.js rename to extension/src/app/api/index.ts index 5590dbfb..fc98e74c 100644 --- a/extension/src/app/api/index.js +++ b/extension/src/app/api/index.ts @@ -1,22 +1,24 @@ -import jsan from 'jsan'; +import jsan, { Options } from 'jsan'; import throttle from 'lodash/throttle'; -import seralizeImmutable from '@redux-devtools/serialize/lib/immutable/serialize'; +import serializeImmutable from '@redux-devtools/serialize/lib/immutable/serialize'; import { getActionsArray } from '@redux-devtools/utils'; import { getLocalFilter, isFiltered } from './filters'; import importState from './importState'; import generateId from './generateInstanceId'; +import { PageScriptToContentScriptMessage } from '../../browser/extension/inject/contentScript'; +import { Config } from '../../browser/extension/inject/pageScript'; const listeners = {}; export const source = '@devtools-page'; -function windowReplacer(key, value) { - if (value && value.window === value) { +function windowReplacer(key: string, value: unknown) { + if (value && (value as Window).window === value) { return '[WINDOW]'; } return value; } -function tryCatchStringify(obj) { +function tryCatchStringify(obj: unknown) { try { return JSON.stringify(obj); } catch (err) { @@ -25,19 +27,19 @@ function tryCatchStringify(obj) { console.log('Failed to stringify', err); } /* eslint-enable no-console */ - return jsan.stringify(obj, windowReplacer, null, { + return jsan.stringify(obj, windowReplacer, undefined, { circular: '[CIRCULAR]', date: true, }); } } -let stringifyWarned; -function stringify(obj, serialize) { +let stringifyWarned: boolean; +function stringify(obj: unknown, serialize?: Serialize | undefined) { const str = typeof serialize === 'undefined' ? tryCatchStringify(obj) - : jsan.stringify(obj, serialize.replacer, null, serialize.options); + : jsan.stringify(obj, serialize.replacer, undefined, serialize.options); if (!stringifyWarned && str && str.length > 16 * 1024 * 1024) { // 16 MB @@ -52,12 +54,21 @@ function stringify(obj, serialize) { return str; } -export function getSeralizeParameter(config, param) { +export interface Serialize { + readonly replacer?: (key: string, value: unknown) => unknown; + readonly reviver?: (key: string, value: unknown) => unknown; + readonly options?: Options | boolean; +} + +export function getSerializeParameter( + config: Config, + param?: 'serializeState' | 'serializeAction' +) { const serialize = config.serialize; if (serialize) { if (serialize === true) return { options: true }; if (serialize.immutable) { - const immutableSerializer = seralizeImmutable( + const immutableSerializer = serializeImmutable( serialize.immutable, serialize.refs, serialize.replacer, @@ -82,23 +93,23 @@ export function getSeralizeParameter(config, param) { }; } - const value = config[param]; + const value = config[param!]; if (typeof value === 'undefined') return undefined; // eslint-disable-next-line no-console console.warn( `\`${param}\` parameter for Redux DevTools Extension is deprecated. Use \`serialize\` parameter instead: https://github.com/zalmoxisus/redux-devtools-extension/releases/tag/v2.12.1` ); - if (typeof serializeState === 'boolean') return { options: value }; - if (typeof serializeState === 'function') return { replacer: value }; + if (typeof value === 'boolean') return { options: value }; + if (typeof value === 'function') return { replacer: value }; return value; } -function post(message) { +function post(message: PageScriptToContentScriptMessage) { window.postMessage(message, '*'); } -function getStackTrace(config, toExcludeFromTrace) { +function getStackTrace(config, toExcludeFromTrace: Function | undefined) { if (!config.trace) return undefined; if (typeof config.trace === 'function') return config.trace(); @@ -123,7 +134,7 @@ function getStackTrace(config, toExcludeFromTrace) { typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit ) { - const frames = stack.split('\n'); + const frames = stack!.split('\n'); if (frames.length > traceLimit) { stack = frames .slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0)) @@ -133,7 +144,11 @@ function getStackTrace(config, toExcludeFromTrace) { return stack; } -function amendActionType(action, config, toExcludeFromTrace) { +function amendActionType( + action, + config, + toExcludeFromTrace: Function | undefined +) { let timestamp = Date.now(); let stack = getStackTrace(config, toExcludeFromTrace); if (typeof action === 'string') { @@ -144,7 +159,11 @@ function amendActionType(action, config, toExcludeFromTrace) { return { action, timestamp, stack }; } -export function toContentScript(message, serializeState, serializeAction) { +export function toContentScript( + message, + serializeState: Serialize | undefined, + serializeAction: Serialize | undefined +) { if (message.type === 'ACTION') { message.action = stringify(message.action, serializeAction); message.payload = stringify(message.payload, serializeState); @@ -235,7 +254,7 @@ export function connect(preConfig) { config.name = document.title && id === 1 ? document.title : `Instance ${id}`; } - if (config.serialize) config.serialize = getSeralizeParameter(config); + if (config.serialize) config.serialize = getSerializeParameter(config); const actionCreators = config.actionCreators || {}; const latency = config.latency; const predicate = config.predicate; @@ -245,7 +264,7 @@ export function connect(preConfig) { let delayedActions = []; let delayedStates = []; - const rootListiner = (action) => { + const rootListener = (action) => { if (autoPause) { if (action.type === 'START') isPaused = false; else if (action.type === 'STOP') isPaused = true; @@ -264,7 +283,7 @@ export function connect(preConfig) { } }; - listeners[id] = [rootListiner]; + listeners[id] = [rootListener]; const subscribe = (listener) => { if (!listener) return undefined; diff --git a/extension/src/app/api/notifyErrors.js b/extension/src/app/api/notifyErrors.ts similarity index 75% rename from extension/src/app/api/notifyErrors.js rename to extension/src/app/api/notifyErrors.ts index 49235a42..8b226945 100644 --- a/extension/src/app/api/notifyErrors.js +++ b/extension/src/app/api/notifyErrors.ts @@ -1,9 +1,9 @@ -let handleError; +let handleError: () => boolean; let lastTime = 0; -function createExpBackoffTimer(step) { +function createExpBackoffTimer(step: number) { let count = 1; - return function (reset) { + return function (reset?: boolean) { // Reset call if (reset) { count = 1; @@ -18,7 +18,7 @@ function createExpBackoffTimer(step) { const nextErrorTimeout = createExpBackoffTimer(5000); -function postError(message) { +function postError(message: string) { if (handleError && !handleError()) return; window.postMessage( { @@ -30,7 +30,7 @@ function postError(message) { ); } -function catchErrors(e) { +function catchErrors(e: ErrorEvent) { if ( (window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors) || e.timeStamp - lastTime < nextErrorTimeout() @@ -42,7 +42,7 @@ function catchErrors(e) { postError(e.message); } -export default function notifyErrors(onError) { +export default function notifyErrors(onError: () => boolean) { handleError = onError; window.addEventListener('error', catchErrors, false); } diff --git a/extension/src/app/api/openWindow.js b/extension/src/app/api/openWindow.ts similarity index 51% rename from extension/src/app/api/openWindow.js rename to extension/src/app/api/openWindow.ts index 58ed5344..c997d104 100644 --- a/extension/src/app/api/openWindow.js +++ b/extension/src/app/api/openWindow.ts @@ -1,4 +1,6 @@ -export default function openWindow(position) { +export type Position = 'left' | 'right' | 'bottom' | 'panel' | 'remote'; + +export default function openWindow(position?: Position) { window.postMessage( { source: '@devtools-page', diff --git a/extension/src/app/middlewares/api.js b/extension/src/app/middlewares/api.ts similarity index 73% rename from extension/src/app/middlewares/api.js rename to extension/src/app/middlewares/api.ts index 785056d4..f861318a 100644 --- a/extension/src/app/middlewares/api.js +++ b/extension/src/app/middlewares/api.ts @@ -5,13 +5,53 @@ import { LIFTED_ACTION, } from '@redux-devtools/app/lib/constants/actionTypes'; import { nonReduxDispatch } from '@redux-devtools/app/lib/utils/monitorActions'; -import syncOptions from '../../browser/extension/options/syncOptions'; +import syncOptions, { + OptionsMessage, + SyncOptions, +} from '../../browser/extension/options/syncOptions'; import openDevToolsWindow from '../../browser/extension/background/openWindow'; import { getReport } from '../../browser/extension/background/logging'; +import { StoreAction } from '@redux-devtools/app/lib/actions'; +import { Dispatch } from 'redux'; + +interface StartAction { + readonly type: 'START'; +} + +interface StopAction { + readonly type: 'STOP'; +} + +interface NAAction { + readonly type: 'NA'; + readonly id: string; +} + +interface UpdateStateAction { + readonly type: typeof UPDATE_STATE; +} + +type TabMessage = StartAction | StopAction | OptionsMessage; +type PanelMessage = NAAction; +type MonitorMessage = UpdateStateAction; + +type TabPort = Omit & { + postMessage: (message: TabMessage) => void; +}; +type PanelPort = Omit & { + postMessage: (message: PanelMessage) => void; +}; +type MonitorPort = Omit & { + postMessage: (message: MonitorMessage) => void; +}; const CONNECTED = 'socket/CONNECTED'; const DISCONNECTED = 'socket/DISCONNECTED'; -const connections = { +const connections: { + readonly tab: { [K in number | string]: TabPort }; + readonly panel: { [K in number | string]: PanelPort }; + readonly monitor: { [K in number | string]: MonitorPort }; +} = { tab: {}, panel: {}, monitor: {}, @@ -20,10 +60,16 @@ const chunks = {}; let monitors = 0; let isMonitored = false; -const getId = (sender, name) => - sender.tab ? sender.tab.id : name || sender.id; +const getId = (sender: chrome.runtime.MessageSender, name?: string) => + sender.tab ? sender.tab.id! : name || sender.id!; -function toMonitors(action, tabId, verbose) { +type MonitorAction = NAAction; + +function toMonitors( + action: MonitorAction, + tabId?: string | number, + verbose?: boolean +) { Object.keys(connections.monitor).forEach((id) => { connections.monitor[id].postMessage( verbose || action.type === 'ERROR' ? action : { type: UPDATE_STATE } @@ -43,16 +89,18 @@ function toContentScript({ message, action, id, instanceId, state }) { }); } -function toAllTabs(msg) { +function toAllTabs(msg: TabMessage) { const tabs = connections.tab; Object.keys(tabs).forEach((id) => { tabs[id].postMessage(msg); }); } -function monitorInstances(shouldMonitor, id) { +function monitorInstances(shouldMonitor: boolean, id?: string) { if (!id && isMonitored === shouldMonitor) return; - const action = { type: shouldMonitor ? 'START' : 'STOP' }; + const action = { + type: shouldMonitor ? ('START' as const) : ('STOP' as const), + }; if (id) { if (connections.tab[id]) connections.tab[id].postMessage(action); } else { @@ -80,8 +128,15 @@ function togglePersist() { } } +type BackgroundStoreMessage = unknown; +type BackgroundStoreResponse = never; + // Receive messages from content scripts -function messaging(request, sender, sendResponse) { +function messaging( + request: BackgroundStoreMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: BackgroundStoreResponse) => void +) { let tabId = getId(sender); if (!tabId) return; if (sender.frameId) tabId = `${tabId}-${sender.frameId}`; @@ -164,7 +219,11 @@ function messaging(request, sender, sendResponse) { } } -function disconnect(type, id, listener) { +function disconnect( + type: 'tab' | 'monitor' | 'panel', + id: number | string, + listener?: (message: any, port: chrome.runtime.Port) => void +) { return function disconnectListener() { const p = connections[type][id]; if (listener && p) p.onMessage.removeListener(listener); @@ -182,17 +241,17 @@ function disconnect(type, id, listener) { }; } -function onConnect(port) { - let id; +function onConnect(port: chrome.runtime.Port) { + let id: number | string; let listener; window.store.dispatch({ type: CONNECTED, port }); if (port.name === 'tab') { - id = getId(port.sender); - if (port.sender.frameId) id = `${id}-${port.sender.frameId}`; + id = getId(port.sender!); + if (port.sender!.frameId) id = `${id}-${port.sender!.frameId}`; connections.tab[id] = port; - listener = (msg) => { + listener = (msg: TabToBackgroundMessage) => { if (msg.name === 'INIT_INSTANCE') { if (typeof id === 'number') { chrome.pageAction.show(id); @@ -218,24 +277,24 @@ function onConnect(port) { return; } if (msg.name === 'RELAY') { - messaging(msg.message, port.sender, id); + messaging(msg.message, port.sender!, id); } }; port.onMessage.addListener(listener); port.onDisconnect.addListener(disconnect('tab', id, listener)); } else if (port.name && port.name.indexOf('monitor') === 0) { - id = getId(port.sender, port.name); + id = getId(port.sender!, port.name); connections.monitor[id] = port; monitorInstances(true); monitors++; port.onDisconnect.addListener(disconnect('monitor', id)); } else { // devpanel - id = port.name || port.sender.frameId; + id = port.name || port.sender!.frameId!; connections.panel[id] = port; monitorInstances(true, port.name); monitors++; - listener = (msg) => { + listener = (msg: StoreAction) => { window.store.dispatch(msg); }; port.onMessage.addListener(listener); @@ -253,10 +312,16 @@ chrome.notifications.onClicked.addListener((id) => { openDevToolsWindow('devtools-right'); }); +declare global { + interface Window { + syncOptions: SyncOptions; + } +} + window.syncOptions = syncOptions(toAllTabs); // Expose to the options page export default function api() { - return (next) => (action) => { + return (next: Dispatch) => (action: StoreAction) => { if (action.type === LIFTED_ACTION) toContentScript(action); else if (action.type === 'TOGGLE_PERSIST') togglePersist(); return next(action); diff --git a/extension/src/app/service/Monitor.js b/extension/src/app/service/Monitor.ts similarity index 70% rename from extension/src/app/service/Monitor.js rename to extension/src/app/service/Monitor.ts index c3036e9f..67bf411f 100644 --- a/extension/src/app/service/Monitor.js +++ b/extension/src/app/service/Monitor.ts @@ -1,5 +1,26 @@ -export default class Monitor { - constructor(update) { +import { Action } from 'redux'; +import { LiftedState } from '@redux-devtools/instrument'; + +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_LOCKED__?: boolean; + } +} + +export default class Monitor> { + update: ( + liftedState?: LiftedState | undefined, + libConfig?: unknown + ) => void; + active?: boolean; + paused?: boolean; + + constructor( + update: ( + liftedState?: LiftedState | undefined, + libConfig?: unknown + ) => void + ) { this.update = update; } reducer = (state = {}, action) => { @@ -15,7 +36,7 @@ export default class Monitor { } return state; }; - start = (skipUpdate) => { + start = (skipUpdate: boolean) => { this.active = true; if (!skipUpdate) this.update(); }; diff --git a/extension/src/app/stores/backgroundStore.ts b/extension/src/app/stores/backgroundStore.ts index 2348804b..62638cb3 100644 --- a/extension/src/app/stores/backgroundStore.ts +++ b/extension/src/app/stores/backgroundStore.ts @@ -3,7 +3,7 @@ import rootReducer, { BackgroundState } from '../reducers/background'; import api from '../middlewares/api'; export default function configureStore( - preloadedState: PreloadedState + preloadedState?: PreloadedState ) { return createStore(rootReducer, preloadedState, applyMiddleware(api)); /* diff --git a/extension/src/app/stores/createStore.js b/extension/src/app/stores/createStore.js deleted file mode 100644 index ecd8492c..00000000 --- a/extension/src/app/stores/createStore.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createStore } from 'redux'; - -export default function configureStore(reducer, initialState, enhance) { - return createStore(reducer, initialState, enhance()); -} diff --git a/extension/src/app/stores/createStore.ts b/extension/src/app/stores/createStore.ts new file mode 100644 index 00000000..f91dd548 --- /dev/null +++ b/extension/src/app/stores/createStore.ts @@ -0,0 +1,15 @@ +import { + Action, + createStore, + PreloadedState, + Reducer, + StoreEnhancer, +} from 'redux'; + +export default function configureStore>( + reducer: Reducer, + initialState: PreloadedState | undefined, + enhance: () => StoreEnhancer +) { + return createStore(reducer, initialState, enhance()); +} diff --git a/extension/src/app/stores/enhancerStore.js b/extension/src/app/stores/enhancerStore.ts similarity index 64% rename from extension/src/app/stores/enhancerStore.js rename to extension/src/app/stores/enhancerStore.ts index fdf2a850..715c5654 100644 --- a/extension/src/app/stores/enhancerStore.js +++ b/extension/src/app/stores/enhancerStore.ts @@ -1,15 +1,26 @@ -import { compose } from 'redux'; -import instrument from '@redux-devtools/instrument'; +import { Action, compose, Reducer, StoreEnhancerStoreCreator } from 'redux'; +import instrument, { + LiftedAction, + LiftedState, +} from '@redux-devtools/instrument'; import persistState from '@redux-devtools/core/lib/persistState'; +import { + Config, + ConfigWithExpandedMaxAge, +} from '../../browser/extension/inject/pageScript'; -export function getUrlParam(key) { +export function getUrlParam(key: string) { const matches = window.location.href.match( new RegExp(`[?&]${key}=([^&#]+)\\b`) ); return matches && matches.length > 0 ? matches[1] : null; } -export default function configureStore(next, monitorReducer, config) { +export default function configureStore( + next: StoreEnhancerStoreCreator, + monitorReducer: Reducer, + config: ConfigWithExpandedMaxAge +) { return compose( instrument(monitorReducer, { maxAge: config.maxAge, diff --git a/extension/src/browser/extension/background/index.ts b/extension/src/browser/extension/background/index.ts index f92a9af5..307e2c4f 100644 --- a/extension/src/browser/extension/background/index.ts +++ b/extension/src/browser/extension/background/index.ts @@ -1,7 +1,16 @@ +import { Store } from 'redux'; +import { StoreAction } from '@redux-devtools/app/lib/actions'; import configureStore from '../../../app/stores/backgroundStore'; -import openDevToolsWindow from './openWindow'; +import openDevToolsWindow, { DevToolsPosition } from './openWindow'; import { createMenu, removeMenu } from './contextMenus'; import syncOptions from '../options/syncOptions'; +import { BackgroundState } from '../../../app/reducers/background'; + +declare global { + interface Window { + store: Store; + } +} // Expose the extension's store globally to access it from the windows // via chrome.runtime.getBackgroundPage @@ -9,7 +18,7 @@ window.store = configureStore(); // Listen for keyboard shortcuts chrome.commands.onCommand.addListener((shortcut) => { - openDevToolsWindow(shortcut); + openDevToolsWindow(shortcut as DevToolsPosition); }); // Create the context menu when installed diff --git a/extension/src/browser/extension/background/logging.js b/extension/src/browser/extension/background/logging.ts similarity index 100% rename from extension/src/browser/extension/background/logging.js rename to extension/src/browser/extension/background/logging.ts diff --git a/extension/src/browser/extension/chromeAPIMock.js b/extension/src/browser/extension/chromeAPIMock.ts similarity index 100% rename from extension/src/browser/extension/chromeAPIMock.js rename to extension/src/browser/extension/chromeAPIMock.ts diff --git a/extension/src/browser/extension/inject/contentScript.js b/extension/src/browser/extension/inject/contentScript.ts similarity index 71% rename from extension/src/browser/extension/inject/contentScript.js rename to extension/src/browser/extension/inject/contentScript.ts index 5f59d5da..f243b6df 100644 --- a/extension/src/browser/extension/inject/contentScript.js +++ b/extension/src/browser/extension/inject/contentScript.ts @@ -8,7 +8,13 @@ const pageSource = '@devtools-page'; // Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts const maxChromeMsgSize = 32 * 1024 * 1024; let connected = false; -let bg; +let bg: chrome.runtime.Port | undefined; + +declare global { + interface Window { + devToolsExtensionID?: string; + } +} function connect() { // Connect to the background script @@ -57,7 +63,10 @@ function handleDisconnect() { bg = undefined; } -function tryCatch(fn, args) { +function tryCatch( + fn: (args: PageScriptToContentScriptMessage) => void, + args: PageScriptToContentScriptMessage +) { try { return fn(args); } catch (err) { @@ -100,18 +109,51 @@ function tryCatch(fn, args) { } } -function send(message) { +interface InitInstancePageScriptToContentScriptMessage { + readonly type: 'INIT_INSTANCE'; + readonly instanceId: number; + readonly source: typeof pageSource; +} + +interface DisconnectMessage { + readonly type: 'DISCONNECT'; + readonly source: typeof pageSource; +} + +export type PageScriptToContentScriptMessage = + | InitInstancePageScriptToContentScriptMessage + | DisconnectMessage; + +interface InitInstanceContentScriptToBackgroundMessage { + readonly name: 'INIT_INSTANCE'; + readonly instanceId: number; +} + +interface RelayMessage { + readonly name: 'RELAY'; + readonly message: unknown; +} + +export type ContentScriptToBackgroundMessage = + | InitInstanceContentScriptToBackgroundMessage + | RelayMessage; + +function postToBackground(message: ContentScriptToBackgroundMessage) { + bg!.postMessage(message); +} + +function send(message: never) { if (!connected) connect(); if (message.type === 'INIT_INSTANCE') { getOptionsFromBg(); - bg.postMessage({ name: 'INIT_INSTANCE', instanceId: message.instanceId }); + postToBackground({ name: 'INIT_INSTANCE', instanceId: message.instanceId }); } else { - bg.postMessage({ name: 'RELAY', message }); + postToBackground({ name: 'RELAY', message }); } } // Resend messages from the page to the background script -function handleMessages(event) { +function handleMessages(event: MessageEvent) { if (!isAllowed()) return; if (!event || event.source !== window || typeof event.data !== 'object') { return; diff --git a/extension/src/browser/extension/inject/pageScript.js b/extension/src/browser/extension/inject/pageScript.ts similarity index 67% rename from extension/src/browser/extension/inject/pageScript.js rename to extension/src/browser/extension/inject/pageScript.ts index a2b6d5a1..a3828748 100644 --- a/extension/src/browser/extension/inject/pageScript.js +++ b/extension/src/browser/extension/inject/pageScript.ts @@ -1,8 +1,10 @@ import { getActionsArray, evalAction } from '@redux-devtools/utils'; import throttle from 'lodash/throttle'; +import { Action, PreloadedState, Reducer, Store, StoreEnhancer } from 'redux'; +import Immutable from 'immutable'; import createStore from '../../../app/stores/createStore'; import configureStore, { getUrlParam } from '../../../app/stores/enhancerStore'; -import { isAllowed } from '../options/syncOptions'; +import { isAllowed, Options } from '../options/syncOptions'; import Monitor from '../../../app/service/Monitor'; import { noFiltersApplied, @@ -13,7 +15,7 @@ import { } from '../../../app/api/filters'; import notifyErrors from '../../../app/api/notifyErrors'; import importState from '../../../app/api/importState'; -import openWindow from '../../../app/api/openWindow'; +import openWindow, { Position } from '../../../app/api/openWindow'; import generateId from '../../../app/api/generateInstanceId'; import { updateStore, @@ -23,14 +25,21 @@ import { connect, disconnect, isInIframe, - getSeralizeParameter, + getSerializeParameter, + Serialize, } from '../../../app/api'; +import { + InstrumentExt, + LiftedAction, + LiftedState, + PerformAction, +} from '@redux-devtools/instrument'; const source = '@devtools-page'; -let stores = {}; -let reportId; +let stores: { [instanceId: number]: Store> } = {}; +let reportId: string | null | undefined; -function deprecateParam(oldParam, newParam) { +function deprecateParam(oldParam: string, newParam: string) { /* eslint-disable no-console */ console.warn( `${oldParam} parameter is deprecated, use ${newParam} instead: https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md` @@ -38,10 +47,96 @@ function deprecateParam(oldParam, newParam) { /* eslint-enable no-console */ } -const __REDUX_DEVTOOLS_EXTENSION__ = function ( - reducer, - preloadedState, - config +interface SerializeWithImmutable extends Serialize { + readonly immutable?: typeof Immutable; + readonly refs?: (new (data: any) => unknown)[] | null; +} + +export interface ConfigWithExpandedMaxAge { + readonly instanceId?: number; + readonly actionsBlacklist?: string | readonly string[]; + readonly actionsWhitelist?: string | readonly string[]; + readonly serialize?: boolean | SerializeWithImmutable; + readonly serializeState?: + | boolean + | ((key: string, value: unknown) => unknown) + | Serialize; + readonly serializeAction?: + | boolean + | ((key: string, value: unknown) => unknown) + | Serialize; + readonly statesFilter?: (state: S, index: number) => S; + readonly actionsFilter?: >( + action: A, + id: number + ) => A; + readonly stateSanitizer?: (state: S, index: number) => S; + readonly actionSanitizer?: >( + action: A, + id: number + ) => A; + readonly predicate?: >( + state: S, + action: A + ) => boolean; + readonly latency?: number; + readonly getMonitor?: >( + monitor: Monitor + ) => void; + readonly maxAge?: + | number + | (>( + currentLiftedAction: LiftedAction, + previousLiftedState: LiftedState | undefined + ) => number); + readonly trace?: + | boolean + | (>(action: A) => string | undefined); + readonly traceLimit?: number; + readonly shouldCatchErrors?: boolean; + readonly shouldHotReload?: boolean; + readonly shouldRecordChanges?: boolean; + readonly shouldStartLocked?: boolean; + readonly pauseActionType?: unknown; + readonly deserializeState?: (state: S) => S; + readonly deserializeAction?: >(action: A) => A; + readonly name?: string; +} + +export interface Config extends ConfigWithExpandedMaxAge { + readonly maxAge?: number; +} + +interface ReduxDevtoolsExtension { + >( + reducer: Reducer, + preloadedState?: PreloadedState, + config?: Config + ): Store; + (config: Config): StoreEnhancer; + open: (position?: Position) => void; + notifyErrors: (onError: () => boolean) => void; + disconnect: () => void; +} + +declare global { + interface Window { + devToolsOptions: Options; + } +} + +const __REDUX_DEVTOOLS_EXTENSION__ = reduxDevtoolsExtension; + +function reduxDevtoolsExtension>( + reducer?: Reducer, + preloadedState?: PreloadedState, + config?: Config +): Store; +function reduxDevtoolsExtension(config: Config): StoreEnhancer; +function reduxDevtoolsExtension>( + reducer?: Reducer | Config | undefined, + preloadedState?: PreloadedState, + config?: Config ) { /* eslint-disable no-param-reassign */ if (typeof reducer === 'object') { @@ -51,15 +146,15 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( /* eslint-enable no-param-reassign */ if (!window.devToolsOptions) window.devToolsOptions = {}; - let store; + let store: Store & InstrumentExt; let errorOccurred = false; - let maxAge; + let maxAge: number | undefined; let actionCreators; let sendingActionId = 1; const instanceId = generateId(config.instanceId); const localFilter = getLocalFilter(config); - const serializeState = getSeralizeParameter(config, 'serializeState'); - const serializeAction = getSeralizeParameter(config, 'serializeAction'); + const serializeState = getSerializeParameter(config, 'serializeState'); + const serializeAction = getSerializeParameter(config, 'serializeAction'); let { statesFilter, actionsFilter, @@ -79,6 +174,19 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( actionSanitizer = actionsFilter; // eslint-disable-line no-param-reassign } + const relayState = throttle( + ( + liftedState?: LiftedState | undefined, + libConfig?: unknown + ) => { + relayAction.cancel(); + const state = liftedState || store.liftedStore.getState(); + sendingActionId = state.nextActionId; + relay('STATE', state, undefined, undefined, libConfig); + }, + latency + ); + const monitor = new Monitor(relayState); if (config.getMonitor) { /* eslint-disable no-console */ @@ -95,7 +203,7 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( function exportState() { const liftedState = store.liftedStore.getState(); const actionsById = liftedState.actionsById; - const payload = []; + const payload: A[] = []; liftedState.stagedActionIds.slice(1).forEach((id) => { // if (isFiltered(actionsById[id].action, localFilter)) return; payload.push(actionsById[id].action); @@ -113,7 +221,30 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( ); } - function relay(type, state, action, nextActionId, libConfig) { + function relay( + type: 'ACTION', + state: S, + action: PerformAction, + nextActionId: number + ): void; + function relay( + type: 'STATE', + state: LiftedState, + action?: undefined, + nextActionId?: undefined, + libConfig?: unknown + ): void; + function relay(type: 'ERROR', message: unknown): void; + function relay(type: 'INIT_INSTANCE'): void; + function relay(type: 'GET_REPORT', reportId: string): void; + function relay(type: 'STOP'): void; + function relay( + type: string, + state?: S | LiftedState | unknown, + action?: PerformAction | undefined, + nextActionId?: number | undefined, + libConfig?: unknown + ) { const message = { type, payload: filterState( @@ -142,13 +273,6 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( toContentScript(message, serializeState, serializeAction); } - const relayState = throttle((liftedState, libConfig) => { - relayAction.cancel(); - const state = liftedState || store.liftedStore.getState(); - sendingActionId = state.nextActionId; - relay('STATE', state, undefined, undefined, libConfig); - }, latency); - const relayAction = throttle(() => { const liftedState = store.liftedStore.getState(); const nextActionId = liftedState.nextActionId; @@ -296,8 +420,11 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( } } - const filteredActionIds = []; // simple circular buffer of non-excluded actions with fixed maxAge-1 length - const getMaxAge = (liftedAction, liftedState) => { + const filteredActionIds: number[] = []; // simple circular buffer of non-excluded actions with fixed maxAge-1 length + const getMaxAge = ( + liftedAction?: LiftedAction, + liftedState?: LiftedState | undefined + ) => { let m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50; if ( !liftedAction || @@ -311,9 +438,9 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( // TODO: check also predicate && !predicate(state, action) with current state maxAge++; } else { - filteredActionIds.push(liftedState.nextActionId); + filteredActionIds.push(liftedState!.nextActionId); if (filteredActionIds.length >= m) { - const stagedActionIds = liftedState.stagedActionIds; + const stagedActionIds = liftedState!.stagedActionIds; let i = 1; while ( maxAge > m && @@ -367,16 +494,16 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( relayState(liftedState); } - const enhance = () => (next) => { - return (reducer_, initialState_, enhancer_) => { + const enhance = (): StoreEnhancer => (next) => { + return (reducer_, initialState_) => { if (!isAllowed(window.devToolsOptions)) { - return next(reducer_, initialState_, enhancer_); + return next(reducer_, initialState_); } store = stores[instanceId] = configureStore(next, monitor.reducer, { ...config, maxAge: getMaxAge, - })(reducer_, initialState_, enhancer_); + })(reducer_, initialState_); if (isInIframe()) setTimeout(init, 3000); else init(); @@ -392,10 +519,16 @@ const __REDUX_DEVTOOLS_EXTENSION__ = function ( ); /* eslint-enable no-console */ return createStore(reducer, preloadedState, enhance); -}; +} + +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION__: ReduxDevtoolsExtension; + } +} // noinspection JSAnnotator -window.__REDUX_DEVTOOLS_EXTENSION__ = __REDUX_DEVTOOLS_EXTENSION__; +window.__REDUX_DEVTOOLS_EXTENSION__ = __REDUX_DEVTOOLS_EXTENSION__ as any; window.__REDUX_DEVTOOLS_EXTENSION__.open = openWindow; window.__REDUX_DEVTOOLS_EXTENSION__.updateStore = updateStore(stores); window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors = notifyErrors; @@ -404,50 +537,6 @@ window.__REDUX_DEVTOOLS_EXTENSION__.listen = setListener; window.__REDUX_DEVTOOLS_EXTENSION__.connect = connect; window.__REDUX_DEVTOOLS_EXTENSION__.disconnect = disconnect; -// Deprecated -/* eslint-disable no-console */ -let varNameDeprecatedWarned; -const varNameDeprecatedWarn = () => { - if (varNameDeprecatedWarned) return; - console.warn( - '`window.devToolsExtension` is deprecated in favor of `window.__REDUX_DEVTOOLS_EXTENSION__`, and will be removed in next version of Redux DevTools: https://git.io/fpEJZ' - ); - varNameDeprecatedWarned = true; -}; -/* eslint-enable no-console */ -window.devToolsExtension = (...args) => { - varNameDeprecatedWarn(); - return __REDUX_DEVTOOLS_EXTENSION__.apply(null, args); -}; -window.devToolsExtension.open = (...args) => { - varNameDeprecatedWarn(); - return openWindow.apply(null, args); -}; -window.devToolsExtension.updateStore = (...args) => { - varNameDeprecatedWarn(); - return updateStore(stores).apply(null, args); -}; -window.devToolsExtension.notifyErrors = (...args) => { - varNameDeprecatedWarn(); - return notifyErrors.apply(null, args); -}; -window.devToolsExtension.send = (...args) => { - varNameDeprecatedWarn(); - return sendMessage.apply(null, args); -}; -window.devToolsExtension.listen = (...args) => { - varNameDeprecatedWarn(); - return setListener.apply(null, args); -}; -window.devToolsExtension.connect = (...args) => { - varNameDeprecatedWarn(); - return connect.apply(null, args); -}; -window.devToolsExtension.disconnect = (...args) => { - varNameDeprecatedWarn(); - return disconnect.apply(null, args); -}; - const preEnhancer = (instanceId) => (next) => (reducer, preloadedState, enhancer) => { const store = next(reducer, preloadedState, enhancer); @@ -464,8 +553,8 @@ const preEnhancer = }; const extensionCompose = - (config) => - (...funcs) => { + (config: Config) => + (...funcs: StoreEnhancer[]) => { return (...args) => { const instanceId = generateId(config.instanceId); return [preEnhancer(instanceId), ...funcs].reduceRight( @@ -475,6 +564,12 @@ const extensionCompose = }; }; +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: unknown; + } +} + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = (...funcs) => { if (funcs.length === 0) { return __REDUX_DEVTOOLS_EXTENSION__(); diff --git a/extension/src/browser/extension/options/index.tsx b/extension/src/browser/extension/options/index.tsx index 7753aaaa..55805985 100644 --- a/extension/src/browser/extension/options/index.tsx +++ b/extension/src/browser/extension/options/index.tsx @@ -6,7 +6,7 @@ import { Options } from './syncOptions'; import '../../views/options.pug'; chrome.runtime.getBackgroundPage((background) => { - const syncOptions = background.syncOptions; + const syncOptions = background!.syncOptions; const saveOption = (name: K, value: Options[K]) => { syncOptions.save(name, value); diff --git a/extension/src/browser/extension/options/syncOptions.ts b/extension/src/browser/extension/options/syncOptions.ts index 051095a5..2164fee1 100644 --- a/extension/src/browser/extension/options/syncOptions.ts +++ b/extension/src/browser/extension/options/syncOptions.ts @@ -31,7 +31,11 @@ interface OldOrNewOptions { let options: Options | undefined; let subscribers: ((options: Options) => void)[] = []; -type ToAllTabs = (msg: { readonly options: Options }) => void; +export interface OptionsMessage { + readonly options: Options; +} + +type ToAllTabs = (msg: OptionsMessage) => void; const save = (toAllTabs: ToAllTabs | undefined) => @@ -132,7 +136,13 @@ export const isAllowed = (localOptions = options) => !localOptions.urls || location.href.match(toReg(localOptions.urls)!); -export default function syncOptions(toAllTabs?: ToAllTabs) { +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 syncOptions(toAllTabs?: ToAllTabs): SyncOptions { if (toAllTabs && !options) get(() => {}); // Initialize return { save: save(toAllTabs), diff --git a/extension/src/browser/extension/window/index.tsx b/extension/src/browser/extension/window/index.tsx index 14aaedea..2af7f6ca 100644 --- a/extension/src/browser/extension/window/index.tsx +++ b/extension/src/browser/extension/window/index.tsx @@ -16,7 +16,8 @@ getPreloadedState(position, (state) => { preloadedState = state; }); -chrome.runtime.getBackgroundPage(({ store }) => { +chrome.runtime.getBackgroundPage((window) => { + const { store } = window!; const localStore = configureStore(store, position, preloadedState); let name = 'monitor'; if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {