Convert redux-devtools package to TypeScript

This commit is contained in:
Nathan Bierema 2020-05-17 07:35:13 -04:00
parent 93d6bc4350
commit 8eb67453aa
13 changed files with 196 additions and 106 deletions

View File

@ -1,4 +1,10 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-proposal-class-properties"]
"presets": [
"@babel/env",
"@babel/react",
"@babel/typescript"
],
"plugins": [
"@babel/proposal-class-properties"
]
}

View File

@ -0,0 +1,2 @@
lib
examples

View File

@ -0,0 +1,21 @@
module.exports = {
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json']
}
},
{
files: ['test/*.ts', 'test/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./test/tsconfig.json']
}
}
]
};

View File

@ -0,0 +1,2 @@
lib
examples

View File

@ -1,18 +0,0 @@
// Redux version 4.0.0
// Type definitions for redux-devtools 3.4.1
// TypeScript Version: 2.8.1
import * as React from "react";
import { StoreEnhancer } from "redux";
export interface DevTools {
new (): JSX.ElementClass;
instrument(opts?: any): StoreEnhancer;
}
export declare function createDevTools(el: React.ReactElement<any>): DevTools;
export declare function persistState(debugSessionKey: string): StoreEnhancer;
declare const factory: { instrument(opts?: any): () => StoreEnhancer };
export default factory;

View File

@ -3,10 +3,17 @@
"version": "3.5.0",
"description": "Redux DevTools with hot reloading and time travel",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"test": "jest",
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extendsions \".ts,.tsx\" --source-maps inline",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tx --fix",
"test": "tsc --project test/tsconfig.json --noEmit && jest",
"prepare": "npm run build",
"prepublishOnly": "npm run test && npm run clean && npm run build"
},
@ -38,22 +45,32 @@
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.9.0",
"@types/lodash": "^4.2.0",
"@types/react": "^0.14.9 || ^15.3.0 || ^16.0.0",
"@types/react-redux": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"babel-loader": "^8.0.5",
"jest": "^24.1.0",
"react": "^16.0.0",
"react": "^0.14.9 || ^15.3.0 || ^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "^6.0.0",
"redux": "^4.0.0",
"rimraf": "^2.3.4"
"react-redux": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"redux": "^3.5.2 || ^4.0.0",
"rimraf": "^2.3.4",
"typescript": "^3.8.3"
},
"peerDependencies": {
"@types/react": "^0.14.9 || ^15.3.0 || ^16.0.0",
"@types/react-redux": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"react": "^0.14.9 || ^15.3.0 || ^16.0.0",
"react-redux": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"redux": "^3.5.2 || ^4.0.0"
},
"dependencies": {
"prop-types": "^15.5.7",
"@types/prop-types": "^15.5.7",
"lodash": "^4.2.0",
"prop-types": "^15.5.7",
"redux-devtools-instrument": "^1.9.0"
}
}

View File

