diff --git a/.eslintignore b/.eslintignore index c66db4fe..4802ed41 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ coverage node_modules __snapshots__ .yarn/* +storybook-static diff --git a/.prettierignore b/.prettierignore index 79ce839b..bf257467 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,4 @@ dev .yarn/* .pnp.* **/demo/public/** +storybook-static diff --git a/extension/src/app/api/filters.ts b/extension/src/app/api/filters.ts index 9f15b9e7..b0d44d45 100644 --- a/extension/src/app/api/filters.ts +++ b/extension/src/app/api/filters.ts @@ -1,7 +1,7 @@ import mapValues from 'lodash/mapValues'; -import { Config } from '../../browser/extension/inject/pageScript'; import { Action } from 'redux'; import { LiftedState, PerformAction } from '@redux-devtools/instrument'; +import { LocalFilter } from '@redux-devtools/utils/lib/filters'; export type FilterStateValue = | 'DO_NOT_FILTER' @@ -14,27 +14,6 @@ export const FilterState: { [K in FilterStateValue]: FilterStateValue } = { ALLOWLIST_SPECIFIC: 'ALLOWLIST_SPECIFIC', }; -function isArray(arg: unknown): arg is readonly unknown[] { - return Array.isArray(arg); -} - -interface LocalFilter { - readonly allowlist: string | undefined; - readonly denylist: string | undefined; -} - -export function getLocalFilter(config: Config): LocalFilter | undefined { - const denylist = config.actionsDenylist ?? config.actionsBlacklist; - const allowlist = config.actionsAllowlist ?? config.actionsWhitelist; - if (denylist || allowlist) { - return { - allowlist: isArray(allowlist) ? allowlist.join('|') : allowlist, - denylist: isArray(denylist) ? denylist.join('|') : denylist, - }; - } - return undefined; -} - export const noFiltersApplied = (localFilter: LocalFilter | undefined) => // !predicate && !localFilter && diff --git a/extension/src/app/api/index.ts b/extension/src/app/api/index.ts index 311ffd63..4128de21 100644 --- a/extension/src/app/api/index.ts +++ b/extension/src/app/api/index.ts @@ -2,7 +2,8 @@ import jsan, { Options } from 'jsan'; import throttle from 'lodash/throttle'; import serializeImmutable from '@redux-devtools/serialize/lib/immutable/serialize'; import { getActionsArray } from '@redux-devtools/utils'; -import { getLocalFilter, isFiltered, PartialLiftedState } from './filters'; +import { getLocalFilter } from '@redux-devtools/utils/lib/filters'; +import { isFiltered, PartialLiftedState } from './filters'; import importState from './importState'; import generateId from './generateInstanceId'; import { Config } from '../../browser/extension/inject/pageScript'; diff --git a/extension/src/browser/extension/inject/pageScript.ts b/extension/src/browser/extension/inject/pageScript.ts index e3c9fa7a..33808c23 100644 --- a/extension/src/browser/extension/inject/pageScript.ts +++ b/extension/src/browser/extension/inject/pageScript.ts @@ -22,7 +22,6 @@ import { isAllowed, Options } from '../options/syncOptions'; import Monitor from '../../../app/service/Monitor'; import { noFiltersApplied, - getLocalFilter, isFiltered, filterState, startingFrom, @@ -52,6 +51,7 @@ import { } from '@redux-devtools/app/lib/actions'; import { ContentScriptToPageScriptMessage } from './contentScript'; import { Features } from '@redux-devtools/app/lib/reducers/instances'; +import { getLocalFilter } from '@redux-devtools/utils/lib/filters'; type EnhancedStoreWithInitialDispatch< S, diff --git a/packages/redux-devtools-remote/.babelrc b/packages/redux-devtools-remote/.babelrc new file mode 100644 index 00000000..3313ff9e --- /dev/null +++ b/packages/redux-devtools-remote/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-typescript"] +} diff --git a/packages/redux-devtools-remote/.eslintignore b/packages/redux-devtools-remote/.eslintignore new file mode 100644 index 00000000..be897dba --- /dev/null +++ b/packages/redux-devtools-remote/.eslintignore @@ -0,0 +1,2 @@ +examples +lib diff --git a/packages/redux-devtools-remote/.eslintrc.js b/packages/redux-devtools-remote/.eslintrc.js new file mode 100644 index 00000000..090f9e70 --- /dev/null +++ b/packages/redux-devtools-remote/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: '../../eslintrc.ts.base.json', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, +}; diff --git a/packages/redux-devtools-remote/LICENSE.md b/packages/redux-devtools-remote/LICENSE.md new file mode 100644 index 00000000..b410d654 --- /dev/null +++ b/packages/redux-devtools-remote/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Mihail Diordiev + +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. diff --git a/packages/redux-devtools-remote/README.md b/packages/redux-devtools-remote/README.md new file mode 100644 index 00000000..8965858d --- /dev/null +++ b/packages/redux-devtools-remote/README.md @@ -0,0 +1,199 @@ +# Remote Redux DevTools + +![Demo](demo.gif) + +Use [Redux DevTools](https://github.com/reduxjs/redux-devtools) remotely for React Native, hybrid, desktop and server side Redux apps. + +### Installation + +``` +yarn add @redux-devtools/remote +``` + +### Usage + +There are 2 ways of usage depending if you're using other store enhancers (middlewares) or not. + +#### Add DevTools enhancer to your store + +If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as described in the official [redux-docs](http://redux.js.org/index.html), simply replace: + +```javascript +import { createStore } from 'redux'; +const store = createStore(reducer); +``` + +with + +```javascript +import { createStore } from 'redux'; +import devToolsEnhancer from '@redux-devtools/remote'; +const store = createStore(reducer, devToolsEnhancer()); +// or const store = createStore(reducer, preloadedState, devToolsEnhancer()); +``` + +> Note: passing enhancer as last argument requires redux@>=3.1.0 + +#### When to use DevTools compose helper + +If you setup your store with [middlewares and enhancers](http://redux.js.org/docs/api/applyMiddleware.html) like [redux-saga](https://github.com/redux-saga/redux-saga) and similar, it is crucial to use `composeWithDevTools` export. Otherwise, actions dispatched from Redux DevTools will not flow to your middlewares. + +In that case change this: + +```javascript +import { createStore, applyMiddleware, compose } from 'redux'; + +const store = createStore( + reducer, + preloadedState, + compose( + applyMiddleware(...middleware) + // other store enhancers if any + ) +); +``` + +to: + +```javascript +import { createStore, applyMiddleware } from 'redux'; +import { composeWithDevTools } from '@redux-devtools/remote'; + +const store = createStore( + reducer, + /* preloadedState, */ composeWithDevTools( + applyMiddleware(...middleware) + // other store enhancers if any + ) +); +``` + +or with devTools' options: + +```javascript +import { createStore, applyMiddleware } from 'redux'; +import { composeWithDevTools } from '@redux-devtools/remote'; + +const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); +const store = createStore( + reducer, + /* preloadedState, */ composeEnhancers( + applyMiddleware(...middleware) + // other store enhancers if any + ) +); +``` + +### Enabling + +In order not to allow it in production by default, the enhancer will have effect only when `process.env.NODE_ENV === 'development'`. + +For Webpack you should add it as following (`webpack.config.dev.js`): + +```js +// ... +plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development') + }) +], +// ... +``` + +In case you don't set `NODE_ENV`, you can set `realtime` parameter to `true` or to other global variable to turn it off in production: + +```js +const store = createStore(reducer, devToolsEnhancer({ realtime: true })); +``` + +### Monitoring + +Use one of our monitor apps to inspect and dispatch actions: + +- [redux-devtools-extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) - Click "Remote" button (or press [`Cmd+Ctrl+Arrow up`](https://github.com/zalmoxisus/redux-devtools-extension#keyboard-shortcuts)) to open remote monitoring. +- [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger) - Used in React Native debugger as a dock monitor. +- [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor. +- [redux-dispatch-cli](https://github.com/jhen0409/redux-dispatch-cli) - A CLI tool for Redux remote dispatch. +- [vscode-redux-devtools](https://github.com/jkzing/vscode-redux-devtools) - Used in Visual Studio Code. + +Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) to create your own monitor app. + +### Communicate via local server + +Use [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). +You can import it in your `server.js` script and start remotedev server together with your development server: + +```js +var reduxDevTools = require('@redux-devtools/cli'); +reduxDevTools({ hostname: 'localhost', port: 8000 }); +``` + +See [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli) for more details. +For React Native you can use [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger), which already include `@redux-devtools/cli`. + +### Parameters + +| Name | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | _String_ representing the instance name to be shown on the remote monitor. | +| `realtime` | _Boolean_ specifies whether to allow remote monitoring. By default is `process.env.NODE_ENV === 'development'`. | +| `hostname` | _String_ used to specify host for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). If `port` is specified, default value is `localhost`. | +| `port` | _Number_ used to specify host's port for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). | +| `secure` | _Boolean_ specifies whether to use `https` protocol for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). | +| `maxAge` | _Number_ of maximum allowed actions to be stored on the history tree, the oldest actions are removed once maxAge is reached. Default is `30`. | +| `actionsBlacklist` | _array_ of actions to be hidden in DevTools. Overwrites corresponding global setting in the options page. See the example bellow. | +| `actionsWhitelist` | _array_ of actions to be shown. All other actions will be hidden in DevTools. | +| `actionSanitizer` | _Function_ which takes action object and id number as arguments, and should return action object back. See the example bellow. | +| `stateSanitizer` | _Function_ which takes state object and index as arguments, and should return state object back. See the example bellow. | +| `startOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should start remote monitoring (when `realtime` is `false`). | +| `stopOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should stop remote monitoring. | +| `sendOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should trigger sending the history to the monitor (without starting it). _Note_: when using it, add a `fetch` polyfill if needed. | +| `sendOnError` | _Numeric_ code: `0` - disabled (default), `1` - send all uncaught exception messages, `2` - send only reducers error messages. | +| `sendTo` | _String_ url of the monitor to send the history when `sendOn` is triggered. By default is `${secure ? 'https' : 'http'}://${hostname}:${port}`. | +| `actionCreators` | _Array_ or _Object_ of action creators to dispatch remotely. See [the example](https://github.com/zalmoxisus/remote-redux-devtools/commit/b54652930dfd4e057991df8471c343957fd7bff7). | +| `shouldHotReload` | _Boolean_ - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`. | +| `shouldRecordChanges` | _Boolean_ - if specified as `false`, it will not record the changes till clicked on "Start recording" button on the monitor app. Default is `true`. | +| `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`. | +| `id` | _String_ to identify the instance when sending the history triggered by `sendOn`. You can use, for example, user id here, to know who sent the data. | +| `suppressConnectErrors` | _Boolean_ - if set to `false`, all socket errors thrown while trying to connect will be printed to the console, regardless of if they've been thrown before. This is primarily for suppressing `SocketProtocolError` errors, which get repeatedly thrown when trying to make a connection. Default is `true`. | + +All parameters are optional. You have to provide at least `port` property to use `localhost` instead of `remotedev.io` server. + +Example: + +```js +export default function configureStore(preloadedState) { + const store = createStore( + reducer, + preloadedState, + devToolsEnhancer({ + name: 'Android app', + realtime: true, + hostname: 'localhost', + port: 8000, + maxAge: 30, + actionsBlacklist: ['EFFECT_RESOLVED'], + actionSanitizer: (action) => + action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data + ? { ...action, data: '<>' } + : action, + stateSanitizer: (state) => + state.data ? { ...state, data: '<>' } : state, + }) + ); + return store; +} +``` + +### Demo + +- [Toggle monitoring](http://zalmoxisus.github.io/monitoring/) + +### Examples + +- [Web](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-remote/examples) +- [React Native](https://github.com/chentsulin/react-native-counter-ios-android) + +### License + +MIT diff --git a/packages/redux-devtools-remote/demo.gif b/packages/redux-devtools-remote/demo.gif new file mode 100644 index 00000000..3feae90f Binary files /dev/null and b/packages/redux-devtools-remote/demo.gif differ diff --git a/packages/redux-devtools-remote/examples/buildAll.js b/packages/redux-devtools-remote/examples/buildAll.js new file mode 100644 index 00000000..3a558760 --- /dev/null +++ b/packages/redux-devtools-remote/examples/buildAll.js @@ -0,0 +1,36 @@ +/** + * Runs an ordered set of commands within each of the build directories. + */ + +import fs from 'fs'; +import path from 'path'; +import { spawnSync } from 'child_process'; + +var exampleDirs = fs.readdirSync(__dirname).filter((file) => { + return fs.statSync(path.join(__dirname, file)).isDirectory(); +}); + +// Ordering is important here. `npm install` must come first. +var cmdArgs = [ + { cmd: 'npm', args: ['install'] }, + { cmd: 'webpack', args: ['index.js'] }, +]; + +for (const dir of exampleDirs) { + for (const cmdArg of cmdArgs) { + // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 + const opts = { + cwd: path.join(__dirname, dir), + stdio: 'inherit', + }; + let result = {}; + if (process.platform === 'win32') { + result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts); + } else { + result = spawnSync(cmdArg.cmd, cmdArg.args, opts); + } + if (result.status !== 0) { + throw new Error('Building examples exited with non-zero'); + } + } +} diff --git a/packages/redux-devtools-remote/examples/counter/.babelrc b/packages/redux-devtools-remote/examples/counter/.babelrc new file mode 100644 index 00000000..9b7d435a --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "stage-0", "react"] +} diff --git a/packages/redux-devtools-remote/examples/counter/actions/counter.js b/packages/redux-devtools-remote/examples/counter/actions/counter.js new file mode 100644 index 00000000..66b52d47 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/actions/counter.js @@ -0,0 +1,34 @@ +export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; +export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; + +export function increment() { + return { + type: INCREMENT_COUNTER, + }; +} + +export function decrement() { + return { + type: DECREMENT_COUNTER, + }; +} + +export function incrementIfOdd() { + return (dispatch, getState) => { + const { counter } = getState(); + + if (counter % 2 === 0) { + return; + } + + dispatch(increment()); + }; +} + +export function incrementAsync(delay = 1000) { + return (dispatch) => { + setTimeout(() => { + dispatch(increment()); + }, delay); + }; +} diff --git a/packages/redux-devtools-remote/examples/counter/components/Counter.js b/packages/redux-devtools-remote/examples/counter/components/Counter.js new file mode 100644 index 00000000..32cdce43 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/components/Counter.js @@ -0,0 +1,26 @@ +import React, { Component, PropTypes } from 'react'; + +class Counter extends Component { + render() { + const { increment, incrementIfOdd, incrementAsync, decrement, counter } = + this.props; + return ( +

+ Clicked: {counter} times {' '} + {' '} + {' '} + +

+ ); + } +} + +Counter.propTypes = { + increment: PropTypes.func.isRequired, + incrementIfOdd: PropTypes.func.isRequired, + incrementAsync: PropTypes.func.isRequired, + decrement: PropTypes.func.isRequired, + counter: PropTypes.number.isRequired, +}; + +export default Counter; diff --git a/packages/redux-devtools-remote/examples/counter/containers/App.js b/packages/redux-devtools-remote/examples/counter/containers/App.js new file mode 100644 index 00000000..47287ecf --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/containers/App.js @@ -0,0 +1,16 @@ +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import Counter from '../components/Counter'; +import * as CounterActions from '../actions/counter'; + +function mapStateToProps(state) { + return { + counter: state.counter, + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(CounterActions, dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(Counter); diff --git a/packages/redux-devtools-remote/examples/counter/index.html b/packages/redux-devtools-remote/examples/counter/index.html new file mode 100644 index 00000000..0f963144 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/index.html @@ -0,0 +1,10 @@ + + + + Redux counter example + + +
+ + + diff --git a/packages/redux-devtools-remote/examples/counter/index.js b/packages/redux-devtools-remote/examples/counter/index.js new file mode 100644 index 00000000..0ab7fd0f --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import App from './containers/App'; +import configureStore from './store/configureStore'; + +const store = configureStore(); + +render( + + + , + document.getElementById('root') +); diff --git a/packages/redux-devtools-remote/examples/counter/package.json b/packages/redux-devtools-remote/examples/counter/package.json new file mode 100644 index 00000000..382d6cd3 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/package.json @@ -0,0 +1,46 @@ +{ + "name": "redux-counter-example", + "version": "0.0.0", + "description": "Redux counter example", + "scripts": { + "start": "node server.js", + "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js", + "test:watch": "npm test -- --watch" + }, + "repository": { + "type": "git", + "url": "https://github.com/rackt/redux.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/rackt/redux/issues" + }, + "homepage": "http://rackt.github.io/redux", + "dependencies": { + "react": "^0.14.0", + "react-dom": "^0.14.0", + "react-redux": "^4.0.0", + "redux": "^3.5.2", + "redux-thunk": "^0.1.0" + }, + "devDependencies": { + "babel-core": "^6.3.15", + "babel-loader": "^6.2.0", + "babel-plugin-react-transform": "^2.0.0-beta1", + "babel-polyfill": "^6.3.14", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "expect": "^1.6.0", + "express": "^4.13.3", + "jsdom": "^5.6.1", + "mocha": "^2.2.5", + "node-libs-browser": "^0.5.2", + "react-addons-test-utils": "^0.14.0", + "react-transform-hmr": "^1.0.0", + "redux-immutable-state-invariant": "^1.1.1", + "webpack": "^1.13.1", + "webpack-dev-middleware": "^1.2.0", + "webpack-hot-middleware": "^2.2.0" + } +} diff --git a/packages/redux-devtools-remote/examples/counter/reducers/counter.js b/packages/redux-devtools-remote/examples/counter/reducers/counter.js new file mode 100644 index 00000000..c16e5d44 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/reducers/counter.js @@ -0,0 +1,12 @@ +import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'; + +export default function counter(state = 0, action) { + switch (action.type) { + case INCREMENT_COUNTER: + return state + 1; + case DECREMENT_COUNTER: + return state - 1; + default: + return state; + } +} diff --git a/packages/redux-devtools-remote/examples/counter/reducers/index.js b/packages/redux-devtools-remote/examples/counter/reducers/index.js new file mode 100644 index 00000000..eba07a15 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/reducers/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import counter from './counter'; + +const rootReducer = combineReducers({ + counter, +}); + +export default rootReducer; diff --git a/packages/redux-devtools-remote/examples/counter/server.js b/packages/redux-devtools-remote/examples/counter/server.js new file mode 100644 index 00000000..7e993c75 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/server.js @@ -0,0 +1,32 @@ +var webpack = require('webpack'); +var webpackDevMiddleware = require('webpack-dev-middleware'); +var webpackHotMiddleware = require('webpack-hot-middleware'); +var config = require('./webpack.config'); + +var app = new require('express')(); +var port = 4001; + +var compiler = webpack(config); +app.use( + webpackDevMiddleware(compiler, { + noInfo: true, + publicPath: config.output.publicPath, + }) +); +app.use(webpackHotMiddleware(compiler)); + +app.get('/', function (req, res) { + res.sendFile(__dirname + '/index.html'); +}); + +app.listen(port, function (error) { + if (error) { + console.error(error); + } else { + console.info( + '==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.', + port, + port + ); + } +}); diff --git a/packages/redux-devtools-remote/examples/counter/store/configureStore.js b/packages/redux-devtools-remote/examples/counter/store/configureStore.js new file mode 100644 index 00000000..753c95db --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/store/configureStore.js @@ -0,0 +1,29 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import invariant from 'redux-immutable-state-invariant'; +import { composeWithDevTools } from 'remote-redux-devtools'; +import reducer from '../reducers'; +import * as actionCreators from '../actions/counter'; + +export default function configureStore(initialState) { + const composeEnhancers = composeWithDevTools({ + realtime: true, + actionCreators, + trace: true, + }); + const store = createStore( + reducer, + initialState, + composeEnhancers(applyMiddleware(invariant(), thunk)) + ); + + if (module.hot) { + // Enable Webpack hot module replacement for reducers + module.hot.accept('../reducers', () => { + const nextReducer = require('../reducers').default; + store.replaceReducer(nextReducer); + }); + } + + return store; +} diff --git a/packages/redux-devtools-remote/examples/counter/test/actions/counter.spec.js b/packages/redux-devtools-remote/examples/counter/test/actions/counter.spec.js new file mode 100644 index 00000000..c65cc240 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/test/actions/counter.spec.js @@ -0,0 +1,73 @@ +import expect from 'expect'; +import { applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import * as actions from '../../actions/counter'; + +const middlewares = [thunk]; + +/* + * Creates a mock of Redux store with middleware. + */ +function mockStore(getState, expectedActions, onLastAction) { + if (!Array.isArray(expectedActions)) { + throw new Error('expectedActions should be an array of expected actions.'); + } + if ( + typeof onLastAction !== 'undefined' && + typeof onLastAction !== 'function' + ) { + throw new Error('onLastAction should either be undefined or function.'); + } + + function mockStoreWithoutMiddleware() { + return { + getState() { + return typeof getState === 'function' ? getState() : getState; + }, + + dispatch(action) { + const expectedAction = expectedActions.shift(); + expect(action).toEqual(expectedAction); + if (onLastAction && !expectedActions.length) { + onLastAction(); + } + return action; + }, + }; + } + + const mockStoreWithMiddleware = applyMiddleware(...middlewares)( + mockStoreWithoutMiddleware + ); + + return mockStoreWithMiddleware(); +} + +describe('actions', () => { + it('increment should create increment action', () => { + expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER }); + }); + + it('decrement should create decrement action', () => { + expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER }); + }); + + it('incrementIfOdd should create increment action', (done) => { + const expectedActions = [{ type: actions.INCREMENT_COUNTER }]; + const store = mockStore({ counter: 1 }, expectedActions, done); + store.dispatch(actions.incrementIfOdd()); + }); + + it('incrementIfOdd shouldnt create increment action if counter is even', (done) => { + const expectedActions = []; + const store = mockStore({ counter: 2 }, expectedActions); + store.dispatch(actions.incrementIfOdd()); + done(); + }); + + it('incrementAsync should create increment action', (done) => { + const expectedActions = [{ type: actions.INCREMENT_COUNTER }]; + const store = mockStore({ counter: 0 }, expectedActions, done); + store.dispatch(actions.incrementAsync(100)); + }); +}); diff --git a/packages/redux-devtools-remote/examples/counter/test/components/Counter.spec.js b/packages/redux-devtools-remote/examples/counter/test/components/Counter.spec.js new file mode 100644 index 00000000..85f0e163 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/test/components/Counter.spec.js @@ -0,0 +1,53 @@ +import expect from 'expect'; +import React from 'react'; +import TestUtils from 'react-addons-test-utils'; +import Counter from '../../components/Counter'; + +function setup() { + const actions = { + increment: expect.createSpy(), + incrementIfOdd: expect.createSpy(), + incrementAsync: expect.createSpy(), + decrement: expect.createSpy(), + }; + const component = TestUtils.renderIntoDocument( + + ); + return { + component: component, + actions: actions, + buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), + p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'), + }; +} + +describe('Counter component', () => { + it('should display count', () => { + const { p } = setup(); + expect(p.textContent).toMatch(/^Clicked: 1 times/); + }); + + it('first button should call increment', () => { + const { buttons, actions } = setup(); + TestUtils.Simulate.click(buttons[0]); + expect(actions.increment).toHaveBeenCalled(); + }); + + it('second button should call decrement', () => { + const { buttons, actions } = setup(); + TestUtils.Simulate.click(buttons[1]); + expect(actions.decrement).toHaveBeenCalled(); + }); + + it('third button should call incrementIfOdd', () => { + const { buttons, actions } = setup(); + TestUtils.Simulate.click(buttons[2]); + expect(actions.incrementIfOdd).toHaveBeenCalled(); + }); + + it('fourth button should call incrementAsync', () => { + const { buttons, actions } = setup(); + TestUtils.Simulate.click(buttons[3]); + expect(actions.incrementAsync).toHaveBeenCalled(); + }); +}); diff --git a/packages/redux-devtools-remote/examples/counter/test/containers/App.spec.js b/packages/redux-devtools-remote/examples/counter/test/containers/App.spec.js new file mode 100644 index 00000000..bfa4d73d --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/test/containers/App.spec.js @@ -0,0 +1,53 @@ +import expect from 'expect'; +import React from 'react'; +import TestUtils from 'react-addons-test-utils'; +import { Provider } from 'react-redux'; +import App from '../../containers/App'; +import configureStore from '../../store/configureStore'; + +function setup(initialState) { + const store = configureStore(initialState); + const app = TestUtils.renderIntoDocument( + + + + ); + return { + app: app, + buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'), + p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'), + }; +} + +describe('containers', () => { + describe('App', () => { + it('should display initial count', () => { + const { p } = setup(); + expect(p.textContent).toMatch(/^Clicked: 0 times/); + }); + + it('should display updated count after increment button click', () => { + const { buttons, p } = setup(); + TestUtils.Simulate.click(buttons[0]); + expect(p.textContent).toMatch(/^Clicked: 1 times/); + }); + + it('should display updated count after decrement button click', () => { + const { buttons, p } = setup(); + TestUtils.Simulate.click(buttons[1]); + expect(p.textContent).toMatch(/^Clicked: -1 times/); + }); + + it('shouldnt change if even and if odd button clicked', () => { + const { buttons, p } = setup(); + TestUtils.Simulate.click(buttons[2]); + expect(p.textContent).toMatch(/^Clicked: 0 times/); + }); + + it('should change if odd and if odd button clicked', () => { + const { buttons, p } = setup({ counter: 1 }); + TestUtils.Simulate.click(buttons[2]); + expect(p.textContent).toMatch(/^Clicked: 2 times/); + }); + }); +}); diff --git a/packages/redux-devtools-remote/examples/counter/test/reducers/counter.spec.js b/packages/redux-devtools-remote/examples/counter/test/reducers/counter.spec.js new file mode 100644 index 00000000..45861187 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/test/reducers/counter.spec.js @@ -0,0 +1,23 @@ +import expect from 'expect'; +import counter from '../../reducers/counter'; +import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../actions/counter'; + +describe('reducers', () => { + describe('counter', () => { + it('should handle initial state', () => { + expect(counter(undefined, {})).toBe(0); + }); + + it('should handle INCREMENT_COUNTER', () => { + expect(counter(1, { type: INCREMENT_COUNTER })).toBe(2); + }); + + it('should handle DECREMENT_COUNTER', () => { + expect(counter(1, { type: DECREMENT_COUNTER })).toBe(0); + }); + + it('should handle unknown action type', () => { + expect(counter(1, { type: 'unknown' })).toBe(1); + }); + }); +}); diff --git a/packages/redux-devtools-remote/examples/counter/test/setup.js b/packages/redux-devtools-remote/examples/counter/test/setup.js new file mode 100644 index 00000000..b4e5ab07 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/test/setup.js @@ -0,0 +1,5 @@ +import { jsdom } from 'jsdom'; + +global.document = jsdom(''); +global.window = document.defaultView; +global.navigator = global.window.navigator; diff --git a/packages/redux-devtools-remote/examples/counter/webpack.config.js b/packages/redux-devtools-remote/examples/counter/webpack.config.js new file mode 100644 index 00000000..4e8d0325 --- /dev/null +++ b/packages/redux-devtools-remote/examples/counter/webpack.config.js @@ -0,0 +1,41 @@ +var path = require('path'); +var webpack = require('webpack'); + +module.exports = { + devtool: 'source-map', + entry: ['webpack-hot-middleware/client', './index'], + output: { + path: path.join(__dirname, 'dist'), + filename: 'bundle.js', + publicPath: '/static/', + }, + plugins: [ + new webpack.optimize.OccurenceOrderPlugin(), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin(), + ], + module: { + loaders: [ + { + test: /\.js$/, + loaders: ['babel'], + exclude: /node_modules/, + include: __dirname, + }, + ], + }, +}; + +var src = path.join(__dirname, '..', '..', 'src'); +var nodeModules = path.join(__dirname, '..', '..', 'node_modules'); +var fs = require('fs'); +if (fs.existsSync(src) && fs.existsSync(nodeModules)) { + // Resolve to source + module.exports.resolve = { alias: { 'remote-redux-devtools': src } }; + // Compile from source + module.exports.module.loaders.push({ + test: /\.js$/, + loaders: ['babel'], + include: src, + }); +} diff --git a/packages/redux-devtools-remote/examples/node-counter/index.js b/packages/redux-devtools-remote/examples/node-counter/index.js new file mode 100644 index 00000000..d842252a --- /dev/null +++ b/packages/redux-devtools-remote/examples/node-counter/index.js @@ -0,0 +1,28 @@ +var createStore = require('redux').createStore; +var devTools = require('remote-redux-devtools').default; + +function counter(state, action) { + if (state === undefined) state = 0; + switch (action.type) { + case 'INCREMENT': + return state + 1; + case 'DECREMENT': + return state - 1; + default: + return state; + } +} + +var store = createStore(counter, devTools({ realtime: true })); +store.subscribe(function () { + console.log(store.getState()); +}); + +function incrementer() { + setTimeout(function () { + store.dispatch({ type: 'INCREMENT' }); + incrementer(); + }, 1000); +} + +incrementer(); diff --git a/packages/redux-devtools-remote/examples/node-counter/package.json b/packages/redux-devtools-remote/examples/node-counter/package.json new file mode 100644 index 00000000..59a12dd4 --- /dev/null +++ b/packages/redux-devtools-remote/examples/node-counter/package.json @@ -0,0 +1,16 @@ +{ + "name": "redux-remote-devtools-node-counter", + "version": "1.0.0", + "description": "Very simple counter for redux remote devtools in node", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "license": "MIT", + "dependencies": { + "redux": "^3.5.2" + }, + "devDependencies": { + "remote-redux-devtools": "^0.5.7" + } +} diff --git a/packages/redux-devtools-remote/examples/router/.babelrc b/packages/redux-devtools-remote/examples/router/.babelrc new file mode 100644 index 00000000..90ff9fc8 --- /dev/null +++ b/packages/redux-devtools-remote/examples/router/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-0", "react"], + "plugins": ["transform-decorators-legacy"] +} diff --git a/packages/redux-devtools-remote/examples/router/actions/todos.js b/packages/redux-devtools-remote/examples/router/actions/todos.js new file mode 100644 index 00000000..64247221 --- /dev/null +++ b/packages/redux-devtools-remote/examples/router/actions/todos.js @@ -0,0 +1,25 @@ +import * as types from '../constants/ActionTypes'; + +export function addTodo(text) { + return { type: types.ADD_TODO, text }; +} + +export function deleteTodo(id) { + return { type: types.DELETE_TODO, id }; +} + +export function editTodo(id, text) { + return { type: types.EDIT_TODO, id, text }; +} + +export function completeTodo(id) { + return { type: types.COMPLETE_TODO, id }; +} + +export function completeAll() { + return { type: types.COMPLETE_ALL }; +} + +export function clearCompleted() { + return { type: types.CLEAR_COMPLETED }; +} diff --git a/packages/redux-devtools-remote/examples/router/components/Footer.js b/packages/redux-devtools-remote/examples/router/components/Footer.js new file mode 100644 index 00000000..8972bd8a --- /dev/null +++ b/packages/redux-devtools-remote/examples/router/components/Footer.js @@ -0,0 +1,76 @@ +import React, { PropTypes, Component } from 'react'; +import classnames from 'classnames'; +import { + SHOW_ALL, + SHOW_COMPLETED, + SHOW_ACTIVE, +} from '../constants/TodoFilters'; + +const FILTER_TITLES = { + [SHOW_ALL]: 'All', + [SHOW_ACTIVE]: 'Active', + [SHOW_COMPLETED]: 'Completed', +}; + +class Footer extends Component { + renderTodoCount() { + const { activeCount } = this.props; + const itemWord = activeCount === 1 ? 'item' : 'items'; + + return ( + + {activeCount || 'No'} {itemWord} left + + ); + } + + renderFilterLink(filter) { + const title = FILTER_TITLES[filter]; + const { filter: selectedFilter, onShow } = this.props; + + return ( + onShow(filter)} + > + {title} + + ); + } + + renderClearButton() { + const { completedCount, onClearCompleted } = this.props; + if (completedCount > 0) { + return ( + + ); + } + } + + render() { + return ( +
+ {this.renderTodoCount()} +
    + {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => ( +
  • {this.renderFilterLink(filter)}
  • + ))} +
