This commit is contained in:
Nathan Bierema 2021-07-16 19:15:26 -04:00
parent 7b4698225b
commit a3df1df2eb
21 changed files with 470 additions and 169 deletions

View File

@ -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<S, A extends Action<unknown>>(
sendingActionId: number,
state: LiftedState<S, A, unknown>,
localFilter: LocalFilter | undefined,
stateSanitizer: (<S>(state: S, index: number) => S) | undefined,
actionSanitizer:
| (<A extends Action<unknown>>(action: A, id: number) => A)
| undefined,
predicate:
| (<S, A extends Action<unknown>>(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<A> } = {};
const newComputedStates = [];
let key;
let currAction;

View File

@ -1,5 +0,0 @@
let id = 0;
export default function generateId(instanceId) {
return instanceId || ++id;
}

View File

@ -0,0 +1,5 @@
let id = 0;
export default function generateId(instanceId: number | undefined) {
return instanceId || ++id;
}

View File

@ -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;

View File

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

View File

@ -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',

View File

@ -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<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: TabMessage) => void;
};
type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: PanelMessage) => void;
};
type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & {
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<StoreAction>) => (action: StoreAction) => {
if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === 'TOGGLE_PERSIST') togglePersist();
return next(action);

View File

@ -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<S, A extends Action<unknown>> {
update: (
liftedState?: LiftedState<S, A, unknown> | undefined,
libConfig?: unknown
) => void;
active?: boolean;
paused?: boolean;
constructor(
update: (
liftedState?: LiftedState<S, A, unknown> | 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();
};

View File

@ -3,7 +3,7 @@ import rootReducer, { BackgroundState } from '../reducers/background';
import api from '../middlewares/api';
export default function configureStore(
preloadedState: PreloadedState<BackgroundState>
preloadedState?: PreloadedState<BackgroundState>
) {
return createStore(rootReducer, preloadedState, applyMiddleware(api));
/*

View File

@ -1,5 +0,0 @@
import { createStore } from 'redux';
export default function configureStore(reducer, initialState, enhance) {
return createStore(reducer, initialState, enhance());
}

View File

@ -0,0 +1,15 @@
import {
Action,
createStore,
PreloadedState,
Reducer,
StoreEnhancer,
} from 'redux';
export default function configureStore<S, A extends Action<unknown>>(
reducer: Reducer<S, A>,
initialState: PreloadedState<S> | undefined,
enhance: () => StoreEnhancer
) {
return createStore(reducer, initialState, enhance());
}

View File

@ -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,

View File

@ -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<BackgroundState, StoreAction>;
}
}
// 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

View File

@ -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<A>(
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<PageScriptToContentScriptMessage>) {
if (!isAllowed()) return;
if (!event || event.source !== window || typeof event.data !== 'object') {
return;

View File

@ -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<unknown, Action<unknown>> } = {};
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?: <S>(state: S, index: number) => S;
readonly actionsFilter?: <A extends Action<unknown>>(
action: A,
id: number
) => A;
readonly stateSanitizer?: <S>(state: S, index: number) => S;
readonly actionSanitizer?: <A extends Action<unknown>>(
action: A,
id: number
) => A;
readonly predicate?: <S, A extends Action<unknown>>(
state: S,
action: A
) => boolean;
readonly latency?: number;
readonly getMonitor?: <S, A extends Action<unknown>>(
monitor: Monitor<S, A>
) => void;
readonly maxAge?:
| number
| (<S, A extends Action<unknown>>(
currentLiftedAction: LiftedAction<S, A, unknown>,
previousLiftedState: LiftedState<S, A, unknown> | undefined
) => number);
readonly trace?:
| boolean
| (<A extends Action<unknown>>(action: A) => string | undefined);
readonly traceLimit?: number;
readonly shouldCatchErrors?: boolean;
readonly shouldHotReload?: boolean;
readonly shouldRecordChanges?: boolean;
readonly shouldStartLocked?: boolean;
readonly pauseActionType?: unknown;
readonly deserializeState?: <S>(state: S) => S;
readonly deserializeAction?: <A extends Action<unknown>>(action: A) => A;
readonly name?: string;
}
export interface Config extends ConfigWithExpandedMaxAge {
readonly maxAge?: number;
}
interface ReduxDevtoolsExtension {
<S, A extends Action<unknown>>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
config?: Config
): Store<S, A>;
(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<S, A extends Action<unknown>>(
reducer?: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
config?: Config
): Store<S, A>;
function reduxDevtoolsExtension(config: Config): StoreEnhancer;
function reduxDevtoolsExtension<S, A extends Action<unknown>>(
reducer?: Reducer<S, A> | Config | undefined,
preloadedState?: PreloadedState<S>,
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<S, A> & InstrumentExt<S, A, unknown>;
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<S, A, unknown> | 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<A>,
nextActionId: number
): void;
function relay(
type: 'STATE',
state: LiftedState<S, A, unknown>,
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<S, A, unknown> | unknown,
action?: PerformAction<A> | 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<S, A, unknown>,
liftedState?: LiftedState<S, A, unknown> | 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__();

View File

@ -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 = <K extends keyof Options>(name: K, value: Options[K]) => {
syncOptions.save(name, value);

View File

@ -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: <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 syncOptions(toAllTabs?: ToAllTabs): SyncOptions {
if (toAllTabs && !options) get(() => {}); // Initialize
return {
save: save(toAllTabs),

View File

@ -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) {