diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ad93c14a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + } +} diff --git a/README.md b/README.md index fed71b7b..77fe0aac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +⚠️⚠️⚠️🚨🚨🚨⚠️⚠️⚠️ +## This repo is no longer the home of the redux-devtools-extension. The new home is https://github.com/reduxjs/redux-devtools. Please file your issues and PRs there. +⚠️⚠️⚠️🚨🚨🚨⚠️⚠️⚠️ + # Redux DevTools Extension [![Join the chat at https://gitter.im/zalmoxisus/redux-devtools-extension](https://badges.gitter.im/zalmoxisus/redux-devtools-extension.svg)](https://gitter.im/zalmoxisus/redux-devtools-extension?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -11,11 +15,12 @@ ### 1. For Chrome - from [Chrome Web Store](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd); + - or download `extension.zip` from [last releases](https://github.com/zalmoxisus/redux-devtools-extension/releases), unzip, open `chrome://extensions` url and turn on developer mode from top left and then click; on `Load Unpacked` and select the extracted folder for use - or build it with `npm i && npm run build:extension` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./build/extension`; - or run it in dev mode with `npm i && npm start` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./dev`. ### 2. For Firefox - - from [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/remotedev/); + - from [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/); - or build it with `npm i && npm run build:firefox` and [load the extension's folder](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox) `./build/firefox` (just select a file from inside the dir). ### 3. For Electron @@ -31,7 +36,7 @@ ## 1. With Redux ### 1.1 Basic store -For a basic [Redux store](http://redux.js.org/docs/api/createStore.html) simply add: +For a basic [Redux store](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) simply add: ```diff const store = createStore( reducer, /* preloadedState, */ @@ -39,9 +44,17 @@ For a basic [Redux store](http://redux.js.org/docs/api/createStore.html) simply ); ``` -Note that [`preloadedState`](http://redux.js.org/docs/api/createStore.html) argument is optional in Redux' [`createStore`](http://redux.js.org/docs/api/createStore.html). +Note that [`preloadedState`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) argument is optional in Redux's [`createStore`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer). > For universal ("isomorphic") apps, prefix it with `typeof window !== 'undefined' &&`. +```js +const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; +``` + +> For TypeScript use [`redux-devtools-extension` npm package](#13-use-redux-devtools-extension-package-from-npm), which contains all the definitions, or just use `(window as any)` (see [Recipes](/docs/Recipes.md#using-in-a-typescript-project) for an example). +```js +const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +``` In case ESLint is configured to not allow using the underscore dangle, wrap it like so: ```diff @@ -92,7 +105,7 @@ const store = createStore(reducer, enhancer); To make things easier, there's an npm package to install: ``` -npm install --save-dev redux-devtools-extension +npm install --save redux-devtools-extension ``` and to use like so: ```js @@ -156,7 +169,7 @@ or with middlewares and enhancers: ``` > You'll have to add `'process.env.NODE_ENV': JSON.stringify('production')` in your Webpack config for the production bundle ([to envify](https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md#exclude-devtools-from-production-builds)). If you use `create-react-app`, [it already does it for you.](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/config/webpack.config.prod.js#L253-L257) - If you're already checking `process.env.NODE_ENV` when creating the store, include `redux-devtools-extension/logOnly` for production enviroment. + If you're already checking `process.env.NODE_ENV` when creating the store, include `redux-devtools-extension/logOnly` for production environment. If you don’t want to allow the extension in production, just use `redux-devtools-extension/developmentOnly`. @@ -173,8 +186,9 @@ See [integrations](docs/Integrations.md) and [the blog post](https://medium.com/ ## Docs - [Options (arguments)](docs/API/Arguments.md) - [Methods (advanced API)](docs/API/Methods.md) - - [Create Redux store for debugging](docs/API/CreateStore.md) - [FAQ](docs/FAQ.md) + - Features + - [Trace actions calls](/docs/Features/Trace.md) - [Troubleshooting](docs/Troubleshooting.md) - [Articles](docs/Articles.md) - [Videos](docs/Videos.md) @@ -187,7 +201,7 @@ Live demos to use the extension with: - [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/) - [Redux Form](http://redux-form.com/6.5.0/examples/simple/) - [React Tetris](https://chvin.github.io/react-tetris/?lan=en) - - [Book Collection (Angular ngrx store)](http://ngrx.github.io/example-app/) + - [Book Collection (Angular ngrx store)](https://ngrx.github.io/platform/example-app/) Also see [`./examples` folder](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/examples). diff --git a/docs/API/Arguments.md b/docs/API/Arguments.md index 641918b8..b92737a3 100644 --- a/docs/API/Arguments.md +++ b/docs/API/Arguments.md @@ -29,6 +29,12 @@ The `options` object is optional, and can include any of the following. ### `maxAge` *number* (>1) - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance. Default is `50`. +### `trace` +*boolean* or *function* - if set to `true`, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code ([more details](../Features/Trace.md)). You can use a function (with action object as argument) which should return `new Error().stack` string, getting the stack outside of reducers. Default to `false`. + +### `traceLimit` +*number* - maximum stack trace frames to be stored (in case `trace` option was provided as `true`). By default it's `10`. Note that, because extension's calls are excluded, the resulted frames could be 1 less. If `trace` option is a function, `traceLimit` will have no effect, as it's supposed to be handled there. + ### `serialize` *boolean* or *object* which contains: diff --git a/docs/API/CreateStore.md b/docs/API/CreateStore.md deleted file mode 100644 index 95a0709a..00000000 --- a/docs/API/CreateStore.md +++ /dev/null @@ -1,13 +0,0 @@ -## Create Redux store for debugging - -- `window.__REDUX_DEVTOOLS_EXTENSION__(reducer, [preloadedState, config])` - -> Note: This is not intended to replace Redux' `createStore`. Use this approach only when you want to inspect changes outside of Redux or when not using Redux inside your application. - -1. `reducer` *(Function)*: A [reducing function](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#reducer) that returns the next [state tree](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#state), given the current state tree and an [action](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#action) to handle. - -2. [`preloadedState`] *(any)*: The initial state. You may optionally specify it to hydrate the state from the server in universal apps, or to restore a previously serialized user session. If you produced `reducer` with [`combineReducers`](https://github.com/reactjs/redux/tree/master/docs/api/combineReducers.md), this must be a plain object with the same shape as the keys passed to it. Otherwise, you are free to pass anything that your `reducer` can understand. - -3. [`config`] *(Object)*: options. See [parameters](Arguments.md) for details. - -See [the example of usage](https://github.com/zalmoxisus/redux-devtools-extension/commit/1810d2c1f0e8be1daf8f2d8f7bbeb4f8c528d90b) and [the post for more details](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f). diff --git a/docs/API/Methods.md b/docs/API/Methods.md index a7fd799d..a2205d7a 100644 --- a/docs/API/Methods.md +++ b/docs/API/Methods.md @@ -21,7 +21,7 @@ Use the following methods of `window.__REDUX_DEVTOOLS_EXTENSION__`: ##### Returns *Object* containing the following methods: -- `subscribe(listener)` - adds a change listener. It will be called any time an action is dispatched form the monitor. Returns a function to unsubscribe the current listener. +- `subscribe(listener)` - adds a change listener. It will be called any time an action is dispatched from the monitor. Returns a function to unsubscribe the current listener. - `unsubscribe()` - unsubscribes all listeners. - `send(action, state)` - sends a new action and state manually to be shown on the monitor. If action is `null` then we suppose we send `liftedState`. - `init(state)` - sends the initial state to the monitor. @@ -40,11 +40,11 @@ devTools.init({ value: 'initial state' }); devTools.send('change state', { value: 'state changed' }) ``` -See [redux enhancer's examle](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/logOnly.js), [react example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/examples/react-counter-messaging/components/Counter.js) and [blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details. +See [redux enhancer's example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/logOnly.js), [react example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/examples/react-counter-messaging/components/Counter.js) and [blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details. ### disconnect() -Remove extensions listener and disconnect extensions background script connection. Usually just unsubscribing the listiner inside the `connect` is enough. +Remove extensions listener and disconnect extensions background script connection. Usually just unsubscribing the listener inside the `connect` is enough. ### send(action, state, [options, instanceId]) @@ -61,11 +61,11 @@ Send a new action and state manually to be shown on the monitor. It's recommende ### listen(onMessage, instanceId) -Listen for messages dispatched for specific `instanceId`. For most of cases it's better to use `subcribe` inside the [`connect`](connect). +Listen for messages dispatched for specific `instanceId`. For most cases it's better to use `subcribe` inside the [`connect`](connect). ##### Arguments -- `onMessage` *Function* to call when there's an action form the monitor. +- `onMessage` *Function* to call when there's an action from the monitor. - `instanceId` *String* - instance id for which to handle actions. @@ -80,7 +80,7 @@ Open the extension's window. This should be conditional (usually you don't need ### notifyErrors([onError]) -When called, the extension will listen for uncaught exceptions on the page, and, if any, will show native notifications. Optionally, you can provide a function to be called when and exception occurs. +When called, the extension will listen for uncaught exceptions on the page, and, if any, will show native notifications. Optionally, you can provide a function to be called when an exception occurs. ##### Arguments diff --git a/docs/API/README.md b/docs/API/README.md index 23100eab..13781355 100644 --- a/docs/API/README.md +++ b/docs/API/README.md @@ -2,4 +2,3 @@ - [Parameters](Arguments.md) - [Methods](Methods.md) -- [Create Redux store for debugging](CreateStore.md) diff --git a/docs/Features/Trace.md b/docs/Features/Trace.md new file mode 100644 index 00000000..1aa2505b --- /dev/null +++ b/docs/Features/Trace.md @@ -0,0 +1,13 @@ +## Trace actions calls + +![trace-demo](https://user-images.githubusercontent.com/7957859/50161148-a1639300-02e3-11e9-80e7-18d3215a0bf8.gif) + +One of the features of Redux DevTools is to select an action in the history and see the callstack that triggered it. It aims to solve the problem of finding the source of events in the event list. + +By default it's disabled as, depending of the use case, generating and serializing stack traces for every action can impact the performance. To enable it, set `trace` option to `true` as in [examples](https://github.com/zalmoxisus/redux-devtools-extension/commit/64717bb9b3534ff616d9db56c2be680627c7b09d). See [the API](../API/Arguments.md#trace) for more details. + +For some edge cases where stack trace cannot be obtained with just `Error().stack`, you can pass a function as `trace` with your implementation. It's useful for cases where the stack is broken, like, for example, [when calling `setTimeout`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/e7c05c98e7e9654cb7db92a2f56c6b5f3ff2452b/test/instrument.spec.js#L735-L737). It takes `action` object as argument and should return `stack` string. This way it can be also used to provide stack conditionally only for certain actions. + +There's also an optional `traceLimit` parameter, which is `10` by default, to prevent consuming too much memory and serializing large stacks and also allows you to get larger stacks than limited by the browser (it will overpass default limit of `10` imposed by Chrome in `Error.stackTraceLimit`). If `trace` option is a function, `traceLimit` will have no effect, that should be handled there like so: `trace: () => new Error().stack.split('\n').slice(0, limit+1).join('\n')` (`+1` is needed for Chrome where's an extra 1st frame for `Error\n`). + +Apart from opening resources in Chrome DevTools, as seen in the demo above, it can open the file (and jump to the line-column) right in your editor. Pretty useful for debugging, and also as an alternative when it's not possible to use openResource (for Firefox or when using the extension from window or for remote debugging). You can click Settings button and enable that, also adding the path to your project root directory to use. It works out of the box for VSCode, Atom, Webstorm/Phpstorm/IntelliJ, Sublime, Emacs, MacVim, Textmate on Mac and Windows. For Linux you can use [`atom-url-handler`](https://github.com/eclemens/atom-url-handler). diff --git a/docs/Integrations.md b/docs/Integrations.md index 94d9ddb4..cb51130e 100644 --- a/docs/Integrations.md +++ b/docs/Integrations.md @@ -1,6 +1,7 @@ # Integrations for js and non-js frameworks Mostly functional: +- [React](#react) - [Angular](#angular) - [Cycle](#cycle) - [Ember](#ember) @@ -8,6 +9,8 @@ Mostly functional: - [Freezer](#freezer) - [Mobx](#mobx) - [PureScript](#purescript) +- [Reductive](#reductive) +- [Aurelia](#aurelia) In progress: - [ClojureScript](#clojurescript) @@ -15,6 +18,40 @@ In progress: - [Python](#python) - [Swift](#swift) +### [React](https://github.com/facebook/react) +#### Inspect React props +##### [`react-inspect-props`](https://github.com/lucasconstantino/react-inspect-props) +```js +import { compose, withState } from 'recompose' +import { inspectProps } from 'react-inspect-props' + +compose( + withState('count', 'setCount', 0), + inspectProps('Counter inspector') +)(Counter) +``` + +#### Inspect React states +##### [`remotedev-react-state`](https://github.com/jhen0409/remotedev-react-state) +```js +import connectToDevTools from 'remotedev-react-state' + +componentWillMount() { + // Connect to devtools after setup initial state + connectToDevTools(this/*, options */) + } +``` + +#### Inspect React hooks (useState and useReducer) +##### [`reinspect`](https://github.com/troch/reinspect) +```js +import { useState } from 'reinspect' + +export function CounterWithUseState({ id }) { + const [count, setCount] = useState(0, id) + // ... +} +``` ### [Mobx](https://github.com/mobxjs/mobx) #### [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev) @@ -66,15 +103,15 @@ import { NgReduxModule, NgRedux, DevToolsExtension } from 'ng2-redux'; ``` For Angular 1 see [ng-redux](https://github.com/angular-redux/ng-redux). -#### [Angular @ngrx/store](https://github.com/ngrx/store) + [`@ngrx/store-devtools`](https://github.com/ngrx/store-devtools) +#### [Angular @ngrx/store](https://ngrx.io/) + [`@ngrx/store-devtools`](https://ngrx.io/guide/store-devtools) ```js import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ - StoreModule.provideStore(rootReducer), - // Note that you must instrument after importing StoreModule - StoreDevtoolsModule.instrumentOnlyWithExtension({ + StoreModule.forRoot(rootReducer), + // Instrumentation must be imported after importing StoreModule (config is optional) + StoreDevtoolsModule.instrument({ maxAge: 5 }) ] @@ -82,7 +119,7 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools'; export class AppModule { } ``` -[`Example of integration`](https://github.com/ngrx/example-app) ([live demo](http://ngrx.github.io/example-app)). +[`Example of integration`](https://github.com/ngrx/platform/tree/master/projects/example-app/) ([live demo](https://ngrx.github.io/platform/example-app/)). ### [Ember](http://emberjs.com/) #### [`ember-redux`](https://github.com/ember-redux/ember-redux) @@ -182,3 +219,40 @@ var middleware: [StoreMiddleware] = [ middleware.append(MonitorMiddleware.create(using: .defaultConfiguration)) #endif ``` + +### [Reductive](https://github.com/reasonml-community/reductive) +#### [`reductive-dev-tools`](https://github.com/ambientlight/reductive-dev-tools) +```reason +let storeEnhancer = + ReductiveDevTools.( + Connectors.reductiveEnhancer( + Extension.enhancerOptions(~name="MyApp", ()), + ) + ); + +let storeCreator = storeEnhancer @@ Reductive.Store.create; +``` + +### [Aurelia](http://aurelia.io) +#### [`aurelia-store`](https://aurelia.io/docs/plugins/store) +```ts +import {Aurelia} from 'aurelia-framework'; +import {initialState} from './state'; + +export function configure(aurelia: Aurelia) { + aurelia.use + .standardConfiguration() + .feature('resources'); + + ... + + aurelia.use.plugin('aurelia-store', { + initialState, + devToolsOptions: { // optional + ... // see https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md + }, + }); + + aurelia.start().then(() => aurelia.setRoot()); +} +``` diff --git a/docs/README.md b/docs/README.md index 04becbda..f40b08be 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,8 @@ * [API Reference](/docs/API/README.md) * [Options (arguments)](/docs/API/Arguments.md) * [Methods (advanced API)](/docs/API/Methods.md) - * [Create Redux store for debugging](/docs/API/CreateStore.md) +* Features + * [Trace actions calls](/docs/Features/Trace.md) * [Integrations](/docs/Integrations.md) * [FAQ](/docs/FAQ.md) * [Troubleshooting](/docs/Troubleshooting.md) diff --git a/docs/Recipes.md b/docs/Recipes.md index ff696fff..eeb31653 100644 --- a/docs/Recipes.md +++ b/docs/Recipes.md @@ -1,5 +1,44 @@ # Recipes +### Using in a typescript project + +The recommended way is to use [`redux-devtools-extension` npm package](/README.md#13-use-redux-devtools-extension-package-from-npm), which contains all typescript definitions. Or you can just use `window as any`: + +```js +const store = createStore( + rootReducer, + initialState, + (window as any).__REDUX_DEVTOOLS_EXTENSION__ && + (window as any).__REDUX_DEVTOOLS_EXTENSION__() +); +``` +Note that you many need to set `no-any` to false in your `tslint.json` file. + +Alternatively you can use typeguard in order to avoid +casting to any. + +```typescript +import { createStore, StoreEnhancer } from "redux"; + +// ... + +type WindowWithDevTools = Window & { + __REDUX_DEVTOOLS_EXTENSION__: () => StoreEnhancer +} + +const isReduxDevtoolsExtenstionExist = +(arg: Window | WindowWithDevTools): + arg is WindowWithDevTools => { + return '__REDUX_DEVTOOLS_EXTENSION__' in arg; +} + +// ... + +const store = createStore(rootReducer, initialState, + isReduxDevtoolsExtenstionExist(window) ? + window.__REDUX_DEVTOOLS_EXTENSION__() : undefined) +``` + ### Export from browser console or from application ```js @@ -7,3 +46,25 @@ store.liftedStore.getState() ``` The extension is not sharing `store` object, so you should take care of that. + +### Applying multiple times with different sets of options + +We're [not allowing that from instrumentation part](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js#L676), because that would re-dispatch every app action in case we'd have many liftedStores, but there's [a helper for logging only](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/logOnly.js), which can be used it like so: + +```js +import { createStore, compose } from 'redux'; +import { devToolsEnhancer } from 'redux-devtools-extension/logOnly'; + +const store = createStore(reducer, /* preloadedState, */ compose( +devToolsEnhancer({ + instaceID: 1, + name: 'Blacklisted', + actionsBlacklist: '...' +}), +devToolsEnhancer({ + instaceID: 2, + name: 'Whitelisted', + actionsWhitelist: '...' +}) +)); +``` diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index ae908093..f7be9e5b 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -16,9 +16,9 @@ If you develop on your local filesystem, make sure to allow Redux DevTools acces Most likely you mutate the state. Check it by [adding `redux-immutable-state-invariant` middleware](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/examples/counter/store/configureStore.js#L3). -### Last actions RE-APPLIED +### @@INIT or REPLACE action resets the state of the app or last actions RE-APPLIED -When you use `store.replaceReducer` the effect will be the same as for hot-reloading, where the extension is recomputing all the history again. To avoid that set [`shouldHotReload`](/docs/API/Arguments.md#shouldhotreload) parameter to `false`. +`@@redux/REPLACE` (or `@@INIT`) is used internally when the application is hot reloaded. When you use `store.replaceReducer` the effect will be the same as for hot-reloading, where the extension is recomputing all the history again. To avoid that set [`shouldHotReload`](/docs/API/Arguments.md#shouldhotreload) parameter to `false`. ### It doesn't work with other store enhancers @@ -38,7 +38,7 @@ Where `batchedSubscribe` is `redux-batched-subscribe` store enhancer. That is happening due to serialization of some huge objects included in the state or action. The solution is to [sanitize them](/docs/API/Arguments.md#actionsanitizer--statesanitizer). -You can do that by including/omitting data containing specific values, having specific types... In the example bellow we're omitting parts of action and state objects with the key `data` (in case of action only when was dispatched action `FILE_DOWNLOAD_SUCCESS`): +You can do that by including/omitting data containing specific values, having specific types... In the example below we're omitting parts of action and state objects with the key `data` (in case of action only when was dispatched action `FILE_DOWNLOAD_SUCCESS`): ```js const actionSanitizer = (action) => ( diff --git a/examples/counter/.babelrc b/examples/counter/.babelrc index 120f832f..61058eb2 100644 --- a/examples/counter/.babelrc +++ b/examples/counter/.babelrc @@ -1,4 +1,3 @@ { - "presets": [ "es2015", "stage-0", "react" ], - "plugins": [ "add-module-exports", "transform-decorators-legacy" ] + "presets": [ "es2015", "stage-0", "react" ] } diff --git a/examples/counter/components/Counter.js b/examples/counter/components/Counter.js index 2f8cf750..63372813 100644 --- a/examples/counter/components/Counter.js +++ b/examples/counter/components/Counter.js @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { isMonitorAction } from '../store/configureStore'; class Counter extends Component { render() { diff --git a/examples/counter/package.json b/examples/counter/package.json index db7e538f..73fb3ab2 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -17,35 +17,25 @@ }, "homepage": "http://rackt.github.io/redux", "dependencies": { - "prop-types": "^15.5.8", - "react": "^15.5.4", - "react-dom": "^15.5.4", - "react-redux": "^4.4.5", - "redux": "^3.5.2", - "redux-devtools-extension": "^1.0.0", - "redux-thunk": "^2.1.0" + "prop-types": "^15.6.2", + "react": "^16.7.0", + "react-dom": "^16.7.0", + "react-redux": "^6.0.0", + "redux": "^4.0.1", + "redux-devtools-extension": "^2.13.7", + "redux-thunk": "^2.3.0" }, "devDependencies": { - "babel-core": "^6.3.15", - "babel-loader": "^6.2.0", - "babel-plugin-add-module-exports": "^0.1.1", - "babel-plugin-react-transform": "^2.0.0", - "babel-plugin-transform-decorators-legacy": "^1.2.0", - "babel-polyfill": "^6.3.14", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "babel-preset-react-hmre": "^1.0.1", + "babel-cli": "^6.3.17", + "babel-core": "^6.3.17", + "babel-loader": "^7.0.0", + "babel-preset-es2015": "^6.0.0", + "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": "^15.1.0", - "react-transform-hmr": "^1.0.0", - "redux-immutable-state-invariant": "^1.1.1", - "webpack": "^1.13.1", - "webpack-dev-middleware": "^1.2.0", + "redux-immutable-state-invariant": "^2.1.0", + "webpack": "^4.0.0", + "webpack-dev-server": "^3.0.0", "webpack-hot-middleware": "^2.2.0" } } diff --git a/examples/counter/store/configureStore.js b/examples/counter/store/configureStore.js index 98de9de3..2456b86b 100644 --- a/examples/counter/store/configureStore.js +++ b/examples/counter/store/configureStore.js @@ -5,9 +5,8 @@ import invariant from 'redux-immutable-state-invariant'; import reducer from '../reducers'; import * as actionCreators from '../actions/counter'; -export let isMonitorAction; export default function configureStore(preloadedState) { - const composeEnhancers = composeWithDevTools({ actionCreators }); + const composeEnhancers = composeWithDevTools({ actionCreators, trace: true, traceLimit: 25 }); const store = createStore(reducer, preloadedState, composeEnhancers( applyMiddleware(invariant(), thunk) )); @@ -15,8 +14,7 @@ export default function configureStore(preloadedState) { if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { - const nextReducer = require('../reducers'); - store.replaceReducer(nextReducer); + store.replaceReducer(require('../reducers').default) }); } diff --git a/examples/counter/webpack.config.js b/examples/counter/webpack.config.js index 6fcc6a3d..014df804 100644 --- a/examples/counter/webpack.config.js +++ b/examples/counter/webpack.config.js @@ -2,7 +2,8 @@ var path = require('path'); var webpack = require('webpack'); module.exports = { - devtool: 'cheap-module-eval-source-map', + mode: 'development', + devtool: 'source-map', entry: [ 'webpack-hot-middleware/client', './index' @@ -13,16 +14,13 @@ module.exports = { publicPath: '/static/' }, plugins: [ - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin() + new webpack.HotModuleReplacementPlugin() ], module: { - loaders: [{ + rules: [{ test: /\.js$/, - loaders: ['babel'], - exclude: /node_modules/, - include: __dirname + loaders: ['babel-loader'], + exclude: /node_modules/ }] } }; diff --git a/examples/react-counter-messaging/.babelrc b/examples/react-counter-messaging/.babelrc index 120f832f..61058eb2 100644 --- a/examples/react-counter-messaging/.babelrc +++ b/examples/react-counter-messaging/.babelrc @@ -1,4 +1,3 @@ { - "presets": [ "es2015", "stage-0", "react" ], - "plugins": [ "add-module-exports", "transform-decorators-legacy" ] + "presets": [ "es2015", "stage-0", "react" ] } diff --git a/examples/react-counter-messaging/components/Counter.js b/examples/react-counter-messaging/components/Counter.js index eea41aeb..b1da358c 100644 --- a/examples/react-counter-messaging/components/Counter.js +++ b/examples/react-counter-messaging/components/Counter.js @@ -1,8 +1,8 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; const withDevTools = ( // process.env.NODE_ENV === 'development' && - typeof window !== 'undefined' && window.devToolsExtension + typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__ ); class Counter extends Component { @@ -16,7 +16,7 @@ class Counter extends Component { componentWillMount() { if (withDevTools) { - this.devTools = window.devToolsExtension.connect(); + this.devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect(); this.unsubscribe = this.devTools.subscribe((message) => { // Implement monitors actions. // For example time traveling: @@ -30,7 +30,7 @@ class Counter extends Component { componentWillUnmount() { if (withDevTools) { this.unsubscribe(); // Use if you have other subscribers from other components. - window.devToolsExtension.disconnect(); // If there aren't other subscribers. + window.__REDUX_DEVTOOLS_EXTENSION__.disconnect(); // If there aren't other subscribers. } } diff --git a/examples/react-counter-messaging/package.json b/examples/react-counter-messaging/package.json index 9784735a..97db9897 100644 --- a/examples/react-counter-messaging/package.json +++ b/examples/react-counter-messaging/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "description": "React counter example", "scripts": { - "start": "node server.js" + "start": "webpack-dev-server --progress" }, "repository": { "type": "git", @@ -15,29 +15,19 @@ }, "homepage": "https://github.com/zalmoxisus/redux-devtools-extension", "dependencies": { - "react": "^15.1.0", - "react-dom": "^15.1.0" + "react": "^16.0.0", + "react-dom": "^16.0.0" }, "devDependencies": { - "babel-core": "^6.3.15", - "babel-loader": "^6.2.0", - "babel-plugin-add-module-exports": "^0.1.1", - "babel-plugin-react-transform": "^2.0.0", - "babel-plugin-transform-decorators-legacy": "^1.2.0", - "babel-polyfill": "^6.3.14", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "babel-preset-react-hmre": "^1.0.1", + "babel-cli": "^6.3.17", + "babel-core": "^6.3.17", + "babel-loader": "^7.0.0", + "babel-preset-es2015": "^6.0.0", + "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": "^15.1.0", - "react-transform-hmr": "^1.0.0", - "webpack": "^1.13.1", - "webpack-dev-middleware": "^1.2.0", + "webpack": "^4.0.0", + "webpack-cli": "^3.2.0", + "webpack-dev-server": "^3.0.0", "webpack-hot-middleware": "^2.2.0" } } diff --git a/examples/react-counter-messaging/server.js b/examples/react-counter-messaging/server.js deleted file mode 100644 index 0fe70397..00000000 --- a/examples/react-counter-messaging/server.js +++ /dev/null @@ -1,23 +0,0 @@ -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/examples/react-counter-messaging/webpack.config.js b/examples/react-counter-messaging/webpack.config.js index 6fcc6a3d..090ebf3c 100644 --- a/examples/react-counter-messaging/webpack.config.js +++ b/examples/react-counter-messaging/webpack.config.js @@ -2,9 +2,9 @@ var path = require('path'); var webpack = require('webpack'); module.exports = { - devtool: 'cheap-module-eval-source-map', + mode: 'development', + devtool: 'source-map', entry: [ - 'webpack-hot-middleware/client', './index' ], output: { @@ -12,17 +12,14 @@ module.exports = { filename: 'bundle.js', publicPath: '/static/' }, - plugins: [ - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin() - ], module: { - loaders: [{ + rules: [{ test: /\.js$/, - loaders: ['babel'], - exclude: /node_modules/, - include: __dirname + loaders: ['babel-loader'], + exclude: /node_modules/ }] + }, + devServer: { + port: 4004 } }; diff --git a/examples/react-counter/.babelrc b/examples/react-counter/.babelrc deleted file mode 100644 index 120f832f..00000000 --- a/examples/react-counter/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": [ "es2015", "stage-0", "react" ], - "plugins": [ "add-module-exports", "transform-decorators-legacy" ] -} diff --git a/examples/react-counter/components/Counter.js b/examples/react-counter/components/Counter.js deleted file mode 100644 index c39ba208..00000000 --- a/examples/react-counter/components/Counter.js +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Component, PropTypes } from 'react'; - -const withDevTools = ( - // process.env.NODE_ENV === 'development' && - typeof window !== 'undefined' && window.devToolsExtension -); - -const reducer = (state = { counter: 0 }, action) => { - switch (action.type) { - case 'INCREMENT': - return { counter: state.counter + 1 }; - case 'DECREMENT': - return { counter: state.counter - 1 }; - default: - return state; - } -}; - -class Counter extends Component { - constructor() { - super(); - this.state = { counter: 0 }; - - this.increment = this.increment.bind(this); - this.decrement = this.decrement.bind(this); - } - - componentWillMount() { - if (withDevTools) { - this.store = window.devToolsExtension(reducer); - this.store.subscribe(() => { this.setState(this.store.getState()); }); - } - } - - dispatch(action) { - if (withDevTools) this.store.dispatch(action); - else this.setState(reducer(this.state, action)); - } - - increment() { - this.dispatch({ type: 'INCREMENT' }); - } - - decrement() { - this.dispatch({ type: 'DECREMENT' }); - } - - render() { - const { counter } = this.state; - return ( -

