chore(extension): convert to TypeScript (#756)

* Start work

* More work

* stash

* stash

* stash

* Eliminate relay

* Fix

* Define page script to content script messages

* Define ContentScriptToPageScriptMessage

* Not required

* Fill out more types

* More work on types

* More type fixes

* Add type

* More improvements to types

* More work on types

* Fix more type errors

* More changes

* More work

* Work

* Fix build

* Fix lint

* Fix more lint

* Fix bug

* Fix tests
This commit is contained in:
Nathan Bierema 2021-08-25 04:22:54 +00:00 committed by GitHub
parent 326cfdf217
commit a418284a4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 3369 additions and 1849 deletions

View File

@ -1,5 +1,9 @@
{ {
"presets": ["@babel/preset-env", "@babel/preset-react"], "presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [ "plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }] ["@babel/plugin-proposal-class-properties", { "loose": true }]

View File

@ -28,7 +28,8 @@
"test:app": "cross-env BABEL_ENV=test jest test/app", "test:app": "cross-env BABEL_ENV=test jest test/app",
"test:chrome": "jest test/chrome", "test:chrome": "jest test/chrome",
"test:electron": "jest test/electron", "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": { "dependencies": {
"@redux-devtools/app": "^1.0.0-8", "@redux-devtools/app": "^1.0.0-8",
@ -37,6 +38,7 @@
"@redux-devtools/serialize": "^0.3.0", "@redux-devtools/serialize": "^0.3.0",
"@redux-devtools/slider-monitor": "^2.0.0-8", "@redux-devtools/slider-monitor": "^2.0.0-8",
"@redux-devtools/utils": "^1.0.0-6", "@redux-devtools/utils": "^1.0.0-6",
"@types/jsan": "^3.1.2",
"jsan": "^3.1.13", "jsan": "^3.1.13",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^16.14.0", "react": "^16.14.0",

View File

@ -1,18 +1,35 @@
import mapValues from 'lodash/mapValues'; import mapValues from 'lodash/mapValues';
import { Config } from '../../browser/extension/inject/pageScript';
import { Action } from 'redux';
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
export const FilterState = { export type FilterStateValue =
| 'DO_NOT_FILTER'
| 'BLACKLIST_SPECIFIC'
| 'WHITELIST_SPECIFIC';
export const FilterState: { [K in FilterStateValue]: FilterStateValue } = {
DO_NOT_FILTER: 'DO_NOT_FILTER', DO_NOT_FILTER: 'DO_NOT_FILTER',
BLACKLIST_SPECIFIC: 'BLACKLIST_SPECIFIC', BLACKLIST_SPECIFIC: 'BLACKLIST_SPECIFIC',
WHITELIST_SPECIFIC: 'WHITELIST_SPECIFIC', 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) { if (config.actionsBlacklist || config.actionsWhitelist) {
return { return {
whitelist: Array.isArray(config.actionsWhitelist) whitelist: isArray(config.actionsWhitelist)
? config.actionsWhitelist.join('|') ? config.actionsWhitelist.join('|')
: config.actionsWhitelist, : config.actionsWhitelist,
blacklist: Array.isArray(config.actionsBlacklist) blacklist: isArray(config.actionsBlacklist)
? config.actionsBlacklist.join('|') ? config.actionsBlacklist.join('|')
: config.actionsBlacklist, : config.actionsBlacklist,
}; };
@ -20,38 +37,48 @@ export function getLocalFilter(config) {
return undefined; return undefined;
} }
export const noFiltersApplied = (localFilter) => export const noFiltersApplied = (localFilter: LocalFilter | undefined) =>
// !predicate && // !predicate &&
!localFilter && !localFilter &&
(!window.devToolsOptions || (!window.devToolsOptions ||
!window.devToolsOptions.filter || !window.devToolsOptions.filter ||
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER); window.devToolsOptions.filter === FilterState.DO_NOT_FILTER);
export function isFiltered(action, localFilter) { export function isFiltered<A extends Action<unknown>>(
action: A | string,
localFilter: LocalFilter | undefined
) {
if ( if (
noFiltersApplied(localFilter) || noFiltersApplied(localFilter) ||
(typeof action !== 'string' && typeof action.type.match !== 'function') (typeof action !== 'string' &&
typeof (action.type as string).match !== 'function')
) { ) {
return false; return false;
} }
const { whitelist, blacklist } = localFilter || window.devToolsOptions || {}; const { whitelist, blacklist } = localFilter || window.devToolsOptions || {};
const actionType = action.type || action; const actionType = ((action as A).type || action) as string;
return ( return (
(whitelist && !actionType.match(whitelist)) || (whitelist && !actionType.match(whitelist)) ||
(blacklist && actionType.match(blacklist)) (blacklist && actionType.match(blacklist))
); );
} }
function filterActions(actionsById, actionSanitizer) { function filterActions<A extends Action<unknown>>(
actionsById: { [p: number]: PerformAction<A> },
actionSanitizer: ((action: A, id: number) => A) | undefined
): { [p: number]: PerformAction<A> } {
if (!actionSanitizer) return actionsById; if (!actionSanitizer) return actionsById;
return mapValues(actionsById, (action, id) => ({ return mapValues(actionsById, (action, id) => ({
...action, ...action,
action: actionSanitizer(action.action, id), action: actionSanitizer(action.action, id as unknown as number),
})); }));
} }
function filterStates(computedStates, stateSanitizer) { function filterStates<S>(
computedStates: { state: S; error?: string | undefined }[],
stateSanitizer: ((state: S, index: number) => S) | undefined
) {
if (!stateSanitizer) return computedStates; if (!stateSanitizer) return computedStates;
return computedStates.map((state, idx) => ({ return computedStates.map((state, idx) => ({
...state, ...state,
@ -59,23 +86,19 @@ function filterStates(computedStates, stateSanitizer) {
})); }));
} }
export function filterState( export function filterState<S, A extends Action<unknown>>(
state, state: LiftedState<S, A, unknown>,
type, localFilter: LocalFilter | undefined,
localFilter, stateSanitizer: ((state: S, index: number) => S) | undefined,
stateSanitizer, actionSanitizer: ((action: A, id: number) => A) | undefined,
actionSanitizer, predicate: ((state: S, action: A) => boolean) | undefined
nextActionId, ): LiftedState<S, A, unknown> {
predicate
) {
if (type === 'ACTION') {
return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1);
} else if (type !== 'STATE') return state;
if (predicate || !noFiltersApplied(localFilter)) { if (predicate || !noFiltersApplied(localFilter)) {
const filteredStagedActionIds = []; const filteredStagedActionIds: number[] = [];
const filteredComputedStates = []; const filteredComputedStates: { state: S; error?: string | undefined }[] =
const sanitizedActionsById = actionSanitizer && {}; [];
const sanitizedActionsById: { [p: number]: PerformAction<A> } | undefined =
actionSanitizer && {};
const { actionsById } = state; const { actionsById } = state;
const { computedStates } = state; const { computedStates } = state;
@ -97,7 +120,7 @@ export function filterState(
: liftedState : liftedState
); );
if (actionSanitizer) { if (actionSanitizer) {
sanitizedActionsById[id] = { sanitizedActionsById![id] = {
...liftedAction, ...liftedAction,
action: actionSanitizer(currAction, id), action: actionSanitizer(currAction, id),
}; };
@ -120,14 +143,27 @@ export function filterState(
}; };
} }
export function startingFrom( export interface PartialLiftedState<S, A extends Action<unknown>> {
sendingActionId, readonly actionsById: { [actionId: number]: PerformAction<A> };
state, readonly computedStates: { state: S; error?: string }[];
localFilter, readonly stagedActionIds: readonly number[];
stateSanitizer, readonly currentStateIndex: number;
actionSanitizer, readonly nextActionId: number;
predicate readonly committedState?: S;
) { }
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
): LiftedState<S, A, unknown> | PartialLiftedState<S, A> | undefined {
const stagedActionIds = state.stagedActionIds; const stagedActionIds = state.stagedActionIds;
if (sendingActionId <= stagedActionIds[1]) return state; if (sendingActionId <= stagedActionIds[1]) return state;
const index = stagedActionIds.indexOf(sendingActionId); const index = stagedActionIds.indexOf(sendingActionId);
@ -137,7 +173,7 @@ export function startingFrom(
const filteredStagedActionIds = shouldFilter ? [0] : stagedActionIds; const filteredStagedActionIds = shouldFilter ? [0] : stagedActionIds;
const actionsById = state.actionsById; const actionsById = state.actionsById;
const computedStates = state.computedStates; const computedStates = state.computedStates;
const newActionsById = {}; const newActionsById: { [key: number]: PerformAction<A> } = {};
const newComputedStates = []; const newComputedStates = [];
let key; let key;
let currAction; 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,74 +0,0 @@
import mapValues from 'lodash/mapValues';
import jsan from 'jsan';
import seralizeImmutable from '@redux-devtools/serialize/lib/immutable/serialize';
function deprecate(param) {
// 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`
);
}
export default function importState(
state,
{ deserializeState, deserializeAction, serialize }
) {
if (!state) return undefined;
let parse = jsan.parse;
if (serialize) {
if (serialize.immutable) {
parse = (v) =>
jsan.parse(
v,
seralizeImmutable(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
).reviver
);
} else if (serialize.reviver) {
parse = (v) => jsan.parse(v, serialize.reviver);
}
}
let preloadedState;
let nextLiftedState = parse(state);
if (nextLiftedState.payload) {
if (nextLiftedState.preloadedState) {
preloadedState = parse(nextLiftedState.preloadedState);
}
nextLiftedState = parse(nextLiftedState.payload);
}
if (deserializeState) {
deprecate('deserializeState');
if (typeof nextLiftedState.computedStates !== 'undefined') {
nextLiftedState.computedStates = nextLiftedState.computedStates.map(
(computedState) => ({
...computedState,
state: deserializeState(computedState.state),
})
);
}
if (typeof nextLiftedState.committedState !== 'undefined') {
nextLiftedState.committedState = deserializeState(
nextLiftedState.committedState
);
}
if (typeof preloadedState !== 'undefined') {
preloadedState = deserializeState(preloadedState);
}
}
if (deserializeAction) {
deprecate('deserializeAction');
nextLiftedState.actionsById = mapValues(
nextLiftedState.actionsById,
(liftedAction) => ({
...liftedAction,
action: deserializeAction(liftedAction.action),
})
);
}
return { nextLiftedState, preloadedState };
}

View File

@ -0,0 +1,116 @@
import mapValues from 'lodash/mapValues';
import jsan from 'jsan';
import seralizeImmutable from '@redux-devtools/serialize/lib/immutable/serialize';
import {
Config,
SerializeWithImmutable,
} from '../../browser/extension/inject/pageScript';
import Immutable from 'immutable';
import { LiftedState } from '@redux-devtools/instrument';
import { Action } from 'redux';
function deprecate(param: string) {
// 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`
);
}
interface SerializeWithRequiredImmutable extends SerializeWithImmutable {
readonly immutable: typeof Immutable;
}
function isSerializeWithImmutable(
serialize: boolean | SerializeWithImmutable
): serialize is SerializeWithRequiredImmutable {
return !!(serialize as SerializeWithImmutable).immutable;
}
interface SerializeWithRequiredReviver extends SerializeWithImmutable {
readonly reviver: (key: string, value: unknown) => unknown;
}
function isSerializeWithReviver(
serialize: boolean | SerializeWithImmutable
): serialize is SerializeWithRequiredReviver {
return !!(serialize as SerializeWithImmutable).immutable;
}
interface ParsedSerializedLiftedState {
readonly payload: string;
readonly preloadedState?: string;
}
export default function importState<S, A extends Action<unknown>>(
state: string | undefined,
{ deserializeState, deserializeAction, serialize }: Config
) {
if (!state) return undefined;
let parse = jsan.parse;
if (serialize) {
if (isSerializeWithImmutable(serialize)) {
parse = (v) =>
jsan.parse(
v,
seralizeImmutable(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
).reviver
);
} else if (isSerializeWithReviver(serialize)) {
parse = (v) => jsan.parse(v, serialize.reviver);
}
}
const parsedSerializedLiftedState:
| ParsedSerializedLiftedState
| LiftedState<S, A, unknown> = parse(state) as
| ParsedSerializedLiftedState
| LiftedState<S, A, unknown>;
let preloadedState =
'payload' in parsedSerializedLiftedState &&
parsedSerializedLiftedState.preloadedState
? (parse(parsedSerializedLiftedState.preloadedState) as S)
: undefined;
const nextLiftedState =
'payload' in parsedSerializedLiftedState
? (parse(parsedSerializedLiftedState.payload) as LiftedState<
S,
A,
unknown
>)
: parsedSerializedLiftedState;
if (deserializeState) {
deprecate('deserializeState');
if (typeof nextLiftedState.computedStates !== 'undefined') {
nextLiftedState.computedStates = nextLiftedState.computedStates.map(
(computedState) => ({
...computedState,
state: deserializeState(computedState.state),
})
);
}
if (typeof nextLiftedState.committedState !== 'undefined') {
nextLiftedState.committedState = deserializeState(
nextLiftedState.committedState
);
}
if (typeof preloadedState !== 'undefined') {
preloadedState = deserializeState(preloadedState);
}
}
if (deserializeAction) {
deprecate('deserializeAction');
nextLiftedState.actionsById = mapValues(
nextLiftedState.actionsById,
(liftedAction) => ({
...liftedAction,
action: deserializeAction(liftedAction.action),
})
);
}
return { nextLiftedState, preloadedState };
}

View File

@ -1,393 +0,0 @@
import jsan from 'jsan';
import throttle from 'lodash/throttle';
import seralizeImmutable 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';
const listeners = {};
export const source = '@devtools-page';
function windowReplacer(key, value) {
if (value && value.window === value) {
return '[WINDOW]';
}
return value;
}
function tryCatchStringify(obj) {
try {
return JSON.stringify(obj);
} catch (err) {
/* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') {
console.log('Failed to stringify', err);
}
/* eslint-enable no-console */
return jsan.stringify(obj, windowReplacer, null, {
circular: '[CIRCULAR]',
date: true,
});
}
}
let stringifyWarned;
function stringify(obj, serialize) {
const str =
typeof serialize === 'undefined'
? tryCatchStringify(obj)
: jsan.stringify(obj, serialize.replacer, null, serialize.options);
if (!stringifyWarned && str && str.length > 16 * 1024 * 1024) {
// 16 MB
/* eslint-disable no-console */
console.warn(
'Application state or actions payloads are too large making Redux DevTools serialization slow and consuming a lot of memory. See https://git.io/fpcP5 on how to configure it.'
);
/* eslint-enable no-console */
stringifyWarned = true;
}
return str;
}
export function getSeralizeParameter(config, param) {
const serialize = config.serialize;
if (serialize) {
if (serialize === true) return { options: true };
if (serialize.immutable) {
const immutableSerializer = seralizeImmutable(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
);
return {
replacer: immutableSerializer.replacer,
reviver: immutableSerializer.reviver,
options:
typeof serialize.options === 'object'
? { ...immutableSerializer.options, ...serialize.options }
: immutableSerializer.options,
};
}
if (!serialize.replacer && !serialize.reviver) {
return { options: serialize.options };
}
return {
replacer: serialize.replacer,
reviver: serialize.reviver,
options: serialize.options || true,
};
}
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 };
return value;
}
function post(message) {
window.postMessage(message, '*');
}
function getStackTrace(config, toExcludeFromTrace) {
if (!config.trace) return undefined;
if (typeof config.trace === 'function') return config.trace();
let stack;
let extraFrames = 0;
let prevStackTraceLimit;
const traceLimit = config.traceLimit;
const error = Error();
if (Error.captureStackTrace) {
if (Error.stackTraceLimit < traceLimit) {
prevStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = traceLimit;
}
Error.captureStackTrace(error, toExcludeFromTrace);
} else {
extraFrames = 3;
}
stack = error.stack;
if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit;
if (
extraFrames ||
typeof Error.stackTraceLimit !== 'number' ||
Error.stackTraceLimit > traceLimit
) {
const frames = stack.split('\n');
if (frames.length > traceLimit) {
stack = frames
.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0))
.join('\n');
}
}
return stack;
}
function amendActionType(action, config, toExcludeFromTrace) {
let timestamp = Date.now();
let stack = getStackTrace(config, toExcludeFromTrace);
if (typeof action === 'string') {
return { action: { type: action }, timestamp, stack };
}
if (!action.type) return { action: { type: 'update' }, timestamp, stack };
if (action.action) return stack ? { stack, ...action } : action;
return { action, timestamp, stack };
}
export function toContentScript(message, serializeState, serializeAction) {
if (message.type === 'ACTION') {
message.action = stringify(message.action, serializeAction);
message.payload = stringify(message.payload, serializeState);
} else if (message.type === 'STATE' || message.type === 'PARTIAL_STATE') {
const { actionsById, computedStates, committedState, ...rest } =
message.payload;
message.payload = rest;
message.actionsById = stringify(actionsById, serializeAction);
message.computedStates = stringify(computedStates, serializeState);
message.committedState = typeof committedState !== 'undefined';
} else if (message.type === 'EXPORT') {
message.payload = stringify(message.payload, serializeAction);
if (typeof message.committedState !== 'undefined') {
message.committedState = stringify(
message.committedState,
serializeState
);
}
}
post(message);
}
export function sendMessage(action, state, config, instanceId, name) {
let amendedAction = action;
if (typeof config !== 'object') {
// Legacy: sending actions not from connected part
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);
}
function handleMessages(event) {
if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) {
return;
}
const message = event.data;
if (!message || message.source !== '@devtools-extension') return;
Object.keys(listeners).forEach((id) => {
if (message.id && id !== message.id) return;
if (typeof listeners[id] === 'function') listeners[id](message);
else {
listeners[id].forEach((fn) => {
fn(message);
});
}
});
}
export function setListener(onMessage, instanceId) {
listeners[instanceId] = onMessage;
window.addEventListener('message', handleMessages, false);
}
const liftListener = (listener, config) => (message) => {
let data = {};
if (message.type === 'IMPORT') {
data.type = 'DISPATCH';
data.payload = {
type: 'IMPORT_STATE',
...importState(message.state, config),
};
} else {
data = message;
}
listener(data);
};
export function disconnect() {
window.removeEventListener('message', handleMessages);
post({ type: 'DISCONNECT', source });
}
export function connect(preConfig) {
const config = preConfig || {};
const id = generateId(config.instanceId);
if (!config.instanceId) config.instanceId = id;
if (!config.name) {
config.name =
document.title && id === 1 ? document.title : `Instance ${id}`;
}
if (config.serialize) config.serialize = getSeralizeParameter(config);
const actionCreators = config.actionCreators || {};
const latency = config.latency;
const predicate = config.predicate;
const localFilter = getLocalFilter(config);
const autoPause = config.autoPause;
let isPaused = autoPause;
let delayedActions = [];
let delayedStates = [];
const rootListiner = (action) => {
if (autoPause) {
if (action.type === 'START') isPaused = false;
else if (action.type === 'STOP') isPaused = true;
}
if (action.type === 'DISPATCH') {
const payload = action.payload;
if (payload.type === 'PAUSE_RECORDING') {
isPaused = payload.status;
toContentScript({
type: 'LIFTED',
liftedState: { isPaused },
instanceId: id,
source,
});
}
}
};
listeners[id] = [rootListiner];
const subscribe = (listener) => {
if (!listener) return undefined;
const liftedListener = liftListener(listener, config);
listeners[id].push(liftedListener);
return function unsubscribe() {
const index = listeners[id].indexOf(liftedListener);
listeners[id].splice(index, 1);
};
};
const unsubscribe = () => {
delete listeners[id];
};
const sendDelayed = throttle(() => {
sendMessage(delayedActions, delayedStates, config);
delayedActions = [];
delayedStates = [];
}, latency);
const send = (action, state) => {
if (
isPaused ||
isFiltered(action, localFilter) ||
(predicate && !predicate(state, action))
) {
return;
}
let amendedAction = action;
const amendedState = config.stateSanitizer
? config.stateSanitizer(state)
: state;
if (action) {
if (config.getActionType) {
amendedAction = config.getActionType(action);
if (typeof amendedAction !== 'object') {
amendedAction = {
action: { type: amendedAction },
timestamp: Date.now(),
};
}
} else if (config.actionSanitizer) {
amendedAction = config.actionSanitizer(action);
}
amendedAction = amendActionType(amendedAction, config, send);
if (latency) {
delayedActions.push(amendedAction);
delayedStates.push(amendedState);
sendDelayed();
return;
}
}
sendMessage(amendedAction, amendedState, config);
};
const init = (state, liftedData) => {
const message = {
type: 'INIT',
payload: stringify(state, config.serialize),
instanceId: id,
source,
};
if (liftedData && Array.isArray(liftedData)) {
// Legacy
message.action = stringify(liftedData);
message.name = config.name;
} else {
if (liftedData) {
message.liftedState = liftedData;
if (liftedData.isPaused) isPaused = true;
}
message.libConfig = {
actionCreators: JSON.stringify(getActionsArray(actionCreators)),
name: config.name || document.title,
features: config.features,
serialize: !!config.serialize,
type: config.type,
};
}
post(message);
};
const error = (payload) => {
post({ type: 'ERROR', payload, id, source });
};
window.addEventListener('message', handleMessages, false);
post({ type: 'INIT_INSTANCE', instanceId: id, source });
return {
init,
subscribe,
unsubscribe,
send,
error,
};
}
export function updateStore(stores) {
return function (newStore, instanceId) {
/* eslint-disable no-console */
console.warn(
'`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' +
"`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead of the extension's store enhancer: " +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'
);
/* eslint-enable no-console */
const store = stores[instanceId || Object.keys(stores)[0]];
// Mutate the store in order to keep the reference
store.liftedStore = newStore.liftedStore;
store.getState = newStore.getState;
store.dispatch = newStore.dispatch;
};
}
export function isInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}

View File

@ -0,0 +1,736 @@
import jsan, { Options } from 'jsan';
import throttle from 'lodash/throttle';
import serializeImmutable from '@redux-devtools/serialize/lib/immutable/serialize';
import { getActionsArray } from '@redux-devtools/utils';
import { getLocalFilter, isFiltered, PartialLiftedState } from './filters';
import importState from './importState';
import generateId from './generateInstanceId';
import { Config } from '../../browser/extension/inject/pageScript';
import { Action } from 'redux';
import {
EnhancedStore,
LiftedState,
PerformAction,
} from '@redux-devtools/instrument';
import { LibConfig } from '@redux-devtools/app/lib/actions';
import {
ContentScriptToPageScriptMessage,
ListenerMessage,
} from '../../browser/extension/inject/contentScript';
import { Position } from './openWindow';
const listeners: {
[instanceId: string]:
| ((message: ContentScriptToPageScriptMessage) => void)
| ((message: ContentScriptToPageScriptMessage) => void)[];
} = {};
export const source = '@devtools-page';
function windowReplacer(key: string, value: unknown) {
if (value && (value as Window).window === value) {
return '[WINDOW]';
}
return value;
}
function tryCatchStringify(obj: unknown) {
try {
return JSON.stringify(obj);
} catch (err) {
/* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') {
console.log('Failed to stringify', err);
}
/* eslint-enable no-console */
return jsan.stringify(obj, windowReplacer, undefined, {
circular: '[CIRCULAR]',
date: true,
});
}
}
let stringifyWarned: boolean;
function stringify(obj: unknown, serialize?: Serialize | undefined) {
const str =
typeof serialize === 'undefined'
? tryCatchStringify(obj)
: jsan.stringify(obj, serialize.replacer, undefined, serialize.options);
if (!stringifyWarned && str && str.length > 16 * 1024 * 1024) {
// 16 MB
/* eslint-disable no-console */
console.warn(
'Application state or actions payloads are too large making Redux DevTools serialization slow and consuming a lot of memory. See https://git.io/fpcP5 on how to configure it.'
);
/* eslint-enable no-console */
stringifyWarned = true;
}
return str;
}
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 = serializeImmutable(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
);
return {
replacer: immutableSerializer.replacer,
reviver: immutableSerializer.reviver,
options:
typeof serialize.options === 'object'
? { ...immutableSerializer.options, ...serialize.options }
: immutableSerializer.options,
};
}
if (!serialize.replacer && !serialize.reviver) {
return { options: serialize.options };
}
return {
replacer: serialize.replacer,
reviver: serialize.reviver,
options: serialize.options || true,
};
}
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 value === 'boolean') return { options: value };
if (typeof value === 'function') return { replacer: value };
return value;
}
interface InitInstancePageScriptToContentScriptMessage {
readonly type: 'INIT_INSTANCE';
readonly instanceId: number;
readonly source: typeof source;
}
interface DisconnectMessage {
readonly type: 'DISCONNECT';
readonly source: typeof source;
}
interface InitMessage<S, A extends Action<unknown>> {
readonly type: 'INIT';
readonly payload: string;
readonly instanceId: number;
readonly source: typeof source;
action?: string;
name?: string | undefined;
liftedState?: LiftedState<S, A, unknown>;
libConfig?: LibConfig;
}
interface SerializedPartialLiftedState {
readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number;
readonly nextActionId: number;
}
interface SerializedPartialStateMessage {
readonly type: 'PARTIAL_STATE';
readonly payload: SerializedPartialLiftedState;
readonly source: typeof source;
readonly instanceId: number;
readonly maxAge: number;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
interface SerializedExportMessage {
readonly type: 'EXPORT';
readonly payload: string;
readonly committedState: string | undefined;
readonly source: typeof source;
readonly instanceId: number;
}
interface SerializedActionMessage {
readonly type: 'ACTION';
readonly payload: string;
readonly source: typeof source;
readonly instanceId: number;
readonly action: string;
readonly maxAge: number;
readonly nextActionId?: number;
}
interface SerializedStateMessage<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
'actionsById' | 'computedStates' | 'committedState'
>;
readonly source: typeof source;
readonly instanceId: number;
readonly libConfig?: LibConfig;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
interface OpenMessage {
readonly source: typeof source;
readonly type: 'OPEN';
readonly position: Position;
}
export type PageScriptToContentScriptMessageForwardedToMonitors<
S,
A extends Action<unknown>
> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
| SerializedExportMessage
| SerializedActionMessage
| SerializedStateMessage<S, A>;
export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<
S,
A extends Action<unknown>
> =
| PageScriptToContentScriptMessageForwardedToMonitors<S, A>
| ErrorMessage
| GetReportMessage
| StopMessage
| OpenMessage;
export type PageScriptToContentScriptMessageWithoutDisconnect<
S,
A extends Action<unknown>
> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| InitInstancePageScriptToContentScriptMessage
| InitInstanceMessage;
export type PageScriptToContentScriptMessage<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| DisconnectMessage;
function post<S, A extends Action<unknown>>(
message: PageScriptToContentScriptMessage<S, A>
) {
window.postMessage(message, '*');
}
function getStackTrace(
config: Config,
toExcludeFromTrace: Function | undefined
) {
if (!config.trace) return undefined;
if (typeof config.trace === 'function') return config.trace();
let stack;
let extraFrames = 0;
let prevStackTraceLimit;
const traceLimit = config.traceLimit;
const error = Error();
if (Error.captureStackTrace) {
if (Error.stackTraceLimit < traceLimit!) {
prevStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = traceLimit!;
}
Error.captureStackTrace(error, toExcludeFromTrace);
} else {
extraFrames = 3;
}
stack = error.stack;
if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit;
if (
extraFrames ||
typeof Error.stackTraceLimit !== 'number' ||
Error.stackTraceLimit > traceLimit!
) {
const frames = stack!.split('\n');
if (frames.length > traceLimit!) {
stack = frames
.slice(0, traceLimit! + extraFrames + (frames[0] === 'Error' ? 1 : 0))
.join('\n');
}
}
return stack;
}
function amendActionType<A extends Action<unknown>>(
action:
| A
| StructuralPerformAction<A>
| StructuralPerformAction<A>[]
| string,
config: Config,
toExcludeFromTrace: Function | undefined
): StructuralPerformAction<A> {
let timestamp = Date.now();
let stack = getStackTrace(config, toExcludeFromTrace);
if (typeof action === 'string') {
return { action: { type: action } as A, timestamp, stack };
}
if (!(action as A).type)
return { action: { type: 'update' } as A, timestamp, stack };
if ((action as StructuralPerformAction<A>).action)
return (
stack ? { stack, ...action } : action
) as StructuralPerformAction<A>;
return { action, timestamp, stack } as StructuralPerformAction<A>;
}
interface LiftedMessage {
readonly type: 'LIFTED';
readonly liftedState: { readonly isPaused: boolean | undefined };
readonly instanceId: number;
readonly source: typeof source;
}
interface PartialStateMessage<S, A extends Action<unknown>> {
readonly type: 'PARTIAL_STATE';
readonly payload: PartialLiftedState<S, A>;
readonly source: typeof source;
readonly instanceId: number;
readonly maxAge: number;
}
interface ExportMessage<S, A extends Action<unknown>> {
readonly type: 'EXPORT';
readonly payload: readonly A[];
readonly committedState: S;
readonly source: typeof source;
readonly instanceId: number;
}
export interface StructuralPerformAction<A extends Action<unknown>> {
readonly action: A;
readonly timestamp?: number;
readonly stack?: string;
}
type SingleUserAction<A extends Action<unknown>> =
| PerformAction<A>
| StructuralPerformAction<A>
| A;
type UserAction<A extends Action<unknown>> =
| SingleUserAction<A>
| readonly SingleUserAction<A>[];
interface ActionMessage<S, A extends Action<unknown>> {
readonly type: 'ACTION';
readonly payload: S;
readonly source: typeof source;
readonly instanceId: number;
readonly action: UserAction<A>;
readonly maxAge: number;
readonly nextActionId?: number;
readonly name?: string;
}
interface StateMessage<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: LiftedState<S, A, unknown>;
readonly source: typeof source;
readonly instanceId: number;
readonly libConfig?: LibConfig;
readonly action?: UserAction<A>;
readonly maxAge?: number;
readonly name?: string;
}
export interface ErrorMessage {
readonly type: 'ERROR';
readonly payload: string;
readonly source: typeof source;
readonly instanceId: number;
readonly message?: string | undefined;
}
interface InitInstanceMessage {
readonly type: 'INIT_INSTANCE';
readonly payload: undefined;
readonly source: typeof source;
readonly instanceId: number;
}
interface GetReportMessage {
readonly type: 'GET_REPORT';
readonly payload: string;
readonly source: typeof source;
readonly instanceId: number;
}
interface StopMessage {
readonly type: 'STOP';
readonly payload: undefined;
readonly source: typeof source;
readonly instanceId: number;
}
type ToContentScriptMessage<S, A extends Action<unknown>> =
| LiftedMessage
| PartialStateMessage<S, A>
| ExportMessage<S, A>
| ActionMessage<S, A>
| StateMessage<S, A>
| ErrorMessage
| InitInstanceMessage
| GetReportMessage
| StopMessage;
export function toContentScript<S, A extends Action<unknown>>(
message: ToContentScriptMessage<S, A>,
serializeState?: Serialize | undefined,
serializeAction?: Serialize | undefined
) {
if (message.type === 'ACTION') {
post({
...message,
action: stringify(message.action, serializeAction),
payload: stringify(message.payload, serializeState),
});
} else if (message.type === 'STATE') {
const { actionsById, computedStates, committedState, ...rest } =
message.payload;
post({
...message,
payload: rest,
actionsById: stringify(actionsById, serializeAction),
computedStates: stringify(computedStates, serializeState),
committedState: typeof committedState !== 'undefined',
});
} else if (message.type === 'PARTIAL_STATE') {
const { actionsById, computedStates, committedState, ...rest } =
message.payload;
post({
...message,
payload: rest,
actionsById: stringify(actionsById, serializeAction),
computedStates: stringify(computedStates, serializeState),
committedState: typeof committedState !== 'undefined',
});
} else if (message.type === 'EXPORT') {
post({
...message,
payload: stringify(message.payload, serializeAction),
committedState:
typeof message.committedState !== 'undefined'
? stringify(message.committedState, serializeState)
: (message.committedState as undefined),
});
} else {
post(message);
}
}
export function sendMessage<S, A extends Action<unknown>>(
action: StructuralPerformAction<A> | StructuralPerformAction<A>[],
state: LiftedState<S, A, unknown>,
config: Config,
instanceId?: number,
name?: string
) {
let amendedAction = action;
if (typeof config !== 'object') {
// Legacy: sending actions not from connected part
config = {}; // eslint-disable-line no-param-reassign
if (action) amendedAction = amendActionType(action, config, sendMessage);
}
if (action) {
toContentScript(
{
type: 'ACTION',
action: amendedAction,
payload: state,
maxAge: config.maxAge!,
source,
name: config.name || name,
instanceId: config.instanceId || instanceId || 1,
},
config.serialize as Serialize | undefined,
config.serialize as Serialize | undefined
);
} else {
toContentScript<S, A>(
{
type: 'STATE',
action: amendedAction,
payload: state,
maxAge: config.maxAge,
source,
name: config.name || name,
instanceId: config.instanceId || instanceId || 1,
},
config.serialize as Serialize | undefined,
config.serialize as Serialize | undefined
);
}
}
function handleMessages(event: MessageEvent<ContentScriptToPageScriptMessage>) {
if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) {
return;
}
const message = event.data;
if (!message || message.source !== '@devtools-extension') return;
Object.keys(listeners).forEach((id) => {
if (message.id && id !== message.id) return;
const listenersForId = listeners[id];
if (typeof listenersForId === 'function') listenersForId(message);
else {
listenersForId.forEach((fn) => {
fn(message);
});
}
});
}
export function setListener(
onMessage: (message: ContentScriptToPageScriptMessage) => void,
instanceId: number
) {
listeners[instanceId] = onMessage;
window.addEventListener('message', handleMessages, false);
}
const liftListener =
<S, A extends Action<unknown>>(
listener: (message: ListenerMessage<S, A>) => void,
config: Config
) =>
(message: ContentScriptToPageScriptMessage) => {
if (message.type === 'IMPORT') {
listener({
type: 'DISPATCH',
payload: {
type: 'IMPORT_STATE',
...importState<S, A>(message.state, config)!,
},
});
} else {
listener(message);
}
};
export function disconnect() {
window.removeEventListener('message', handleMessages);
post({ type: 'DISCONNECT', source });
}
export interface ConnectResponse {
init: <S, A extends Action<unknown>>(
state: S,
liftedData: LiftedState<S, A, unknown>
) => void;
subscribe: <S, A extends Action<unknown>>(
listener: (message: ListenerMessage<S, A>) => void
) => (() => void) | undefined;
unsubscribe: () => void;
send: <S, A extends Action<unknown>>(
action: A,
state: LiftedState<S, A, unknown>
) => void;
error: (payload: string) => void;
}
export function connect(preConfig: Config): ConnectResponse {
const config = preConfig || {};
const id = generateId(config.instanceId);
if (!config.instanceId) config.instanceId = id;
if (!config.name) {
config.name =
document.title && id === 1 ? document.title : `Instance ${id}`;
}
if (config.serialize) config.serialize = getSerializeParameter(config);
const actionCreators = config.actionCreators || {};
const latency = config.latency;
const predicate = config.predicate;
const localFilter = getLocalFilter(config);
const autoPause = config.autoPause;
let isPaused = autoPause;
let delayedActions: StructuralPerformAction<Action<unknown>>[] = [];
let delayedStates: LiftedState<unknown, Action<unknown>, unknown>[] = [];
const rootListener = (action: ContentScriptToPageScriptMessage) => {
if (autoPause) {
if (action.type === 'START') isPaused = false;
else if (action.type === 'STOP') isPaused = true;
}
if (action.type === 'DISPATCH') {
const payload = action.payload;
if (payload.type === 'PAUSE_RECORDING') {
isPaused = payload.status;
toContentScript({
type: 'LIFTED',
liftedState: { isPaused },
instanceId: id,
source,
});
}
}
};
listeners[id] = [rootListener];
const subscribe = <S, A extends Action<unknown>>(
listener: (message: ListenerMessage<S, A>) => void
) => {
if (!listener) return undefined;
const liftedListener = liftListener(listener, config);
const listenersForId = listeners[id] as ((
message: ContentScriptToPageScriptMessage
) => void)[];
listenersForId.push(liftedListener);
return function unsubscribe() {
const index = listenersForId.indexOf(liftedListener);
listenersForId.splice(index, 1);
};
};
const unsubscribe = () => {
delete listeners[id];
};
const sendDelayed = throttle(() => {
sendMessage(delayedActions, delayedStates as any, config);
delayedActions = [];
delayedStates = [];
}, latency);
const send = <S, A extends Action<unknown>>(
action: A,
state: LiftedState<S, A, unknown>
) => {
if (
isPaused ||
isFiltered(action, localFilter) ||
(predicate && !predicate(state, action))
) {
return;
}
let amendedAction: A | StructuralPerformAction<A> = action;
const amendedState = config.stateSanitizer
? config.stateSanitizer(state)
: state;
if (action) {
if (config.getActionType) {
amendedAction = config.getActionType(action);
if (typeof amendedAction !== 'object') {
amendedAction = {
action: { type: amendedAction },
timestamp: Date.now(),
} as unknown as A;
}
} else if (config.actionSanitizer) {
amendedAction = config.actionSanitizer(action);
}
amendedAction = amendActionType(amendedAction, config, send);
if (latency) {
delayedActions.push(amendedAction);
delayedStates.push(amendedState);
sendDelayed();
return;
}
}
sendMessage(
amendedAction as StructuralPerformAction<A>,
amendedState,
config
);
};
const init = <S, A extends Action<unknown>>(
state: S,
liftedData: LiftedState<S, A, unknown>
) => {
const message: InitMessage<S, A> = {
type: 'INIT',
payload: stringify(state, config.serialize as Serialize | undefined),
instanceId: id,
source,
};
if (liftedData && Array.isArray(liftedData)) {
// Legacy
message.action = stringify(liftedData);
message.name = config.name;
} else {
if (liftedData) {
message.liftedState = liftedData;
if (liftedData.isPaused) isPaused = true;
}
message.libConfig = {
actionCreators: JSON.stringify(getActionsArray(actionCreators)),
name: config.name || document.title,
features: config.features,
serialize: !!config.serialize,
type: config.type,
};
}
post(message);
};
const error = (payload: string) => {
post({ type: 'ERROR', payload, instanceId: id, source });
};
window.addEventListener('message', handleMessages, false);
post({ type: 'INIT_INSTANCE', instanceId: id, source });
return {
init,
subscribe,
unsubscribe,
send,
error,
};
}
export function updateStore<S, A extends Action<unknown>>(
stores: {
[K in string | number]: EnhancedStore<S, A, unknown>;
}
) {
return function (newStore: EnhancedStore<S, A, unknown>, instanceId: number) {
/* eslint-disable no-console */
console.warn(
'`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' +
"`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead of the extension's store enhancer: " +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'
);
/* eslint-enable no-console */
const store = stores[instanceId || Object.keys(stores)[0]];
// Mutate the store in order to keep the reference
store.liftedStore = newStore.liftedStore;
store.getState = newStore.getState;
store.dispatch = newStore.dispatch;
};
}
export function isInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}