+ {this.renderClearButton()} +
+ ); + } +} + +Footer.propTypes = { + completedCount: PropTypes.number.isRequired, + activeCount: PropTypes.number.isRequired, + filter: PropTypes.string.isRequired, + onClearCompleted: PropTypes.func.isRequired, + onShow: PropTypes.func.isRequired, +}; + +export default Footer; diff --git a/packages/redux-devtools-remote/examples/router/components/Header.js b/packages/redux-devtools-remote/examples/router/components/Header.js new file mode 100644 index 00000000..e09a2e27 --- /dev/null +++ b/packages/redux-devtools-remote/examples/router/components/Header.js @@ -0,0 +1,30 @@ +import React, { PropTypes, Component } from 'react'; +import TodoTextInput from './TodoTextInput'; + +class Header extends Component { + handleSave(text) { + if (text.length !== 0) { + this.props.addTodo(text); + } + } + + render() { + const { path } = this.props; + return ( +
+

{path}

+ +
+ ); + } +} + +Header.propTypes = { + addTodo: PropTypes.func.isRequired, +}; + +export default Header; diff --git a/packages/redux-devtools-remote/examples/router/components/MainSection.js b/packages/redux-devtools-remote/examples/router/components/MainSection.js new file mode 100644 index 00000000..51b86378 --- /dev/null +++ b/packages/redux-devtools-remote/examples/router/components/MainSection.js @@ -0,0 +1,94 @@ +import React, { Component, PropTypes } from 'react'; +import TodoItem from './TodoItem'; +import Footer from './Footer'; +import { + SHOW_ALL, + SHOW_COMPLETED, + SHOW_ACTIVE, +} from '../constants/TodoFilters'; + +const TODO_FILTERS = { + [SHOW_ALL]: () => true, + [SHOW_ACTIVE]: (todo) => !todo.completed, + [SHOW_COMPLETED]: (todo) => todo.completed, +}; + +class MainSection extends Component { + constructor(props, context) { + super(props, context); + this.state = { filter: SHOW_ALL }; + } + + handleClearCompleted() { + const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed); + if (atLeastOneCompleted) { + this.props.actions.clearCompleted(); + } + } + + handleShow(filter) { + this.setState({ filter }); + } + + renderToggleAll(completedCount) { + const { todos, actions } = this.props; + if (todos.length > 0) { + return ( + + ); + } + } + + renderFooter(completedCount) { + const { todos } = this.props; + const { filter } = this.state; + const activeCount = todos.length - completedCount; + + if (todos.length) { + return ( +