├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .github ├── CODE_OF_CONDUCT.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .watchmanconfig ├── .yarnrc ├── CONTRIBUTING.md ├── LICENSE ├── OVERVIEW.md ├── README.md ├── assets ├── RelayDevtools.psd ├── active-128.png └── store-icon.png ├── babel.config.js ├── fixtures └── standalone │ └── index.html ├── flow-typed ├── chrome.js ├── jest.js └── npm │ └── react-test-renderer_v16.x.x.js ├── flow.js ├── package.json ├── packages ├── relay-devtools-core │ ├── README.md │ ├── backend.js │ ├── package.json │ ├── src │ │ ├── backend.js │ │ ├── launchEditor.js │ │ └── standalone.js │ ├── standalone.js │ ├── webpack.backend.js │ └── webpack.standalone.js └── relay-devtools │ ├── README.md │ ├── app.html │ ├── app.js │ ├── bin.js │ ├── icons │ └── icon128.png │ ├── index.js │ └── package.json ├── relay.config.js ├── shells ├── browser │ ├── chrome │ │ ├── README.md │ │ ├── build.js │ │ ├── deploy.js │ │ ├── manifest.json │ │ ├── now.json │ │ ├── test.js │ │ └── watch.js │ ├── firefox │ │ ├── README.md │ │ ├── build.js │ │ ├── deploy.js │ │ ├── manifest.json │ │ ├── now.json │ │ └── test.js │ └── shared │ │ ├── build.js │ │ ├── deploy.chrome.html │ │ ├── deploy.firefox.html │ │ ├── deploy.html │ │ ├── deploy.js │ │ ├── icons │ │ ├── disabled128.png │ │ ├── disabled16.png │ │ ├── disabled32.png │ │ ├── disabled48.png │ │ ├── enabled128.png │ │ ├── enabled16.png │ │ ├── enabled32.png │ │ └── enabled48.png │ │ ├── main.html │ │ ├── panel.html │ │ ├── popups │ │ ├── disabled.html │ │ ├── enabled.html │ │ └── shared.js │ │ ├── src │ │ ├── backend.js │ │ ├── background.js │ │ ├── contentScript.js │ │ ├── injectGlobalHook.js │ │ ├── injectedRelayDevToolsDetector.js │ │ ├── main.js │ │ ├── panel.js │ │ ├── renderer.js │ │ └── utils.js │ │ ├── webpack.backend.js │ │ └── webpack.config.js ├── dev │ ├── index.html │ ├── now.json │ ├── relay-app │ │ ├── FriendsList │ │ │ ├── App.js │ │ │ ├── FriendCard.css │ │ │ ├── FriendCard.js │ │ │ ├── Friends.css │ │ │ ├── Friends.js │ │ │ ├── __generated__ │ │ │ │ ├── AppQuery.graphql.js │ │ │ │ ├── FriendCard_user.graphql.js │ │ │ │ ├── FriendsQuery.graphql.js │ │ │ │ └── Friends_user.graphql.js │ │ │ ├── createInBrowserNetwork.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── schema.graphql │ │ └── styles.css │ ├── src │ │ ├── backend.js │ │ └── devtools.js │ └── webpack.config.js └── utils.js ├── src ├── Logger.js ├── __tests__ │ ├── __mocks__ │ │ └── cssMock.js │ ├── bridge-test.js │ ├── setupEnv.js │ ├── setupTests.js │ ├── store-test.js │ ├── storeSerializer.js │ └── utils.js ├── backend │ ├── EnvironmentWrapper.js │ ├── agent.js │ ├── index.js │ ├── types.js │ ├── util │ │ └── RelayTypes.js │ └── utils.js ├── bridge.js ├── constants.js ├── devtools │ ├── cache.js │ ├── index.js │ ├── store.js │ └── views │ │ ├── Button.css │ │ ├── Button.js │ │ ├── ButtonIcon.css │ │ ├── ButtonIcon.js │ │ ├── Components │ │ ├── ExpandCollapseToggle.css │ │ ├── ExpandCollapseToggle.js │ │ ├── InspectedElementTree.css │ │ ├── InspectedElementTree.js │ │ ├── KeyValue.css │ │ └── KeyValue.js │ │ ├── DevTools.css │ │ ├── DevTools.js │ │ ├── ErrorBoundary.css │ │ ├── ErrorBoundary.js │ │ ├── Icon.css │ │ ├── Icon.js │ │ ├── ModalDialog.css │ │ ├── ModalDialog.js │ │ ├── Network │ │ ├── Network.css │ │ └── Network.js │ │ ├── RelayLogo.css │ │ ├── RelayLogo.js │ │ ├── Settings │ │ ├── GeneralSettings.js │ │ ├── SettingsContext.js │ │ ├── SettingsModal.css │ │ ├── SettingsModal.js │ │ ├── SettingsModalContext.js │ │ ├── SettingsModalContextToggle.js │ │ └── SettingsShared.css │ │ ├── StoreInspector │ │ ├── EventLogger │ │ │ ├── AllEventsList.js │ │ │ ├── EventLogger.css │ │ │ ├── EventLogger.js │ │ │ ├── NetworkEventDisplay.js │ │ │ └── StoreEventDisplay.js │ │ ├── InspectedElementTreeStoreInspector.css │ │ ├── InspectedElementTreeStoreInspector.js │ │ ├── KeyValue.css │ │ ├── KeyValue.js │ │ ├── OptimisticUpdates.css │ │ ├── OptimisticUpdates.js │ │ ├── RecordDetails.css │ │ ├── RecordDetails.js │ │ ├── RecordList.css │ │ ├── RecordList.js │ │ ├── RecordingImportExportButtons.css │ │ ├── RecordingImportExportButtons.js │ │ ├── Snapshot.css │ │ ├── Snapshot.js │ │ ├── StoreInspector.css │ │ ├── StoreInspector.js │ │ ├── StoreTabBar.css │ │ ├── StoreTabBar.js │ │ └── utils.js │ │ ├── TabBar.css │ │ ├── TabBar.js │ │ ├── Toggle.css │ │ ├── Toggle.js │ │ ├── Tooltip.css │ │ ├── context.js │ │ ├── hooks.js │ │ ├── portaledContent.js │ │ ├── root.css │ │ └── utils.js ├── hook.js ├── registerDevToolsEventLogger.js ├── storage.js ├── types.js └── utils.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | shells/browser/chrome/build 4 | shells/browser/firefox/build 5 | shells/browser/shared/build 6 | shells/dev/dist 7 | vendor 8 | *.js.snap 9 | 10 | package-lock.json 11 | yarn.lock 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:prettier/recommended"], 3 | "plugins": ["react-hooks"], 4 | "rules": { 5 | "jsx-a11y/anchor-has-content": "off", 6 | "no-loop-func": "off", 7 | "react-hooks/exhaustive-deps": "error", 8 | "react-hooks/rules-of-hooks": "error", 9 | "no-shadow": "error", 10 | "prefer-const": "error", 11 | }, 12 | "settings": { 13 | "version": "detect" 14 | }, 15 | "globals": { 16 | "__DEV__": "readonly", 17 | "__ENABLE_LOGGER__": "readonly", 18 | "jasmine": "readonly", 19 | "spyOn": "readonly" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | shells/browser/chrome/build/* 3 | shells/browser/firefox/build/* 4 | shells/dev/build/* 5 | 6 | [declarations] 7 | /node_modules/graphql 8 | 9 | [include] 10 | 11 | [libs] 12 | ./flow.js 13 | 14 | [lints] 15 | 16 | [options] 17 | module.name_mapper='^src' ->'/src' 18 | suppress_type=$FlowIssue 19 | suppress_type=$FlowFixMe 20 | suppress_type=$FlowFixMeProps 21 | suppress_type=$FlowFixMeState 22 | suppress_type=$FlowExpectedError 23 | 24 | [strict] 25 | 26 | [version] 27 | ^0.241.0 28 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name: CI 7 | 8 | on: [push, pull_request] 9 | 10 | jobs: 11 | build: 12 | name: Tests (Node ${{ matrix.node-version }}) 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 20.x 19 | - name: Install dependencies 20 | run: yarn install --frozen-lockfile 21 | - name: Check formatting 22 | run: yarn prettier:ci 23 | - name: Check lint 24 | run: yarn lint:ci 25 | - name: Check Flow 26 | run: yarn flow 27 | - name: Test 28 | run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /shells/browser/chrome/*.crx 2 | /shells/browser/chrome/*.pem 3 | /shells/browser/firefox/*.xpi 4 | /shells/browser/firefox/*.pem 5 | /shells/browser/shared/build 6 | /packages/relay-devtools-core/dist 7 | /shells/dev/dist 8 | build 9 | node_modules 10 | npm-debug.log 11 | yarn-error.log 12 | .DS_Store 13 | yarn-error.log 14 | .vscode 15 | .idea 16 | *.pem 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | shells/browser/chrome/build 4 | shells/browser/firefox/build 5 | shells/dev/build 6 | vendor 7 | 8 | package-lock.json 9 | yarn.lock 10 | 11 | __generated__ 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | yarn-offline-mirror false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to relay-devtools 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Pull Requests 7 | 8 | We actively welcome your pull requests. 9 | 10 | 1. Fork the repo and create your branch from `master`. 11 | 2. If you've added code that should be tested, add tests. 12 | 3. If you've changed APIs, update the documentation. 13 | 4. Ensure the test suite passes. (`yarn test`) 14 | 5. Make sure your code lints. (`yarn prettier`) 15 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 16 | 17 | ## Contributor License Agreement ("CLA") 18 | 19 | In order to accept your pull request, we need you to submit a CLA. You only need 20 | to do this once to work on any of Facebook's open source projects. 21 | 22 | Complete your CLA here: 23 | 24 | ## Issues 25 | 26 | We use GitHub issues to track public bugs. Please ensure your description is 27 | clear and has sufficient instructions to be able to reproduce the issue. 28 | 29 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 30 | disclosure of security bugs. In those cases, please go through the process 31 | outlined on that page and do not file a public issue. 32 | 33 | ## License 34 | 35 | By contributing to relay-devtools, you agree that your contributions will be licensed 36 | under the LICENSE file in the root directory of this source tree. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Relay DevTools 2 | 3 | **NOTE:** The Relay DevTools were started as a clone from the React DevTools. There is still code in here that hasn't been updated yet and should be either updated or removed. 4 | 5 | ## Installation 6 | 7 | ### Chrome 8 | 9 | From the Chrome Web Store: [here](https://chrome.google.com/webstore/detail/relay-developer-tools/ncedobpgnmkhcmnnkcimnobpfepidadl). 10 | 11 | ### Firefox 12 | 13 | We haven't tested and released the Firefox extension yet. [Issue #39](https://github.com/relayjs/relay-devtools/issues/39) tracks this. 14 | 15 | ### From Source 16 | 17 | ```sh 18 | git clone git@github.com:relayjs/relay-devtools.git 19 | 20 | cd relay-devtools 21 | 22 | yarn install 23 | 24 | yarn build:extension:chrome # builds at "shells/browser/chrome/build" 25 | yarn run test:chrome # Test Chrome extension 26 | 27 | yarn build:extension:firefox # builds at "shells/browser/firefox/build" 28 | ``` 29 | 30 | ## Contribute 31 | 32 | We actively welcome pull requests, learn how to [contribute](./CONTRIBUTING.md). 33 | 34 | ## Support 35 | 36 | As this extension is in a beta period, it is not officially supported. However if you find a bug, we'd appreciate you reporting it as a [GitHub issue](https://github.com/relayjs/relay-devtools/issues) with repro instructions. 37 | 38 | ## License 39 | 40 | Relay DevTools are [MIT licensed](./LICENSE). 41 | -------------------------------------------------------------------------------- /assets/RelayDevtools.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/assets/RelayDevtools.psd -------------------------------------------------------------------------------- /assets/active-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/assets/active-128.png -------------------------------------------------------------------------------- /assets/store-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/assets/store-icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | const chromeManifest = require('./shells/browser/chrome/manifest.json'); 11 | const firefoxManifest = require('./shells/browser/firefox/manifest.json'); 12 | 13 | const minChromeVersion = parseInt(chromeManifest.minimum_chrome_version, 10); 14 | const minFirefoxVersion = parseInt( 15 | firefoxManifest.applications.gecko.strict_min_version, 16 | 10 17 | ); 18 | validateVersion(minChromeVersion); 19 | validateVersion(minFirefoxVersion); 20 | 21 | function validateVersion(version) { 22 | if (version > 0 && version < 200) { 23 | return; 24 | } 25 | throw new Error('Suspicious browser version in manifest: ' + version); 26 | } 27 | 28 | module.exports = api => { 29 | const isTest = api.env('test'); 30 | const targets = {}; 31 | if (isTest) { 32 | targets.node = 'current'; 33 | } else { 34 | targets.chrome = minChromeVersion.toString(); 35 | targets.firefox = minFirefoxVersion.toString(); 36 | 37 | // This targets RN/Hermes. 38 | targets.ie = '11'; 39 | } 40 | const plugins = [ 41 | ['relay'], 42 | ['@babel/plugin-proposal-nullish-coalescing-operator'], 43 | ['@babel/plugin-proposal-optional-chaining'], 44 | ['@babel/plugin-transform-flow-strip-types'], 45 | ['@babel/plugin-proposal-class-properties', { loose: false }], 46 | ]; 47 | if (process.env.NODE_ENV !== 'production') { 48 | plugins.push(['@babel/plugin-transform-react-jsx-source']); 49 | } 50 | return { 51 | plugins, 52 | sourceType: 'unambiguous', 53 | ignore: [/\/node_modules\//], 54 | presets: [ 55 | [ 56 | '@babel/preset-env', 57 | { 58 | useBuiltIns: 'entry', 59 | targets, 60 | }, 61 | ], 62 | '@babel/preset-react', 63 | '@babel/preset-flow', 64 | ], 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /flow-typed/chrome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | declare var chrome: { 11 | devtools: { 12 | network: { 13 | onNavigated: { 14 | addListener: (cb: (url: string) => void) => void, 15 | removeListener: (cb: () => void) => void, 16 | }, 17 | }, 18 | inspectedWindow: { 19 | eval: (code: string, cb?: (res: any, err: ?Object) => any) => void, 20 | tabId: number, 21 | }, 22 | panels: { 23 | create: ( 24 | title: string, 25 | icon: string, 26 | filename: string, 27 | cb: (panel: { 28 | onHidden: { 29 | addListener: (cb: (window: Object) => void) => void, 30 | }, 31 | onShown: { 32 | addListener: (cb: (window: Object) => void) => void, 33 | }, 34 | }) => void 35 | ) => void, 36 | themeName: ?string, 37 | }, 38 | }, 39 | tabs: { 40 | create: (options: Object) => void, 41 | executeScript: (tabId: number, options: Object, fn: () => void) => void, 42 | onUpdated: { 43 | addListener: ( 44 | fn: (tabId: number, changeInfo: Object, tab: Object) => void 45 | ) => void, 46 | }, 47 | query: (options: Object, fn: (tabArray: Array) => void) => void, 48 | }, 49 | browserAction: { 50 | setIcon: (options: { 51 | tabId: number, 52 | path: { [key: string]: string }, 53 | }) => void, 54 | setPopup: (options: { 55 | tabId: number, 56 | popup: string, 57 | }) => void, 58 | }, 59 | action: { 60 | setIcon: (options: { 61 | tabId: number, 62 | path: { [key: string]: string }, 63 | }) => void, 64 | setPopup: (options: { 65 | tabId: number, 66 | popup: string, 67 | }) => void, 68 | }, 69 | runtime: { 70 | getURL: (path: string) => string, 71 | sendMessage: (config: Object) => void, 72 | connect: ( 73 | config: Object 74 | ) => { 75 | disconnect: () => void, 76 | onMessage: { 77 | addListener: (fn: (message: Object) => void) => void, 78 | }, 79 | onDisconnect: { 80 | addListener: (fn: (message: Object) => void) => void, 81 | }, 82 | postMessage: (data: Object) => void, 83 | }, 84 | onConnect: { 85 | addListener: ( 86 | fn: (port: { 87 | name: string, 88 | sender: { 89 | tab: { 90 | id: number, 91 | url: string, 92 | }, 93 | }, 94 | }) => void 95 | ) => void, 96 | }, 97 | onMessage: { 98 | addListener: ( 99 | fn: ( 100 | req: Object, 101 | sender: { 102 | url: string, 103 | tab: { 104 | id: number, 105 | }, 106 | } 107 | ) => void 108 | ) => void, 109 | }, 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /flow-typed/npm/react-test-renderer_v16.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b6bb53397d83d2d821e258cc73818d1b 2 | // flow-typed version: 9c71eca8ef/react-test-renderer_v16.x.x/flow_>=v0.47.x 3 | 4 | // Type definitions for react-test-renderer 16.x.x 5 | // Ported from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer 6 | 7 | type ReactComponentInstance = React$Component; 8 | 9 | type ReactTestRendererJSON = { 10 | type: string, 11 | props: { [propName: string]: any }, 12 | children: null | ReactTestRendererJSON[], 13 | }; 14 | 15 | type ReactTestRendererTree = ReactTestRendererJSON & { 16 | nodeType: 'component' | 'host', 17 | instance: ?ReactComponentInstance, 18 | rendered: null | ReactTestRendererTree, 19 | }; 20 | 21 | type ReactTestInstance = { 22 | instance: ?ReactComponentInstance, 23 | type: string, 24 | props: { [propName: string]: any }, 25 | parent: null | ReactTestInstance, 26 | children: Array, 27 | 28 | find(predicate: (node: ReactTestInstance) => boolean): ReactTestInstance, 29 | findByType(type: React$ElementType): ReactTestInstance, 30 | findByProps(props: { [propName: string]: any }): ReactTestInstance, 31 | 32 | findAll( 33 | predicate: (node: ReactTestInstance) => boolean, 34 | options?: { deep: boolean } 35 | ): ReactTestInstance[], 36 | findAllByType( 37 | type: React$ElementType, 38 | options?: { deep: boolean } 39 | ): ReactTestInstance[], 40 | findAllByProps( 41 | props: { [propName: string]: any }, 42 | options?: { deep: boolean } 43 | ): ReactTestInstance[], 44 | }; 45 | 46 | type TestRendererOptions = { 47 | createNodeMock(element: React$Element): any, 48 | }; 49 | 50 | declare module 'react-test-renderer' { 51 | declare export type ReactTestRenderer = { 52 | toJSON(): null | ReactTestRendererJSON, 53 | toTree(): null | ReactTestRendererTree, 54 | unmount(nextElement?: React$Element): void, 55 | update(nextElement: React$Element): void, 56 | getInstance(): ?ReactComponentInstance, 57 | root: ReactTestInstance, 58 | }; 59 | 60 | declare type Thenable = { 61 | then(resolve: () => mixed, reject?: () => mixed): mixed, 62 | }; 63 | 64 | declare function create( 65 | nextElement: React$Element, 66 | options?: TestRendererOptions 67 | ): ReactTestRenderer; 68 | 69 | declare function act(callback: () => void): Thenable; 70 | } 71 | 72 | declare module 'react-test-renderer/shallow' { 73 | declare export default class ShallowRenderer { 74 | static createRenderer(): ShallowRenderer; 75 | getMountedInstance(): ReactTestInstance; 76 | getRenderOutput>(): E; 77 | getRenderOutput(): React$Element; 78 | render(element: React$Element, context?: any): void; 79 | unmount(): void; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | declare module 'events' { 9 | declare class EventEmitter { 10 | addListener>( 11 | event: Event, 12 | listener: (...$ElementType) => any 13 | ): void; 14 | emit: >( 15 | event: Event, 16 | ...$ElementType 17 | ) => void; 18 | removeListener(event: $Keys, listener: Function): void; 19 | removeAllListeners(event?: $Keys): void; 20 | } 21 | 22 | declare export default typeof EventEmitter; 23 | } 24 | 25 | declare var __DEV__: boolean; 26 | declare var __ENABLE_LOGGER__: boolean; 27 | 28 | declare var jasmine: {| 29 | getEnv: () => {| 30 | afterEach: (callback: Function) => void, 31 | beforeEach: (callback: Function) => void, 32 | |}, 33 | |}; 34 | -------------------------------------------------------------------------------- /packages/relay-devtools-core/README.md: -------------------------------------------------------------------------------- 1 | # `relay-devtools-core` 2 | 3 | A standalone Relay DevTools implementation. 4 | 5 | This is a low-level package. If you're looking for the Electron app you can run, **use `relay-devtools` package instead.** 6 | 7 | ## API 8 | 9 | ### `relay-devtools-core` 10 | 11 | This is similar requiring the `relay-devtools` package, but provides several configurable options. Unlike `relay-devtools`, requiring `relay-devtools-core` doesn't connect immediately but instead exports a function: 12 | 13 | ```js 14 | const { connectToDevTools } = require('relay-devtools-core'); 15 | connectToDevTools({ 16 | // Config options 17 | }); 18 | ``` 19 | 20 | Run `connectToDevTools()` in the same context as React to set up a connection to DevTools. 21 | Be sure to run this function _before_ importing e.g. `react`, `react-dom`, `react-native`. 22 | 23 | The `options` object may contain: 24 | 25 | - `host: string` (defaults to "localhost") - Websocket will connect to this host. 26 | - `port: number` (defaults to `8097`) - Websocket will connect to this port. 27 | - `websocket: Websocket` - Custom websocked to use. Overrides `host` and `port` settings if provided. 28 | - `resolveNativeStyle: (style: number) => ?Object` - Used by the React Native style plug-in. 29 | - `isAppActive: () => boolean` - If provided, DevTools will poll this method and wait until it returns true before connecting to React. 30 | 31 | ## `relay-devtools-core/standalone` 32 | 33 | Renders the DevTools interface into a DOM node. 34 | 35 | ```js 36 | require('relay-devtools-core/standalone') 37 | .setContentDOMNode(document.getElementById('container')) 38 | .setStatusListener(status => { 39 | // This callback is optional... 40 | }) 41 | .startServer(port); 42 | ``` 43 | 44 | Reference the `relay-devtools` package for a complete integration example. 45 | -------------------------------------------------------------------------------- /packages/relay-devtools-core/backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = require('./dist/backend'); 9 | -------------------------------------------------------------------------------- /packages/relay-devtools-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-devtools-core", 3 | "description": "Use relay-devtools outside of the browser", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "main": "./dist/backend.js", 7 | "repository": { 8 | "url": "https://github.com/relayjs/relay-devtools.git", 9 | "type": "git" 10 | }, 11 | "files": [ 12 | "dist", 13 | "backend.js", 14 | "standalone.js" 15 | ], 16 | "scripts": { 17 | "build": "yarn build:backend && yarn build:standalone", 18 | "build:backend": "cross-env NODE_ENV=production webpack --config webpack.backend.js", 19 | "build:standalone": "cross-env NODE_ENV=production webpack --config webpack.standalone.js", 20 | "prepublish": "yarn run build", 21 | "start:backend": "cross-env NODE_ENV=development webpack --config webpack.backend.js --watch", 22 | "start:standalone": "cross-env NODE_ENV=development webpack --config webpack.standalone.js --watch" 23 | }, 24 | "dependencies": { 25 | "shell-quote": "^1.7.3", 26 | "ws": "^7" 27 | }, 28 | "devDependencies": { 29 | "cross-env": "^6.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/relay-devtools-core/standalone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = require('./dist/standalone'); 9 | -------------------------------------------------------------------------------- /packages/relay-devtools-core/webpack.backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const { resolve } = require('path'); 9 | const Webpack = require('webpack'); 10 | const { 11 | getGitHubIssuesURL, 12 | getGitHubURL, 13 | getInternalDevToolsFeedbackGroup, 14 | getVersionString, 15 | } = require('../../shells/utils'); 16 | 17 | const NODE_ENV = process.env.NODE_ENV; 18 | if (!NODE_ENV) { 19 | console.error('NODE_ENV not set'); 20 | process.exit(1); 21 | } 22 | 23 | const __DEV__ = NODE_ENV === 'development'; 24 | 25 | const GITHUB_URL = getGitHubURL(); 26 | const DEVTOOLS_VERSION = getVersionString(); 27 | const GITHUB_ISSUES_URL = getGitHubIssuesURL(); 28 | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); 29 | 30 | module.exports = { 31 | mode: 'development', // TODO TESTING __DEV__ ? 'development' : 'production', 32 | devtool: __DEV__ ? 'eval-cheap-module-source-map' : false, 33 | entry: { 34 | backend: './src/backend.js', 35 | }, 36 | output: { 37 | path: __dirname + '/dist', 38 | filename: '[name].js', 39 | 40 | // This name is important; standalone references it in order to connect. 41 | library: { 42 | name: 'RelayDevToolsBackend', 43 | type: 'umd', 44 | }, 45 | }, 46 | resolve: { 47 | alias: { 48 | src: resolve(__dirname, '../../src'), 49 | }, 50 | }, 51 | plugins: [ 52 | new Webpack.DefinePlugin({ 53 | __DEV__: true, 54 | 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 55 | 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 56 | 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 57 | 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`, 58 | }), 59 | ], 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.js$/, 64 | loader: 'babel-loader', 65 | options: { 66 | configFile: resolve(__dirname, '../../babel.config.js'), 67 | }, 68 | }, 69 | ], 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /packages/relay-devtools-core/webpack.standalone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const { resolve } = require('path'); 9 | const Webpack = require('webpack'); 10 | const { 11 | getGitHubIssuesURL, 12 | getGitHubURL, 13 | getInternalDevToolsFeedbackGroup, 14 | getVersionString, 15 | } = require('../../shells/utils'); 16 | 17 | const NODE_ENV = process.env.NODE_ENV; 18 | if (!NODE_ENV) { 19 | console.error('NODE_ENV not set'); 20 | process.exit(1); 21 | } 22 | 23 | const __DEV__ = NODE_ENV === 'development'; 24 | 25 | const GITHUB_URL = getGitHubURL(); 26 | const DEVTOOLS_VERSION = getVersionString(); 27 | const GITHUB_ISSUES_URL = getGitHubIssuesURL(); 28 | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); 29 | 30 | module.exports = { 31 | mode: __DEV__ ? 'development' : 'production', 32 | devtool: __DEV__ ? 'eval-cheap-module-source-map' : false, 33 | target: 'electron-main', 34 | entry: { 35 | standalone: './src/standalone.js', 36 | }, 37 | output: { 38 | path: __dirname + '/dist', 39 | filename: '[name].js', 40 | library: { 41 | name: '[name]', 42 | type: 'commonjs2', 43 | }, 44 | }, 45 | resolve: { 46 | alias: { 47 | src: resolve(__dirname, '../../src'), 48 | }, 49 | }, 50 | plugins: [ 51 | new Webpack.DefinePlugin({ 52 | __DEV__: false, 53 | 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 54 | 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 55 | 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 56 | 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`, 57 | 'process.env.NODE_ENV': `"${NODE_ENV}"`, 58 | }), 59 | ], 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.js$/, 64 | loader: 'babel-loader', 65 | options: { 66 | configFile: resolve(__dirname, '../../babel.config.js'), 67 | }, 68 | }, 69 | { 70 | test: /\.css$/, 71 | use: [ 72 | { 73 | loader: 'style-loader', 74 | }, 75 | { 76 | loader: 'css-loader', 77 | options: { 78 | sourceMap: true, 79 | modules: true, 80 | localIdentName: '[local]___[hash:base64:5]', 81 | }, 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /packages/relay-devtools/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Developer Tools 5 | 6 | 60 | 61 | 62 |
63 |
67 |

Waiting for React to connect…

68 |
69 |

React Native

70 |
The active app will automatically connect in a few seconds.
71 |
72 |

React DOM

73 |
74 | Add
76 | or 77 |
78 |
79 | to the top of the page you want to debug, 80 |
81 | before importing React DOM. 82 |
83 |
84 |
85 |
86 |
Starting the server…
87 |
88 |
89 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /packages/relay-devtools/app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const { app, BrowserWindow } = require('electron'); // Module to create native browser window. 10 | const { join } = require('path'); 11 | 12 | const argv = require('minimist')(process.argv.slice(2)); 13 | const projectRoots = argv._; 14 | const defaultThemeName = argv.theme; 15 | 16 | let mainWindow: null | typeof BrowserWindow = null; 17 | 18 | app.on('window-all-closed', function() { 19 | app.quit(); 20 | }); 21 | 22 | app.on('ready', function() { 23 | // Create the browser window. 24 | mainWindow = new BrowserWindow({ 25 | width: 800, 26 | height: 600, 27 | icon: join(__dirname, 'icons/icon128.png'), 28 | frame: false, 29 | //titleBarStyle: 'customButtonsOnHover', 30 | webPreferences: { 31 | nodeIntegration: true, 32 | }, 33 | }); 34 | const mw = mainWindow; 35 | 36 | // and load the index.html of the app. 37 | mw.loadURL('file://' + __dirname + '/app.html'); // eslint-disable-line no-path-concat 38 | mw.webContents.executeJavaScript( 39 | // We use this so that RN can keep relative JSX __source filenames 40 | // but "click to open in editor" still works. js1 passes project roots 41 | // as the argument to DevTools. 42 | 'window.devtools.setProjectRoots(' + JSON.stringify(projectRoots) + ')' 43 | ); 44 | 45 | if (argv.theme) { 46 | mw.webContents.executeJavaScript( 47 | 'window.devtools.setDefaultThemeName(' + 48 | JSON.stringify(defaultThemeName) + 49 | ')' 50 | ); 51 | } 52 | 53 | // Emitted when the window is closed. 54 | mw.on('closed', function() { 55 | mainWindow = null; 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/relay-devtools/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * @flow 9 | */ 10 | 11 | const electron = require('electron'); 12 | const spawn = require('cross-spawn'); 13 | const argv = process.argv.slice(2); 14 | const pkg = require('./package.json'); 15 | const updateNotifier = require('update-notifier'); 16 | 17 | // notify if there's an update 18 | updateNotifier({ pkg }).notify({ defer: false }); 19 | 20 | const result = spawn.sync(electron, [require.resolve('./app')].concat(argv), { 21 | stdio: 'ignore', 22 | }); 23 | 24 | process.exit(result.status); 25 | -------------------------------------------------------------------------------- /packages/relay-devtools/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/packages/relay-devtools/icons/icon128.png -------------------------------------------------------------------------------- /packages/relay-devtools/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | const { connectToDevTools } = require('relay-devtools-core/backend'); 11 | 12 | // Connect immediately with default options. 13 | // If you need more control, use `relay-devtools-core` directly instead of `relay-devtools`. 14 | connectToDevTools(); 15 | -------------------------------------------------------------------------------- /packages/relay-devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-devtools", 3 | "version": "1.0.0", 4 | "description": "Use relay-devtools outside of the browser", 5 | "license": "MIT", 6 | "repository": { 7 | "url": "https://github.com/relayjs/relay-devtools.git", 8 | "type": "git" 9 | }, 10 | "bin": { 11 | "relay-devtools": "./bin.js" 12 | }, 13 | "files": [ 14 | "bin.js", 15 | "app.html", 16 | "app.js", 17 | "index.js", 18 | "icons" 19 | ], 20 | "scripts": { 21 | "start": "node bin.js" 22 | }, 23 | "dependencies": { 24 | "cross-spawn": "^7.0.1", 25 | "electron": "^15.5.5", 26 | "ip": "^1.1.4", 27 | "minimist": "^1.2.6", 28 | "relay-devtools-core": "^1.0.0", 29 | "update-notifier": "^3.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /relay.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | src: './shells/dev/relay-app', 10 | schema: './shells/dev/relay-app/schema.graphql', 11 | language: 'flow', 12 | excludes: ['**/node_modules/**', '**/__generated__/**'], 13 | }; 14 | -------------------------------------------------------------------------------- /shells/browser/chrome/README.md: -------------------------------------------------------------------------------- 1 | # The Chrome extension 2 | 3 | The source code for this extension has moved to `shells/webextension`. 4 | 5 | Modify the source code there and then rebuild this extension by running `node build` from this directory or `yarn run build:extension:chrome` from the root directory. 6 | 7 | ## Testing in Chrome 8 | 9 | You can test a local build of the web extension like so: 10 | 11 | 1. Build the extension: `node build` 12 | 1. Follow the on-screen instructions. 13 | -------------------------------------------------------------------------------- /shells/browser/chrome/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const chalk = require('chalk'); 10 | const { execSync } = require('child_process'); 11 | const { existsSync } = require('fs'); 12 | const { isAbsolute, join, relative } = require('path'); 13 | const { argv } = require('yargs'); 14 | const build = require('../shared/build'); 15 | 16 | const main = async () => { 17 | const { crx, keyPath } = argv; 18 | 19 | if (crx) { 20 | if (!keyPath || !existsSync(keyPath)) { 21 | console.error('Must specify a key file (.pem) to build CRX'); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | await build('chrome'); 27 | 28 | if (crx) { 29 | const cwd = join(__dirname, 'build'); 30 | 31 | let safeKeyPath = keyPath; 32 | if (!isAbsolute(keyPath)) { 33 | safeKeyPath = join(relative(cwd, process.cwd()), keyPath); 34 | } 35 | 36 | execSync(`crx pack ./unpacked -o RelayDevTools.crx -p ${safeKeyPath}`, { 37 | cwd, 38 | }); 39 | } 40 | 41 | console.log(chalk.green('\nThe Chrome extension has been built!')); 42 | console.log(chalk.green('You can test this build by running:')); 43 | console.log(chalk.gray('\n# From the relay-devtools root directory:')); 44 | console.log('yarn run test:chrome'); 45 | }; 46 | 47 | main(); 48 | -------------------------------------------------------------------------------- /shells/browser/chrome/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const deploy = require('../shared/deploy'); 10 | 11 | const main = async () => await deploy('chrome'); 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /shells/browser/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Relay Developer Tools", 4 | "description": "Adds Relay debugging tools to the Chrome Developer Tools.", 5 | "version": "0.9.17", 6 | "version_name": "0.9.17", 7 | "minimum_chrome_version": "88", 8 | "icons": { 9 | "16": "icons/enabled16.png", 10 | "32": "icons/enabled32.png", 11 | "48": "icons/enabled48.png", 12 | "128": "icons/enabled128.png" 13 | }, 14 | "action": { 15 | "default_icon": { 16 | "16": "icons/disabled16.png", 17 | "32": "icons/disabled32.png", 18 | "48": "icons/disabled48.png", 19 | "128": "icons/disabled128.png" 20 | }, 21 | "default_popup": "popups/disabled.html" 22 | }, 23 | "devtools_page": "main.html", 24 | "content_security_policy": { 25 | "extension_pages": "script-src 'self'; object-src 'self'" 26 | }, 27 | "web_accessible_resources": [ 28 | { 29 | "resources": ["main.html", "panel.html", "build/*.js"], 30 | "matches": [""] 31 | } 32 | ], 33 | "background": { 34 | "service_worker": "build/background.js" 35 | }, 36 | "permissions": ["webNavigation", "scripting"], 37 | "content_scripts": [ 38 | { 39 | "matches": [""], 40 | "js": ["build/injectGlobalHook.js", "build/contentScript.js"], 41 | "run_at": "document_start" 42 | } 43 | ], 44 | "host_permissions": [""] 45 | } 46 | -------------------------------------------------------------------------------- /shells/browser/chrome/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-devtools-experimental-chrome", 3 | "alias": ["relay-devtools-experimental-chrome"], 4 | "files": ["index.html", "RelayDevTools.zip"] 5 | } 6 | -------------------------------------------------------------------------------- /shells/browser/chrome/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const chromeLaunch = require('chrome-launch'); // eslint-disable-line import/no-extraneous-dependencies 10 | const { resolve } = require('path'); 11 | 12 | const EXTENSION_PATH = resolve('shells/browser/chrome/build/unpacked'); 13 | const START_URL = 'https://facebook.github.io/react/'; 14 | 15 | chromeLaunch(START_URL, { 16 | args: [`--load-extension=${EXTENSION_PATH}`], 17 | }); 18 | -------------------------------------------------------------------------------- /shells/browser/chrome/watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const { execSync } = require('child_process'); 10 | const { join } = require('path'); 11 | 12 | const webpackPath = join( 13 | __dirname, 14 | '..', 15 | '..', 16 | '..', 17 | 'node_modules', 18 | '.bin', 19 | 'webpack' 20 | ); 21 | const binPath = join(__dirname, 'build', 'unpacked', 'build'); 22 | execSync( 23 | `${webpackPath} --config ../shared/webpack.config.js --output-path ${binPath} --watch`, 24 | { 25 | cwd: join(__dirname, '..', 'shared'), 26 | env: process.env, 27 | stdio: 'inherit', 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /shells/browser/firefox/README.md: -------------------------------------------------------------------------------- 1 | # The Firefox extension 2 | 3 | The source code for this extension has moved to `shells/webextension`. 4 | 5 | Modify the source code there and then rebuild this extension by running `node build` from this directory or `yarn run build:extension:firefox` from the root directory. 6 | 7 | ## Testing in Firefox 8 | 9 | 1. Build the extension: `node build` 10 | 1. Follow the on-screen instructions. 11 | 12 | You can test upcoming releases of Firefox by downloading the Beta or Nightly build from the [Firefox releases](https://www.mozilla.org/en-US/firefox/channel/desktop/) page and then following the on-screen instructions after building. 13 | -------------------------------------------------------------------------------- /shells/browser/firefox/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const chalk = require('chalk'); 10 | const build = require('../shared/build'); 11 | 12 | const main = async () => { 13 | await build('firefox'); 14 | 15 | console.log(chalk.green('\nThe Firefox extension has been built!')); 16 | console.log(chalk.green('You can test this build by running:')); 17 | console.log(chalk.gray('\n# From the relay-devtools root directory:')); 18 | console.log('yarn run test:firefox'); 19 | console.log( 20 | chalk.gray('\n# You can also test against upcoming Firefox releases.') 21 | ); 22 | console.log( 23 | chalk.gray( 24 | '# First download a release from https://www.mozilla.org/en-US/firefox/channel/desktop/' 25 | ) 26 | ); 27 | console.log( 28 | chalk.gray( 29 | '# And then tell web-ext which release to use (eg firefoxdeveloperedition, nightly, beta):' 30 | ) 31 | ); 32 | console.log('WEB_EXT_FIREFOX=nightly yarn run test:firefox'); 33 | console.log(chalk.gray('\n# You can test against older versions too:')); 34 | console.log( 35 | 'WEB_EXT_FIREFOX=/Applications/Firefox52.app/Contents/MacOS/firefox-bin yarn run test:firefox' 36 | ); 37 | }; 38 | 39 | main(); 40 | 41 | module.exports = { main }; 42 | -------------------------------------------------------------------------------- /shells/browser/firefox/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const deploy = require('../shared/deploy'); 10 | 11 | const main = async () => await deploy('firefox'); 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /shells/browser/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "React Developer Tools", 4 | "description": "Adds React debugging tools to the Firefox Developer Tools.", 5 | "version": "4.0.0", 6 | 7 | "applications": { 8 | "gecko": { 9 | "id": "@relay-devtools", 10 | "strict_min_version": "54.0" 11 | } 12 | }, 13 | 14 | "icons": { 15 | "16": "icons/enabled16.png", 16 | "32": "icons/enabled32.png", 17 | "48": "icons/enabled48.png", 18 | "128": "icons/enabled128.png" 19 | }, 20 | 21 | "browser_action": { 22 | "default_icon": { 23 | "16": "icons/disabled16.png", 24 | "32": "icons/disabled32.png", 25 | "48": "icons/disabled48.png", 26 | "128": "icons/disabled128.png" 27 | }, 28 | 29 | "default_popup": "popups/disabled.html", 30 | "browser_style": true 31 | }, 32 | 33 | "devtools_page": "main.html", 34 | 35 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 36 | "web_accessible_resources": [ 37 | "main.html", 38 | "panel.html", 39 | "build/backend.js", 40 | "build/renderer.js" 41 | ], 42 | 43 | "background": { 44 | "scripts": ["build/background.js"] 45 | }, 46 | 47 | "permissions": ["file:///*", "http://*/*", "https://*/*"], 48 | 49 | "content_scripts": [ 50 | { 51 | "matches": [""], 52 | "js": ["build/injectGlobalHook.js"], 53 | "run_at": "document_start" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /shells/browser/firefox/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-devtools-experimental-firefox", 3 | "alias": ["relay-devtools-experimental-firefox"], 4 | "files": ["index.html", "RelayDevTools.zip"] 5 | } 6 | -------------------------------------------------------------------------------- /shells/browser/firefox/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const { exec } = require('child-process-promise'); 10 | const { Finder } = require('firefox-profile'); 11 | const { resolve } = require('path'); 12 | 13 | const EXTENSION_PATH = resolve('shells/browser/firefox/build/unpacked'); 14 | const START_URL = 'https://facebook.github.io/react/'; 15 | 16 | const main = async () => { 17 | const finder = new Finder(); 18 | 19 | // Use default Firefox profile for testing purposes. 20 | // This prevents users from having to re-login-to sites before testing. 21 | const findPathPromise = new Promise((resolvePromise, rejectPromise) => { 22 | finder.getPath('default', (error, profile) => { 23 | if (error) { 24 | rejectPromise(error); 25 | } else { 26 | resolvePromise(profile); 27 | } 28 | }); 29 | }); 30 | 31 | const options = [ 32 | `--source-dir=${EXTENSION_PATH}`, 33 | `--start-url=${START_URL}`, 34 | '--browser-console', 35 | ]; 36 | 37 | try { 38 | const path = await findPathPromise; 39 | const trimmedPath = path.replace(' ', '\\ '); 40 | options.push(`--firefox-profile=${trimmedPath}`); 41 | } catch (err) { 42 | console.warn('Could not find default profile, using temporary profile.'); 43 | } 44 | 45 | try { 46 | await exec(`web-ext run ${options.join(' ')}`); 47 | } catch (err) { 48 | console.error('`web-ext run` failed', err.stdout, err.stderr); 49 | } 50 | }; 51 | 52 | main(); 53 | -------------------------------------------------------------------------------- /shells/browser/shared/deploy.chrome.html: -------------------------------------------------------------------------------- 1 |
    2 |
  1. download extension
  2. 3 |
  3. Double-click to extract
  4. 4 |
  5. Navigate to chrome://extensions/
  6. 5 |
  7. Enable "Developer mode"
  8. 6 |
  9. Click "LOAD UNPACKED"
  10. 7 |
  11. Select extracted extension folder (RelayDevTools)
  12. 8 |
9 | -------------------------------------------------------------------------------- /shells/browser/shared/deploy.firefox.html: -------------------------------------------------------------------------------- 1 |
    2 |
  1. download extension
  2. 3 |
  3. Extract/unzip
  4. 4 |
  5. Visit about:debugging
  6. 5 |
  7. Click "Load Temporary Add-on"
  8. 6 |
  9. 7 | Select the manifest.json file inside of the extracted extension 8 | folder (RelayDevTools) 9 |
  10. 10 |
11 | -------------------------------------------------------------------------------- /shells/browser/shared/deploy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Relay DevTools pre-release 5 | 6 | 13 | 14 | 15 |

16 | Relay DevTools pre-release 17 |

18 | 19 |

20 | Created on %date% from 21 | %commit% 24 |

25 | 26 |

27 | This is a preview build of an 28 | unreleased DevTools extension. It has no developer support. 31 |

32 | 33 |

Installation instructions

34 | %installation% 35 |

36 | If you already have the Relay DevTools extension installed, you will need 37 | to temporarily disable or remove it in order to install this prerelease 38 | build. 39 |

40 | 41 |

Bug reports

42 |

43 | Please report bugs as 44 | GitHub issues. Please include all of the info required to reproduce the bug (e.g. 47 | links, code, instructions). 48 |

49 | 50 |

Feature requests

51 |

52 | Feature requests are not being accepted at this time. 53 |

54 | 55 | 56 | -------------------------------------------------------------------------------- /shells/browser/shared/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const { exec, execSync } = require('child_process'); 10 | const { readFileSync, writeFileSync } = require('fs'); 11 | const { join } = require('path'); 12 | 13 | const main = async buildId => { 14 | const root = join(__dirname, '..', buildId); 15 | const buildPath = join(root, 'build'); 16 | 17 | execSync(`node ${join(root, './build')}`, { 18 | cwd: __dirname, 19 | env: { 20 | ...process.env, 21 | NODE_ENV: 'production', 22 | }, 23 | stdio: 'inherit', 24 | }); 25 | 26 | await exec(`cp ${join(root, 'now.json')} ${join(buildPath, 'now.json')}`, { 27 | cwd: root, 28 | }); 29 | 30 | const file = readFileSync(join(root, 'now.json')); 31 | const json = JSON.parse(file); 32 | const alias = json.alias[0]; 33 | 34 | const commit = execSync('git rev-parse HEAD') 35 | .toString() 36 | .trim() 37 | .substr(0, 7); 38 | 39 | let date = new Date(); 40 | date = `${date.toLocaleDateString()} – ${date.toLocaleTimeString()}`; 41 | 42 | const installationInstructions = 43 | buildId === 'chrome' 44 | ? readFileSync(join(__dirname, 'deploy.chrome.html')) 45 | : readFileSync(join(__dirname, 'deploy.firefox.html')); 46 | 47 | let html = readFileSync(join(__dirname, 'deploy.html')).toString(); 48 | html = html.replace(/%commit%/g, commit); 49 | html = html.replace(/%date%/g, date); 50 | html = html.replace(/%installation%/, installationInstructions); 51 | 52 | writeFileSync(join(buildPath, 'index.html'), html); 53 | 54 | await exec(`now deploy && now alias ${alias}`, { 55 | cwd: buildPath, 56 | stdio: 'inherit', 57 | }); 58 | 59 | console.log(`Deployed to https://${alias}.now.sh`); 60 | }; 61 | 62 | module.exports = main; 63 | -------------------------------------------------------------------------------- /shells/browser/shared/icons/disabled128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/disabled128.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/disabled16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/disabled16.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/disabled32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/disabled32.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/disabled48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/disabled48.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/enabled128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/enabled128.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/enabled16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/enabled16.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/enabled32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/enabled32.png -------------------------------------------------------------------------------- /shells/browser/shared/icons/enabled48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relayjs/relay-devtools/b3893c29958d89f4760a9e88322cacbbddf15f3c/shells/browser/shared/icons/enabled48.png -------------------------------------------------------------------------------- /shells/browser/shared/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /shells/browser/shared/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 26 | 27 | 28 | 29 |
Unable to find Relay on the page.
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /shells/browser/shared/popups/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 18 |

19 | This page doesn’t appear to be using Relay. 20 |

21 | -------------------------------------------------------------------------------- /shells/browser/shared/popups/enabled.html: -------------------------------------------------------------------------------- 1 | 2 | 18 |

19 | This page is using Relay. ✅ 20 |
21 | Open the developer tools, and the Relay tab will appear to the right. 22 |

23 | -------------------------------------------------------------------------------- /shells/browser/shared/popups/shared.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /* globals chrome */ 9 | 10 | document.addEventListener('DOMContentLoaded', function() { 11 | // Make links work 12 | const links = document.getElementsByTagName('a'); 13 | for (let i = 0; i < links.length; i++) { 14 | (function() { 15 | const ln = links[i]; 16 | const location = ln.href; 17 | ln.onclick = function() { 18 | chrome.tabs.create({ active: true, url: location }); 19 | }; 20 | })(); 21 | } 22 | 23 | // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=428044 24 | document.body.style.opacity = 0; 25 | document.body.style.transition = 'opacity ease-out .4s'; 26 | requestAnimationFrame(function() { 27 | document.body.style.opacity = 1; 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /shells/browser/shared/src/backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | // Do not use imports or top-level requires here! 11 | // Running module factories is intentionally delayed until we know the hook exists. 12 | // This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039 13 | 14 | function welcome(event: any) { 15 | if ( 16 | event.source !== window || 17 | event.data.source !== 'relay-devtools-content-script' 18 | ) { 19 | return; 20 | } 21 | 22 | window.removeEventListener('message', welcome); 23 | 24 | setup(window.__RELAY_DEVTOOLS_HOOK__); 25 | } 26 | 27 | window.addEventListener('message', welcome); 28 | 29 | function setup(hook: any) { 30 | const Agent = require('src/backend/agent').default; 31 | const Bridge = require('src/bridge').default; 32 | const { initBackend } = require('src/backend'); 33 | 34 | const bridge = new Bridge({ 35 | listen(fn) { 36 | const listener = (event: any) => { 37 | if ( 38 | event.source !== window || 39 | !event.data || 40 | event.data.source !== 'relay-devtools-content-script' || 41 | !event.data.payload 42 | ) { 43 | return; 44 | } 45 | fn(event.data.payload); 46 | }; 47 | window.addEventListener('message', listener); 48 | return () => { 49 | window.removeEventListener('message', listener); 50 | }; 51 | }, 52 | sendAll(events) { 53 | window.postMessage( 54 | { 55 | source: 'relay-devtools-bridge', 56 | payload: events, 57 | }, 58 | '*' 59 | ); 60 | }, 61 | }); 62 | 63 | const agent = new Agent(bridge); 64 | agent.addListener('shutdown', () => { 65 | // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down, 66 | // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here. 67 | hook.emit('shutdown'); 68 | }); 69 | 70 | initBackend(hook, agent, window); 71 | } 72 | -------------------------------------------------------------------------------- /shells/browser/shared/src/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | /* global chrome */ 11 | 12 | const ports: $FlowFixMe = {}; 13 | 14 | const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0; 15 | 16 | chrome.runtime.onConnect.addListener(function(port) { 17 | let tab = null; 18 | let name = null; 19 | if (isNumeric(port.name)) { 20 | tab = port.name; 21 | name = 'devtools'; 22 | installContentScript(+port.name); 23 | } else { 24 | tab = port.sender.tab.id; 25 | name = 'content-script'; 26 | } 27 | 28 | if (!ports[tab]) { 29 | ports[tab] = { 30 | devtools: null, 31 | 'content-script': null, 32 | }; 33 | } 34 | ports[tab][name] = port; 35 | 36 | if (ports[tab].devtools && ports[tab]['content-script']) { 37 | doublePipe(ports[tab].devtools, ports[tab]['content-script']); 38 | } 39 | }); 40 | 41 | function isNumeric(str: string): boolean { 42 | return +str + '' === str; 43 | } 44 | 45 | function installContentScript(tabId: number) { 46 | chrome.scripting.executeScript({ 47 | target: { tabId: tabId }, 48 | files: ['/build/contentScript.js'], 49 | }); 50 | } 51 | 52 | function doublePipe(one: any, two: any) { 53 | one.onMessage.addListener(lOne); 54 | function lOne(message: any) { 55 | two.postMessage(message); 56 | } 57 | two.onMessage.addListener(lTwo); 58 | function lTwo(message: any) { 59 | one.postMessage(message); 60 | } 61 | function shutdown() { 62 | one.onMessage.removeListener(lOne); 63 | two.onMessage.removeListener(lTwo); 64 | one.disconnect(); 65 | two.disconnect(); 66 | } 67 | one.onDisconnect.addListener(shutdown); 68 | two.onDisconnect.addListener(shutdown); 69 | } 70 | 71 | function setIconAndPopup(relayBuildType: string, tabId: number) { 72 | chrome.action.setIcon({ 73 | tabId: tabId, 74 | path: { 75 | '16': chrome.runtime.getURL(`icons/${relayBuildType}16.png`), 76 | '32': chrome.runtime.getURL(`icons/${relayBuildType}32.png`), 77 | '48': chrome.runtime.getURL(`icons/${relayBuildType}48.png`), 78 | '128': chrome.runtime.getURL(`icons/${relayBuildType}128.png`), 79 | }, 80 | }); 81 | chrome.action.setPopup({ 82 | tabId: tabId, 83 | popup: chrome.runtime.getURL(`popups/${relayBuildType}.html`), 84 | }); 85 | } 86 | 87 | // Listen to URL changes on the active tab and reset the DevTools icon. 88 | // This prevents non-disabled icons from sticking in Firefox. 89 | // Don't listen to this event in Chrome though. 90 | // It fires more frequently, often after onMessage() has been called. 91 | if (IS_FIREFOX) { 92 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 93 | if (tab.active && changeInfo.status === 'loading') { 94 | setIconAndPopup('disabled', tabId); 95 | } 96 | }); 97 | } 98 | 99 | chrome.runtime.onMessage.addListener((request, sender) => { 100 | if (sender.tab) { 101 | // This is sent from the hook content script. 102 | // It tells us a renderer has attached. 103 | if (request.hasDetectedReact) { 104 | setIconAndPopup('enabled', sender.tab.id); 105 | } 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /shells/browser/shared/src/contentScript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | /* global chrome */ 11 | 12 | let backendDisconnected: boolean = false; 13 | let backendInitialized: boolean = false; 14 | 15 | function sayHelloToBackend() { 16 | window.postMessage( 17 | { 18 | source: 'relay-devtools-content-script', 19 | hello: true, 20 | }, 21 | '*' 22 | ); 23 | } 24 | 25 | function handleMessageFromDevtools(message: any) { 26 | window.postMessage( 27 | { 28 | source: 'relay-devtools-content-script', 29 | payload: message, 30 | }, 31 | '*' 32 | ); 33 | } 34 | 35 | function handleMessageFromPage(evt: any) { 36 | if ( 37 | evt.source === window && 38 | evt.data && 39 | evt.data.source === 'relay-devtools-bridge' 40 | ) { 41 | backendInitialized = true; 42 | 43 | port.postMessage(evt.data.payload); 44 | } 45 | } 46 | 47 | function handleDisconnect() { 48 | backendDisconnected = true; 49 | 50 | window.removeEventListener('message', handleMessageFromPage); 51 | 52 | window.postMessage( 53 | { 54 | source: 'relay-devtools-content-script', 55 | payload: { 56 | type: 'event', 57 | event: 'shutdown', 58 | }, 59 | }, 60 | '*' 61 | ); 62 | } 63 | 64 | // proxy from main page to devtools (via the background page) 65 | var port = chrome.runtime.connect({ 66 | name: 'content-script', 67 | }); 68 | port.onMessage.addListener(handleMessageFromDevtools); 69 | port.onDisconnect.addListener(handleDisconnect); 70 | 71 | window.addEventListener('message', handleMessageFromPage); 72 | 73 | sayHelloToBackend(); 74 | 75 | // The backend waits to install the global hook until notified by the content script. 76 | // In the event of a page reload, the content script might be loaded before the backend is injected. 77 | // Because of this we need to poll the backend until it has been initialized. 78 | if (!backendInitialized) { 79 | const intervalID: IntervalID = setInterval(() => { 80 | if (backendInitialized || backendDisconnected) { 81 | clearInterval(intervalID); 82 | } else { 83 | sayHelloToBackend(); 84 | } 85 | }, 500); 86 | } 87 | -------------------------------------------------------------------------------- /shells/browser/shared/src/injectGlobalHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | * @format 9 | */ 10 | 11 | /* global chrome */ 12 | 13 | import nullthrows from 'nullthrows'; 14 | 15 | function injectCode() { 16 | const script = document.createElement('script'); 17 | script.src = chrome.runtime.getURL('build/injectedRelayDevToolsDetector.js'); 18 | document.documentElement.appendChild(script); 19 | script.remove(); 20 | } 21 | 22 | let lastDetectionResult; 23 | 24 | // We want to detect when a renderer attaches, and notify the "background page" 25 | // (which is shared between tabs and can highlight the React icon). 26 | // Currently we are in "content script" context, so we can't listen to the hook directly 27 | // (it will be injected directly into the page). 28 | // So instead, the hook will use postMessage() to pass message to us here. 29 | // And when this happens, we'll send a message to the "background page". 30 | window.addEventListener('message', function(evt) { 31 | if (evt.source !== window || !evt.data) { 32 | return; 33 | } 34 | if (evt.data.source === 'relay-devtools-detector') { 35 | lastDetectionResult = { 36 | hasDetectedReact: true, 37 | }; 38 | chrome.runtime.sendMessage(lastDetectionResult); 39 | } else if (evt.data.source === 'relay-devtools-inject-backend') { 40 | const script = document.createElement('script'); 41 | script.src = chrome.runtime.getURL('build/backend.js'); 42 | nullthrows(document.documentElement).appendChild(script); 43 | nullthrows(script.parentNode).removeChild(script); 44 | } 45 | }); 46 | 47 | // NOTE: Firefox WebExtensions content scripts are still alive and not re-injected 48 | // while navigating the history to a document that has not been destroyed yet, 49 | // replay the last detection result if the content script is active and the 50 | // document has been hidden and shown again. 51 | window.addEventListener('pageshow', function(evt) { 52 | if (!lastDetectionResult || evt.target !== window.document) { 53 | return; 54 | } 55 | chrome.runtime.sendMessage(lastDetectionResult); 56 | }); 57 | 58 | injectCode(); 59 | -------------------------------------------------------------------------------- /shells/browser/shared/src/injectedRelayDevToolsDetector.js: -------------------------------------------------------------------------------- 1 | import { installHook } from 'src/hook'; 2 | 3 | // Inject a `__RELAY_DEVTOOLS_HOOK__` global so that Relay can detect that the 4 | // devtools are installed (and skip its suggestion to install the devtools). 5 | (function() { 6 | installHook(window); 7 | window.__RELAY_DEVTOOLS_HOOK__.on('environment', function(evt) { 8 | window.postMessage( 9 | { 10 | source: 'relay-devtools-detector', 11 | }, 12 | '*' 13 | ); 14 | }); 15 | })(); 16 | -------------------------------------------------------------------------------- /shells/browser/shared/src/panel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Portal target container. 9 | window.container = document.getElementById('container'); 10 | 11 | let hasInjectedStyles = false; 12 | 13 | // DevTools styles are injected into the top-level document head (where the main React app is rendered). 14 | // This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled. 15 | window.injectStyles = getLinkTags => { 16 | if (!hasInjectedStyles) { 17 | hasInjectedStyles = true; 18 | 19 | const linkTags = getLinkTags(); 20 | 21 | for (const linkTag of linkTags) { 22 | document.head.appendChild(linkTag); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /shells/browser/shared/src/renderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | /** 11 | * In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts. 12 | * Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself, 13 | * So this entry point (one of the web_accessible_resources) provcides a way to eagerly inject it. 14 | * The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early. 15 | * The normal case (not a reload-and-profile) will not make use of this entry point though. 16 | */ 17 | 18 | import { attach } from 'src/backend/EnvironmentWrapper'; 19 | 20 | Object.defineProperty( 21 | window, 22 | '__REACT_DEVTOOLS_ATTACH__', 23 | ({ 24 | enumerable: false, 25 | // This property needs to be configurable to allow third-party integrations 26 | // to attach their own renderer. Note that using third-party integrations 27 | // is not officially supported. Use at your own risk. 28 | configurable: true, 29 | get() { 30 | return attach; 31 | }, 32 | }: Object) 33 | ); 34 | -------------------------------------------------------------------------------- /shells/browser/shared/src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /* global chrome */ 9 | 10 | const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0; 11 | 12 | export function createViewElementSource(bridge: Bridge, store: Store) { 13 | return function viewElementSource(id) { 14 | const rendererID = store.getRendererIDForElement(id); 15 | if (rendererID != null) { 16 | // Ask the renderer interface to determine the component function, 17 | // and store it as a global variable on the window 18 | bridge.send('viewElementSource', { id, rendererID }); 19 | 20 | setTimeout(() => { 21 | // Ask Chrome to display the location of the component function, 22 | // assuming the renderer found one. 23 | chrome.devtools.inspectedWindow.eval(` 24 | if (window.$type != null) { 25 | inspect(window.$type); 26 | } 27 | `); 28 | }, 100); 29 | } 30 | }; 31 | } 32 | 33 | export type BrowserName = 'Chrome' | 'Firefox'; 34 | 35 | export function getBrowserName(): BrowserName { 36 | return IS_CHROME ? 'Chrome' : 'Firefox'; 37 | } 38 | 39 | export type BrowserTheme = 'dark' | 'light'; 40 | 41 | export function getBrowserTheme(): BrowserTheme { 42 | if (IS_CHROME) { 43 | // chrome.devtools.panels added in Chrome 18. 44 | // chrome.devtools.panels.themeName added in Chrome 54. 45 | return chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light'; 46 | } else { 47 | // chrome.devtools.panels.themeName added in Firefox 55. 48 | // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName 49 | if (chrome.devtools && chrome.devtools.panels) { 50 | switch (chrome.devtools.panels.themeName) { 51 | case 'dark': 52 | return 'dark'; 53 | default: 54 | return 'light'; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /shells/browser/shared/webpack.backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const { resolve } = require('path'); 9 | const { DefinePlugin } = require('webpack'); 10 | const { 11 | getGitHubIssuesURL, 12 | getGitHubURL, 13 | getInternalDevToolsFeedbackGroup, 14 | getVersionString, 15 | } = require('../../utils'); 16 | 17 | const NODE_ENV = process.env.NODE_ENV; 18 | if (!NODE_ENV) { 19 | console.error('NODE_ENV not set'); 20 | process.exit(1); 21 | } 22 | const LOGGING_URL = process.env.LOGGING_URL; 23 | 24 | const __DEV__ = NODE_ENV === 'development'; 25 | const __ENABLE_LOGGER__ = LOGGING_URL != null; 26 | 27 | const GITHUB_URL = getGitHubURL(); 28 | const DEVTOOLS_VERSION = getVersionString(); 29 | const GITHUB_ISSUES_URL = getGitHubIssuesURL(); 30 | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); 31 | 32 | module.exports = { 33 | mode: __DEV__ ? 'development' : 'production', 34 | devtool: __DEV__ ? 'eval-cheap-module-source-map' : false, 35 | entry: { 36 | backend: './src/backend.js', 37 | }, 38 | output: { 39 | path: __dirname + '/build', 40 | filename: '[name].js', 41 | }, 42 | resolve: { 43 | alias: { 44 | src: resolve(__dirname, '../../../src'), 45 | }, 46 | }, 47 | plugins: [ 48 | new DefinePlugin({ 49 | __DEV__: true, 50 | __ENABLE_LOGGER__, 51 | 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 52 | 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 53 | 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 54 | 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`, 55 | 'process.env.LOGGING_URL': `"${LOGGING_URL}"`, 56 | }), 57 | ], 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.js$/, 62 | loader: 'babel-loader', 63 | options: { 64 | configFile: resolve(__dirname, '../../../babel.config.js'), 65 | }, 66 | }, 67 | ], 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /shells/browser/shared/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const { resolve } = require('path'); 9 | const { DefinePlugin } = require('webpack'); 10 | const { 11 | getGitHubIssuesURL, 12 | getGitHubURL, 13 | getInternalDevToolsFeedbackGroup, 14 | getVersionString, 15 | } = require('../../utils'); 16 | 17 | const NODE_ENV = process.env.NODE_ENV; 18 | if (!NODE_ENV) { 19 | console.error('NODE_ENV not set'); 20 | process.exit(1); 21 | } 22 | const LOGGING_URL = process.env.LOGGING_URL; 23 | 24 | const __DEV__ = NODE_ENV === 'development'; 25 | const __ENABLE_LOGGER__ = LOGGING_URL != null; 26 | 27 | const GITHUB_URL = getGitHubURL(); 28 | const DEVTOOLS_VERSION = getVersionString(); 29 | const GITHUB_ISSUES_URL = getGitHubIssuesURL(); 30 | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); 31 | 32 | module.exports = { 33 | mode: __DEV__ ? 'development' : 'production', 34 | devtool: __DEV__ ? 'eval-cheap-module-source-map' : false, 35 | entry: { 36 | background: './src/background.js', 37 | contentScript: './src/contentScript.js', 38 | injectGlobalHook: './src/injectGlobalHook.js', 39 | main: './src/main.js', 40 | panel: './src/panel.js', 41 | renderer: './src/renderer.js', 42 | injectedRelayDevToolsDetector: './src/injectedRelayDevToolsDetector.js', 43 | }, 44 | output: { 45 | path: __dirname + '/build', 46 | filename: '[name].js', 47 | }, 48 | resolve: { 49 | alias: { 50 | src: resolve(__dirname, '../../../src'), 51 | }, 52 | }, 53 | plugins: [ 54 | new DefinePlugin({ 55 | __DEV__: false, 56 | __ENABLE_LOGGER__, 57 | 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 58 | 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 59 | 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 60 | 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`, 61 | 'process.env.NODE_ENV': `"${NODE_ENV}"`, 62 | 'process.env.LOGGING_URL': `"${LOGGING_URL}"`, 63 | }), 64 | ], 65 | module: { 66 | rules: [ 67 | { 68 | test: /\.js$/, 69 | loader: 'babel-loader', 70 | options: { 71 | configFile: resolve(__dirname, '../../../babel.config.js'), 72 | }, 73 | }, 74 | { 75 | test: /\.css$/, 76 | use: [ 77 | { 78 | loader: 'style-loader', 79 | }, 80 | { 81 | loader: 'css-loader', 82 | options: { 83 | sourceMap: true, 84 | modules: true, 85 | localIdentName: '[local]___[hash:base64:5]', 86 | }, 87 | }, 88 | ], 89 | }, 90 | ], 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /shells/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Relay DevTools 6 | 7 | 44 | 45 | 46 |
47 | 48 |
 
49 | 50 | Chrome extension 51 | Firefox extension 52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /shells/dev/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-devtools-experimental", 3 | "alias": ["relay-devtools-experimental"], 4 | "files": ["index.html", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useCallback, useState, Fragment } from 'react'; 11 | import { Environment, RecordSource, Store } from 'relay-runtime'; 12 | import { graphql, QueryRenderer } from 'react-relay'; 13 | import createInBrowserNetwork from './createInBrowserNetwork'; 14 | import Friends from './Friends'; 15 | 16 | function createNewEnvironment(configName: string) { 17 | const source = new RecordSource(); 18 | const store = new Store(source); 19 | var environment = new Environment({ 20 | configName, 21 | network: createInBrowserNetwork(), 22 | store, 23 | log(event) { 24 | console.log('[APP]', event); 25 | }, 26 | }); 27 | return environment; 28 | } 29 | 30 | const initialEnvironment = createNewEnvironment('Example Environment'); 31 | 32 | export type Item = {| 33 | id: number, 34 | isComplete: boolean, 35 | text: string, 36 | |}; 37 | 38 | type Props = {||}; 39 | 40 | export default function App(props: Props): React$MixedElement { 41 | // Add initial environment to environmentList 42 | const [environmentList, updateEnvironmentList] = useState({ 43 | 'Example Environment': initialEnvironment, 44 | }); 45 | const [currentEnvironment, setCurrentEnvironment] = useState( 46 | initialEnvironment 47 | ); 48 | 49 | const createUpdateEnvironmentList = useCallback(() => { 50 | const newEnvironment = createNewEnvironment( 51 | 'Example Environment ' + Object.keys(environmentList).length 52 | ); 53 | 54 | updateEnvironmentList({ 55 | ...environmentList, 56 | [(newEnvironment.configName: $FlowFixMe)]: newEnvironment, 57 | }); 58 | }, [environmentList]); 59 | 60 | const selectNewEnvironment = useCallback( 61 | (e: any) => { 62 | setCurrentEnvironment(environmentList[e.target.value]); 63 | }, 64 | [environmentList] 65 | ); 66 | 67 | return ( 68 | 69 |
70 |

Example Relay App

71 | 74 | 79 |
80 | { 95 | if (props) { 96 | return ( 97 | 98 |
Hello, {props.user.name}!
99 | 100 |
101 | ); 102 | } 103 | return 'Data is not ready.'; 104 | }} 105 | /> 106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/FriendCard.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .Card { 9 | display: flex; 10 | align-items: center; 11 | } 12 | 13 | .ProfilePic { 14 | margin-right: 0.25rem; 15 | width: 2rem; 16 | } 17 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/FriendCard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | import { graphql, createFragmentContainer } from 'react-relay'; 12 | import styles from './FriendCard.css'; 13 | import type { FriendCard_user$data } from './__generated__/FriendCard_user.graphql'; 14 | 15 | type Props = {| 16 | +user: FriendCard_user$data, 17 | |}; 18 | 19 | export default (createFragmentContainer( 20 | function FriendCard(props: Props) { 21 | return ( 22 |
23 | {props.user.name} 28 | {props.user.name} 29 |
30 | ); 31 | }, 32 | { 33 | user: graphql` 34 | fragment FriendCard_user on User { 35 | name 36 | profilePicture { 37 | url 38 | } 39 | } 40 | `, 41 | } 42 | ): $FlowFixMe); 43 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/Friends.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .FriendsList { 9 | padding: 0; 10 | margin: 0; 11 | } 12 | 13 | .ListRow { 14 | list-style: none; 15 | padding: 0; 16 | margin: 0 0 0.5rem; 17 | } 18 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/Friends.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { Fragment } from 'react'; 11 | import { graphql, createPaginationContainer } from 'react-relay'; 12 | import FriendCard from './FriendCard'; 13 | import styles from './Friends.css'; 14 | import type { Friends_user$data } from './__generated__/Friends_user.graphql'; 15 | import type { $RelayProps } from 'react-relay'; 16 | 17 | type Props = {| 18 | +user: Friends_user$data, 19 | +relay: $RelayProps, 20 | |}; 21 | 22 | export default (createPaginationContainer( 23 | function Friends(props: Props) { 24 | const edges = Array.isArray(props.user.friends?.edges) 25 | ? props.user.friends?.edges 26 | : null; 27 | if (edges == null) { 28 | return null; 29 | } 30 | 31 | if (edges.length < 5) { 32 | setTimeout(() => { 33 | props.relay.loadMore(); 34 | }, 300); 35 | } 36 | 37 | return ( 38 | 39 |
    40 | {edges.map(edge => { 41 | return ( 42 |
  • 43 | 44 |
  • 45 | ); 46 | })} 47 |
48 | 56 |
57 | ); 58 | }, 59 | { 60 | user: graphql` 61 | fragment Friends_user on User { 62 | friends(first: 10) @connection(key: "User_friends") { 63 | count 64 | edges { 65 | node { 66 | id 67 | ...FriendCard_user 68 | } 69 | } 70 | } 71 | } 72 | `, 73 | }, 74 | { 75 | getConnectionFromProps(props) { 76 | return props.user && props.user.friends; 77 | }, 78 | getFragmentVariables() { 79 | return {}; 80 | }, 81 | getVariables(props) { 82 | return props; 83 | }, 84 | query: graphql` 85 | query FriendsQuery @relay_test_operation { 86 | user: node(id: "my-id") { 87 | ... on User { 88 | id 89 | ...Friends_user 90 | } 91 | } 92 | } 93 | `, 94 | } 95 | ): $FlowFixMe); 96 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/__generated__/FriendCard_user.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @generated SignedSource<> 3 | * @flow 4 | * @lightSyntaxTransform 5 | * @nogrep 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | 'use strict'; 11 | 12 | /*:: 13 | import type { Fragment, ReaderFragment } from 'relay-runtime'; 14 | import type { FragmentType } from "relay-runtime"; 15 | declare export opaque type FriendCard_user$fragmentType: FragmentType; 16 | export type FriendCard_user$data = {| 17 | +name: ?string, 18 | +profilePicture: ?{| 19 | +url: ?string, 20 | |}, 21 | +$fragmentType: FriendCard_user$fragmentType, 22 | |}; 23 | export type FriendCard_user$key = { 24 | +$data?: FriendCard_user$data, 25 | +$fragmentSpreads: FriendCard_user$fragmentType, 26 | ... 27 | }; 28 | */ 29 | 30 | var node/*: ReaderFragment*/ = { 31 | "argumentDefinitions": [], 32 | "kind": "Fragment", 33 | "metadata": null, 34 | "name": "FriendCard_user", 35 | "selections": [ 36 | { 37 | "alias": null, 38 | "args": null, 39 | "kind": "ScalarField", 40 | "name": "name", 41 | "storageKey": null 42 | }, 43 | { 44 | "alias": null, 45 | "args": null, 46 | "concreteType": "ProfilePicture", 47 | "kind": "LinkedField", 48 | "name": "profilePicture", 49 | "plural": false, 50 | "selections": [ 51 | { 52 | "alias": null, 53 | "args": null, 54 | "kind": "ScalarField", 55 | "name": "url", 56 | "storageKey": null 57 | } 58 | ], 59 | "storageKey": null 60 | } 61 | ], 62 | "type": "User", 63 | "abstractKey": null 64 | }; 65 | 66 | (node/*: any*/).hash = "aa6a813be40074e272758f996347f889"; 67 | 68 | module.exports = ((node/*: any*/)/*: Fragment< 69 | FriendCard_user$fragmentType, 70 | FriendCard_user$data, 71 | >*/); 72 | -------------------------------------------------------------------------------- /shells/dev/relay-app/FriendsList/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import App from './App'; 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /shells/dev/relay-app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | // This test harness mounts each test app as a separate root to test multi-root applications. 11 | 12 | import { createElement } from 'react'; 13 | import { 14 | // $FlowFixMe Flow does not yet know about createRoot() 15 | unstable_createRoot as createRoot, 16 | } from 'react-dom'; 17 | import FriendsList from './FriendsList'; 18 | 19 | import './styles.css'; 20 | 21 | const roots = []; 22 | 23 | function mountHelper(App: (_: mixed) => React$MixedElement) { 24 | const container = document.createElement('div'); 25 | 26 | ((document.body: any): HTMLBodyElement).appendChild(container); 27 | 28 | const root = createRoot(container); 29 | root.render((createElement: $FlowFixMe)(App)); 30 | 31 | roots.push(root); 32 | } 33 | 34 | function mountTestApp() { 35 | mountHelper((FriendsList: $FlowFixMe)); 36 | } 37 | 38 | function unmountTestApp() { 39 | roots.forEach(root => root.unmount()); 40 | } 41 | 42 | mountTestApp(); 43 | 44 | window.parent.mountTestApp = mountTestApp; 45 | window.parent.unmountTestApp = unmountTestApp; 46 | -------------------------------------------------------------------------------- /shells/dev/relay-app/schema.graphql: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | type Query { 7 | node(id: ID!): Node 8 | } 9 | 10 | interface Node { 11 | id: ID! 12 | } 13 | 14 | type User implements Node { 15 | id: ID! 16 | name: String 17 | profilePicture: ProfilePicture 18 | friends(after: ID, before: ID, first: Int, last: Int): FriendsConnection 19 | } 20 | 21 | type ProfilePicture { 22 | url: String 23 | } 24 | 25 | type FriendsConnection { 26 | count: Int 27 | edges: [FriendsEdge] 28 | pageInfo: PageInfo 29 | } 30 | 31 | type FriendsEdge { 32 | cursor: String 33 | node: User 34 | } 35 | 36 | type PageInfo { 37 | hasPreviousPage: Boolean 38 | hasNextPage: Boolean 39 | endCursor: String 40 | startCursor: String 41 | } 42 | -------------------------------------------------------------------------------- /shells/dev/relay-app/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | body { 9 | /* GitHub.com frontend fonts */ 10 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, 11 | sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 12 | font-size: 14px; 13 | line-height: 1.5; 14 | } 15 | 16 | h1 { 17 | font-size: 1.5rem; 18 | font-weight: bold; 19 | margin-bottom: 0.5rem; 20 | } 21 | 22 | button { 23 | margin-bottom: 10px; 24 | margin-right: 5px; 25 | padding: 3px; 26 | } 27 | 28 | select { 29 | padding: 3px; 30 | } 31 | -------------------------------------------------------------------------------- /shells/dev/src/backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import Agent from 'src/backend/agent'; 11 | import Bridge from 'src/bridge'; 12 | import { initBackend } from 'src/backend'; 13 | import type { WallEvent } from 'src/types'; 14 | 15 | const bridge = new Bridge({ 16 | listen(fn) { 17 | const listener = (event: any) => { 18 | fn(event.data); 19 | }; 20 | window.addEventListener('message', listener); 21 | return () => { 22 | window.removeEventListener('message', listener); 23 | }; 24 | }, 25 | sendAll(events: Array) { 26 | window.parent.postMessage(events, '*'); 27 | }, 28 | }); 29 | 30 | const agent = new Agent(bridge); 31 | 32 | initBackend(window.__RELAY_DEVTOOLS_HOOK__, agent, window.parent); 33 | -------------------------------------------------------------------------------- /shells/dev/src/devtools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { createElement } from 'react'; 11 | // $FlowFixMe Flow does not yet know about createRoot() 12 | import { unstable_createRoot as createRoot } from 'react-dom'; 13 | import Bridge from 'src/bridge'; 14 | import { installHook } from 'src/hook'; 15 | import { initDevTools } from 'src/devtools'; 16 | import Store from 'src/devtools/store'; 17 | import DevTools from 'src/devtools/views/DevTools'; 18 | 19 | const iframe = ((document.getElementById('target'): any): HTMLIFrameElement); 20 | 21 | const { contentDocument, contentWindow } = iframe; 22 | 23 | installHook(contentWindow); 24 | 25 | const container = ((document.getElementById('devtools'): any): HTMLElement); 26 | 27 | let isTestAppMounted = true; 28 | 29 | const mountButton = ((document.getElementById( 30 | 'mountButton' 31 | ): any): HTMLButtonElement); 32 | mountButton.addEventListener('click', function() { 33 | if (isTestAppMounted) { 34 | if (typeof window.unmountTestApp === 'function') { 35 | window.unmountTestApp(); 36 | mountButton.innerText = 'Mount test app'; 37 | isTestAppMounted = false; 38 | } 39 | } else { 40 | if (typeof window.mountTestApp === 'function') { 41 | window.mountTestApp(); 42 | mountButton.innerText = 'Unmount test app'; 43 | isTestAppMounted = true; 44 | } 45 | } 46 | }); 47 | 48 | inject('dist/app.js', () => { 49 | initDevTools({ 50 | connect(cb) { 51 | const bridge = new Bridge({ 52 | listen(fn) { 53 | const listener = ({ data }: any) => { 54 | fn(data); 55 | }; 56 | // Preserve the reference to the window we subscribe to, so we can unsubscribe from it when required. 57 | const contentWindowParent = contentWindow.parent; 58 | contentWindowParent.addEventListener('message', listener); 59 | return () => { 60 | contentWindowParent.removeEventListener('message', listener); 61 | }; 62 | }, 63 | sendAll(events) { 64 | contentWindow.postMessage(events, '*'); 65 | }, 66 | }); 67 | 68 | cb(bridge); 69 | 70 | const store = new Store(bridge); 71 | 72 | const root = createRoot(container); 73 | const batch = root.createBatch(); 74 | batch.render( 75 | (createElement: $FlowFixMe)(DevTools, { 76 | bridge, 77 | browserTheme: 'light', 78 | showTabBar: true, 79 | store, 80 | }) 81 | ); 82 | batch.then(() => { 83 | batch.commit(); 84 | 85 | // Initialize the backend only once the DevTools frontend Store has been initialized. 86 | // Otherwise the Store may miss important initial tree op codes. 87 | inject('dist/backend.js'); 88 | }); 89 | }, 90 | 91 | onReload(reloadFn) { 92 | iframe.onload = reloadFn; 93 | }, 94 | }); 95 | }); 96 | 97 | function inject(sourcePath: string, callback: void | (() => void)) { 98 | const script = contentDocument.createElement('script'); 99 | script.onload = callback; 100 | script.src = sourcePath; 101 | 102 | ((contentDocument.body: any): HTMLBodyElement).appendChild(script); 103 | } 104 | -------------------------------------------------------------------------------- /shells/dev/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const Webpack = require('webpack'); 9 | const { 10 | getGitHubIssuesURL, 11 | getGitHubURL, 12 | getInternalDevToolsFeedbackGroup, 13 | getVersionString, 14 | } = require('../utils'); 15 | const path = require('path'); 16 | 17 | const NODE_ENV = process.env.NODE_ENV; 18 | if (!NODE_ENV) { 19 | console.error('NODE_ENV not set'); 20 | process.exit(1); 21 | } 22 | 23 | const TARGET = process.env.TARGET; 24 | if (!TARGET) { 25 | console.error('TARGET not set'); 26 | process.exit(1); 27 | } 28 | 29 | const __DEV__ = NODE_ENV === 'development'; 30 | 31 | const GITHUB_URL = getGitHubURL(); 32 | const DEVTOOLS_VERSION = getVersionString(); 33 | const GITHUB_ISSUES_URL = getGitHubIssuesURL(); 34 | const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); 35 | 36 | const config = { 37 | mode: __DEV__ ? 'development' : 'production', 38 | devtool: false, 39 | entry: { 40 | app: './relay-app/index.js', 41 | backend: './src/backend.js', 42 | devtools: './src/devtools.js', 43 | }, 44 | resolve: { 45 | alias: { 46 | src: path.resolve(__dirname, '../../src'), 47 | '@babel/runtime': path.resolve( 48 | __dirname, 49 | '../../node_modules/@babel/runtime' 50 | ), 51 | }, 52 | }, 53 | plugins: [ 54 | new Webpack.DefinePlugin({ 55 | __DEV__: __DEV__, 56 | 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 57 | 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 58 | 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 59 | 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"`, 60 | }), 61 | ], 62 | module: { 63 | rules: [ 64 | { 65 | test: /\.js$/, 66 | loader: 'babel-loader', 67 | options: { 68 | configFile: require.resolve('../../babel.config.js'), 69 | }, 70 | }, 71 | { 72 | test: /\.css$/, 73 | use: [ 74 | { 75 | loader: 'style-loader', 76 | }, 77 | { 78 | loader: 'css-loader', 79 | options: { 80 | sourceMap: true, 81 | modules: true, 82 | localIdentName: '[local]___[hash:base64:5]', 83 | }, 84 | }, 85 | ], 86 | }, 87 | ], 88 | }, 89 | }; 90 | 91 | config.output = { 92 | path: path.resolve(__dirname, 'dist'), 93 | filename: '[name].js', 94 | publicPath: '/dist/', 95 | }; 96 | if (TARGET === 'local') { 97 | config.devServer = { 98 | static: { 99 | directory: path.join(__dirname, '/'), 100 | }, 101 | hot: true, 102 | port: 8080, 103 | }; 104 | } 105 | 106 | module.exports = config; 107 | -------------------------------------------------------------------------------- /shells/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const { execSync } = require('child_process'); 9 | const { readFileSync, existsSync } = require('fs'); 10 | const { resolve } = require('path'); 11 | 12 | function getCommit() { 13 | if (existsSync(resolve(__dirname, '../.git'))) { 14 | return execSync('git show -s --format=%h') 15 | .toString() 16 | .trim(); 17 | } 18 | return execSync('hg id -i') 19 | .toString() 20 | .trim(); 21 | } 22 | 23 | function getGitHubURL() { 24 | return 'https://github.com/relayjs/relay-devtools'; 25 | } 26 | 27 | function getGitHubIssuesURL() { 28 | return 'https://github.com/relayjs/relay-devtools/issues/new'; 29 | } 30 | 31 | function getInternalDevToolsFeedbackGroup() { 32 | return 'https://fburl.com/ieftwi8l'; 33 | } 34 | 35 | function getVersionString() { 36 | const packageVersion = JSON.parse( 37 | readFileSync(resolve(__dirname, '../package.json')) 38 | ).version; 39 | 40 | const commit = getCommit(); 41 | 42 | return `${packageVersion}-${commit}`; 43 | } 44 | 45 | module.exports = { 46 | getCommit, 47 | getGitHubIssuesURL, 48 | getGitHubURL, 49 | getInternalDevToolsFeedbackGroup, 50 | getVersionString, 51 | }; 52 | -------------------------------------------------------------------------------- /src/Logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow strict-local 8 | */ 9 | 10 | import type { TabID } from './devtools/views/DevTools'; 11 | 12 | export type LogEvent = 13 | | {| 14 | +event_name: 'loaded-dev-tools', 15 | |} 16 | | {| 17 | +event_name: 'selected-tab', 18 | +extra: TabID, // selected tab 19 | |} 20 | | {| 21 | +event_name: 'selected-store-tab', 22 | +extra: string, // selected tab 23 | |}; 24 | 25 | export type LogFunction = LogEvent => void; 26 | 27 | let logFunctions: Array = []; 28 | export function logEvent(event: LogEvent): void { 29 | logFunctions.forEach(log => { 30 | log(event); 31 | }); 32 | } 33 | 34 | export function registerEventLogger(logFunction: LogFunction): () => void { 35 | logFunctions.push(logFunction); 36 | return function unregisterEventLogger() { 37 | logFunctions = logFunctions.filter(log => log !== logFunction); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/__tests__/__mocks__/cssMock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | module.exports = {}; 11 | -------------------------------------------------------------------------------- /src/__tests__/bridge-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type { WallEvent } from '../types'; 11 | describe('Bridge', () => { 12 | let Bridge; 13 | 14 | beforeEach(() => { 15 | Bridge = require('src/bridge').default; 16 | }); 17 | 18 | it('should shutdown properly', () => { 19 | const wall = { 20 | listen: jest.fn<[any], _>(() => () => {}), 21 | sendAll: jest.fn<[Array], void>(), 22 | }; 23 | const bridge = new Bridge(wall); 24 | 25 | // Check that we're wired up correctly. 26 | bridge.send('init'); 27 | jest.runAllTimers(); 28 | expect(wall.sendAll).toHaveBeenCalledWith([ 29 | { event: 'init', payload: undefined }, 30 | ]); 31 | 32 | // Should flush pending messages and then shut down. 33 | wall.sendAll.mockClear(); 34 | bridge.send('update', '1'); 35 | bridge.send('update', '2'); 36 | bridge.shutdown(); 37 | jest.runAllTimers(); 38 | expect(wall.sendAll).toHaveBeenCalledWith([ 39 | { event: 'update', payload: '1' }, 40 | { event: 'update', payload: '2' }, 41 | { event: 'shutdown', payload: undefined }, 42 | ]); 43 | 44 | // Verify that the Bridge doesn't send messages after shutdown. 45 | spyOn(console, 'warn'); 46 | wall.sendAll.mockClear(); 47 | bridge.send('should not send'); 48 | jest.runAllTimers(); 49 | expect(wall.sendAll).not.toHaveBeenCalled(); 50 | expect(console.warn).toHaveBeenCalledWith( 51 | 'Cannot send message "should not send" through a Bridge that has been shutdown.' 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/__tests__/setupEnv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import storage from 'local-storage-fallback'; 11 | 12 | // In case async/await syntax is used in a test. 13 | import 'regenerator-runtime/runtime'; 14 | 15 | // DevTools stores preferences between sessions in localStorage 16 | if (!global.hasOwnProperty('localStorage')) { 17 | global.localStorage = storage; 18 | } 19 | 20 | // Mimic the global we set with Webpack's DefinePlugin 21 | global.__DEV__ = process.env.NODE_ENV !== 'production'; 22 | -------------------------------------------------------------------------------- /src/__tests__/setupTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type { BackendBridge, FrontendBridge } from 'src/bridge'; 11 | 12 | const env = jasmine.getEnv(); 13 | env.beforeEach(() => { 14 | // These files should be required (and re-reuired) before each test, 15 | // rather than imported at the head of the module. 16 | // That's because we reset modules between tests, 17 | // which disconnects the DevTool's cache from the current dispatcher ref. 18 | const Agent = require('src/backend/agent').default; 19 | const { initBackend } = require('src/backend'); 20 | const Bridge = require('src/bridge').default; 21 | const Store = require('src/devtools/store').default; 22 | const { installHook } = require('src/hook'); 23 | 24 | // Fake timers let us flush Bridge operations between setup and assertions. 25 | jest.useFakeTimers(); 26 | 27 | const originalConsoleError = console.error; 28 | // $FlowFixMe 29 | console.error = (...args) => { 30 | if (args[0] === 'Warning: Relay DevTools encountered an error: %s') { 31 | // Rethrow errors from React. 32 | throw args[1]; 33 | } 34 | originalConsoleError.apply(console, args); 35 | }; 36 | 37 | installHook(global); 38 | 39 | const bridgeListeners = []; 40 | const bridge = new Bridge({ 41 | listen(callback) { 42 | bridgeListeners.push(callback); 43 | return () => { 44 | const index = bridgeListeners.indexOf(callback); 45 | if (index >= 0) { 46 | bridgeListeners.splice(index, 1); 47 | } 48 | }; 49 | }, 50 | sendAll(events) { 51 | bridgeListeners.forEach(callback => callback(events)); 52 | }, 53 | }); 54 | 55 | const agent = new Agent(((bridge: any): BackendBridge)); 56 | 57 | const hook = global.__RELAY_DEVTOOLS_HOOK__; 58 | 59 | initBackend(hook, agent, global); 60 | 61 | const store = new Store(((bridge: any): FrontendBridge)); 62 | 63 | global.agent = agent; 64 | global.bridge = bridge; 65 | global.store = store; 66 | }); 67 | env.afterEach(() => { 68 | delete global.__RELAY_DEVTOOLS_HOOK__; 69 | 70 | // It's important to reset modules between test runs; 71 | // Without this, ReactDOM won't re-inject itself into the new hook. 72 | // It's also important to reset after tests, rather than before, 73 | // so that we don't disconnect the ReactCurrentDispatcher ref. 74 | jest.resetModules(); 75 | }); 76 | -------------------------------------------------------------------------------- /src/__tests__/storeSerializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // test() is part of Jest's serializer API 9 | export function test(maybeStore) { 10 | // It's important to lazy-require the Store rather than imported at the head of the module. 11 | // Because we reset modules between tests, different Store implementations will be used for each test. 12 | // Unfortunately Jest does not reset its own serializer modules. 13 | return maybeStore instanceof require('src/devtools/store').default; 14 | } 15 | 16 | // print() is part of Jest's serializer API 17 | export function print(store, serialize, indent) { 18 | return printStore(store); 19 | } 20 | 21 | export function printElement(element, includeWeight = false) { 22 | let prefix = ' '; 23 | if (element.children.length > 0) { 24 | prefix = element.isCollapsed ? '▸' : '▾'; 25 | } 26 | 27 | let key = ''; 28 | if (element.key !== null) { 29 | key = ` key="${element.key}"`; 30 | } 31 | 32 | let hocs = ''; 33 | if (element.hocDisplayNames !== null) { 34 | hocs = ` [${element.hocDisplayNames.join('][')}]`; 35 | } 36 | 37 | let suffix = ''; 38 | if (includeWeight) { 39 | suffix = ` (${element.isCollapsed ? 1 : element.weight})`; 40 | } 41 | 42 | return `${' '.repeat(element.depth + 1)}${prefix} <${element.displayName || 43 | 'null'}${key}>${hocs}${suffix}`; 44 | } 45 | 46 | export function printOwnersList(elements, includeWeight = false) { 47 | return elements 48 | .map(element => printElement(element, includeWeight)) 49 | .join('\n'); 50 | } 51 | 52 | // Used for Jest snapshot testing. 53 | // May also be useful for visually debugging the tree, so it lives on the Store. 54 | export function printStore(store, includeWeight = false) { 55 | const snapshotLines = []; 56 | 57 | let rootWeight = 0; 58 | 59 | store.roots.forEach(rootID => { 60 | const { weight } = store.getElementByID(rootID); 61 | 62 | snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : '')); 63 | 64 | for (let i = rootWeight; i < rootWeight + weight; i++) { 65 | const element = store.getElementAtIndex(i); 66 | 67 | if (element == null) { 68 | throw Error(`Could not find element at index ${i}`); 69 | } 70 | 71 | snapshotLines.push(printElement(element, includeWeight)); 72 | } 73 | 74 | rootWeight += weight; 75 | }); 76 | 77 | // Make sure the pretty-printed test align with the Store's reported number of total rows. 78 | if (rootWeight !== store.numElements) { 79 | throw Error( 80 | `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})` 81 | ); 82 | } 83 | 84 | // If roots have been unmounted, verify that they've been removed from maps. 85 | // This helps ensure the Store doesn't leak memory. 86 | store.assertExpectedRootMapSizes(); 87 | 88 | return snapshotLines.join('\n'); 89 | } 90 | -------------------------------------------------------------------------------- /src/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import typeof ReactTestRenderer from 'react-test-renderer'; 11 | 12 | export function act(callback: Function): void { 13 | const TestUtils = require('react-dom/test-utils'); 14 | TestUtils.act(() => { 15 | callback(); 16 | }); 17 | 18 | // Flush Bridge operations 19 | TestUtils.act(() => { 20 | jest.runAllTimers(); 21 | }); 22 | } 23 | 24 | export async function actAsync( 25 | cb: () => mixed, 26 | recursivelyFlush: boolean = true 27 | ): Promise { 28 | const TestUtils = require('react-dom/test-utils'); 29 | 30 | // $FlowFixMe Flow doens't know about "await act()" yet 31 | await TestUtils.act(async () => { 32 | await cb(); 33 | }); 34 | 35 | if (recursivelyFlush) { 36 | while (jest.getTimerCount() > 0) { 37 | // $FlowFixMe Flow doens't know about "await act()" yet 38 | await TestUtils.act(async () => { 39 | jest.runAllTimers(); 40 | }); 41 | } 42 | } else { 43 | // $FlowFixMe Flow doesn't know about "await act()" yet 44 | await TestUtils.act(async () => { 45 | jest.runOnlyPendingTimers(); 46 | }); 47 | } 48 | } 49 | 50 | export function beforeEachProfiling(): void { 51 | // Mock React's timing information so that test runs are predictable. 52 | jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); 53 | 54 | // DevTools itself uses performance.now() to offset commit times 55 | // so they appear relative to when profiling was started in the UI. 56 | jest 57 | .spyOn(performance, 'now') 58 | .mockImplementation( 59 | jest.requireActual('scheduler/unstable_mock').unstable_now 60 | ); 61 | } 62 | 63 | export function getRendererID(): number { 64 | if (global.agent == null) { 65 | throw Error('Agent unavailable.'); 66 | } 67 | const ids = Object.keys(global.agent._rendererInterfaces); 68 | if (ids.length !== 1) { 69 | throw Error('Multiple renderers attached.'); 70 | } 71 | return parseInt(ids[0], 10); 72 | } 73 | 74 | export function requireTestRenderer(): ReactTestRenderer { 75 | let hook; 76 | try { 77 | // Hide the hook before requiring TestRenderer, so we don't end up with a loop. 78 | hook = global.__RELAY_DEVTOOLS_HOOK__; 79 | delete global.__RELAY_DEVTOOLS_HOOK__; 80 | 81 | return require('react-test-renderer'); 82 | } finally { 83 | global.__RELAY_DEVTOOLS_HOOK__ = hook; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/backend/agent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import EventEmitter from 'events'; 11 | import type { BackendBridge } from 'src/bridge'; 12 | 13 | import type { EnvironmentID, EnvironmentWrapper } from './types'; 14 | 15 | export default class Agent extends EventEmitter<{| 16 | shutdown: [], 17 | refreshStore: [], 18 | |}> { 19 | _bridge: BackendBridge; 20 | _recordChangeDescriptions: boolean = false; 21 | _environmentWrappers: { 22 | [key: EnvironmentID]: EnvironmentWrapper, 23 | } = {}; 24 | 25 | constructor(bridge: BackendBridge) { 26 | super(); 27 | 28 | this._bridge = bridge; 29 | 30 | bridge.addListener('shutdown', this.shutdown); 31 | bridge.addListener('refreshStore', this.refreshStore); 32 | } 33 | 34 | get environmentWrappers(): { 35 | [key: EnvironmentID]: EnvironmentWrapper, 36 | } { 37 | return this._environmentWrappers; 38 | } 39 | 40 | shutdown = () => { 41 | // Clean up the overlay if visible, and associated events. 42 | this.emit('shutdown'); 43 | }; 44 | 45 | refreshStore = (id: EnvironmentID) => { 46 | const wrapper = this._environmentWrappers[id]; 47 | wrapper.sendStoreRecords(); 48 | }; 49 | 50 | onEnvironmentInitialized = (data: mixed) => { 51 | this._bridge.send('environmentInitialized', [data]); 52 | }; 53 | 54 | setEnvironmentWrapper = ( 55 | id: number, 56 | environmentWrapper: EnvironmentWrapper 57 | ) => { 58 | this._environmentWrappers[id] = environmentWrapper; 59 | }; 60 | 61 | onStoreData = (data: mixed) => { 62 | this._bridge.send('storeRecords', [data]); 63 | }; 64 | 65 | onEnvironmentEvent = (data: mixed) => { 66 | this._bridge.send('events', [data]); 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/backend/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type { 11 | DevToolsHook, 12 | RelayEnvironment, 13 | EnvironmentWrapper, 14 | } from './types'; 15 | import type Agent from './agent'; 16 | 17 | import { attach } from './EnvironmentWrapper'; 18 | 19 | export function initBackend( 20 | hook: DevToolsHook, 21 | agent: Agent, 22 | global: Object 23 | ): () => void { 24 | const subs = [ 25 | hook.sub('environment.event', data => { 26 | agent.onEnvironmentEvent(data); 27 | }), 28 | hook.sub('environment.store', data => { 29 | agent.onStoreData(data); 30 | }), 31 | hook.sub( 32 | 'environment-attached', 33 | ({ 34 | id, 35 | environment, 36 | environmentWrapper, 37 | }: { 38 | id: number, 39 | environment: RelayEnvironment, 40 | environmentWrapper: EnvironmentWrapper, 41 | }) => { 42 | agent.setEnvironmentWrapper(id, environmentWrapper); 43 | agent.onEnvironmentInitialized({ 44 | id: id, 45 | environmentName: environment.configName, 46 | }); 47 | // Now that the Store and the renderer interface are connected, 48 | // it's time to flush the pending operation codes to the frontend. 49 | environmentWrapper.flushInitialOperations(); 50 | } 51 | ), 52 | ]; 53 | 54 | const attachEnvironment = (id: number, environment: RelayEnvironment) => { 55 | let environmentWrapper = hook.environmentWrappers.get(id); 56 | 57 | // Inject any not-yet-injected renderers (if we didn't reload-and-profile) 58 | if (!environmentWrapper) { 59 | environmentWrapper = attach(hook, id, environment, global); 60 | hook.environmentWrappers.set(id, environmentWrapper); 61 | } 62 | 63 | // Notify the DevTools frontend about new renderers. 64 | hook.emit('environment-attached', { 65 | id, 66 | environment, 67 | environmentWrapper, 68 | }); 69 | }; 70 | 71 | // Connect renderers that have already injected themselves. 72 | hook.environments.forEach((environment, id) => { 73 | attachEnvironment(id, environment); 74 | }); 75 | 76 | // Connect any new renderers that injected themselves. 77 | subs.push( 78 | hook.sub( 79 | 'environment', 80 | ({ id, environment }: { id: number, environment: RelayEnvironment }) => { 81 | attachEnvironment(id, environment); 82 | } 83 | ) 84 | ); 85 | 86 | return () => { 87 | subs.forEach(fn => fn()); 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/backend/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | export type EnvironmentID = number; 11 | 12 | export type RelayRecordSource = { 13 | getRecordIDs: () => string, 14 | get: (id: string) => any, 15 | toJSON: () => any, 16 | }; 17 | 18 | export type RelayStore = { 19 | getSource: () => RelayRecordSource, 20 | __log: (event: Object) => void, 21 | }; 22 | 23 | export type RelayEnvironment = { 24 | execute: (options: any) => any, 25 | configName: ?string, 26 | getStore: () => RelayStore, 27 | __log: (event: Object) => void, 28 | }; 29 | 30 | export type EnvironmentWrapper = { 31 | flushInitialOperations: () => void, 32 | sendStoreRecords: () => void, 33 | cleanup: () => void, 34 | }; 35 | 36 | export type Handler = (data: any) => void; 37 | 38 | export type DevToolsHook = { 39 | registerEnvironment: (env: RelayEnvironment) => number | null, 40 | // listeners: { [key: string]: Array }, 41 | environmentWrappers: Map, 42 | environments: Map, 43 | 44 | emit: (event: string, data: any) => void, 45 | on: (event: string, handler: Handler) => void, 46 | off: (event: string, handler: Handler) => void, 47 | // reactDevtoolsAgent?: ?Object, 48 | sub: (event: string, handler: Handler) => () => void, 49 | }; 50 | -------------------------------------------------------------------------------- /src/backend/util/RelayTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | export type Disposable = { 11 | dispose(): void, 12 | }; 13 | 14 | // Supports legacy SubscribeFunction definitions. Do not use in new code. 15 | export type LegacyObserver<-T> = {| 16 | +onCompleted?: ?() => void, 17 | +onError?: ?(error: Error) => void, 18 | +onNext?: ?(data: T) => void, 19 | |}; 20 | -------------------------------------------------------------------------------- /src/backend/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | export function copyWithSet( 11 | obj: Object | Array, 12 | path: Array, 13 | value: any, 14 | index: number = 0 15 | ): Object | Array { 16 | console.log('[utils] copyWithSet()', obj, path, index, value); 17 | if (index >= path.length) { 18 | return value; 19 | } 20 | const key = parseInt(path[index]); 21 | const updated = Array.isArray(obj) ? obj.slice() : { ...obj }; 22 | updated[key] = copyWithSet(obj[key], path, value, index + 1); 23 | return updated; 24 | } 25 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | // Flip this flag to true to enable verbose console debug logging. 11 | export const __DEBUG__ = true; 12 | -------------------------------------------------------------------------------- /src/devtools/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type { FrontendBridge } from 'src/bridge'; 11 | 12 | type Shell = {| 13 | connect: (callback: Function) => void, 14 | onReload: (reloadFn: Function) => void, 15 | |}; 16 | 17 | export function initDevTools(shell: Shell) { 18 | shell.connect((bridge: FrontendBridge) => { 19 | // TODO ... 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/devtools/views/Button.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Button { 11 | border: none; 12 | background: var(--color-button-background); 13 | color: var(--color-button); 14 | padding: 0; 15 | border-radius: 0.25rem; 16 | flex: 0 0 auto; 17 | } 18 | .ButtonContent { 19 | display: inline-flex; 20 | align-items: center; 21 | border-radius: 0.25rem; 22 | padding: 0.25rem; 23 | } 24 | 25 | .Button:hover { 26 | color: var(--color-button-hover); 27 | } 28 | .Button:active { 29 | color: var(--color-button-focus); 30 | outline: none; 31 | } 32 | .Button:focus, 33 | .ButtonContent:focus { 34 | outline: none; 35 | } 36 | 37 | .Button:focus > .ButtonContent { 38 | background: var(--color-button-background-focus); 39 | } 40 | 41 | .Button:disabled, 42 | .Button:disabled:active { 43 | background: var(--color-button-background); 44 | color: var(--color-button-disabled); 45 | cursor: default; 46 | } 47 | -------------------------------------------------------------------------------- /src/devtools/views/Button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | import Tooltip from '@reach/tooltip'; 12 | 13 | import styles from './Button.css'; 14 | import tooltipStyles from './Tooltip.css'; 15 | 16 | type Props = { 17 | children: React$Node, 18 | className?: string, 19 | title?: string, 20 | ... 21 | }; 22 | 23 | export default function Button({ 24 | children, 25 | className = '', 26 | title = '', 27 | ...rest 28 | }: Props): React$MixedElement { 29 | const innerButton = ( 30 | 35 | ); 36 | 37 | if (title) { 38 | return ( 39 | 40 | {innerButton} 41 | 42 | ); 43 | } 44 | 45 | return innerButton; 46 | } 47 | -------------------------------------------------------------------------------- /src/devtools/views/ButtonIcon.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .ButtonIcon { 11 | width: 1rem; 12 | height: 1rem; 13 | fill: currentColor; 14 | } 15 | -------------------------------------------------------------------------------- /src/devtools/views/Components/ExpandCollapseToggle.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .ExpandCollapseToggle { 11 | flex: 0 0 1rem; 12 | width: 1rem; 13 | height: 1rem; 14 | padding: 0; 15 | color: var(--color-expand-collapse-toggle); 16 | } 17 | -------------------------------------------------------------------------------- /src/devtools/views/Components/ExpandCollapseToggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | import Button from '../Button'; 12 | import ButtonIcon from '../ButtonIcon'; 13 | 14 | import styles from './ExpandCollapseToggle.css'; 15 | 16 | type ExpandCollapseToggleProps = {| 17 | isOpen: boolean, 18 | setIsOpen: Function, 19 | |}; 20 | 21 | export default function ExpandCollapseToggle({ 22 | isOpen, 23 | setIsOpen, 24 | }: ExpandCollapseToggleProps): React$MixedElement { 25 | return ( 26 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/devtools/views/Components/InspectedElementTree.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .InspectedElementTree { 11 | padding: 0.25rem; 12 | border-top: 1px solid var(--color-border); 13 | } 14 | .InspectedElementTree:first-of-type { 15 | border-top: none; 16 | } 17 | 18 | .HeaderRow { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | .Header { 24 | flex: 1 1; 25 | font-family: var(--font-family-sans); 26 | } 27 | 28 | .Item { 29 | display: flex; 30 | } 31 | 32 | .Name { 33 | color: var(--color-attribute-name); 34 | flex: 0 0 auto; 35 | } 36 | .Name:after { 37 | content: ': '; 38 | color: var(--color-text); 39 | margin-right: 0.5rem; 40 | } 41 | 42 | .Value { 43 | color: var(--color-attribute-value); 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | } 47 | 48 | .None { 49 | color: var(--color-dimmer); 50 | font-style: italic; 51 | } 52 | 53 | .Empty { 54 | color: var(--color-dimmer); 55 | font-style: italic; 56 | padding-left: 0.75rem; 57 | } 58 | -------------------------------------------------------------------------------- /src/devtools/views/Components/InspectedElementTree.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { copy } from 'clipboard-js'; 11 | import React, { useCallback } from 'react'; 12 | import Button from '../Button'; 13 | import ButtonIcon from '../ButtonIcon'; 14 | import KeyValue from './KeyValue'; 15 | import { alphaSortEntries, serializeDataForCopy } from '../utils'; 16 | import styles from './InspectedElementTree.css'; 17 | 18 | type Props = {| 19 | data: Object | null, 20 | label: string, 21 | showWhenEmpty?: boolean, 22 | |}; 23 | 24 | export default function InspectedElementTree({ 25 | data, 26 | label, 27 | showWhenEmpty = false, 28 | }: Props): null | React$MixedElement { 29 | //TODO(damassart): Clean this up 30 | const entries = data != null ? Object.entries(data) : null; 31 | if (entries !== null) { 32 | entries.sort(alphaSortEntries); 33 | } 34 | 35 | const isEmpty = entries === null || entries.length === 0; 36 | 37 | const handleCopy = useCallback(() => copy(serializeDataForCopy(data)), [ 38 | data, 39 | ]); 40 | 41 | if (isEmpty && !showWhenEmpty) { 42 | return null; 43 | } else { 44 | return ( 45 |
46 |
47 |
{label}
48 | {!isEmpty && ( 49 | 52 | )} 53 |
54 | {isEmpty &&
None
} 55 | {!isEmpty && 56 | (entries: any).map(([name, value]) => ( 57 | 65 | ))} 66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/devtools/views/Components/KeyValue.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Item:not([hidden]) { 11 | display: flex; 12 | } 13 | 14 | .Name { 15 | color: var(--color-dim); 16 | flex: 0 0 auto; 17 | user-select: none; 18 | } 19 | .Name:after { 20 | content: ': '; 21 | color: var(--color-text); 22 | margin-right: 0.5rem; 23 | } 24 | 25 | .Value { 26 | color: var(--color-attribute-value); 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .None { 33 | color: var(--color-dimmer); 34 | font-style: italic; 35 | } 36 | 37 | .ExpandCollapseToggleSpacer { 38 | flex: 0 0 1rem; 39 | width: 1rem; 40 | } 41 | 42 | .Empty { 43 | color: var(--color-dimmer); 44 | } 45 | -------------------------------------------------------------------------------- /src/devtools/views/DevTools.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .DevTools { 11 | width: 100%; 12 | height: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | background-color: var(--color-background); 16 | color: var(--color-text); 17 | } 18 | 19 | .TabBar { 20 | flex: 0 0 auto; 21 | display: flex; 22 | align-items: center; 23 | padding: 0rem; 24 | background-color: var(--color-background); 25 | border-top: 1px solid var(--color-border); 26 | font-family: var(--font-family-sans); 27 | font-size: var(--font-size-sans-large); 28 | user-select: none; 29 | 30 | /* Electron drag area */ 31 | -webkit-app-region: drag; 32 | } 33 | 34 | .FeedbackLinks { 35 | display: flex; 36 | flex-direction: row; 37 | align-items: center; 38 | } 39 | 40 | .FeedbackLinks a { 41 | text-decoration: none; 42 | color: var(--color-text); 43 | font-size: var(--font-size-sans-normal); 44 | margin: 3px; 45 | } 46 | 47 | .Spacer { 48 | flex: 1; 49 | } 50 | 51 | select { 52 | padding: 2px; 53 | margin-right: 50px; 54 | border-radius: 5px; 55 | } 56 | 57 | .TabContent { 58 | flex: 1 1 100%; 59 | overflow: auto; 60 | } 61 | 62 | .DevToolsVersion { 63 | font-size: var(--font-size-sans-normal); 64 | margin-right: 0.5rem; 65 | } 66 | 67 | .DevToolsVersion:before { 68 | font-size: var(--font-size-sans-large); 69 | content: 'DevTools '; 70 | } 71 | 72 | .environmentDropDown { 73 | width: 35%; 74 | text-overflow: ellipsis; 75 | } 76 | 77 | @media screen and (max-width: 400px) { 78 | .DevToolsVersion:before { 79 | content: ''; 80 | } 81 | } 82 | 83 | @media screen and (max-width: 300px) { 84 | .DevToolsVersion { 85 | display: none; 86 | } 87 | } 88 | 89 | .IconSizeLarge { 90 | margin-right: 0.5rem; 91 | color: var(--color-button-active); 92 | } 93 | 94 | .IconSizeLarge { 95 | width: 1.5rem; 96 | height: 1.5rem; 97 | } 98 | 99 | @media screen and (max-width: 525px) { 100 | .IconSizeLarge { 101 | margin-right: 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/devtools/views/ErrorBoundary.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .ErrorBoundary { 11 | height: 100%; 12 | width: 100%; 13 | background-color: white; 14 | color: red; 15 | padding: 0.5rem; 16 | overflow: auto; 17 | } 18 | 19 | .Header { 20 | font-size: var(--font-size-sans-large); 21 | font-weight: bold; 22 | } 23 | 24 | .Stack { 25 | margin-top: 0.5rem; 26 | white-space: pre-wrap; 27 | font-family: var(--font-family-monospace); 28 | font-size: var(--font-size-monospace-small); 29 | background-color: hsl(0, 100%, 97%); 30 | border: 1px solid hsl(0, 100%, 92%); 31 | border-radius: 0.25rem; 32 | padding: 0.5rem; 33 | } 34 | -------------------------------------------------------------------------------- /src/devtools/views/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { Component } from 'react'; 11 | import styles from './ErrorBoundary.css'; 12 | 13 | type Props = {| 14 | children: React$Node, 15 | |}; 16 | 17 | type State = {| 18 | callStack: string | null, 19 | componentStack: string | null, 20 | errorMessage: string | null, 21 | hasError: boolean, 22 | |}; 23 | 24 | export default class ErrorBoundary extends Component { 25 | state: State = { 26 | callStack: null, 27 | componentStack: null, 28 | errorMessage: null, 29 | hasError: false, 30 | }; 31 | 32 | componentDidCatch(error: any, { componentStack }: any) { 33 | const errorMessage = 34 | typeof error === 'object' && error.hasOwnProperty('message') 35 | ? error.message 36 | : error; 37 | 38 | const callStack = 39 | typeof error === 'object' && error.hasOwnProperty('stack') 40 | ? error.stack 41 | .split('\n') 42 | .slice(1) 43 | .join('\n') 44 | : null; 45 | 46 | this.setState({ 47 | callStack, 48 | componentStack, 49 | errorMessage, 50 | hasError: true, 51 | }); 52 | } 53 | 54 | render(): $FlowFixMe { 55 | const { children } = this.props; 56 | const { callStack, componentStack, errorMessage, hasError } = this.state; 57 | 58 | let bugURL = process.env.GITHUB_URL; 59 | if (bugURL) { 60 | const title = `Error: "${errorMessage || ''}"`; 61 | const label = '😭 bug'; 62 | 63 | let body = '\n'; 64 | body += '\n---------------------------------------------'; 65 | body += '\nPlease do not remove the text below this line'; 66 | body += '\n---------------------------------------------'; 67 | body += `\n\nDevTools version: ${process.env.DEVTOOLS_VERSION || ''}`; 68 | if (callStack) { 69 | body += `\n\nCall stack: ${callStack.trim()}`; 70 | } 71 | if (componentStack) { 72 | body += `\n\nComponent stack: ${componentStack.trim()}`; 73 | } 74 | 75 | bugURL += `/issues/new?labels=${encodeURI(label)}&title=${encodeURI( 76 | title 77 | )}&body=${encodeURI(body)}`; 78 | } 79 | 80 | if (hasError) { 81 | return ( 82 |
83 |
84 | An error was thrown: "{errorMessage}" 85 |
86 | {bugURL && ( 87 | 93 | Report this issue 94 | 95 | )} 96 | {!!callStack && ( 97 |
98 | The error was thrown {callStack.trim()} 99 |
100 | )} 101 | {!!componentStack && ( 102 |
103 | The error occurred {componentStack.trim()} 104 |
105 | )} 106 |
107 | ); 108 | } 109 | 110 | return children; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/devtools/views/Icon.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Icon { 11 | width: 1rem; 12 | height: 1rem; 13 | fill: currentColor; 14 | } 15 | -------------------------------------------------------------------------------- /src/devtools/views/ModalDialog.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Background { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | display: flex; 15 | align-items: flex-start; 16 | justify-content: center; 17 | padding: 1rem; 18 | background-color: var(--color-modal-background); 19 | overflow: auto; 20 | } 21 | 22 | .Dialog { 23 | position: relative; 24 | z-index: 3; 25 | width: 25rem; 26 | min-width: 20rem; 27 | max-width: 100%; 28 | display: inline-block; 29 | background-color: var(--color-background); 30 | padding: 0.5rem; 31 | border: 1px solid var(--color-border); 32 | border-radius: 0.25rem; 33 | } 34 | 35 | .Title { 36 | font-size: var(--font-size-sans-large); 37 | margin-bottom: 0.5rem; 38 | } 39 | 40 | .Buttons { 41 | text-align: right; 42 | margin-top: 0.5rem; 43 | } 44 | -------------------------------------------------------------------------------- /src/devtools/views/ModalDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { 11 | createContext, 12 | useCallback, 13 | useContext, 14 | useMemo, 15 | useReducer, 16 | useRef, 17 | } from 'react'; 18 | import Button from './Button'; 19 | import { useModalDismissSignal } from './hooks'; 20 | 21 | import styles from './ModalDialog.css'; 22 | 23 | type DIALOG_ACTION_HIDE = {| 24 | type: 'HIDE', 25 | |}; 26 | type DIALOG_ACTION_SHOW = {| 27 | type: 'SHOW', 28 | content: React$Node, 29 | title?: React$Node | null, 30 | |}; 31 | 32 | type Action = DIALOG_ACTION_HIDE | DIALOG_ACTION_SHOW; 33 | 34 | type Dispatch = (action: Action) => void; 35 | 36 | type State = {| 37 | content: React$Node | null, 38 | isVisible: boolean, 39 | title: React$Node | null, 40 | |}; 41 | 42 | type ModalDialogContextType = {| 43 | ...State, 44 | dispatch: Dispatch, 45 | |}; 46 | 47 | const ModalDialogContext: $FlowFixMe = createContext( 48 | ((null: any): ModalDialogContextType) 49 | ); 50 | ModalDialogContext.displayName = 'ModalDialogContext'; 51 | 52 | function dialogReducer(state: State, action: Action) { 53 | switch (action.type) { 54 | case 'HIDE': 55 | return { 56 | content: null, 57 | isVisible: false, 58 | title: null, 59 | }; 60 | case 'SHOW': 61 | return { 62 | content: action.content, 63 | isVisible: true, 64 | title: action.title || null, 65 | }; 66 | default: 67 | throw new Error(`Invalid action "${action.type}"`); 68 | } 69 | } 70 | 71 | type Props = {| 72 | children: React$Node, 73 | |}; 74 | 75 | function ModalDialogContextController({ children }: Props): React$MixedElement { 76 | const [state, dispatch] = useReducer(dialogReducer, { 77 | content: null, 78 | isVisible: false, 79 | title: null, 80 | }); 81 | 82 | const value = useMemo( 83 | () => ({ 84 | content: state.content, 85 | isVisible: state.isVisible, 86 | title: state.title, 87 | dispatch, 88 | }), 89 | [state, dispatch] 90 | ); 91 | 92 | return ( 93 | 94 | {children} 95 | 96 | ); 97 | } 98 | 99 | function ModalDialog(_: {||}): React$MixedElement | null { 100 | const { isVisible } = useContext(ModalDialogContext); 101 | return isVisible ? : null; 102 | } 103 | 104 | function ModalDialogImpl(_: {||}) { 105 | const { content, dispatch, title } = useContext(ModalDialogContext); 106 | const dismissModal = useCallback(() => dispatch({ type: 'HIDE' }), [ 107 | dispatch, 108 | ]); 109 | const modalRef = useRef(null); 110 | 111 | useModalDismissSignal(modalRef, dismissModal); 112 | 113 | return ( 114 |
115 |
116 | {title !== null &&
{title}
} 117 | {content} 118 |
119 | 122 |
123 |
124 |
125 | ); 126 | } 127 | 128 | export { ModalDialog, ModalDialogContext, ModalDialogContextController }; 129 | -------------------------------------------------------------------------------- /src/devtools/views/Network/Network.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Network { 11 | width: 100%; 12 | height: 100%; 13 | position: relative; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: stretch; 17 | font-family: var(--font-family-sans); 18 | font-size: var(--font-size-sans-normal); 19 | background-color: var(--color-background); 20 | color: var(--color-text); 21 | } 22 | 23 | .Toolbar { 24 | padding: 0 0.25rem; 25 | flex: 0; 26 | display: flex; 27 | align-items: center; 28 | border-bottom: 1px solid var(--color-border); 29 | } 30 | 31 | .Spacer { 32 | flex: 1; 33 | } 34 | 35 | .SectionTitle { 36 | color: #616161; 37 | font-weight: bold; 38 | } 39 | 40 | .SectionContent { 41 | margin-left: 20px; 42 | white-space: pre-wrap; 43 | } 44 | 45 | .Requests { 46 | border-right: 1px solid #d0d0d0; 47 | background-color: var(--color-background); 48 | color: var(--color-text); 49 | flex: 1; 50 | overflow: scroll; 51 | } 52 | 53 | .RequestsSearchBar { 54 | width: 100%; 55 | padding: 4px; 56 | background-color: var(--color-background-search-bar); 57 | color: var(--color-search-bar); 58 | } 59 | 60 | .RequestNotFound { 61 | margin: 5px; 62 | font-style: italic; 63 | color: var(--color-text); 64 | } 65 | 66 | .Content { 67 | display: flex; 68 | height: 100%; 69 | overflow: hidden; 70 | } 71 | 72 | .Request { 73 | background-color: var(--color-background); 74 | cursor: pointer; 75 | padding: 1px 4px; 76 | } 77 | .Request:hover { 78 | background-color: var(--color-background-hover); 79 | } 80 | .Request:nth-child(2n) { 81 | background-color: var(--color-background); 82 | } 83 | .Request:nth-child(2n):hover { 84 | background-color: var(--color-background-hover); 85 | } 86 | .Request.SelectedRequest, 87 | .Request.SelectedRequest:hover { 88 | background-color: var(--color-background-selected); 89 | color: var(--color-text-selected); 90 | } 91 | .Request::before { 92 | display: inline-block; 93 | width: 1em; 94 | content: ''; 95 | } 96 | .StatusActive::before { 97 | color: #6a6; 98 | content: '●'; 99 | } 100 | .StatusUnsubscribed { 101 | color: #aaa; 102 | } 103 | .StatusError { 104 | color: #a66; 105 | } 106 | 107 | .RequestDetails { 108 | flex: 3; 109 | overflow: scroll; 110 | padding: 3px; 111 | } 112 | -------------------------------------------------------------------------------- /src/devtools/views/RelayLogo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .RelayLogo { 11 | width: 1.75rem; 12 | height: 1.75rem; 13 | margin: 0 0.75rem 0 0.25rem; 14 | color: var(--color-button-active); 15 | } 16 | -------------------------------------------------------------------------------- /src/devtools/views/RelayLogo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | 12 | import styles from './RelayLogo.css'; 13 | 14 | export default function RelayLogo(): React$MixedElement { 15 | return ( 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/devtools/views/Settings/GeneralSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useCallback, useContext } from 'react'; 11 | import { SettingsContext } from './SettingsContext'; 12 | 13 | import styles from './SettingsShared.css'; 14 | 15 | export default function GeneralSettings(_: {||}): React$MixedElement { 16 | const { displayDensity, setDisplayDensity, theme, setTheme } = useContext( 17 | SettingsContext 18 | ); 19 | 20 | const updateDisplayDensity = useCallback( 21 | ({ currentTarget }: any) => { 22 | setDisplayDensity(currentTarget.value); 23 | }, 24 | [setDisplayDensity] 25 | ); 26 | const updateTheme = useCallback( 27 | ({ currentTarget }: any) => { 28 | setTheme(currentTarget.value); 29 | }, 30 | [setTheme] 31 | ); 32 | 33 | return ( 34 |
35 |
36 |
Theme
37 | 42 |
43 | 44 |
45 |
Display density
46 | 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/devtools/views/Settings/SettingsModal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Background { 11 | position: absolute; 12 | width: 100%; 13 | top: 0; 14 | bottom: 0; 15 | background-color: var(--color-modal-background); 16 | display: flex; 17 | align-items: flex-start; 18 | justify-content: center; 19 | font-size: var(--font-size-sans-normal); 20 | padding: 1rem; 21 | } 22 | 23 | .Modal { 24 | display: flex; 25 | flex-direction: column; 26 | flex: 0 1 auto; 27 | max-height: 100%; 28 | background-color: var(--color-background); 29 | border: 1px solid var(--color-border); 30 | border-radius: 0.25rem; 31 | overflow: auto; 32 | width: 400px; 33 | max-width: 100%; 34 | } 35 | 36 | .Spacer { 37 | flex: 1; 38 | } 39 | 40 | .Tabs { 41 | display: flex; 42 | flex-direction: row; 43 | border-bottom: 1px solid var(--color-border); 44 | padding-right: 0.25rem; 45 | flex: 0 0 auto; 46 | } 47 | 48 | .Content { 49 | padding: 0.5rem; 50 | flex: 0 1 auto; 51 | overflow: auto; 52 | } 53 | -------------------------------------------------------------------------------- /src/devtools/views/Settings/SettingsModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useCallback, useContext, useEffect, useRef } from 'react'; 11 | import { SettingsModalContext } from './SettingsModalContext'; 12 | import Button from '../Button'; 13 | import ButtonIcon from '../ButtonIcon'; 14 | import TabBar from '../TabBar'; 15 | import { useLocalStorage, useModalDismissSignal } from '../hooks'; 16 | import GeneralSettings from './GeneralSettings'; 17 | 18 | import styles from './SettingsModal.css'; 19 | 20 | type TabID = 'general' | 'components'; 21 | 22 | export default function SettingsModal(_: {||}): React$MixedElement | null { 23 | const { isModalShowing } = useContext(SettingsModalContext); 24 | return !isModalShowing ? null : ; 25 | } 26 | 27 | function getSelectedTabView(selectedTabID: TabID) { 28 | switch (selectedTabID) { 29 | case 'general': 30 | return ; 31 | // case 'components': 32 | // return ; 33 | // break; 34 | default: 35 | break; 36 | } 37 | return null; 38 | } 39 | 40 | function SettingsModalImpl(_: {||}) { 41 | const { setIsModalShowing } = useContext(SettingsModalContext); 42 | const dismissModal = useCallback(() => setIsModalShowing(false), [ 43 | setIsModalShowing, 44 | ]); 45 | 46 | const [selectedTabID, selectTab] = useLocalStorage( 47 | 'Relay::DevTools::selectedSettingsTabID', 48 | 'general' 49 | ); 50 | 51 | const modalRef = useRef(null); 52 | useModalDismissSignal(modalRef, dismissModal); 53 | 54 | useEffect(() => { 55 | if (modalRef.current !== null) { 56 | modalRef.current.focus(); 57 | } 58 | }, [modalRef]); 59 | 60 | const view = getSelectedTabView(selectedTabID); 61 | 62 | return ( 63 |
64 |
65 |
66 | 73 |
74 | 77 |
78 |
{view}
79 |
80 |
81 | ); 82 | } 83 | 84 | const tabs = [ 85 | { 86 | id: 'general', 87 | icon: 'settings', 88 | label: 'General', 89 | }, 90 | { 91 | id: 'components', 92 | icon: 'components', 93 | label: 'Components', 94 | }, 95 | ]; 96 | -------------------------------------------------------------------------------- /src/devtools/views/Settings/SettingsModalContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { createContext, useMemo, useState } from 'react'; 11 | 12 | export type DisplayDensity = 'comfortable' | 'compact'; 13 | export type Theme = 'auto' | 'light' | 'dark'; 14 | 15 | type Context = { 16 | isModalShowing: boolean, 17 | setIsModalShowing: (value: boolean) => void, 18 | }; 19 | 20 | const SettingsModalContext: $FlowFixMe = createContext( 21 | ((null: any): Context) 22 | ); 23 | SettingsModalContext.displayName = 'SettingsModalContext'; 24 | 25 | function SettingsModalContextController({ 26 | children, 27 | }: {| 28 | children: React$Node, 29 | |}): React$MixedElement { 30 | const [isModalShowing, setIsModalShowing] = useState(false); 31 | 32 | const value = useMemo(() => ({ isModalShowing, setIsModalShowing }), [ 33 | isModalShowing, 34 | setIsModalShowing, 35 | ]); 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | } 43 | 44 | export { SettingsModalContext, SettingsModalContextController }; 45 | -------------------------------------------------------------------------------- /src/devtools/views/Settings/SettingsModalContextToggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useCallback, useContext } from 'react'; 11 | import { SettingsModalContext } from './SettingsModalContext'; 12 | import Button from '../Button'; 13 | import ButtonIcon from '../ButtonIcon'; 14 | 15 | export default function SettingsModalContextToggle(): React$MixedElement { 16 | const { setIsModalShowing } = useContext(SettingsModalContext); 17 | 18 | const showFilterModal = useCallback(() => setIsModalShowing(true), [ 19 | setIsModalShowing, 20 | ]); 21 | 22 | return ( 23 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/devtools/views/Settings/SettingsShared.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Settings { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: flex-start; 14 | justify-content: flex-start; 15 | font-family: var(--font-family-sans); 16 | font-size: var(--font-size-sans-normal); 17 | } 18 | 19 | .Setting { 20 | margin-bottom: 0.5rem; 21 | } 22 | .Setting:last-of-type { 23 | margin-bottom: 0; 24 | } 25 | 26 | .OptionGroup { 27 | display: inline-flex; 28 | flex-direction: row; 29 | align-items: center; 30 | user-select: none; 31 | margin: 0 1rem 0.5rem 0; 32 | } 33 | .OptionGroup:last-of-type { 34 | margin-right: 0; 35 | } 36 | 37 | .RadioLabel { 38 | display: inline; 39 | margin-right: 0.5rem; 40 | } 41 | 42 | .Select { 43 | } 44 | 45 | .CheckboxOption { 46 | display: block; 47 | padding: 0 0 0.5rem; 48 | } 49 | 50 | .ScreenshotThrottling { 51 | display: inline-block; 52 | background-color: var(--color-background-hover); 53 | padding: 0.25rem 0.5rem; 54 | border-radius: 0.25rem; 55 | } 56 | 57 | .HRule { 58 | height: 1px; 59 | background-color: var(--color-border); 60 | width: 100%; 61 | border: none; 62 | margin: 0.5rem 0; 63 | } 64 | 65 | .Header { 66 | font-size: var(--font-size-sans-large); 67 | margin-top: 0.5rem; 68 | } 69 | 70 | .ButtonIcon { 71 | margin-right: 0.25rem; 72 | } 73 | 74 | .NoFiltersCell { 75 | padding: 0.25rem 0; 76 | color: var(--color-dim); 77 | } 78 | 79 | .Table { 80 | min-width: 20rem; 81 | margin-top: 0.5rem; 82 | border-spacing: 0; 83 | } 84 | 85 | .TableRow { 86 | padding-bottom: 0.5rem; 87 | } 88 | 89 | .TableCell { 90 | padding: 0; 91 | padding-right: 0.5rem; 92 | } 93 | .TableCell:last-of-type { 94 | text-align: right; 95 | padding-right: 0; 96 | } 97 | 98 | .Input { 99 | border: 1px solid var(--color-border); 100 | border-radius: 0.125rem; 101 | padding: 0.125rem; 102 | } 103 | 104 | .InvalidRegExp, 105 | .InvalidRegExp:active, 106 | .InvalidRegExp:focus, 107 | .InvalidRegExp:hover { 108 | color: var(--color-value-invalid); 109 | } 110 | 111 | .ToggleOffInvalid, 112 | .ToggleOnInvalid, 113 | .ToggleOff, 114 | .ToggleOn { 115 | border-radius: 0.75rem; 116 | width: 1rem; 117 | height: 0.625rem; 118 | display: flex; 119 | align-items: center; 120 | padding: 0.125rem; 121 | } 122 | .ToggleOffInvalid { 123 | background-color: var(--color-toggle-background-invalid); 124 | justify-content: flex-start; 125 | } 126 | .ToggleOnInvalid { 127 | background-color: var(--color-toggle-background-invalid); 128 | justify-content: flex-end; 129 | } 130 | .ToggleOff { 131 | background-color: var(--color-toggle-background-off); 132 | justify-content: flex-start; 133 | } 134 | .ToggleOn { 135 | background-color: var(--color-toggle-background-on); 136 | justify-content: flex-end; 137 | } 138 | 139 | .ToggleInsideOff, 140 | .ToggleInsideOn { 141 | border-radius: 0.375rem; 142 | width: 0.375rem; 143 | height: 0.375rem; 144 | background-color: var(--color-toggle-text); 145 | } 146 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/EventLogger/EventLogger.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Record { 11 | background-color: var(--color-background); 12 | cursor: pointer; 13 | padding: 1px 4px; 14 | } 15 | 16 | .Record:hover { 17 | background-color: var(--color-background-hover); 18 | } 19 | 20 | .Record:nth-child(2n) { 21 | background-color: var(--color-background-inactive); 22 | } 23 | 24 | .Record:nth-child(2n):hover { 25 | background-color: var(--color-background-hover); 26 | } 27 | 28 | .Record.SelectedRecord, 29 | .Record.SelectedRecord:hover { 30 | background-color: var(--color-background-selected); 31 | color: var(--color-text-selected); 32 | } 33 | 34 | .Record.RelatedRecord, 35 | .Record.RelatedRecord:hover { 36 | background-color: var(--color-background-related); 37 | } 38 | 39 | .Record::before { 40 | display: inline-block; 41 | width: 1em; 42 | content: ''; 43 | } 44 | 45 | .AllEventsList { 46 | padding-right: 0em; 47 | border-right: 1px solid #d0d0d0; 48 | flex: 1; 49 | overflow: scroll; 50 | } 51 | 52 | .EventsSearchBar { 53 | width: 100%; 54 | padding: 4px; 55 | background-color: var(--color-background-search-bar); 56 | color: var(--color-search-bar); 57 | } 58 | 59 | .EventsTabContent { 60 | display: inline-flex; 61 | width: 100%; 62 | height: 100%; 63 | } 64 | 65 | .RecordNotFound { 66 | margin: 5px; 67 | font-style: italic; 68 | color: var(--color-text); 69 | } 70 | 71 | .RequestDetails { 72 | flex: 2; 73 | overflow: scroll; 74 | padding: 3px; 75 | } 76 | 77 | .RestoreEvent { 78 | padding: 0.5em; 79 | font-size: 16px; 80 | flex: 2; 81 | } 82 | 83 | .gcEvent { 84 | flex: 2; 85 | width: 100%; 86 | height: 100%; 87 | overflow: scroll; 88 | display: flex; 89 | position: relative; 90 | flex-direction: column; 91 | align-items: stretch; 92 | background-color: var(--color-background); 93 | color: var(--color-text); 94 | } 95 | 96 | .gcExplained { 97 | border-bottom: 1px solid var(--color-border); 98 | font-size: 14px; 99 | padding: 0.3em; 100 | padding-bottom: 0.4em; 101 | } 102 | 103 | .RecordsTabContent { 104 | flex: 2; 105 | overflow: scroll; 106 | display: inline-flex; 107 | } 108 | 109 | .TabBar { 110 | flex: 0 0 auto; 111 | display: flex; 112 | align-items: center; 113 | padding-left: 0.5rem; 114 | background-color: var(--color-background); 115 | border-top: 1px solid var(--color-border); 116 | border-bottom: 1px solid var(--color-border); 117 | font-family: var(--font-family-sans); 118 | font-size: var(--font-size-sans-large); 119 | user-select: none; 120 | margin-bottom: 0.2em; 121 | 122 | /* Electron drag area */ 123 | -webkit-app-region: drag; 124 | } 125 | 126 | .NotifyComplete { 127 | width: 100%; 128 | height: 100%; 129 | flex: 2; 130 | overflow: scroll; 131 | position: relative; 132 | display: flex; 133 | flex-direction: column; 134 | align-items: stretch; 135 | background-color: var(--color-background); 136 | color: var(--color-text); 137 | } 138 | 139 | .Spacer { 140 | flex: 1; 141 | } 142 | 143 | .NotRecording { 144 | font-size: 16px; 145 | padding: 0.5em; 146 | } 147 | 148 | .SectionTitle { 149 | color: #616161; 150 | font-weight: bold; 151 | } 152 | 153 | .SectionContent { 154 | margin-left: 20px; 155 | white-space: pre-wrap; 156 | } 157 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/EventLogger/EventLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useState } from 'react'; 11 | import AllEventsList from './AllEventsList'; 12 | import NetworkEventDisplay from './NetworkEventDisplay'; 13 | import StoreEventDisplay from './StoreEventDisplay'; 14 | import type { LogEvent } from '../../../../types'; 15 | 16 | import styles from './EventLogger.css'; 17 | import type { ReactSetStateFunction } from 'react'; 18 | 19 | export type Props = {| 20 | allEvents: ?$ReadOnlyArray, 21 | isRecording: boolean, 22 | checked: { [string]: boolean }, 23 | |}; 24 | 25 | function AllEventsDetails({ 26 | events, 27 | selectedEventID, 28 | setSelectedEventID, 29 | }: {| 30 | events: $ReadOnlyArray, 31 | selectedEventID: number, 32 | setSelectedEventID: ReactSetStateFunction, 33 | |}) { 34 | const [selectedRecordID, setSelectedRecordID] = useState(''); 35 | const selectedEvent = events[selectedEventID]; 36 | 37 | if (events == null) { 38 | return null; 39 | } 40 | if (selectedEvent == null) { 41 | return ( 42 |
43 | This event may have been deleted 44 |
45 | ); 46 | } 47 | 48 | return selectedEvent.name.startsWith('store') ? ( 49 | 54 | ) : ( 55 | 56 | ); 57 | } 58 | 59 | export default function EventLogger({ 60 | allEvents, 61 | isRecording, 62 | checked, 63 | }: Props): React$MixedElement | null { 64 | const [selectedEventID, setSelectedEventID] = useState(0); 65 | 66 | if (allEvents == null && !isRecording) { 67 | return ( 68 |
69 | Event Logger is not recording. To record, hit the record button on the 70 | top left of the tab. 71 |
72 | ); 73 | } else if (allEvents == null && isRecording) { 74 | return
Loading events...
; 75 | } else if (allEvents == null) { 76 | return null; 77 | } 78 | 79 | return ( 80 |
81 | 87 | 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/InspectedElementTreeStoreInspector.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .InspectedElementTree { 11 | padding: 0.25rem; 12 | border-top: 1px solid var(--color-border); 13 | } 14 | .InspectedElementTree:first-of-type { 15 | border-top: none; 16 | } 17 | 18 | .HeaderRow { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | .Header { 24 | flex: 1 1; 25 | font-family: var(--font-family-sans); 26 | } 27 | 28 | .Item { 29 | display: flex; 30 | } 31 | 32 | .Name { 33 | color: var(--color-attribute-name); 34 | flex: 0 0 auto; 35 | } 36 | .Name:after { 37 | content: ': '; 38 | color: var(--color-text); 39 | margin-right: 0.5rem; 40 | } 41 | 42 | .Value { 43 | color: var(--color-attribute-value); 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | } 47 | 48 | .None { 49 | color: var(--color-dimmer); 50 | font-style: italic; 51 | } 52 | 53 | .Empty { 54 | color: var(--color-dimmer); 55 | font-style: italic; 56 | padding-left: 0.75rem; 57 | } 58 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/InspectedElementTreeStoreInspector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { copy } from 'clipboard-js'; 11 | import React, { useCallback, useMemo } from 'react'; 12 | import Button from '../Button'; 13 | import ButtonIcon from '../ButtonIcon'; 14 | import KeyValue from './KeyValue'; 15 | import { alphaSortEntries, serializeDataForCopy } from '../utils'; 16 | import styles from './InspectedElementTreeStoreInspector.css'; 17 | import type { StoreRecords } from '../../../types.js'; 18 | 19 | type Props = {| 20 | data: Object, 21 | label: string, 22 | records: StoreRecords, 23 | setSelectedRecordID: (id: string) => void, 24 | showWhenEmpty?: boolean, 25 | |}; 26 | 27 | export default function InspectedElementTree({ 28 | data, 29 | label, 30 | records, 31 | setSelectedRecordID, 32 | showWhenEmpty = false, 33 | }: Props): null | React$MixedElement { 34 | const sortedEntries = useMemo( 35 | () => Object.entries(data).sort(alphaSortEntries), 36 | [data] 37 | ); 38 | 39 | const isEmpty = sortedEntries.length === 0; 40 | 41 | const handleCopy = useCallback(() => copy(serializeDataForCopy(data)), [ 42 | data, 43 | ]); 44 | 45 | if (isEmpty && !showWhenEmpty) { 46 | return null; 47 | } else { 48 | return ( 49 |
50 |
51 |
{label}
52 | {!isEmpty && ( 53 | 56 | )} 57 |
58 | {isEmpty &&
None
} 59 | {!isEmpty && 60 | (sortedEntries: any).map(([name, value]) => ( 61 | 71 | ))} 72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/KeyValue.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Item:not([hidden]) { 11 | display: flex; 12 | } 13 | 14 | .Name { 15 | color: var(--color-dim); 16 | flex: 0 0 auto; 17 | user-select: none; 18 | } 19 | .Name:after { 20 | content: ': '; 21 | color: var(--color-text); 22 | margin-right: 0.5rem; 23 | } 24 | 25 | .Value { 26 | color: var(--color-attribute-value); 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .None { 33 | color: var(--color-dimmer); 34 | font-style: italic; 35 | } 36 | 37 | .ExpandCollapseToggleSpacer { 38 | flex: 0 0 1rem; 39 | width: 1rem; 40 | } 41 | 42 | .Empty { 43 | color: var(--color-dimmer); 44 | } 45 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/OptimisticUpdates.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .TabContent { 11 | display: inline-flex; 12 | width: 100%; 13 | } 14 | 15 | .NoOptimisticUpdates { 16 | font-size: 16px; 17 | padding: 0.5em; 18 | } 19 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/OptimisticUpdates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useState } from 'react'; 11 | import RecordList from './RecordList'; 12 | import RecordDetails from './RecordDetails'; 13 | import type { StoreRecords } from '../../../types'; 14 | 15 | import styles from './OptimisticUpdates.css'; 16 | 17 | export type Props = {| 18 | optimisticUpdates: ?StoreRecords, 19 | |}; 20 | 21 | export default function Optimistic({ 22 | optimisticUpdates, 23 | }: Props): React$MixedElement { 24 | const [selectedRecordID, setSelectedRecordID] = useState(''); 25 | if (optimisticUpdates == null) { 26 | return ( 27 |
No Optimistic Updates!
28 | ); 29 | } 30 | const optimisticUpdatesByType = new Map>(); 31 | 32 | for (const key in optimisticUpdates) { 33 | const rec = optimisticUpdates[key]; 34 | if (rec != null) { 35 | const arr = optimisticUpdatesByType.get(rec.__typename); 36 | if (arr) { 37 | arr.push(key); 38 | } else { 39 | optimisticUpdatesByType.set(rec.__typename, [key]); 40 | } 41 | } 42 | } 43 | 44 | const selectedRecord = optimisticUpdates[selectedRecordID]; 45 | 46 | return ( 47 |
48 | 54 | 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/RecordDetails.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .RecordDetails { 11 | flex: 2; 12 | overflow: scroll; 13 | padding: 3px; 14 | } 15 | 16 | .NoRecordDetails { 17 | flex: 2; 18 | overflow: scroll; 19 | font-size: 16px; 20 | padding: 0.5em; 21 | } 22 | 23 | .SectionTitle { 24 | color: #616161; 25 | font-weight: bold; 26 | } 27 | 28 | .SectionContent { 29 | margin-left: 20px; 30 | white-space: pre-wrap; 31 | } 32 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/RecordDetails.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | import InspectedElementTreeStoreInspector from './InspectedElementTreeStoreInspector'; 12 | import type { StoreRecords, Record } from '../../../types'; 13 | 14 | import styles from './RecordDetails.css'; 15 | 16 | export type Props = {| 17 | records: StoreRecords, 18 | selectedRecord: ?Record, 19 | setSelectedRecordID: string => void, 20 | |}; 21 | 22 | function Section(props: {| title: string, children: React$Node |}) { 23 | return ( 24 | <> 25 |
{props.title}
26 |
{props.children}
27 | 28 | ); 29 | } 30 | 31 | export default function RecordDetails({ 32 | records, 33 | selectedRecord, 34 | setSelectedRecordID, 35 | }: Props): React$MixedElement { 36 | if (selectedRecord == null) { 37 | return
No record selected
; 38 | } 39 | 40 | const { __id, __typename, ...data } = selectedRecord; 41 | 42 | const typename: string = (__typename: any); 43 | const id: string = (__id: any); 44 | 45 | return ( 46 |
47 |
{id}
48 |
{typename}
49 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/RecordList.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Collapse { 11 | display: flex; 12 | } 13 | 14 | .Type { 15 | background-color: var(--color-background); 16 | color: var(--color-text); 17 | font-weight: bold; 18 | cursor: pointer; 19 | width: 100%; 20 | border: none; 21 | text-align: left; 22 | outline: none; 23 | } 24 | 25 | .PlusMinusCollapse { 26 | float: right; 27 | font-size: 15px; 28 | margin: 0px 5px; 29 | } 30 | 31 | .RecordListContent { 32 | display: block; 33 | } 34 | 35 | .Record { 36 | background-color: var(--color-background); 37 | cursor: pointer; 38 | padding: 1px 4px; 39 | } 40 | 41 | .Record:hover { 42 | background-color: var(--color-background-hover); 43 | } 44 | 45 | .Record:nth-child(2n) { 46 | background-color: var(--color-background-inactive); 47 | } 48 | 49 | .Record:nth-child(2n):hover { 50 | background-color: var(--color-background-hover); 51 | } 52 | 53 | .Record.SelectedRecord, 54 | .Record.SelectedRecord:hover { 55 | background-color: var(--color-background-selected); 56 | color: var(--color-text-selected); 57 | } 58 | .Record::before { 59 | display: inline-block; 60 | width: 1em; 61 | content: ''; 62 | } 63 | 64 | .Records { 65 | border-right: 1px solid #d0d0d0; 66 | background-color: var(--color-background); 67 | color: var(--color-text); 68 | flex: 1; 69 | overflow: scroll; 70 | } 71 | 72 | .RecordsSearchBar { 73 | width: 100%; 74 | padding: 4px; 75 | background-color: var(--color-background-search-bar); 76 | color: var(--color-search-bar); 77 | } 78 | 79 | .RecordNotFound { 80 | margin: 5px; 81 | font-style: italic; 82 | color: var(--color-text); 83 | } 84 | 85 | .Loading { 86 | font-size: 16px; 87 | padding: 0.5em; 88 | } 89 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/RecordingImportExportButtons.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .VRule { 11 | height: 20px; 12 | width: 1px; 13 | border-left: 1px solid var(--color-border); 14 | padding-left: 0.25rem; 15 | margin-left: 0.25rem; 16 | } 17 | 18 | /** 19 | * https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications 20 | */ 21 | .Input { 22 | position: absolute !important; 23 | height: 1px; 24 | width: 1px; 25 | overflow: hidden; 26 | clip: rect(1px, 1px, 1px, 1px); 27 | } 28 | 29 | .Errors { 30 | margin: 0em 0.5em; 31 | } 32 | 33 | .ErrorMsg { 34 | margin: 4px; 35 | text-align: center; 36 | vertical-align: middle; 37 | color: #dc3545; 38 | } 39 | 40 | .environmentDropDown { 41 | width: 25%; 42 | text-overflow: ellipsis; 43 | margin-left: 0.4em; 44 | margin-right: 0em; 45 | } 46 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/Snapshot.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Record { 11 | background-color: var(--color-background); 12 | cursor: pointer; 13 | padding: 1px 4px; 14 | } 15 | 16 | .Record:hover { 17 | background-color: var(--color-background-hover); 18 | } 19 | 20 | .Record:nth-child(2n) { 21 | background-color: var(--color-background-inactive); 22 | } 23 | 24 | .Record:nth-child(2n):hover { 25 | background-color: var(--color-background-hover); 26 | } 27 | 28 | .Record.SelectedRecord, 29 | .Record.SelectedRecord:hover { 30 | background-color: var(--color-background-selected); 31 | color: var(--color-text-selected); 32 | } 33 | .Record::before { 34 | display: inline-block; 35 | width: 1em; 36 | content: ''; 37 | } 38 | 39 | .SnapshotList { 40 | width: 30%; 41 | margin-left: 0.5em; 42 | padding-right: 0.5em; 43 | border-right: 1px solid #d0d0d0; 44 | } 45 | 46 | .SnapshotList h2 { 47 | margin: 0px; 48 | } 49 | 50 | .TabContent { 51 | display: inline-flex; 52 | width: 100%; 53 | } 54 | 55 | .NoSnapshots { 56 | font-size: 16px; 57 | padding: 0.5em; 58 | } 59 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/Snapshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useState } from 'react'; 11 | import RecordList from './RecordList'; 12 | import RecordDetails from './RecordDetails'; 13 | 14 | import styles from './Snapshot.css'; 15 | import type { ReactSetStateFunction } from 'react'; 16 | 17 | export type Props = {| 18 | envSnapshotList: Object, 19 | envSnapshotListByType: Object, 20 | currentEnvID: ?number, 21 | |}; 22 | 23 | function SnapshotList({ 24 | snapshotList, 25 | setSelectedSnapshotID, 26 | selectedSnapshotID, 27 | }: {| 28 | selectedSnapshotID: number | string, 29 | setSelectedSnapshotID: ReactSetStateFunction, 30 | snapshotList: any, 31 | |}) { 32 | const snapshotIDs = Object.keys(snapshotList).map(snapshotID => { 33 | return ( 34 |
{ 37 | setSelectedSnapshotID((snapshotID: $FlowFixMe)); 38 | }} 39 | className={`${styles.Record} ${ 40 | snapshotID === selectedSnapshotID ? styles.SelectedRecord : '' 41 | }`} 42 | > 43 | {snapshotID} 44 |
45 | ); 46 | }); 47 | 48 | return ( 49 |
50 |

Snapshots

51 |
{snapshotIDs}
52 |
53 | ); 54 | } 55 | 56 | function SnapshotDetails({ 57 | snapshotList, 58 | snapshotListByType, 59 | selectedSnapshotID, 60 | }: {| 61 | selectedSnapshotID: number | string, 62 | snapshotList: any, 63 | snapshotListByType: any, 64 | |}) { 65 | const [selectedRecordID, setSelectedRecordID] = useState(''); 66 | const snapshotRecords = snapshotList[selectedSnapshotID]; 67 | if (snapshotRecords == null) { 68 | return null; 69 | } 70 | const snapshotRecordsByType = snapshotListByType[selectedSnapshotID]; 71 | const selectedRecord = snapshotRecords[selectedRecordID]; 72 | 73 | return ( 74 |
75 | 81 | 86 |
87 | ); 88 | } 89 | 90 | export default function Snapshots({ 91 | envSnapshotList, 92 | envSnapshotListByType, 93 | currentEnvID, 94 | }: Props): React$MixedElement { 95 | const [selectedSnapshotID, setSelectedSnapshotID] = useState(0); 96 | 97 | if ( 98 | envSnapshotList == null || 99 | Object.keys(envSnapshotList).length <= 0 || 100 | currentEnvID == null || 101 | envSnapshotList[currentEnvID] == null 102 | ) { 103 | return ( 104 |
105 | No Snapshots!
To take a snapshot, hit the snapshot button! 106 |
107 | ); 108 | } 109 | 110 | const snapshotList = envSnapshotList[currentEnvID]; 111 | const snapshotListByType = envSnapshotListByType[currentEnvID]; 112 | 113 | return ( 114 |
115 | 120 | 125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/StoreTabBar.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Tab, 11 | .TabCurrent, 12 | .TabDisabled { 13 | height: 100%; 14 | margin-right: 0.5em; 15 | display: flex; 16 | align-items: center; 17 | cursor: pointer; 18 | border-top: 3px solid transparent; 19 | border-bottom: 3px solid transparent; 20 | user-select: none; 21 | color: var(--color-text); 22 | 23 | /* Electron drag area */ 24 | -webkit-app-region: no-drag; 25 | } 26 | .Tab:hover, 27 | .TabCurrent:hover { 28 | background-color: var(--color-background-hover); 29 | } 30 | .Tab:focus-within, 31 | .TabCurrent:focus-within { 32 | background-color: var(--color-background-hover); 33 | } 34 | 35 | .TabCurrent { 36 | border-bottom: 3px solid var(--color-tab-selected-border); 37 | } 38 | 39 | .TabDisabled { 40 | color: var(--color-dim); 41 | cursor: default; 42 | } 43 | 44 | .TabSizeLarge { 45 | font-size: var(--font-size-sans-large); 46 | padding: 0.5rem 1rem; 47 | } 48 | .TabSizeSmall { 49 | font-size: var(--font-size-sans-normal); 50 | padding: 0.25rem 0.5rem; 51 | } 52 | 53 | .Input { 54 | width: 0; 55 | margin: 0; 56 | opacity: 0; 57 | } 58 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/StoreTabBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import classNames from 'classnames'; 11 | import React, { Fragment, useCallback } from 'react'; 12 | import Tooltip from '@reach/tooltip'; 13 | 14 | import styles from './StoreTabBar.css'; 15 | import tooltipStyles from '../Tooltip.css'; 16 | 17 | type TabInfo = {| 18 | id: string, 19 | label: string, 20 | title?: string, 21 | |}; 22 | 23 | export type Props = {| 24 | tabID: string, 25 | disabled?: boolean, 26 | id: string, 27 | selectTab: (tab: TabInfo) => void, 28 | size: 'large' | 'small', 29 | tabs: Array, 30 | |}; 31 | 32 | export default function TabBar({ 33 | tabID, 34 | disabled = false, 35 | id: groupName, 36 | selectTab, 37 | size, 38 | tabs, 39 | }: Props): React$MixedElement { 40 | if (!tabs.some(tab => tab.id === tabID)) { 41 | selectTab(tabs[0]); 42 | } 43 | 44 | const onChange = useCallback( 45 | ({ currentTarget }: any) => selectTab(currentTarget.value), 46 | [selectTab] 47 | ); 48 | 49 | const tabClassName = 50 | size === 'large' ? styles.TabSizeLarge : styles.TabSizeSmall; 51 | 52 | return ( 53 | 54 | {tabs.map(tab => { 55 | const innerButton = ( 56 | 82 | ); 83 | 84 | if (tab.title) { 85 | return ( 86 | 91 | {innerButton} 92 | 93 | ); 94 | } 95 | 96 | return innerButton; 97 | })} 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/devtools/views/StoreInspector/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type Store from '../../store'; 11 | import type { LogEvent } from '../../../types'; 12 | 13 | export function deepCopyFunction( 14 | inObject: any 15 | ): any | Map | { ... } { 16 | if (typeof inObject !== 'object' || inObject === null) { 17 | return inObject; 18 | } 19 | 20 | if (Array.isArray(inObject)) { 21 | const outObject = []; 22 | for (let i = 0; i < inObject.length; i++) { 23 | const value = inObject[i]; 24 | outObject[i] = deepCopyFunction(value); 25 | } 26 | return outObject; 27 | } else if (inObject instanceof Map) { 28 | const outObject = new Map(); 29 | inObject.forEach((val: any, key: mixed) => { 30 | outObject.set(key, deepCopyFunction(val)); 31 | }); 32 | return outObject; 33 | } else { 34 | const outObject: $FlowFixMe = {}; 35 | for (const key in inObject) { 36 | const value = inObject[key]; 37 | if (typeof key === 'string' && key != null) { 38 | outObject[key] = deepCopyFunction(value); 39 | } 40 | } 41 | return outObject; 42 | } 43 | } 44 | 45 | export function serializeEventLoggerRecording( 46 | store: Store 47 | ): Array<[string, mixed]> { 48 | const allEvents = Array.from(store.getAllEventsMap().entries()); 49 | return (allEvents.map(entry => { 50 | const envID = entry[0]; 51 | const data = entry[1]; 52 | const envName = store.getEnvironmentName(envID) || ''; 53 | const environment = envID + ' ' + envName; 54 | return [environment, data]; 55 | }): Array<[string, mixed]>); 56 | } 57 | 58 | export function deserializeEventLoggerRecording( 59 | raw: string, 60 | store: Store 61 | ): Array { 62 | const parsedDataRecording = ((new Map(JSON.parse(raw)): any): Map< 63 | string, 64 | Array 65 | >); 66 | const envNames: $FlowFixMe = {}; 67 | const envIDs = (Array.from(parsedDataRecording.keys()).map(key => { 68 | const environment = String(key).split(' '); 69 | // Taking out the id from the environment string 70 | const id = parseInt(environment.shift()); 71 | // We are left with the environment name 72 | const name = environment.join(' '); 73 | envNames[id] = name; 74 | const events = parsedDataRecording.get(String(key)) || []; 75 | store.setAllEventsMap(id, events); 76 | return id; 77 | }): Array); 78 | store.setImportEnvID(envIDs[0]); 79 | return envIDs; 80 | } 81 | -------------------------------------------------------------------------------- /src/devtools/views/TabBar.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Tab, 11 | .TabCurrent, 12 | .TabDisabled { 13 | height: 100%; 14 | display: flex; 15 | align-items: center; 16 | cursor: pointer; 17 | border-top: 3px solid transparent; 18 | border-bottom: 3px solid transparent; 19 | user-select: none; 20 | color: var(--color-text); 21 | 22 | /* Electron drag area */ 23 | -webkit-app-region: no-drag; 24 | } 25 | .Tab:hover, 26 | .TabCurrent:hover { 27 | background-color: var(--color-background-hover); 28 | } 29 | .Tab:focus-within, 30 | .TabCurrent:focus-within { 31 | background-color: var(--color-background-hover); 32 | } 33 | 34 | .TabCurrent { 35 | border-bottom: 3px solid var(--color-tab-selected-border); 36 | } 37 | 38 | .TabDisabled { 39 | color: var(--color-dim); 40 | cursor: default; 41 | } 42 | 43 | .TabSizeLarge { 44 | font-size: var(--font-size-sans-large); 45 | padding: 0.5rem 1rem; 46 | } 47 | .TabSizeSmall { 48 | font-size: var(--font-size-sans-normal); 49 | padding: 0.25rem 0.5rem; 50 | } 51 | 52 | .Input { 53 | width: 0; 54 | margin: 0; 55 | opacity: 0; 56 | } 57 | 58 | .IconSizeLarge, 59 | .IconSizeSmall { 60 | margin-right: 0.5rem; 61 | color: var(--color-button-active); 62 | } 63 | 64 | .IconDisabled { 65 | color: var(--color-dim); 66 | } 67 | 68 | .IconSizeLarge { 69 | width: 1.5rem; 70 | height: 1.5rem; 71 | } 72 | 73 | .IconSizeSmall { 74 | width: 1rem; 75 | height: 1rem; 76 | } 77 | 78 | .TabLabelLarge, 79 | .TabLabelSmall { 80 | } 81 | 82 | @media screen and (max-width: 900px) { 83 | .TabLabelSmall { 84 | display: none; 85 | } 86 | 87 | .IconSizeSmall { 88 | margin-right: 0; 89 | } 90 | } 91 | 92 | @media screen and (max-width: 525px) { 93 | .IconSizeLarge { 94 | margin-right: 0; 95 | } 96 | 97 | .TabLabelLarge { 98 | display: none; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/devtools/views/TabBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import classNames from 'classnames'; 11 | import React, { Fragment, useCallback } from 'react'; 12 | import Tooltip from '@reach/tooltip'; 13 | import Icon from './Icon'; 14 | 15 | import styles from './TabBar.css'; 16 | import tooltipStyles from './Tooltip.css'; 17 | 18 | import type { IconType } from './Icon'; 19 | 20 | type TabInfo = {| 21 | icon: IconType, 22 | id: string, 23 | label: string, 24 | title?: string, 25 | |}; 26 | 27 | export type Props = {| 28 | currentTab: any, 29 | disabled?: boolean, 30 | id: string, 31 | selectTab: (tabID: any) => void, 32 | size: 'large' | 'small', 33 | tabs: Array, 34 | |}; 35 | 36 | export default function TabBar({ 37 | currentTab, 38 | disabled = false, 39 | id: groupName, 40 | selectTab, 41 | size, 42 | tabs, 43 | }: Props): React$MixedElement { 44 | if (!tabs.some(tab => tab.id === currentTab)) { 45 | selectTab(tabs[0].id); 46 | } 47 | 48 | const onChange = useCallback( 49 | ({ currentTarget }: any) => selectTab(currentTarget.value), 50 | [selectTab] 51 | ); 52 | 53 | const handleKeyDown = useCallback((event: any) => { 54 | switch (event.key) { 55 | case 'ArrowDown': 56 | case 'ArrowLeft': 57 | case 'ArrowRight': 58 | case 'ArrowUp': 59 | event.stopPropagation(); 60 | break; 61 | default: 62 | break; 63 | } 64 | }, []); 65 | 66 | const tabClassName = 67 | size === 'large' ? styles.TabSizeLarge : styles.TabSizeSmall; 68 | 69 | return ( 70 | 71 | {tabs.map(({ icon, id, label, title }) => { 72 | const innerButton = ( 73 | 106 | ); 107 | 108 | if (title) { 109 | return ( 110 | 111 | {innerButton} 112 | 113 | ); 114 | } 115 | 116 | return innerButton; 117 | })} 118 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/devtools/views/Toggle.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .ToggleDisabled, 11 | .ToggleOn, 12 | .ToggleOff { 13 | background: var(--color-button-background); 14 | border: none; 15 | border-radius: 0.25rem; 16 | padding: 0; 17 | flex: 0 0 auto; 18 | } 19 | 20 | .ToggleContent { 21 | display: inline-flex; 22 | align-items: center; 23 | border-radius: 0.25rem; 24 | padding: 0.25rem; 25 | } 26 | 27 | .ToggleOff { 28 | border: none; 29 | background: var(--color-button-background); 30 | color: var(--color-button); 31 | } 32 | .ToggleOff:hover { 33 | color: var(--color-button-hover); 34 | } 35 | 36 | .ToggleOn, 37 | .ToggleOn:active { 38 | color: var(--color-button-active); 39 | outline: none; 40 | } 41 | 42 | .ToggleOn:focus, 43 | .ToggleOff:focus, 44 | .ToggleContent:focus { 45 | outline: none; 46 | } 47 | 48 | .ToggleOn:focus > .ToggleContent, 49 | .ToggleOff:focus > .ToggleContent { 50 | background: var(--color-button-background-focus); 51 | } 52 | 53 | .ToggleDisabled { 54 | background: var(--color-button-background); 55 | color: var(--color-button-disabled); 56 | cursor: default; 57 | } 58 | 59 | .Input { 60 | width: 0; 61 | margin: 0; 62 | opacity: 0; 63 | } 64 | -------------------------------------------------------------------------------- /src/devtools/views/Toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React, { useCallback } from 'react'; 11 | import Tooltip from '@reach/tooltip'; 12 | 13 | import styles from './Toggle.css'; 14 | import tooltipStyles from './Tooltip.css'; 15 | 16 | type Props = { 17 | children: React$Node, 18 | className?: string, 19 | isChecked: boolean, 20 | isDisabled?: boolean, 21 | onChange: (isChecked: boolean) => void, 22 | title?: string, 23 | }; 24 | 25 | export default function Toggle({ 26 | children, 27 | className = '', 28 | isDisabled = false, 29 | isChecked, 30 | onChange, 31 | title, 32 | }: Props): React$MixedElement { 33 | const defaultClassName = isDisabled 34 | ? styles.ToggleDisabled 35 | : isChecked 36 | ? styles.ToggleOn 37 | : styles.ToggleOff; 38 | 39 | const handleClick = useCallback(() => onChange(!isChecked), [ 40 | isChecked, 41 | onChange, 42 | ]); 43 | 44 | const innerToggle = ( 45 | 54 | ); 55 | 56 | if (title) { 57 | return ( 58 | 59 | {innerToggle} 60 | 61 | ); 62 | } 63 | 64 | return innerToggle; 65 | } 66 | -------------------------------------------------------------------------------- /src/devtools/views/Tooltip.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | .Tooltip { 11 | border: none; 12 | border-radius: 0.25rem; 13 | padding: 0.25rem 0.5rem; 14 | font-size: 12px; 15 | background-color: var(--color-tooltip-background); 16 | color: var(--color-tooltip-text); 17 | 18 | /* Make sure this is above the DevTools, which are above the Overlay */ 19 | z-index: 10000002; 20 | } 21 | -------------------------------------------------------------------------------- /src/devtools/views/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { createContext } from 'react'; 11 | 12 | import type { FrontendBridge } from 'src/bridge'; 13 | import type Store from '../store'; 14 | 15 | export const BridgeContext: $FlowFixMe = createContext( 16 | ((null: any): FrontendBridge) 17 | ); 18 | BridgeContext.displayName = 'BridgeContext'; 19 | 20 | export const StoreContext: $FlowFixMe = createContext( 21 | ((null: any): Store) 22 | ); 23 | StoreContext.displayName = 'StoreContext'; 24 | -------------------------------------------------------------------------------- /src/devtools/views/portaledContent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | import { createPortal } from 'react-dom'; 12 | import ErrorBoundary from './ErrorBoundary'; 13 | 14 | export type Props = { 15 | portalContainer?: Element, 16 | }; 17 | 18 | export default function portaledContent(Component: any): any { 19 | return function PortaledContent({ portalContainer, ...rest }: Props) { 20 | const children = ( 21 | 22 | 23 | 24 | ); 25 | 26 | return portalContainer != null 27 | ? createPortal(children, portalContainer) 28 | : children; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/devtools/views/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | export function alphaSortEntries( 11 | entryA: [string, mixed], 12 | entryB: [string, mixed] 13 | ): number { 14 | const a = entryA[0]; 15 | const b = entryB[0]; 16 | if ('' + +a === a) { 17 | if ('' + +b !== b) { 18 | return -1; 19 | } 20 | return +a < +b ? -1 : 1; 21 | } 22 | return a < b ? -1 : 1; 23 | } 24 | 25 | export function serializeDataForCopy(props: Object): string { 26 | try { 27 | return JSON.stringify(props, null, 2); 28 | } catch (error) { 29 | return ''; 30 | } 31 | } 32 | 33 | export function truncateText(text: string, maxLength: number): string { 34 | const { length } = text; 35 | if (length > maxLength) { 36 | return ( 37 | text.substr(0, Math.floor(maxLength / 2)) + 38 | '…' + 39 | text.substr(length - Math.ceil(maxLength / 2) - 1) 40 | ); 41 | } else { 42 | return text; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type { 11 | RelayEnvironment as $IMPORTED_TYPE$_RelayEnvironment, 12 | EnvironmentWrapper, 13 | EnvironmentID, 14 | } from 'src/backend/types'; 15 | import type { RelayEnvironment, Handler } from 'src/backend/types'; 16 | /** 17 | * Install the hook on window, which is an event emitter. 18 | * Note because Chrome content scripts cannot directly modify the window object, 19 | * we are evaling this function by inserting a script tag. 20 | * That's why we have to inline the whole event emitter implementation here. 21 | */ 22 | 23 | import type { DevToolsHook } from 'src/backend/types'; 24 | 25 | declare var window: any; 26 | 27 | export function installHook(target: any): DevToolsHook | null { 28 | if (target.hasOwnProperty('__RELAY_DEVTOOLS_HOOK__')) { 29 | return null; 30 | } 31 | const listeners: { [string]: Array } = {}; 32 | const environments = new Map< 33 | EnvironmentID, 34 | $IMPORTED_TYPE$_RelayEnvironment 35 | >(); 36 | 37 | let uidCounter = 0; 38 | 39 | function registerEnvironment(environment: RelayEnvironment) { 40 | const id = ++uidCounter; 41 | environments.set(id, environment); 42 | 43 | hook.emit('environment', { id, environment }); 44 | 45 | return id; 46 | } 47 | 48 | function sub(event: string, fn: Handler) { 49 | hook.on(event, fn); 50 | return () => hook.off(event, fn); 51 | } 52 | 53 | function on(event: string, fn: Handler) { 54 | if (!listeners[event]) { 55 | listeners[event] = []; 56 | } 57 | listeners[event].push(fn); 58 | } 59 | 60 | function off(event: string, fn: Handler) { 61 | if (!listeners[event]) { 62 | return; 63 | } 64 | const index = listeners[event].indexOf(fn); 65 | if (index !== -1) { 66 | listeners[event].splice(index, 1); 67 | } 68 | if (!listeners[event].length) { 69 | delete listeners[event]; 70 | } 71 | } 72 | 73 | function emit(event: string, data: any) { 74 | if (listeners[event]) { 75 | listeners[event].map(fn => fn(data)); 76 | } 77 | } 78 | 79 | const environmentWrappers = new Map(); 80 | 81 | const hook: DevToolsHook = { 82 | registerEnvironment, 83 | environmentWrappers, 84 | // listeners, 85 | environments, 86 | 87 | emit, 88 | // inject, 89 | on, 90 | off, 91 | sub, 92 | }; 93 | 94 | Object.defineProperty( 95 | target, 96 | '__RELAY_DEVTOOLS_HOOK__', 97 | ({ 98 | // This property needs to be configurable for the test environment, 99 | // else we won't be able to delete and recreate it beween tests. 100 | configurable: __DEV__, 101 | enumerable: false, 102 | get() { 103 | return hook; 104 | }, 105 | }: Object) 106 | ); 107 | 108 | return hook; 109 | } 110 | -------------------------------------------------------------------------------- /src/registerDevToolsEventLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow strict-local 8 | */ 9 | 10 | import type { LogEvent } from './Logger'; 11 | 12 | import { registerEventLogger } from './Logger'; 13 | 14 | let loggingIFrame = null; 15 | let missedEvents = []; 16 | 17 | export default function registerDevToolsEventLogger( 18 | surface: string, 19 | version: string 20 | ) { 21 | if (__ENABLE_LOGGER__) { 22 | function logEvent(event: LogEvent) { 23 | if (loggingIFrame != null) { 24 | loggingIFrame.contentWindow.postMessage( 25 | { 26 | source: 'relay-devtools-logging', 27 | event: { 28 | surface, 29 | version, 30 | ...event, 31 | }, 32 | }, 33 | '*' 34 | ); 35 | } else { 36 | missedEvents.push(event); 37 | } 38 | } 39 | 40 | function handleLoggingIFrameLoaded(iframe: HTMLIFrameElement) { 41 | if (loggingIFrame != null) { 42 | return; 43 | } 44 | loggingIFrame = iframe; 45 | if (missedEvents.length > 0) { 46 | missedEvents.forEach(logEvent); 47 | missedEvents = []; 48 | } 49 | } 50 | 51 | // If logger is enabled, register a logger that captures logged events 52 | // and render iframe where the logged events will be reported to 53 | const loggingUrl = process.env.LOGGING_URL; 54 | const body = document.body; 55 | if ( 56 | typeof loggingUrl === 'string' && 57 | loggingUrl.length > 0 && 58 | body != null 59 | ) { 60 | registerEventLogger(logEvent); 61 | 62 | const iframe = document.createElement('iframe'); 63 | iframe.src = loggingUrl; 64 | iframe.onload = function(...args) { 65 | handleLoggingIFrameLoaded(iframe); 66 | }; 67 | body.appendChild(iframe); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | export function localStorageGetItem(key: string): any { 11 | try { 12 | return localStorage.getItem(key); 13 | } catch (error) { 14 | return null; 15 | } 16 | } 17 | 18 | export function localStorageSetItem(key: string, value: any): void { 19 | try { 20 | return localStorage.setItem(key, value); 21 | } catch (error) {} 22 | } 23 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | * @format 9 | */ 10 | 11 | export type WallEvent = {| 12 | event: string, 13 | payload: any, 14 | |}; 15 | export type Wall = {| 16 | // `listen` returns the "unlisten" function. 17 | listen: (fn: Function) => Function, 18 | sendAll: (Array) => void, 19 | |}; 20 | 21 | export type Record = { [key: string]: mixed, ... }; 22 | export type DataID = string; 23 | export type UpdatedRecords = { [dataID: DataID]: boolean, ... }; 24 | 25 | export type StoreRecords = { [DataID]: ?Record, ... }; 26 | 27 | // Copied from relay 28 | // TODO: keep this up to date with relay 29 | export type LogEvent = 30 | | {| 31 | +name: 'queryresource.fetch', 32 | +operation: $FlowFixMe, 33 | // FetchPolicy from relay-experimental 34 | +fetchPolicy: string, 35 | // RenderPolicy from relay-experimental 36 | +renderPolicy: string, 37 | +hasFullQuery: boolean, 38 | +shouldFetch: boolean, 39 | |} 40 | | {| 41 | +name: 'store.publish', 42 | +source: any, 43 | +optimistic: boolean, 44 | |} 45 | | {| 46 | +name: 'store.gc', // TODO: Support the new GC event name 47 | references: Array, 48 | gcRecords: StoreRecords, 49 | |} 50 | | {| 51 | +name: 'store.restore', 52 | |} 53 | | {| 54 | +name: 'store.snapshot', 55 | |} 56 | | {| 57 | +name: 'store.notify.start', 58 | |} 59 | | {| 60 | +name: 'store.notify.complete', 61 | +updatedRecordIDs: UpdatedRecords, 62 | +invalidatedRecordIDs: Array, 63 | updatedRecords: StoreRecords, 64 | invalidatedRecords: StoreRecords, 65 | |} 66 | | {| 67 | +name: 'network.info', 68 | +transactionID?: ?number, 69 | +networkRequestId?: ?number, 70 | +info: mixed, 71 | params: $FlowFixMe, 72 | variables: $FlowFixMe, 73 | |} 74 | | {| 75 | +name: 'network.start', 76 | +transactionID?: ?number, 77 | +networkRequestId?: ?number, 78 | +info: mixed, 79 | +params: $FlowFixMe, 80 | +variables: $FlowFixMe, 81 | |} 82 | | {| 83 | +name: 'network.next', 84 | +transactionID?: ?number, 85 | +networkRequestId?: ?number, 86 | +response: $FlowFixMe, 87 | params: $FlowFixMe, 88 | variables: $FlowFixMe, 89 | |} 90 | | {| 91 | +name: 'network.error', 92 | +transactionID?: ?number, 93 | +networkRequestId?: ?number, 94 | +error: Error, 95 | params: $FlowFixMe, 96 | variables: $FlowFixMe, 97 | |} 98 | | {| 99 | +name: 'network.complete', 100 | +transactionID?: ?number, 101 | +networkRequestId?: ?number, 102 | params: $FlowFixMe, 103 | variables: $FlowFixMe, 104 | |} 105 | | {| 106 | +name: 'network.unsubscribe', 107 | +transactionID?: ?number, 108 | +networkRequestId?: ?number, 109 | params: $FlowFixMe, 110 | variables: $FlowFixMe, 111 | |}; 112 | 113 | export type EventData = {| 114 | +id: number, 115 | +data: LogEvent, 116 | +source: StoreRecords, 117 | +eventType: string, 118 | |}; 119 | 120 | export type StoreData = {| 121 | +name: string, 122 | +id: number, 123 | +records: StoreRecords, 124 | |}; 125 | 126 | export type EnvironmentInfo = {| 127 | +id: number, 128 | +environmentName: string, 129 | |}; 130 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | * @format 9 | */ 10 | 11 | // Pulled from react-compat 12 | // https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349 13 | export function shallowDiffers(prev: Object, next: Object): boolean { 14 | for (const attribute in prev) { 15 | if (!(attribute in next)) { 16 | return true; 17 | } 18 | } 19 | for (const attribute in next) { 20 | if (prev[attribute] !== next[attribute]) { 21 | return true; 22 | } 23 | } 24 | return false; 25 | } 26 | 27 | export function getEventId(event: { 28 | +transactionID?: ?number, 29 | +networkRequestId?: ?number, 30 | ... 31 | }): number { 32 | const id = event.transactionID ?? event.networkRequestId; 33 | if (id == null) { 34 | throw new Error( 35 | 'Expected a transactionID or networkRequestId for the event.' 36 | ); 37 | } 38 | return id; 39 | } 40 | --------------------------------------------------------------------------------