Merge pull request #416 from reduxjs/monorepo

Monorepo initial configuration
This commit is contained in:
Mihail Diordiev 2018-12-13 03:37:27 +02:00 committed by GitHub
commit 6fbb2ab7c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 9922 additions and 200 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules
.DS_Store
lib
coverage
.idea

View File

@ -3,3 +3,10 @@ language: node_js
node_js:
- "lts/*"
- "stable"
cache:
yarn: true
directories:
- "node_modules"
script:
- npm run lint
- npm test

1
.yarnrc Normal file
View File

@ -0,0 +1 @@
workspaces-experimental true

134
README.md
View File

@ -1,130 +1,24 @@
Redux DevTools
=========================
[![Build Status](https://travis-ci.org/reduxjs/redux-devtools.svg?branch=master)](https://travis-ci.org/reduxjs/redux-devtools) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=round-square)](https://github.com/reduxjs/redux-devtools/pulls)
A live-editing time travel environment for [Redux](https://github.com/reactjs/redux).
**[See Dan's React Europe talk demoing it!](http://youtube.com/watch?v=xsSnOQynTHs)**
# Redux DevTools
> Note that the implemention in this repository is different from [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension). Please refer to the latter for browser extension.
Developer Tools to power-up [Redux](https://github.com/reactjs/redux) development workflow or any other architecture which handles the state change (see [integrations](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Integrations.md)).
### Table of Contents
It can be used as a browser extension (for [Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/remotedev/)), as [a standalone app](https://github.com/zalmoxisus/remotedev-app) or as [a React component](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools) integrated in the client app.
- [Features](#features)
- [Overview](#overview)
- [Browser Extension](#browser-extension)
- [Setup Instructions](#setup-instructions)
- [Custom Monitors](#custom-monitors)
- [License](#license)
![image](https://user-images.githubusercontent.com/7957859/48663602-3aac4900-ea9b-11e8-921f-97059cbb599c.png)
[![build status](https://img.shields.io/travis/reduxjs/redux-devtools/master.svg?style=flat-square)](https://travis-ci.org/reduxjs/redux-devtools)
[![npm version](https://img.shields.io/npm/v/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
[![npm downloads](https://img.shields.io/npm/dm/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
[![redux channel on discord](https://img.shields.io/badge/discord-redux@reactiflux-738bd7.svg?style=flat-square)](https://discord.gg/0ZcbPKXt5bWb10Ma)
> Note that this repository is work in progress for [the monorepo](https://github.com/reduxjs/redux-devtools/issues/412), which will merge all the packages. Please refer to [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) and [Redux DevTools package](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools).
![](http://i.imgur.com/J4GeW0M.gif)
### Documentation
### Features
* Lets you inspect every state and action payload
* Lets you go back in time by “cancelling” actions
* If you change the reducer code, each “staged” action will be re-evaluated
* If the reducers throw, you will see during which action this happened, and what the error was
* With `persistState()` store enhancer, you can persist debug sessions across page reloads
### Overview
Redux DevTools is a development time package that provides power-ups for your Redux development workflow. Be careful to strip its code in production (see [walkthrough](./docs/Walkthrough.md) for instructions)! To use Redux DevTools, you need to choose a “monitor”—a React component that will serve as a UI for the DevTools. Different tasks and workflows require different UIs, so Redux DevTools is built to be flexible in this regard. We recommend using [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) for inspecting the state and time travel, and wrap it in a [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) to quickly move it across the screen. That said, when youre comfortable rolling up your own setup, feel free to do this, and share it with us.
If you came here looking for what do the “Reset”, “Revert”, “Sweep” or “Commit” buttons do, check out [the `LogMonitor` documentation](https://github.com/gaearon/redux-devtools-log-monitor/blob/master/README.md#features).
### Browser Extension
If you dont want to bother with installing Redux DevTools and integrating it into your project, consider using [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) for Chrome and Firefox. It provides access to the most popular monitors, is easy to configure to filter actions, and doesnt require installing any packages.
### Setup Instructions
Read the installation [walkthrough](./docs/Walkthrough.md) for integration instructions and usage examples (`<DevTools>` component, `DevTools.instrument()`, exclude from production builds, gotchas).
### Running Examples
Clone the project:
```
git clone https://github.com/reduxjs/redux-devtools.git
cd redux-devtools
```
Run `npm install` in the root folder:
```
npm install
```
Now you can open an example folder and run `npm install` there:
```
cd examples/counter # or examples/todomvc
npm install
```
Finally, run the development server and open the page:
```
npm start
open http://localhost:3000
```
Try clicking on actions in the log, or changing some code inside the reducers. You should see the action log re-evaluate the state on every code change.
Also try opening `http://localhost:3000/?debug_session=123`, click around, and then refresh. You should see that all actions have been restored from the local storage.
### Custom Monitors
**DevTools accepts monitor components so you can build a completely custom UI.** [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) and [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) are just examples of what is possible.
**[I challenge you to build a custom monitor for Redux DevTools!](https://github.com/reduxjs/redux-devtools/issues/3)**
Some crazy ideas for custom monitors:
* A slider that lets you jump between computed states just by dragging it
* An in-app layer that shows the last N states right in the app (e.g. for animation)
* A time machine like interface where the last N states of your app reside on different Z layers
* Feel free to come up with and implement your own! Check [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) `propTypes` to see what you can do.
In fact some of these are implemented already:
#### [Slider Monitor](https://github.com/calesce/redux-slider-monitor)
![](https://camo.githubusercontent.com/47a3f427c9d2e0c763b74e33417b3001fe8604b6/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f662e636c2e6c792f6974656d732f3149335032323243334e3252314d3279314b33622f53637265656e2532305265636f7264696e67253230323031352d31322d3232253230617425323030372e3230253230504d2e6769663f763d3162363236376537)
#### [Inspector](https://github.com/alexkuz/redux-devtools-inspector)
![](http://i.imgur.com/fYh8fk5.gif)
#### [Diff Monitor](https://github.com/whetstone/redux-devtools-diff-monitor)
![](https://camo.githubusercontent.com/c2c0ba1ad82d003b5386404ae09c00763d73510c/687474703a2f2f692e696d6775722e636f6d2f72764352394f512e706e67)
#### [Filterable Log Monitor](https://github.com/bvaughn/redux-devtools-filterable-log-monitor/)
![redux-devtools-filterable-log-monitor](https://cloud.githubusercontent.com/assets/29597/12440009/182bb31c-beec-11e5-8fd0-bdda48e646b2.gif)
#### [Chart Monitor](https://github.com/romseguy/redux-devtools-chart-monitor)
![redux-devtools-chart-monitor](http://i.imgur.com/MSgvU6l.gif)
#### [Filter Actions](https://github.com/zalmoxisus/redux-devtools-filter-actions)
(Does not have a UI but can wrap any other monitor)
<img src='http://i.imgur.com/TlqnU0J.png' width='400'>
#### [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch)
![redux-devtools-dispatch](https://cloud.githubusercontent.com/assets/969003/12874321/2c3624ec-cdd2-11e5-9856-fd7e24efb8d5.gif)
#### Keep them coming!
Create a PR to add your custom monitor.
- [Brower Extension Instalation and Configuration](https://github.com/zalmoxisus/redux-devtools-extension#installation)
- [Manual Integration as a React Component](./docs/Walkthrough.md#manual-integration)
- [Extension Options (Arguments)](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API/Arguments.md)
- [Extension Methods (Advanced API)](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API/Methods.md)
- [Troubleshooting](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/Troubleshooting.md)
- [Recipes](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/Recipes.md)
- [FAQ](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/FAQ.md)
### License

View File

@ -73,7 +73,7 @@ A store enhancer is a function that enhances the behavior of `createStore()`. Yo
The easiest way to apply several store enhancers in a row is to use the [`compose()`](http://redux.js.org/docs/api/compose.html) utility function that ships with Redux. It is the same `compose()` that you can find in Underscore and Lodash. In our case, we would use it to compose several store enhancers into one: `compose(applyMiddleware(m1, m2, m3), DevTools.instrument())`.
You can add additional options to it: `DevTools.instrument({ maxAge: 50, shouldCatchErrors: true })`. See [`redux-devtools-instrument`'s API](https://github.com/zalmoxisus/redux-devtools-instrument#api) for more details.
You can add additional options to it: `DevTools.instrument({ maxAge: 50, shouldCatchErrors: true })`. See [`redux-devtools-instrument`'s API](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-instrument#api) for more details.
Its important that you should add `DevTools.instrument()` *after* `applyMiddleware` in your `compose()` function arguments. This is because `applyMiddleware` is potentially asynchronous, but `DevTools.instrument()` expects all actions to be plain objects rather than actions interpreted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk). So make sure `applyMiddleware()` goes first in the `compose()` call, and `DevTools.instrument()` goes after it.

16
lerna.json Normal file
View File

@ -0,0 +1,16 @@
{
"lerna": "3.4.2",
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"allowBranch": "master"
}
},
"ignoreChanges": [
"**/test/**",
"**/examples/**",
"**/*.md"
]
}

View File

@ -1,72 +1,20 @@
{
"name": "redux-devtools",
"version": "3.4.2",
"description": "Redux DevTools with hot reloading and time travel",
"main": "lib/index.js",
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"lint": "eslint src test examples",
"test": "cross-env NODE_ENV=test mocha --compilers js:babel-core/register --recursive",
"test:watch": "cross-env NODE_ENV=test mocha --compilers js:babel-core/register --recursive --watch",
"test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive",
"prepublish": "npm run lint && npm run test && npm run clean && npm run build"
},
"files": [
"lib",
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/gaearon/redux-devtools.git"
},
"keywords": [
"redux",
"devtools",
"flux",
"hot reloading",
"time travel",
"live edit"
],
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)",
"license": "MIT",
"bugs": {
"url": "https://github.com/gaearon/redux-devtools/issues"
},
"homepage": "https://github.com/gaearon/redux-devtools",
"private": true,
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-core": "^6.3.17",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-preset-es2015-loose": "^6.1.3",
"babel-preset-react": "6.3.13",
"babel-preset-stage-0": "^6.3.13",
"cross-env": "3.1.3",
"eslint": "^0.23",
"eslint-config-airbnb": "0.0.6",
"eslint-plugin-react": "^2.3.0",
"expect": "^1.6.0",
"isparta": "^3.0.3",
"jsdom": "^6.5.1",
"mocha": "^2.2.5",
"mocha-jsdom": "^1.0.0",
"react": "^0.14.9",
"react-addons-test-utils": "^0.14.0",
"react-dom": "^0.14.0",
"react-redux": "^4.0.0",
"redux": "^3.5.2",
"rimraf": "^2.3.4",
"webpack": "^1.11.0"
"lerna": "3.4.2"
},
"peerDependencies": {
"react": "^0.14.9 || ^15.3.0 || ^16.0.0",
"react-redux": "^4.0.0 || ^5.0.0",
"redux": "^3.5.2 || ^4.0.0"
"scripts": {
"lerna": "lerna",
"prepare": "lerna run prepare --since master --stream --sort -- --scripts-prepend-node-path",
"publish": "lerna publish",
"canary": "lerna publish --canary preminor --npm-tag alpha",
"next": "lerna publish --bump prerelease --npm-tag next",
"lint": "lerna run lint --since master -- --color",
"lint:all": "lerna run lint -- --color",
"test": "lerna run test --since master -- --colors",
"test:all": "lerna run test -- --colors"
},
"dependencies": {
"prop-types": "^15.5.7",
"lodash": "^4.2.0",
"redux-devtools-instrument": "^1.9.0"
}
"workspaces": [
"packages/*"
]
}

View File

@ -0,0 +1,3 @@
{
"presets": ["es2015-loose", "stage-0"]
}

View File

@ -0,0 +1,57 @@
Redux DevTools Instrumentation
==============================
Redux enhancer used along with [Redux DevTools](https://github.com/reduxjs/redux-devtools) or [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools).
### Installation
```
npm install --save-dev redux-devtools-instrument
```
### Usage
Add the store enhancer:
##### `store/configureStore.js`
```js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import devTools from 'remote-redux-devtools';
import reducer from '../reducers';
// Usually you import the reducer from the monitor
// or apply with createDevTools as explained in Redux DevTools
const monitorReducer = (state = {}, action) => state;
export default function configureStore(initialState) {
const enhancer = compose(
applyMiddleware(...middlewares),
// other enhancers and applyMiddleware should be added before the instrumentation
instrument(monitorReducer, { maxAge: 50 })
);
// Note: passing enhancer as last argument requires redux@>=3.1.0
return createStore(reducer, initialState, enhancer);
}
```
### API
`instrument(monitorReducer, [options])`
- arguments
- **monitorReducer** *function* called whenever an action is dispatched ([see the example of a monitor reducer](https://github.com/gaearon/redux-devtools-log-monitor/blob/master/src/reducers.js#L13)).
- **options** *object*
- **maxAge** *number* or *function*(currentLiftedAction, previousLiftedState) - maximum allowed actions to be stored on the history tree, the oldest actions are removed once `maxAge` is reached. Can be generated dynamically with a function getting current action as argument.
- **shouldCatchErrors** *boolean* - if specified as `true`, whenever there's an exception in reducers, the monitors will show the error message, and next actions will not be dispatched.
- **shouldRecordChanges** *boolean* - if specified as `false`, it will not record the changes till `pauseRecording(false)` is dispatched. Default is `true`.
- **pauseActionType** *string* - if specified, whenever `pauseRecording(false)` lifted action is dispatched and there are actions in the history log, will add this action type. If not specified, will commit when paused.
- **shouldStartLocked** *boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`.
- **shouldHotReload** *boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`.
- **shouldIncludeCallstack** *boolean* - if set to `true`, will include callstack for every dispatched action. Default to `false`.
### License
MIT

View File

@ -0,0 +1,60 @@
{
"name": "redux-devtools-instrument",
"version": "1.9.3",
"description": "Redux DevTools instrumentation",
"main": "lib/instrument.js",
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"lint": "eslint src test",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive",
"test:watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive --watch",
"test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive",
"prepare": "npm run build",
"prepublishOnly": "npm run lint && 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",
"flux",
"hot reloading",
"time travel",
"live edit"
],
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)",
"license": "MIT",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-core": "^6.3.17",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-preset-es2015-loose": "^6.1.3",
"babel-preset-stage-0": "^6.3.13",
"eslint": "^0.23",
"eslint-config-airbnb": "0.0.6",
"eslint-plugin-react": "^2.3.0",
"expect": "^1.6.0",
"isparta": "^3.0.3",
"mocha": "^2.2.5",
"redux": "^4.0.0",
"rimraf": "^2.3.4",
"rxjs": "^5.0.0-beta.6",
"webpack": "^1.11.0"
},
"dependencies": {
"lodash": "^4.2.0",
"symbol-observable": "^1.0.2"
}
}

View File

@ -0,0 +1,685 @@
import difference from 'lodash/difference';
import union from 'lodash/union';
import isPlainObject from 'lodash/isPlainObject';
import $$observable from 'symbol-observable';
export const ActionTypes = {
PERFORM_ACTION: 'PERFORM_ACTION',
RESET: 'RESET',
ROLLBACK: 'ROLLBACK',
COMMIT: 'COMMIT',
SWEEP: 'SWEEP',
TOGGLE_ACTION: 'TOGGLE_ACTION',
SET_ACTIONS_ACTIVE: 'SET_ACTIONS_ACTIVE',
JUMP_TO_STATE: 'JUMP_TO_STATE',
JUMP_TO_ACTION: 'JUMP_TO_ACTION',
REORDER_ACTION: 'REORDER_ACTION',
IMPORT_STATE: 'IMPORT_STATE',
LOCK_CHANGES: 'LOCK_CHANGES',
PAUSE_RECORDING: 'PAUSE_RECORDING'
};
/**
* Action creators to change the History state.
*/
export const ActionCreators = {
performAction(action, shouldIncludeCallstack) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
);
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
);
}
return {
type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now(),
stack: shouldIncludeCallstack ? Error().stack : undefined
};
},
reset() {
return { type: ActionTypes.RESET, timestamp: Date.now() };
},
rollback() {
return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
},
commit() {
return { type: ActionTypes.COMMIT, timestamp: Date.now() };
},
sweep() {
return { type: ActionTypes.SWEEP };
},
toggleAction(id) {
return { type: ActionTypes.TOGGLE_ACTION, id };
},
setActionsActive(start, end, active=true) {
return { type: ActionTypes.SET_ACTIONS_ACTIVE, start, end, active };
},
reorderAction(actionId, beforeActionId) {
return { type: ActionTypes.REORDER_ACTION, actionId, beforeActionId };
},
jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index };
},
jumpToAction(actionId) {
return { type: ActionTypes.JUMP_TO_ACTION, actionId };
},
importState(nextLiftedState, noRecompute) {
return { type: ActionTypes.IMPORT_STATE, nextLiftedState, noRecompute };
},
lockChanges(status) {
return { type: ActionTypes.LOCK_CHANGES, status };
},
pauseRecording(status) {
return { type: ActionTypes.PAUSE_RECORDING, status };
}
};
export const INIT_ACTION = { type: '@@INIT' };
/**
* Computes the next entry with exceptions catching.
*/
function computeWithTryCatch(reducer, action, state) {
let nextState = state;
let nextError;
try {
nextState = reducer(state, action);
} catch (err) {
nextError = err.toString();
if (
typeof window === 'object' && (
typeof window.chrome !== 'undefined' ||
typeof window.process !== 'undefined' &&
window.process.type === 'renderer'
)) {
// In Chrome, rethrowing provides better source map support
setTimeout(() => { throw err; });
} else {
console.error(err);
}
}
return {
state: nextState,
error: nextError
};
}
/**
* Computes the next entry in the log by applying an action.
*/
function computeNextEntry(reducer, action, state, shouldCatchErrors) {
if (!shouldCatchErrors) {
return { state: reducer(state, action) };
}
return computeWithTryCatch(reducer, action, state);
}
/**
* Runs the reducer on invalidated actions to get a fresh computation log.
*/
function recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds,
shouldCatchErrors
) {
// Optimization: exit early and return the same reference
// if we know nothing could have changed.
if (
!computedStates || minInvalidatedStateIndex === -1 ||
(minInvalidatedStateIndex >= computedStates.length &&
computedStates.length === stagedActionIds.length)
) {
return computedStates;
}
const nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
for (let i = minInvalidatedStateIndex; i < stagedActionIds.length; i++) {
const actionId = stagedActionIds[i];
const action = actionsById[actionId].action;
const previousEntry = nextComputedStates[i - 1];
const previousState = previousEntry ? previousEntry.state : committedState;
const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
let entry;
if (shouldSkip) {
entry = previousEntry;
} else {
if (shouldCatchErrors && previousEntry && previousEntry.error) {
entry = {
state: previousState,
error: 'Interrupted by an error up the chain'
};
} else {
entry = computeNextEntry(reducer, action, previousState, shouldCatchErrors);
}
}
nextComputedStates.push(entry);
}
return nextComputedStates;
}
/**
* Lifts an app's action into an action on the lifted store.
*/
export function liftAction(action, shouldIncludeCallstack) {
return ActionCreators.performAction(action, shouldIncludeCallstack);
}
/**
* Creates a history state reducer from an app's reducer.
*/
export function liftReducerWith(reducer, initialCommittedState, monitorReducer, options) {
const initialLiftedState = {
monitorState: monitorReducer(undefined, {}),
nextActionId: 1,
actionsById: { 0: liftAction(INIT_ACTION) },
stagedActionIds: [0],
skippedActionIds: [],
committedState: initialCommittedState,
currentStateIndex: 0,
computedStates: [],
isLocked: options.shouldStartLocked === true,
isPaused: options.shouldRecordChanges === false
};
/**
* Manages how the history actions modify the history state.
*/
return (liftedState, liftedAction) => {
let {
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates,
isLocked,
isPaused
} = liftedState || initialLiftedState;
if (!liftedState) {
// Prevent mutating initialLiftedState
actionsById = { ...actionsById };
}
function commitExcessActions(n) {
// Auto-commits n-number of excess actions.
let excess = n;
let idsToDelete = stagedActionIds.slice(1, excess + 1);
for (let i = 0; i < idsToDelete.length; i++) {
if (computedStates[i + 1].error) {
// Stop if error is found. Commit actions up to error.
excess = i;
idsToDelete = stagedActionIds.slice(1, excess + 1);
break;
} else {
delete actionsById[idsToDelete[i]];
}
}
skippedActionIds = skippedActionIds.filter(id => idsToDelete.indexOf(id) === -1);
stagedActionIds = [0, ...stagedActionIds.slice(excess + 1)];
committedState = computedStates[excess].state;
computedStates = computedStates.slice(excess);
currentStateIndex = currentStateIndex > excess
? currentStateIndex - excess
: 0;
}
function computePausedAction(shouldInit) {
let computedState;
if (shouldInit) {
computedState = computedStates[currentStateIndex];
monitorState = monitorReducer(monitorState, liftedAction);
} else {
computedState = computeNextEntry(
reducer, liftedAction.action, computedStates[currentStateIndex].state, false
);
}
if (!options.pauseActionType || nextActionId === 1) {
return {
monitorState,
actionsById: { 0: liftAction(INIT_ACTION) },
nextActionId: 1,
stagedActionIds: [0],
skippedActionIds: [],
committedState: computedState.state,
currentStateIndex: 0,
computedStates: [computedState],
isLocked,
isPaused: true
};
}
if (shouldInit) {
if (currentStateIndex === stagedActionIds.length - 1) {
currentStateIndex++;
}
stagedActionIds = [...stagedActionIds, nextActionId];
nextActionId++;
}
return {
monitorState,
actionsById: {
...actionsById,
[nextActionId - 1]: liftAction({ type: options.pauseActionType })
},
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates: [...computedStates.slice(0, stagedActionIds.length - 1), computedState],
isLocked,
isPaused: true
};
}
// By default, agressively 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;
// maxAge number can be changed dynamically
let maxAge = options.maxAge;
if (typeof maxAge === 'function') maxAge = maxAge(liftedAction, liftedState);
if (/^@@redux\/(INIT|REPLACE)/.test(liftedAction.type)) {
if (options.shouldHotReload === false) {
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates.length === 0 ? initialCommittedState :
computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
}
// Recompute states on hot reload and init.
minInvalidatedStateIndex = 0;
if (maxAge && stagedActionIds.length > maxAge) {
// States must be recomputed before committing excess.
computedStates = recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds,
options.shouldCatchErrors
);
commitExcessActions(stagedActionIds.length - maxAge);
// Avoid double computation.
minInvalidatedStateIndex = Infinity;
}
} else {
switch (liftedAction.type) {
case ActionTypes.PERFORM_ACTION: {
if (isLocked) return liftedState || initialLiftedState;
if (isPaused) return computePausedAction();
// Auto-commit as new actions come in.
if (maxAge && stagedActionIds.length >= maxAge) {
commitExcessActions(stagedActionIds.length - maxAge + 1);
}
if (currentStateIndex === stagedActionIds.length - 1) {
currentStateIndex++;
}
const actionId = nextActionId++;
// Mutation! This is the hottest path, and we optimize on purpose.
// It is safe because we set a new key in a cache dictionary.
actionsById[actionId] = liftedAction;
stagedActionIds = [...stagedActionIds, actionId];
// Optimization: we know that only the new action needs computing.
minInvalidatedStateIndex = stagedActionIds.length - 1;
break;
}
case ActionTypes.RESET: {
// Get back to the state the store was created with.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = initialCommittedState;
currentStateIndex = 0;
computedStates = [];
break;
}
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) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.ROLLBACK: {
// Forget about any staged actions.
// Start again from the last committed state.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.TOGGLE_ACTION: {
// Toggle whether an action with given ID is skipped.
// Being skipped means it is a no-op during the computation.
const { id: actionId } = liftedAction;
const index = skippedActionIds.indexOf(actionId);
if (index === -1) {
skippedActionIds = [actionId, ...skippedActionIds];
} else {
skippedActionIds = skippedActionIds.filter(id => id !== actionId);
}
// Optimization: we know history before this action hasn't changed
minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
break;
}
case ActionTypes.SET_ACTIONS_ACTIVE: {
// Toggle whether an action with given ID is skipped.
// Being skipped means it is a no-op during the computation.
const { start, end, active } = liftedAction;
const actionIds = [];
for (let i = start; i < end; i++) actionIds.push(i);
if (active) {
skippedActionIds = difference(skippedActionIds, actionIds);
} else {
skippedActionIds = union(skippedActionIds, actionIds);
}
// Optimization: we know history before this action hasn't changed
minInvalidatedStateIndex = stagedActionIds.indexOf(start);
break;
}
case ActionTypes.JUMP_TO_STATE: {
// Without recomputing anything, move the pointer that tell us
// which state is considered the current one. Useful for sliders.
currentStateIndex = liftedAction.index;
// Optimization: we know the history has not changed.
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.JUMP_TO_ACTION: {
// Jumps to a corresponding state to a specific action.
// Useful when filtering actions.
const index = stagedActionIds.indexOf(liftedAction.actionId);
if (index !== -1) currentStateIndex = index;
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.SWEEP: {
// Forget any actions that are currently being skipped.
stagedActionIds = difference(stagedActionIds, skippedActionIds);
skippedActionIds = [];
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
break;
}
case ActionTypes.REORDER_ACTION: {
// Recompute actions in a new order.
const actionId = liftedAction.actionId;
const idx = stagedActionIds.indexOf(actionId);
// do nothing in case the action is already removed or trying to move the first action
if (idx < 1) break;
const beforeActionId = liftedAction.beforeActionId;
let newIdx = stagedActionIds.indexOf(beforeActionId);
if (newIdx < 1) { // move to the beginning or to the end
const count = stagedActionIds.length;
newIdx = beforeActionId > stagedActionIds[count - 1] ? count : 1;
}
const diff = idx - newIdx;
if (diff > 0) { // move left
stagedActionIds = [
...stagedActionIds.slice(0, newIdx),
actionId,
...stagedActionIds.slice(newIdx, idx),
...stagedActionIds.slice(idx + 1)
];
minInvalidatedStateIndex = newIdx;
} else if (diff < 0) { // move right
stagedActionIds = [
...stagedActionIds.slice(0, idx),
...stagedActionIds.slice(idx + 1, newIdx),
actionId,
...stagedActionIds.slice(newIdx)
];
minInvalidatedStateIndex = idx;
}
break;
}
case ActionTypes.IMPORT_STATE: {
if (Array.isArray(liftedAction.nextLiftedState)) {
// recompute array of actions
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
currentStateIndex = liftedAction.nextLiftedState.length;
computedStates = [];
committedState = liftedAction.preloadedState;
minInvalidatedStateIndex = 0;
// iterate through actions
liftedAction.nextLiftedState.forEach(action => {
actionsById[nextActionId] = liftAction(action, options.shouldIncludeCallstack);
stagedActionIds.push(nextActionId);
nextActionId++;
});
} else {
// Completely replace everything.
({
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates
} = liftedAction.nextLiftedState);
if (liftedAction.noRecompute) {
minInvalidatedStateIndex = Infinity;
}
}
break;
}
case ActionTypes.LOCK_CHANGES: {
isLocked = liftedAction.status;
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.PAUSE_RECORDING: {
isPaused = liftedAction.status;
if (isPaused) {
return computePausedAction(true);
}
// Commit when unpausing
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
break;
}
default: {
// If the action is not recognized, it's a monitor action.
// Optimization: a monitor action can't change history.
minInvalidatedStateIndex = Infinity;
break;
}
}
}
computedStates = recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds,
options.shouldCatchErrors
);
monitorState = monitorReducer(monitorState, liftedAction);
return {
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates,
isLocked,
isPaused
};
};
}
/**
* Provides an app's view into the state of the lifted store.
*/
export function unliftState(liftedState) {
const { computedStates, currentStateIndex } = liftedState;
const { state } = computedStates[currentStateIndex];
return state;
}
/**
* Provides an app's view into the lifted store.
*/
export function unliftStore(liftedStore, liftReducer, options) {
let lastDefinedState;
const { shouldIncludeCallstack } = options;
function getState() {
const state = unliftState(liftedStore.getState());
if (state !== undefined) {
lastDefinedState = state;
}
return lastDefinedState;
}
return {
...liftedStore,
liftedStore,
dispatch(action) {
liftedStore.dispatch(liftAction(action, shouldIncludeCallstack));
return action;
},
getState,
replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer));
},
[$$observable]() {
return {
...liftedStore[$$observable](),
subscribe(observer) {
if (typeof observer !== 'object') {
throw new TypeError('Expected the observer to be an object.');
}
function observeState() {
if (observer.next) {
observer.next(getState());
}
}
observeState();
const unsubscribe = liftedStore.subscribe(observeState);
return { unsubscribe };
}
};
}
};
}
/**
* Redux instrumentation store enhancer.
*/
export default function instrument(monitorReducer = () => null, options = {}) {
if (typeof options.maxAge === 'number' && options.maxAge < 2) {
throw new Error(
'DevTools.instrument({ maxAge }) option, if specified, ' +
'may not be less than 2.'
);
}
return createStore => (reducer, initialState, enhancer) => {
function liftReducer(r) {
if (typeof r !== 'function') {
if (r && typeof r.default === 'function') {
throw new Error(
'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? ' +
'Try passing require(...).default instead.'
);
}
throw new Error('Expected the reducer to be a function.');
}
return liftReducerWith(r, initialState, monitorReducer, options);
}
const liftedStore = createStore(liftReducer(reducer), enhancer);
if (liftedStore.liftedStore) {
throw new Error(
'DevTools instrumentation should not be applied more than once. ' +
'Check your store configuration.'
);
}
return unliftStore(liftedStore, liftReducer, options);
};
}

View File

@ -0,0 +1,953 @@
import expect, { spyOn } from 'expect';
import { createStore, compose } from 'redux';
import instrument, { ActionCreators } from '../src/instrument';
import { Observable } from 'rxjs';
import _ from 'lodash';
import 'rxjs/add/observable/from';
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
}
function counterWithBug(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return mistake - 1; // eslint-disable-line no-undef
case 'SET_UNDEFINED': return undefined;
default: return state;
}
}
function counterWithAnotherBug(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return mistake + 1; // eslint-disable-line no-undef
case 'DECREMENT': return state - 1;
case 'SET_UNDEFINED': return undefined;
default: return state;
}
}
function doubleCounter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 2;
case 'DECREMENT': return state - 2;
default: return state;
}
}
function counterWithMultiply(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
case 'MULTIPLY': return state * 2;
default: return state;
}
}
describe('instrument', () => {
let store;
let liftedStore;
beforeEach(() => {
store = createStore(counter, instrument());
liftedStore = store.liftedStore;
});
it('should perform actions', () => {
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
});
it('should provide observable', () => {
let lastValue;
let calls = 0;
Observable.from(store)
.subscribe(state => {
lastValue = state;
calls++;
});
expect(lastValue).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(lastValue).toBe(1);
});
it('should rollback state to the last committed state', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(4);
liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2);
store.dispatch({ type: 'DECREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2);
});
it('should reset to initial state', () => {
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.reset());
expect(store.getState()).toBe(0);
});
it('should toggle an action', () => {
// actionId 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(1);
});
it('should set multiple action skip', () => {
// actionId 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(3);
liftedStore.dispatch(ActionCreators.setActionsActive(1, 3, false));
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.setActionsActive(0, 2, true));
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.setActionsActive(0, 1, true));
expect(store.getState()).toBe(2);
});
it('should sweep disabled actions', () => {
// actionId 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
expect(liftedStore.getState().stagedActionIds).toEqual([0, 1, 2, 3, 4]);
expect(liftedStore.getState().skippedActionIds).toEqual([]);
liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(3);
expect(liftedStore.getState().stagedActionIds).toEqual([0, 1, 2, 3, 4]);
expect(liftedStore.getState().skippedActionIds).toEqual([2]);
liftedStore.dispatch(ActionCreators.sweep());
expect(store.getState()).toBe(3);
expect(liftedStore.getState().stagedActionIds).toEqual([0, 1, 3, 4]);
expect(liftedStore.getState().skippedActionIds).toEqual([]);
});
it('should jump to state', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToState(0));
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.jumpToState(1));
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToState(2));
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.jumpToState(4));
expect(store.getState()).toBe(2);
});
it('should jump to action', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToAction(0));
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.jumpToAction(1));
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToAction(10));
expect(store.getState()).toBe(1);
});
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([0, 1, 2, 3, 4]);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.reorderAction(4, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 1, 2, 3]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(4, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 1, 2, 3]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(4, 2));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 4, 2, 3]);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.reorderAction(1, 10));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 2, 3, 1]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(10, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 2, 3, 1]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(1, -2));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 4, 2, 3]);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.reorderAction(0, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 4, 2, 3]);
expect(store.getState()).toBe(2);
});
it('should replace the reducer', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
store.replaceReducer(doubleCounter);
expect(store.getState()).toBe(2);
});
it('should replace the reducer without recomputing actions', () => {
store = createStore(counter, instrument(undefined, { shouldHotReload: false }));
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
store.replaceReducer(doubleCounter);
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(3);
store.replaceReducer(() => ({ test: true }));
expect(store.getState()).toEqual({ test: true });
});
it('should catch and record errors', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { shouldCatchErrors: true })
);
storeWithBug.dispatch({ type: 'INCREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
let { computedStates } = storeWithBug.liftedStore.getState();
expect(computedStates[2].error).toMatch(
/ReferenceError/
);
expect(computedStates[3].error).toMatch(
/Interrupted by an error up the chain/
);
expect(spy.calls[0].arguments[0].toString()).toMatch(
/ReferenceError/
);
spy.restore();
});
it('should catch invalid action type', () => {
expect(() => {
store.dispatch({ type: undefined });
}).toThrow(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
);
});
it('should catch invalid action type', () => {
function ActionClass() {
this.type = 'test';
}
expect(() => {
store.dispatch(new ActionClass());
}).toThrow(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
);
});
it('should return the last non-undefined state from getState', () => {
let storeWithBug = createStore(counterWithBug, instrument());
storeWithBug.dispatch({ type: 'INCREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
expect(storeWithBug.getState()).toBe(2);
storeWithBug.dispatch({ type: 'SET_UNDEFINED' });
expect(storeWithBug.getState()).toBe(2);
});
it('should not recompute states on every action', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
});
it('should not recompute old states when toggling an action', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
let monitoredLiftedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1);
// actionId 0 = @@INIT
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(5);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(6);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(8);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(1));
expect(reducerCalls).toBe(10);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(11);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(11);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(1));
expect(reducerCalls).toBe(12);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(13);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(15);
});
it('should not recompute states when jumping to state', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
let monitoredLiftedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
let savedComputedStates = monitoredLiftedStore.getState().computedStates;
monitoredLiftedStore.dispatch(ActionCreators.jumpToState(0));
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.jumpToState(1));
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.jumpToState(3));
expect(reducerCalls).toBe(4);
expect(monitoredLiftedStore.getState().computedStates).toBe(savedComputedStates);
});
it('should not recompute states on monitor actions', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
let monitoredLiftedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
let savedComputedStates = monitoredLiftedStore.getState().computedStates;
monitoredLiftedStore.dispatch({ type: 'lol' });
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch({ type: 'wat' });
expect(reducerCalls).toBe(4);
expect(monitoredLiftedStore.getState().computedStates).toBe(savedComputedStates);
});
describe('maxAge option', () => {
let configuredStore;
let configuredLiftedStore;
beforeEach(() => {
configuredStore = createStore(counter, instrument(undefined, { maxAge: 3 }));
configuredLiftedStore = configuredStore.liftedStore;
});
it('should auto-commit earliest non-@@INIT action when maxAge is reached', () => {
configuredStore.dispatch({ type: 'INCREMENT' });
configuredStore.dispatch({ type: 'INCREMENT' });
let liftedStoreState = configuredLiftedStore.getState();
expect(configuredStore.getState()).toBe(2);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.committedState).toBe(undefined);
expect(liftedStoreState.stagedActionIds).toInclude(1);
// Trigger auto-commit.
configuredStore.dispatch({ type: 'INCREMENT' });
liftedStoreState = configuredLiftedStore.getState();
expect(configuredStore.getState()).toBe(3);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(1);
expect(liftedStoreState.committedState).toBe(1);
expect(liftedStoreState.currentStateIndex).toBe(2);
});
it('should remove skipped actions once committed', () => {
configuredStore.dispatch({ type: 'INCREMENT' });
configuredLiftedStore.dispatch(ActionCreators.toggleAction(1));
configuredStore.dispatch({ type: 'INCREMENT' });
expect(configuredLiftedStore.getState().skippedActionIds).toInclude(1);
configuredStore.dispatch({ type: 'INCREMENT' });
expect(configuredLiftedStore.getState().skippedActionIds).toExclude(1);
});
it('should not auto-commit errors', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3);
storeWithBug.dispatch({ type: 'INCREMENT' });
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(4);
spy.restore();
});
it('should auto-commit actions after hot reload fixes error', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let 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);
// Auto-commit 2 actions by "fixing" reducer bug, but introducing another.
storeWithBug.replaceReducer(counterWithAnotherBug);
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(5);
// Auto-commit 2 more actions by "fixing" other reducer bug.
storeWithBug.replaceReducer(counter);
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3);
spy.restore();
});
it('should update currentStateIndex when auto-committing', () => {
let liftedStoreState;
let currentComputedState;
configuredStore.dispatch({ type: 'INCREMENT' });
configuredStore.dispatch({ type: 'INCREMENT' });
liftedStoreState = configuredLiftedStore.getState();
expect(liftedStoreState.currentStateIndex).toBe(2);
// currentStateIndex should stay at 2 as actions are committed.
configuredStore.dispatch({ type: 'INCREMENT' });
liftedStoreState = configuredLiftedStore.getState();
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 = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let 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 = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(4);
expect(currentComputedState.state).toBe(0);
expect(currentComputedState.error).toExist();
spy.restore();
});
it('should adjust currentStateIndex correctly when multiple actions are committed', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
// Auto-commit 2 actions by "fixing" reducer bug.
storeWithBug.replaceReducer(counter);
let liftedStoreState = liftedStoreWithBug.getState();
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(2);
expect(currentComputedState.state).toBe(-4);
spy.restore();
});
it('should not allow currentStateIndex to drop below 0', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
liftedStoreWithBug.dispatch(ActionCreators.jumpToState(1));
// Auto-commit 2 actions by "fixing" reducer bug.
storeWithBug.replaceReducer(counter);
let liftedStoreState = liftedStoreWithBug.getState();
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(0);
expect(currentComputedState.state).toBe(-2);
spy.restore();
});
it('should use dynamic maxAge', () => {
let max = 3;
const getMaxAge = expect.createSpy().andCall(() => max);
store = createStore(counter, instrument(undefined, { maxAge: getMaxAge }));
expect(getMaxAge.calls.length).toEqual(1);
store.dispatch({ type: 'INCREMENT' });
expect(getMaxAge.calls.length).toEqual(2);
store.dispatch({ type: 'INCREMENT' });
expect(getMaxAge.calls.length).toEqual(3);
let liftedStoreState = store.liftedStore.getState();
expect(getMaxAge.calls[0].arguments[0].type).toInclude('INIT');
expect(getMaxAge.calls[0].arguments[1]).toBe(undefined);
expect(getMaxAge.calls[1].arguments[0].type).toBe('PERFORM_ACTION');
expect(getMaxAge.calls[1].arguments[1].nextActionId).toBe(1);
expect(getMaxAge.calls[1].arguments[1].stagedActionIds).toEqual([0]);
expect(getMaxAge.calls[2].arguments[1].nextActionId).toBe(2);
expect(getMaxAge.calls[2].arguments[1].stagedActionIds).toEqual([0, 1]);
expect(store.getState()).toBe(2);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.committedState).toBe(undefined);
expect(liftedStoreState.stagedActionIds).toInclude(1);
// Trigger auto-commit.
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(3);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(1);
expect(liftedStoreState.committedState).toBe(1);
expect(liftedStoreState.currentStateIndex).toBe(2);
max = 4;
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(4);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(4);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(1);
expect(liftedStoreState.committedState).toBe(1);
expect(liftedStoreState.currentStateIndex).toBe(3);
max = 3;
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(5);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(3);
expect(liftedStoreState.committedState).toBe(3);
expect(liftedStoreState.currentStateIndex).toBe(2);
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(6);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(4);
expect(liftedStoreState.committedState).toBe(4);
expect(liftedStoreState.currentStateIndex).toBe(2);
});
it('should throw error when maxAge < 2', () => {
expect(() => {
createStore(counter, instrument(undefined, { maxAge: 1 }));
}).toThrow(/may not be less than 2/);
});
});
describe('Import State', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;
beforeEach(() => {
monitoredStore = createStore(counter, instrument());
monitoredLiftedStore = monitoredStore.liftedStore;
// Set up state to export
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
});
it('should replay all the steps when a state is imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredLiftedStore.dispatch(ActionCreators.importState(exportedState));
expect(importMonitoredLiftedStore.getState()).toEqual(exportedState);
});
it('should replace the existing action log with the one imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(exportedState));
expect(importMonitoredLiftedStore.getState()).toEqual(exportedState);
});
it('should allow for state to be imported without replaying actions', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
let noComputedExportedState = Object.assign({}, exportedState);
delete noComputedExportedState.computedStates;
importMonitoredLiftedStore.dispatch(ActionCreators.importState(noComputedExportedState, true));
let expectedImportedState = Object.assign({}, noComputedExportedState, {
computedStates: undefined
});
expect(importMonitoredLiftedStore.getState()).toEqual(expectedImportedState);
});
it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
let 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[1].stack).toBeA('string');
importMonitoredLiftedStore.dispatch(ActionCreators.importState(oldState));
expect(importMonitoredLiftedStore.getState()).toEqual(oldState);
expect(importMonitoredLiftedStore.getState().actionsById[0].stack).toBe(undefined);
expect(importMonitoredLiftedStore.getState().actionsById[1]).toEqual(oldState.actionsById[1]);
});
});
function filterStackAndTimestamps(state) {
state.actionsById = _.mapValues(state.actionsById, (action) => {
delete action.timestamp;
delete action.stack;
return action;
});
return state;
}
describe('Import Actions', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;
let savedActions = [
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'INCREMENT' }
];
beforeEach(() => {
monitoredStore = createStore(counter, instrument());
monitoredLiftedStore = monitoredStore.liftedStore;
// Pass actions through component
savedActions.forEach(action => monitoredStore.dispatch(action));
// get the final state
exportedState = filterStackAndTimestamps(monitoredLiftedStore.getState());
});
it('should replay all the steps when a state is imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredLiftedStore.dispatch(ActionCreators.importState(savedActions));
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
it('should replace the existing action log with the one imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(savedActions));
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(savedActions));
expect(importMonitoredLiftedStore.getState().actionsById[0].stack).toBe(undefined);
expect(importMonitoredLiftedStore.getState().actionsById[1].stack).toBeA('string');
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
});
describe('Lock Changes', () => {
it('should lock', () => {
store.dispatch({ type: 'INCREMENT' });
store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: true });
expect(store.liftedStore.getState().isLocked).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(2);
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(2);
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.toggleAction(1));
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.toggleAction(1));
expect(store.getState()).toBe(1);
store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: false });
expect(store.liftedStore.getState().isLocked).toBe(false);
expect(store.liftedStore.getState().nextActionId).toBe(2);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);
});
it('should start locked', () => {
store = createStore(counter, instrument(undefined, { shouldStartLocked: true }));
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().isLocked).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.getState()).toBe(0);
const savedActions = [{ type: 'INCREMENT' }, { type: 'INCREMENT' }];
store.liftedStore.dispatch(ActionCreators.importState(savedActions));
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: false });
expect(store.liftedStore.getState().isLocked).toBe(false);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(4);
expect(store.getState()).toBe(3);
});
});
describe('Pause recording', () => {
it('should pause', () => {
expect(store.liftedStore.getState().isPaused).toBe(false);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.pauseRecording(true));
expect(store.liftedStore.getState().isPaused).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.liftedStore.getState().actionsById[0].action).toEqual({ type: '@@INIT' });
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.liftedStore.getState().actionsById[0].action).toEqual({ type: '@@INIT' });
expect(store.getState()).toBe(4);
store.liftedStore.dispatch(ActionCreators.pauseRecording(false));
expect(store.liftedStore.getState().isPaused).toBe(false);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.liftedStore.getState().actionsById[2].action).toEqual({ type: 'INCREMENT' });
expect(store.getState()).toBe(6);
});
it('should maintain the history while paused', () => {
store = createStore(counter, instrument(undefined, { pauseActionType: '@@PAUSED' }));
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.liftedStore.getState().isPaused).toBe(false);
store.liftedStore.dispatch(ActionCreators.pauseRecording(true));
expect(store.liftedStore.getState().isPaused).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(4);
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(4);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(4);
expect(store.getState()).toBe(4);
store.liftedStore.dispatch(ActionCreators.pauseRecording(false));
expect(store.liftedStore.getState().isPaused).toBe(false);
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.getState()).toBe(4);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(2);
expect(store.getState()).toBe(5);
store.liftedStore.dispatch(ActionCreators.commit());
store.liftedStore.dispatch(ActionCreators.pauseRecording(true));
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.getState()).toBe(6);
});
});
it('throws if reducer is not a function', () => {
expect(() =>
createStore(undefined, 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(
'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? ' +
'Try passing require(...).default instead.'
);
});
it('throws if there are more than one instrument enhancer included', () => {
expect(() => {
createStore(counter, compose(instrument(), instrument()));
}).toThrow(
'DevTools instrumentation should not be applied more than once. ' +
'Check your store configuration.'
);
});
});