- Clicked: {counter} times - {' '} - - {' '} - -

- ); - } -} - -export default Counter; diff --git a/examples/react-counter/index.html b/examples/react-counter/index.html deleted file mode 100644 index ff28e56e..00000000 --- a/examples/react-counter/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - React counter example - - -
-
- - - diff --git a/examples/react-counter/index.js b/examples/react-counter/index.js deleted file mode 100644 index 70e40595..00000000 --- a/examples/react-counter/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { render } from 'react-dom'; -import Counter from './components/Counter'; - -render( - , - document.getElementById('root') -); diff --git a/examples/react-counter/package.json b/examples/react-counter/package.json deleted file mode 100644 index 9784735a..00000000 --- a/examples/react-counter/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "react-counter-example", - "version": "0.0.0", - "description": "React counter example", - "scripts": { - "start": "node server.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/zalmoxisus/redux-devtools-extension.git" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/zalmoxisus/redux-devtools-extension/issues" - }, - "homepage": "https://github.com/zalmoxisus/redux-devtools-extension", - "dependencies": { - "react": "^15.1.0", - "react-dom": "^15.1.0" - }, - "devDependencies": { - "babel-core": "^6.3.15", - "babel-loader": "^6.2.0", - "babel-plugin-add-module-exports": "^0.1.1", - "babel-plugin-react-transform": "^2.0.0", - "babel-plugin-transform-decorators-legacy": "^1.2.0", - "babel-polyfill": "^6.3.14", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "babel-preset-react-hmre": "^1.0.1", - "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": "^15.1.0", - "react-transform-hmr": "^1.0.0", - "webpack": "^1.13.1", - "webpack-dev-middleware": "^1.2.0", - "webpack-hot-middleware": "^2.2.0" - } -} diff --git a/examples/react-counter/server.js b/examples/react-counter/server.js deleted file mode 100644 index 0fe70397..00000000 --- a/examples/react-counter/server.js +++ /dev/null @@ -1,23 +0,0 @@ -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/examples/react-counter/webpack.config.js b/examples/react-counter/webpack.config.js deleted file mode 100644 index 6fcc6a3d..00000000 --- a/examples/react-counter/webpack.config.js +++ /dev/null @@ -1,28 +0,0 @@ -var path = require('path'); -var webpack = require('webpack'); - -module.exports = { - devtool: 'cheap-module-eval-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 - }] - } -}; diff --git a/examples/saga-counter/.babelrc b/examples/saga-counter/.babelrc new file mode 100644 index 00000000..61058eb2 --- /dev/null +++ b/examples/saga-counter/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [ "es2015", "stage-0", "react" ] +} diff --git a/examples/saga-counter/package.json b/examples/saga-counter/package.json index c47f7f70..38c59f46 100644 --- a/examples/saga-counter/package.json +++ b/examples/saga-counter/package.json @@ -3,9 +3,7 @@ "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" + "start": "webpack-dev-server --progress" }, "repository": { "type": "git", @@ -17,33 +15,23 @@ }, "homepage": "http://rackt.github.io/redux", "dependencies": { - "react": "^15.1.0", - "react-dom": "^15.1.0", - "react-redux": "^4.4.5", - "redux": "^3.5.2", + "prop-types": "^15.6.2", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-redux": "^6.0.0", + "redux": "^4.0.0", "redux-saga": "^0.10.5" }, "devDependencies": { - "babel-core": "^6.3.15", - "babel-loader": "^6.2.0", - "babel-plugin-add-module-exports": "^0.1.1", - "babel-plugin-react-transform": "^2.0.0", - "babel-plugin-transform-decorators-legacy": "^1.2.0", - "babel-polyfill": "^6.3.14", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "babel-preset-react-hmre": "^1.0.1", + "babel-cli": "^6.3.17", + "babel-core": "^6.3.17", + "babel-loader": "^7.0.0", + "babel-preset-es2015": "^6.0.0", + "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": "^15.1.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" + "webpack": "^4.0.0", + "webpack-cli": "^3.2.0", + "webpack-dev-server": "^3.1.0", + "webpack-hot-middleware": "^2.24.0" } } diff --git a/examples/saga-counter/server.js b/examples/saga-counter/server.js deleted file mode 100644 index 348676d0..00000000 --- a/examples/saga-counter/server.js +++ /dev/null @@ -1,23 +0,0 @@ -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 = 3000 - -var compiler = webpack(config) -app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) -app.use(webpackHotMiddleware(compiler)) - -app.use(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/examples/saga-counter/src/components/Counter.js b/examples/saga-counter/src/components/Counter.js index 1f617344..93d8c13f 100644 --- a/examples/saga-counter/src/components/Counter.js +++ b/examples/saga-counter/src/components/Counter.js @@ -1,5 +1,5 @@ -/*eslint-disable no-unused-vars */ -import React, { Component, PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' const Counter = ({ value, onIncrement, onIncrementAsync, onDecrement, onIncrementIfOdd }) =>

diff --git a/examples/saga-counter/src/main.js b/examples/saga-counter/src/main.js index 50080355..e997ceb6 100644 --- a/examples/saga-counter/src/main.js +++ b/examples/saga-counter/src/main.js @@ -1,4 +1,3 @@ -/*eslint-disable no-unused-vars*/ import "babel-polyfill" import React from 'react' @@ -13,7 +12,8 @@ import rootSaga from './sagas' const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */) -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, traceLimit: 25 }) || compose; const store = createStore( reducer, composeEnhancers(applyMiddleware(sagaMiddleware)) diff --git a/examples/saga-counter/test/sagas.js b/examples/saga-counter/test/sagas.js deleted file mode 100644 index eee0c09a..00000000 --- a/examples/saga-counter/test/sagas.js +++ /dev/null @@ -1,29 +0,0 @@ -import test from 'tape'; - -import { put, call } from '../../../src/effects' -import { delay } from '../../../src' -import { incrementAsync } from '../src/sagas' - -test('incrementAsync Saga test', (t) => { - const generator = incrementAsync() - - t.deepEqual( - generator.next().value, - call(delay, 1000), - 'counter Saga must call delay(1000)' - ) - - t.deepEqual( - generator.next().value, - put({type: 'INCREMENT'}), - 'counter Saga must dispatch an INCREMENT action' - ) - - t.deepEqual( - generator.next(), - { done: true, value: undefined }, - 'counter Saga must be done' - ) - - t.end() -}); diff --git a/examples/saga-counter/webpack.config.js b/examples/saga-counter/webpack.config.js index 2d30cf06..0eff171d 100644 --- a/examples/saga-counter/webpack.config.js +++ b/examples/saga-counter/webpack.config.js @@ -1,10 +1,10 @@ -var path = require('path') -var webpack = require('webpack') +var path = require('path'); +var webpack = require('webpack'); module.exports = { - devtool: 'cheap-module-eval-source-map', + mode: 'development', + devtool: 'source-map', entry: [ - 'webpack-hot-middleware/client', path.join(__dirname, 'src', 'main') ], output: { @@ -12,20 +12,14 @@ module.exports = { filename: 'bundle.js', publicPath: '/static/' }, - plugins: [ - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin(), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') - }) - ], module: { - loaders: [{ + rules: [{ test: /\.js$/, - loaders: [ 'babel' ], - exclude: /node_modules/, - include: __dirname + loaders: ['babel-loader'], + exclude: /node_modules/ }] + }, + devServer: { + port: 4003 } -} +}; diff --git a/examples/todomvc/.babelrc b/examples/todomvc/.babelrc index f0a6f40f..ad37c427 100644 --- a/examples/todomvc/.babelrc +++ b/examples/todomvc/.babelrc @@ -1,4 +1,3 @@ { - "presets": [ "es2015", "stage-0", "react" ], - "plugins": [ "add-module-exports", "transform-decorators-legacy" ] + "presets": [ "es2015", "stage-0", "react" ] } \ No newline at end of file diff --git a/examples/todomvc/components/Footer.js b/examples/todomvc/components/Footer.js index 6652e768..e2110fb9 100644 --- a/examples/todomvc/components/Footer.js +++ b/examples/todomvc/components/Footer.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import classnames from 'classnames'; import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; diff --git a/examples/todomvc/components/Header.js b/examples/todomvc/components/Header.js index 9c735f3c..98abf6b7 100644 --- a/examples/todomvc/components/Header.js +++ b/examples/todomvc/components/Header.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import TodoTextInput from './TodoTextInput'; class Header extends Component { diff --git a/examples/todomvc/components/MainSection.js b/examples/todomvc/components/MainSection.js index 307663fe..3005d8cd 100644 --- a/examples/todomvc/components/MainSection.js +++ b/examples/todomvc/components/MainSection.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import TodoItem from './TodoItem'; import Footer from './Footer'; import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; diff --git a/examples/todomvc/components/TodoItem.js b/examples/todomvc/components/TodoItem.js index b2830b76..60585b72 100644 --- a/examples/todomvc/components/TodoItem.js +++ b/examples/todomvc/components/TodoItem.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import classnames from 'classnames'; import TodoTextInput from './TodoTextInput'; diff --git a/examples/todomvc/components/TodoTextInput.js b/examples/todomvc/components/TodoTextInput.js index df098aa6..6fafca6d 100644 --- a/examples/todomvc/components/TodoTextInput.js +++ b/examples/todomvc/components/TodoTextInput.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import classnames from 'classnames'; class TodoTextInput extends Component { @@ -38,7 +39,7 @@ class TodoTextInput extends Component { })} type="text" placeholder={this.props.placeholder} - autoFocus="true" + autoFocus={true} value={this.state.text} onBlur={this.handleBlur.bind(this)} onChange={this.handleChange.bind(this)} diff --git a/examples/todomvc/containers/App.js b/examples/todomvc/containers/App.js index 5e6b4605..ab91a9ff 100644 --- a/examples/todomvc/containers/App.js +++ b/examples/todomvc/containers/App.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import Header from '../components/Header'; diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 5cd61164..9fd03bf4 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -18,33 +18,25 @@ "homepage": "http://rackt.github.io/redux", "dependencies": { "classnames": "^2.1.2", - "react": "^0.14.0", - "react-dom": "^0.14.0", - "react-redux": "^4.0.0", - "redux": "^3.1.5" + "prop-types": "^15.6.2", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-redux": "^6.0.0", + "redux": "^4.0.0" }, "devDependencies": { - "babel-core": "^6.3.15", - "babel-loader": "^6.2.0", - "babel-plugin-add-module-exports": "^0.1.1", - "babel-plugin-react-transform": "^2.0.0-beta1", - "babel-plugin-transform-decorators-legacy": "^1.2.0", - "babel-polyfill": "^6.3.14", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", + "babel-cli": "^6.3.17", + "babel-core": "^6.3.17", + "babel-loader": "^7.0.0", + "babel-preset-es2015": "^6.0.0", + "babel-preset-react": "6.3.13", "babel-preset-stage-0": "^6.3.13", - "expect": "^1.8.0", "express": "^4.13.3", - "jsdom": "^5.6.1", - "mocha": "^2.2.5", - "node-libs-browser": "^0.5.2", - "raw-loader": "^0.5.1", - "react-addons-test-utils": "^0.14.0", - "react-transform-hmr": "^1.0.0", - "style-loader": "^0.12.3", + "raw-loader": "^1.0.0", + "style-loader": "^0.23.0", "todomvc-app-css": "^2.0.1", - "webpack": "^1.9.11", - "webpack-dev-middleware": "^1.2.0", + "webpack": "^4.0.0", + "webpack-dev-server": "^3.0.0", "webpack-hot-middleware": "^2.2.0" } } diff --git a/examples/todomvc/reducers/todos.js b/examples/todomvc/reducers/todos.js index 8eca4f3a..d972d145 100644 --- a/examples/todomvc/reducers/todos.js +++ b/examples/todomvc/reducers/todos.js @@ -3,6 +3,7 @@ import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_CO const initialState = [{ text: 'Use Redux', completed: false, + modified: new Date(), id: 0 }]; @@ -12,6 +13,7 @@ export default function todos(state = initialState, action) { return [{ id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, completed: false, + modified: new Date(), text: action.text }, ...state]; @@ -23,21 +25,21 @@ export default function todos(state = initialState, action) { case EDIT_TODO: return state.map(todo => todo.id === action.id ? - Object.assign({}, todo, { text: action.text }) : + Object.assign({}, todo, { text: action.text, modified: new Date() }) : todo ); case COMPLETE_TODO: return state.map(todo => todo.id === action.id ? - Object.assign({}, todo, { completed: !todo.completed }) : + Object.assign({}, todo, { completed: !todo.completed, modified: new Date() }) : todo ); case COMPLETE_ALL: const areAllMarked = state.every(todo => todo.completed); return state.map(todo => Object.assign({}, todo, { - completed: !areAllMarked + completed: !areAllMarked, modified: new Date() })); case CLEAR_COMPLETED: diff --git a/examples/todomvc/store/configureStore.js b/examples/todomvc/store/configureStore.js index b483cf42..5c7daab8 100644 --- a/examples/todomvc/store/configureStore.js +++ b/examples/todomvc/store/configureStore.js @@ -4,7 +4,7 @@ import * as actionCreators from '../actions'; export default function configureStore(preloadedState) { const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ && - window.__REDUX_DEVTOOLS_EXTENSION__({ actionCreators }); + window.__REDUX_DEVTOOLS_EXTENSION__({ actionCreators, serialize: true, trace: true }); if (!enhancer) { console.warn('Install Redux DevTools Extension to inspect the app state: ' + 'https://github.com/zalmoxisus/redux-devtools-extension#installation') @@ -15,8 +15,7 @@ export default function configureStore(preloadedState) { if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { - const nextReducer = require('../reducers'); - store.replaceReducer(nextReducer); + store.replaceReducer(require('../reducers').default) }); } diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js index 41800a27..f80d059d 100644 --- a/examples/todomvc/webpack.config.js +++ b/examples/todomvc/webpack.config.js @@ -2,7 +2,8 @@ var path = require('path'); var webpack = require('webpack'); module.exports = { - devtool: 'cheap-module-eval-source-map', + mode: 'development', + devtool: 'source-map', entry: [ 'webpack-hot-middleware/client', './index' @@ -13,19 +14,16 @@ module.exports = { publicPath: '/static/' }, plugins: [ - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin() + new webpack.HotModuleReplacementPlugin() ], module: { - loaders: [{ + rules: [{ test: /\.js$/, - loaders: ['babel'], - exclude: /node_modules/, - include: __dirname + loaders: ['babel-loader'], + exclude: /node_modules/ }, { test: /\.css?$/, - loaders: ['style', 'raw'], + loaders: ['style-loader', 'raw-loader'], include: __dirname }] } diff --git a/npm-package/index.d.ts b/npm-package/index.d.ts index 7169659d..738b8706 100644 --- a/npm-package/index.d.ts +++ b/npm-package/index.d.ts @@ -154,8 +154,17 @@ export interface EnhancerOptions { */ test?: boolean; }; + /** + * Set to true or a stacktrace-returning function to record call stack traces for dispatched actions. + * Defaults to false. + */ + trace?: boolean | ((action: A) => string); + /** + * The maximum number of stack trace entries to record per action. Defaults to 10. + */ + traceLimit?: number; } -export function composeWithDevTools(...funcs: Function[]): StoreEnhancer; +export function composeWithDevTools(...funcs: Array>): StoreEnhancer; export function composeWithDevTools(options: EnhancerOptions): typeof compose; export function devToolsEnhancer(options: EnhancerOptions): StoreEnhancer; diff --git a/npm-package/package.json b/npm-package/package.json index b69e70c7..17f92c3e 100644 --- a/npm-package/package.json +++ b/npm-package/package.json @@ -1,6 +1,6 @@ { "name": "redux-devtools-extension", - "version": "2.13.5", + "version": "2.13.8", "description": "Wrappers for Redux DevTools Extension.", "main": "index.js", "repository": { diff --git a/package.json b/package.json index df070f06..2507d45a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "2.15.4", + "version": "2.17.1", "name": "remotedev-redux-devtools-extension", "description": "Redux Developer Tools for debugging application state changes.", "scripts": { @@ -69,10 +69,10 @@ "selenium-webdriver": "^3.0.1", "sinon-chrome": "^1.1.2", "style-loader": "^0.18.2", - "webpack": "^3.1.0" + "webpack": "^4.27.1" }, "dependencies": { - "jsan": "^3.1.11", + "jsan": "^3.1.13", "lodash": "^4.17.2", "react": "^15.4.1", "react-dom": "^15.4.1", @@ -81,11 +81,12 @@ "react-redux": "^4.4.5", "redux": "^3.5.2", "redux-devtools": "^3.4.1", - "redux-devtools-instrument": "^1.9.0", - "remotedev-app": "^0.10.8", + "redux-devtools-instrument": "^1.9.6", + "remotedev-app": "^0.10.13-beta", "remotedev-monitor-components": "^0.0.5", - "remotedev-serialize": "^0.1.2", + "remotedev-serialize": "^0.1.8", "remotedev-slider": "^1.1.1", - "remotedev-utils": "0.0.1" + "remotedev-utils": "0.0.1", + "terser-webpack-plugin": "^1.1.0" } } diff --git a/src/app/api/filters.js b/src/app/api/filters.js index 506b6ebf..7be43f80 100644 --- a/src/app/api/filters.js +++ b/src/app/api/filters.js @@ -9,24 +9,30 @@ export const FilterState = { export function getLocalFilter(config) { if (config.actionsBlacklist || config.actionsWhitelist) { return { - whitelist: config.actionsWhitelist && config.actionsWhitelist.join('|'), - blacklist: config.actionsBlacklist && config.actionsBlacklist.join('|') + whitelist: Array.isArray(config.actionsWhitelist) ? config.actionsWhitelist.join('|') : config.actionsWhitelist, + blacklist: Array.isArray(config.actionsBlacklist) ? config.actionsBlacklist.join('|') : config.actionsBlacklist }; } return undefined; } +export const noFiltersApplied = (localFilter) => ( + // !predicate && + !localFilter && (!window.devToolsOptions || !window.devToolsOptions.filter || + window.devToolsOptions.filter === FilterState.DO_NOT_FILTER) +); + export function isFiltered(action, localFilter) { if ( - !localFilter || window.devToolsOptions && - window.devToolsOptions.filter === FilterState.DO_NOT_FILTER || - typeof action.type.match !== 'function' + noFiltersApplied(localFilter) || + typeof action !== 'string' && typeof action.type.match !== 'function' ) return false; - const { whitelist, blacklist } = localFilter || window.devToolsOptions; + const { whitelist, blacklist } = localFilter || window.devToolsOptions || {}; + const actionType = action.type || action; return ( - whitelist && !action.type.match(whitelist) || - blacklist && action.type.match(blacklist) + whitelist && !actionType.match(whitelist) || + blacklist && actionType.match(blacklist) ); } @@ -48,10 +54,7 @@ export function filterState(state, type, localFilter, stateSanitizer, actionSani if (type === 'ACTION') return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1); else if (type !== 'STATE') return state; - if ( - predicate || localFilter || window.devToolsOptions && - window.devToolsOptions.filter && window.devToolsOptions.filter !== FilterState.DO_NOT_FILTER - ) { + if (predicate || !noFiltersApplied(localFilter)) { const filteredStagedActionIds = []; const filteredComputedStates = []; const sanitizedActionsById = actionSanitizer && {}; @@ -104,8 +107,7 @@ export function startingFrom( const index = stagedActionIds.indexOf(sendingActionId); if (index === -1) return state; - const shouldFilter = predicate || localFilter || - window.devToolsOptions.filter !== FilterState.DO_NOT_FILTER; + const shouldFilter = predicate || !noFiltersApplied(localFilter); const filteredStagedActionIds = shouldFilter ? [0] : stagedActionIds; const actionsById = state.actionsById; const computedStates = state.computedStates; diff --git a/src/app/api/importState.js b/src/app/api/importState.js index 947dcc91..83b1a758 100644 --- a/src/app/api/importState.js +++ b/src/app/api/importState.js @@ -11,7 +11,9 @@ export default function importState(state, { deserializeState, deserializeAction let parse = jsan.parse; if (serialize) { if (serialize.immutable) { - parse = v => jsan.parse(v, seralizeImmutable(serialize.immutable, serialize.refs).reviver); + parse = v => jsan.parse(v, seralizeImmutable( + serialize.immutable, serialize.refs, serialize.replacer, serialize.reviver + ).reviver); } else if (serialize.reviver) { parse = v => jsan.parse(v, serialize.reviver); } diff --git a/src/app/api/index.js b/src/app/api/index.js index c596b51e..ff2d8c44 100644 --- a/src/app/api/index.js +++ b/src/app/api/index.js @@ -47,11 +47,14 @@ export function getSeralizeParameter(config, param) { if (serialize) { if (serialize === true) return { options: true }; if (serialize.immutable) { - const immutableSerializer = seralizeImmutable(serialize.immutable, serialize.refs); + const immutableSerializer = seralizeImmutable( + serialize.immutable, serialize.refs, serialize.replacer, serialize.reviver + ); return { replacer: immutableSerializer.replacer, reviver: immutableSerializer.reviver, - options: serialize.options || true + options: typeof serialize.options === 'object' ? + { ...immutableSerializer.options, ...serialize.options } : immutableSerializer.options }; } if (!serialize.replacer && !serialize.reviver) return { options: serialize.options }; @@ -71,11 +74,42 @@ function post(message) { window.postMessage(message, '*'); } -function amendActionType(action) { - if (typeof action === 'string') return { action: { type: action }, timestamp: Date.now() }; - if (!action.type) return { action: { type: 'update' }, timestamp: Date.now() }; - if (action.action) return action; - return { action, timestamp: Date.now() }; +function getStackTrace(config, toExcludeFromTrace) { + if (!config.trace) return undefined; + if (typeof config.trace === 'function') return config.trace(); + + let stack; + let extraFrames = 0; + let prevStackTraceLimit; + const traceLimit = config.traceLimit; + const error = Error(); + if (Error.captureStackTrace) { + if (Error.stackTraceLimit < traceLimit) { + prevStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = traceLimit; + } + Error.captureStackTrace(error, toExcludeFromTrace); + } else { + extraFrames = 3; + } + stack = error.stack; + if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit; + if (extraFrames || typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit) { + const frames = stack.split('\n'); + if (frames.length > traceLimit) { + stack = frames.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0)).join('\n'); + } + } + return stack; +} + +function amendActionType(action, config, toExcludeFromTrace) { + let timestamp = Date.now(); + let stack = getStackTrace(config, toExcludeFromTrace); + if (typeof action === 'string') return { action: { type: action }, timestamp, stack }; + if (!action.type) return { action: { type: 'update' }, timestamp, stack }; + if (action.action) return stack ? { stack, ...action } : action; + return { action, timestamp, stack }; } export function toContentScript(message, serializeState, serializeAction) { @@ -102,7 +136,7 @@ export function sendMessage(action, state, config, instanceId, name) { if (typeof config !== 'object') { // Legacy: sending actions not from connected part config = {}; // eslint-disable-line no-param-reassign - if (action) amendedAction = amendActionType(action); + if (action) amendedAction = amendActionType(action, config, sendMessage); } const message = { type: action ? 'ACTION' : 'STATE', @@ -223,7 +257,7 @@ export function connect(preConfig) { } } else if (config.actionSanitizer) amendedAction = config.actionSanitizer(action); - amendedAction = amendActionType(amendedAction); + amendedAction = amendActionType(amendedAction, config, send); if (latency) { delayedActions.push(amendedAction); delayedStates.push(amendedState); @@ -280,7 +314,7 @@ export function connect(preConfig) { export function updateStore(stores) { return function(newStore, instanceId) { /* eslint-disable no-console */ - console.warn('`devToolsExtension.updateStore` is deprecated, remove it and just use ' + + console.warn('`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' + '`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead of the extension\'s store enhancer: ' + 'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'); /* eslint-enable no-console */ diff --git a/src/app/containers/App.js b/src/app/containers/App.js index 1787bf73..9d7847a1 100644 --- a/src/app/containers/App.js +++ b/src/app/containers/App.js @@ -31,12 +31,26 @@ class App extends Component { openWindow = (position) => { chrome.runtime.sendMessage({ type: 'OPEN', position }); }; + openOptionsPage = () => { + if (navigator.userAgent.indexOf('Firefox') !== -1) { + chrome.runtime.sendMessage({ type: 'OPEN_OPTIONS' }); + } else { + chrome.runtime.openOptionsPage(); + } + }; render() { const { monitor, position, togglePersist, dispatcherIsOpen, sliderIsOpen, options, liftedState } = this.props; + if (!position && (!options || !options.features)) { + return ( +

+ No store found. Make sure to follow the instructions. +
+ ); + } const features = options.features || {}; return (
@@ -119,10 +133,10 @@ class App extends Component { onClick={() => { this.openWindow('remote'); }} >Remote } - {chrome.runtime.openOptionsPage && + {(chrome.runtime.openOptionsPage || navigator.userAgent.indexOf('Firefox') !== -1) && }
diff --git a/src/app/middlewares/api.js b/src/app/middlewares/api.js index 382c9248..0d4a172c 100644 --- a/src/app/middlewares/api.js +++ b/src/app/middlewares/api.js @@ -12,10 +12,11 @@ const connections = { panel: {}, monitor: {} }; +const chunks = {}; let monitors = 0; let isMonitored = false; -const getId = sender => sender.tab ? sender.tab.id : sender.id; +const getId = (sender, name) => sender.tab ? sender.tab.id : name || sender.id; function toMonitors(action, tabId, verbose) { Object.keys(connections.monitor).forEach(id => { @@ -86,6 +87,10 @@ function messaging(request, sender, sendResponse) { } return; } + if (request.type === 'OPEN_OPTIONS') { + chrome.runtime.openOptionsPage(); + return; + } if (request.type === 'GET_OPTIONS') { window.syncOptions.get(options => { sendResponse({ options }); @@ -122,8 +127,21 @@ function messaging(request, sender, sendResponse) { } const action = { type: UPDATE_STATE, request, id: tabId }; + const instanceId = `${tabId}/${request.instanceId}`; + if (request.split) { + if (request.split === 'start') { + chunks[instanceId] = request; + return; + } + if (request.split === 'chunk') { + chunks[instanceId][request.chunk[0]] = (chunks[instanceId][request.chunk[0]] || '') + request.chunk[1]; + return; + } + action.request = chunks[instanceId]; + delete chunks[instanceId]; + } if (request.instanceId) { - action.request.instanceId = `${tabId}/${request.instanceId}`; + action.request.instanceId = instanceId; } window.store.dispatch(action); @@ -137,8 +155,8 @@ function messaging(request, sender, sendResponse) { function disconnect(type, id, listener) { return function disconnectListener() { const p = connections[type][id]; - if (listener) p.onMessage.removeListener(listener); - p.onDisconnect.removeListener(disconnectListener); + if (listener && p) p.onMessage.removeListener(listener); + if (p) p.onDisconnect.removeListener(disconnectListener); delete connections[type][id]; if (type === 'tab') { if (!window.store.getState().persistStates) { @@ -164,7 +182,10 @@ function onConnect(port) { connections.tab[id] = port; listener = msg => { if (msg.name === 'INIT_INSTANCE') { - if (typeof id === 'number') chrome.pageAction.show(id); + if (typeof id === 'number') { + chrome.pageAction.show(id); + chrome.pageAction.setIcon({tabId: id, path: 'img/logo/38x38.png'}); + } if (isMonitored) port.postMessage({ type: 'START' }); const state = window.store.getState(); @@ -186,8 +207,8 @@ function onConnect(port) { }; port.onMessage.addListener(listener); port.onDisconnect.addListener(disconnect('tab', id, listener)); - } else if (port.name === 'monitor') { - id = getId(port.sender); + } else if (port.name && port.name.indexOf('monitor') === 0) { + id = getId(port.sender, port.name); connections.monitor[id] = port; monitorInstances(true); monitors++; diff --git a/src/app/middlewares/instanceSelector.js b/src/app/middlewares/instanceSelector.js new file mode 100644 index 00000000..6dc15040 --- /dev/null +++ b/src/app/middlewares/instanceSelector.js @@ -0,0 +1,35 @@ +import { SELECT_INSTANCE, UPDATE_STATE } from 'remotedev-app/lib/constants/actionTypes'; + +function selectInstance(tabId, store, next) { + const instances = store.getState().instances; + if (instances.current === 'default') return; + const connections = instances.connections[tabId]; + if (connections && connections.length === 1) { + next({ type: SELECT_INSTANCE, selected: connections[0] }); + } +} + +function getCurrentTabId(next) { + chrome.tabs.query({ + active: true, + lastFocusedWindow: true + }, tabs => { + const tab = tabs[0]; + if (!tab) return; + next(tab.id); + }); +} + +export default function popupSelector(store) { + return next => action => { + const result = next(action); + if (action.type === UPDATE_STATE) { + if (chrome.devtools && chrome.devtools.inspectedWindow) { + selectInstance(chrome.devtools.inspectedWindow.tabId, store, next); + } else { + getCurrentTabId(tabId => selectInstance(tabId, store, next)); + } + } + return result; + }; +} diff --git a/src/app/middlewares/popupSelector.js b/src/app/middlewares/popupSelector.js deleted file mode 100644 index b9b4ba51..00000000 --- a/src/app/middlewares/popupSelector.js +++ /dev/null @@ -1,25 +0,0 @@ -import { SELECT_INSTANCE } from 'remotedev-app/lib/constants/actionTypes'; - -export default function popupSelector(store) { - let autoselected = false; - return next => action => { - const result = next(action); - if (!autoselected) { - chrome.tabs.query({ - active: true, - lastFocusedWindow: true - }, tabs => { - const instances = store.getState().instances; - if (instances.current === 'default') return; - autoselected = true; - const tab = tabs[0]; - if (!tab) return; - const connections = instances.connections[tab.id]; - if (connections && connections.length === 1) { - next({ type: SELECT_INSTANCE, selected: connections[0] }); - } - }); - } - return result; - }; -} diff --git a/src/app/stores/enhancerStore.js b/src/app/stores/enhancerStore.js index 4117a766..e3881dce 100644 --- a/src/app/stores/enhancerStore.js +++ b/src/app/stores/enhancerStore.js @@ -12,7 +12,9 @@ export default function configureStore(next, monitorReducer, config) { instrument( monitorReducer, { - maxAge: config.maxAge || window.devToolsOptions.maxAge || 50, + maxAge: config.maxAge, + trace: config.trace, + traceLimit: config.traceLimit, shouldCatchErrors: config.shouldCatchErrors || window.shouldCatchErrors, shouldHotReload: config.shouldHotReload, shouldRecordChanges: config.shouldRecordChanges, diff --git a/src/app/stores/windowStore.js b/src/app/stores/windowStore.js index b5f18307..a779dcd4 100644 --- a/src/app/stores/windowStore.js +++ b/src/app/stores/windowStore.js @@ -4,19 +4,21 @@ import exportState from 'remotedev-app/lib/middlewares/exportState'; import api from 'remotedev-app/lib/middlewares/api'; import { CONNECT_REQUEST } from 'remotedev-app/lib/constants/socketActionTypes'; import syncStores from '../middlewares/windowSync'; -import popupSelector from '../middlewares/popupSelector'; +import instanceSelector from '../middlewares/instanceSelector'; import rootReducer from '../reducers/window'; export default function configureStore(baseStore, position, preloadedState) { let enhancer; const middlewares = [exportState, api, syncStores(baseStore), persist(position)]; - if (position === '#popup') middlewares.push(popupSelector); + if (!position || position === '#popup') { // select current tab instance for devPanel and pageAction + middlewares.push(instanceSelector); + } if (process.env.NODE_ENV === 'production') { enhancer = applyMiddleware(...middlewares); } else { enhancer = compose( applyMiddleware(...middlewares), - window.devToolsExtension ? window.devToolsExtension() : noop => noop + window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : noop => noop ); } const store = createStore(rootReducer, preloadedState, enhancer); diff --git a/src/assets/img/logo/gray.png b/src/assets/img/logo/gray.png new file mode 100644 index 00000000..2787e3c3 Binary files /dev/null and b/src/assets/img/logo/gray.png differ diff --git a/src/browser/extension/chromeAPIMock.js b/src/browser/extension/chromeAPIMock.js index c43985b3..8d2b6563 100644 --- a/src/browser/extension/chromeAPIMock.js +++ b/src/browser/extension/chromeAPIMock.js @@ -3,11 +3,13 @@ window.isElectron = window.navigator && window.navigator.userAgent.indexOf('Electron') !== -1; +const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; + // Background page only if ( window.isElectron && location.pathname === '/_generated_background_page.html' || - navigator.userAgent.indexOf('Firefox') !== -1 + isFirefox ) { chrome.runtime.onConnectExternal = { addListener() {} @@ -84,3 +86,7 @@ if (window.isElectron) { return originSendMessage(...arguments); }; } + +if (isFirefox) { + chrome.storage.sync = chrome.storage.local; +} diff --git a/src/browser/extension/devtools/index.js b/src/browser/extension/devtools/index.js index 6b1dcf2c..b8e17f1c 100644 --- a/src/browser/extension/devtools/index.js +++ b/src/browser/extension/devtools/index.js @@ -1,3 +1,14 @@ -chrome.devtools.panels.create( - 'Redux', 'img/logo/scalable.png', 'devpanel.html', function() {} -); +function createPanel(url) { + chrome.devtools.panels.create( + 'Redux', 'img/logo/scalable.png', url, function() {} + ); +} + +if (chrome.runtime.getBackgroundPage) { + // Check if the background page's object is accessible (not in incognito) + chrome.runtime.getBackgroundPage(background => { + createPanel(background ? 'window.html' : 'devpanel.html'); + }); +} else { + createPanel('devpanel.html'); +} diff --git a/src/browser/extension/inject/contentScript.js b/src/browser/extension/inject/contentScript.js index fab54d11..235834c3 100644 --- a/src/browser/extension/inject/contentScript.js +++ b/src/browser/extension/inject/contentScript.js @@ -1,5 +1,8 @@ -import { injectOptions, isAllowed } from '../options/syncOptions'; +import { injectOptions, getOptionsFromBg, isAllowed } from '../options/syncOptions'; const source = '@devtools-extension'; +const pageSource = '@devtools-page'; +// Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts +const maxChromeMsgSize = 32 * 1024 * 1024; let connected = false; let bg; @@ -48,6 +51,36 @@ function tryCatch(fn, args) { try { return fn(args); } catch (err) { + if (err.message === 'Message length exceeded maximum allowed length.') { + const instanceId = args.instanceId; + const newArgs = { split: 'start' }; + const toSplit = []; + let size = 0; + let arg; + Object.keys(args).map(key => { + arg = args[key]; + if (typeof arg === 'string') { + size += arg.length; + if (size > maxChromeMsgSize) { + toSplit.push([key, arg]); + return; + } + } + newArgs[key] = arg; + }); + fn(newArgs); + for (let i = 0; i < toSplit.length; i++) { + for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) { + fn({ + instanceId, + source: pageSource, + split: 'chunk', + chunk: [toSplit[i][0], toSplit[i][1].substr(j, maxChromeMsgSize)] + }); + } + } + return fn({ instanceId, source: pageSource, split: 'end' }); + } handleDisconnect(); /* eslint-disable no-console */ if (process.env.NODE_ENV !== 'production') console.error('Failed to send message', err); @@ -58,6 +91,7 @@ function tryCatch(fn, args) { function send(message) { if (!connected) connect(); if (message.type === 'INIT_INSTANCE') { + getOptionsFromBg(); bg.postMessage({ name: 'INIT_INSTANCE', instanceId: message.instanceId }); } else { bg.postMessage({ name: 'RELAY', message }); @@ -69,7 +103,7 @@ function handleMessages(event) { if (!isAllowed()) return; if (!event || event.source !== window || typeof event.data !== 'object') return; const message = event.data; - if (message.source !== '@devtools-page') return; + if (message.source !== pageSource) return; if (message.type === 'DISCONNECT') { if (bg) { bg.disconnect(); diff --git a/src/browser/extension/inject/index.js b/src/browser/extension/inject/index.js index c48712c0..c380d216 100644 --- a/src/browser/extension/inject/index.js +++ b/src/browser/extension/inject/index.js @@ -12,5 +12,5 @@ chrome.runtime.sendMessage(window.devToolsExtensionID, { type: 'GET_OPTIONS' }, } window.devToolsOptions = response.options; - window.devToolsExtension.notifyErrors(); + window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors(); }); diff --git a/src/browser/extension/inject/pageScript.js b/src/browser/extension/inject/pageScript.js index 89bfbac5..77886b5e 100644 --- a/src/browser/extension/inject/pageScript.js +++ b/src/browser/extension/inject/pageScript.js @@ -4,7 +4,9 @@ import createStore from '../../../app/stores/createStore'; import configureStore, { getUrlParam } from '../../../app/stores/enhancerStore'; import { isAllowed } from '../options/syncOptions'; import Monitor from '../../../app/service/Monitor'; -import { getLocalFilter, isFiltered, filterState, startingFrom } from '../../../app/api/filters'; +import { + noFiltersApplied, getLocalFilter, isFiltered, filterState, startingFrom +} from '../../../app/api/filters'; import notifyErrors from '../../../app/api/notifyErrors'; import importState from '../../../app/api/importState'; import openWindow from '../../../app/api/openWindow'; @@ -24,7 +26,7 @@ function deprecateParam(oldParam, newParam) { /* eslint-enable no-console */ } -const devToolsExtension = function(reducer, preloadedState, config) { +const __REDUX_DEVTOOLS_EXTENSION__ = function(reducer, preloadedState, config) { /* eslint-disable no-param-reassign */ if (typeof reducer === 'object') { config = reducer; reducer = undefined; @@ -91,7 +93,7 @@ const devToolsExtension = function(reducer, preloadedState, config) { if (type === 'ACTION') { message.action = !actionSanitizer ? action : actionSanitizer(action.action, nextActionId - 1); - message.maxAge = maxAge; + message.maxAge = getMaxAge(); message.nextActionId = nextActionId; } else if (libConfig) { message.libConfig = libConfig; @@ -144,7 +146,7 @@ const devToolsExtension = function(reducer, preloadedState, config) { payload, source, instanceId, - maxAge + maxAge: getMaxAge() }, serializeState, serializeAction); }, latency); @@ -233,9 +235,29 @@ const devToolsExtension = function(reducer, preloadedState, config) { } } - function init() { - maxAge = config.maxAge || window.devToolsOptions.maxAge || 50; + const filteredActionIds = []; // simple circular buffer of non-excluded actions with fixed maxAge-1 length + const getMaxAge = (liftedAction, liftedState) => { + let m = config && config.maxAge || window.devToolsOptions.maxAge || 50; + if (!liftedAction || noFiltersApplied(localFilter) || !liftedAction.action) return m; + if (!maxAge || maxAge < m) maxAge = m; // it can be modified in process on options page + if (isFiltered(liftedAction.action, localFilter)) { + // TODO: check also predicate && !predicate(state, action) with current state + maxAge++; + } else { + filteredActionIds.push(liftedState.nextActionId); + if (filteredActionIds.length >= m) { + const stagedActionIds = liftedState.stagedActionIds; + let i = 1; + while (maxAge > m && filteredActionIds.indexOf(stagedActionIds[i]) === -1) { + maxAge--; i++; + } + filteredActionIds.shift(); + } + } + return maxAge; + }; + function init() { setListener(onMessage, instanceId); notifyErrors(() => { errorOccurred = true; @@ -274,7 +296,7 @@ const devToolsExtension = function(reducer, preloadedState, config) { if (!isAllowed(window.devToolsOptions)) return next(reducer_, initialState_, enhancer_); store = stores[instanceId] = - configureStore(next, monitor.reducer, config)(reducer_, initialState_, enhancer_); + configureStore(next, monitor.reducer, { ...config, maxAge: getMaxAge })(reducer_, initialState_, enhancer_); if (isInIframe()) setTimeout(init, 3000); else init(); @@ -284,30 +306,70 @@ const devToolsExtension = function(reducer, preloadedState, config) { }; if (!reducer) return enhance(); + /* eslint-disable no-console */ + console.warn('Creating a Redux store directly from DevTools extension is discouraged and will not be supported in future major version. For more details see: https://git.io/fphCe'); + /* eslint-enable no-console */ return createStore(reducer, preloadedState, enhance); }; // noinspection JSAnnotator -window.devToolsExtension = devToolsExtension; -window.devToolsExtension.open = openWindow; -window.devToolsExtension.updateStore = updateStore(stores); -window.devToolsExtension.notifyErrors = notifyErrors; -window.devToolsExtension.send = sendMessage; -window.devToolsExtension.listen = setListener; -window.devToolsExtension.connect = connect; -window.devToolsExtension.disconnect = disconnect; - -window.__REDUX_DEVTOOLS_EXTENSION__ = window.devToolsExtension; +window.__REDUX_DEVTOOLS_EXTENSION__ = __REDUX_DEVTOOLS_EXTENSION__; +window.__REDUX_DEVTOOLS_EXTENSION__.open = openWindow; +window.__REDUX_DEVTOOLS_EXTENSION__.updateStore = updateStore(stores); +window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors = notifyErrors; +window.__REDUX_DEVTOOLS_EXTENSION__.send = sendMessage; +window.__REDUX_DEVTOOLS_EXTENSION__.listen = setListener; +window.__REDUX_DEVTOOLS_EXTENSION__.connect = connect; +window.__REDUX_DEVTOOLS_EXTENSION__.disconnect = disconnect; + +// Deprecated +/* eslint-disable no-console */ +let varNameDeprecatedWarned; +const varNameDeprecatedWarn = () => { + if (varNameDeprecatedWarned) return; + console.warn('`window.devToolsExtension` is deprecated in favor of `window.__REDUX_DEVTOOLS_EXTENSION__`, and will be removed in next version of Redux DevTools: https://git.io/fpEJZ'); + varNameDeprecatedWarned = true; +}; +/* eslint-enable no-console */ +window.devToolsExtension = (...args) => { + varNameDeprecatedWarn(); + return __REDUX_DEVTOOLS_EXTENSION__.apply(null, args); +}; +window.devToolsExtension.open = (...args) => { + varNameDeprecatedWarn(); + return openWindow.apply(null, args); +}; +window.devToolsExtension.updateStore = (...args) => { + varNameDeprecatedWarn(); + return updateStore(stores).apply(null, args); +}; +window.devToolsExtension.notifyErrors = (...args) => { + varNameDeprecatedWarn(); + return notifyErrors.apply(null, args); +}; +window.devToolsExtension.send = (...args) => { + varNameDeprecatedWarn(); + return sendMessage.apply(null, args); +}; +window.devToolsExtension.listen = (...args) => { + varNameDeprecatedWarn(); + return setListener.apply(null, args); +}; +window.devToolsExtension.connect = (...args) => { + varNameDeprecatedWarn(); + return connect.apply(null, args); +}; +window.devToolsExtension.disconnect = (...args) => { + varNameDeprecatedWarn(); + return disconnect.apply(null, args); +}; const preEnhancer = instanceId => next => (reducer, preloadedState, enhancer) => { const store = next(reducer, preloadedState, enhancer); - // Mutate the store in order to keep the reference if (stores[instanceId]) { stores[instanceId].initialDispatch = store.dispatch; - stores[instanceId].liftedStore = store.liftedStore; - stores[instanceId].getState = store.getState; } return { @@ -322,14 +384,14 @@ const extensionCompose = (config) => (...funcs) => { return (...args) => { const instanceId = generateId(config.instanceId); return [preEnhancer(instanceId), ...funcs].reduceRight( - (composed, f) => f(composed), devToolsExtension({ ...config, instanceId })(...args) + (composed, f) => f(composed), __REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId })(...args) ); }; }; window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = (...funcs) => { if (funcs.length === 0) { - return devToolsExtension(); + return __REDUX_DEVTOOLS_EXTENSION__(); } if (funcs.length === 1 && typeof funcs[0] === 'object') { return extensionCompose(funcs[0]); diff --git a/src/browser/extension/manifest.json b/src/browser/extension/manifest.json index 6b09cba9..42e4e769 100644 --- a/src/browser/extension/manifest.json +++ b/src/browser/extension/manifest.json @@ -1,12 +1,12 @@ { - "version": "2.15.4", + "version": "2.17.1", "name": "Redux DevTools", "short_name": "Redux DevTools", "description": "Redux DevTools for debugging application's state changes.", "homepage_url": "https://github.com/zalmoxisus/redux-devtools-extension", "manifest_version": 2, "page_action": { - "default_icon": "img/logo/38x38.png", + "default_icon": "img/logo/gray.png", "default_title": "Redux DevTools", "default_popup": "window.html#popup" }, diff --git a/src/browser/extension/options/EditorGroup.js b/src/browser/extension/options/EditorGroup.js new file mode 100644 index 00000000..4cd06721 --- /dev/null +++ b/src/browser/extension/options/EditorGroup.js @@ -0,0 +1,59 @@ +import React from 'react'; + +export default ({ options, saveOption }) => { + const EditorState = { + BROWSER: 0, + EXTERNAL: 1 + }; + + return ( +
+ Editor for stack traces + +
+ saveOption('useEditor', EditorState.BROWSER)}/> + +
+ +
+ saveOption('useEditor', EditorState.EXTERNAL)}/> + + saveOption('editor', e.target.value.replace(/\W/g, ''))}/> +
+
+ +
+