├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── settings.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── __e2e__ ├── app.spec.js ├── buildTestBundle.js ├── fixture │ ├── apollo.js │ ├── app.js │ ├── mobx.js │ ├── redux.js │ ├── remotedev.js │ ├── setup.js │ └── xhr-test.js └── mockRNServer.js ├── app ├── actions │ ├── debugger.js │ └── setting.js ├── components │ ├── Draggable.js │ └── FormInput.js ├── containers │ ├── App.js │ ├── ReactInspector.js │ └── redux │ │ ├── DevTools.js │ │ ├── Header.js │ │ └── Settings.js ├── globalStyles.js ├── index.js ├── middlewares │ ├── debuggerAPI.js │ └── reduxAPI.js ├── reducers │ ├── debugger.js │ ├── index.js │ └── setting.js ├── setup.js ├── store │ └── configureStore.js ├── utils │ ├── adb.js │ ├── config.js │ ├── devMenu.js │ └── devtools.js └── worker │ ├── .eslintrc │ ├── apollo.js │ ├── asyncStorage.js │ ├── devMenu.js │ ├── index.js │ ├── networkInspect.js │ ├── polyfills │ └── fetch.js │ ├── reactDevTools.js │ ├── reduxAPI.js │ ├── remotedev.js │ ├── setup.js │ └── utils.js ├── auto_update.json ├── auto_updater.json ├── babel.config.js ├── dist ├── app.html ├── css │ └── style.css ├── devtools-helper │ ├── main.html │ ├── main.js │ └── manifest.json ├── logo.png ├── package.json ├── patches │ └── apollo-client-devtools+4.1.4.patch └── yarn.lock ├── docs ├── README.md ├── apollo-client-devtools-integration.md ├── config-file-in-home-directory.md ├── contributing.md ├── debugger-integration.md ├── enable-open-in-editor-in-console.md ├── getting-started.md ├── network-inspect-of-chrome-devtools.md ├── react-devtools-integration.md ├── redux-devtools-integration.md ├── shortcut-references.md └── troubleshooting.md ├── electron ├── app.html ├── config │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ └── index.test.js │ ├── index.js │ └── template.js ├── context-menu.js ├── debug.js ├── devtools.js ├── extensions.js ├── logo.icns ├── logo.ico ├── logo.png ├── main.js ├── menu │ ├── common.js │ ├── darwin.js │ ├── dialog.js │ ├── index.js │ └── linux+win.js ├── sync-state.js ├── update.js ├── url-handle │ ├── handleURL.js │ ├── index.js │ └── port.js └── window.js ├── examples ├── .eslintrc └── test-old-bridge │ ├── .gitignore │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png │ ├── babel.config.js │ ├── examples │ ├── apollo │ │ ├── App.js │ │ └── SimpleQuery.js │ └── redux │ │ ├── App.js │ │ ├── app │ │ └── store.js │ │ └── features │ │ └── counter │ │ ├── Counter.js │ │ ├── counterAPI.js │ │ └── counterSlice.js │ ├── package-lock.json │ └── package.json ├── npm-package ├── .eslintrc ├── .gitignore ├── README.md ├── babel.config.js ├── bin │ └── rndebugger-open.js ├── lib │ └── injectDevToolsMiddleware.tmpl.js ├── package.json ├── src │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── injectDevToolsMiddleware.test.js.snap │ │ └── injectDevToolsMiddleware.test.js │ ├── injectDevToolsMiddleware.js │ ├── main.js │ └── open.js └── yarn.lock ├── package.json ├── patches ├── @redux-devtools+inspector-monitor-trace-tab+1.0.0.patch ├── @redux-devtools+ui+1.3.0.patch ├── apollo-client-devtools+4.1.4.patch ├── electron-gh-releases+2.0.4.patch └── react-dev-utils+4.2.3.patch ├── scripts ├── config.json ├── mac │ ├── createDMG.js │ ├── createUniversalApp.js │ ├── dmg-background.png │ ├── dmg-background@2x.png │ └── entitlements.plist ├── package-linux.sh ├── package-macos.sh ├── package-windows.sh ├── patch-modules.js └── postinstall.js ├── webpack ├── .eslintrc ├── base.js ├── main.prod.js ├── renderer.dev.js └── renderer.prod.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | npm-package/lib/ 2 | *.tmpl.js 3 | *.bundle.js 4 | dist/ 5 | release/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": ["airbnb", "prettier"], 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "settings": { 10 | "import/core-modules": ["electron"] 11 | }, 12 | "rules": { 13 | "linebreak-style": 0, 14 | "react/prefer-stateless-function": 0, 15 | "consistent-return": 0, 16 | "strict": 0, 17 | "no-console": 0, 18 | "no-param-reassign": ["error", { "props": false }], 19 | "no-underscore-dangle": [ 20 | "error", 21 | { 22 | "allow": [ 23 | "__IS_REDUX_NATIVE_MESSAGE__", 24 | "__AVAILABLE_METHODS_CAN_CALL_BY_RNDEBUGGER__", 25 | "__PLATFORM__", 26 | "__REPORT_REACT_DEVTOOLS_PORT__", 27 | "__FETCH_SUPPORT__", 28 | "__NETWORK_INSPECT__", 29 | "__FROM_DEBUGGER_WORKER__" 30 | ] 31 | } 32 | ], 33 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 34 | "import/prefer-default-export": 0, 35 | "import/no-extraneous-dependencies": ["error", { "optionalDependencies": true }], 36 | "semi": ["error", "never"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jhen0409 2 | open_collective: react-native-debugger 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | React Native Debugger app version: [FILL THIS OUT] 17 | React Native version: [FILL THIS OUT] 18 | Platform: [FILL THIS OUT: iOS, Android, ...] 19 | Is real device of platform: [Yes or No] 20 | Operating System: [FILL THIS OUT: macOS, Linux, Windows] 21 | 22 | 24 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: react-native-debugger 3 | has_issues: true 4 | has_wiki: true 5 | has_downloads: true 6 | default_branch: master 7 | allow_squash_merge: true 8 | allow_merge_commit: true 9 | allow_rebase_merge: true 10 | 11 | labels: 12 | - name: bug 13 | color: CC0000 14 | - name: help wanted 15 | color: 33aa3f 16 | - name: duplicate 17 | color: cfd3d7 18 | - name: enhancement 19 | color: a2eeef 20 | - name: question 21 | color: d876e3 22 | - name: invalid 23 | color: e4e669 24 | - name: wontfix 25 | color: ffffff 26 | - name: feature 27 | color: 336699 28 | - name: trivial 29 | oldname: good first issue 30 | color: afffb2 31 | - name: has PR 32 | color: 3100ad 33 | - name: documentation 34 | color: a1ed95 35 | - name: RFC 36 | color: 3100ad 37 | - name: WIP 38 | color: e4e669 39 | - name: don't merge 40 | color: CC0000 41 | - name: stale 42 | color: ffffff 43 | - name: cannot-reproduce 44 | color: bfdadc 45 | - name: more-information-needed 46 | color: c2e0c6 47 | - name: upstream 48 | color: 5319e7 49 | - name: waiting-for-response 50 | color: 3100ad 51 | 52 | # Integrations 53 | - name: integration/react-devtools 54 | color: 1b307c 55 | - name: integration/redux-devtools 56 | color: 7c2d9e 57 | - name: integration/apollo-client-devtools 58 | color: 7066e8 59 | 60 | # Features 61 | - name: feature/network-inspect 62 | color: fce885 63 | 64 | # Packages 65 | - name: package/react-native-debugger-open 66 | color: f7e78f 67 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build-test-linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3.7.0 15 | with: 16 | node-version: 18.x 17 | cache: 'yarn' 18 | - name: Setup 19 | run: sudo apt-get install -y libgbm-dev 20 | - name: Test 21 | id: test 22 | run: | 23 | yarn 24 | cd npm-package && yarn && cd .. 25 | yarn test 26 | yarn build 27 | xvfb-run --auto-servernum yarn test-e2e 28 | - name: Upload artifacts on failure 29 | if: ${{ failure() && steps.test.conclusion == 'failure' }} 30 | uses: actions/upload-artifact@v3 31 | with: 32 | name: artifacts 33 | path: artifacts 34 | retention-days: 1 35 | build-test-macos: 36 | runs-on: macOS-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-node@v3.7.0 40 | with: 41 | node-version: 18.x 42 | cache: 'yarn' 43 | - name: Test 44 | id: test 45 | run: | 46 | yarn 47 | cd npm-package && yarn && cd .. 48 | yarn test 49 | yarn build 50 | yarn test-e2e 51 | - name: Upload artifacts on failure 52 | if: ${{ failure() && steps.test.conclusion == 'failure' }} 53 | uses: actions/upload-artifact@v3 54 | with: 55 | name: artifacts 56 | path: artifacts 57 | retention-days: 1 58 | build-test-windows: 59 | runs-on: windows-2022 60 | steps: 61 | - uses: actions/checkout@v3 62 | - uses: actions/setup-node@v3.7.0 63 | with: 64 | node-version: 18.x 65 | cache: 'yarn' 66 | - name: Test 67 | id: test 68 | shell: bash 69 | run: | 70 | yarn config set network-timeout 500000 -g 71 | yarn 72 | cd npm-package && yarn && cd .. 73 | yarn build 74 | yarn test 75 | yarn test-e2e 76 | - name: Upload artifacts on failure 77 | if: ${{ failure() && steps.test.conclusion == 'failure' }} 78 | uses: actions/upload-artifact@v3 79 | with: 80 | name: artifacts 81 | path: artifacts 82 | retention-days: 1 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release-linux: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3.7.0 13 | with: 14 | node-version: 18.x 15 | cache: 'yarn' 16 | - name: Package 17 | run: | 18 | yarn 19 | yarn build 20 | yarn pack-linux 21 | - name: Upload assets to release 22 | uses: jhen0409/release-asset-action@master 23 | with: 24 | file: release/rn-debugger-linux-x64.zip 25 | pattern: release/react-native-debugger* 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | release-windows: 28 | runs-on: windows-2022 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-node@v3.7.0 32 | with: 33 | node-version: 18.x 34 | cache: 'yarn' 35 | - name: Package 36 | shell: bash 37 | run: | 38 | yarn config set network-timeout 500000 -g 39 | yarn 40 | yarn build 41 | yarn pack-windows 42 | - name: Upload assets to release 43 | uses: jhen0409/release-asset-action@master 44 | with: 45 | file: release/rn-debugger-windows-x64.zip 46 | pattern: release/react_native_debugger*.exe 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn-error.log 4 | .DS_Store 5 | dist/js 6 | dist/main.js* 7 | release/ 8 | *.bundle.js 9 | tmp/ 10 | config_test 11 | .idea/ 12 | artifacts/ 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Jhen-Jie Hong 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Debugger 2 | 3 | [![Backers on Open Collective](https://opencollective.com/react-native-debugger/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/react-native-debugger/sponsors/badge.svg)](#sponsors) [![CI Status](https://github.com/jhen0409/react-native-debugger/workflows/CI/badge.svg)](https://github.com/jhen0409/react-native-debugger) 4 | 5 | ⚠️ This app is currently only supported old [Remote Debugger](https://reactnative.dev/docs/debugging#chrome-developer-tools), if you're looking for new debugger support (e.g. Hermes / JSI / New Architecture) of React Native Debugger, please follow [discussion#774](https://github.com/jhen0409/react-native-debugger/discussions/774). 6 | 7 | ![React Native Debugger](https://user-images.githubusercontent.com/3001525/29451479-6621bf1a-83c8-11e7-8ebb-b4e98b1af91c.png) 8 | 9 | > Run the redux example of [react-navigation](https://github.com/react-navigation/react-navigation/tree/master/example) with Redux DevTools setup 10 | 11 | This is a standalone app for debugging React Native apps: 12 | 13 | - Based on official [Remote Debugger](https://reactnative.dev/docs/debugging#chrome-developer-tools) and provide more functionality. 14 | - Includes [React Inspector](docs/react-devtools-integration.md) from [`react-devtools-core`](https://github.com/facebook/react/tree/master/packages/react-devtools-core). 15 | - Includes Redux DevTools, made [the same API](docs/redux-devtools-integration.md) with [`redux-devtools-extension`](https://github.com/reduxjs/redux-devtools/tree/main/extension). 16 | - Includes [Apollo Client DevTools](docs/apollo-client-devtools-integration.md) ([`apollographql/apollo-client-devtools`](https://github.com/apollographql/apollo-client-devtools)) as devtools extension. 17 | 18 | ## Installation 19 | 20 | To install the app, you can download a prebuilt binary from the [release page](https://github.com/jhen0409/react-native-debugger/releases). 21 | 22 | For **macOS**, you can use [Homebrew Cask](https://caskroom.github.io) to install: 23 | 24 | ### < Homebrew 2.6.0 25 | 26 | ```bash 27 | brew update && brew install --cask react-native-debugger 28 | ``` 29 | 30 | ### >= Homebrew 2.6.0 31 | 32 | ```bash 33 | brew install --cask react-native-debugger 34 | ``` 35 | 36 | This puts `React Native Debugger.app` in your `/applications/` folder. 37 | 38 | ### NOTICE: React Native Compatibility 39 | 40 | To use this app you need to ensure you are using the correct version of React Native Debugger and react-native: 41 | 42 | | React Native Debugger | react-native | 43 | | --------------------- | ------------ | 44 | | >= 0.11 | >= 0.62 | 45 | | <= 0.10 | <= 0.61 | 46 | 47 | We used different auto-update feed for `v0.10` and `v0.11`, so you won't see update tips of `v0.11` from `v0.10`. 48 | 49 | Install last release of v0.10 (0.10.7) 50 | 51 | ### < Homebrew 2.6.0 52 | 53 | `brew update && brew cask install https://raw.githubusercontent.com/Homebrew/homebrew-cask/b6ac3795c1df9f97242481c0817b1165e3e6306a/Casks/react-native-debugger.rb` 54 | 55 | ### >= Homebrew 2.6.0 56 | 57 | `brew install --cask https://raw.githubusercontent.com/Homebrew/homebrew-cask/b6ac3795c1df9f97242481c0817b1165e3e6306a/Casks/react-native-debugger.rb` 58 | 59 | ### Arch-based distributions 60 | 61 | You can install [react-native-debugger-bin][1] from Arch User Repository: 62 | 63 | ```shell 64 | git clone https://aur.archlinux.org/react-native-debugger-bin.git 65 | cd react-native-debugger-bin 66 | makepkg -si 67 | 68 | # or using AUR helper 69 | paru -S react-native-debugger-bin 70 | ``` 71 | 72 | ## Build from source 73 | 74 | Please read [Development section](docs/contributing.md#development) in docs/contributing.md for how to build the app from source. 75 | 76 | ## Documentation 77 | 78 | - [Getting Started](docs/getting-started.md) 79 | - [Debugger Integration](docs/debugger-integration.md) 80 | - [React DevTools Integration](docs/react-devtools-integration.md) 81 | - [Redux DevTools Integration](docs/redux-devtools-integration.md) 82 | - [Apollo Client DevTools Integration](docs/apollo-client-devtools-integration.md) 83 | - [Shortcut references](docs/shortcut-references.md) 84 | - [Network inspect of Chrome Developer Tools](docs/network-inspect-of-chrome-devtools.md) 85 | - [Enable open in editor in console](docs/enable-open-in-editor-in-console.md) 86 | - [Config file in home directory](docs/config-file-in-home-directory.md) 87 | - [Troubleshooting](docs/troubleshooting.md) 88 | - [Contributing](docs/contributing.md) 89 | 90 | ## Documentation (v0.10) 91 | 92 | Please visit [`v0.10 branch`](https://github.com/jhen0409/react-native-debugger/tree/v0.10). 93 | 94 | ## Credits 95 | 96 | - Great work of [React DevTools](https://github.com/facebook/react/tree/master/packages/react-devtools) 97 | - Great work of [Redux DevTools](https://github.com/gaearon/redux-devtools) / [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) and all third-party monitors. 98 | - Great work of [Apollo Client DevTools](https://github.com/apollographql/apollo-client-devtools)). (Special thanks to [@Gongreg](https://github.com/Gongreg) for integrating this!) 99 | 100 | ## Backers 101 | 102 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/react-native-debugger#backer)] 103 | 104 | 105 | 106 | ## Sponsors 107 | 108 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/react-native-debugger#sponsor)] 109 | 110 | 111 | 112 | ## LICENSE 113 | 114 | [MIT](LICENSE.md) 115 | 116 | [1]: https://aur.archlinux.org/packages/react-native-debugger-bin 117 | -------------------------------------------------------------------------------- /__e2e__/buildTestBundle.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | 4 | const outputPath = '__e2e__/fixture' 5 | const filename = 'app.bundle.js' 6 | 7 | export const bundlePath = path.join(outputPath, filename) 8 | 9 | // Build a bundle for simulate RNDebugger worker run react-native bundle, 10 | // it included redux, mobx, remotedev tests 11 | export default function buildTestBundle() { 12 | return new Promise((resolve) => { 13 | webpack({ 14 | mode: 'development', 15 | entry: './__e2e__/fixture/app', 16 | output: { 17 | path: path.resolve(__dirname, '..', outputPath), 18 | filename, 19 | }, 20 | resolve: { 21 | mainFields: ['react-native', 'main', 'browser'], 22 | }, 23 | }).run(resolve) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /__e2e__/fixture/apollo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* 3 | * Create an Apollo Client to test the bridge messages sent 4 | * wouldn't break the debugger proxy. 5 | */ 6 | 7 | import { ApolloClient, InMemoryCache } from '@apollo/client' 8 | import gql from 'graphql-tag' 9 | 10 | const client = new ApolloClient({ 11 | uri: 'https://spacex-production.up.railway.app/', 12 | cache: new InMemoryCache(), 13 | }) 14 | 15 | export default async function run() { 16 | return client.query({ 17 | query: gql` 18 | query ExampleQuery { 19 | company { 20 | name 21 | ceo 22 | employees 23 | } 24 | } 25 | `, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /__e2e__/fixture/app.js: -------------------------------------------------------------------------------- 1 | import './setup' 2 | 3 | import runXHRTest from './xhr-test' // Install fetch polyfill before initial apollo-client 4 | import runApolloTest from './apollo' 5 | import runReduxTest from './redux' 6 | import runMobXTest from './mobx' 7 | import runRemoteDevTest from './remotedev' 8 | 9 | runReduxTest() 10 | runMobXTest() 11 | runRemoteDevTest() 12 | runApolloTest().catch((e) => console.error(e)) 13 | runXHRTest().catch((e) => console.error(e)) 14 | -------------------------------------------------------------------------------- /__e2e__/fixture/mobx.js: -------------------------------------------------------------------------------- 1 | /* eslint prefer-arrow-callback: 0 */ 2 | import { observable, action, useStrict } from 'mobx' 3 | import remotedev from 'mobx-remotedev/lib/dev' 4 | 5 | useStrict(true) 6 | 7 | export default function run() { 8 | const store = observable({ value: 0 }) 9 | store.testPassForMobXStore1 = action(function testPassForMobXStore1() {}) 10 | 11 | remotedev(store, { name: 'MobX store instance 1' }).testPassForMobXStore1() 12 | 13 | const store2 = observable({ value: 1 }) 14 | store2.testPassForMobXStore2 = action(function testPassForMobXStore2() {}) 15 | 16 | remotedev(store2, { name: 'MobX store instance 2' }).testPassForMobXStore2() 17 | } 18 | -------------------------------------------------------------------------------- /__e2e__/fixture/redux.js: -------------------------------------------------------------------------------- 1 | /* eslint no-underscore-dangle: 0 */ 2 | 3 | import { createStore } from 'redux' 4 | 5 | export default function run() { 6 | // Enhancer 7 | const store1 = createStore( 8 | (state) => state, 9 | { value: 0 }, 10 | window.__REDUX_DEVTOOLS_EXTENSION__({ 11 | name: 'Redux store instance 1', 12 | actionsWhitelist: ['@@INIT', 'TEST_PASS_FOR_REDUX_STORE_1', '^SHOW_FOR_REDUX_STORE_1$'], 13 | }), 14 | ) 15 | 16 | // Compose enhancers 17 | const store2 = createStore( 18 | (state) => state, 19 | { value: 1 }, 20 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 21 | name: 'Redux store instance 2', 22 | actionsBlacklist: ['NOT_SHOW_1_FOR_REDUX_STORE_2', 'NOT_SHOW_2_FOR_REDUX_STORE_2'], 23 | predicate: (state, action) => action.type !== 'NOT_SHOW_3_FOR_REDUX_STORE_2', 24 | })(/* No enhancers */), 25 | ) 26 | 27 | store1.dispatch({ type: 'TEST_PASS_FOR_REDUX_STORE_1' }) 28 | store1.dispatch({ type: 'SHOW_FOR_REDUX_STORE_1' }) 29 | store1.dispatch({ type: 'NOT_SHOW_FOR_REDUX_STORE_1' }) 30 | 31 | store2.dispatch({ type: 'TEST_PASS_FOR_REDUX_STORE_2' }) 32 | store2.dispatch({ type: 'NOT_SHOW_1_FOR_REDUX_STORE_2' }) 33 | store2.dispatch({ type: 'NOT_SHOW_2_FOR_REDUX_STORE_2' }) 34 | store2.dispatch({ type: 'NOT_SHOW_3_FOR_REDUX_STORE_2' }) 35 | } 36 | -------------------------------------------------------------------------------- /__e2e__/fixture/remotedev.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | 3 | const connectViaExtension = window.devToolsExtension.connect 4 | 5 | const logReducer = (reducer) => { 6 | const remotedev = connectViaExtension({ 7 | name: 'RemoteDev store instance 1', 8 | actionCreators: { 9 | test: () => {}, 10 | }, 11 | }) 12 | return (state, action) => { 13 | const reducedState = reducer(state, action) 14 | remotedev.send(action, reducedState) 15 | return reducedState 16 | } 17 | } 18 | 19 | const logRemotely = (next) => (reducer, initialState) => next(logReducer(reducer), initialState) 20 | 21 | export default function run() { 22 | const store = logRemotely(createStore)((state) => state, { value: 0 }) 23 | 24 | store.dispatch({ type: 'TEST_PASS_FOR_REMOTEDEV_STORE_1' }) 25 | } 26 | -------------------------------------------------------------------------------- /__e2e__/fixture/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | /* eslint no-underscore-dangle: 0 */ 3 | 4 | self.window = global 5 | 6 | // Remove native fetch as react-native use whatwg-fetch polyfill 7 | self.fetch = undefined 8 | 9 | const MessageQueue = function MessageQueue() {} 10 | MessageQueue.spy = () => {} 11 | MessageQueue.prototype.__spy = null 12 | 13 | const requiredModules = { 14 | NativeModules: {}, 15 | Platform: {}, 16 | setupDevtools: undefined, 17 | AsyncStorage: {}, 18 | MessageQueue, 19 | } 20 | // Simulate React Native's window.require polyfill 21 | window.require = (moduleName) => { 22 | if (typeof moduleName !== 'number') { 23 | // From https://github.com/facebook/react-native/blob/5403946f098cc72c3d33ea5cee263fb3dd03891d/packager/src/Resolver/polyfills/require.js#L97 24 | console.warn( 25 | `Requiring module '${moduleName}' by name is only supported for ` 26 | + 'debugging purposes and will BREAK IN PRODUCTION!', 27 | ) 28 | } 29 | return requiredModules[moduleName] 30 | } 31 | window.__DEV__ = true 32 | window.__fbBatchedBridge = new MessageQueue() 33 | -------------------------------------------------------------------------------- /__e2e__/fixture/xhr-test.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch' 2 | 3 | export default async function run() { 4 | // Fetch with forbidden header names 5 | await fetch('http://localhost:8099', { 6 | headers: { 7 | Origin: 'custom_origin_here', 8 | 'User-Agent': 'react-native', 9 | }, 10 | }) 11 | 12 | // It will log warning 13 | // because we can't use worker's FormData for upload file 14 | const data = { uri: 'uri' } 15 | const form = new FormData() 16 | form.append('file', data) 17 | } 18 | -------------------------------------------------------------------------------- /__e2e__/mockRNServer.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import WebSocket from 'ws' 3 | 4 | export default function createMockRNServer(port = 8081) { 5 | const server = http.createServer((req, res) => { 6 | if (req.method === 'GET' && req.url === '/debugger-ui') { 7 | res.writeHead(200, { 'Content-Type': 'text/html' }) 8 | res.end('') 9 | } 10 | }) 11 | const wss = new WebSocket.Server({ server }) 12 | server.listen(port) 13 | return { server, wss } 14 | } 15 | -------------------------------------------------------------------------------- /app/actions/debugger.js: -------------------------------------------------------------------------------- 1 | export const SET_DEBUGGER_LOCATION = 'SET_DEBUGGER_LOCATION' 2 | export const SET_DEBUGGER_STATUS = 'SET_DEBUGGER_STATUS' 3 | export const SET_DEBUGGER_WORKER = 'SET_DEBUGGER_WORKER' 4 | export const SYNC_STATE = 'SYNC_STATE' 5 | export const BEFORE_WINDOW_CLOSE = 'BEFORE_WINDOW_CLOSE' 6 | 7 | export const setDebuggerLocation = (loc) => ({ 8 | type: SET_DEBUGGER_LOCATION, 9 | loc, 10 | }) 11 | 12 | export const setDebuggerStatus = (status) => ({ 13 | type: SET_DEBUGGER_STATUS, 14 | status, 15 | }) 16 | 17 | export const setDebuggerWorker = (worker, status) => ({ 18 | type: SET_DEBUGGER_WORKER, 19 | worker, 20 | status, 21 | }) 22 | 23 | export const syncState = (payload) => ({ 24 | type: SYNC_STATE, 25 | payload, 26 | }) 27 | 28 | export const beforeWindowClose = () => ({ 29 | type: BEFORE_WINDOW_CLOSE, 30 | }) 31 | -------------------------------------------------------------------------------- /app/actions/setting.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_DEVTOOLS = 'TOGGLE_DEVTOOLS' 2 | export const RESIZE_DEVTOOLS = 'RESIZE_DEVTOOLS' 3 | export const CHANGE_DEFAULT_THEME = 'CHANGE_DEFAULT_THEME' 4 | 5 | export const toggleDevTools = (name) => ({ 6 | type: TOGGLE_DEVTOOLS, 7 | name, 8 | }) 9 | 10 | export const resizeDevTools = (size) => ({ 11 | type: RESIZE_DEVTOOLS, 12 | size, 13 | }) 14 | 15 | export const changeDefaultTheme = (themeName) => ({ 16 | type: CHANGE_DEFAULT_THEME, 17 | themeName, 18 | }) 19 | -------------------------------------------------------------------------------- /app/components/Draggable.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const styles = { 5 | draggable: { 6 | position: 'relative', 7 | zIndex: 1, 8 | cursor: 'ns-resize', 9 | padding: '3px 0', 10 | margin: '-3px 0', 11 | width: '100%', 12 | }, 13 | } 14 | 15 | export default class Draggable extends PureComponent { 16 | onMove = (evt) => { 17 | evt.preventDefault() 18 | const { onMove } = this.props 19 | onMove?.(evt.pageX, evt.pageY) 20 | } 21 | 22 | onUp = (evt) => { 23 | evt.preventDefault() 24 | document.removeEventListener('mousemove', this.onMove) 25 | document.removeEventListener('mouseup', this.onUp) 26 | const { onStop } = this.props 27 | onStop?.() 28 | } 29 | 30 | startDragging = (evt) => { 31 | evt.preventDefault() 32 | document.addEventListener('mousemove', this.onMove) 33 | document.addEventListener('mouseup', this.onUp) 34 | const { onStart } = this.props 35 | onStart?.() 36 | } 37 | 38 | render() { 39 | return ( 40 |
47 | ) 48 | } 49 | } 50 | 51 | Draggable.propTypes = { 52 | onStart: PropTypes.func, 53 | onMove: PropTypes.func.isRequired, 54 | onStop: PropTypes.func, 55 | } 56 | 57 | Draggable.defaultProps = { 58 | onStart: undefined, 59 | onStop: undefined, 60 | } 61 | -------------------------------------------------------------------------------- /app/components/FormInput.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const styles = { 5 | title: { textAlign: 'center' }, 6 | form: { 7 | display: 'flex', 8 | flexDirection: 'row', 9 | justifyContent: 'center', 10 | alignItems: 'center', 11 | margin: 8, 12 | }, 13 | input: { 14 | appearance: 'none', 15 | fontSize: '16px', 16 | margin: 2, 17 | padding: '8px', 18 | border: 0, 19 | borderRadius: 2, 20 | }, 21 | button: { 22 | fontSize: '16px', 23 | margin: 2, 24 | padding: '8px', 25 | border: 0, 26 | borderRadius: 2, 27 | }, 28 | } 29 | 30 | export default class FormInput extends PureComponent { 31 | constructor(props) { 32 | super(props) 33 | this.state = { value: undefined } 34 | } 35 | 36 | handleOnChange = (evt) => { 37 | const { onInputChange } = this.props 38 | let { value } = evt.target 39 | if (onInputChange) { 40 | value = onInputChange(value) 41 | } 42 | this.setState({ value }) 43 | } 44 | 45 | handleClick = (evt) => { 46 | const { inputProps, onSubmit } = this.props 47 | const { value } = this.state 48 | onSubmit(evt, value || inputProps.value) 49 | } 50 | 51 | render() { 52 | const { title, inputProps, button } = this.props 53 | const { value } = this.state 54 | const val = typeof value !== 'undefined' ? value : inputProps.value 55 | return ( 56 |
57 |
{title}
58 |
59 | { 61 | // Enter/Return key pressed 62 | if (e.key === 'Enter') this.handleClick() 63 | }} 64 | type={inputProps.type} 65 | value={val} 66 | style={styles.input} 67 | onChange={this.handleOnChange} 68 | /> 69 | 72 |
73 |
74 | ) 75 | } 76 | } 77 | 78 | FormInput.propTypes = { 79 | title: PropTypes.string.isRequired, 80 | inputProps: PropTypes.shape({ 81 | type: PropTypes.string, 82 | value: PropTypes.string, 83 | }), 84 | button: PropTypes.string, 85 | onInputChange: PropTypes.func, 86 | onSubmit: PropTypes.func.isRequired, 87 | } 88 | 89 | FormInput.defaultProps = { 90 | inputProps: { type: 'input' }, 91 | button: 'Confirm', 92 | onInputChange: null, 93 | } 94 | -------------------------------------------------------------------------------- /app/containers/ReactInspector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | import { tryADBReverse } from '../utils/adb' 5 | 6 | let ReactServer 7 | const getReactInspector = () => { 8 | if (ReactServer) return ReactServer 9 | // eslint-disable-next-line 10 | ReactServer = ReactServer || require('react-devtools-core/standalone').default; 11 | 12 | return ReactServer 13 | } 14 | const containerId = 'react-devtools-container' 15 | 16 | const styles = { 17 | container: { 18 | display: 'flex', 19 | height: '100%', 20 | justifyContent: 'center', 21 | position: 'relative', 22 | }, 23 | waiting: { 24 | height: '100%', 25 | display: 'flex', 26 | WebkitUserSelect: 'none', 27 | textAlign: 'center', 28 | color: '#aaa', 29 | justifyContent: 'center', 30 | alignItems: 'center', 31 | position: 'absolute', 32 | top: 0, 33 | left: 0, 34 | right: 0, 35 | bottom: 0, 36 | }, 37 | } 38 | 39 | const isReactPanelOpen = (props) => props.settingState.react 40 | 41 | class ReactInspector extends Component { 42 | static setProjectRoots(projectRoots) { 43 | getReactInspector().setProjectRoots(projectRoots) 44 | } 45 | 46 | listeningPort = window.reactDevToolsPort 47 | 48 | componentDidMount() { 49 | const { debuggerState } = this.props 50 | const { worker } = debuggerState 51 | if (worker) { 52 | this.server = this.startServer() 53 | worker.addEventListener('message', this.workerOnMessage) 54 | } 55 | } 56 | 57 | UNSAFE_componentWillReceiveProps(nextProps) { 58 | const { debuggerState } = this.props 59 | const { worker } = debuggerState 60 | const { worker: nextWorker } = nextProps.debuggerState 61 | if (nextWorker && nextWorker !== worker) { 62 | this.closeServerIfExists() 63 | if (isReactPanelOpen(this.props)) { 64 | this.server = this.startServer() 65 | } 66 | nextWorker.addEventListener('message', this.workerOnMessage) 67 | } else if (!nextWorker) { 68 | this.closeServerIfExists() 69 | } 70 | // Open / Close server when react panel opened / hidden 71 | if (!worker && !nextWorker) return 72 | if (isReactPanelOpen(this.props) && !isReactPanelOpen(nextProps)) { 73 | this.closeServerIfExists() 74 | } else if (!isReactPanelOpen(this.props) && isReactPanelOpen(nextProps)) { 75 | this.closeServerIfExists() 76 | this.server = this.startServer() 77 | } 78 | } 79 | 80 | shouldComponentUpdate() { 81 | return false 82 | } 83 | 84 | componentWillUnmount() { 85 | this.closeServerIfExists() 86 | } 87 | 88 | workerOnMessage = (message) => { 89 | const { data } = message 90 | if (!data || !data.__REPORT_REACT_DEVTOOLS_PORT__) return 91 | 92 | const port = Number(data.__REPORT_REACT_DEVTOOLS_PORT__) 93 | const { platform } = data 94 | if (port && port !== this.listeningPort) { 95 | this.listeningPort = port 96 | this.closeServerIfExists() 97 | if (isReactPanelOpen(this.props)) { 98 | this.server = this.startServer(port) 99 | } 100 | if (platform === 'android') tryADBReverse(port).catch(() => {}) 101 | } 102 | } 103 | 104 | closeServerIfExists = () => { 105 | if (this.server) { 106 | this.server.close() 107 | this.server = null 108 | } 109 | } 110 | 111 | startServer(port = this.listeningPort) { 112 | let loggedWarn = false 113 | 114 | return getReactInspector() 115 | .setStatusListener((status) => { 116 | if (!loggedWarn && status === 'Failed to start the server.') { 117 | const message = port !== 8097 118 | ? 're-open the debugger window might be helpful.' 119 | : 'we recommended to upgrade React Native version to 0.39+ for random port support.' 120 | console.error( 121 | '[RNDebugger]', 122 | `Failed to start React DevTools server with port \`${port}\`,`, 123 | 'because another server is listening,', 124 | message, 125 | ) 126 | loggedWarn = true 127 | } 128 | }) 129 | .setContentDOMNode(document.getElementById(containerId)) 130 | .startServer(port) 131 | } 132 | 133 | render() { 134 | return ( 135 |
136 |
137 |

Waiting for React to connect…

138 |
139 |
140 | ) 141 | } 142 | } 143 | 144 | ReactInspector.propTypes = { 145 | debuggerState: PropTypes.shape({ 146 | worker: PropTypes.shape({ 147 | addEventListener: PropTypes.func.isRequired, 148 | }), 149 | }).isRequired, 150 | } 151 | 152 | export default connect( 153 | (state) => ({ 154 | settingState: state.setting, 155 | debuggerState: state.debugger, 156 | }), 157 | (dispatch) => ({ dispatch }), 158 | )(ReactInspector) 159 | -------------------------------------------------------------------------------- /app/containers/redux/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import styled from 'styled-components' 4 | import { Container, Notification } from '@redux-devtools/ui' 5 | import { clearNotification } from '@redux-devtools/app/lib/esm/actions' 6 | import Actions from '@redux-devtools/app/lib/esm/containers/Actions' 7 | import Settings from './Settings' 8 | import Header from './Header' 9 | 10 | const StyledContainer = styled(Container)`overflow: hidden;` 11 | 12 | function App() { 13 | const section = useSelector((state) => state.section) 14 | const theme = useSelector((state) => state.theme) 15 | const notification = useSelector((state) => state.notification) 16 | 17 | const dispatch = useDispatch() 18 | 19 | let body 20 | switch (section) { 21 | case 'Settings': 22 | body = 23 | break 24 | default: 25 | body = 26 | } 27 | 28 | return ( 29 | 30 |
31 | {body} 32 | {notification && ( 33 | dispatch(clearNotification())} 36 | > 37 | {notification.message} 38 | 39 | )} 40 | 41 | ) 42 | } 43 | 44 | export default App 45 | -------------------------------------------------------------------------------- /app/containers/redux/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useDispatch } from 'react-redux' 4 | import { 5 | Tabs, Toolbar, Button, Divider, 6 | } from '@redux-devtools/ui' 7 | import { GoBook } from 'react-icons/go' 8 | import styled from 'styled-components' 9 | import { changeSection } from '@redux-devtools/app/lib/esm/actions' 10 | import { shell } from 'electron' 11 | 12 | const WindowDraggable = styled.div` 13 | display: flex; 14 | flex: 1; 15 | height: 100%; 16 | -webkit-app-region: drag; 17 | -webkit-user-select: none; 18 | ` 19 | 20 | const tabs = [{ name: 'Actions' }, { name: 'Settings' }] 21 | 22 | function Header(props) { 23 | const { section } = props 24 | 25 | const dispatch = useDispatch() 26 | 27 | const handleChangeSection = useCallback( 28 | (sec) => dispatch(changeSection(sec)), 29 | [dispatch, changeSection], 30 | ) 31 | 32 | const openHelp = useCallback(() => shell.openExternal('https://goo.gl/SHU4yL'), []) 33 | 34 | return ( 35 | 36 | 44 | 45 | 46 | 53 | 54 | ) 55 | } 56 | 57 | Header.propTypes = { 58 | section: PropTypes.string.isRequired, 59 | } 60 | 61 | export default Header 62 | -------------------------------------------------------------------------------- /app/containers/redux/Settings.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-as-default */ 2 | import React, { Component } from 'react' 3 | import Tabs from '@redux-devtools/ui/lib/esm/Tabs/Tabs' 4 | import Themes from '@redux-devtools/app/lib/esm/components/Settings/Themes' 5 | 6 | export default class Settings extends Component { 7 | tabs = [ 8 | { name: 'Themes', component: Themes }, 9 | ] 10 | 11 | constructor(props) { 12 | super(props) 13 | this.state = { selected: 'Themes' } 14 | } 15 | 16 | handleSelect = (selected) => { 17 | this.setState({ selected }) 18 | } 19 | 20 | render() { 21 | const { selected } = this.state 22 | return ( 23 | 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { css, createGlobalStyle } from 'styled-components' 2 | 3 | const commonStyles = css`` 4 | 5 | export const GlobalStyle = 6 | process.platform !== 'darwin' 7 | ? createGlobalStyle` 8 | ${commonStyles} 9 | ::-webkit-scrollbar { 10 | width: 8px; 11 | height: 8px; 12 | background-color: #95959588; 13 | } 14 | ::-webkit-scrollbar-thumb { 15 | background-color: #33333366; 16 | } 17 | ` 18 | : createGlobalStyle`${commonStyles}` 19 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import { findAPortNotInUse } from 'portscanner' 2 | import { webFrame } from 'electron' 3 | import { getCurrentWindow } from '@electron/remote' 4 | import React from 'react' 5 | import { createRoot } from 'react-dom/client' 6 | import { Provider } from 'react-redux' 7 | import launchEditor from 'react-dev-utils/launchEditor' 8 | import { PersistGate } from 'redux-persist/integration/react' 9 | import './setup' 10 | import App from './containers/App' 11 | import configureStore from './store/configureStore' 12 | import { beforeWindowClose } from './actions/debugger' 13 | import { invokeDevMethod } from './utils/devMenu' 14 | import { client, tryADBReverse } from './utils/adb' 15 | import config from './utils/config' 16 | import { toggleOpenInEditor, isOpenInEditorEnabled } from './utils/devtools' 17 | import { GlobalStyle } from './globalStyles' 18 | 19 | const currentWindow = getCurrentWindow() 20 | 21 | webFrame.setZoomFactor(1) 22 | webFrame.setVisualZoomLevelLimits(1, 1) 23 | 24 | // Prevent dropped file 25 | document.addEventListener('drop', (e) => { 26 | e.preventDefault() 27 | e.stopPropagation() 28 | }) 29 | document.addEventListener('dragover', (e) => { 30 | e.preventDefault() 31 | e.stopPropagation() 32 | }) 33 | 34 | let store 35 | let persistor 36 | const handleReady = () => { 37 | const { defaultReactDevToolsPort = 19567 } = config 38 | findAPortNotInUse(Number(defaultReactDevToolsPort)).then((port) => { 39 | window.reactDevToolsPort = port 40 | const root = createRoot(document.getElementById('root')) 41 | root.render( 42 | <> 43 | 44 | 45 | 46 | 47 | 48 | 49 | , 50 | ) 51 | }) 52 | } 53 | 54 | ;({ store, persistor } = configureStore(handleReady)) 55 | 56 | // Provide for user 57 | window.adb = client 58 | window.adb.reverseAll = tryADBReverse 59 | window.adb.reversePackager = () => 60 | tryADBReverse(store.getState().debugger.location.port) 61 | 62 | window.checkWindowInfo = () => { 63 | const debuggerState = store.getState().debugger 64 | return { 65 | isWorkerRunning: !!debuggerState.worker, 66 | location: debuggerState.location, 67 | isPortSettingRequired: debuggerState.isPortSettingRequired, 68 | } 69 | } 70 | 71 | window.beforeWindowClose = () => 72 | new Promise((resolve) => { 73 | if (store.dispatch(beforeWindowClose())) { 74 | setTimeout(resolve, 200) 75 | } else { 76 | resolve() 77 | } 78 | }) 79 | 80 | // For security, we should disable nodeIntegration when user use this open a website 81 | const originWindowOpen = window.open 82 | window.open = (url, frameName, features = '') => { 83 | const featureList = features.split(',') 84 | featureList.push('nodeIntegration=0') 85 | return originWindowOpen.call(window, url, frameName, featureList.join(',')) 86 | } 87 | 88 | window.openInEditor = (file, lineNumber) => launchEditor(file, lineNumber) 89 | window.toggleOpenInEditor = () => { 90 | const { port } = store.getState().debugger.location 91 | return toggleOpenInEditor(currentWindow, port) 92 | } 93 | window.isOpenInEditorEnabled = () => isOpenInEditorEnabled(currentWindow) 94 | 95 | window.invokeDevMethod = (name) => invokeDevMethod(name)() 96 | 97 | // Package will missing /usr/local/bin, 98 | // we need fix it for ensure child process work 99 | // (like launchEditor of react-devtools) 100 | if ( 101 | process.env.NODE_ENV === 'production' && 102 | process.platform === 'darwin' && 103 | process.env.PATH.indexOf('/usr/local/bin') === -1 104 | ) { 105 | process.env.PATH = `${process.env.PATH}:/usr/local/bin` 106 | } 107 | -------------------------------------------------------------------------------- /app/middlewares/reduxAPI.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { ipcRenderer } from 'electron' 3 | import { getGlobal } from '@electron/remote' 4 | 5 | import { UPDATE_STATE, LIFTED_ACTION } from '@redux-devtools/app/lib/esm/constants/actionTypes' 6 | import { DISCONNECTED } from '@redux-devtools/app/lib/esm/constants/socketActionTypes' 7 | import { nonReduxDispatch } from '@redux-devtools/app/lib/esm/utils/monitorActions' 8 | import { showNotification, liftedDispatch } from '@redux-devtools/app/lib/esm/actions' 9 | import { getActiveInstance } from '@redux-devtools/app/lib/esm/reducers/instances' 10 | 11 | import { SET_DEBUGGER_WORKER, SYNC_STATE } from '../actions/debugger' 12 | import { setReduxDevToolsMethods, updateSliderContent } from '../utils/devMenu' 13 | 14 | const unboundActions = { 15 | showNotification, 16 | updateState: (request) => ({ 17 | type: UPDATE_STATE, 18 | request: request.data ? { ...request.data, id: request.id } : request, 19 | }), 20 | liftedDispatch, 21 | } 22 | let actions 23 | let worker 24 | let store 25 | 26 | const toWorker = ({ 27 | message, action, state, toAll, 28 | }) => { 29 | if (!worker) return 30 | 31 | const { instances } = store.getState() 32 | const instanceId = getActiveInstance(instances) 33 | const id = instances.options[instanceId].connectionId 34 | worker.postMessage({ 35 | method: 'emitReduxMessage', 36 | content: { 37 | type: message, 38 | action, 39 | state: nonReduxDispatch(store, message, instanceId, action, state, instances), 40 | instanceId, 41 | id, 42 | toAll, 43 | }, 44 | }) 45 | } 46 | 47 | const postImportMessage = (state) => { 48 | if (!worker) return 49 | 50 | const { instances } = store.getState() 51 | const instanceId = getActiveInstance(instances) 52 | const id = instances.options[instanceId].connectionId 53 | worker.postMessage({ 54 | method: 'emitReduxMessage', 55 | content: { 56 | type: 'IMPORT', 57 | state, 58 | instanceId, 59 | id, 60 | }, 61 | }) 62 | } 63 | 64 | // Receive messages from worker 65 | const messaging = (message) => { 66 | const { data } = message 67 | if (!data || !data.__IS_REDUX_NATIVE_MESSAGE__) return 68 | 69 | const { content: request } = data 70 | if (request.type === 'ERROR') { 71 | actions.showNotification(request.payload) 72 | return 73 | } 74 | actions.updateState(request) 75 | } 76 | 77 | const initWorker = (wkr) => { 78 | wkr.addEventListener('message', messaging) 79 | worker = wkr 80 | } 81 | 82 | const removeWorker = () => { 83 | worker = null 84 | } 85 | 86 | const syncLiftedState = (liftedState) => { 87 | if (!getGlobal('isSyncState')()) return 88 | 89 | const { actionsById } = liftedState 90 | const payload = [] 91 | liftedState.stagedActionIds.slice(1).forEach((id) => { 92 | payload.push(actionsById[id].action) 93 | }) 94 | const serialized = JSON.stringify({ payload: JSON.stringify(payload) }) 95 | ipcRenderer.send('sync-state', serialized) 96 | } 97 | 98 | export default (inStore) => { 99 | store = inStore 100 | actions = bindActionCreators(unboundActions, store.dispatch) 101 | return (next) => (action) => { 102 | if (action.type === SET_DEBUGGER_WORKER) { 103 | if (action.worker) { 104 | initWorker(action.worker) 105 | } else { 106 | removeWorker(action.worker) 107 | setReduxDevToolsMethods(false) 108 | next({ type: DISCONNECTED }) 109 | } 110 | } 111 | if (action.type === LIFTED_ACTION) { 112 | toWorker(action) 113 | } 114 | if ( 115 | action.type === UPDATE_STATE 116 | || action.type === LIFTED_ACTION 117 | || action.type === SYNC_STATE 118 | ) { 119 | next(action) 120 | const state = store.getState() 121 | const { instances } = state 122 | const id = getActiveInstance(instances) 123 | const liftedState = instances.states[id] 124 | if (liftedState && liftedState.computedStates.length > 1) { 125 | setReduxDevToolsMethods(true, actions.liftedDispatch) 126 | } else if (liftedState && liftedState.computedStates.length <= 1) { 127 | setReduxDevToolsMethods(false) 128 | } 129 | updateSliderContent(liftedState, action.action && action.action.dontUpdateTouchBarSlider) 130 | if (action.request && action.request.type === 'ACTION') { 131 | syncLiftedState(liftedState) 132 | } 133 | if (action.type === SYNC_STATE) { 134 | postImportMessage(action.payload) 135 | } 136 | return 137 | } 138 | return next(action) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/reducers/debugger.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_DEBUGGER_STATUS, 3 | SET_DEBUGGER_WORKER, 4 | SET_DEBUGGER_LOCATION, 5 | } from '../actions/debugger' 6 | import config from '../utils/config' 7 | 8 | function getStatusMessage(status, port) { 9 | let message 10 | switch (status) { 11 | case 'new': 12 | message = 'New Window' 13 | break 14 | case 'waiting': 15 | message = 'Waiting for client connection' 16 | break 17 | case 'connected': 18 | message = 'Connected' 19 | break 20 | case 'disconnected': 21 | default: 22 | message = 'Attempting reconnection' 23 | } 24 | if (status !== 'new') { 25 | message += ` (port ${port})` 26 | } 27 | const title = `React Native Debugger - ${message}` 28 | if (title !== document.title) { 29 | document.title = title 30 | } 31 | return message 32 | } 33 | 34 | const initialState = { 35 | worker: null, 36 | status: 'disconnected', 37 | statusMessage: getStatusMessage(config.isPortSettingRequired ? 'new' : 'disconnected', 8081), 38 | location: { 39 | host: 'localhost', 40 | port: config.port || 8081, 41 | }, 42 | isPortSettingRequired: config.isPortSettingRequired, 43 | } 44 | 45 | const actionsMap = { 46 | [SET_DEBUGGER_STATUS]: (state, action) => { 47 | const status = action.status || initialState.status 48 | const newState = { 49 | ...state, 50 | status, 51 | statusMessage: getStatusMessage(status, state.location.port), 52 | } 53 | return newState 54 | }, 55 | [SET_DEBUGGER_WORKER]: (state, action) => { 56 | const status = action.status || initialState.status 57 | const newState = { 58 | ...state, 59 | worker: action.worker, 60 | status, 61 | statusMessage: getStatusMessage(status, state.location.port), 62 | } 63 | return newState 64 | }, 65 | [SET_DEBUGGER_LOCATION]: (state, action) => { 66 | const location = { ...state.location, ...action.loc } 67 | const newState = { 68 | ...state, 69 | location, 70 | statusMessage: getStatusMessage(state.status, location.port), 71 | isPortSettingRequired: false, 72 | } 73 | return newState 74 | }, 75 | } 76 | 77 | export default (state = initialState, action = {}) => { 78 | const reduceFn = actionsMap[action.type] 79 | if (!reduceFn) return state 80 | return reduceFn(state, action) 81 | } 82 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { section } from '@redux-devtools/app/lib/esm/reducers/section' 3 | // import { connection } from '@redux-devtools/app/lib/esm/reducers/connection'; 4 | // import { socket } from '@redux-devtools/app/lib/esm/reducers/socket'; 5 | import { monitor } from '@redux-devtools/app/lib/esm/reducers/monitor' 6 | import { notification } from '@redux-devtools/app/lib/esm/reducers/notification' 7 | import { instances } from '@redux-devtools/app/lib/esm/reducers/instances' 8 | import { reports } from '@redux-devtools/app/lib/esm/reducers/reports' 9 | import { theme } from '@redux-devtools/app/lib/esm/reducers/theme' 10 | 11 | import setting from './setting' 12 | import debuggerReducer from './debugger' 13 | 14 | export default combineReducers({ 15 | section, 16 | instances, 17 | reports, 18 | theme, 19 | monitor, 20 | notification, 21 | 22 | setting, 23 | debugger: debuggerReducer, 24 | }) 25 | -------------------------------------------------------------------------------- /app/reducers/setting.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_DEVTOOLS, RESIZE_DEVTOOLS, CHANGE_DEFAULT_THEME } from '../actions/setting' 2 | 3 | const initialState = { 4 | react: true, 5 | redux: true, 6 | size: 0.6, 7 | themeName: null, 8 | } 9 | 10 | const actionsMap = { 11 | [TOGGLE_DEVTOOLS]: (state, action) => ({ 12 | ...state, 13 | [action.name]: !state[action.name], 14 | }), 15 | [RESIZE_DEVTOOLS]: (state, action) => { 16 | if (!state.redux || !state.react) { 17 | return state 18 | } 19 | const { size } = action 20 | if (size < 0.2) return { ...state, size: 0.2 } 21 | if (size > 0.8) return { ...state, size: 0.8 } 22 | return { ...state, size } 23 | }, 24 | [CHANGE_DEFAULT_THEME]: (state, action) => ({ 25 | ...state, 26 | themeName: action.themeName, 27 | }), 28 | } 29 | 30 | export default (state = initialState, action = {}) => { 31 | const reduceFn = actionsMap[action.type] 32 | if (!reduceFn) return state 33 | return reduceFn(state, action) 34 | } 35 | -------------------------------------------------------------------------------- /app/setup.js: -------------------------------------------------------------------------------- 1 | import config from './utils/config' 2 | 3 | if (config.editor) { 4 | process.env.EDITOR = config.editor 5 | } 6 | 7 | if (config.fontFamily) { 8 | const styleEl = document.createElement('style') 9 | document.head.appendChild(styleEl) 10 | styleEl.sheet.insertRule( 11 | `div *, span * { font-family: ${config.fontFamily} !important; }`, 12 | 0, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { persistReducer, persistStore } from 'redux-persist' 3 | import localForage from 'localforage' 4 | import { exportStateMiddleware } from '@redux-devtools/app/lib/cjs/middlewares/exportState' 5 | import { instancesInitialState } from '@redux-devtools/app/lib/esm/reducers/instances' 6 | import debuggerAPI from '../middlewares/debuggerAPI' 7 | import reduxAPI from '../middlewares/reduxAPI' 8 | import rootReducer from '../reducers' 9 | 10 | const persistConfig = { 11 | key: 'redux-devtools', 12 | blacklist: ['instances', 'debugger'], 13 | storage: localForage, 14 | } 15 | 16 | const persistedReducer = persistReducer(persistConfig, rootReducer) 17 | 18 | const middlewares = applyMiddleware( 19 | debuggerAPI, 20 | exportStateMiddleware, 21 | reduxAPI, 22 | ) 23 | 24 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 25 | /* eslint-disable no-underscore-dangle */ 26 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 27 | /* eslint-enable no-underscore-dangle */ 28 | const enhancer = composeEnhancers(middlewares) 29 | 30 | const initialState = { 31 | instances: { 32 | ...instancesInitialState, 33 | selected: '', 34 | }, 35 | } 36 | 37 | export default (callback) => { 38 | const store = createStore(persistedReducer, initialState, enhancer) 39 | const persistor = persistStore(store, null, () => callback?.(store)) 40 | return { store, persistor } 41 | } 42 | -------------------------------------------------------------------------------- /app/utils/adb.js: -------------------------------------------------------------------------------- 1 | import adb from 'adbkit' 2 | 3 | export const client = adb.createClient({ host: '127.0.0.1' }) 4 | 5 | const reverse = (device, port) => client.reverse(device, `tcp:${port}`, `tcp:${port}`) 6 | 7 | export const tryADBReverse = async (port) => { 8 | const devices = await client.listDevices().filter((device) => device.type === 'device') 9 | return Promise.all(devices.map((device) => reverse(device.id, port))) 10 | } 11 | -------------------------------------------------------------------------------- /app/utils/config.js: -------------------------------------------------------------------------------- 1 | import { getCurrentWindow } from '@electron/remote' 2 | 3 | export default getCurrentWindow().debuggerConfig || {} 4 | -------------------------------------------------------------------------------- /app/utils/devMenu.js: -------------------------------------------------------------------------------- 1 | import { TouchBar, nativeImage, getCurrentWindow } from '@electron/remote' 2 | 3 | import { ipcRenderer } from 'electron' 4 | import config from './config' 5 | 6 | const { TouchBarButton, TouchBarSlider } = TouchBar || {} 7 | const currentWindow = getCurrentWindow() 8 | 9 | let worker 10 | let availableMethods = [] 11 | 12 | /* reload, toggleElementInspector, networkInspect */ 13 | let leftBar = {} 14 | 15 | let isSliderEnabled 16 | let storeLiftedState 17 | /* slider, prev, next */ 18 | let rightBar = {} 19 | 20 | const getBarItems = (bar) => Object.keys(bar) 21 | .map((key) => bar[key]) 22 | .filter((barItem) => !!barItem) 23 | const setTouchBar = () => currentWindow.setTouchBar( 24 | new TouchBar({ 25 | items: [ 26 | ...getBarItems(leftBar), 27 | ...(isSliderEnabled ? getBarItems(rightBar) : []), 28 | ], 29 | }), 30 | ) 31 | 32 | const invokeDevMenuMethod = ({ name, args }) => worker && worker.postMessage({ method: 'invokeDevMenuMethod', name, args }) 33 | 34 | let networkInspectEnabled = !!config.networkInspect 35 | const sendContextMenuUpdate = () => { 36 | ipcRenderer.send(`context-menu-available-methods-update-${currentWindow.id}`, { 37 | availableMethods, 38 | networkInspectEnabled, 39 | }) 40 | } 41 | 42 | export const networkInspect = { 43 | isEnabled: () => !!networkInspectEnabled, 44 | getHighlightColor: () => (networkInspectEnabled ? '#7A7A7A' : '#363636'), 45 | toggle() { 46 | networkInspectEnabled = !networkInspectEnabled 47 | sendContextMenuUpdate() 48 | }, 49 | } 50 | 51 | const devMenuMethods = { 52 | reload: () => invokeDevMenuMethod({ name: 'reload' }), 53 | toggleElementInspector: () => invokeDevMenuMethod({ name: 'toggleElementInspector' }), 54 | show: () => invokeDevMenuMethod({ name: 'show' }), 55 | networkInspect: () => { 56 | networkInspect.toggle() 57 | if (leftBar.networkInspect) { 58 | leftBar.networkInspect.backgroundColor = networkInspect.getHighlightColor() 59 | } 60 | invokeDevMenuMethod({ 61 | name: 'networkInspect', 62 | args: [networkInspectEnabled], 63 | }) 64 | }, 65 | showAsyncStorage: () => { 66 | invokeDevMenuMethod({ name: 'showAsyncStorage' }) 67 | }, 68 | clearAsyncStorage: () => { 69 | if ( 70 | window.confirm( 71 | 'Call `AsyncStorage.clear()` in current React Native debug session?', 72 | ) 73 | ) { 74 | invokeDevMenuMethod({ name: 'clearAsyncStorage' }) 75 | } 76 | }, 77 | } 78 | 79 | export const invokeDevMethod = (name) => () => { 80 | if (availableMethods.includes(name)) { 81 | return devMenuMethods[name]() 82 | } 83 | } 84 | 85 | const hslShift = [0.5, 0.2, 0.8] 86 | const icon = (name, resizeOpts) => { 87 | const image = nativeImage.createFromNamedImage(name, hslShift) 88 | return image.resize(resizeOpts) 89 | } 90 | 91 | let namedImages 92 | const initNamedImages = () => { 93 | if (process.platform !== 'darwin' || namedImages) return 94 | namedImages = { 95 | reload: icon('NSTouchBarRefreshTemplate', { height: 20 }), 96 | toggleElementInspector: icon('NSTouchBarQuickLookTemplate', { height: 18 }), 97 | networkInspect: icon('NSTouchBarRecordStartTemplate', { height: 20 }), 98 | prev: icon('NSTouchBarGoBackTemplate', { height: 20 }), 99 | next: icon('NSTouchBarGoForwardTemplate', { height: 20 }), 100 | } 101 | } 102 | 103 | const setDevMenuMethodsForTouchBar = () => { 104 | if (process.platform !== 'darwin') return 105 | initNamedImages() 106 | 107 | leftBar = { 108 | // Default items 109 | networkInspect: new TouchBarButton({ 110 | icon: namedImages.networkInspect, 111 | click: devMenuMethods.networkInspect, 112 | backgroundColor: networkInspect.getHighlightColor(), 113 | }), 114 | } 115 | if (availableMethods.includes('reload')) { 116 | leftBar.reload = new TouchBarButton({ 117 | icon: namedImages.reload, 118 | click: devMenuMethods.reload, 119 | }) 120 | } 121 | if (availableMethods.includes('toggleElementInspector')) { 122 | leftBar.toggleElementInspector = new TouchBarButton({ 123 | icon: namedImages.toggleElementInspector, 124 | click: devMenuMethods.toggleElementInspector, 125 | }) 126 | } 127 | setTouchBar() 128 | } 129 | 130 | // Reset TouchBar when reload the app 131 | setDevMenuMethodsForTouchBar([]) 132 | 133 | export const setDevMenuMethods = (list, wkr) => { 134 | worker = wkr 135 | availableMethods = list 136 | sendContextMenuUpdate() 137 | setDevMenuMethodsForTouchBar() 138 | } 139 | 140 | export const setReduxDevToolsMethods = (enabled, dispatch) => { 141 | if (process.platform !== 'darwin') return 142 | initNamedImages() 143 | 144 | // Already setup 145 | if (enabled && isSliderEnabled) return 146 | 147 | const handleSliderChange = (nextIndex, dontUpdateTouchBarSlider = false) => dispatch({ 148 | type: 'JUMP_TO_STATE', 149 | actionId: storeLiftedState.stagedActionIds[nextIndex], 150 | index: nextIndex, 151 | dontUpdateTouchBarSlider, 152 | }) 153 | 154 | rightBar = { 155 | slider: new TouchBarSlider({ 156 | value: 0, 157 | minValue: 0, 158 | maxValue: 0, 159 | change(nextIndex) { 160 | if (nextIndex !== storeLiftedState.currentStateIndex) { 161 | // Set `dontUpdateTouchBarSlider` true for keep slide experience 162 | handleSliderChange(nextIndex, true) 163 | } 164 | }, 165 | }), 166 | prev: new TouchBarButton({ 167 | icon: namedImages.prev, 168 | click() { 169 | const nextIndex = storeLiftedState.currentStateIndex - 1 170 | if (nextIndex >= 0) { 171 | handleSliderChange(nextIndex) 172 | } 173 | }, 174 | }), 175 | next: new TouchBarButton({ 176 | icon: namedImages.next, 177 | click() { 178 | const nextIndex = storeLiftedState.currentStateIndex + 1 179 | if (nextIndex < storeLiftedState.computedStates.length) { 180 | handleSliderChange(nextIndex) 181 | } 182 | }, 183 | }), 184 | } 185 | isSliderEnabled = enabled 186 | setTouchBar() 187 | } 188 | 189 | export const updateSliderContent = (liftedState, dontUpdateTouchBarSlider) => { 190 | if (process.platform !== 'darwin') return 191 | 192 | storeLiftedState = liftedState 193 | if (isSliderEnabled && !dontUpdateTouchBarSlider) { 194 | const { currentStateIndex, computedStates } = liftedState 195 | rightBar.slider.maxValue = computedStates.length - 1 196 | rightBar.slider.value = currentStateIndex 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/utils/devtools.js: -------------------------------------------------------------------------------- 1 | import { getCatchConsoleLogScript } from '../../electron/devtools' 2 | 3 | let enabled = false 4 | export const toggleOpenInEditor = (win, port) => { 5 | if (win.devToolsWebContents) { 6 | enabled = !enabled 7 | return win.devToolsWebContents.executeJavaScript(`(() => { 8 | ${getCatchConsoleLogScript(port)} 9 | window.__IS_OPEN_IN_EDITOR_ENABLED__ = ${enabled}; 10 | })()`) 11 | } 12 | } 13 | 14 | export const isOpenInEditorEnabled = () => enabled 15 | 16 | export const clearNetworkLogs = (win) => { 17 | if (win.devToolsWebContents) { 18 | return win.devToolsWebContents.executeJavaScript(`setTimeout(() => { 19 | const { network } = UI.panels; 20 | if (network && network.networkLogView && network.networkLogView.reset) { 21 | network.networkLogView.reset() 22 | } 23 | }, 100)`) 24 | } 25 | } 26 | 27 | export const selectRNDebuggerWorkerContext = (win) => { 28 | if (win.devToolsWebContents) { 29 | return win.devToolsWebContents.executeJavaScript(`setTimeout(() => { 30 | const { console } = UI.panels; 31 | if (console && console.view && console.view.consoleContextSelector) { 32 | const selector = console.view.consoleContextSelector; 33 | const item = selector.items.items.find( 34 | item => item.label() === 'RNDebuggerWorker.js' 35 | ); 36 | if (item) { 37 | selector.itemSelected(item); 38 | } 39 | } 40 | }, 100)`) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/worker/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-restricted-globals": "off" 4 | } 5 | } -------------------------------------------------------------------------------- /app/worker/apollo.js: -------------------------------------------------------------------------------- 1 | 2 | export function handleApolloClient() { 3 | // eslint-disable-next-line global-require 4 | require('apollo-client-devtools/build/hook') 5 | } 6 | -------------------------------------------------------------------------------- /app/worker/asyncStorage.js: -------------------------------------------------------------------------------- 1 | export const getClearAsyncStorageFn = (AsyncStorage) => { 2 | if (!AsyncStorage.clear) return 3 | return () => AsyncStorage.clear().catch((f) => f) 4 | } 5 | 6 | function convertError(error) { 7 | if (!error) { 8 | return null 9 | } 10 | const out = new Error(error.message) 11 | out.key = error.key 12 | return out 13 | } 14 | 15 | function convertErrors(errs) { 16 | if (!errs) { 17 | return null 18 | } 19 | return (Array.isArray(errs) ? errs : [errs]).map((e) => convertError(e)) 20 | } 21 | 22 | export const getSafeAsyncStorage = (NativeModules) => { 23 | const RCTAsyncStorage = NativeModules 24 | && (NativeModules.RNC_AsyncSQLiteDBStorage 25 | || NativeModules.RNCAsyncStorage 26 | || NativeModules.PlatformLocalStorage 27 | || NativeModules.AsyncRocksDBStorage 28 | || NativeModules.AsyncSQLiteDBStorage 29 | || NativeModules.AsyncLocalStorage) 30 | 31 | return { 32 | getItem(key) { 33 | if (!RCTAsyncStorage) return Promise.resolve(null) 34 | return new Promise((resolve, reject) => { 35 | RCTAsyncStorage.multiGet([key], (errors, result) => { 36 | // Unpack result to get value from [[key,value]] 37 | const value = result && result[0] && result[0][1] ? result[0][1] : null 38 | const errs = convertErrors(errors) 39 | if (errs) { 40 | reject(errs[0]) 41 | } else { 42 | resolve(value) 43 | } 44 | }) 45 | }) 46 | }, 47 | async setItem(key, value) { 48 | if (!RCTAsyncStorage) return Promise.resolve(null) 49 | return new Promise((resolve, reject) => { 50 | RCTAsyncStorage.multiSet([[key, value]], (errors) => { 51 | const errs = convertErrors(errors) 52 | if (errs) { 53 | reject(errs[0]) 54 | } else { 55 | resolve(null) 56 | } 57 | }) 58 | }) 59 | }, 60 | clear() { 61 | if (!RCTAsyncStorage) return Promise.resolve(null) 62 | return new Promise((resolve, reject) => { 63 | RCTAsyncStorage.clear((error) => { 64 | if (error && convertError(error)) { 65 | reject(convertError(error)) 66 | } else { 67 | resolve(null) 68 | } 69 | }) 70 | }) 71 | }, 72 | getAllKeys() { 73 | if (!RCTAsyncStorage) return Promise.resolve(null) 74 | return new Promise((resolve, reject) => { 75 | RCTAsyncStorage.getAllKeys((error, keys) => { 76 | if (error) { 77 | reject(convertError(error)) 78 | } else { 79 | resolve(keys) 80 | } 81 | }) 82 | }) 83 | }, 84 | } 85 | } 86 | 87 | export const getShowAsyncStorageFn = (AsyncStorage) => { 88 | if (!AsyncStorage.getAllKeys || !AsyncStorage.getItem) return 89 | return async () => { 90 | const keys = await AsyncStorage.getAllKeys() 91 | if (keys && keys.length) { 92 | const items = await Promise.all( 93 | keys.map((key) => AsyncStorage.getItem(key)), 94 | ) 95 | const table = {} 96 | keys.forEach((key, index) => { 97 | table[key] = { content: items[index] } 98 | }) 99 | console.table(table) 100 | } else { 101 | console.log('[RNDebugger] No AsyncStorage content.') 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/worker/devMenu.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | 3 | import { toggleNetworkInspect } from './networkInspect' 4 | import { getClearAsyncStorageFn, getShowAsyncStorageFn, getSafeAsyncStorage } from './asyncStorage' 5 | 6 | let availableDevMenuMethods = {} 7 | 8 | export const checkAvailableDevMenuMethods = ({ NativeModules }) => { 9 | // RN 0.43 use DevSettings, DevMenu will be deprecated 10 | const DevSettings = NativeModules.DevSettings || NativeModules.DevMenu 11 | // Currently `show dev menu` is only on DevMenu 12 | const showDevMenu = (DevSettings && DevSettings.show) 13 | || (NativeModules.DevMenu && NativeModules.DevMenu.show) 14 | || undefined 15 | 16 | const AsyncStorage = getSafeAsyncStorage(NativeModules) 17 | const methods = { 18 | ...DevSettings, 19 | show: showDevMenu, 20 | networkInspect: toggleNetworkInspect, 21 | showAsyncStorage: getShowAsyncStorageFn(AsyncStorage), 22 | clearAsyncStorage: getClearAsyncStorageFn(AsyncStorage), 23 | } 24 | if (methods.showAsyncStorage) { 25 | window.showAsyncStorageContentInDev = methods.showAsyncStorage 26 | } 27 | const result = Object.keys(methods).filter((key) => !!methods[key]) 28 | availableDevMenuMethods = methods 29 | 30 | postMessage({ __AVAILABLE_METHODS_CAN_CALL_BY_RNDEBUGGER__: result }) 31 | } 32 | 33 | export const invokeDevMenuMethodIfAvailable = (name, args = []) => { 34 | const method = availableDevMenuMethods[name] 35 | if (method) method(...args) 36 | } 37 | -------------------------------------------------------------------------------- /app/worker/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | /* global __fbBatchedBridge, importScripts: true */ 10 | 11 | // Edit from https://github.com/facebook/react-native/blob/master/local-cli/server/util/debuggerWorker.js 12 | 13 | import './setup' 14 | import { checkAvailableDevMenuMethods, invokeDevMenuMethodIfAvailable } from './devMenu' 15 | import { reportDefaultReactDevToolsPort } from './reactDevTools' 16 | import devToolsEnhancer, { composeWithDevTools } from './reduxAPI' 17 | import * as RemoteDev from './remotedev' 18 | import { getRequiredModules } from './utils' 19 | import { toggleNetworkInspect } from './networkInspect' 20 | import { handleApolloClient } from './apollo' 21 | 22 | /* eslint-disable no-underscore-dangle */ 23 | self.__REMOTEDEV__ = RemoteDev 24 | 25 | devToolsEnhancer.send = RemoteDev.send 26 | devToolsEnhancer.connect = RemoteDev.connect 27 | devToolsEnhancer.disconnect = RemoteDev.disconnect 28 | 29 | // Deprecated API, these may removed when redux-devtools-extension 3.0 release 30 | self.devToolsExtension = devToolsEnhancer 31 | self.reduxNativeDevTools = devToolsEnhancer 32 | self.reduxNativeDevToolsCompose = composeWithDevTools 33 | 34 | self.__REDUX_DEVTOOLS_EXTENSION__ = devToolsEnhancer 35 | self.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = composeWithDevTools 36 | 37 | const setupRNDebuggerBeforeImportScript = (message) => { 38 | self.__REACT_DEVTOOLS_PORT__ = message.reactDevToolsPort 39 | if (message.networkInspect) { 40 | self.__NETWORK_INSPECT__ = toggleNetworkInspect 41 | } 42 | } 43 | 44 | const noop = (f) => f 45 | const setupRNDebugger = async (message) => { 46 | // We need to regularly update JS runtime 47 | // because the changes of worker message (Redux DevTools, DevMenu) 48 | // doesn't notify to the remote JS runtime 49 | self.__RND_INTERVAL__ = setInterval(noop, 100); // eslint-disable-line 50 | 51 | handleApolloClient() 52 | toggleNetworkInspect(message.networkInspect) 53 | const modules = await getRequiredModules(message.moduleSize) 54 | if (modules) { 55 | checkAvailableDevMenuMethods(modules) 56 | reportDefaultReactDevToolsPort(modules) 57 | } 58 | } 59 | 60 | const messageHandlers = { 61 | executeApplicationScript(message, sendReply) { 62 | setupRNDebuggerBeforeImportScript(message) 63 | 64 | Object.keys(message.inject).forEach((key) => { 65 | self[key] = JSON.parse(message.inject[key]) 66 | }) 67 | let error 68 | try { 69 | importScripts(message.url) 70 | } catch (err) { 71 | error = err.message 72 | } 73 | 74 | if (!error) { 75 | setupRNDebugger(message) 76 | } 77 | 78 | sendReply(null /* result */, error) 79 | 80 | return false 81 | }, 82 | emitReduxMessage() { 83 | // pass to other listeners 84 | return true 85 | }, 86 | emitApolloMessage() { 87 | // pass to other listeners 88 | return true 89 | }, 90 | invokeDevMenuMethod({ name, args }) { 91 | invokeDevMenuMethodIfAvailable(name, args) 92 | return false 93 | }, 94 | beforeTerminate() { 95 | // Clean for notify native bridge 96 | if (window.__RND_INTERVAL__) { 97 | clearInterval(window.__RND_INTERVAL__) 98 | window.__RND_INTERVAL__ = null 99 | } 100 | return false 101 | }, 102 | } 103 | 104 | addEventListener('message', (message) => { 105 | const object = message.data 106 | 107 | const sendReply = (result, error) => { 108 | postMessage({ replyID: object.id, result, error }) 109 | } 110 | 111 | const handler = messageHandlers[object.method] 112 | if (handler) { 113 | // Special cased handlers 114 | return handler(object, sendReply) 115 | } 116 | // Other methods get called on the bridge 117 | let returnValue = [[], [], [], 0] 118 | let error 119 | try { 120 | if (typeof __fbBatchedBridge === 'object') { 121 | returnValue = __fbBatchedBridge[object.method].apply(null, object.arguments) 122 | } else { 123 | error = 'Failed to call function, __fbBatchedBridge is undefined' 124 | } 125 | } catch (err) { 126 | error = err.message 127 | } finally { 128 | sendReply(JSON.stringify(returnValue), error) 129 | } 130 | return false 131 | }) 132 | -------------------------------------------------------------------------------- /app/worker/networkInspect.js: -------------------------------------------------------------------------------- 1 | import getRNDebuggerFetchPolyfills from './polyfills/fetch' 2 | 3 | const isWorkerMethod = (fn) => String(fn).indexOf('[native code]') > -1 4 | 5 | /* eslint-disable no-underscore-dangle */ 6 | let networkInspect 7 | 8 | export const toggleNetworkInspect = (enabled) => { 9 | if (!enabled && networkInspect) { 10 | self.fetch = networkInspect.fetch 11 | self.XMLHttpRequest = networkInspect.XMLHttpRequest 12 | self.FormData = networkInspect.FormData 13 | self.Headers = networkInspect.Headers 14 | self.Request = networkInspect.Request 15 | self.Response = networkInspect.Response 16 | networkInspect = null 17 | return 18 | } 19 | if (!enabled) return 20 | if (enabled && networkInspect) return 21 | if (isWorkerMethod(self.XMLHttpRequest) || isWorkerMethod(self.FormData)) { 22 | console.warn( 23 | '[RNDebugger] ' 24 | + 'I tried to enable Network Inspect but XHR ' 25 | + "have been replaced by worker's XHR. " 26 | + 'You can disable Network Inspect (documentation: https://goo.gl/BVvEkJ) ' 27 | + 'or tracking your app code if you have called ' 28 | + '`global.XMLHttpRequest = global.originalXMLHttpRequest`.', 29 | ) 30 | return 31 | } 32 | networkInspect = { 33 | fetch: self.fetch, 34 | XMLHttpRequest: self.XMLHttpRequest, 35 | FormData: self.FormData, 36 | Headers: self.Headers, 37 | Request: self.Request, 38 | Response: self.Response, 39 | } 40 | 41 | self.XMLHttpRequest = self.originalXMLHttpRequest 42 | ? self.originalXMLHttpRequest 43 | : self.XMLHttpRequest 44 | self.FormData = self.originalFormData ? self.originalFormData : self.FormData 45 | const { 46 | fetch, Headers, Request, Response, 47 | } = getRNDebuggerFetchPolyfills() 48 | self.fetch = fetch 49 | self.Headers = Headers 50 | self.Request = Request 51 | self.Response = Response 52 | 53 | console.log( 54 | '[RNDebugger]', 55 | 'Network Inspect is enabled,', 56 | 'see the documentation (https://goo.gl/yEcRrU) for more information.', 57 | ) 58 | } 59 | 60 | /* 61 | * `originalXMLHttpRequest` haven't permission to set forbidden header name 62 | * (https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name) 63 | * We have to use Electron session to solve this problem (See electron/main.js) 64 | */ 65 | const forbiddenHeaderNames = [ 66 | 'Accept-Charset', 67 | 'Accept-Encoding', 68 | 'Access-Control-Request-Headers', 69 | 'Access-Control-Request-Method', 70 | 'Connection', 71 | 'Content-Length', 72 | 'Cookie', 73 | 'Cookie2', 74 | 'Date', 75 | 'DNT', 76 | 'Expect', 77 | 'Host', 78 | 'Keep-Alive', 79 | 'Origin', 80 | 'Referer', 81 | 'TE', 82 | 'Trailer', 83 | 'Transfer-Encoding', 84 | 'Upgrade', 85 | 'Via', 86 | // Actually it still blocked on Chrome 87 | 'User-Agent', 88 | ] 89 | forbiddenHeaderNames.forEach((name) => forbiddenHeaderNames.push(name.toLowerCase())) 90 | 91 | const isForbiddenHeaderName = (header) => forbiddenHeaderNames.includes(header) 92 | || header.startsWith('Proxy-') 93 | || header.startsWith('proxy-') 94 | || header.startsWith('Sec-') 95 | || header.startsWith('sec-') 96 | 97 | export const replaceForbiddenHeadersForWorkerXHR = () => { 98 | if (!isWorkerMethod(self.XMLHttpRequest)) return 99 | const originalSetRequestHeader = self.XMLHttpRequest.prototype.setRequestHeader 100 | self.XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(header, value) { 101 | let replacedHeader = header 102 | if (isForbiddenHeaderName(header)) { 103 | replacedHeader = `__RN_DEBUGGER_SET_HEADER_REQUEST_${header}` 104 | } 105 | return originalSetRequestHeader.call(this, replacedHeader, value) 106 | } 107 | } 108 | 109 | export const addURIWarningForWorkerFormData = () => { 110 | if (!isWorkerMethod(self.FormData)) return 111 | const originAppend = FormData.prototype.append 112 | self.FormData.prototype.append = function append(key, value) { 113 | if (value && value.uri) { 114 | console.warn( 115 | '[RNDebugger] ' 116 | + "Detected you're enabled Network Inspect and using `uri` in FormData, " 117 | + 'it will be a problem if you use it for upload, ' 118 | + 'please see the documentation (https://goo.gl/yEcRrU) for more information.', 119 | ) 120 | } 121 | return originAppend.call(this, key, value) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/worker/reactDevTools.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | 3 | const methodGlobalName = '__REPORT_REACT_DEVTOOLS_PORT__' 4 | 5 | const reportReactDevToolsPort = (port, platform) => postMessage({ 6 | [methodGlobalName]: port, 7 | platform, 8 | }) 9 | 10 | export const reportDefaultReactDevToolsPort = async ({ setupDevtools, Platform }) => { 11 | if (Platform.__empty) return 12 | /* 13 | * [Fallback] React Native version under 0.39 can't specified the port 14 | */ 15 | if ( 16 | typeof setupDevtools === 'function' 17 | && setupDevtools.toString().indexOf('window.__REACT_DEVTOOLS_PORT__') === -1 18 | ) { 19 | reportReactDevToolsPort(8097, Platform.OS) 20 | } else { 21 | // React Inspector will keep the last reported port even if reload JS, 22 | // because we don't want to icrease the user waiting time for reload JS. 23 | // We need back to use the random port if we don't need fallback 24 | reportReactDevToolsPort(window.__REACT_DEVTOOLS_PORT__, Platform.OS) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/worker/remotedev.js: -------------------------------------------------------------------------------- 1 | // Edit from https://github.com/zalmoxisus/remotedev/blob/master/src/devTools.js 2 | 3 | import { stringify, parse } from 'jsan' 4 | import { generateId, getActionsArray } from '@redux-devtools/utils' 5 | 6 | let listenerAdded 7 | const listeners = {} 8 | 9 | export function extractState(message) { 10 | if (!message || !message.state) return undefined 11 | if (typeof message.state === 'string') return parse(message.state) 12 | return message.state 13 | } 14 | 15 | function handleMessages(message) { 16 | if (!message.payload) { 17 | message.payload = message.action 18 | } 19 | const fn = listeners[message.instanceId] 20 | if (!fn) return true 21 | 22 | if (typeof fn === 'function') { 23 | fn(message) 24 | } else { 25 | fn.forEach((func) => func(message)) 26 | } 27 | return false 28 | } 29 | 30 | export function start() { 31 | if (!listenerAdded) { 32 | self.addEventListener('message', (message) => { 33 | const { method, content } = message.data 34 | if (method === 'emitReduxMessage') { 35 | return handleMessages(content) 36 | } 37 | }) 38 | listenerAdded = true 39 | } 40 | } 41 | 42 | function transformAction(action, config) { 43 | if (action.action) return action 44 | const liftedAction = { timestamp: Date.now() } 45 | if (action) { 46 | if (config.getActionType) { 47 | liftedAction.action = config.getActionType(action) 48 | } else if (typeof action === 'string') { 49 | liftedAction.action = { type: action } 50 | } else if (!action.type) { 51 | liftedAction.action = { type: 'update' } 52 | } else { 53 | liftedAction.action = action 54 | } 55 | } else { 56 | liftedAction.action = { type: action } 57 | } 58 | return liftedAction 59 | } 60 | 61 | export function send(action, state, type, options) { 62 | start() 63 | setTimeout(() => { 64 | const message = { 65 | payload: state ? stringify(state) : '', 66 | action: type === 'ACTION' ? stringify(transformAction(action, options)) : action, 67 | type: type || 'ACTION', 68 | id: options.instanceId, 69 | instanceId: options.instanceId, 70 | name: options.name, 71 | } 72 | message.libConfig = { 73 | type: options.type, 74 | name: options.name, 75 | serialize: !!options.serialize, 76 | actionCreators: options.actionCreators, 77 | } 78 | postMessage({ __IS_REDUX_NATIVE_MESSAGE__: true, content: message }) 79 | }, 0) 80 | } 81 | 82 | export function connect(options = {}) { 83 | const id = generateId(options.instanceId) 84 | const opts = { 85 | ...options, 86 | instanceId: id, 87 | name: options.name || id, 88 | actionCreators: JSON.stringify(getActionsArray(options.actionCreators || {})), 89 | } 90 | start() 91 | return { 92 | init(state, action) { 93 | send(action || {}, state, 'INIT', opts) 94 | }, 95 | subscribe(listener) { 96 | if (!listener) return undefined 97 | if (!listeners[id]) listeners[id] = [] 98 | listeners[id].push(listener) 99 | 100 | return function unsubscribe() { 101 | const index = listeners[id].indexOf(listener) 102 | listeners[id].splice(index, 1) 103 | } 104 | }, 105 | unsubscribe() { 106 | delete listeners[id] 107 | }, 108 | send(action, payload) { 109 | if (action) { 110 | send(action, payload, 'ACTION', opts) 111 | } else { 112 | send(undefined, payload, 'STATE', opts) 113 | } 114 | }, 115 | error(payload) { 116 | send(undefined, payload, 'Error', opts) 117 | }, 118 | } 119 | } 120 | 121 | // Not implemented 122 | export function disconnect() {} 123 | -------------------------------------------------------------------------------- /app/worker/setup.js: -------------------------------------------------------------------------------- 1 | import { 2 | replaceForbiddenHeadersForWorkerXHR, 3 | addURIWarningForWorkerFormData, 4 | } from './networkInspect' 5 | 6 | // Add the missing `global` for WebWorker 7 | self.global = self 8 | 9 | /* 10 | * Blob is not supported for RN < 0.54, 11 | * we should remove it in WebWorker because 12 | * it will used for `whatwg-fetch` on older RN versions 13 | */ 14 | if (self.Blob && self.Blob.toString() === 'function Blob() { [native code] }') { 15 | /* 16 | * RN > 0.54 will polyfill Blob. 17 | * If it is deleted before the RN setup, RN will not add a reference to the original. 18 | * We will need to be able to restore the original when running RN > 0.54 for networking tools, 19 | * so add the reference here as react-native will not do it if the original is deleted 20 | */ 21 | self.originalBlob = self.Blob 22 | delete self.Blob 23 | } 24 | 25 | if ( 26 | self.XMLHttpRequest 27 | && self.XMLHttpRequest.toString() === 'function XMLHttpRequest() { [native code] }' 28 | ) { 29 | self.originalXMLHttpRequest = self.XMLHttpRequest 30 | } 31 | 32 | if (self.FormData && self.FormData.toString() === 'function FormData() { [native code] }') { 33 | self.originalFormData = self.FormData 34 | } 35 | 36 | // Catch native fetch 37 | if (self.fetch && self.fetch.toString() === 'function fetch() { [native code] }') { 38 | /* eslint-disable-next-line no-underscore-dangle */ 39 | self.__ORIGINAL_FETCH__ = self.fetch 40 | } 41 | 42 | replaceForbiddenHeadersForWorkerXHR() 43 | addURIWarningForWorkerFormData() 44 | -------------------------------------------------------------------------------- /app/worker/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | 3 | // Avoid warning of use metro require on dev mode 4 | // it actually unnecessary for RN >= 0.56, so it is backward compatibility 5 | const avoidWarnForRequire = (moduleNames) => { 6 | if (!moduleNames.length) moduleNames.push('NativeModules') 7 | return new Promise((resolve) => { 8 | setTimeout(() => { 9 | // It's replaced console.warn of react-native 10 | const originalWarn = console.warn 11 | console.warn = (...args) => { 12 | if ( 13 | args[0] 14 | && moduleNames.some( 15 | (name) => args[0].indexOf(`Requiring module '${name}' by name`) > -1, 16 | ) 17 | ) { 18 | return 19 | } 20 | return originalWarn(...args) 21 | } 22 | resolve(() => { 23 | console.warn = originalWarn 24 | }) 25 | }) 26 | }) 27 | } 28 | 29 | let reactNative 30 | 31 | const getRequireMethod = () => { 32 | // RN >= 0.57 33 | if (typeof window.__r === 'function') return window.__r 34 | // RN < 0.57 35 | if (typeof window.require === 'function') return window.require 36 | } 37 | 38 | const lookupForRNModules = (size = 999) => { 39 | const metroRequire = getRequireMethod() 40 | let actualSize = size 41 | let getModule = metroRequire 42 | if (metroRequire.getModules) { 43 | const mods = metroRequire.getModules() 44 | actualSize = Object.keys(mods).length 45 | getModule = (moduleId) => { 46 | const mod = mods && mods[moduleId] 47 | return (mod && mod.publicModule && mod.publicModule.exports) || null 48 | } 49 | } else { 50 | getModule = (moduleId) => metroRequire(moduleId) 51 | } 52 | for (let moduleId = 0; moduleId <= actualSize - 1; moduleId += 1) { 53 | const rn = getModule(moduleId) 54 | if (rn && rn.requireNativeComponent && rn.NativeModules) { 55 | return rn 56 | } 57 | } 58 | return null 59 | } 60 | 61 | const getModule = (name, size) => { 62 | let result 63 | try { 64 | const metroRequire = getRequireMethod() 65 | // RN >= 0.56 66 | if (metroRequire.name === 'metroRequire') { 67 | const rn = reactNative || lookupForRNModules(size) 68 | reactNative = rn 69 | global.$reactNative = rn 70 | result = reactNative && reactNative[name] 71 | } else if (metroRequire.name === '_require') { 72 | result = metroRequire(name) 73 | } 74 | } catch (e) {} // eslint-disable-line 75 | return result || { __empty: true } 76 | } 77 | 78 | const requiredModules = { 79 | MessageQueue: (size) => (self.__fbBatchedBridge 80 | && Object.getPrototypeOf(self.__fbBatchedBridge).constructor) 81 | || getModule('MessageQueue', size), 82 | NativeModules: (size) => getModule('NativeModules', size), 83 | Platform: (size) => getModule('Platform', size), 84 | setupDevtools: (size) => getModule('setupDevtools', size), 85 | } 86 | 87 | export const getRequiredModules = async (size) => { 88 | if (!window.__DEV__ || !getRequireMethod()) return 89 | const done = await avoidWarnForRequire(Object.keys(requiredModules)) 90 | const modules = {} 91 | Object.keys(requiredModules).forEach((name) => { 92 | modules[name] = requiredModules[name](size) 93 | }) 94 | done() 95 | return modules 96 | } 97 | 98 | const RN_DEBUGGER_URL_PART = 'RNDebuggerWorker.js' 99 | const BUNDLE_URL_REGEXP = /(http[\S]*?index\.bundle\?[\S]*?)(:\d+:?\d?)/ 100 | 101 | const addInlineSourceMap = (_, urlGroup1, urlGroup2) => `${urlGroup1}&inlineSourceMap=true${urlGroup2}` 102 | const mapStackLines = (line) => line.replace(BUNDLE_URL_REGEXP, addInlineSourceMap) 103 | const filterRnDebuggerLines = (line) => !line.includes(RN_DEBUGGER_URL_PART) 104 | 105 | export function updateStackWithSourceMap(stack) { 106 | const lines = stack.split('\n') 107 | const linesWithoutRNDebugger = lines.filter(filterRnDebuggerLines) 108 | const lineWithSourceMap = linesWithoutRNDebugger.map(mapStackLines) 109 | return lineWithSourceMap.join('\n') 110 | } 111 | -------------------------------------------------------------------------------- /auto_update.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://github.com/jhen0409/react-native-debugger/releases/download/v0.14.0/rn-debugger-macos-universal.zip", 3 | "name": "v0.14.0", 4 | "notes": "- Upgrade react-devtools-core to v4.28.0\n- Upgrade redux-devtools to latest version\n- Upgrade apollo-client-devtools to v4- More: https://github.com/jhen0409/react-native-debugger/releases/tag/v0.14.0" 5 | } 6 | -------------------------------------------------------------------------------- /auto_updater.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://github.com/jhen0409/react-native-debugger/releases/download/v0.10.13/rn-debugger-macos-x64.zip", 3 | "name": "v0.10.13", 4 | "notes": "Update apollo-client-devtools to v2.3.5" 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true) 3 | return { 4 | presets: [['@babel/preset-env', { targets: { node: '18.5' } }], '@babel/preset-react'], 5 | plugins: [], 6 | env: { 7 | production: { 8 | plugins: [ 9 | '@babel/plugin-transform-react-inline-elements', 10 | '@babel/plugin-transform-react-constant-elements', 11 | 'transform-react-remove-prop-types', 12 | ], 13 | }, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dist/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Native Debugger 6 | 7 | 8 | 9 |
10 |
Loading...
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /dist/css/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: monaco, Consolas, Lucida Console, monospace; 4 | overflow: hidden; 5 | font-size: 100%; 6 | margin: 0; 7 | padding: 0; 8 | width: 100%; 9 | height: 100%; 10 | background-color: rgb(53, 59, 70); 11 | } 12 | 13 | #root { 14 | width: 100%; 15 | height: 100%; 16 | } 17 | #logs { 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | white-space: pre; 22 | } 23 | #loading { 24 | color: #aaa; 25 | font-size: 30px; 26 | display: flex; 27 | height: 100%; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | ::-webkit-scrollbar { 33 | width: 8px; 34 | height: 8px; 35 | background-color: #555; 36 | } 37 | ::-webkit-scrollbar-thumb { 38 | background-color: #333; 39 | } 40 | ::-webkit-scrollbar-corner { 41 | background-color: #333; 42 | } 43 | 44 | @media print { 45 | @page { 46 | size: auto; 47 | margin: 0; 48 | } 49 | body { 50 | position: static; 51 | } 52 | } 53 | .CodeMirror { 54 | font-family: monaco, Consolas, Lucida Console, monospace !important; 55 | } 56 | -------------------------------------------------------------------------------- /dist/devtools-helper/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /dist/devtools-helper/main.js: -------------------------------------------------------------------------------- 1 | const detectChromeDevToolsTheme = () => chrome.devtools.panels.themeName || 'default'; 2 | 3 | const themeName = detectChromeDevToolsTheme(); 4 | 5 | chrome.devtools.inspectedWindow.eval(` 6 | window.chromeDevToolsTheme = '${themeName}'; 7 | if (window.notifyDevToolsThemeChange) { 8 | window.notifyDevToolsThemeChange(window.chromeDevToolsTheme); 9 | } 10 | `); 11 | 12 | window.addEventListener('message', ({ data }) => { 13 | if (data.type !== 'open-in-editor') { 14 | return; 15 | } 16 | const arr = data.source.split(':'); 17 | const lineNumber = arr.pop(-1); 18 | const file = arr.join(':'); 19 | chrome.devtools.inspectedWindow.eval(` 20 | if (window.openInEditor) { 21 | window.openInEditor('${file}', ${Number(lineNumber)}); 22 | } 23 | `); 24 | }); 25 | -------------------------------------------------------------------------------- /dist/devtools-helper/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "RNDebugger devtools helper", 4 | "version": "0.0.1", 5 | "devtools_page": "main.html" 6 | } 7 | -------------------------------------------------------------------------------- /dist/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/dist/logo.png -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-debugger", 3 | "version": "0.14.0", 4 | "productName": "React Native Debugger", 5 | "description": "The standalone app for React Native Debugger, with React DevTools / Redux DevTools", 6 | "main": "main.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/jhen0409/react-native-debugger.git" 10 | }, 11 | "author": "Jhen ", 12 | "license": "MIT", 13 | "scripts": { 14 | "postinstall": "patch-package" 15 | }, 16 | "dependencies": { 17 | "adbkit": "^2.11.0", 18 | "electron-store": "^1.2.0", 19 | "react-devtools-core": "^4.28.0" 20 | }, 21 | "devDependencies": { 22 | "apollo-client-devtools": "^4.1.4", 23 | "patch-package": "^6.2.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - [Getting Started](getting-started.md) 4 | - [Debugger Integration](debugger-integration.md) 5 | - [React DevTools Integration](react-devtools-integration.md) 6 | - [Redux DevTools Integration](redux-devtools-integration.md) 7 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 8 | - [Shortcuts references](shortcut-references.md) 9 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 10 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 11 | - [Config file in home directory](config-file-in-home-directory.md) 12 | - [Troubleshooting](troubleshooting.md) 13 | - [Contributing](contributing.md) 14 | -------------------------------------------------------------------------------- /docs/apollo-client-devtools-integration.md: -------------------------------------------------------------------------------- 1 | # Apollo Client DevTools Integration 2 | 3 | React Native debugger has integration for the [Apollo Client DevTools](https://github.com/apollographql/apollo-client-devtools), you can see the `Apollo` tab in Developer Tools: 4 | 5 | screen shot 2019-02-01 at 1 51 27 pm 6 | 7 | To ensure it works, you must use Apollo Client ^2.0. 8 | 9 | If the apollo tab doesn't appear, toggle developer tools off and on again. 10 | 11 | You can read Apollo DevTools [documentation](https://github.com/apollographql/apollo-client-devtools#apollo-client-devtools)). 12 | 13 | 14 | ## Other documentations 15 | 16 | - [Getting Started](getting-started.md) 17 | - [Debugger Integration](debugger-integration.md) 18 | - [React DevTools Integration](react-devtools-integration.md) 19 | - [Redux DevTools Integration](redux-devtools-integration.md) 20 | - [Shortcut references](shortcut-references.md) 21 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 22 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 23 | - [Config file in home directory](config-file-in-home-directory.md) 24 | - [Troubleshooting](troubleshooting.md) 25 | - [Contributing](contributing.md) 26 | -------------------------------------------------------------------------------- /docs/config-file-in-home-directory.md: -------------------------------------------------------------------------------- 1 | # Config file in home directory 2 | 3 | We could configure RNDebugger app in `~/.rndebuggerrc` (the config can be opened from the main menu: Debugger → Open Config File), the file used [json5](https://github.com/json5/json5) as format, see the following default template: 4 | 5 | 6 | ```json5 7 | { 8 | // Font family of the debugger window 9 | fontFamily: 'monaco, Consolas, Lucida Console, monospace', 10 | 11 | // Zoom level of the debugger window, it will override persited zoomLevel 12 | zoomLevel: 0, 13 | 14 | // Settings of debugger window, 15 | windowBounds: { 16 | // Size of the debugger window, it will override persisted size 17 | width: 1024, 18 | height: 750, 19 | 20 | // Show frame for debugger window 21 | // but due to https://github.com/electron/electron/issues/3647 22 | // so we can't have custom title bar if no frame 23 | frame: true, 24 | }, 25 | 26 | // Auto check update on RNDebugger startup 27 | autoUpdate: true, 28 | 29 | // RNDebugger will open debugger window with the ports when app launched 30 | defaultRNPackagerPorts: [8081], 31 | 32 | // Env for 33 | // open React DevTools source file link 34 | // and enable open in editor for console log for RNDebugger 35 | editor: '', 36 | 37 | // Set default react-devtools theme (default is match Chrome DevTools theme) 38 | // but the default theme doesn't change manually changed theme 39 | // see https://github.com/facebook/react-devtools/blob/master/frontend/Themes/Themes.js to get more 40 | defaultReactDevToolsTheme: 'RNDebugger', 41 | 42 | // Set default react-devtools port (default is \`19567+\` if it is not being used). 43 | // The devtools backend of React Native will use the port to connect to the devtools server. 44 | // You should use that if you have some rules for binding port. 45 | // (like https://github.com/jhen0409/react-native-debugger/issues/397) 46 | defaultReactDevToolsPort: 19567, 47 | 48 | // Enable Network Inspect by default 49 | // See https://github.com/jhen0409/react-native-debugger/blob/master/docs/network-inspect-of-chrome-devtools.md 50 | defaultNetworkInspect: false, 51 | 52 | // Refresh devtools when doing JS reload every N times. (-1 for disabled) 53 | // This can effectively avoid possible memory leaks (Like 54 | // https://github.com/jhen0409/react-native-debugger/issues/405) in devtools. 55 | timesJSLoadToRefreshDevTools: -1, 56 | } 57 | ``` 58 | 59 | ## Other documentations 60 | 61 | - [Getting Started](getting-started.md) 62 | - [Debugger Integration](debugger-integration.md) 63 | - [React DevTools Integration](react-devtools-integration.md) 64 | - [Redux DevTools Integration](redux-devtools-integration.md) 65 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 66 | - [Shortcut references](shortcut-references.md) 67 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 68 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 69 | - [Troubleshooting](troubleshooting.md) 70 | - [Contributing](contributing.md) 71 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development 4 | 5 | ### Fork this repo & install dependencies 6 | 7 | We're recommended use yarn because we keep the dependencies lock of yarn. 8 | 9 | ```bash 10 | # In react-native-debugger directory 11 | $ yarn 12 | $ cd npm-package && yarn && cd .. 13 | ``` 14 | 15 | If you want to debug the [NPM package](../npm-package), just run `npm link ` on your React Native project. 16 | 17 | ### Run on development mode 18 | _Please ensure the `React Native Debugger` production / distribution app is closed._ 19 | 20 | ```bash 21 | $ yarn dev:webpack # Then open the another terminal tab 22 | $ yarn dev:electron 23 | ``` 24 | 1. From here, you can open a react-native project with remote debugging enabled. 25 | 1. To see the development build of the react-native-debugger, do x,y,z 26 | 27 | ### Run on production mode 28 | 29 | ```bash 30 | $ yarn build 31 | $ yarn start 32 | ``` 33 | 34 | ### Run test 35 | 36 | Run lint and test, currently we just wrote E2E test for RNDebugger. 37 | 38 | ```bash 39 | $ yarn test 40 | $ yarn test-e2e 41 | ``` 42 | 43 | You need to closes all React Native packager (make sure `8081` or `8088` port not listening) when running the test. 44 | 45 | ### Packaging app 46 | 47 | ```bash 48 | $ yarn run pack-macos # Use --notarize to notarize app 49 | # On macOS: brew install fakeroot dpkg rpm 50 | $ yarn run pack-linux 51 | # On macOS: brew install wine mono 52 | $ yarn run pack-windows 53 | $ yarn run pack # all 54 | ``` 55 | 56 | If you want to build binaries yourself, please remove [../electron/update.js](electron/update.js) (and [electon/main.js usage](electon/main.js)). 57 | 58 | For macOS, note that if your app binary is not code signed, you will often get a firewall prompt from React DevTools server. 59 | 60 | ### [Optional] Prerequisites for packaging Linux / Windows app on macOS 61 | 62 | ```bash 63 | # Linux 64 | brew install fakeroot dpkg rpm 65 | 66 | # Windows 67 | brew tap homebrew/cask-versions 68 | brew install wine-stable mono 69 | ``` 70 | 71 | ## Financial contributions 72 | 73 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/react-native-debugger). 74 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. 75 | 76 | ## Credits 77 | 78 | ### Contributors 79 | 80 | Thank you to all the people who have already contributed to react-native-debugger! 81 | 82 | ### Backers 83 | 84 | Thank you to all our backers! [[Become a backer](https://opencollective.com/react-native-debugger#backer)] 85 | 86 | 87 | 88 | ### Sponsors 89 | 90 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/react-native-debugger#sponsor)) 91 | 92 | 93 | 94 | ## Other documentations 95 | 96 | - [Getting Started](getting-started.md) 97 | - [Debugger Integration](debugger-integration.md) 98 | - [React DevTools Integration](react-devtools-integration.md) 99 | - [Redux DevTools Integration](redux-devtools-integration.md) 100 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 101 | - [Shortcut references](shortcut-references.md) 102 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 103 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 104 | - [Config file in home directory](config-file-in-home-directory.md) 105 | - [Troubleshooting](troubleshooting.md) 106 | -------------------------------------------------------------------------------- /docs/debugger-integration.md: -------------------------------------------------------------------------------- 1 | # Debugger integration 2 | 3 | The Debugger worker is referenced from [react-native](https://github.com/facebook/react-native/blob/master/local-cli/server/util/) debugger-ui, so it's only working if you're enabled `Debug JS Remotely`, you can debug your app in Chrome Developer Tools, we keep the following tabs: 4 | 5 | - `Console` 6 | - `Sources` 7 | - `Network` (Inspect Network requests if you are enabled [Network Inspect](network-inspect-of-chrome-devtools.md)) 8 | - `Memory` 9 | 10 | ## Multiple React Native packager (custom port) support 11 | 12 | We can use [`react-native-debugger-open`](../npm-package) package to detect RN packager port, it will open an another window automatically if another debugger workers are running. 13 | 14 | If you don't use [the npm package](../npm-package) and want to change port, click `Debugger` -> `New Window` (`Command⌘ + T` for macOS, `Ctrl + T` for Linux / Windows) in application menu, you need to type an another RN packager port. The default port is use [`Expo`](https://github.com/expo/expo) (and [`create-react-native-app`](https://github.com/react-community/create-react-native-app)) default port. 15 | 16 | For macOS (10.12+), it used native tabs feature, see [the support page](https://support.apple.com/en-us/HT206998) for known how to use and setting. 17 | 18 | ## Debugging tips 19 | 20 | #### Global variables in console 21 | 22 | When you enabled remote debugging, RNDebugger should switched context to `RNDebuggerWorker.js` automatically, so you can get global variables of React Native runtime in the console. 23 | 24 | - `$r`: You selected element on react-devtools. 25 | - `showAsyncStorageContentInDev()` - Log AsyncStorage content 26 | - `$reactNative.*` - Get react-native modules. For example, you can get `$reactNative.AsyncStorage` 27 | 28 | #### Enable `Debug Remotely` programmatically 29 | 30 | For enable `Debug Remotely` without using dev menu, you can use the built-in `DevSettings` native module: 31 | 32 | ```js 33 | import { NativeModules } from 'react-native' 34 | 35 | if (__DEV__) { 36 | NativeModules.DevSettings.setIsDebuggingRemotely(true) 37 | } 38 | ``` 39 | 40 | If you're using Expo, you can still use the method, but it probably only works with `jsEngine: jsc` in `app.json`, `jsEngine: hermes` may not works. 41 | 42 | ## Other documentations 43 | 44 | - [Getting Started](getting-started.md) 45 | - [React DevTools Integration](react-devtools-integration.md) 46 | - [Redux DevTools Integration](redux-devtools-integration.md) 47 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 48 | - [Shortcut references](shortcut-references.md) 49 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 50 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 51 | - [Config file in home directory](config-file-in-home-directory.md) 52 | - [Troubleshooting](troubleshooting.md) 53 | - [Contributing](contributing.md) 54 | -------------------------------------------------------------------------------- /docs/enable-open-in-editor-in-console.md: -------------------------------------------------------------------------------- 1 | # Enable open in editor in console 2 | 3 | You can toggle the application menu item: 4 | 5 | 2017-08-16 10 44 41 6 | 7 | Instead of open file in `Sources` tab, you can open file in editor by click source link in console. This feature is disabled by default. 8 | 9 | ## Known issues 10 | 11 | - Currently this feature doesn't work with Haul bundler, please tracking [issue #141](https://github.com/jhen0409/react-native-debugger/issues/141). 12 | 13 | ## Other documentations 14 | 15 | - [Getting Started](getting-started.md) 16 | - [Debugger Integration](debugger-integration.md) 17 | - [React DevTools Integration](react-devtools-integration.md) 18 | - [Redux DevTools Integration](redux-devtools-integration.md) 19 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 20 | - [Shortcut references](shortcut-references.md) 21 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 22 | - [Config file in home directory](config-file-in-home-directory.md) 23 | - [Troubleshooting](troubleshooting.md) 24 | - [Contributing](contributing.md) 25 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Just these steps will let you start RNDebugger out of box: 4 | 5 | - Install the latest version ([download page](https://github.com/jhen0409/react-native-debugger/releases)). 6 | - Make sure all debugger clients of React Native are closed, usually are `http://localhost:/debugger-ui` 7 | - Make sure RNDebugger is open and wait state. 8 | - RNDebugger will try connect to debugger proxy, use port `8081` by default, you can create a new debugger window (macOS: `Command+T`, Linux/Windows: `Ctrl+T`) to specify the port if you want. 9 | - Enable `Debug JS Remotely` of [developer menu](https://reactnative.dev/docs/debugging#accessing-the-in-app-developer-menu) on your app 10 | 11 | ## Launch by CLI or React Native packager 12 | 13 | Platform: macOS / Linux 14 | 15 | ### The `rndebugger:` URI scheme 16 | 17 | Launch RNDebugger by typing the following command: 18 | 19 | ```bash 20 | $ open "rndebugger://set-debugger-loc?host=localhost&port=8081" 21 | ``` 22 | 23 | Or `xdg-open` for Linux: 24 | 25 | ```bash 26 | $ xdg-open "rndebugger://set-debugger-loc?host=localhost&port=8081" 27 | ``` 28 | 29 | The `host` / `port` means React Native packager. You may need to set `port` if you customize the packager port. (`8081` by default) 30 | 31 | From [`Debugging using a custom JavaScript debugger`](https://reactnative.dev/docs/0.71/debugging#debugging-using-a-custom-javascript-debugger) of React Native docs, you can use `REACT_DEBUGGER` env on react-native packager, it will try to launch RNDebugger when you turn on `Debug JS Remotely`: 32 | 33 | ```bash 34 | $ REACT_DEBUGGER="unset ELECTRON_RUN_AS_NODE && open -g 'rndebugger://set-debugger-loc?port=19000' ||" npm start 35 | ``` 36 | 37 | You can use `open` on macOS or `xdg-open` on Linux, currently it is not supported for Windows. 38 | 39 | ### Use [`react-native-debugger-open`](../npm-package) 40 | 41 | If you don‘t need to add a dependency, you can use the package, it can help with: 42 | 43 | - Replace `open debugger-ui with Chrome` to `open React Native Debugger` in react-native packager, saving you from closing the debugger-ui page everytime it automatically opens :) 44 | - Detect react-native packager port then send to the app, if you launch packager with custom `--port` or use Expo, this will be very useful 45 | 46 | ### What about Windows support? 47 | 48 | Currently the `rndebugger:` URI scheme doesn't support for Windows. 49 | 50 | In [`react-native-debugger-open`](../npm-package), it can be sent the `host` / `port` setting if RNDebugger opened, but can't automatically open if closed. 51 | 52 | If you want to have the feature (`rndebugger:` or another way), you are welcome to contribute. Please read [contributing](https://github.com/jhen0409/react-native-debugger/blob/master/docs/contributing.md) to become a maintainer. 53 | 54 | ## Use Redux DevTools Extension API 55 | 56 | Using the same API as [`redux-devtools-extension`](https://github.com/zalmoxisus/redux-devtools-extension#1-with-redux) is very simple: 57 | 58 | ```js 59 | const store = createStore( 60 | reducer /* preloadedState, */, 61 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), 62 | ) 63 | ``` 64 | 65 | See [`Redux DevTools Integration`](redux-devtools-integration.md) section for more information. 66 | 67 | ## Platform support 68 | 69 | - [React Native](https://github.com/facebook/react-native) >= 0.43 70 | - [React Native for macOS](https://github.com/ptmt/react-native-macos) (formerly react-native-desktop) >= 0.14.0 71 | - [React Native for Windows](https://github.com/Microsoft/react-native-windows) 72 | 73 | ## Auto-update RNDebugger app (Supported v0.5.0 after) 74 | 75 | Currently auto-update is only supported for macOS. Linux and Windows will show a dialog of new versions available for download. 76 | 77 | You can also click `React Native Debugger` (`RND` for Linux / Windows) -> `Check for Updates...` in the application menu. 78 | 79 | ## Other documentations 80 | 81 | - [Debugger Integration](debugger-integration.md) 82 | - [React DevTools Integration](react-devtools-integration.md) 83 | - [Redux DevTools Integration](redux-devtools-integration.md) 84 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 85 | - [Shortcut references](shortcut-references.md) 86 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 87 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 88 | - [Config file in home directory](config-file-in-home-directory.md) 89 | - [Troubleshooting](troubleshooting.md) 90 | - [Contributing](contributing.md) 91 | -------------------------------------------------------------------------------- /docs/network-inspect-of-chrome-devtools.md: -------------------------------------------------------------------------------- 1 | # Network Inspect of Chrome Developer Tools 2 | 3 | **_WARNING_**: You should read [the limitations](#limitations) if you want to use this feature. 4 | 5 | When you have Network Inspect enabled you can inspect network requests that use `XMLHttpRequest` or `fetch` on the `Network` tab of Chrome Developer Tools. 6 | 7 | You can enable this feature by one of these ways: 8 | 9 | - [context menu or Touch Bar](shortcut-references.md) (Network Inspect will be enabled while the RNDebugger is running, after closing it will reset to the default value); 10 | - by the `defaultNetworkInspect` option in the [config file](config-file-in-home-directory.md) (Network Inspect will be enabled permanently). 11 | 12 | ## How it works 13 | 14 | See [the comments of `react-native/Libraries/Utilities/PolyfillFunctions#L15-L27`](https://github.com/facebook/react-native/blob/ab97b9f6021d2b31b7155970c2be0c83f7e43f04/Libraries/Utilities/PolyfillFunctions.js#L15-L27). It uses `XMLHttpRequest` from WebWorker in Chrome, basically it can manually setup by: 15 | 16 | ```js 17 | global.XMLHttpRequest = global.originalXMLHttpRequest 18 | ? global.originalXMLHttpRequest 19 | : global.XMLHttpRequest; 20 | global.FormData = global.originalFormData 21 | ? global.originalFormData 22 | : global.FormData; 23 | 24 | fetch; // Ensure to get the lazy property 25 | 26 | if (window.__FETCH_SUPPORT__) { 27 | // it's RNDebugger only to have 28 | window.__FETCH_SUPPORT__.blob = false; 29 | } else { 30 | /* 31 | * Set __FETCH_SUPPORT__ to false is just work for `fetch`. 32 | * If you're using another way you can just use the native Blob and remove the `else` statement 33 | */ 34 | global.Blob = global.originalBlob ? global.originalBlob : global.Blob; 35 | global.FileReader = global.originalFileReader 36 | ? global.originalFileReader 37 | : global.FileReader; 38 | } 39 | ``` 40 | 41 | > Note that replace `global.Blob` will cause issue like [#56](https://github.com/jhen0409/react-native-debugger/issues/56). 42 | 43 | This allows you can open the `Network` tab in devtools to inspect requests of `fetch` and `XMLHttpRequest`. 44 | 45 | You can also do this on the official remote debugger, but it has two differences: 46 | 47 | - RNDebugger is based on [Electron](https://github.com/electron/electron) so it doesn't have the CORS issue 48 | - We support setting [`Forbidden header names`](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name), so you can use headers like `Origin` and `User-Agent`. 49 | 50 | ## Limitations 51 | 52 | There are some limitations of debugging network requests using Network Inspect: 53 | 54 | - [iOS] Requests pass `NSExceptionDomains` checks. If you forget to set a domain name, the requests will break in production. You should be clear about the difference. 55 | - [Android] If your network request would have caused `java.security.cert.CertPathValidatorException`, the Network Inpsect will skip that because it uses Debugger's network client. 56 | - React Native `FormData` supports the `uri` property. You can use files from `CameraRoll`, but `originalFormData` isn't supported. 57 | - It can't inspect request like `Image`s loaded from urls for `src`, so if your `Image` source has a set session, the session can't apply to `fetch` and `XMLHttpRequest`. 58 | 59 | If you want to inspect deeper network requests (like requests made with `Image`), use tools like [Charles](https://www.charlesproxy.com) or [Flipper](https://github.com/facebook/flipper). 60 | 61 | ## Other documentations 62 | 63 | - [Getting Started](getting-started.md) 64 | - [Debugger Integration](debugger-integration.md) 65 | - [React DevTools Integration](react-devtools-integration.md) 66 | - [Redux DevTools Integration](redux-devtools-integration.md) 67 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 68 | - [Shortcut references](shortcut-references.md) 69 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 70 | - [Config file in home directory](config-file-in-home-directory.md) 71 | - [Troubleshooting](troubleshooting.md) 72 | - [Contributing](contributing.md) 73 | -------------------------------------------------------------------------------- /docs/react-devtools-integration.md: -------------------------------------------------------------------------------- 1 | # React DevTools Integration 2 | 3 | **_NOTE_** Supported React Native version is `>= 0.62`. Please downgrade RNDebugger version to `0.10` if you're using older versions of React Native. 4 | 5 | The [React DevTools](https://reactnative.dev/docs/debugging#react-developer-tools) is built by [`facebook/react/packages/react-devtools-core`](https://github.com/facebook/react/tree/master/packages/react-devtools-core). 6 | 7 | It will open a WebSocket server to waiting React Native connection. The connection is already included in React Native (see [`setUpReactDevTools.js`](https://github.com/facebook/react-native/blob/0.62-stable/Libraries/Core/setUpReactDevTools.js)), it will keep trying to connect the React DevTools server in development mode, it should work well without any specification. 8 | 9 | We made the server listen to a random port and inject `window.__REACT_DEVTOOLS_PORT__` global variable in debugger worker. 10 | 11 | For Android, we have the built-in `adb` util and it will reverse the port automatically. 12 | 13 | ## Get `$r` global variable of React Native runtime in the console 14 | 15 | Refer to [`Debugger Integration`](debugger-integration.md#debugging-tips). 16 | 17 | ## **_Question_**: I got `Unsupported` message from React DevTools 18 | 19 | If you're using React Native version >= 0.62 and keep React Native Debugger as the latest version, here is what you can do: 20 | 21 | In your app project, make sure the `react-devtools-core` dependency to match the React DevTools version. Add resolutions in your `package.json` for Yarn: 22 | 23 | ```json 24 | { 25 | "resolutions": { 26 | "react-devtools-core": "~4.28.0" 27 | } 28 | } 29 | ``` 30 | 31 | or NPM: 32 | 33 | ```json 34 | { 35 | "overrides": { 36 | "react-devtools-core": "~4.28.0" 37 | } 38 | } 39 | ``` 40 | 41 | Reference: [Unsupported DevTools backend version - # React Native Debugger](https://gist.github.com/bvaughn/4bc90775530873fdf8e7ade4a039e579#react-native-debugger) 42 | 43 | If the React Native version of your project doesn't support `react-devtools-core@4.25`, please consider downgrade React Native Debugger version to v0.12. 44 | 45 | ## Other documentations 46 | 47 | - [Getting Started](getting-started.md) 48 | - [Debugger Integration](debugger-integration.md) 49 | - [Redux DevTools Integration](redux-devtools-integration.md) 50 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 51 | - [Shortcut references](shortcut-references.md) 52 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 53 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 54 | - [Config file in home directory](config-file-in-home-directory.md) 55 | - [Troubleshooting](troubleshooting.md) 56 | - [Contributing](contributing.md) 57 | -------------------------------------------------------------------------------- /docs/redux-devtools-integration.md: -------------------------------------------------------------------------------- 1 | # Redux DevTools Integration 2 | 3 | We used [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) and made the API same with [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools/tree/main/extension). 4 | 5 | If you've enabled `Debug JS remotely` with React Native Debugger, the following API is already included in global: 6 | 7 | - `window.__REDUX_DEVTOOLS_EXTENSION__` 8 | - `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` 9 | - `window.__REDUX_DEVTOOLS_EXTENSION__.connect` 10 | - You can just use [`redux-devtools-extension`](https://www.npmjs.com/package/redux-devtools-extension) npm package. 11 | 12 | See also: 13 | 14 | - [Redux DevTools main repository]](https://github.com/reduxjs/redux-devtools/blob/main/README.md) 15 | - [API Reference](https://github.com/reduxjs/redux-devtools/tree/main/extension/docs/API) 16 | - [Troubleshooting](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Troubleshooting.md) 17 | - Other Integrations 18 | - [`mobx-state-tree`](https://github.com/mobxjs/mobx-state-tree) - Use [`connectReduxDevtools`](https://github.com/mobxjs/mobx-state-tree/tree/3fc79b0b3ce7ad3e26d6bd5745fd9412d35c431c/packages/mst-middlewares#connectreduxdevtools) middleware. 19 | 20 | You can ignore the things specified by the browser extension. 21 | 22 | ## About `trace` feature 23 | 24 | - The debugger app might be slowed down if you enabled the `trace` feature and visited the `Trace` tab, because it will load and parse the source map for every selected action. 25 | 26 | ## Other documentations 27 | 28 | - [Getting Started](getting-started.md) 29 | - [Debugger Integration](debugger-integration.md) 30 | - [React DevTools Integration](react-devtools-integration.md) 31 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 32 | - [Shortcut references](shortcut-references.md) 33 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 34 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 35 | - [Config file in home directory](config-file-in-home-directory.md) 36 | - [Troubleshooting](troubleshooting.md) 37 | - [Contributing](contributing.md) 38 | -------------------------------------------------------------------------------- /docs/shortcut-references.md: -------------------------------------------------------------------------------- 1 | # Shortcut references 2 | 3 | This section will explain about the following items in RNDebugger. 4 | 5 | - [Content menu](#context-menu) 6 | - [Touch Bar](#touch-bar-in-macos) 7 | - [Keyboard shortcuts](#keyboard-shortcuts) 8 | 9 | ## Context menu 10 | 11 | We have context menu (right-click) for provides useful features: 12 | 13 | ![Context menu](https://cloud.githubusercontent.com/assets/3001525/25920996/5c488966-3606-11e7-8d0c-cb564671067b.gif) 14 | 15 | - Reload 16 | - Toggle Elements Inspector 17 | - Show Developer Menu [iOS only] 18 | - Enable / Disable [Network Inspect](debugger-integration.md#how-network-inspect-works) 19 | - Log AsyncStorage content 20 | - Clear AsyncStorage 21 | 22 | It includes the developer menu features, these would be useful for real device, instead of open developer menu in device manually. 23 | 24 | ## Keyboard shortcuts 25 | 26 | - Reload JS (macOS: `Command+R`, Windows / Linux: `Ctrl+R`) 27 | - Toggle Elements Inspector (macOS: `Command+I`, Windows / Linux: `Ctrl+I`) 28 | - New Debugger Window (macOS: `Command+T`, Windows / Linux: `Ctrl+T`) 29 | - Toggle Developer Tools (macOS: `Command+Option+I`, Windows / Linux: `Ctrl+Alt+I`) 30 | - Toggle Redux DevTools (macOS: `Command+Option+J`, Windows / Linux: `Ctrl+Alt+J`) 31 | - Toggle React DevTools (macOS: `Command+Option+K`, Windows / Linux: `Ctrl+Alt+K`) 32 | - Quickly into search field of React DevTools (Type `/`) 33 | 34 | You can also read [Keyboard Shortcuts Reference of Chrome Developer Tools](https://developers.google.com/web/tools/chrome-devtools/shortcuts). 35 | 36 | ## Touch Bar in macOS 37 | 38 | touch-bar 39 | 40 | The `Redux Slider` will shown on right if you're using [`Redux API`](redux-devtools-integration.md), 41 | 42 | If your Mac haven't TouchBar support and you still want to use the feature, you can use [Touché](https://redsweater.com/touche/). 43 | 44 | ## Other documentations 45 | 46 | - [Getting Started](getting-started.md) 47 | - [Debugger Integration](debugger-integration.md) 48 | - [React DevTools Integration](react-devtools-integration.md) 49 | - [Redux DevTools Integration](redux-devtools-integration.md) 50 | - [Apollo Client DevTools Integration](apollo-client-devtools-integration.md) 51 | - [Network inspect of Chrome Developer Tools](network-inspect-of-chrome-devtools.md) 52 | - [Enable open in editor in console](enable-open-in-editor-in-console.md) 53 | - [Config file in home directory](config-file-in-home-directory.md) 54 | - [Troubleshooting](troubleshooting.md) 55 | - [Contributing](contributing.md) 56 | -------------------------------------------------------------------------------- /electron/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Native Debugger 6 | 7 | 8 | 9 |
10 |
Loading...
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /electron/config/__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`openConfigFile 1`] = ` 4 | { 5 | "config": { 6 | "autoUpdate": true, 7 | "defaultNetworkInspect": false, 8 | "defaultRNPackagerPorts": [ 9 | 8081, 10 | ], 11 | "defaultReactDevToolsPort": 19567, 12 | "defaultReactDevToolsTheme": "RNDebugger", 13 | "editor": "", 14 | "timesJSLoadToRefreshDevTools": -1, 15 | "windowBounds": { 16 | "frame": true, 17 | }, 18 | }, 19 | } 20 | `; 21 | 22 | exports[`readConfig 1`] = ` 23 | { 24 | "config": { 25 | "autoUpdate": true, 26 | "defaultNetworkInspect": false, 27 | "defaultRNPackagerPorts": [ 28 | 8081, 29 | ], 30 | "defaultReactDevToolsPort": 19567, 31 | "defaultReactDevToolsTheme": "RNDebugger", 32 | "editor": "", 33 | "timesJSLoadToRefreshDevTools": -1, 34 | "windowBounds": { 35 | "frame": true, 36 | }, 37 | }, 38 | } 39 | `; 40 | 41 | exports[`readConfig 2`] = ` 42 | { 43 | "config": { 44 | "autoUpdate": false, 45 | }, 46 | } 47 | `; 48 | 49 | exports[`readConfig 3`] = ` 50 | { 51 | "config": { 52 | "autoUpdate": true, 53 | "defaultNetworkInspect": false, 54 | "defaultRNPackagerPorts": [ 55 | 8081, 56 | ], 57 | "defaultReactDevToolsPort": 19567, 58 | "defaultReactDevToolsTheme": "RNDebugger", 59 | "editor": "", 60 | "timesJSLoadToRefreshDevTools": -1, 61 | "windowBounds": { 62 | "frame": true, 63 | }, 64 | }, 65 | "error": [SyntaxError: Unexpected 'i' at line 1 column 16 of the JSON5 data. Still to read: "is_broken, }"], 66 | "isConfigBroken": true, 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /electron/config/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | jest.mock('electron', () => ({ 5 | shell: { 6 | openPath: jest.fn(), 7 | }, 8 | })) 9 | 10 | const testFile = path.join(__dirname, 'config_test') 11 | 12 | beforeAll(() => fs.existsSync(testFile) && fs.unlinkSync(testFile)) 13 | 14 | /* eslint-disable global-require */ 15 | test('readConfig', () => { 16 | const { readConfig } = require('..') 17 | 18 | expect(readConfig(testFile)).toMatchSnapshot() 19 | 20 | // User custom config 21 | fs.writeFileSync(testFile, '{ autoUpdate: false, }') 22 | expect(readConfig(testFile)).toMatchSnapshot() 23 | 24 | // Broken config 25 | fs.writeFileSync(testFile, '{ autoUpdate: is_broken, }') 26 | expect(readConfig(testFile)).toMatchSnapshot() 27 | }) 28 | 29 | test('openConfigFile', () => { 30 | const { readConfig, openConfigFile } = require('..') 31 | const { shell } = require('electron') 32 | 33 | openConfigFile(testFile) 34 | expect(shell.openPath).toBeCalledWith(testFile) 35 | shell.openPath.mockClear() 36 | 37 | fs.unlinkSync(testFile) 38 | openConfigFile(testFile) 39 | expect(readConfig(testFile)).toMatchSnapshot() 40 | }) 41 | -------------------------------------------------------------------------------- /electron/config/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import json5 from 'json5' 4 | import { shell } from 'electron' 5 | import template from './template' 6 | 7 | export const filePath = path.join( 8 | process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'], 9 | '.rndebuggerrc', 10 | ) 11 | 12 | export const readConfig = (configFile = filePath) => { 13 | if (!fs.existsSync(configFile)) { 14 | // Create a new one 15 | fs.writeFileSync(configFile, template) 16 | return { config: json5.parse(template) } 17 | } 18 | try { 19 | // eslint-disable-next-line 20 | return { config: json5.parse(fs.readFileSync(configFile, 'utf-8')) }; 21 | } catch (error) { 22 | // Alert parse config not successful 23 | return { config: json5.parse(template), isConfigBroken: true, error } 24 | } 25 | } 26 | 27 | export const openConfigFile = (configFile = filePath) => { 28 | readConfig() 29 | shell.openPath(configFile) 30 | } 31 | -------------------------------------------------------------------------------- /electron/config/template.js: -------------------------------------------------------------------------------- 1 | // json5 2 | module.exports = `{ 3 | // Font family of the debugger window 4 | // fontFamily: 'monaco, Consolas, Lucida Console, monospace', 5 | 6 | // Zoom level of the debugger window, it will override persited zoomLevel 7 | // zoomLevel: 0, 8 | 9 | // Settings of debugger window, 10 | windowBounds: { 11 | // Size of the debugger window, it will override persisted size 12 | // width: 1024, 13 | // height: 750, 14 | 15 | // Show frame for debugger window 16 | // but due to https://github.com/electron/electron/issues/3647 17 | // so we can't have custom title bar if no frame 18 | // titleBarStyle: 'hidden', 19 | frame: true, 20 | }, 21 | 22 | // Auto check update on RNDebugger startup 23 | autoUpdate: true, 24 | 25 | // RNDebugger will open debugger window with the ports when app launched 26 | defaultRNPackagerPorts: [8081], 27 | 28 | // Env for 29 | // open React DevTools source file link 30 | // and enable open in editor for console log for RNDebugger 31 | editor: '', 32 | 33 | // Set default react-devtools theme (default is match Chrome DevTools theme) 34 | // but the default theme doesn't change manually changed theme 35 | // see https://github.com/facebook/react-devtools/blob/master/frontend/Themes/Themes.js to get more 36 | defaultReactDevToolsTheme: 'RNDebugger', 37 | 38 | // Set default react-devtools port (default is \`19567+\` if it is not being used). 39 | // The devtools backend of React Native will use the port to connect to the devtools server. 40 | // You should use that if you have some rules for binding port. 41 | // (like https://github.com/jhen0409/react-native-debugger/issues/397) 42 | defaultReactDevToolsPort: 19567, 43 | 44 | // Enable Network Inspect by default 45 | // See https://github.com/jhen0409/react-native-debugger/blob/master/docs/network-inspect-of-chrome-devtools.md 46 | defaultNetworkInspect: false, 47 | 48 | // Refresh devtools when doing JS reload every N times. (-1 for disabled) 49 | // This can effectively avoid possible memory leaks (Like 50 | // https://github.com/jhen0409/react-native-debugger/issues/405) in devtools. 51 | timesJSLoadToRefreshDevTools: -1, 52 | } 53 | ` 54 | -------------------------------------------------------------------------------- /electron/context-menu.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import contextMenu from 'electron-context-menu' 3 | import { readConfig } from './config' 4 | import { 5 | toggleDevTools, n, item, separator, 6 | } from './menu/common' 7 | 8 | const invokeDevMethod = (win, name) => win.webContents.executeJavaScript( 9 | `window.invokeDevMethod && window.invokeDevMethod('${name}')`, 10 | ) 11 | 12 | export const registerContextMenu = (win) => { 13 | const { config } = readConfig() 14 | const defaultContextMenuItems = [ 15 | item('Toggle Developer Tools', n, () => toggleDevTools(win, 'chrome')), 16 | item('Toggle React DevTools', n, () => toggleDevTools(win, 'react')), 17 | item('Toggle Redux DevTools', n, () => toggleDevTools(win, 'redux')), 18 | ] 19 | let networkInspectEnabled = !!config.networkInspect 20 | let availableMethods = [] 21 | contextMenu({ 22 | window: win, 23 | showInspectElement: process.env.NODE_ENV === 'development', 24 | prepend: () => [ 25 | availableMethods.includes('reload') 26 | && item('Reload JS', n, () => invokeDevMethod(win, 'reload')), 27 | availableMethods.includes('toggleElementInspector') 28 | && item('Toggle Element Inspector', n, () => invokeDevMethod(win, 'toggleElementInspector')), 29 | availableMethods.includes('show') 30 | && item('Show Developer Menu', n, () => invokeDevMethod(win, 'show')), 31 | item( 32 | networkInspectEnabled 33 | ? 'Disable Network Inspect' 34 | : 'Enable Network Inspect', 35 | n, 36 | () => invokeDevMethod(win, 'networkInspect'), 37 | ), 38 | availableMethods.includes('showAsyncStorage') 39 | && item('Log AsyncStorage content', n, () => invokeDevMethod(win, 'showAsyncStorage')), 40 | availableMethods.includes('clearAsyncStorage') 41 | && item('Clear AsyncStorage', n, () => invokeDevMethod(win, 'clearAsyncStorage')), 42 | separator, 43 | ] 44 | .filter((menuItem) => !!menuItem) 45 | .concat(defaultContextMenuItems), 46 | }) 47 | 48 | const listener = (event, data) => { 49 | availableMethods = data.availableMethods || availableMethods 50 | networkInspectEnabled = typeof data.networkInspectEnabled === 'boolean' 51 | ? data.networkInspectEnabled 52 | : networkInspectEnabled 53 | } 54 | 55 | ipcMain.on(`context-menu-available-methods-update-${win.id}`, listener) 56 | return () => { 57 | ipcMain.off(`context-menu-available-methods-update-${win.id}`, listener) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /electron/debug.js: -------------------------------------------------------------------------------- 1 | require('electron-debug')(); // eslint-disable-line 2 | -------------------------------------------------------------------------------- /electron/devtools.js: -------------------------------------------------------------------------------- 1 | export const getCatchConsoleLogScript = (port) => ` 2 | window.__RN_PACKAGER_MATCHER__ = /^http:\\/\\/[^:]+:${port}/; 3 | if (!window.__INJECT_OPEN_IN_EDITOR_SCRIPT__) { 4 | const rndHelperQuery = 'iframe[data-devtools-extension="RNDebugger devtools helper"]'; 5 | document.addEventListener('click', event => { 6 | if (!window.__IS_OPEN_IN_EDITOR_ENABLED__) { 7 | return; 8 | } 9 | const { target } = event; 10 | if (target.className === 'devtools-link') { 11 | const source = target.title; 12 | if (source && source.match(window.__RN_PACKAGER_MATCHER__)) { 13 | const rndHelper = document.querySelector(rndHelperQuery); 14 | if (rndHelper && rndHelper.contentWindow) { 15 | rndHelper.contentWindow.postMessage( 16 | { 17 | type: 'open-in-editor', 18 | source: source.replace(window.__RN_PACKAGER_MATCHER__, '') 19 | }, 20 | '*' 21 | ); 22 | return event.stopPropagation(); 23 | } 24 | } 25 | } 26 | }, true); 27 | window.__INJECT_OPEN_IN_EDITOR_SCRIPT__ = true; 28 | } 29 | ` 30 | 31 | export const catchConsoleLogLink = (win, host = 'localhost', port = 8081) => { 32 | if (win.devToolsWebContents) { 33 | return win.devToolsWebContents.executeJavaScript(`(() => { 34 | ${getCatchConsoleLogScript(host, port)} 35 | })()`) 36 | } 37 | } 38 | 39 | export const removeUnecessaryTabs = (win) => { 40 | if ( 41 | process.env.NODE_ENV === 'production' 42 | && !process.env.DEBUG_RNDEBUGGER 43 | && win.devToolsWebContents 44 | ) { 45 | return win.devToolsWebContents.executeJavaScript(`(() => { 46 | const tabbedPane = UI.inspectorView.tabbedPane; 47 | if (tabbedPane) { 48 | tabbedPane.closeTab('elements'); 49 | tabbedPane.closeTab('security'); 50 | tabbedPane.closeTab('audits'); 51 | tabbedPane.closeTab('audits2'); 52 | tabbedPane.closeTab('lighthouse'); 53 | 54 | tabbedPane.leftToolbar().element.remove(); 55 | } 56 | })()`) 57 | } 58 | } 59 | 60 | export const activeTabs = (win) => { 61 | if (win.devToolsWebContents) { 62 | // Active network tab so we can do clearNetworkLogs 63 | return win.devToolsWebContents.executeJavaScript(`(() => { 64 | DevToolsAPI.showPanel('network'); 65 | DevToolsAPI.showPanel('console'); 66 | })()`) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /electron/extensions.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { session } from 'electron' 3 | 4 | export default async () => { 5 | if (process.env.NODE_ENV === 'development') { 6 | await session.defaultSession.loadExtension( 7 | path.resolve('dist/devtools-helper/'), 8 | { allowFileAccess: true }, 9 | ) 10 | await session.defaultSession.loadExtension( 11 | path.join( 12 | __dirname, 13 | '../node_modules/apollo-client-devtools/build', 14 | ), 15 | { allowFileAccess: true }, 16 | ) 17 | } else if (process.env.PACKAGE === 'no') { 18 | await session.defaultSession.loadExtension( 19 | path.join(__dirname, 'devtools-helper/'), 20 | { allowFileAccess: true }, 21 | ) 22 | await session.defaultSession.loadExtension( 23 | path.join( 24 | __dirname, 25 | 'node_modules/apollo-client-devtools/build', 26 | ), 27 | { allowFileAccess: true }, 28 | ) 29 | } else { 30 | await session.defaultSession.loadExtension( 31 | path.join(__dirname, '../devtools-helper/'), 32 | { allowFileAccess: true }, 33 | ) 34 | await session.defaultSession.loadExtension( 35 | path.join( 36 | __dirname, 37 | '../ac-devtools-ext-build/', // See package script for why 38 | ), 39 | { allowFileAccess: true }, 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /electron/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/logo.icns -------------------------------------------------------------------------------- /electron/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/logo.ico -------------------------------------------------------------------------------- /electron/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/logo.png -------------------------------------------------------------------------------- /electron/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { 3 | app, ipcMain, session, BrowserWindow, Menu, 4 | } from 'electron' 5 | import { initialize } from '@electron/remote/main' 6 | import normalizeHeaderCase from 'header-case-normalizer' 7 | import installExtensions from './extensions' 8 | import { checkWindowInfo, createWindow } from './window' 9 | import { startListeningHandleURL, handleURL, parseUrl } from './url-handle' 10 | import { createMenuTemplate } from './menu' 11 | import { readConfig } from './config' 12 | import { sendSyncState } from './sync-state' 13 | 14 | initialize() 15 | 16 | // Uncomment if want to debug devtools backend 17 | // app.commandLine.appendSwitch('remote-debugging-port', '9222'); 18 | 19 | app.commandLine.appendSwitch('disable-http-cache') 20 | 21 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 1 22 | 23 | const iconPath = path.resolve(__dirname, 'logo.png') 24 | const defaultOptions = { iconPath } 25 | 26 | const findWindow = async (_, port) => { 27 | const browserWindows = BrowserWindow.getAllWindows() 28 | const browserWindow = await browserWindows.reduce(async (promise, win) => { 29 | const acc = await promise 30 | if (acc) return acc 31 | 32 | const { isWorkerRunning, isPortSettingRequired, location } = await checkWindowInfo(win) 33 | return (!isWorkerRunning || location.port === port) 34 | && !isPortSettingRequired 35 | ? win 36 | : null 37 | }, Promise.resolve(null)) 38 | if (!browserWindow) createWindow(defaultOptions) 39 | if (browserWindow) { 40 | if (browserWindow.isMinimized()) browserWindow.restore() 41 | browserWindow.focus() 42 | } 43 | return browserWindow 44 | } 45 | 46 | const handleCommandLine = async (commandLine) => { 47 | const url = commandLine.find((arg) => arg.startsWith('rndebugger://')) 48 | if (!url) { 49 | return 50 | } 51 | await handleURL(findWindow, url) 52 | } 53 | 54 | if (process.platform === 'linux') { 55 | const singleInstanceLock = app.requestSingleInstanceLock() 56 | if (!singleInstanceLock) { 57 | process.exit() 58 | } else { 59 | app.on('second-instance', async (event, commandLine) => { 60 | await handleCommandLine(commandLine) 61 | }) 62 | } 63 | } 64 | 65 | startListeningHandleURL(findWindow) 66 | 67 | ipcMain.on('check-port-available', async (event, arg) => { 68 | const port = Number(arg) 69 | const windows = BrowserWindow.getAllWindows() 70 | const isPortAvailable = await windows.reduce(async (promise, win) => { 71 | const isAvailable = await promise 72 | if (!isAvailable) return false 73 | 74 | if (win.webContents !== event.sender) { 75 | const { isPortSettingRequired, location } = await checkWindowInfo(win) 76 | if (location.port === port && !isPortSettingRequired) { 77 | return false 78 | } 79 | } 80 | return true 81 | }, Promise.resolve(true)) 82 | event.sender.send('check-port-available-reply', isPortAvailable) 83 | }) 84 | 85 | ipcMain.on('sync-state', sendSyncState) 86 | 87 | app.on('activate', () => { 88 | if (BrowserWindow.getAllWindows().length !== 0) return 89 | createWindow(defaultOptions) 90 | }) 91 | 92 | app.on('new-window-for-tab', () => { 93 | createWindow({ ...defaultOptions, isPortSettingRequired: true }) 94 | }) 95 | 96 | app.on('window-all-closed', () => { 97 | if (process.platform !== 'darwin') { 98 | app.quit() 99 | } 100 | }) 101 | 102 | if (process.platform === 'darwin') { 103 | app.on('before-quit', async (event) => { 104 | event.preventDefault() 105 | BrowserWindow.getAllWindows().forEach((win) => { 106 | win.removeAllListeners('close') 107 | win.close() 108 | }) 109 | process.exit() 110 | }) 111 | } 112 | 113 | app.on('ready', async () => { 114 | await installExtensions() 115 | 116 | const { config } = readConfig() 117 | 118 | let { defaultRNPackagerPorts } = config 119 | if (!Array.isArray(defaultRNPackagerPorts)) { 120 | defaultRNPackagerPorts = [8081] 121 | } 122 | 123 | if (process.platform === 'linux') { 124 | const url = process.argv.find((arg) => arg.startsWith('rndebugger://')) 125 | const query = url ? parseUrl(url) : undefined 126 | if (query && query.port) { 127 | defaultRNPackagerPorts = [query.port] 128 | } 129 | } 130 | 131 | defaultRNPackagerPorts.forEach((port) => { 132 | createWindow({ port, ...defaultOptions }) 133 | }) 134 | 135 | const menuTemplate = createMenuTemplate(defaultOptions) 136 | const menu = Menu.buildFromTemplate(menuTemplate) 137 | Menu.setApplicationMenu(menu) 138 | 139 | const replaceHeaderPrefix = '__RN_DEBUGGER_SET_HEADER_REQUEST_' 140 | session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { 141 | delete details.requestHeaders.Origin 142 | Object.entries(details.requestHeaders).forEach(([header, value]) => { 143 | if (header.startsWith(replaceHeaderPrefix)) { 144 | const originalHeader = normalizeHeaderCase( 145 | header.replace(replaceHeaderPrefix, ''), 146 | ) 147 | details.requestHeaders[originalHeader] = value 148 | delete details.requestHeaders[header] 149 | } 150 | }) 151 | callback({ cancel: false, requestHeaders: details.requestHeaders }) 152 | }) 153 | }) 154 | 155 | // Pass all certificate errors in favor of Network Inspect feature 156 | app.on( 157 | 'certificate-error', 158 | (event, webContents, url, error, certificate, callback) => { 159 | event.preventDefault() 160 | callback(true) 161 | }, 162 | ) 163 | -------------------------------------------------------------------------------- /electron/menu/common.js: -------------------------------------------------------------------------------- 1 | export const toggleDevTools = (win, type) => { 2 | if (!win || !type) return 3 | if (type === 'chrome') { 4 | win.toggleDevTools() 5 | return 6 | } 7 | win.webContents.send('toggle-devtools', type) 8 | } 9 | 10 | export const toggleFullscreen = (win) => win && win.setFullScreen(!win.isFullScreen()) 11 | export const setAlwaysOnTop = (win, checked) => win && win.setAlwaysOnTop(checked) 12 | export const reload = (win) => win && win.webContents.reload() 13 | export const close = (win) => win && win.close() 14 | export const zoom = (win, val) => { 15 | if (!win) return 16 | const contents = win.webContents 17 | contents.zoomLevel += val 18 | } 19 | export const resetZoom = (win) => { 20 | if (win) { 21 | win.webContents.zoomLevel = 0 22 | } 23 | } 24 | export const toggleOpenInEditor = (win) => win && win.webContents.executeJavaScript('window.toggleOpenInEditor()') 25 | 26 | export const menu = (label, submenu, role) => ({ label, submenu, role }) 27 | export const item = (label, accelerator, click, rest) => ({ 28 | label, 29 | accelerator, 30 | click, 31 | ...rest, 32 | }) 33 | export const separator = { type: 'separator' } 34 | export const n = undefined 35 | -------------------------------------------------------------------------------- /electron/menu/darwin.js: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow } from 'electron' 2 | import { createWindow } from '../window' 3 | import checkUpdate from '../update' 4 | import { 5 | menu, 6 | item, 7 | separator, 8 | n, 9 | toggleDevTools, 10 | toggleFullscreen, 11 | setAlwaysOnTop, 12 | reload, 13 | zoom, 14 | resetZoom, 15 | toggleOpenInEditor, 16 | } from './common' 17 | import { haveOpenedWindow, showAboutDialog } from './dialog' 18 | import { openConfigFile } from '../config' 19 | import { isSyncState, toggleSyncState } from '../sync-state' 20 | 21 | const getWin = () => BrowserWindow.getFocusedWindow() 22 | 23 | const viewItems = process.env.NODE_ENV === 'developemnt' 24 | ? [item('Reload Window', 'Alt+Command+R', () => reload(getWin()))] 25 | : [] 26 | 27 | export default ({ iconPath }) => [ 28 | menu('React Native Debugger', [ 29 | item('About', n, () => showAboutDialog(iconPath)), 30 | item('Check for Updates...', n, () => checkUpdate(iconPath, true)), 31 | separator, 32 | item('Hide', 'Command+H', n, { selector: 'hide:' }), 33 | item('Hide Others', 'Command+Shift+H', n, { selector: 'hideOtherApplications:' }), 34 | item('Show All', n, n, { selector: 'unhideAllApplications:' }), 35 | separator, 36 | item('Quit', 'Command+Q', () => app.quit()), 37 | ]), 38 | menu( 39 | 'Debugger', 40 | [ 41 | item('New Window', 'Command+T', () => createWindow({ iconPath, isPortSettingRequired: haveOpenedWindow() })), 42 | item('Enable Open in Editor for Console Log', n, () => toggleOpenInEditor(getWin()), { 43 | type: 'checkbox', 44 | checked: false, 45 | }), 46 | item('Toggle Device Sync', n, toggleSyncState, { 47 | type: 'checkbox', 48 | checked: isSyncState(), 49 | }), 50 | item('Open Config File', n, () => openConfigFile()), 51 | separator, 52 | item('Minimize', 'Command+M', n, { selector: 'performMiniaturize:' }), 53 | item('Close', 'Command+W', n, { selector: 'performClose:' }), 54 | separator, 55 | item('Bring All to Front', n, n, { selector: 'arrangeInFront:' }), 56 | item('Stay in Front', n, ({ checked }) => setAlwaysOnTop(getWin(), checked), { 57 | type: 'checkbox', 58 | checked: false, 59 | }), 60 | ], 61 | 'window', 62 | ), 63 | menu('Edit', [ 64 | item('Undo', 'Command+Z', n, { selector: 'undo:' }), 65 | item('Redo', 'Shift+Command+Z', n, { selector: 'redo:' }), 66 | separator, 67 | item('Cut', 'Command+X', n, { selector: 'cut:' }), 68 | item('Copy', 'Command+C', n, { selector: 'copy:' }), 69 | item('Paste', 'Command+V', n, { selector: 'paste:' }), 70 | item('Select All', 'Command+A', n, { selector: 'selectAll:' }), 71 | ]), 72 | menu( 73 | 'View', 74 | viewItems.concat([ 75 | item('Toggle Full Screen', 'F11', () => toggleFullscreen(getWin())), 76 | item('Toggle Developer Tools', 'Alt+Command+I', () => toggleDevTools(getWin(), 'chrome')), 77 | item('Toggle React DevTools', 'Alt+Command+J', () => toggleDevTools(getWin(), 'react')), 78 | item('Toggle Redux DevTools', 'Alt+Command+K', () => toggleDevTools(getWin(), 'redux')), 79 | separator, 80 | item('Zoom In', 'Command+=', () => zoom(getWin(), 1)), 81 | item('Zoom Out', 'Command+-', () => zoom(getWin(), -1)), 82 | item('Reset Zoom', 'Command+0', () => resetZoom(getWin())), 83 | ]), 84 | ), 85 | menu('Help', [ 86 | item('Documentation', n, () => shell.openExternal('https://github.com/jhen0409/react-native-debugger/tree/master/docs')), 87 | item('Issues', n, () => shell.openExternal('https://github.com/jhen0409/react-native-debugger/issues')), 88 | item('Open Collective', n, () => shell.openExternal('https://opencollective.com/react-native-debugger')), 89 | ]), 90 | ] 91 | -------------------------------------------------------------------------------- /electron/menu/dialog.js: -------------------------------------------------------------------------------- 1 | import { app, dialog, BrowserWindow } from 'electron' 2 | import multiline from 'multiline-template' 3 | 4 | const appName = app.name 5 | const detail = multiline` 6 | | Created by Jhen-Jie Hong 7 | | (https://github.com/jhen0409) 8 | 9 | | This software includes the following projects: 10 | 11 | | https://github.com/facebook/react-devtools 12 | | https://github.com/reduxjs/redux-devtools 13 | | https://github.com/apollographql/apollo-client-devtools 14 | ` 15 | 16 | export const showAboutDialog = (iconPath) => dialog.showMessageBoxSync({ 17 | title: 'About', 18 | message: `${appName} ${app.getVersion()}`, 19 | detail, 20 | icon: iconPath, 21 | buttons: [], 22 | }) 23 | 24 | export const haveOpenedWindow = () => !!BrowserWindow.getAllWindows().length 25 | -------------------------------------------------------------------------------- /electron/menu/index.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | 3 | import createMenuTemplateDarwin from './darwin' 4 | import createMenuTemplateLinuxWin from './linux+win' 5 | 6 | const createMenuTemplate = process.platform === 'darwin' 7 | ? createMenuTemplateDarwin 8 | : createMenuTemplateLinuxWin 9 | 10 | export { createMenuTemplate } 11 | -------------------------------------------------------------------------------- /electron/menu/linux+win.js: -------------------------------------------------------------------------------- 1 | import { shell, BrowserWindow } from 'electron' 2 | import { createWindow } from '../window' 3 | import checkUpdate from '../update' 4 | import { 5 | menu, 6 | item, 7 | separator, 8 | n, 9 | toggleDevTools, 10 | toggleFullscreen, 11 | setAlwaysOnTop, 12 | reload, 13 | close, 14 | zoom, 15 | resetZoom, 16 | toggleOpenInEditor, 17 | } from './common' 18 | import { 19 | showAboutDialog, 20 | haveOpenedWindow, 21 | } from './dialog' 22 | import { openConfigFile } from '../config' 23 | import { toggleSyncState, isSyncState } from '../sync-state' 24 | 25 | const getWin = () => BrowserWindow.getFocusedWindow() 26 | const viewItems = process.env.NODE_ENV === 'developemnt' 27 | ? [item('Reload Window', 'Alt+CTRL+R', () => reload(getWin()))] 28 | : [] 29 | 30 | export default ({ iconPath }) => [ 31 | menu('RND', [ 32 | item('About', n, () => showAboutDialog(iconPath)), 33 | item('Check for Updates...', n, () => checkUpdate(iconPath, true)), 34 | separator, 35 | item('Stay in Front', n, ({ checked }) => setAlwaysOnTop(getWin(), checked), { 36 | type: 'checkbox', 37 | checked: false, 38 | }), 39 | ]), 40 | menu( 41 | 'Debugger', 42 | [ 43 | item('New Window', 'Ctrl+T', () => createWindow({ iconPath, isPortSettingRequired: haveOpenedWindow() })), 44 | item('Enable Open in Editor for Console Log', n, () => toggleOpenInEditor(getWin()), { 45 | type: 'checkbox', 46 | checked: false, 47 | }), 48 | item('Toggle Device Sync', n, toggleSyncState, { 49 | type: 'checkbox', 50 | checked: isSyncState(), 51 | }), 52 | item('Open Config File', n, () => openConfigFile()), 53 | separator, 54 | item('Close', 'Ctrl+W', () => close(getWin())), 55 | ], 56 | 'window', 57 | ), 58 | menu('Edit', [ 59 | item('Undo', 'Ctrl+Z', n, { selector: 'undo:' }), 60 | item('Redo', 'Shift+Ctrl+Z', n, { selector: 'redo:' }), 61 | separator, 62 | item('Cut', 'Ctrl+X', n, { selector: 'cut:' }), 63 | item('Copy', 'Ctrl+C', n, { selector: 'copy:' }), 64 | item('Paste', 'Ctrl+V', n, { selector: 'paste:' }), 65 | item('Select All', 'Ctrl+A', n, { selector: 'selectAll:' }), 66 | ]), 67 | menu( 68 | 'View', 69 | viewItems.concat([ 70 | item('Toggle Full Screen', 'F11', () => toggleFullscreen(getWin())), 71 | item('Toggle Developer Tools', 'Alt+Ctrl+I', () => toggleDevTools(getWin(), 'chrome')), 72 | item('Toggle React DevTools', 'Alt+Ctrl+J', () => toggleDevTools(getWin(), 'react')), 73 | item('Toggle Redux DevTools', 'Alt+Ctrl+K', () => toggleDevTools(getWin(), 'redux')), 74 | separator, 75 | item('Zoom In', 'Ctrl+=', () => zoom(getWin(), 1)), 76 | item('Zoom Out', 'Ctrl+-', () => zoom(getWin(), -1)), 77 | item('Reset Zoom', 'Ctrl+0', () => resetZoom(getWin())), 78 | ]), 79 | ), 80 | menu('Help', [ 81 | item('Documentation', n, () => shell.openExternal('https://github.com/jhen0409/react-native-debugger/tree/master/docs')), 82 | item('Issues', n, () => shell.openExternal('https://github.com/jhen0409/react-native-debugger/issues')), 83 | item('Open Collective', n, () => shell.openExternal('https://opencollective.com/react-native-debugger')), 84 | ]), 85 | ] 86 | -------------------------------------------------------------------------------- /electron/sync-state.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | 3 | let syncState = false 4 | 5 | export const isSyncState = () => syncState 6 | 7 | // Take by renderer 8 | global.isSyncState = isSyncState 9 | 10 | export const toggleSyncState = () => { 11 | syncState = !syncState 12 | } 13 | 14 | export const sendSyncState = (event, payload) => { 15 | if (!isSyncState) return 16 | 17 | BrowserWindow.getAllWindows() 18 | .filter((win) => Number(win.webContents.id) !== event.sender.id) 19 | .forEach((win) => { 20 | win.webContents.send('sync-state', payload) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /electron/update.js: -------------------------------------------------------------------------------- 1 | import { app, dialog, shell } from 'electron' 2 | import GhReleases from 'electron-gh-releases' 3 | import fetch from 'electron-fetch' 4 | 5 | const repo = 'jhen0409/react-native-debugger' 6 | 7 | const getFeed = () => fetch(`https://raw.githubusercontent.com/${repo}/master/auto_update.json`).then((res) => res.json()) 8 | 9 | const showDialog = ({ 10 | icon, buttons, message, detail, 11 | }) => dialog.showMessageBoxSync({ 12 | type: 'info', 13 | buttons, 14 | title: 'React Native Debugger', 15 | icon, 16 | message, 17 | detail, 18 | }) 19 | 20 | const notifyUpdateAvailable = ({ icon, detail }) => { 21 | const index = showDialog({ 22 | message: 'A newer version is available.', 23 | buttons: ['Download', 'Later'], 24 | icon, 25 | detail, 26 | }) 27 | return index === 0 28 | } 29 | 30 | const notifyUpdateDownloaded = ({ icon }) => { 31 | const index = showDialog({ 32 | message: 33 | 'The newer version has been downloaded. ' 34 | + 'Please restart the application to apply the update.', 35 | buttons: ['Restart', 'Later'], 36 | icon, 37 | }) 38 | return index === 0 39 | } 40 | 41 | let checking = false 42 | 43 | export default (icon, notify) => { 44 | if (checking) return 45 | 46 | checking = true 47 | const updater = new GhReleases({ 48 | repo, 49 | currentVersion: app.getVersion(), 50 | }) 51 | 52 | updater.check(async (err, status) => { 53 | if (process.platform === 'linux' && err.message === 'This platform is not supported.') { 54 | err = null; // eslint-disable-line 55 | status = true; // eslint-disable-line 56 | } 57 | if (notify && err) { 58 | showDialog({ message: err.message, buttons: ['OK'] }) 59 | checking = false 60 | return 61 | } 62 | if (err || !status) { 63 | checking = false 64 | return 65 | } 66 | const feed = await getFeed() 67 | const detail = `${feed.name}\n\n${feed.notes}` 68 | if (notify) { 69 | const open = notifyUpdateAvailable({ icon, detail }) 70 | if (open) shell.openExternal('https://github.com/jhen0409/react-native-debugger/releases') 71 | } else if ( 72 | process.env.NODE_ENV === 'production' 73 | && process.platform === 'darwin' 74 | && notifyUpdateAvailable({ icon, detail }) 75 | ) { 76 | updater.download() 77 | console.log('[RNDebugger] Update downloading...') 78 | } 79 | checking = false 80 | }) 81 | 82 | updater.on('update-downloaded', () => { 83 | console.log('[RNDebugger] Update downloaded') 84 | if (notifyUpdateDownloaded({ icon })) { 85 | updater.install() 86 | } 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /electron/url-handle/handleURL.js: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import net from 'net' 3 | import url from 'url' 4 | import qs from 'querystring' 5 | import fs from 'fs' 6 | import * as portfile from './port' 7 | 8 | const filterPaths = (list) => { 9 | const filteredList = list.filter((dir) => { 10 | try { 11 | return fs.lstatSync(dir).isDirectory() 12 | } catch (e) { 13 | return false 14 | } 15 | }) 16 | if (!filteredList.length) { 17 | return 18 | } 19 | return filteredList 20 | } 21 | 22 | const resolveHost = (host) => ( 23 | !host || host === 'undefined' || host === 'null' ? 'localhost' : host 24 | ) 25 | 26 | export const parseUrl = (_url) => { 27 | const route = url.parse(_url) 28 | if (route.host !== 'set-debugger-loc') return 29 | const { host, port, projectRoots } = qs.parse(route.query) 30 | const query = { 31 | host: resolveHost(host), 32 | port: Number(port) || 8081, 33 | projectRoots: filterPaths(Array.isArray(projectRoots) ? projectRoots : [projectRoots]), 34 | } 35 | return query 36 | } 37 | 38 | export const handleURL = async (getWindow, path) => { 39 | const query = parseUrl(path) 40 | if (!query) { 41 | return 42 | } 43 | const payload = JSON.stringify(query) 44 | 45 | // This env will be get by new debugger window 46 | process.env.DEBUGGER_SETTING = payload 47 | const win = await getWindow(query.host, query.port) 48 | // if we can get the exists window, it will send the IPC event 49 | if (win) { 50 | win.webContents.send('set-debugger-loc', payload) 51 | } 52 | } 53 | 54 | const listenOpenURL = (getWindow) => app.on('open-url', (e, path) => { 55 | handleURL(getWindow, path) 56 | }) 57 | 58 | const createHandleURLServer = (getWindow) => net 59 | .createServer((socket) => { 60 | socket.setEncoding('utf-8') 61 | socket.on('data', async (data) => { 62 | try { 63 | const obj = JSON.parse(data) 64 | if (typeof obj.path === 'string') { 65 | await handleURL(getWindow, obj.path) 66 | } 67 | socket.write('success') 68 | } catch (e) { 69 | socket.write('fail') 70 | } finally { 71 | socket.end() 72 | } 73 | }) 74 | }) 75 | .listen(0, '127.0.0.1') 76 | .on('listening', function server() { 77 | const { port } = this.address() 78 | portfile.write(port) 79 | portfile.watchExists(port) 80 | process.on('exit', () => portfile.unlink()) 81 | 82 | console.log(`Starting listen set-debugger-loc request on port ${port}`) 83 | console.log('Will save port to `$HOME/.rndebugger_port` file') 84 | }) 85 | 86 | export default (getWindow) => { 87 | // Handle set-debugger-loc for macOS 88 | // It's can be automatically open the app 89 | listenOpenURL(getWindow) 90 | // Handle set-debugger-loc for macOS/Linux/Windows 91 | createHandleURLServer(getWindow) 92 | } 93 | -------------------------------------------------------------------------------- /electron/url-handle/index.js: -------------------------------------------------------------------------------- 1 | import startListeningHandleURL, { handleURL, parseUrl } from './handleURL' 2 | import * as port from './port' 3 | 4 | export { 5 | startListeningHandleURL, handleURL, parseUrl, port, 6 | } 7 | -------------------------------------------------------------------------------- /electron/url-handle/port.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | const homeEnv = process.platform === 'win32' ? 'USERPROFILE' : 'HOME' 5 | const portFile = path.join(process.env[homeEnv], '.rndebugger_port') 6 | let isWatching = false 7 | 8 | export const write = (port) => { 9 | fs.writeFileSync(portFile, String(port)) 10 | } 11 | 12 | export function read() { 13 | if (!fs.existsSync(portFile)) return null 14 | return Number(fs.readFileSync(portFile, 'utf8')) 15 | } 16 | 17 | export const unlink = () => { 18 | if (fs.existsSync(portFile)) { 19 | fs.unlinkSync(portFile) 20 | } 21 | } 22 | 23 | export const watchExists = (port) => { 24 | if (isWatching) return 25 | isWatching = true 26 | fs.watchFile(portFile, (curr, prev) => { 27 | if (curr.mtime !== prev.mtime) write(port) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /electron/window.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { 3 | BrowserWindow, Menu, globalShortcut, dialog, 4 | } from 'electron' 5 | import Store from 'electron-store' 6 | import { enable } from '@electron/remote/main' 7 | import autoUpdate from './update' 8 | import { catchConsoleLogLink, removeUnecessaryTabs, activeTabs } from './devtools' 9 | import { selectRNDebuggerWorkerContext } from '../app/utils/devtools' 10 | import { readConfig, filePath as configFile } from './config' 11 | import { registerContextMenu } from './context-menu' 12 | 13 | const store = new Store() 14 | 15 | const executeJavaScript = (win, script) => win.webContents.executeJavaScript(script) 16 | 17 | export const checkWindowInfo = (win) => executeJavaScript(win, 'window.checkWindowInfo()') 18 | 19 | const checkIsOpenInEditorEnabled = (win) => executeJavaScript(win, 'window.isOpenInEditorEnabled()') 20 | 21 | const changeMenuItems = (menus) => { 22 | const rootMenuItems = Menu.getApplicationMenu().items 23 | Object.entries(menus).forEach(([key, subMenu]) => { 24 | const rootMenuItem = rootMenuItems.find(({ label }) => label === key) 25 | if (!rootMenuItem || !rootMenuItem.submenu) return 26 | 27 | Object.entries(subMenu).forEach(([subKey, menuSet]) => { 28 | const menuItem = rootMenuItem.submenu.items.find( 29 | ({ label }) => label === subKey, 30 | ) 31 | if (!menuItem) return 32 | 33 | Object.assign(menuItem, menuSet) 34 | }) 35 | }) 36 | } 37 | 38 | const invokeDevMethod = (win, name) => executeJavaScript( 39 | win, 40 | `window.invokeDevMethod && window.invokeDevMethod('${name}')`, 41 | ) 42 | 43 | const registerKeyboradShortcut = (win) => { 44 | const prefix = process.platform === 'darwin' ? 'Command' : 'Ctrl' 45 | // If another window focused, register a new shortcut 46 | if ( 47 | globalShortcut.isRegistered(`${prefix}+R`) 48 | || globalShortcut.isRegistered(`${prefix}+I`) 49 | ) { 50 | globalShortcut.unregisterAll() 51 | } 52 | globalShortcut.register(`${prefix}+R`, () => invokeDevMethod(win, 'reload')) 53 | globalShortcut.register(`${prefix}+I`, () => invokeDevMethod(win, 'toggleElementInspector')) 54 | } 55 | 56 | const unregisterKeyboradShortcut = () => globalShortcut.unregisterAll() 57 | 58 | const registerShortcuts = async (win) => { 59 | registerKeyboradShortcut(win) 60 | changeMenuItems({ 61 | Debugger: { 62 | 'Stay in Front': { 63 | checked: win.isAlwaysOnTop(), 64 | }, 65 | 'Enable Open in Editor for Console Log': { 66 | checked: await checkIsOpenInEditorEnabled(win), 67 | }, 68 | }, 69 | }) 70 | } 71 | 72 | const minSize = 100 73 | export const createWindow = ({ iconPath, isPortSettingRequired, port }) => { 74 | const { config, isConfigBroken, error } = readConfig() 75 | 76 | if (isConfigBroken) { 77 | dialog.showErrorBox( 78 | 'Root config error', 79 | `Parse root config failed, please checkout \`${configFile}\`, the error trace:\n\n` 80 | + `${error}\n\n` 81 | + 'RNDebugger will load default config instead. ' 82 | + 'You can click `Debugger` -> `Open Config File` in application menu.', 83 | ) 84 | } 85 | 86 | const winBounds = store.get('winBounds') || {} 87 | const increasePosition = BrowserWindow.getAllWindows().length * 10 || 0 88 | const { 89 | width, height, x = 0, y = 0, 90 | } = winBounds 91 | const win = new BrowserWindow({ 92 | ...winBounds, 93 | width: width && width >= minSize ? width : 1024, 94 | height: height && height >= minSize ? height : 750, 95 | minWidth: minSize, 96 | minHeight: minSize, 97 | x: x + increasePosition, 98 | y: y + increasePosition, 99 | backgroundColor: '#272c37', 100 | tabbingIdentifier: 'rndebugger', 101 | webPreferences: { 102 | contextIsolation: false, 103 | nodeIntegration: true, 104 | // experimentalFeatures: true, 105 | // webSecurity: false, 106 | // webviewTag: true, // Use this for new inspector in the future 107 | }, 108 | ...config.windowBounds, 109 | }) 110 | enable(win.webContents) 111 | 112 | const isFirstWindow = BrowserWindow.getAllWindows().length === 1 113 | 114 | const { timesJSLoadToRefreshDevTools = -1 } = config 115 | win.debuggerConfig = { 116 | port, 117 | editor: config.editor, 118 | fontFamily: config.fontFamily, 119 | defaultReactDevToolsTheme: config.defaultReactDevToolsTheme, 120 | defaultReactDevToolsPort: config.defaultReactDevToolsPort, 121 | networkInspect: config.defaultNetworkInspect && 1, 122 | isPortSettingRequired: isPortSettingRequired && 1, 123 | timesJSLoadToRefreshDevTools, 124 | } 125 | win.loadURL(`file://${path.resolve(__dirname)}/app.html`) 126 | let unregisterContextMenu 127 | win.webContents.on('did-finish-load', () => { 128 | win.webContents.zoomLevel = config.zoomLevel || store.get('zoomLevel', 0) 129 | win.focus() 130 | unregisterContextMenu = registerContextMenu(win) 131 | registerShortcuts(win) 132 | if (!isPortSettingRequired) win.openDevTools() 133 | const checkUpdate = config.autoUpdate !== false 134 | if (checkUpdate && isFirstWindow) { 135 | autoUpdate(iconPath) 136 | } 137 | }) 138 | win.webContents.on('devtools-opened', async () => { 139 | const { location } = await checkWindowInfo(win) 140 | activeTabs(win) 141 | catchConsoleLogLink(win, location.host, location.port) 142 | if (config.showAllDevToolsTab !== true) { 143 | removeUnecessaryTabs(win) 144 | } 145 | selectRNDebuggerWorkerContext(win) 146 | }) 147 | win.on('show', () => { 148 | if (!win.isFocused()) return 149 | registerShortcuts(win) 150 | }) 151 | win.on('focus', () => registerShortcuts(win)) 152 | win.on('restore', () => registerShortcuts(win)) 153 | win.on('hide', () => unregisterKeyboradShortcut()) 154 | win.on('blur', () => unregisterKeyboradShortcut()) 155 | win.on('minimize', () => unregisterKeyboradShortcut()) 156 | win.close = async () => { 157 | unregisterKeyboradShortcut() 158 | store.set('winBounds', win.getBounds()) 159 | store.set('zoomLevel', win.webContents.zoomLevel) 160 | await executeJavaScript( 161 | win, 162 | 'window.beforeWindowClose && window.beforeWindowClose()', 163 | ) 164 | win.destroy() 165 | } 166 | win.on('close', (event) => { 167 | event.preventDefault() 168 | win.close() 169 | if (unregisterContextMenu) unregisterContextMenu() 170 | }) 171 | return win 172 | } 173 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": 0, 4 | "import/no-unresolved": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/test-old-bridge/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | -------------------------------------------------------------------------------- /examples/test-old-bridge/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/style-prop-object */ 2 | import { StatusBar } from 'expo-status-bar' 3 | import React from 'react' 4 | import { StyleSheet, View } from 'react-native' 5 | import ReduxApp from './examples/redux/App' 6 | import ApolloApp from './examples/apollo/App' 7 | 8 | const styles = StyleSheet.create({ 9 | container: { 10 | flex: 1, 11 | backgroundColor: '#fff', 12 | }, 13 | }) 14 | 15 | export default function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/test-old-bridge/README.md: -------------------------------------------------------------------------------- 1 | # test-old-bridge 2 | 3 | This is example created by `npx create-expo-app -t blank@48`, `"jsEngine": "jsc"` to `app.json`. 4 | 5 | The main purpose is for test functionality of React Native Debugger in old bridge. 6 | 7 | Currently the examples included 8 | - simple counter example for Redux 9 | - simple query example for Apollo Client 10 | -------------------------------------------------------------------------------- /examples/test-old-bridge/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "jsEngine": "jsc", 4 | "name": "test-old-bridge", 5 | "slug": "test-old-bridge", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": [ 16 | "**/*" 17 | ], 18 | "ios": { 19 | "supportsTablet": true 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | } 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/test-old-bridge/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/examples/test-old-bridge/assets/adaptive-icon.png -------------------------------------------------------------------------------- /examples/test-old-bridge/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/examples/test-old-bridge/assets/favicon.png -------------------------------------------------------------------------------- /examples/test-old-bridge/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/examples/test-old-bridge/assets/icon.png -------------------------------------------------------------------------------- /examples/test-old-bridge/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhen0409/react-native-debugger/bd3435a456a29e1eda017ad37b94451834b4ee3a/examples/test-old-bridge/assets/splash.png -------------------------------------------------------------------------------- /examples/test-old-bridge/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/test-old-bridge/examples/apollo/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, Text, View } from 'react-native' 3 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client' 4 | import SimpleQuery from './SimpleQuery' 5 | 6 | const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | justifyContent: 'center', 10 | alignItems: 'center', 11 | backgroundColor: '#F5FCFF', 12 | }, 13 | title: { 14 | marginBottom: 20, 15 | fontSize: 25, 16 | textAlign: 'center', 17 | fontWeight: 'bold', 18 | }, 19 | }) 20 | 21 | const client = new ApolloClient({ 22 | uri: 'https://spacex-production.up.railway.app/', 23 | cache: new InMemoryCache(), 24 | }) 25 | 26 | export default function App() { 27 | return ( 28 | 29 | 30 | Apollo Client example 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /examples/test-old-bridge/examples/apollo/SimpleQuery.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, Text, Button } from 'react-native' 3 | import { useQuery } from '@apollo/client' 4 | import gql from 'graphql-tag' 5 | 6 | const styles = StyleSheet.create({ 7 | text: { 8 | fontSize: 20, 9 | textAlign: 'center', 10 | margin: 10, 11 | }, 12 | }) 13 | 14 | const GET_DATA = gql` 15 | query ExampleQuery { 16 | company { 17 | name 18 | ceo 19 | employees 20 | } 21 | } 22 | ` 23 | 24 | export default function SimpleQuery() { 25 | const { loading, error, data, refetch } = useQuery(GET_DATA) 26 | 27 | if (loading) return Loading... 28 | if (error) return Error :({error.message}) 29 | 30 | return ( 31 | <> 32 | Company: {data.company.name} 33 | CEO: {data.company.ceo} 34 | Employees: {data.company.employees} 35 |