View File

@ -1,9 +1,9 @@
let handleError; let handleError: (() => boolean) | undefined;
let lastTime = 0; let lastTime = 0;
function createExpBackoffTimer(step) { function createExpBackoffTimer(step: number) {
let count = 1; let count = 1;
return function (reset) { return function (reset?: boolean) {
// Reset call // Reset call
if (reset) { if (reset) {
count = 1; count = 1;
@ -18,7 +18,7 @@ function createExpBackoffTimer(step) {
const nextErrorTimeout = createExpBackoffTimer(5000); const nextErrorTimeout = createExpBackoffTimer(5000);
function postError(message) { function postError(message: string) {
if (handleError && !handleError()) return; if (handleError && !handleError()) return;
window.postMessage( window.postMessage(
{ {
@ -30,7 +30,7 @@ function postError(message) {
); );
} }
function catchErrors(e) { function catchErrors(e: ErrorEvent) {
if ( if (
(window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors) || (window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors) ||
e.timeStamp - lastTime < nextErrorTimeout() e.timeStamp - lastTime < nextErrorTimeout()
@ -42,7 +42,7 @@ function catchErrors(e) {
postError(e.message); postError(e.message);
} }
export default function notifyErrors(onError) { export default function notifyErrors(onError?: () => boolean) {
handleError = onError; handleError = onError;
window.addEventListener('error', catchErrors, false); window.addEventListener('error', catchErrors, false);
} }

View File

@ -1,10 +0,0 @@
export default function openWindow(position) {
window.postMessage(
{
source: '@devtools-page',
type: 'OPEN',
position: position || 'right',
},
'*'
);
}

View File

@ -0,0 +1,18 @@
import { Action } from 'redux';
import { PageScriptToContentScriptMessage } from './index';
export type Position = 'left' | 'right' | 'bottom' | 'panel' | 'remote';
function post<S, A extends Action<unknown>>(
message: PageScriptToContentScriptMessage<S, A>
) {
window.postMessage(message, '*');
}
export default function openWindow(position?: Position) {
post({
source: '@devtools-page',
type: 'OPEN',
position: position || 'right',
});
}

View File

@ -1,19 +1,33 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect, ResolveThunks } from 'react-redux';
import { Container, Notification } from 'devui'; import { Container, Notification } from 'devui';
import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances'; import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances';
import Settings from '@redux-devtools/app/lib/components/Settings'; import Settings from '@redux-devtools/app/lib/components/Settings';
import Actions from '@redux-devtools/app/lib/containers/Actions'; import Actions from '@redux-devtools/app/lib/containers/Actions';
import Header from '@redux-devtools/app/lib/components/Header'; import Header from '@redux-devtools/app/lib/components/Header';
import { clearNotification } from '@redux-devtools/app/lib/actions'; 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';
class App extends Component { type StateProps = ReturnType<typeof mapStateToProps>;
openWindow = (position) => { type DispatchProps = ResolveThunks<typeof actionCreators>;
chrome.runtime.sendMessage({ type: 'OPEN', position }); interface OwnProps {
readonly position: string;
}
type Props = StateProps & DispatchProps & OwnProps;
function sendMessage(message: SingleMessage) {
chrome.runtime.sendMessage(message);
}
class App extends Component<Props> {
openWindow = (position: Position) => {
sendMessage({ type: 'OPEN', position });
}; };
openOptionsPage = () => { openOptionsPage = () => {
if (navigator.userAgent.indexOf('Firefox') !== -1) { if (navigator.userAgent.indexOf('Firefox') !== -1) {
chrome.runtime.sendMessage({ type: 'OPEN_OPTIONS' }); sendMessage({ type: 'OPEN_OPTIONS' });
} else { } else {
chrome.runtime.openOptionsPage(); chrome.runtime.openOptionsPage();
} }
@ -62,7 +76,7 @@ class App extends Component {
} }
} }
function mapStateToProps(state) { function mapStateToProps(state: StoreState) {
const instances = state.instances; const instances = state.instances;
const id = getActiveInstance(instances); const id = getActiveInstance(instances);
return { return {

View File

@ -1,264 +0,0 @@
import stringifyJSON from '@redux-devtools/app/lib/utils/stringifyJSON';
import {
UPDATE_STATE,
REMOVE_INSTANCE,
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 openDevToolsWindow from '../../browser/extension/background/openWindow';
import { getReport } from '../../browser/extension/background/logging';
const CONNECTED = 'socket/CONNECTED';
const DISCONNECTED = 'socket/DISCONNECTED';
const connections = {
tab: {},
panel: {},
monitor: {},
};
const chunks = {};
let monitors = 0;
let isMonitored = false;
const getId = (sender, name) =>
sender.tab ? sender.tab.id : name || sender.id;
function toMonitors(action, tabId, verbose) {
Object.keys(connections.monitor).forEach((id) => {
connections.monitor[id].postMessage(
verbose || action.type === 'ERROR' ? action : { type: UPDATE_STATE }
);
});
Object.keys(connections.panel).forEach((id) => {
connections.panel[id].postMessage(action);
});
}
function toContentScript({ message, action, id, instanceId, state }) {
connections.tab[id].postMessage({
type: message,
action,
state: nonReduxDispatch(window.store, message, instanceId, action, state),
id: instanceId.toString().replace(/^[^\/]+\//, ''),
});
}
function toAllTabs(msg) {
const tabs = connections.tab;
Object.keys(tabs).forEach((id) => {
tabs[id].postMessage(msg);
});
}
function monitorInstances(shouldMonitor, id) {
if (!id && isMonitored === shouldMonitor) return;
const action = { type: shouldMonitor ? 'START' : 'STOP' };
if (id) {
if (connections.tab[id]) connections.tab[id].postMessage(action);
} else {
toAllTabs(action);
}
isMonitored = shouldMonitor;
}
function getReducerError() {
const instancesState = window.store.getState().instances;
const payload = instancesState.states[instancesState.current];
const computedState = payload.computedStates[payload.currentStateIndex];
if (!computedState) return false;
return computedState.error;
}
function togglePersist() {
const state = window.store.getState();
if (state.persistStates) {
Object.keys(state.instances.connections).forEach((id) => {
if (connections.tab[id]) return;
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
});
}
}
// Receive messages from content scripts
function messaging(request, sender, sendResponse) {
let tabId = getId(sender);
if (!tabId) return;
if (sender.frameId) tabId = `${tabId}-${sender.frameId}`;
if (request.type === 'STOP') {
if (!Object.keys(window.store.getState().instances.connections).length) {
window.store.dispatch({ type: DISCONNECTED });
}
return;
}
if (request.type === 'OPEN_OPTIONS') {
chrome.runtime.openOptionsPage();
return;
}
if (request.type === 'GET_OPTIONS') {
window.syncOptions.get((options) => {
sendResponse({ options });
});
return;
}
if (request.type === 'GET_REPORT') {
getReport(request.payload, tabId, request.instanceId);
return;
}
if (request.type === 'OPEN') {
let position = 'devtools-left';
if (
['remote', 'panel', 'left', 'right', 'bottom'].indexOf(
request.position
) !== -1
) {
position = 'devtools-' + request.position;
}
openDevToolsWindow(position);
return;
}
if (request.type === 'ERROR') {
if (request.payload) {
toMonitors(request, tabId);
return;
}
if (!request.message) return;
const reducerError = getReducerError();
chrome.notifications.create('app-error', {
type: 'basic',
title: reducerError
? 'An error occurred in the reducer'
: 'An error occurred in the app',
message: reducerError || request.message,
iconUrl: 'img/logo/48x48.png',
isClickable: !!reducerError,
});
return;
}
const action = { type: UPDATE_STATE, request, id: tabId };
const instanceId = `${tabId}/${request.instanceId}`;
if (request.split) {
if (request.split === 'start') {
chunks[instanceId] = request;
return;
}
if (request.split === 'chunk') {
chunks[instanceId][request.chunk[0]] =
(chunks[instanceId][request.chunk[0]] || '') + request.chunk[1];
return;
}
action.request = chunks[instanceId];
delete chunks[instanceId];
}
if (request.instanceId) {
action.request.instanceId = instanceId;
}
window.store.dispatch(action);
if (request.type === 'EXPORT') {
toMonitors(action, tabId, true);
} else {
toMonitors(action, tabId);
}
}
function disconnect(type, id, listener) {
return function disconnectListener() {
const p = connections[type][id];
if (listener && p) p.onMessage.removeListener(listener);
if (p) p.onDisconnect.removeListener(disconnectListener);
delete connections[type][id];
if (type === 'tab') {
if (!window.store.getState().persistStates) {
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
} else {
monitors--;
if (!monitors) monitorInstances(false);
}
};
}
function onConnect(port) {
let id;
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}`;
connections.tab[id] = port;
listener = (msg) => {
if (msg.name === 'INIT_INSTANCE') {
if (typeof id === 'number') {
chrome.pageAction.show(id);
chrome.pageAction.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
}
if (isMonitored) port.postMessage({ type: 'START' });
const state = window.store.getState();
if (state.persistStates) {
const instanceId = `${id}/${msg.instanceId}`;
const persistedState = state.instances.states[instanceId];
if (!persistedState) return;
toContentScript({
message: 'IMPORT',
id,
instanceId,
state: stringifyJSON(
persistedState,
state.instances.options[instanceId].serialize
),
});
}
return;
}
if (msg.name === 'RELAY') {
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);
connections.monitor[id] = port;
monitorInstances(true);
monitors++;
port.onDisconnect.addListener(disconnect('monitor', id));
} else {
// devpanel
id = port.name || port.sender.frameId;
connections.panel[id] = port;
monitorInstances(true, port.name);
monitors++;
listener = (msg) => {
window.store.dispatch(msg);
};
port.onMessage.addListener(listener);
port.onDisconnect.addListener(disconnect('panel', id, listener));
}
}
chrome.runtime.onConnect.addListener(onConnect);
chrome.runtime.onConnectExternal.addListener(onConnect);
chrome.runtime.onMessage.addListener(messaging);
chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener((id) => {
chrome.notifications.clear(id);
openDevToolsWindow('devtools-right');
});
window.syncOptions = syncOptions(toAllTabs); // Expose to the options page
export default function api() {
return (next) => (action) => {
if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === 'TOGGLE_PERSIST') togglePersist();
return next(action);
};
}

View File

@ -0,0 +1,591 @@
import stringifyJSON from '@redux-devtools/app/lib/utils/stringifyJSON';
import {
UPDATE_STATE,
REMOVE_INSTANCE,
LIFTED_ACTION,
} from '@redux-devtools/app/lib/constants/actionTypes';
import { nonReduxDispatch } from '@redux-devtools/app/lib/utils/monitorActions';
import syncOptions, {
Options,
OptionsMessage,
SyncOptions,
} from '../../browser/extension/options/syncOptions';
import openDevToolsWindow, {
DevToolsPosition,
} from '../../browser/extension/background/openWindow';
import { getReport } from '../../browser/extension/background/logging';
import {
CustomAction,
DispatchAction as AppDispatchAction,
LibConfig,
} from '@redux-devtools/app/lib/actions';
import { Action, Dispatch } from 'redux';
import {
ContentScriptToBackgroundMessage,
SplitMessage,
} from '../../browser/extension/inject/contentScript';
import {
ErrorMessage,
PageScriptToContentScriptMessageForwardedToMonitors,
PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance,
} from '../api';
import { LiftedState } from '@redux-devtools/instrument';
import {
BackgroundAction,
LiftedActionAction,
} from '../stores/backgroundStore';
import { Position } from '../api/openWindow';
interface TabMessageBase {
readonly type: string;
readonly state?: string | undefined;
readonly id?: string;
}
interface StartAction extends TabMessageBase {
readonly type: 'START';
readonly state?: never;
readonly id?: never;
}
interface StopAction extends TabMessageBase {
readonly type: 'STOP';
readonly state?: never;
readonly id?: never;
}
interface DispatchAction extends TabMessageBase {
readonly type: 'DISPATCH';
readonly action: AppDispatchAction;
readonly state: string | undefined;
readonly id: string;
}
interface ImportAction extends TabMessageBase {
readonly type: 'IMPORT';
readonly action: undefined;
readonly state: string | undefined;
readonly id: string;
}
interface ActionAction extends TabMessageBase {
readonly type: 'ACTION';
readonly action: string | CustomAction;
readonly state: string | undefined;
readonly id: string;
}
interface ExportAction extends TabMessageBase {
readonly type: 'EXPORT';
readonly action: undefined;
readonly state: string | undefined;
readonly id: string;
}
export interface NAAction {
readonly type: 'NA';
readonly id: string | number;
}
interface InitMessage<S, A extends Action<unknown>> {
readonly type: 'INIT';
readonly payload: string;
instanceId: string;
readonly source: '@devtools-page';
action?: string;
name?: string | undefined;
liftedState?: LiftedState<S, A, unknown>;
libConfig?: LibConfig;
}
interface LiftedMessage {
type: 'LIFTED';
liftedState: { isPaused: boolean | undefined };
instanceId: number;
source: '@devtools-page';
}
interface SerializedPartialLiftedState {
readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number;
readonly nextActionId: number;
}
interface SerializedPartialStateMessage {
readonly type: 'PARTIAL_STATE';
readonly payload: SerializedPartialLiftedState;
readonly source: '@devtools-page';
instanceId: number;
readonly maxAge: number;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
interface SerializedExportMessage {
readonly type: 'EXPORT';
readonly payload: string;
readonly committedState: string | undefined;
readonly source: '@devtools-page';
instanceId: number;
}
interface SerializedActionMessage {
readonly type: 'ACTION';
readonly payload: string;
readonly source: '@devtools-page';
instanceId: number;
readonly action: string;
readonly maxAge: number;
readonly nextActionId: number;
}
interface SerializedStateMessage<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
'actionsById' | 'computedStates' | 'committedState'
>;
readonly source: '@devtools-page';
instanceId: string;
readonly libConfig?: LibConfig;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
type UpdateStateRequest<S, A extends Action<unknown>> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
| SerializedExportMessage
| SerializedActionMessage
| SerializedStateMessage<S, A>;
export interface EmptyUpdateStateAction {
readonly type: typeof UPDATE_STATE;
}
interface UpdateStateAction<S, A extends Action<unknown>> {
readonly type: typeof UPDATE_STATE;
request: UpdateStateRequest<S, A>;
readonly id: string | number;
}
export type TabMessage =
| StartAction
| StopAction
| OptionsMessage
| DispatchAction
| ImportAction
| ActionAction
| ExportAction;
export type PanelMessage<S, A extends Action<unknown>> =
| NAAction
| ErrorMessage
| UpdateStateAction<S, A>;
export type MonitorMessage = NAAction | ErrorMessage | EmptyUpdateStateAction;
type TabPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: TabMessage) => void;
};
type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: <S, A extends Action<unknown>>(
message: PanelMessage<S, A>
) => void;
};
type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: MonitorMessage) => void;
};
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 };
readonly monitor: { [K in number | string]: MonitorPort };
} = {
tab: {},
panel: {},
monitor: {},
};
const chunks: {
[instanceId: string]: PageScriptToContentScriptMessageForwardedToMonitors<
unknown,
Action<unknown>
>;
} = {};
let monitors = 0;
let isMonitored = false;
const getId = (sender: chrome.runtime.MessageSender, name?: string) =>
sender.tab ? sender.tab.id! : name || sender.id!;
type MonitorAction<S, A extends Action<unknown>> =
| NAAction
| ErrorMessage
| UpdateStateAction<S, A>;
function toMonitors<S, A extends Action<unknown>>(
action: MonitorAction<S, A>,
tabId?: string | number,
verbose?: boolean
) {
Object.keys(connections.monitor).forEach((id) => {
connections.monitor[id].postMessage(
verbose || action.type === 'ERROR' ? action : { type: UPDATE_STATE }
);
});
Object.keys(connections.panel).forEach((id) => {
connections.panel[id].postMessage(action);
});
}
interface ImportMessage {
readonly message: 'IMPORT';
readonly id: string | number;
readonly instanceId: string;
readonly state: string;
readonly action?: never;
}
type ToContentScriptMessage = ImportMessage | LiftedActionAction;
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) {
const tabs = connections.tab;
Object.keys(tabs).forEach((id) => {
tabs[id].postMessage(msg);
});
}
function monitorInstances(shouldMonitor: boolean, id?: string) {
if (!id && isMonitored === shouldMonitor) return;
const action = {
type: shouldMonitor ? ('START' as const) : ('STOP' as const),
};
if (id) {
if (connections.tab[id]) connections.tab[id].postMessage(action);
} else {
toAllTabs(action);
}
isMonitored = shouldMonitor;
}
function getReducerError() {
const instancesState = window.store.getState().instances;
const payload = instancesState.states[instancesState.current];
const computedState = payload.computedStates[payload.currentStateIndex];
if (!computedState) return false;
return computedState.error;
}
function togglePersist() {
const state = window.store.getState();
if (state.persistStates) {
Object.keys(state.instances.connections).forEach((id) => {
if (connections.tab[id]) return;
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
});
}
}
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<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage
| SingleMessage;
type BackgroundStoreResponse = { readonly options: Options };
// Receive messages from content scripts
function messaging<S, A extends Action<unknown>>(
request: BackgroundStoreMessage<S, A>,
sender: chrome.runtime.MessageSender,
sendResponse?: (response?: BackgroundStoreResponse) => void
) {
let tabId = getId(sender);
if (!tabId) return;
if (sender.frameId) tabId = `${tabId}-${sender.frameId}`;
if (request.type === 'STOP') {
if (!Object.keys(window.store.getState().instances.connections).length) {
window.store.dispatch({ type: DISCONNECTED });
}
return;
}
if (request.type === 'OPEN_OPTIONS') {
chrome.runtime.openOptionsPage();
return;
}
if (request.type === 'GET_OPTIONS') {
window.syncOptions.get((options) => {
sendResponse!({ options });
});
return;
}
if (request.type === 'GET_REPORT') {
getReport(request.payload, tabId, request.instanceId);
return;
}
if (request.type === 'OPEN') {
let position: DevToolsPosition = 'devtools-left';
if (
['remote', 'panel', 'left', 'right', 'bottom'].indexOf(
request.position
) !== -1
) {
position = ('devtools-' + request.position) as DevToolsPosition;
}
openDevToolsWindow(position);
return;
}
if (request.type === 'ERROR') {
if (request.payload) {
toMonitors(request, tabId);
return;
}
if (!request.message) return;
const reducerError = getReducerError();
chrome.notifications.create('app-error', {
type: 'basic',
title: reducerError
? 'An error occurred in the reducer'
: 'An error occurred in the app',
message: reducerError || request.message,
iconUrl: 'img/logo/48x48.png',
isClickable: !!reducerError,
});
return;
}
const action: UpdateStateAction<S, A> = {
type: UPDATE_STATE,
request,
id: tabId,
} as UpdateStateAction<S, A>;
const instanceId = `${tabId}/${request.instanceId}`;
if ('split' in request) {
if (request.split === 'start') {
chunks[instanceId] = request as any;
return;
}
if (request.split === 'chunk') {
(chunks[instanceId] as any)[request.chunk[0]] =
((chunks[instanceId] as any)[request.chunk[0]] || '') +
request.chunk[1];
return;
}
action.request = chunks[instanceId] as any;
delete chunks[instanceId];
}
if (request.instanceId) {
action.request.instanceId = instanceId;
}
window.store.dispatch(action);
if (request.type === 'EXPORT') {
toMonitors(action, tabId, true);
} else {
toMonitors(action, tabId);
}
}
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);
if (p) p.onDisconnect.removeListener(disconnectListener);
delete connections[type][id];
if (type === 'tab') {
if (!window.store.getState().persistStates) {
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
} else {
monitors--;
if (!monitors) monitorInstances(false);
}
};
}
function onConnect<S, A extends Action<unknown>>(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}`;
connections.tab[id] = port;
listener = (msg: ContentScriptToBackgroundMessage<S, A>) => {
if (msg.name === 'INIT_INSTANCE') {
if (typeof id === 'number') {
chrome.pageAction.show(id);
chrome.pageAction.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
}
if (isMonitored) port.postMessage({ type: 'START' });
const state = window.store.getState();
if (state.persistStates) {
const instanceId = `${id}/${msg.instanceId}`;
const persistedState = state.instances.states[instanceId];
if (!persistedState) return;
toContentScript({
message: 'IMPORT',
id,
instanceId,
state: stringifyJSON(
persistedState,
state.instances.options[instanceId].serialize
),
});
}
return;
}
if (msg.name === 'RELAY') {
messaging(msg.message, port.sender!);
}
};
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);
connections.monitor[id] = port;
monitorInstances(true);
monitors++;
port.onDisconnect.addListener(disconnect('monitor', id));
} else {
// devpanel
id = port.name || port.sender!.frameId!;
connections.panel[id] = port;
monitorInstances(true, port.name);
monitors++;
listener = (msg: BackgroundAction) => {
window.store.dispatch(msg);
};
port.onMessage.addListener(listener);
port.onDisconnect.addListener(disconnect('panel', id, listener));
}
}
chrome.runtime.onConnect.addListener(onConnect);
chrome.runtime.onConnectExternal.addListener(onConnect);
chrome.runtime.onMessage.addListener(messaging);
chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener((id) => {
chrome.notifications.clear(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: Dispatch<BackgroundAction>) => (action: BackgroundAction) => {
if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === 'TOGGLE_PERSIST') togglePersist();
return next(action);
};
}

View File

@ -1,9 +1,16 @@
import { Dispatch, MiddlewareAPI } from 'redux';
import { import {
SELECT_INSTANCE, SELECT_INSTANCE,
UPDATE_STATE, UPDATE_STATE,
} from '@redux-devtools/app/lib/constants/actionTypes'; } from '@redux-devtools/app/lib/constants/actionTypes';
import { StoreAction } from '@redux-devtools/app/lib/actions';
import { StoreState } from '@redux-devtools/app/lib/reducers';
function selectInstance(tabId, store, next) { function selectInstance(
tabId: number,
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
next: Dispatch<StoreAction>
) {
const instances = store.getState().instances; const instances = store.getState().instances;
if (instances.current === 'default') return; if (instances.current === 'default') return;
const connections = instances.connections[tabId]; const connections = instances.connections[tabId];
@ -12,7 +19,7 @@ function selectInstance(tabId, store, next) {
} }
} }
function getCurrentTabId(next) { function getCurrentTabId(next: (tabId: number) => void) {
chrome.tabs.query( chrome.tabs.query(
{ {
active: true, active: true,
@ -21,13 +28,15 @@ function getCurrentTabId(next) {
(tabs) => { (tabs) => {
const tab = tabs[0]; const tab = tabs[0];
if (!tab) return; if (!tab) return;
next(tab.id); next(tab.id!);
} }
); );
} }
export default function popupSelector(store) { export default function popupSelector(
return (next) => (action) => { store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>
) {
return (next: Dispatch<StoreAction>) => (action: StoreAction) => {
const result = next(action); const result = next(action);
if (action.type === UPDATE_STATE) { if (action.type === UPDATE_STATE) {
if (chrome.devtools && chrome.devtools.inspectedWindow) { if (chrome.devtools && chrome.devtools.inspectedWindow) {

View File

@ -1,31 +0,0 @@
import {
LIFTED_ACTION,
UPDATE_STATE,
SELECT_INSTANCE,
} from '@redux-devtools/app/lib/constants/actionTypes';
import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances';
function panelDispatcher(bgConnection) {
let autoselected = false;
const tabId = chrome.devtools.inspectedWindow.tabId;
return (store) => (next) => (action) => {
const result = next(action);
if (!autoselected && action.type === UPDATE_STATE && tabId) {
autoselected = true;
const connections = store.getState().instances.connections[tabId];
if (connections && connections.length === 1) {
next({ type: SELECT_INSTANCE, selected: connections[0] });
}
}
if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
bgConnection.postMessage({ ...action, instanceId, id });
}
return result;
};
}
export default panelDispatcher;

View File

@ -0,0 +1,38 @@
import {
LIFTED_ACTION,
UPDATE_STATE,
SELECT_INSTANCE,
} from '@redux-devtools/app/lib/constants/actionTypes';
import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances';
import { Dispatch, MiddlewareAPI } from 'redux';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import { StoreActionWithTogglePersist } from '../stores/windowStore';
function panelDispatcher(bgConnection: chrome.runtime.Port) {
let autoselected = false;
const tabId = chrome.devtools.inspectedWindow.tabId;
return (
store: MiddlewareAPI<Dispatch<StoreActionWithTogglePersist>, StoreState>
) =>
(next: Dispatch<StoreActionWithTogglePersist>) =>
(action: StoreActionWithTogglePersist) => {
const result = next(action);
if (!autoselected && action.type === UPDATE_STATE && tabId) {
autoselected = true;
const connections = store.getState().instances.connections[tabId];
if (connections && connections.length === 1) {
next({ type: SELECT_INSTANCE, selected: connections[0] });
}
}
if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
bgConnection.postMessage({ ...action, instanceId, id });
}
return result;
};
}
export default panelDispatcher;

View File

@ -1,23 +0,0 @@
import {
UPDATE_STATE,
LIFTED_ACTION,
} from '@redux-devtools/app/lib/constants/actionTypes';
import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances';
const syncStores = (baseStore) => (store) => (next) => (action) => {
if (action.type === UPDATE_STATE) {
return next({
...action,
instances: baseStore.getState().instances,
});
}
if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
baseStore.dispatch({ ...action, instanceId, id });
}
return next(action);
};
export default syncStores;

View File

@ -0,0 +1,36 @@
import {
UPDATE_STATE,
LIFTED_ACTION,
} from '@redux-devtools/app/lib/constants/actionTypes';
import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances';
import { Dispatch, MiddlewareAPI, Store } from 'redux';
import { BackgroundState } from '../reducers/background';
import { StoreAction } from '@redux-devtools/app/lib/actions';
import {
WindowStoreAction,
StoreActionWithTogglePersist,
} from '../stores/windowStore';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import { BackgroundAction } from '../stores/backgroundStore';
const syncStores =
(baseStore: Store<BackgroundState, BackgroundAction>) =>
(store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>) =>
(next: Dispatch<WindowStoreAction>) =>
(action: StoreActionWithTogglePersist) => {
if (action.type === UPDATE_STATE) {
return next({
...action,
instances: baseStore.getState().instances,
});
}
if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
baseStore.dispatch({ ...action, instanceId, id } as any);
}
return next(action);
};
export default syncStores;

View File

@ -1,10 +0,0 @@
import { combineReducers } from 'redux';
import instances from '@redux-devtools/app/lib/reducers/instances';
import persistStates from './persistStates';
const rootReducer = combineReducers({
instances,
persistStates,
});
export default rootReducer;

View File

@ -0,0 +1,19 @@
import { combineReducers, Reducer } from 'redux';
import instances, {
InstancesState,
} from '@redux-devtools/app/lib/reducers/instances';
import persistStates from './persistStates';
import { BackgroundAction } from '../../stores/backgroundStore';
export interface BackgroundState {
readonly instances: InstancesState;
readonly persistStates: boolean;
}
const rootReducer: Reducer<BackgroundState, BackgroundAction> =
combineReducers<BackgroundState>({
instances,
persistStates,
});
export default rootReducer;

View File

@ -1,4 +0,0 @@
export default function persistStates(state = false, action) {
if (action.type === 'TOGGLE_PERSIST') return !state;
return state;
}

View File

@ -0,0 +1,6 @@
import { BackgroundAction } from '../../stores/backgroundStore';
export default function persistStates(state = false, action: BackgroundAction) {
if (action.type === 'TOGGLE_PERSIST') return !state;
return state;
}

View File

@ -1,18 +0,0 @@
import { combineReducers } from 'redux';
import instances from '@redux-devtools/app/lib/reducers/instances';
import monitor from '@redux-devtools/app/lib/reducers/monitor';
import notification from '@redux-devtools/app/lib/reducers/notification';
import reports from '@redux-devtools/app/lib/reducers/reports';
import section from '@redux-devtools/app/lib/reducers/section';
import theme from '@redux-devtools/app/lib/reducers/theme';
const rootReducer = combineReducers({
instances,
monitor,
reports,
notification,
section,
theme,
});
export default rootReducer;

View File

@ -0,0 +1,25 @@
import { combineReducers, Reducer } from 'redux';
import instances from '@redux-devtools/app/lib/reducers/instances';
import monitor from '@redux-devtools/app/lib/reducers/monitor';
import notification from '@redux-devtools/app/lib/reducers/notification';
import reports from '@redux-devtools/app/lib/reducers/reports';
import section from '@redux-devtools/app/lib/reducers/section';
import theme from '@redux-devtools/app/lib/reducers/theme';
import connection from '@redux-devtools/app/lib/reducers/connection';
import socket from '@redux-devtools/app/lib/reducers/socket';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import { StoreActionWithTogglePersist } from '../../stores/windowStore';
const rootReducer: Reducer<StoreState, StoreActionWithTogglePersist> =
combineReducers<StoreState>({
instances,
monitor,
reports,
notification,
section,
theme,
connection,
socket,
});
export default rootReducer;

View File

@ -1,4 +1,4 @@
import { combineReducers } from 'redux'; import { combineReducers, Reducer } from 'redux';
import instances from './instances'; import instances from './instances';
import monitor from '@redux-devtools/app/lib/reducers/monitor'; import monitor from '@redux-devtools/app/lib/reducers/monitor';
import notification from '@redux-devtools/app/lib/reducers/notification'; import notification from '@redux-devtools/app/lib/reducers/notification';
@ -6,15 +6,20 @@ import socket from '@redux-devtools/app/lib/reducers/socket';
import reports from '@redux-devtools/app/lib/reducers/reports'; import reports from '@redux-devtools/app/lib/reducers/reports';
import section from '@redux-devtools/app/lib/reducers/section'; import section from '@redux-devtools/app/lib/reducers/section';
import theme from '@redux-devtools/app/lib/reducers/theme'; import theme from '@redux-devtools/app/lib/reducers/theme';
import connection from '@redux-devtools/app/lib/reducers/connection';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import { WindowStoreAction } from '../../stores/windowStore';
const rootReducer = combineReducers({ const rootReducer: Reducer<StoreState, WindowStoreAction> =
instances, combineReducers<StoreState>({
monitor, instances,
socket, monitor,
reports, socket,
notification, reports,
section, notification,
theme, section,
}); theme,
connection,
});
export default rootReducer; export default rootReducer;

View File

@ -7,11 +7,21 @@ import {
SELECT_INSTANCE, SELECT_INSTANCE,
LIFTED_ACTION, LIFTED_ACTION,
} from '@redux-devtools/app/lib/constants/actionTypes'; } from '@redux-devtools/app/lib/constants/actionTypes';
import {
ExpandedUpdateStateAction,
WindowStoreAction,
} from '../../stores/windowStore';
export default function instances(state = initialState, action) { export default function instances(
state = initialState,
action: WindowStoreAction
) {
switch (action.type) { switch (action.type) {
case UPDATE_STATE: case UPDATE_STATE:
return { ...action.instances, selected: state.selected }; return {
...(action as ExpandedUpdateStateAction).instances,
selected: state.selected,
};
case LIFTED_ACTION: case LIFTED_ACTION:
if (action.message === 'DISPATCH') return dispatchAction(state, action); if (action.message === 'DISPATCH') return dispatchAction(state, action);
return state; return state;

View File

@ -1,8 +1,32 @@
export default class Monitor { import { Action } from 'redux';
constructor(update) { import { LiftedState } from '@redux-devtools/instrument';
import { DispatchAction, LibConfig } from '@redux-devtools/app/lib/actions';
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?: LibConfig
) => void;
active?: boolean;
paused?: boolean;
lastAction?: string;
waitingTimeout?: number;
constructor(
update: (
liftedState?: LiftedState<S, A, unknown> | undefined,
libConfig?: LibConfig
) => void
) {
this.update = update; this.update = update;
} }
reducer = (state = {}, action) => { reducer = (state = {}, action: DispatchAction) => {
if (!this.active) return state; if (!this.active) return state;
this.lastAction = action.type; this.lastAction = action.type;
if (action.type === 'LOCK_CHANGES') { if (action.type === 'LOCK_CHANGES') {
@ -15,7 +39,7 @@ export default class Monitor {
} }
return state; return state;
}; };
start = (skipUpdate) => { start = (skipUpdate: boolean) => {
this.active = true; this.active = true;
if (!skipUpdate) this.update(); if (!skipUpdate) this.update();
}; };

View File

@ -1,18 +0,0 @@
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers/background';
import api from '../middlewares/api';
export default function configureStore(preloadedState) {
return createStore(rootReducer, preloadedState, applyMiddleware(api));
/*
let enhancer;
if (process.env.NODE_ENV === 'production') {
enhancer = applyMiddleware(api);
} else {
const logger = require('redux-logger');
enhancer = applyMiddleware(api, logger());
}
return createStore(rootReducer, preloadedState, enhancer);
*/
}

View File

@ -0,0 +1,84 @@
import { createStore, applyMiddleware, PreloadedState } from 'redux';
import rootReducer, { BackgroundState } from '../reducers/background';
import api, { CONNECTED, DISCONNECTED } from '../middlewares/api';
import { LIFTED_ACTION } from '@redux-devtools/app/lib/constants/actionTypes';
import {
CustomAction,
DispatchAction,
StoreActionWithoutLiftedAction,
} from '@redux-devtools/app/lib/actions';
interface LiftedActionActionBase {
action?: DispatchAction | string | CustomAction;
state?: string;
toAll?: boolean;
readonly instanceId: string | number;
readonly id: string | number | undefined;
}
interface LiftedActionDispatchAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'DISPATCH';
action: DispatchAction;
toAll?: boolean;
}
interface LiftedActionImportAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'IMPORT';
state: string;
preloadedState?: unknown | undefined;
action?: never;
}
interface LiftedActionActionAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'ACTION';
action: string | CustomAction;
}
interface LiftedActionExportAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'EXPORT';
toExport: boolean;
action?: never;
}
export type LiftedActionAction =
| LiftedActionDispatchAction
| LiftedActionImportAction
| LiftedActionActionAction
| LiftedActionExportAction;
interface TogglePersistAction {
readonly type: 'TOGGLE_PERSIST';
readonly instanceId: string | number;
readonly id: string | number | undefined;
}
interface ConnectedAction {
readonly type: typeof CONNECTED;
}
interface DisconnectedAction {
readonly type: typeof DISCONNECTED;
}
export type BackgroundAction =
| StoreActionWithoutLiftedAction
| LiftedActionAction
| TogglePersistAction
| ConnectedAction
| DisconnectedAction;
export default function configureStore(
preloadedState?: PreloadedState<BackgroundState>
) {
return createStore(rootReducer, preloadedState, applyMiddleware(api));
/*
let enhancer;
if (process.env.NODE_ENV === 'production') {
enhancer = applyMiddleware(api);
} else {
const logger = require('redux-logger');
enhancer = applyMiddleware(api, logger());
}
return createStore(rootReducer, preloadedState, enhancer);
*/
}

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,31 @@
import { compose } from 'redux'; import { Action, compose, Reducer, StoreEnhancerStoreCreator } from 'redux';
import instrument from '@redux-devtools/instrument'; import instrument from '@redux-devtools/instrument';
import persistState from '@redux-devtools/core/lib/persistState'; import persistState from '@redux-devtools/core/lib/persistState';
import { ConfigWithExpandedMaxAge } from '../../browser/extension/inject/pageScript';
export function getUrlParam(key) { export function getUrlParam(key: string) {
const matches = window.location.href.match( const matches = window.location.href.match(
new RegExp(`[?&]${key}=([^&#]+)\\b`) new RegExp(`[?&]${key}=([^&#]+)\\b`)
); );
return matches && matches.length > 0 ? matches[1] : null; return matches && matches.length > 0 ? matches[1] : null;
} }
export default function configureStore(next, monitorReducer, config) { declare global {
interface Window {
shouldCatchErrors?: boolean;
}
}
export default function configureStore<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
>(
next: StoreEnhancerStoreCreator,
monitorReducer: Reducer<MonitorState, MonitorAction>,
config: ConfigWithExpandedMaxAge
) {
return compose( return compose(
instrument(monitorReducer, { instrument(monitorReducer, {
maxAge: config.maxAge, maxAge: config.maxAge,

View File

@ -1,9 +1,14 @@
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware, PreloadedState } from 'redux';
import exportState from '@redux-devtools/app/lib/middlewares/exportState'; import exportState from '@redux-devtools/app/lib/middlewares/exportState';
import panelDispatcher from '../middlewares/panelSync'; import panelDispatcher from '../middlewares/panelSync';
import rootReducer from '../reducers/panel'; import rootReducer from '../reducers/panel';
import { StoreState } from '@redux-devtools/app/lib/reducers';
export default function configureStore(position, bgConnection, preloadedState) { export default function configureStore(
position: string,
bgConnection: chrome.runtime.Port,
preloadedState: PreloadedState<StoreState>
) {
const enhancer = applyMiddleware(exportState, panelDispatcher(bgConnection)); const enhancer = applyMiddleware(exportState, panelDispatcher(bgConnection));
return createStore(rootReducer, preloadedState, enhancer); return createStore(rootReducer, preloadedState, enhancer);
} }

View File

@ -1,13 +1,51 @@
import { createStore, compose, applyMiddleware } from 'redux'; import {
createStore,
compose,
applyMiddleware,
Store,
PreloadedState,
StoreEnhancer,
} from 'redux';
import exportState from '@redux-devtools/app/lib/middlewares/exportState'; import exportState from '@redux-devtools/app/lib/middlewares/exportState';
import api from '@redux-devtools/app/lib/middlewares/api'; import api from '@redux-devtools/app/lib/middlewares/api';
import { CONNECT_REQUEST } from '@redux-devtools/app/lib/constants/socketActionTypes'; import { CONNECT_REQUEST } from '@redux-devtools/app/lib/constants/socketActionTypes';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import {
StoreAction,
StoreActionWithoutUpdateState,
UpdateStateAction,
} from '@redux-devtools/app/lib/actions';
import { InstancesState } from '@redux-devtools/app/lib/reducers/instances';
import syncStores from '../middlewares/windowSync'; import syncStores from '../middlewares/windowSync';
import instanceSelector from '../middlewares/instanceSelector'; import instanceSelector from '../middlewares/instanceSelector';
import rootReducer from '../reducers/window'; import rootReducer from '../reducers/window';
import { BackgroundState } from '../reducers/background';
import { BackgroundAction } from './backgroundStore';
import { EmptyUpdateStateAction, NAAction } from '../middlewares/api';
export default function configureStore(baseStore, position, preloadedState) { export interface TogglePersistAction {
let enhancer; readonly type: 'TOGGLE_PERSIST';
}
export type StoreActionWithTogglePersist = StoreAction | TogglePersistAction;
export interface ExpandedUpdateStateAction extends UpdateStateAction {
readonly instances: InstancesState;
}
export type WindowStoreAction =
| StoreActionWithoutUpdateState
| TogglePersistAction
| ExpandedUpdateStateAction
| NAAction
| EmptyUpdateStateAction;
export default function configureStore(
baseStore: Store<BackgroundState, BackgroundAction>,
position: string,
preloadedState: PreloadedState<StoreState>
) {
let enhancer: StoreEnhancer;
const middlewares = [exportState, api, syncStores(baseStore)]; const middlewares = [exportState, api, syncStores(baseStore)];
if (!position || position === '#popup') { if (!position || position === '#popup') {
// select current tab instance for devPanel and pageAction // select current tab instance for devPanel and pageAction
@ -20,7 +58,7 @@ export default function configureStore(baseStore, position, preloadedState) {
applyMiddleware(...middlewares), applyMiddleware(...middlewares),
window.__REDUX_DEVTOOLS_EXTENSION__ window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__() ? window.__REDUX_DEVTOOLS_EXTENSION__()
: (noop) => noop : (noop: unknown) => noop
); );
} }
const store = createStore(rootReducer, preloadedState, enhancer); const store = createStore(rootReducer, preloadedState, enhancer);
@ -33,7 +71,7 @@ export default function configureStore(baseStore, position, preloadedState) {
hostname: options['s:hostname'], hostname: options['s:hostname'],
port: options['s:port'], port: options['s:port'],
secure: options['s:secure'], secure: options['s:secure'],
}, } as any,
}); });
}); });

View File

@ -12,10 +12,10 @@ export function createMenu() {
{ id: 'devtools-remote', title: 'Open Remote DevTools' }, { id: 'devtools-remote', title: 'Open Remote DevTools' },
]; ];
let shortcuts = {}; let shortcuts: { [commandName: string]: string | undefined } = {};
chrome.commands.getAll((commands) => { chrome.commands.getAll((commands) => {
commands.forEach(({ name, shortcut }) => { commands.forEach(({ name, shortcut }) => {
shortcuts[name] = shortcut; shortcuts[name!] = shortcut;
}); });
menus.forEach(({ id, title }) => { menus.forEach(({ id, title }) => {

View File

@ -1,11 +1,17 @@
const getIfExists = (sel, template) => import { PreloadedState } from 'redux';
import { StoreState } from '@redux-devtools/app/lib/reducers';
const getIfExists = (sel: any, template: any) =>
typeof sel === 'undefined' || typeof sel === 'undefined' ||
typeof template === 'undefined' || typeof template === 'undefined' ||
typeof template[sel] === 'undefined' typeof template[sel] === 'undefined'
? 0 ? 0
: sel; : sel;
export default function getPreloadedState(position, cb) { export default function getPreloadedState(
position: string,
cb: (state: PreloadedState<StoreState>) => void
) {
chrome.storage.local.get( chrome.storage.local.get(
[ [
'monitor' + position, 'monitor' + position,
@ -28,7 +34,7 @@ export default function getPreloadedState(position, cb) {
), ),
templates: options['test-templates'], templates: options['test-templates'],
}, },
}); } as any);
} }
); );
} }

View File

@ -1,7 +1,17 @@
import configureStore from '../../../app/stores/backgroundStore'; import { Store } from 'redux';
import openDevToolsWindow from './openWindow'; import configureStore, {
BackgroundAction,
} from '../../../app/stores/backgroundStore';
import openDevToolsWindow, { DevToolsPosition } from './openWindow';
import { createMenu, removeMenu } from './contextMenus'; import { createMenu, removeMenu } from './contextMenus';
import syncOptions from '../options/syncOptions'; import syncOptions from '../options/syncOptions';
import { BackgroundState } from '../../../app/reducers/background';
declare global {
interface Window {
store: Store<BackgroundState, BackgroundAction>;
}
}
// Expose the extension's store globally to access it from the windows // Expose the extension's store globally to access it from the windows
// via chrome.runtime.getBackgroundPage // via chrome.runtime.getBackgroundPage
@ -9,7 +19,7 @@ window.store = configureStore();
// Listen for keyboard shortcuts // Listen for keyboard shortcuts
chrome.commands.onCommand.addListener((shortcut) => { chrome.commands.onCommand.addListener((shortcut) => {
openDevToolsWindow(shortcut); openDevToolsWindow(shortcut as DevToolsPosition);
}); });
// Create the context menu when installed // Create the context menu when installed

View File

@ -1,6 +1,10 @@
import { LIFTED_ACTION } from '@redux-devtools/app/lib/constants/actionTypes'; import { LIFTED_ACTION } from '@redux-devtools/app/lib/constants/actionTypes';
export function getReport(reportId, tabId, instanceId) { export function getReport(
reportId: string,
tabId: string | number,
instanceId: number
) {
chrome.storage.local.get(['s:hostname', 's:port', 's:secure'], (options) => { chrome.storage.local.get(['s:hostname', 's:port', 's:secure'], (options) => {
if (!options['s:hostname'] || !options['s:port']) return; if (!options['s:hostname'] || !options['s:port']) return;
const url = `${options['s:secure'] ? 'https' : 'http'}://${ const url = `${options['s:secure'] ? 'https' : 'http'}://${

View File

@ -1,9 +1,20 @@
let windows = {}; export type DevToolsPosition =
let lastPosition = null; | 'devtools-left'
| 'devtools-right'
| 'devtools-bottom'
| 'devtools-panel'
| 'devtools-remote';
export default function openDevToolsWindow(position) { let windows: { [K in DevToolsPosition]?: number } = {};
function popWindow(action, url, customOptions) { let lastPosition: DevToolsPosition | null = null;
function focusIfExist(callback) {
export default function openDevToolsWindow(position: DevToolsPosition) {
function popWindow(
action: string,
url: string,
customOptions: chrome.windows.CreateData & chrome.windows.UpdateInfo
) {
function focusIfExist(callback: () => void) {
if (!windows[position]) { if (!windows[position]) {
callback(); callback();
lastPosition = position; lastPosition = position;
@ -12,7 +23,7 @@ export default function openDevToolsWindow(position) {
if (lastPosition !== position && position !== 'devtools-panel') { if (lastPosition !== position && position !== 'devtools-panel') {
params = { ...params, ...customOptions }; params = { ...params, ...customOptions };
} }
chrome.windows.update(windows[position], params, () => { chrome.windows.update(windows[position]!, params, () => {
lastPosition = null; lastPosition = null;
if (chrome.runtime.lastError) callback(); if (chrome.runtime.lastError) callback();
}); });
@ -20,7 +31,7 @@ export default function openDevToolsWindow(position) {
} }
focusIfExist(() => { focusIfExist(() => {
let options = { let options: chrome.windows.CreateData = {
type: 'popup', type: 'popup',
...customOptions, ...customOptions,
}; };
@ -29,16 +40,19 @@ export default function openDevToolsWindow(position) {
url + '#' + position.substr(position.indexOf('-') + 1) url + '#' + position.substr(position.indexOf('-') + 1)
); );
chrome.windows.create(options, (win) => { chrome.windows.create(options, (win) => {
windows[position] = win.id; windows[position] = win!.id;
if (navigator.userAgent.indexOf('Firefox') !== -1) { if (navigator.userAgent.indexOf('Firefox') !== -1) {
chrome.windows.update(win.id, { focused: true, ...customOptions }); chrome.windows.update(win!.id!, {
focused: true,
...customOptions,
});
} }
}); });
} }
}); });
} }
let params = { let params: chrome.windows.CreateData & chrome.windows.UpdateInfo = {
left: 0, left: 0,
top: 0, top: 0,
width: 380, width: 380,
@ -48,7 +62,9 @@ export default function openDevToolsWindow(position) {
switch (position) { switch (position) {
case 'devtools-right': case 'devtools-right':
params.left = params.left =
window.screen.availLeft + window.screen.availWidth - params.width; (window.screen as unknown as { availLeft: number }).availLeft +
window.screen.availWidth -
params.width!;
break; break;
case 'devtools-bottom': case 'devtools-bottom':
params.height = 420; params.height = 420;

View File

@ -1,48 +1,48 @@
// Mock not supported chrome.* API for Firefox and Electron // Mock not supported chrome.* API for Firefox and Electron
window.isElectron = (window as any).isElectron =
window.navigator && window.navigator.userAgent.indexOf('Electron') !== -1; window.navigator && window.navigator.userAgent.indexOf('Electron') !== -1;
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
// Background page only // Background page only
if ( if (
(window.isElectron && ((window as any).isElectron &&
location.pathname === '/_generated_background_page.html') || location.pathname === '/_generated_background_page.html') ||
isFirefox isFirefox
) { ) {
chrome.runtime.onConnectExternal = { (chrome.runtime as any).onConnectExternal = {
addListener() {}, addListener() {},
}; };
chrome.runtime.onMessageExternal = { (chrome.runtime as any).onMessageExternal = {
addListener() {}, addListener() {},
}; };
if (window.isElectron) { if ((window as any).isElectron) {
chrome.notifications = { (chrome.notifications as any) = {
onClicked: { onClicked: {
addListener() {}, addListener() {},
}, },
create() {}, create() {},
clear() {}, clear() {},
}; };
chrome.contextMenus = { (chrome.contextMenus as any) = {
onClicked: { onClicked: {
addListener() {}, addListener() {},
}, },
}; };
} else { } else {
chrome.storage.sync = chrome.storage.local; (chrome.storage as any).sync = chrome.storage.local;
chrome.runtime.onInstalled = { (chrome.runtime as any).onInstalled = {
addListener: (cb) => cb(), addListener: (cb: any) => cb(),
}; };
} }
} }
if (window.isElectron) { if ((window as any).isElectron) {
if (!chrome.storage.local || !chrome.storage.local.remove) { if (!chrome.storage.local || !chrome.storage.local.remove) {
chrome.storage.local = { (chrome.storage as any).local = {
set(obj, callback) { set(obj: any, callback: any) {
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
localStorage.setItem(key, obj[key]); localStorage.setItem(key, obj[key]);
}); });
@ -50,8 +50,8 @@ if (window.isElectron) {
callback(); callback();
} }
}, },
get(obj, callback) { get(obj: any, callback: any) {
const result = {}; const result: any = {};
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
result[key] = localStorage.getItem(key) || obj[key]; result[key] = localStorage.getItem(key) || obj[key];
}); });
@ -60,7 +60,7 @@ if (window.isElectron) {
} }
}, },
// Electron ~ 1.4.6 // Electron ~ 1.4.6
remove(items, callback) { remove(items: any, callback: any) {
if (Array.isArray(items)) { if (Array.isArray(items)) {
items.forEach((name) => { items.forEach((name) => {
localStorage.removeItem(name); localStorage.removeItem(name);
@ -75,7 +75,7 @@ if (window.isElectron) {
}; };
} }
// Avoid error: chrome.runtime.sendMessage is not supported responseCallback // 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 () { chrome.runtime.sendMessage = function () {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
return originSendMessage(...arguments); return originSendMessage(...arguments);
@ -87,6 +87,6 @@ if (window.isElectron) {
}; };
} }
if (isFirefox || window.isElectron) { if (isFirefox || (window as any).isElectron) {
chrome.storage.sync = chrome.storage.local; (chrome.storage as any).sync = chrome.storage.local;
} }

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { CSSProperties } from 'react';
import { render, unmountComponentAtNode } from 'react-dom'; import { render, unmountComponentAtNode } from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { REMOVE_INSTANCE } from '@redux-devtools/app/lib/constants/actionTypes'; import { REMOVE_INSTANCE } from '@redux-devtools/app/lib/constants/actionTypes';
@ -7,15 +7,23 @@ import configureStore from '../../../app/stores/panelStore';
import getPreloadedState from '../background/getPreloadedState'; import getPreloadedState from '../background/getPreloadedState';
import '../../views/devpanel.pug'; import '../../views/devpanel.pug';
import { Action, PreloadedState, Store } from 'redux';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import { PanelMessage } from '../../../app/middlewares/api';
import { StoreActionWithTogglePersist } from '../../../app/stores/windowStore';
const position = location.hash; const position = location.hash;
const messageStyle = { padding: '20px', width: '100%', textAlign: 'center' }; const messageStyle: CSSProperties = {
padding: '20px',
width: '100%',
textAlign: 'center',
};
let rendered; let rendered: boolean | undefined;
let store; let store: Store<StoreState, StoreActionWithTogglePersist> | undefined;
let bgConnection; let bgConnection: chrome.runtime.Port;
let naTimeout; let naTimeout: NodeJS.Timeout;
let preloadedState; let preloadedState: PreloadedState<StoreState>;
const isChrome = navigator.userAgent.indexOf('Firefox') === -1; const isChrome = navigator.userAgent.indexOf('Firefox') === -1;
@ -25,7 +33,7 @@ getPreloadedState(position, (state) => {
function renderDevTools() { function renderDevTools() {
const node = document.getElementById('root'); const node = document.getElementById('root');
unmountComponentAtNode(node); unmountComponentAtNode(node!);
clearTimeout(naTimeout); clearTimeout(naTimeout);
store = configureStore(position, bgConnection, preloadedState); store = configureStore(position, bgConnection, preloadedState);
render( render(
@ -71,33 +79,35 @@ function renderNA() {
} }
const node = document.getElementById('root'); const node = document.getElementById('root');
unmountComponentAtNode(node); unmountComponentAtNode(node!);
render(message, node); render(message, node);
store = undefined; store = undefined;
}); });
} else { } else {
const node = document.getElementById('root'); const node = document.getElementById('root');
unmountComponentAtNode(node); unmountComponentAtNode(node!);
render(message, node); render(message, node);
store = undefined; store = undefined;
} }
}, 3500); }, 3500);
} }
function init(id) { function init(id: number) {
renderNA(); renderNA();
bgConnection = chrome.runtime.connect({ bgConnection = chrome.runtime.connect({
name: id ? id.toString() : undefined, name: id ? id.toString() : undefined,
}); });
bgConnection.onMessage.addListener((message) => { bgConnection.onMessage.addListener(
if (message.type === 'NA') { <S, A extends Action<unknown>>(message: PanelMessage<S, A>) => {
if (message.id === id) renderNA(); if (message.type === 'NA') {
else store.dispatch({ type: REMOVE_INSTANCE, id: message.id }); if (message.id === id) renderNA();
} else { else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id });
if (!rendered) renderDevTools(); } else {
store.dispatch(message); if (!rendered) renderDevTools();
store!.dispatch(message);
}
} }
}); );
} }
init(chrome.devtools.inspectedWindow.tabId); init(chrome.devtools.inspectedWindow.tabId);

View File

@ -1,6 +1,6 @@
import '../../views/devtools.pug'; import '../../views/devtools.pug';
function createPanel(url) { function createPanel(url: string) {
chrome.devtools.panels.create( chrome.devtools.panels.create(
'Redux', 'Redux',
'img/logo/scalable.png', 'img/logo/scalable.png',

View File

@ -1,132 +0,0 @@
import {
injectOptions,
getOptionsFromBg,
isAllowed,
} from '../options/syncOptions';
const source = '@devtools-extension';
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;
function connect() {
// Connect to the background script
connected = true;
const name = 'tab';
if (window.devToolsExtensionID) {
bg = chrome.runtime.connect(window.devToolsExtensionID, { name });
} else {
bg = chrome.runtime.connect({ name });
}
// Relay background script messages to the page script
bg.onMessage.addListener((message) => {
if (message.action) {
window.postMessage(
{
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
},
'*'
);
} else if (message.options) {
injectOptions(message.options);
} else {
window.postMessage(
{
type: message.type,
state: message.state,
id: message.id,
source,
},
'*'
);
}
});
bg.onDisconnect.addListener(handleDisconnect);
}
function handleDisconnect() {
window.removeEventListener('message', handleMessages);
window.postMessage({ type: 'STOP', failed: true, source }, '*');
bg = undefined;
}
function tryCatch(fn, args) {
try {
return fn(args);
} catch (err) {
if (err.message === 'Message length exceeded maximum allowed length.') {
const instanceId = args.instanceId;
const newArgs = { split: 'start' };
const toSplit = [];
let size = 0;
let arg;
Object.keys(args).map((key) => {
arg = args[key];
if (typeof arg === 'string') {
size += arg.length;
if (size > maxChromeMsgSize) {
toSplit.push([key, arg]);
return;
}
}
newArgs[key] = arg;
});
fn(newArgs);
for (let i = 0; i < toSplit.length; i++) {
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
fn({
instanceId,
source: pageSource,
split: 'chunk',
chunk: [toSplit[i][0], toSplit[i][1].substr(j, maxChromeMsgSize)],
});
}
}
return fn({ instanceId, source: pageSource, split: 'end' });
}
handleDisconnect();
/* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') {
console.error('Failed to send message', err);
}
/* eslint-enable no-console */
}
}
function send(message) {
if (!connected) connect();
if (message.type === 'INIT_INSTANCE') {
getOptionsFromBg();
bg.postMessage({ name: 'INIT_INSTANCE', instanceId: message.instanceId });
} else {
bg.postMessage({ name: 'RELAY', message });
}
}
// Resend messages from the page to the background script
function handleMessages(event) {
if (!isAllowed()) return;
if (!event || event.source !== window || typeof event.data !== 'object') {
return;
}
const message = event.data;
if (message.source !== pageSource) return;
if (message.type === 'DISCONNECT') {
if (bg) {
bg.disconnect();
connected = false;
}
return;
}
tryCatch(send, message);
}
window.addEventListener('message', handleMessages, false);

View File

@ -0,0 +1,313 @@
import {
injectOptions,
getOptionsFromBg,
isAllowed,
} from '../options/syncOptions';
import { TabMessage } from '../../../app/middlewares/api';
import {
PageScriptToContentScriptMessage,
PageScriptToContentScriptMessageWithoutDisconnect,
PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance,
} from '../../../app/api';
import { Action } from 'redux';
import {
CustomAction,
DispatchAction as AppDispatchAction,
} from '@redux-devtools/app/lib/actions';
import { LiftedState } from '@redux-devtools/instrument';
const source = '@devtools-extension';
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: chrome.runtime.Port | undefined;
declare global {
interface Window {
devToolsExtensionID?: string;
}
}
interface StartAction {
readonly type: 'START';
readonly state: undefined;
readonly id: undefined;
readonly source: typeof source;
}
interface StopAction {
readonly type: 'STOP';
readonly state: undefined;
readonly id: undefined;
readonly source: typeof source;
readonly failed?: boolean;
}
interface DispatchAction {
readonly type: 'DISPATCH';
readonly payload: AppDispatchAction;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface ImportAction {
readonly type: 'IMPORT';
readonly payload: undefined;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface ActionAction {
readonly type: 'ACTION';
readonly payload: string | CustomAction;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface ExportAction {
readonly type: 'EXPORT';
readonly payload: undefined;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface UpdateAction {
readonly type: 'UPDATE';
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
export type ContentScriptToPageScriptMessage =
| StartAction
| StopAction
| DispatchAction
| ImportAction
| ActionAction
| ExportAction
| UpdateAction;
interface ImportStatePayload<S, A extends Action<unknown>> {
readonly type: 'IMPORT_STATE';
readonly nextLiftedState: LiftedState<S, A, unknown> | readonly A[];
readonly preloadedState?: S;
}
interface ImportStateDispatchAction<S, A extends Action<unknown>> {
readonly type: 'DISPATCH';
readonly payload: ImportStatePayload<S, A>;
}
export type ListenerMessage<S, A extends Action<unknown>> =
| StartAction
| StopAction
| DispatchAction
| ImportAction
| ActionAction
| ExportAction
| UpdateAction
| ImportStateDispatchAction<S, A>;
function postToPageScript(message: ContentScriptToPageScriptMessage) {
window.postMessage(message, '*');
}
function connect() {
// Connect to the background script
connected = true;
const name = 'tab';
if (window.devToolsExtensionID) {
bg = chrome.runtime.connect(window.devToolsExtensionID, { name });
} else {
bg = chrome.runtime.connect({ name });
}
// Relay background script messages to the page script
bg.onMessage.addListener((message: TabMessage) => {
if ('action' in message) {
if (message.type === 'DISPATCH') {
postToPageScript({
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
});
} else if (message.type === 'ACTION') {
postToPageScript({
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
});
} else {
postToPageScript({
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
});
}
} else if ('options' in message) {
injectOptions(message.options);
} else {
postToPageScript({
type: message.type,
state: message.state,
id: message.id,
source,
});
}
});
bg.onDisconnect.addListener(handleDisconnect);
}
function handleDisconnect() {
window.removeEventListener('message', handleMessages);
window.postMessage({ type: 'STOP', failed: true, source }, '*');
bg = undefined;
}
interface SplitMessageBase {
readonly type?: never;
}
interface SplitMessageStart extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'start';
}
interface SplitMessageChunk extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'chunk';
readonly chunk: [string, string];
}
interface SplitMessageEnd extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'end';
}
export type SplitMessage =
| SplitMessageStart
| SplitMessageChunk
| SplitMessageEnd;
function tryCatch<S, A extends Action<unknown>>(
fn: (
args: PageScriptToContentScriptMessageWithoutDisconnect<S, A> | SplitMessage
) => void,
args: PageScriptToContentScriptMessageWithoutDisconnect<S, A>
) {
try {
return fn(args);
} catch (err) {
if (err.message === 'Message length exceeded maximum allowed length.') {
const instanceId = (args as any).instanceId;
const newArgs = {
split: 'start',
};
const toSplit: [string, string][] = [];
let size = 0;
let arg;
Object.keys(args).map((key) => {
arg = args[key as keyof typeof args];
if (typeof arg === 'string') {
size += arg.length;
if (size > maxChromeMsgSize) {
toSplit.push([key, arg]);
return;
}
}
newArgs[key as keyof typeof newArgs] = arg;
});
fn(newArgs as any);
for (let i = 0; i < toSplit.length; i++) {
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
fn({
instanceId,
source: pageSource,
split: 'chunk',
chunk: [toSplit[i][0], toSplit[i][1].substr(j, maxChromeMsgSize)],
});
}
}
return fn({ instanceId, source: pageSource, split: 'end' });
}
handleDisconnect();
/* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') {
console.error('Failed to send message', err);
}
/* eslint-enable no-console */
}
}
interface InitInstanceContentScriptToBackgroundMessage {
readonly name: 'INIT_INSTANCE';
readonly instanceId: number;
}
interface RelayMessage<S, A extends Action<unknown>> {
readonly name: 'RELAY';
readonly message:
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage;
}
export type ContentScriptToBackgroundMessage<S, A extends Action<unknown>> =
| InitInstanceContentScriptToBackgroundMessage
| RelayMessage<S, A>;
function postToBackground<S, A extends Action<unknown>>(
message: ContentScriptToBackgroundMessage<S, A>
) {
bg!.postMessage(message);
}
function send<S, A extends Action<unknown>>(
message:
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| SplitMessage
) {
if (!connected) connect();
if (message.type === 'INIT_INSTANCE') {
getOptionsFromBg();
postToBackground({ name: 'INIT_INSTANCE', instanceId: message.instanceId });
} else {
postToBackground({ name: 'RELAY', message });
}
}
// Resend messages from the page to the background script
function handleMessages<S, A extends Action<unknown>>(
event: MessageEvent<PageScriptToContentScriptMessage<S, A>>
) {
if (!isAllowed()) return;
if (!event || event.source !== window || typeof event.data !== 'object') {
return;
}
const message = event.data;
if (message.source !== pageSource) return;
if (message.type === 'DISCONNECT') {
if (bg) {
bg.disconnect();
connected = false;
}
return;
}
tryCatch(send, message);
}
window.addEventListener('message', handleMessages, false);

View File

@ -1,6 +1,8 @@
// Include this script in Chrome apps and extensions for remote debugging // Include this script in Chrome apps and extensions for remote debugging
// <script src="chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.bundle.js"></script> // <script src="chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.bundle.js"></script>
import { Options } from '../options/syncOptions';
window.devToolsExtensionID = 'lmhkpmbekcpmknklioeibfkpmmfibljd'; window.devToolsExtensionID = 'lmhkpmbekcpmknklioeibfkpmmfibljd';
require('./contentScript'); require('./contentScript');
require('./pageScript'); require('./pageScript');
@ -8,7 +10,7 @@ require('./pageScript');
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
window.devToolsExtensionID, window.devToolsExtensionID,
{ type: 'GET_OPTIONS' }, { type: 'GET_OPTIONS' },
function (response) { function (response: { readonly options: Options }) {
if (!response.options.inject) { if (!response.options.inject) {
const urls = response.options.urls.split('\n').filter(Boolean).join('|'); const urls = response.options.urls.split('\n').filter(Boolean).join('|');
if (!location.href.match(new RegExp(urls))) return; if (!location.href.match(new RegExp(urls))) return;

View File

@ -1,486 +0,0 @@
import { getActionsArray, evalAction } from '@redux-devtools/utils';
import throttle from 'lodash/throttle';
import createStore from '../../../app/stores/createStore';
import configureStore, { getUrlParam } from '../../../app/stores/enhancerStore';
import { isAllowed } from '../options/syncOptions';
import Monitor from '../../../app/service/Monitor';
import {
noFiltersApplied,
getLocalFilter,
isFiltered,
filterState,
startingFrom,
} from '../../../app/api/filters';
import notifyErrors from '../../../app/api/notifyErrors';
import importState from '../../../app/api/importState';
import openWindow from '../../../app/api/openWindow';
import generateId from '../../../app/api/generateInstanceId';
import {
updateStore,
toContentScript,
sendMessage,
setListener,
connect,
disconnect,
isInIframe,
getSeralizeParameter,
} from '../../../app/api';
const source = '@devtools-page';
let stores = {};
let reportId;
function deprecateParam(oldParam, newParam) {
/* 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`
);
/* eslint-enable no-console */
}
const __REDUX_DEVTOOLS_EXTENSION__ = function (
reducer,
preloadedState,
config
) {
/* eslint-disable no-param-reassign */
if (typeof reducer === 'object') {
config = reducer;
reducer = undefined;
} else if (typeof config !== 'object') config = {};
/* eslint-enable no-param-reassign */
if (!window.devToolsOptions) window.devToolsOptions = {};
let store;
let errorOccurred = false;
let maxAge;
let actionCreators;
let sendingActionId = 1;
const instanceId = generateId(config.instanceId);
const localFilter = getLocalFilter(config);
const serializeState = getSeralizeParameter(config, 'serializeState');
const serializeAction = getSeralizeParameter(config, 'serializeAction');
let {
statesFilter,
actionsFilter,
stateSanitizer,
actionSanitizer,
predicate,
latency = 500,
} = config;
// Deprecate statesFilter and actionsFilter
if (statesFilter) {
deprecateParam('statesFilter', 'stateSanitizer');
stateSanitizer = statesFilter; // eslint-disable-line no-param-reassign
}
if (actionsFilter) {
deprecateParam('actionsFilter', 'actionSanitizer');
actionSanitizer = actionsFilter; // eslint-disable-line no-param-reassign
}
const monitor = new Monitor(relayState);
if (config.getMonitor) {
/* eslint-disable no-console */
console.warn(
"Redux DevTools extension's `getMonitor` parameter is deprecated and will be not " +
'supported in the next version, please remove it and just use ' +
'`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead: ' +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'
);
/* eslint-enable no-console */
config.getMonitor(monitor);
}
function exportState() {
const liftedState = store.liftedStore.getState();
const actionsById = liftedState.actionsById;
const payload = [];
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
);
}
function relay(type, state, action, nextActionId, libConfig) {
const message = {
type,
payload: filterState(
state,
type,
localFilter,
stateSanitizer,
actionSanitizer,
nextActionId,
predicate
),
source,
instanceId,
};
if (type === 'ACTION') {
message.action = !actionSanitizer
? action
: actionSanitizer(action.action, nextActionId - 1);
message.maxAge = getMaxAge();
message.nextActionId = nextActionId;
} else if (libConfig) {
message.libConfig = libConfig;
}
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;
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;
relay(
'ACTION',
state,
liftedState.actionsById[nextActionId - 1],
nextActionId
);
return;
}
// Send multiple actions
const payload = startingFrom(
sendingActionId,
liftedState,
localFilter,
stateSanitizer,
actionSanitizer,
predicate
);
sendingActionId = nextActionId;
if (typeof payload === 'undefined') return;
if (typeof payload.skippedActionIds !== 'undefined') {
relay('STATE', payload);
return;
}
toContentScript(
{
type: 'PARTIAL_STATE',
payload,
source,
instanceId,
maxAge: getMaxAge(),
},
serializeState,
serializeAction
);
}, latency);
function dispatchRemotely(action) {
if (config.features && !config.features.dispatch) return;
try {
const result = evalAction(action, actionCreators);
(store.initialDispatch || store.dispatch)(result);
} catch (e) {
relay('ERROR', e.message);
}
}
function importPayloadFrom(state) {
if (config.features && !config.features.import) return;
try {
const nextLiftedState = importState(state, config);
if (!nextLiftedState) return;
store.liftedStore.dispatch({ type: 'IMPORT_STATE', ...nextLiftedState });
} catch (e) {
relay('ERROR', e.message);
}
}
function dispatchMonitorAction(action) {
const type = action.type;
const features = config.features;
if (features) {
if (
!features.jump &&
(type === 'JUMP_TO_STATE' || type === 'JUMP_TO_ACTION')
) {
return;
}
if (!features.skip && type === 'TOGGLE_ACTION') return;
if (!features.reorder && type === 'REORDER_ACTION') return;
if (!features.import && type === 'IMPORT_STATE') return;
if (!features.lock && type === 'LOCK_CHANGES') return;
if (!features.pause && type === 'PAUSE_RECORDING') return;
}
if (type === 'JUMP_TO_STATE') {
const liftedState = store.liftedStore.getState();
const index = liftedState.stagedActionIds.indexOf(action.actionId);
if (index === -1) return;
store.liftedStore.dispatch({ type, index });
return;
}
store.liftedStore.dispatch(action);
}
function onMessage(message) {
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) {
relay('GET_REPORT', reportId);
reportId = null;
}
return;
case 'STOP':
monitor.stop();
relayAction.cancel();
relayState.cancel();
if (!message.failed) relay('STOP');
}
}
const filteredActionIds = []; // simple circular buffer of non-excluded actions with fixed maxAge-1 length
const getMaxAge = (liftedAction, liftedState) => {
let m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50;
if (
!liftedAction ||
noFiltersApplied(localFilter) ||
!liftedAction.action
) {
return m;
}
if (!maxAge || maxAge < m) maxAge = m; // it can be modified in process on options page
if (isFiltered(liftedAction.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;
});
relay('INIT_INSTANCE');
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 = () => (next) => {
return (reducer_, initialState_, enhancer_) => {
if (!isAllowed(window.devToolsOptions)) {
return next(reducer_, initialState_, enhancer_);
}
store = stores[instanceId] = configureStore(next, monitor.reducer, {
...config,
maxAge: getMaxAge,
})(reducer_, initialState_, enhancer_);
if (isInIframe()) setTimeout(init, 3000);
else init();
return store;
};
};
if (!reducer) return enhance();
/* eslint-disable no-console */
console.warn(
'Creating a Redux store directly from DevTools extension is discouraged and will not be supported in future major version. For more details see: https://git.io/fphCe'
);
/* eslint-enable no-console */
return createStore(reducer, preloadedState, enhance);
};
// noinspection JSAnnotator
window.__REDUX_DEVTOOLS_EXTENSION__ = __REDUX_DEVTOOLS_EXTENSION__;
window.__REDUX_DEVTOOLS_EXTENSION__.open = openWindow;
window.__REDUX_DEVTOOLS_EXTENSION__.updateStore = updateStore(stores);
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;
// 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);
if (stores[instanceId]) {
stores[instanceId].initialDispatch = store.dispatch;
}
return {
...store,
dispatch: (...args) =>
!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__ && store.dispatch(...args),
};
};
const extensionCompose =
(config) =>
(...funcs) => {
return (...args) => {
const instanceId = generateId(config.instanceId);
return [preEnhancer(instanceId), ...funcs].reduceRight(
(composed, f) => f(composed),
__REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId })(...args)
);
};
};
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = (...funcs) => {
if (funcs.length === 0) {
return __REDUX_DEVTOOLS_EXTENSION__();
}
if (funcs.length === 1 && typeof funcs[0] === 'object') {
return extensionCompose(funcs[0]);
}
return extensionCompose({})(...funcs);
};

View File

@ -0,0 +1,681 @@
import {
getActionsArray,
evalAction,
ActionCreatorObject,
} from '@redux-devtools/utils';
import throttle from 'lodash/throttle';
import {
Action,
ActionCreator,
Dispatch,
PreloadedState,
Reducer,
Store,
StoreEnhancer,
StoreEnhancerStoreCreator,
} from 'redux';
import Immutable from 'immutable';
import { EnhancedStore, PerformAction } from '@redux-devtools/instrument';
import createStore from '../../../app/stores/createStore';
import configureStore, { getUrlParam } from '../../../app/stores/enhancerStore';
import { isAllowed, Options } from '../options/syncOptions';
import Monitor from '../../../app/service/Monitor';
import {
noFiltersApplied,
getLocalFilter,
isFiltered,
filterState,
startingFrom,
} from '../../../app/api/filters';
import notifyErrors from '../../../app/api/notifyErrors';
import importState from '../../../app/api/importState';
import openWindow, { Position } from '../../../app/api/openWindow';
import generateId from '../../../app/api/generateInstanceId';
import {
updateStore,
toContentScript,
sendMessage,
setListener,
connect,
disconnect,
isInIframe,
getSerializeParameter,
Serialize,
StructuralPerformAction,
ConnectResponse,
} from '../../../app/api';
import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
import {
CustomAction,
DispatchAction,
LibConfig,
} from '@redux-devtools/app/lib/actions';
import { ContentScriptToPageScriptMessage } from './contentScript';
import { Features } from '@redux-devtools/app/lib/reducers/instances';
type EnhancedStoreWithInitialDispatch<
S,
A extends Action<unknown>,
MonitorState
> = EnhancedStore<S, A, MonitorState> & { initialDispatch: Dispatch<A> };
const source = '@devtools-page';
let stores: {
[K in string | number]: EnhancedStoreWithInitialDispatch<
unknown,
Action<unknown>,
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/zalmoxisus/redux-devtools-extension/blob/master/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;
readonly actionsBlacklist?: string | readonly string[];
readonly actionsWhitelist?: string | readonly string[];
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 | (() => 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;
name?: string;
readonly autoPause?: boolean;
readonly features?: Features;
readonly type?: string;
readonly getActionType?: <A extends Action<unknown>>(action: A) => A;
readonly actionCreators?: {
readonly [key: string]: ActionCreator<Action<unknown>>;
};
}
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;
updateStore: (
newStore: EnhancedStore<unknown, Action<unknown>, unknown>,
instanceId: number
) => void;
notifyErrors: (onError?: () => boolean) => void;
send: <S, A extends Action<unknown>>(
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<unknown>>(
reducer?: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
config?: Config
): Store<S, A>;
function __REDUX_DEVTOOLS_EXTENSION__(config: Config): StoreEnhancer;
function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<unknown>>(
reducer?: Reducer<S, A> | Config | undefined,
preloadedState?: PreloadedState<S>,
config?: Config
): Store<S, A> | StoreEnhancer {
/* eslint-disable no-param-reassign */
if (typeof reducer === 'object') {
config = reducer;
reducer = undefined;
} else 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, 'serializeState');
const serializeAction = getSerializeParameter(config, 'serializeAction');
let {
statesFilter,
actionsFilter,
stateSanitizer,
actionSanitizer,
predicate,
latency = 500,
} = config;
// Deprecate statesFilter and actionsFilter
if (statesFilter) {
deprecateParam('statesFilter', 'stateSanitizer');
stateSanitizer = statesFilter; // eslint-disable-line no-param-reassign
}
if (actionsFilter) {
deprecateParam('actionsFilter', 'actionSanitizer');
actionSanitizer = actionsFilter; // eslint-disable-line no-param-reassign
}
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);
if (config.getMonitor) {
/* eslint-disable no-console */
console.warn(
"Redux DevTools extension's `getMonitor` parameter is deprecated and will be not " +
'supported in the next version, please remove it and just use ' +
'`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead: ' +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'
);
/* eslint-enable no-console */
config.getMonitor(monitor);
}
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.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.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;
}
if (action.type === 'JUMP_TO_STATE') {
const liftedState = store.liftedStore.getState();
const index = liftedState.stagedActionIds.indexOf(action.actionId);
if (index === -1) return;
store.liftedStore.dispatch({ type: action.type, index });
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
);
}
}
}
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 =>
<NextExt, NextStateExt>(
next: StoreEnhancerStoreCreator<NextExt, NextStateExt>
): any => {
return <S2 extends S, A2 extends A>(
reducer_: Reducer<S2, A2>,
initialState_?: PreloadedState<S2>
) => {
if (!isAllowed(window.devToolsOptions)) {
return next(reducer_, initialState_);
}
store = stores[instanceId] = configureStore(next, monitor.reducer, {
...config,
maxAge: getMaxAge as any,
})(reducer_, initialState_) as any;
if (isInIframe()) setTimeout(init, 3000);
else init();
return store;
};
};
if (!reducer) return enhance();
/* eslint-disable no-console */
console.warn(
'Creating a Redux store directly from DevTools extension is discouraged and will not be supported in future major version. For more details see: https://git.io/fphCe'
);
/* 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__ as any;
window.__REDUX_DEVTOOLS_EXTENSION__.open = openWindow;
window.__REDUX_DEVTOOLS_EXTENSION__.updateStore = updateStore(stores);
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;
};
const extensionCompose =
(config: Config) =>
(...funcs: StoreEnhancer[]) => {
return (...args: any[]) => {
const instanceId = generateId(config.instanceId);
return [preEnhancer(instanceId), ...funcs].reduceRight(
(composed, f) => f(composed),
(__REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId }) as any)(
...args
)
);
};
};
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: unknown;
}
}
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = (...funcs: any[]) => {
if (funcs.length === 0) {
return __REDUX_DEVTOOLS_EXTENSION__();
}
if (funcs.length === 1 && typeof funcs[0] === 'object') {
return extensionCompose(funcs[0]);
}
return extensionCompose({})(...funcs);
};

View File

@ -5,11 +5,13 @@ if (process.env.NODE_ENV === 'production') {
const { default: script } = require('raw-loader!tmp/page.bundle.js'); const { default: script } = require('raw-loader!tmp/page.bundle.js');
s.appendChild(document.createTextNode(script)); s.appendChild(document.createTextNode(script));
(document.head || document.documentElement).appendChild(s); (document.head || document.documentElement).appendChild(s);
s.parentNode.removeChild(s); s.parentNode!.removeChild(s);
} else { } else {
s.src = chrome.extension.getURL('page.bundle.js'); s.src = chrome.extension.getURL('page.bundle.js');
s.onload = function () { s.onload = function () {
this.parentNode.removeChild(this); (this as HTMLScriptElement).parentNode!.removeChild(
this as HTMLScriptElement
);
}; };
(document.head || document.documentElement).appendChild(s); (document.head || document.documentElement).appendChild(s);
} }

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { OptionsProps } from './Options';
export default ({ options, saveOption }) => { export default ({ options, saveOption }: OptionsProps) => {
const AllowToRunState = { const AllowToRunState = {
EVERYWHERE: true, EVERYWHERE: true,
ON_SPECIFIC_URLS: false, ON_SPECIFIC_URLS: false,

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { OptionsProps } from './Options';
export default ({ options, saveOption }) => { export default ({ options, saveOption }: OptionsProps) => {
return ( return (
<fieldset className="option-group"> <fieldset className="option-group">
<legend className="option-group__title">Context Menu</legend> <legend className="option-group__title">Context Menu</legend>

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { OptionsProps } from './Options';
export default ({ options, saveOption }) => { export default ({ options, saveOption }: OptionsProps) => {
const EditorState = { const EditorState = {
BROWSER: 0, BROWSER: 0,
EXTERNAL: 1, EXTERNAL: 1,
@ -45,7 +46,7 @@ export default ({ options, saveOption }) => {
className="option__element" className="option__element"
id="editor" id="editor"
type="text" type="text"
size="33" size={33}
maxLength={30} maxLength={30}
placeholder="vscode, atom, webstorm, sublime..." placeholder="vscode, atom, webstorm, sublime..."
value={options.editor} value={options.editor}

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { FilterState } from '../../../app/api/filters'; import { FilterState } from '../../../app/api/filters';
import { OptionsProps } from './Options';
export default ({ options, saveOption }) => { export default ({ options, saveOption }: OptionsProps) => {
return ( return (
<fieldset className="option-group"> <fieldset className="option-group">
<legend className="option-group__title"> <legend className="option-group__title">

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { OptionsProps } from './Options';
export default ({ options, saveOption }) => { export default ({ options, saveOption }: OptionsProps) => {
const browserName = navigator.userAgent.includes('Firefox') const browserName = navigator.userAgent.includes('Firefox')
? 'Firefox' ? 'Firefox'
: 'Chrome'; : 'Chrome';

View File

@ -4,8 +4,17 @@ import FilterGroup from './FilterGroup';
import AllowToRunGroup from './AllowToRunGroup'; import AllowToRunGroup from './AllowToRunGroup';
import MiscellaneousGroup from './MiscellaneousGroup'; import MiscellaneousGroup from './MiscellaneousGroup';
import ContextMenuGroup from './ContextMenuGroup'; import ContextMenuGroup from './ContextMenuGroup';
import { Options } from './syncOptions';
export default (props) => ( export interface OptionsProps {
readonly options: Options;
readonly saveOption: <K extends keyof Options>(
name: K,
value: Options[K]
) => void;
}
export default (props: OptionsProps) => (
<div> <div>
<EditorGroup {...props} /> <EditorGroup {...props} />
<FilterGroup {...props} /> <FilterGroup {...props} />

View File

@ -1,19 +1,20 @@
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import Options from './Options'; import OptionsComponent from './Options';
import { Options } from './syncOptions';
import '../../views/options.pug'; import '../../views/options.pug';
chrome.runtime.getBackgroundPage((background) => { chrome.runtime.getBackgroundPage((background) => {
const syncOptions = background.syncOptions; const syncOptions = background!.syncOptions;
const saveOption = (name, value) => { const saveOption = <K extends keyof Options>(name: K, value: Options[K]) => {
syncOptions.save(name, value); syncOptions.save(name, value);
}; };
const renderOptions = (options) => { const renderOptions = (options: Options) => {
render( render(
<Options options={options} saveOption={saveOption} />, <OptionsComponent options={options} saveOption={saveOption} />,
document.getElementById('root') document.getElementById('root')
); );
}; };

View File

@ -1,108 +0,0 @@
import { FilterState } from '../../../app/api/filters';
let options;
let subscribers = [];
const save = (toAllTabs) => (key, value) => {
let obj = {};
obj[key] = value;
chrome.storage.sync.set(obj);
options[key] = value;
toAllTabs({ options: options });
subscribers.forEach((s) => s(options));
};
const migrateOldOptions = (oldOptions) => {
let newOptions = Object.assign({}, oldOptions);
// Migrate the old `filter` option from 2.2.1
if (typeof oldOptions.filter === 'boolean') {
if (oldOptions.filter && oldOptions.whitelist.length > 0) {
newOptions.filter = FilterState.WHITELIST_SPECIFIC;
} else if (oldOptions.filter) {
newOptions.filter = FilterState.BLACKLIST_SPECIFIC;
} else {
newOptions.filter = FilterState.DO_NOT_FILTER;
}
}
return newOptions;
};
const get = (callback) => {
if (options) callback(options);
else {
chrome.storage.sync.get(
{
useEditor: 0,
editor: '',
projectPath: '',
maxAge: 50,
filter: FilterState.DO_NOT_FILTER,
whitelist: '',
blacklist: '',
shouldCatchErrors: false,
inject: true,
urls: '^https?://localhost|0\\.0\\.0\\.0:\\d+\n^https?://.+\\.github\\.io',
showContextMenus: true,
},
function (items) {
options = migrateOldOptions(items);
callback(options);
}
);
}
};
const subscribe = (callback) => {
subscribers = subscribers.concat(callback);
};
const toReg = (str) =>
str !== '' ? str.split('\n').filter(Boolean).join('|') : null;
export const injectOptions = (newOptions) => {
if (!newOptions) return;
if (newOptions.filter !== FilterState.DO_NOT_FILTER) {
newOptions.whitelist = toReg(newOptions.whitelist);
newOptions.blacklist = toReg(newOptions.blacklist);
}
options = newOptions;
let s = document.createElement('script');
s.type = 'text/javascript';
s.appendChild(
document.createTextNode(
'window.devToolsOptions = Object.assign(window.devToolsOptions||{},' +
JSON.stringify(options) +
');'
)
);
(document.head || document.documentElement).appendChild(s);
s.parentNode.removeChild(s);
};
export const getOptionsFromBg = () => {
/* chrome.runtime.sendMessage({ type: 'GET_OPTIONS' }, response => {
if (response && response.options) injectOptions(response.options);
});
*/
get((newOptions) => {
injectOptions(newOptions);
}); // Legacy
};
export const isAllowed = (localOptions = options) =>
!localOptions ||
localOptions.inject ||
!localOptions.urls ||
location.href.match(toReg(localOptions.urls));
export default function syncOptions(toAllTabs) {
if (toAllTabs && !options) get(() => {}); // Initialize
return {
save: save(toAllTabs),
get: get,
subscribe: subscribe,
};
}

View File

@ -0,0 +1,152 @@
import { FilterState, FilterStateValue } from '../../../app/api/filters';
export interface Options {
readonly useEditor: number;
readonly editor: string;
readonly projectPath: string;
readonly maxAge: number;
readonly filter: FilterStateValue;
readonly whitelist: string;
readonly blacklist: string;
readonly shouldCatchErrors: boolean;
readonly inject: boolean;
readonly urls: string;
readonly showContextMenus: boolean;
}
interface OldOrNewOptions {
readonly useEditor: number;
readonly editor: string;
readonly projectPath: string;
readonly maxAge: number;
readonly filter: FilterStateValue | boolean;
readonly whitelist: string;
readonly blacklist: string;
readonly shouldCatchErrors: boolean;
readonly inject: boolean;
readonly urls: string;
readonly showContextMenus: boolean;
}
let options: Options | undefined;
let subscribers: ((options: Options) => void)[] = [];
export interface OptionsMessage {
readonly options: Options;
}
type ToAllTabs = (msg: OptionsMessage) => void;
const save =
(toAllTabs: ToAllTabs | undefined) =>
<K extends keyof Options>(key: K, value: Options[K]) => {
let obj: { [K1 in keyof Options]?: Options[K1] } = {};
obj[key] = value;
chrome.storage.sync.set(obj);
options![key] = value;
toAllTabs!({ options: options! });
subscribers.forEach((s) => s(options!));
};
const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({
...oldOptions,
filter:
// Migrate the old `filter` option from 2.2.1
typeof oldOptions.filter === 'boolean'
? oldOptions.filter && oldOptions.whitelist.length > 0
? FilterState.WHITELIST_SPECIFIC
: oldOptions.filter
? FilterState.BLACKLIST_SPECIFIC
: FilterState.DO_NOT_FILTER
: oldOptions.filter,
});
const get = (callback: (options: Options) => void) => {
if (options) callback(options);
else {
chrome.storage.sync.get(
{
useEditor: 0,
editor: '',
projectPath: '',
maxAge: 50,
filter: FilterState.DO_NOT_FILTER,
whitelist: '',
blacklist: '',
shouldCatchErrors: false,
inject: true,
urls: '^https?://localhost|0\\.0\\.0\\.0:\\d+\n^https?://.+\\.github\\.io',
showContextMenus: true,
},
function (items) {
options = migrateOldOptions(items as OldOrNewOptions);
callback(options);
}
);
}
};
const subscribe = (callback: (options: Options) => void) => {
subscribers = subscribers.concat(callback);
};
const toReg = (str: string) =>
str !== '' ? str.split('\n').filter(Boolean).join('|') : null;
export const injectOptions = (newOptions: Options) => {
if (!newOptions) return;
options = {
...newOptions,
whitelist:
newOptions.filter !== FilterState.DO_NOT_FILTER
? toReg(newOptions.whitelist)!
: newOptions.whitelist,
blacklist:
newOptions.filter !== FilterState.DO_NOT_FILTER
? toReg(newOptions.blacklist)!
: newOptions.blacklist,
};
let s = document.createElement('script');
s.type = 'text/javascript';
s.appendChild(
document.createTextNode(
'window.devToolsOptions = Object.assign(window.devToolsOptions||{},' +
JSON.stringify(options) +
');'
)
);
(document.head || document.documentElement).appendChild(s);
s.parentNode!.removeChild(s);
};
export const getOptionsFromBg = () => {
/* chrome.runtime.sendMessage({ type: 'GET_OPTIONS' }, response => {
if (response && response.options) injectOptions(response.options);
});
*/
get((newOptions) => {
injectOptions(newOptions);
}); // Legacy
};
export const isAllowed = (localOptions = options) =>
!localOptions ||
localOptions.inject ||
!localOptions.urls ||
location.href.match(toReg(localOptions.urls)!);
export interface SyncOptions {
readonly save: <K extends keyof Options>(key: K, value: Options[K]) => void;
readonly get: (callback: (options: Options) => void) => void;
readonly subscribe: (callback: (options: Options) => void) => void;
}
export default function syncOptions(toAllTabs?: ToAllTabs): SyncOptions {
if (toAllTabs && !options) get(() => {}); // Initialize
return {
save: save(toAllTabs),
get: get,
subscribe: subscribe,
};
}

View File

@ -1,27 +1,31 @@
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { PreloadedState } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { UPDATE_STATE } from '@redux-devtools/app/lib/constants/actionTypes'; import { UPDATE_STATE } from '@redux-devtools/app/lib/constants/actionTypes';
import { StoreState } from '@redux-devtools/app/lib/reducers';
import App from '../../../app/containers/App'; import App from '../../../app/containers/App';
import configureStore from '../../../app/stores/windowStore'; import configureStore from '../../../app/stores/windowStore';
import getPreloadedState from '../background/getPreloadedState'; import getPreloadedState from '../background/getPreloadedState';
import '../../views/window.pug'; import '../../views/window.pug';
import { MonitorMessage } from '../../../app/middlewares/api';
const position = location.hash; const position = location.hash;
let preloadedState; let preloadedState: PreloadedState<StoreState>;
getPreloadedState(position, (state) => { getPreloadedState(position, (state) => {
preloadedState = state; preloadedState = state;
}); });
chrome.runtime.getBackgroundPage(({ store }) => { chrome.runtime.getBackgroundPage((window) => {
const { store } = window!;
const localStore = configureStore(store, position, preloadedState); const localStore = configureStore(store, position, preloadedState);
let name = 'monitor'; let name = 'monitor';
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) { if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
name += chrome.devtools.inspectedWindow.tabId; name += chrome.devtools.inspectedWindow.tabId;
} }
const bg = chrome.runtime.connect({ name }); const bg = chrome.runtime.connect({ name });
const update = (action) => { const update = (action?: MonitorMessage) => {
localStore.dispatch(action || { type: UPDATE_STATE }); localStore.dispatch(action || { type: UPDATE_STATE });
}; };
bg.onMessage.addListener(update); bg.onMessage.addListener(update);

View File

@ -14,12 +14,12 @@ chrome.storage.local.get(
's:secure': null, 's:secure': null,
}, },
(options) => { (options) => {
const AppAsAny = App as any;
render( render(
<App <AppAsAny
selectMonitor={options['select-monitor']} selectMonitor={options['select-monitor']}
testTemplates={options['test-templates']} testTemplates={options['test-templates']}
selectedTemplate={options['test-templates-sel']} selectedTemplate={options['test-templates-sel']}
testTemplates={options['test-templates']}
useCodemirror useCodemirror
socketOptions={ socketOptions={
options['s:hostname'] && options['s:port'] options['s:hostname'] && options['s:port']

View File

@ -2,7 +2,7 @@ import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import configureStore from '../../../src/app/stores/windowStore'; import configureStore from '../../../src/app/stores/windowStore';
import App from '../../../src/app/containers/App.js'; import App from '../../../src/app/containers/App';
const store = configureStore(store); const store = configureStore(store);
const component = mount( const component = mount(

4
extension/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "../tsconfig.react.base.json",
"include": ["src"]
}

View File

@ -3,7 +3,7 @@ import webpack from 'webpack';
import CopyPlugin from 'copy-webpack-plugin'; import CopyPlugin from 'copy-webpack-plugin';
const extpath = path.join(__dirname, '../src/browser/extension/'); const extpath = path.join(__dirname, '../src/browser/extension/');
const mock = `${extpath}chromeAPIMock.js`; const mock = `${extpath}chromeAPIMock`;
const baseConfig = (params) => ({ const baseConfig = (params) => ({
// devtool: 'source-map', // devtool: 'source-map',
@ -64,7 +64,7 @@ const baseConfig = (params) => ({
app: path.join(__dirname, '../src/app'), app: path.join(__dirname, '../src/app'),
tmp: path.join(__dirname, '../build/tmp'), tmp: path.join(__dirname, '../build/tmp'),
}, },
extensions: ['.js'], extensions: ['.js', '.jsx', '.ts', '.tsx'],
}, },
module: { module: {
rules: [ rules: [
@ -72,7 +72,7 @@ const baseConfig = (params) => ({
? params.loaders ? params.loaders
: [ : [
{ {
test: /\.js$/, test: /\.(js|ts)x?$/,
use: 'babel-loader', use: 'babel-loader',
exclude: /(node_modules|tmp\/page\.bundle)/, exclude: /(node_modules|tmp\/page\.bundle)/,
}, },

View File

@ -45,6 +45,7 @@ import { Features, State } from '../reducers/instances';
import { MonitorStateMonitorState } from '../reducers/monitor'; import { MonitorStateMonitorState } from '../reducers/monitor';
import { LiftedAction } from '@redux-devtools/core'; import { LiftedAction } from '@redux-devtools/core';
import { Data } from '../reducers/reports'; import { Data } from '../reducers/reports';
import { LiftedState } from '@redux-devtools/instrument';
let monitorReducer: ( let monitorReducer: (
monitorProps: unknown, monitorProps: unknown,
@ -53,7 +54,7 @@ let monitorReducer: (
) => unknown; ) => unknown;
let monitorProps: unknown = {}; let monitorProps: unknown = {};
interface ChangeSectionAction { export interface ChangeSectionAction {
readonly type: typeof CHANGE_SECTION; readonly type: typeof CHANGE_SECTION;
readonly section: string; readonly section: string;
} }
@ -69,7 +70,7 @@ interface ChangeThemeFormData {
interface ChangeThemeData { interface ChangeThemeData {
readonly formData: ChangeThemeFormData; readonly formData: ChangeThemeFormData;
} }
interface ChangeThemeAction { export interface ChangeThemeAction {
readonly type: typeof CHANGE_THEME; readonly type: typeof CHANGE_THEME;
readonly theme: Theme; readonly theme: Theme;
readonly scheme: Scheme; readonly scheme: Scheme;
@ -119,13 +120,28 @@ export interface LockChangesAction {
} }
export interface ToggleActionAction { export interface ToggleActionAction {
type: 'TOGGLE_ACTION'; type: 'TOGGLE_ACTION';
id: number;
} }
export interface RollbackAction { export interface RollbackAction {
type: 'ROLLBACK'; type: 'ROLLBACK';
timestamp: number;
} }
export interface SweepAction { export interface SweepAction {
type: 'SWEEP'; type: 'SWEEP';
} }
interface ReorderActionAction {
type: 'REORDER_ACTION';
actionId: number;
beforeActionId: number;
}
interface ImportStateAction {
type: 'IMPORT_STATE';
nextLiftedState:
| LiftedState<unknown, Action<unknown>, unknown>
| readonly Action<unknown>[];
preloadedState?: unknown;
noRecompute?: boolean | undefined;
}
export type DispatchAction = export type DispatchAction =
| JumpToStateAction | JumpToStateAction
| JumpToActionAction | JumpToActionAction
@ -133,7 +149,9 @@ export type DispatchAction =
| LockChangesAction | LockChangesAction
| ToggleActionAction | ToggleActionAction
| RollbackAction | RollbackAction
| SweepAction; | SweepAction
| ReorderActionAction
| ImportStateAction;
interface LiftedActionActionBase { interface LiftedActionActionBase {
action?: DispatchAction | string | CustomAction; action?: DispatchAction | string | CustomAction;
state?: string; state?: string;
@ -145,18 +163,18 @@ export interface LiftedActionDispatchAction extends LiftedActionActionBase {
action: DispatchAction; action: DispatchAction;
toAll?: boolean; toAll?: boolean;
} }
interface LiftedActionImportAction extends LiftedActionActionBase { export interface LiftedActionImportAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION; type: typeof LIFTED_ACTION;
message: 'IMPORT'; message: 'IMPORT';
state: string; state: string;
preloadedState: unknown | undefined; preloadedState: unknown | undefined;
} }
interface LiftedActionActionAction extends LiftedActionActionBase { export interface LiftedActionActionAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION; type: typeof LIFTED_ACTION;
message: 'ACTION'; message: 'ACTION';
action: string | CustomAction; action: string | CustomAction;
} }
interface LiftedActionExportAction extends LiftedActionActionBase { export interface LiftedActionExportAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION; type: typeof LIFTED_ACTION;
message: 'EXPORT'; message: 'EXPORT';
toExport: boolean; toExport: boolean;
@ -192,15 +210,15 @@ export function liftedDispatch(
} as LiftedActionDispatchAction; } as LiftedActionDispatchAction;
} }
interface SelectInstanceAction { export interface SelectInstanceAction {
type: typeof SELECT_INSTANCE; type: typeof SELECT_INSTANCE;
selected: string; selected: string | number;
} }
export function selectInstance(selected: string): SelectInstanceAction { export function selectInstance(selected: string): SelectInstanceAction {
return { type: SELECT_INSTANCE, selected }; return { type: SELECT_INSTANCE, selected };
} }
interface SelectMonitorAction { export interface SelectMonitorAction {
type: typeof SELECT_MONITOR; type: typeof SELECT_MONITOR;
monitor: string; monitor: string;
monitorState?: MonitorStateMonitorState; monitorState?: MonitorStateMonitorState;
@ -219,7 +237,7 @@ interface NextState {
subTabName: string; subTabName: string;
inspectedStatePath?: string[]; inspectedStatePath?: string[];
} }
interface UpdateMonitorStateAction { export interface UpdateMonitorStateAction {
type: typeof UPDATE_MONITOR_STATE; type: typeof UPDATE_MONITOR_STATE;
nextState: NextState; nextState: NextState;
} }
@ -240,7 +258,7 @@ export function importState(
return { type: LIFTED_ACTION, message: 'IMPORT', state, preloadedState }; return { type: LIFTED_ACTION, message: 'IMPORT', state, preloadedState };
} }
interface ExportAction { export interface ExportAction {
type: typeof EXPORT; type: typeof EXPORT;
} }
export function exportState(): ExportAction { export function exportState(): ExportAction {
@ -268,7 +286,7 @@ export function pauseRecording(status: boolean): LiftedActionDispatchAction {
export interface CustomAction { export interface CustomAction {
name: string; name: string;
selected: number; selected: number;
args: (string | undefined)[]; args: string[];
rest: string; rest: string;
} }
export function dispatchRemotely( export function dispatchRemotely(
@ -277,28 +295,28 @@ export function dispatchRemotely(
return { type: LIFTED_ACTION, message: 'ACTION', action }; return { type: LIFTED_ACTION, message: 'ACTION', action };
} }
interface TogglePersistAction { export interface TogglePersistAction {
type: typeof TOGGLE_PERSIST; type: typeof TOGGLE_PERSIST;
} }
export function togglePersist(): TogglePersistAction { export function togglePersist(): TogglePersistAction {
return { type: TOGGLE_PERSIST }; return { type: TOGGLE_PERSIST };
} }
interface ToggleSyncAction { export interface ToggleSyncAction {
type: typeof TOGGLE_SYNC; type: typeof TOGGLE_SYNC;
} }
export function toggleSync(): ToggleSyncAction { export function toggleSync(): ToggleSyncAction {
return { type: TOGGLE_SYNC }; return { type: TOGGLE_SYNC };
} }
interface ToggleSliderAction { export interface ToggleSliderAction {
type: typeof TOGGLE_SLIDER; type: typeof TOGGLE_SLIDER;
} }
export function toggleSlider(): ToggleSliderAction { export function toggleSlider(): ToggleSliderAction {
return { type: TOGGLE_SLIDER }; return { type: TOGGLE_SLIDER };
} }
interface ToggleDispatcherAction { export interface ToggleDispatcherAction {
type: typeof TOGGLE_DISPATCHER; type: typeof TOGGLE_DISPATCHER;
} }
export function toggleDispatcher(): ToggleDispatcherAction { export function toggleDispatcher(): ToggleDispatcherAction {
@ -312,7 +330,7 @@ export interface ConnectionOptions {
readonly port: number; readonly port: number;
readonly secure: boolean; readonly secure: boolean;
} }
interface ReconnectAction { export interface ReconnectAction {
readonly type: typeof RECONNECT; readonly type: typeof RECONNECT;
readonly options: ConnectionOptions; readonly options: ConnectionOptions;
} }
@ -326,7 +344,7 @@ interface Notification {
readonly type: 'error'; readonly type: 'error';
readonly message: string; readonly message: string;
} }
interface ShowNotificationAction { export interface ShowNotificationAction {
readonly type: typeof SHOW_NOTIFICATION; readonly type: typeof SHOW_NOTIFICATION;
readonly notification: Notification; readonly notification: Notification;
} }
@ -334,14 +352,14 @@ export function showNotification(message: string): ShowNotificationAction {
return { type: SHOW_NOTIFICATION, notification: { type: 'error', message } }; return { type: SHOW_NOTIFICATION, notification: { type: 'error', message } };
} }
interface ClearNotificationAction { export interface ClearNotificationAction {
readonly type: typeof CLEAR_NOTIFICATION; readonly type: typeof CLEAR_NOTIFICATION;
} }
export function clearNotification(): ClearNotificationAction { export function clearNotification(): ClearNotificationAction {
return { type: CLEAR_NOTIFICATION }; return { type: CLEAR_NOTIFICATION };
} }
interface GetReportRequest { export interface GetReportRequest {
readonly type: typeof GET_REPORT_REQUEST; readonly type: typeof GET_REPORT_REQUEST;
readonly report: unknown; readonly report: unknown;
} }
@ -354,7 +372,7 @@ export interface ActionCreator {
name: string; name: string;
} }
interface LibConfig { export interface LibConfig {
actionCreators?: string; actionCreators?: string;
name?: string; name?: string;
type?: string; type?: string;
@ -363,10 +381,10 @@ interface LibConfig {
} }
export interface RequestBase { export interface RequestBase {
id: string; id?: string;
instanceId?: string; instanceId?: string | number;
action?: string; action?: string;
name?: string; name?: string | undefined;
libConfig?: LibConfig; libConfig?: LibConfig;
actionsById?: string; actionsById?: string;
computedStates?: string; computedStates?: string;
@ -376,14 +394,15 @@ export interface RequestBase {
} }
interface InitRequest extends RequestBase { interface InitRequest extends RequestBase {
type: 'INIT'; type: 'INIT';
action: string; action?: string;
payload?: string;
} }
interface ActionRequest extends RequestBase { interface ActionRequest extends RequestBase {
type: 'ACTION'; type: 'ACTION';
isExcess: boolean; isExcess?: boolean;
nextActionId: number; nextActionId: number;
maxAge: number; maxAge: number;
batched: boolean; batched?: boolean;
} }
interface StateRequest extends RequestBase { interface StateRequest extends RequestBase {
type: 'STATE'; type: 'STATE';
@ -409,23 +428,23 @@ export type Request =
| LiftedRequest | LiftedRequest
| ExportRequest; | ExportRequest;
interface UpdateStateAction { export interface UpdateStateAction {
type: typeof UPDATE_STATE; type: typeof UPDATE_STATE;
request?: Request; request?: Request;
id?: string; id?: string | number;
} }
interface SetStateAction { export interface SetStateAction {
type: typeof SET_STATE; type: typeof SET_STATE;
newState: State; newState: State;
} }
interface RemoveInstanceAction { export interface RemoveInstanceAction {
type: typeof REMOVE_INSTANCE; type: typeof REMOVE_INSTANCE;
id: string; id: string | number;
} }
interface ConnectRequestAction { export interface ConnectRequestAction {
type: typeof CONNECT_REQUEST; type: typeof CONNECT_REQUEST;
options: ConnectionOptions; options: ConnectionOptions;
} }
@ -435,58 +454,58 @@ interface ConnectSuccessPayload {
authState: AuthStates; authState: AuthStates;
socketState: States; socketState: States;
} }
interface ConnectSuccessAction { export interface ConnectSuccessAction {
type: typeof CONNECT_SUCCESS; type: typeof CONNECT_SUCCESS;
payload: ConnectSuccessPayload; payload: ConnectSuccessPayload;
error: Error | undefined; error: Error | undefined;
} }
interface ConnectErrorAction { export interface ConnectErrorAction {
type: typeof CONNECT_ERROR; type: typeof CONNECT_ERROR;
error: Error | undefined; error: Error | undefined;
} }
interface AuthRequestAction { export interface AuthRequestAction {
type: typeof AUTH_REQUEST; type: typeof AUTH_REQUEST;
} }
interface AuthSuccessAction { export interface AuthSuccessAction {
type: typeof AUTH_SUCCESS; type: typeof AUTH_SUCCESS;
baseChannel: string; baseChannel: string;
} }
interface AuthErrorAction { export interface AuthErrorAction {
type: typeof AUTH_ERROR; type: typeof AUTH_ERROR;
error: Error; error: Error;
} }
interface DisconnectedAction { export interface DisconnectedAction {
type: typeof DISCONNECTED; type: typeof DISCONNECTED;
code: number; code: number;
} }
interface DeauthenticateAction { export interface DeauthenticateAction {
type: typeof DEAUTHENTICATE; type: typeof DEAUTHENTICATE;
} }
interface SubscribeRequestAction { export interface SubscribeRequestAction {
type: typeof SUBSCRIBE_REQUEST; type: typeof SUBSCRIBE_REQUEST;
channel: string; channel: string;
subscription: typeof UPDATE_STATE | typeof UPDATE_REPORTS; subscription: typeof UPDATE_STATE | typeof UPDATE_REPORTS;
} }
interface SubscribeSuccessAction { export interface SubscribeSuccessAction {
type: typeof SUBSCRIBE_SUCCESS; type: typeof SUBSCRIBE_SUCCESS;
channel: string; channel: string;
} }
interface SubscribeErrorAction { export interface SubscribeErrorAction {
type: typeof SUBSCRIBE_ERROR; type: typeof SUBSCRIBE_ERROR;
error: Error; error: Error;
status: string; status: string;
} }
interface UnsubscribeAction { export interface UnsubscribeAction {
type: typeof UNSUBSCRIBE; type: typeof UNSUBSCRIBE;
channel: string; channel: string;
} }
@ -494,8 +513,8 @@ interface UnsubscribeAction {
export interface EmitAction { export interface EmitAction {
type: typeof EMIT; type: typeof EMIT;
message: string; message: string;
id?: string | false; id?: string | number | false;
instanceId?: string; instanceId?: string | number;
action?: unknown; action?: unknown;
state?: unknown; state?: unknown;
} }
@ -514,31 +533,30 @@ interface RemoveRequest {
id: unknown; id: unknown;
} }
export type UpdateReportsRequest = ListRequest | AddRequest | RemoveRequest; export type UpdateReportsRequest = ListRequest | AddRequest | RemoveRequest;
interface UpdateReportsAction { export interface UpdateReportsAction {
type: typeof UPDATE_REPORTS; type: typeof UPDATE_REPORTS;
request: UpdateReportsRequest; request: UpdateReportsRequest;
} }
interface GetReportError { export interface GetReportError {
type: typeof GET_REPORT_ERROR; type: typeof GET_REPORT_ERROR;
error: Error; error: Error;
} }
interface GetReportSuccess { export interface GetReportSuccess {
type: typeof GET_REPORT_SUCCESS; type: typeof GET_REPORT_SUCCESS;
data: { payload: string }; data: { payload: string };
} }
interface ErrorAction { export interface ErrorAction {
type: typeof ERROR; type: typeof ERROR;
payload: string; payload: string;
} }
export type StoreAction = export type StoreActionWithoutUpdateStateOrLiftedAction =
| ChangeSectionAction | ChangeSectionAction
| ChangeThemeAction | ChangeThemeAction
| MonitorActionAction | MonitorActionAction
| LiftedActionAction
| SelectInstanceAction | SelectInstanceAction
| SelectMonitorAction | SelectMonitorAction
| UpdateMonitorStateAction | UpdateMonitorStateAction
@ -552,7 +570,6 @@ export type StoreAction =
| ClearNotificationAction | ClearNotificationAction
| GetReportRequest | GetReportRequest
| SetStateAction | SetStateAction
| UpdateStateAction
| RemoveInstanceAction | RemoveInstanceAction
| ConnectRequestAction | ConnectRequestAction
| ConnectSuccessAction | ConnectSuccessAction
@ -571,3 +588,13 @@ export type StoreAction =
| GetReportError | GetReportError
| GetReportSuccess | GetReportSuccess
| ErrorAction; | ErrorAction;
export type StoreActionWithoutUpdateState =
| StoreActionWithoutUpdateStateOrLiftedAction
| LiftedActionAction;
export type StoreActionWithoutLiftedAction =
| StoreActionWithoutUpdateStateOrLiftedAction
| UpdateStateAction;
export type StoreAction = StoreActionWithoutUpdateState | UpdateStateAction;

View File

@ -9,7 +9,7 @@ type DispatchProps = ResolveThunks<typeof actionCreators>;
type Props = StateProps & DispatchProps; type Props = StateProps & DispatchProps;
class InstanceSelector extends Component<Props> { class InstanceSelector extends Component<Props> {
select?: { readonly value: string; readonly label: string }[]; select?: { readonly value: string; readonly label: string | number }[];
render() { render() {
this.select = [{ value: '', label: 'Autoselect instances' }]; this.select = [{ value: '', label: 'Autoselect instances' }];

View File

@ -55,7 +55,7 @@ type Props = DispatchProps & OwnProps;
interface State { interface State {
selected: 'default' | number; selected: 'default' | number;
customAction: string; customAction: string;
args: (string | undefined)[]; args: string[];
rest: string; rest: string;
changed: boolean; changed: boolean;
} }
@ -108,7 +108,7 @@ class Dispatcher extends Component<Props, State> {
handleArg = (argIndex: number) => (value: string) => { handleArg = (argIndex: number) => (value: string) => {
const args = [ const args = [
...this.state.args.slice(0, argIndex), ...this.state.args.slice(0, argIndex),
value || undefined, (value || undefined)!,
...this.state.args.slice(argIndex + 1), ...this.state.args.slice(argIndex + 1),
]; ];
this.setState({ args, changed: true }); this.setState({ args, changed: true });

View File

@ -30,7 +30,7 @@ let socket: SCClientSocket;
let store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>; let store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>;
function emit({ message: type, id, instanceId, action, state }: EmitAction) { function emit({ message: type, id, instanceId, action, state }: EmitAction) {
socket.emit(id ? 'sc-' + id : 'respond', { type, action, state, instanceId }); socket.emit(id ? `sc-${id}` : 'respond', { type, action, state, instanceId });
} }
function startMonitoring(channel: string) { function startMonitoring(channel: string) {

View File

@ -5,7 +5,7 @@ import { Dispatch, MiddlewareAPI } from 'redux';
import { ExportRequest, StoreAction } from '../actions'; import { ExportRequest, StoreAction } from '../actions';
import { StoreState } from '../reducers'; import { StoreState } from '../reducers';
let toExport: string | undefined; let toExport: string | number | undefined;
function download(state: string) { function download(state: string) {
const blob = new Blob([state], { type: 'octet/stream' }); const blob = new Blob([state], { type: 'octet/stream' });

View File

@ -34,8 +34,8 @@ export interface Features {
} }
export interface Options { export interface Options {
name?: string; name?: string | number;
connectionId?: string; connectionId?: string | number;
explicitLib?: string; explicitLib?: string;
lib?: string; lib?: string;
actionCreators?: ActionCreator[]; actionCreators?: ActionCreator[];
@ -56,10 +56,10 @@ export interface State {
} }
export interface InstancesState { export interface InstancesState {
selected: string | null; selected: string | number | null;
current: string; current: string | number;
sync: boolean; sync: boolean;
connections: { [id: string]: string[] }; connections: { [id: string]: (string | number)[] };
options: { [id: string]: Options }; options: { [id: string]: Options };
states: { [id: string]: State }; states: { [id: string]: State };
persisted?: boolean; persisted?: boolean;
@ -86,7 +86,7 @@ export const initialState: InstancesState = {
function updateState( function updateState(
state: { [id: string]: State }, state: { [id: string]: State },
request: Request, request: Request,
id: string, id: string | number,
serialize: boolean | undefined serialize: boolean | undefined
) { ) {
let payload: State = request.payload as State; let payload: State = request.payload as State;
@ -231,7 +231,7 @@ export function dispatchAction(
return state; return state;
} }
function removeState(state: InstancesState, connectionId: string) { function removeState(state: InstancesState, connectionId: string | number) {
const instanceIds = state.connections[connectionId]; const instanceIds = state.connections[connectionId];
if (!instanceIds) return state; if (!instanceIds) return state;
@ -268,8 +268,8 @@ function removeState(state: InstancesState, connectionId: string) {
function init( function init(
{ type, action, name, libConfig = {} }: Request, { type, action, name, libConfig = {} }: Request,
connectionId: string, connectionId: string | number,
current: string current: string | number
): Options { ): Options {
let lib; let lib;
let actionCreators; let actionCreators;
@ -310,7 +310,7 @@ export default function instances(
case UPDATE_STATE: { case UPDATE_STATE: {
const { request } = action; const { request } = action;
if (!request) return state; if (!request) return state;
const connectionId = action.id || request.id; const connectionId = (action.id || request.id)!;
const current = request.instanceId || connectionId; const current = request.instanceId || connectionId;
let connections = state.connections; let connections = state.connections;
let options = state.options; let options = state.options;

View File

@ -18,7 +18,7 @@ export interface MonitorStateMonitorState {
} }
export interface MonitorState { export interface MonitorState {
selected: string; selected: string;
monitorState: MonitorStateMonitorState | undefined; monitorState?: MonitorStateMonitorState | undefined;
sliderIsOpen: boolean; sliderIsOpen: boolean;
dispatcherIsOpen: boolean; dispatcherIsOpen: boolean;
} }

View File

@ -4,8 +4,7 @@ import stringifyJSON from './stringifyJSON';
import { SET_STATE } from '../constants/actionTypes'; import { SET_STATE } from '../constants/actionTypes';
import { InstancesState, State } from '../reducers/instances'; import { InstancesState, State } from '../reducers/instances';
import { Dispatch, MiddlewareAPI } from 'redux'; import { Dispatch, MiddlewareAPI } from 'redux';
import { DispatchAction, StoreAction } from '../actions'; import { DispatchAction, StoreActionWithoutLiftedAction } from '../actions';
import { StoreState } from '../reducers';
export function sweep(state: State): State { export function sweep(state: State): State {
return { return {
@ -21,12 +20,15 @@ export function sweep(state: State): State {
} }
export function nonReduxDispatch( export function nonReduxDispatch(
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>, store: MiddlewareAPI<
Dispatch<StoreActionWithoutLiftedAction>,
{ readonly instances: InstancesState }
>,
message: string, message: string,
instanceId: string, instanceId: string | number,
action: DispatchAction, action: DispatchAction,
initialState: string | undefined, initialState: string | undefined,
preInstances: InstancesState preInstances?: InstancesState
) { ) {
const instances = preInstances || store.getState().instances; const instances = preInstances || store.getState().instances;
const state = instances.states[instanceId]; const state = instances.states[instanceId];

View File

@ -59,7 +59,7 @@
"@redux-devtools/inspector-monitor": "^1.0.0", "@redux-devtools/inspector-monitor": "^1.0.0",
"@types/es6template": "^1.0.0", "@types/es6template": "^1.0.0",
"@types/history": "^4.7.8", "@types/history": "^4.7.8",
"@types/jsan": "^3.1.0", "@types/jsan": "^3.1.2",
"@types/lodash.shuffle": "^4.2.6", "@types/lodash.shuffle": "^4.2.6",
"@types/object-path": "^0.11.0", "@types/object-path": "^0.11.0",
"@types/react": "^16.14.8", "@types/react": "^16.14.8",

View File

@ -29,7 +29,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.14.5", "@babel/code-frame": "^7.14.5",
"@types/chrome": "^0.0.124", "@types/chrome": "^0.0.145",
"anser": "^1.4.10", "anser": "^1.4.10",
"html-entities": "^1.4.0", "html-entities": "^1.4.0",
"redux-devtools-themes": "^1.0.0", "redux-devtools-themes": "^1.0.0",

View File

@ -32,11 +32,11 @@ function openAndCloseTab(url: string) {
const removeTab = () => { const removeTab = () => {
chrome.windows.onFocusChanged.removeListener(removeTab); chrome.windows.onFocusChanged.removeListener(removeTab);
if (tab && tab.id) { if (tab && tab.id) {
chrome.tabs.remove(tab.id, () => { chrome.tabs.remove(tab.id, async () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
if (chrome.runtime.lastError) console.log(chrome.runtime.lastError); if (chrome.runtime.lastError) console.log(chrome.runtime.lastError);
else if (chrome.devtools && chrome.devtools.inspectedWindow) { else if (chrome.devtools && chrome.devtools.inspectedWindow) {
chrome.tabs.update(chrome.devtools.inspectedWindow.tabId, { await chrome.tabs.update(chrome.devtools.inspectedWindow.tabId, {
active: true, active: true,
}); });
} }

View File

@ -100,7 +100,7 @@ interface ImportStateAction<S, A extends Action<unknown>, MonitorState> {
type: typeof ActionTypes.IMPORT_STATE; type: typeof ActionTypes.IMPORT_STATE;
nextLiftedState: LiftedState<S, A, MonitorState> | readonly A[]; nextLiftedState: LiftedState<S, A, MonitorState> | readonly A[];
preloadedState?: S; preloadedState?: S;
noRecompute: boolean | undefined; noRecompute?: boolean | undefined;
} }
interface LockChangesAction { interface LockChangesAction {

View File

@ -35,7 +35,7 @@
"jsan": "^3.1.13" "jsan": "^3.1.13"
}, },
"devDependencies": { "devDependencies": {
"@types/jsan": "^3.1.0", "@types/jsan": "^3.1.2",
"immutable": "^4.0.0-rc.12" "immutable": "^4.0.0-rc.12"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -3,22 +3,24 @@ import jsan from 'jsan';
import { nanoid } from 'nanoid/non-secure'; import { nanoid } from 'nanoid/non-secure';
import { immutableSerialize } from '@redux-devtools/serialize'; import { immutableSerialize } from '@redux-devtools/serialize';
import Immutable from 'immutable'; import Immutable from 'immutable';
import { Action } from 'redux'; import { Action, ActionCreator } from 'redux';
export function generateId(id: string | undefined) { export function generateId(id: string | undefined) {
return id || nanoid(7); return id || nanoid(7);
} }
export interface ActionCreatorObject {
readonly name: string;
readonly func: ActionCreator<Action<unknown>>;
readonly args: readonly string[];
}
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
function flatTree( function flatTree(
obj: { [key: string]: (...args: any[]) => unknown }, obj: { [key: string]: ActionCreator<Action<unknown>> },
namespace = '' namespace = ''
) { ) {
let functions: { let functions: ActionCreatorObject[] = [];
name: string;
func: (...args: any[]) => unknown;
args: string[];
}[] = [];
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
const prop = obj[key]; const prop = obj[key];
if (typeof prop === 'function') { if (typeof prop === 'function') {
@ -63,7 +65,7 @@ export function getMethods(obj: unknown) {
} }
export function getActionsArray(actionCreators: { export function getActionsArray(actionCreators: {
[key: string]: (...args: any[]) => unknown; [key: string]: ActionCreator<Action<unknown>>;
}) { }) {
if (Array.isArray(actionCreators)) return actionCreators; if (Array.isArray(actionCreators)) return actionCreators;
return flatTree(actionCreators); return flatTree(actionCreators);
@ -81,10 +83,8 @@ function evalArgs(inArgs: string[], restArgs: string) {
} }
export function evalAction( export function evalAction(
action: string | { args: string[]; rest: string; selected: string }, action: string | { args: string[]; rest: string; selected: number },
actionCreators: { actionCreators: readonly ActionCreatorObject[]
[selected: string]: { func: (...args: any[]) => Action<unknown> };
}
) { ) {
if (typeof action === 'string') { if (typeof action === 'string') {
// eslint-disable-next-line @typescript-eslint/no-implied-eval // eslint-disable-next-line @typescript-eslint/no-implied-eval

View File

@ -3520,7 +3520,7 @@ __metadata:
"@redux-devtools/inspector-monitor": ^1.0.0 "@redux-devtools/inspector-monitor": ^1.0.0
"@types/es6template": ^1.0.0 "@types/es6template": ^1.0.0
"@types/history": ^4.7.8 "@types/history": ^4.7.8
"@types/jsan": ^3.1.0 "@types/jsan": ^3.1.2
"@types/lodash.shuffle": ^4.2.6 "@types/lodash.shuffle": ^4.2.6
"@types/object-path": ^0.11.0 "@types/object-path": ^0.11.0
"@types/prop-types": ^15.7.3 "@types/prop-types": ^15.7.3
@ -3567,7 +3567,7 @@ __metadata:
"@redux-devtools/core": ^3.9.0 "@redux-devtools/core": ^3.9.0
"@redux-devtools/inspector-monitor": ^1.0.0 "@redux-devtools/inspector-monitor": ^1.0.0
"@types/babel__code-frame": ^7.0.2 "@types/babel__code-frame": ^7.0.2
"@types/chrome": ^0.0.124 "@types/chrome": ^0.0.145
"@types/enzyme": ^3.10.8 "@types/enzyme": ^3.10.8
"@types/enzyme-adapter-react-16": ^1.0.6 "@types/enzyme-adapter-react-16": ^1.0.6
"@types/html-entities": ^1.3.4 "@types/html-entities": ^1.3.4
@ -3682,7 +3682,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@redux-devtools/serialize@workspace:packages/redux-devtools-serialize" resolution: "@redux-devtools/serialize@workspace:packages/redux-devtools-serialize"
dependencies: dependencies:
"@types/jsan": ^3.1.0 "@types/jsan": ^3.1.2
immutable: ^4.0.0-rc.12 immutable: ^4.0.0-rc.12
jsan: ^3.1.13 jsan: ^3.1.13
peerDependencies: peerDependencies:
@ -4825,13 +4825,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/chrome@npm:^0.0.124": "@types/chrome@npm:^0.0.145":
version: 0.0.124 version: 0.0.145
resolution: "@types/chrome@npm:0.0.124" resolution: "@types/chrome@npm:0.0.145"
dependencies: dependencies:
"@types/filesystem": "*" "@types/filesystem": "*"
"@types/har-format": "*" "@types/har-format": "*"
checksum: 6499edca5f608dd48651b20d57d9fb30bbcb02cd695cd94d879a11dba7b9492c618edc6b8b5f718e82b58eceea94fb920c871546c2c3bc867595cb7dd020d527 checksum: f826d0a071ac7ea68aa97b2f8e34a944c470fbd036fdd6a413987fea03062d354fb14708cc0b59693b4a78ec88f9c5b1fce0c622a8e29e3e4b0917509eaf2a1a
languageName: node languageName: node
linkType: hard linkType: hard
@ -5246,10 +5246,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/jsan@npm:^3.1.0": "@types/jsan@npm:^3.1.2":
version: 3.1.0 version: 3.1.2
resolution: "@types/jsan@npm:3.1.0" resolution: "@types/jsan@npm:3.1.2"
checksum: a0670d90e4bee7110504be73eefff9196b46235faf490062865136b1cbad4d3bac2adb9303e308c523f07716c026bb8e72a12ae7d47a948424d4ca4d7883587d checksum: 2ff652807d6067bbc650aaefcda4e3c07b54ddfd7d72283d7c1f1892ad1e18e907b1bbdbee7d0a163efa9e8aed9af5fa9f4ed8e2f27243c46383d31e1181fc11
languageName: node languageName: node
linkType: hard linkType: hard
@ -23231,6 +23231,7 @@ fsevents@^1.2.7:
"@redux-devtools/serialize": ^0.3.0 "@redux-devtools/serialize": ^0.3.0
"@redux-devtools/slider-monitor": ^2.0.0-8 "@redux-devtools/slider-monitor": ^2.0.0-8
"@redux-devtools/utils": ^1.0.0-6 "@redux-devtools/utils": ^1.0.0-6
"@types/jsan": ^3.1.2
bestzip: ^2.2.0 bestzip: ^2.2.0
chromedriver: ^91.0.1 chromedriver: ^91.0.1
electron: ^13.1.2 electron: ^13.1.2