mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-22 01:26:48 +03:00
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:
parent
326cfdf217
commit
a418284a4a
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }]
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"test:app": "cross-env BABEL_ENV=test jest test/app",
|
||||
"test:chrome": "jest test/chrome",
|
||||
"test:electron": "jest test/electron",
|
||||
"test": "npm run test:app && npm run build:extension && npm run test:chrome && npm run test:electron"
|
||||
"test": "npm run test:app && npm run build:extension && npm run test:chrome && npm run test:electron",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@redux-devtools/app": "^1.0.0-8",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"@redux-devtools/serialize": "^0.3.0",
|
||||
"@redux-devtools/slider-monitor": "^2.0.0-8",
|
||||
"@redux-devtools/utils": "^1.0.0-6",
|
||||
"@types/jsan": "^3.1.2",
|
||||
"jsan": "^3.1.13",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^16.14.0",
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
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',
|
||||
BLACKLIST_SPECIFIC: 'BLACKLIST_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) {
|
||||
return {
|
||||
whitelist: Array.isArray(config.actionsWhitelist)
|
||||
whitelist: isArray(config.actionsWhitelist)
|
||||
? config.actionsWhitelist.join('|')
|
||||
: config.actionsWhitelist,
|
||||
blacklist: Array.isArray(config.actionsBlacklist)
|
||||
blacklist: isArray(config.actionsBlacklist)
|
||||
? config.actionsBlacklist.join('|')
|
||||
: config.actionsBlacklist,
|
||||
};
|
||||
|
@ -20,38 +37,48 @@ export function getLocalFilter(config) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export const noFiltersApplied = (localFilter) =>
|
||||
export const noFiltersApplied = (localFilter: LocalFilter | undefined) =>
|
||||
// !predicate &&
|
||||
!localFilter &&
|
||||
(!window.devToolsOptions ||
|
||||
!window.devToolsOptions.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 (
|
||||
noFiltersApplied(localFilter) ||
|
||||
(typeof action !== 'string' && typeof action.type.match !== 'function')
|
||||
(typeof action !== 'string' &&
|
||||
typeof (action.type as string).match !== 'function')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { whitelist, blacklist } = localFilter || window.devToolsOptions || {};
|
||||
const actionType = action.type || action;
|
||||
const actionType = ((action as A).type || action) as string;
|
||||
return (
|
||||
(whitelist && !actionType.match(whitelist)) ||
|
||||
(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;
|
||||
return mapValues(actionsById, (action, id) => ({
|
||||
...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;
|
||||
return computedStates.map((state, idx) => ({
|
||||
...state,
|
||||
|
@ -59,23 +86,19 @@ function filterStates(computedStates, stateSanitizer) {
|
|||
}));
|
||||
}
|
||||
|
||||
export function filterState(
|
||||
state,
|
||||
type,
|
||||
localFilter,
|
||||
stateSanitizer,
|
||||
actionSanitizer,
|
||||
nextActionId,
|
||||
predicate
|
||||
) {
|
||||
if (type === 'ACTION') {
|
||||
return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1);
|
||||
} else if (type !== 'STATE') return state;
|
||||
|
||||
export function filterState<S, A extends Action<unknown>>(
|
||||
state: LiftedState<S, A, unknown>,
|
||||
localFilter: LocalFilter | undefined,
|
||||
stateSanitizer: ((state: S, index: number) => S) | undefined,
|
||||
actionSanitizer: ((action: A, id: number) => A) | undefined,
|
||||
predicate: ((state: S, action: A) => boolean) | undefined
|
||||
): LiftedState<S, A, unknown> {
|
||||
if (predicate || !noFiltersApplied(localFilter)) {
|
||||
const filteredStagedActionIds = [];
|
||||
const filteredComputedStates = [];
|
||||
const sanitizedActionsById = actionSanitizer && {};
|
||||
const filteredStagedActionIds: number[] = [];
|
||||
const filteredComputedStates: { state: S; error?: string | undefined }[] =
|
||||
[];
|
||||
const sanitizedActionsById: { [p: number]: PerformAction<A> } | undefined =
|
||||
actionSanitizer && {};
|
||||
const { actionsById } = state;
|
||||
const { computedStates } = state;
|
||||
|
||||
|
@ -97,7 +120,7 @@ export function filterState(
|
|||
: liftedState
|
||||
);
|
||||
if (actionSanitizer) {
|
||||
sanitizedActionsById[id] = {
|
||||
sanitizedActionsById![id] = {
|
||||
...liftedAction,
|
||||
action: actionSanitizer(currAction, id),
|
||||
};
|
||||
|
@ -120,14 +143,27 @@ export function filterState(
|
|||
};
|
||||
}
|
||||
|
||||
export function startingFrom(
|
||||
sendingActionId,
|
||||
state,
|
||||
localFilter,
|
||||
stateSanitizer,
|
||||
actionSanitizer,
|
||||
predicate
|
||||
) {
|
||||
export interface PartialLiftedState<S, A extends Action<unknown>> {
|
||||
readonly actionsById: { [actionId: number]: PerformAction<A> };
|
||||
readonly computedStates: { state: S; error?: string }[];
|
||||
readonly stagedActionIds: readonly number[];
|
||||
readonly currentStateIndex: number;
|
||||
readonly nextActionId: number;
|
||||
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;
|
||||
if (sendingActionId <= stagedActionIds[1]) return state;
|
||||
const index = stagedActionIds.indexOf(sendingActionId);
|
||||
|
@ -137,7 +173,7 @@ export function startingFrom(
|
|||
const filteredStagedActionIds = shouldFilter ? [0] : stagedActionIds;
|
||||
const actionsById = state.actionsById;
|
||||
const computedStates = state.computedStates;
|
||||
const newActionsById = {};
|
||||
const newActionsById: { [key: number]: PerformAction<A> } = {};
|
||||
const newComputedStates = [];
|
||||
let key;
|
||||
let currAction;
|
|
@ -1,5 +0,0 @@
|
|||
let id = 0;
|
||||
|
||||
export default function generateId(instanceId) {
|
||||
return instanceId || ++id;
|
||||
}
|
5
extension/src/app/api/generateInstanceId.ts
Normal file
5
extension/src/app/api/generateInstanceId.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
let id = 0;
|
||||
|
||||
export default function generateId(instanceId: number | undefined) {
|
||||
return instanceId || ++id;
|
||||
}
|
|
@ -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 };
|
||||
}
|
116
extension/src/app/api/importState.ts
Normal file
116
extension/src/app/api/importState.ts
Normal 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 };
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
736
extension/src/app/api/index.ts
Normal file
736
extension/src/app/api/index.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
let handleError;
|
||||
let handleError: (() => boolean) | undefined;
|
||||
let lastTime = 0;
|
||||
|
||||
function createExpBackoffTimer(step) {
|
||||
function createExpBackoffTimer(step: number) {
|
||||
let count = 1;
|
||||
return function (reset) {
|
||||
return function (reset?: boolean) {
|
||||
// Reset call
|
||||
if (reset) {
|
||||
count = 1;
|
||||
|
@ -18,7 +18,7 @@ function createExpBackoffTimer(step) {
|
|||
|
||||
const nextErrorTimeout = createExpBackoffTimer(5000);
|
||||
|
||||
function postError(message) {
|
||||
function postError(message: string) {
|
||||
if (handleError && !handleError()) return;
|
||||
window.postMessage(
|
||||
{
|
||||
|
@ -30,7 +30,7 @@ function postError(message) {
|
|||
);
|
||||
}
|
||||
|
||||
function catchErrors(e) {
|
||||
function catchErrors(e: ErrorEvent) {
|
||||
if (
|
||||
(window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors) ||
|
||||
e.timeStamp - lastTime < nextErrorTimeout()
|
||||
|
@ -42,7 +42,7 @@ function catchErrors(e) {
|
|||
postError(e.message);
|
||||
}
|
||||
|
||||
export default function notifyErrors(onError) {
|
||||
export default function notifyErrors(onError?: () => boolean) {
|
||||
handleError = onError;
|
||||
window.addEventListener('error', catchErrors, false);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
export default function openWindow(position) {
|
||||
window.postMessage(
|
||||
{
|
||||
source: '@devtools-page',
|
||||
type: 'OPEN',
|
||||
position: position || 'right',
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
18
extension/src/app/api/openWindow.ts
Normal file
18
extension/src/app/api/openWindow.ts
Normal 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',
|
||||
});
|
||||
}
|
|
@ -1,19 +1,33 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, ResolveThunks } from 'react-redux';
|
||||
import { Container, Notification } from 'devui';
|
||||
import { getActiveInstance } from '@redux-devtools/app/lib/reducers/instances';
|
||||
import Settings from '@redux-devtools/app/lib/components/Settings';
|
||||
import Actions from '@redux-devtools/app/lib/containers/Actions';
|
||||
import Header from '@redux-devtools/app/lib/components/Header';
|
||||
import { clearNotification } from '@redux-devtools/app/lib/actions';
|
||||
import { StoreState } from '@redux-devtools/app/lib/reducers';
|
||||
import { SingleMessage } from '../middlewares/api';
|
||||
import { Position } from '../api/openWindow';
|
||||
|
||||
class App extends Component {
|
||||
openWindow = (position) => {
|
||||
chrome.runtime.sendMessage({ type: 'OPEN', position });
|
||||
type StateProps = ReturnType<typeof mapStateToProps>;
|
||||
type DispatchProps = ResolveThunks<typeof actionCreators>;
|
||||
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 = () => {
|
||||
if (navigator.userAgent.indexOf('Firefox') !== -1) {
|
||||
chrome.runtime.sendMessage({ type: 'OPEN_OPTIONS' });
|
||||
sendMessage({ type: 'OPEN_OPTIONS' });
|
||||
} else {
|
||||
chrome.runtime.openOptionsPage();
|
||||
}
|
||||
|
@ -62,7 +76,7 @@ class App extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state: StoreState) {
|
||||
const instances = state.instances;
|
||||
const id = getActiveInstance(instances);
|
||||
return {
|
|
@ -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);
|
||||
};
|
||||
}
|
591
extension/src/app/middlewares/api.ts
Normal file
591
extension/src/app/middlewares/api.ts
Normal 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);
|
||||
};
|
||||
}
|
|
@ -1,9 +1,16 @@
|
|||
import { Dispatch, MiddlewareAPI } from 'redux';
|
||||
import {
|
||||
SELECT_INSTANCE,
|
||||
UPDATE_STATE,
|
||||
} 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;
|
||||
if (instances.current === 'default') return;
|
||||
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(
|
||||
{
|
||||
active: true,
|
||||
|
@ -21,13 +28,15 @@ function getCurrentTabId(next) {
|
|||
(tabs) => {
|
||||
const tab = tabs[0];
|
||||
if (!tab) return;
|
||||
next(tab.id);
|
||||
next(tab.id!);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function popupSelector(store) {
|
||||
return (next) => (action) => {
|
||||
export default function popupSelector(
|
||||
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>
|
||||
) {
|
||||
return (next: Dispatch<StoreAction>) => (action: StoreAction) => {
|
||||
const result = next(action);
|
||||
if (action.type === UPDATE_STATE) {
|
||||
if (chrome.devtools && chrome.devtools.inspectedWindow) {
|
|
@ -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;
|
38
extension/src/app/middlewares/panelSync.ts
Normal file
38
extension/src/app/middlewares/panelSync.ts
Normal 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;
|
|
@ -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;
|
36
extension/src/app/middlewares/windowSync.ts
Normal file
36
extension/src/app/middlewares/windowSync.ts
Normal 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;
|
|
@ -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;
|
19
extension/src/app/reducers/background/index.ts
Normal file
19
extension/src/app/reducers/background/index.ts
Normal 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;
|
|
@ -1,4 +0,0 @@
|
|||
export default function persistStates(state = false, action) {
|
||||
if (action.type === 'TOGGLE_PERSIST') return !state;
|
||||
return state;
|
||||
}
|
6
extension/src/app/reducers/background/persistStates.ts
Normal file
6
extension/src/app/reducers/background/persistStates.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
25
extension/src/app/reducers/panel/index.ts
Normal file
25
extension/src/app/reducers/panel/index.ts
Normal 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;
|
|
@ -1,4 +1,4 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import { combineReducers, Reducer } from 'redux';
|
||||
import instances from './instances';
|
||||
import monitor from '@redux-devtools/app/lib/reducers/monitor';
|
||||
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 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 { StoreState } from '@redux-devtools/app/lib/reducers';
|
||||
import { WindowStoreAction } from '../../stores/windowStore';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
instances,
|
||||
monitor,
|
||||
socket,
|
||||
reports,
|
||||
notification,
|
||||
section,
|
||||
theme,
|
||||
});
|
||||
const rootReducer: Reducer<StoreState, WindowStoreAction> =
|
||||
combineReducers<StoreState>({
|
||||
instances,
|
||||
monitor,
|
||||
socket,
|
||||
reports,
|
||||
notification,
|
||||
section,
|
||||
theme,
|
||||
connection,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
|
@ -7,11 +7,21 @@ import {
|
|||
SELECT_INSTANCE,
|
||||
LIFTED_ACTION,
|
||||
} 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) {
|
||||
case UPDATE_STATE:
|
||||
return { ...action.instances, selected: state.selected };
|
||||
return {
|
||||
...(action as ExpandedUpdateStateAction).instances,
|
||||
selected: state.selected,
|
||||
};
|
||||
case LIFTED_ACTION:
|
||||
if (action.message === 'DISPATCH') return dispatchAction(state, action);
|
||||
return state;
|
|
@ -1,8 +1,32 @@
|
|||
export default class Monitor {
|
||||
constructor(update) {
|
||||
import { Action } from 'redux';
|
||||
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;
|
||||
}
|
||||
reducer = (state = {}, action) => {
|
||||
reducer = (state = {}, action: DispatchAction) => {
|
||||
if (!this.active) return state;
|
||||
this.lastAction = action.type;
|
||||
if (action.type === 'LOCK_CHANGES') {
|
||||
|
@ -15,7 +39,7 @@ export default class Monitor {
|
|||
}
|
||||
return state;
|
||||
};
|
||||
start = (skipUpdate) => {
|
||||
start = (skipUpdate: boolean) => {
|
||||
this.active = true;
|
||||
if (!skipUpdate) this.update();
|
||||
};
|
|
@ -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);
|
||||
*/
|
||||
}
|
84
extension/src/app/stores/backgroundStore.ts
Normal file
84
extension/src/app/stores/backgroundStore.ts
Normal 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);
|
||||
*/
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { createStore } from 'redux';
|
||||
|
||||
export default function configureStore(reducer, initialState, enhance) {
|
||||
return createStore(reducer, initialState, enhance());
|
||||
}
|
15
extension/src/app/stores/createStore.ts
Normal file
15
extension/src/app/stores/createStore.ts
Normal 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());
|
||||
}
|
|
@ -1,15 +1,31 @@
|
|||
import { compose } from 'redux';
|
||||
import { Action, compose, Reducer, StoreEnhancerStoreCreator } from 'redux';
|
||||
import instrument from '@redux-devtools/instrument';
|
||||
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(
|
||||
new RegExp(`[?&]${key}=([^&#]+)\\b`)
|
||||
);
|
||||
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(
|
||||
instrument(monitorReducer, {
|
||||
maxAge: config.maxAge,
|
|
@ -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 panelDispatcher from '../middlewares/panelSync';
|
||||
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));
|
||||
return createStore(rootReducer, preloadedState, enhancer);
|
||||
}
|
|
@ -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 api from '@redux-devtools/app/lib/middlewares/api';
|
||||
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 instanceSelector from '../middlewares/instanceSelector';
|
||||
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) {
|
||||
let enhancer;
|
||||
export interface TogglePersistAction {
|
||||
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)];
|
||||
if (!position || position === '#popup') {
|
||||
// select current tab instance for devPanel and pageAction
|
||||
|
@ -20,7 +58,7 @@ export default function configureStore(baseStore, position, preloadedState) {
|
|||
applyMiddleware(...middlewares),
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||
: (noop) => noop
|
||||
: (noop: unknown) => noop
|
||||
);
|
||||
}
|
||||
const store = createStore(rootReducer, preloadedState, enhancer);
|
||||
|
@ -33,7 +71,7 @@ export default function configureStore(baseStore, position, preloadedState) {
|
|||
hostname: options['s:hostname'],
|
||||
port: options['s:port'],
|
||||
secure: options['s:secure'],
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
});
|
||||
|
|
@ -12,10 +12,10 @@ export function createMenu() {
|
|||
{ id: 'devtools-remote', title: 'Open Remote DevTools' },
|
||||
];
|
||||
|
||||
let shortcuts = {};
|
||||
let shortcuts: { [commandName: string]: string | undefined } = {};
|
||||
chrome.commands.getAll((commands) => {
|
||||
commands.forEach(({ name, shortcut }) => {
|
||||
shortcuts[name] = shortcut;
|
||||
shortcuts[name!] = shortcut;
|
||||
});
|
||||
|
||||
menus.forEach(({ id, title }) => {
|
|
@ -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 template === 'undefined' ||
|
||||
typeof template[sel] === 'undefined'
|
||||
? 0
|
||||
: sel;
|
||||
|
||||
export default function getPreloadedState(position, cb) {
|
||||
export default function getPreloadedState(
|
||||
position: string,
|
||||
cb: (state: PreloadedState<StoreState>) => void
|
||||
) {
|
||||
chrome.storage.local.get(
|
||||
[
|
||||
'monitor' + position,
|
||||
|
@ -28,7 +34,7 @@ export default function getPreloadedState(position, cb) {
|
|||
),
|
||||
templates: options['test-templates'],
|
||||
},
|
||||
});
|
||||
} as any);
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,7 +1,17 @@
|
|||
import configureStore from '../../../app/stores/backgroundStore';
|
||||
import openDevToolsWindow from './openWindow';
|
||||
import { Store } from 'redux';
|
||||
import configureStore, {
|
||||
BackgroundAction,
|
||||
} from '../../../app/stores/backgroundStore';
|
||||
import openDevToolsWindow, { DevToolsPosition } from './openWindow';
|
||||
import { createMenu, removeMenu } from './contextMenus';
|
||||
import syncOptions from '../options/syncOptions';
|
||||
import { BackgroundState } from '../../../app/reducers/background';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
store: Store<BackgroundState, BackgroundAction>;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose the extension's store globally to access it from the windows
|
||||
// via chrome.runtime.getBackgroundPage
|
||||
|
@ -9,7 +19,7 @@ window.store = configureStore();
|
|||
|
||||
// Listen for keyboard shortcuts
|
||||
chrome.commands.onCommand.addListener((shortcut) => {
|
||||
openDevToolsWindow(shortcut);
|
||||
openDevToolsWindow(shortcut as DevToolsPosition);
|
||||
});
|
||||
|
||||
// Create the context menu when installed
|
|
@ -1,6 +1,10 @@
|
|||
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) => {
|
||||
if (!options['s:hostname'] || !options['s:port']) return;
|
||||
const url = `${options['s:secure'] ? 'https' : 'http'}://${
|
|
@ -1,9 +1,20 @@
|
|||
let windows = {};
|
||||
let lastPosition = null;
|
||||
export type DevToolsPosition =
|
||||
| 'devtools-left'
|
||||
| 'devtools-right'
|
||||
| 'devtools-bottom'
|
||||
| 'devtools-panel'
|
||||
| 'devtools-remote';
|
||||
|
||||
export default function openDevToolsWindow(position) {
|
||||
function popWindow(action, url, customOptions) {
|
||||
function focusIfExist(callback) {
|
||||
let windows: { [K in DevToolsPosition]?: number } = {};
|
||||
let lastPosition: DevToolsPosition | null = null;
|
||||
|
||||
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]) {
|
||||
callback();
|
||||
lastPosition = position;
|
||||
|
@ -12,7 +23,7 @@ export default function openDevToolsWindow(position) {
|
|||
if (lastPosition !== position && position !== 'devtools-panel') {
|
||||
params = { ...params, ...customOptions };
|
||||
}
|
||||
chrome.windows.update(windows[position], params, () => {
|
||||
chrome.windows.update(windows[position]!, params, () => {
|
||||
lastPosition = null;
|
||||
if (chrome.runtime.lastError) callback();
|
||||
});
|
||||
|
@ -20,7 +31,7 @@ export default function openDevToolsWindow(position) {
|
|||
}
|
||||
|
||||
focusIfExist(() => {
|
||||
let options = {
|
||||
let options: chrome.windows.CreateData = {
|
||||
type: 'popup',
|
||||
...customOptions,
|
||||
};
|
||||
|
@ -29,16 +40,19 @@ export default function openDevToolsWindow(position) {
|
|||
url + '#' + position.substr(position.indexOf('-') + 1)
|
||||
);
|
||||
chrome.windows.create(options, (win) => {
|
||||
windows[position] = win.id;
|
||||
windows[position] = win!.id;
|
||||
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,
|
||||
top: 0,
|
||||
width: 380,
|
||||
|
@ -48,7 +62,9 @@ export default function openDevToolsWindow(position) {
|
|||
switch (position) {
|
||||
case 'devtools-right':
|
||||
params.left =
|
||||
window.screen.availLeft + window.screen.availWidth - params.width;
|
||||
(window.screen as unknown as { availLeft: number }).availLeft +
|
||||
window.screen.availWidth -
|
||||
params.width!;
|
||||
break;
|
||||
case 'devtools-bottom':
|
||||
params.height = 420;
|
|
@ -1,48 +1,48 @@
|
|||
// Mock not supported chrome.* API for Firefox and Electron
|
||||
|
||||
window.isElectron =
|
||||
(window as any).isElectron =
|
||||
window.navigator && window.navigator.userAgent.indexOf('Electron') !== -1;
|
||||
|
||||
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
|
||||
|
||||
// Background page only
|
||||
if (
|
||||
(window.isElectron &&
|
||||
((window as any).isElectron &&
|
||||
location.pathname === '/_generated_background_page.html') ||
|
||||
isFirefox
|
||||
) {
|
||||
chrome.runtime.onConnectExternal = {
|
||||
(chrome.runtime as any).onConnectExternal = {
|
||||
addListener() {},
|
||||
};
|
||||
chrome.runtime.onMessageExternal = {
|
||||
(chrome.runtime as any).onMessageExternal = {
|
||||
addListener() {},
|
||||
};
|
||||
|
||||
if (window.isElectron) {
|
||||
chrome.notifications = {
|
||||
if ((window as any).isElectron) {
|
||||
(chrome.notifications as any) = {
|
||||
onClicked: {
|
||||
addListener() {},
|
||||
},
|
||||
create() {},
|
||||
clear() {},
|
||||
};
|
||||
chrome.contextMenus = {
|
||||
(chrome.contextMenus as any) = {
|
||||
onClicked: {
|
||||
addListener() {},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
chrome.storage.sync = chrome.storage.local;
|
||||
chrome.runtime.onInstalled = {
|
||||
addListener: (cb) => cb(),
|
||||
(chrome.storage as any).sync = chrome.storage.local;
|
||||
(chrome.runtime as any).onInstalled = {
|
||||
addListener: (cb: any) => cb(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (window.isElectron) {
|
||||
if ((window as any).isElectron) {
|
||||
if (!chrome.storage.local || !chrome.storage.local.remove) {
|
||||
chrome.storage.local = {
|
||||
set(obj, callback) {
|
||||
(chrome.storage as any).local = {
|
||||
set(obj: any, callback: any) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
localStorage.setItem(key, obj[key]);
|
||||
});
|
||||
|
@ -50,8 +50,8 @@ if (window.isElectron) {
|
|||
callback();
|
||||
}
|
||||
},
|
||||
get(obj, callback) {
|
||||
const result = {};
|
||||
get(obj: any, callback: any) {
|
||||
const result: any = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
result[key] = localStorage.getItem(key) || obj[key];
|
||||
});
|
||||
|
@ -60,7 +60,7 @@ if (window.isElectron) {
|
|||
}
|
||||
},
|
||||
// Electron ~ 1.4.6
|
||||
remove(items, callback) {
|
||||
remove(items: any, callback: any) {
|
||||
if (Array.isArray(items)) {
|
||||
items.forEach((name) => {
|
||||
localStorage.removeItem(name);
|
||||
|
@ -75,7 +75,7 @@ if (window.isElectron) {
|
|||
};
|
||||
}
|
||||
// Avoid error: chrome.runtime.sendMessage is not supported responseCallback
|
||||
const originSendMessage = chrome.runtime.sendMessage;
|
||||
const originSendMessage = (chrome.runtime as any).sendMessage;
|
||||
chrome.runtime.sendMessage = function () {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return originSendMessage(...arguments);
|
||||
|
@ -87,6 +87,6 @@ if (window.isElectron) {
|
|||
};
|
||||
}
|
||||
|
||||
if (isFirefox || window.isElectron) {
|
||||
chrome.storage.sync = chrome.storage.local;
|
||||
if (isFirefox || (window as any).isElectron) {
|
||||
(chrome.storage as any).sync = chrome.storage.local;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
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 '../../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 messageStyle = { padding: '20px', width: '100%', textAlign: 'center' };
|
||||
const messageStyle: CSSProperties = {
|
||||
padding: '20px',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
let rendered;
|
||||
let store;
|
||||
let bgConnection;
|
||||
let naTimeout;
|
||||
let preloadedState;
|
||||
let rendered: boolean | undefined;
|
||||
let store: Store<StoreState, StoreActionWithTogglePersist> | undefined;
|
||||
let bgConnection: chrome.runtime.Port;
|
||||
let naTimeout: NodeJS.Timeout;
|
||||
let preloadedState: PreloadedState<StoreState>;
|
||||
|
||||
const isChrome = navigator.userAgent.indexOf('Firefox') === -1;
|
||||
|
||||
|
@ -25,7 +33,7 @@ getPreloadedState(position, (state) => {
|
|||
|
||||
function renderDevTools() {
|
||||
const node = document.getElementById('root');
|
||||
unmountComponentAtNode(node);
|
||||
unmountComponentAtNode(node!);
|
||||
clearTimeout(naTimeout);
|
||||
store = configureStore(position, bgConnection, preloadedState);
|
||||
render(
|
||||
|
@ -71,33 +79,35 @@ function renderNA() {
|
|||
}
|
||||
|
||||
const node = document.getElementById('root');
|
||||
unmountComponentAtNode(node);
|
||||
unmountComponentAtNode(node!);
|
||||
render(message, node);
|
||||
store = undefined;
|
||||
});
|
||||
} else {
|
||||
const node = document.getElementById('root');
|
||||
unmountComponentAtNode(node);
|
||||
unmountComponentAtNode(node!);
|
||||
render(message, node);
|
||||
store = undefined;
|
||||
}
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function init(id) {
|
||||
function init(id: number) {
|
||||
renderNA();
|
||||
bgConnection = chrome.runtime.connect({
|
||||
name: id ? id.toString() : undefined,
|
||||
});
|
||||
bgConnection.onMessage.addListener((message) => {
|
||||
if (message.type === 'NA') {
|
||||
if (message.id === id) renderNA();
|
||||
else store.dispatch({ type: REMOVE_INSTANCE, id: message.id });
|
||||
} else {
|
||||
if (!rendered) renderDevTools();
|
||||
store.dispatch(message);
|
||||
bgConnection.onMessage.addListener(
|
||||
<S, A extends Action<unknown>>(message: PanelMessage<S, A>) => {
|
||||
if (message.type === 'NA') {
|
||||
if (message.id === id) renderNA();
|
||||
else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id });
|
||||
} else {
|
||||
if (!rendered) renderDevTools();
|
||||
store!.dispatch(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
init(chrome.devtools.inspectedWindow.tabId);
|
|
@ -1,6 +1,6 @@
|
|||
import '../../views/devtools.pug';
|
||||
|
||||
function createPanel(url) {
|
||||
function createPanel(url: string) {
|
||||
chrome.devtools.panels.create(
|
||||
'Redux',
|
||||
'img/logo/scalable.png',
|
|
@ -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);
|
313
extension/src/browser/extension/inject/contentScript.ts
Normal file
313
extension/src/browser/extension/inject/contentScript.ts
Normal 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);
|
|
@ -1,6 +1,8 @@
|
|||
// Include this script in Chrome apps and extensions for remote debugging
|
||||
// <script src="chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.bundle.js"></script>
|
||||
|
||||
import { Options } from '../options/syncOptions';
|
||||
|
||||
window.devToolsExtensionID = 'lmhkpmbekcpmknklioeibfkpmmfibljd';
|
||||
require('./contentScript');
|
||||
require('./pageScript');
|
||||
|
@ -8,7 +10,7 @@ require('./pageScript');
|
|||
chrome.runtime.sendMessage(
|
||||
window.devToolsExtensionID,
|
||||
{ type: 'GET_OPTIONS' },
|
||||
function (response) {
|
||||
function (response: { readonly options: Options }) {
|
||||
if (!response.options.inject) {
|
||||
const urls = response.options.urls.split('\n').filter(Boolean).join('|');
|
||||
if (!location.href.match(new RegExp(urls))) return;
|
|
@ -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);
|
||||
};
|
681
extension/src/browser/extension/inject/pageScript.ts
Normal file
681
extension/src/browser/extension/inject/pageScript.ts
Normal 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);
|
||||
};
|
|
@ -5,11 +5,13 @@ if (process.env.NODE_ENV === 'production') {
|
|||
const { default: script } = require('raw-loader!tmp/page.bundle.js');
|
||||
s.appendChild(document.createTextNode(script));
|
||||
(document.head || document.documentElement).appendChild(s);
|
||||
s.parentNode.removeChild(s);
|
||||
s.parentNode!.removeChild(s);
|
||||
} else {
|
||||
s.src = chrome.extension.getURL('page.bundle.js');
|
||||
s.onload = function () {
|
||||
this.parentNode.removeChild(this);
|
||||
(this as HTMLScriptElement).parentNode!.removeChild(
|
||||
this as HTMLScriptElement
|
||||
);
|
||||
};
|
||||
(document.head || document.documentElement).appendChild(s);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }) => {
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
const AllowToRunState = {
|
||||
EVERYWHERE: true,
|
||||
ON_SPECIFIC_URLS: false,
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }) => {
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
return (
|
||||
<fieldset className="option-group">
|
||||
<legend className="option-group__title">Context Menu</legend>
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }) => {
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
const EditorState = {
|
||||
BROWSER: 0,
|
||||
EXTERNAL: 1,
|
||||
|
@ -45,7 +46,7 @@ export default ({ options, saveOption }) => {
|
|||
className="option__element"
|
||||
id="editor"
|
||||
type="text"
|
||||
size="33"
|
||||
size={33}
|
||||
maxLength={30}
|
||||
placeholder="vscode, atom, webstorm, sublime..."
|
||||
value={options.editor}
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { FilterState } from '../../../app/api/filters';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }) => {
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
return (
|
||||
<fieldset className="option-group">
|
||||
<legend className="option-group__title">
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }) => {
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
const browserName = navigator.userAgent.includes('Firefox')
|
||||
? 'Firefox'
|
||||
: 'Chrome';
|
|
@ -4,8 +4,17 @@ import FilterGroup from './FilterGroup';
|
|||
import AllowToRunGroup from './AllowToRunGroup';
|
||||
import MiscellaneousGroup from './MiscellaneousGroup';
|
||||
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>
|
||||
<EditorGroup {...props} />
|
||||
<FilterGroup {...props} />
|
|
@ -1,19 +1,20 @@
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import Options from './Options';
|
||||
import OptionsComponent from './Options';
|
||||
import { Options } from './syncOptions';
|
||||
|
||||
import '../../views/options.pug';
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const renderOptions = (options) => {
|
||||
const renderOptions = (options: Options) => {
|
||||
render(
|
||||
<Options options={options} saveOption={saveOption} />,
|
||||
<OptionsComponent options={options} saveOption={saveOption} />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
152
extension/src/browser/extension/options/syncOptions.ts
Normal file
152
extension/src/browser/extension/options/syncOptions.ts
Normal 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,
|
||||
};
|
||||
}
|
|
@ -1,27 +1,31 @@
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { PreloadedState } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
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 configureStore from '../../../app/stores/windowStore';
|
||||
import getPreloadedState from '../background/getPreloadedState';
|
||||
|
||||
import '../../views/window.pug';
|
||||
import { MonitorMessage } from '../../../app/middlewares/api';
|
||||
|
||||
const position = location.hash;
|
||||
let preloadedState;
|
||||
let preloadedState: PreloadedState<StoreState>;
|
||||
getPreloadedState(position, (state) => {
|
||||
preloadedState = state;
|
||||
});
|
||||
|
||||
chrome.runtime.getBackgroundPage(({ store }) => {
|
||||
chrome.runtime.getBackgroundPage((window) => {
|
||||
const { store } = window!;
|
||||
const localStore = configureStore(store, position, preloadedState);
|
||||
let name = 'monitor';
|
||||
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
|
||||
name += chrome.devtools.inspectedWindow.tabId;
|
||||
}
|
||||
const bg = chrome.runtime.connect({ name });
|
||||
const update = (action) => {
|
||||
const update = (action?: MonitorMessage) => {
|
||||
localStore.dispatch(action || { type: UPDATE_STATE });
|
||||
};
|
||||
bg.onMessage.addListener(update);
|
|
@ -14,12 +14,12 @@ chrome.storage.local.get(
|
|||
's:secure': null,
|
||||
},
|
||||
(options) => {
|
||||
const AppAsAny = App as any;
|
||||
render(
|
||||
<App
|
||||
<AppAsAny
|
||||
selectMonitor={options['select-monitor']}
|
||||
testTemplates={options['test-templates']}
|
||||
selectedTemplate={options['test-templates-sel']}
|
||||
testTemplates={options['test-templates']}
|
||||
useCodemirror
|
||||
socketOptions={
|
||||
options['s:hostname'] && options['s:port']
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { mount } from 'enzyme';
|
||||
import { Provider } from 'react-redux';
|
||||
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 component = mount(
|
||||
|
|
4
extension/tsconfig.json
Normal file
4
extension/tsconfig.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "../tsconfig.react.base.json",
|
||||
"include": ["src"]
|
||||
}
|
|
@ -3,7 +3,7 @@ import webpack from 'webpack';
|
|||
import CopyPlugin from 'copy-webpack-plugin';
|
||||
|
||||
const extpath = path.join(__dirname, '../src/browser/extension/');
|
||||
const mock = `${extpath}chromeAPIMock.js`;
|
||||
const mock = `${extpath}chromeAPIMock`;
|
||||
|
||||
const baseConfig = (params) => ({
|
||||
// devtool: 'source-map',
|
||||
|
@ -64,7 +64,7 @@ const baseConfig = (params) => ({
|
|||
app: path.join(__dirname, '../src/app'),
|
||||
tmp: path.join(__dirname, '../build/tmp'),
|
||||
},
|
||||
extensions: ['.js'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
@ -72,7 +72,7 @@ const baseConfig = (params) => ({
|
|||
? params.loaders
|
||||
: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
test: /\.(js|ts)x?$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /(node_modules|tmp\/page\.bundle)/,
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@ import { Features, State } from '../reducers/instances';
|
|||
import { MonitorStateMonitorState } from '../reducers/monitor';
|
||||
import { LiftedAction } from '@redux-devtools/core';
|
||||
import { Data } from '../reducers/reports';
|
||||
import { LiftedState } from '@redux-devtools/instrument';
|
||||
|
||||
let monitorReducer: (
|
||||
monitorProps: unknown,
|
||||
|
@ -53,7 +54,7 @@ let monitorReducer: (
|
|||
) => unknown;
|
||||
let monitorProps: unknown = {};
|
||||
|
||||
interface ChangeSectionAction {
|
||||
export interface ChangeSectionAction {
|
||||
readonly type: typeof CHANGE_SECTION;
|
||||
readonly section: string;
|
||||
}
|
||||
|
@ -69,7 +70,7 @@ interface ChangeThemeFormData {
|
|||
interface ChangeThemeData {
|
||||
readonly formData: ChangeThemeFormData;
|
||||
}
|
||||
interface ChangeThemeAction {
|
||||
export interface ChangeThemeAction {
|
||||
readonly type: typeof CHANGE_THEME;
|
||||
readonly theme: Theme;
|
||||
readonly scheme: Scheme;
|
||||
|
@ -119,13 +120,28 @@ export interface LockChangesAction {
|
|||
}
|
||||
export interface ToggleActionAction {
|
||||
type: 'TOGGLE_ACTION';
|
||||
id: number;
|
||||
}
|
||||
export interface RollbackAction {
|
||||
type: 'ROLLBACK';
|
||||
timestamp: number;
|
||||
}
|
||||
export interface SweepAction {
|
||||
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 =
|
||||
| JumpToStateAction
|
||||
| JumpToActionAction
|
||||
|
@ -133,7 +149,9 @@ export type DispatchAction =
|
|||
| LockChangesAction
|
||||
| ToggleActionAction
|
||||
| RollbackAction
|
||||
| SweepAction;
|
||||
| SweepAction
|
||||
| ReorderActionAction
|
||||
| ImportStateAction;
|
||||
interface LiftedActionActionBase {
|
||||
action?: DispatchAction | string | CustomAction;
|
||||
state?: string;
|
||||
|
@ -145,18 +163,18 @@ export interface LiftedActionDispatchAction extends LiftedActionActionBase {
|
|||
action: DispatchAction;
|
||||
toAll?: boolean;
|
||||
}
|
||||
interface LiftedActionImportAction extends LiftedActionActionBase {
|
||||
export interface LiftedActionImportAction extends LiftedActionActionBase {
|
||||
type: typeof LIFTED_ACTION;
|
||||
message: 'IMPORT';
|
||||
state: string;
|
||||
preloadedState: unknown | undefined;
|
||||
}
|
||||
interface LiftedActionActionAction extends LiftedActionActionBase {
|
||||
export interface LiftedActionActionAction extends LiftedActionActionBase {
|
||||
type: typeof LIFTED_ACTION;
|
||||
message: 'ACTION';
|
||||
action: string | CustomAction;
|
||||
}
|
||||
interface LiftedActionExportAction extends LiftedActionActionBase {
|
||||
export interface LiftedActionExportAction extends LiftedActionActionBase {
|
||||
type: typeof LIFTED_ACTION;
|
||||
message: 'EXPORT';
|
||||
toExport: boolean;
|
||||
|
@ -192,15 +210,15 @@ export function liftedDispatch(
|
|||
} as LiftedActionDispatchAction;
|
||||
}
|
||||
|
||||
interface SelectInstanceAction {
|
||||
export interface SelectInstanceAction {
|
||||
type: typeof SELECT_INSTANCE;
|
||||
selected: string;
|
||||
selected: string | number;
|
||||
}
|
||||
export function selectInstance(selected: string): SelectInstanceAction {
|
||||
return { type: SELECT_INSTANCE, selected };
|
||||
}
|
||||
|
||||
interface SelectMonitorAction {
|
||||
export interface SelectMonitorAction {
|
||||
type: typeof SELECT_MONITOR;
|
||||
monitor: string;
|
||||
monitorState?: MonitorStateMonitorState;
|
||||
|
@ -219,7 +237,7 @@ interface NextState {
|
|||
subTabName: string;
|
||||
inspectedStatePath?: string[];
|
||||
}
|
||||
interface UpdateMonitorStateAction {
|
||||
export interface UpdateMonitorStateAction {
|
||||
type: typeof UPDATE_MONITOR_STATE;
|
||||
nextState: NextState;
|
||||
}
|
||||
|
@ -240,7 +258,7 @@ export function importState(
|
|||
return { type: LIFTED_ACTION, message: 'IMPORT', state, preloadedState };
|
||||
}
|
||||
|
||||
interface ExportAction {
|
||||
export interface ExportAction {
|
||||
type: typeof EXPORT;
|
||||
}
|
||||
export function exportState(): ExportAction {
|
||||
|
@ -268,7 +286,7 @@ export function pauseRecording(status: boolean): LiftedActionDispatchAction {
|
|||
export interface CustomAction {
|
||||
name: string;
|
||||
selected: number;
|
||||
args: (string | undefined)[];
|
||||
args: string[];
|
||||
rest: string;
|
||||
}
|
||||
export function dispatchRemotely(
|
||||
|
@ -277,28 +295,28 @@ export function dispatchRemotely(
|
|||
return { type: LIFTED_ACTION, message: 'ACTION', action };
|
||||
}
|
||||
|
||||
interface TogglePersistAction {
|
||||
export interface TogglePersistAction {
|
||||
type: typeof TOGGLE_PERSIST;
|
||||
}
|
||||
export function togglePersist(): TogglePersistAction {
|
||||
return { type: TOGGLE_PERSIST };
|
||||
}
|
||||
|
||||
interface ToggleSyncAction {
|
||||
export interface ToggleSyncAction {
|
||||
type: typeof TOGGLE_SYNC;
|
||||
}
|
||||
export function toggleSync(): ToggleSyncAction {
|
||||
return { type: TOGGLE_SYNC };
|
||||
}
|
||||
|
||||
interface ToggleSliderAction {
|
||||
export interface ToggleSliderAction {
|
||||
type: typeof TOGGLE_SLIDER;
|
||||
}
|
||||
export function toggleSlider(): ToggleSliderAction {
|
||||
return { type: TOGGLE_SLIDER };
|
||||
}
|
||||
|
||||
interface ToggleDispatcherAction {
|
||||
export interface ToggleDispatcherAction {
|
||||
type: typeof TOGGLE_DISPATCHER;
|
||||
}
|
||||
export function toggleDispatcher(): ToggleDispatcherAction {
|
||||
|
@ -312,7 +330,7 @@ export interface ConnectionOptions {
|
|||
readonly port: number;
|
||||
readonly secure: boolean;
|
||||
}
|
||||
interface ReconnectAction {
|
||||
export interface ReconnectAction {
|
||||
readonly type: typeof RECONNECT;
|
||||
readonly options: ConnectionOptions;
|
||||
}
|
||||
|
@ -326,7 +344,7 @@ interface Notification {
|
|||
readonly type: 'error';
|
||||
readonly message: string;
|
||||
}
|
||||
interface ShowNotificationAction {
|
||||
export interface ShowNotificationAction {
|
||||
readonly type: typeof SHOW_NOTIFICATION;
|
||||
readonly notification: Notification;
|
||||
}
|
||||
|
@ -334,14 +352,14 @@ export function showNotification(message: string): ShowNotificationAction {
|
|||
return { type: SHOW_NOTIFICATION, notification: { type: 'error', message } };
|
||||
}
|
||||
|
||||
interface ClearNotificationAction {
|
||||
export interface ClearNotificationAction {
|
||||
readonly type: typeof CLEAR_NOTIFICATION;
|
||||
}
|
||||
export function clearNotification(): ClearNotificationAction {
|
||||
return { type: CLEAR_NOTIFICATION };
|
||||
}
|
||||
|
||||
interface GetReportRequest {
|
||||
export interface GetReportRequest {
|
||||
readonly type: typeof GET_REPORT_REQUEST;
|
||||
readonly report: unknown;
|
||||
}
|
||||
|
@ -354,7 +372,7 @@ export interface ActionCreator {
|
|||
name: string;
|
||||
}
|
||||
|
||||
interface LibConfig {
|
||||
export interface LibConfig {
|
||||
actionCreators?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
|
@ -363,10 +381,10 @@ interface LibConfig {
|
|||
}
|
||||
|
||||
export interface RequestBase {
|
||||
id: string;
|
||||
instanceId?: string;
|
||||
id?: string;
|
||||
instanceId?: string | number;
|
||||
action?: string;
|
||||
name?: string;
|
||||
name?: string | undefined;
|
||||
libConfig?: LibConfig;
|
||||
actionsById?: string;
|
||||
computedStates?: string;
|
||||
|
@ -376,14 +394,15 @@ export interface RequestBase {
|
|||
}
|
||||
interface InitRequest extends RequestBase {
|
||||
type: 'INIT';
|
||||
action: string;
|
||||
action?: string;
|
||||
payload?: string;
|
||||
}
|
||||
interface ActionRequest extends RequestBase {
|
||||
type: 'ACTION';
|
||||
isExcess: boolean;
|
||||
isExcess?: boolean;
|
||||
nextActionId: number;
|
||||
maxAge: number;
|
||||
batched: boolean;
|
||||
batched?: boolean;
|
||||
}
|
||||
interface StateRequest extends RequestBase {
|
||||
type: 'STATE';
|
||||
|
@ -409,23 +428,23 @@ export type Request =
|
|||
| LiftedRequest
|
||||
| ExportRequest;
|
||||
|
||||
interface UpdateStateAction {
|
||||
export interface UpdateStateAction {
|
||||
type: typeof UPDATE_STATE;
|
||||
request?: Request;
|
||||
id?: string;
|
||||
id?: string | number;
|
||||
}
|
||||
|
||||
interface SetStateAction {
|
||||
export interface SetStateAction {
|
||||
type: typeof SET_STATE;
|
||||
newState: State;
|
||||
}
|
||||
|
||||
interface RemoveInstanceAction {
|
||||
export interface RemoveInstanceAction {
|
||||
type: typeof REMOVE_INSTANCE;
|
||||
id: string;
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
interface ConnectRequestAction {
|
||||
export interface ConnectRequestAction {
|
||||
type: typeof CONNECT_REQUEST;
|
||||
options: ConnectionOptions;
|
||||
}
|
||||
|
@ -435,58 +454,58 @@ interface ConnectSuccessPayload {
|
|||
authState: AuthStates;
|
||||
socketState: States;
|
||||
}
|
||||
interface ConnectSuccessAction {
|
||||
export interface ConnectSuccessAction {
|
||||
type: typeof CONNECT_SUCCESS;
|
||||
payload: ConnectSuccessPayload;
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
interface ConnectErrorAction {
|
||||
export interface ConnectErrorAction {
|
||||
type: typeof CONNECT_ERROR;
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
interface AuthRequestAction {
|
||||
export interface AuthRequestAction {
|
||||
type: typeof AUTH_REQUEST;
|
||||
}
|
||||
|
||||
interface AuthSuccessAction {
|
||||
export interface AuthSuccessAction {
|
||||
type: typeof AUTH_SUCCESS;
|
||||
baseChannel: string;
|
||||
}
|
||||
|
||||
interface AuthErrorAction {
|
||||
export interface AuthErrorAction {
|
||||
type: typeof AUTH_ERROR;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
interface DisconnectedAction {
|
||||
export interface DisconnectedAction {
|
||||
type: typeof DISCONNECTED;
|
||||
code: number;
|
||||
}
|
||||
|
||||
interface DeauthenticateAction {
|
||||
export interface DeauthenticateAction {
|
||||
type: typeof DEAUTHENTICATE;
|
||||
}
|
||||
|
||||
interface SubscribeRequestAction {
|
||||
export interface SubscribeRequestAction {
|
||||
type: typeof SUBSCRIBE_REQUEST;
|
||||
channel: string;
|
||||
subscription: typeof UPDATE_STATE | typeof UPDATE_REPORTS;
|
||||
}
|
||||
|
||||
interface SubscribeSuccessAction {
|
||||
export interface SubscribeSuccessAction {
|
||||
type: typeof SUBSCRIBE_SUCCESS;
|
||||
channel: string;
|
||||
}
|
||||
|
||||
interface SubscribeErrorAction {
|
||||
export interface SubscribeErrorAction {
|
||||
type: typeof SUBSCRIBE_ERROR;
|
||||
error: Error;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface UnsubscribeAction {
|
||||
export interface UnsubscribeAction {
|
||||
type: typeof UNSUBSCRIBE;
|
||||
channel: string;
|
||||
}
|
||||
|
@ -494,8 +513,8 @@ interface UnsubscribeAction {
|
|||
export interface EmitAction {
|
||||
type: typeof EMIT;
|
||||
message: string;
|
||||
id?: string | false;
|
||||
instanceId?: string;
|
||||
id?: string | number | false;
|
||||
instanceId?: string | number;
|
||||
action?: unknown;
|
||||
state?: unknown;
|
||||
}
|
||||
|
@ -514,31 +533,30 @@ interface RemoveRequest {
|
|||
id: unknown;
|
||||
}
|
||||
export type UpdateReportsRequest = ListRequest | AddRequest | RemoveRequest;
|
||||
interface UpdateReportsAction {
|
||||
export interface UpdateReportsAction {
|
||||
type: typeof UPDATE_REPORTS;
|
||||
request: UpdateReportsRequest;
|
||||
}
|
||||
|
||||
interface GetReportError {
|
||||
export interface GetReportError {
|
||||
type: typeof GET_REPORT_ERROR;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
interface GetReportSuccess {
|
||||
export interface GetReportSuccess {
|
||||
type: typeof GET_REPORT_SUCCESS;
|
||||
data: { payload: string };
|
||||
}
|
||||
|
||||
interface ErrorAction {
|
||||
export interface ErrorAction {
|
||||
type: typeof ERROR;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export type StoreAction =
|
||||
export type StoreActionWithoutUpdateStateOrLiftedAction =
|
||||
| ChangeSectionAction
|
||||
| ChangeThemeAction
|
||||
| MonitorActionAction
|
||||
| LiftedActionAction
|
||||
| SelectInstanceAction
|
||||
| SelectMonitorAction
|
||||
| UpdateMonitorStateAction
|
||||
|
@ -552,7 +570,6 @@ export type StoreAction =
|
|||
| ClearNotificationAction
|
||||
| GetReportRequest
|
||||
| SetStateAction
|
||||
| UpdateStateAction
|
||||
| RemoveInstanceAction
|
||||
| ConnectRequestAction
|
||||
| ConnectSuccessAction
|
||||
|
@ -571,3 +588,13 @@ export type StoreAction =
|
|||
| GetReportError
|
||||
| GetReportSuccess
|
||||
| ErrorAction;
|
||||
|
||||
export type StoreActionWithoutUpdateState =
|
||||
| StoreActionWithoutUpdateStateOrLiftedAction
|
||||
| LiftedActionAction;
|
||||
|
||||
export type StoreActionWithoutLiftedAction =
|
||||
| StoreActionWithoutUpdateStateOrLiftedAction
|
||||
| UpdateStateAction;
|
||||
|
||||
export type StoreAction = StoreActionWithoutUpdateState | UpdateStateAction;
|
||||
|
|
|
@ -9,7 +9,7 @@ type DispatchProps = ResolveThunks<typeof actionCreators>;
|
|||
type Props = StateProps & DispatchProps;
|
||||
|
||||
class InstanceSelector extends Component<Props> {
|
||||
select?: { readonly value: string; readonly label: string }[];
|
||||
select?: { readonly value: string; readonly label: string | number }[];
|
||||
|
||||
render() {
|
||||
this.select = [{ value: '', label: 'Autoselect instances' }];
|
||||
|
|
|
@ -55,7 +55,7 @@ type Props = DispatchProps & OwnProps;
|
|||
interface State {
|
||||
selected: 'default' | number;
|
||||
customAction: string;
|
||||
args: (string | undefined)[];
|
||||
args: string[];
|
||||
rest: string;
|
||||
changed: boolean;
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ class Dispatcher extends Component<Props, State> {
|
|||
handleArg = (argIndex: number) => (value: string) => {
|
||||
const args = [
|
||||
...this.state.args.slice(0, argIndex),
|
||||
value || undefined,
|
||||
(value || undefined)!,
|
||||
...this.state.args.slice(argIndex + 1),
|
||||
];
|
||||
this.setState({ args, changed: true });
|
||||
|
|
|
@ -30,7 +30,7 @@ let socket: SCClientSocket;
|
|||
let store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>;
|
||||
|
||||
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) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Dispatch, MiddlewareAPI } from 'redux';
|
|||
import { ExportRequest, StoreAction } from '../actions';
|
||||
import { StoreState } from '../reducers';
|
||||
|
||||
let toExport: string | undefined;
|
||||
let toExport: string | number | undefined;
|
||||
|
||||
function download(state: string) {
|
||||
const blob = new Blob([state], { type: 'octet/stream' });
|
||||
|
|
|
@ -34,8 +34,8 @@ export interface Features {
|
|||
}
|
||||
|
||||
export interface Options {
|
||||
name?: string;
|
||||
connectionId?: string;
|
||||
name?: string | number;
|
||||
connectionId?: string | number;
|
||||
explicitLib?: string;
|
||||
lib?: string;
|
||||
actionCreators?: ActionCreator[];
|
||||
|
@ -56,10 +56,10 @@ export interface State {
|
|||
}
|
||||
|
||||
export interface InstancesState {
|
||||
selected: string | null;
|
||||
current: string;
|
||||
selected: string | number | null;
|
||||
current: string | number;
|
||||
sync: boolean;
|
||||
connections: { [id: string]: string[] };
|
||||
connections: { [id: string]: (string | number)[] };
|
||||
options: { [id: string]: Options };
|
||||
states: { [id: string]: State };
|
||||
persisted?: boolean;
|
||||
|
@ -86,7 +86,7 @@ export const initialState: InstancesState = {
|
|||
function updateState(
|
||||
state: { [id: string]: State },
|
||||
request: Request,
|
||||
id: string,
|
||||
id: string | number,
|
||||
serialize: boolean | undefined
|
||||
) {
|
||||
let payload: State = request.payload as State;
|
||||
|
@ -231,7 +231,7 @@ export function dispatchAction(
|
|||
return state;
|
||||
}
|
||||
|
||||
function removeState(state: InstancesState, connectionId: string) {
|
||||
function removeState(state: InstancesState, connectionId: string | number) {
|
||||
const instanceIds = state.connections[connectionId];
|
||||
if (!instanceIds) return state;
|
||||
|
||||
|
@ -268,8 +268,8 @@ function removeState(state: InstancesState, connectionId: string) {
|
|||
|
||||
function init(
|
||||
{ type, action, name, libConfig = {} }: Request,
|
||||
connectionId: string,
|
||||
current: string
|
||||
connectionId: string | number,
|
||||
current: string | number
|
||||
): Options {
|
||||
let lib;
|
||||
let actionCreators;
|
||||
|
@ -310,7 +310,7 @@ export default function instances(
|
|||
case UPDATE_STATE: {
|
||||
const { request } = action;
|
||||
if (!request) return state;
|
||||
const connectionId = action.id || request.id;
|
||||
const connectionId = (action.id || request.id)!;
|
||||
const current = request.instanceId || connectionId;
|
||||
let connections = state.connections;
|
||||
let options = state.options;
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface MonitorStateMonitorState {
|
|||
}
|
||||
export interface MonitorState {
|
||||
selected: string;
|
||||
monitorState: MonitorStateMonitorState | undefined;
|
||||
monitorState?: MonitorStateMonitorState | undefined;
|
||||
sliderIsOpen: boolean;
|
||||
dispatcherIsOpen: boolean;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@ import stringifyJSON from './stringifyJSON';
|
|||
import { SET_STATE } from '../constants/actionTypes';
|
||||
import { InstancesState, State } from '../reducers/instances';
|
||||
import { Dispatch, MiddlewareAPI } from 'redux';
|
||||
import { DispatchAction, StoreAction } from '../actions';
|
||||
import { StoreState } from '../reducers';
|
||||
import { DispatchAction, StoreActionWithoutLiftedAction } from '../actions';
|
||||
|
||||
export function sweep(state: State): State {
|
||||
return {
|
||||
|
@ -21,12 +20,15 @@ export function sweep(state: State): State {
|
|||
}
|
||||
|
||||
export function nonReduxDispatch(
|
||||
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
|
||||
store: MiddlewareAPI<
|
||||
Dispatch<StoreActionWithoutLiftedAction>,
|
||||
{ readonly instances: InstancesState }
|
||||
>,
|
||||
message: string,
|
||||
instanceId: string,
|
||||
instanceId: string | number,
|
||||
action: DispatchAction,
|
||||
initialState: string | undefined,
|
||||
preInstances: InstancesState
|
||||
preInstances?: InstancesState
|
||||
) {
|
||||
const instances = preInstances || store.getState().instances;
|
||||
const state = instances.states[instanceId];
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
"@redux-devtools/inspector-monitor": "^1.0.0",
|
||||
"@types/es6template": "^1.0.0",
|
||||
"@types/history": "^4.7.8",
|
||||
"@types/jsan": "^3.1.0",
|
||||
"@types/jsan": "^3.1.2",
|
||||
"@types/lodash.shuffle": "^4.2.6",
|
||||
"@types/object-path": "^0.11.0",
|
||||
"@types/react": "^16.14.8",
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@types/chrome": "^0.0.124",
|
||||
"@types/chrome": "^0.0.145",
|
||||
"anser": "^1.4.10",
|
||||
"html-entities": "^1.4.0",
|
||||
"redux-devtools-themes": "^1.0.0",
|
||||
|
|
|
@ -32,11 +32,11 @@ function openAndCloseTab(url: string) {
|
|||
const removeTab = () => {
|
||||
chrome.windows.onFocusChanged.removeListener(removeTab);
|
||||
if (tab && tab.id) {
|
||||
chrome.tabs.remove(tab.id, () => {
|
||||
chrome.tabs.remove(tab.id, async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
if (chrome.runtime.lastError) console.log(chrome.runtime.lastError);
|
||||
else if (chrome.devtools && chrome.devtools.inspectedWindow) {
|
||||
chrome.tabs.update(chrome.devtools.inspectedWindow.tabId, {
|
||||
await chrome.tabs.update(chrome.devtools.inspectedWindow.tabId, {
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ interface ImportStateAction<S, A extends Action<unknown>, MonitorState> {
|
|||
type: typeof ActionTypes.IMPORT_STATE;
|
||||
nextLiftedState: LiftedState<S, A, MonitorState> | readonly A[];
|
||||
preloadedState?: S;
|
||||
noRecompute: boolean | undefined;
|
||||
noRecompute?: boolean | undefined;
|
||||
}
|
||||
|
||||
interface LockChangesAction {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"jsan": "^3.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsan": "^3.1.0",
|
||||
"@types/jsan": "^3.1.2",
|
||||
"immutable": "^4.0.0-rc.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -3,22 +3,24 @@ import jsan from 'jsan';
|
|||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { immutableSerialize } from '@redux-devtools/serialize';
|
||||
import Immutable from 'immutable';
|
||||
import { Action } from 'redux';
|
||||
import { Action, ActionCreator } from 'redux';
|
||||
|
||||
export function generateId(id: string | undefined) {
|
||||
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
|
||||
function flatTree(
|
||||
obj: { [key: string]: (...args: any[]) => unknown },
|
||||
obj: { [key: string]: ActionCreator<Action<unknown>> },
|
||||
namespace = ''
|
||||
) {
|
||||
let functions: {
|
||||
name: string;
|
||||
func: (...args: any[]) => unknown;
|
||||
args: string[];
|
||||
}[] = [];
|
||||
let functions: ActionCreatorObject[] = [];
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const prop = obj[key];
|
||||
if (typeof prop === 'function') {
|
||||
|
@ -63,7 +65,7 @@ export function getMethods(obj: unknown) {
|
|||
}
|
||||
|
||||
export function getActionsArray(actionCreators: {
|
||||
[key: string]: (...args: any[]) => unknown;
|
||||
[key: string]: ActionCreator<Action<unknown>>;
|
||||
}) {
|
||||
if (Array.isArray(actionCreators)) return actionCreators;
|
||||
return flatTree(actionCreators);
|
||||
|
@ -81,10 +83,8 @@ function evalArgs(inArgs: string[], restArgs: string) {
|
|||
}
|
||||
|
||||
export function evalAction(
|
||||
action: string | { args: string[]; rest: string; selected: string },
|
||||
actionCreators: {
|
||||
[selected: string]: { func: (...args: any[]) => Action<unknown> };
|
||||
}
|
||||
action: string | { args: string[]; rest: string; selected: number },
|
||||
actionCreators: readonly ActionCreatorObject[]
|
||||
) {
|
||||
if (typeof action === 'string') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -3520,7 +3520,7 @@ __metadata:
|
|||
"@redux-devtools/inspector-monitor": ^1.0.0
|
||||
"@types/es6template": ^1.0.0
|
||||
"@types/history": ^4.7.8
|
||||
"@types/jsan": ^3.1.0
|
||||
"@types/jsan": ^3.1.2
|
||||
"@types/lodash.shuffle": ^4.2.6
|
||||
"@types/object-path": ^0.11.0
|
||||
"@types/prop-types": ^15.7.3
|
||||
|
@ -3567,7 +3567,7 @@ __metadata:
|
|||
"@redux-devtools/core": ^3.9.0
|
||||
"@redux-devtools/inspector-monitor": ^1.0.0
|
||||
"@types/babel__code-frame": ^7.0.2
|
||||
"@types/chrome": ^0.0.124
|
||||
"@types/chrome": ^0.0.145
|
||||
"@types/enzyme": ^3.10.8
|
||||
"@types/enzyme-adapter-react-16": ^1.0.6
|
||||
"@types/html-entities": ^1.3.4
|
||||
|
@ -3682,7 +3682,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@redux-devtools/serialize@workspace:packages/redux-devtools-serialize"
|
||||
dependencies:
|
||||
"@types/jsan": ^3.1.0
|
||||
"@types/jsan": ^3.1.2
|
||||
immutable: ^4.0.0-rc.12
|
||||
jsan: ^3.1.13
|
||||
peerDependencies:
|
||||
|
@ -4825,13 +4825,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/chrome@npm:^0.0.124":
|
||||
version: 0.0.124
|
||||
resolution: "@types/chrome@npm:0.0.124"
|
||||
"@types/chrome@npm:^0.0.145":
|
||||
version: 0.0.145
|
||||
resolution: "@types/chrome@npm:0.0.145"
|
||||
dependencies:
|
||||
"@types/filesystem": "*"
|
||||
"@types/har-format": "*"
|
||||
checksum: 6499edca5f608dd48651b20d57d9fb30bbcb02cd695cd94d879a11dba7b9492c618edc6b8b5f718e82b58eceea94fb920c871546c2c3bc867595cb7dd020d527
|
||||
checksum: f826d0a071ac7ea68aa97b2f8e34a944c470fbd036fdd6a413987fea03062d354fb14708cc0b59693b4a78ec88f9c5b1fce0c622a8e29e3e4b0917509eaf2a1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -5246,10 +5246,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsan@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@types/jsan@npm:3.1.0"
|
||||
checksum: a0670d90e4bee7110504be73eefff9196b46235faf490062865136b1cbad4d3bac2adb9303e308c523f07716c026bb8e72a12ae7d47a948424d4ca4d7883587d
|
||||
"@types/jsan@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "@types/jsan@npm:3.1.2"
|
||||
checksum: 2ff652807d6067bbc650aaefcda4e3c07b54ddfd7d72283d7c1f1892ad1e18e907b1bbdbee7d0a163efa9e8aed9af5fa9f4ed8e2f27243c46383d31e1181fc11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -23231,6 +23231,7 @@ fsevents@^1.2.7:
|
|||
"@redux-devtools/serialize": ^0.3.0
|
||||
"@redux-devtools/slider-monitor": ^2.0.0-8
|
||||
"@redux-devtools/utils": ^1.0.0-6
|
||||
"@types/jsan": ^3.1.2
|
||||
bestzip: ^2.2.0
|
||||
chromedriver: ^91.0.1
|
||||
electron: ^13.1.2
|
||||
|
|
Loading…
Reference in New Issue
Block a user