From 97adc01b78274b26d820ac551cc163e9ac1ed635 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Sat, 22 Aug 2020 20:16:16 -0400 Subject: [PATCH] feature(redux-devtools): convert to TypeScript (#605) --- package.json | 6 -- packages/redux-devtools/.babelrc | 6 +- packages/redux-devtools/.eslintignore | 3 + packages/redux-devtools/.eslintrc.js | 21 +++++ packages/redux-devtools/index.d.ts | 18 ----- packages/redux-devtools/jest.config.js | 3 + packages/redux-devtools/package.json | 70 ++++++++-------- .../{createDevTools.js => createDevTools.tsx} | 80 ++++++++++++++++--- .../redux-devtools/src/{index.js => index.ts} | 0 .../src/{persistState.js => persistState.ts} | 32 +++++--- .../test/globalLocalStorage.d.ts | 5 ++ ...sistState.spec.js => persistState.spec.ts} | 26 ++++-- packages/redux-devtools/test/tsconfig.json | 4 + packages/redux-devtools/tsconfig.json | 7 ++ yarn.lock | 24 +++++- 15 files changed, 215 insertions(+), 90 deletions(-) create mode 100644 packages/redux-devtools/.eslintignore create mode 100644 packages/redux-devtools/.eslintrc.js delete mode 100644 packages/redux-devtools/index.d.ts create mode 100644 packages/redux-devtools/jest.config.js rename packages/redux-devtools/src/{createDevTools.js => createDevTools.tsx} (56%) rename packages/redux-devtools/src/{index.js => index.ts} (100%) rename packages/redux-devtools/src/{persistState.js => persistState.ts} (66%) create mode 100644 packages/redux-devtools/test/globalLocalStorage.d.ts rename packages/redux-devtools/test/{persistState.spec.js => persistState.spec.ts} (85%) create mode 100644 packages/redux-devtools/test/tsconfig.json create mode 100644 packages/redux-devtools/tsconfig.json diff --git a/package.json b/package.json index 8ebdde44..9299a057 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/redux-devtools/.babelrc b/packages/redux-devtools/.babelrc index e60d3036..0d42ef44 100644 --- a/packages/redux-devtools/.babelrc +++ b/packages/redux-devtools/.babelrc @@ -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"] } diff --git a/packages/redux-devtools/.eslintignore b/packages/redux-devtools/.eslintignore new file mode 100644 index 00000000..ed3bf25e --- /dev/null +++ b/packages/redux-devtools/.eslintignore @@ -0,0 +1,3 @@ +examples +lib +umd diff --git a/packages/redux-devtools/.eslintrc.js b/packages/redux-devtools/.eslintrc.js new file mode 100644 index 00000000..eca0de40 --- /dev/null +++ b/packages/redux-devtools/.eslintrc.js @@ -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'], + }, + }, + ], +}; diff --git a/packages/redux-devtools/index.d.ts b/packages/redux-devtools/index.d.ts deleted file mode 100644 index c56d1619..00000000 --- a/packages/redux-devtools/index.d.ts +++ /dev/null @@ -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): DevTools; -export declare function persistState(debugSessionKey: string): StoreEnhancer; - -declare const factory: { instrument(opts?: any): () => StoreEnhancer }; - -export default factory; diff --git a/packages/redux-devtools/jest.config.js b/packages/redux-devtools/jest.config.js new file mode 100644 index 00000000..8824c114 --- /dev/null +++ b/packages/redux-devtools/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'ts-jest', +}; diff --git a/packages/redux-devtools/package.json b/packages/redux-devtools/package.json index f5505f5f..8a732d72 100644 --- a/packages/redux-devtools/package.json +++ b/packages/redux-devtools/package.json @@ -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 (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 (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" } } diff --git a/packages/redux-devtools/src/createDevTools.js b/packages/redux-devtools/src/createDevTools.tsx similarity index 56% rename from packages/redux-devtools/src/createDevTools.js rename to packages/redux-devtools/src/createDevTools.tsx index 43050e06..13f76ad5 100644 --- a/packages/redux-devtools/src/createDevTools.js +++ b/packages/redux-devtools/src/createDevTools.tsx @@ -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, + MonitorState, + MonitorAction extends Action +> { + store?: EnhancedStore; +} + +type Monitor< + S, + A extends Action, + MonitorProps, + MonitorState, + MonitorAction extends Action +> = React.ReactElement< + MonitorProps, + React.ComponentType> & { + update( + monitorProps: MonitorProps, + state: MonitorState | undefined, + action: MonitorAction + ): MonitorState; + } +>; + +export default function createDevTools< + S, + A extends Action, + MonitorProps, + MonitorState, + MonitorAction extends Action +>(children: Monitor) { const monitorElement = Children.only(children); const monitorProps = monitorElement.props; const Monitor = monitorElement.type; - const ConnectedMonitor = connect((state) => state)(Monitor); + const ConnectedMonitor = connect( + (state: LiftedState) => state + )(Monitor as React.ComponentType); - return class DevTools extends Component { + return class DevTools extends Component< + Props + > { static contextTypes = { store: PropTypes.object, }; @@ -36,13 +76,18 @@ export default function createDevTools(children) { store: PropTypes.object, }; - static instrument = (options) => + liftedStore?: LiftedStore; + + static instrument = (options: Options) => instrument( (state, action) => Monitor.update(monitorProps, state, action), options ); - constructor(props, context) { + constructor( + props: Props, + context: { store?: EnhancedStore } + ) { 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) + .liftedStore + ) { logError('NoLiftedStore'); return null; } return ( - + ).liftedStore + } + > ); diff --git a/packages/redux-devtools/src/index.js b/packages/redux-devtools/src/index.ts similarity index 100% rename from packages/redux-devtools/src/index.js rename to packages/redux-devtools/src/index.ts diff --git a/packages/redux-devtools/src/persistState.js b/packages/redux-devtools/src/persistState.ts similarity index 66% rename from packages/redux-devtools/src/persistState.js rename to packages/redux-devtools/src/persistState.ts index ec351550..c619754f 100644 --- a/packages/redux-devtools/src/persistState.js +++ b/packages/redux-devtools/src/persistState.ts @@ -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, + 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 + ): LiftedState { return { ...state, actionsById: mapValues(state.actionsById, (liftedAction) => ({ @@ -25,7 +33,10 @@ export default function persistState( }; } - return (next) => (reducer, initialState, enhancer) => { + return (next) => >( + reducer: Reducer, + initialState?: PreloadedState + ) => { 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 | undefined + ); return { ...store, - dispatch(action) { + dispatch(action: T) { store.dispatch(action); try { diff --git a/packages/redux-devtools/test/globalLocalStorage.d.ts b/packages/redux-devtools/test/globalLocalStorage.d.ts new file mode 100644 index 00000000..a25de90e --- /dev/null +++ b/packages/redux-devtools/test/globalLocalStorage.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface Global { + localStorage: Storage; + } +} diff --git a/packages/redux-devtools/test/persistState.spec.js b/packages/redux-devtools/test/persistState.spec.ts similarity index 85% rename from packages/redux-devtools/test/persistState.spec.js rename to packages/redux-devtools/test/persistState.spec.ts index b925f8c1..6b6e3cb7 100644 --- a/packages/redux-devtools/test/persistState.spec.js +++ b/packages/redux-devtools/test/persistState.spec.ts @@ -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, diff --git a/packages/redux-devtools/test/tsconfig.json b/packages/redux-devtools/test/tsconfig.json new file mode 100644 index 00000000..ca19def4 --- /dev/null +++ b/packages/redux-devtools/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.react.base.json", + "include": ["../src", "."] +} diff --git a/packages/redux-devtools/tsconfig.json b/packages/redux-devtools/tsconfig.json new file mode 100644 index 00000000..7b7d1492 --- /dev/null +++ b/packages/redux-devtools/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.react.base.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["src"] +} diff --git a/yarn.lock b/yarn.lock index 2c13c519..accff456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==