redux-devtools/extension/src/app/api/index.ts
Nathan Bierema b82de74592
Add ESM builds (#997)
* Use rollup for d3tooltip

* Use rollup for map2tree

* Set moduleResolution

* Use rollup for d3-state-visualizer

* Use rollup for react-base16-styling

* Use rollup for react-dock

* Use rollup for react-json-tree

* Use rollup for redux-devtools

* Use rollup for redux-devtools-intrument

* Use rollup for redux-devtools-chart-monitor

* Update export

* Use rollup for redux-devtools-dock-monitor

* Use rollup for redux-devtools-inspector-monitor

* Fix inspector demo

* Fix invalid eslint config

* Use rollup for inspector-monitor-test-tab

* Use rollup for inspector-monitor-trace-tab

* Use rollup for redux-devtools-log-monitor

* Use rollup for redux-devtools-remote

* Use rollup in redux-devtools-rtk-query-monitor

* Use rollup for redux-devtools-serialize

* Fix redux-devtools examples

* Use rollup for redux-devtools-slider-monitor

* Fix slider examples

* Use rollup for redux-devtools-ui

* Use rollup for redux-devtools-utils

* Use rollup for redux-devtools-extension

* Use rollup for redux-devtools-app

* Fix Webpack app build

* Fix extension build

* Turn on minimization

* Update CLI
2022-01-10 15:41:53 +00:00

700 lines
19 KiB
TypeScript

import jsan, { Options } from 'jsan';
import throttle from 'lodash/throttle';
import { immutableSerialize } from '@redux-devtools/serialize';
import { getActionsArray, getLocalFilter } from '@redux-devtools/utils';
import { isFiltered, PartialLiftedState } from './filters';
import importState from './importState';
import generateId from './generateInstanceId';
import { Config } from '../../browser/extension/inject/pageScript';
import { Action } from 'redux';
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
import { LibConfig } from '@redux-devtools/app';
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) {
const serialize = config.serialize;
if (serialize) {
if (serialize === true) return { options: true };
if (serialize.immutable) {
const immutableSerializer = immutableSerialize(
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,
};
}
return undefined;
}
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 isInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}