feature(redux-devtools): convert to TypeScript (#605)

This commit is contained in:
Nathan Bierema 2020-08-22 20:16:16 -04:00 committed by GitHub
parent 07a9919a68
commit 97adc01b78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 215 additions and 90 deletions

View File

@ -8,9 +8,6 @@
"@babel/preset-typescript": "^7.10.4",
"@types/jest": "^26.0.9",
"@types/node": "^14.6.0",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/react-test-renderer": "^16.9.3",
"@types/webpack": "^4.41.21",
"@types/webpack-dev-server": "^3.11.0",
"@typescript-eslint/eslint-plugin": "^3.9.0",
@ -25,9 +22,6 @@
"jest": "^26.2.2",
"lerna": "^3.22.1",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.2.0",
"ts-node": "^9.0.0",

View File

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

View File

@ -0,0 +1,3 @@
examples
lib
umd

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

@ -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

@ -0,0 +1,3 @@
module.exports = {
preset: 'ts-jest',
};

View File

@ -2,22 +2,6 @@
"name": "redux-devtools",
"version": "3.6.1",
"description": "Redux DevTools with hot reloading and time travel",
"main": "lib/index.js",
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"test": "jest",
"prepare": "npm run build",
"prepublishOnly": "npm run test && npm run clean && npm run build"
},
"files": [
"lib",
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"keywords": [
"redux",
"devtools",
@ -26,34 +10,52 @@
"time travel",
"live edit"
],
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)",
"license": "MIT",
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"license": "MIT",
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)",
"files": [
"lib",
"src"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"scripts": {
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"clean": "rimraf lib",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"preversion": "npm run type-check && npm run lint && npm run test",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@types/prop-types": "^15.7.3",
"lodash": "^4.17.19",
"prop-types": "^15.7.2",
"redux-devtools-instrument": "^1.9.7"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"jest": "^26.2.2",
"@types/lodash": "^4.14.159",
"@types/react": "^16.3.18",
"@types/react-redux": "^7.1.9",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"rimraf": "^3.0.2"
"redux": "^4.0.5"
},
"peerDependencies": {
"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": {
"lodash": "^4.17.19",
"prop-types": "^15.7.2",
"redux-devtools-instrument": "^1.9.7"
}
}

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) {
interface Props<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> {
store?: EnhancedStore<S, A, MonitorState>;
}
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>;
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> }
) {
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,23 @@ export default function createDevTools(children) {
logError('NoStore');
return null;
}
if (!props.store.liftedStore) {
if (
!((props.store as unknown) as EnhancedStore<S, A, MonitorState>)
.liftedStore
) {
logError('NoLiftedStore');
return null;
}
return (
<Provider store={props.store.liftedStore}>
<Provider
store={
((props.store as unknown) as EnhancedStore<
S,
A,
MonitorState
>).liftedStore
}
>
<ConnectedMonitor {...monitorProps} />
</Provider>
);

View File

@ -1,16 +1,24 @@
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>,
MonitorState
>(
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, MonitorState>
): LiftedState<S, A, MonitorState> {
return {
...state,
actionsById: mapValues(state.actionsById, (liftedAction) => ({
@ -25,7 +33,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 +55,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

@ -2,7 +2,7 @@ import { instrument, persistState } from '../src';
import { compose, createStore } from 'redux';
describe('persistState', () => {
let savedLocalStorage = global.localStorage;
const savedLocalStorage = global.localStorage;
delete global.localStorage;
beforeEach(() => {
@ -20,6 +20,12 @@ describe('persistState', () => {
clear() {
this.store = {};
},
get length() {
return this.store.length;
},
key(index) {
throw new Error('Unimplemented');
},
};
});
@ -27,7 +33,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;
@ -69,7 +76,8 @@ describe('persistState', () => {
});
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))
@ -88,8 +96,8 @@ describe('persistState', () => {
});
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))
@ -108,7 +116,9 @@ describe('persistState', () => {
});
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')));
@ -120,7 +130,9 @@ 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,

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"]
}

View File

@ -3136,6 +3136,14 @@
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/html-minifier-terser@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"
@ -3209,7 +3217,7 @@
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
"@types/lodash@*", "@types/lodash@^4.14.159":
version "4.14.159"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065"
integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg==
@ -3286,6 +3294,16 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.9":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3"
integrity sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-test-renderer@^16.9.3":
version "16.9.3"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.3.tgz#96bab1860904366f4e848b739ba0e2f67bcae87e"
@ -3300,7 +3318,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.11", "@types/react@^16.9.35", "@types/react@^16.9.46":
"@types/react@*", "@types/react@^16.3.18", "@types/react@^16.9.11", "@types/react@^16.9.35", "@types/react@^16.9.46":
version "16.9.46"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e"
integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg==
@ -13717,7 +13735,7 @@ redux-thunk@^2.3.0:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^4.0.1, redux@^4.0.5:
redux@^4.0.0, redux@^4.0.1, redux@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==