Compare commits

..

No commits in common. "main" and "redux-devtools-instrument@1.10.0" have entirely different histories.

1129 changed files with 37223 additions and 61010 deletions

View File

@ -1,13 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}

View File

@ -7,5 +7,6 @@ build
coverage coverage
node_modules node_modules
__snapshots__ __snapshots__
storybook-static
.vscode/* # Ignore until TS linting is configured in redux-devtools package
packages/redux-devtools/index.d.ts

28
.eslintrc Normal file
View File

@ -0,0 +1,28 @@
{
"root": true,
"parser": "babel-eslint",
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
"globals": {
"chrome": true
},
"env": {
"es6": true,
"browser": true,
"jest": true,
"node": true
},
"rules": {
"eol-last": ["warn"],
"max-len": ["warn", { "code": 120, "ignoreComments": true }],
"quotes": ["warn", "single", "avoid-escape"],
"jsx-quotes": ["warn", "prefer-double"],
"react/prop-types": 0,
"prettier/prettier": "error"
},
"plugins": ["prettier", "react", "babel"],
"settings": {
"react": {
"version": "detect"
}
}
}

9
.gitattributes vendored
View File

@ -1 +1,8 @@
* text=auto eol=lf *.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.css text eol=lf
*.html text eol=lf
*.md text eol=lf

2
.github/FUNDING.yml vendored
View File

@ -1,2 +0,0 @@
github: Methuselah96
open_collective: redux-devtools-extension

View File

@ -1,31 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: 'ubuntu-22.04'
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Check formatting
run: pnpm run format:check
- name: Build
run: pnpm run build:all
- name: Lint
run: pnpm run lint:all
- name: Test
uses: coactions/setup-xvfb@v1
with:
run: pnpm run test:all

View File

@ -1,58 +0,0 @@
name: Release
on:
push:
branches:
- main
permissions: write-all
jobs:
release:
name: Release
runs-on: 'ubuntu-22.04'
steps:
- name: Checkout Repo
uses: actions/checkout@v6
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
version: pnpm changeset version
publish: pnpm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Archive Chrome Extension
uses: actions/upload-artifact@v7
with:
name: chrome
path: extension/chrome/dist
- name: Archive Edge Extension
uses: actions/upload-artifact@v7
with:
name: edge
path: extension/edge/dist
- name: Archive Firefox Extension
uses: actions/upload-artifact@v7
with:
name: firefox
path: extension/firefox/dist

1
.gitignore vendored
View File

@ -9,4 +9,3 @@ coverage
.idea .idea
.eslintcache .eslintcache
!packages/redux-devtools-slider-monitor/examples/todomvc/dist/index.html !packages/redux-devtools-slider-monitor/examples/todomvc/dist/index.html
.nx

View File

@ -7,8 +7,3 @@ build
coverage coverage
node_modules node_modules
__snapshots__ __snapshots__
dev
**/demo/public/**
storybook-static
.vscode/*
pnpm-lock.yaml

13
.travis.yml Normal file
View File

@ -0,0 +1,13 @@
sudo: false
language: node_js
node_js:
- 'stable'
cache:
yarn: true
directories:
- 'node_modules'
script:
- yarn build:all
- yarn lint:all
- yarn prettier:check
- yarn test:all

4
CHANGELOG.md Normal file
View File

@ -0,0 +1,4 @@
# Change log
This project adheres to [Semantic Versioning](http://semver.org/).
See all notable changes on [Releases](https://github.com/gaearon/redux-devtools/releases) page.

View File

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

View File

@ -1,100 +1,25 @@
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/reduxjs/redux-devtools/CI) [![Build Status](https://travis-ci.org/reduxjs/redux-devtools.svg?branch=master)](https://travis-ci.org/reduxjs/redux-devtools) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=round-square)](https://github.com/reduxjs/redux-devtools/pulls)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=round-square)](https://github.com/reduxjs/redux-devtools/pulls)
[![OpenCollective](https://opencollective.com/redux-devtools-extension/backers/badge.svg)](#backers)
[![OpenCollective](https://opencollective.com/redux-devtools-extension/sponsors/badge.svg)](#sponsors)
# Redux DevTools # Redux DevTools
Developer Tools to power-up [Redux](https://redux.js.org/) development workflow or any other architecture which handles the state change (see [integrations](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Integrations.md)). Developer Tools to power-up [Redux](https://github.com/reactjs/redux) development workflow or any other architecture which handles the state change (see [integrations](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Integrations.md)).
It can be used as a browser extension (for [Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd), [Edge](https://microsoftedge.microsoft.com/addons/detail/redux-devtools/nnkgneoiohoecpdiaponcejilbhhikei) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/)), as [a standalone app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) or as [a React component](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools) integrated in the client app. It can be used as a browser extension (for [Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd), [Edge](https://microsoftedge.microsoft.com/addons/detail/redux-devtools/nnkgneoiohoecpdiaponcejilbhhikei) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/)), as [a standalone app](https://github.com/zalmoxisus/remotedev-app) or as [a React component](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools) integrated in the client app.
![image](https://user-images.githubusercontent.com/7957859/48663602-3aac4900-ea9b-11e8-921f-97059cbb599c.png) ![image](https://user-images.githubusercontent.com/7957859/48663602-3aac4900-ea9b-11e8-921f-97059cbb599c.png)
## Documentation > Note that this repository is work in progress for [the monorepo](https://github.com/reduxjs/redux-devtools/issues/412), which will merge all the packages. Please refer to [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) and [Redux DevTools package](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools).
- [Browser Extension Installation and Configuration](https://github.com/reduxjs/redux-devtools/tree/main/extension#installation) ### Documentation
- [Browser Extension Installation and Configuration](https://github.com/zalmoxisus/redux-devtools-extension#installation)
- [Manual Integration as a React Component](./docs/Walkthrough.md#manual-integration) - [Manual Integration as a React Component](./docs/Walkthrough.md#manual-integration)
- [Extension Options (Arguments)](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md) - [Extension Options (Arguments)](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API/Arguments.md)
- [Extension Methods (Advanced API)](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Methods.md) - [Extension Methods (Advanced API)](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API/Methods.md)
- [Remote monitoring](./docs/Integrations/Remote.md) - [Remote monitoring](./docs/Integrations/Remote.md)
- [Troubleshooting](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Troubleshooting.md) - [Troubleshooting](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/Troubleshooting.md)
- [Recipes](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Recipes.md) - [Recipes](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/Recipes.md)
- [FAQ](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/FAQ.md) - [FAQ](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/FAQ.md)
## Development
This is a monorepo powered by [pnpm](https://pnpm.io/). [Install pnpm](https://pnpm.io/installation) and run `pnpm install` to get started. Each package's dependencies need to be built before the package itself can be built. You can either build all the packages (i.e., `pnpm run build:all`) or use pnpm workspace commands to build only the packages necessary for the packages you're working on (i.e., `pnpm --filter "remotedev-redux-devtools-extension" build`).
## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/redux-devtools-extension#backer)]
<a href="https://opencollective.com/redux-devtools-extension/backer/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/1/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/2/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/3/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/4/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/5/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/6/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/7/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/8/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/9/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/10/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/11/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/12/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/13/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/14/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/15/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/16/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/17/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/18/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/19/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/20/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/21/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/22/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/23/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/24/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/25/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/26/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/27/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/28/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/29/avatar.svg"></a>
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/redux-devtools-extension#sponsor)]
<a href="https://opencollective.com/redux-devtools-extension/sponsor/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/1/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/2/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/3/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/4/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/5/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/6/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/7/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/8/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/9/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/10/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/11/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/12/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/13/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/14/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/15/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/16/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/17/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/18/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/19/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/20/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/21/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/22/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/23/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/24/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/25/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/26/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/27/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/28/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/29/avatar.svg"></a>
### License ### License

14
babel.config.js Executable file
View File

@ -0,0 +1,14 @@
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-flow'],
plugins: [
[
'@babel/plugin-transform-runtime',
{
regenerator: true,
},
],
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-export-default-from',
],
};

View File

@ -1,6 +1,6 @@
## Remote monitoring ## Remote monitoring
By installing [`@redux-devtools/cli`](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-cli#usage), starting the server server and launching the Redux DevTools app (`redux-devtools --open`), you can connect any remote application, even not javascript. There are some integrations for javascript like [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools) and [remotedev](https://github.com/zalmoxisus/remotedev), but the plan is to deprecate them and support it out of the box from the extension without a websocket server. It is more useful for non-js apps. By installing [`redux-devtools-cli`](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-cli#usage), starting the server server and launching the Redux DevTools app (`redux-devtools --open`), you can connect any remote application, even not javascript. There are some integrations for javascript like [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools) and [remotedev](https://github.com/zalmoxisus/remotedev), but the plan is to deprecate them and support it out of the box from the extension without a websocket server. It is more useful for non-js apps.
### WebSocket Clients ### WebSocket Clients

View File

@ -2,7 +2,7 @@
## Browser Extension ## Browser Extension
If you dont want to bother with installing Redux DevTools and integrating it into your project, consider using [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools/tree/master/extension) for Chrome and Firefox. It provides access to the most popular monitors, is easy to configure to filter actions, and doesnt require installing any packages. If you dont want to bother with installing Redux DevTools and integrating it into your project, consider using [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) for Chrome and Firefox. It provides access to the most popular monitors, is easy to configure to filter actions, and doesnt require installing any packages.
## Manual Integration ## Manual Integration
@ -12,14 +12,14 @@ Its more steps, but you will have full control over monitors and their config
### Installation ### Installation
``` ```
npm install --save-dev @redux-devtools/core npm install --save-dev redux-devtools
``` ```
Youll also likely want to install some monitors: Youll also likely want to install some monitors:
``` ```
npm install --save-dev @redux-devtools/log-monitor npm install --save-dev redux-devtools-log-monitor
npm install --save-dev @redux-devtools/dock-monitor npm install --save-dev redux-devtools-dock-monitor
``` ```
### Usage ### Usage
@ -34,11 +34,11 @@ Somewhere in your project, create a `DevTools` component by passing a `monitor`
import React from 'react'; import React from 'react';
// Exported from redux-devtools // Exported from redux-devtools
import { createDevTools } from '@redux-devtools/core'; import { createDevTools } from 'redux-devtools';
// Monitors are separate packages, and you can make a custom one // Monitors are separate packages, and you can make a custom one
import LogMonitor from '@redux-devtools/log-monitor'; import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from '@redux-devtools/dock-monitor'; import DockMonitor from 'redux-devtools-dock-monitor';
// createDevTools takes a monitor and produces a DevTools component // createDevTools takes a monitor and produces a DevTools component
const DevTools = createDevTools( const DevTools = createDevTools(
@ -52,7 +52,7 @@ const DevTools = createDevTools(
defaultIsVisible={true} defaultIsVisible={true}
> >
<LogMonitor theme="tomorrow" /> <LogMonitor theme="tomorrow" />
</DockMonitor>, </DockMonitor>
); );
export default DevTools; export default DevTools;
@ -88,7 +88,7 @@ const enhancer = compose(
// Middleware you want to use in development: // Middleware you want to use in development:
applyMiddleware(d1, d2, d3), applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose // Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(), DevTools.instrument()
); );
export default function configureStore(initialState) { export default function configureStore(initialState) {
@ -100,8 +100,8 @@ export default function configureStore(initialState) {
if (module.hot) { if (module.hot) {
module.hot.accept('../reducers', () => module.hot.accept('../reducers', () =>
store.replaceReducer( store.replaceReducer(
require('../reducers') /*.default if you use Babel 6+ */, require('../reducers') /*.default if you use Babel 6+ */
), )
); );
} }
@ -113,7 +113,7 @@ If youd like, you may add another store enhancer called `persistState()`. It
```js ```js
// ... // ...
import { persistState } from '@redux-devtools/core'; import { persistState } from 'redux-devtools';
const enhancer = compose( const enhancer = compose(
// Middleware you want to use in development: // Middleware you want to use in development:
@ -121,7 +121,7 @@ const enhancer = compose(
// Required! Enable Redux DevTools with the monitors you chose // Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(), DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions // Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey()), persistState(getDebugSessionKey())
); );
function getDebugSessionKey() { function getDebugSessionKey() {
@ -190,7 +190,7 @@ export default function configureStore(initialState) {
```js ```js
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import { persistState } from '@redux-devtools/core'; import { persistState } from 'redux-devtools';
import rootReducer from '../reducers'; import rootReducer from '../reducers';
import DevTools from '../containers/DevTools'; import DevTools from '../containers/DevTools';
@ -200,7 +200,7 @@ const enhancer = compose(
// Required! Enable Redux DevTools with the monitors you chose // Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(), DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions // Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey()), persistState(getDebugSessionKey())
); );
function getDebugSessionKey() { function getDebugSessionKey() {
@ -219,8 +219,8 @@ export default function configureStore(initialState) {
if (module.hot) { if (module.hot) {
module.hot.accept('../reducers', () => module.hot.accept('../reducers', () =>
store.replaceReducer( store.replaceReducer(
require('../reducers') /*.default if you use Babel 6+ */, require('../reducers') /*.default if you use Babel 6+ */
), )
); );
} }
@ -333,7 +333,7 @@ render(
<Provider store={store}> <Provider store={store}>
<App /> <App />
</Provider>, </Provider>,
document.getElementById('root'), document.getElementById('root')
); );
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
@ -353,7 +353,7 @@ export default function showDevTools(store) {
const popup = window.open( const popup = window.open(
null, null,
'Redux DevTools', 'Redux DevTools',
'menubar=no,location=no,resizable=yes,scrollbars=no,status=no', 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no'
); );
// Reload in case it already exists // Reload in case it already exists
popup.location.reload(); popup.location.reload();
@ -362,7 +362,7 @@ export default function showDevTools(store) {
popup.document.write('<div id="react-devtools-root"></div>'); popup.document.write('<div id="react-devtools-root"></div>');
render( render(
<DevTools store={store} />, <DevTools store={store} />,
popup.document.getElementById('react-devtools-root'), popup.document.getElementById('react-devtools-root')
); );
}, 10); }, 10);
} }

View File

@ -1,5 +0,0 @@
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
export default defineConfig([eslint.configs.recommended, eslintConfigPrettier]);

View File

@ -1,41 +0,0 @@
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jest from 'eslint-plugin-jest';
import eslintConfigPrettier from 'eslint-config-prettier';
export default defineConfig([
{
files: ['test/**/*.js', 'test/**/*.jsx'],
...eslint.configs.recommended,
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
...react.configs.flat.recommended,
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
settings: {
react: {
version: 'detect',
},
},
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
...reactHooks.configs.flat.recommended,
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
...jest.configs['flat/recommended'],
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
...jest.configs['jest/style'],
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
...eslintConfigPrettier,
},
]);

View File

@ -1,57 +0,0 @@
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (tsconfigRootDir, files = ['**/*.ts'], project = true) =>
defineConfig([
{
files,
...eslint.configs.recommended,
},
...tseslint.configs.recommendedTypeChecked.map((config) => ({
files,
...config,
})),
...tseslint.configs.stylisticTypeChecked.map((config) => ({
files,
...config,
})),
{
files,
languageOptions: {
parserOptions: {
project,
tsconfigRootDir,
},
},
},
{
files,
...eslintConfigPrettier,
},
{
files,
rules: {
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/prefer-for-of': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/class-literal-property-style': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
'@typescript-eslint/no-duplicate-type-constituents': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/prefer-function-type': 'off',
},
},
]);

View File

@ -1,66 +0,0 @@
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import jest from 'eslint-plugin-jest';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (tsconfigRootDir) =>
defineConfig([
{
files: ['test/**/*.ts'],
...eslint.configs.recommended,
},
...tseslint.configs.recommendedTypeChecked.map((config) => ({
files: ['test/**/*.ts'],
...config,
})),
...tseslint.configs.stylisticTypeChecked.map((config) => ({
files: ['test/**/*.ts'],
...config,
})),
{
files: ['test/**/*.ts'],
languageOptions: {
parserOptions: {
project: ['./tsconfig.test.json'],
tsconfigRootDir,
},
},
},
{
files: ['test/**/*.ts'],
...jest.configs['flat/recommended'],
},
{
files: ['test/**/*.ts'],
...jest.configs['jest/style'],
},
{
files: ['test/**/*.ts'],
...eslintConfigPrettier,
},
{
files: ['test/**/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/prefer-for-of': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/class-literal-property-style': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
'@typescript-eslint/no-duplicate-type-constituents': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/prefer-function-type': 'off',
},
},
]);

View File

@ -1,88 +0,0 @@
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (
tsconfigRootDir,
files = ['**/*.ts', '**/*.tsx'],
project = true,
) =>
defineConfig([
{
files,
...eslint.configs.recommended,
},
...tseslint.configs.recommendedTypeChecked.map((config) => ({
files,
...config,
})),
...tseslint.configs.stylisticTypeChecked.map((config) => ({
files,
...config,
})),
{
files,
languageOptions: {
parserOptions: {
project,
tsconfigRootDir,
},
},
},
{
files,
...react.configs.flat.recommended,
},
{
files,
settings: {
react: {
version: 'detect',
},
},
},
{
files,
...reactHooks.configs.flat.recommended,
},
{
files,
...eslintConfigPrettier,
},
{
files,
rules: {
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: {
attributes: false,
},
},
],
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/prefer-for-of': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/class-literal-property-style': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
'@typescript-eslint/no-duplicate-type-constituents': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/prefer-function-type': 'off',
'react/prop-types': 'off',
},
},
]);

View File

@ -1,84 +0,0 @@
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jest from 'eslint-plugin-jest';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (tsconfigRootDir) =>
defineConfig([
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
...eslint.configs.recommended,
},
...tseslint.configs.recommendedTypeChecked.map((config) => ({
files: ['test/**/*.ts', 'test/**/*.tsx'],
...config,
})),
...tseslint.configs.stylisticTypeChecked.map((config) => ({
files: ['test/**/*.ts', 'test/**/*.tsx'],
...config,
})),
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
languageOptions: {
parserOptions: {
project: ['./tsconfig.test.json'],
tsconfigRootDir,
},
},
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
...react.configs.flat.recommended,
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
settings: {
react: {
version: 'detect',
},
},
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
...reactHooks.configs.flat.recommended,
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
...jest.configs['flat/recommended'],
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
...jest.configs['jest/style'],
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
...eslintConfigPrettier,
},
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
rules: {
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/prefer-for-of': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/class-literal-property-style': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
'@typescript-eslint/no-duplicate-type-constituents': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/prefer-function-type': 'off',
},
},
]);

18
eslintrc.ts.base.json Normal file
View File

@ -0,0 +1,18 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
}
}

View File

@ -0,0 +1,19 @@
{
"plugins": ["jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:jest/recommended",
"plugin:jest/style",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
}
}

View File

@ -0,0 +1,30 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "react"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:react/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint",
"prettier/react"
],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
}
}

View File

@ -0,0 +1,21 @@
{
"plugins": ["jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:react/recommended",
"plugin:jest/recommended",
"plugin:jest/style",
"plugin:prettier/recommended",
"prettier/@typescript-eslint",
"prettier/react"
],
"rules": {
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
}
}

View File

@ -1,2 +0,0 @@
node_modules
dist

View File

@ -1,265 +0,0 @@
# remotedev-redux-devtools-extension
## 3.2.12
### Patch Changes
- Updated dependencies [3f90241]
- Updated dependencies [d61d31a]
- Updated dependencies [804e729]
- Updated dependencies [12849a4]
- Updated dependencies [804d6bd]
- Updated dependencies [6481386]
- @redux-devtools/instrument@3.0.0
- @redux-devtools/ui@3.0.0
- @redux-devtools/slider-monitor@7.0.0
- @redux-devtools/core@5.0.0
- @redux-devtools/app@8.0.0
- @redux-devtools/serialize@1.0.0
- @redux-devtools/utils@4.0.0
## 3.2.11
### Patch Changes
- Updated dependencies [6163276]
- @redux-devtools/app@7.0.0
- @redux-devtools/slider-monitor@6.0.0
- @redux-devtools/ui@2.0.0
## 3.2.10
### Patch Changes
- @redux-devtools/app@6.2.2
## 3.2.9
### Patch Changes
- Updated dependencies [91f21b2]
- @redux-devtools/core@4.1.1
- @redux-devtools/slider-monitor@5.1.1
- @redux-devtools/utils@3.1.1
- @redux-devtools/app@6.2.1
## 3.2.8
### Patch Changes
- Updated dependencies [6830118]
- react-json-tree@0.20.0
- @redux-devtools/app@6.2.0
- @redux-devtools/slider-monitor@6.0.0
- @redux-devtools/ui@1.4.0
- @redux-devtools/core@4.1.0
- @redux-devtools/utils@4.0.0
## 3.2.7
### Patch Changes
- b25bf13: Send state from background when monitor connects
## 3.2.6
### Patch Changes
- 50d7682: Fix DevTools from losing connection
## 3.2.5
### Patch Changes
- eb3ac09: Add logging to background service worker
## 3.2.4
### Patch Changes
- f1d6158: Fix mocking Chrome API for Electron
## 3.2.3
### Patch Changes
- fd9f950: Fix monitoring on opening panel
- e49708d: Fix manifest.json for Edge
## 3.2.1
### Patch Changes
- abd03a7: Fix: only send data to extension if DevTools are open
## 3.2.0
### Minor Changes
- 83b2c19: Upgrade to Manifest V3
## 3.1.11
### Patch Changes
- 73688e1: Fix releasing Firefox extension
## 3.1.10
### Patch Changes
- 2163bc3: Split large messages sent from background page to devpanel
## 3.1.9
### Patch Changes
- Updated dependencies [bbb1a40]
- react-json-tree@0.19.0
- @redux-devtools/slider-monitor@5.0.1
- @redux-devtools/ui@1.3.2
## 3.1.8
### Patch Changes
- 191d419: Convert d3 packages to ESM
- Updated dependencies [191d419]
- @redux-devtools/app@6.0.1
## 3.1.7
### Patch Changes
- Updated dependencies [5cfe3e5]
- Updated dependencies [decc035]
- @redux-devtools/app@6.0.0
- @redux-devtools/slider-monitor@5.0.0
- @redux-devtools/core@4.0.0
- @redux-devtools/utils@3.0.0
## 3.1.6
### Patch Changes
- Updated dependencies [158ba2c]
- @redux-devtools/app@5.0.0
## 3.1.5
### Patch Changes
- 65205f90: Replace Action<unknown> with Action<string>
- Updated dependencies [65205f90]
- @redux-devtools/app@4.0.1
- @redux-devtools/core@3.13.2
## 3.1.4
### Patch Changes
- Updated dependencies [e57bcb39]
- @redux-devtools/app@4.0.0
## 3.1.3
### Patch Changes
- bca76009: Fix missing CSS for code editor
## 3.1.2
### Patch Changes
- 64ed81b0: Fix extension in Firefox and Chrome Incognito
## 3.1.1
### Patch Changes
- d18525b5: Increase min-width of popup
- Updated dependencies [57751ff9]
- @redux-devtools/app@3.0.0
## 3.1.0
### Minor Changes
- d54adb76: Option to sort State Tree keys alphabetically
Option to disable collapsing of object keys
### Patch Changes
- @redux-devtools/app@2.2.2
## 3.0.19
### Patch Changes
- 450cde6e: Fix responsive layout
## 3.0.18
### Patch Changes
- Updated dependencies [81926f32]
- react-json-tree@0.18.0
- @redux-devtools/app@2.2.1
## 3.0.17
### Patch Changes
- 1aa6c4f7: Fix remounting root for devpanel
## 3.0.16
### Patch Changes
- 20ebf725: Remove source map from page wrap bundle
## 3.0.14
### Patch Changes
- 24f60a7a: bump min popup window width to 760px #1126 #1129
## 3.0.13
### Patch Changes
- Updated dependencies [8a7eae4]
- react-json-tree@0.17.0
- @redux-devtools/app@2.2.0
- @redux-devtools/slider-monitor@4.0.0
- @redux-devtools/ui@1.3.0
- @redux-devtools/core@3.13.0
- @redux-devtools/utils@2.0.0
## 3.0.12
### Patch Changes
- Updated dependencies [4891bf6]
- @redux-devtools/core@3.12.0
- @redux-devtools/slider-monitor@3.1.2
- @redux-devtools/utils@1.2.1
- @redux-devtools/app@2.1.4
## 3.0.11
### Patch Changes
- ab3c0e2: Avoid persisting the selected action index between sessions
- Updated dependencies [ab3c0e2]
- Updated dependencies [4c9a890]
- @redux-devtools/app@2.1.3
- react-json-tree@0.16.2
## 3.0.10
### Patch Changes
- 55cc37e: Fix filter to show state-controlled search value
- Updated dependencies [55cc37e]
- @redux-devtools/app@2.1.2