@ -1,10 +1,15 @@
import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import { connect, Provider, ReactReduxContext } from 'react-redux';
import instrument from 'redux-devtools-instrument';
import instrument, {
EnhancedStore,
LiftedState,
LiftedStore,
Options
} from 'redux-devtools-instrument';
import { Action } from 'redux';
function logError(type) {
/* eslint-disable no-console */
function logError(type: string) {
if (type === 'NoStore') {
console.error(
'Redux DevTools could not render. You must pass the Redux store ' +
@ -18,16 +23,51 @@ function logError(type) {
'using createStore()?'
);
}
/* eslint-enable no-console */
}
export default function createDevTools(children) {
const monitorElement = Children.only(children);
interface Props<
S,
A extends Action,
MonitorState,
MonitorAction extends Action<unknown>
> {
store?: EnhancedStore<S, A, MonitorState, MonitorAction>;
}
type Monitor<
S,
A extends Action<unknown>,
MonitorProps,
MonitorState,
MonitorAction extends Action<unknown>
> = React.ReactElement<
MonitorProps,
React.ComponentType<MonitorProps & LiftedState<S, A, MonitorState>> & {
update(
monitorProps: MonitorProps,
state: MonitorState | undefined,
action: MonitorAction
): MonitorState;
}
>;
export default function createDevTools<
S,
A extends Action<unknown>,
MonitorProps,
MonitorState,
MonitorAction extends Action<unknown>
>(children: Monitor<S, A, MonitorProps, MonitorState, MonitorAction>) {
const monitorElement = Children.only(children)!;
const monitorProps = monitorElement.props;
const Monitor = monitorElement.type;
const ConnectedMonitor = connect(state => state)(Monitor);
const ConnectedMonitor = connect(
(state: LiftedState<S, A, MonitorState>) => state
)(Monitor as React.ComponentType<any>);
return class DevTools extends Component {
return class DevTools extends Component<
Props<S, A, MonitorState, MonitorAction>
> {
static contextTypes = {
store: PropTypes.object
};
@ -36,13 +76,18 @@ export default function createDevTools(children) {
store: PropTypes.object
};
static instrument = options =>
liftedStore?: LiftedStore<S, A, MonitorState, MonitorAction>;
static instrument = (options: Options<S, A, MonitorState, MonitorAction>) =>
instrument(
(state, action) => Monitor.update(monitorProps, state, action),
options
);
constructor(props, context) {
constructor(
props: Props<S, A, MonitorState, MonitorAction>,
context: { store?: EnhancedStore<S, A, MonitorState, MonitorAction> }
) {
super(props, context);
if (ReactReduxContext) {
@ -60,7 +105,7 @@ export default function createDevTools(children) {
if (context.store) {
this.liftedStore = context.store.liftedStore;
} else {
this.liftedStore = props.store.liftedStore;
this.liftedStore = props.store!.liftedStore;
}
if (!this.liftedStore) {
@ -88,12 +133,28 @@ export default function createDevTools(children) {
logError('NoStore');
return null;
}
if (!props.store.liftedStore) {
if (
!((props.store as unknown) as EnhancedStore<
S,
A,
MonitorState,
MonitorAction
>).liftedStore
) {
logError('NoLiftedStore');
return null;
}
return (
<Provider store={props.store.liftedStore}>
<Provider
store={
((props.store as unknown) as EnhancedStore<
S,
A,
MonitorState,
MonitorAction
>).liftedStore
}
>
<ConnectedMonitor {...monitorProps} />
</Provider>
);

View File

@ -1,7 +1,8 @@
export {
default as instrument,
ActionCreators,
ActionTypes
ActionTypes,
LiftedAction
} from 'redux-devtools-instrument';
export { default as persistState } from './persistState';
export { default as createDevTools } from './createDevTools';

View File

@ -1,16 +1,18 @@
import mapValues from 'lodash/mapValues';
import identity from 'lodash/identity';
import { Action, PreloadedState, Reducer, StoreEnhancer } from 'redux';
import { LiftedState } from 'redux-devtools-instrument';
export default function persistState(
sessionId,
deserializeState = identity,
deserializeAction = identity
) {
export default function persistState<S, A extends Action<unknown>>(
sessionId?: string,
deserializeState: (state: S) => S = identity,
deserializeAction: (action: A) => A = identity
): StoreEnhancer {
if (!sessionId) {
return next => (...args) => next(...args);
}
function deserialize(state) {
function deserialize(state: LiftedState<S, A>): LiftedState<S, A> {
return {
...state,
actionsById: mapValues(state.actionsById, liftedAction => ({
@ -25,7 +27,10 @@ export default function persistState(
};
}
return next => (reducer, initialState, enhancer) => {
return next => <S, A extends Action<unknown>>(
reducer: Reducer<S, A>,
initialState?: PreloadedState<S>
) => {
const key = `redux-dev-session-${sessionId}`;
let finalInitialState;
@ -44,11 +49,14 @@ export default function persistState(
}
}
const store = next(reducer, finalInitialState, enhancer);
const store = next(
reducer,
finalInitialState as PreloadedState<S> | undefined
);
return {
...store,
dispatch(action) {
dispatch<T extends A>(action: T) {
store.dispatch(action);
try {

View File

@ -0,0 +1,5 @@
declare namespace NodeJS {
interface Global {
localStorage: Storage;
}
}

View File

@ -1,8 +1,9 @@
import { instrument, persistState } from '../src';
import { compose, createStore } from 'redux';
import './globalLocalStorage.d.ts';
describe('persistState', () => {
let savedLocalStorage = global.localStorage;
const savedLocalStorage = global.localStorage;
delete global.localStorage;
beforeEach(() => {
@ -19,6 +20,12 @@ describe('persistState', () => {
},
clear() {
this.store = {};
},
get length() {
return this.store.length;
},
key(index) {
throw new Error('Unimplemented');
}
};
});
@ -27,7 +34,8 @@ describe('persistState', () => {
global.localStorage = savedLocalStorage;
});
const reducer = (state = 0, action) => {
type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' };
const reducer = (state = 0, action: Action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
@ -41,10 +49,7 @@ describe('persistState', () => {
it('should persist state', () => {
const store = createStore(
reducer,
compose(
instrument(),
persistState('id')
)
compose(instrument(), persistState('id'))
);
expect(store.getState()).toBe(0);
@ -54,46 +59,29 @@ describe('persistState', () => {
const store2 = createStore(
reducer,
compose(
instrument(),
persistState('id')
)
compose(instrument(), persistState('id'))
);
expect(store2.getState()).toBe(2);
});
it('should not persist state if no session id', () => {
const store = createStore(
reducer,
compose(
instrument(),
persistState()
)
);
const store = createStore(reducer, compose(instrument(), persistState()));
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
const store2 = createStore(
reducer,
compose(
instrument(),
persistState()
)
);
const store2 = createStore(reducer, compose(instrument(), persistState()));
expect(store2.getState()).toBe(0);
});
it('should run with a custom state deserializer', () => {
const oneLess = state => (state === undefined ? -1 : state - 1);
const oneLess = (state: number | undefined) =>
state === undefined ? -1 : state - 1;
const store = createStore(
reducer,
compose(
instrument(),
persistState('id', oneLess)
)
compose(instrument(), persistState('id', oneLess))
);
expect(store.getState()).toBe(0);
@ -103,23 +91,17 @@ describe('persistState', () => {
const store2 = createStore(
reducer,
compose(
instrument(),
persistState('id', oneLess)
)
compose(instrument(), persistState('id', oneLess))
);
expect(store2.getState()).toBe(1);
});
it('should run with a custom action deserializer', () => {
const incToDec = action =>
action.type === 'INCREMENT' ? { type: 'DECREMENT' } : action;
const incToDec = (action: Action) =>
action.type === 'INCREMENT' ? ({ type: 'DECREMENT' } as const) : action;
const store = createStore(
reducer,
compose(
instrument(),
persistState('id', undefined, incToDec)
)
compose(instrument(), persistState('id', undefined, incToDec))
);
expect(store.getState()).toBe(0);
@ -129,24 +111,17 @@ describe('persistState', () => {
const store2 = createStore(
reducer,
compose(
instrument(),
persistState('id', undefined, incToDec)
)
compose(instrument(), persistState('id', undefined, incToDec))
);
expect(store2.getState()).toBe(-2);
});
it('should warn if read from localStorage fails', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {
// noop
});
delete global.localStorage.getItem;
createStore(
reducer,
compose(
instrument(),
persistState('id')
)
);
createStore(reducer, compose(instrument(), persistState('id')));
expect(spy.mock.calls[0]).toContain(
'Could not read debug session from localStorage:'
@ -156,14 +131,13 @@ describe('persistState', () => {
});
it('should warn if write to localStorage fails', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {
// noop
});
delete global.localStorage.setItem;
const store = createStore(
reducer,
compose(
instrument(),
persistState('id')
)
compose(instrument(), persistState('id'))
);
store.dispatch({ type: 'INCREMENT' });

View File

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

View File

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