diff --git a/extension/package.json b/extension/package.json index 926e0281..94fa0b81 100644 --- a/extension/package.json +++ b/extension/package.json @@ -28,7 +28,8 @@ "test:app": "cross-env BABEL_ENV=test jest test/app", "test:chrome": "jest test/chrome", "test:electron": "jest test/electron", - "test": "npm run test:app && npm run build:extension && npm run test:chrome && npm run test:electron" + "test": "npm run test:app && npm run build:extension && npm run test:chrome && npm run test:electron", + "type-check": "tsc --noEmit" }, "dependencies": { "@redux-devtools/app": "^1.0.0-8", diff --git a/extension/src/app/api/index.ts b/extension/src/app/api/index.ts index db8ee46b..d23120ad 100644 --- a/extension/src/app/api/index.ts +++ b/extension/src/app/api/index.ts @@ -357,6 +357,7 @@ export interface ErrorMessage { readonly payload: string; readonly source: typeof source; readonly instanceId: number; + readonly message?: string | undefined; } interface InitInstanceMessage { @@ -449,16 +450,34 @@ export function sendMessage>( config = {}; // eslint-disable-line no-param-reassign if (action) amendedAction = amendActionType(action, config, sendMessage); } - const message = { - type: action ? 'ACTION' : 'STATE', - action: amendedAction, - payload: state, - maxAge: config.maxAge, - source, - name: config.name || name, - instanceId: config.instanceId || instanceId || 1, - }; - toContentScript(message, config.serialize, config.serialize); + if (action) { + toContentScript( + { + type: 'ACTION', + action: amendedAction, + payload: state, + maxAge: config.maxAge, + source, + name: config.name || name, + instanceId: config.instanceId || instanceId || 1, + }, + config.serialize, + config.serialize + ); + } + toContentScript( + { + type: 'STATE', + action: amendedAction, + payload: state, + maxAge: config.maxAge, + source, + name: config.name || name, + instanceId: config.instanceId || instanceId || 1, + }, + config.serialize, + config.serialize + ); } function handleMessages(event: MessageEvent) { @@ -609,7 +628,11 @@ export function connect(preConfig: Config) { return; } } - sendMessage(amendedAction, amendedState, config); + sendMessage( + amendedAction as StructuralPerformAction, + amendedState, + config + ); }; const init = >( diff --git a/extension/src/app/containers/App.tsx b/extension/src/app/containers/App.tsx index c030f400..85d13db0 100644 --- a/extension/src/app/containers/App.tsx +++ b/extension/src/app/containers/App.tsx @@ -7,6 +7,8 @@ import Actions from '@redux-devtools/app/lib/containers/Actions'; import Header from '@redux-devtools/app/lib/components/Header'; import { clearNotification } from '@redux-devtools/app/lib/actions'; import { StoreState } from '@redux-devtools/app/lib/reducers'; +import { SingleMessage } from '../middlewares/api'; +import { Position } from '../api/openWindow'; type StateProps = ReturnType; type DispatchProps = ResolveThunks; @@ -15,13 +17,17 @@ interface OwnProps { } type Props = StateProps & DispatchProps & OwnProps; +function sendMessage(message: SingleMessage) { + chrome.runtime.sendMessage(message); +} + class App extends Component { - openWindow = (position: string) => { - chrome.runtime.sendMessage({ type: 'OPEN', position }); + openWindow = (position: Position) => { + sendMessage({ type: 'OPEN', position }); }; openOptionsPage = () => { if (navigator.userAgent.indexOf('Firefox') !== -1) { - chrome.runtime.sendMessage({ type: 'OPEN_OPTIONS' }); + sendMessage({ type: 'OPEN_OPTIONS' }); } else { chrome.runtime.openOptionsPage(); } diff --git a/extension/src/app/middlewares/api.ts b/extension/src/app/middlewares/api.ts index b09557c6..4896c0da 100644 --- a/extension/src/app/middlewares/api.ts +++ b/extension/src/app/middlewares/api.ts @@ -10,7 +10,9 @@ import syncOptions, { OptionsMessage, SyncOptions, } from '../../browser/extension/options/syncOptions'; -import openDevToolsWindow from '../../browser/extension/background/openWindow'; +import openDevToolsWindow, { + DevToolsPosition, +} from '../../browser/extension/background/openWindow'; import { getReport } from '../../browser/extension/background/logging'; import { CustomAction, @@ -32,6 +34,7 @@ import { BackgroundAction, LiftedActionAction, } from '../stores/backgroundStore'; +import { Position } from '../api/openWindow'; interface TabMessageBase { readonly type: string; @@ -87,7 +90,7 @@ interface NAAction { interface InitMessage> { readonly type: 'INIT'; readonly payload: string; - readonly instanceId: string; + instanceId: string; readonly source: '@devtools-page'; action?: string; name?: string | undefined; @@ -98,7 +101,7 @@ interface InitMessage> { interface LiftedMessage { readonly type: 'LIFTED'; readonly liftedState: { readonly isPaused: boolean | undefined }; - readonly instanceId: string; + instanceId: number; readonly source: '@devtools-page'; } @@ -112,7 +115,7 @@ interface SerializedPartialStateMessage { readonly type: 'PARTIAL_STATE'; readonly payload: SerializedPartialLiftedState; readonly source: '@devtools-page'; - readonly instanceId: string; + instanceId: number; readonly maxAge: number; readonly actionsById: string; readonly computedStates: string; @@ -124,14 +127,14 @@ interface SerializedExportMessage { readonly payload: string; readonly committedState: string | undefined; readonly source: '@devtools-page'; - readonly instanceId: string; + instanceId: number; } interface SerializedActionMessage { readonly type: 'ACTION'; readonly payload: string; readonly source: '@devtools-page'; - readonly instanceId: string; + instanceId: number; readonly action: string; readonly maxAge: number; readonly nextActionId: number; @@ -144,7 +147,7 @@ interface SerializedStateMessage> { 'actionsById' | 'computedStates' | 'committedState' >; readonly source: '@devtools-page'; - readonly instanceId: string; + instanceId: string; readonly libConfig?: LibConfig; readonly actionsById: string; readonly computedStates: string; @@ -165,7 +168,7 @@ interface EmptyUpdateStateAction { interface UpdateStateAction> { readonly type: typeof UPDATE_STATE; - readonly request: UpdateStateRequest; + request: UpdateStateRequest; readonly id: string | number; } @@ -195,8 +198,8 @@ type MonitorPort = Omit & { postMessage: (message: MonitorMessage) => void; }; -const CONNECTED = 'socket/CONNECTED'; -const DISCONNECTED = 'socket/DISCONNECTED'; +export const CONNECTED = 'socket/CONNECTED'; +export const DISCONNECTED = 'socket/DISCONNECTED'; const connections: { readonly tab: { [K in number | string]: TabPort }; readonly panel: { [K in number | string]: PanelPort }; @@ -248,19 +251,78 @@ interface ImportMessage { type ToContentScriptMessage = ImportMessage | LiftedActionAction; -function toContentScript({ - message, - action, - id, - instanceId, - state, -}: ToContentScriptMessage) { - connections.tab[id!].postMessage({ - type: message, - action, - state: nonReduxDispatch(window.store, message, instanceId, action, state), - id: instanceId.toString().replace(/^[^\/]+\//, ''), - }); +function toContentScript(messageBody: ToContentScriptMessage) { + if (messageBody.message === 'DISPATCH') { + const { message, action, id, instanceId, state } = messageBody; + connections.tab[id!].postMessage({ + type: message, + action, + state: nonReduxDispatch( + window.store, + message, + instanceId, + action as AppDispatchAction, + state + ), + id: instanceId.toString().replace(/^[^\/]+\//, ''), + }); + } else if (messageBody.message === 'IMPORT') { + const { message, action, id, instanceId, state } = messageBody; + connections.tab[id!].postMessage({ + type: message, + action, + state: nonReduxDispatch( + window.store, + message, + instanceId, + action as unknown as AppDispatchAction, + state + ), + id: instanceId.toString().replace(/^[^\/]+\//, ''), + }); + } else if (messageBody.message === 'ACTION') { + const { message, action, id, instanceId, state } = messageBody; + connections.tab[id!].postMessage({ + type: message, + action, + state: nonReduxDispatch( + window.store, + message, + instanceId, + action as unknown as AppDispatchAction, + state + ), + id: instanceId.toString().replace(/^[^\/]+\//, ''), + }); + } else if (messageBody.message === 'EXPORT') { + const { message, action, id, instanceId, state } = messageBody; + connections.tab[id!].postMessage({ + type: message, + action, + state: nonReduxDispatch( + window.store, + message, + instanceId, + action as unknown as AppDispatchAction, + state + ), + id: instanceId.toString().replace(/^[^\/]+\//, ''), + }); + } else { + const { message, action, id, instanceId, state } = messageBody; + connections.tab[id!].postMessage({ + type: message, + action, + state: nonReduxDispatch( + window.store, + message, + instanceId, + action as AppDispatchAction, + state + ), + id: (instanceId as number).toString().replace(/^[^\/]+\//, ''), + }); + } } function toAllTabs(msg: TabMessage) { @@ -302,9 +364,28 @@ function togglePersist() { } } +interface OpenMessage { + readonly type: 'OPEN'; + readonly position: Position; +} + +interface OpenOptionsMessage { + readonly type: 'OPEN_OPTIONS'; +} + +interface GetOptionsMessage { + readonly type: 'GET_OPTIONS'; +} + +export type SingleMessage = + | OpenMessage + | OpenOptionsMessage + | GetOptionsMessage; + type BackgroundStoreMessage> = | PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance - | SplitMessage; + | SplitMessage + | SingleMessage; type BackgroundStoreResponse = { readonly options: Options }; // Receive messages from content scripts @@ -338,13 +419,13 @@ function messaging>( return; } if (request.type === 'OPEN') { - let position = 'devtools-left'; + let position: DevToolsPosition = 'devtools-left'; if ( ['remote', 'panel', 'left', 'right', 'bottom'].indexOf( request.position ) !== -1 ) { - position = 'devtools-' + request.position; + position = ('devtools-' + request.position) as DevToolsPosition; } openDevToolsWindow(position); return; @@ -372,19 +453,20 @@ function messaging>( type: UPDATE_STATE, request, id: tabId, - }; + } as UpdateStateAction; const instanceId = `${tabId}/${request.instanceId}`; if ('split' in request) { if (request.split === 'start') { - chunks[instanceId] = request; + chunks[instanceId] = request as any; return; } if (request.split === 'chunk') { - chunks[instanceId][request.chunk[0]] = - (chunks[instanceId][request.chunk[0]] || '') + request.chunk[1]; + (chunks[instanceId] as any)[request.chunk[0]] = + ((chunks[instanceId] as any)[request.chunk[0]] || '') + + request.chunk[1]; return; } - action.request = chunks[instanceId]; + action.request = chunks[instanceId] as any; delete chunks[instanceId]; } if (request.instanceId) { diff --git a/extension/src/app/stores/backgroundStore.ts b/extension/src/app/stores/backgroundStore.ts index 5b5ec8e4..ace3e77e 100644 --- a/extension/src/app/stores/backgroundStore.ts +++ b/extension/src/app/stores/backgroundStore.ts @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, PreloadedState } from 'redux'; import rootReducer, { BackgroundState } from '../reducers/background'; -import api from '../middlewares/api'; +import api, { CONNECTED, DISCONNECTED } from '../middlewares/api'; import { LIFTED_ACTION } from '@redux-devtools/app/lib/constants/actionTypes'; import { CustomAction, @@ -26,6 +26,7 @@ interface LiftedActionImportAction extends LiftedActionActionBase { message: 'IMPORT'; state: string; preloadedState?: unknown | undefined; + action?: never; } interface LiftedActionActionAction extends LiftedActionActionBase { type: typeof LIFTED_ACTION; @@ -36,6 +37,7 @@ interface LiftedActionExportAction extends LiftedActionActionBase { type: typeof LIFTED_ACTION; message: 'EXPORT'; toExport: boolean; + action?: never; } export type LiftedActionAction = | LiftedActionDispatchAction @@ -49,10 +51,20 @@ interface TogglePersistAction { readonly id: string | number | undefined; } +interface ConnectedAction { + readonly type: typeof CONNECTED; +} + +interface DisconnectedAction { + readonly type: typeof DISCONNECTED; +} + export type BackgroundAction = | StoreActionWithoutLiftedAction | LiftedActionAction - | TogglePersistAction; + | TogglePersistAction + | ConnectedAction + | DisconnectedAction; export default function configureStore( preloadedState?: PreloadedState diff --git a/extension/src/browser/extension/chromeAPIMock.ts b/extension/src/browser/extension/chromeAPIMock.ts index 1081f37a..0ee79b89 100644 --- a/extension/src/browser/extension/chromeAPIMock.ts +++ b/extension/src/browser/extension/chromeAPIMock.ts @@ -1,48 +1,48 @@ // Mock not supported chrome.* API for Firefox and Electron -window.isElectron = +(window as any).isElectron = window.navigator && window.navigator.userAgent.indexOf('Electron') !== -1; const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; // Background page only if ( - (window.isElectron && + ((window as any).isElectron && location.pathname === '/_generated_background_page.html') || isFirefox ) { - chrome.runtime.onConnectExternal = { + (chrome.runtime as any).onConnectExternal = { addListener() {}, }; - chrome.runtime.onMessageExternal = { + (chrome.runtime as any).onMessageExternal = { addListener() {}, }; - if (window.isElectron) { - chrome.notifications = { + if ((window as any).isElectron) { + (chrome.notifications as any) = { onClicked: { addListener() {}, }, create() {}, clear() {}, }; - chrome.contextMenus = { + (chrome.contextMenus as any) = { onClicked: { addListener() {}, }, }; } else { - chrome.storage.sync = chrome.storage.local; - chrome.runtime.onInstalled = { - addListener: (cb) => cb(), + (chrome.storage as any).sync = chrome.storage.local; + (chrome.runtime as any).onInstalled = { + addListener: (cb: any) => cb(), }; } } -if (window.isElectron) { +if ((window as any).isElectron) { if (!chrome.storage.local || !chrome.storage.local.remove) { - chrome.storage.local = { - set(obj, callback) { + (chrome.storage as any).local = { + set(obj: any, callback: any) { Object.keys(obj).forEach((key) => { localStorage.setItem(key, obj[key]); }); @@ -50,8 +50,8 @@ if (window.isElectron) { callback(); } }, - get(obj, callback) { - const result = {}; + get(obj: any, callback: any) { + const result: any = {}; Object.keys(obj).forEach((key) => { result[key] = localStorage.getItem(key) || obj[key]; }); @@ -60,7 +60,7 @@ if (window.isElectron) { } }, // Electron ~ 1.4.6 - remove(items, callback) { + remove(items: any, callback: any) { if (Array.isArray(items)) { items.forEach((name) => { localStorage.removeItem(name); @@ -75,7 +75,7 @@ if (window.isElectron) { }; } // Avoid error: chrome.runtime.sendMessage is not supported responseCallback - const originSendMessage = chrome.runtime.sendMessage; + const originSendMessage = (chrome.runtime as any).sendMessage; chrome.runtime.sendMessage = function () { if (process.env.NODE_ENV === 'development') { return originSendMessage(...arguments); @@ -87,6 +87,6 @@ if (window.isElectron) { }; } -if (isFirefox || window.isElectron) { - chrome.storage.sync = chrome.storage.local; +if (isFirefox || (window as any).isElectron) { + (chrome.storage as any).sync = chrome.storage.local; } diff --git a/extension/src/browser/extension/inject/index.ts b/extension/src/browser/extension/inject/index.ts index 9537c081..dfb369b7 100644 --- a/extension/src/browser/extension/inject/index.ts +++ b/extension/src/browser/extension/inject/index.ts @@ -1,6 +1,8 @@ // Include this script in Chrome apps and extensions for remote debugging // +import { Options } from '../options/syncOptions'; + window.devToolsExtensionID = 'lmhkpmbekcpmknklioeibfkpmmfibljd'; require('./contentScript'); require('./pageScript'); @@ -8,7 +10,7 @@ require('./pageScript'); chrome.runtime.sendMessage( window.devToolsExtensionID, { type: 'GET_OPTIONS' }, - function (response) { + function (response: { readonly options: Options }) { if (!response.options.inject) { const urls = response.options.urls.split('\n').filter(Boolean).join('|'); if (!location.href.match(new RegExp(urls))) return; diff --git a/packages/redux-devtools-app/src/utils/monitorActions.ts b/packages/redux-devtools-app/src/utils/monitorActions.ts index c45dbd58..063e3fa6 100644 --- a/packages/redux-devtools-app/src/utils/monitorActions.ts +++ b/packages/redux-devtools-app/src/utils/monitorActions.ts @@ -5,7 +5,6 @@ import { SET_STATE } from '../constants/actionTypes'; import { InstancesState, State } from '../reducers/instances'; import { Dispatch, MiddlewareAPI } from 'redux'; import { DispatchAction, StoreAction } from '../actions'; -import { StoreState } from '../reducers'; export function sweep(state: State): State { return {