View File

@ -1,63 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015-present 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.

View File

@ -1,325 +0,0 @@
# 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)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=round-square)](http://makeapullrequest.com)
[![OpenCollective](https://opencollective.com/redux-devtools-extension/backers/badge.svg)](#backers)
[![OpenCollective](https://opencollective.com/redux-devtools-extension/sponsors/badge.svg)](#sponsors)
![Demo](https://cloud.githubusercontent.com/assets/7957859/18002950/aacb82fc-6b93-11e6-9ae9-609862c18302.png)
## Installation
### 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/docs/extensions/get-started/tutorial/hello-world#load-unpacked) `./build/extension`;
- or run it in dev mode with `npm i && npm start` and [load the extension's folder](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#load-unpacked) `./dev`.
### 2. For Firefox
- 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
- just specify `REDUX_DEVTOOLS` in [`electron-devtools-installer`](https://github.com/GPMDP/electron-devtools-installer).
### 4. For other browsers and non-browser environment
- use [`remote-redux-devtools`](https://github.com/zalmoxisus/remote-redux-devtools).
## Usage
> Note that starting from v2.7, `window.devToolsExtension` was renamed to `window.__REDUX_DEVTOOLS_EXTENSION__` / `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`.
## 1. With Redux
### 1.1 Basic store
For a basic [Redux store](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) simply add:
```diff
const store = createStore(
reducer, /* preloadedState, */
+ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
```
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-devtoolsextension-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
+ /* eslint-disable no-underscore-dangle */
const store = createStore(
reducer, /* preloadedState, */
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
+ /* eslint-enable */
```
> **Note**: Passing enhancer as last argument requires **redux@>=3.1.0**. For older versions apply it like [here](https://github.com/zalmoxisus/redux-devtools-extension/blob/v0.4.2/examples/todomvc/store/configureStore.js) or [here](https://github.com/zalmoxisus/redux-devtools-extension/blob/v0.4.2/examples/counter/store/configureStore.js#L7-L12). Don't mix the old Redux API with the new one.
> You don't need to npm install [`redux-devtools`](https://github.com/gaearon/redux-devtools) when using the extension (that's a different lib).
### 1.2 Advanced store setup
If you setup your store with [middleware and enhancers](http://redux.js.org/docs/api/applyMiddleware.html), change:
```diff
import { createStore, applyMiddleware, compose } from 'redux';
+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+ const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
- const store = createStore(reducer, /* preloadedState, */ compose(
applyMiddleware(...middleware)
));
```
> Note that when the extension is not installed, were using Redux compose here.
To specify [extensions options](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md), use it like so:
```js
const composeEnhancers =
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extensions options like name, actionsDenylist, actionsCreators, serialize...
})
: compose;
const enhancer = composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
);
const store = createStore(reducer, enhancer);
```
> [See the post for more details](https://medium.com/@zalmoxis/improve-your-development-workflow-with-redux-devtools-extension-f0379227ff83).
### 1.3 Use `@redux-devtools/extension` package from npm
To make things easier, there's an npm package to install:
```
npm install --save @redux-devtools/extension
```
and to use like so:
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/extension';
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
),
);
```
To specify [extensions options](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#windowdevtoolsextensionconfig):
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/extension';
const composeEnhancers = composeWithDevTools({
// Specify name here, actionsDenylist, actionsCreators and other options if needed
});
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
),
);
```
> There are just a [few lines of code](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/index.js) added to your bundle.
In case you don't include other enhancers and middlewares, just use `devToolsEnhancer`:
```js
import { createStore } from 'redux';
import { devToolsEnhancer } from '@redux-devtools/extension';
const store = createStore(
reducer,
/* preloadedState, */ devToolsEnhancer(),
// Specify name here, actionsDenylist, actionsCreators and other options if needed
);
```
### 1.4 Using in production
It's useful to include the extension in production as well. Usually you [can use it for development](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f).
If you want to restrict it there, use `composeWithDevToolsLogOnlyInProduction` or `devToolsEnhancerLogOnlyInProduction`:
```js
import { createStore } from 'redux';
import { devToolsEnhancerLogOnlyInProduction } from '@redux-devtools/extension';
const store = createStore(
reducer,
/* preloadedState, */ devToolsEnhancerLogOnlyInProduction(),
// options like actionSanitizer, stateSanitizer
);
```
or with middlewares and enhancers:
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevToolsLogOnlyInProduction } from '@redux-devtools/extension';
const composeEnhancers = composeWithDevToolsLogOnlyInProduction({
// options like actionSanitizer, stateSanitizer
});
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
),
);
```
> 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, import `composeWithDevToolsLogOnly` or `devToolsEnhancerLogOnly` for production environment.
If you dont want to allow the extension in production, just use `composeWithDevToolsDevelopmentOnly` or `devToolsEnhancerDevelopmentOnly`.
> See [the article](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f) for more details.
### 1.5 For React Native, hybrid, desktop and server side Redux apps
For React Native we can use [`react-native-debugger`](https://github.com/jhen0409/react-native-debugger), which already included [the same API](https://github.com/jhen0409/react-native-debugger/blob/master/docs/redux-devtools-integration.md) with Redux DevTools Extension.
For most platforms, include [`Remote Redux DevTools`](https://github.com/zalmoxisus/remote-redux-devtools)'s store enhancer, and from the extension's context menu choose 'Open Remote DevTools' for remote monitoring.
## 2. Without Redux
See [integrations](docs/Integrations.md) and [the blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details on how to use the extension with any architecture.
## Docs
- [Options (arguments)](docs/API/Arguments.md)
- [Methods (advanced API)](docs/API/Methods.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)
- [Feedback](docs/Feedback.md)
## Demo
Live demos to use the extension with:
- [Counter](http://zalmoxisus.github.io/examples/counter/)
- [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)](https://ngrx.github.io/platform/example-app/)
Also see [`./examples` folder](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/examples).
## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/redux-devtools-extension#backer)]
<a href="https://opencollective.com/redux-devtools-extension/backer/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/1/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/2/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/3/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/4/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/5/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/6/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/7/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/8/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/9/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/10/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/11/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/12/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/13/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/14/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/15/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/16/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/17/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/18/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/19/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/20/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/21/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/22/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/23/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/24/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/25/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/26/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/27/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/28/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/29/avatar.svg"></a>
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/redux-devtools-extension#sponsor)]
<a href="https://opencollective.com/redux-devtools-extension/sponsor/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/1/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/2/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/3/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/4/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/5/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/6/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/7/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/8/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/9/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/10/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/11/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/12/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/13/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/14/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/15/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/16/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/17/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/18/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/19/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/20/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/21/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/22/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/23/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/24/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/25/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/26/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/27/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/28/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/sponsor/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/29/avatar.svg"></a>
## License
MIT
## Created By
If you like this, follow [@mdiordiev](https://twitter.com/mdiordiev) on twitter.

View File

@ -1,7 +0,0 @@
{
"presets": [
["@babel/preset-env", { "targets": "defaults" }],
"@babel/preset-react",
"@babel/preset-typescript"
]
}

View File

@ -1,55 +0,0 @@
import * as fs from 'node:fs';
import * as esbuild from 'esbuild';
import pug from 'pug';
const args = process.argv.slice(2);
const prod = !args.includes('--dev');
await esbuild.build({
bundle: true,
logLevel: 'info',
outdir: 'dist',
minify: prod,
sourcemap: !prod,
define: {
'process.env.NODE_ENV': prod ? '"production"' : '"development"',
'process.env.BABEL_ENV': prod ? '"production"' : '"development"',
},
entryPoints: [
{ out: 'background.bundle', in: 'src/background/index.ts' },
{ out: 'options.bundle', in: 'src/options/index.tsx' },
{ out: 'remote.bundle', in: 'src/remote/index.tsx' },
{ out: 'devpanel.bundle', in: 'src/devpanel/index.tsx' },
{ out: 'devtools.bundle', in: 'src/devtools/index.ts' },
{ out: 'content.bundle', in: 'src/contentScript/index.ts' },
{ out: 'page.bundle', in: 'src/pageScript/index.ts' },
],
loader: {
'.woff2': 'file',
},
});
console.log();
console.log('Creating HTML files...');
const htmlFiles = ['devpanel', 'devtools', 'options', 'remote'];
for (const htmlFile of htmlFiles) {
fs.writeFileSync(
`dist/${htmlFile}.html`,
pug.renderFile(`src/${htmlFile}/${htmlFile}.pug`),
);
}
console.log('Copying manifest.json...');
fs.copyFileSync('chrome/manifest.json', 'dist/manifest.json');
console.log('Copying assets...');
fs.cpSync('src/assets', 'dist', { recursive: true });
console.log('Copying dist for each browser...');
fs.cpSync('dist', 'chrome/dist', { recursive: true });
fs.copyFileSync('chrome/manifest.json', 'chrome/dist/manifest.json');
fs.cpSync('dist', 'edge/dist', { recursive: true });
fs.copyFileSync('edge/manifest.json', 'edge/dist/manifest.json');
fs.cpSync('dist', 'firefox/dist', { recursive: true });
fs.copyFileSync('firefox/manifest.json', 'firefox/dist/manifest.json');

View File

@ -1,64 +0,0 @@
{
"version": "3.2.10",
"name": "Redux DevTools",
"description": "Redux DevTools for debugging application's state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"manifest_version": 3,
"action": {
"default_icon": "img/logo/gray.png",
"default_title": "Redux DevTools",
"default_popup": "devpanel.html#popup"
},
"commands": {
"devtools-window": {
"description": "DevTools window"
},
"devtools-remote": {
"description": "Remote DevTools"
},
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+E"
}
}
},
"icons": {
"16": "img/logo/16x16.png",
"48": "img/logo/48x48.png",
"128": "img/logo/128x128.png"
},
"options_ui": {
"page": "options.html"
},
"background": {
"service_worker": "background.bundle.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["content.bundle.js"],
"run_at": "document_start",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["page.bundle.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
}
],
"devtools_page": "devtools.html",
"externally_connectable": {
"ids": ["*"]
},
"permissions": ["notifications", "contextMenus", "storage"],
"host_permissions": ["file:///*", "http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;"
},
"update_url": "https://clients2.google.com/service/update2/crx",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdJEPwY92xUACA9CcDBDBmbdbp8Ap3cKQ0DJTUuVQvqb4FQAv8RtKY3iUjGvdwuAcSJQIZwHXcP2aNDH3TiFik/NhRK2GRW8X3OZyTdkuDueABGP2KEX8q1WQDgjX/rPIinGYztUrvoICw/UerMPwNW62jwGoVU3YhAGf+15CgX2Y6a4tppnf/+1mPedKPidh0RsM+aJY98rX+r1SPAHPcGzMjocLkqcT75DZBXer8VQN14tOOzRCd6T6oy7qm7eWru8lJwcY66qMQvhk0osqEod2G3nA7aTWpmqPFS66VEiecP9PgZlp8gQdgZ3dFhA62exydlD55JuRhiMIR63yQIDAQAB"
}

View File

@ -1,323 +0,0 @@
# Options
Use with
- `window.__REDUX_DEVTOOLS_EXTENSION__([options])`
- `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__([options])()`
- `window.__REDUX_DEVTOOLS_EXTENSION__.connect([options])`
- `@redux-devtools/extension` npm package:
```js
import { composeWithDevTools } from '@redux-devtools/extension';
const composeEnhancers = composeWithDevTools(options);
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
),
);
```
The `options` object is optional, and can include any of the following.
### `name`
_string_ - the instance name to be shown on the monitor page. Default value is `document.title`. If not specified and there's no document title, it will consist of `tabId` and `instanceId`.
### `actionCreators`
_array_ or _object_ - action creators functions to be available in the Dispatcher. See [the example](https://github.com/zalmoxisus/redux-devtools-extension/commit/477e69d8649dfcdc9bf84dd45605dab7d9775c03).
### `latency`
_number (in ms)_ - if more than one action is dispatched in the indicated interval, all new actions will be collected and sent at once. It is the joint between performance and speed. When set to `0`, all actions will be sent instantly. Set it to a higher value when experiencing perf issues (also `maxAge` to a lower value). Default is `500 ms`.
### `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:
- **options** `object or boolean`:
- `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
- `false` - will handle also circular references.
- `true` - will handle also date, regex, undefined, primitives, error objects, symbols, maps, sets and functions.
- object, which contains `date`, `regex`, `undefined`, `nan`, `infinity`, `error`, `symbol`, `map`, `set` and `function` keys. For each of them you can indicate if to include (by setting as `true`). For `function` key you can also specify a custom function which handles serialization. See [`jsan`](https://github.com/kolodny/jsan) for more details. Example:
```js
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
options: {
undefined: true,
function: function (fn) {
return fn.toString();
},
},
},
}),
);
```
- **replacer** `function(key, value)` - [JSON `replacer` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter) used for both actions and states stringify.
Example of usage with [mori data structures](https://github.com/swannodette/mori):
```js
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
replacer: (key, value) =>
value && mori.isMap(value) ? mori.toJs(value) : value,
},
}),
);
```
In addition, you can specify a data type by adding a [`__serializedType__`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/helpers/index.js#L4) key. So you can deserialize it back while importing or persisting data. Moreover, it will also [show a nice preview showing the provided custom type](https://cloud.githubusercontent.com/assets/7957859/21814330/a17d556a-d761-11e6-85ef-159dd12f36c5.png):
```js
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
replacer: (key, value) => {
if (Immutable.List.isList(value)) {
// use your custom data type checker
return {
data: value.toArray(), // ImmutableJS custom method to get JS data as array
__serializedType__: 'ImmutableList', // mark you custom data type to show and retrieve back
};
}
},
},
}),
);
```
- **reviver** `function(key, value)` - [JSON `reviver` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) used for parsing the imported actions and states. See [`remotedev-serialize`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/immutable/serialize.js#L8-L41) as an example on how to serialize special data types and get them back:
```js
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
reviver: (key, value) => {
if (
typeof value === 'object' &&
value !== null &&
'__serializedType__' in value
) {
switch (value.__serializedType__) {
case 'ImmutableList':
return Immutable.List(value.data);
}
}
},
},
}),
);
```
- **immutable** `object` - automatically serialize/deserialize immutablejs via [remotedev-serialize](https://github.com/zalmoxisus/remotedev-serialize). Just pass the Immutable library like so:
```js
import Immutable from 'immutable'; // https://facebook.github.io/immutable-js/
// ...
// Like above, only showing off compose this time. Reminder you might not want this in prod.
const composeEnhancers =
typeof window === 'object' &&
typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined'
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
serialize: {
immutable: Immutable,
},
})
: compose;
```
It will support all ImmutableJS structures. You can even export them into a file and get them back. The only exception is `Record` class, for which you should pass in addition the references to your classes in `refs`.
- **refs** `array` - ImmutableJS `Record` classes used to make possible restore its instances back when importing, persisting... Example of usage:
```js
import Immutable from 'immutable';
// ...
const ABRecord = Immutable.Record({ a: 1, b: 2 });
const myRecord = new ABRecord({ b: 3 }); // used in the reducers
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
immutable: Immutable,
refs: [ABRecord],
},
}),
);
```
Also you can specify alternative values right in the state object (in the initial state of the reducer) by adding `toJSON` function:
In the example bellow it will always send `{ component: '[React]' }`, regardless of the state's `component` value (useful when you don't want to send lots of unnecessary data):
```js
function component(
state = { component: null, toJSON: () => ({ component: '[React]' }) },
action,
) {
switch (action.type) {
case 'ADD_COMPONENT':
return { component: action.component };
default:
return state;
}
}
```
You could also alter the value. For example when state is `{ count: 1 }`, we'll send `{ counter: 10 }` (notice we don't have an arrow function this time to use the object's `this`):
```js
function counter(
state = {
count: 0,
toJSON: function () {
return { conter: this.count * 10 };
},
},
action,
) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}
```
### `actionSanitizer` / `stateSanitizer`
- **actionSanitizer** (_function_) - function which takes `action` object and id number as arguments, and should return `action` object back. See the example bellow.
- **stateSanitizer** (_function_) - function which takes `state` object and index as arguments, and should return `state` object back.
Example of usage:
```js
const actionSanitizer = (action) =>
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
? { ...action, data: '<<LONG_BLOB>>' }
: action;
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
actionSanitizer,
stateSanitizer: (state) =>
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
}),
);
```
### `actionsDenylist` / `actionsAllowlist`
_string or array of strings as regex_ - actions types to be hidden / shown in the monitors (while passed to the reducers). If `actionsAllowlist` specified, `actionsDenylist` is ignored.
Example:
```js
createStore(
reducer,
remotedev({
sendTo: 'http://localhost:8000',
actionsDenylist: 'SOME_ACTION',
// or actionsDenylist: ['SOME_ACTION', 'SOME_OTHER_ACTION']
// or just actionsDenylist: 'SOME_' to omit both
}),
);
```
### `predicate`
_function_ - called for every action before sending, takes `state` and `action` object, and returns `true` in case it allows sending the current data to the monitor. Use it as a more advanced version of `actionsDenylist`/`actionsAllowlist` parameters.
Example of usage:
```js
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
predicate: (state, action) =>
state.dev.logLevel === VERBOSE && !action.forwarded,
}),
);
```
### `shouldRecordChanges`
_boolean_ - if specified as `false`, it will not record the changes till clicking on `Start recording` button. Default is `true`. Available only for Redux enhancer, for others use `autoPause`.
### `pauseActionType`
_string_ - if specified, whenever clicking on `Pause recording` button and there are actions in the history log, will add this action type. If not specified, will commit when paused. Available only for Redux enhancer. Default is `@@PAUSED`.
### `autoPause`
_boolean_ - auto pauses when the extensions window is not opened, and so has zero impact on your app when not in use. Not available for Redux enhancer (as it already does it but storing the data to be sent). Default is `false`.
### `shouldStartLocked`
_boolean_ - if specified as `true`, it will not allow any non-monitor actions to be dispatched till clicking on `Unlock changes` button. Available only for Redux enhancer. Default is `false`.
### `shouldHotReload`
_boolean_ - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Available only for Redux enhancer. Default to `true`.
### `shouldCatchErrors`
_boolean_ - if specified as `true`, whenever there's an exception in reducers, the monitors will show the error message, and next actions will not be dispatched.
### `features`
If you want to restrict the extension, just specify the features you allow:
```js
const composeEnhancers = composeWithDevTools({
features: {
pause: true, // start/pause recording of dispatched actions
lock: true, // lock/unlock dispatching actions and side effects
persist: true, // persist states on page reloading
export: true, // export history of actions in a file
import: 'custom', // import history of actions from a file
jump: true, // jump back and forth (time travelling)
skip: true, // skip (cancel) actions
reorder: true, // drag and drop actions in the history list
dispatch: true, // dispatch custom actions or action creators
test: true, // generate tests for the selected actions
},
// other options like actionSanitizer, stateSanitizer
});
```
If not specified, all of the features are enabled. When set as an object, only those included as `true` will be allowed.
Note that except `true`/`false`, `import` and `export` can be set as `custom` (which is by default for Redux enhancer), meaning that the importing/exporting occurs on the client side. Otherwise, you'll get/set the data right from the monitor part.

View File

@ -1,93 +0,0 @@
## Communicate with the extension directly
> Note this is advanced API, which you usually don't need to use with Redux enhancer.
Use the following methods of `window.__REDUX_DEVTOOLS_EXTENSION__`:
- [connect](#connect)
- [disconnect](#disconnect)
- [send](#send)
- [listen](#listen)
- [open](#open)
- [notifyErrors](#notifyerrors)
<a id="connect"></a>
### connect([options])
##### Arguments
- [`options`] _Object_ - [see the available options](Arguments.md).
##### Returns
_Object_ containing the following methods:
- `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.
- `error(message)` - sends the error message to be shown in the extension's monitor.
Example of usage:
```js
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect(config);
devTools.subscribe((message) => {
if (message.type === 'DISPATCH' && message.state) {
console.log('DevTools requested to change the state to', message.state);
}
});
devTools.init({ value: 'initial state' });
devTools.send('change state', { value: 'state changed' });
```
See [redux enhancer's example](https://github.com/reduxjs/redux-devtools/blob/main/packages/redux-devtools-extension/src/logOnly.ts), [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 listener inside the `connect` is enough.
<a id="send"></a>
### send(action, state, [options, instanceId])
Send a new action and state manually to be shown on the monitor. It's recommended to use [`connect`](connect), unless you want to hook into an already created instance.
##### Arguments
- `action` _String_ (action type) or _Object_ with required `type` key.
- `state` _any_ - usually object to expand.
- [`options`] _Object_ - [see the available options](Arguments.md).
- [`instanceId`] _String_ - instance id for which to include the log. If not specified and not present in the `options` object, will be the first available instance.
<a id="listen"></a>
### listen(onMessage, instanceId)
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 from the monitor.
- `instanceId` _String_ - instance id for which to handle actions.
<a id="open"></a>
### open([position])
Open the extension's window. This should be conditional (usually you don't need to open extension's window automatically).
##### Arguments
- [`position`] _String_ - window position: `left`, `right`, `bottom`. Also can be `panel` to [open it in a Chrome panel](../FAQ.md#how-to-keep-devtools-window-focused-all-the-time-in-a-chrome-panel). Or `remote` to [open remote monitor](../FAQ.md#how-to-get-it-work-with-webworkers-react-native-hybrid-desktop-and-server-side-apps). By default is `left`.
<a id="notifyErrors"></a>
### 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 an exception occurs.
##### Arguments
- [`onError`] _Function_ to call when there's an exceptions.

View File

@ -1,4 +0,0 @@
# API Reference
- [Parameters](Arguments.md)
- [Methods](Methods.md)

View File

@ -1,37 +0,0 @@
# Architecture Notes
This document exists to keep track of how the different parts of the Redux DevTools interact, since it's easy to forget how it all works together. This is intended for internal purposes and is just a collection of notes to myself.
## Entry Points
### Window
This is the default view that is shown in the Redux DevTools popup, the Chrome DevTools tab (if direct access to the background page is available), and new popup windows that are created. It has direct access to the background page via `chrome.runtime.getBackgroundPage`.
### DevPanel
This is the view that is shown in the Chrome DevTools tab if direct access to the background page is not available.
Initially this was the view that was always used for the Chrome DevTools tab, but when support to directly access the background page from the DevTools tab was added, [the Window View became the preferred view](https://github.com/zalmoxisus/redux-devtools-extension/pull/580).
### Remote
This does not interact with the other parts of the extension at all, it just renders the `App` component from `@redux-devtools/app`.
It can be triggered by hitting the "Remote" button in any of the other views, which calls `chrome.windows.create` and creates a new window.
### DevTools
This is the script that adds the Redux panel in the Chrome DevTools using `chrome.devtools.panels.create`.
It creates a Window View if it has direct access to the background page, otherwise it creates a DevPanel View.
Note that this used to always show the DevPanel View, but [started using the Window View by default](https://github.com/zalmoxisus/redux-devtools-extension/pull/580) once direct access to the background page was added to Chrome DevTools tabs.
### Content Script
Passes messages between the injected page script and the background page.
It listens for messages from the injected page script using `window.addEventListener('message', ...)`. It knows the message is from the injected page script if `message.source` is `'@devtools-page'`. See the Chrome DevTools docs where this approach [is documented](https://developer.chrome.com/docs/extensions/how-to/devtools/extend-devtools#evaluated-scripts-to-devtools).
It creates a connection to the background page using `chrome.runtime.connect` with the name `'tab'` when it receives the first message from the injected page script.

View File

@ -1,5 +0,0 @@
# Articles
- [Improve your development workflow with Redux DevTools Extension](https://medium.com/@zalmoxis/improve-your-development-workflow-with-redux-devtools-extension-f0379227ff83)
- [Using Redux DevTools in production](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f)
- [Redux DevTools without Redux](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f)

View File

@ -1,11 +0,0 @@
# Credits
- Built using [Crossbuilder](https://github.com/zalmoxisus/crossbuilder) boilerplate.
- Includes [Dan Abramov](https://github.com/gaearon)'s [redux-devtools](https://github.com/gaearon/redux-devtools) and the following monitors:
- [Log Monitor](https://github.com/gaearon/redux-devtools-log-monitor)
- [Inspector](https://github.com/alexkuz/redux-devtools-inspector)
- [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch)
- [Slider](https://github.com/calesce/redux-slider-monitor)
- [Chart](https://github.com/romseguy/redux-devtools-chart-monitor)
- [The logo icon](https://github.com/reactjs/redux/issues/151) made by [Keith Yong](https://github.com/keithyong) .
- Examples from [Redux](https://github.com/rackt/redux/tree/master/examples).

View File

@ -1,45 +0,0 @@
# Redux DevTools Extension FAQ
## Table of Contents
- [How to get it work](#how-to-get-it-work)
- [How to disable/enable it in production](#how-to-disable-it-in-production)
- [How to persist debug sessions across page reloads](#how-to-persist-debug-sessions-across-page-reloads)
- [How to open DevTools programmatically](#how-to-open-devtools-programmatically)
- [How to enable/disable errors notifying](#how-to-enabledisable-errors-notifying)
- [How to get it work with WebWorkers, React Native, hybrid, desktop and server side apps](#how-to-get-it-work-with-webworkers-react-native-hybrid-desktop-and-server-side-apps)
- [Keyboard shortcuts](#keyboard-shortcuts)
#### How to get it work
- Check the extension with [Counter](http://zalmoxisus.github.io/examples/counter/) or [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/) demo.
- Reload the extension on the extensions page (`chrome://extensions/`).
- If something goes wrong, [open an issue](https://github.com/zalmoxisus/redux-devtools-extension/issues) or tweet me: [@mdiordiev](https://twitter.com/mdiordiev).
#### How to disable it in production
Usually you don't have to. See [the article for details on how to include it in production](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f).
#### How to persist debug sessions across page reloads
Just click the `Persist` button or add `?debug_session=<session_name>` to the url.
#### How to open DevTools programmatically
```js
window.__REDUX_DEVTOOLS_EXTENSION__.open();
```
Make sure to have it conditionally. Auto opening windows is a bad DX. See the [API](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Methods.md#open) for details.
#### How to enable/disable errors notifying
Just find `Redux DevTools` on the extensions page (`chrome://extensions/`) and click the `Options` link to customize everything. The errors notifying is disabled by default. If enabled, it works only when the store enhancer is called (in order not to show notifications for any sites you visit). In case you want notifications for a non-redux app, init it explicitly by calling `window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors()` (probably you'll check if `window.__REDUX_DEVTOOLS_EXTENSION__` exists before calling it).
#### How to get it work with WebWorkers, React Native, hybrid, desktop and server side apps
It is not possible to inject extension's script there and to communicate directly. To solve this, use [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools). After including it inside the app, click `Remote` button for remote monitoring.
#### Keyboard shortcuts
To set/change the keyboard shortcuts, click "Keyboard shortcuts" button on the bottom of the extensions page (`chrome://extensions/`). By default only `Cmd` (`Ctrl`) + `Shift` + `E` is available, which will open the extension popup (only when the Redux store is available in the current page).

View File

@ -1,13 +0,0 @@
## 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).

View File

@ -1,5 +0,0 @@
# Feedback wanted
[File an issue](https://github.com/zalmoxisus/redux-devtools-extension/issues) or [submit a PR](https://github.com/zalmoxisus/redux-devtools-extension/pulls) if you have suggestions, rate us and leave a review on [Chrome Store](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/reviews), post feature requests and bug reports on [Product Pains](https://productpains.com/product/redux-devtools-extension), or ping me on Twitter as [@mdiordiev](https://twitter.com/mdiordiev).
<iframe width="100%" height="400px" scrolling="no" style="border:0" src="https://productpains.com/widget.html?token=fc7887ce-f3f9-105a-e5cb-43b9eeb668d5"></iframe><script type="text/javascript" src="https://productpains.com/js/lib/iframeResizer.min.js"></script>

View File

@ -1,302 +0,0 @@
# Integrations for js and non-js frameworks
Mostly functional:
- [React](#react)
- [Angular](#angular)
- [Cycle](#cycle)
- [Ember](#ember)
- [Fable](#fable)
- [Freezer](#freezer)
- [Mobx](#mobx)
- [PureScript](#purescript)
- [Reductive](#reductive)
- [Aurelia](#aurelia)
In progress:
- [ClojureScript](#clojurescript)
- [Horizon](#horizon)
- [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)
```js
import remotedev from 'mobx-remotedev';
// or import remotedev from 'mobx-remotedev/lib/dev'
// in case you want to use it in production or don't have process.env.NODE_ENV === 'development'
const appStore = observable({
// ...
});
// Or
class appStore {
// ...
}
export default remotedev(appStore);
```
### [Angular](https://github.com/angular/angular)
#### [ng2-redux](https://github.com/angular-redux/ng2-redux)
```js
import { NgReduxModule, NgRedux, DevToolsExtension } from 'ng2-redux';
@NgModule({
/* ... */
imports: [ /* ... */, NgReduxModule ]
})export class AppModule {
constructor(
private ngRedux: NgRedux,
private devTools: DevToolsExtension) {
let enhancers = [];
// ... add whatever other enhancers you want.
// You probably only want to expose this tool in devMode.
if (__DEVMODE__ && devTools.isEnabled()) {
enhancers = [ ...enhancers, devTools.enhancer() ];
}
this.ngRedux.configureStore(
rootReducer,
initialState,
[],
enhancers);
}
}
```
For Angular 1 see [ng-redux](https://github.com/angular-redux/ng-redux).
#### [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.forRoot(rootReducer),
// Instrumentation must be imported after importing StoreModule (config is optional)
StoreDevtoolsModule.instrument({
maxAge: 5,
}),
],
})
export class AppModule {}
```
[`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)
```js
//app/enhancers/index.js
import { compose } from 'redux';
var devtools = window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (f) => f;
export default compose(devtools);
```
### [Cycle](https://github.com/cyclejs/cyclejs)
#### [`@culli/store`](https://github.com/milankinen/culli/tree/master/packages/store)
```js
import { run } from '@cycle/most-run';
import { makeDOMDriver as DOM } from '@cycle/dom';
import Store, { ReduxDevtools } from '@culli/store';
import App, { newId } from './App';
run(App, {
DOM: DOM('#app'),
Store: Store(
ReduxDevtools({
items: [
{ id: newId(), num: 0 },
{ id: newId(), num: 0 },
],
}),
),
});
```
### [Freezer](https://github.com/arqex/freezer)
#### [`freezer-redux-devtools`](https://github.com/arqex/freezer-redux-devtools)
```js
import React, { Component } from 'react';
import { supportChromeExtension } from 'freezer-redux-devtools/freezer-redux-middleware';
import Freezer from 'freezer-js';
// Our state is a freezer object
var State = new Freezer({ hello: 'world' });
// Enable the extension
supportChromeExtension(State);
```
### [Horizon](https://github.com/rethinkdb/horizon)
#### [`horizon-remotedev`](https://github.com/zalmoxisus/horizon-remotedev)
```js
// import hzRemotedev from 'horizon-remotedev';
// or import hzRemotedev from 'horizon-remotedev/lib/dev'
// in case you want to use it in production or don't have process.env.NODE_ENV === 'development'
//Setup Horizon connection
const horizon = Horizon();
// ...
// Specify the horizon instance to monitor
hzRemotedev(horizon('react_messages'));
```
### [Fable](https://github.com/fable-compiler/Fable)
#### [`fable-elmish/debugger`](https://github.com/fable-elmish/debugger)
```fsharp
open Elmish.Debug
Program.mkProgram init update view
|> Program.withDebugger // connect to a devtools monitor via Chrome extension if available
|> Program.run
```
or
```fsharp
open Elmish.Debug
Program.mkProgram init update view
|> Program.withDebuggerAt (Remote("localhost",8000)) // connect to a server running on localhost:8000
|> Program.run
```
### [PureScript](https://github.com/purescript/purescript)
#### [`purescript-react-redux`](https://github.com/ethul/purescript-react-redux)
[`Example of integration`](https://github.com/ethul/purescript-react-redux-example).
### [ClojureScript](https://github.com/clojure/clojurescript)
[`Example of integration`](http://gitlab.xet.ru:9999/publicpr/clojurescript-redux/tree/master#dev-setup)
### [Python](https://www.python.org/)
#### [`pyredux`](https://github.com/peterpeter5/pyredux)
[WIP](https://github.com/zalmoxisus/remotedev-server/issues/34)
### [Swift](https://github.com/apple/swift)
#### [`katanaMonitor`](https://github.com/bolismauro/katanaMonitor-lib-swift) for [`katana-swift`](https://github.com/BendingSpoons/katana-swift)
```swift
import KatanaMonitor
var middleware: [StoreMiddleware] = [
// other middleware
]
#if DEBUG
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());
}
```

View File

@ -1,21 +0,0 @@
# Documentation
- [Extension](/README.md)
- [Installation](/README.md#installation)
- [Usage](/README.md#usage)
- [Demo](/README.md#demo)
- [API Reference](/docs/API/README.md)
- [Options (arguments)](/docs/API/Arguments.md)
- [Methods (advanced API)](/docs/API/Methods.md)
- Features
- [Trace actions calls](/docs/Features/Trace.md)
- [Integrations](/docs/Integrations.md)
- [FAQ](/docs/FAQ.md)
- [Troubleshooting](/docs/Troubleshooting.md)
- [Recipes](/docs/Recipes.md)
- [Articles](/docs/Articles.md)
- [Videos](/docs/Videos.md)
- [Credits](/docs/Credits.md)
- [Support us](/README.md#backers)
- [Feedback](/docs/Feedback.md)
- [Change Log](https://github.com/zalmoxisus/redux-devtools-extension/releases)

View File

@ -1,78 +0,0 @@
# 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 type-guard in order to avoid
casting to any.
```typescript
import { createStore, StoreEnhancer } from 'redux';
// ...
type WindowWithDevTools = Window & {
__REDUX_DEVTOOLS_EXTENSION__: () => StoreEnhancer<unknown, {}>;
};
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
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/reduxjs/redux-devtools/blob/main/packages/redux-devtools-extension/src/logOnly.ts), which can be used it like so:
```js
import { createStore, compose } from 'redux';
import { devToolsEnhancerLogOnly } from '@redux-devtools/extension';
const store = createStore(
reducer,
/* preloadedState, */ compose(
devToolsEnhancerLogOnly({
instaceID: 1,
name: 'Denylisted',
actionsDenylist: '...',
}),
devToolsEnhancerLogOnly({
instaceID: 2,
name: 'Allowlisted',
actionsAllowlist: '...',
}),
),
);
```

View File

@ -1,131 +0,0 @@
# Troubleshooting
### I just see empty log or "No store found"
Make sure you [applied the enhancer](https://github.com/zalmoxisus/redux-devtools-extension#2-use-with-redux). Note that passing enhancer as last argument requires redux@>=3.1.0. For older versions apply it like [here](https://github.com/zalmoxisus/redux-devtools-extension/blob/v0.4.2/examples/todomvc/store/configureStore.js) or [here](https://github.com/zalmoxisus/redux-devtools-extension/blob/v0.4.2/examples/counter/store/configureStore.js#L7-L12).
Don't mix the old Redux API with the new one. Pass enhancers and applyMiddleware as last createStore argument.
### Access file url (`file:///`)
If you develop on your local filesystem, make sure to allow Redux DevTools access to `file:///` URLs in the settings of this extension:
<img width="746" alt="extensions" src="https://cloud.githubusercontent.com/assets/7957859/19075220/a0fad99e-8a4c-11e6-8b87-757f2dc179cb.png">
### It shows only the `@@INIT` action or moving back and forth doesn't update the state
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).
Another cause could be that you are creating multiple stores, which means that the devtools get attached to one but the application uses another. See [https://github.com/reduxjs/redux-toolkit/issues/2753](this issue).
### @@INIT or REPLACE action resets the state of the app or last actions RE-APPLIED
`@@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
Usually the extension's store enhancer should be last in the compose. When you're using [`window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`](/README.md#12-advanced-store-setup) or [`composeWithDevTools`](/README.md#13-use-redux-devtools-extension-package-from-npm) helper you don't have to worry about the enhancers order. However some enhancers ([like `redux-batched-subscribe`](https://github.com/zalmoxisus/redux-devtools-extension/issues/261)) also have this requirement to be the last in the compose. In this case you can use it like so:
```js
const store = createStore(
reducer,
preloadedState,
compose(
// applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (noop) => noop,
batchedSubscribe(/* ... */),
),
);
```
Where `batchedSubscribe` is `redux-batched-subscribe` store enhancer.
### Excessive use of memory and CPU
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 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) =>
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
? { ...action, data: '<<LONG_BLOB>>' }
: action;
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
actionSanitizer,
stateSanitizer: (state) =>
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
}),
);
```
There's a more advanced [example on how to implement that for `ui-router`](https://github.com/zalmoxisus/redux-devtools-extension/issues/455#issuecomment-404538385).
The extension is in different process and cannot access the store object directly, unlike vanilla [`redux-devtools`](https://github.com/reduxjs/redux-devtools) which doesn't have this issue. In case sanitizing doesn't fit your use case, you might consider including it directly as a react component, so there will be no need to serialize the data, but it would add some complexity.
### It fails to serialize data when [passing synthetic events](https://github.com/zalmoxisus/redux-devtools-extension/issues/275) or [calling an action directly with `redux-actions`](https://github.com/zalmoxisus/redux-devtools-extension/issues/287)
React synthetic event cannot be reused for performance reason. So, it's not possible to serialize event objects you pass to action payloads.
1. The best solution is **not to pass the whole event object to reducers, but the data you need**:
```diff
function click(event) {
return {
type: ELEMENT_CLICKED,
- event: event
+ value: event.target.value
};
}
```
2. If you cannot pick data from the event object or, for some reason, you need the whole object, use `event.persist()` as suggested in [React Docs](https://facebook.github.io/react/docs/events.html#event-pooling), but it will consume RAM while not needed.
```diff
function increment(event) {
+ event.persist();
return {
type: ELEMENT_CLICKED,
event: event,
};
}
```
3. A workaround, to pass the whole object and at the same time not to persist it, is to override this key of the stringified payload in your action creator. Add a custom `toJSON` function right in the action object (which will be called by the extension before accessing the object):
```diff
function increment(event) {
return {
type: ELEMENT_CLICKED,
event: event,
+ toJSON: function (){
+ return { ...this, event: '[Event]' };
+ }
};
}
```
Note that it shouldn't be arrow function as we want to have access to the function's `this`.
As we don't have access to the original object, skipping and recomputing actions during hot reloading will not work in this case. We recommend to use the first solution whenever possible.
### Symbols or other unserializable data not shown
To get data which cannot be serialized by `JSON.stringify`, set [`serialize` parameter](/docs/API/Arguments.md#serialize):
```js
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: true,
}),
);
```
It will handle also date, regex, undefined, error objects, symbols, maps, sets and functions.

View File

@ -1,6 +0,0 @@
# Videos
- [Debugging flux applications in production at React Europe 2016](https://youtu.be/YU8jQ2HtqH4)
- [Hot Reloading with Time Travel at React Europe 2015](https://youtu.be/xsSnOQynTHs)
- [Getting Started with Redux DevTools Extension](https://egghead.io/lessons/javascript-getting-started-with-redux-dev-tools)
- [React & Redux With ExpressJS](https://www.youtube.com/watch?v=6ygcbRpZFR4)

View File

@ -1,62 +0,0 @@
{
"version": "3.2.10",
"name": "Redux DevTools",
"description": "Redux DevTools for debugging application's state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"manifest_version": 3,
"action": {
"default_icon": "img/logo/gray.png",
"default_title": "Redux DevTools",
"default_popup": "devpanel.html#popup"
},
"commands": {
"devtools-window": {
"description": "DevTools window"
},
"devtools-remote": {
"description": "Remote DevTools"
},
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+E"
}
}
},
"icons": {
"16": "img/logo/16x16.png",
"48": "img/logo/48x48.png",
"128": "img/logo/128x128.png"
},
"options_ui": {
"page": "options.html"
},
"background": {
"service_worker": "background.bundle.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["content.bundle.js"],
"run_at": "document_start",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["page.bundle.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
}
],
"devtools_page": "devtools.html",
"externally_connectable": {
"ids": ["*"]
},
"permissions": ["notifications", "contextMenus", "storage"],
"host_permissions": ["file:///*", "http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;"
}
}

View File

@ -1,39 +0,0 @@
import globals from 'globals';
import eslintJs from '../eslint.js.config.base.mjs';
import eslintTsReact from '../eslint.ts.react.config.base.mjs';
import eslintJsReactJest from '../eslint.js.react.jest.config.base.mjs';
export default [
...eslintJs,
...eslintTsReact(import.meta.dirname),
...eslintJsReactJest,
{
ignores: [
'chrome',
'dist',
'edge',
'examples',
'firefox',
'jest.config.ts',
'test/electron/fixture/dist',
],
},
{
files: ['build.mjs'],
languageOptions: {
globals: {
...globals.nodeBuiltin,
},
},
},
{
files: ['test/**/*.js', 'test/**/*.jsx'],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
EUI: true,
},
},
},
];

View File

@ -1,57 +0,0 @@
{
"version": "3.2.10",
"name": "Redux DevTools",
"manifest_version": 3,
"description": "Redux Developer Tools for debugging application state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"browser_specific_settings": {
"gecko": {
"id": "extension@redux.devtools"
}
},
"action": {
"default_icon": "img/logo/38x38.png",
"default_title": "Redux DevTools",
"default_popup": "devpanel.html#popup"
},
"commands": {
"devtools-window": {
"description": "DevTools window"
},
"devtools-remote": {
"description": "Remote DevTools"
}
},
"icons": {
"16": "img/logo/16x16.png",
"48": "img/logo/48x48.png",
"128": "img/logo/128x128.png"
},
"options_ui": {
"page": "options.html"
},
"background": {
"scripts": ["background.bundle.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.bundle.js"],
"run_at": "document_start",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["page.bundle.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
}
],
"devtools_page": "devtools.html",
"permissions": ["notifications", "contextMenus", "tabs", "storage"],
"host_permissions": ["file:///*", "http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data:;"
}
}

View File

@ -1,18 +0,0 @@
import { createJsWithTsEsmPreset, type JestConfigWithTsJest } from 'ts-jest';
const presetConfig = createJsWithTsEsmPreset({
tsconfig: 'tsconfig.json',
});
const jestConfig: JestConfigWithTsJest = {
...presetConfig,
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
testPathIgnorePatterns: ['<rootDir>/examples'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
},
};
export default jestConfig;

View File

@ -1,81 +0,0 @@
{
"private": true,
"name": "remotedev-redux-devtools-extension",
"version": "3.2.12",
"description": "Redux Developer Tools for debugging application state changes.",
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/extension",
"license": "MIT",
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"scripts": {
"build": "pnpm run build:extension && pnpm run type-check",
"build:extension": "node build.mjs",
"clean": "rimraf dist && rimraf chrome/dist && rimraf edge/dist && rimraf firefox/dist",
"test:app": "cross-env BABEL_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js test/app",
"test:chrome": "cross-env SE_FORCE_BROWSER_DOWNLOAD=true node --experimental-vm-modules node_modules/jest/bin/jest.js test/chrome",
"build:test:electron:fixture": "webpack --config test/electron/fixture/webpack.config.js",
"test:electron": "pnpm run build:test:electron:fixture && node --experimental-vm-modules node_modules/jest/bin/jest.js test/electron",
"test": "pnpm run test:app && pnpm run test:chrome && pnpm run test:electron",
"lint": "eslint .",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@redux-devtools/app": "workspace:^",
"@redux-devtools/core": "workspace:^",
"@redux-devtools/instrument": "workspace:^",
"@redux-devtools/serialize": "workspace:^",
"@redux-devtools/slider-monitor": "workspace:^",
"@redux-devtools/ui": "workspace:^",
"@redux-devtools/utils": "workspace:^",
"@reduxjs/toolkit": "^2.11.2",
"@types/jsan": "^3.1.5",
"jsan": "^3.1.14",
"localforage": "^1.10.0",
"lodash-es": "^4.17.23",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-icons": "^5.6.0",
"react-is": "^19.2.4",
"react-json-tree": "workspace:^",
"react-redux": "^9.2.0",
"redux": "^5.0.1",
"redux-persist": "^6.0.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.28.6",
"@jest/globals": "^30.2.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/chrome": "^0.1.37",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"chromedriver": "^126.0.5",
"cross-env": "^10.1.0",
"electron": "^31.7.7",
"esbuild": "^0.27.3",
"globals": "^17.4.0",
"immutable": "^5.1.4",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"pug": "^3.0.3",
"rimraf": "^6.1.3",
"selenium-webdriver": "^4.41.0",
"sinon-chrome": "^3.0.1",
"ts-jest": "^29.4.6",
"typescript": "~5.9.3",
"webpack": "^5.105.3",
"webpack-cli": "^6.0.1"
}
}

View File

@ -1,141 +0,0 @@
import React, { Component } from 'react';
import { connect, ResolveThunks } from 'react-redux';
import { Button, Container, Divider, Toolbar } from '@redux-devtools/ui';
import {
DevTools,
Dispatcher,
DispatcherButton,
ExportButton,
getActiveInstance,
getReport,
ImportButton,
liftedDispatch,
MonitorSelector,
PrintButton,
SliderButton,
SliderMonitor,
StoreState,
TopButtons,
} from '@redux-devtools/app';
import { GoBroadcast } from 'react-icons/go';
import { MdOutlineWindow } from 'react-icons/md';
import type { Position } from '../pageScript/api/openWindow.js';
import type { SingleMessage } from '../background/store/apiMiddleware.js';
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = ResolveThunks<typeof actionCreators>;
interface OwnProps {
readonly position: string;
}
type Props = StateProps & DispatchProps & OwnProps;
const isElectron = navigator.userAgent.includes('Electron');
async function sendMessage(message: SingleMessage) {
await chrome.runtime.sendMessage(message);
}
class Actions extends Component<Props> {
openWindow = async (position: Position) => {
await sendMessage({ type: 'OPEN', position });
};
openOptionsPage = async () => {
if (navigator.userAgent.includes('Firefox')) {
await sendMessage({ type: 'OPEN_OPTIONS' });
} else {
await chrome.runtime.openOptionsPage();
}
};
render() {
const {
monitor,
dispatcherIsOpen,
sliderIsOpen,
options,
liftedState,
liftedDispatch,
position,
stateTreeSettings,
} = this.props;
const { features } = options;
return (
<Container>
<TopButtons
dispatch={liftedDispatch}
liftedState={liftedState}
options={options}
/>
<DevTools
monitor={monitor}
liftedState={liftedState}
monitorState={this.props.monitorState}
dispatch={liftedDispatch}
features={options.features}
stateTreeSettings={stateTreeSettings}
/>
{sliderIsOpen && options.connectionId && options.features.jump && (
<SliderMonitor liftedState={liftedState} dispatch={liftedDispatch} />
)}
{dispatcherIsOpen &&
options.connectionId &&
options.features.dispatch && <Dispatcher options={options} />}
<Toolbar borderPosition="top">
{features.export && <ExportButton />}
{features.import && <ImportButton />}
{position &&
(position !== '#popup' ||
navigator.userAgent.includes('Firefox')) && <PrintButton />}
<Divider />
<MonitorSelector />
<Divider />
{features.jump && <SliderButton isOpen={this.props.sliderIsOpen} />}
{features.dispatch && (
<DispatcherButton dispatcherIsOpen={this.props.dispatcherIsOpen} />
)}
<Divider />
{!isElectron && (
<Button
onClick={async () => {
await this.openWindow('window');
}}
>
<MdOutlineWindow />
</Button>
)}
{!isElectron && (
<Button
onClick={async () => {
await this.openWindow('remote');
}}
>
<GoBroadcast />
</Button>
)}
</Toolbar>
</Container>
);
}
}
const mapStateToProps = (state: StoreState) => {
const instances = state.instances;
const id = getActiveInstance(instances);
return {
liftedState: instances.states[id],
monitorState: state.monitor.monitorState,
options: instances.options[id],
monitor: state.monitor.selected,
dispatcherIsOpen: state.monitor.dispatcherIsOpen,
sliderIsOpen: state.monitor.sliderIsOpen,
reports: state.reports.data,
stateTreeSettings: state.stateTreeSettings,
};
};
const actionCreators = {
liftedDispatch,
getReport,
};
export default connect(mapStateToProps, actionCreators)(Actions);

View File

@ -1,80 +0,0 @@
import React, { Component } from 'react';
import { connect, ResolveThunks } from 'react-redux';
import { Container, Notification } from '@redux-devtools/ui';
import {
clearNotification,
getActiveInstance,
Header,
Settings,
StoreState,
} from '@redux-devtools/app';
import Actions from './Actions.js';
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = ResolveThunks<typeof actionCreators>;
interface OwnProps {
readonly position: string;
}
type Props = StateProps & DispatchProps & OwnProps;
class App extends Component<Props> {
render() {
const { position, options, section, theme, notification } = this.props;
if (!position && (!options || !options.features)) {
return (
<div style={{ padding: '20px', width: '100%', textAlign: 'center' }}>
No store found. Make sure to follow{' '}
<a
href="https://github.com/zalmoxisus/redux-devtools-extension#usage"
target="_blank"
rel="noreferrer"
>
the instructions
</a>
.
</div>
);
}
let body;
switch (section) {
case 'Settings':
body = <Settings />;
break;
default:
body = <Actions position={position} />;
}
return (
<Container themeData={theme}>
<Header section={section} />
{body}
{notification && (
<Notification
type={notification.type}
onClose={this.props.clearNotification}
>
{notification.message}
</Notification>
)}
</Container>
);
}
}
function mapStateToProps(state: StoreState) {
const instances = state.instances;
const id = getActiveInstance(instances);
return {
options: instances.options[id],
section: state.section,
theme: state.theme,
notification: state.notification,
};
}
const actionCreators = {
clearNotification,
};
export default connect(mapStateToProps, actionCreators)(App);

View File

@ -1,42 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="45" height="45" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd" transform="translate(1 1)" stroke-width="2">
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="1.5s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="1.5s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="1.5s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="3s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="3s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="3s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="8">
<animate attributeName="r"
begin="0s" dur="1.5s"
values="6;1;2;3;4;5;6"
calcMode="linear"
repeatCount="indefinite" />
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,31 +0,0 @@
import openDevToolsWindow, { DevToolsPosition } from './openWindow.js';
export function createMenu() {
const menus = [
{ id: 'devtools-window', title: 'Open in a window' },
{ id: 'devtools-remote', title: 'Open Remote DevTools' },
];
const shortcuts: { [commandName: string]: string | undefined } = {};
chrome.commands.getAll((commands) => {
for (const { name, shortcut } of commands) {
shortcuts[name!] = shortcut;
}
for (const { id, title } of menus) {
chrome.contextMenus.create({
id: id,
title: title + (shortcuts[id] ? ' (' + shortcuts[id] + ')' : ''),
contexts: ['all'],
});
}
});
}
export async function removeMenu() {
await chrome.contextMenus.removeAll();
}
chrome.contextMenus.onClicked.addListener(({ menuItemId }) => {
openDevToolsWindow(menuItemId as DevToolsPosition);
});

View File

@ -1,38 +0,0 @@
import '../chromeApiMock.js';
import configureStore from './store/backgroundStore.js';
import openDevToolsWindow, { DevToolsPosition } from './openWindow.js';
import { createMenu, removeMenu } from './contextMenus.js';
import { getOptions } from '../options/syncOptions.js';
// Expose the extension's store globally to access it from the windows
// via chrome.runtime.getBackgroundPage
export const store = configureStore();
// Listen for keyboard shortcuts
chrome.commands.onCommand.addListener((shortcut) => {
openDevToolsWindow(shortcut as DevToolsPosition);
});
// Disable the action by default and create the context menu when installed
chrome.runtime.onInstalled.addListener(() => {
void chrome.action.disable();
getOptions((option) => {
if (option.showContextMenus) createMenu();
});
});
// Create or Remove context menu when config changed
chrome.storage.onChanged.addListener((changes) => {
if (changes.showContextMenus) {
if (changes.showContextMenus.newValue) createMenu();
else void removeMenu();
}
});
// https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers#keep_a_service_worker_alive_continuously
setInterval(
() =>
void chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() }),
20000,
);

View File

@ -1,46 +0,0 @@
import { LIFTED_ACTION } from '@redux-devtools/app';
import { store } from './index.js';
export function getReport(
reportId: string,
tabId: string | number,
instanceId: number,
) {
chrome.storage.local.get<{
's:hostname': string | undefined;
's:port': string | undefined;
's:secure': string | undefined;
}>(['s:hostname', 's:port', 's:secure'], (options) => {
if (!options['s:hostname'] || !options['s:port']) return;
const url = `${options['s:secure'] ? 'https' : 'http'}://${
options['s:hostname']
}:${options['s:port']}`;
fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ op: 'get', id: reportId }),
})
.then((response) => {
return response.json();
})
.then((json) => {
const { payload, preloadedState } = json;
if (!payload) return;
store.dispatch({
type: LIFTED_ACTION,
message: 'IMPORT',
state: JSON.stringify({ payload, preloadedState }),
id: tabId,
instanceId: `${tabId}/${instanceId}`,
});
})
.catch(function (err) {
/* eslint-disable no-console */
console.warn(err);
/* eslint-enable no-console */
});
});
}

View File

@ -1,34 +0,0 @@
export type DevToolsPosition = 'devtools-window' | 'devtools-remote';
const windows: { [K in DevToolsPosition]?: number } = {};
export default function openDevToolsWindow(position: DevToolsPosition) {
if (!windows[position]) {
createWindow(position);
} else {
chrome.windows.update(windows[position], { focused: true }, () => {
if (chrome.runtime.lastError) createWindow(position);
});
}
}
function createWindow(position: DevToolsPosition) {
const url = chrome.runtime.getURL(getPath(position));
chrome.windows.create({ type: 'popup', url }, (win) => {
windows[position] = win!.id;
if (navigator.userAgent.includes('Firefox')) {
void chrome.windows.update(win!.id!, { focused: true });
}
});
}
function getPath(position: DevToolsPosition) {
switch (position) {
case 'devtools-window':
return 'devpanel.html';
case 'devtools-remote':
return 'remote.html';
default:
throw new Error('Unrecognized position');
}
}

View File

@ -1,654 +0,0 @@
import {
CustomAction,
DispatchAction as AppDispatchAction,
LibConfig,
LIFTED_ACTION,
nonReduxDispatch,
REMOVE_INSTANCE,
SET_PERSIST,
SetPersistAction,
stringifyJSON,
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import type { Options, OptionsMessage } from '../../options/syncOptions.js';
import openDevToolsWindow, { DevToolsPosition } from '../openWindow.js';
import { getReport } from '../logging.js';
import { Action, Dispatch, Middleware } from 'redux';
import type {
ContentScriptToBackgroundMessage,
SplitMessage,
} from '../../contentScript/index.js';
import type {
ErrorMessage,
PageScriptToContentScriptMessageForwardedToMonitors,
PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance,
} from '../../pageScript/api/index.js';
import { LiftedState } from '@redux-devtools/instrument';
import type {
BackgroundAction,
LiftedActionAction,
} from './backgroundStore.js';
import type { Position } from '../../pageScript/api/openWindow.js';
import type { BackgroundState } from './backgroundReducer.js';
import { store } from '../index.js';
interface TabMessageBase {
readonly type: string;
readonly state?: string | undefined;
readonly id?: string;
}
interface StartAction extends TabMessageBase {
readonly type: 'START';
readonly state?: never;
readonly id?: never;
}
interface StopAction extends TabMessageBase {
readonly type: 'STOP';
readonly state?: never;
readonly id?: never;
}
interface OptionsAction {
readonly type: 'OPTIONS';
readonly options: Options;
}
interface DispatchAction extends TabMessageBase {
readonly type: 'DISPATCH';
readonly action: AppDispatchAction;
readonly state: string | undefined;
readonly id: string;
}
interface ImportAction extends TabMessageBase {
readonly type: 'IMPORT';
readonly action: undefined;
readonly state: string | undefined;
readonly id: string;
}
interface ActionAction extends TabMessageBase {
readonly type: 'ACTION';
readonly action: string | CustomAction;
readonly state: string | undefined;
readonly id: string;
}
interface ExportAction extends TabMessageBase {
readonly type: 'EXPORT';
readonly action: undefined;
readonly state: string | undefined;
readonly id: string;
}
export interface NAAction {
readonly type: 'NA';
readonly id: string | number;
}
interface InitMessage<S, A extends Action<string>> {
readonly type: 'INIT';
readonly payload: string;
instanceId: string;
readonly source: '@devtools-page';
action?: string;
name?: string | undefined;
liftedState?: LiftedState<S, A, unknown>;
libConfig?: LibConfig;
}
interface LiftedMessage {
type: 'LIFTED';
liftedState: { isPaused: boolean | undefined };
instanceId: number;
source: '@devtools-page';
}
interface SerializedPartialLiftedState {
readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number;
readonly nextActionId: number;
}
interface SerializedPartialStateMessage {
readonly type: 'PARTIAL_STATE';
readonly payload: SerializedPartialLiftedState;
readonly source: '@devtools-page';
instanceId: number;
readonly maxAge: number;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
interface SerializedExportMessage {
readonly type: 'EXPORT';
readonly payload: string;
readonly committedState: string | undefined;
readonly source: '@devtools-page';
instanceId: number;
}
interface SerializedActionMessage {
readonly type: 'ACTION';
readonly payload: string;
readonly source: '@devtools-page';
instanceId: number;
readonly action: string;
readonly maxAge: number;
readonly nextActionId: number;
}
interface SerializedStateMessage<S, A extends Action<string>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
'actionsById' | 'computedStates' | 'committedState'
>;
readonly source: '@devtools-page';
instanceId: string;
readonly libConfig?: LibConfig;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
export type UpdateStateRequest<S, A extends Action<string>> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
| SerializedExportMessage
| SerializedActionMessage
| SerializedStateMessage<S, A>;
interface UpdateStateAction<S, A extends Action<string>> {
readonly type: typeof UPDATE_STATE;
request: UpdateStateRequest<S, A>;
readonly id: string | number;
}
type SplitUpdateStateRequestStart<S, A extends Action<string>> = {
split: 'start';
} & Partial<UpdateStateRequest<S, A>>;
interface SplitUpdateStateRequestChunk {
readonly split: 'chunk';
readonly chunk: [string, string];
}
interface SplitUpdateStateRequestEnd {
readonly split: 'end';
}
export type SplitUpdateStateRequest<S, A extends Action<string>> =
| SplitUpdateStateRequestStart<S, A>
| SplitUpdateStateRequestChunk
| SplitUpdateStateRequestEnd;
interface SplitUpdateStateAction<S, A extends Action<string>> {
readonly type: typeof UPDATE_STATE;
request: SplitUpdateStateRequest<S, A>;
readonly id: string | number;
}
export type TabMessage =
| StartAction
| StopAction
| OptionsAction
| DispatchAction
| ImportAction
| ActionAction
| ExportAction;
export type PanelMessageWithoutNA<S, A extends Action<string>> =
| ErrorMessage
| UpdateStateAction<S, A>
| SetPersistAction;
export type PanelMessage<S, A extends Action<string>> =
| PanelMessageWithoutNA<S, A>
| NAAction;
export type PanelMessageWithSplitAction<S, A extends Action<string>> =
| PanelMessage<S, A>
| SplitUpdateStateAction<S, A>;
type TabPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: TabMessage) => void;
};
type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: <S, A extends Action<string>>(
message: PanelMessageWithSplitAction<S, A>,
) => void;
};
export const CONNECTED = 'socket/CONNECTED';
export const DISCONNECTED = 'socket/DISCONNECTED';
const connections: {
readonly tab: { [K in number | string]: TabPort };
readonly panel: { [K in number | string]: PanelPort };
} = {
tab: {},
panel: {},
};
const chunks: {
[instanceId: string]: PageScriptToContentScriptMessageForwardedToMonitors<
unknown,
Action<string>
>;
} = {};
let monitors = 0;
const getId = (sender: chrome.runtime.MessageSender, name?: string) =>
sender.tab ? sender.tab.id! : name || sender.id!;
type MonitorAction<S, A extends Action<string>> =
| NAAction
| ErrorMessage
| UpdateStateAction<S, A>
| SetPersistAction;
// Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts
const maxChromeMsgSize = 32 * 1024 * 1024;
function toMonitors<S, A extends Action<string>>(action: MonitorAction<S, A>) {
console.log(`Message to monitors: ${action.type}`);
for (const port of Object.values(connections.panel)) {
try {
port.postMessage(action);
} catch (err) {
if (
action.type !== UPDATE_STATE ||
err == null ||
(err as Error).message !==
'Message length exceeded maximum allowed length.'
) {
throw err;
}
const splitMessageStart: SplitUpdateStateRequestStart<S, A> = {
split: 'start',
};
const toSplit: [string, string][] = [];
let size = 0;
for (const [key, value] of Object.entries(
action.request as unknown as Record<string, unknown>,
)) {
if (typeof value === 'string') {
size += value.length;
if (size > maxChromeMsgSize) {
toSplit.push([key, value]);
continue;
}
}
(splitMessageStart as any)[key as keyof typeof splitMessageStart] =
value;
}
port.postMessage({ ...action, request: splitMessageStart });
for (let i = 0; i < toSplit.length; i++) {
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
port.postMessage({
...action,
request: {
split: 'chunk',
chunk: [
toSplit[i][0],
toSplit[i][1].substring(j, j + maxChromeMsgSize),
],
},
});
}
}
port.postMessage({ ...action, request: { split: 'end' } });
}
}
}
interface ImportMessage {
readonly message: 'IMPORT';
readonly id: string | number;
readonly instanceId: string;
readonly state: string;
readonly action?: never;
}
type ToContentScriptMessage = ImportMessage | LiftedActionAction;
function toContentScript(messageBody: ToContentScriptMessage) {
console.log(`Message to tab ${messageBody.id}: ${messageBody.message}`);
if (messageBody.message === 'DISPATCH') {
const { message, action, id, instanceId, state } = messageBody;
connections.tab[id!].postMessage({
type: message,
action,
state: nonReduxDispatch(store, message, instanceId, action, state),
id: instanceId.toString().replace(/^[^/]+\//, ''),
});
} else if (messageBody.message === 'IMPORT') {
const { message, action, id, instanceId, state } = messageBody;
connections.tab[id!].postMessage({
type: message,
action,
state: nonReduxDispatch(
store,
message,
instanceId,
action as unknown as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^/]+\//, ''),
});
} else if (messageBody.message === 'ACTION') {
const { message, action, id, instanceId, state } = messageBody;
connections.tab[id!].postMessage({
type: message,
action,
state: nonReduxDispatch(
store,
message,
instanceId,
action as unknown as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^/]+\//, ''),
});
} else if (messageBody.message === 'EXPORT') {
const { message, action, id, instanceId, state } = messageBody;
connections.tab[id!].postMessage({
type: message,
action,
state: nonReduxDispatch(
store,
message,
instanceId,
action as unknown as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^/]+\//, ''),
});
} else {
const { message, action, id, instanceId, state } = messageBody;
connections.tab[id].postMessage({
type: message,
action,
state: nonReduxDispatch(
store,
message,
instanceId,
action as AppDispatchAction,
state,
),
id: (instanceId as number).toString().replace(/^[^/]+\//, ''),
});
}
}
function toAllTabs(msg: TabMessage) {
console.log(`Message to all tabs: ${msg.type}`);
for (const tabPort of Object.values(connections.tab)) {
tabPort.postMessage(msg);
}
}
function getReducerError() {
const instancesState = store.getState().instances;
const payload = instancesState.states[instancesState.current];
const computedState = payload.computedStates[payload.currentStateIndex];
if (!computedState) return false;
return computedState.error;
}
function togglePersist() {
const state = store.getState();
if (state.instances.persisted) {
for (const id of Object.keys(state.instances.connections)) {
if (connections.tab[id]) return;
store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
}
}
interface OpenMessage {
readonly type: 'OPEN';
readonly position: Position;
}
interface OpenOptionsMessage {
readonly type: 'OPEN_OPTIONS';
}
export type SingleMessage = OpenMessage | OpenOptionsMessage | OptionsMessage;
type BackgroundStoreMessage<S, A extends Action<string>> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage
| SingleMessage;
// Receive messages from content scripts
function messaging<S, A extends Action<string>>(
request: BackgroundStoreMessage<S, A>,
sender: chrome.runtime.MessageSender,
) {
let tabId = getId(sender);
console.log(`Message from tab ${tabId}: ${request.type ?? request.split}`);
if (!tabId) return;
if (sender.frameId) tabId = `${tabId}-${sender.frameId}`;
if (request.type === 'STOP') {
if (!Object.keys(store.getState().instances.connections).length) {
store.dispatch({ type: DISCONNECTED });
}
return;
}
if (request.type === 'OPEN_OPTIONS') {
void chrome.runtime.openOptionsPage();
return;
}
if (request.type === 'OPTIONS') {
toAllTabs({ type: 'OPTIONS', options: request.options });
return;
}
if (request.type === 'GET_REPORT') {
getReport(request.payload, tabId, request.instanceId);
return;
}
if (request.type === 'OPEN') {
let position: DevToolsPosition = 'devtools-window';
if (['remote', 'window'].includes(request.position)) {
position = ('devtools-' + request.position) as DevToolsPosition;
}
openDevToolsWindow(position);
return;
}
if (request.type === 'ERROR') {
if (request.payload) {
toMonitors(request);
return;
}
if (!request.message) return;
const reducerError = getReducerError();
void chrome.notifications.create('app-error', {
type: 'basic',
title: reducerError
? 'An error occurred in the reducer'
: 'An error occurred in the app',
message: reducerError || request.message,
iconUrl: 'img/logo/48x48.png',
isClickable: !!reducerError,
});
return;
}
const action: UpdateStateAction<S, A> = {
type: UPDATE_STATE,
request,
id: tabId,
} as UpdateStateAction<S, A>;
const instanceId = `${tabId}/${request.instanceId}`;
if ('split' in request) {
if (request.split === 'start') {
chunks[instanceId] = request as any;
return;
}
if (request.split === 'chunk') {
(chunks[instanceId] as any)[request.chunk[0]] =
((chunks[instanceId] as any)[request.chunk[0]] || '') +
request.chunk[1];
return;
}
action.request = chunks[instanceId] as any;
delete chunks[instanceId];
}
if (request.instanceId) {
action.request.instanceId = instanceId;
}
store.dispatch(action);
toMonitors(action);
}
function disconnect(
type: 'tab' | 'panel',
id: number | string,
listener: (message: any, port: chrome.runtime.Port) => void,
) {
return function disconnectListener() {
console.log(`Disconnected from ${type} ${id}`);
const p = connections[type][id];
if (listener && p) p.onMessage.removeListener(listener);
if (p) p.onDisconnect.removeListener(disconnectListener);
delete connections[type][id];
if (type === 'tab') {
if (!store.getState().instances.persisted) {
store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
} else {
monitors--;
if (monitors === 0) toAllTabs({ type: 'STOP' });
}
};
}
function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
let id: number | string;
let listener;
store.dispatch({ type: CONNECTED, port });
if (port.name === 'tab') {
id = getId(port.sender!);
console.log(`Connected to tab ${id}`);
if (port.sender!.frameId) id = `${id}-${port.sender!.frameId}`;
connections.tab[id] = port;
listener = (msg: ContentScriptToBackgroundMessage<S, A>) => {
console.log(`Message from tab ${id}: ${msg.name}`);
if (msg.name === 'INIT_INSTANCE') {
if (typeof id === 'number') {
void chrome.action.enable(id);
void chrome.action.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
}
if (monitors > 0) port.postMessage({ type: 'START' });
const state = store.getState();
if (state.instances.persisted) {
const instanceId = `${id}/${msg.instanceId}`;
const persistedState = state.instances.states[instanceId];
if (!persistedState) return;
toContentScript({
message: 'IMPORT',
id,
instanceId,
state: stringifyJSON(
persistedState,
state.instances.options[instanceId].serialize,
),
});
}
return;
}
if (msg.name === 'RELAY') {
messaging(msg.message, port.sender!);
}
};
port.onMessage.addListener(listener);
port.onDisconnect.addListener(disconnect('tab', id, listener));
} else if (port.name && port.name.indexOf('monitor') === 0) {
// devpanel
id = getId(port.sender!, port.name);
console.log(`Connected to monitor ${id}`);
connections.panel[id] = port;
monitors++;
toAllTabs({ type: 'START' });
listener = (msg: BackgroundAction) => {
console.log(`Message from monitor ${id}: ${msg.type}`);
store.dispatch(msg);
};
port.onMessage.addListener(listener);
port.onDisconnect.addListener(disconnect('panel', id, listener));
const { current } = store.getState().instances;
if (current !== 'default') {
const connectionId = Object.entries(
store.getState().instances.connections,
).find(([, instanceIds]) => instanceIds.includes(current))?.[0];
const options = store.getState().instances.options[current];
const state = store.getState().instances.states[current];
const { actionsById, computedStates, committedState, ...rest } = state;
toMonitors({
type: UPDATE_STATE,
request: {
type: 'STATE',
payload: rest as Omit<
LiftedState<S, A, unknown>,
'actionsById' | 'computedStates' | 'committedState'
>,
source: '@devtools-page',
instanceId:
typeof current === 'number' ? current.toString() : current,
actionsById: stringifyJSON(actionsById, options.serialize),
computedStates: stringifyJSON(computedStates, options.serialize),
committedState: typeof committedState !== 'undefined',
},
id: connectionId ?? current,
});
}
}
}
chrome.runtime.onConnect.addListener(onConnect);
chrome.runtime.onConnectExternal.addListener(onConnect);
chrome.runtime.onMessage.addListener(messaging);
chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener((id) => {
void chrome.notifications.clear(id);
openDevToolsWindow('devtools-window');
});
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
const api: Middleware<{}, BackgroundState, Dispatch<BackgroundAction>> =
(store) => (next) => (untypedAction) => {
const action = untypedAction as BackgroundAction;
if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === TOGGLE_PERSIST) {
togglePersist();
toMonitors({
type: SET_PERSIST,
payload: !store.getState().instances.persisted,
});
}
return next(action);
};
export default api;

View File

@ -1,17 +0,0 @@
import { combineReducers, Reducer } from 'redux';
import { instances, InstancesState } from '@redux-devtools/app';
import { BackgroundAction } from './backgroundStore.js';
export interface BackgroundState {
readonly instances: InstancesState;
}
const rootReducer: Reducer<
BackgroundState,
BackgroundAction,
Partial<BackgroundState>
> = combineReducers({
instances,
}) as any;
export default rootReducer;

View File

@ -1,77 +0,0 @@
import { createStore, applyMiddleware } from 'redux';
import {
CustomAction,
DispatchAction,
LIFTED_ACTION,
StoreActionWithoutLiftedAction,
} from '@redux-devtools/app';
import rootReducer, { BackgroundState } from './backgroundReducer.js';
import api, { CONNECTED, DISCONNECTED } from './apiMiddleware.js';
interface LiftedActionActionBase {
action?: DispatchAction | string | CustomAction;
state?: string;
toAll?: boolean;
readonly instanceId: string | number;
readonly id: string | number | undefined;
}
interface LiftedActionDispatchAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'DISPATCH';
action: DispatchAction;
toAll?: boolean;
}
interface LiftedActionImportAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'IMPORT';
state: string;
preloadedState?: unknown | undefined;
action?: never;
}
interface LiftedActionActionAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'ACTION';
action: string | CustomAction;
}
interface LiftedActionExportAction extends LiftedActionActionBase {
type: typeof LIFTED_ACTION;
message: 'EXPORT';
toExport: boolean;
action?: never;
}
export type LiftedActionAction =
| LiftedActionDispatchAction
| LiftedActionImportAction
| LiftedActionActionAction
| LiftedActionExportAction;
interface ConnectedAction {
readonly type: typeof CONNECTED;
}
interface DisconnectedAction {
readonly type: typeof DISCONNECTED;
}
export type BackgroundAction =
| StoreActionWithoutLiftedAction
| LiftedActionAction
| ConnectedAction
| DisconnectedAction;
export default function configureStore(
preloadedState?: Partial<BackgroundState>,
) {
return createStore(rootReducer, preloadedState, applyMiddleware(api));
/*
let enhancer;
if (process.env.NODE_ENV === 'production') {
enhancer = applyMiddleware(api);
} else {
const logger = require('redux-logger');
enhancer = applyMiddleware(api, logger());
}
return createStore(rootReducer, preloadedState, enhancer);
*/
}

View File

@ -1,113 +0,0 @@
// Mock not supported chrome.* API for Firefox and Electron
const isElectron = navigator.userAgent.includes('Electron');
const isFirefox = navigator.userAgent.includes('Firefox');
// Background page only
if (
(isElectron && location.pathname === '/background.bundle.js') ||
isFirefox
) {
(chrome.runtime as any).onConnectExternal = {
addListener() {
// do nothing.
},
};
(chrome.runtime as any).onMessageExternal = {
addListener() {
// do nothing.
},
};
if (isElectron) {
(chrome.notifications as any) = {
onClicked: {
addListener() {
// do nothing.
},
},
create() {
// do nothing.
},
clear() {
// do nothing.
},
};
(chrome.contextMenus as any) = {
onClicked: {
addListener() {
// do nothing.
},
},
};
(chrome.commands as any) = {
onCommand: {
addListener() {
// do nothing.
},
},
};
} else {
(chrome.storage as any).sync = chrome.storage.local;
(chrome.runtime as any).onInstalled = {
addListener: (cb: any) => cb(),
};
}
}
if (isElectron) {
if (!chrome.storage.local || !chrome.storage.local.remove) {
(chrome.storage as any).local = {
set(items: { [key: string]: string }, callback: () => void) {
for (const [key, value] of Object.entries(items)) {
localStorage.setItem(key, value);
}
if (callback) {
callback();
}
},
get(
keys: { [key: string]: any },
callback: (items: { [key: string]: any }) => void,
) {
const result = Object.fromEntries(
Object.entries(keys).map(([key, value]) => [
key,
localStorage.getItem(key) ?? value,
]),
);
if (callback) {
callback(result);
}
},
// Electron ~ 1.4.6
remove(keys: string | string[], callback: () => void) {
if (Array.isArray(keys)) {
for (const key of keys) {
localStorage.removeItem(key);
}
} else {
localStorage.removeItem(keys);
}
if (callback) {
callback();
}
},
};
}
// Avoid error: chrome.runtime.sendMessage is not supported responseCallback
const originSendMessage = (chrome.runtime as any).sendMessage;
(chrome.runtime as any).sendMessage = function (...args: unknown[]) {
if (process.env.NODE_ENV === 'development') {
return originSendMessage(...args);
}
if (typeof args[arguments.length - 1] === 'function') {
Array.prototype.pop.call(args);
}
return originSendMessage(...args);
};
}
if (isFirefox || isElectron) {
(chrome.storage as any).sync = chrome.storage.local;
}

View File

@ -1,345 +0,0 @@
import '../chromeApiMock.js';
import {
getOptions,
isAllowed,
Options,
prefetchOptions,
prepareOptionsForPage,
} from '../options/syncOptions.js';
import type { TabMessage } from '../background/store/apiMiddleware.js';
import type {
PageScriptToContentScriptMessage,
PageScriptToContentScriptMessageWithoutDisconnect,
PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance,
} from '../pageScript/api/index.js';
import { Action } from 'redux';
import {
CustomAction,
DispatchAction as AppDispatchAction,
} from '@redux-devtools/app';
import { LiftedState } from '@redux-devtools/instrument';
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: chrome.runtime.Port | undefined;
declare global {
interface Window {
devToolsExtensionID?: string;
}
}
interface StartAction {
readonly type: 'START';
readonly state: undefined;
readonly id: undefined;
readonly source: typeof source;
}
interface StopAction {
readonly type: 'STOP';
readonly state: undefined;
readonly id: undefined;
readonly source: typeof source;
readonly failed?: boolean;
}
interface DispatchAction {
readonly type: 'DISPATCH';
readonly payload: AppDispatchAction;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface ImportAction {
readonly type: 'IMPORT';
readonly payload: undefined;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface ActionAction {
readonly type: 'ACTION';
readonly payload: string | CustomAction;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface ExportAction {
readonly type: 'EXPORT';
readonly payload: undefined;
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface UpdateAction {
readonly type: 'UPDATE';
readonly state: string | undefined;
readonly id: string;
readonly source: typeof source;
}
interface OptionsAction {
readonly type: 'OPTIONS';
readonly options: Options;
readonly id: undefined;
readonly source: typeof source;
}
export type ContentScriptToPageScriptMessage =
| StartAction
| StopAction
| DispatchAction
| ImportAction
| ActionAction
| ExportAction
| UpdateAction
| OptionsAction;
interface ImportStatePayload<S, A extends Action<string>> {
readonly type: 'IMPORT_STATE';
readonly nextLiftedState: LiftedState<S, A, unknown> | readonly A[];
readonly preloadedState?: S;
}
interface ImportStateDispatchAction<S, A extends Action<string>> {
readonly type: 'DISPATCH';
readonly payload: ImportStatePayload<S, A>;
}
export type ListenerMessage<S, A extends Action<string>> =
| StartAction
| StopAction
| DispatchAction
| ImportAction
| ActionAction
| ExportAction
| UpdateAction
| OptionsAction
| ImportStateDispatchAction<S, A>;
function postToPageScript(message: ContentScriptToPageScriptMessage) {
window.postMessage(message, '*');
}
function connect() {
// Connect to the background script
connected = true;
const name = 'tab';
if (window.devToolsExtensionID) {
bg = chrome.runtime.connect(window.devToolsExtensionID, { name });
} else {
bg = chrome.runtime.connect({ name });
}
// Relay background script messages to the page script
bg.onMessage.addListener((message: TabMessage) => {
if ('action' in message) {
if (message.type === 'DISPATCH') {
postToPageScript({
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
});
} else if (message.type === 'ACTION') {
postToPageScript({
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
});
} else {
postToPageScript({
type: message.type,
payload: message.action,
state: message.state,
id: message.id,
source,
});
}
} else if (message.type === 'OPTIONS') {
postToPageScript({
type: message.type,
options: prepareOptionsForPage(message.options),
id: undefined,
source,
});
} else {
postToPageScript({
type: message.type,
state: message.state,
id: message.id,
source,
});
}
});
bg.onDisconnect.addListener(handleDisconnect);
}
function handleDisconnect() {
window.removeEventListener('message', handleMessages);
window.postMessage({ type: 'STOP', failed: true, source }, '*');
bg = undefined;
}
interface SplitMessageBase {
readonly type?: never;
}
interface SplitMessageStart extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'start';
}
interface SplitMessageChunk extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'chunk';
readonly chunk: [string, string];
}
interface SplitMessageEnd extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'end';
}
export type SplitMessage =
| SplitMessageStart
| SplitMessageChunk
| SplitMessageEnd;
function tryCatch<S, A extends Action<string>>(
fn: (
args:
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| SplitMessage,
) => void,
args: PageScriptToContentScriptMessageWithoutDisconnect<S, A>,
) {
try {
return fn(args);
} catch (err) {
if (
(err as Error).message ===
'Message length exceeded maximum allowed length.'
) {
const instanceId = (args as any).instanceId;
const newArgs = {
split: 'start',
};
const toSplit: [string, string][] = [];
let size = 0;
let arg;
Object.keys(args).map((key) => {
arg = args[key as keyof typeof args];
if (typeof arg === 'string') {
size += arg.length;
if (size > maxChromeMsgSize) {
toSplit.push([key, arg]);
return;
}
}
newArgs[key as keyof typeof newArgs] = arg;
});
fn(newArgs as SplitMessage);
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);
}
/* eslint-enable no-console */
}
}
interface InitInstanceContentScriptToBackgroundMessage {
readonly name: 'INIT_INSTANCE';
readonly instanceId: number;
}
interface RelayMessage<S, A extends Action<string>> {
readonly name: 'RELAY';
readonly message:
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage;
}
export type ContentScriptToBackgroundMessage<S, A extends Action<string>> =
| InitInstanceContentScriptToBackgroundMessage
| RelayMessage<S, A>;
function postToBackground<S, A extends Action<string>>(
message: ContentScriptToBackgroundMessage<S, A>,
) {
bg!.postMessage(message);
}
function send<S, A extends Action<string>>(
message:
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| SplitMessage,
) {
if (!connected) connect();
if (message.type === 'INIT_INSTANCE') {
getOptions((options) => {
postToPageScript({
type: 'OPTIONS',
options: prepareOptionsForPage(options),
id: undefined,
source,
});
});
postToBackground({ name: 'INIT_INSTANCE', instanceId: message.instanceId });
} else {
postToBackground({ name: 'RELAY', message });
}
}
// Resend messages from the page to the background script
function handleMessages<S, A extends Action<string>>(
event: MessageEvent<PageScriptToContentScriptMessage<S, A>>,
) {
if (!isAllowed()) return;
if (!event || event.source !== window || typeof event.data !== 'object') {
return;
}
const message = event.data;
if (message.source !== pageSource) return;
if (message.type === 'DISCONNECT') {
if (bg) {
bg.disconnect();
connected = false;
}
return;
}
tryCatch(send, message);
}
prefetchOptions();
window.addEventListener('message', handleMessages, false);

View File

@ -1,17 +0,0 @@
doctype html
html
head
meta(charset='UTF-8')
title Redux DevTools
include ../style.pug
body
#root
div(style='display: flex; justify-content: center; align-items: center')
img(
src='/img/loading.svg',
height=300, width=350,
)
link(href='/devpanel.bundle.css', rel='stylesheet')
script(src='/devpanel.bundle.js')

View File

@ -1,182 +0,0 @@
import '../chromeApiMock.js';
import React, { CSSProperties, ReactNode } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { Provider } from 'react-redux';
import { Persistor } from 'redux-persist';
import {
REMOVE_INSTANCE,
StoreAction,
StoreState,
UPDATE_STATE,
} from '@redux-devtools/app';
import App from '../app/App.js';
import configureStore from './store/panelStore.js';
import { Action, Store } from 'redux';
import {
PanelMessageWithoutNA,
PanelMessageWithSplitAction,
SplitUpdateStateRequest,
UpdateStateRequest,
} from '../background/store/apiMiddleware.js';
import { PersistGate } from 'redux-persist/integration/react';
const position = location.hash;
const messageStyle: CSSProperties = {
paddingTop: '20px',
width: '100%',
textAlign: 'center',
boxSizing: 'border-box',
};
let rendered: boolean | undefined;
let currentRoot: Root | undefined;
let store: Store<StoreState, StoreAction> | undefined;
let persistor: Persistor | undefined;
let bgConnection: chrome.runtime.Port;
let naTimeout: NodeJS.Timeout;
const isChrome = !navigator.userAgent.includes('Firefox');
function renderNodeAtRoot(node: ReactNode) {
if (currentRoot) currentRoot.unmount();
currentRoot = createRoot(document.getElementById('root')!);
currentRoot.render(node);
}
function renderDevTools() {
clearTimeout(naTimeout);
({ store, persistor } = configureStore(position, bgConnection));
renderNodeAtRoot(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App position={position} />
</PersistGate>
</Provider>,
);
rendered = true;
}
function renderNA() {
if (rendered === false) return;
rendered = false;
naTimeout = setTimeout(() => {
let message = (
<div style={messageStyle}>
No store found. Make sure to follow{' '}
<a
href="https://github.com/zalmoxisus/redux-devtools-extension#usage"
target="_blank"
rel="noreferrer"
>
the instructions
</a>
.
</div>
);
if (
isChrome &&
chrome &&
chrome.devtools &&
chrome.devtools.inspectedWindow
) {
chrome.devtools.inspectedWindow.getResources((resources) => {
if (resources[0].url.substr(0, 4) === 'file') {
message = (
<div style={messageStyle}>
No store found. Most likely you did not allow access to file URLs.{' '}
<a
href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Troubleshooting.md#access-file-url-file"
target="_blank"
rel="noreferrer"
>
See details
</a>
.
</div>
);
}
renderNodeAtRoot(message);
store = undefined;
});
} else {
renderNodeAtRoot(message);
store = undefined;
}
}, 3500);
}
let splitMessage: SplitUpdateStateRequest<unknown, Action<string>>;
function init() {
renderNA();
let name = 'monitor';
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
name += chrome.devtools.inspectedWindow.tabId;
}
bgConnection = chrome.runtime.connect({ name });
bgConnection.onMessage.addListener(
<S, A extends Action<string>>(
message: PanelMessageWithSplitAction<S, A>,
) => {
if (message.type === 'NA') {
// TODO Double-check this now that the name is different
if (message.id === name) renderNA();
else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id });
} else {
if (!rendered) renderDevTools();
if (
message.type === UPDATE_STATE &&
(message.request as SplitUpdateStateRequest<S, A>).split
) {
const request = message.request as SplitUpdateStateRequest<S, A>;
if (request.split === 'start') {
splitMessage = request;
return;
}
if (request.split === 'chunk') {
if (
(splitMessage as unknown as Record<string, string>)[
request.chunk[0]
]
) {
(splitMessage as unknown as Record<string, string>)[
request.chunk[0]
] += request.chunk[1];
} else {
(splitMessage as unknown as Record<string, string>)[
request.chunk[0]
] = request.chunk[1];
}
return;
}
if (request.split === 'end') {
store!.dispatch({
...message,
request: splitMessage as UpdateStateRequest<S, A>,
});
return;
}
throw new Error(
`Unable to process split message with type: ${(request as any).split}`,
);
} else {
store!.dispatch(message as PanelMessageWithoutNA<S, A>);
}
}
},
);
}
if (position === '#popup') document.body.style.minWidth = '760px';
if (position !== '#popup') document.body.style.minHeight = '100%';
init();

View File

@ -1,32 +0,0 @@
import { combineReducers, Reducer } from 'redux';
import {
connection,
instances,
monitor,
notification,
reports,
section,
socket,
stateTreeSettings,
StoreAction,
StoreState,
theme,
} from '@redux-devtools/app';
const rootReducer: Reducer<
StoreState,
StoreAction,
Partial<StoreState>
> = combineReducers({
instances,
monitor,
reports,
notification,
section,
socket,
theme,
connection,
stateTreeSettings,
}) as any;
export default rootReducer;

View File

@ -1,34 +0,0 @@
import { createStore, applyMiddleware, Reducer, Store } from 'redux';
import localForage from 'localforage';
import { persistReducer, persistStore } from 'redux-persist';
import {
exportStateMiddleware,
StoreAction,
StoreState,
} from '@redux-devtools/app';
import panelDispatcher from './panelSyncMiddleware.js';
import rootReducer from './panelReducer.js';
const persistConfig = {
key: 'redux-devtools',
blacklist: ['instances', 'socket'],
storage: localForage,
};
const persistedReducer: Reducer<StoreState, StoreAction> = persistReducer(
persistConfig,
rootReducer,
) as any;
export default function configureStore(
position: string,
bgConnection: chrome.runtime.Port,
) {
const enhancer = applyMiddleware(
exportStateMiddleware,
panelDispatcher(bgConnection),
);
const store = createStore(persistedReducer, enhancer);
const persistor = persistStore(store as Store);
return { store, persistor };
}

View File

@ -1,68 +0,0 @@
import {
getActiveInstance,
LIFTED_ACTION,
SELECT_INSTANCE,
StoreAction,
StoreState,
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import { Dispatch, Middleware, MiddlewareAPI } from 'redux';
function selectInstance(
tabId: number,
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
next: (action: unknown) => unknown,
) {
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: (tabId: number) => void) {
chrome.tabs.query(
{
active: true,
lastFocusedWindow: true,
},
(tabs) => {
const tab = tabs[0];
if (!tab) return;
next(tab.id!);
},
);
}
function panelDispatcher(
bgConnection: chrome.runtime.Port,
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
): Middleware<{}, StoreState, Dispatch<StoreAction>> {
let autoselected = false;
return (store) => (next) => (untypedAction) => {
const action = untypedAction as StoreAction;
const result = next(action);
if (!autoselected && action.type === UPDATE_STATE) {
autoselected = true;
if (chrome.devtools && chrome.devtools.inspectedWindow) {
selectInstance(chrome.devtools.inspectedWindow.tabId, store, next);
} else {
getCurrentTabId((tabId) => selectInstance(tabId, store, next));
}
}
if (action.type === LIFTED_ACTION || action.type === TOGGLE_PERSIST) {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
bgConnection.postMessage({ ...action, instanceId, id });
}
return result;
};
}
export default panelDispatcher;

View File

@ -1,10 +0,0 @@
doctype html
html
head
meta(charset='UTF-8')
title Redux DevTools
body
#root
script(src='/devtools.bundle.js')

View File

@ -1,8 +0,0 @@
chrome.devtools.panels.create(
'Redux',
'img/logo/scalable.png',
'devpanel.html',
() => {
// do nothing.
},
);

View File

@ -1,53 +0,0 @@
import React from 'react';
import { OptionsProps } from './Options.js';
export default function AllowToRunGroup({ options, saveOption }: OptionsProps) {
const AllowToRunState = {
EVERYWHERE: true,
ON_SPECIFIC_URLS: false,
};
return (
<fieldset className="option-group">
<legend className="option-group__title">Allow to run</legend>
<div className="option option_type_radio">
<input
className="option__element"
id="inject-always"
name="inject"
type="radio"
checked={options.inject === AllowToRunState.EVERYWHERE}
onChange={() => saveOption('inject', AllowToRunState.EVERYWHERE)}
/>
<label className="option__label" htmlFor="inject-always">
Everywhere
</label>
</div>
<div className="option option_type_radio">
<input
className="option__element"
id="inject-specific"
name="inject"
type="radio"
checked={options.inject === AllowToRunState.ON_SPECIFIC_URLS}
onChange={() =>
saveOption('inject', AllowToRunState.ON_SPECIFIC_URLS)
}
/>
<label className="option__label" htmlFor="inject-specific">
Only on the following URLs:
</label>
<br />
<textarea
className="option__textarea"
value={options.urls}
disabled={options.inject !== AllowToRunState.ON_SPECIFIC_URLS}
onChange={(e) => saveOption('urls', e.target.value)}
/>
<div className="option__hint">Each RegExp from the new line</div>
</div>
</fieldset>
);
}

View File

@ -1,29 +0,0 @@
import React from 'react';
import { OptionsProps } from './Options.js';
export default function ContextMenuGroup({
options,
saveOption,
}: OptionsProps) {
return (
<fieldset className="option-group">
<legend className="option-group__title">Context Menu</legend>
<div className="option option_type_checkbox">
<input
className="option__element"
id="showContextMenus"
type="checkbox"
checked={options.showContextMenus}
onChange={(e) => saveOption('showContextMenus', e.target.checked)}
/>
<label className="option__label" htmlFor="showContextMenus">
Add Context Menus
</label>
<div className="option__hint">
Add Redux DevTools to right-click context menu
</div>
</div>
</fieldset>
);
}

View File

@ -1,83 +0,0 @@
import React from 'react';
import { OptionsProps } from './Options.js';
export default function EditorGroup({ options, saveOption }: OptionsProps) {
const EditorState = {
BROWSER: 0,
EXTERNAL: 1,
};
return (
<fieldset className="option-group">
<legend className="option-group__title">Editor for stack traces</legend>
<div className="option option_type_radio">
<input
className="option__element"
id="editor-browser"
name="useEditor"
type="radio"
checked={options.useEditor === EditorState.BROWSER}
onChange={() => saveOption('useEditor', EditorState.BROWSER)}
/>
<label className="option__label" htmlFor="editor-browser">
{navigator.userAgent.includes('Firefox')
? "Don't open in external editor"
: "Use browser's debugger (from browser devpanel only)"}
</label>
</div>
<div
className="option option_type_radio"
style={{ display: 'flex', alignItems: 'center' }}
>
<input
className="option__element"
id="editor-external"
name="useEditor"
type="radio"
checked={options.useEditor === EditorState.EXTERNAL}
onChange={() => saveOption('useEditor', EditorState.EXTERNAL)}
/>
<label className="option__label" htmlFor="editor-external">
External editor:&nbsp;
</label>
<input
className="option__element"
id="editor"
type="text"
size={33}
maxLength={30}
placeholder="vscode, atom, webstorm, sublime..."
value={options.editor}
disabled={options.useEditor !== EditorState.EXTERNAL}
onChange={(e) =>
saveOption('editor', e.target.value.replace(/\W/g, ''))
}
/>
</div>
<div className="option option_type_radio">
<label
className="option__label"
htmlFor="editor-external"
style={{ marginLeft: '20px' }}
>
Absolute path to the project directory to open:
</label>
<br />
<textarea
className="option__textarea"
placeholder="/home/user/my-awesome-app"
value={options.projectPath}
disabled={options.useEditor !== EditorState.EXTERNAL}
onChange={(e) =>
saveOption('projectPath', e.target.value.replace('\n', ''))
}
/>
<div className="option__hint">
Run `pwd` in your project root directory to get it
</div>
</div>
</fieldset>
);
}

View File

@ -1,71 +0,0 @@
import React from 'react';
import { FilterState } from '../pageScript/api/filters.js';
import { OptionsProps } from './Options.js';
export default function FilterGroup({ options, saveOption }: OptionsProps) {
return (
<fieldset className="option-group">
<legend className="option-group__title">
Filter actions in DevTools
</legend>
<div className="option option_type_radio">
<input
className="option__element"
id="filter-do-not"
name="filter"
type="radio"
checked={options.filter === FilterState.DO_NOT_FILTER}
onChange={() => saveOption('filter', FilterState.DO_NOT_FILTER)}
/>
<label className="option__label" htmlFor="filter-do-not">
Dont filter
</label>
</div>
<div className="option option_type_radio">
<input
className="option__element"
id="filter-hide"
name="filter"
type="radio"
checked={options.filter === FilterState.DENYLIST_SPECIFIC}
onChange={() => saveOption('filter', FilterState.DENYLIST_SPECIFIC)}
/>
<label className="option__label" htmlFor="filter-hide">
Hide the following:
</label>
<br />
<textarea
className="option__textarea"
value={options.denylist}
disabled={options.filter !== FilterState.DENYLIST_SPECIFIC}
onChange={(e) => saveOption('denylist', e.target.value)}
/>
<div className="option__hint">Each action from the new line</div>
</div>
<div className="option option_type_radio">
<input
className="option__element"
id="filter-show"
name="filter"
type="radio"
checked={options.filter === FilterState.ALLOWLIST_SPECIFIC}
onChange={() => saveOption('filter', FilterState.ALLOWLIST_SPECIFIC)}
/>
<label className="option__label" htmlFor="filter-show">
Show the following:
</label>
<br />
<textarea
className="option__textarea"
value={options.allowlist}
disabled={options.filter !== FilterState.ALLOWLIST_SPECIFIC}
onChange={(e) => saveOption('allowlist', e.target.value)}
/>
<div className="option__hint">Each action from the new line</div>
</div>
</fieldset>
);
}

View File

@ -1,53 +0,0 @@
import React from 'react';
import { OptionsProps } from './Options.js';
export default function MiscellaneousGroup({
options,
saveOption,
}: OptionsProps) {
return (
<fieldset className="option-group">
<legend className="option-group__title">Miscellaneous</legend>
<div className="option option_value_max-age">
<label className="option__label" htmlFor="maxAge">
Limit the action history to
</label>{' '}
<input
className="option__element"
id="maxAge"
type="number"
min="2"
value={options.maxAge}
onChange={(e) => saveOption('maxAge', Number(e.target.value))}
/>{' '}
<label className="option__label" htmlFor="maxAge">
items
</label>
<div className="option__hint">
When the number is reached, DevTools start removing the oldest
actions. Improves the DevTools performance.{' '}
<a href="https://github.com/zalmoxisus/redux-devtools-extension/pull/54#issuecomment-188167725">
More info
</a>
</div>
</div>
<div className="option option_type_checkbox">
<input
className="option__element"
id="notifyErrors"
type="checkbox"
checked={options.shouldCatchErrors}
onChange={(e) => saveOption('shouldCatchErrors', e.target.checked)}
/>
<label className="option__label" htmlFor="notifyErrors">
Show errors
</label>
<div className="option__hint">
Show the browser notifications when errors occur in the app
</div>
</div>
</fieldset>
);
}

View File

@ -1,51 +0,0 @@
import React from 'react';
import EditorGroup from './EditorGroup.js';
import FilterGroup from './FilterGroup.js';
import AllowToRunGroup from './AllowToRunGroup.js';
import MiscellaneousGroup from './MiscellaneousGroup.js';
import ContextMenuGroup from './ContextMenuGroup.js';
import { Options } from './syncOptions.js';
export interface OptionsProps {
readonly options: Options;
readonly saveOption: <K extends keyof Options>(
name: K,
value: Options[K],
) => void;
}
export default function OptionsComponent(props: OptionsProps) {
return (
<div>
<EditorGroup {...props} />
<FilterGroup {...props} />
<AllowToRunGroup {...props} />
<MiscellaneousGroup {...props} />
<ContextMenuGroup {...props} />
<div style={{ color: 'red' }}>
<br />
<hr />
Setting options here is discouraged, and will not be possible in the
next major release. Please{' '}
<a
href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md"
target="_blank"
rel="noreferrer"
style={{ color: 'red' }}
>
specify them as parameters
</a>
. See{' '}
<a
href="https://github.com/zalmoxisus/redux-devtools-extension/issues/296"
target="_blank"
rel="noreferrer"
style={{ color: 'red' }}
>
the issue
</a>{' '}
for more details.
</div>
</div>
);
}

View File

@ -1,26 +0,0 @@
import '../chromeApiMock.js';
import React from 'react';
import { createRoot } from 'react-dom/client';
import OptionsComponent from './Options.js';
import {
getOptions,
Options,
OptionsMessage,
saveOption,
subscribeToOptions,
} from './syncOptions.js';
subscribeToOptions((options) => {
const message: OptionsMessage = { type: 'OPTIONS', options };
void chrome.runtime.sendMessage(message);
});
const renderOptions = (options: Options) => {
const root = createRoot(document.getElementById('root')!);
root.render(<OptionsComponent options={options} saveOption={saveOption} />);
};
subscribeToOptions(renderOptions);
getOptions((options) => {
renderOptions(options);
});

View File

@ -1,93 +0,0 @@
doctype html
html
head
meta(charset='UTF-8')
title Redux DevTools Options
style.
body {
padding: 2px;
min-width: 380px;
}
.option-group {
/* Reset the default fieldset styles */
margin: initial;
border: initial;
padding: initial;
}
.option-group + .option-group {
margin-top: 30px;
}
.option-group__title {
/* Reset the default legend styles */
margin: initial;
padding: initial;
margin-bottom: 8px;
font-weight: bold;
font-size: 30px;
}
.option + .option {
margin-top: 5px;
}
.option__textarea {
margin-top: 2px;
width: 300px;
min-height: 50px;
}
.option__hint {
margin-top: 2px;
font-size: 10px;
}
.option__textarea + .option__hint {
margin-top: -2px;
}
/* Checkbox and radio styling */
.option_type_checkbox .option__element,
.option_type_radio .option__element {
vertical-align: bottom;
}
.option_type_checkbox .option__label,
.option_type_radio .option__label {
margin-left: 4px;
}
.option_type_checkbox .option__textarea,
.option_type_checkbox .option__hint,
.option_type_radio .option__textarea,
.option_type_radio .option__hint {
margin-left: 20px;
}
/* Checkbox styling */
.option_type_checkbox .option__element {
/* Checkboxes in Chrome are 2px narrower than radio buttons.
These margins align them. */
margin-left: 1px;
/* ...margin-right is 2px instead of 1px
because both radios and checkboxes have initial margin-right of 1px */
margin-right: 2px;
}
/* Value-based styling */
.option_value_max-age {
margin-left: 20px;
}
.option_value_max-age .option__element {
width: 50px;
}
body
#root
script(src='/options.bundle.js')

View File

@ -1,130 +0,0 @@
import { FilterState, FilterStateValue } from '../pageScript/api/filters.js';
export interface Options {
readonly useEditor: number;
readonly editor: string;
readonly projectPath: string;
readonly maxAge: number;
readonly filter: FilterStateValue;
readonly allowlist: string;
readonly denylist: string;
readonly shouldCatchErrors: boolean;
readonly inject: boolean;
readonly urls: string;
readonly showContextMenus: boolean;
}
interface OldOrNewOptions {
readonly useEditor: number;
readonly editor: string;
readonly projectPath: string;
readonly maxAge: number;
readonly filter:
| FilterStateValue
| 'WHITELIST_SPECIFIC'
| 'BLACKLIST_SPECIFIC'
| boolean;
readonly allowlist: string;
readonly denylist: string;
readonly whitelist: string;
readonly blacklist: string;
readonly shouldCatchErrors: boolean;
readonly inject: boolean;
readonly urls: string;
readonly showContextMenus: boolean;
}
let options: Options | undefined;
let subscribers: ((options: Options) => void)[] = [];
export interface OptionsMessage {
readonly type: 'OPTIONS';
readonly options: Options;
}
export const saveOption = <K extends keyof Options>(
key: K,
value: Options[K],
) => {
const obj: { [K1 in keyof Options]?: Options[K1] } = {};
obj[key] = value;
void chrome.storage.sync.set(obj);
options![key] = value;
for (const subscriber of subscribers) {
subscriber(options!);
}
};
const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({
...oldOptions,
filter:
// Migrate the old `filter` option from 2.2.1
typeof oldOptions.filter === 'boolean'
? oldOptions.filter && oldOptions.whitelist.length > 0
? FilterState.ALLOWLIST_SPECIFIC
: oldOptions.filter
? FilterState.DENYLIST_SPECIFIC
: FilterState.DO_NOT_FILTER
: oldOptions.filter === 'WHITELIST_SPECIFIC'
? FilterState.ALLOWLIST_SPECIFIC
: oldOptions.filter === 'BLACKLIST_SPECIFIC'
? FilterState.DENYLIST_SPECIFIC
: oldOptions.filter,
});
export const getOptions = (callback: (options: Options) => void) => {
if (options) callback(options);
else {
chrome.storage.sync.get<OldOrNewOptions>(
{
useEditor: 0,
editor: '',
projectPath: '',
maxAge: 50,
filter: FilterState.DO_NOT_FILTER,
whitelist: '',
blacklist: '',
allowlist: '',
denylist: '',
shouldCatchErrors: false,
inject: true,
urls: '^https?://localhost|0\\.0\\.0\\.0:\\d+\n^https?://.+\\.github\\.io',
showContextMenus: true,
},
function (items) {
options = migrateOldOptions(items);
callback(options);
},
);
}
};
export const prefetchOptions = () =>
getOptions(() => {
// do nothing.
});
export const subscribeToOptions = (callback: (options: Options) => void) => {
subscribers = subscribers.concat(callback);
};
const toReg = (str: string) =>
str !== '' ? str.split('\n').filter(Boolean).join('|') : null;
export const prepareOptionsForPage = (options: Options): Options => ({
...options,
allowlist:
options.filter !== FilterState.DO_NOT_FILTER
? toReg(options.allowlist)!
: options.allowlist,
denylist:
options.filter !== FilterState.DO_NOT_FILTER
? toReg(options.denylist)!
: options.denylist,
});
export const isAllowed = (localOptions = options) =>
!localOptions ||
localOptions.inject ||
!localOptions.urls ||
location.href.match(toReg(localOptions.urls)!);

View File

@ -1,78 +0,0 @@
import { Action } from 'redux';
import { LiftedState } from '@redux-devtools/instrument';
import { DispatchAction, LibConfig } from '@redux-devtools/app';
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_LOCKED__?: boolean;
}
}
export default class Monitor<S, A extends Action<string>> {
update: (
liftedState?: LiftedState<S, A, unknown> | undefined,
libConfig?: LibConfig,
) => void;
active?: boolean;
paused?: boolean;
lastAction?: string;
waitingTimeout?: number;
constructor(
update: (
liftedState?: LiftedState<S, A, unknown> | undefined,
libConfig?: LibConfig,
) => void,
) {
this.update = update;
}
reducer = (state = {}, action: DispatchAction) => {
if (!this.active) return state;
this.lastAction = action.type;
if (action.type === 'LOCK_CHANGES') {
window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__ = action.status;
} else if (action.type === 'PAUSE_RECORDING') {
this.paused = action.status;
} else if (this.isHotReloaded()) {
// Send new lifted state on hot-reloading
setTimeout(this.update, 0);
}
return state;
};
start = (skipUpdate: boolean) => {
this.active = true;
if (!skipUpdate) this.update();
};
stop = () => {
this.active = false;
clearTimeout(this.waitingTimeout);
};
isHotReloaded = () =>
this.lastAction && /^@@redux\/(INIT|REPLACE)/.test(this.lastAction);
isMonitorAction = () =>
this.lastAction && this.lastAction !== 'PERFORM_ACTION';
isTimeTraveling = () =>
this.lastAction === 'JUMP_TO_STATE' || this.lastAction === 'JUMP_TO_ACTION';
isPaused = () => {
if (this.paused) {
if (this.lastAction !== 'BLOCKED') {
if (!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__) {
this.lastAction = 'BLOCKED';
}
return false;
}
return true;
}
return false;
};
isLocked = () => {
if (window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__) {
if (this.lastAction !== 'BLOCKED') {
this.lastAction = 'BLOCKED';
return false;
}
return true;
}
return false;
};
}

View File

@ -1,197 +0,0 @@
import { Action } from 'redux';
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
import { LocalFilter } from '@redux-devtools/utils';
export type FilterStateValue =
| 'DO_NOT_FILTER'
| 'DENYLIST_SPECIFIC'
| 'ALLOWLIST_SPECIFIC';
export const FilterState: { [K in FilterStateValue]: FilterStateValue } = {
DO_NOT_FILTER: 'DO_NOT_FILTER',
DENYLIST_SPECIFIC: 'DENYLIST_SPECIFIC',
ALLOWLIST_SPECIFIC: 'ALLOWLIST_SPECIFIC',
};
export const noFiltersApplied = (localFilter: LocalFilter | undefined) =>
// !predicate &&
!localFilter &&
(!window.devToolsOptions ||
!window.devToolsOptions.filter ||
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER);
export function isFiltered<A extends Action<string>>(
action: A | string,
localFilter: LocalFilter | undefined,
) {
if (
noFiltersApplied(localFilter) ||
(typeof action !== 'string' && typeof action.type.match !== 'function')
) {
return false;
}
const { allowlist, denylist } = localFilter || window.devToolsOptions || {};
const actionType = ((action as A).type || action) as string;
return (
(allowlist && !actionType.match(allowlist)) ||
(denylist && actionType.match(denylist))
);
}
function filterActions<A extends Action<string>>(
actionsById: { [p: number]: PerformAction<A> },
actionSanitizer: ((action: A, id: number) => A) | undefined,
): { [p: number]: PerformAction<A> } {
if (!actionSanitizer) return actionsById;
return Object.fromEntries(
Object.entries(actionsById).map(([actionId, action]) => [
actionId,
{
...action,
action: actionSanitizer(action.action, actionId as unknown as number),
},
]),
);
}
function filterStates<S>(
computedStates: { state: S; error?: string | undefined }[],
stateSanitizer: ((state: S, index: number) => S) | undefined,
) {
if (!stateSanitizer) return computedStates;
return computedStates.map((state, idx) => ({
...state,
state: stateSanitizer(state.state, idx),
}));
}
export function filterState<S, A extends Action<string>>(
state: LiftedState<S, A, unknown>,
localFilter: LocalFilter | undefined,
stateSanitizer: ((state: S, index: number) => S) | undefined,
actionSanitizer: ((action: A, id: number) => A) | undefined,
predicate: ((state: S, action: A) => boolean) | undefined,
): LiftedState<S, A, unknown> {
if (predicate || !noFiltersApplied(localFilter)) {
const filteredStagedActionIds: number[] = [];
const filteredComputedStates: { state: S; error?: string | undefined }[] =
[];
const sanitizedActionsById: { [p: number]: PerformAction<A> } | undefined =
actionSanitizer && {};
const { actionsById } = state;
const { computedStates } = state;
state.stagedActionIds.forEach((id, idx) => {
const liftedAction = actionsById[id];
if (!liftedAction) return;
const currAction = liftedAction.action;
const liftedState = computedStates[idx];
const currState = liftedState.state;
if (idx) {
if (predicate && !predicate(currState, currAction)) return;
if (isFiltered(currAction, localFilter)) return;
}
filteredStagedActionIds.push(id);
filteredComputedStates.push(
stateSanitizer
? { ...liftedState, state: stateSanitizer(currState, idx) }
: liftedState,
);
if (actionSanitizer) {
sanitizedActionsById![id] = {
...liftedAction,
action: actionSanitizer(currAction, id),
};
}
});
return {
...state,
actionsById: sanitizedActionsById || actionsById,
stagedActionIds: filteredStagedActionIds,
computedStates: filteredComputedStates,
};
}
if (!stateSanitizer && !actionSanitizer) return state;
return {
...state,
actionsById: filterActions(state.actionsById, actionSanitizer),
computedStates: filterStates(state.computedStates, stateSanitizer),
};
}
export interface PartialLiftedState<S, A extends Action<string>> {
readonly actionsById: { [actionId: number]: PerformAction<A> };
readonly computedStates: { state: S; error?: string }[];
readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number;
readonly nextActionId: number;
readonly committedState?: S;
}
export function startingFrom<S, A extends Action<string>>(
sendingActionId: number,
state: LiftedState<S, A, unknown>,
localFilter: LocalFilter | undefined,
stateSanitizer: (<S>(state: S, index: number) => S) | undefined,
actionSanitizer:
| (<A extends Action<string>>(action: A, id: number) => A)
| undefined,
predicate:
| (<S, A extends Action<string>>(state: S, action: A) => boolean)
| undefined,
): LiftedState<S, A, unknown> | PartialLiftedState<S, A> | undefined {
const stagedActionIds = state.stagedActionIds;
if (sendingActionId <= stagedActionIds[1]) return state;
const index = stagedActionIds.indexOf(sendingActionId);
if (index === -1) return state;
const shouldFilter = predicate || !noFiltersApplied(localFilter);
const filteredStagedActionIds = shouldFilter ? [0] : stagedActionIds;
const actionsById = state.actionsById;
const computedStates = state.computedStates;
const newActionsById: { [key: number]: PerformAction<A> } = {};
const newComputedStates = [];
let key;
let currAction;
let currState;
for (let i = shouldFilter ? 1 : index; i < stagedActionIds.length; i++) {
key = stagedActionIds[i];
currAction = actionsById[key];
currState = computedStates[i];
if (shouldFilter) {
if (
(predicate && !predicate(currState.state, currAction.action)) ||
isFiltered(currAction.action, localFilter)
) {
continue;
}
filteredStagedActionIds.push(key);
if (i < index) continue;
}
newActionsById[key] = !actionSanitizer
? currAction
: { ...currAction, action: actionSanitizer(currAction.action, key) };
newComputedStates.push(
!stateSanitizer
? currState
: { ...currState, state: stateSanitizer(currState.state, i) },
);
}
if (newComputedStates.length === 0) return undefined;
return {
actionsById: newActionsById,
computedStates: newComputedStates,
stagedActionIds: filteredStagedActionIds,
currentStateIndex: state.currentStateIndex,
nextActionId: state.nextActionId,
};
}

View File

@ -1,5 +0,0 @@
let id = 0;
export default function generateId(instanceId: number | undefined) {
return instanceId || ++id;
}

View File

@ -1,76 +0,0 @@
import jsan from 'jsan';
import { immutableSerialize } from '@redux-devtools/serialize';
import type { Config, SerializeWithImmutable } from '../index.js';
import Immutable from 'immutable';
import { LiftedState } from '@redux-devtools/instrument';
import { Action } from 'redux';
interface SerializeWithRequiredImmutable extends SerializeWithImmutable {
readonly immutable: typeof Immutable;
}
function isSerializeWithImmutable(
serialize: boolean | SerializeWithImmutable,
): serialize is SerializeWithRequiredImmutable {
return !!(serialize as SerializeWithImmutable).immutable;
}
interface SerializeWithRequiredReviver extends SerializeWithImmutable {
readonly reviver: (key: string, value: unknown) => unknown;
}
function isSerializeWithReviver(
serialize: boolean | SerializeWithImmutable,
): serialize is SerializeWithRequiredReviver {
return !!(serialize as SerializeWithImmutable).immutable;
}
interface ParsedSerializedLiftedState {
readonly payload: string;
readonly preloadedState?: string;
}
export default function importState<S, A extends Action<string>>(
state: string | undefined,
{ serialize }: Config,
) {
if (!state) return undefined;
let parse = jsan.parse;
if (serialize) {
if (isSerializeWithImmutable(serialize)) {
parse = (v) =>
jsan.parse(
v,
immutableSerialize(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver,
).reviver,
);
} else if (isSerializeWithReviver(serialize)) {
parse = (v) => jsan.parse(v, serialize.reviver);
}
}
const parsedSerializedLiftedState:
| ParsedSerializedLiftedState
| LiftedState<S, A, unknown> = parse(state) as
| ParsedSerializedLiftedState
| LiftedState<S, A, unknown>;
const preloadedState =
'payload' in parsedSerializedLiftedState &&
parsedSerializedLiftedState.preloadedState
? (parse(parsedSerializedLiftedState.preloadedState) as S)
: undefined;
const nextLiftedState =
'payload' in parsedSerializedLiftedState
? (parse(parsedSerializedLiftedState.payload) as LiftedState<
S,
A,
unknown
>)
: parsedSerializedLiftedState;
return { nextLiftedState, preloadedState };
}

View File

@ -1,706 +0,0 @@
import jsan, { Options } from 'jsan';
import { throttle } from 'lodash-es';
import { immutableSerialize } from '@redux-devtools/serialize';
import { getActionsArray, getLocalFilter } from '@redux-devtools/utils';
import { isFiltered, PartialLiftedState } from './filters.js';
import importState from './importState.js';
import generateId from './generateInstanceId.js';
import type { Config } from '../index.js';
import { Action } from 'redux';
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
import { LibConfig } from '@redux-devtools/app';
import type {
ContentScriptToPageScriptMessage,
ListenerMessage,
} from '../../contentScript/index.js';
import type { Position } from './openWindow.js';
const listeners: {
[instanceId: string]:
| ((message: ContentScriptToPageScriptMessage) => void)
| ((message: ContentScriptToPageScriptMessage) => void)[];
} = {};
export const source = '@devtools-page';
function windowReplacer(key: string, value: unknown) {
if (value && (value as Window).window === value) {
return '[WINDOW]';
}
return value;
}
function tryCatchStringify(obj: unknown) {
try {
return JSON.stringify(obj);
} catch (err) {
/* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') {
console.log('Failed to stringify', err);
}
/* eslint-enable no-console */
return jsan.stringify(obj, windowReplacer, undefined, {
circular: '[CIRCULAR]',
date: true,
});
}
}
let stringifyWarned: boolean;
function stringify(obj: unknown, serialize?: Serialize | undefined) {
const str =
typeof serialize === 'undefined'
? tryCatchStringify(obj)
: jsan.stringify(obj, serialize.replacer, undefined, serialize.options);
if (!stringifyWarned && str && str.length > 16 * 1024 * 1024) {
// 16 MB
/* eslint-disable no-console */
console.warn(
'Application state or actions payloads are too large making Redux DevTools serialization slow and consuming a lot of memory. See https://github.com/reduxjs/redux-devtools-extension/blob/master/docs/Troubleshooting.md#excessive-use-of-memory-and-cpu on how to configure it.',
);
/* eslint-enable no-console */
stringifyWarned = true;
}
return str;
}
export interface Serialize {
readonly replacer?: (key: string, value: unknown) => unknown;
readonly reviver?: (key: string, value: unknown) => unknown;
readonly options?: Options | boolean;
}
export function getSerializeParameter(config: Config) {
const serialize = config.serialize;
if (serialize) {
if (serialize === true) return { options: true };
if (serialize.immutable) {
const immutableSerializer = immutableSerialize(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver,
);
return {
replacer: immutableSerializer.replacer,
reviver: immutableSerializer.reviver,
options:
typeof serialize.options === 'object'
? { ...immutableSerializer.options, ...serialize.options }
: immutableSerializer.options,
};
}
if (!serialize.replacer && !serialize.reviver) {
return { options: serialize.options };
}
return {
replacer: serialize.replacer,
reviver: serialize.reviver,
options: serialize.options || true,
};
}
return undefined;
}
interface InitInstancePageScriptToContentScriptMessage {
readonly type: 'INIT_INSTANCE';
readonly instanceId: number;
readonly source: typeof source;
}
interface DisconnectMessage {
readonly type: 'DISCONNECT';
readonly source: typeof source;
}
interface InitMessage<S, A extends Action<string>> {
readonly type: 'INIT';
readonly payload: string;
readonly instanceId: number;
readonly source: typeof source;
action?: string;
name?: string | undefined;
liftedState?: LiftedState<S, A, unknown>;
libConfig?: LibConfig;
}
interface SerializedPartialLiftedState {
readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number;
readonly nextActionId: number;
}
interface SerializedPartialStateMessage {
readonly type: 'PARTIAL_STATE';
readonly payload: SerializedPartialLiftedState;
readonly source: typeof source;
readonly instanceId: number;
readonly maxAge: number;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
interface SerializedExportMessage {
readonly type: 'EXPORT';
readonly payload: string;
readonly committedState: string | undefined;
readonly source: typeof source;
readonly instanceId: number;
}
interface SerializedActionMessage {
readonly type: 'ACTION';
readonly payload: string;
readonly source: typeof source;
readonly instanceId: number;
readonly action: string;
readonly maxAge: number;
readonly nextActionId?: number;
}
interface SerializedStateMessage<S, A extends Action<string>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
'actionsById' | 'computedStates' | 'committedState'
>;
readonly source: typeof source;
readonly instanceId: number;
readonly libConfig?: LibConfig;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
interface OpenMessage {
readonly source: typeof source;
readonly type: 'OPEN';
readonly position: Position;
}
export type PageScriptToContentScriptMessageForwardedToMonitors<
S,
A extends Action<string>,
> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
| SerializedExportMessage
| SerializedActionMessage
| SerializedStateMessage<S, A>;
export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<
S,
A extends Action<string>,
> =
| PageScriptToContentScriptMessageForwardedToMonitors<S, A>
| ErrorMessage
| GetReportMessage
| StopMessage
| OpenMessage;
export type PageScriptToContentScriptMessageWithoutDisconnect<
S,
A extends Action<string>,
> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| InitInstancePageScriptToContentScriptMessage
| InitInstanceMessage;
export type PageScriptToContentScriptMessage<S, A extends Action<string>> =
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| DisconnectMessage;
function post<S, A extends Action<string>>(
message: PageScriptToContentScriptMessage<S, A>,
) {
window.postMessage(message, '*');
}
function getStackTrace(
config: Config,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
toExcludeFromTrace: Function | undefined,
) {
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!
) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
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<A extends Action<string>>(
action:
| A
| StructuralPerformAction<A>
| StructuralPerformAction<A>[]
| string,
config: Config,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
toExcludeFromTrace: Function | undefined,
): StructuralPerformAction<A> {
const timestamp = Date.now();
const stack = getStackTrace(config, toExcludeFromTrace);
if (typeof action === 'string') {
return { action: { type: action } as A, timestamp, stack };
}
if (!(action as A).type)
return { action: { type: 'update' } as A, timestamp, stack };
if ((action as StructuralPerformAction<A>).action)
return (
stack ? { stack, ...action } : action
) as StructuralPerformAction<A>;
return { action, timestamp, stack } as StructuralPerformAction<A>;
}
interface LiftedMessage {
readonly type: 'LIFTED';
readonly liftedState: { readonly isPaused: boolean | undefined };
readonly instanceId: number;
readonly source: typeof source;
}
interface PartialStateMessage<S, A extends Action<string>> {
readonly type: 'PARTIAL_STATE';
readonly payload: PartialLiftedState<S, A>;
readonly source: typeof source;
readonly instanceId: number;
readonly maxAge: number;
}
interface ExportMessage<S, A extends Action<string>> {
readonly type: 'EXPORT';
readonly payload: readonly A[];
readonly committedState: S;
readonly source: typeof source;
readonly instanceId: number;
}
export interface StructuralPerformAction<A extends Action<string>> {
readonly action: A;
readonly timestamp?: number;
readonly stack?: string;
}
type SingleUserAction<A extends Action<string>> =
| PerformAction<A>
| StructuralPerformAction<A>
| A;
type UserAction<A extends Action<string>> =
| SingleUserAction<A>
| readonly SingleUserAction<A>[];
interface ActionMessage<S, A extends Action<string>> {
readonly type: 'ACTION';
readonly payload: S;
readonly source: typeof source;
readonly instanceId: number;
readonly action: UserAction<A>;
readonly maxAge: number;
readonly nextActionId?: number;
readonly name?: string;
}
interface StateMessage<S, A extends Action<string>> {
readonly type: 'STATE';
readonly payload: LiftedState<S, A, unknown>;
readonly source: typeof source;
readonly instanceId: number;
readonly libConfig?: LibConfig;
readonly action?: UserAction<A>;
readonly maxAge?: number;
readonly name?: string;
}
export interface ErrorMessage {
readonly type: 'ERROR';
readonly payload: string;
readonly source: typeof source;
readonly instanceId: number;
readonly message?: string | undefined;
}
interface InitInstanceMessage {
readonly type: 'INIT_INSTANCE';
readonly payload: undefined;
readonly source: typeof source;
readonly instanceId: number;
}
interface GetReportMessage {
readonly type: 'GET_REPORT';
readonly payload: string;
readonly source: typeof source;
readonly instanceId: number;
}
interface StopMessage {
readonly type: 'STOP';
readonly payload: undefined;
readonly source: typeof source;
readonly instanceId: number;
}
type ToContentScriptMessage<S, A extends Action<string>> =
| LiftedMessage
| PartialStateMessage<S, A>
| ExportMessage<S, A>
| ActionMessage<S, A>
| StateMessage<S, A>
| ErrorMessage
| InitInstanceMessage
| GetReportMessage
| StopMessage;
export function toContentScript<S, A extends Action<string>>(
message: ToContentScriptMessage<S, A>,
serializeState?: Serialize | undefined,
serializeAction?: Serialize | undefined,
) {
if (message.type === 'ACTION') {
post({
...message,
action: stringify(message.action, serializeAction),
payload: stringify(message.payload, serializeState),
});
} else if (message.type === 'STATE') {
const { actionsById, computedStates, committedState, ...rest } =
message.payload;
post({
...message,
payload: rest,
actionsById: stringify(actionsById, serializeAction),
computedStates: stringify(computedStates, serializeState),
committedState: typeof committedState !== 'undefined',
});
} else if (message.type === 'PARTIAL_STATE') {
const { actionsById, computedStates, committedState, ...rest } =
message.payload;
post({
...message,
payload: rest,
actionsById: stringify(actionsById, serializeAction),
computedStates: stringify(computedStates, serializeState),
committedState: typeof committedState !== 'undefined',
});
} else if (message.type === 'EXPORT') {
post({
...message,
payload: stringify(message.payload, serializeAction),
committedState:
typeof message.committedState !== 'undefined'
? stringify(message.committedState, serializeState)
: (message.committedState as undefined),
});
} else {
post(message);
}
}
export function sendMessage<S, A extends Action<string>>(
action: StructuralPerformAction<A> | StructuralPerformAction<A>[],
state: LiftedState<S, A, unknown>,
config: Config,
instanceId?: number,
name?: string,
) {
let amendedAction = action;
if (typeof config !== 'object') {
// Legacy: sending actions not from connected part
config = {}; // eslint-disable-line no-param-reassign
if (action) amendedAction = amendActionType(action, config, sendMessage);
}
if (action) {
toContentScript(
{
type: 'ACTION',
action: amendedAction,
payload: state,
maxAge: config.maxAge!,
source,
name: config.name || name,
instanceId: config.instanceId || instanceId || 1,
},
config.serialize as Serialize | undefined,
config.serialize as Serialize | undefined,
);
} else {
toContentScript<S, A>(
{
type: 'STATE',
action: amendedAction,
payload: state,
maxAge: config.maxAge,
source,
name: config.name || name,
instanceId: config.instanceId || instanceId || 1,
},
config.serialize as Serialize | undefined,
config.serialize as Serialize | undefined,
);
}
}
function handleMessages(event: MessageEvent<ContentScriptToPageScriptMessage>) {
if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) {
return;
}
const message = event.data;
if (!message || message.source !== '@devtools-extension') return;
Object.keys(listeners).forEach((id) => {
if (message.id && id !== message.id) return;
const listenersForId = listeners[id];
if (typeof listenersForId === 'function') listenersForId(message);
else {
listenersForId.forEach((fn) => {
fn(message);
});
}
});
}
export function setListener(
onMessage: (message: ContentScriptToPageScriptMessage) => void,
instanceId: number,
) {
listeners[instanceId] = onMessage;
window.addEventListener('message', handleMessages, false);
}
const liftListener =
<S, A extends Action<string>>(
listener: (message: ListenerMessage<S, A>) => void,
config: Config,
) =>
(message: ContentScriptToPageScriptMessage) => {
if (message.type === 'IMPORT') {
listener({
type: 'DISPATCH',
payload: {
type: 'IMPORT_STATE',
...importState<S, A>(message.state, config)!,
},
});
} else {
listener(message);
}
};
export function disconnect() {
window.removeEventListener('message', handleMessages);
post({ type: 'DISCONNECT', source });
}
export interface ConnectResponse {
init: <S, A extends Action<string>>(
state: S,
liftedData?: LiftedState<S, A, unknown>,
) => void;
subscribe: <S, A extends Action<string>>(
listener: (message: ListenerMessage<S, A>) => void,
) => (() => void) | undefined;
unsubscribe: () => void;
send: <S, A extends Action<string>>(
action: A,
state: LiftedState<S, A, unknown>,
) => void;
error: (payload: string) => void;
}
export function connect(preConfig: Config): ConnectResponse {
const config = preConfig || {};
const id = generateId(config.instanceId);
if (!config.instanceId) config.instanceId = id;
if (!config.name) {
config.name =
document.title && id === 1 ? document.title : `Instance ${id}`;
}
if (config.serialize) config.serialize = getSerializeParameter(config);
const actionCreators = config.actionCreators || {};
const latency = config.latency;
const predicate = config.predicate;
const localFilter = getLocalFilter(config);
const autoPause = config.autoPause;
let isPaused = autoPause;
let delayedActions: StructuralPerformAction<Action<string>>[] = [];
let delayedStates: LiftedState<unknown, Action<string>, unknown>[] = [];
const rootListener = (action: ContentScriptToPageScriptMessage) => {
if (autoPause) {
if (action.type === 'START') isPaused = false;
else if (action.type === 'STOP') isPaused = true;
}
if (action.type === 'DISPATCH') {
const payload = action.payload;
if (payload.type === 'PAUSE_RECORDING') {
isPaused = payload.status;
toContentScript({
type: 'LIFTED',
liftedState: { isPaused },
instanceId: id,
source,
});
}
}
};
listeners[id] = [rootListener];
const subscribe = <S, A extends Action<string>>(
listener: (message: ListenerMessage<S, A>) => void,
) => {
if (!listener) return undefined;
const liftedListener = liftListener(listener, config);
const listenersForId = listeners[id] as ((
message: ContentScriptToPageScriptMessage,
) => void)[];
listenersForId.push(liftedListener);
return function unsubscribe() {
const index = listenersForId.indexOf(liftedListener);
listenersForId.splice(index, 1);
};
};
const unsubscribe = () => {
delete listeners[id];
};
const sendDelayed = throttle(() => {
sendMessage(
delayedActions,
delayedStates as unknown as LiftedState<unknown, Action<string>, unknown>,
config,
);
delayedActions = [];
delayedStates = [];
}, latency);
const send = <S, A extends Action<string>>(
action: A,
state: LiftedState<S, A, unknown>,
) => {
if (
isPaused ||
isFiltered(action, localFilter) ||
(predicate && !predicate(state, action))
) {
return;
}
let amendedAction: A | StructuralPerformAction<A> = action;
const amendedState = config.stateSanitizer
? config.stateSanitizer(state)
: state;
if (action) {
if (config.getActionType) {
amendedAction = config.getActionType(action);
if (typeof amendedAction !== 'object') {
amendedAction = {
action: { type: amendedAction },
timestamp: Date.now(),
} as unknown as A;
}
} else if (config.actionSanitizer) {
amendedAction = config.actionSanitizer(action);
}
amendedAction = amendActionType(amendedAction, config, send);
if (latency) {
delayedActions.push(amendedAction);
delayedStates.push(amendedState);
sendDelayed();
return;
}
}
sendMessage(
amendedAction as StructuralPerformAction<A>,
amendedState,
config,
);
};
const init = <S, A extends Action<string>>(
state: S,
liftedData?: LiftedState<S, A, unknown>,
) => {
const message: InitMessage<S, A> = {
type: 'INIT',
payload: stringify(state, config.serialize as Serialize | undefined),
instanceId: id,
source,
};
if (liftedData && Array.isArray(liftedData)) {
// Legacy
message.action = stringify(liftedData);
message.name = config.name;
} else {
if (liftedData) {
message.liftedState = liftedData;
if (liftedData.isPaused) isPaused = true;
}
message.libConfig = {
actionCreators: JSON.stringify(getActionsArray(actionCreators)),
name: config.name || document.title,
features: config.features,
serialize: !!config.serialize,
type: config.type,
};
}
post(message);
};
const error = (payload: string) => {
post({ type: 'ERROR', payload, instanceId: id, source });
};
window.addEventListener('message', handleMessages, false);
post({ type: 'INIT_INSTANCE', instanceId: id, source });
return {
init,
subscribe,
unsubscribe,
send,
error,
};
}
export function isInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}

View File

@ -1,48 +0,0 @@
let handleError: (() => boolean) | undefined;
let lastTime = 0;
function createExpBackoffTimer(step: number) {
let count = 1;
return function (reset?: boolean) {
// Reset call
if (reset) {
count = 1;
return 0;
}
// Calculate next timeout
const timeout = Math.pow(2, count - 1);
if (count < 5) count += 1;
return timeout * step;
};
}
const nextErrorTimeout = createExpBackoffTimer(5000);
function postError(message: string) {
if (handleError && !handleError()) return;
window.postMessage(
{
source: '@devtools-page',
type: 'ERROR',
message: message,
},
'*',
);
}
function catchErrors(e: ErrorEvent) {
if (
(window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors) ||
e.timeStamp - lastTime < nextErrorTimeout()
) {
return;
}
lastTime = e.timeStamp;
nextErrorTimeout(true);
postError(e.message);
}
export default function notifyErrors(onError?: () => boolean) {
handleError = onError;
window.addEventListener('error', catchErrors, false);
}

View File

@ -1,18 +0,0 @@
import { Action } from 'redux';
import type { PageScriptToContentScriptMessage } from './index.js';
export type Position = 'window' | 'remote';
function post<S, A extends Action<string>>(
message: PageScriptToContentScriptMessage<S, A>,
) {
window.postMessage(message, '*');
}
export default function openWindow(position?: Position) {
post({
source: '@devtools-page',
type: 'OPEN',
position: position ?? 'window',
});
}

View File

@ -1,42 +0,0 @@
import { Action, compose, Reducer, StoreEnhancerStoreCreator } from 'redux';
import { instrument } from '@redux-devtools/instrument';
import { persistState } from '@redux-devtools/core';
import type { ConfigWithExpandedMaxAge } from './index.js';
export function getUrlParam(key: string) {
const matches = new RegExp(`[?&]${key}=([^&#]+)\\b`).exec(
window.location.href,
);
return matches && matches.length > 0 ? matches[1] : null;
}
declare global {
interface Window {
shouldCatchErrors?: boolean;
}
}
export default function configureStore<
S,
A extends Action<string>,
MonitorState,
MonitorAction extends Action<string>,
>(
next: StoreEnhancerStoreCreator,
monitorReducer: Reducer<MonitorState, MonitorAction>,
config: ConfigWithExpandedMaxAge,
) {
return compose(
instrument(monitorReducer, {
maxAge: config.maxAge,
trace: config.trace,
traceLimit: config.traceLimit,
shouldCatchErrors: config.shouldCatchErrors || window.shouldCatchErrors,
shouldHotReload: config.shouldHotReload,
shouldRecordChanges: config.shouldRecordChanges,
shouldStartLocked: config.shouldStartLocked,
pauseActionType: config.pauseActionType || '@@PAUSED',
}),
persistState(getUrlParam('debug_session')),
)(next);
}

View File

@ -1,646 +0,0 @@
import {
ActionCreatorObject,
evalAction,
getActionsArray,
getLocalFilter,
} from '@redux-devtools/utils';
import { throttle } from 'lodash-es';
import { Action, ActionCreator, Dispatch, Reducer, StoreEnhancer } from 'redux';
import Immutable from 'immutable';
import {
EnhancedStore,
LiftedAction,
LiftedState,
PerformAction,
} from '@redux-devtools/instrument';
import {
CustomAction,
DispatchAction,
LibConfig,
Features,
} from '@redux-devtools/app';
import configureStore, { getUrlParam } from './enhancerStore.js';
import { isAllowed, Options } from '../options/syncOptions.js';
import Monitor from './Monitor.js';
import {
noFiltersApplied,
isFiltered,
filterState,
startingFrom,
} from './api/filters.js';
import notifyErrors from './api/notifyErrors.js';
import importState from './api/importState.js';
import openWindow, { Position } from './api/openWindow.js';
import generateId from './api/generateInstanceId.js';
import {
toContentScript,
sendMessage,
setListener,
connect,
disconnect,
isInIframe,
getSerializeParameter,
Serialize,
StructuralPerformAction,
ConnectResponse,
} from './api/index.js';
import type { ContentScriptToPageScriptMessage } from '../contentScript/index.js';
type EnhancedStoreWithInitialDispatch<
S,
A extends Action<string>,
MonitorState,
> = EnhancedStore<S, A, MonitorState> & { initialDispatch: Dispatch<A> };
const source = '@devtools-page';
const stores: {
[K in string | number]: EnhancedStoreWithInitialDispatch<
unknown,
Action<string>,
unknown
>;
} = {};
let reportId: string | null | undefined;
function deprecateParam(oldParam: string, newParam: string) {
/* eslint-disable no-console */
console.warn(
`${oldParam} parameter is deprecated, use ${newParam} instead: https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md`,
);
/* eslint-enable no-console */
}
export interface SerializeWithImmutable extends Serialize {
readonly immutable?: typeof Immutable;
readonly refs?: (new (data: any) => unknown)[] | null;
}
export interface ConfigWithExpandedMaxAge {
instanceId?: number;
/**
* @deprecated Use actionsDenylist instead.
*/
readonly actionsBlacklist?: string | readonly string[];
/**
* @deprecated Use actionsAllowlist instead.
*/
readonly actionsWhitelist?: string | readonly string[];
readonly actionsDenylist?: string | readonly string[];
readonly actionsAllowlist?: string | readonly string[];
serialize?: boolean | SerializeWithImmutable;
readonly stateSanitizer?: <S>(state: S, index?: number) => S;
readonly actionSanitizer?: <A extends Action<string>>(
action: A,
id?: number,
) => A;
readonly predicate?: <S, A extends Action<string>>(
state: S,
action: A,
) => boolean;
readonly latency?: number;
readonly maxAge?:
| number
| (<S, A extends Action<string>>(
currentLiftedAction: LiftedAction<S, A, unknown>,
previousLiftedState: LiftedState<S, A, unknown> | undefined,
) => number);
readonly trace?: boolean | (() => string | undefined);
readonly traceLimit?: number;
readonly shouldCatchErrors?: boolean;
readonly shouldHotReload?: boolean;
readonly shouldRecordChanges?: boolean;
readonly shouldStartLocked?: boolean;
readonly pauseActionType?: unknown;
name?: string;
readonly autoPause?: boolean;
readonly features?: Features;
readonly type?: string;
readonly getActionType?: <A extends Action<string>>(action: A) => A;
readonly actionCreators?: {
readonly [key: string]: ActionCreator<Action<string>>;
};
}
export interface Config extends ConfigWithExpandedMaxAge {
readonly maxAge?: number;
}
interface ReduxDevtoolsExtension {
(config?: Config): StoreEnhancer;
open: (position?: Position) => void;
notifyErrors: (onError?: () => boolean) => void;
send: <S, A extends Action<string>>(
action: StructuralPerformAction<A> | StructuralPerformAction<A>[],
state: LiftedState<S, A, unknown>,
config: Config,
instanceId?: number,
name?: string,
) => void;
listen: (
onMessage: (message: ContentScriptToPageScriptMessage) => void,
instanceId: number,
) => void;
connect: (preConfig: Config) => ConnectResponse;
disconnect: () => void;
}
declare global {
interface Window {
devToolsOptions: Options;
}
}
function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
config?: Config,
): StoreEnhancer {
/* eslint-disable no-param-reassign */
if (typeof config !== 'object') config = {};
/* eslint-enable no-param-reassign */
if (!window.devToolsOptions) window.devToolsOptions = {} as any;
let store: EnhancedStoreWithInitialDispatch<S, A, unknown>;
let errorOccurred = false;
let maxAge: number | undefined;
let actionCreators: readonly ActionCreatorObject[];
let sendingActionId = 1;
const instanceId = generateId(config.instanceId);
const localFilter = getLocalFilter(config);
const serializeState = getSerializeParameter(config);
const serializeAction = getSerializeParameter(config);
const { stateSanitizer, actionSanitizer, predicate, latency = 500 } = config;
// Deprecate actionsWhitelist and actionsBlacklist
if (config.actionsWhitelist) {
deprecateParam('actionsWhiteList', 'actionsAllowlist');
}
if (config.actionsBlacklist) {
deprecateParam('actionsBlacklist', 'actionsDenylist');
}
const relayState = throttle(
(
liftedState?: LiftedState<S, A, unknown> | undefined,
libConfig?: LibConfig,
) => {
relayAction.cancel();
const state = liftedState || store.liftedStore.getState();
sendingActionId = state.nextActionId;
toContentScript(
{
type: 'STATE',
payload: filterState(
state,
localFilter,
stateSanitizer,
actionSanitizer,
predicate,
),
source,
instanceId,
libConfig,
},
serializeState,
serializeAction,
);
},
latency,
);
const monitor = new Monitor(relayState);
function exportState() {
const liftedState = store.liftedStore.getState();
const actionsById = liftedState.actionsById;
const payload: A[] = [];
liftedState.stagedActionIds.slice(1).forEach((id) => {
// if (isFiltered(actionsById[id].action, localFilter)) return;
payload.push(actionsById[id].action);
});
toContentScript(
{
type: 'EXPORT',
payload,
committedState: liftedState.committedState,
source,
instanceId,
},
serializeState,
serializeAction,
);
}
const relayAction = throttle(() => {
const liftedState = store.liftedStore.getState();
const nextActionId = liftedState.nextActionId;
const currentActionId = nextActionId - 1;
const liftedAction = liftedState.actionsById[currentActionId];
// Send a single action
if (sendingActionId === currentActionId) {
sendingActionId = nextActionId;
const action = liftedAction.action;
const computedStates = liftedState.computedStates;
if (
isFiltered(action, localFilter) ||
(predicate &&
!predicate(computedStates[computedStates.length - 1].state, action))
) {
return;
}
const state =
liftedState.computedStates[liftedState.computedStates.length - 1].state;
toContentScript(
{
type: 'ACTION',
payload: !stateSanitizer
? state
: stateSanitizer(state, nextActionId - 1),
source,
instanceId,
action: !actionSanitizer
? liftedState.actionsById[nextActionId - 1]
: actionSanitizer(
liftedState.actionsById[nextActionId - 1].action,
nextActionId - 1,
),
maxAge: getMaxAge(),
nextActionId,
},
serializeState,
serializeAction,
);
return;
}
// Send multiple actions
const payload = startingFrom(
sendingActionId,
liftedState,
localFilter,
stateSanitizer,
actionSanitizer,
predicate,
);
sendingActionId = nextActionId;
if (typeof payload === 'undefined') return;
if ('skippedActionIds' in payload) {
toContentScript(
{
type: 'STATE',
payload: filterState(
payload,
localFilter,
stateSanitizer,
actionSanitizer,
predicate,
),
source,
instanceId,
},
serializeState,
serializeAction,
);
return;
}
toContentScript(
{
type: 'PARTIAL_STATE',
payload,
source,
instanceId,
maxAge: getMaxAge(),
},
serializeState,
serializeAction,
);
}, latency);
function dispatchRemotely(action: string | CustomAction) {
if (config!.features && !config!.features.dispatch) return;
try {
const result = evalAction(action, actionCreators);
(store.initialDispatch || store.dispatch)(result);
} catch (e) {
toContentScript(
{
type: 'ERROR',
payload: (e as Error).message,
source,
instanceId,
},
serializeState,
serializeAction,
);
}
}
function importPayloadFrom(state: string | undefined) {
if (config!.features && !config!.features.import) return;
try {
const nextLiftedState = importState<S, A>(state, config!);
if (!nextLiftedState) return;
store.liftedStore.dispatch({ type: 'IMPORT_STATE', ...nextLiftedState });
} catch (e) {
toContentScript(
{
type: 'ERROR',
payload: (e as Error).message,
source,
instanceId,
},
serializeState,
serializeAction,
);
}
}
function dispatchMonitorAction(action: DispatchAction) {
const features = config!.features;
if (features) {
if (
!features.jump &&
(action.type === 'JUMP_TO_STATE' || action.type === 'JUMP_TO_ACTION')
) {
return;
}
if (!features.skip && action.type === 'TOGGLE_ACTION') return;
if (!features.reorder && action.type === 'REORDER_ACTION') return;
if (!features.import && action.type === 'IMPORT_STATE') return;
if (!features.lock && action.type === 'LOCK_CHANGES') return;
if (!features.pause && action.type === 'PAUSE_RECORDING') return;
}
store.liftedStore.dispatch(action as any);
}
function onMessage(message: ContentScriptToPageScriptMessage) {
switch (message.type) {
case 'DISPATCH':
dispatchMonitorAction(message.payload);
return;
case 'ACTION':
dispatchRemotely(message.payload);
return;
case 'IMPORT':
importPayloadFrom(message.state);
return;
case 'EXPORT':
exportState();
return;
case 'UPDATE':
relayState();
return;
case 'START':
monitor.start(true);
if (!actionCreators && config!.actionCreators) {
actionCreators = getActionsArray(config!.actionCreators);
}
relayState(undefined, {
name: config!.name || document.title,
actionCreators: JSON.stringify(actionCreators),
features: config!.features,
serialize: !!config!.serialize,
type: 'redux',
});
if (reportId) {
toContentScript(
{
type: 'GET_REPORT',
payload: reportId,
source,
instanceId,
},
serializeState,
serializeAction,
);
reportId = null;
}
return;
case 'STOP':
monitor.stop();
relayAction.cancel();
relayState.cancel();
if (!message.failed) {
toContentScript(
{
type: 'STOP',
payload: undefined,
source,
instanceId,
},
serializeState,
serializeAction,
);
}
return;
case 'OPTIONS':
window.devToolsOptions = Object.assign(
window.devToolsOptions || {},
message.options,
);
return;
}
}
const filteredActionIds: number[] = []; // simple circular buffer of non-excluded actions with fixed maxAge-1 length
const getMaxAge = (
liftedAction?: LiftedAction<S, A, unknown>,
liftedState?: LiftedState<S, A, unknown> | undefined,
) => {
const m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50;
if (
!liftedAction ||
noFiltersApplied(localFilter) ||
!(liftedAction as PerformAction<A>).action
) {
return m;
}
if (!maxAge || maxAge < m) maxAge = m; // it can be modified in process on options page
if (isFiltered((liftedAction as PerformAction<A>).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.includes(stagedActionIds[i])) {
maxAge--;
i++;
}
filteredActionIds.shift();
}
}
return maxAge;
};
function init() {
setListener(onMessage, instanceId);
notifyErrors(() => {
errorOccurred = true;
const state = store.liftedStore.getState();
if (state.computedStates[state.currentStateIndex].error) {
relayState(state);
}
return true;
});
toContentScript(
{
type: 'INIT_INSTANCE',
payload: undefined,
source,
instanceId,
},
serializeState,
serializeAction,
);
store.subscribe(handleChange);
if (typeof reportId === 'undefined') {
reportId = getUrlParam('remotedev_report');
if (reportId) openWindow();
}
}
function handleChange() {
if (!monitor.active) return;
if (!errorOccurred && !monitor.isMonitorAction()) {
relayAction();
return;
}
if (monitor.isPaused() || monitor.isLocked() || monitor.isTimeTraveling()) {
return;
}
const liftedState = store.liftedStore.getState();
if (
errorOccurred &&
!liftedState.computedStates[liftedState.currentStateIndex].error
) {
errorOccurred = false;
}
relayState(liftedState);
}
const enhance = (): StoreEnhancer => (next) => {
return <S2, A2 extends Action<string>, PreloadedState>(
reducer_: Reducer<S2, A2, PreloadedState>,
initialState_?: PreloadedState | undefined,
) => {
if (!isAllowed(window.devToolsOptions)) {
return next(reducer_, initialState_);
}
store = stores[instanceId] = (
configureStore(next, monitor.reducer, {
...config,
maxAge: getMaxAge as any,
}) as any
)(reducer_, initialState_);
if (isInIframe()) setTimeout(init, 3000);
else init();
return store as any;
};
};
return enhance();
}
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION__: ReduxDevtoolsExtension;
}
}
// noinspection JSAnnotator
window.__REDUX_DEVTOOLS_EXTENSION__ = __REDUX_DEVTOOLS_EXTENSION__ as any;
window.__REDUX_DEVTOOLS_EXTENSION__.open = openWindow;
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;
const preEnhancer =
(instanceId: number): StoreEnhancer =>
(next) =>
(reducer, preloadedState) => {
const store = next(reducer, preloadedState);
if (stores[instanceId]) {
(stores[instanceId].initialDispatch as any) = store.dispatch;
}
return {
...store,
dispatch: (...args: any[]) =>
!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__ &&
(store.dispatch as any)(...args),
} as any;
};
export type InferComposedStoreExt<StoreEnhancers> = StoreEnhancers extends [
infer HeadStoreEnhancer,
...infer RestStoreEnhancers,
]
? HeadStoreEnhancer extends StoreEnhancer<infer StoreExt>
? StoreExt & InferComposedStoreExt<RestStoreEnhancers>
: never
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
{};
const extensionCompose =
(config: Config) =>
<StoreEnhancers extends readonly StoreEnhancer[]>(
...funcs: StoreEnhancers
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>> => {
// @ts-expect-error FIXME
return (...args) => {
const instanceId = generateId(config.instanceId);
return [preEnhancer(instanceId), ...funcs].reduceRight(
(composed, f) => f(composed),
__REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId })(...args),
);
};
};
interface ReduxDevtoolsExtensionCompose {
(
config: Config,
): <StoreEnhancers extends readonly StoreEnhancer[]>(
...funcs: StoreEnhancers
) => StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
<StoreEnhancers extends readonly StoreEnhancer[]>(
...funcs: StoreEnhancers
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
}
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ReduxDevtoolsExtensionCompose;
}
}
function reduxDevtoolsExtensionCompose(
config: Config,
): <StoreEnhancers extends readonly StoreEnhancer[]>(
...funcs: StoreEnhancers
) => StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
function reduxDevtoolsExtensionCompose<
StoreEnhancers extends readonly StoreEnhancer[],
>(
...funcs: StoreEnhancers
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
function reduxDevtoolsExtensionCompose(...funcs: [Config] | StoreEnhancer[]) {
if (funcs.length === 0) {
return __REDUX_DEVTOOLS_EXTENSION__();
}
if (funcs.length === 1 && typeof funcs[0] === 'object') {
return extensionCompose(funcs[0]);
}
return extensionCompose({})(...(funcs as StoreEnhancer[]));
}
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = reduxDevtoolsExtensionCompose;

View File

@ -1,35 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Root } from '@redux-devtools/app';
chrome.storage.local.get(
{
'select-monitor': 'InspectorMonitor',
'test-templates': null,
'test-templates-sel': null,
's:hostname': null,
's:port': null,
's:secure': null,
},
(options) => {
const AppAsAny = Root as any;
const root = createRoot(document.getElementById('root')!);
root.render(
<AppAsAny
selectMonitor={options['select-monitor']}
testTemplates={options['test-templates']}
selectedTemplate={options['test-templates-sel']}
useCodemirror
socketOptions={
options['s:hostname'] && options['s:port']
? {
hostname: options['s:hostname'],
port: options['s:port'],
secure: options['s:secure'],
}
: undefined
}
/>,
);
},
);

View File

@ -1,12 +0,0 @@
doctype html
html
head
meta(charset='UTF-8')
title RemoteDev
include ../style.pug
body
#root
link(href='/remote.bundle.css', rel='stylesheet')
script(src='/remote.bundle.js')

View File

@ -1,61 +0,0 @@
style.
html {
height: 100%;
width: 100%;
}
body {
overflow: hidden;
height: 100%;
width: 100%;
min-width: 350px;
min-height: 400px;
margin: 0;
padding: 0;
font-family: "Helvetica Neue", "Lucida Grande", sans-serif;
font-size: 11px;
background-color: rgb(53, 59, 70);
color: #fff;
}
#root {
height: 100%;
}
#root > div {
height: 100%;
}
#root > div > div:nth-child(2) {
min-height: 0;
}
.ReactCodeMirror {
overflow: auto;
height: 100%;
}
button:disabled {
opacity: 0.5;
cursor: initial !important;
}
@media print {
@page {
size: auto;
margin: 0;
}
body {
position: static;
}
#root > div > div:not(:nth-child(2)) {
display: none !important;
}
#root > div > div:nth-child(2) {
overflow: visible !important;
position: absolute !important;
z-index: 2147483647;
page-break-after: avoid;
}
#root > div > div:nth-child(2) * {
overflow: visible !important;
}
}

Some files were not shown because too many files have changed in this diff Show More