From 8eb67453aa6f44bf6c78a220f9827ae304988755 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Sun, 17 May 2020 07:35:13 -0400 Subject: [PATCH] Convert redux-devtools package to TypeScript --- packages/redux-devtools/.babelrc | 10 ++- packages/redux-devtools/.eslintignore | 2 + packages/redux-devtools/.eslintrc.js | 21 +++++ packages/redux-devtools/.prettierignore | 2 + packages/redux-devtools/index.d.ts | 18 ---- packages/redux-devtools/package.json | 31 +++++-- .../{createDevTools.js => createDevTools.tsx} | 87 ++++++++++++++++--- .../redux-devtools/src/{index.js => index.ts} | 3 +- .../src/{persistState.js => persistState.ts} | 26 ++++-- .../test/globalLocalStorage.d.ts | 5 ++ ...sistState.spec.js => persistState.spec.ts} | 86 +++++++----------- packages/redux-devtools/test/tsconfig.json | 4 + packages/redux-devtools/tsconfig.json | 7 ++ 13 files changed, 196 insertions(+), 106 deletions(-) create mode 100644 packages/redux-devtools/.eslintignore create mode 100644 packages/redux-devtools/.eslintrc.js create mode 100644 packages/redux-devtools/.prettierignore delete mode 100644 packages/redux-devtools/index.d.ts rename packages/redux-devtools/src/{createDevTools.js => createDevTools.tsx} (53%) rename packages/redux-devtools/src/{index.js => index.ts} (87%) rename packages/redux-devtools/src/{persistState.js => persistState.ts} (68%) create mode 100644 packages/redux-devtools/test/globalLocalStorage.d.ts rename packages/redux-devtools/test/{persistState.spec.js => persistState.spec.ts} (69%) create mode 100644 packages/redux-devtools/test/tsconfig.json create mode 100644 packages/redux-devtools/tsconfig.json diff --git a/packages/redux-devtools/.babelrc b/packages/redux-devtools/.babelrc index e60d3036..e073a7e7 100644 --- a/packages/redux-devtools/.babelrc +++ b/packages/redux-devtools/.babelrc @@ -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" + ] } diff --git a/packages/redux-devtools/.eslintignore b/packages/redux-devtools/.eslintignore new file mode 100644 index 00000000..d6126e3d --- /dev/null +++ b/packages/redux-devtools/.eslintignore @@ -0,0 +1,2 @@ +lib +examples diff --git a/packages/redux-devtools/.eslintrc.js b/packages/redux-devtools/.eslintrc.js new file mode 100644 index 00000000..72e1f8dd --- /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/.prettierignore b/packages/redux-devtools/.prettierignore new file mode 100644 index 00000000..d6126e3d --- /dev/null +++ b/packages/redux-devtools/.prettierignore @@ -0,0 +1,2 @@ +lib +examples diff --git a/packages/redux-devtools/index.d.ts b/packages/redux-devtools/index.d.ts deleted file mode 100644 index 26611ccb..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/package.json b/packages/redux-devtools/package.json index 0305cb52..9f01435a 100644 --- a/packages/redux-devtools/package.json +++ b/packages/redux-devtools/package.json @@ -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" } } diff --git a/packages/redux-devtools/src/createDevTools.js b/packages/redux-devtools/src/createDevTools.tsx similarity index 53% rename from packages/redux-devtools/src/createDevTools.js rename to packages/redux-devtools/src/createDevTools.tsx index 1525429d..8bd8be96 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) { - const monitorElement = Children.only(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,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 ( - + ).liftedStore + } + > ); diff --git a/packages/redux-devtools/src/index.js b/packages/redux-devtools/src/index.ts similarity index 87% rename from packages/redux-devtools/src/index.js rename to packages/redux-devtools/src/index.ts index dd75d9d8..7613a79e 100644 --- a/packages/redux-devtools/src/index.js +++ b/packages/redux-devtools/src/index.ts @@ -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'; diff --git a/packages/redux-devtools/src/persistState.js b/packages/redux-devtools/src/persistState.ts similarity index 68% rename from packages/redux-devtools/src/persistState.js rename to packages/redux-devtools/src/persistState.ts index cf4d0ee0..fc801e3d 100644 --- a/packages/redux-devtools/src/persistState.js +++ b/packages/redux-devtools/src/persistState.ts @@ -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>( + 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 +27,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 +49,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 69% rename from packages/redux-devtools/test/persistState.spec.js rename to packages/redux-devtools/test/persistState.spec.ts index ea816ecc..35dfe9cd 100644 --- a/packages/redux-devtools/test/persistState.spec.js +++ b/packages/redux-devtools/test/persistState.spec.ts @@ -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' }); 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"] +}