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", "@babel/preset-typescript": "^7.10.4",
"@types/jest": "^26.0.9", "@types/jest": "^26.0.9",
"@types/node": "^14.6.0", "@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": "^4.41.21",
"@types/webpack-dev-server": "^3.11.0", "@types/webpack-dev-server": "^3.11.0",
"@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/eslint-plugin": "^3.9.0",
@ -25,9 +22,6 @@
"jest": "^26.2.2", "jest": "^26.2.2",
"lerna": "^3.22.1", "lerna": "^3.22.1",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-jest": "^26.2.0", "ts-jest": "^26.2.0",
"ts-node": "^9.0.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"] "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", "name": "redux-devtools",
"version": "3.6.1", "version": "3.6.1",
"description": "Redux DevTools with hot reloading and time travel", "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": [ "keywords": [
"redux", "redux",
"devtools", "devtools",
@ -26,34 +10,52 @@
"time travel", "time travel",
"live edit" "live edit"
], ],
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)", "homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools",
"license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues" "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": { "devDependencies": {
"@babel/cli": "^7.10.5", "@types/lodash": "^4.14.159",
"@babel/core": "^7.11.1", "@types/react": "^16.3.18",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@types/react-redux": "^7.1.9",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"jest": "^26.2.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.1", "react-redux": "^7.2.1",
"redux": "^4.0.5", "redux": "^4.0.5"
"rimraf": "^3.0.2"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^0.14.9 || ^15.3.0 || ^16.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", "react-redux": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"redux": "^3.5.2 || ^4.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 React, { Children, Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect, Provider, ReactReduxContext } from 'react-redux'; 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) { function logError(type: string) {
/* eslint-disable no-console */
if (type === 'NoStore') { if (type === 'NoStore') {
console.error( console.error(
'Redux DevTools could not render. You must pass the Redux store ' + 'Redux DevTools could not render. You must pass the Redux store ' +
@ -18,16 +23,51 @@ function logError(type) {
'using createStore()?' '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 monitorElement = Children.only(children);
const monitorProps = monitorElement.props; const monitorProps = monitorElement.props;
const Monitor = monitorElement.type; 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 = { static contextTypes = {
store: PropTypes.object, store: PropTypes.object,
}; };
@ -36,13 +76,18 @@ export default function createDevTools(children) {
store: PropTypes.object, store: PropTypes.object,
}; };
static instrument = (options) => liftedStore?: LiftedStore<S, A, MonitorState>;
static instrument = (options: Options<S, A, MonitorState, MonitorAction>) =>
instrument( instrument(
(state, action) => Monitor.update(monitorProps, state, action), (state, action) => Monitor.update(monitorProps, state, action),
options options
); );
constructor(props, context) { constructor(
props: Props<S, A, MonitorState, MonitorAction>,
context: { store?: EnhancedStore<S, A, MonitorState> }
) {
super(props, context); super(props, context);
if (ReactReduxContext) { if (ReactReduxContext) {
@ -60,7 +105,7 @@ export default function createDevTools(children) {
if (context.store) { if (context.store) {
this.liftedStore = context.store.liftedStore; this.liftedStore = context.store.liftedStore;
} else { } else {
this.liftedStore = props.store.liftedStore; this.liftedStore = props.store!.liftedStore;
} }
if (!this.liftedStore) { if (!this.liftedStore) {
@ -88,12 +133,23 @@ export default function createDevTools(children) {
logError('NoStore'); logError('NoStore');
return null; return null;
} }
if (!props.store.liftedStore) { if (
!((props.store as unknown) as EnhancedStore<S, A, MonitorState>)
.liftedStore
) {
logError('NoLiftedStore'); logError('NoLiftedStore');
return null; return null;
} }
return ( return (
<Provider store={props.store.liftedStore}> <Provider
store={
((props.store as unknown) as EnhancedStore<
S,
A,
MonitorState
>).liftedStore
}
>
<ConnectedMonitor {...monitorProps} /> <ConnectedMonitor {...monitorProps} />
</Provider> </Provider>
); );

View File

@ -1,16 +1,24 @@
import mapValues from 'lodash/mapValues'; import mapValues from 'lodash/mapValues';
import identity from 'lodash/identity'; import identity from 'lodash/identity';
import { Action, PreloadedState, Reducer, StoreEnhancer } from 'redux';
import { LiftedState } from 'redux-devtools-instrument';
export default function persistState( export default function persistState<
sessionId, S,
deserializeState = identity, A extends Action<unknown>,
deserializeAction = identity MonitorState
) { >(
sessionId?: string,
deserializeState: (state: S) => S = identity,
deserializeAction: (action: A) => A = identity
): StoreEnhancer {
if (!sessionId) { if (!sessionId) {
return (next) => (...args) => next(...args); return (next) => (...args) => next(...args);
} }
function deserialize(state) { function deserialize(
state: LiftedState<S, A, MonitorState>
): LiftedState<S, A, MonitorState> {
return { return {
...state, ...state,
actionsById: mapValues(state.actionsById, (liftedAction) => ({ 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}`; const key = `redux-dev-session-${sessionId}`;
let finalInitialState; 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 { return {
...store, ...store,
dispatch(action) { dispatch<T extends A>(action: T) {
store.dispatch(action); store.dispatch(action);
try { 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'; import { compose, createStore } from 'redux';
describe('persistState', () => { describe('persistState', () => {
let savedLocalStorage = global.localStorage; const savedLocalStorage = global.localStorage;
delete global.localStorage; delete global.localStorage;
beforeEach(() => { beforeEach(() => {
@ -20,6 +20,12 @@ describe('persistState', () => {
clear() { clear() {
this.store = {}; this.store = {};
}, },
get length() {
return this.store.length;
},
key(index) {
throw new Error('Unimplemented');
},
}; };
}); });
@ -27,7 +33,8 @@ describe('persistState', () => {
global.localStorage = savedLocalStorage; global.localStorage = savedLocalStorage;
}); });
const reducer = (state = 0, action) => { type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' };
const reducer = (state = 0, action: Action) => {
switch (action.type) { switch (action.type) {
case 'INCREMENT': case 'INCREMENT':
return state + 1; return state + 1;
@ -69,7 +76,8 @@ describe('persistState', () => {
}); });
it('should run with a custom state deserializer', () => { 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( const store = createStore(
reducer, reducer,
compose(instrument(), persistState('id', oneLess)) compose(instrument(), persistState('id', oneLess))
@ -88,8 +96,8 @@ describe('persistState', () => {
}); });
it('should run with a custom action deserializer', () => { it('should run with a custom action deserializer', () => {
const incToDec = (action) => const incToDec = (action: Action) =>
action.type === 'INCREMENT' ? { type: 'DECREMENT' } : action; action.type === 'INCREMENT' ? ({ type: 'DECREMENT' } as const) : action;
const store = createStore( const store = createStore(
reducer, reducer,
compose(instrument(), persistState('id', undefined, incToDec)) compose(instrument(), persistState('id', undefined, incToDec))
@ -108,7 +116,9 @@ describe('persistState', () => {
}); });
it('should warn if read from localStorage fails', () => { 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; delete global.localStorage.getItem;
createStore(reducer, compose(instrument(), persistState('id'))); createStore(reducer, compose(instrument(), persistState('id')));
@ -120,7 +130,9 @@ describe('persistState', () => {
}); });
it('should warn if write to localStorage fails', () => { 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; delete global.localStorage.setItem;
const store = createStore( const store = createStore(
reducer, 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: dependencies:
"@types/node" "*" "@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": "@types/html-minifier-terser@^5.0.0":
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"
@ -3209,7 +3217,7 @@
dependencies: dependencies:
"@types/lodash" "*" "@types/lodash" "*"
"@types/lodash@*": "@types/lodash@*", "@types/lodash@^4.14.159":
version "4.14.159" version "4.14.159"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065"
integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg==
@ -3286,6 +3294,16 @@
dependencies: dependencies:
"@types/react" "*" "@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": "@types/react-test-renderer@^16.9.3":
version "16.9.3" version "16.9.3"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.3.tgz#96bab1860904366f4e848b739ba0e2f67bcae87e" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.3.tgz#96bab1860904366f4e848b739ba0e2f67bcae87e"
@ -3300,7 +3318,7 @@
dependencies: dependencies:
"@types/react" "*" "@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" version "16.9.46"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e"
integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg== 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" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== 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" version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==