Compare commits

..

No commits in common. "main" and "remotedev-redux-devtools-extensions@3.1.3" have entirely different histories.

600 changed files with 25085 additions and 23583 deletions

View File

@ -6,8 +6,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
"ignore": []
}

15
.gitattributes vendored
View File

@ -1 +1,14 @@
* 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
*.yml text eol=lf
*.graphql text eol=lf
.eslintrc text eol=lf
.prettierrc text eol=lf
.babelrc text eol=lf
.stylelintrc text eol=lf

View File

@ -8,12 +8,15 @@ on:
jobs:
build:
runs-on: 'ubuntu-22.04'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'pnpm'
@ -26,6 +29,6 @@ jobs:
- name: Lint
run: pnpm run lint:all
- name: Test
uses: coactions/setup-xvfb@v1
uses: GabrielBB/xvfb-action@v1
with:
run: pnpm run test:all

View File

@ -10,7 +10,7 @@ permissions: write-all
jobs:
release:
name: Release
runs-on: 'ubuntu-22.04'
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
@ -18,10 +18,10 @@ jobs:
# 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
- uses: pnpm/action-setup@v2
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'pnpm'
@ -40,19 +40,19 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Archive Chrome Extension
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: chrome
path: extension/chrome/dist
- name: Archive Edge Extension
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: edge
path: extension/edge/dist
- name: Archive Firefox Extension
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: firefox
path: extension/firefox/dist

1
.gitignore vendored
View File

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

View File

