mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-03-29 14:04:22 +03:00
* Update Chrome manifest.json * Remove use of window in background * Test devpanel * Inject pageScript using new API * Keep connection from devpanel to background alive * Keep connection from content script to background alive * Replace page action with action * Cleanup syncOptions * Update options to not rely on background page access * Start work on updating popup * Updates * Remove window * Get opening in a separate window working * Remove pageScriptWrap * Add socket to panelStore * Fix tests * Try to use MV3 for Firefox * Fix path * Fix Chrome E2E tests * Revert unintentional change * Skip Electron tests for now Looks like they're still working through stuff in https://github.com/electron/electron/issues/41613 * Better image centering The Firefox popup did not like the old CSS. This is still not perfect, but it's better than it was. * Create shaggy-taxis-cross.md
650 lines
18 KiB
TypeScript
650 lines
18 KiB
TypeScript
import {
|
|
ActionCreatorObject,
|
|
evalAction,
|
|
getActionsArray,
|
|
getLocalFilter,
|
|
} from '@redux-devtools/utils';
|
|
import { throttle } from 'lodash-es';
|
|
import { Action, ActionCreator, Dispatch, Reducer, StoreEnhancer } from 'redux';
|
|
import Immutable from 'immutable';
|
|
import {
|
|
EnhancedStore,
|
|
LiftedAction,
|
|
LiftedState,
|
|
PerformAction,
|
|
} from '@redux-devtools/instrument';
|
|
import {
|
|
CustomAction,
|
|
DispatchAction,
|
|
LibConfig,
|
|
Features,
|
|
} from '@redux-devtools/app';
|
|
import configureStore, { getUrlParam } from './enhancerStore';
|
|
import { isAllowed, Options } from '../options/syncOptions';
|
|
import Monitor from './Monitor';
|
|
import {
|
|
noFiltersApplied,
|
|
isFiltered,
|
|
filterState,
|
|
startingFrom,
|
|
} from './api/filters';
|
|
import notifyErrors from './api/notifyErrors';
|
|
import importState from './api/importState';
|
|
import openWindow, { Position } from './api/openWindow';
|
|
import generateId from './api/generateInstanceId';
|
|
import {
|
|
toContentScript,
|
|
sendMessage,
|
|
setListener,
|
|
connect,
|
|
disconnect,
|
|
isInIframe,
|
|
getSerializeParameter,
|
|
Serialize,
|
|
StructuralPerformAction,
|
|
ConnectResponse,
|
|
} from './api';
|
|
import type { ContentScriptToPageScriptMessage } from '../contentScript';
|
|
|
|
type EnhancedStoreWithInitialDispatch<
|
|
S,
|
|
A extends Action<string>,
|
|
MonitorState,
|
|
> = EnhancedStore<S, A, MonitorState> & { initialDispatch: Dispatch<A> };
|
|
|
|
const source = '@devtools-page';
|
|
let stores: {
|
|
[K in string | number]: EnhancedStoreWithInitialDispatch<
|
|
unknown,
|
|
Action<string>,
|
|
unknown
|
|
>;
|
|
} = {};
|
|
let reportId: string | null | undefined;
|
|
|
|
function deprecateParam(oldParam: string, newParam: string) {
|
|
/* eslint-disable no-console */
|
|
console.warn(
|
|
`${oldParam} parameter is deprecated, use ${newParam} instead: https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md`,
|
|
);
|
|
/* eslint-enable no-console */
|
|
}
|
|
|
|
export interface SerializeWithImmutable extends Serialize {
|
|
readonly immutable?: typeof Immutable;
|
|
readonly refs?: (new (data: any) => unknown)[] | null;
|
|
}
|
|
|
|
export interface ConfigWithExpandedMaxAge {
|
|
instanceId?: number;
|
|
/**
|
|
* @deprecated Use actionsDenylist instead.
|
|
*/
|
|
readonly actionsBlacklist?: string | readonly string[];
|
|
/**
|
|
* @deprecated Use actionsAllowlist instead.
|
|
*/
|
|
readonly actionsWhitelist?: string | readonly string[];
|
|
readonly actionsDenylist?: string | readonly string[];
|
|
readonly actionsAllowlist?: string | readonly string[];
|
|
serialize?: boolean | SerializeWithImmutable;
|
|
readonly stateSanitizer?: <S>(state: S, index?: number) => S;
|
|
readonly actionSanitizer?: <A extends Action<string>>(
|
|
action: A,
|
|
id?: number,
|
|
) => A;
|
|
readonly predicate?: <S, A extends Action<string>>(
|
|
state: S,
|
|
action: A,
|
|
) => boolean;
|
|
readonly latency?: number;
|
|
readonly maxAge?:
|
|
| number
|
|
| (<S, A extends Action<string>>(
|
|
currentLiftedAction: LiftedAction<S, A, unknown>,
|
|
previousLiftedState: LiftedState<S, A, unknown> | undefined,
|
|
) => number);
|
|
readonly trace?: boolean | (() => string | undefined);
|
|
readonly traceLimit?: number;
|
|
readonly shouldCatchErrors?: boolean;
|
|
readonly shouldHotReload?: boolean;
|
|
readonly shouldRecordChanges?: boolean;
|
|
readonly shouldStartLocked?: boolean;
|
|
readonly pauseActionType?: unknown;
|
|
name?: string;
|
|
readonly autoPause?: boolean;
|
|
readonly features?: Features;
|
|
readonly type?: string;
|
|
readonly getActionType?: <A extends Action<string>>(action: A) => A;
|
|
readonly actionCreators?: {
|
|
readonly [key: string]: ActionCreator<Action<string>>;
|
|
};
|
|
}
|
|
|
|
export interface Config extends ConfigWithExpandedMaxAge {
|
|
readonly maxAge?: number;
|
|
}
|
|
|
|
interface ReduxDevtoolsExtension {
|
|
(config?: Config): StoreEnhancer;
|
|
open: (position?: Position) => void;
|
|
notifyErrors: (onError?: () => boolean) => void;
|
|
send: <S, A extends Action<string>>(
|
|
action: StructuralPerformAction<A> | StructuralPerformAction<A>[],
|
|
state: LiftedState<S, A, unknown>,
|
|
config: Config,
|
|
instanceId?: number,
|
|
name?: string,
|
|
) => void;
|
|
listen: (
|
|
onMessage: (message: ContentScriptToPageScriptMessage) => void,
|
|
instanceId: number,
|
|
) => void;
|
|
connect: (preConfig: Config) => ConnectResponse;
|
|
disconnect: () => void;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
devToolsOptions: Options;
|
|
}
|
|
}
|
|
|
|
function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
|
|
config?: Config,
|
|
): StoreEnhancer {
|
|
/* eslint-disable no-param-reassign */
|
|
if (typeof config !== 'object') config = {};
|
|
/* eslint-enable no-param-reassign */
|
|
if (!window.devToolsOptions) window.devToolsOptions = {} as any;
|
|
|
|
let store: EnhancedStoreWithInitialDispatch<S, A, unknown>;
|
|
let errorOccurred = false;
|
|
let maxAge: number | undefined;
|
|
let actionCreators: readonly ActionCreatorObject[];
|
|
let sendingActionId = 1;
|
|
const instanceId = generateId(config.instanceId);
|
|
const localFilter = getLocalFilter(config);
|
|
const serializeState = getSerializeParameter(config);
|
|
const serializeAction = getSerializeParameter(config);
|
|
let { stateSanitizer, actionSanitizer, predicate, latency = 500 } = config;
|
|
|
|
// Deprecate actionsWhitelist and actionsBlacklist
|
|
if (config.actionsWhitelist) {
|
|
deprecateParam('actionsWhiteList', 'actionsAllowlist');
|
|
}
|
|
if (config.actionsBlacklist) {
|
|
deprecateParam('actionsBlacklist', 'actionsDenylist');
|
|
}
|
|
|
|
const relayState = throttle(
|
|
(
|
|
liftedState?: LiftedState<S, A, unknown> | undefined,
|
|
libConfig?: LibConfig,
|
|
) => {
|
|
relayAction.cancel();
|
|
const state = liftedState || store.liftedStore.getState();
|
|
sendingActionId = state.nextActionId;
|
|
toContentScript(
|
|
{
|
|
type: 'STATE',
|
|
payload: filterState(
|
|
state,
|
|
localFilter,
|
|
stateSanitizer,
|
|
actionSanitizer,
|
|
predicate,
|
|
),
|
|
source,
|
|
instanceId,
|
|
libConfig,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
},
|
|
latency,
|
|
);
|
|
|
|
const monitor = new Monitor(relayState);
|
|
|
|
function exportState() {
|
|
const liftedState = store.liftedStore.getState();
|
|
const actionsById = liftedState.actionsById;
|
|
const payload: A[] = [];
|
|
liftedState.stagedActionIds.slice(1).forEach((id) => {
|
|
// if (isFiltered(actionsById[id].action, localFilter)) return;
|
|
payload.push(actionsById[id].action);
|
|
});
|
|
toContentScript(
|
|
{
|
|
type: 'EXPORT',
|
|
payload,
|
|
committedState: liftedState.committedState,
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
}
|
|
|
|
const relayAction = throttle(() => {
|
|
const liftedState = store.liftedStore.getState();
|
|
const nextActionId = liftedState.nextActionId;
|
|
const currentActionId = nextActionId - 1;
|
|
const liftedAction = liftedState.actionsById[currentActionId];
|
|
|
|
// Send a single action
|
|
if (sendingActionId === currentActionId) {
|
|
sendingActionId = nextActionId;
|
|
const action = liftedAction.action;
|
|
const computedStates = liftedState.computedStates;
|
|
if (
|
|
isFiltered(action, localFilter) ||
|
|
(predicate &&
|
|
!predicate(computedStates[computedStates.length - 1].state, action))
|
|
) {
|
|
return;
|
|
}
|
|
const state =
|
|
liftedState.computedStates[liftedState.computedStates.length - 1].state;
|
|
toContentScript(
|
|
{
|
|
type: 'ACTION',
|
|
payload: !stateSanitizer
|
|
? state
|
|
: stateSanitizer(state, nextActionId - 1),
|
|
source,
|
|
instanceId,
|
|
action: !actionSanitizer
|
|
? liftedState.actionsById[nextActionId - 1]
|
|
: actionSanitizer(
|
|
liftedState.actionsById[nextActionId - 1].action,
|
|
nextActionId - 1,
|
|
),
|
|
maxAge: getMaxAge(),
|
|
nextActionId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Send multiple actions
|
|
const payload = startingFrom(
|
|
sendingActionId,
|
|
liftedState,
|
|
localFilter,
|
|
stateSanitizer,
|
|
actionSanitizer,
|
|
predicate,
|
|
);
|
|
sendingActionId = nextActionId;
|
|
if (typeof payload === 'undefined') return;
|
|
if ('skippedActionIds' in payload) {
|
|
toContentScript(
|
|
{
|
|
type: 'STATE',
|
|
payload: filterState(
|
|
payload,
|
|
localFilter,
|
|
stateSanitizer,
|
|
actionSanitizer,
|
|
predicate,
|
|
),
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
return;
|
|
}
|
|
toContentScript(
|
|
{
|
|
type: 'PARTIAL_STATE',
|
|
payload,
|
|
source,
|
|
instanceId,
|
|
maxAge: getMaxAge(),
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
}, latency);
|
|
|
|
function dispatchRemotely(action: string | CustomAction) {
|
|
if (config!.features && !config!.features.dispatch) return;
|
|
try {
|
|
const result = evalAction(action, actionCreators);
|
|
(store.initialDispatch || store.dispatch)(result);
|
|
} catch (e) {
|
|
toContentScript(
|
|
{
|
|
type: 'ERROR',
|
|
payload: (e as Error).message,
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
}
|
|
}
|
|
|
|
function importPayloadFrom(state: string | undefined) {
|
|
if (config!.features && !config!.features.import) return;
|
|
try {
|
|
const nextLiftedState = importState<S, A>(state, config!);
|
|
if (!nextLiftedState) return;
|
|
store.liftedStore.dispatch({ type: 'IMPORT_STATE', ...nextLiftedState });
|
|
} catch (e) {
|
|
toContentScript(
|
|
{
|
|
type: 'ERROR',
|
|
payload: (e as Error).message,
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
}
|
|
}
|
|
|
|
function dispatchMonitorAction(action: DispatchAction) {
|
|
const features = config!.features;
|
|
if (features) {
|
|
if (
|
|
!features.jump &&
|
|
(action.type === 'JUMP_TO_STATE' || action.type === 'JUMP_TO_ACTION')
|
|
) {
|
|
return;
|
|
}
|
|
if (!features.skip && action.type === 'TOGGLE_ACTION') return;
|
|
if (!features.reorder && action.type === 'REORDER_ACTION') return;
|
|
if (!features.import && action.type === 'IMPORT_STATE') return;
|
|
if (!features.lock && action.type === 'LOCK_CHANGES') return;
|
|
if (!features.pause && action.type === 'PAUSE_RECORDING') return;
|
|
}
|
|
store.liftedStore.dispatch(action as any);
|
|
}
|
|
|
|
function onMessage(message: ContentScriptToPageScriptMessage) {
|
|
switch (message.type) {
|
|
case 'DISPATCH':
|
|
dispatchMonitorAction(message.payload);
|
|
return;
|
|
case 'ACTION':
|
|
dispatchRemotely(message.payload);
|
|
return;
|
|
case 'IMPORT':
|
|
importPayloadFrom(message.state);
|
|
return;
|
|
case 'EXPORT':
|
|
exportState();
|
|
return;
|
|
case 'UPDATE':
|
|
relayState();
|
|
return;
|
|
case 'START':
|
|
monitor.start(true);
|
|
if (!actionCreators && config!.actionCreators) {
|
|
actionCreators = getActionsArray(config!.actionCreators);
|
|
}
|
|
relayState(undefined, {
|
|
name: config!.name || document.title,
|
|
actionCreators: JSON.stringify(actionCreators),
|
|
features: config!.features,
|
|
serialize: !!config!.serialize,
|
|
type: 'redux',
|
|
});
|
|
|
|
if (reportId) {
|
|
toContentScript(
|
|
{
|
|
type: 'GET_REPORT',
|
|
payload: reportId,
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
reportId = null;
|
|
}
|
|
return;
|
|
case 'STOP':
|
|
monitor.stop();
|
|
relayAction.cancel();
|
|
relayState.cancel();
|
|
if (!message.failed) {
|
|
toContentScript(
|
|
{
|
|
type: 'STOP',
|
|
payload: undefined,
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
}
|
|
return;
|
|
case 'OPTIONS':
|
|
window.devToolsOptions = Object.assign(
|
|
window.devToolsOptions || {},
|
|
message.options,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 ||
|
|
noFiltersApplied(localFilter) ||
|
|
!(liftedAction as PerformAction<A>).action
|
|
) {
|
|
return m;
|
|
}
|
|
if (!maxAge || maxAge < m) maxAge = m; // it can be modified in process on options page
|
|
if (isFiltered((liftedAction as PerformAction<A>).action, localFilter)) {
|
|
// TODO: check also predicate && !predicate(state, action) with current state
|
|
maxAge++;
|
|
} else {
|
|
filteredActionIds.push(liftedState!.nextActionId);
|
|
if (filteredActionIds.length >= m) {
|
|
const stagedActionIds = liftedState!.stagedActionIds;
|
|
let i = 1;
|
|
while (
|
|
maxAge > m &&
|
|
filteredActionIds.indexOf(stagedActionIds[i]) === -1
|
|
) {
|
|
maxAge--;
|
|
i++;
|
|
}
|
|
filteredActionIds.shift();
|
|
}
|
|
}
|
|
return maxAge;
|
|
};
|
|
|
|
function init() {
|
|
setListener(onMessage, instanceId);
|
|
notifyErrors(() => {
|
|
errorOccurred = true;
|
|
const state = store.liftedStore.getState();
|
|
if (state.computedStates[state.currentStateIndex].error) {
|
|
relayState(state);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
toContentScript(
|
|
{
|
|
type: 'INIT_INSTANCE',
|
|
payload: undefined,
|
|
source,
|
|
instanceId,
|
|
},
|
|
serializeState,
|
|
serializeAction,
|
|
);
|
|
store.subscribe(handleChange);
|
|
|
|
if (typeof reportId === 'undefined') {
|
|
reportId = getUrlParam('remotedev_report');
|
|
if (reportId) openWindow();
|
|
}
|
|
}
|
|
|
|
function handleChange() {
|
|
if (!monitor.active) return;
|
|
if (!errorOccurred && !monitor.isMonitorAction()) {
|
|
relayAction();
|
|
return;
|
|
}
|
|
if (monitor.isPaused() || monitor.isLocked() || monitor.isTimeTraveling()) {
|
|
return;
|
|
}
|
|
const liftedState = store.liftedStore.getState();
|
|
if (
|
|
errorOccurred &&
|
|
!liftedState.computedStates[liftedState.currentStateIndex].error
|
|
) {
|
|
errorOccurred = false;
|
|
}
|
|
relayState(liftedState);
|
|
}
|
|
|
|
const enhance = (): StoreEnhancer => (next) => {
|
|
return <S2, A2 extends Action<string>, PreloadedState>(
|
|
reducer_: Reducer<S2, A2, PreloadedState>,
|
|
initialState_?: PreloadedState | undefined,
|
|
) => {
|
|
if (!isAllowed(window.devToolsOptions)) {
|
|
return next(reducer_, initialState_);
|
|
}
|
|
|
|
store = stores[instanceId] = (
|
|
configureStore(next, monitor.reducer, {
|
|
...config,
|
|
maxAge: getMaxAge as any,
|
|
}) as any
|
|
)(reducer_, initialState_) as any;
|
|
|
|
if (isInIframe()) setTimeout(init, 3000);
|
|
else init();
|
|
|
|
return store as any;
|
|
};
|
|
};
|
|
|
|
return enhance();
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__REDUX_DEVTOOLS_EXTENSION__: ReduxDevtoolsExtension;
|
|
}
|
|
}
|
|
|
|
// noinspection JSAnnotator
|
|
window.__REDUX_DEVTOOLS_EXTENSION__ = __REDUX_DEVTOOLS_EXTENSION__ as any;
|
|
window.__REDUX_DEVTOOLS_EXTENSION__.open = openWindow;
|
|
window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors = notifyErrors;
|
|
window.__REDUX_DEVTOOLS_EXTENSION__.send = sendMessage;
|
|
window.__REDUX_DEVTOOLS_EXTENSION__.listen = setListener;
|
|
window.__REDUX_DEVTOOLS_EXTENSION__.connect = connect;
|
|
window.__REDUX_DEVTOOLS_EXTENSION__.disconnect = disconnect;
|
|
|
|
const preEnhancer =
|
|
(instanceId: number): StoreEnhancer =>
|
|
(next) =>
|
|
(reducer, preloadedState) => {
|
|
const store = next(reducer, preloadedState);
|
|
|
|
if (stores[instanceId]) {
|
|
(stores[instanceId].initialDispatch as any) = store.dispatch;
|
|
}
|
|
|
|
return {
|
|
...store,
|
|
dispatch: (...args: any[]) =>
|
|
!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__ &&
|
|
(store.dispatch as any)(...args),
|
|
} as any;
|
|
};
|
|
|
|
export type InferComposedStoreExt<StoreEnhancers> = StoreEnhancers extends [
|
|
infer HeadStoreEnhancer,
|
|
...infer RestStoreEnhancers,
|
|
]
|
|
? HeadStoreEnhancer extends StoreEnhancer<infer StoreExt>
|
|
? StoreExt & InferComposedStoreExt<RestStoreEnhancers>
|
|
: never
|
|
: {};
|
|
|
|
const extensionCompose =
|
|
(config: Config) =>
|
|
<StoreEnhancers extends readonly StoreEnhancer[]>(
|
|
...funcs: StoreEnhancers
|
|
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>> => {
|
|
// @ts-ignore FIXME
|
|
return (...args) => {
|
|
const instanceId = generateId(config.instanceId);
|
|
return [preEnhancer(instanceId), ...funcs].reduceRight(
|
|
// @ts-ignore FIXME
|
|
(composed, f) => f(composed),
|
|
__REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId })(...args),
|
|
);
|
|
};
|
|
};
|
|
|
|
interface ReduxDevtoolsExtensionCompose {
|
|
(
|
|
config: Config,
|
|
): <StoreEnhancers extends readonly StoreEnhancer[]>(
|
|
...funcs: StoreEnhancers
|
|
) => StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
|
|
<StoreEnhancers extends readonly StoreEnhancer[]>(
|
|
...funcs: StoreEnhancers
|
|
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ReduxDevtoolsExtensionCompose;
|
|
}
|
|
}
|
|
|
|
function reduxDevtoolsExtensionCompose(
|
|
config: Config,
|
|
): <StoreEnhancers extends readonly StoreEnhancer[]>(
|
|
...funcs: StoreEnhancers
|
|
) => StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
|
|
function reduxDevtoolsExtensionCompose<
|
|
StoreEnhancers extends readonly StoreEnhancer[],
|
|
>(
|
|
...funcs: StoreEnhancers
|
|
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
|
|
function reduxDevtoolsExtensionCompose(...funcs: [Config] | StoreEnhancer[]) {
|
|
if (funcs.length === 0) {
|
|
return __REDUX_DEVTOOLS_EXTENSION__();
|
|
}
|
|
if (funcs.length === 1 && typeof funcs[0] === 'object') {
|
|
return extensionCompose(funcs[0]);
|
|
}
|
|
return extensionCompose({})(...(funcs as StoreEnhancer[]));
|
|
}
|
|
|
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = reduxDevtoolsExtensionCompose;
|