From 57fa5c883413c38c11fb5c4f570a4388cd83b9ce Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Sat, 22 Aug 2020 10:28:08 -0400 Subject: [PATCH] feature(react-devtools-instrument): convert to TypeScript (#604) --- packages/redux-devtools-instrument/.babelrc | 3 +- .../redux-devtools-instrument/.eslintignore | 1 + .../redux-devtools-instrument/.eslintrc.js | 21 + .../redux-devtools-instrument/jest.config.js | 3 + .../redux-devtools-instrument/package.json | 63 +-- .../src/{instrument.js => instrument.ts} | 469 +++++++++++++---- ...{instrument.spec.js => instrument.spec.ts} | 491 +++++++++++------- .../test/tsconfig.json | 4 + .../redux-devtools-instrument/tsconfig.json | 7 + 9 files changed, 742 insertions(+), 320 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/jest.config.js rename packages/redux-devtools-instrument/src/{instrument.js => instrument.ts} (63%) rename packages/redux-devtools-instrument/test/{instrument.spec.js => instrument.spec.ts} (73%) 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..5259cd24 100644 --- a/packages/redux-devtools-instrument/.babelrc +++ b/packages/redux-devtools-instrument/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["@babel/preset-env"] + "presets": ["@babel/preset-env", "@babel/preset-typescript"], + "plugins": ["@babel/plugin-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..486b7fd4 --- /dev/null +++ b/packages/redux-devtools-instrument/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + extends: '../../.eslintrc', + overrides: [ + { + files: ['*.ts', '*.tsx'], + extends: '../../eslintrc.ts.base.json', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + }, + { + files: ['test/*.ts', 'test/*.tsx'], + extends: '../../eslintrc.ts.jest.base.json', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./test/tsconfig.json'], + }, + }, + ], +}; diff --git a/packages/redux-devtools-instrument/jest.config.js b/packages/redux-devtools-instrument/jest.config.js new file mode 100644 index 00000000..8824c114 --- /dev/null +++ b/packages/redux-devtools-instrument/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'ts-jest', +}; diff --git a/packages/redux-devtools-instrument/package.json b/packages/redux-devtools-instrument/package.json index b6e9a7fd..2d063098 100644 --- a/packages/redux-devtools-instrument/package.json +++ b/packages/redux-devtools-instrument/package.json @@ -2,22 +2,6 @@ "name": "redux-devtools-instrument", "version": "1.9.7", "description": "Redux DevTools instrumentation", - "main": "lib/instrument.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,25 +10,46 @@ "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-instrument", "bugs": { "url": "https://github.com/reduxjs/redux-devtools/issues" }, - "homepage": "https://github.com/reduxjs/redux-devtools", - "devDependencies": { - "@babel/cli": "^7.10.5", - "@babel/core": "^7.11.1", - "@babel/preset-env": "^7.11.0", - "babel-loader": "^8.1.0", - "expect": "^26.2.0", - "jest": "^26.2.2", - "redux": "^4.0.5", - "rimraf": "^3.0.2", - "rxjs": "^6.6.2" + "license": "MIT", + "author": "Dan Abramov (http://github.com/gaearon)", + "files": [ + "lib", + "src" + ], + "main": "lib/instrument.js", + "types": "lib/instrument.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\" --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": { "lodash": "^4.17.19", "symbol-observable": "^1.2.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.159", + "jest": "^26.2.2", + "redux": "^4.0.5", + "rxjs": "^6.6.2" + }, + "peerDependencies": { + "redux": "^3.4.0 || ^4.0.0" } } diff --git a/packages/redux-devtools-instrument/src/instrument.js b/packages/redux-devtools-instrument/src/instrument.ts similarity index 63% rename from packages/redux-devtools-instrument/src/instrument.js rename to packages/redux-devtools-instrument/src/instrument.ts index 14afbbef..0829bb8f 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,103 @@ const isChromeOrNode = process.release && process.release.name === 'node'); +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, MonitorState> { + 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, MonitorState> = + | 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, + // eslint-disable-next-line @typescript-eslint/ban-types + toExcludeFromTrace?: Function + ) { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + @@ -60,7 +163,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,18 +176,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].startsWith('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'); + } } } } @@ -98,51 +203,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, MonitorState = null>( + 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 }; }, }; @@ -152,7 +264,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 { @@ -178,7 +294,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) }; } @@ -188,15 +309,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. @@ -245,7 +366,13 @@ function recomputeStates( /** * Lifts an app's action into an action on the lifted store. */ -export function liftAction(action, trace, traceLimit, toExcludeFromTrace) { +export function liftAction>( + action: A, + trace?: ((action: A) => string | undefined) | boolean, + traceLimit?: number, + // eslint-disable-next-line @typescript-eslint/ban-types + toExcludeFromTrace?: Function +) { return ActionCreators.performAction( action, trace, @@ -254,22 +381,46 @@ export function liftAction(action, trace, traceLimit, toExcludeFromTrace) { ); } +function isArray, MonitorState>( + nextLiftedState: LiftedState | readonly A[] +): nextLiftedState is readonly A[] { + return Array.isArray(nextLiftedState); +} + +export interface LiftedState, MonitorState> { + monitorState: MonitorState; + nextActionId: number; + actionsById: { [actionId: number]: PerformAction }; + stagedActionIds: number[]; + skippedActionIds: number[]; + committedState: S; + currentStateIndex: number; + computedStates: { state: S; error?: string }[]; + isLocked: boolean; + isPaused: boolean; +} + /** * 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< + S, + A extends Action, + MonitorState, + MonitorAction extends Action +>( + reducer: Reducer, + initialCommittedState: PreloadedState | undefined, + monitorReducer: Reducer, + options: Options +): Reducer, LiftedAction> { + const initialLiftedState: LiftedState = { + monitorState: monitorReducer(undefined, {} as MonitorAction), 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, @@ -279,7 +430,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, @@ -298,7 +452,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); @@ -324,15 +478,20 @@ export function liftReducerWith( currentStateIndex > excess ? currentStateIndex - excess : 0; } - function computePausedAction(shouldInit) { + function computePausedAction( + shouldInit?: boolean + ): LiftedState { let computedState; if (shouldInit) { computedState = computedStates[currentStateIndex]; - monitorState = monitorReducer(monitorState, liftedAction); + monitorState = monitorReducer( + monitorState, + liftedAction as MonitorAction + ); } else { computedState = computeNextEntry( reducer, - liftedAction.action, + (liftedAction as PerformAction).action, computedStates[currentStateIndex].state, false ); @@ -340,7 +499,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: [], @@ -362,7 +521,9 @@ export function liftReducerWith( monitorState, actionsById: { ...actionsById, - [nextActionId - 1]: liftAction({ type: options.pauseActionType }), + [nextActionId - 1]: liftAction({ + type: options.pauseActionType, + } as A), }, nextActionId, stagedActionIds, @@ -378,7 +539,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; @@ -390,13 +551,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 = []; @@ -407,7 +568,7 @@ export function liftReducerWith( if (maxAge && stagedActionIds.length > maxAge) { // States must be recomputed before committing excess. - computedStates = recomputeStates( + computedStates = recomputeStates( computedStates, minInvalidatedStateIndex, reducer, @@ -448,11 +609,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; @@ -460,7 +621,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 = []; @@ -472,7 +633,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 = []; @@ -573,15 +734,15 @@ 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) => { @@ -623,7 +784,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 = []; @@ -651,7 +812,7 @@ export function liftReducerWith( skippedActionIds, options.shouldCatchErrors ); - monitorState = monitorReducer(monitorState, liftedAction); + monitorState = monitorReducer(monitorState, liftedAction as MonitorAction); return { monitorState, actionsById, @@ -670,34 +831,78 @@ export function liftReducerWith( /** * Provides an app's view into the state of the lifted store. */ -export function unliftState(liftedState) { +export function unliftState< + S, + A extends Action, + MonitorState, + NextStateExt +>( + liftedState: LiftedState & NextStateExt +): S & NextStateExt { const { computedStates, currentStateIndex } = liftedState; const { state } = computedStates[currentStateIndex]; - return state; + return state as S & NextStateExt; } +export type LiftedReducer, MonitorState> = Reducer< + LiftedState, + LiftedAction +>; + +export type LiftedStore, MonitorState> = Store< + LiftedState, + LiftedAction +>; + +export type InstrumentExt, MonitorState> = { + liftedStore: LiftedStore; +}; + +export type EnhancedStore, MonitorState> = Store< + S, + A +> & + 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, + MonitorState, + MonitorAction extends Action, + NextExt, + NextStateExt +>( + liftedStore: Store< + LiftedState & NextStateExt, + LiftedAction + > & + NextExt, + liftReducer: (r: Reducer) => LiftedReducer, + options: Options +) { + 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() + ); 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, @@ -706,13 +911,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.'); @@ -728,15 +940,56 @@ export function unliftStore(liftedStore, liftReducer, options) { const unsubscribe = liftedStore.subscribe(observeState); return { unsubscribe }; }, + + [$$observable]() { + return this; + }, }; }, - }; + } as unknown) as Store & + NextExt & { + liftedStore: Store< + LiftedState & NextStateExt, + LiftedAction + >; + }; +} + +export interface Options< + S, + A extends Action, + MonitorState, + MonitorAction extends Action +> { + 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< + OptionsS, + OptionsA extends Action, + MonitorState = null, + MonitorAction extends Action = never +>( + monitorReducer: Reducer = ((() => + null) as unknown) as Reducer, + options: Options = {} +): StoreEnhancer> { if (typeof options.maxAge === 'number' && options.maxAge < 2) { throw new Error( 'DevTools.instrument({ maxAge }) option, if specified, ' + @@ -744,10 +997,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. ' + @@ -757,17 +1015,44 @@ 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< + LiftedState, + 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< + S, + A, + MonitorState, + MonitorAction, + NextExt, + NextStateExt + >( + 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 73% rename from packages/redux-devtools-instrument/test/instrument.spec.js rename to packages/redux-devtools-instrument/test/instrument.spec.ts index 5a59bfc6..93ab1740 100644 --- a/packages/redux-devtools-instrument/test/instrument.spec.js +++ b/packages/redux-devtools-instrument/test/instrument.spec.ts @@ -1,9 +1,15 @@ -import { createStore, compose } from 'redux'; -import instrument, { ActionCreators } from '../src/instrument'; -import { from } from 'rxjs'; +import { createStore, compose, Reducer, Store, Action } from 'redux'; +import instrument, { + ActionCreators, + EnhancedStore, + LiftedStore, + LiftedState, +} from '../src/instrument'; +import { from, Observable } from 'rxjs'; import _ from 'lodash'; -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; @@ -14,23 +20,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': - return mistake - 1; // eslint-disable-line no-undef + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return mistake - 1; 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: CounterWithBugAction) { switch (action.type) { case 'INCREMENT': - return mistake + 1; // eslint-disable-line no-undef + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return ((mistake as unknown) as number) + 1; case 'DECREMENT': return state - 1; case 'SET_UNDEFINED': @@ -40,7 +58,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; @@ -51,7 +70,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; @@ -65,8 +88,8 @@ function counterWithMultiply(state = 0, action) { } describe('instrument', () => { - let store; - let liftedStore; + let store: EnhancedStore; + let liftedStore: LiftedStore; beforeEach(() => { store = createStore(counter, instrument()); @@ -85,7 +108,7 @@ describe('instrument', () => { let lastValue; // let calls = 0; - from(store).subscribe((state) => { + from((store as unknown) as Observable).subscribe((state) => { lastValue = state; // calls++; }); @@ -229,89 +252,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', () => { @@ -340,13 +363,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 }) ); @@ -355,7 +383,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/ @@ -365,22 +393,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.' @@ -388,7 +416,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); @@ -399,7 +427,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' }); @@ -409,8 +437,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 @@ -452,8 +480,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' }); @@ -461,7 +489,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); @@ -479,8 +507,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' }); @@ -488,12 +516,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( @@ -502,8 +530,8 @@ describe('instrument', () => { }); describe('maxAge option', () => { - let configuredStore; - let configuredLiftedStore; + let configuredStore: EnhancedStore; + let configuredLiftedStore: LiftedStore; beforeEach(() => { configuredStore = createStore( @@ -519,8 +547,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. @@ -528,7 +556,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); @@ -547,53 +575,68 @@ 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, + null + >; + 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, + null + >; + 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' }); @@ -603,28 +646,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); @@ -634,13 +677,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' }); @@ -648,9 +691,16 @@ 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, + null + >; + const liftedStoreState = liftedStore.getState(); + const currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex]; expect(liftedStoreState.currentStateIndex).toBe(2); expect(currentComputedState.state).toBe(-4); @@ -659,13 +709,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' }); @@ -674,9 +724,16 @@ 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, + null + >; + const liftedStoreState = liftedStore.getState(); + const currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex]; expect(liftedStoreState.currentStateIndex).toBe(0); expect(currentComputedState.state).toBe(-2); @@ -692,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]); @@ -708,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. @@ -717,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); @@ -728,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); @@ -739,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); @@ -749,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); @@ -774,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', () => { @@ -788,30 +845,34 @@ 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` } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); }); @@ -825,34 +886,39 @@ 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(/\bfn1\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn2\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 ); } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); }); it('should force traceLimit value of 3 when Error.stackTraceLimit is 10', () => { const stackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 10; + function fn1() { monitoredStore = createStore( counter, @@ -862,28 +928,32 @@ 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(/\bfn1\b/); expect(exportedState.actionsById[1].stack).toMatch(/\bfn2\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 ); } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); Error.stackTraceLimit = stackTraceLimit; }); @@ -900,18 +970,21 @@ 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', () => { const stackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 3; + function fn1() { monitoredStore = createStore( counter, @@ -922,34 +995,39 @@ 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 ); } + function fn2() { return fn1(); } + function fn3() { return fn2(); } + function fn4() { return fn3(); } + fn4(); }); it('should include 3 extra frames when Error.captureStackTrace not suported', () => { + // eslint-disable-next-line @typescript-eslint/unbound-method const captureStackTrace = Error.captureStackTrace; - Error.captureStackTrace = undefined; + Error.captureStackTrace = (undefined as unknown) as () => unknown; monitoredStore = createStore( counter, instrument(undefined, { trace: true, traceLimit: 5 }) @@ -959,14 +1037,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 ); }); @@ -981,47 +1059,48 @@ 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 Object.performAction' ); - 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' ); }); - 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', () => + 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 Object.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()); @@ -1035,8 +1114,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) @@ -1045,8 +1124,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' }); @@ -1058,17 +1137,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( @@ -1077,31 +1156,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; @@ -1111,14 +1192,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()); @@ -1130,8 +1211,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) @@ -1142,8 +1223,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' }); @@ -1157,11 +1238,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' }); @@ -1169,9 +1250,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'); @@ -1216,8 +1297,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); @@ -1304,13 +1390,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? ' + 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"] +}