Repository: oslabs-beta/protostar-relay Branch: master Commit: ec442a7c2a9f Files: 66 Total size: 142.3 KB Directory structure: gitextract_ezpa51h4/ ├── .eslintignore ├── .flowconfig ├── .gitignore ├── .gitmodules ├── .travis.yml ├── .yarnrc ├── Dockerfile ├── LICENSE ├── README.md ├── __tests__/ │ ├── DevTools.spec.js │ ├── Global.spec.js │ ├── NetworkDisplayer.spec.js │ ├── Record.spec.js │ ├── StoreDisplayer.spec.js │ ├── StoreTimeline.spec.js │ ├── __mocks__/ │ │ └── styleMock.js │ ├── __snapshots__/ │ │ ├── DevTools.spec.js.snap │ │ ├── NetworkDisplayer.spec.js.snap │ │ ├── StoreDisplayer.spec.js.snap │ │ └── StoreTimeline.spec.js.snap │ ├── bridge.spec.js │ ├── global-setup.js │ └── store.spec.js ├── babel.config.js ├── docker-compose.yml ├── flow.js ├── package.json ├── shells/ │ ├── browser/ │ │ ├── chrome/ │ │ │ ├── build.js │ │ │ ├── manifest.json │ │ │ ├── nottest.js │ │ │ ├── now.json │ │ │ └── watch.js │ │ └── shared/ │ │ ├── build.js │ │ ├── index.html │ │ ├── main.html │ │ ├── src/ │ │ │ ├── backend.js │ │ │ ├── background.js │ │ │ ├── contentScript.js │ │ │ ├── inject.js │ │ │ ├── injectGlobalHook.js │ │ │ ├── main.js │ │ │ └── utils.js │ │ ├── view/ │ │ │ ├── App.jsx │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── webpack.backend.js │ │ └── webpack.config.js │ └── utils.js └── src/ ├── backend/ │ ├── EnvironmentWrapper.js │ ├── agent.js │ ├── index.js │ ├── types.js │ └── utils.js ├── bridge.js ├── devtools/ │ ├── DevTools.js │ ├── context.js │ ├── store.js │ ├── utils.js │ └── view/ │ ├── Components/ │ │ ├── EnvironmentSelector.js │ │ ├── Record.js │ │ └── SnapshotLinks.js │ ├── NetworkDisplayer.js │ ├── StoreDisplayer.js │ └── StoreTimeline.js ├── hook.js └── types.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules shells/browser/chrome/build shells/browser/firefox/build shells/browser/shared/build shells/dev/dist vendor *.js.snap package-lock.json yarn.lock ================================================ FILE: .flowconfig ================================================ [ignore] shells/browser/chrome/build/* shells/browser/firefox/build/* shells/dev/build/* [declarations] /node_modules/graphql [include] [libs] /flow-typed/ ./flow.js [lints] [options] esproposal.class_instance_fields=enable suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe suppress_comment=\\(.\\|\n\\)*\\$FlowIssue suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore module.name_mapper='^src' ->'/src' esproposal.optional_chaining=enable [strict] [version] ^0.113.0 ================================================ FILE: .gitignore ================================================ /shells/browser/chrome/*.crx /shells/browser/chrome/*.pem /shells/browser/firefox/*.xpi /shells/browser/firefox/*.pem /shells/browser/shared/build /packages/relay-devtools-core/dist /shells/dev/dist build node_modules npm-debug.log yarn-error.log .DS_Store yarn-error.log .vscode .idea *.pem dist package-lock.json ================================================ FILE: .gitmodules ================================================ [submodule "relay-examples"] path = relay-examples url = https://github.com/relayjs/relay-examples.git ================================================ FILE: .travis.yml ================================================ services: - docker dist: xenial script: - docker-compose up --abort-on-container-exit ================================================ FILE: .yarnrc ================================================ yarn-offline-mirror false ================================================ FILE: Dockerfile ================================================ FROM node:12.18.3 WORKDIR /usr/src/app COPY . /usr/src/app RUN npm install CMD npm run test ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Facebook, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Proto Relay

Proto Relay is a Chrome extension devtool for React Relay based off the official devtool. It is designed to be light-weight, performant, and easy-to-use. ## Features - [x] Preview Relay store content from the Chrome devtools panel - [x] View store content over time with included snapshots - [x] View store mutations and network queries ## Installation 1. Fork and clone this repository onto your local computer 2. Install dependencies and run a build using either the 'Yarn' or 'NPM' commands below: ```node # Yarn yarn install yarn build # NPM npm run install npm run build ``` 3. Access the Chome extensions within the browser 4. Access [Chrome extensions](chrome://extensions/) within the browser 5. Click on "Load Unpacked" 6. Navigate and select the folder: protostar-relay > Shells > browser > chrome > build > unpacked 7. Go to a website built with Relay and open the "proto*" panel. Websites that use Relay include: - [facebook.com](https://www.facebook.com/) - [artsy.com](https://www.artsy.net/) - [oculus.com](https://www.oculus.com/) ## How to Use - Example view of interacting with the Relay store. - Example of snapshot functionality and viewing mutations. ## Contributing Protostar-relay is currently in beta release. We encourage you to submit issues for any bugs or ideas for enhancements. Also feel free to fork this repo and submit pull requests to contribute as well. Below are some features we would like to add as we iterate on this project: - Optimistic updates: - Visual representation. - List of all optimistic updates with pending/resolved status. - Control data flow. ## Google Chrome Web Store Get it on the Chrome Extension Store: [coming soon](). ## Contributors [Aryeh Kobrinsky](https://github.com/akobrinsky), [Liz Lotto](https://github.com/elizlotto), [Marc Burnie](https://github.com/marcburnie), [Qwen Ballard](https://github.com/qwenballard) ## License This project is licensed under the MIT License- see the [LICENSE.md](https://github.com/oslabs-beta/protostar-relay/blob/master/LICENSE) for more details. * Inspired by [Facebook's Relay Devtool](https://github.com/relayjs/relay-devtools) ================================================ FILE: __tests__/DevTools.spec.js ================================================ import React from 'react'; import { configure, shallow, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import renderer from 'react-test-renderer'; import DevTools from '../src/devtools/DevTools'; configure({ adapter: new Adapter() }); describe('DevTools', () => { let wrapper; const RealDate = Date; const names = { 1: 'first', 2: 'second', 3: 'third' }; const props = { store: { getEnvironmentIDs: () => [1, 2, 3], getEnvironmentName: id => names[id] }, bridge: 'hi' }; beforeAll(() => { wrapper = shallow(); }); it('Passes the bridge to provider', () => { expect(wrapper.prop('value')).toEqual(props.bridge); }); it('Passes the store to provider', () => { expect( wrapper .children() .first() .prop('value') ).toEqual(props.store); }); it('Has a dropdown select element for environmentID with an onChange method', () => { expect(wrapper.find('select').length).toEqual(1); expect(wrapper.find('select').prop('onChange')).not.toEqual(undefined); }); it('Lists environments in dropdown selector', () => { const option = wrapper.find('option'); expect(option.length).toEqual(3); expect(option.at(0).text()).toEqual(names[1]); expect(option.at(1).text()).toEqual(names[2]); expect(option.at(2).text()).toEqual(names[3]); expect(option.at(0).prop('value')).toEqual(1); expect(option.at(1).prop('value')).toEqual(2); expect(option.at(2).prop('value')).toEqual(3); }); it('Has a StoreTimeline component', () => { expect(wrapper.find('StoreTimeline').length).toEqual(1); }); it('Has a NetworkDisplayer component', () => { expect(wrapper.find('NetworkDisplayer').length).toEqual(1); }); it('Passes the current environment to a currentEnvID prop on StoreTimeline and defaults to the first ID', () => { expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(1); }); it('Can select between different environments and pass the current environment to a currentEnvID prop on StoreTimeline', () => { wrapper.find('select').simulate('change', { target: { value: 2 } }); expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(2); wrapper.find('select').simulate('change', { target: { value: 3 } }); expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(3); wrapper.find('select').simulate('change', { target: { value: 1 } }); expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(1); }); it('Has network hidden by default and store is visible', () => { expect( wrapper .find('StoreTimeline') .parent() .hasClass('is-hidden') ).toEqual(false); expect( wrapper .find('NetworkDisplayer') .parent() .hasClass('is-hidden') ).toEqual(true); }); it('Allows user to select between store and network view', () => { const networkSelector = wrapper.find('#networkSelector'); expect(networkSelector.length).toEqual(1); expect(networkSelector.prop('onClick')).not.toEqual(undefined); networkSelector.simulate('click'); expect( wrapper .find('StoreTimeline') .parent() .hasClass('is-hidden') ).toEqual(true); expect( wrapper .find('NetworkDisplayer') .parent() .hasClass('is-hidden') ).toEqual(false); const storeSelector = wrapper.find('#storeSelector'); expect(storeSelector.length).toEqual(1); expect(storeSelector.prop('onClick')).not.toEqual(undefined); storeSelector.simulate('click'); expect( wrapper .find('StoreTimeline') .parent() .hasClass('is-hidden') ).toEqual(false); expect( wrapper .find('NetworkDisplayer') .parent() .hasClass('is-hidden') ).toEqual(true); }); it('Renders correctly', () => { const date = new Date(Date.UTC(2020)); global.Date = jest.fn(() => date); const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); jest.clearAllMocks(); }); }); ================================================ FILE: __tests__/Global.spec.js ================================================ describe('Timezones', () => { it('should always be UTC', () => { expect(new Date().getTimezoneOffset()).toBe(0); }); }); ================================================ FILE: __tests__/NetworkDisplayer.spec.js ================================================ import React from 'react'; import { configure, shallow, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import renderer from 'react-test-renderer'; import NetworkDisplayer from '../src/devtools/view/NetworkDisplayer'; configure({ adapter: new Adapter() }); describe('NetworkDisplayer', () => { let wrapper; const props = {}; beforeAll(() => { wrapper = shallow(); }); it('My Test Case', () => { expect(true).toEqual(true); }); it('My Test Case', () => { expect(wrapper.find('Record').length).toEqual(0); }); it('Renders correctly', () => { const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); }); ================================================ FILE: __tests__/Record.spec.js ================================================ import React from 'react'; import { configure, shallow, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; // import toJson from 'enzyme-to-json'; import renderer from 'react-test-renderer'; import Record from '../src/devtools/view/Components/Record'; configure({ adapter: new Adapter() }); describe('Record', () => { let wrapper; let children; //alternative to .next, not always required. const props = { //hardcode in what to pass into component hi: true, nested: { this: 'that' } }; beforeAll(() => { wrapper = shallow(); children = wrapper.children(); }); it('Renders a
tag with a className of "records"', () => { //we are using methods from enzyme library so look at the docs expect(wrapper.type()).toEqual('div'); expect(wrapper.hasClass('records')).toEqual(true); }); it('Has two children divs: one with className objectProperty and the other with className nestedObject', () => { expect(children.length).toEqual(2); expect(children.first().hasClass('objectProperty')).toEqual(true); expect(children.last().hasClass('nestedObject')).toEqual(true); }); it('Has a object property child with two spans, first has class of key and second has class of value. And has text values for the key and stringifies boolean values.', () => { const rootChildren = children.first().children(); expect(rootChildren.length).toEqual(2); expect(rootChildren.first().hasClass('key')).toEqual(true); expect(rootChildren.first().text()).toEqual('hi: '); expect(rootChildren.last().hasClass('value')).toEqual(true); expect(rootChildren.last().text()).toEqual('true'); }); it('has a nested object child that has a span with class of key and a div with class of records. It also has a first child that has text of "nested: "', () => { const rootChildren = children.last().children(); expect(rootChildren.first().text()).toEqual('nested: '); expect(rootChildren.length).toEqual(2); expect(rootChildren.first().hasClass('key')).toEqual(true); expect(rootChildren.last().find(Record).length).toEqual(1); }); }); ================================================ FILE: __tests__/StoreDisplayer.spec.js ================================================ import React from 'react'; import { configure, shallow, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import renderer from 'react-test-renderer'; import StoreDisplayer from '../src/devtools/view/StoreDisplayer'; configure({ adapter: new Adapter() }); describe('StoreDisplayer', () => { let wrapper; let useEffect; const store = { '1': { '__id': '1', '__typename': 'User', 'name': 'Marc' }, '2': { '__id': '2', '__typename': 'User', 'name': 'Aryeh' }, '3': { '__id': '3', '__typename': 'User', 'name': 'Liz' }, '4': { '__id': '4', '__typename': 'User', 'name': 'Qwen' }, '5': { '__id': '5', '__typename': 'Post', 'text': 'Hi' } } const mockUseEffect = () => { useEffect.mockImplementationOnce(f => f()); }; beforeEach(() => { useEffect = jest.spyOn(React, "useEffect"); mockUseEffect(); wrapper = shallow(); }); it("Has a Record component with the filtered records passed as props", () => { expect(wrapper.find('Record').length).toEqual(1); expect(wrapper.find('Record').props()).toEqual(store); }) it("Has a menu", () => { expect(wrapper.find('.menu').length).toEqual(1); }) describe("Menu", () => { let menu; beforeEach(() => { menu = wrapper.find('.menu'); }) it("Generates a list of menu items from the store object", () => { expect(menu.find("#type-User").length).toEqual(1); expect(menu.find("#type-Post").length).toEqual(1); Object.keys(store).forEach(k => { expect(menu.find(`#id-${k}`).length).toEqual(1); }) }) it("Has menu items with an onClick event that filters the results displayed on the screen based on ID", () => { Object.keys(store).forEach(k => { menu.find(`#id-${k}`).simulate('click'); expect(wrapper.find("Record").props()).toEqual({ [k]: store[k] }) }) }) it("Has menu items with an onClick event that filters the results displayed on the screen based on type", () => { menu.find(`#type-User`).simulate('click'); expect(wrapper.find("Record").props()).toEqual({ '1': { '__id': '1', '__typename': 'User', 'name': 'Marc' }, '2': { '__id': '2', '__typename': 'User', 'name': 'Aryeh' }, '3': { '__id': '3', '__typename': 'User', 'name': 'Liz' }, '4': { '__id': '4', '__typename': 'User', 'name': 'Qwen' } }) }) it("Adds an 'is-active' class to the currently selected menu item", () => { Object.keys(store).forEach(k => { let menuItem = wrapper.find(`#id-${k}`) expect(menuItem.length).toEqual(1) menuItem.props().onClick(); menuItem = wrapper.find(`#id-${k}`) expect(menuItem.hasClass('is-active')).toEqual(true) expect(menuItem.hasClass('is-active')).toEqual(true) menu.find("a").forEach(el => { if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false) }) }) let menuItem = wrapper.find(`#type-User`) expect(menuItem.length).toEqual(1) menuItem.simulate('click'); menuItem = wrapper.find(`#type-User`) expect(menuItem.hasClass('is-active')).toEqual(true) menu.find("a").forEach(el => { if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false) }) menuItem = wrapper.find(`#type-Post`) expect(menuItem.length).toEqual(1) menuItem.simulate('click'); menuItem = wrapper.find(`#type-Post`) expect(menuItem.hasClass('is-active')).toEqual(true) menu.find("a").forEach(el => { if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false) }) }) it("Removes the 'is-active' class when the reset button is clicked", () => { let menuItem = menu.find(`#type-User`) expect(menuItem.length).toEqual(1) menuItem.simulate('click'); expect(wrapper.find('.menu').find(".is-active").length).toEqual(1); wrapper.find('button').simulate('click'); expect(wrapper.find('.menu').find(".is-active").length).toEqual(0); }) it('Has a search input with an onChange property', () => { expect(wrapper.find('input').length).toEqual(1); expect(wrapper.find('input').prop('onChange')).not.toBe(undefined); }); describe('Search Box', () => { let search; beforeEach(() => { search = wrapper.find('input'); }) it("Filters the menu items", () => { search.prop('onChange')({ target: { value: 'Marc' } }); jest.runAllTimers(); expect(wrapper.find(`#id-1`).length).toEqual(1); Object.keys(store).forEach(k => { if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(0); }) }) it("Debounces the input", () => { search.prop('onChange')({ target: { value: 'Marc' } }); Object.keys(store).forEach(k => { if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(1); }) jest.runAllTimers(); Object.keys(store).forEach(k => { if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(0); }) }) }); it('Has a Reset Button with an onClick property', () => { expect(wrapper.find('button').length).toEqual(1); expect(wrapper.find('button').prop('onClick')).not.toBe(undefined); }); describe('Reset Button', () => { it("Has a reset button that removes any selectors", () => { menu.find(`#type-User`).simulate('click'); expect(wrapper.find('Record').props()).not.toEqual(store) wrapper.find('button').simulate('click'); expect(wrapper.find('Record').props()).toEqual(store) }) }); }) it('Renders correctly', () => { const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }) }); ================================================ FILE: __tests__/StoreTimeline.spec.js ================================================ import React from 'react'; import { configure, shallow, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import renderer from 'react-test-renderer'; import StoreTimeline from '../src/devtools/view/StoreTimeline'; configure({ adapter: new Adapter() }); describe('StoreTimeline', () => { let wrapper; const props = { currentEnvID: 1 }; beforeEach(() => { wrapper = shallow(); }); it('Renders a StoreDisplayer component and passes store as a prop', () => { expect(wrapper.find('StoreDisplayer').length).toEqual(1); }); // it("Passes the store based on the currentEnvID", () => { // }) // describe("Snapshots", () => { // it("Takes a snapshot at startup", () => { // }) // it("Has a snapshot button that takes and saves a snapshot", () => { // }) // it("Defaults to displaying the latest store value when a snapshot is taken", () => { // }) // it("Remembers snapshots when switching between environments", () => { // }) // it("Has a snapshot text input", () => { // }) // it("Has a previous buttons to move to the previous snapshot", () => { // }) // it("Has a next button to move to the previous snapshot", () => { // }) // it("Has a current button that shows the current store value", () => { // }) // it("Has a slider that updates when a new snapshot is taken and when switching between environments", () => { // }) // }) it('Renders correctly', () => { const date = new Date(Date.UTC(2020)); global.Date = jest.fn(() => date); const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); jest.clearAllMocks(); }); }); ================================================ FILE: __tests__/__mocks__/styleMock.js ================================================ module.exports = {}; ================================================ FILE: __tests__/__snapshots__/DevTools.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DevTools Renders correctly 1`] = ` Array [
,

,

, ] `; ================================================ FILE: __tests__/__snapshots__/NetworkDisplayer.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NetworkDisplayer Renders correctly 1`] = ` Array [

,
, ] `; ================================================ FILE: __tests__/__snapshots__/StoreDisplayer.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`StoreDisplayer Renders correctly 1`] = ` Array [ ,
, ] `; ================================================ FILE: __tests__/__snapshots__/StoreTimeline.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`StoreTimeline Renders correctly 1`] = ` Array [
,

,
, ] `; ================================================ FILE: __tests__/bridge.spec.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ describe('Bridge', () => { let Bridge; beforeEach(() => { Bridge = require('../src/bridge').default; }); it('should shutdown properly', () => { const wall = { listen: jest.fn(() => () => {}), send: jest.fn() }; const bridge = new Bridge(wall); // Check that we're wired up correctly. bridge.send('init'); jest.runAllTimers(); expect(wall.send).toHaveBeenCalledWith('init', undefined, undefined); // Should flush pending messages and then shut down. wall.send.mockClear(); bridge.send('update', '1'); bridge.send('update', '2'); bridge.shutdown(); jest.runAllTimers(); expect(wall.send).toHaveBeenCalledWith('update', '1', undefined); expect(wall.send).toHaveBeenCalledWith('update', '2', undefined); expect(wall.send).toHaveBeenCalledWith('shutdown', undefined, undefined); // Verify that the Bridge doesn't send messages after shutdown. spyOn(console, 'warn'); wall.send.mockClear(); bridge.send('should not send'); jest.runAllTimers(); expect(wall.send).not.toHaveBeenCalled(); expect(console.warn).toHaveBeenCalledWith( 'Cannot send message "should not send" through a Bridge that has been shutdown.' ); }); }); ================================================ FILE: __tests__/global-setup.js ================================================ module.exports = async () => { process.env.TZ = 'UTC'; }; ================================================ FILE: __tests__/store.spec.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ describe('Store', () => { let Store; let Bridge; beforeEach(() => { Bridge = require('../src/bridge').default; Store = require('../src/devtools/store').default; }); it('should delete individual records correctly', () => { const wall = { listen: jest.fn(() => () => {}), send: jest.fn() }; const bridge = new Bridge(wall); const store = new Store(bridge); store.mergeRecords(1, { Bob: { __id: 'Bob', __typename: 'User', profile_pic: 'a_different_url' }, Lisa: { __id: 'Lisa', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); expect(store.getRecords(1)).toEqual({ Bob: { __id: 'Bob', __typename: 'User', profile_pic: 'a_different_url' }, Lisa: { __id: 'Lisa', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); store.removeRecord(1, 'Lisa'); store.removeRecord(1, 'Bob'); expect(store.getRecords(1)).toEqual({ user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); }); it('should merge records correctly', () => { const wall = { listen: jest.fn(() => () => {}), send: jest.fn() }; const bridge = new Bridge(wall); const store = new Store(bridge); // Testing case when oldRecords is null and we just set the map to the newRecords store.mergeRecords(1, { user: { __id: 'user', __typename: 'User' } }); expect(store.getRecords(1)).toEqual({ user: { __id: 'user', __typename: 'User' } }); // Testing case when newRecords is null/undefined and we don't change anything store.mergeRecords(1, null); expect(store.getRecords(1)).toEqual({ user: { __id: 'user', __typename: 'User' } }); store.mergeRecords(1, undefined); expect(store.getRecords(1)).toEqual({ user: { __id: 'user', __typename: 'User' } }); // Testing multiple environments store.mergeRecords(2, { user: { __id: 'user', __typename: 'User' } }); expect(store.getRecords(1)).toEqual({ user: { __id: 'user', __typename: 'User' } }); expect(store.getRecords(2)).toEqual({ user: { __id: 'user', __typename: 'User' } }); // Testing multiple records store.mergeRecords(1, { Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'some_url' } }); expect(store.getRecords(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'some_url' }, user: { __id: 'user', __typename: 'User' } }); //Testing overwriting a record store.mergeRecords(1, { Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' } }); expect(store.getRecords(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User' } }); store.mergeRecords(1, { Bob: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Lisa: { __id: 'Lisa', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); expect(store.getRecords(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Bob: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Lisa: { __id: 'Lisa', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); expect(store.getRecords(2)).toEqual({ user: { __id: 'user', __typename: 'User' } }); store.mergeRecords(1, { Jonathan: { __id: 'Jonathan', __typename: 'User', nickname: 'Zuck' }, Bob: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Lisa: { __id: 'Lisa', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); expect(store.getRecords(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url', nickname: 'Zuck' }, Bob: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Lisa: { __id: 'Lisa', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); expect(store.getRecords(2)).toEqual({ user: { __id: 'user', __typename: 'User' } }); // Deleting records store.mergeRecords(1, { Bob: null, Lisa: null, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); expect(store.getRecords(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url', nickname: 'Zuck' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); }); it('should merge optimistic updates correctly', () => { const wall = { listen: jest.fn(() => () => {}), send: jest.fn() }; const bridge = new Bridge(wall); const store = new Store(bridge); // Testing with a real optimistic source // Testing case when oldRecords is null and we just set the map to the newRecords store.mergeOptimisticRecords(1, { 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': { 'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0, 'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0, 'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null, __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==', __typename: 'Bookmark' } }); expect(store.getOptimisticUpdates(1)).toEqual({ 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': { 'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0, 'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0, 'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null, __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==', __typename: 'Bookmark' } }); // Testing case when newRecords is null/undefined and we don't change anything store.mergeOptimisticRecords(1, null); jest.runAllTimers(); expect(store.getOptimisticUpdates(1)).toEqual({ 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': { 'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0, 'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0, 'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null, __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==', __typename: 'Bookmark' } }); store.mergeOptimisticRecords(1, undefined); jest.runAllTimers(); expect(store.getOptimisticUpdates(1)).toEqual({ 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': { 'unread_count(bookmark_render_location:"COMET_LEFT_NAV")': 0, 'unread_count(bookmark_render_location:"COMET_TOP_TAB")': 0, 'unread_count_string(bookmark_render_location:"COMET_LEFT_NAV")': null, __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==', __typename: 'Bookmark' } }); // Removing all optimistic updates // Simulating the store.restore event store.clearOptimisticUpdates(1); jest.runAllTimers(); expect(store.getRecords(1)).toEqual(undefined); // Testing multiple records store.mergeOptimisticRecords(1, { Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'some_url' }, Lilly: { __id: 'Lilly', __typename: 'User', profile_pic: 'url' } }); jest.runAllTimers(); expect(store.getOptimisticUpdates(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'some_url' }, Lilly: { __id: 'Lilly', __typename: 'User', profile_pic: 'url' } }); //Testing overwriting a record store.mergeOptimisticRecords(1, { Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' } }); jest.runAllTimers(); expect(store.getOptimisticUpdates(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Lilly: { __id: 'Lilly', __typename: 'User', profile_pic: 'url' } }); store.mergeOptimisticRecords(1, { Jonathan: { __id: 'Jonathan', __typename: 'User', nickname: 'Zuck' }, Bob: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); jest.runAllTimers(); expect(store.getOptimisticUpdates(1)).toEqual({ Jonathan: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url', nickname: 'Zuck' }, Bob: { __id: 'Jonathan', __typename: 'User', profile_pic: 'a_different_url' }, Lilly: { __id: 'Lilly', __typename: 'User', profile_pic: 'url' }, user: { __id: 'user', __typename: 'User', profile_pic: 'new_url' } }); }); }); ================================================ FILE: babel.config.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const chromeManifest = require('./shells/browser/chrome/manifest.json'); const minChromeVersion = parseInt(chromeManifest.minimum_chrome_version, 10); validateVersion(minChromeVersion); function validateVersion(version) { if (version > 0 && version < 200) { return; } throw new Error('Suspicious browser version in manifest: ' + version); } module.exports = api => { const isTest = api.env('test'); const targets = {}; if (isTest) { targets.node = 'current'; } else { targets.chrome = minChromeVersion.toString(); // This targets RN/Hermes. targets.ie = '11'; } const plugins = [ ['relay'], ['@babel/plugin-proposal-optional-chaining'], ['@babel/plugin-transform-flow-strip-types'], ['@babel/plugin-proposal-class-properties', { loose: false }], ]; if (process.env.NODE_ENV !== 'production') { plugins.push(['@babel/plugin-transform-react-jsx-source']); } return { plugins, presets: [ ['@babel/preset-env', { targets }], '@babel/preset-react', '@babel/preset-flow', ], }; }; ================================================ FILE: docker-compose.yml ================================================ version: '3.0' services: test: image: 'protorelay/protostar' container_name: 'protostar-test' volumes: - .:/usr/src/app - node_modules:/usr/src/app/node_modules command: npm run test volumes: node_modules: {} ================================================ FILE: flow.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ declare module 'events' { declare class EventEmitter { addListener>( event: Event, listener: (...$ElementType) => any ): void; emit: >( event: Event, ...$ElementType ) => void; removeListener(event: $Keys, listener: Function): void; removeAllListeners(event?: $Keys): void; } declare export default typeof EventEmitter; } declare var __DEV__: boolean; declare var jasmine: {| getEnv: () => {| afterEach: (callback: Function) => void, beforeEach: (callback: Function) => void, |}, |}; ================================================ FILE: package.json ================================================ { "version": "1.0.0", "name": "protostar-relay", "repository": "oslabs-beta/protostar-relay", "license": "MIT", "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "cross-env NODE_ENV=production node ./shells/browser/chrome/build", "test": "cross-env TZ=\"UTC\" jest", "install-app": "npm i --prefix ./relay-examples/todo/", "start-test-app": "npm start --prefix ./relay-examples/todo/", "watch:chrome:frontend": "cross-env NODE_ENV=development node ./shells/browser/chrome/watch", "docker-test": "docker-compose up" }, "jest": { "verbose": true, "testRegex": "((\\.|/*.)(spec))\\.js?$", "moduleNameMapper": { "\\.(css|less)$": "/__tests__/__mocks__/styleMock.js" }, "timers": "fake", "globalSetup": "/__tests__/global-setup.js" }, "devEngines": { "node": "10.x || 11.x" }, "lint-staged": { "{shells,src}/**/*.{js,json,css}": [ "prettier --write", "git add" ], "**/*.js": "eslint --max-warnings 0" }, "devDependencies": { "@babel/core": "^7.7.5", "@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/plugin-proposal-optional-chaining": "^7.7.5", "@babel/plugin-transform-flow-strip-types": "^7.7.4", "@babel/plugin-transform-react-jsx-source": "^7.7.4", "@babel/preset-env": "^7.7.6", "@babel/preset-flow": "^7.7.4", "@babel/preset-react": "^7.7.4", "@reach/menu-button": "^0.5.4", "@reach/tooltip": "^0.5.4", "archiver": "^3.0.0", "babel-core": "^7.0.0-bridge", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "babel-loader": "^8.0.6", "babel-plugin-relay": "master", "chance": "^1.0.18", "child-process-promise": "^2.2.1", "chrome-launch": "^1.1.4", "classnames": "2.2.1", "clipboard-js": "^0.3.6", "cross-env": "^6.0.3", "crx": "^5.0.0", "css-loader": "^1.0.1", "es6-symbol": "3.0.2", "eslint": "^6.6.0", "eslint-config-prettier": "^6.5.0", "eslint-config-react-app": "^5.0.2", "eslint-plugin-flowtype": "^4.3.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-react": "^7.16.0", "eslint-plugin-react-hooks": "^2.2.0", "events": "^3.0.0", "flow-bin": "^0.113.0", "fs-extra": "^3.0.1", "graphql": "^14.4.2", "jest": "^24.9.0", "lint-staged": "^7.0.5", "local-storage-fallback": "^4.1.1", "lodash.throttle": "^4.1.1", "log-update": "^2.0.0", "lru-cache": "^4.1.3", "nullthrows": "^1.0.0", "object-assign": "4.0.1", "opener": "^1.5.1", "prettier": "^1.19.1", "prop-types": "^15.7.2", "react": "^0.0.0-50b50c26f", "react-dom": "^0.0.0-50b50c26f", "react-relay": "master", "react-test-renderer": "0.0.0-fec00a869", "react-virtualized-auto-sizer": "^1.0.2", "relay-compiler": "master", "relay-config": "master", "rimraf": "^2.6.3", "sass-loader": "^10.0.2", "scheduler": "^0.0.0-50b50c26f", "style-loader": "^0.23.1", "web-ext": "^3.0.0", "webpack": "^4.41.3", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.10.0", "yargs": "^14.2.0" }, "dependencies": { "bulma": "^0.9.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.4", "node-sass": "^4.14.1", "react-input-range": "^1.3.0", "sass": "^1.26.10" } } ================================================ FILE: shells/browser/chrome/build.js ================================================ #!/usr/bin/env node /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const chalk = require('chalk'); const { execSync } = require('child_process'); const { existsSync } = require('fs'); const { isAbsolute, join, relative } = require('path'); const { argv } = require('yargs'); const build = require('../shared/build'); const main = async () => { const { crx, keyPath } = argv; if (crx) { if (!keyPath || !existsSync(keyPath)) { console.error('Must specify a key file (.pem) to build CRX'); process.exit(1); } } await build('chrome'); if (crx) { const cwd = join(__dirname, 'build'); let safeKeyPath = keyPath; if (!isAbsolute(keyPath)) { safeKeyPath = join(relative(cwd, process.cwd()), keyPath); } execSync(`crx pack ./unpacked -o RelayDevTools.crx -p ${safeKeyPath}`, { cwd, }); } console.log(chalk.green('\nThe Chrome extension has been built!')); console.log(chalk.green('You can test this build by running:')); console.log(chalk.gray('\n# From the relay-devtools root directory:')); console.log('yarn run test:chrome'); }; main(); ================================================ FILE: shells/browser/chrome/manifest.json ================================================ { "manifest_version": 2, "name": "Proto Relay", "description": "Adds Relay debugging tools to the Chrome DevTool panel", "version": "1.0.0", "version_name": "1.0.0", "update_url": "https://github.com/oslabs-beta/protostar-relay", "minimum_chrome_version": "78", "icons": { "16": "assets/proto16.png", "32": "assets/proto32.png", "48": "assets/proto48.png", "128": "assets/proto128.png" }, "browser_action": { "default_icon": { "16": "assets/proto16.png", "32": "assets/proto32.png", "48": "assets/proto48.png", "128": "assets/proto128.png" }, "default_popup": "popups/disabled.html" }, "devtools_page": "main.html", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "web_accessible_resources": [ "main.html", "build/backend.js", "build/renderer.js", "assets/protorelay.png" ], "background": { "scripts": [ "build/background.js" ], "persistent": false }, "permissions": [ "file:///*", "http://*/*", "https://*/*", "webNavigation" ], "content_scripts": [ { "matches": [ "" ], "js": [ "build/injectGlobalHook.js" ], "run_at": "document_start" } ] } ================================================ FILE: shells/browser/chrome/nottest.js ================================================ #!/usr/bin/env node /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const chromeLaunch = require('chrome-launch'); // eslint-disable-line import/no-extraneous-dependencies const { resolve } = require('path'); const EXTENSION_PATH = resolve('shells/browser/chrome/build/unpacked'); const START_URL = 'https://facebook.github.io/react/'; chromeLaunch(START_URL, { args: [`--load-extension=${EXTENSION_PATH}`], }); ================================================ FILE: shells/browser/chrome/now.json ================================================ { "name": "relay-devtools-experimental-chrome", "alias": ["relay-devtools-experimental-chrome"], "files": ["index.html", "RelayDevTools.zip"] } ================================================ FILE: shells/browser/chrome/watch.js ================================================ #!/usr/bin/env node /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const { execSync } = require('child_process'); const { join } = require('path'); const webpackPath = join( __dirname, '..', '..', '..', 'node_modules', '.bin', 'webpack' ); const binPath = join(__dirname, 'build', 'unpacked', 'build'); execSync( `${webpackPath} --config ../shared/webpack.config.js --output-path ${binPath} --watch`, { cwd: join(__dirname, '..', 'shared'), env: process.env, stdio: 'inherit', } ); ================================================ FILE: shells/browser/shared/build.js ================================================ #!/usr/bin/env node /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const archiver = require('archiver'); const { execSync } = require('child_process'); const { readFileSync, writeFileSync, createWriteStream } = require('fs'); const { copy, ensureDir, move, remove } = require('fs-extra'); const { join } = require('path'); const { getCommit } = require('../../utils'); // These files are copied along with Webpack-bundled files // to produce the final web extension const STATIC_FILES = ['assets', 'main.html', 'index.html']; const preProcess = async (destinationPath, tempPath) => { await remove(destinationPath); // Clean up from previously completed builds await remove(tempPath); // Clean up from any previously failed builds await ensureDir(tempPath); // Create temp dir for this new build }; const build = async (tempPath, manifestPath) => { const binPath = join(tempPath, 'bin'); const zipPath = join(tempPath, 'zip'); const webpackPath = join(__dirname, '..', '..', '..', 'node_modules', '.bin', 'webpack'); execSync(`${webpackPath} --config webpack.config.js --output-path ${binPath}`, { cwd: __dirname, env: process.env, stdio: 'inherit' }); execSync(`${webpackPath} --config webpack.backend.js --output-path ${binPath}`, { cwd: __dirname, env: process.env, stdio: 'inherit' }); // Make temp dir await ensureDir(zipPath); const copiedManifestPath = join(zipPath, 'manifest.json'); // Copy unbuilt source files to zip dir to be packaged: await copy(binPath, join(zipPath, 'build')); await copy(manifestPath, copiedManifestPath); await Promise.all(STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file)))); const commit = getCommit(); const versionDateString = `${commit} (${new Date().toLocaleDateString()})`; const manifest = JSON.parse(readFileSync(copiedManifestPath).toString()); if (manifest.version_name) { manifest.version_name = versionDateString; } else { manifest.description += `\n\nCreated from revision ${versionDateString}`; } writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2)); // Pack the extension const archive = archiver('zip', { zlib: { level: 9 } }); const zipStream = createWriteStream(join(tempPath, 'RelayDevTools.zip')); await new Promise((resolve, reject) => { archive .directory(zipPath, false) .on('error', err => reject(err)) .pipe(zipStream); archive.finalize(); zipStream.on('close', () => resolve()); }); }; const postProcess = async (tempPath, destinationPath) => { const unpackedSourcePath = join(tempPath, 'zip'); const packedSourcePath = join(tempPath, 'RelayDevTools.zip'); const packedDestPath = join(destinationPath, 'RelayDevTools.zip'); const unpackedDestPath = join(destinationPath, 'unpacked'); await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination await move(packedSourcePath, packedDestPath); // Copy built files to destination await remove(tempPath); // Clean up temp directory and files }; const main = async buildId => { const root = join(__dirname, '..', buildId); const manifestPath = join(root, 'manifest.json'); const destinationPath = join(root, 'build'); try { const tempPath = join(__dirname, 'build', buildId); await preProcess(destinationPath, tempPath); await build(tempPath, manifestPath); const builtUnpackedPath = join(destinationPath, 'unpacked'); await postProcess(tempPath, destinationPath); return builtUnpackedPath; } catch (error) { console.error(error); process.exit(1); } return null; }; module.exports = main; ================================================ FILE: shells/browser/shared/index.html ================================================ Document
Unable to find Relay on the page.
================================================ FILE: shells/browser/shared/main.html ================================================ ================================================ FILE: shells/browser/shared/src/backend.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ // Do not use imports or top-level requires here! // Running module factories is intentionally delayed until we know the hook exists. // This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039 function welcome(event) { if (event.source !== window || event.data.source !== 'relay-devtools-content-script') { return; } window.removeEventListener('message', welcome); setup(window.__RELAY_DEVTOOLS_HOOK__); } window.addEventListener('message', welcome); function setup(hook) { const Agent = require('src/backend/agent').default; const Bridge = require('src/bridge').default; const { initBackend } = require('src/backend'); const bridge = new Bridge({ listen(fn) { const listener = event => { if ( event.source !== window || !event.data || event.data.source !== 'relay-devtools-content-script' || !event.data.payload ) { return; } fn(event.data.payload); }; window.addEventListener('message', listener); return () => { window.removeEventListener('message', listener); }; }, send(event: string, payload: any, transferable?: Array) { window.postMessage( { source: 'relay-devtools-bridge', payload: { event, payload: JSON.parse(JSON.stringify(payload)) } }, '*', transferable ); } }); const agent = new Agent(bridge); agent.addListener('shutdown', () => { // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down, // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here. hook.emit('shutdown'); }); initBackend(hook, agent, window); } ================================================ FILE: shells/browser/shared/src/background.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /* global chrome */ const ports = {}; chrome.runtime.onConnect.addListener(function(port) { let tab = null; let name = null; if (isNumeric(port.name)) { tab = port.name; name = 'devtools'; installContentScript(+port.name); } else { tab = port.sender.tab.id; name = 'content-script'; } if (!ports[tab]) { ports[tab] = { devtools: null, 'content-script': null }; } ports[tab][name] = port; if (ports[tab].devtools && ports[tab]['content-script']) { doublePipe(ports[tab].devtools, ports[tab]['content-script']); } }); function isNumeric(str: string): boolean { return +str + '' === str; } function installContentScript(tabId: number) { chrome.tabs.executeScript(tabId, { file: '/build/contentScript.js' }, function() {}); } function doublePipe(one, two) { one.onMessage.addListener(lOne); function lOne(message) { two.postMessage(message); } two.onMessage.addListener(lTwo); function lTwo(message) { one.postMessage(message); } function shutdown() { one.onMessage.removeListener(lOne); two.onMessage.removeListener(lTwo); one.disconnect(); two.disconnect(); } one.onDisconnect.addListener(shutdown); two.onDisconnect.addListener(shutdown); } ================================================ FILE: shells/browser/shared/src/contentScript.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /* global chrome */ let backendDisconnected: boolean = false; let backendInitialized: boolean = false; function sayHelloToBackend() { window.postMessage( { source: 'relay-devtools-content-script', hello: true }, '*' ); } function handleMessageFromDevtools(message) { window.postMessage( { source: 'relay-devtools-content-script', payload: message }, '*' ); } function handleMessageFromPage(evt) { if (evt.source === window && evt.data && evt.data.source === 'relay-devtools-bridge') { backendInitialized = true; port.postMessage(evt.data.payload); } } function handleDisconnect() { backendDisconnected = true; window.removeEventListener('message', handleMessageFromPage); window.postMessage( { source: 'relay-devtools-content-script', payload: { type: 'event', event: 'shutdown' } }, '*' ); } // proxy from main page to devtools (via the background page) var port = chrome.runtime.connect({ name: 'content-script' }); port.onMessage.addListener(handleMessageFromDevtools); port.onDisconnect.addListener(handleDisconnect); window.addEventListener('message', handleMessageFromPage); sayHelloToBackend(); // The backend waits to install the global hook until notified by the content script. // In the event of a page reload, the content script might be loaded before the backend is injected. // Because of this we need to poll the backend until it has been initialized. if (!backendInitialized) { const intervalID = setInterval(() => { if (backendInitialized || backendDisconnected) { clearInterval(intervalID); } else { sayHelloToBackend(); } }, 500); } ================================================ FILE: shells/browser/shared/src/inject.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* global chrome */ export default function inject(scriptName: string, done: ?Function) { const source = ` // the prototype stuff is in case document.createElement has been modified (function () { var script = document.constructor.prototype.createElement.call(document, 'script'); script.src = "${scriptName}"; script.charset = "utf-8"; document.documentElement.appendChild(script); script.parentNode.removeChild(script); })() `; chrome.devtools.inspectedWindow.eval(source, function(response, error) { if (error) { console.log(error); } if (typeof done === 'function') { done(); } }); } ================================================ FILE: shells/browser/shared/src/injectGlobalHook.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /* global chrome */ import nullthrows from 'nullthrows'; import { installHook } from 'src/hook'; function injectCode(code) { const script = document.createElement('script'); script.textContent = code; // This script runs before the element is created, // so we add the script to instead. nullthrows(document.documentElement).appendChild(script); nullthrows(script.parentNode).removeChild(script); } let lastDetectionResult; // We want to detect when a renderer attaches, and notify the "background page" // (which is shared between tabs and can highlight the React icon). // Currently we are in "content script" context, so we can't listen to the hook directly // (it will be injected directly into the page). // So instead, the hook will use postMessage() to pass message to us here. // And when this happens, we'll send a message to the "background page". window.addEventListener('message', function(evt) { if (evt.source === window && evt.data && evt.data.source === 'relay-devtools-detector') { lastDetectionResult = { hasDetectedReact: true }; chrome.runtime.sendMessage(lastDetectionResult); } }); // NOTE: Firefox WebExtensions content scripts are still alive and not re-injected // while navigating the history to a document that has not been destroyed yet, // replay the last detection result if the content script is active and the // document has been hidden and shown again. window.addEventListener('pageshow', function(evt) { if (!lastDetectionResult || evt.target !== window.document) { return; } chrome.runtime.sendMessage(lastDetectionResult); }); const detectRelay = ` window.__RELAY_DEVTOOLS_HOOK__.on('environment', function(evt) { window.postMessage({ source: 'relay-devtools-detector', }, '*'); }); `; // Inject a `__RELAY_DEVTOOLS_HOOK__` global so that Relay can detect that the // devtools are installed (and skip its suggestion to install the devtools). injectCode(';(' + installHook.toString() + '(window))' + detectRelay); ================================================ FILE: shells/browser/shared/src/main.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* global chrome */ import { createElement } from 'react'; import { unstable_createRoot as createRoot, flushSync } from 'react-dom'; import Bridge from 'src/bridge'; import Store from 'src/devtools/store'; import inject from './inject'; import { createViewElementSource } from './utils'; import DevTools from 'src/devtools/DevTools'; let panelCreated = false; function createPanelIfReactLoaded() { if (panelCreated) { return; } chrome.devtools.inspectedWindow.eval( 'window.__RELAY_DEVTOOLS_HOOK__ && window.__RELAY_DEVTOOLS_HOOK__.environments.size > 0', (pageHasRelay, error) => { if (!pageHasRelay || panelCreated) { return; } panelCreated = true; clearInterval(loadCheckInterval); let bridge = null; let store = null; let cloneStyleTags = null; let render = null; let root = null; let currentPanel = null; const tabId = chrome.devtools.inspectedWindow.tabId; function initBridgeAndStore() { const port = chrome.runtime.connect({ name: '' + tabId }); // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation, // so it makes no sense to handle it here. bridge = new Bridge({ listen(fn) { const listener = message => fn(message); // Store the reference so that we unsubscribe from the same object. const portOnMessage = port.onMessage; portOnMessage.addListener(listener); return () => { portOnMessage.removeListener(listener); }; }, send(event: string, payload: any, transferable?: Array) { port.postMessage({ event, payload }, transferable); } }); store = new Store(bridge); // Initialize the backend only once the Store has been initialized. // Otherwise the Store may miss important initial tree op codes. inject(chrome.runtime.getURL('build/backend.js')); const viewElementSourceFunction = createViewElementSource(bridge, store); render = () => { console.log('Rendering...'); if (root) { root.render( createElement(DevTools, { bridge, // showTabBar: true, store, // viewElementSourceFunction, rootContainer: currentPanel.container }) ); } }; render(); } cloneStyleTags = () => { const linkTags = []; for (const linkTag of document.getElementsByTagName('link')) { if (linkTag.rel === 'stylesheet') { const newLinkTag = document.createElement('link'); for (const attribute of linkTag.attributes) { newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue); } linkTags.push(newLinkTag); } } return linkTags; }; initBridgeAndStore(); function ensureInitialHTMLIsCleared(container) { if (container._hasInitialHTMLBeenCleared) { return; } container.innerHTML = ''; container._hasInitialHTMLBeenCleared = true; } chrome.devtools.panels.create('proto*', '', 'index.html', panel => { panel.onShown.addListener(listenPanel => { if (currentPanel === listenPanel) { return; } currentPanel = listenPanel; if (listenPanel.container != null) { listenPanel.injectStyles(cloneStyleTags); ensureInitialHTMLIsCleared(listenPanel.container); root = createRoot(listenPanel.container); render(); } }); panel.onHidden.addListener(() => { // TODO: Stop highlighting and stuff. }); }); chrome.devtools.network.onNavigated.removeListener(checkPageForReact); // Shutdown bridge before a new page is loaded. chrome.webNavigation.onBeforeNavigate.addListener(function onBeforeNavigate(details) { // Ignore navigation events from other tabs (or from within frames). if (details.tabId !== tabId || details.frameId !== 0) { return; } // `bridge.shutdown()` will remove all listeners we added, so we don't have to. bridge.shutdown(); }); // Re-initialize DevTools panel when a new page is loaded. chrome.devtools.network.onNavigated.addListener(function onNavigated() { // It's easiest to recreate the DevTools panel (to clean up potential stale state). // We can revisit this in the future as a small optimization. flushSync(() => { root.unmount(() => { initBridgeAndStore(); }); }); }); } ); } // Load (or reload) the DevTools extension when the user navigates to a new page. function checkPageForReact() { createPanelIfReactLoaded(); } chrome.devtools.network.onNavigated.addListener(checkPageForReact); // Check to see if React has loaded once per second in case React is added // after page load const loadCheckInterval = setInterval(function() { createPanelIfReactLoaded(); }, 1000); createPanelIfReactLoaded(); ================================================ FILE: shells/browser/shared/src/utils.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* global chrome */ export function createViewElementSource(bridge: Bridge, store: Store) { return function viewElementSource(id) { const rendererID = store.getRendererIDForElement(id); if (rendererID != null) { // Ask the renderer interface to determine the component function, // and store it as a global variable on the window bridge.send('viewElementSource', { id, rendererID }); setTimeout(() => { // Ask Chrome to display the location of the component function, // assuming the renderer found one. chrome.devtools.inspectedWindow.eval(` if (window.$type != null) { inspect(window.$type); } `); }, 100); } }; } ================================================ FILE: shells/browser/shared/view/App.jsx ================================================ /** @format */ import React, { useEffect, useState } from 'react'; const port = chrome.runtime.connect({ name: 'test' }); const App = () => { const [tree, setTree] = useState(); // const [history, setHistory] = useState([]); // const [count, setCount] = useState(1); // function is receiving fibernode state changes from backend and is saving that data to tree hook useEffect(() => { port.postMessage({ name: 'connect', tabID: chrome.devtools.inspectedWindow.tabId }); port.onMessage.addListener(message => { if (message.length === 3) { setTree(message); } }); }, []); return
; }; export default App; ================================================ FILE: shells/browser/shared/view/index.js ================================================ /** @format */ import React from 'react'; import { render } from 'react-dom'; import App from './App.jsx'; import styles from './styles.scss'; /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ // Portal target container. window.container = document.getElementById('container'); let hasInjectedStyles = false; // DevTools styles are injected into the top-level document head (where the main React app is rendered). // This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled. window.injectStyles = getLinkTags => { if (!hasInjectedStyles) { hasInjectedStyles = true; const linkTags = getLinkTags(); for (const linkTag of linkTags) { document.head.appendChild(linkTag); } } }; render(, document.getElementById('root')); ================================================ FILE: shells/browser/shared/view/styles.scss ================================================ /** @format */ //*********************************** //********* VARIABLES *********** //*********************************** //*********************************** //********** GENERAL ************ //*********************************** .button { border-radius: 5px; border-color: gray; border-width: 3px; } .snapshot-nav button { margin-right: 12px; } #container { padding: 10px 0 0 10px; } .navigation { padding: 0; overflow: auto; } .navigation .tabs { margin: 0 0.75em; } button.button.is-small.is-link { margin-left: 10px; } .column { border-right: 1px solid #e4e0e0; height: 90vh; } #timeline-mini-col { border-right: none !important; } #snapshot-info-col { border-right: none !important; } .scrollable { overflow: scroll; } //*********************************** //********** STORE ************ //*********************************** .slider-textcolor { color: #060606; font-weight: bold; margin: 20px 0; } //***** MENU ***** .type { background: lightblue; } .menu-list { width: 100%; font-size: 10px; } .menu-list a { word-break: break-all; } .records { width: 100%; margin-left: 2em; } .records:first-child { margin-left: 0em; border-bottom: 1px grey solid; } //***** STORE DISPLAY ***** .display-box { border-radius: 5px; width: 100%; font-size: 10px; } .key { font-weight: bold; word-wrap: break-word; } .logo img { height: 30px; } .value { word-wrap: break-word; } .snapshots { padding: 0 10px; } .snapshot-nav { margin-top: 30px; } .input-range { margin: 35px 0px; } .tabs.is-toggle li.is-active a { background-color: #00d1b2; border-color: #00d1b2; color: #fff; z-index: 1; } .input-range__slider { appearance: none; background: #00d1b2; border: 1px solid #0bc3a8; border-radius: 100%; cursor: pointer; display: block; height: 1rem; margin-left: -0.5rem; margin-top: -0.65rem; outline: none; position: absolute; top: 50%; transition: transform 0.3s ease-out, box-shadow 0.3s ease-out; width: 1rem; } .input-range__slider:active { transform: scale(1.3); } .input-range__slider:focus { box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2); } .input-range--disabled .input-range__slider { background: #cccccc; border: 1px solid #cccccc; box-shadow: none; transform: none; } .input-range__slider-container { transition: left 0.3s ease-out; } .input-range__label { color: #aaaaaa; font-family: "Helvetica Neue", san-serif; font-size: 0.8rem; transform: translateZ(0); white-space: nowrap; } .input-range__label--min, .input-range__label--max { bottom: -1.4rem; position: absolute; } .input-range__label--min { left: 0; } .input-range__label--max { right: 0; } .input-range__label--value { position: absolute; top: -1.8rem; } .input-range__label-container { left: -50%; position: relative; } .input-range__label--max .input-range__label-container { left: 50%; } .input-range__track { background: #eeeeee; border-radius: 0.3rem; cursor: pointer; display: block; height: 0.3rem; position: relative; transition: left 0.3s ease-out, width 0.3s ease-out; } .input-range--disabled .input-range__track { background: #eeeeee; } .input-range__track--background { left: 0; margin-top: -0.15rem; position: absolute; right: 0; top: 50%; } .input-range__track--active { background: #3f51b5; } .input-range { height: 1rem; position: relative; width: 100%; } .type { background: lightblue; } .menu-list { width: 100%; } .record-line { border-bottom: 1px grey solid; } .records { width: 100%; margin-left: 2em; } .records:first-child { margin-left: 0em; } .snapshots .column { height: auto; } @media screen and (max-width: 768px) { .column { height: auto; } .column.is-half-mobile { padding-top: 0; } .snapshots { .column { align-items: center; height: auto; } } .snapshot-nav { margin-top: 0; width: 100%; } .input-range { width: 70%; margin: 25px 0; } } //768 - 1020 beside snapshots get rid of columns and multi-line// added //issue now between 712ish 988 // @media screen and (min-width: 769px) and (max-width: 1020px) { // .column { // height: auto; // } // .column.is-half-mobile { // padding-top: 0; // } // .snapshots { // .column { // align-items: center; // height: auto; // } // } // .snapshot-nav { // margin-top: 0; // width: 100%; // } // .input-range { // width: 70%; // margin: 25px 0; // } // } ================================================ FILE: shells/browser/shared/webpack.backend.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const { resolve } = require('path'); const { DefinePlugin } = require('webpack'); const { getGitHubIssuesURL, getGitHubURL, getInternalDevToolsFeedbackGroup, getVersionString } = require('../../utils'); const NODE_ENV = process.env.NODE_ENV; if (!NODE_ENV) { console.error('NODE_ENV not set'); process.exit(1); } const __DEV__ = NODE_ENV === 'development'; const GITHUB_URL = getGitHubURL(); const DEVTOOLS_VERSION = getVersionString(); const GITHUB_ISSUES_URL = getGitHubIssuesURL(); const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : false, entry: { backend: './src/backend.js' }, output: { path: __dirname + '/build', filename: '[name].js' }, resolve: { alias: { src: resolve(__dirname, '../../../src') } }, plugins: [ new DefinePlugin({ __DEV__: true, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"` }) ], module: { rules: [ { test: /\.js$/, loader: 'babel-loader', options: { configFile: resolve(__dirname, '../../../babel.config.js') } }, { test: /.(css|scss)$/, use: ['style-loader', 'css-loader', 'sass-loader'] } ] } }; ================================================ FILE: shells/browser/shared/webpack.config.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const { resolve } = require('path'); const { DefinePlugin } = require('webpack'); const { getGitHubIssuesURL, getGitHubURL, getInternalDevToolsFeedbackGroup, getVersionString } = require('../../utils'); const NODE_ENV = process.env.NODE_ENV; if (!NODE_ENV) { console.error('NODE_ENV not set'); process.exit(1); } const __DEV__ = NODE_ENV === 'development'; const GITHUB_URL = getGitHubURL(); const DEVTOOLS_VERSION = getVersionString(); const GITHUB_ISSUES_URL = getGitHubIssuesURL(); const DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup(); module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : false, entry: { background: './src/background.js', contentScript: './src/contentScript.js', injectGlobalHook: './src/injectGlobalHook.js', index: './view/index.js', main: './src/main.js' }, output: { path: __dirname + '/build', filename: '[name].js' }, resolve: { alias: { src: resolve(__dirname, '../../../src') } }, plugins: [ new DefinePlugin({ __DEV__: false, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.GITHUB_ISSUES_URL': `"${GITHUB_ISSUES_URL}"`, 'process.env.DEVTOOLS_FEEDBACK_GROUP': `"${DEVTOOLS_FEEDBACK_GROUP}"` }) ], module: { rules: [ { test: /.jsx?$/, exclude: /node_modules/, loader: 'babel-loader', options: { configFile: resolve(__dirname, '../../../babel.config.js') } }, { test: /.(css|scss)$/, use: ['style-loader', 'css-loader', 'sass-loader'] } ] } }; ================================================ FILE: shells/utils.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const { execSync } = require('child_process'); const { readFileSync, existsSync } = require('fs'); const { resolve } = require('path'); function getCommit() { if (existsSync(resolve(__dirname, '../.git'))) { return execSync('git show -s --format=%h') .toString() .trim(); } return execSync('hg id -i') .toString() .trim(); } function getGitHubURL() { return 'https://github.com/relayjs/relay-devtools'; } function getGitHubIssuesURL() { return 'https://github.com/relayjs/relay-devtools/issues/new'; } function getInternalDevToolsFeedbackGroup() { return 'https://fburl.com/ieftwi8l'; } function getVersionString() { const packageVersion = JSON.parse(readFileSync(resolve(__dirname, '../package.json'))).version; const commit = getCommit(); return `${packageVersion}-${commit}`; } module.exports = { getCommit, getGitHubIssuesURL, getGitHubURL, getInternalDevToolsFeedbackGroup, getVersionString }; ================================================ FILE: src/backend/EnvironmentWrapper.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type { DevToolsHook, RelayEnvironment, EnvironmentWrapper } from './types'; export function attach( hook: DevToolsHook, rendererID: number, environment: RelayEnvironment, global: Object ): EnvironmentWrapper { let pendingEventsQueue = []; const store = environment.getStore(); const originalLog = environment.__log; environment.__log = event => { originalLog(event); // TODO(damassart): Make this a modular function if (pendingEventsQueue !== null) { pendingEventsQueue.push(event); } else { hook.emit('environment.event', { id: rendererID, data: event, eventType: 'environment' }); } }; const storeOriginalLog = store.__log; // TODO(damassart): Make this cleaner store.__log = event => { if (storeOriginalLog !== null) { storeOriginalLog(event); } switch (event.name) { case 'store.gc': // references is a Set, but we can't serialize Sets, // so we convert references to an Array event.references = Array.from(event.references); hook.emit('environment.event', { id: rendererID, data: event, eventType: 'store' }); break; case 'store.notify.complete': event.invalidatedRecordIDs = Array.from(event.invalidatedRecordIDs); hook.emit('environment.event', { id: rendererID, data: event, eventType: 'store' }); break; default: hook.emit('environment.event', { id: rendererID, data: event, eventType: 'store' }); break; } }; function cleanup() { // We don't patch any methods so there is no cleanup. environment.__log = originalLog; store.__log = storeOriginalLog; } function sendStoreRecords() { const records = store.getSource().toJSON(); hook.emit('environment.store', { name: 'refresh.store', id: rendererID, records }); } function flushInitialOperations() { // TODO(damassart): Make this a modular function if (pendingEventsQueue != null) { pendingEventsQueue.forEach(pendingEvent => { hook.emit('environment.event', { id: rendererID, envName: environment.configName, data: pendingEvent, eventType: 'environment' }); }); pendingEventsQueue = null; } this.sendStoreRecords(); } return { cleanup, sendStoreRecords, flushInitialOperations }; } ================================================ FILE: src/backend/agent.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import EventEmitter from 'events'; import type { BackendBridge } from 'src/bridge'; import type { EnvironmentID, EnvironmentWrapper } from './types'; export default class Agent extends EventEmitter<{| shutdown: [], refreshStore: [] |}> { _bridge: BackendBridge; _recordChangeDescriptions: boolean = false; _environmentWrappers: { [key: EnvironmentID]: EnvironmentWrapper } = {}; constructor(bridge: BackendBridge) { super(); this._bridge = bridge; bridge.addListener('shutdown', this.shutdown); bridge.addListener('refreshStore', this.refreshStore); } get environmentWrappers(): { [key: EnvironmentID]: EnvironmentWrapper } { return this._environmentWrappers; } shutdown = () => { // Clean up the overlay if visible, and associated events. this.emit('shutdown'); }; refreshStore = (id: EnvironmentID) => { const wrapper = this._environmentWrappers[id]; wrapper && wrapper.sendStoreRecords(); }; onEnvironmentInitialized = (data: mixed) => { this._bridge.send('environmentInitialized', [data]); }; setEnvironmentWrapper = (id: number, environmentWrapper: EnvironmentWrapper) => { this._environmentWrappers[id] = environmentWrapper; }; onStoreData = (data: mixed) => { this._bridge.send('storeRecords', [data]); }; onEnvironmentEvent = (data: mixed) => { this._bridge.send('events', [data]); }; } ================================================ FILE: src/backend/index.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type { DevToolsHook, RelayEnvironment, EnvironmentWrapper } from './types'; import type Agent from './agent'; import { attach } from './EnvironmentWrapper'; export function initBackend(hook: DevToolsHook, agent: Agent, global: Object): () => void { const subs = [ hook.sub('environment.event', data => { agent.onEnvironmentEvent(data); }), hook.sub('environment.store', data => { agent.onStoreData(data); }), hook.sub( 'environment-attached', ({ id, environment, environmentWrapper }: { id: number, environment: RelayEnvironment, environmentWrapper: EnvironmentWrapper }) => { agent.setEnvironmentWrapper(id, environmentWrapper); agent.onEnvironmentInitialized({ id: id, environmentName: environment.configName }); // Now that the Store and the renderer interface are connected, // it's time to flush the pending operation codes to the frontend. environmentWrapper.flushInitialOperations(); } ) ]; const attachEnvironment = (id: number, environment: RelayEnvironment) => { let environmentWrapper = hook.environmentWrappers.get(id); // Inject any not-yet-injected renderers (if we didn't reload-and-profile) if (!environmentWrapper) { environmentWrapper = attach(hook, id, environment, global); hook.environmentWrappers.set(id, environmentWrapper); } // Notify the DevTools frontend about new renderers. hook.emit('environment-attached', { id, environment, environmentWrapper }); }; // Connect renderers that have already injected themselves. hook.environments.forEach((environment, id) => { attachEnvironment(id, environment); }); // Connect any new renderers that injected themselves. subs.push( hook.sub( 'environment', ({ id, environment }: { id: number, environment: RelayEnvironment }) => { attachEnvironment(id, environment); } ) ); return () => { subs.forEach(fn => fn()); }; } ================================================ FILE: src/backend/types.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ export type EnvironmentID = number; export type RelayRecordSource = { getRecordIDs: () => string, get: (id: string) => any, toJSON: () => any }; export type RelayStore = { getSource: () => RelayRecordSource, __log: (event: Object) => void }; export type RelayEnvironment = { execute: (options: any) => any, configName: ?string, getStore: () => RelayStore, __log: (event: Object) => void }; export type EnvironmentWrapper = { flushInitialOperations: () => void, sendStoreRecords: () => void, cleanup: () => void }; export type Handler = (data: any) => void; export type DevToolsHook = { registerEnvironment: (env: RelayEnvironment) => number | null, // listeners: { [key: string]: Array }, environmentWrappers: Map, environments: Map, emit: (event: string, data: any) => void, on: (event: string, handler: Handler) => void, off: (event: string, handler: Handler) => void, // reactDevtoolsAgent?: ?Object, sub: (event: string, handler: Handler) => () => void }; ================================================ FILE: src/backend/utils.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ export function copyWithSet( obj: Object | Array, path: Array, value: any, index: number = 0 ): Object | Array { console.log('[utils] copyWithSet()', obj, path, index, value); if (index >= path.length) { return value; } const key = parseInt(path[index]); const updated = Array.isArray(obj) ? obj.slice() : { ...obj }; updated[key] = copyWithSet(obj[key], path, value, index + 1); return updated; } ================================================ FILE: src/bridge.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import EventEmitter from 'events'; import type { EnvironmentInfo, EventData, StoreData, Wall } from './types'; const BATCH_DURATION = 100; type Message = {| event: string, payload: any |}; type BackendEvents = {| events: [Array], shutdown: [], environmentInitialized: [Array], storeRecords: [Array] |}; type FrontendEvents = {| refreshStore: [number] |}; class Bridge extends EventEmitter<{| ...IncomingEvents, ...OutgoingEvents |}> { _isShutdown: boolean = false; _messageQueue: Array = []; _timeoutID: TimeoutID | null = null; _wall: Wall; _wallUnlisten: Function | null = null; constructor(wall: Wall) { super(); this._wall = wall; this._wallUnlisten = wall.listen((message: Message) => { (this: any).emit(message.event, message.payload); }) || null; } send(event: string, payload: any, transferable?: Array) { if (this._isShutdown) { console.warn(`Cannot send message "${event}" through a Bridge that has been shutdown.`); return; } // When we receive a message: // - we add it to our queue of messages to be sent // - if there hasn't been a message recently, we set a timer for 0 ms in // the future, allowing all messages created in the same tick to be sent // together // - if there *has* been a message flushed in the last BATCH_DURATION ms // (or we're waiting for our setTimeout-0 to fire), then _timeoutID will // be set, and we'll simply add to the queue and wait for that this._messageQueue.push(event, payload, transferable); if (!this._timeoutID) { this._timeoutID = setTimeout(this._flush, 0); } } shutdown() { if (this._isShutdown) { console.warn('Bridge was already shutdown.'); return; } // Queue the shutdown outgoing message for subscribers. this.send('shutdown'); // Mark this bridge as destroyed, i.e. disable its public API. this._isShutdown = true; // Disable the API inherited from EventEmitter that can add more listeners and send more messages. (this: any).addListener = function() {}; this.emit = function() {}; // NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter. // Unsubscribe this bridge incoming message listeners to be sure, and so they don't have to do that. this.removeAllListeners(); // Stop accepting and emitting incoming messages from the wall. const wallUnlisten = this._wallUnlisten; if (wallUnlisten) { wallUnlisten(); } // Synchronously flush all queued outgoing messages. // At this step the subscribers' code may run in this call stack. do { this._flush(); } while (this._messageQueue.length); // Make sure once again that there is no dangling timer. clearTimeout(this._timeoutID); this._timeoutID = null; } _flush = () => { // This method is used after the bridge is marked as destroyed in shutdown sequence, // so we do not bail out if the bridge marked as destroyed. // It is a private method that the bridge ensures is only called at the right times. clearTimeout(this._timeoutID); this._timeoutID = null; if (this._messageQueue.length) { for (let i = 0; i < this._messageQueue.length; i += 3) { this._wall.send( this._messageQueue[i], this._messageQueue[i + 1], this._messageQueue[i + 2] ); } this._messageQueue.length = 0; // Check again for queued messages in BATCH_DURATION ms. This will keep // flushing in a loop as long as messages continue to be added. Once no // more are, the timer expires. this._timeoutID = setTimeout(this._flush, BATCH_DURATION); } }; } export type BackendBridge = Bridge; export type FrontendBridge = Bridge; export default Bridge; ================================================ FILE: src/devtools/DevTools.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ // Reach styles need to come before any component styles. // This makes overridding the styles simpler. import React, { useState, useCallback, useEffect } from 'react'; import type { FrontendBridge } from 'src/bridge'; import Store from './store'; import { BridgeContext, StoreContext } from './context'; import NetworkDisplayer from './view/NetworkDisplayer'; import StoreTimeline from './view/StoreTimeline'; // export type TabID = 'network' | 'settings' | 'store-inspector'; export type ViewElementSource = (id: number) => void; export type Props = {| bridge: FrontendBridge, // defaultTab?: TabID, // showTabBar?: boolean, store: Store, viewElementSourceFunction?: ?ViewElementSource, viewElementSourceRequiresFileLocation?: boolean, // This property is used only by the web extension target. // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs. // This is done to save space within the app. // Because of this, the extension needs to be able to change which tab is active/rendered. // overrideTab?: TabID, // TODO: Cleanup multi-tabs in webextensions // To avoid potential multi-root trickiness, the web extension uses portals to render tabs. // The root app is rendered in the top-level extension window, // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels. rootContainer?: Element, // networkPortalContainer?: Element, settingsPortalContainer?: Element, storeInspectorPortalContainer?: Element |}; const networkTab = { id: ('network': TabID), icon: 'network', label: 'Network', title: 'Relay Network' }; const storeInspectorTab = { id: ('store-inspector': TabID), icon: 'store-inspector', label: 'Store', title: 'Relay Store' }; const tabs = [networkTab, storeInspectorTab]; export default function DevTools({ bridge, rootContainer, networkPortalContainer, storeInspectorPortalContainer, settingsPortalContainer, store, viewElementSourceFunction, viewElementSourceRequiresFileLocation = false }: Props) { const [environmentIDs, setEnvironmentIDs] = useState(store.getEnvironmentIDs()); const [currentEnvID, setCurrentEnvID] = useState(environmentIDs[0]); const [selector, setSelector] = useState('Store'); const setEnv = useCallback(() => { const ids = store.getEnvironmentIDs(); if (currentEnvID === undefined) { const firstKey = ids[0]; setCurrentEnvID(firstKey); } setEnvironmentIDs(ids); }, [store, currentEnvID]); useEffect(() => { setEnv(); store.addListener('environmentInitialized', setEnv); return () => { store.removeListener('environmentInitialized', setEnv); }; }, [store, setEnv]); function handleTabClick(e, tab) { setSelector(tab); } const handleChange = useCallback(e => { setCurrentEnvID(parseInt(e.target.value)); }, []); console.log('currentenvid before render', currentEnvID); return (
{currentEnvID && ( )}
{currentEnvID && }
); } ================================================ FILE: src/devtools/context.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import { createContext } from 'react'; import type { FrontendBridge } from 'src/bridge'; import type Store from 'store'; export const BridgeContext = createContext(((null: any): FrontendBridge)); BridgeContext.displayName = 'BridgeContext'; export const StoreContext = createContext(((null: any): Store)); StoreContext.displayName = 'StoreContext'; ================================================ FILE: src/devtools/store.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ const __DEBUG__ = true; import EventEmitter from 'events'; import type { FrontendBridge } from 'src/bridge'; import type { DataID, LogEvent, EventData, EnvironmentInfo, StoreData, StoreRecords, Record } from '../types'; const debug = (methodName, ...args) => { if (__DEBUG__) { console.log( `%cStore %c${methodName}`, 'color: green; font-weight: bold;', 'font-weight: bold;', ...args ); } }; const storeEventNames = [ 'queryresource.fetch', 'store.publish', 'store.restore', 'store.gc', 'store.snapshot', 'store.notify.complete', 'store.notify.start' ]; /** * The store is the single source of truth for updates from the backend. * ContextProviders can subscribe to the Store for specific things they want to provide. */ export default class Store extends EventEmitter<{| collapseNodesByDefault: [], componentFilters: [], environmentInitialized: [], mutated: [], storeDataReceived: [], allEventsReceived: [], recordChangeDescriptions: [], roots: [] |}> { _bridge: FrontendBridge; _environmentEventsMap: Map> = new Map(); _environmentNames: Map = new Map(); _environmentStoreData: Map = new Map(); _environmentStoreOptimisticData: Map = new Map(); _environmentAllEvents: Map> = new Map(); _recordedRequests: Map> = new Map(); _isRecording: boolean = false; _importEnvID: ?number = null; constructor(bridge: FrontendBridge) { super(); this._bridge = bridge; bridge.addListener('events', this.onBridgeEvents); bridge.addListener('shutdown', this.onBridgeShutdown); bridge.addListener('environmentInitialized', this.onBridgeEnvironmentInit); bridge.addListener('storeRecords', this.onBridgeStoreSnapshot); bridge.addListener('mutationComplete', this.setEnvironmentEvents); bridge.addListener('all', this.setEnvironmentEvents); } getAllEventsArray(): $ReadOnlyArray { const allEvents = []; this._environmentAllEvents.forEach((value, _) => allEvents.push(...value)); return allEvents; } setAllEventsMap(environmentID: number, events: Array) { this._environmentAllEvents.set(environmentID, events); this.emit('allEventsReceived'); } getAllEventsMap(): Map> { return this._environmentAllEvents; } getEvents(environmentID: number): ?$ReadOnlyArray { return this._environmentAllEvents.get(environmentID); } getAllEnvironmentEvents(): $ReadOnlyArray { const allEnvironmentEvents = []; this._environmentEventsMap.forEach((value, _) => allEnvironmentEvents.push(...value)); return allEnvironmentEvents; } getEnvironmentEvents(environmentID: number): ?$ReadOnlyArray { return this._environmentEventsMap.get(environmentID); } getEnvironmentIDs(): $ReadOnlyArray { return Array.from(this._environmentNames.keys()); } getImportEnvID(): ?number { return this._importEnvID; } setImportEnvID(envID: ?number) { this._importEnvID = envID; this.emit('allEventsReceived'); } getEnvironmentName(environmentID: number): ?string { return this._environmentNames.get(environmentID); } getRecords(environmentID: number): ?StoreRecords { return this._environmentStoreData.get(environmentID); } getRecordIDs(environmentID: number): ?$ReadOnlyArray { const storeRecords = this._environmentStoreData.get(environmentID); return storeRecords ? Object.keys(storeRecords) : null; } removeRecord(environmentID: number, recordID: string) { const storeRecords = this._environmentStoreData.get(environmentID); if (storeRecords != null) { delete storeRecords[recordID]; } } getAllRecords(): ?$ReadOnlyArray { return Array.from(this._environmentStoreData.values()); } getOptimisticUpdates(environmentID: number): ?StoreRecords { return this._environmentStoreOptimisticData.get(environmentID); } mergeRecords(id: number, newRecords: ?StoreRecords) { if (newRecords == null) { return; } const oldRecords = this._environmentStoreData.get(id); if (oldRecords == null) { this._environmentStoreData.set(id, newRecords); return; } const dataIDs = Object.keys(newRecords); for (let ii = 0; ii < dataIDs.length; ii++) { const dataID = dataIDs[ii]; const oldRecord = oldRecords[dataID]; const newRecord = newRecords[dataID]; if (oldRecord && newRecord) { let updated: Record | null = null; const keys = Object.keys(newRecord); for (let iii = 0; iii < keys.length; iii++) { const key = keys[iii]; if (updated || oldRecord[key] !== newRecord[key]) { updated = updated !== null ? updated : { ...oldRecord }; updated[key] = newRecord[key]; } } updated = updated !== null ? updated : oldRecord; if (updated !== newRecord) { oldRecords[dataID] = updated; } } else if (oldRecord == null) { oldRecords[dataID] = newRecord; } else if (newRecord == null) { delete oldRecords[dataID]; } } this._environmentStoreData.set(id, oldRecords); } mergeOptimisticRecords(id: number, newRecords: ?StoreRecords) { if (newRecords == null) { return; } const oldRecords = this._environmentStoreOptimisticData.get(id); if (oldRecords == null) { this._environmentStoreOptimisticData.set(id, newRecords); return; } const dataIDs = Object.keys(newRecords); for (let ii = 0; ii < dataIDs.length; ii++) { const dataID = dataIDs[ii]; const oldRecord = oldRecords[dataID]; const newRecord = newRecords[dataID]; if (oldRecord && newRecord) { let updated: Record | null = null; const keys = Object.keys(newRecord); for (let iii = 0; iii < keys.length; iii++) { const key = keys[iii]; if (updated || oldRecord[key] !== newRecord[key]) { updated = updated !== null ? updated : { ...oldRecord }; updated[key] = newRecord[key]; } } updated = updated !== null ? updated : oldRecord; if (updated !== newRecord) { oldRecords[dataID] = updated; } } else if (oldRecord == null) { oldRecords[dataID] = newRecord; } else if (newRecord == null) { delete oldRecords[dataID]; } } this._environmentStoreOptimisticData.set(id, oldRecords); } onBridgeStoreSnapshot = (data: Array) => { for (const { id, records } of data) { this._environmentStoreData.set(id, records); this.emit('storeDataReceived'); } }; setStoreEvents = (id: number, data: LogEvent) => { switch (data.name) { case 'store.publish': this.mergeRecords(id, data.source); if (data.optimistic) { this.mergeOptimisticRecords(id, data.source); } break; case 'store.restore': this.clearOptimisticUpdates(id); break; case 'store.gc': this.garbageCollectRecords(id, data.references); break; default: break; } this.emit('storeDataReceived'); }; setEnvironmentEvents = (id: number, data: LogEvent) => { const arr = this._environmentEventsMap.get(id); if (arr) { arr.push(data); } else { this._environmentEventsMap.set(id, [data]); } this.emit('mutated'); if (data.name === 'execute.complete') { this.emit('mutationComplete'); } }; appendInformationToRequest = (id: number, data: LogEvent) => { switch (data.name) { case 'execute.start': const requestArr = this._recordedRequests.get(id); if (requestArr) { requestArr.set(data.transactionID, data); } else { const newRequest = new Map(); newRequest.set(data.transactionID, data); this._recordedRequests.set(id, newRequest); } break; case 'execute.next': case 'execute.info': case 'execute.complete': case 'execute.error': case 'execute.unsubscribe': const requests = this._recordedRequests.get(id); if (requests) { const request = requests.get(data.transactionID); if (request && request.name === 'execute.start') { data.params = request.params; data.variables = request.variables; } } break; default: break; } }; startRecording = () => { this._isRecording = true; this.clearAllEvents(); }; stopRecording = () => { this._isRecording = false; }; onBridgeEvents = (events: Array) => { for (const { id, data, eventType } of events) { if (this._isRecording) { const allEvents = this._environmentAllEvents.get(id); if (allEvents) { if (data.name === 'store.gc') { const records = this.getRecords(id); if (records != null) { data.gcRecords = {}; data.references = Object.keys(records) .filter(recID => recID != null && !data.references.includes(recID)) .map(recID => { data.gcRecords[recID] = records[recID]; return recID; }); } } else if (data.name === 'store.notify.complete') { const records = this.getRecords(id); if (records != null) { data.invalidatedRecords = {}; data.updatedRecords = {}; Object.keys(data.updatedRecordIDs).forEach(recID => { data.updatedRecords[recID] = { ...records[recID] }; }); data.invalidatedRecordIDs.forEach( recID => (data.invalidatedRecords[recID] = { ...records[recID] }) ); } } else if (data.name.startsWith('execute')) { this.appendInformationToRequest(id, data); } allEvents.push(data); } else { this._environmentAllEvents.set(id, [data]); } this.emit('allEventsReceived'); } if (eventType === 'store') { this.setStoreEvents(id, data); } else if (eventType === 'environment') { this.setEnvironmentEvents(id, data); } } }; onBridgeEnvironmentInit = (data: Array) => { for (const { id, environmentName } of data) { this._environmentNames.set(id, environmentName); } this.emit('environmentInitialized'); }; clearOptimisticUpdates = (envID: number) => { this._environmentStoreOptimisticData.delete(envID); }; garbageCollectRecords = (envID: number, references: $ReadOnlyArray) => { if (references.length === 0) { this._environmentStoreData.delete(envID); } else { const storeIDs = this.getRecordIDs(envID); if (storeIDs == null) { return; } for (const dataID of storeIDs) { if (!references.includes(dataID)) { this.removeRecord(envID, dataID); } } } }; clearAllEvents = () => { this._environmentAllEvents.forEach((_, key) => this.clearEvents(key)); this.emit('allEventsReceived'); }; clearEvents = (environmentID: number) => { this._environmentAllEvents.delete(environmentID); }; clearAllNetworkEvents = () => { this._environmentEventsMap.forEach((_, key) => this.clearNetworkEvents(key)); this.emit('mutated'); }; clearNetworkEvents = (environmentID: number) => { const completed = new Set(); let networkEventArray = this._environmentEventsMap.get(environmentID); if (networkEventArray !== undefined && networkEventArray.length > 0) { for (const event of networkEventArray) { if ( event.name === 'execute.complete' || event.name === 'execute.error' || event.name === 'execute.unsubscribe' ) { completed.add(event.transactionID); } } networkEventArray = networkEventArray.filter( event => storeEventNames.includes(event.name) && event.transactionID != null && !completed.has(event.transactionID) ); this._environmentEventsMap.set(environmentID, networkEventArray); this.emit('mutated'); } }; onBridgeShutdown = () => { if (__DEBUG__) { debug('onBridgeShutdown', 'unsubscribing from Bridge'); } this._bridge.removeListener('events', this.onBridgeEvents); this._bridge.removeListener('shutdown', this.onBridgeShutdown); this._bridge.removeListener('environmentInitialized', this.onBridgeEnvironmentInit); this._bridge.removeListener('storeRecords', this.onBridgeStoreSnapshot); }; } ================================================ FILE: src/devtools/utils.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ export function deepCopyFunction(inObject: any) { if (typeof inObject !== 'object' || inObject === null) { return inObject; } if (Array.isArray(inObject)) { const outObject = []; for (let i = 0; i < inObject.length; i++) { const value = inObject[i]; outObject[i] = deepCopyFunction(value); } return outObject; } else if (inObject instanceof Map) { const outObject = new Map(); inObject.forEach((val, key) => { outObject.set(key, deepCopyFunction(val)); }); return outObject; } else { const outObject = {}; for (const key in inObject) { const value = inObject[key]; if (typeof key === 'string' && key != null) { outObject[key] = deepCopyFunction(value); } } return outObject; } } export function debounce(func, wait) { let timeout = null; return function() { const newfunc = () => { timeout = null; func.apply(this, arguments); }; clearTimeout(timeout); timeout = setTimeout(newfunc, wait); }; } ================================================ FILE: src/devtools/view/Components/EnvironmentSelector.js ================================================ import React, { useState, useCallback, useEffect } from 'react'; function EnvironmentSelector(props) { const [selectEnv, setSelectEnv] = useState(''); return (
); } export default EnvironmentSelector; ================================================ FILE: src/devtools/view/Components/Record.js ================================================ import React from 'react'; function Record(props) { /* Maps through array and recursively calls component if the props[value] is an object, otherwise, it will store a key/value pair */ const records = Object.keys(props).map(key => { return typeof props[key] === 'object' ? (
{key}:
) : (
{key}: {JSON.stringify(props[key])}
); }); return
{records}
; } export default Record; ================================================ FILE: src/devtools/view/Components/SnapshotLinks.js ================================================ import React, { useState } from 'react'; const SnapshotLinks = ({ timeline, currentEnvID, handleSnapshot }) => { const [active, setActive] = useState(null); // Create links with date and label of snapshot; rendered in the left snapshot column using Bulma menu-list. Active state is used to toggle active link. return (
); }; export default SnapshotLinks; ================================================ FILE: src/devtools/view/NetworkDisplayer.js ================================================ import React, { useState, useEffect, useContext } from 'react'; import { StoreContext } from '../context'; import Record from './Components/Record'; import { execute } from 'graphql'; import { debounce } from '../utils'; //iterates over each event and joins events based on transactionID and sorts by type const combineEvents = events => { const combinedEvents = {}; const eventTypes = {}; //join events by transactionID events.forEach(event => { const tempObj = {}; if (event.name === 'execute.start') { tempObj.request = event.params; tempObj.variables = event.variables; } else if (event.name === 'execute.info') { tempObj.info = event.info; } else if (event.name === 'execute.next') { tempObj.response = event.response; } else if (event.name === 'execute.complete') { // tempObj.complete = true } combinedEvents[event.transactionID] ? (combinedEvents[event.transactionID] = Object.assign( combinedEvents[event.transactionID], tempObj )) : (combinedEvents[event.transactionID] = tempObj); }); //sort by type Object.keys(combinedEvents).forEach(transactionID => { const op = combinedEvents[transactionID].request.operationKind; eventTypes[op] ? (eventTypes[op] = Object.assign(eventTypes[op], { [transactionID]: combinedEvents[transactionID] })) : (eventTypes[op] = { [transactionID]: combinedEvents[transactionID] }); }); return eventTypes; }; //generates a list of elements for the menu and the events listing const generateElementList = (events, searchResults, selection, handleMenuClick) => { const eventMenu = []; const eventsList = []; //for each event - add to menu list for (let type in events) { //creates an array of menu items for all events belonging to a given type const typeList = []; for (let id in events[type]) { //filter out results based on search input if (new RegExp(searchResults, 'i').test(JSON.stringify(events[type][id]))) { typeList.push(
  • { handleMenuClick(e, id); }} > {events[type][id].request.name}
  • ); //creates an array of elements for all events eventsList.push(
    ); } } //pushes the new type element with child events to the typeList component array eventMenu.push(
  • { handleMenuClick(e, type); }} > {type}
      {typeList}
  • ); } return { eventMenu, eventsList }; }; const NetworkDisplayer = ({ currentEnvID }) => { const [selection, setSelection] = useState(''); const [events, setEvents] = useState([]); const [searchResults, setSearchResults] = useState(''); const store = useContext(StoreContext); useEffect(() => { //on mutation all store events are pulled and processed with events state updated const onMutated = () => { setEvents(combineEvents(store._environmentEventsMap.get(currentEnvID) || [])); }; store.addListener('mutated', onMutated); return () => { store.removeListener('mutated', onMutated); }; }, [store]); //handle type menu click events function handleMenuClick(e, id) { //set new selection setSelection(id); } //shows you the entire network function handleReset(e) { //remove selection; setSelection(''); } //updates search results const debounced = debounce(val => setSearchResults(val), 300); function handleSearch(e) { //debounce search debounced(e.target.value); } //generate menu list and events list const { eventMenu, eventsList } = generateElementList( events, searchResults, selection, handleMenuClick ); return (

    { handleSearch(e); }} >

    {eventsList}
    ); }; export default NetworkDisplayer; ================================================ FILE: src/devtools/view/StoreDisplayer.js ================================================ import React, { useState } from 'react'; import Record from './Components/Record'; import { debounce } from '../utils'; //update record list to current selection function updateRecords(store, selection) { if (store) { if (selection === '') { return store; //id selected - filter out everything except selected id } else if (selection[0] === 'i') { const id = selection.slice(3); return Object.keys(store).reduce((newRL, key) => { if (store[key].__id === id) newRL[key] = store[key]; return newRL; }, {}); //type selected - filter out everything except selected type } else { const type = selection.slice(5); return Object.keys(store).reduce((newRL, key) => { if (store[key].__typename === type) newRL[key] = store[key]; return newRL; }, {}); } } } //generate list of menu elements function generateComponentsList(store, searchResults, recordsList, selection, handleMenuClick) { //create menu list of all types const menuList = {}; const typeList = []; for (let id in store) { const record = store[id]; menuList[record.__typename] ? menuList[record.__typename].push(record.__id) : (menuList[record.__typename] = [record.__id]); } //loop through each type and generate menu item for (let type in menuList) { //creates an array of elements for all ids belonging to a given type const idList = menuList[type] .filter(id => new RegExp(searchResults, 'i').test(JSON.stringify(recordsList[id]))) .map(id => { return (
  • { handleMenuClick('id-' + id); }} > {id}
  • ); }); //pushes the new type element with child ids to the typeList component array if (idList.length !== 0) { typeList.push(
  • { handleMenuClick('type-' + type); }} > {type}
      {idList}
  • ); } } return typeList; } const StoreDisplayer = ({ store }) => { const [recordsList, setRecordsList] = useState({}); const [selection, setSelection] = useState(''); const [searchResults, setSearchResults] = useState(''); React.useEffect(() => { //initialize store setRecordsList(store); }, [store]); //handle menu click events function handleMenuClick(selection) { //set new selection setSelection(selection); //update display with current selection setRecordsList(updateRecords(store, selection)); } //shows you the entire store function handleReset(e) { //remove selection setSelection(''); //reset back to original store setRecordsList(store); } //updates search results const debounced = debounce(val => { setSelection(''); setRecordsList(store); setSearchResults(val); }, 300); function handleSearch(e) { //debounce search debounced(e.target.value); } //generates the menu element list //verify recordsList is not undefined and then generate list of components const typeList = recordsList === undefined ? [] : generateComponentsList(store, searchResults, recordsList, selection, handleMenuClick); return (

    { handleSearch(e); }} >

    ); }; export default StoreDisplayer; ================================================ FILE: src/devtools/view/StoreTimeline.js ================================================ import React, { useState, useContext, useEffect } from 'react'; import InputRange from 'react-input-range'; import { BridgeContext, StoreContext } from '../context'; import StoreDisplayer from './StoreDisplayer'; import SnapshotLinks from './Components/SnapshotLinks'; const StoreTimeline = ({ currentEnvID }) => { const store = useContext(StoreContext); const bridge = useContext(BridgeContext); const [snapshotIndex, setSnapshotIndex] = useState(0); const [timelineLabel, setTimelineLabel] = useState(''); const [liveStore, setLiveStore] = useState({}); // Each envId has an array of orbject built up for loading snapshots via the handleClick const [timeline, setTimeline] = useState({ [currentEnvID]: [ { label: 'at startup', date: new Date(), storage: liveStore } ] }); // build snapshot object and insert into timeline const handleClick = e => { e.preventDefault(); const timelineInsert = {}; const timeStamp = new Date(); timelineInsert.label = timelineLabel; timelineInsert.date = timeStamp; timelineInsert.storage = liveStore; const newTimeline = timeline[currentEnvID].concat([timelineInsert]); setTimeline({ ...timeline, [currentEnvID]: newTimeline }); setTimelineLabel(''); setSnapshotIndex(newTimeline.length); }; const handleSnapshot = index => { setSnapshotIndex(index); }; const updateStoreHelper = storeObj => { setLiveStore(storeObj); }; // triggering refresh of store on completed mutation React.useEffect(() => { const refreshLiveStore = () => { bridge.send('refreshStore', currentEnvID); }; const refreshEvents = () => { const allRecords = store.getRecords(currentEnvID); updateStoreHelper(allRecords); }; store.addListener('storeDataReceived', refreshEvents); store.addListener('allEventsReceived', refreshEvents); store.addListener('mutationComplete', refreshLiveStore); return () => { store.removeListener('mutationComplete', refreshLiveStore); store.removeListener('storeDataReceived', refreshEvents); store.removeListener('allEventsReceived', refreshEvents); }; }, [store]); React.useEffect(() => { const allRecords = store.getRecords(currentEnvID); setLiveStore(allRecords); if (!timeline[currentEnvID]) { const newTimeline = { ...timeline, [currentEnvID]: [ { label: 'current', date: new Date(), storage: allRecords } ] }; setTimeline(newTimeline); setSnapshotIndex(1); } else { setSnapshotIndex(timeline[currentEnvID].length); } }, [currentEnvID]); console.log( 'showing livestore', !timeline[currentEnvID] || !timeline[currentEnvID][snapshotIndex] || snapshotIndex === timeline[currentEnvID].length ); return (
    setTimelineLabel(e.target.value)} placeholder="take a store snapshot" >
    setSnapshotIndex(value)} />
    {timeline[currentEnvID] && ( )}
    ); }; export default StoreTimeline; ================================================ FILE: src/hook.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /** * Install the hook on window, which is an event emitter. * Note because Chrome content scripts cannot directly modify the window object, * we are evaling this function by inserting a script tag. * That's why we have to inline the whole event emitter implementation here. */ import type { DevToolsHook } from 'src/backend/types'; declare var window: any; export function installHook(target: any): DevToolsHook | null { if (target.hasOwnProperty('__RELAY_DEVTOOLS_HOOK__')) { return null; } const listeners = {}; const environments = new Map(); let uidCounter = 0; function registerEnvironment(environment) { const id = ++uidCounter; environments.set(id, environment); hook.emit('environment', { id, environment }); return id; } function sub(event, fn) { hook.on(event, fn); return () => hook.off(event, fn); } function on(event, fn) { if (!listeners[event]) { listeners[event] = []; } listeners[event].push(fn); } function off(event, fn) { if (!listeners[event]) { return; } const index = listeners[event].indexOf(fn); if (index !== -1) { listeners[event].splice(index, 1); } if (!listeners[event].length) { delete listeners[event]; } } function emit(event, data) { if (listeners[event]) { listeners[event].map(fn => fn(data)); } } const environmentWrappers = new Map(); const hook: DevToolsHook = { registerEnvironment, environmentWrappers, // listeners, environments, emit, // inject, on, off, sub }; Object.defineProperty( target, '__RELAY_DEVTOOLS_HOOK__', ({ // This property needs to be configurable for the test environment, // else we won't be able to delete and recreate it beween tests. configurable: __DEV__, enumerable: false, get() { return hook; } }: Object) ); return hook; } ================================================ FILE: src/types.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ export type Wall = {| // `listen` returns the "unlisten" function. listen: (fn: Function) => Function, send: (event: string, payload: any, transferable?: Array) => void |}; export type Record = { [key: string]: mixed, ... }; export type DataID = string; export type UpdatedRecords = { [dataID: DataID]: boolean, ... }; export type StoreRecords = { [DataID]: ?Record, ... }; // Copied from relay export type LogEvent = | {| +name: 'queryresource.fetch', +operation: $FlowFixMe, // FetchPolicy from relay-experimental +fetchPolicy: string, // RenderPolicy from relay-experimental +renderPolicy: string, +hasFullQuery: boolean, +shouldFetch: boolean |} | {| +name: 'store.publish', +source: any, +optimistic: boolean |} | {| +name: 'store.gc', references: Array, gcRecords: StoreRecords |} | {| +name: 'store.restore' |} | {| +name: 'store.snapshot' |} | {| +name: 'store.notify.start' |} | {| +name: 'store.notify.complete', +updatedRecordIDs: UpdatedRecords, +invalidatedRecordIDs: Array, updatedRecords: StoreRecords, invalidatedRecords: StoreRecords |} | {| +name: 'execute.info', +transactionID: number, +info: mixed, params: $FlowFixMe, variables: $FlowFixMe |} | {| +name: 'execute.start', +transactionID: number, +params: $FlowFixMe, +variables: $FlowFixMe |} | {| +name: 'execute.next', +transactionID: number, +response: $FlowFixMe, params: $FlowFixMe, variables: $FlowFixMe |} | {| +name: 'execute.error', +transactionID: number, +error: Error, params: $FlowFixMe, variables: $FlowFixMe |} | {| +name: 'execute.complete', +transactionID: number, params: $FlowFixMe, variables: $FlowFixMe |} | {| +name: 'execute.unsubscribe', +transactionID: number, params: $FlowFixMe, variables: $FlowFixMe |}; export type EventData = {| +id: number, +data: LogEvent, +source: StoreRecords, +eventType: string |}; export type StoreData = {| +name: string, +id: number, +records: StoreRecords |}; export type EnvironmentInfo = {| +id: number, +environmentName: string |};