From 3b9d9443fb6dbf9847be010908f1a6cdd36535a5 Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Fri, 15 May 2020 00:10:54 -0400 Subject: [PATCH] Convert redux-devtools-instrument to TypeScript --- packages/redux-devtools-instrument/.babelrc | 8 +- .../redux-devtools-instrument/.eslintignore | 1 + .../redux-devtools-instrument/.eslintrc.js | 21 + .../redux-devtools-instrument/.prettierignore | 1 + .../redux-devtools-instrument/package.json | 25 +- .../src/{instrument.js => instrument.ts} | 419 +++++++++++---- ...{instrument.spec.js => instrument.spec.ts} | 490 ++++++++++-------- .../test/tsconfig.json | 4 + .../redux-devtools-instrument/tsconfig.json | 7 + yarn.lock | 20 +- 10 files changed, 689 insertions(+), 307 deletions(-) create mode 100644 packages/redux-devtools-instrument/.eslintignore create mode 100644 packages/redux-devtools-instrument/.eslintrc.js create mode 100644 packages/redux-devtools-instrument/.prettierignore rename packages/redux-devtools-instrument/src/{instrument.js => instrument.ts} (65%) rename packages/redux-devtools-instrument/test/{instrument.spec.js => instrument.spec.ts} (72%) create mode 100644 packages/redux-devtools-instrument/test/tsconfig.json create mode 100644 packages/redux-devtools-instrument/tsconfig.json diff --git a/packages/redux-devtools-instrument/.babelrc b/packages/redux-devtools-instrument/.babelrc index 1320b9a3..03607f87 100644 --- a/packages/redux-devtools-instrument/.babelrc +++ b/packages/redux-devtools-instrument/.babelrc @@ -1,3 +1,9 @@ { - "presets": ["@babel/preset-env"] + "presets": [ + "@babel/env", + "@babel/typescript" + ], + "plugins": [ + "@babel/proposal-class-properties" + ] } diff --git a/packages/redux-devtools-instrument/.eslintignore b/packages/redux-devtools-instrument/.eslintignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/redux-devtools-instrument/.eslintignore @@ -0,0 +1 @@ +lib diff --git a/packages/redux-devtools-instrument/.eslintrc.js b/packages/redux-devtools-instrument/.eslintrc.js new file mode 100644 index 00000000..b80a54c0 --- /dev/null +++ b/packages/redux-devtools-instrument/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + extends: '../../.eslintrc', + overrides: [ + { + files: ['*.ts'], + extends: '../../eslintrc.ts.base.json', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'] + } + }, + { + files: ['test/*.ts'], + extends: '../../eslintrc.ts.jest.base.json', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./test/tsconfig.json'] + } + } + ] +}; diff --git a/packages/redux-devtools-instrument/.prettierignore b/packages/redux-devtools-instrument/.prettierignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/redux-devtools-instrument/.prettierignore @@ -0,0 +1 @@ +lib diff --git a/packages/redux-devtools-instrument/package.json b/packages/redux-devtools-instrument/package.json index d56380f5..6a594c04 100644 --- a/packages/redux-devtools-instrument/package.json +++ b/packages/redux-devtools-instrument/package.json @@ -3,10 +3,17 @@ "version": "1.9.6", "description": "Redux DevTools instrumentation", "main": "lib/instrument.js", + "types": "lib/instrument.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 --extensions \".ts\" --source-maps inline", + "lint": "eslint . --ext .js,.ts", + "lint:fix": "eslint . --ext .js,.ts --fix", + "test": "tsc --project test/tsconfig.json --noEmit && jest", "prepare": "npm run build", "prepublishOnly": "npm run test && npm run clean && npm run build" }, @@ -35,16 +42,26 @@ "devDependencies": { "@babel/cli": "^7.2.3", "@babel/core": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.3.0", "@babel/preset-env": "^7.3.1", + "@babel/preset-typescript": "^7.9.0", + "@types/jest": "^25.2.1", + "@types/lodash": "^4.2.0", + "@typescript-eslint/eslint-plugin": "^2.31.0", + "@typescript-eslint/parser": "^2.31.0", "babel-loader": "^8.0.5", "expect": "^1.6.0", "jest": "^24.1.0", - "redux": "^4.0.0", + "redux": "^4.0.5", "rimraf": "^2.3.4", - "rxjs": "^5.0.0-beta.6" + "rxjs": "^5.0.0-beta.6", + "typescript": "^3.8.3" }, "dependencies": { "lodash": "^4.2.0", "symbol-observable": "^1.0.2" + }, + "peerDependencies": { + "redux": "^4.0.5" } } diff --git a/packages/redux-devtools-instrument/src/instrument.js b/packages/redux-devtools-instrument/src/instrument.ts similarity index 65% rename from packages/redux-devtools-instrument/src/instrument.js rename to packages/redux-devtools-instrument/src/instrument.ts index ff61c0f8..6ba337a2 100644 --- a/packages/redux-devtools-instrument/src/instrument.js +++ b/packages/redux-devtools-instrument/src/instrument.ts @@ -2,6 +2,15 @@ import difference from 'lodash/difference'; import union from 'lodash/union'; import isPlainObject from 'lodash/isPlainObject'; import $$observable from 'symbol-observable'; +import { + Action, + Observable, + PreloadedState, + Reducer, + Store, + StoreEnhancer, + StoreEnhancerStoreCreator +} from 'redux'; export const ActionTypes = { PERFORM_ACTION: 'PERFORM_ACTION', @@ -17,13 +26,15 @@ export const ActionTypes = { IMPORT_STATE: 'IMPORT_STATE', LOCK_CHANGES: 'LOCK_CHANGES', PAUSE_RECORDING: 'PAUSE_RECORDING' -}; +} as const; const isChrome = typeof window === 'object' && - (typeof window.chrome !== 'undefined' || + (typeof (window as typeof window & { chrome: unknown }).chrome !== + 'undefined' || (typeof window.process !== 'undefined' && - window.process.type === 'renderer')); + (window.process as typeof window.process & { type: unknown }).type === + 'renderer')); const isChromeOrNode = isChrome || @@ -31,11 +42,102 @@ const isChromeOrNode = process.release && process.release.name === 'node'); +export interface PerformAction> { + type: typeof ActionTypes.PERFORM_ACTION; + action: A; + timestamp: number; + stack: string | undefined; +} + +interface ResetAction { + type: typeof ActionTypes.RESET; + timestamp: number; +} + +interface RollbackAction { + type: typeof ActionTypes.ROLLBACK; + timestamp: number; +} + +interface CommitAction { + type: typeof ActionTypes.COMMIT; + timestamp: number; +} + +interface SweepAction { + type: typeof ActionTypes.SWEEP; +} + +interface ToggleAction { + type: typeof ActionTypes.TOGGLE_ACTION; + id: number; +} + +interface SetActionsActiveAction { + type: typeof ActionTypes.SET_ACTIONS_ACTIVE; + start: number; + end: number; + active: boolean; +} + +interface ReorderAction { + type: typeof ActionTypes.REORDER_ACTION; + actionId: number; + beforeActionId: number; +} + +interface JumpToStateAction { + type: typeof ActionTypes.JUMP_TO_STATE; + index: number; +} + +interface JumpToActionAction { + type: typeof ActionTypes.JUMP_TO_ACTION; + actionId: number; +} + +interface ImportStateAction> { + type: typeof ActionTypes.IMPORT_STATE; + nextLiftedState: LiftedState | readonly A[]; + preloadedState?: S; + noRecompute: boolean | undefined; +} + +interface LockChangesAction { + type: typeof ActionTypes.LOCK_CHANGES; + status: boolean; +} + +interface PauseRecordingAction { + type: typeof ActionTypes.PAUSE_RECORDING; + status: boolean; +} + +export type LiftedAction> = + | PerformAction + | ResetAction + | RollbackAction + | CommitAction + | SweepAction + | ToggleAction + | SetActionsActiveAction + | ReorderAction + | JumpToStateAction + | JumpToActionAction + | ImportStateAction + | LockChangesAction + | PauseRecordingAction; + /** * Action creators to change the History state. */ export const ActionCreators = { - performAction(action, trace, traceLimit, toExcludeFromTrace) { + performAction>( + action: A, + trace?: ((action: A) => string | undefined) | boolean, + traceLimit?: number, + toExcludeFromTrace?: Function + ): PerformAction { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + @@ -60,7 +162,7 @@ export const ActionCreators = { let prevStackTraceLimit; if (Error.captureStackTrace && isChromeOrNode) { // avoid error-polyfill - if (Error.stackTraceLimit < traceLimit) { + if (traceLimit && Error.stackTraceLimit < traceLimit) { prevStackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = traceLimit; } @@ -73,16 +175,20 @@ export const ActionCreators = { if ( extraFrames || typeof Error.stackTraceLimit !== 'number' || - Error.stackTraceLimit > traceLimit + (traceLimit && Error.stackTraceLimit > traceLimit) ) { - const frames = stack.split('\n'); - if (frames.length > traceLimit) { - stack = frames - .slice( - 0, - traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0) - ) - .join('\n'); + if (stack != null) { + const frames = stack.split('\n'); + if (traceLimit && frames.length > traceLimit) { + stack = frames + .slice( + 0, + traceLimit + + extraFrames + + (frames[0].startsWith('Error') ? 1 : 0) + ) + .join('\n'); + } } } } @@ -96,51 +202,58 @@ export const ActionCreators = { }; }, - reset() { + reset(): ResetAction { return { type: ActionTypes.RESET, timestamp: Date.now() }; }, - rollback() { + rollback(): RollbackAction { return { type: ActionTypes.ROLLBACK, timestamp: Date.now() }; }, - commit() { + commit(): CommitAction { return { type: ActionTypes.COMMIT, timestamp: Date.now() }; }, - sweep() { + sweep(): SweepAction { return { type: ActionTypes.SWEEP }; }, - toggleAction(id) { + toggleAction(id: number): ToggleAction { return { type: ActionTypes.TOGGLE_ACTION, id }; }, - setActionsActive(start, end, active = true) { + setActionsActive( + start: number, + end: number, + active = true + ): SetActionsActiveAction { return { type: ActionTypes.SET_ACTIONS_ACTIVE, start, end, active }; }, - reorderAction(actionId, beforeActionId) { + reorderAction(actionId: number, beforeActionId: number): ReorderAction { return { type: ActionTypes.REORDER_ACTION, actionId, beforeActionId }; }, - jumpToState(index) { + jumpToState(index: number): JumpToStateAction { return { type: ActionTypes.JUMP_TO_STATE, index }; }, - jumpToAction(actionId) { + jumpToAction(actionId: number): JumpToActionAction { return { type: ActionTypes.JUMP_TO_ACTION, actionId }; }, - importState(nextLiftedState, noRecompute) { + importState>( + nextLiftedState: LiftedState | readonly A[], + noRecompute?: boolean + ): ImportStateAction { return { type: ActionTypes.IMPORT_STATE, nextLiftedState, noRecompute }; }, - lockChanges(status) { + lockChanges(status: boolean): LockChangesAction { return { type: ActionTypes.LOCK_CHANGES, status }; }, - pauseRecording(status) { + pauseRecording(status: boolean): PauseRecordingAction { return { type: ActionTypes.PAUSE_RECORDING, status }; } }; @@ -150,7 +263,11 @@ export const INIT_ACTION = { type: '@@INIT' }; /** * Computes the next entry with exceptions catching. */ -function computeWithTryCatch(reducer, action, state) { +function computeWithTryCatch>( + reducer: Reducer, + action: A, + state: S +) { let nextState = state; let nextError; try { @@ -176,7 +293,12 @@ function computeWithTryCatch(reducer, action, state) { /** * Computes the next entry in the log by applying an action. */ -function computeNextEntry(reducer, action, state, shouldCatchErrors) { +function computeNextEntry>( + reducer: Reducer, + action: A, + state: S, + shouldCatchErrors: boolean | undefined +) { if (!shouldCatchErrors) { return { state: reducer(state, action) }; } @@ -186,15 +308,15 @@ function computeNextEntry(reducer, action, state, shouldCatchErrors) { /** * Runs the reducer on invalidated actions to get a fresh computation log. */ -function recomputeStates( - computedStates, - minInvalidatedStateIndex, - reducer, - committedState, - actionsById, - stagedActionIds, - skippedActionIds, - shouldCatchErrors +function recomputeStates>( + computedStates: { state: S; error?: string }[], + minInvalidatedStateIndex: number, + reducer: Reducer, + committedState: S, + actionsById: { [actionId: number]: PerformAction }, + stagedActionIds: number[], + skippedActionIds: number[], + shouldCatchErrors: boolean | undefined ) { // Optimization: exit early and return the same reference // if we know nothing could have changed. @@ -215,7 +337,7 @@ function recomputeStates( const previousEntry = nextComputedStates[i - 1]; const previousState = previousEntry ? previousEntry.state : committedState; - const shouldSkip = skippedActionIds.indexOf(actionId) > -1; + const shouldSkip = skippedActionIds.includes(actionId); let entry; if (shouldSkip) { entry = previousEntry; @@ -243,8 +365,13 @@ function recomputeStates( /** * Lifts an app's action into an action on the lifted store. */ -export function liftAction(action, trace, traceLimit, toExcludeFromTrace) { - return ActionCreators.performAction( +export function liftAction>( + action: A, + trace?: ((action: A) => string | undefined) | boolean, + traceLimit?: number, + toExcludeFromTrace?: Function +): PerformAction { + return ActionCreators.performAction( action, trace, traceLimit, @@ -252,22 +379,41 @@ export function liftAction(action, trace, traceLimit, toExcludeFromTrace) { ); } +export interface LiftedState> { + monitorState: unknown; + nextActionId: number; + actionsById: { [actionId: number]: PerformAction }; + stagedActionIds: number[]; + skippedActionIds: number[]; + committedState: S; + currentStateIndex: number; + computedStates: { state: S; error?: string }[]; + isLocked: boolean; + isPaused: boolean; +} + +function isArray>( + nextLiftedState: LiftedState | readonly A[] +): nextLiftedState is readonly A[] { + return Array.isArray(nextLiftedState); +} + /** * Creates a history state reducer from an app's reducer. */ -export function liftReducerWith( - reducer, - initialCommittedState, - monitorReducer, - options -) { - const initialLiftedState = { - monitorState: monitorReducer(undefined, {}), +export function liftReducerWith>( + reducer: Reducer, + initialCommittedState: PreloadedState | undefined, + monitorReducer: Reducer>, + options: Options +): Reducer, LiftedAction> { + const initialLiftedState: LiftedState = { + monitorState: monitorReducer(undefined, {} as Action), nextActionId: 1, - actionsById: { 0: liftAction(INIT_ACTION) }, + actionsById: { 0: liftAction(INIT_ACTION as A) }, stagedActionIds: [0], skippedActionIds: [], - committedState: initialCommittedState, + committedState: initialCommittedState as S, currentStateIndex: 0, computedStates: [], isLocked: options.shouldStartLocked === true, @@ -277,7 +423,10 @@ export function liftReducerWith( /** * Manages how the history actions modify the history state. */ - return (liftedState, liftedAction) => { + return ( + liftedState: LiftedState | undefined, + liftedAction: LiftedAction + ): LiftedState => { let { monitorState, actionsById, @@ -296,7 +445,7 @@ export function liftReducerWith( actionsById = { ...actionsById }; } - function commitExcessActions(n) { + function commitExcessActions(n: number) { // Auto-commits n-number of excess actions. let excess = n; let idsToDelete = stagedActionIds.slice(1, excess + 1); @@ -313,7 +462,7 @@ export function liftReducerWith( } skippedActionIds = skippedActionIds.filter( - id => idsToDelete.indexOf(id) === -1 + id => !idsToDelete.includes(id) ); stagedActionIds = [0, ...stagedActionIds.slice(excess + 1)]; committedState = computedStates[excess].state; @@ -322,7 +471,7 @@ export function liftReducerWith( currentStateIndex > excess ? currentStateIndex - excess : 0; } - function computePausedAction(shouldInit) { + function computePausedAction(shouldInit?: boolean): LiftedState { let computedState; if (shouldInit) { computedState = computedStates[currentStateIndex]; @@ -330,7 +479,7 @@ export function liftReducerWith( } else { computedState = computeNextEntry( reducer, - liftedAction.action, + (liftedAction as PerformAction).action, computedStates[currentStateIndex].state, false ); @@ -338,7 +487,7 @@ export function liftReducerWith( if (!options.pauseActionType || nextActionId === 1) { return { monitorState, - actionsById: { 0: liftAction(INIT_ACTION) }, + actionsById: { 0: liftAction(INIT_ACTION as A) }, nextActionId: 1, stagedActionIds: [0], skippedActionIds: [], @@ -360,7 +509,9 @@ export function liftReducerWith( monitorState, actionsById: { ...actionsById, - [nextActionId - 1]: liftAction({ type: options.pauseActionType }) + [nextActionId - 1]: liftAction({ + type: options.pauseActionType + } as A) }, nextActionId, stagedActionIds, @@ -376,7 +527,7 @@ export function liftReducerWith( }; } - // By default, agressively recompute every state whatever happens. + // By default, aggressively recompute every state whatever happens. // This has O(n) performance, so we'll override this to a sensible // value whenever we feel like we don't have to recompute the states. let minInvalidatedStateIndex = 0; @@ -388,13 +539,13 @@ export function liftReducerWith( if (/^@@redux\/(INIT|REPLACE)/.test(liftedAction.type)) { if (options.shouldHotReload === false) { - actionsById = { 0: liftAction(INIT_ACTION) }; + actionsById = { 0: liftAction(INIT_ACTION as A) }; nextActionId = 1; stagedActionIds = [0]; skippedActionIds = []; committedState = computedStates.length === 0 - ? initialCommittedState + ? (initialCommittedState as S) : computedStates[currentStateIndex].state; currentStateIndex = 0; computedStates = []; @@ -405,7 +556,7 @@ export function liftReducerWith( if (maxAge && stagedActionIds.length > maxAge) { // States must be recomputed before committing excess. - computedStates = recomputeStates( + computedStates = recomputeStates( computedStates, minInvalidatedStateIndex, reducer, @@ -446,11 +597,11 @@ export function liftReducerWith( } case ActionTypes.RESET: { // Get back to the state the store was created with. - actionsById = { 0: liftAction(INIT_ACTION) }; + actionsById = { 0: liftAction(INIT_ACTION as A) }; nextActionId = 1; stagedActionIds = [0]; skippedActionIds = []; - committedState = initialCommittedState; + committedState = initialCommittedState as S; currentStateIndex = 0; computedStates = []; break; @@ -458,7 +609,7 @@ export function liftReducerWith( case ActionTypes.COMMIT: { // Consider the last committed state the new starting point. // Squash any staged actions into a single committed state. - actionsById = { 0: liftAction(INIT_ACTION) }; + actionsById = { 0: liftAction(INIT_ACTION as A) }; nextActionId = 1; stagedActionIds = [0]; skippedActionIds = []; @@ -470,7 +621,7 @@ export function liftReducerWith( case ActionTypes.ROLLBACK: { // Forget about any staged actions. // Start again from the last committed state. - actionsById = { 0: liftAction(INIT_ACTION) }; + actionsById = { 0: liftAction(INIT_ACTION as A) }; nextActionId = 1; stagedActionIds = [0]; skippedActionIds = []; @@ -571,19 +722,19 @@ export function liftReducerWith( break; } case ActionTypes.IMPORT_STATE: { - if (Array.isArray(liftedAction.nextLiftedState)) { + if (isArray(liftedAction.nextLiftedState)) { // recompute array of actions - actionsById = { 0: liftAction(INIT_ACTION) }; + actionsById = { 0: liftAction(INIT_ACTION as A) }; nextActionId = 1; stagedActionIds = [0]; skippedActionIds = []; currentStateIndex = liftedAction.nextLiftedState.length; computedStates = []; - committedState = liftedAction.preloadedState; + committedState = liftedAction.preloadedState as S; minInvalidatedStateIndex = 0; // iterate through actions liftedAction.nextLiftedState.forEach(action => { - actionsById[nextActionId] = liftAction( + actionsById[nextActionId] = liftAction( action, options.trace || options.shouldIncludeCallstack ); @@ -621,7 +772,7 @@ export function liftReducerWith( return computePausedAction(true); } // Commit when unpausing - actionsById = { 0: liftAction(INIT_ACTION) }; + actionsById = { 0: liftAction(INIT_ACTION as A) }; nextActionId = 1; stagedActionIds = [0]; skippedActionIds = []; @@ -668,34 +819,64 @@ export function liftReducerWith( /** * Provides an app's view into the state of the lifted store. */ -export function unliftState(liftedState) { +export function unliftState, NextStateExt>( + liftedState: LiftedState & NextStateExt +) { const { computedStates, currentStateIndex } = liftedState; const { state } = computedStates[currentStateIndex]; return state; } +export type LiftedStore> = Store< + LiftedState, + LiftedAction +>; + +export type InstrumentExt> = { + liftedStore: LiftedStore; +}; + +export type EnhancedStore> = Store & + InstrumentExt; + /** * Provides an app's view into the lifted store. */ -export function unliftStore(liftedStore, liftReducer, options) { - let lastDefinedState; +export function unliftStore< + S, + A extends Action, + NextExt, + NextStateExt +>( + liftedStore: Store & NextStateExt, LiftedAction> & + NextExt, + liftReducer: ( + r: Reducer + ) => Reducer, LiftedAction>, + options: Options +): Store & + NextExt & { + liftedStore: Store & NextStateExt, LiftedAction>; + } { + let lastDefinedState: S & NextStateExt; const trace = options.trace || options.shouldIncludeCallstack; const traceLimit = options.traceLimit || 10; - function getState() { - const state = unliftState(liftedStore.getState()); + function getState(): S & NextStateExt { + const state = unliftState(liftedStore.getState()) as S & + NextStateExt; if (state !== undefined) { lastDefinedState = state; } return lastDefinedState; } - function dispatch(action) { - liftedStore.dispatch(liftAction(action, trace, traceLimit, dispatch)); + function dispatch(action: T): T { + liftedStore.dispatch(liftAction(action, trace, traceLimit, dispatch)); return action; } - return { + return ({ ...liftedStore, liftedStore, @@ -704,13 +885,20 @@ export function unliftStore(liftedStore, liftReducer, options) { getState, - replaceReducer(nextReducer) { - liftedStore.replaceReducer(liftReducer(nextReducer)); + replaceReducer(nextReducer: Reducer) { + liftedStore.replaceReducer( + (liftReducer( + (nextReducer as unknown) as Reducer + ) as unknown) as Reducer< + LiftedState & NextStateExt, + LiftedAction + > + ); }, - [$$observable]() { + [$$observable](): Observable { return { - ...liftedStore[$$observable](), + ...(liftedStore as any)[$$observable](), subscribe(observer) { if (typeof observer !== 'object') { throw new TypeError('Expected the observer to be an object.'); @@ -725,16 +913,43 @@ export function unliftStore(liftedStore, liftReducer, options) { observeState(); const unsubscribe = liftedStore.subscribe(observeState); return { unsubscribe }; + }, + + [$$observable]() { + return this; } }; } - }; + } as unknown) as Store & + NextExt & { + liftedStore: Store & NextStateExt, LiftedAction>; + }; +} + +export interface Options> { + maxAge?: + | number + | (( + currentLiftedAction: LiftedAction, + previousLiftedState: LiftedState | undefined + ) => number); + shouldCatchErrors?: boolean; + shouldRecordChanges?: boolean; + pauseActionType?: unknown; + shouldStartLocked?: boolean; + shouldHotReload?: boolean; + trace?: boolean | ((action: A) => string | undefined); + traceLimit?: number; + shouldIncludeCallstack?: boolean; } /** * Redux instrumentation store enhancer. */ -export default function instrument(monitorReducer = () => null, options = {}) { +export default function instrument>( + monitorReducer: Reducer> = () => null, + options: Options = {} +): StoreEnhancer> { if (typeof options.maxAge === 'number' && options.maxAge < 2) { throw new Error( 'DevTools.instrument({ maxAge }) option, if specified, ' + @@ -742,10 +957,15 @@ export default function instrument(monitorReducer = () => null, options = {}) { ); } - return createStore => (reducer, initialState, enhancer) => { - function liftReducer(r) { + return ( + createStore: StoreEnhancerStoreCreator + ) => >( + reducer: Reducer, + initialState?: PreloadedState + ) => { + function liftReducer(r: Reducer) { if (typeof r !== 'function') { - if (r && typeof r.default === 'function') { + if (r && typeof (r as { default: unknown }).default === 'function') { throw new Error( 'Expected the reducer to be a function. ' + 'Instead got an object with a "default" field. ' + @@ -755,17 +975,34 @@ export default function instrument(monitorReducer = () => null, options = {}) { } throw new Error('Expected the reducer to be a function.'); } - return liftReducerWith(r, initialState, monitorReducer, options); + return liftReducerWith( + r, + initialState, + monitorReducer, + (options as unknown) as Options + ); } - const liftedStore = createStore(liftReducer(reducer), enhancer); - if (liftedStore.liftedStore) { + const liftedStore = createStore(liftReducer(reducer)); + if ( + (liftedStore as Store< + LiftedState & NextStateExt, + LiftedAction + > & + NextExt & { + liftedStore: Store, LiftedAction>; + }).liftedStore + ) { throw new Error( 'DevTools instrumentation should not be applied more than once. ' + 'Check your store configuration.' ); } - return unliftStore(liftedStore, liftReducer, options); + return unliftStore( + liftedStore, + liftReducer, + (options as unknown) as Options + ); }; } diff --git a/packages/redux-devtools-instrument/test/instrument.spec.js b/packages/redux-devtools-instrument/test/instrument.spec.ts similarity index 72% rename from packages/redux-devtools-instrument/test/instrument.spec.js rename to packages/redux-devtools-instrument/test/instrument.spec.ts index 90773530..648a50f7 100644 --- a/packages/redux-devtools-instrument/test/instrument.spec.js +++ b/packages/redux-devtools-instrument/test/instrument.spec.ts @@ -1,11 +1,17 @@ -import { createStore, compose } from 'redux'; -import instrument, { ActionCreators } from '../src/instrument'; +import { createStore, compose, Store, Action, Reducer } from 'redux'; +import instrument, { + ActionCreators, + EnhancedStore, + LiftedState, + LiftedStore +} from '../src/instrument'; import { Observable } from 'rxjs'; import _ from 'lodash'; import 'rxjs/add/observable/from'; -function counter(state = 0, action) { +type CounterAction = { type: 'INCREMENT' } | { type: 'DECREMENT' }; +function counter(state = 0, action: CounterAction) { switch (action.type) { case 'INCREMENT': return state + 1; @@ -16,23 +22,35 @@ function counter(state = 0, action) { } } -function counterWithBug(state = 0, action) { +type CounterWithBugAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'SET_UNDEFINED' }; +function counterWithBug(state = 0, action: CounterWithBugAction) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore return mistake - 1; // eslint-disable-line no-undef case 'SET_UNDEFINED': - return undefined; + return (undefined as unknown) as number; default: return state; } } -function counterWithAnotherBug(state = 0, action) { +type CounterWithAnotherBugAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'SET_UNDEFINED' }; +function counterWithAnotherBug(state = 0, action: CounterWithAnotherBugAction) { switch (action.type) { case 'INCREMENT': - return mistake + 1; // eslint-disable-line no-undef + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + return ((mistake as unknown) as number) + 1; // eslint-disable-line no-undef case 'DECREMENT': return state - 1; case 'SET_UNDEFINED': @@ -42,7 +60,8 @@ function counterWithAnotherBug(state = 0, action) { } } -function doubleCounter(state = 0, action) { +type DoubleCounterAction = { type: 'INCREMENT' } | { type: 'DECREMENT' }; +function doubleCounter(state = 0, action: DoubleCounterAction) { switch (action.type) { case 'INCREMENT': return state + 2; @@ -53,7 +72,11 @@ function doubleCounter(state = 0, action) { } } -function counterWithMultiply(state = 0, action) { +type CounterWithMultiplyAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'MULTIPLY' }; +function counterWithMultiply(state = 0, action: CounterWithMultiplyAction) { switch (action.type) { case 'INCREMENT': return state + 1; @@ -67,8 +90,8 @@ function counterWithMultiply(state = 0, action) { } describe('instrument', () => { - let store; - let liftedStore; + let store: EnhancedStore; + let liftedStore: LiftedStore; beforeEach(() => { store = createStore(counter, instrument()); @@ -87,10 +110,12 @@ describe('instrument', () => { let lastValue; // let calls = 0; - Observable.from(store).subscribe(state => { - lastValue = state; - // calls++; - }); + Observable.from((store as unknown) as Observable).subscribe( + state => { + lastValue = state; + // calls++; + } + ); expect(lastValue).toBe(0); store.dispatch({ type: 'INCREMENT' }); @@ -231,89 +256,89 @@ describe('instrument', () => { }); it('should reorder actions', () => { - store = createStore(counterWithMultiply, instrument()); - store.dispatch({ type: 'INCREMENT' }); - store.dispatch({ type: 'DECREMENT' }); - store.dispatch({ type: 'INCREMENT' }); - store.dispatch({ type: 'MULTIPLY' }); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + const storeWithMultiply = createStore(counterWithMultiply, instrument()); + storeWithMultiply.dispatch({ type: 'INCREMENT' }); + storeWithMultiply.dispatch({ type: 'DECREMENT' }); + storeWithMultiply.dispatch({ type: 'INCREMENT' }); + storeWithMultiply.dispatch({ type: 'MULTIPLY' }); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 1, 2, 3, 4 ]); - expect(store.getState()).toBe(2); + expect(storeWithMultiply.getState()).toBe(2); - store.liftedStore.dispatch(ActionCreators.reorderAction(4, 1)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(4, 1)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 4, 1, 2, 3 ]); - expect(store.getState()).toBe(1); + expect(storeWithMultiply.getState()).toBe(1); - store.liftedStore.dispatch(ActionCreators.reorderAction(4, 1)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(4, 1)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 4, 1, 2, 3 ]); - expect(store.getState()).toBe(1); + expect(storeWithMultiply.getState()).toBe(1); - store.liftedStore.dispatch(ActionCreators.reorderAction(4, 2)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(4, 2)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 1, 4, 2, 3 ]); - expect(store.getState()).toBe(2); + expect(storeWithMultiply.getState()).toBe(2); - store.liftedStore.dispatch(ActionCreators.reorderAction(1, 10)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(1, 10)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 4, 2, 3, 1 ]); - expect(store.getState()).toBe(1); + expect(storeWithMultiply.getState()).toBe(1); - store.liftedStore.dispatch(ActionCreators.reorderAction(10, 1)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(10, 1)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 4, 2, 3, 1 ]); - expect(store.getState()).toBe(1); + expect(storeWithMultiply.getState()).toBe(1); - store.liftedStore.dispatch(ActionCreators.reorderAction(1, -2)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(1, -2)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 1, 4, 2, 3 ]); - expect(store.getState()).toBe(2); + expect(storeWithMultiply.getState()).toBe(2); - store.liftedStore.dispatch(ActionCreators.reorderAction(0, 1)); - expect(store.liftedStore.getState().stagedActionIds).toEqual([ + storeWithMultiply.liftedStore.dispatch(ActionCreators.reorderAction(0, 1)); + expect(storeWithMultiply.liftedStore.getState().stagedActionIds).toEqual([ 0, 1, 4, 2, 3 ]); - expect(store.getState()).toBe(2); + expect(storeWithMultiply.getState()).toBe(2); }); it('should replace the reducer', () => { @@ -342,13 +367,18 @@ describe('instrument', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(3); - store.replaceReducer(() => ({ test: true })); - expect(store.getState()).toEqual({ test: true }); + store.replaceReducer( + (() => ({ test: true } as unknown)) as Reducer + ); + const newStore = (store as unknown) as Store<{ test: boolean }>; + expect(newStore.getState()).toEqual({ test: true }); }); it('should catch and record errors', () => { - let spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - let storeWithBug = createStore( + const spy = jest.spyOn(console, 'error').mockImplementation(() => { + // noop + }); + const storeWithBug = createStore( counterWithBug, instrument(undefined, { shouldCatchErrors: true }) ); @@ -357,7 +387,7 @@ describe('instrument', () => { storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); - let { computedStates } = storeWithBug.liftedStore.getState(); + const { computedStates } = storeWithBug.liftedStore.getState(); expect(computedStates[2].error).toMatch(/ReferenceError/); expect(computedStates[3].error).toMatch( /Interrupted by an error up the chain/ @@ -367,22 +397,22 @@ describe('instrument', () => { spy.mockReset(); }); - it('should catch invalid action type', () => { + it('should catch invalid action type (undefined type)', () => { expect(() => { - store.dispatch({ type: undefined }); + store.dispatch(({ type: undefined } as unknown) as CounterAction); }).toThrow( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ); }); - it('should catch invalid action type', () => { - function ActionClass() { + it('should catch invalid action type (function)', () => { + function ActionClass(this: any) { this.type = 'test'; } expect(() => { - store.dispatch(new ActionClass()); + store.dispatch(new (ActionClass as any)() as CounterAction); }).toThrow( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' @@ -390,7 +420,7 @@ describe('instrument', () => { }); it('should return the last non-undefined state from getState', () => { - let storeWithBug = createStore(counterWithBug, instrument()); + const storeWithBug = createStore(counterWithBug, instrument()); storeWithBug.dispatch({ type: 'INCREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); expect(storeWithBug.getState()).toBe(2); @@ -401,7 +431,7 @@ describe('instrument', () => { it('should not recompute states on every action', () => { let reducerCalls = 0; - let monitoredStore = createStore(() => reducerCalls++, instrument()); + const monitoredStore = createStore(() => reducerCalls++, instrument()); expect(reducerCalls).toBe(1); monitoredStore.dispatch({ type: 'INCREMENT' }); monitoredStore.dispatch({ type: 'INCREMENT' }); @@ -411,8 +441,8 @@ describe('instrument', () => { it('should not recompute old states when toggling an action', () => { let reducerCalls = 0; - let monitoredStore = createStore(() => reducerCalls++, instrument()); - let monitoredLiftedStore = monitoredStore.liftedStore; + const monitoredStore = createStore(() => reducerCalls++, instrument()); + const monitoredLiftedStore = monitoredStore.liftedStore; expect(reducerCalls).toBe(1); // actionId 0 = @@INIT @@ -454,8 +484,8 @@ describe('instrument', () => { it('should not recompute states when jumping to state', () => { let reducerCalls = 0; - let monitoredStore = createStore(() => reducerCalls++, instrument()); - let monitoredLiftedStore = monitoredStore.liftedStore; + const monitoredStore = createStore(() => reducerCalls++, instrument()); + const monitoredLiftedStore = monitoredStore.liftedStore; expect(reducerCalls).toBe(1); monitoredStore.dispatch({ type: 'INCREMENT' }); @@ -463,7 +493,7 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); expect(reducerCalls).toBe(4); - let savedComputedStates = monitoredLiftedStore.getState().computedStates; + const savedComputedStates = monitoredLiftedStore.getState().computedStates; monitoredLiftedStore.dispatch(ActionCreators.jumpToState(0)); expect(reducerCalls).toBe(4); @@ -481,8 +511,8 @@ describe('instrument', () => { it('should not recompute states on monitor actions', () => { let reducerCalls = 0; - let monitoredStore = createStore(() => reducerCalls++, instrument()); - let monitoredLiftedStore = monitoredStore.liftedStore; + const monitoredStore = createStore(() => reducerCalls++, instrument()); + const monitoredLiftedStore = monitoredStore.liftedStore; expect(reducerCalls).toBe(1); monitoredStore.dispatch({ type: 'INCREMENT' }); @@ -490,12 +520,12 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); expect(reducerCalls).toBe(4); - let savedComputedStates = monitoredLiftedStore.getState().computedStates; + const savedComputedStates = monitoredLiftedStore.getState().computedStates; - monitoredLiftedStore.dispatch({ type: 'lol' }); + monitoredLiftedStore.dispatch({ type: 'lol' } as Action); expect(reducerCalls).toBe(4); - monitoredLiftedStore.dispatch({ type: 'wat' }); + monitoredLiftedStore.dispatch({ type: 'wat' } as Action); expect(reducerCalls).toBe(4); expect(monitoredLiftedStore.getState().computedStates).toBe( @@ -504,8 +534,8 @@ describe('instrument', () => { }); describe('maxAge option', () => { - let configuredStore; - let configuredLiftedStore; + let configuredStore: EnhancedStore; + let configuredLiftedStore: LiftedStore; beforeEach(() => { configuredStore = createStore( @@ -521,8 +551,8 @@ describe('instrument', () => { let liftedStoreState = configuredLiftedStore.getState(); expect(configuredStore.getState()).toBe(2); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(3); - expect(liftedStoreState.committedState).toBe(undefined); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(3); + expect(liftedStoreState.committedState).toBeUndefined(); expect(liftedStoreState.stagedActionIds).toContain(1); // Trigger auto-commit. @@ -530,7 +560,7 @@ describe('instrument', () => { liftedStoreState = configuredLiftedStore.getState(); expect(configuredStore.getState()).toBe(3); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(3); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(3); expect(liftedStoreState.stagedActionIds).not.toContain(1); expect(liftedStoreState.computedStates[0].state).toBe(1); expect(liftedStoreState.committedState).toBe(1); @@ -549,53 +579,66 @@ describe('instrument', () => { }); it('should not auto-commit errors', () => { - let spy = jest.spyOn(console, 'error'); + const spy = jest.spyOn(console, 'error'); - let storeWithBug = createStore( + const storeWithBug = createStore( counterWithBug, instrument(undefined, { maxAge: 3, shouldCatchErrors: true }) ); - let liftedStoreWithBug = storeWithBug.liftedStore; + const liftedStoreWithBug = storeWithBug.liftedStore; storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); - expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3); + expect(liftedStoreWithBug.getState().stagedActionIds).toHaveLength(3); storeWithBug.dispatch({ type: 'INCREMENT' }); - expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(4); + expect(liftedStoreWithBug.getState().stagedActionIds).toHaveLength(4); spy.mockReset(); }); it('should auto-commit actions after hot reload fixes error', () => { - let spy = jest.spyOn(console, 'error'); + const spy = jest.spyOn(console, 'error'); - let storeWithBug = createStore( + const storeWithBug = createStore( counterWithBug, instrument(undefined, { maxAge: 3, shouldCatchErrors: true }) ); - let liftedStoreWithBug = storeWithBug.liftedStore; + const liftedStoreWithBug = storeWithBug.liftedStore; storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); - expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(7); + expect(liftedStoreWithBug.getState().stagedActionIds).toHaveLength(7); // Auto-commit 2 actions by "fixing" reducer bug, but introducing another. - storeWithBug.replaceReducer(counterWithAnotherBug); - expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(5); + storeWithBug.replaceReducer( + counterWithAnotherBug as Reducer + ); + const liftedStoreWithAnotherBug = (liftedStoreWithBug as unknown) as LiftedStore< + number, + CounterWithAnotherBugAction + >; + expect(liftedStoreWithAnotherBug.getState().stagedActionIds).toHaveLength( + 5 + ); // Auto-commit 2 more actions by "fixing" other reducer bug. - storeWithBug.replaceReducer(counter); - expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3); + storeWithBug.replaceReducer( + counter as Reducer + ); + const liftedStore = liftedStoreWithBug as LiftedStore< + number, + CounterAction + >; + expect(liftedStore.getState().stagedActionIds).toHaveLength(3); spy.mockReset(); }); it('should update currentStateIndex when auto-committing', () => { let liftedStoreState; - let currentComputedState; configuredStore.dispatch({ type: 'INCREMENT' }); configuredStore.dispatch({ type: 'INCREMENT' }); @@ -605,28 +648,28 @@ describe('instrument', () => { // currentStateIndex should stay at 2 as actions are committed. configuredStore.dispatch({ type: 'INCREMENT' }); liftedStoreState = configuredLiftedStore.getState(); - currentComputedState = + const currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex]; expect(liftedStoreState.currentStateIndex).toBe(2); expect(currentComputedState.state).toBe(3); }); it('should continue to increment currentStateIndex while error blocks commit', () => { - let spy = jest.spyOn(console, 'error'); + const spy = jest.spyOn(console, 'error'); - let storeWithBug = createStore( + const storeWithBug = createStore( counterWithBug, instrument(undefined, { maxAge: 3, shouldCatchErrors: true }) ); - let liftedStoreWithBug = storeWithBug.liftedStore; + const liftedStoreWithBug = storeWithBug.liftedStore; storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); - let liftedStoreState = liftedStoreWithBug.getState(); - let currentComputedState = + const liftedStoreState = liftedStoreWithBug.getState(); + const currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex]; expect(liftedStoreState.currentStateIndex).toBe(4); expect(currentComputedState.state).toBe(0); @@ -636,13 +679,13 @@ describe('instrument', () => { }); it('should adjust currentStateIndex correctly when multiple actions are committed', () => { - let spy = jest.spyOn(console, 'error'); + const spy = jest.spyOn(console, 'error'); - let storeWithBug = createStore( + const storeWithBug = createStore( counterWithBug, instrument(undefined, { maxAge: 3, shouldCatchErrors: true }) ); - let liftedStoreWithBug = storeWithBug.liftedStore; + const liftedStoreWithBug = storeWithBug.liftedStore; storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); @@ -650,9 +693,15 @@ describe('instrument', () => { storeWithBug.dispatch({ type: 'DECREMENT' }); // Auto-commit 2 actions by "fixing" reducer bug. - storeWithBug.replaceReducer(counter); - let liftedStoreState = liftedStoreWithBug.getState(); - let currentComputedState = + storeWithBug.replaceReducer( + counter as Reducer + ); + const liftedStore = liftedStoreWithBug as LiftedStore< + number, + CounterAction + >; + const liftedStoreState = liftedStore.getState(); + const currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex]; expect(liftedStoreState.currentStateIndex).toBe(2); expect(currentComputedState.state).toBe(-4); @@ -661,13 +710,13 @@ describe('instrument', () => { }); it('should not allow currentStateIndex to drop below 0', () => { - let spy = jest.spyOn(console, 'error'); + const spy = jest.spyOn(console, 'error'); - let storeWithBug = createStore( + const storeWithBug = createStore( counterWithBug, instrument(undefined, { maxAge: 3, shouldCatchErrors: true }) ); - let liftedStoreWithBug = storeWithBug.liftedStore; + const liftedStoreWithBug = storeWithBug.liftedStore; storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); @@ -676,9 +725,15 @@ describe('instrument', () => { liftedStoreWithBug.dispatch(ActionCreators.jumpToState(1)); // Auto-commit 2 actions by "fixing" reducer bug. - storeWithBug.replaceReducer(counter); - let liftedStoreState = liftedStoreWithBug.getState(); - let currentComputedState = + storeWithBug.replaceReducer( + counter as Reducer + ); + const liftedStore = liftedStoreWithBug as LiftedStore< + number, + CounterAction + >; + const liftedStoreState = liftedStore.getState(); + const currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex]; expect(liftedStoreState.currentStateIndex).toBe(0); expect(currentComputedState.state).toBe(-2); @@ -694,15 +749,15 @@ describe('instrument', () => { instrument(undefined, { maxAge: getMaxAge }) ); - expect(getMaxAge.mock.calls.length).toEqual(1); + expect(getMaxAge.mock.calls).toHaveLength(1); store.dispatch({ type: 'INCREMENT' }); - expect(getMaxAge.mock.calls.length).toEqual(2); + expect(getMaxAge.mock.calls).toHaveLength(2); store.dispatch({ type: 'INCREMENT' }); - expect(getMaxAge.mock.calls.length).toEqual(3); + expect(getMaxAge.mock.calls).toHaveLength(3); let liftedStoreState = store.liftedStore.getState(); expect(getMaxAge.mock.calls[0][0].type).toContain('INIT'); - expect(getMaxAge.mock.calls[0][1]).toBe(undefined); + expect(getMaxAge.mock.calls[0][1]).toBeUndefined(); expect(getMaxAge.mock.calls[1][0].type).toBe('PERFORM_ACTION'); expect(getMaxAge.mock.calls[1][1].nextActionId).toBe(1); expect(getMaxAge.mock.calls[1][1].stagedActionIds).toEqual([0]); @@ -710,8 +765,8 @@ describe('instrument', () => { expect(getMaxAge.mock.calls[2][1].stagedActionIds).toEqual([0, 1]); expect(store.getState()).toBe(2); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(3); - expect(liftedStoreState.committedState).toBe(undefined); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(3); + expect(liftedStoreState.committedState).toBeUndefined(); expect(liftedStoreState.stagedActionIds).toContain(1); // Trigger auto-commit. @@ -719,7 +774,7 @@ describe('instrument', () => { liftedStoreState = store.liftedStore.getState(); expect(store.getState()).toBe(3); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(3); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(3); expect(liftedStoreState.stagedActionIds).not.toContain(1); expect(liftedStoreState.computedStates[0].state).toBe(1); expect(liftedStoreState.committedState).toBe(1); @@ -730,7 +785,7 @@ describe('instrument', () => { liftedStoreState = store.liftedStore.getState(); expect(store.getState()).toBe(4); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(4); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(4); expect(liftedStoreState.stagedActionIds).not.toContain(1); expect(liftedStoreState.computedStates[0].state).toBe(1); expect(liftedStoreState.committedState).toBe(1); @@ -741,7 +796,7 @@ describe('instrument', () => { liftedStoreState = store.liftedStore.getState(); expect(store.getState()).toBe(5); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(3); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(3); expect(liftedStoreState.stagedActionIds).not.toContain(1); expect(liftedStoreState.computedStates[0].state).toBe(3); expect(liftedStoreState.committedState).toBe(3); @@ -751,7 +806,7 @@ describe('instrument', () => { liftedStoreState = store.liftedStore.getState(); expect(store.getState()).toBe(6); - expect(Object.keys(liftedStoreState.actionsById).length).toBe(3); + expect(Object.keys(liftedStoreState.actionsById)).toHaveLength(3); expect(liftedStoreState.stagedActionIds).not.toContain(1); expect(liftedStoreState.computedStates[0].state).toBe(4); expect(liftedStoreState.committedState).toBe(4); @@ -776,8 +831,8 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); - expect(exportedState.actionsById[1].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); + expect(exportedState.actionsById[1].stack).toBeUndefined(); }); it('should include stack trace', () => { @@ -790,18 +845,18 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); expect(exportedState.actionsById[1].stack).toMatch(/^Error/); - expect(exportedState.actionsById[1].stack).not.toMatch(/instrument.js/); + expect(exportedState.actionsById[1].stack).not.toMatch(/instrument.ts/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn1\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn2\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn3\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn4\b/); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'instrument.spec.ts' ); - expect(exportedState.actionsById[1].stack.split('\n').length).toBe( + expect(exportedState.actionsById[1].stack!.split('\n')).toHaveLength( 10 + 1 ); // +1 is for `Error\n` } @@ -827,17 +882,16 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); - expect(exportedState.actionsById[1].stack).toMatch(/\bat dispatch\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn1\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn2\b/); - expect(exportedState.actionsById[1].stack).not.toMatch(/\bfn3\b/); + expect(exportedState.actionsById[1].stack).toMatch(/\bfn3\b/); expect(exportedState.actionsById[1].stack).not.toMatch(/\bfn4\b/); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'instrument.spec.ts' ); - expect(exportedState.actionsById[1].stack.split('\n').length).toBe( + expect(exportedState.actionsById[1].stack!.split('\n')).toHaveLength( 3 + 1 ); } @@ -865,17 +919,16 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); - expect(exportedState.actionsById[1].stack).toMatch(/\bat dispatch\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn1\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn2\b/); - expect(exportedState.actionsById[1].stack).not.toMatch(/\bfn3\b/); + expect(exportedState.actionsById[1].stack).toMatch(/\bfn3\b/); expect(exportedState.actionsById[1].stack).not.toMatch(/\bfn4\b/); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'instrument.spec.ts' ); - expect(exportedState.actionsById[1].stack.split('\n').length).toBe( + expect(exportedState.actionsById[1].stack!.split('\n')).toHaveLength( 3 + 1 ); } @@ -904,13 +957,15 @@ describe('instrument', () => { Error.stackTraceLimit = stackTraceLimit; exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); expect(exportedState.actionsById[1].stack).toMatch(/^Error/); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'instrument.spec.ts' + ); + expect(exportedState.actionsById[1].stack!.split('\n')).toHaveLength( + 5 + 1 ); - expect(exportedState.actionsById[1].stack.split('\n').length).toBe(5 + 1); }); it('should force default limit of 10 even when Error.stackTraceLimit is 3', () => { @@ -926,16 +981,16 @@ describe('instrument', () => { Error.stackTraceLimit = stackTraceLimit; exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); expect(exportedState.actionsById[1].stack).toMatch(/\bfn1\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn2\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn3\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn4\b/); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'instrument.spec.ts' ); - expect(exportedState.actionsById[1].stack.split('\n').length).toBe( + expect(exportedState.actionsById[1].stack!.split('\n')).toHaveLength( 10 + 1 ); } @@ -951,9 +1006,10 @@ describe('instrument', () => { fn4(); }); - it('should include 3 extra frames when Error.captureStackTrace not suported', () => { + it('should include 3 extra frames when Error.captureStackTrace not supported', () => { + // eslint-disable-next-line @typescript-eslint/unbound-method const captureStackTrace = Error.captureStackTrace; - Error.captureStackTrace = undefined; + Error.captureStackTrace = (undefined as unknown) as () => {}; monitoredStore = createStore( counter, instrument(undefined, { trace: true, traceLimit: 5 }) @@ -963,14 +1019,14 @@ describe('instrument', () => { Error.captureStackTrace = captureStackTrace; exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); expect(exportedState.actionsById[1].stack).toMatch(/^Error/); - expect(exportedState.actionsById[1].stack).toContain('instrument.js'); + expect(exportedState.actionsById[1].stack).toContain('instrument.ts'); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'instrument.spec.ts' ); - expect(exportedState.actionsById[1].stack.split('\n').length).toBe( + expect(exportedState.actionsById[1].stack!.split('\n')).toHaveLength( 5 + 3 + 1 ); }); @@ -985,45 +1041,49 @@ describe('instrument', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); + expect(exportedState.actionsById[0].stack).toBeUndefined(); expect(typeof exportedState.actionsById[1].stack).toBe('string'); - expect(exportedState.actionsById[1].stack).toContain('at performAction'); - expect(exportedState.actionsById[1].stack).toContain('instrument.js'); expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' + 'at Object.performAction' + ); + expect(exportedState.actionsById[1].stack).toContain('instrument.ts'); + expect(exportedState.actionsById[1].stack).toContain( + 'instrument.spec.ts' ); }); - it('should get stack trace inside setTimeout using a function', done => { - const stack = new Error().stack; - setTimeout(() => { - const traceFn = () => stack + new Error().stack; - monitoredStore = createStore( - counter, - instrument(undefined, { trace: traceFn }) - ); - monitoredLiftedStore = monitoredStore.liftedStore; - monitoredStore.dispatch({ type: 'INCREMENT' }); + it('should get stack trace inside setTimeout using a function', () => { + return new Promise(done => { + const stack = new Error().stack; + setTimeout(() => { + const traceFn = () => stack + new Error().stack!; + monitoredStore = createStore( + counter, + instrument(undefined, { trace: traceFn }) + ); + monitoredLiftedStore = monitoredStore.liftedStore; + monitoredStore.dispatch({ type: 'INCREMENT' }); - exportedState = monitoredLiftedStore.getState(); - expect(exportedState.actionsById[0].stack).toBe(undefined); - expect(typeof exportedState.actionsById[1].stack).toBe('string'); - expect(exportedState.actionsById[1].stack).toContain( - 'at performAction' - ); - expect(exportedState.actionsById[1].stack).toContain('instrument.js'); - expect(exportedState.actionsById[1].stack).toContain( - 'instrument.spec.js' - ); - done(); + exportedState = monitoredLiftedStore.getState(); + expect(exportedState.actionsById[0].stack).toBeUndefined(); + expect(typeof exportedState.actionsById[1].stack).toBe('string'); + expect(exportedState.actionsById[1].stack).toContain( + 'at Object.performAction' + ); + expect(exportedState.actionsById[1].stack).toContain('instrument.ts'); + expect(exportedState.actionsById[1].stack).toContain( + 'instrument.spec.ts' + ); + done(); + }); }); }); }); describe('Import State', () => { - let monitoredStore; - let monitoredLiftedStore; - let exportedState; + let monitoredStore: EnhancedStore; + let monitoredLiftedStore: LiftedStore; + let exportedState: LiftedState; beforeEach(() => { monitoredStore = createStore(counter, instrument()); @@ -1037,8 +1097,8 @@ describe('instrument', () => { }); it('should replay all the steps when a state is imported', () => { - let importMonitoredStore = createStore(counter, instrument()); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredStore = createStore(counter, instrument()); + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; importMonitoredLiftedStore.dispatch( ActionCreators.importState(exportedState) @@ -1047,8 +1107,8 @@ describe('instrument', () => { }); it('should replace the existing action log with the one imported', () => { - let importMonitoredStore = createStore(counter, instrument()); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredStore = createStore(counter, instrument()); + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; importMonitoredStore.dispatch({ type: 'DECREMENT' }); importMonitoredStore.dispatch({ type: 'DECREMENT' }); @@ -1060,17 +1120,17 @@ describe('instrument', () => { }); it('should allow for state to be imported without replaying actions', () => { - let importMonitoredStore = createStore(counter, instrument()); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredStore = createStore(counter, instrument()); + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; - let noComputedExportedState = Object.assign({}, exportedState); + const noComputedExportedState = Object.assign({}, exportedState); delete noComputedExportedState.computedStates; importMonitoredLiftedStore.dispatch( ActionCreators.importState(noComputedExportedState, true) ); - let expectedImportedState = Object.assign({}, noComputedExportedState, { + const expectedImportedState = Object.assign({}, noComputedExportedState, { computedStates: undefined }); expect(importMonitoredLiftedStore.getState()).toEqual( @@ -1079,31 +1139,33 @@ describe('instrument', () => { }); it('should include stack trace', () => { - let importMonitoredStore = createStore( + const importMonitoredStore = createStore( counter, instrument(undefined, { trace: true }) ); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; importMonitoredStore.dispatch({ type: 'DECREMENT' }); importMonitoredStore.dispatch({ type: 'DECREMENT' }); const oldState = importMonitoredLiftedStore.getState(); - expect(oldState.actionsById[0].stack).toBe(undefined); + expect(oldState.actionsById[0].stack).toBeUndefined(); expect(typeof oldState.actionsById[1].stack).toBe('string'); importMonitoredLiftedStore.dispatch(ActionCreators.importState(oldState)); expect(importMonitoredLiftedStore.getState()).toEqual(oldState); - expect(importMonitoredLiftedStore.getState().actionsById[0].stack).toBe( - undefined - ); + expect( + importMonitoredLiftedStore.getState().actionsById[0].stack + ).toBeUndefined(); expect(importMonitoredLiftedStore.getState().actionsById[1]).toEqual( oldState.actionsById[1] ); }); }); - function filterStackAndTimestamps(state) { + function filterStackAndTimestamps>( + state: LiftedState + ) { state.actionsById = _.mapValues(state.actionsById, action => { delete action.timestamp; delete action.stack; @@ -1113,14 +1175,14 @@ describe('instrument', () => { } describe('Import Actions', () => { - let monitoredStore; - let monitoredLiftedStore; - let exportedState; - let savedActions = [ + let monitoredStore: EnhancedStore; + let monitoredLiftedStore: LiftedStore; + let exportedState: LiftedState; + const savedActions = [ { type: 'INCREMENT' }, { type: 'INCREMENT' }, { type: 'INCREMENT' } - ]; + ] as const; beforeEach(() => { monitoredStore = createStore(counter, instrument()); @@ -1132,8 +1194,8 @@ describe('instrument', () => { }); it('should replay all the steps when a state is imported', () => { - let importMonitoredStore = createStore(counter, instrument()); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredStore = createStore(counter, instrument()); + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; importMonitoredLiftedStore.dispatch( ActionCreators.importState(savedActions) @@ -1144,8 +1206,8 @@ describe('instrument', () => { }); it('should replace the existing action log with the one imported', () => { - let importMonitoredStore = createStore(counter, instrument()); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredStore = createStore(counter, instrument()); + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; importMonitoredStore.dispatch({ type: 'DECREMENT' }); importMonitoredStore.dispatch({ type: 'DECREMENT' }); @@ -1159,11 +1221,11 @@ describe('instrument', () => { }); it('should include stack trace', () => { - let importMonitoredStore = createStore( + const importMonitoredStore = createStore( counter, instrument(undefined, { trace: true }) ); - let importMonitoredLiftedStore = importMonitoredStore.liftedStore; + const importMonitoredLiftedStore = importMonitoredStore.liftedStore; importMonitoredStore.dispatch({ type: 'DECREMENT' }); importMonitoredStore.dispatch({ type: 'DECREMENT' }); @@ -1171,9 +1233,9 @@ describe('instrument', () => { importMonitoredLiftedStore.dispatch( ActionCreators.importState(savedActions) ); - expect(importMonitoredLiftedStore.getState().actionsById[0].stack).toBe( - undefined - ); + expect( + importMonitoredLiftedStore.getState().actionsById[0].stack + ).toBeUndefined(); expect( typeof importMonitoredLiftedStore.getState().actionsById[1].stack ).toBe('string'); @@ -1218,8 +1280,13 @@ describe('instrument', () => { expect(store.liftedStore.getState().nextActionId).toBe(1); expect(store.getState()).toBe(0); - const savedActions = [{ type: 'INCREMENT' }, { type: 'INCREMENT' }]; - store.liftedStore.dispatch(ActionCreators.importState(savedActions)); + const savedActions = [ + { type: 'INCREMENT' }, + { type: 'INCREMENT' } + ] as const; + store.liftedStore.dispatch( + ActionCreators.importState(savedActions) + ); expect(store.liftedStore.getState().nextActionId).toBe(3); expect(store.getState()).toBe(2); @@ -1306,13 +1373,22 @@ describe('instrument', () => { }); it('throws if reducer is not a function', () => { - expect(() => createStore(undefined, instrument())).toThrow( - 'Expected the reducer to be a function.' - ); + expect(() => + createStore((undefined as unknown) as Reducer, instrument()) + ).toThrow('Expected the reducer to be a function.'); }); it('warns if the reducer is not a function but has a default field that is', () => { - expect(() => createStore({ default: () => {} }, instrument())).toThrow( + expect(() => + createStore( + ({ + default: () => { + // noop + } + } as unknown) as Reducer, + instrument() + ) + ).toThrow( 'Expected the reducer to be a function. ' + 'Instead got an object with a "default" field. ' + 'Did you pass a module instead of the default export? ' + @@ -1322,13 +1398,7 @@ describe('instrument', () => { it('throws if there are more than one instrument enhancer included', () => { expect(() => { - createStore( - counter, - compose( - instrument(), - instrument() - ) - ); + createStore(counter, compose(instrument(), instrument())); }).toThrow( 'DevTools instrumentation should not be applied more than once. ' + 'Check your store configuration.' diff --git a/packages/redux-devtools-instrument/test/tsconfig.json b/packages/redux-devtools-instrument/test/tsconfig.json new file mode 100644 index 00000000..b55532d2 --- /dev/null +++ b/packages/redux-devtools-instrument/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["../src", "."] +} diff --git a/packages/redux-devtools-instrument/tsconfig.json b/packages/redux-devtools-instrument/tsconfig.json new file mode 100644 index 00000000..84575cb5 --- /dev/null +++ b/packages/redux-devtools-instrument/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["src"] +} diff --git a/yarn.lock b/yarn.lock index 26aebd2d..4b8f0571 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2598,6 +2598,24 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@^25.2.1": + version "25.2.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5" + integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + +"@types/json-schema@^7.0.3": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" + integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== + +"@types/lodash@^4.2.0": + version "4.14.150" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" + integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -14091,7 +14109,7 @@ redux@^3.0.5, redux@^3.6.0: loose-envify "^1.1.0" symbol-observable "^1.0.3" -redux@^4.0.0, redux@^4.0.1: +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==