View File

@ -0,0 +1,4 @@
lib
**/node_modules
**/webpack.config.js
examples/**/server.js

View File

@ -0,0 +1,20 @@
{
"extends": "eslint-config-airbnb",
"env": {
"browser": true,
"mocha": true,
"node": true
},
"rules": {
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"no-console": 0,
// Temporarily disabled due to babel-eslint issues:
"block-scoped-var": 0,
"padded-blocks": 0,
},
"plugins": [
"react"
]
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Dan Abramov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,131 @@
Redux DevTools
=========================
A live-editing time travel environment for [Redux](https://github.com/reactjs/redux).
**[See Dan's React Europe talk demoing it!](http://youtube.com/watch?v=xsSnOQynTHs)**
> Note that the implemention in this repository is different from [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension). Please refer to the latter for browser extension.
### Table of Contents
- [Features](#features)
- [Overview](#overview)
- [Browser Extension](#browser-extension)
- [Setup Instructions](#setup-instructions)
- [Custom Monitors](#custom-monitors)
- [License](#license)
[![build status](https://img.shields.io/travis/reduxjs/redux-devtools/master.svg?style=flat-square)](https://travis-ci.org/reduxjs/redux-devtools)
[![npm version](https://img.shields.io/npm/v/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
[![npm downloads](https://img.shields.io/npm/dm/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
[![redux channel on discord](https://img.shields.io/badge/discord-redux@reactiflux-738bd7.svg?style=flat-square)](https://discord.gg/0ZcbPKXt5bWb10Ma)
![](http://i.imgur.com/J4GeW0M.gif)
### Features
* Lets you inspect every state and action payload
* Lets you go back in time by “cancelling” actions
* If you change the reducer code, each “staged” action will be re-evaluated
* If the reducers throw, you will see during which action this happened, and what the error was
* With `persistState()` store enhancer, you can persist debug sessions across page reloads
### Overview
Redux DevTools is a development time package that provides power-ups for your Redux development workflow. Be careful to strip its code in production (see [walkthrough](./docs/Walkthrough.md) for instructions)! To use Redux DevTools, you need to choose a “monitor”—a React component that will serve as a UI for the DevTools. Different tasks and workflows require different UIs, so Redux DevTools is built to be flexible in this regard. We recommend using [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) for inspecting the state and time travel, and wrap it in a [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) to quickly move it across the screen. That said, when youre comfortable rolling up your own setup, feel free to do this, and share it with us.
If you came here looking for what do the “Reset”, “Revert”, “Sweep” or “Commit” buttons do, check out [the `LogMonitor` documentation](https://github.com/gaearon/redux-devtools-log-monitor/blob/master/README.md#features).
### Browser Extension
If you dont want to bother with installing Redux DevTools and integrating it into your project, consider using [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) for Chrome and Firefox. It provides access to the most popular monitors, is easy to configure to filter actions, and doesnt require installing any packages.
### Setup Instructions
Read the installation [walkthrough](./docs/Walkthrough.md) for integration instructions and usage examples (`<DevTools>` component, `DevTools.instrument()`, exclude from production builds, gotchas).
### Running Examples
Clone the project:
```
git clone https://github.com/reduxjs/redux-devtools.git
cd redux-devtools/packages/redux-devtools
```
Run `npm install` in the package folder:
```
npm install
```
Now you can open an example folder and run `npm install` there:
```
cd examples/counter # or examples/todomvc
npm install
```
Finally, run the development server and open the page:
```
npm start
open http://localhost:3000
```
Try clicking on actions in the log, or changing some code inside the reducers. You should see the action log re-evaluate the state on every code change.
Also try opening `http://localhost:3000/?debug_session=123`, click around, and then refresh. You should see that all actions have been restored from the local storage.
### Custom Monitors
**DevTools accepts monitor components so you can build a completely custom UI.** [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) and [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) are just examples of what is possible.
**[I challenge you to build a custom monitor for Redux DevTools!](https://github.com/reduxjs/redux-devtools/issues/3)**
Some crazy ideas for custom monitors:
* A slider that lets you jump between computed states just by dragging it
* An in-app layer that shows the last N states right in the app (e.g. for animation)
* A time machine like interface where the last N states of your app reside on different Z layers
* Feel free to come up with and implement your own! Check [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) `propTypes` to see what you can do.
In fact some of these are implemented already:
#### [Slider Monitor](https://github.com/calesce/redux-slider-monitor)
![](https://camo.githubusercontent.com/47a3f427c9d2e0c763b74e33417b3001fe8604b6/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f662e636c2e6c792f6974656d732f3149335032323243334e3252314d3279314b33622f53637265656e2532305265636f7264696e67253230323031352d31322d3232253230617425323030372e3230253230504d2e6769663f763d3162363236376537)
#### [Inspector](https://github.com/alexkuz/redux-devtools-inspector)
![](http://i.imgur.com/fYh8fk5.gif)
#### [Diff Monitor](https://github.com/whetstone/redux-devtools-diff-monitor)
![](https://camo.githubusercontent.com/c2c0ba1ad82d003b5386404ae09c00763d73510c/687474703a2f2f692e696d6775722e636f6d2f72764352394f512e706e67)
#### [Filterable Log Monitor](https://github.com/bvaughn/redux-devtools-filterable-log-monitor/)
![redux-devtools-filterable-log-monitor](https://cloud.githubusercontent.com/assets/29597/12440009/182bb31c-beec-11e5-8fd0-bdda48e646b2.gif)
#### [Chart Monitor](https://github.com/romseguy/redux-devtools-chart-monitor)
![redux-devtools-chart-monitor](http://i.imgur.com/MSgvU6l.gif)
#### [Filter Actions](https://github.com/zalmoxisus/redux-devtools-filter-actions)
(Does not have a UI but can wrap any other monitor)
<img src='http://i.imgur.com/TlqnU0J.png' width='400'>
#### [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch)
![redux-devtools-dispatch](https://cloud.githubusercontent.com/assets/969003/12874321/2c3624ec-cdd2-11e5-9856-fd7e24efb8d5.gif)
#### Keep them coming!
Create a PR to add your custom monitor.
### License
MIT

View File

@ -5,13 +5,13 @@
First, clone the project:
```
git clone https://github.com/gaearon/redux-devtools.git
git clone https://github.com/reduxjs/redux-devtools.git
```
Then install the dependencies in the root folder:
Then install the dependencies in the package folder:
```
cd redux-devtools
cd redux-devtools/packages/redux-devtools
npm install
```

View File

@ -8,13 +8,13 @@
},
"repository": {
"type": "git",
"url": "https://github.com/gaearon/redux-devtools.git"
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/gaearon/redux-devtools/issues"
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/gaearon/redux-devtools#readme",
"homepage": "https://github.com/reduxjs/redux-devtools#readme",
"dependencies": {
"prop-types": "^15.5.7",
"react": "^15.3.0",

View File

@ -5,13 +5,13 @@
First, clone the project:
```
git clone https://github.com/gaearon/redux-devtools.git
git clone https://github.com/reduxjs/redux-devtools.git
```
Then install the dependencies in the root folder:
Then install the dependencies in the package folder:
```
cd redux-devtools
cd redux-devtools/packages/redux-devtools
npm install
```

View File

@ -8,7 +8,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/gaearon/redux-devtools.git"
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"keywords": [
"react",
@ -24,9 +24,9 @@
],
"license": "MIT",
"bugs": {
"url": "https://github.com/gaearon/redux-devtools/issues"
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/gaearon/redux-devtools#readme",
"homepage": "https://github.com/reduxjs/redux-devtools#readme",
"dependencies": {
"classnames": "^2.1.2",
"prop-types": "^15.5.7",

View File

@ -0,0 +1,73 @@
{
"name": "redux-devtools",
"version": "3.4.2",
"description": "Redux DevTools with hot reloading and time travel",
"main": "lib/index.js",
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"lint": "eslint src test examples",
"test": "cross-env NODE_ENV=test mocha --compilers js:babel-core/register --recursive",
"test:watch": "cross-env NODE_ENV=test mocha --compilers js:babel-core/register --recursive --watch",
"test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive",
"prepare": "npm run build",
"prepublishOnly": "npm run lint && 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",
"flux",
"hot reloading",
"time travel",
"live edit"
],
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)",
"license": "MIT",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-core": "^6.3.17",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-preset-es2015-loose": "^6.1.3",
"babel-preset-react": "6.3.13",
"babel-preset-stage-0": "^6.3.13",
"cross-env": "3.1.3",
"eslint": "^0.23",
"eslint-config-airbnb": "0.0.6",
"eslint-plugin-react": "^2.3.0",
"expect": "^1.6.0",
"isparta": "^3.0.3",
"jsdom": "^6.5.1",
"mocha": "^2.2.5",
"mocha-jsdom": "^1.0.0",
"react": "^0.14.9",
"react-addons-test-utils": "^0.14.0",
"react-dom": "^0.14.0",
"react-redux": "^4.0.0",
"redux": "^3.5.2",
"rimraf": "^2.3.4",
"webpack": "^1.11.0"
},
"peerDependencies": {
"react": "^0.14.9 || ^15.3.0 || ^16.0.0",
"react-redux": "^4.0.0 || ^5.0.0",
"redux": "^3.5.2 || ^4.0.0"
},
"dependencies": {
"prop-types": "^15.5.7",
"lodash": "^4.2.0",
"redux-devtools-instrument": "^1.9.0"
}
}

7848
yarn.lock Normal file

File diff suppressed because it is too large Load Diff