@ -24,7 +24,7 @@ It can be used as a browser extension (for [Chrome](https://chrome.google.com/we
## 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`).
This is a monorepo powered by [pnpm](https://pnpm.io/) and [Nx](https://nx.dev/). [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 Nx commands to build only the packages necessary for the packages you're working on (i.e., `pnpm nx build remotedev-redux-devtools-extension`).
## Backers

View File

@ -1,4 +0,0 @@
import eslint from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
export default [eslint.configs.recommended, eslintConfigPrettier];

View File

@ -1,43 +0,0 @@
import eslint from '@eslint/js';
import react from 'eslint-plugin-react';
import { fixupPluginRules } from '@eslint/compat';
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import jest from 'eslint-plugin-jest';
import eslintConfigPrettier from 'eslint-config-prettier';
export default [
{
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'],
plugins: {
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
},
},
{
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,55 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (tsconfigRootDir, files = ['**/*.ts'], project = true) => [
{
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,64 +0,0 @@
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) => [
{
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,89 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import { fixupPluginRules } from '@eslint/compat';
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (
tsconfigRootDir,
files = ['**/*.ts', '**/*.tsx'],
project = true,
) => [
{
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,
plugins: {
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
},
},
{
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,85 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import { fixupPluginRules } from '@eslint/compat';
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import jest from 'eslint-plugin-jest';
import eslintConfigPrettier from 'eslint-config-prettier';
export default (tsconfigRootDir) => [
{
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'],
plugins: {
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
},
},
{
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',
},
},
];

3
eslintrc.js.base.json Normal file
View File

@ -0,0 +1,3 @@
{
"parser": "@babel/eslint-parser"
}

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

@ -0,0 +1,31 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"prettier"
],
"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

@ -0,0 +1,32 @@
{
"plugins": ["jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:jest/recommended",
"plugin:jest/style",
"prettier"
],
"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

@ -0,0 +1,51 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "react"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"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",
"@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"
}
}

View File

@ -0,0 +1,34 @@
{
"plugins": ["jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jest/recommended",
"plugin:jest/style",
"prettier"
],
"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"
}
}

3
extension/.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
examples

31
extension/.eslintrc Normal file
View File

@ -0,0 +1,31 @@
{
"root": true,
"extends": "eslint-config-airbnb",
"globals": {
"chrome": true,
"__DEVELOPMENT__": true
},
"env": {
"browser": true,
"node": true
},
"rules": {
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"react/jsx-quotes": 0,
"block-scoped-var": 0,
"padded-blocks": 0,
"quotes": [1, "single"],
"comma-style": [2, "last"],
"no-use-before-define": [0, "nofunc"],
"func-names": 0,
"prefer-const": 0,
"comma-dangle": 0,
"id-length": 0,
"indent": [2, 2, { "SwitchCase": 1 }],
"new-cap": [2, { "capIsNewExceptions": ["Test"] }],
"default-case": 0
},
"plugins": ["react"]
}

View File

@ -1,139 +1,5 @@
# remotedev-redux-devtools-extension
## 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

View File

@ -13,8 +13,8 @@
- 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`.
- or build it with `npm i && npm run build:extension` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./build/extension`;
- or run it in dev mode with `npm i && npm start` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./dev`.
### 2. For Firefox

View File

@ -5,7 +5,7 @@ import pug from 'pug';
const args = process.argv.slice(2);
const prod = !args.includes('--dev');
await esbuild.build({
const commonEsbuildOptions = {
bundle: true,
logLevel: 'info',
outdir: 'dist',
@ -15,24 +15,40 @@ await esbuild.build({
'process.env.NODE_ENV': prod ? '"production"' : '"development"',
'process.env.BABEL_ENV': prod ? '"production"' : '"development"',
},
};
await esbuild.build({
...commonEsbuildOptions,
entryPoints: [
{ out: 'background.bundle', in: 'src/background/index.ts' },
{ out: 'options.bundle', in: 'src/options/index.tsx' },
{ out: 'window.bundle', in: 'src/window/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' },
...(prod ? [] : [{ out: 'pagewrap.bundle', in: 'src/pageScriptWrap.ts' }]),
],
loader: {
'.woff2': 'file',
},
});
if (prod) {
await esbuild.build({
...commonEsbuildOptions,
entryPoints: [{ out: 'pagewrap.bundle', in: 'src/pageScriptWrap.ts' }],
loader: {
'.js': 'text',
},
});
}
console.log();
console.log('Creating HTML files...');
const htmlFiles = ['devpanel', 'devtools', 'options', 'remote'];
const htmlFiles = ['devpanel', 'devtools', 'options', 'remote', 'window'];
for (const htmlFile of htmlFiles) {
fs.writeFileSync(
`dist/${htmlFile}.html`,

View File

@ -1,22 +1,28 @@
{
"version": "3.2.10",
"version": "3.1.3",
"name": "Redux DevTools",
"description": "Redux DevTools for debugging application's state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"manifest_version": 3,
"action": {
"manifest_version": 2,
"page_action": {
"default_icon": "img/logo/gray.png",
"default_title": "Redux DevTools",
"default_popup": "devpanel.html#popup"
"default_popup": "window.html#popup"
},
"commands": {
"devtools-window": {
"description": "DevTools window"
"devtools-left": {
"description": "DevTools window to left"
},
"devtools-right": {
"description": "DevTools window to right"
},
"devtools-bottom": {
"description": "DevTools window to bottom"
},
"devtools-remote": {
"description": "Remote DevTools"
},
"_execute_action": {
"_execute_page_action": {
"suggested_key": {
"default": "Ctrl+Shift+E"
}
@ -28,37 +34,36 @@
"128": "img/logo/128x128.png"
},
"options_ui": {
"page": "options.html"
"page": "options.html",
"chrome_style": true
},
"background": {
"service_worker": "background.bundle.js"
"scripts": ["background.bundle.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["content.bundle.js"],
"js": ["content.bundle.js", "pagewrap.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",
"web_accessible_resources": ["page.bundle.js"],
"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:;"
},
"permissions": [
"notifications",
"contextMenus",
"storage",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; 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,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,22 +1,28 @@
{
"version": "3.2.10",
"version": "3.1.3",
"name": "Redux DevTools",
"description": "Redux DevTools for debugging application's state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"manifest_version": 3,
"action": {
"manifest_version": 2,
"page_action": {
"default_icon": "img/logo/gray.png",
"default_title": "Redux DevTools",
"default_popup": "devpanel.html#popup"
"default_popup": "window.html#popup"
},
"commands": {
"devtools-window": {
"description": "DevTools window"
"devtools-left": {
"description": "DevTools window to left"
},
"devtools-right": {
"description": "DevTools window to right"
},
"devtools-bottom": {
"description": "DevTools window to bottom"
},
"devtools-remote": {
"description": "Remote DevTools"
},
"_execute_action": {
"_execute_page_action": {
"suggested_key": {
"default": "Ctrl+Shift+E"
}
@ -28,35 +34,34 @@
"128": "img/logo/128x128.png"
},
"options_ui": {
"page": "options.html"
"page": "options.html",
"chrome_style": true
},
"background": {
"service_worker": "background.bundle.js"
"scripts": ["background.bundle.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"exclude_globs": ["https://www.google*"],
"js": ["content.bundle.js"],
"js": ["content.bundle.js", "pagewrap.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",
"web_accessible_resources": ["page.bundle.js"],
"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:;"
}
"permissions": [
"notifications",
"contextMenus",
"storage",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;"
}

View File

@ -1,38 +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',
'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,22 +1,29 @@
{
"version": "3.2.10",
"version": "3.1.3",
"name": "Redux DevTools",
"manifest_version": 3,
"manifest_version": 2,
"description": "Redux Developer Tools for debugging application state changes.",
"homepage_url": "https://github.com/reduxjs/redux-devtools",
"browser_specific_settings": {
"applications": {
"gecko": {
"id": "extension@redux.devtools"
"id": "extension@redux.devtools",
"strict_min_version": "54.0"
}
},
"action": {
"page_action": {
"default_icon": "img/logo/38x38.png",
"default_title": "Redux DevTools",
"default_popup": "devpanel.html#popup"
"default_popup": "window.html#popup"
},
"commands": {
"devtools-window": {
"description": "DevTools window"
"devtools-left": {
"description": "DevTools window to left"
},
"devtools-right": {
"description": "DevTools window to right"
},
"devtools-bottom": {
"description": "DevTools window to bottom"
},
"devtools-remote": {
"description": "Remote DevTools"
@ -36,22 +43,21 @@
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.bundle.js"],
"js": ["content.bundle.js", "pagewrap.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:;"
}
"web_accessible_resources": ["page.bundle.js"],
"permissions": [
"notifications",
"contextMenus",
"tabs",
"storage",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_security_policy": "script-src 'self'; object-src 'self'; img-src 'self' data:;"
}

View File

@ -3,9 +3,9 @@ module.exports = {
testPathIgnorePatterns: ['<rootDir>/examples'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
'\\.css$': '<rootDir>/test/__mocks__/styleMock.ts',
},
transformIgnorePatterns: [
'node_modules/(?!.pnpm|@babel/code-frame|@babel/highlight|@babel/helper-validator-identifier|chalk|d3|dateformat|delaunator|internmap|jsondiffpatch|lodash-es|nanoid|robust-predicates|uuid)',
'node_modules/(?!.pnpm|@babel/code-frame|@babel/highlight|@babel/helper-validator-identifier|d3|dateformat|delaunator|internmap|nanoid|robust-predicates|uuid)',
],
};

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "remotedev-redux-devtools-extension",
"version": "3.2.10",
"version": "3.1.3",
"description": "Redux Developer Tools for debugging application state changes.",
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/extension",
"license": "MIT",
@ -17,65 +17,67 @@
"clean": "rimraf dist && rimraf chrome/dist && rimraf edge/dist && rimraf firefox/dist",
"test:app": "cross-env BABEL_ENV=test jest test/app",
"test:chrome": "jest test/chrome",
"build:test:electron:fixture": "webpack --config test/electron/fixture/webpack.config.js",
"test:electron": "pnpm run build:test:electron:fixture && jest test/electron",
"test": "pnpm run test:app && pnpm run test:chrome && pnpm run test:electron",
"lint": "eslint .",
"build:test:electron:fixture": "webpack --config test/electron/fixture/webpack.config.js",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@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.6.0",
"@types/jsan": "^3.1.5",
"@babel/polyfill": "^7.12.1",
"@redux-devtools/app": "^3.0.0",
"@redux-devtools/core": "^3.13.0",
"@redux-devtools/instrument": "^2.1.0",
"@redux-devtools/serialize": "^0.4.1",
"@redux-devtools/slider-monitor": "^4.0.0",
"@redux-devtools/ui": "^1.3.0",
"@redux-devtools/utils": "^2.0.0",
"@types/jsan": "^3.1.2",
"jsan": "^3.1.14",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-is": "^18.3.1",
"react-json-tree": "workspace:^",
"react-redux": "^9.2.0",
"redux": "^5.0.1",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
"react-is": "^18.2.0",
"react-json-tree": "^0.18.0",
"react-redux": "^8.1.2",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"styled-components": "^5.3.11"
},
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.25.9",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/chrome": "^0.0.308",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/styled-components": "^5.1.34",
"chromedriver": "^126.0.5",
"@babel/core": "^7.22.11",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.11",
"@babel/register": "^7.22.5",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "^14.0.0",
"@types/chrome": "^0.0.244",
"@types/lodash": "^4.14.197",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/styled-components": "^5.1.26",
"chromedriver": "^116.0.0",
"cross-env": "^7.0.3",
"electron": "^31.7.7",
"esbuild": "^0.25.0",
"globals": "^15.15.0",
"immutable": "^5.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"pug": "^3.0.3",
"rimraf": "^6.0.1",
"selenium-webdriver": "^4.29.0",
"electron": "^26.1.0",
"esbuild": "^0.19.2",
"eslint": "^8.48.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"immutable": "^4.3.4",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
"pug": "^3.0.2",
"rimraf": "^5.0.1",
"selenium-webdriver": "^4.12.0",
"sinon-chrome": "^3.0.1",
"ts-jest": "^29.2.6",
"typescript": "~5.8.2",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1"
"ts-jest": "^29.1.1",
"typescript": "~5.2.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
}
}

View File

@ -18,7 +18,7 @@ import {
TopButtons,
} from '@redux-devtools/app';
import { GoBroadcast } from 'react-icons/go';
import { MdOutlineWindow } from 'react-icons/md';
import { MdBorderBottom, MdBorderLeft, MdBorderRight } from 'react-icons/md';
import type { Position } from '../pageScript/api/openWindow';
import type { SingleMessage } from '../background/store/apiMiddleware';
@ -29,21 +29,25 @@ interface OwnProps {
}
type Props = StateProps & DispatchProps & OwnProps;
const isElectron = navigator.userAgent.includes('Electron');
declare global {
interface Window {
isElectron?: boolean;
}
}
async function sendMessage(message: SingleMessage) {
await chrome.runtime.sendMessage(message);
function sendMessage(message: SingleMessage) {
chrome.runtime.sendMessage(message);
}
class Actions extends Component<Props> {
openWindow = async (position: Position) => {
await sendMessage({ type: 'OPEN', position });
openWindow = (position: Position) => {
sendMessage({ type: 'OPEN', position });
};
openOptionsPage = async () => {
if (navigator.userAgent.includes('Firefox')) {
await sendMessage({ type: 'OPEN_OPTIONS' });
openOptionsPage = () => {
if (navigator.userAgent.indexOf('Firefox') !== -1) {
sendMessage({ type: 'OPEN_OPTIONS' });
} else {
await chrome.runtime.openOptionsPage();
chrome.runtime.openOptionsPage();
}
};
@ -85,7 +89,7 @@ class Actions extends Component<Props> {
{features.import && <ImportButton />}
{position &&
(position !== '#popup' ||
navigator.userAgent.includes('Firefox')) && <PrintButton />}
navigator.userAgent.indexOf('Firefox') !== -1) && <PrintButton />}
<Divider />
<MonitorSelector />
<Divider />
@ -94,19 +98,37 @@ class Actions extends Component<Props> {
<DispatcherButton dispatcherIsOpen={this.props.dispatcherIsOpen} />
)}
<Divider />
{!isElectron && (
{!window.isElectron && position !== '#left' && (
<Button
onClick={async () => {
await this.openWindow('window');
onClick={() => {
this.openWindow('left');
}}
>
<MdOutlineWindow />
<MdBorderLeft />
</Button>
)}
{!isElectron && (
{!window.isElectron && position !== '#right' && (
<Button
onClick={async () => {
await this.openWindow('remote');
onClick={() => {
this.openWindow('right');
}}
>
<MdBorderRight />
</Button>
)}
{!window.isElectron && position !== '#bottom' && (
<Button
onClick={() => {
this.openWindow('bottom');
}}
>
<MdBorderBottom />
</Button>
)}
{!window.isElectron && (
<Button
onClick={() => {
this.openWindow('remote');
}}
>
<GoBroadcast />

View File

@ -27,7 +27,6 @@ class App extends Component<Props> {
<a
href="https://github.com/zalmoxisus/redux-devtools-extension#usage"
target="_blank"
rel="noreferrer"
>
the instructions
</a>

View File

@ -2,28 +2,34 @@ import openDevToolsWindow, { DevToolsPosition } from './openWindow';
export function createMenu() {
const menus = [
{ id: 'devtools-window', title: 'Open in a window' },
{ id: 'devtools-left', title: 'To left' },
{ id: 'devtools-right', title: 'To right' },
{ id: 'devtools-bottom', title: 'To bottom' },
{
id: 'devtools-panel',
title: 'Open in a panel (enable in browser settings)',
},
{ id: 'devtools-remote', title: 'Open Remote DevTools' },
];
const shortcuts: { [commandName: string]: string | undefined } = {};
let shortcuts: { [commandName: string]: string | undefined } = {};
chrome.commands.getAll((commands) => {
for (const { name, shortcut } of commands) {
commands.forEach(({ name, shortcut }) => {
shortcuts[name!] = shortcut;
}
});
for (const { id, title } of menus) {
menus.forEach(({ id, title }) => {
chrome.contextMenus.create({
id: id,
title: title + (shortcuts[id] ? ' (' + shortcuts[id] + ')' : ''),
contexts: ['all'],
});
}
});
});
}
export async function removeMenu() {
await chrome.contextMenus.removeAll();
export function removeMenu() {
chrome.contextMenus.removeAll();
}
chrome.contextMenus.onClicked.addListener(({ menuItemId }) => {

View File

@ -1,23 +1,29 @@
import '../chromeApiMock';
import configureStore from './store/backgroundStore';
import { Store } from 'redux';
import configureStore, { BackgroundAction } from './store/backgroundStore';
import openDevToolsWindow, { DevToolsPosition } from './openWindow';
import { createMenu, removeMenu } from './contextMenus';
import { getOptions } from '../options/syncOptions';
import syncOptions from '../options/syncOptions';
import { BackgroundState } from './store/backgroundReducer';
declare global {
interface Window {
store: Store<BackgroundState, BackgroundAction>;
}
}
// Expose the extension's store globally to access it from the windows
// via chrome.runtime.getBackgroundPage
export const store = configureStore();
window.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
// Create the context menu when installed
chrome.runtime.onInstalled.addListener(() => {
void chrome.action.disable();
getOptions((option) => {
syncOptions().get((option) => {
if (option.showContextMenus) createMenu();
});
});
@ -26,13 +32,6 @@ chrome.runtime.onInstalled.addListener(() => {
chrome.storage.onChanged.addListener((changes) => {
if (changes.showContextMenus) {
if (changes.showContextMenus.newValue) createMenu();
else void removeMenu();
else 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,5 +1,4 @@
import { LIFTED_ACTION } from '@redux-devtools/app';
import { store } from './index';
export function getReport(
reportId: string,
@ -25,7 +24,7 @@ export function getReport(
.then((json) => {
const { payload, preloadedState } = json;
if (!payload) return;
store.dispatch({
window.store.dispatch({
type: LIFTED_ACTION,
message: 'IMPORT',
state: JSON.stringify({ payload, preloadedState }),

View File

@ -1,34 +1,83 @@
export type DevToolsPosition = 'devtools-window' | 'devtools-remote';
export type DevToolsPosition =
| 'devtools-left'
| 'devtools-right'
| 'devtools-bottom'
| 'devtools-panel'
| 'devtools-remote';
const windows: { [K in DevToolsPosition]?: number } = {};
let windows: { [K in DevToolsPosition]?: number } = {};
let lastPosition: DevToolsPosition | null = null;
export default function openDevToolsWindow(position: DevToolsPosition) {
function popWindow(
action: string,
url: string,
customOptions: chrome.windows.CreateData & chrome.windows.UpdateInfo,
) {
function focusIfExist(callback: () => void) {
if (!windows[position]) {
createWindow(position);
callback();
lastPosition = position;
} else {
chrome.windows.update(windows[position], { focused: true }, () => {
if (chrome.runtime.lastError) createWindow(position);
let params = { focused: true };
if (lastPosition !== position && position !== 'devtools-panel') {
params = { ...params, ...customOptions };
}
chrome.windows.update(windows[position]!, params, () => {
lastPosition = null;
if (chrome.runtime.lastError) callback();
});
}
}
}
function createWindow(position: DevToolsPosition) {
const url = chrome.runtime.getURL(getPath(position));
chrome.windows.create({ type: 'popup', url }, (win) => {
focusIfExist(() => {
let options: chrome.windows.CreateData = {
type: 'popup',
...customOptions,
};
if (action === 'open') {
options.url = chrome.extension.getURL(
url + '#' + position.substr(position.indexOf('-') + 1),
);
chrome.windows.create(options, (win) => {
windows[position] = win!.id;
if (navigator.userAgent.includes('Firefox')) {
void chrome.windows.update(win!.id!, { focused: true });
if (navigator.userAgent.indexOf('Firefox') !== -1) {
chrome.windows.update(win!.id!, {
focused: true,
...customOptions,
});
}
});
}
function getPath(position: DevToolsPosition) {
switch (position) {
case 'devtools-window':
return 'devpanel.html';
case 'devtools-remote':
return 'remote.html';
default:
throw new Error('Unrecognized position');
}
});
}
let params: chrome.windows.CreateData & chrome.windows.UpdateInfo = {
left: 0,
top: 0,
width: 380,
height: window.screen.availHeight,
};
let url = 'window.html';
switch (position) {
case 'devtools-right':
params.left =
(window.screen as unknown as { availLeft: number }).availLeft +
window.screen.availWidth -
params.width!;
break;
case 'devtools-bottom':
params.height = 420;
params.top = window.screen.height - params.height;
params.width = window.screen.availWidth;
break;
case 'devtools-panel':
params.type = 'panel';
break;
case 'devtools-remote':
params = { width: 850, height: 600 };
url = 'remote.html';
break;
}
popWindow('open', url, params);
}

View File

@ -11,10 +11,14 @@ import {
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import type { Options, OptionsMessage } from '../../options/syncOptions';
import syncOptions, {
Options,
OptionsMessage,
SyncOptions,
} from '../../options/syncOptions';
import openDevToolsWindow, { DevToolsPosition } from '../openWindow';
import { getReport } from '../logging';
import { Action, Dispatch, Middleware } from 'redux';
import { Action, Dispatch, MiddlewareAPI } from 'redux';
import type {
ContentScriptToBackgroundMessage,
SplitMessage,
@ -28,7 +32,6 @@ import { LiftedState } from '@redux-devtools/instrument';
import type { BackgroundAction, LiftedActionAction } from './backgroundStore';
import type { Position } from '../../pageScript/api/openWindow';
import type { BackgroundState } from './backgroundReducer';
import { store } from '../index';
interface TabMessageBase {
readonly type: string;
@ -48,11 +51,6 @@ interface StopAction extends TabMessageBase {
readonly id?: never;
}
interface OptionsAction {
readonly type: 'OPTIONS';
readonly options: Options;
}
interface DispatchAction extends TabMessageBase {
readonly type: 'DISPATCH';
readonly action: AppDispatchAction;
@ -86,7 +84,7 @@ export interface NAAction {
readonly id: string | number;
}
interface InitMessage<S, A extends Action<string>> {
interface InitMessage<S, A extends Action<unknown>> {
readonly type: 'INIT';
readonly payload: string;
instanceId: string;
@ -139,7 +137,7 @@ interface SerializedActionMessage {
readonly nextActionId: number;
}
interface SerializedStateMessage<S, A extends Action<string>> {
interface SerializedStateMessage<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
@ -153,7 +151,7 @@ interface SerializedStateMessage<S, A extends Action<string>> {
readonly committedState: boolean;
}
export type UpdateStateRequest<S, A extends Action<string>> =
type UpdateStateRequest<S, A extends Action<unknown>> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
@ -161,149 +159,91 @@ export type UpdateStateRequest<S, A extends Action<string>> =
| SerializedActionMessage
| SerializedStateMessage<S, A>;
interface UpdateStateAction<S, A extends Action<string>> {
export interface EmptyUpdateStateAction {
readonly type: typeof UPDATE_STATE;
}
interface UpdateStateAction<S, A extends Action<unknown>> {
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
| OptionsMessage
| DispatchAction
| ImportAction
| ActionAction
| ExportAction;
export type PanelMessageWithoutNA<S, A extends Action<string>> =
export type PanelMessage<S, A extends Action<unknown>> =
| NAAction
| 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>;
export type MonitorMessage =
| NAAction
| ErrorMessage
| EmptyUpdateStateAction
| SetPersistAction;
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>,
postMessage: <S, A extends Action<unknown>>(
message: PanelMessage<S, A>,
) => void;
};
type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: MonitorMessage) => 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 };
readonly monitor: { [K in number | string]: MonitorPort };
} = {
tab: {},
panel: {},
monitor: {},
};
const chunks: {
[instanceId: string]: PageScriptToContentScriptMessageForwardedToMonitors<
unknown,
Action<string>
Action<unknown>
>;
} = {};
let monitors = 0;
let isMonitored = false;
const getId = (sender: chrome.runtime.MessageSender, name?: string) =>
sender.tab ? sender.tab.id! : name || sender.id!;
type MonitorAction<S, A extends Action<string>> =
type MonitorAction<S, A extends Action<unknown>> =
| 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),
],
},
function toMonitors<S, A extends Action<unknown>>(
action: MonitorAction<S, A>,
tabId?: string | number,
verbose?: boolean,
) {
Object.keys(connections.monitor).forEach((id) => {
connections.monitor[id].postMessage(
verbose || action.type === 'ERROR' || action.type === SET_PERSIST
? action
: { type: UPDATE_STATE },
);
});
Object.keys(connections.panel).forEach((id) => {
connections.panel[id].postMessage(action);
});
}
}
port.postMessage({ ...action, request: { split: 'end' } });
}
}
}
interface ImportMessage {
@ -317,15 +257,19 @@ interface ImportMessage {
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(/^[^/]+\//, ''),
state: nonReduxDispatch(
window.store,
message,
instanceId,
action as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^\/]+\//, ''),
});
} else if (messageBody.message === 'IMPORT') {
const { message, action, id, instanceId, state } = messageBody;
@ -333,13 +277,13 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
store,
window.store,
message,
instanceId,
action as unknown as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^/]+\//, ''),
id: instanceId.toString().replace(/^[^\/]+\//, ''),
});
} else if (messageBody.message === 'ACTION') {
const { message, action, id, instanceId, state } = messageBody;
@ -347,13 +291,13 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
store,
window.store,
message,
instanceId,
action as unknown as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^/]+\//, ''),
id: instanceId.toString().replace(/^[^\/]+\//, ''),
});
} else if (messageBody.message === 'EXPORT') {
const { message, action, id, instanceId, state } = messageBody;
@ -361,41 +305,53 @@ function toContentScript(messageBody: ToContentScriptMessage) {
type: message,
action,
state: nonReduxDispatch(
store,
window.store,
message,
instanceId,
action as unknown as AppDispatchAction,
state,
),
id: instanceId.toString().replace(/^[^/]+\//, ''),
id: instanceId.toString().replace(/^[^\/]+\//, ''),
});
} else {
const { message, action, id, instanceId, state } = messageBody;
connections.tab[id].postMessage({
connections.tab[id!].postMessage({
type: message,
action,
state: nonReduxDispatch(
store,
window.store,
message,
instanceId,
action as AppDispatchAction,
state,
),
id: (instanceId as number).toString().replace(/^[^/]+\//, ''),
id: (instanceId as number).toString().replace(/^[^\/]+\//, ''),
});
}
}
function toAllTabs(msg: TabMessage) {
console.log(`Message to all tabs: ${msg.type}`);
const tabs = connections.tab;
Object.keys(tabs).forEach((id) => {
tabs[id].postMessage(msg);
});
}
for (const tabPort of Object.values(connections.tab)) {
tabPort.postMessage(msg);
function monitorInstances(shouldMonitor: boolean, id?: string) {
if (!id && isMonitored === shouldMonitor) return;
const action = {
type: shouldMonitor ? ('START' as const) : ('STOP' as const),
};
if (id) {
if (connections.tab[id]) connections.tab[id].postMessage(action);
} else {
toAllTabs(action);
}
isMonitored = shouldMonitor;
}
function getReducerError() {
const instancesState = store.getState().instances;
const instancesState = window.store.getState().instances;
const payload = instancesState.states[instancesState.current];
const computedState = payload.computedStates[payload.currentStateIndex];
if (!computedState) return false;
@ -403,13 +359,13 @@ function getReducerError() {
}
function togglePersist() {
const state = store.getState();
const state = window.store.getState();
if (state.instances.persisted) {
for (const id of Object.keys(state.instances.connections)) {
Object.keys(state.instances.connections).forEach((id) => {
if (connections.tab[id]) return;
store.dispatch({ type: REMOVE_INSTANCE, id });
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
});
}
}
@ -422,35 +378,45 @@ interface OpenOptionsMessage {
readonly type: 'OPEN_OPTIONS';
}
export type SingleMessage = OpenMessage | OpenOptionsMessage | OptionsMessage;
interface GetOptionsMessage {
readonly type: 'GET_OPTIONS';
}
type BackgroundStoreMessage<S, A extends Action<string>> =
export type SingleMessage =
| OpenMessage
| OpenOptionsMessage
| GetOptionsMessage;
type BackgroundStoreMessage<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage
| SingleMessage;
type BackgroundStoreResponse = { readonly options: Options };
// Receive messages from content scripts
function messaging<S, A extends Action<string>>(
function messaging<S, A extends Action<unknown>>(
request: BackgroundStoreMessage<S, A>,
sender: chrome.runtime.MessageSender,
sendResponse?: (response?: BackgroundStoreResponse) => void,
) {
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 });
if (!Object.keys(window.store.getState().instances.connections).length) {
window.store.dispatch({ type: DISCONNECTED });
}
return;
}
if (request.type === 'OPEN_OPTIONS') {
void chrome.runtime.openOptionsPage();
chrome.runtime.openOptionsPage();
return;
}
if (request.type === 'OPTIONS') {
toAllTabs({ type: 'OPTIONS', options: request.options });
if (request.type === 'GET_OPTIONS') {
window.syncOptions.get((options) => {
sendResponse!({ options });
});
return;
}
if (request.type === 'GET_REPORT') {
@ -458,8 +424,12 @@ function messaging<S, A extends Action<string>>(
return;
}
if (request.type === 'OPEN') {
let position: DevToolsPosition = 'devtools-window';
if (['remote', 'window'].includes(request.position)) {
let position: DevToolsPosition = 'devtools-left';
if (
['remote', 'panel', 'left', 'right', 'bottom'].indexOf(
request.position,
) !== -1
) {
position = ('devtools-' + request.position) as DevToolsPosition;
}
openDevToolsWindow(position);
@ -467,7 +437,7 @@ function messaging<S, A extends Action<string>>(
}
if (request.type === 'ERROR') {
if (request.payload) {
toMonitors(request);
toMonitors(request, tabId);
return;
}
if (!request.message) return;
@ -507,56 +477,56 @@ function messaging<S, A extends Action<string>>(
if (request.instanceId) {
action.request.instanceId = instanceId;
}
store.dispatch(action);
window.store.dispatch(action);
toMonitors(action);
if (request.type === 'EXPORT') {
toMonitors(action, tabId, true);
} else {
toMonitors(action, tabId);
}
}
function disconnect(
type: 'tab' | 'panel',
type: 'tab' | 'monitor' | 'panel',
id: number | string,
listener: (message: any, port: chrome.runtime.Port) => void,
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 });
if (!window.store.getState().instances.persisted) {
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
}
} else {
monitors--;
if (monitors === 0) toAllTabs({ type: 'STOP' });
if (!monitors) monitorInstances(false);
}
};
}
function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
function onConnect<S, A extends Action<unknown>>(port: chrome.runtime.Port) {
let id: number | string;
let listener;
store.dispatch({ type: CONNECTED, port });
window.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' });
chrome.pageAction.show(id);
chrome.pageAction.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
}
if (monitors > 0) port.postMessage({ type: 'START' });
if (isMonitored) port.postMessage({ type: 'START' });
const state = store.getState();
const state = window.store.getState();
if (state.instances.persisted) {
const instanceId = `${id}/${msg.instanceId}`;
const persistedState = state.instances.states[instanceId];
@ -580,45 +550,22 @@ function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
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;
connections.monitor[id] = port;
monitorInstances(true);
monitors++;
port.onDisconnect.addListener(disconnect('monitor', id));
} else {
// devpanel
id = port.name || port.sender!.frameId!;
connections.panel[id] = port;
monitorInstances(true, port.name);
monitors++;
toAllTabs({ type: 'START' });
listener = (msg: BackgroundAction) => {
console.log(`Message from monitor ${id}: ${msg.type}`);
store.dispatch(msg);
window.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,
});
}
}
}
@ -629,14 +576,21 @@ chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener((id) => {
chrome.notifications.clear(id);
openDevToolsWindow('devtools-window');
openDevToolsWindow('devtools-right');
});
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
const api: Middleware<{}, BackgroundState, Dispatch<BackgroundAction>> =
(store) => (next) => (untypedAction) => {
const action = untypedAction as BackgroundAction;
declare global {
interface Window {
syncOptions: SyncOptions;
}
}
window.syncOptions = syncOptions(toAllTabs); // Expose to the options page
export default function api(
store: MiddlewareAPI<Dispatch<BackgroundAction>, BackgroundState>,
) {
return (next: Dispatch<BackgroundAction>) => (action: BackgroundAction) => {
if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === TOGGLE_PERSIST) {
togglePersist();
@ -647,5 +601,4 @@ const api: Middleware<{}, BackgroundState, Dispatch<BackgroundAction>> =
}
return next(action);
};
export default api;
}

View File

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

View File

@ -1,4 +1,4 @@
import { createStore, applyMiddleware } from 'redux';
import { createStore, applyMiddleware, PreloadedState } from 'redux';
import {
CustomAction,
DispatchAction,
@ -60,7 +60,7 @@ export type BackgroundAction =
| DisconnectedAction;
export default function configureStore(
preloadedState?: Partial<BackgroundState>,
preloadedState?: PreloadedState<BackgroundState>,
) {
return createStore(rootReducer, preloadedState, applyMiddleware(api));
/*

View File

@ -1,50 +1,34 @@
// Mock not supported chrome.* API for Firefox and Electron
const isElectron = navigator.userAgent.includes('Electron');
const isFirefox = navigator.userAgent.includes('Firefox');
window.isElectron =
window.navigator && window.navigator.userAgent.indexOf('Electron') !== -1;
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
// Background page only
if (
(isElectron && location.pathname === '/background.bundle.js') ||
(window.isElectron &&
location.pathname === '/_generated_background_page.html') ||
isFirefox
) {
(chrome.runtime as any).onConnectExternal = {
addListener() {
// do nothing.
},
addListener() {},
};
(chrome.runtime as any).onMessageExternal = {
addListener() {
// do nothing.
},
addListener() {},
};
if (isElectron) {
if (window.isElectron) {
(chrome.notifications as any) = {
onClicked: {
addListener() {
// do nothing.
},
},
create() {
// do nothing.
},
clear() {
// do nothing.
addListener() {},
},
create() {},
clear() {},
};
(chrome.contextMenus as any) = {
onClicked: {
addListener() {
// do nothing.
},
},
};
(chrome.commands as any) = {
onCommand: {
addListener() {
// do nothing.
},
addListener() {},
},
};
} else {
@ -55,39 +39,34 @@ if (
}
}
if (isElectron) {
if (window.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);
}
set(obj: any, callback: any) {
Object.keys(obj).forEach((key) => {
localStorage.setItem(key, obj[key]);
});
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,
]),
);
get(obj: any, callback: any) {
const result: any = {};
Object.keys(obj).forEach((key) => {
result[key] = localStorage.getItem(key) || obj[key];
});
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);
}
remove(items: any, callback: any) {
if (Array.isArray(items)) {
items.forEach((name) => {
localStorage.removeItem(name);
});
} else {
localStorage.removeItem(keys);
localStorage.removeItem(items);
}
if (callback) {
callback();
@ -97,17 +76,17 @@ if (isElectron) {
}
// Avoid error: chrome.runtime.sendMessage is not supported responseCallback
const originSendMessage = (chrome.runtime as any).sendMessage;
(chrome.runtime as any).sendMessage = function (...args: unknown[]) {
chrome.runtime.sendMessage = function () {
if (process.env.NODE_ENV === 'development') {
return originSendMessage(...args);
return originSendMessage(...arguments);
}
if (typeof args[arguments.length - 1] === 'function') {
Array.prototype.pop.call(args);
if (typeof arguments[arguments.length - 1] === 'function') {
Array.prototype.pop.call(arguments);
}
return originSendMessage(...args);
return originSendMessage(...arguments);
};
}
if (isFirefox || isElectron) {
if (isFirefox || window.isElectron) {
(chrome.storage as any).sync = chrome.storage.local;
}

View File

@ -1,10 +1,8 @@
import '../chromeApiMock';
import {
getOptions,
injectOptions,
getOptionsFromBg,
isAllowed,
Options,
prefetchOptions,
prepareOptionsForPage,
} from '../options/syncOptions';
import type { TabMessage } from '../background/store/apiMiddleware';
import type {
@ -18,7 +16,6 @@ import {
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
@ -86,13 +83,6 @@ interface UpdateAction {
readonly source: typeof source;
}
interface OptionsAction {
readonly type: 'OPTIONS';
readonly options: Options;
readonly id: undefined;
readonly source: typeof source;
}
export type ContentScriptToPageScriptMessage =
| StartAction
| StopAction
@ -100,21 +90,20 @@ export type ContentScriptToPageScriptMessage =
| ImportAction
| ActionAction
| ExportAction
| UpdateAction
| OptionsAction;
| UpdateAction;
interface ImportStatePayload<S, A extends Action<string>> {
interface ImportStatePayload<S, A extends Action<unknown>> {
readonly type: 'IMPORT_STATE';
readonly nextLiftedState: LiftedState<S, A, unknown> | readonly A[];
readonly preloadedState?: S;
}
interface ImportStateDispatchAction<S, A extends Action<string>> {
interface ImportStateDispatchAction<S, A extends Action<unknown>> {
readonly type: 'DISPATCH';
readonly payload: ImportStatePayload<S, A>;
}
export type ListenerMessage<S, A extends Action<string>> =
export type ListenerMessage<S, A extends Action<unknown>> =
| StartAction
| StopAction
| DispatchAction
@ -122,7 +111,6 @@ export type ListenerMessage<S, A extends Action<string>> =
| ActionAction
| ExportAction
| UpdateAction
| OptionsAction
| ImportStateDispatchAction<S, A>;
function postToPageScript(message: ContentScriptToPageScriptMessage) {
@ -167,13 +155,8 @@ function connect() {
source,
});
}
} else if (message.type === 'OPTIONS') {
postToPageScript({
type: message.type,
options: prepareOptionsForPage(message.options),
id: undefined,
source,
});
} else if ('options' in message) {
injectOptions(message.options);
} else {
postToPageScript({
type: message.type,
@ -221,7 +204,7 @@ export type SplitMessage =
| SplitMessageChunk
| SplitMessageEnd;
function tryCatch<S, A extends Action<string>>(
function tryCatch<S, A extends Action<unknown>>(
fn: (
args:
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
@ -254,7 +237,7 @@ function tryCatch<S, A extends Action<string>>(
}
newArgs[key as keyof typeof newArgs] = arg;
});
fn(newArgs as SplitMessage);
fn(newArgs as any);
for (let i = 0; i < toSplit.length; i++) {
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
fn({
@ -281,38 +264,31 @@ interface InitInstanceContentScriptToBackgroundMessage {
readonly instanceId: number;
}
interface RelayMessage<S, A extends Action<string>> {
interface RelayMessage<S, A extends Action<unknown>> {
readonly name: 'RELAY';
readonly message:
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage;
}
export type ContentScriptToBackgroundMessage<S, A extends Action<string>> =
export type ContentScriptToBackgroundMessage<S, A extends Action<unknown>> =
| InitInstanceContentScriptToBackgroundMessage
| RelayMessage<S, A>;
function postToBackground<S, A extends Action<string>>(
function postToBackground<S, A extends Action<unknown>>(
message: ContentScriptToBackgroundMessage<S, A>,
) {
bg!.postMessage(message);
}
function send<S, A extends Action<string>>(
function send<S, A extends Action<unknown>>(
message:
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| SplitMessage,
) {
if (!connected) connect();
if (message.type === 'INIT_INSTANCE') {
getOptions((options) => {
postToPageScript({
type: 'OPTIONS',
options: prepareOptionsForPage(options),
id: undefined,
source,
});
});
getOptionsFromBg();
postToBackground({ name: 'INIT_INSTANCE', instanceId: message.instanceId });
} else {
postToBackground({ name: 'RELAY', message });
@ -320,7 +296,7 @@ function send<S, A extends Action<string>>(
}
// Resend messages from the page to the background script
function handleMessages<S, A extends Action<string>>(
function handleMessages<S, A extends Action<unknown>>(
event: MessageEvent<PageScriptToContentScriptMessage<S, A>>,
) {
if (!isAllowed()) return;
@ -340,6 +316,4 @@ function handleMessages<S, A extends Action<string>>(
tryCatch(send, message);
}
prefetchOptions();
window.addEventListener('message', handleMessages, false);

View File

@ -5,13 +5,12 @@ html
meta(charset='UTF-8')
title Redux DevTools
include ../style.pug
style.
body {
min-height: 100px;
}
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

@ -3,40 +3,30 @@ 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 { REMOVE_INSTANCE, StoreAction } from '@redux-devtools/app';
import App from '../app/App';
import configureStore from './store/panelStore';
import { Action, Store } from 'redux';
import {
PanelMessageWithoutNA,
PanelMessageWithSplitAction,
SplitUpdateStateRequest,
UpdateStateRequest,
} from '../background/store/apiMiddleware';
import type { PanelMessage } from '../background/store/apiMiddleware';
import type { StoreStateWithoutSocket } from './store/panelReducer';
import { PersistGate } from 'redux-persist/integration/react';
const position = location.hash;
const messageStyle: CSSProperties = {
paddingTop: '20px',
padding: '20px',
width: '100%',
textAlign: 'center',
boxSizing: 'border-box',
};
let rendered: boolean | undefined;
let currentRoot: Root | undefined;
let store: Store<StoreState, StoreAction> | undefined;
let store: Store<StoreStateWithoutSocket, StoreAction> | undefined;
let persistor: Persistor | undefined;
let bgConnection: chrome.runtime.Port;
let naTimeout: NodeJS.Timeout;
const isChrome = !navigator.userAgent.includes('Firefox');
const isChrome = navigator.userAgent.indexOf('Firefox') === -1;
function renderNodeAtRoot(node: ReactNode) {
if (currentRoot) currentRoot.unmount();
@ -67,19 +57,13 @@ function renderNA() {
<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
) {
if (isChrome) {
chrome.devtools.inspectedWindow.getResources((resources) => {
if (resources[0].url.substr(0, 4) === 'file') {
message = (
@ -88,7 +72,6 @@ function renderNA() {
<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>
@ -107,76 +90,22 @@ function renderNA() {
}, 3500);
}
let splitMessage: SplitUpdateStateRequest<unknown, Action<string>>;
function init() {
function init(id: number) {
renderNA();
let name = 'monitor';
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
name += chrome.devtools.inspectedWindow.tabId;
}
bgConnection = chrome.runtime.connect({ name });
bgConnection = chrome.runtime.connect({
name: id ? id.toString() : undefined,
});
bgConnection.onMessage.addListener(
<S, A extends Action<string>>(
message: PanelMessageWithSplitAction<S, A>,
) => {
<S, A extends Action<unknown>>(message: PanelMessage<S, A>) => {
if (message.type === 'NA') {
// TODO Double-check this now that the name is different
if (message.id === name) renderNA();
if (message.id === id) 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>);
}
store!.dispatch(message);
}
},
);
}
if (position === '#popup') document.body.style.minWidth = '760px';
if (position !== '#popup') document.body.style.minHeight = '100%';
init();
init(chrome.devtools.inspectedWindow.tabId);

View File

@ -1,32 +1,45 @@
import { combineReducers, Reducer } from 'redux';
import {
connection,
ConnectionState,
instances,
InstancesState,
monitor,
MonitorState,
notification,
NotificationState,
reports,
ReportsState,
section,
socket,
SectionState,
StateTreeSettings,
stateTreeSettings,
StoreAction,
StoreState,
theme,
ThemeState,
} from '@redux-devtools/app';
const rootReducer: Reducer<
StoreState,
StoreAction,
Partial<StoreState>
> = combineReducers({
export interface StoreStateWithoutSocket {
readonly section: SectionState;
readonly theme: ThemeState;
readonly connection: ConnectionState;
readonly monitor: MonitorState;
readonly instances: InstancesState;
readonly reports: ReportsState;
readonly notification: NotificationState;
readonly stateTreeSettings: StateTreeSettings;
}
const rootReducer: Reducer<StoreStateWithoutSocket, StoreAction> =
combineReducers<StoreStateWithoutSocket>({
instances,
monitor,
reports,
notification,
section,
socket,
theme,
connection,
stateTreeSettings,
}) as any;
});
export default rootReducer;

View File

@ -1,13 +1,9 @@
import { createStore, applyMiddleware, Reducer, Store } from 'redux';
import { createStore, applyMiddleware, Reducer } from 'redux';
import localForage from 'localforage';
import { persistReducer, persistStore } from 'redux-persist';
import {
exportStateMiddleware,
StoreAction,
StoreState,
} from '@redux-devtools/app';
import { exportStateMiddleware, StoreAction } from '@redux-devtools/app';
import panelDispatcher from './panelSyncMiddleware';
import rootReducer from './panelReducer';
import rootReducer, { StoreStateWithoutSocket } from './panelReducer';
const persistConfig = {
key: 'redux-devtools',
@ -15,10 +11,8 @@ const persistConfig = {
storage: localForage,
};
const persistedReducer: Reducer<StoreState, StoreAction> = persistReducer(
persistConfig,
rootReducer,
) as any;
const persistedReducer: Reducer<StoreStateWithoutSocket, StoreAction> =
persistReducer(persistConfig, rootReducer) as any;
export default function configureStore(
position: string,
@ -29,6 +23,6 @@ export default function configureStore(
panelDispatcher(bgConnection),
);
const store = createStore(persistedReducer, enhancer);
const persistor = persistStore(store as Store);
const persistor = persistStore(store);
return { store, persistor };
}

View File

@ -7,53 +7,22 @@ import {
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import { Dispatch, Middleware, MiddlewareAPI } from 'redux';
import { Dispatch, 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];
function panelDispatcher(bgConnection: chrome.runtime.Port) {
let autoselected = false;
const tabId = chrome.devtools.inspectedWindow.tabId;
return (store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>) =>
(next: Dispatch<StoreAction>) =>
(action: StoreAction) => {
const result = next(action);
if (!autoselected && action.type === UPDATE_STATE && tabId) {
autoselected = true;
const connections = store.getState().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;

View File

@ -1,8 +1,17 @@
chrome.devtools.panels.create(
function createPanel(url: string) {
chrome.devtools.panels.create(
'Redux',
'img/logo/scalable.png',
'devpanel.html',
() => {
// do nothing.
},
);
url,
function () {},
);
}
if (chrome.runtime.getBackgroundPage) {
// Check if the background page's object is accessible (not in incognito)
chrome.runtime.getBackgroundPage((background) => {
createPanel(background ? 'window.html' : 'devpanel.html');
});
} else {
createPanel('devpanel.html');
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { OptionsProps } from './Options';
export default function AllowToRunGroup({ options, saveOption }: OptionsProps) {
export default ({ options, saveOption }: OptionsProps) => {
const AllowToRunState = {
EVERYWHERE: true,
ON_SPECIFIC_URLS: false,
@ -50,4 +50,4 @@ export default function AllowToRunGroup({ options, saveOption }: OptionsProps) {
</div>
</fieldset>
);
}
};

View File

@ -1,10 +1,7 @@
import React from 'react';
import { OptionsProps } from './Options';
export default function ContextMenuGroup({
options,
saveOption,
}: OptionsProps) {
export default ({ options, saveOption }: OptionsProps) => {
return (
<fieldset className="option-group">
<legend className="option-group__title">Context Menu</legend>
@ -26,4 +23,4 @@ export default function ContextMenuGroup({
</div>
</fieldset>
);
}
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { OptionsProps } from './Options';
export default function EditorGroup({ options, saveOption }: OptionsProps) {
export default ({ options, saveOption }: OptionsProps) => {
const EditorState = {
BROWSER: 0,
EXTERNAL: 1,
@ -21,7 +21,7 @@ export default function EditorGroup({ options, saveOption }: OptionsProps) {
onChange={() => saveOption('useEditor', EditorState.BROWSER)}
/>
<label className="option__label" htmlFor="editor-browser">
{navigator.userAgent.includes('Firefox')
{navigator.userAgent.indexOf('Firefox') !== -1
? "Don't open in external editor"
: "Use browser's debugger (from browser devpanel only)"}
</label>
@ -80,4 +80,4 @@ export default function EditorGroup({ options, saveOption }: OptionsProps) {
</div>
</fieldset>
);
}
};

View File

@ -2,7 +2,7 @@ import React from 'react';
import { FilterState } from '../pageScript/api/filters';
import { OptionsProps } from './Options';
export default function FilterGroup({ options, saveOption }: OptionsProps) {
export default ({ options, saveOption }: OptionsProps) => {
return (
<fieldset className="option-group">
<legend className="option-group__title">
@ -68,4 +68,4 @@ export default function FilterGroup({ options, saveOption }: OptionsProps) {
</div>
</fieldset>
);
}
};

View File

@ -1,10 +1,7 @@
import React from 'react';
import { OptionsProps } from './Options';
export default function MiscellaneousGroup({
options,
saveOption,
}: OptionsProps) {
export default ({ options, saveOption }: OptionsProps) => {
return (
<fieldset className="option-group">
<legend className="option-group__title">Miscellaneous</legend>
@ -50,4 +47,4 @@ export default function MiscellaneousGroup({
</div>
</fieldset>
);
}
};

View File

@ -14,8 +14,7 @@ export interface OptionsProps {
) => void;
}
export default function OptionsComponent(props: OptionsProps) {
return (
export default (props: OptionsProps) => (
<div>
<EditorGroup {...props} />
<FilterGroup {...props} />
@ -25,12 +24,11 @@ export default function OptionsComponent(props: OptionsProps) {
<div style={{ color: 'red' }}>
<br />
<hr />
Setting options here is discouraged, and will not be possible in the
next major release. Please{' '}
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
@ -39,7 +37,6 @@ export default function OptionsComponent(props: OptionsProps) {
<a
href="https://github.com/zalmoxisus/redux-devtools-extension/issues/296"
target="_blank"
rel="noreferrer"
style={{ color: 'red' }}
>
the issue
@ -47,5 +44,4 @@ export default function OptionsComponent(props: OptionsProps) {
for more details.
</div>
</div>
);
}
);

View File

@ -2,25 +2,22 @@ import '../chromeApiMock';
import React from 'react';
import { createRoot } from 'react-dom/client';
import OptionsComponent from './Options';
import {
getOptions,
Options,
OptionsMessage,
saveOption,
subscribeToOptions,
} from './syncOptions';
import { Options } from './syncOptions';
subscribeToOptions((options) => {
const message: OptionsMessage = { type: 'OPTIONS', options };
void chrome.runtime.sendMessage(message);
});
chrome.runtime.getBackgroundPage((background) => {
const syncOptions = background!.syncOptions;
const renderOptions = (options: Options) => {
const saveOption = <K extends keyof Options>(name: K, value: Options[K]) => {
syncOptions.save(name, value);
};
const renderOptions = (options: Options) => {
const root = createRoot(document.getElementById('root')!);
root.render(<OptionsComponent options={options} saveOption={saveOption} />);
};
};
subscribeToOptions(renderOptions);
getOptions((options) => {
syncOptions.subscribe(renderOptions);
syncOptions.get((options) => {
renderOptions(options);
});
});

View File

@ -38,22 +38,21 @@ 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] } = {};
type ToAllTabs = (msg: OptionsMessage) => void;
const save =
(toAllTabs: ToAllTabs | undefined) =>
<K extends keyof Options>(key: K, value: Options[K]) => {
let obj: { [K1 in keyof Options]?: Options[K1] } = {};
obj[key] = value;
void chrome.storage.sync.set(obj);
chrome.storage.sync.set(obj);
options![key] = value;
for (const subscriber of subscribers) {
subscriber(options!);
}
};
toAllTabs!({ options: options! });
subscribers.forEach((s) => s(options!));
};
const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({
...oldOptions,
@ -72,7 +71,7 @@ const migrateOldOptions = (oldOptions: OldOrNewOptions): Options => ({
: oldOptions.filter,
});
export const getOptions = (callback: (options: Options) => void) => {
const get = (callback: (options: Options) => void) => {
if (options) callback(options);
else {
chrome.storage.sync.get(
@ -99,32 +98,67 @@ export const getOptions = (callback: (options: Options) => void) => {
}
};
export const prefetchOptions = () =>
getOptions(() => {
// do nothing.
});
export const subscribeToOptions = (callback: (options: Options) => void) => {
const subscribe = (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,
export const injectOptions = (newOptions: Options) => {
if (!newOptions) return;
options = {
...newOptions,
allowlist:
options.filter !== FilterState.DO_NOT_FILTER
? toReg(options.allowlist)!
: options.allowlist,
newOptions.filter !== FilterState.DO_NOT_FILTER
? toReg(newOptions.allowlist)!
: newOptions.allowlist,
denylist:
options.filter !== FilterState.DO_NOT_FILTER
? toReg(options.denylist)!
: options.denylist,
});
newOptions.filter !== FilterState.DO_NOT_FILTER
? toReg(newOptions.denylist)!
: newOptions.denylist,
};
let s = document.createElement('script');
s.type = 'text/javascript';
s.appendChild(
document.createTextNode(
'window.devToolsOptions = Object.assign(window.devToolsOptions||{},' +
JSON.stringify(options) +
');',
),
);
(document.head || document.documentElement).appendChild(s);
s.parentNode!.removeChild(s);
};
export const getOptionsFromBg = () => {
/* chrome.runtime.sendMessage({ type: 'GET_OPTIONS' }, response => {
if (response && response.options) injectOptions(response.options);
});
*/
get((newOptions) => {
injectOptions(newOptions);
}); // Legacy
};
export const isAllowed = (localOptions = options) =>
!localOptions ||
localOptions.inject ||
!localOptions.urls ||
location.href.match(toReg(localOptions.urls)!);
export interface SyncOptions {
readonly save: <K extends keyof Options>(key: K, value: Options[K]) => void;
readonly get: (callback: (options: Options) => void) => void;
readonly subscribe: (callback: (options: Options) => void) => void;
}
export default function syncOptions(toAllTabs?: ToAllTabs): SyncOptions {
if (toAllTabs && !options) get(() => {}); // Initialize
return {
save: save(toAllTabs),
get: get,
subscribe: subscribe,
};
}

View File

@ -8,7 +8,7 @@ declare global {
}
}
export default class Monitor<S, A extends Action<string>> {
export default class Monitor<S, A extends Action<unknown>> {
update: (
liftedState?: LiftedState<S, A, unknown> | undefined,
libConfig?: LibConfig,

View File

@ -1,3 +1,4 @@
import mapValues from 'lodash/mapValues';
import { Action } from 'redux';
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
import { LocalFilter } from '@redux-devtools/utils';
@ -20,13 +21,14 @@ export const noFiltersApplied = (localFilter: LocalFilter | undefined) =>
!window.devToolsOptions.filter ||
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER);
export function isFiltered<A extends Action<string>>(
export function isFiltered<A extends Action<unknown>>(
action: A | string,
localFilter: LocalFilter | undefined,
) {
if (
noFiltersApplied(localFilter) ||
(typeof action !== 'string' && typeof action.type.match !== 'function')
(typeof action !== 'string' &&
typeof (action.type as string).match !== 'function')
) {
return false;
}
@ -39,20 +41,15 @@ export function isFiltered<A extends Action<string>>(
);
}
function filterActions<A extends Action<string>>(
function filterActions<A extends Action<unknown>>(
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,
{
return mapValues(actionsById, (action, id) => ({
...action,
action: actionSanitizer(action.action, actionId as unknown as number),
},
]),
);
action: actionSanitizer(action.action, id as unknown as number),
}));
}
function filterStates<S>(
@ -66,7 +63,7 @@ function filterStates<S>(
}));
}
export function filterState<S, A extends Action<string>>(
export function filterState<S, A extends Action<unknown>>(
state: LiftedState<S, A, unknown>,
localFilter: LocalFilter | undefined,
stateSanitizer: ((state: S, index: number) => S) | undefined,
@ -123,7 +120,7 @@ export function filterState<S, A extends Action<string>>(
};
}
export interface PartialLiftedState<S, A extends Action<string>> {
export interface PartialLiftedState<S, A extends Action<unknown>> {
readonly actionsById: { [actionId: number]: PerformAction<A> };
readonly computedStates: { state: S; error?: string }[];
readonly stagedActionIds: readonly number[];
@ -132,16 +129,16 @@ export interface PartialLiftedState<S, A extends Action<string>> {
readonly committedState?: S;
}
export function startingFrom<S, A extends Action<string>>(
export function startingFrom<S, A extends Action<unknown>>(
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)
| (<A extends Action<unknown>>(action: A, id: number) => A)
| undefined,
predicate:
| (<S, A extends Action<string>>(state: S, action: A) => boolean)
| (<S, A extends Action<unknown>>(state: S, action: A) => boolean)
| undefined,
): LiftedState<S, A, unknown> | PartialLiftedState<S, A> | undefined {
const stagedActionIds = state.stagedActionIds;

View File

@ -30,7 +30,7 @@ interface ParsedSerializedLiftedState {
readonly preloadedState?: string;
}
export default function importState<S, A extends Action<string>>(
export default function importState<S, A extends Action<unknown>>(
state: string | undefined,
{ serialize }: Config,
) {
@ -58,7 +58,7 @@ export default function importState<S, A extends Action<string>>(
| LiftedState<S, A, unknown> = parse(state) as
| ParsedSerializedLiftedState
| LiftedState<S, A, unknown>;
const preloadedState =
let preloadedState =
'payload' in parsedSerializedLiftedState &&
parsedSerializedLiftedState.preloadedState
? (parse(parsedSerializedLiftedState.preloadedState) as S)

View File

@ -1,5 +1,5 @@
import jsan, { Options } from 'jsan';
import { throttle } from 'lodash-es';
import throttle from 'lodash/throttle';
import { immutableSerialize } from '@redux-devtools/serialize';
import { getActionsArray, getLocalFilter } from '@redux-devtools/utils';
import { isFiltered, PartialLiftedState } from './filters';
@ -115,7 +115,7 @@ interface DisconnectMessage {
readonly source: typeof source;
}
interface InitMessage<S, A extends Action<string>> {
interface InitMessage<S, A extends Action<unknown>> {
readonly type: 'INIT';
readonly payload: string;
readonly instanceId: number;
@ -161,7 +161,7 @@ interface SerializedActionMessage {
readonly nextActionId?: number;
}
interface SerializedStateMessage<S, A extends Action<string>> {
interface SerializedStateMessage<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
@ -183,7 +183,7 @@ interface OpenMessage {
export type PageScriptToContentScriptMessageForwardedToMonitors<
S,
A extends Action<string>,
A extends Action<unknown>,
> =
| InitMessage<S, A>
| LiftedMessage
@ -194,7 +194,7 @@ export type PageScriptToContentScriptMessageForwardedToMonitors<
export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<
S,
A extends Action<string>,
A extends Action<unknown>,
> =
| PageScriptToContentScriptMessageForwardedToMonitors<S, A>
| ErrorMessage
@ -204,17 +204,17 @@ export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<
export type PageScriptToContentScriptMessageWithoutDisconnect<
S,
A extends Action<string>,
A extends Action<unknown>,
> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| InitInstancePageScriptToContentScriptMessage
| InitInstanceMessage;
export type PageScriptToContentScriptMessage<S, A extends Action<string>> =
export type PageScriptToContentScriptMessage<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| DisconnectMessage;
function post<S, A extends Action<string>>(
function post<S, A extends Action<unknown>>(
message: PageScriptToContentScriptMessage<S, A>,
) {
window.postMessage(message, '*');
@ -222,7 +222,6 @@ function post<S, A extends Action<string>>(
function getStackTrace(
config: Config,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
toExcludeFromTrace: Function | undefined,
) {
if (!config.trace) return undefined;
@ -249,7 +248,6 @@ function getStackTrace(
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
@ -260,18 +258,17 @@ function getStackTrace(
return stack;
}
function amendActionType<A extends Action<string>>(
function amendActionType<A extends Action<unknown>>(
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);
let timestamp = Date.now();
let stack = getStackTrace(config, toExcludeFromTrace);
if (typeof action === 'string') {
return { action: { type: action } as A, timestamp, stack };
}
@ -291,7 +288,7 @@ interface LiftedMessage {
readonly source: typeof source;
}
interface PartialStateMessage<S, A extends Action<string>> {
interface PartialStateMessage<S, A extends Action<unknown>> {
readonly type: 'PARTIAL_STATE';
readonly payload: PartialLiftedState<S, A>;
readonly source: typeof source;
@ -299,7 +296,7 @@ interface PartialStateMessage<S, A extends Action<string>> {
readonly maxAge: number;
}
interface ExportMessage<S, A extends Action<string>> {
interface ExportMessage<S, A extends Action<unknown>> {
readonly type: 'EXPORT';
readonly payload: readonly A[];
readonly committedState: S;
@ -307,21 +304,21 @@ interface ExportMessage<S, A extends Action<string>> {
readonly instanceId: number;
}
export interface StructuralPerformAction<A extends Action<string>> {
export interface StructuralPerformAction<A extends Action<unknown>> {
readonly action: A;
readonly timestamp?: number;
readonly stack?: string;
}
type SingleUserAction<A extends Action<string>> =
type SingleUserAction<A extends Action<unknown>> =
| PerformAction<A>
| StructuralPerformAction<A>
| A;
type UserAction<A extends Action<string>> =
type UserAction<A extends Action<unknown>> =
| SingleUserAction<A>
| readonly SingleUserAction<A>[];
interface ActionMessage<S, A extends Action<string>> {
interface ActionMessage<S, A extends Action<unknown>> {
readonly type: 'ACTION';
readonly payload: S;
readonly source: typeof source;
@ -332,7 +329,7 @@ interface ActionMessage<S, A extends Action<string>> {
readonly name?: string;
}
interface StateMessage<S, A extends Action<string>> {
interface StateMessage<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: LiftedState<S, A, unknown>;
readonly source: typeof source;
@ -372,7 +369,7 @@ interface StopMessage {
readonly instanceId: number;
}
type ToContentScriptMessage<S, A extends Action<string>> =
type ToContentScriptMessage<S, A extends Action<unknown>> =
| LiftedMessage
| PartialStateMessage<S, A>
| ExportMessage<S, A>
@ -383,7 +380,7 @@ type ToContentScriptMessage<S, A extends Action<string>> =
| GetReportMessage
| StopMessage;
export function toContentScript<S, A extends Action<string>>(
export function toContentScript<S, A extends Action<unknown>>(
message: ToContentScriptMessage<S, A>,
serializeState?: Serialize | undefined,
serializeAction?: Serialize | undefined,
@ -428,7 +425,7 @@ export function toContentScript<S, A extends Action<string>>(
}
}
export function sendMessage<S, A extends Action<string>>(
export function sendMessage<S, A extends Action<unknown>>(
action: StructuralPerformAction<A> | StructuralPerformAction<A>[],
state: LiftedState<S, A, unknown>,
config: Config,
@ -499,7 +496,7 @@ export function setListener(
}
const liftListener =
<S, A extends Action<string>>(
<S, A extends Action<unknown>>(
listener: (message: ListenerMessage<S, A>) => void,
config: Config,
) =>
@ -523,15 +520,15 @@ export function disconnect() {
}
export interface ConnectResponse {
init: <S, A extends Action<string>>(
init: <S, A extends Action<unknown>>(
state: S,
liftedData?: LiftedState<S, A, unknown>,
) => void;
subscribe: <S, A extends Action<string>>(
subscribe: <S, A extends Action<unknown>>(
listener: (message: ListenerMessage<S, A>) => void,
) => (() => void) | undefined;
unsubscribe: () => void;
send: <S, A extends Action<string>>(
send: <S, A extends Action<unknown>>(
action: A,
state: LiftedState<S, A, unknown>,
) => void;
@ -553,8 +550,8 @@ export function connect(preConfig: Config): ConnectResponse {
const localFilter = getLocalFilter(config);
const autoPause = config.autoPause;
let isPaused = autoPause;
let delayedActions: StructuralPerformAction<Action<string>>[] = [];
let delayedStates: LiftedState<unknown, Action<string>, unknown>[] = [];
let delayedActions: StructuralPerformAction<Action<unknown>>[] = [];
let delayedStates: LiftedState<unknown, Action<unknown>, unknown>[] = [];
const rootListener = (action: ContentScriptToPageScriptMessage) => {
if (autoPause) {
@ -577,7 +574,7 @@ export function connect(preConfig: Config): ConnectResponse {
listeners[id] = [rootListener];
const subscribe = <S, A extends Action<string>>(
const subscribe = <S, A extends Action<unknown>>(
listener: (message: ListenerMessage<S, A>) => void,
) => {
if (!listener) return undefined;
@ -598,16 +595,12 @@ export function connect(preConfig: Config): ConnectResponse {
};
const sendDelayed = throttle(() => {
sendMessage(
delayedActions,
delayedStates as unknown as LiftedState<unknown, Action<string>, unknown>,
config,
);
sendMessage(delayedActions, delayedStates as any, config);
delayedActions = [];
delayedStates = [];
}, latency);
const send = <S, A extends Action<string>>(
const send = <S, A extends Action<unknown>>(
action: A,
state: LiftedState<S, A, unknown>,
) => {
@ -650,7 +643,7 @@ export function connect(preConfig: Config): ConnectResponse {
);
};
const init = <S, A extends Action<string>>(
const init = <S, A extends Action<unknown>>(
state: S,
liftedData?: LiftedState<S, A, unknown>,
) => {

View File

@ -10,7 +10,7 @@ function createExpBackoffTimer(step: number) {
return 0;
}
// Calculate next timeout
const timeout = Math.pow(2, count - 1);
let timeout = Math.pow(2, count - 1);
if (count < 5) count += 1;
return timeout * step;
};

View File

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

View File

@ -4,8 +4,8 @@ import { persistState } from '@redux-devtools/core';
import type { ConfigWithExpandedMaxAge } from './index';
export function getUrlParam(key: string) {
const matches = new RegExp(`[?&]${key}=([^&#]+)\\b`).exec(
window.location.href,
const matches = window.location.href.match(
new RegExp(`[?&]${key}=([^&#]+)\\b`),
);
return matches && matches.length > 0 ? matches[1] : null;
}
@ -18,9 +18,9 @@ declare global {
export default function configureStore<
S,
A extends Action<string>,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<string>,
MonitorAction extends Action<unknown>,
>(
next: StoreEnhancerStoreCreator,
monitorReducer: Reducer<MonitorState, MonitorAction>,

View File

@ -4,8 +4,16 @@ import {
getActionsArray,
getLocalFilter,
} from '@redux-devtools/utils';
import { throttle } from 'lodash-es';
import { Action, ActionCreator, Dispatch, Reducer, StoreEnhancer } from 'redux';
import throttle from 'lodash/throttle';
import {
Action,
ActionCreator,
Dispatch,
PreloadedState,
Reducer,
StoreEnhancer,
StoreEnhancerStoreCreator,
} from 'redux';
import Immutable from 'immutable';
import {
EnhancedStore,
@ -48,15 +56,15 @@ import type { ContentScriptToPageScriptMessage } from '../contentScript';
type EnhancedStoreWithInitialDispatch<
S,
A extends Action<string>,
A extends Action<unknown>,
MonitorState,
> = EnhancedStore<S, A, MonitorState> & { initialDispatch: Dispatch<A> };
const source = '@devtools-page';
const stores: {
let stores: {
[K in string | number]: EnhancedStoreWithInitialDispatch<
unknown,
Action<string>,
Action<unknown>,
unknown
>;
} = {};
@ -89,18 +97,18 @@ export interface ConfigWithExpandedMaxAge {
readonly actionsAllowlist?: string | readonly string[];
serialize?: boolean | SerializeWithImmutable;
readonly stateSanitizer?: <S>(state: S, index?: number) => S;
readonly actionSanitizer?: <A extends Action<string>>(
readonly actionSanitizer?: <A extends Action<unknown>>(
action: A,
id?: number,
) => A;
readonly predicate?: <S, A extends Action<string>>(
readonly predicate?: <S, A extends Action<unknown>>(
state: S,
action: A,
) => boolean;
readonly latency?: number;
readonly maxAge?:
| number
| (<S, A extends Action<string>>(
| (<S, A extends Action<unknown>>(
currentLiftedAction: LiftedAction<S, A, unknown>,
previousLiftedState: LiftedState<S, A, unknown> | undefined,
) => number);
@ -115,9 +123,9 @@ export interface ConfigWithExpandedMaxAge {
readonly autoPause?: boolean;
readonly features?: Features;
readonly type?: string;
readonly getActionType?: <A extends Action<string>>(action: A) => A;
readonly getActionType?: <A extends Action<unknown>>(action: A) => A;
readonly actionCreators?: {
readonly [key: string]: ActionCreator<Action<string>>;
readonly [key: string]: ActionCreator<Action<unknown>>;
};
}
@ -129,7 +137,7 @@ interface ReduxDevtoolsExtension {
(config?: Config): StoreEnhancer;
open: (position?: Position) => void;
notifyErrors: (onError?: () => boolean) => void;
send: <S, A extends Action<string>>(
send: <S, A extends Action<unknown>>(
action: StructuralPerformAction<A> | StructuralPerformAction<A>[],
state: LiftedState<S, A, unknown>,
config: Config,
@ -150,7 +158,7 @@ declare global {
}
}
function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<unknown>>(
config?: Config,
): StoreEnhancer {
/* eslint-disable no-param-reassign */
@ -167,7 +175,7 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
const localFilter = getLocalFilter(config);
const serializeState = getSerializeParameter(config);
const serializeAction = getSerializeParameter(config);
const { stateSanitizer, actionSanitizer, predicate, latency = 500 } = config;
let { stateSanitizer, actionSanitizer, predicate, latency = 500 } = config;
// Deprecate actionsWhitelist and actionsBlacklist
if (config.actionsWhitelist) {
@ -432,13 +440,6 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
serializeAction,
);
}
return;
case 'OPTIONS':
window.devToolsOptions = Object.assign(
window.devToolsOptions || {},
message.options,
);
return;
}
}
@ -447,7 +448,7 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
liftedAction?: LiftedAction<S, A, unknown>,
liftedState?: LiftedState<S, A, unknown> | undefined,
) => {
const m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50;
let m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50;
if (
!liftedAction ||
noFiltersApplied(localFilter) ||
@ -464,7 +465,10 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
if (filteredActionIds.length >= m) {
const stagedActionIds = liftedState!.stagedActionIds;
let i = 1;
while (maxAge > m && !filteredActionIds.includes(stagedActionIds[i])) {
while (
maxAge > m &&
filteredActionIds.indexOf(stagedActionIds[i]) === -1
) {
maxAge--;
i++;
}
@ -522,26 +526,32 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
relayState(liftedState);
}
const enhance = (): StoreEnhancer => (next) => {
return <S2, A2 extends Action<string>, PreloadedState>(
reducer_: Reducer<S2, A2, PreloadedState>,
initialState_?: PreloadedState | undefined,
const enhance =
(): StoreEnhancer =>
<NextExt, NextStateExt>(
next: StoreEnhancerStoreCreator<NextExt, NextStateExt>,
): any => {
return <S2 extends S, A2 extends A>(
reducer_: Reducer<S2, A2>,
initialState_?: PreloadedState<S2>,
) => {
if (!isAllowed(window.devToolsOptions)) {
return next(reducer_, initialState_);
}
store = stores[instanceId] = (
configureStore(next, monitor.reducer, {
store = stores[instanceId] = configureStore(
next as StoreEnhancerStoreCreator,
monitor.reducer,
{
...config,
maxAge: getMaxAge as any,
}) as any
)(reducer_, initialState_);
},
)(reducer_, initialState_) as any;
if (isInIframe()) setTimeout(init, 3000);
else init();
return store as any;
return store;
};
};
@ -588,18 +598,18 @@ export type InferComposedStoreExt<StoreEnhancers> = StoreEnhancers extends [
? HeadStoreEnhancer extends StoreEnhancer<infer StoreExt>
? StoreExt & InferComposedStoreExt<RestStoreEnhancers>
: never
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
{};
: unknown;
const extensionCompose =
(config: Config) =>
<StoreEnhancers extends readonly StoreEnhancer[]>(
<StoreEnhancers extends readonly StoreEnhancer<unknown>[]>(
...funcs: StoreEnhancers
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>> => {
// @ts-expect-error FIXME
// @ts-ignore FIXME
return (...args) => {
const instanceId = generateId(config.instanceId);
return [preEnhancer(instanceId), ...funcs].reduceRight(
// @ts-ignore FIXME
(composed, f) => f(composed),
__REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId })(...args),
);
@ -609,10 +619,10 @@ const extensionCompose =
interface ReduxDevtoolsExtensionCompose {
(
config: Config,
): <StoreEnhancers extends readonly StoreEnhancer[]>(
): <StoreEnhancers extends readonly StoreEnhancer<unknown>[]>(
...funcs: StoreEnhancers
) => StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
<StoreEnhancers extends readonly StoreEnhancer[]>(
<StoreEnhancers extends readonly StoreEnhancer<unknown>[]>(
...funcs: StoreEnhancers
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
}
@ -625,22 +635,24 @@ declare global {
function reduxDevtoolsExtensionCompose(
config: Config,
): <StoreEnhancers extends readonly StoreEnhancer[]>(
): <StoreEnhancers extends readonly StoreEnhancer<unknown>[]>(
...funcs: StoreEnhancers
) => StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
function reduxDevtoolsExtensionCompose<
StoreEnhancers extends readonly StoreEnhancer[],
StoreEnhancers extends readonly StoreEnhancer<unknown>[],
>(
...funcs: StoreEnhancers
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>>;
function reduxDevtoolsExtensionCompose(...funcs: [Config] | StoreEnhancer[]) {
function reduxDevtoolsExtensionCompose(
...funcs: [Config] | StoreEnhancer<unknown>[]
) {
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[]));
return extensionCompose({})(...(funcs as StoreEnhancer<unknown>[]));
}
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = reduxDevtoolsExtensionCompose;

View File

@ -0,0 +1,19 @@
// @ts-ignore
import script from '../dist/page.bundle.js';
let s = document.createElement('script');
s.type = 'text/javascript';
if (process.env.NODE_ENV === 'production') {
s.appendChild(document.createTextNode(script));
(document.head || document.documentElement).appendChild(s);
s.parentNode!.removeChild(s);
} else {
s.src = chrome.extension.getURL('page.bundle.js');
s.onload = function () {
(this as HTMLScriptElement).parentNode!.removeChild(
this as HTMLScriptElement,
);
};
(document.head || document.documentElement).appendChild(s);
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { UPDATE_STATE } from '@redux-devtools/app';
import App from '../app/App';
import configureStore from './store/windowStore';
import type { MonitorMessage } from '../background/store/apiMiddleware';
const position = location.hash;
chrome.runtime.getBackgroundPage((window) => {
const { store } = window!;
const { store: localStore, persistor } = configureStore(store, position);
let name = 'monitor';
if (chrome && chrome.devtools && chrome.devtools.inspectedWindow) {
name += chrome.devtools.inspectedWindow.tabId;
}
const bg = chrome.runtime.connect({ name });
const update = (action?: MonitorMessage) => {
localStore.dispatch(action || { type: UPDATE_STATE });
};
bg.onMessage.addListener(update);
update();
const root = createRoot(document.getElementById('root')!);
root.render(
<Provider store={localStore}>
<PersistGate loading={null} persistor={persistor}>
<App position={position} />
</PersistGate>
</Provider>,
);
});
if (position === '#popup') document.body.style.minWidth = '760px';
if (position !== '#popup') document.body.style.minHeight = '100%';

View File

@ -0,0 +1,50 @@
import { Dispatch, MiddlewareAPI } from 'redux';
import {
SELECT_INSTANCE,
StoreAction,
StoreState,
UPDATE_STATE,
} from '@redux-devtools/app';
function selectInstance(
tabId: number,
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
next: Dispatch<StoreAction>,
) {
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!);
},
);
}
export default function popupSelector(
store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>,
) {
return (next: Dispatch<StoreAction>) => (action: StoreAction) => {
const result = next(action);
if (action.type === UPDATE_STATE) {
if (chrome.devtools && chrome.devtools.inspectedWindow) {
selectInstance(chrome.devtools.inspectedWindow.tabId, store, next);
} else {
getCurrentTabId((tabId) => selectInstance(tabId, store, next));
}
}
return result;
};
}

View File

@ -0,0 +1,34 @@
import {
instancesInitialState,
dispatchAction,
UPDATE_STATE,
SELECT_INSTANCE,
LIFTED_ACTION,
SET_PERSIST,
} from '@redux-devtools/app';
import type {
ExpandedUpdateStateAction,
WindowStoreAction,
} from './windowStore';
export default function instances(
state = instancesInitialState,
action: WindowStoreAction,
) {
switch (action.type) {
case UPDATE_STATE:
return {
...(action as ExpandedUpdateStateAction).instances,
selected: state.selected,
};
case LIFTED_ACTION:
if (action.message === 'DISPATCH') return dispatchAction(state, action);
return state;
case SELECT_INSTANCE:
return { ...state, selected: action.selected };
case SET_PERSIST:
return { ...state, persisted: action.payload };
default:
return state;
}
}

View File

@ -0,0 +1,29 @@
import { combineReducers, Reducer } from 'redux';
import {
connection,
monitor,
notification,
reports,
section,
socket,
theme,
stateTreeSettings,
StoreState,
} from '@redux-devtools/app';
import instances from './instancesReducer';
import type { WindowStoreAction } from './windowStore';
const rootReducer: Reducer<StoreState, WindowStoreAction> =
combineReducers<StoreState>({
instances,
monitor,
socket,
reports,
notification,
section,
theme,
connection,
stateTreeSettings,
});
export default rootReducer;

View File

@ -0,0 +1,81 @@
import {
createStore,
compose,
applyMiddleware,
Store,
StoreEnhancer,
Reducer,
} from 'redux';
import localForage from 'localforage';
import { persistReducer, persistStore } from 'redux-persist';
import {
api,
CONNECT_REQUEST,
exportStateMiddleware,
InstancesState,
StoreActionWithoutUpdateState,
StoreState,
UpdateStateAction,
} from '@redux-devtools/app';
import syncStores from './windowSyncMiddleware';
import instanceSelector from './instanceSelectorMiddleware';
import rootReducer from './windowReducer';
import type { BackgroundState } from '../../background/store/backgroundReducer';
import type { BackgroundAction } from '../../background/store/backgroundStore';
import type {
EmptyUpdateStateAction,
NAAction,
} from '../../background/store/apiMiddleware';
export interface ExpandedUpdateStateAction extends UpdateStateAction {
readonly instances: InstancesState;
}
export type WindowStoreAction =
| StoreActionWithoutUpdateState
| ExpandedUpdateStateAction
| NAAction
| EmptyUpdateStateAction;
const persistConfig = {
key: 'redux-devtools',
blacklist: ['instances', 'socket'],
storage: localForage,
};
const persistedReducer: Reducer<StoreState, WindowStoreAction> = persistReducer(
persistConfig,
rootReducer,
) as any;
export default function configureStore(
baseStore: Store<BackgroundState, BackgroundAction>,
position: string,
) {
let enhancer: StoreEnhancer;
const middlewares = [exportStateMiddleware, api, syncStores(baseStore)];
if (!position || position === '#popup') {
// select current tab instance for devPanel and pageAction
middlewares.push(instanceSelector);
}
if (process.env.NODE_ENV === 'production') {
enhancer = applyMiddleware(...middlewares);
} else {
enhancer = compose(
applyMiddleware(...middlewares),
window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (noop: unknown) => noop,
);
}
const store = createStore(persistedReducer, enhancer);
const persistor = persistStore(store, null, () => {
if (store.getState().connection.type !== 'disabled') {
store.dispatch({
type: CONNECT_REQUEST,
});
}
});
return { store, persistor };
}

View File

@ -0,0 +1,34 @@
import {
getActiveInstance,
LIFTED_ACTION,
StoreAction,
StoreState,
TOGGLE_PERSIST,
UPDATE_STATE,
} from '@redux-devtools/app';
import { Dispatch, MiddlewareAPI, Store } from 'redux';
import type { BackgroundState } from '../../background/store/backgroundReducer';
import type { WindowStoreAction } from './windowStore';
import type { BackgroundAction } from '../../background/store/backgroundStore';
const syncStores =
(baseStore: Store<BackgroundState, BackgroundAction>) =>
(store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>) =>
(next: Dispatch<WindowStoreAction>) =>
(action: StoreAction) => {
if (action.type === UPDATE_STATE) {
return next({
...action,
instances: baseStore.getState().instances,
});
}
if (action.type === LIFTED_ACTION || action.type === TOGGLE_PERSIST) {
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = instances.options[instanceId].connectionId;
baseStore.dispatch({ ...action, instanceId, id } as any);
}
return next(action);
};
export default syncStores;

View File

@ -0,0 +1,18 @@
doctype html
html
head
meta(charset='UTF-8')
title Redux DevTools
include ../style.pug
body
#root
div(style='position: relative')
img(
src='/img/loading.svg',
height=300, width=350,
style='position: absolute; top: 50%; left: 50%; margin-top: -175px; margin-left: -175px;'
)
link(href='/window.bundle.css', rel='stylesheet')
script(src='/window.bundle.js')

View File

@ -1 +0,0 @@
module.exports = {};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from '../../../src/devpanel/store/panelStore';
import configureStore from '../../../src/window/store/windowStore';
import App from '../../../src/app/App';
Object.defineProperty(window, 'matchMedia', {

View File

@ -20,7 +20,16 @@ describe('API', () => {
expect(message).toEqual({
source: '@devtools-page',
type: 'OPEN',
position: 'window',
position: 'right',
});
message = await listenMessage(() => {
window.__REDUX_DEVTOOLS_EXTENSION__.open('left');
});
expect(message).toEqual({
source: '@devtools-page',
type: 'OPEN',
position: 'left',
});
});

View File

@ -1,3 +1,4 @@
import '@babel/polyfill';
import { createStore, compose } from 'redux';
import { insertScript, listenMessage } from '../../utils/inject';
import '../../../src/pageScript';

View File

@ -1,8 +1,10 @@
import { resolve } from 'path';
import webdriver from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import chromedriver from 'chromedriver';
import { switchMonitorTests, delay } from '../utils/e2e';
const port = 9515;
const path = resolve(__dirname, '..', '..', 'dist');
const extensionId = 'lmhkpmbekcpmknklioeibfkpmmfibljd';
const actionsPattern =
@ -12,11 +14,12 @@ describe('Chrome extension', function () {
let driver;
beforeAll(async () => {
chromedriver.start();
await delay(2000);
driver = new webdriver.Builder()
.usingServer(`http://localhost:${port}`)
.setChromeOptions(
new chrome.Options()
.setBrowserVersion('stable')
.addArguments(`load-extension=${path}`),
new chrome.Options().addArguments(`load-extension=${path}`),
)
.forBrowser('chrome')
.build();
@ -24,12 +27,13 @@ describe('Chrome extension', function () {
afterAll(async () => {
await driver.quit();
chromedriver.stop();
});
it("should open extension's window", async () => {
await driver.get(`chrome-extension://${extensionId}/devpanel.html`);
await driver.get(`chrome-extension://${extensionId}/window.html#left`);
const url = await driver.getCurrentUrl();
expect(url).toBe(`chrome-extension://${extensionId}/devpanel.html`);
expect(url).toBe(`chrome-extension://${extensionId}/window.html#left`);
});
it('should match document title', async () => {
@ -37,8 +41,29 @@ describe('Chrome extension', function () {
expect(title).toBe('Redux DevTools');
});
it("should contain inspector monitor's component", async () => {
await delay(1000);
const val = await driver
.findElement(webdriver.By.xpath('//div[contains(@class, "inspector-")]'))
.getText();
expect(val).toBeDefined();
});
it('should contain an empty actions list', async () => {
const val = await driver
.findElement(
webdriver.By.xpath('//div[contains(@class, "actionListRows-")]'),
)
.getText();
expect(val).toBe('');
});
Object.keys(switchMonitorTests).forEach((description) =>
it(description, () => switchMonitorTests[description](driver)),
);
it('should get actions list', async () => {
const url = 'https://zalmoxisus.github.io/examples/router/';
const url = 'http://zalmoxisus.github.io/examples/router/';
await driver.executeScript(`window.open('${url}')`);
await delay(2000);
@ -49,10 +74,11 @@ describe('Chrome extension', function () {
await driver.switchTo().window(tabs[0]);
await delay(1000);
const result = await driver.wait(
driver
.findElement(webdriver.By.xpath('//div[@data-testid="actionListRows"]'))
.findElement(
webdriver.By.xpath('//div[contains(@class, "actionListRows-")]'),
)
.getText()
.then((val) => {
return actionsPattern.test(val);
@ -62,16 +88,4 @@ describe('Chrome extension', function () {
);
expect(result).toBeTruthy();
});
it("should contain inspector monitor's component", async () => {
const val = await driver
.findElement(webdriver.By.xpath('//div[@data-testid="inspector"]'))
.getText();
expect(val).toBeDefined();
});
Object.keys(switchMonitorTests).forEach((description) =>
// eslint-disable-next-line jest/expect-expect,jest/valid-title
it(description, () => switchMonitorTests[description](driver)),
);
});

View File

@ -5,8 +5,9 @@ import electronPath from 'electron';
import chromedriver from 'chromedriver';
import { switchMonitorTests, delay } from '../utils/e2e';
const port = 9515;
const devPanelPath =
'chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/devpanel.html';
'chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/window.html';
describe('DevTools panel for Electron', function () {
let driver;
@ -15,7 +16,7 @@ describe('DevTools panel for Electron', function () {
chromedriver.start();
await delay(1000);
driver = new webdriver.Builder()
.usingServer('http://localhost:9515')
.usingServer(`http://localhost:${port}`)
.setChromeOptions(
new chrome.Options()
.setChromeBinaryPath(electronPath)
@ -52,14 +53,13 @@ describe('DevTools panel for Electron', function () {
if (attempts === 0) {
return callback('Redux panel not found');
}
if (EUI.InspectorView) {
const instance = EUI.InspectorView.InspectorView.instance();
const tabs = instance.tabbedPane.tabs;
if (UI.inspectorView) {
const tabs = UI.inspectorView.tabbedPane.tabs;
const idList = tabs.map((tab) => tab.id);
const reduxPanelId =
'chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljdRedux';
if (idList.indexOf(reduxPanelId) !== -1) {
instance.showPanel(reduxPanelId);
UI.inspectorView.showPanel(reduxPanelId);
return callback(reduxPanelId);
}
}
@ -76,7 +76,6 @@ describe('DevTools panel for Electron', function () {
expect(className).not.toMatch(/hidden/); // not hidden
});
// eslint-disable-next-line jest/expect-expect
it('should have Redux DevTools UI on current tab', async () => {
await driver
.switchTo()
@ -91,7 +90,7 @@ describe('DevTools panel for Electron', function () {
it('should contain INIT action', async () => {
const element = await driver.wait(
webdriver.until.elementLocated(
webdriver.By.xpath('//div[@data-testid="actionListRows"]'),
webdriver.By.xpath('//div[contains(@class, "actionListRows-")]'),
),
5000,
'Element not found',
@ -102,17 +101,15 @@ describe('DevTools panel for Electron', function () {
it("should contain Inspector monitor's component", async () => {
const val = await driver
.findElement(webdriver.By.xpath('//div[@data-testid="inspector"]'))
.findElement(webdriver.By.xpath('//div[contains(@class, "inspector-")]'))
.getText();
expect(val).toBeDefined();
});
Object.keys(switchMonitorTests).forEach((description) =>
// eslint-disable-next-line jest/expect-expect,jest/valid-title
it(description, () => switchMonitorTests[description](driver)),
);
// eslint-disable-next-line jest/no-commented-out-tests
/* it('should be no logs in console of main window', async () => {
const handles = await driver.getAllWindowHandles();
await driver.switchTo().window(handles[1]); // Change to main window

View File

@ -1,3 +1,4 @@
import '@babel/polyfill';
import { bigArray, bigString, circularData } from './data';
import { listenMessage } from '../utils/inject';
import '../../src/browser/extension/inject/pageScript';

View File

@ -1,3 +1,4 @@
require('@babel/polyfill');
global.chrome = require('sinon-chrome');
require('@testing-library/jest-dom');

40
nx.json Normal file
View File

@ -0,0 +1,40 @@
{
"extends": "nx/presets/npm.json",
"npmScope": "undetermined",
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint", "package", "prepare"]
}
}
},
"targetDependencies": {
"build": [
{
"target": "build",
"projects": "dependencies"
}
],
"prepare": [
{
"target": "prepare",
"projects": "dependencies"
}
],
"package": [
{
"target": "package",
"projects": "dependencies"
}
]
},
"affected": {
"defaultBase": "main"
},
"pluginsConfig": {
"@nrwl/js": {
"analyzeSourceFiles": false
}
}
}

View File

@ -1,28 +1,43 @@
{
"private": true,
"devDependencies": {
"@babel/core": "^7.26.9",
"@changesets/cli": "^2.28.1",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",
"prettier": "3.5.3",
"typescript": "~5.8.2",
"typescript-eslint": "^8.26.0"
"@babel/core": "^7.22.11",
"@babel/eslint-parser": "^7.22.11",
"@changesets/cli": "^2.26.2",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.6.4",
"prettier": "3.0.3",
"typescript": "~5.2.2",
"nx": "^16.7.4",
"@nrwl/nx-cloud": "^16.3.0"
},
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"build:all": "pnpm --recursive run build",
"lint:all": "pnpm --recursive run lint",
"test:all": "pnpm --recursive run test",
"clean:all": "pnpm --recursive run clean",
"release": "pnpm build:all && pnpm publish -r"
"build:all": "nx run-many --target=build --all --parallel=1",
"lint:all": "nx run-many --target=lint --all --parallel=1",
"test:all": "nx run-many --target=test --all --parallel=1",
"clean:all": "nx run-many --target=clean --all --parallel=1",
"release": "pnpm build:all && changeset publish"
},
"packageManager": "pnpm@9.15.6"
"workspaces": [
"extension",
"packages/*",
"packages/d3-state-visualizer/examples/tree",
"packages/react-dock/demo",
"packages/react-json-tree/examples",
"packages/redux-devtools/examples/counter",
"packages/redux-devtools/examples/todomvc",
"packages/redux-devtools-inspector-monitor/demo",
"packages/redux-devtools-inspector-monitor-test-tab/demo",
"packages/redux-devtools-rtk-query-monitor/demo",
"packages/redux-devtools-slider-monitor/examples/todomvc"
],
"packageManager": "pnpm@8.7.1"
}

View File

@ -0,0 +1,2 @@
examples
lib

View File

@ -0,0 +1,13 @@
module.exports = {
extends: '../../eslintrc.js.base.json',
overrides: [
{
files: ['*.ts'],
extends: '../../eslintrc.ts.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: true,
},
},
],
};

View File

@ -1,17 +1,5 @@
# Change Log
## 3.0.0
### Major Changes
- 191d419: Convert d3 packages to ESM
### Patch Changes
- Updated dependencies [191d419]
- d3tooltip@4.0.0
- map2tree@4.0.0
## 2.0.0
### Major Changes

View File

@ -0,0 +1,7 @@
{
"presets": [
["@babel/preset-env", { "targets": "defaults", "modules": false }],
"@babel/preset-typescript"
],
"plugins": ["@babel/plugin-transform-runtime"]
}

View File

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

View File

@ -1,10 +0,0 @@
import eslintJs from '../../eslint.js.config.base.mjs';
import eslintTs from '../../eslint.ts.config.base.mjs';
export default [
...eslintJs,
...eslintTs(import.meta.dirname),
{
ignores: ['examples', 'lib'],
},
];

View File

@ -0,0 +1 @@
dist

View File

@ -0,0 +1,17 @@
module.exports = {
extends: '../../../../eslintrc.ts.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: true,
},
overrides: [
{
files: ['webpack.config.ts'],
extends: '../../../../eslintrc.ts.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.webpack.json'],
},
},
],
};

View File

@ -1,13 +1,5 @@
# d3-state-visualizer-tree-example
## 0.1.6
### Patch Changes
- Updated dependencies [191d419]
- d3-state-visualizer@3.0.0
- map2tree@4.0.0
## 0.1.5
### Patch Changes

View File

@ -1,15 +0,0 @@
import eslintJs from '../../../../eslint.js.config.base.mjs';
import eslintTs from '../../../../eslint.ts.config.base.mjs';
export default [
...eslintJs,
...eslintTs(import.meta.dirname),
...eslintTs(
import.meta.dirname,
['webpack.config.ts'],
['./tsconfig.webpack.json'],
),
{
ignores: ['dist'],
},
];

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "d3-state-visualizer-tree-example",
"version": "0.1.6",
"version": "0.1.5",
"description": "Visualize your app state as a tree",
"keywords": [
"d3",
@ -21,26 +21,30 @@
"scripts": {
"start": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack",
"lint": "eslint .",
"lint": "eslint . --ext .ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"d3-state-visualizer": "workspace:^",
"map2tree": "workspace:^"
"d3-state-visualizer": "^2.0.0",
"map2tree": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.26.0",
"@types/node": "^22.13.9",
"babel-loader": "^10.0.0",
"@babel/core": "^7.22.11",
"@babel/preset-env": "^7.22.14",
"@babel/preset-typescript": "^7.22.11",
"@types/node": "^18.17.14",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"babel-loader": "^9.1.3",
"cross-env": "^7.0.3",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"html-webpack-plugin": "^5.6.3",
"ts-node": "^10.9.2",
"typescript": "~5.8.2",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"fork-ts-checker-webpack-plugin": "^8.0.0",
"html-webpack-plugin": "^5.5.3",
"ts-node": "^10.9.1",
"typescript": "~5.2.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "d3-state-visualizer",
"version": "3.0.0",
"version": "2.0.0",
"description": "Visualize your app state with a range of reusable charts",
"keywords": [
"d3",
@ -20,33 +20,46 @@
"lib",
"src"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"type": "module",
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
"types": "lib/types/index.d.ts",
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"scripts": {
"build": "tsc",
"build": "pnpm run build:cjs && pnpm run build:esm && pnpm run build:types",
"build:cjs": "babel src --extensions \".ts\" --out-dir lib/cjs",
"build:esm": "babel src --config-file ./babel.config.esm.json --extensions \".ts\" --out-dir lib/esm",
"build:types": "tsc --emitDeclarationOnly",
"clean": "rimraf lib",
"lint": "eslint .",
"lint": "eslint . --ext .ts",
"type-check": "tsc --noEmit",
"prepack": "pnpm run clean && pnpm run build",
"prepublish": "pnpm run lint"
"prepublish": "pnpm run type-check && pnpm run lint"
},
"dependencies": {
"@types/d3": "^7.4.3",
"d3": "^7.9.0",
"d3tooltip": "workspace:^",
"@babel/runtime": "^7.22.11",
"@types/d3": "^7.4.0",
"d3": "^7.8.5",
"d3tooltip": "^3.0.0",
"deepmerge": "^4.3.1",
"map2tree": "workspace:^",
"ramda": "^0.30.1"
"map2tree": "^3.0.0",
"ramda": "^0.29.0"
},
"devDependencies": {
"@types/ramda": "^0.30.2",
"rimraf": "^6.0.1",
"typescript": "~5.8.2"
"@babel/cli": "^7.22.10",
"@babel/core": "^7.22.11",
"@babel/eslint-parser": "^7.22.11",
"@babel/preset-env": "^7.22.14",
"@babel/preset-typescript": "^7.22.11",
"@types/ramda": "^0.29.3",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"rimraf": "^5.0.1",
"typescript": "~5.2.2"
}
}

View File

@ -1,4 +1,4 @@
export type { HierarchyPointNode } from 'd3';
export type { StyleValue } from 'd3tooltip';
export { default as tree } from './tree/tree.js';
export type { Node, Options } from './tree/tree.js';
export { default as tree } from './tree/tree';
export type { Node, Options } from './tree/tree';

View File

@ -9,14 +9,14 @@ import {
toggleChildren,
visit,
getNodeGroupByDepthCount,
} from './utils.js';
} from './utils';
import { tooltip } from 'd3tooltip';
import type { StyleValue } from 'd3tooltip';
export interface Options {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
// eslint-disable-next-line @typescript-eslint/ban-types
state?: {} | null;
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
// eslint-disable-next-line @typescript-eslint/ban-types
tree?: Node | {};
rootKeyName: string;
@ -473,10 +473,9 @@ export default function (DOMNode: HTMLElement, options: Partial<Options> = {}) {
// update the links
const link = vis
.selectAll<
SVGPathElement,
HierarchyPointLink<InternalNode>
>('path.link')
.selectAll<SVGPathElement, HierarchyPointLink<InternalNode>>(
'path.link',
)
.data(links, (d) => d.target.data.id);
// enter any new links at the parent's previous position

View File

@ -1,6 +1,6 @@
import { is, join, pipe, replace } from 'ramda';
import sortAndSerialize from './sortAndSerialize.js';
import type { InternalNode } from './tree.js';
import sortAndSerialize from './sortAndSerialize';
import type { InternalNode } from './tree';
export function collapseChildren(node: InternalNode) {
if (node.children) {

View File

@ -1,7 +1,2 @@
export { tree } from './charts/index.js';
export type {
HierarchyPointNode,
Node,
Options,
StyleValue,
} from './charts/index.js';
export { tree } from './charts';
export type { HierarchyPointNode, Node, Options, StyleValue } from './charts';

View File

@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.esm.base.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib"
"outDir": "lib/types"
},
"include": ["src"]
}

View File

@ -0,0 +1 @@
lib

View File

@ -0,0 +1,13 @@
module.exports = {
extends: '../../eslintrc.js.base.json',
overrides: [
{
files: ['*.ts'],
extends: '../../eslintrc.ts.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: true,
},
},
],
};

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