[
  {
    "path": ".eslintignore",
    "content": "node_modules\n\nshells/browser/chrome/build\nshells/browser/firefox/build\nshells/browser/shared/build\nshells/dev/dist\nvendor\n*.js.snap\n\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": ".flowconfig",
    "content": "[ignore]\nshells/browser/chrome/build/*\nshells/browser/firefox/build/*\nshells/dev/build/*\n\n[declarations]\n<PROJECT_ROOT>/node_modules/graphql\n\n[include]\n\n[libs]\n/flow-typed/\n./flow.js\n\n[lints]\n\n[options]\nesproposal.class_instance_fields=enable\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowFixMe\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowIssue\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowIgnore\nmodule.name_mapper='^src' ->'<PROJECT_ROOT>/src'\nesproposal.optional_chaining=enable\n\n[strict]\n\n[version]\n^0.113.0\n"
  },
  {
    "path": ".gitignore",
    "content": "/shells/browser/chrome/*.crx\n/shells/browser/chrome/*.pem\n/shells/browser/firefox/*.xpi\n/shells/browser/firefox/*.pem\n/shells/browser/shared/build\n/packages/relay-devtools-core/dist\n/shells/dev/dist\nbuild\nnode_modules\nnpm-debug.log\nyarn-error.log\n.DS_Store\nyarn-error.log\n.vscode\n.idea\n*.pem\ndist\npackage-lock.json"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"relay-examples\"]\n\tpath = relay-examples\n\turl = https://github.com/relayjs/relay-examples.git\n"
  },
  {
    "path": ".travis.yml",
    "content": "services:\n  - docker\ndist: xenial\nscript:\n  - docker-compose up --abort-on-container-exit"
  },
  {
    "path": ".yarnrc",
    "content": "yarn-offline-mirror false\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:12.18.3\nWORKDIR /usr/src/app\nCOPY . /usr/src/app\nRUN npm install\nCMD npm run test\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n<div align=\"center\">\n  <img width=\"50%\" src='./assets/protologo.png'></img>\n</div>\n\n<h1>Proto Relay</h1>\nProto 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. \n\n## Features\n- [x] Preview Relay store content from the Chrome devtools panel\n- [x] View store content over time with included snapshots\n- [x] View store mutations and network queries\n\n## Installation\n1. Fork and clone this repository onto your local computer\n2. Install dependencies and run a build using either the 'Yarn' or 'NPM' commands below:\n```node\n# Yarn\nyarn install\nyarn build\n\n# NPM\nnpm run install\nnpm run build\n```\n3. Access the Chome extensions within the browser\n4. Access [Chrome extensions](chrome://extensions/) within the browser\n5. Click on \"Load Unpacked\"\n6. Navigate and select the folder: protostar-relay  > Shells > browser > chrome > build > unpacked\n7. Go to a website built with Relay and open the \"proto*\" panel. Websites that use Relay include:\n   - [facebook.com](https://www.facebook.com/)\n   - [artsy.com](https://www.artsy.net/)\n   - [oculus.com](https://www.oculus.com/)\n      \n\n## How to Use\n<img width=\"100%\" src='./assets/protostar-records-filter.gif'></img>\n- Example view of interacting with the Relay store.\n\n<img width=\"100%\" src='./assets/protostar-mutations.gif'></img>\n- Example of snapshot functionality and viewing mutations.\n\n## Contributing\nProtostar-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:\n- Optimistic updates:\n  - Visual representation.\n  - List of all optimistic updates with pending/resolved status.\n  - Control data flow.\n  \n## Google Chrome Web Store\nGet it on the Chrome Extension Store: [coming soon]().\n\n## Contributors\n[Aryeh Kobrinsky](https://github.com/akobrinsky), \n[Liz Lotto](https://github.com/elizlotto), \n[Marc Burnie](https://github.com/marcburnie), \n[Qwen Ballard](https://github.com/qwenballard)\n\n\n## License\nThis project is licensed under the MIT License- see the [LICENSE.md](https://github.com/oslabs-beta/protostar-relay/blob/master/LICENSE) for more details.\n\n* Inspired by [Facebook's Relay Devtool](https://github.com/relayjs/relay-devtools)\n"
  },
  {
    "path": "__tests__/DevTools.spec.js",
    "content": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\nimport renderer from 'react-test-renderer';\nimport DevTools from '../src/devtools/DevTools';\n\nconfigure({ adapter: new Adapter() });\n\ndescribe('DevTools', () => {\n  let wrapper;\n  const RealDate = Date;\n  const names = { 1: 'first', 2: 'second', 3: 'third' };\n  const props = {\n    store: {\n      getEnvironmentIDs: () => [1, 2, 3],\n      getEnvironmentName: id => names[id]\n    },\n    bridge: 'hi'\n  };\n\n  beforeAll(() => {\n    wrapper = shallow(<DevTools {...props} />);\n  });\n\n  it('Passes the bridge to provider', () => {\n    expect(wrapper.prop('value')).toEqual(props.bridge);\n  });\n\n  it('Passes the store to provider', () => {\n    expect(\n      wrapper\n        .children()\n        .first()\n        .prop('value')\n    ).toEqual(props.store);\n  });\n\n  it('Has a dropdown select element for environmentID with an onChange method', () => {\n    expect(wrapper.find('select').length).toEqual(1);\n    expect(wrapper.find('select').prop('onChange')).not.toEqual(undefined);\n  });\n\n  it('Lists environments in dropdown selector', () => {\n    const option = wrapper.find('option');\n    expect(option.length).toEqual(3);\n    expect(option.at(0).text()).toEqual(names[1]);\n    expect(option.at(1).text()).toEqual(names[2]);\n    expect(option.at(2).text()).toEqual(names[3]);\n    expect(option.at(0).prop('value')).toEqual(1);\n    expect(option.at(1).prop('value')).toEqual(2);\n    expect(option.at(2).prop('value')).toEqual(3);\n  });\n\n  it('Has a StoreTimeline component', () => {\n    expect(wrapper.find('StoreTimeline').length).toEqual(1);\n  });\n\n  it('Has a NetworkDisplayer component', () => {\n    expect(wrapper.find('NetworkDisplayer').length).toEqual(1);\n  });\n\n  it('Passes the current environment to a currentEnvID prop on StoreTimeline and defaults to the first ID', () => {\n    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(1);\n  });\n\n  it('Can select between different environments and pass the current environment to a currentEnvID prop on StoreTimeline', () => {\n    wrapper.find('select').simulate('change', { target: { value: 2 } });\n    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(2);\n    wrapper.find('select').simulate('change', { target: { value: 3 } });\n    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(3);\n    wrapper.find('select').simulate('change', { target: { value: 1 } });\n    expect(wrapper.find('StoreTimeline').prop('currentEnvID')).toEqual(1);\n  });\n\n  it('Has network hidden by default and store is visible', () => {\n    expect(\n      wrapper\n        .find('StoreTimeline')\n        .parent()\n        .hasClass('is-hidden')\n    ).toEqual(false);\n    expect(\n      wrapper\n        .find('NetworkDisplayer')\n        .parent()\n        .hasClass('is-hidden')\n    ).toEqual(true);\n  });\n\n  it('Allows user to select between store and network view', () => {\n    const networkSelector = wrapper.find('#networkSelector');\n    expect(networkSelector.length).toEqual(1);\n    expect(networkSelector.prop('onClick')).not.toEqual(undefined);\n    networkSelector.simulate('click');\n    expect(\n      wrapper\n        .find('StoreTimeline')\n        .parent()\n        .hasClass('is-hidden')\n    ).toEqual(true);\n    expect(\n      wrapper\n        .find('NetworkDisplayer')\n        .parent()\n        .hasClass('is-hidden')\n    ).toEqual(false);\n\n    const storeSelector = wrapper.find('#storeSelector');\n    expect(storeSelector.length).toEqual(1);\n    expect(storeSelector.prop('onClick')).not.toEqual(undefined);\n    storeSelector.simulate('click');\n    expect(\n      wrapper\n        .find('StoreTimeline')\n        .parent()\n        .hasClass('is-hidden')\n    ).toEqual(false);\n    expect(\n      wrapper\n        .find('NetworkDisplayer')\n        .parent()\n        .hasClass('is-hidden')\n    ).toEqual(true);\n  });\n\n  it('Renders correctly', () => {\n    const date = new Date(Date.UTC(2020));\n    global.Date = jest.fn(() => date);\n    const tree = renderer.create(<DevTools {...props} />).toJSON();\n    expect(tree).toMatchSnapshot();\n    jest.clearAllMocks();\n  });\n});\n"
  },
  {
    "path": "__tests__/Global.spec.js",
    "content": "describe('Timezones', () => {\n  it('should always be UTC', () => {\n    expect(new Date().getTimezoneOffset()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "__tests__/NetworkDisplayer.spec.js",
    "content": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\nimport renderer from 'react-test-renderer';\nimport NetworkDisplayer from '../src/devtools/view/NetworkDisplayer';\n\nconfigure({ adapter: new Adapter() });\n\ndescribe('NetworkDisplayer', () => {\n  let wrapper;\n  const props = {};\n\n  beforeAll(() => {\n    wrapper = shallow(<NetworkDisplayer {...props} />);\n  });\n\n  it('My Test Case', () => {\n    expect(true).toEqual(true);\n  });\n\n  it('My Test Case', () => {\n    expect(wrapper.find('Record').length).toEqual(0);\n  });\n\n  it('Renders correctly', () => {\n    const tree = renderer.create(<NetworkDisplayer {...props} />).toJSON();\n    expect(tree).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "__tests__/Record.spec.js",
    "content": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\n// import toJson from 'enzyme-to-json';\n\nimport renderer from 'react-test-renderer';\n\nimport Record from '../src/devtools/view/Components/Record';\n\nconfigure({ adapter: new Adapter() });\n\ndescribe('Record', () => {\n  let wrapper;\n  let children; //alternative to .next, not always required.\n  const props = {\n    //hardcode in what to pass into component\n    hi: true,\n    nested: { this: 'that' }\n  };\n\n  beforeAll(() => {\n    wrapper = shallow(<Record {...props} />);\n    children = wrapper.children();\n  });\n\n  it('Renders a <div> tag with a className of \"records\"', () => {\n    //we are using methods from enzyme library so look at the docs\n    expect(wrapper.type()).toEqual('div');\n    expect(wrapper.hasClass('records')).toEqual(true);\n  });\n\n  it('Has two children divs: one with className objectProperty and the other with className nestedObject', () => {\n    expect(children.length).toEqual(2);\n    expect(children.first().hasClass('objectProperty')).toEqual(true);\n    expect(children.last().hasClass('nestedObject')).toEqual(true);\n  });\n\n  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.', () => {\n    const rootChildren = children.first().children();\n    expect(rootChildren.length).toEqual(2);\n    expect(rootChildren.first().hasClass('key')).toEqual(true);\n    expect(rootChildren.first().text()).toEqual('hi: ');\n    expect(rootChildren.last().hasClass('value')).toEqual(true);\n    expect(rootChildren.last().text()).toEqual('true');\n  });\n\n  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: \"', () => {\n    const rootChildren = children.last().children();\n    expect(rootChildren.first().text()).toEqual('nested: ');\n    expect(rootChildren.length).toEqual(2);\n    expect(rootChildren.first().hasClass('key')).toEqual(true);\n    expect(rootChildren.last().find(Record).length).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "__tests__/StoreDisplayer.spec.js",
    "content": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\nimport renderer from 'react-test-renderer';\nimport StoreDisplayer from '../src/devtools/view/StoreDisplayer';\n\nconfigure({ adapter: new Adapter() });\n\ndescribe('StoreDisplayer', () => {\n\n  let wrapper;\n  let useEffect;\n  const store = {\n    '1': {\n      '__id': '1',\n      '__typename': 'User',\n      'name': 'Marc'\n    },\n    '2': {\n      '__id': '2',\n      '__typename': 'User',\n      'name': 'Aryeh'\n    },\n    '3': {\n      '__id': '3',\n      '__typename': 'User',\n      'name': 'Liz'\n    },\n    '4': {\n      '__id': '4',\n      '__typename': 'User',\n      'name': 'Qwen'\n    },\n    '5': {\n      '__id': '5',\n      '__typename': 'Post',\n      'text': 'Hi'\n    }\n  }\n\n  const mockUseEffect = () => {\n    useEffect.mockImplementationOnce(f => f());\n  };\n\n\n  beforeEach(() => {\n    useEffect = jest.spyOn(React, \"useEffect\");\n    mockUseEffect();\n    wrapper = shallow(<StoreDisplayer store={store} />);\n  });\n\n  it(\"Has a Record component with the filtered records passed as props\", () => {\n    expect(wrapper.find('Record').length).toEqual(1);\n    expect(wrapper.find('Record').props()).toEqual(store);\n  })\n\n  it(\"Has a menu\", () => {\n    expect(wrapper.find('.menu').length).toEqual(1);\n  })\n\n  describe(\"Menu\", () => {\n    let menu;\n\n    beforeEach(() => {\n      menu = wrapper.find('.menu');\n    })\n\n    it(\"Generates a list of menu items from the store object\", () => {\n      expect(menu.find(\"#type-User\").length).toEqual(1);\n      expect(menu.find(\"#type-Post\").length).toEqual(1);\n      Object.keys(store).forEach(k => {\n        expect(menu.find(`#id-${k}`).length).toEqual(1);\n      })\n    })\n\n    it(\"Has menu items with an onClick event that filters the results displayed on the screen based on ID\", () => {\n      Object.keys(store).forEach(k => {\n        menu.find(`#id-${k}`).simulate('click');\n        expect(wrapper.find(\"Record\").props()).toEqual({ [k]: store[k] })\n      })\n    })\n\n    it(\"Has menu items with an onClick event that filters the results displayed on the screen based on type\", () => {\n      menu.find(`#type-User`).simulate('click');\n      expect(wrapper.find(\"Record\").props()).toEqual({\n        '1': {\n          '__id': '1',\n          '__typename': 'User',\n          'name': 'Marc'\n        },\n        '2': {\n          '__id': '2',\n          '__typename': 'User',\n          'name': 'Aryeh'\n        },\n        '3': {\n          '__id': '3',\n          '__typename': 'User',\n          'name': 'Liz'\n        },\n        '4': {\n          '__id': '4',\n          '__typename': 'User',\n          'name': 'Qwen'\n        }\n      })\n    })\n\n    it(\"Adds an 'is-active' class to the currently selected menu item\", () => {\n      Object.keys(store).forEach(k => {\n        let menuItem = wrapper.find(`#id-${k}`)\n        expect(menuItem.length).toEqual(1)\n        menuItem.props().onClick();\n        menuItem = wrapper.find(`#id-${k}`)\n        expect(menuItem.hasClass('is-active')).toEqual(true)\n        expect(menuItem.hasClass('is-active')).toEqual(true)\n        menu.find(\"a\").forEach(el => {\n          if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false)\n        })\n      })\n\n      let menuItem = wrapper.find(`#type-User`)\n      expect(menuItem.length).toEqual(1)\n      menuItem.simulate('click');\n      menuItem = wrapper.find(`#type-User`)\n      expect(menuItem.hasClass('is-active')).toEqual(true)\n      menu.find(\"a\").forEach(el => {\n        if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false)\n      })\n      menuItem = wrapper.find(`#type-Post`)\n      expect(menuItem.length).toEqual(1)\n      menuItem.simulate('click');\n      menuItem = wrapper.find(`#type-Post`)\n      expect(menuItem.hasClass('is-active')).toEqual(true)\n      menu.find(\"a\").forEach(el => {\n        if (el !== menuItem) expect(el.hasClass('is-active')).toEqual(false)\n      })\n    })\n\n    it(\"Removes the 'is-active' class when the reset button is clicked\", () => {\n      let menuItem = menu.find(`#type-User`)\n      expect(menuItem.length).toEqual(1)\n      menuItem.simulate('click');\n      expect(wrapper.find('.menu').find(\".is-active\").length).toEqual(1);\n      wrapper.find('button').simulate('click');\n      expect(wrapper.find('.menu').find(\".is-active\").length).toEqual(0);\n    })\n\n    it('Has a search input with an onChange property', () => {\n      expect(wrapper.find('input').length).toEqual(1);\n      expect(wrapper.find('input').prop('onChange')).not.toBe(undefined);\n    });\n\n    describe('Search Box', () => {\n      let search;\n\n      beforeEach(() => {\n        search = wrapper.find('input');\n      })\n\n      it(\"Filters the menu items\", () => {\n        search.prop('onChange')({ target: { value: 'Marc' } });\n        jest.runAllTimers();\n        expect(wrapper.find(`#id-1`).length).toEqual(1);\n        Object.keys(store).forEach(k => {\n          if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(0);\n        })\n      })\n\n      it(\"Debounces the input\", () => {\n        search.prop('onChange')({ target: { value: 'Marc' } });\n        Object.keys(store).forEach(k => {\n          if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(1);\n        })\n        jest.runAllTimers();\n        Object.keys(store).forEach(k => {\n          if (k !== '1') expect(wrapper.find(`#id-${k}`).length).toEqual(0);\n        })\n      })\n    });\n\n    it('Has a Reset Button with an onClick property', () => {\n      expect(wrapper.find('button').length).toEqual(1);\n      expect(wrapper.find('button').prop('onClick')).not.toBe(undefined);\n    });\n\n    describe('Reset Button', () => {\n      it(\"Has a reset button that removes any selectors\", () => {\n        menu.find(`#type-User`).simulate('click');\n        expect(wrapper.find('Record').props()).not.toEqual(store)\n        wrapper.find('button').simulate('click');\n        expect(wrapper.find('Record').props()).toEqual(store)\n      })\n    });\n\n  })\n\n  it('Renders correctly', () => {\n    const tree = renderer.create(<StoreDisplayer store={store} />).toJSON();\n    expect(tree).toMatchSnapshot();\n  })\n\n});"
  },
  {
    "path": "__tests__/StoreTimeline.spec.js",
    "content": "import React from 'react';\nimport { configure, shallow, render } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\nimport renderer from 'react-test-renderer';\nimport StoreTimeline from '../src/devtools/view/StoreTimeline';\n\nconfigure({ adapter: new Adapter() });\n\ndescribe('StoreTimeline', () => {\n  let wrapper;\n  const props = {\n    currentEnvID: 1\n  };\n\n  beforeEach(() => {\n    wrapper = shallow(<StoreTimeline {...props} />);\n  });\n\n  it('Renders a StoreDisplayer component and passes store as a prop', () => {\n    expect(wrapper.find('StoreDisplayer').length).toEqual(1);\n  });\n\n  // it(\"Passes the store based on the currentEnvID\", () => {\n  // })\n\n  // describe(\"Snapshots\", () => {\n\n  //   it(\"Takes a snapshot at startup\", () => {\n\n  //   })\n\n  //   it(\"Has a snapshot button that takes and saves a snapshot\", () => {\n\n  //   })\n\n  //   it(\"Defaults to displaying the latest store value when a snapshot is taken\", () => {\n\n  //   })\n\n  //   it(\"Remembers snapshots when switching between environments\", () => {\n\n  //   })\n\n  //   it(\"Has a snapshot text input\", () => {\n\n  //   })\n\n  //   it(\"Has a previous buttons to move to the previous snapshot\", () => {\n\n  //   })\n\n  //   it(\"Has a next button to move to the previous snapshot\", () => {\n\n  //   })\n\n  //   it(\"Has a current button that shows the current store value\", () => {\n\n  //   })\n\n  //   it(\"Has a slider that updates when a new snapshot is taken and when switching between environments\", () => {\n\n  //   })\n\n  // })\n\n  it('Renders correctly', () => {\n    const date = new Date(Date.UTC(2020));\n    global.Date = jest.fn(() => date);\n    const tree = renderer.create(<StoreTimeline {...props} />).toJSON();\n    expect(tree).toMatchSnapshot();\n    jest.clearAllMocks();\n  });\n});\n"
  },
  {
    "path": "__tests__/__mocks__/styleMock.js",
    "content": "module.exports = {};"
  },
  {
    "path": "__tests__/__snapshots__/DevTools.spec.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`DevTools Renders correctly 1`] = `\nArray [\n  <div\n    className=\"navigation\"\n  >\n    <form\n      className=\"env-select select is-small is-pulled-left\"\n    >\n      <select\n        className=\"env-select\"\n        onChange={[Function]}\n      >\n        <option\n          value={1}\n        >\n          first\n        </option>\n        <option\n          value={2}\n        >\n          second\n        </option>\n        <option\n          value={3}\n        >\n          third\n        </option>\n      </select>\n    </form>\n    <div\n      className=\"tabs is-toggle is-small is-pulled-left\"\n    >\n      <ul>\n        <li\n          className=\"is-active\"\n        >\n          <a\n            id=\"storeSelector\"\n            onClick={[Function]}\n          >\n            <span\n              className=\"icon is-small\"\n            >\n              <i\n                className=\"fas fa-database\"\n              />\n            </span>\n            <span>\n              Store\n            </span>\n          </a>\n        </li>\n        <li\n          className={false}\n        >\n          <a\n            id=\"networkSelector\"\n            onClick={[Function]}\n          >\n            <span\n              className=\"icon is-small\"\n            >\n              <i\n                className=\"fas fa-network-wired\"\n              />\n            </span>\n            <span>\n              Network\n            </span>\n          </a>\n        </li>\n      </ul>\n    </div>\n    <div\n      className=\"logo is-pulled-right\"\n    >\n      <a\n        href=\"https://github.com/oslabs-beta/protostar-relay\"\n        target=\"_blank\"\n      >\n        <img\n          src=\"../../assets/protorelay.png\"\n        />\n      </a>\n    </div>\n  </div>,\n  <div\n    className=\"columns mb-0 is-multiline is-mobile\"\n  >\n    <div\n      className=\"column is-full-mobile is-one-quarter-desktop\"\n    >\n      <div\n        className=\"display-box\"\n      >\n        <div\n          className=\"snapshot-wrapper is-flex ml-2\"\n        >\n          <input\n            className=\"input is-small snapshot-btn is-primary\"\n            onChange={[Function]}\n            placeholder=\"take a store snapshot\"\n            type=\"text\"\n            value=\"\"\n          />\n          <button\n            className=\"button is-small is-link\"\n            onClick={[Function]}\n          >\n            Snapshot\n          </button>\n        </div>\n      </div>\n      <div\n        className=\"snapshots\"\n      >\n        <div\n          className=\"timeline-nav column is-full-desktop is-flex-mobile\"\n          id=\"timeline-mini-col\"\n        >\n          <div\n            aria-disabled={false}\n            className=\"input-range\"\n            onKeyDown={[Function]}\n            onKeyUp={[Function]}\n            onMouseDown={[Function]}\n            onTouchStart={[Function]}\n          >\n            <span\n              className=\"input-range__label input-range__label--min\"\n            >\n              <span\n                className=\"input-range__label-container\"\n              >\n                0\n              </span>\n            </span>\n            <div\n              className=\"input-range__track input-range__track--background\"\n              onMouseDown={[Function]}\n              onTouchStart={[Function]}\n            >\n              <div\n                className=\"input-range__track input-range__track--active\"\n                style={\n                  Object {\n                    \"left\": \"0%\",\n                    \"width\": \"0%\",\n                  }\n                }\n              />\n              <span\n                className=\"input-range__slider-container\"\n                style={\n                  Object {\n                    \"left\": \"0%\",\n                    \"position\": \"absolute\",\n                  }\n                }\n              >\n                <span\n                  className=\"input-range__label input-range__label--value\"\n                >\n                  <span\n                    className=\"input-range__label-container\"\n                  >\n                    0\n                  </span>\n                </span>\n                <div\n                  aria-valuemax={1}\n                  aria-valuemin={0}\n                  aria-valuenow={0}\n                  className=\"input-range__slider\"\n                  draggable=\"false\"\n                  onKeyDown={[Function]}\n                  onMouseDown={[Function]}\n                  onTouchStart={[Function]}\n                  role=\"slider\"\n                  tabIndex=\"0\"\n                />\n              </span>\n            </div>\n            <span\n              className=\"input-range__label input-range__label--max\"\n            >\n              <span\n                className=\"input-range__label-container\"\n              >\n                1\n              </span>\n            </span>\n          </div>\n          <div\n            className=\"snapshot-nav has-text-centered has-text-right-mobile\"\n          >\n            <button\n              class=\"button is-small is-info is-light\"\n              onClick={[Function]}\n            >\n              <span\n                className=\"icon is-medium\"\n              >\n                <i\n                  className=\"fas fa-fast-backward\"\n                />\n              </span>\n            </button>\n            <button\n              class=\"button is-small is-info is-light\"\n              onClick={[Function]}\n            >\n              Current\n            </button>\n            <button\n              class=\"button is-small is-info is-light\"\n              onClick={[Function]}\n            >\n              <span\n                className=\"icon is-medium\"\n              >\n                <i\n                  className=\"fas fa-fast-forward\"\n                />\n              </span>\n            </button>\n          </div>\n        </div>\n        <div\n          className=\"snapshot-info is-size-7 column is-full-desktop pt-0\"\n          id=\"snapshot-info-col\"\n        >\n          <div>\n            <aside\n              className=\"menu\"\n            >\n              <ul\n                className=\"menu-list\"\n              >\n                <li\n                  onClick={[Function]}\n                >\n                  <a\n                    className={false}\n                    href=\"#\"\n                  >\n                    12:00:00 AM\n                    : \n                    at startup\n                  </a>\n                </li>\n              </ul>\n            </aside>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div\n      className=\"column is-half-mobile scrollable\"\n    >\n      <p\n        class=\"control has-icons-left is-flex\"\n      >\n        <input\n          className=\"input is-small is-primary\"\n          onChange={[Function]}\n          placeholder=\"Search\"\n          type=\"text\"\n        />\n        <button\n          className=\"button is-small is-link\"\n          onClick={[Function]}\n        >\n          Reset\n        </button>\n        <span\n          class=\"icon is-left\"\n        >\n          <i\n            class=\"fas fa-search\"\n          />\n        </span>\n      </p>\n      <aside\n        className=\"menu\"\n      >\n        <p\n          className=\"menu-label mt-1\"\n        >\n          Record List\n        </p>\n        <ul\n          className=\"menu-list\"\n        />\n      </aside>\n    </div>\n    <div\n      className=\"column is-half-mobile scrollable\"\n    >\n      <div\n        className=\"display-box\"\n      >\n        <div\n          className=\"records\"\n        />\n      </div>\n    </div>\n  </div>,\n  <div\n    className=\"is-hidden\"\n  >\n    <div\n      className=\"column is-one-third scrollable\"\n    >\n      <p\n        class=\"control has-icons-left is-flex ml-2\"\n      >\n        <input\n          className=\"input is-small is-primary mt-2\"\n          onChange={[Function]}\n          placeholder=\"Search\"\n          type=\"text\"\n        />\n        <button\n          className=\"button is-small is-link my-2\"\n          onClick={[Function]}\n        >\n          Reset\n        </button>\n        <span\n          class=\"icon is-left mt-2\"\n        >\n          <i\n            class=\"fas fa-search\"\n          />\n        </span>\n      </p>\n      <aside\n        className=\"menu\"\n      >\n        <p\n          className=\"menu-label ml-2\"\n        >\n          Event List\n        </p>\n        <ul\n          className=\"menu-list\"\n        />\n      </aside>\n    </div>\n    <div\n      className=\"column scrollable\"\n    >\n      <div\n        className=\"display-box\"\n      />\n    </div>\n  </div>,\n]\n`;\n"
  },
  {
    "path": "__tests__/__snapshots__/NetworkDisplayer.spec.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`NetworkDisplayer Renders correctly 1`] = `\nArray [\n  <div\n    className=\"column is-one-third scrollable\"\n  >\n    <p\n      class=\"control has-icons-left is-flex ml-2\"\n    >\n      <input\n        className=\"input is-small is-primary mt-2\"\n        onChange={[Function]}\n        placeholder=\"Search\"\n        type=\"text\"\n      />\n      <button\n        className=\"button is-small is-link my-2\"\n        onClick={[Function]}\n      >\n        Reset\n      </button>\n      <span\n        class=\"icon is-left mt-2\"\n      >\n        <i\n          class=\"fas fa-search\"\n        />\n      </span>\n    </p>\n    <aside\n      className=\"menu\"\n    >\n      <p\n        className=\"menu-label ml-2\"\n      >\n        Event List\n      </p>\n      <ul\n        className=\"menu-list\"\n      />\n    </aside>\n  </div>,\n  <div\n    className=\"column scrollable\"\n  >\n    <div\n      className=\"display-box\"\n    />\n  </div>,\n]\n`;\n"
  },
  {
    "path": "__tests__/__snapshots__/StoreDisplayer.spec.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`StoreDisplayer Renders correctly 1`] = `\nArray [\n  <div\n    className=\"column is-half-mobile scrollable\"\n  >\n    <p\n      class=\"control has-icons-left is-flex\"\n    >\n      <input\n        className=\"input is-small is-primary\"\n        onChange={[Function]}\n        placeholder=\"Search\"\n        type=\"text\"\n      />\n      <button\n        className=\"button is-small is-link\"\n        onClick={[Function]}\n      >\n        Reset\n      </button>\n      <span\n        class=\"icon is-left\"\n      >\n        <i\n          class=\"fas fa-search\"\n        />\n      </span>\n    </p>\n    <aside\n      className=\"menu\"\n    >\n      <p\n        className=\"menu-label mt-1\"\n      >\n        Record List\n      </p>\n      <ul\n        className=\"menu-list\"\n      >\n        <li>\n          <a\n            className={false}\n            id=\"type-User\"\n            onClick={[Function]}\n          >\n            User\n          </a>\n          <ul>\n            <li>\n              <a\n                className={false}\n                id=\"id-1\"\n                onClick={[Function]}\n              >\n                1\n              </a>\n            </li>\n            <li>\n              <a\n                className={false}\n                id=\"id-2\"\n                onClick={[Function]}\n              >\n                2\n              </a>\n            </li>\n            <li>\n              <a\n                className={false}\n                id=\"id-3\"\n                onClick={[Function]}\n              >\n                3\n              </a>\n            </li>\n            <li>\n              <a\n                className={false}\n                id=\"id-4\"\n                onClick={[Function]}\n              >\n                4\n              </a>\n            </li>\n          </ul>\n        </li>\n        <li>\n          <a\n            className={false}\n            id=\"type-Post\"\n            onClick={[Function]}\n          >\n            Post\n          </a>\n          <ul>\n            <li>\n              <a\n                className={false}\n                id=\"id-5\"\n                onClick={[Function]}\n              >\n                5\n              </a>\n            </li>\n          </ul>\n        </li>\n      </ul>\n    </aside>\n  </div>,\n  <div\n    className=\"column is-half-mobile scrollable\"\n  >\n    <div\n      className=\"display-box\"\n    >\n      <div\n        className=\"records\"\n      />\n    </div>\n  </div>,\n]\n`;\n"
  },
  {
    "path": "__tests__/__snapshots__/StoreTimeline.spec.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`StoreTimeline Renders correctly 1`] = `\nArray [\n  <div\n    className=\"column is-full-mobile is-one-quarter-desktop\"\n  >\n    <div\n      className=\"display-box\"\n    >\n      <div\n        className=\"snapshot-wrapper is-flex ml-2\"\n      >\n        <input\n          className=\"input is-small snapshot-btn is-primary\"\n          onChange={[Function]}\n          placeholder=\"take a store snapshot\"\n          type=\"text\"\n          value=\"\"\n        />\n        <button\n          className=\"button is-small is-link\"\n          onClick={[Function]}\n        >\n          Snapshot\n        </button>\n      </div>\n    </div>\n    <div\n      className=\"snapshots\"\n    >\n      <div\n        className=\"timeline-nav column is-full-desktop is-flex-mobile\"\n        id=\"timeline-mini-col\"\n      >\n        <div\n          aria-disabled={false}\n          className=\"input-range\"\n          onKeyDown={[Function]}\n          onKeyUp={[Function]}\n          onMouseDown={[Function]}\n          onTouchStart={[Function]}\n        >\n          <span\n            className=\"input-range__label input-range__label--min\"\n          >\n            <span\n              className=\"input-range__label-container\"\n            >\n              0\n            </span>\n          </span>\n          <div\n            className=\"input-range__track input-range__track--background\"\n            onMouseDown={[Function]}\n            onTouchStart={[Function]}\n          >\n            <div\n              className=\"input-range__track input-range__track--active\"\n              style={\n                Object {\n                  \"left\": \"0%\",\n                  \"width\": \"0%\",\n                }\n              }\n            />\n            <span\n              className=\"input-range__slider-container\"\n              style={\n                Object {\n                  \"left\": \"0%\",\n                  \"position\": \"absolute\",\n                }\n              }\n            >\n              <span\n                className=\"input-range__label input-range__label--value\"\n              >\n                <span\n                  className=\"input-range__label-container\"\n                >\n                  0\n                </span>\n              </span>\n              <div\n                aria-valuemax={1}\n                aria-valuemin={0}\n                aria-valuenow={0}\n                className=\"input-range__slider\"\n                draggable=\"false\"\n                onKeyDown={[Function]}\n                onMouseDown={[Function]}\n                onTouchStart={[Function]}\n                role=\"slider\"\n                tabIndex=\"0\"\n              />\n            </span>\n          </div>\n          <span\n            className=\"input-range__label input-range__label--max\"\n          >\n            <span\n              className=\"input-range__label-container\"\n            >\n              1\n            </span>\n          </span>\n        </div>\n        <div\n          className=\"snapshot-nav has-text-centered has-text-right-mobile\"\n        >\n          <button\n            class=\"button is-small is-info is-light\"\n            onClick={[Function]}\n          >\n            <span\n              className=\"icon is-medium\"\n            >\n              <i\n                className=\"fas fa-fast-backward\"\n              />\n            </span>\n          </button>\n          <button\n            class=\"button is-small is-info is-light\"\n            onClick={[Function]}\n          >\n            Current\n          </button>\n          <button\n            class=\"button is-small is-info is-light\"\n            onClick={[Function]}\n          >\n            <span\n              className=\"icon is-medium\"\n            >\n              <i\n                className=\"fas fa-fast-forward\"\n              />\n            </span>\n          </button>\n        </div>\n      </div>\n      <div\n        className=\"snapshot-info is-size-7 column is-full-desktop pt-0\"\n        id=\"snapshot-info-col\"\n      >\n        <div>\n          <aside\n            className=\"menu\"\n          >\n            <ul\n              className=\"menu-list\"\n            >\n              <li\n                onClick={[Function]}\n              >\n                <a\n                  className={false}\n                  href=\"#\"\n                >\n                  12:00:00 AM\n                  : \n                  at startup\n                </a>\n              </li>\n            </ul>\n          </aside>\n        </div>\n      </div>\n    </div>\n  </div>,\n  <div\n    className=\"column is-half-mobile scrollable\"\n  >\n    <p\n      class=\"control has-icons-left is-flex\"\n    >\n      <input\n        className=\"input is-small is-primary\"\n        onChange={[Function]}\n        placeholder=\"Search\"\n        type=\"text\"\n      />\n      <button\n        className=\"button is-small is-link\"\n        onClick={[Function]}\n      >\n        Reset\n      </button>\n      <span\n        class=\"icon is-left\"\n      >\n        <i\n          class=\"fas fa-search\"\n        />\n      </span>\n    </p>\n    <aside\n      className=\"menu\"\n    >\n      <p\n        className=\"menu-label mt-1\"\n      >\n        Record List\n      </p>\n      <ul\n        className=\"menu-list\"\n      />\n    </aside>\n  </div>,\n  <div\n    className=\"column is-half-mobile scrollable\"\n  >\n    <div\n      className=\"display-box\"\n    >\n      <div\n        className=\"records\"\n      />\n    </div>\n  </div>,\n]\n`;\n"
  },
  {
    "path": "__tests__/bridge.spec.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\ndescribe('Bridge', () => {\n  let Bridge;\n\n  beforeEach(() => {\n    Bridge = require('../src/bridge').default;\n  });\n\n  it('should shutdown properly', () => {\n    const wall = {\n      listen: jest.fn(() => () => {}),\n      send: jest.fn()\n    };\n    const bridge = new Bridge(wall);\n\n    // Check that we're wired up correctly.\n    bridge.send('init');\n    jest.runAllTimers();\n    expect(wall.send).toHaveBeenCalledWith('init', undefined, undefined);\n\n    // Should flush pending messages and then shut down.\n    wall.send.mockClear();\n    bridge.send('update', '1');\n    bridge.send('update', '2');\n    bridge.shutdown();\n    jest.runAllTimers();\n    expect(wall.send).toHaveBeenCalledWith('update', '1', undefined);\n    expect(wall.send).toHaveBeenCalledWith('update', '2', undefined);\n    expect(wall.send).toHaveBeenCalledWith('shutdown', undefined, undefined);\n\n    // Verify that the Bridge doesn't send messages after shutdown.\n    spyOn(console, 'warn');\n    wall.send.mockClear();\n    bridge.send('should not send');\n    jest.runAllTimers();\n    expect(wall.send).not.toHaveBeenCalled();\n    expect(console.warn).toHaveBeenCalledWith(\n      'Cannot send message \"should not send\" through a Bridge that has been shutdown.'\n    );\n  });\n});\n"
  },
  {
    "path": "__tests__/global-setup.js",
    "content": "module.exports = async () => {\n  process.env.TZ = 'UTC';\n};\n"
  },
  {
    "path": "__tests__/store.spec.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\ndescribe('Store', () => {\n  let Store;\n  let Bridge;\n\n  beforeEach(() => {\n    Bridge = require('../src/bridge').default;\n    Store = require('../src/devtools/store').default;\n  });\n\n  it('should delete individual records correctly', () => {\n    const wall = {\n      listen: jest.fn(() => () => {}),\n      send: jest.fn()\n    };\n    const bridge = new Bridge(wall);\n    const store = new Store(bridge);\n\n    store.mergeRecords(1, {\n      Bob: {\n        __id: 'Bob',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lisa: {\n        __id: 'Lisa',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    expect(store.getRecords(1)).toEqual({\n      Bob: {\n        __id: 'Bob',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lisa: {\n        __id: 'Lisa',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    store.removeRecord(1, 'Lisa');\n    store.removeRecord(1, 'Bob');\n\n    expect(store.getRecords(1)).toEqual({\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n  });\n\n  it('should merge records correctly', () => {\n    const wall = {\n      listen: jest.fn(() => () => {}),\n      send: jest.fn()\n    };\n    const bridge = new Bridge(wall);\n    const store = new Store(bridge);\n\n    // Testing case when oldRecords is null and we just set the map to the newRecords\n    store.mergeRecords(1, { user: { __id: 'user', __typename: 'User' } });\n\n    expect(store.getRecords(1)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    // Testing case when newRecords is null/undefined and we don't change anything\n    store.mergeRecords(1, null);\n\n    expect(store.getRecords(1)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    store.mergeRecords(1, undefined);\n\n    expect(store.getRecords(1)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    // Testing multiple environments\n    store.mergeRecords(2, { user: { __id: 'user', __typename: 'User' } });\n\n    expect(store.getRecords(1)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    expect(store.getRecords(2)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    // Testing multiple records\n    store.mergeRecords(1, {\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'some_url'\n      }\n    });\n\n    expect(store.getRecords(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'some_url'\n      },\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    //Testing overwriting a record\n    store.mergeRecords(1, {\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      }\n    });\n\n    expect(store.getRecords(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    store.mergeRecords(1, {\n      Bob: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lisa: {\n        __id: 'Lisa',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    expect(store.getRecords(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Bob: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lisa: {\n        __id: 'Lisa',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    expect(store.getRecords(2)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    store.mergeRecords(1, {\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        nickname: 'Zuck'\n      },\n      Bob: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lisa: {\n        __id: 'Lisa',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    expect(store.getRecords(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url',\n        nickname: 'Zuck'\n      },\n      Bob: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lisa: {\n        __id: 'Lisa',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    expect(store.getRecords(2)).toEqual({\n      user: { __id: 'user', __typename: 'User' }\n    });\n\n    // Deleting records\n    store.mergeRecords(1, {\n      Bob: null,\n      Lisa: null,\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n\n    expect(store.getRecords(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url',\n        nickname: 'Zuck'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n  });\n\n  it('should merge optimistic updates correctly', () => {\n    const wall = {\n      listen: jest.fn(() => () => {}),\n      send: jest.fn()\n    };\n    const bridge = new Bridge(wall);\n    const store = new Store(bridge);\n\n    // Testing with a real optimistic source\n    // Testing case when oldRecords is null and we just set the map to the newRecords\n    store.mergeOptimisticRecords(1, {\n      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {\n        'unread_count(bookmark_render_location:\"COMET_LEFT_NAV\")': 0,\n        'unread_count(bookmark_render_location:\"COMET_TOP_TAB\")': 0,\n        'unread_count_string(bookmark_render_location:\"COMET_LEFT_NAV\")': null,\n        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',\n        __typename: 'Bookmark'\n      }\n    });\n\n    expect(store.getOptimisticUpdates(1)).toEqual({\n      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {\n        'unread_count(bookmark_render_location:\"COMET_LEFT_NAV\")': 0,\n        'unread_count(bookmark_render_location:\"COMET_TOP_TAB\")': 0,\n        'unread_count_string(bookmark_render_location:\"COMET_LEFT_NAV\")': null,\n        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',\n        __typename: 'Bookmark'\n      }\n    });\n\n    // Testing case when newRecords is null/undefined and we don't change anything\n    store.mergeOptimisticRecords(1, null);\n    jest.runAllTimers();\n\n    expect(store.getOptimisticUpdates(1)).toEqual({\n      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {\n        'unread_count(bookmark_render_location:\"COMET_LEFT_NAV\")': 0,\n        'unread_count(bookmark_render_location:\"COMET_TOP_TAB\")': 0,\n        'unread_count_string(bookmark_render_location:\"COMET_LEFT_NAV\")': null,\n        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',\n        __typename: 'Bookmark'\n      }\n    });\n\n    store.mergeOptimisticRecords(1, undefined);\n    jest.runAllTimers();\n\n    expect(store.getOptimisticUpdates(1)).toEqual({\n      'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==': {\n        'unread_count(bookmark_render_location:\"COMET_LEFT_NAV\")': 0,\n        'unread_count(bookmark_render_location:\"COMET_TOP_TAB\")': 0,\n        'unread_count_string(bookmark_render_location:\"COMET_LEFT_NAV\")': null,\n        __id: 'Ym9va21hcms6MTAwMDAxNzg1MzU1MDU0OjY0NDcxNTQ0NTY1MDkyNDoyNTAxMDA4NjU3MDg1NDU60g==',\n        __typename: 'Bookmark'\n      }\n    });\n\n    // Removing all optimistic updates\n    // Simulating the store.restore event\n\n    store.clearOptimisticUpdates(1);\n    jest.runAllTimers();\n\n    expect(store.getRecords(1)).toEqual(undefined);\n\n    // Testing multiple records\n    store.mergeOptimisticRecords(1, {\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'some_url'\n      },\n      Lilly: {\n        __id: 'Lilly',\n        __typename: 'User',\n        profile_pic: 'url'\n      }\n    });\n    jest.runAllTimers();\n\n    expect(store.getOptimisticUpdates(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'some_url'\n      },\n      Lilly: {\n        __id: 'Lilly',\n        __typename: 'User',\n        profile_pic: 'url'\n      }\n    });\n\n    //Testing overwriting a record\n    store.mergeOptimisticRecords(1, {\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      }\n    });\n    jest.runAllTimers();\n\n    expect(store.getOptimisticUpdates(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lilly: {\n        __id: 'Lilly',\n        __typename: 'User',\n        profile_pic: 'url'\n      }\n    });\n\n    store.mergeOptimisticRecords(1, {\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        nickname: 'Zuck'\n      },\n      Bob: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n    jest.runAllTimers();\n\n    expect(store.getOptimisticUpdates(1)).toEqual({\n      Jonathan: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url',\n        nickname: 'Zuck'\n      },\n      Bob: {\n        __id: 'Jonathan',\n        __typename: 'User',\n        profile_pic: 'a_different_url'\n      },\n      Lilly: {\n        __id: 'Lilly',\n        __typename: 'User',\n        profile_pic: 'url'\n      },\n      user: {\n        __id: 'user',\n        __typename: 'User',\n        profile_pic: 'new_url'\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "babel.config.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst chromeManifest = require('./shells/browser/chrome/manifest.json');\n\n\nconst minChromeVersion = parseInt(chromeManifest.minimum_chrome_version, 10);\n\nvalidateVersion(minChromeVersion);\n\n\nfunction validateVersion(version) {\n  if (version > 0 && version < 200) {\n    return;\n  }\n  throw new Error('Suspicious browser version in manifest: ' + version);\n}\n\nmodule.exports = api => {\n  const isTest = api.env('test');\n  const targets = {};\n  if (isTest) {\n    targets.node = 'current';\n  } else {\n    targets.chrome = minChromeVersion.toString();\n\n    // This targets RN/Hermes.\n    targets.ie = '11';\n  }\n  const plugins = [\n    ['relay'],\n    ['@babel/plugin-proposal-optional-chaining'],\n    ['@babel/plugin-transform-flow-strip-types'],\n    ['@babel/plugin-proposal-class-properties', { loose: false }],\n  ];\n  if (process.env.NODE_ENV !== 'production') {\n    plugins.push(['@babel/plugin-transform-react-jsx-source']);\n  }\n  return {\n    plugins,\n    presets: [\n      ['@babel/preset-env', { targets }],\n      '@babel/preset-react',\n      '@babel/preset-flow',\n    ],\n  };\n};\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.0'\nservices:\n  test:\n    image: 'protorelay/protostar'\n    container_name: 'protostar-test'\n    volumes: \n      - .:/usr/src/app\n      - node_modules:/usr/src/app/node_modules\n    command: npm run test\nvolumes:\n  node_modules: {}"
  },
  {
    "path": "flow.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\ndeclare module 'events' {\n  declare class EventEmitter<Events: Object> {\n    addListener<Event: $Keys<Events>>(\n      event: Event,\n      listener: (...$ElementType<Events, Event>) => any\n    ): void;\n    emit: <Event: $Keys<Events>>(\n      event: Event,\n      ...$ElementType<Events, Event>\n    ) => void;\n    removeListener(event: $Keys<Events>, listener: Function): void;\n    removeAllListeners(event?: $Keys<Events>): void;\n  }\n\n  declare export default typeof EventEmitter;\n}\n\ndeclare var __DEV__: boolean;\n\ndeclare var jasmine: {|\n  getEnv: () => {|\n    afterEach: (callback: Function) => void,\n    beforeEach: (callback: Function) => void,\n  |},\n|};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"version\": \"1.0.0\",\n  \"name\": \"protostar-relay\",\n  \"repository\": \"oslabs-beta/protostar-relay\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"cross-env NODE_ENV=production node ./shells/browser/chrome/build\",\n    \"test\": \"cross-env TZ=\\\"UTC\\\" jest\",\n    \"install-app\": \"npm i --prefix ./relay-examples/todo/\",\n    \"start-test-app\": \"npm start --prefix ./relay-examples/todo/\",\n    \"watch:chrome:frontend\": \"cross-env NODE_ENV=development node ./shells/browser/chrome/watch\",\n    \"docker-test\": \"docker-compose up\"\n  },\n  \"jest\": {\n    \"verbose\": true,\n    \"testRegex\": \"((\\\\.|/*.)(spec))\\\\.js?$\",\n    \"moduleNameMapper\": {\n      \"\\\\.(css|less)$\": \"<rootDir>/__tests__/__mocks__/styleMock.js\"\n    },\n    \"timers\": \"fake\",\n    \"globalSetup\": \"<rootDir>/__tests__/global-setup.js\"\n  },\n  \"devEngines\": {\n    \"node\": \"10.x || 11.x\"\n  },\n  \"lint-staged\": {\n    \"{shells,src}/**/*.{js,json,css}\": [\n      \"prettier --write\",\n      \"git add\"\n    ],\n    \"**/*.js\": \"eslint --max-warnings 0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.7.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.7.4\",\n    \"@babel/plugin-proposal-optional-chaining\": \"^7.7.5\",\n    \"@babel/plugin-transform-flow-strip-types\": \"^7.7.4\",\n    \"@babel/plugin-transform-react-jsx-source\": \"^7.7.4\",\n    \"@babel/preset-env\": \"^7.7.6\",\n    \"@babel/preset-flow\": \"^7.7.4\",\n    \"@babel/preset-react\": \"^7.7.4\",\n    \"@reach/menu-button\": \"^0.5.4\",\n    \"@reach/tooltip\": \"^0.5.4\",\n    \"archiver\": \"^3.0.0\",\n    \"babel-core\": \"^7.0.0-bridge\",\n    \"babel-eslint\": \"^10.0.3\",\n    \"babel-jest\": \"^24.9.0\",\n    \"babel-loader\": \"^8.0.6\",\n    \"babel-plugin-relay\": \"master\",\n    \"chance\": \"^1.0.18\",\n    \"child-process-promise\": \"^2.2.1\",\n    \"chrome-launch\": \"^1.1.4\",\n    \"classnames\": \"2.2.1\",\n    \"clipboard-js\": \"^0.3.6\",\n    \"cross-env\": \"^6.0.3\",\n    \"crx\": \"^5.0.0\",\n    \"css-loader\": \"^1.0.1\",\n    \"es6-symbol\": \"3.0.2\",\n    \"eslint\": \"^6.6.0\",\n    \"eslint-config-prettier\": \"^6.5.0\",\n    \"eslint-config-react-app\": \"^5.0.2\",\n    \"eslint-plugin-flowtype\": \"^4.3.0\",\n    \"eslint-plugin-import\": \"^2.18.2\",\n    \"eslint-plugin-jsx-a11y\": \"^6.2.3\",\n    \"eslint-plugin-prettier\": \"^3.1.1\",\n    \"eslint-plugin-react\": \"^7.16.0\",\n    \"eslint-plugin-react-hooks\": \"^2.2.0\",\n    \"events\": \"^3.0.0\",\n    \"flow-bin\": \"^0.113.0\",\n    \"fs-extra\": \"^3.0.1\",\n    \"graphql\": \"^14.4.2\",\n    \"jest\": \"^24.9.0\",\n    \"lint-staged\": \"^7.0.5\",\n    \"local-storage-fallback\": \"^4.1.1\",\n    \"lodash.throttle\": \"^4.1.1\",\n    \"log-update\": \"^2.0.0\",\n    \"lru-cache\": \"^4.1.3\",\n    \"nullthrows\": \"^1.0.0\",\n    \"object-assign\": \"4.0.1\",\n    \"opener\": \"^1.5.1\",\n    \"prettier\": \"^1.19.1\",\n    \"prop-types\": \"^15.7.2\",\n    \"react\": \"^0.0.0-50b50c26f\",\n    \"react-dom\": \"^0.0.0-50b50c26f\",\n    \"react-relay\": \"master\",\n    \"react-test-renderer\": \"0.0.0-fec00a869\",\n    \"react-virtualized-auto-sizer\": \"^1.0.2\",\n    \"relay-compiler\": \"master\",\n    \"relay-config\": \"master\",\n    \"rimraf\": \"^2.6.3\",\n    \"sass-loader\": \"^10.0.2\",\n    \"scheduler\": \"^0.0.0-50b50c26f\",\n    \"style-loader\": \"^0.23.1\",\n    \"web-ext\": \"^3.0.0\",\n    \"webpack\": \"^4.41.3\",\n    \"webpack-cli\": \"^3.1.2\",\n    \"webpack-dev-server\": \"^3.10.0\",\n    \"yargs\": \"^14.2.0\"\n  },\n  \"dependencies\": {\n    \"bulma\": \"^0.9.0\",\n    \"enzyme\": \"^3.11.0\",\n    \"enzyme-adapter-react-16\": \"^1.15.4\",\n    \"node-sass\": \"^4.14.1\",\n    \"react-input-range\": \"^1.3.0\",\n    \"sass\": \"^1.26.10\"\n  }\n}\n"
  },
  {
    "path": "shells/browser/chrome/build.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst chalk = require('chalk');\nconst { execSync } = require('child_process');\nconst { existsSync } = require('fs');\nconst { isAbsolute, join, relative } = require('path');\nconst { argv } = require('yargs');\nconst build = require('../shared/build');\n\nconst main = async () => {\n  const { crx, keyPath } = argv;\n\n  if (crx) {\n    if (!keyPath || !existsSync(keyPath)) {\n      console.error('Must specify a key file (.pem) to build CRX');\n      process.exit(1);\n    }\n  }\n\n  await build('chrome');\n\n  if (crx) {\n    const cwd = join(__dirname, 'build');\n\n    let safeKeyPath = keyPath;\n    if (!isAbsolute(keyPath)) {\n      safeKeyPath = join(relative(cwd, process.cwd()), keyPath);\n    }\n\n    execSync(`crx pack ./unpacked -o RelayDevTools.crx -p ${safeKeyPath}`, {\n      cwd,\n    });\n  }\n\n  console.log(chalk.green('\\nThe Chrome extension has been built!'));\n  console.log(chalk.green('You can test this build by running:'));\n  console.log(chalk.gray('\\n# From the relay-devtools root directory:'));\n  console.log('yarn run test:chrome');\n};\n\nmain();\n"
  },
  {
    "path": "shells/browser/chrome/manifest.json",
    "content": "{\n  \"manifest_version\": 2,\n  \"name\": \"Proto Relay\",\n  \"description\": \"Adds Relay debugging tools to the Chrome DevTool panel\",\n  \"version\": \"1.0.0\",\n  \"version_name\": \"1.0.0\",\n  \"update_url\": \"https://github.com/oslabs-beta/protostar-relay\",\n  \"minimum_chrome_version\": \"78\",\n  \"icons\": {\n    \"16\": \"assets/proto16.png\",\n    \"32\": \"assets/proto32.png\",\n    \"48\": \"assets/proto48.png\",\n    \"128\": \"assets/proto128.png\"\n  },\n  \"browser_action\": {\n    \"default_icon\": {\n      \"16\": \"assets/proto16.png\",\n      \"32\": \"assets/proto32.png\",\n      \"48\": \"assets/proto48.png\",\n      \"128\": \"assets/proto128.png\"\n    },\n    \"default_popup\": \"popups/disabled.html\"\n  },\n  \"devtools_page\": \"main.html\",\n  \"content_security_policy\": \"script-src 'self' 'unsafe-eval'; object-src 'self'\",\n  \"web_accessible_resources\": [\n    \"main.html\",\n    \"build/backend.js\",\n    \"build/renderer.js\",\n    \"assets/protorelay.png\"\n  ],\n  \"background\": {\n    \"scripts\": [\n      \"build/background.js\"\n    ],\n    \"persistent\": false\n  },\n  \"permissions\": [\n    \"file:///*\",\n    \"http://*/*\",\n    \"https://*/*\",\n    \"webNavigation\"\n  ],\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"<all_urls>\"\n      ],\n      \"js\": [\n        \"build/injectGlobalHook.js\"\n      ],\n      \"run_at\": \"document_start\"\n    }\n  ]\n}"
  },
  {
    "path": "shells/browser/chrome/nottest.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst chromeLaunch = require('chrome-launch'); // eslint-disable-line import/no-extraneous-dependencies\nconst { resolve } = require('path');\n\nconst EXTENSION_PATH = resolve('shells/browser/chrome/build/unpacked');\nconst START_URL = 'https://facebook.github.io/react/';\n\nchromeLaunch(START_URL, {\n  args: [`--load-extension=${EXTENSION_PATH}`],\n});\n"
  },
  {
    "path": "shells/browser/chrome/now.json",
    "content": "{\n  \"name\": \"relay-devtools-experimental-chrome\",\n  \"alias\": [\"relay-devtools-experimental-chrome\"],\n  \"files\": [\"index.html\", \"RelayDevTools.zip\"]\n}\n"
  },
  {
    "path": "shells/browser/chrome/watch.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst { execSync } = require('child_process');\nconst { join } = require('path');\n\n\nconst webpackPath = join(\n  __dirname,\n  '..',\n  '..',\n  '..',\n  'node_modules',\n  '.bin',\n  'webpack'\n);\nconst binPath = join(__dirname, 'build', 'unpacked', 'build');\nexecSync(\n  `${webpackPath} --config ../shared/webpack.config.js --output-path ${binPath} --watch`,\n  {\n    cwd: join(__dirname, '..', 'shared'),\n    env: process.env,\n    stdio: 'inherit',\n  }\n);\n"
  },
  {
    "path": "shells/browser/shared/build.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst archiver = require('archiver');\nconst { execSync } = require('child_process');\nconst { readFileSync, writeFileSync, createWriteStream } = require('fs');\nconst { copy, ensureDir, move, remove } = require('fs-extra');\nconst { join } = require('path');\nconst { getCommit } = require('../../utils');\n\n// These files are copied along with Webpack-bundled files\n// to produce the final web extension\nconst STATIC_FILES = ['assets', 'main.html', 'index.html'];\n\nconst preProcess = async (destinationPath, tempPath) => {\n  await remove(destinationPath); // Clean up from previously completed builds\n  await remove(tempPath); // Clean up from any previously failed builds\n  await ensureDir(tempPath); // Create temp dir for this new build\n};\n\nconst build = async (tempPath, manifestPath) => {\n  const binPath = join(tempPath, 'bin');\n  const zipPath = join(tempPath, 'zip');\n\n  const webpackPath = join(__dirname, '..', '..', '..', 'node_modules', '.bin', 'webpack');\n  execSync(`${webpackPath} --config webpack.config.js --output-path ${binPath}`, {\n    cwd: __dirname,\n    env: process.env,\n    stdio: 'inherit'\n  });\n  execSync(`${webpackPath} --config webpack.backend.js --output-path ${binPath}`, {\n    cwd: __dirname,\n    env: process.env,\n    stdio: 'inherit'\n  });\n\n  // Make temp dir\n  await ensureDir(zipPath);\n\n  const copiedManifestPath = join(zipPath, 'manifest.json');\n\n  // Copy unbuilt source files to zip dir to be packaged:\n  await copy(binPath, join(zipPath, 'build'));\n  await copy(manifestPath, copiedManifestPath);\n  await Promise.all(STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))));\n\n  const commit = getCommit();\n  const versionDateString = `${commit} (${new Date().toLocaleDateString()})`;\n\n  const manifest = JSON.parse(readFileSync(copiedManifestPath).toString());\n  if (manifest.version_name) {\n    manifest.version_name = versionDateString;\n  } else {\n    manifest.description += `\\n\\nCreated from revision ${versionDateString}`;\n  }\n\n  writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));\n\n  // Pack the extension\n  const archive = archiver('zip', { zlib: { level: 9 } });\n  const zipStream = createWriteStream(join(tempPath, 'RelayDevTools.zip'));\n  await new Promise((resolve, reject) => {\n    archive\n      .directory(zipPath, false)\n      .on('error', err => reject(err))\n      .pipe(zipStream);\n    archive.finalize();\n    zipStream.on('close', () => resolve());\n  });\n};\n\nconst postProcess = async (tempPath, destinationPath) => {\n  const unpackedSourcePath = join(tempPath, 'zip');\n  const packedSourcePath = join(tempPath, 'RelayDevTools.zip');\n  const packedDestPath = join(destinationPath, 'RelayDevTools.zip');\n  const unpackedDestPath = join(destinationPath, 'unpacked');\n\n  await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination\n  await move(packedSourcePath, packedDestPath); // Copy built files to destination\n  await remove(tempPath); // Clean up temp directory and files\n};\n\nconst main = async buildId => {\n  const root = join(__dirname, '..', buildId);\n  const manifestPath = join(root, 'manifest.json');\n  const destinationPath = join(root, 'build');\n\n  try {\n    const tempPath = join(__dirname, 'build', buildId);\n    await preProcess(destinationPath, tempPath);\n    await build(tempPath, manifestPath);\n\n    const builtUnpackedPath = join(destinationPath, 'unpacked');\n    await postProcess(tempPath, destinationPath);\n\n    return builtUnpackedPath;\n  } catch (error) {\n    console.error(error);\n    process.exit(1);\n  }\n\n  return null;\n};\n\nmodule.exports = main;\n"
  },
  {
    "path": "shells/browser/shared/index.html",
    "content": "<!-- @format -->\n\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\" />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://use.fontawesome.com/releases/v5.14.0/css/all.css\"\n      integrity=\"sha384-HzLeBuhoNPvSl5KYnjx0BT+WB0QEEqLprO+NBkkk5gbc67FTaL7XIGa2w1L0Xbgc\"\n      crossorigin=\"anonymous\"\n    />\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"./styles.scss\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <div id=\"container\">Unable to find Relay on the page.</div>\n    <script src=\"./build/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "shells/browser/shared/main.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <script src=\"./build/main.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "shells/browser/shared/src/backend.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\n// Do not use imports or top-level requires here!\n// Running module factories is intentionally delayed until we know the hook exists.\n// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039\n\nfunction welcome(event) {\n  if (event.source !== window || event.data.source !== 'relay-devtools-content-script') {\n    return;\n  }\n\n  window.removeEventListener('message', welcome);\n\n  setup(window.__RELAY_DEVTOOLS_HOOK__);\n}\n\nwindow.addEventListener('message', welcome);\n\nfunction setup(hook) {\n  const Agent = require('src/backend/agent').default;\n  const Bridge = require('src/bridge').default;\n  const { initBackend } = require('src/backend');\n\n  const bridge = new Bridge({\n    listen(fn) {\n      const listener = event => {\n        if (\n          event.source !== window ||\n          !event.data ||\n          event.data.source !== 'relay-devtools-content-script' ||\n          !event.data.payload\n        ) {\n          return;\n        }\n        fn(event.data.payload);\n      };\n      window.addEventListener('message', listener);\n      return () => {\n        window.removeEventListener('message', listener);\n      };\n    },\n    send(event: string, payload: any, transferable?: Array<any>) {\n      window.postMessage(\n        {\n          source: 'relay-devtools-bridge',\n          payload: { event, payload: JSON.parse(JSON.stringify(payload)) }\n        },\n        '*',\n        transferable\n      );\n    }\n  });\n\n  const agent = new Agent(bridge);\n  agent.addListener('shutdown', () => {\n    // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,\n    // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.\n    hook.emit('shutdown');\n  });\n\n  initBackend(hook, agent, window);\n}\n"
  },
  {
    "path": "shells/browser/shared/src/background.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\n/* global chrome */\n\nconst ports = {};\n\nchrome.runtime.onConnect.addListener(function(port) {\n  let tab = null;\n  let name = null;\n  if (isNumeric(port.name)) {\n    tab = port.name;\n    name = 'devtools';\n    installContentScript(+port.name);\n  } else {\n    tab = port.sender.tab.id;\n    name = 'content-script';\n  }\n\n  if (!ports[tab]) {\n    ports[tab] = {\n      devtools: null,\n      'content-script': null\n    };\n  }\n  ports[tab][name] = port;\n\n  if (ports[tab].devtools && ports[tab]['content-script']) {\n    doublePipe(ports[tab].devtools, ports[tab]['content-script']);\n  }\n});\n\nfunction isNumeric(str: string): boolean {\n  return +str + '' === str;\n}\n\nfunction installContentScript(tabId: number) {\n  chrome.tabs.executeScript(tabId, { file: '/build/contentScript.js' }, function() {});\n}\n\nfunction doublePipe(one, two) {\n  one.onMessage.addListener(lOne);\n  function lOne(message) {\n    two.postMessage(message);\n  }\n  two.onMessage.addListener(lTwo);\n  function lTwo(message) {\n    one.postMessage(message);\n  }\n  function shutdown() {\n    one.onMessage.removeListener(lOne);\n    two.onMessage.removeListener(lTwo);\n    one.disconnect();\n    two.disconnect();\n  }\n  one.onDisconnect.addListener(shutdown);\n  two.onDisconnect.addListener(shutdown);\n}\n"
  },
  {
    "path": "shells/browser/shared/src/contentScript.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\n/* global chrome */\n\nlet backendDisconnected: boolean = false;\nlet backendInitialized: boolean = false;\n\nfunction sayHelloToBackend() {\n  window.postMessage(\n    {\n      source: 'relay-devtools-content-script',\n      hello: true\n    },\n    '*'\n  );\n}\n\nfunction handleMessageFromDevtools(message) {\n  window.postMessage(\n    {\n      source: 'relay-devtools-content-script',\n      payload: message\n    },\n    '*'\n  );\n}\n\nfunction handleMessageFromPage(evt) {\n  if (evt.source === window && evt.data && evt.data.source === 'relay-devtools-bridge') {\n    backendInitialized = true;\n\n    port.postMessage(evt.data.payload);\n  }\n}\n\nfunction handleDisconnect() {\n  backendDisconnected = true;\n\n  window.removeEventListener('message', handleMessageFromPage);\n\n  window.postMessage(\n    {\n      source: 'relay-devtools-content-script',\n      payload: {\n        type: 'event',\n        event: 'shutdown'\n      }\n    },\n    '*'\n  );\n}\n\n// proxy from main page to devtools (via the background page)\nvar port = chrome.runtime.connect({\n  name: 'content-script'\n});\nport.onMessage.addListener(handleMessageFromDevtools);\nport.onDisconnect.addListener(handleDisconnect);\n\nwindow.addEventListener('message', handleMessageFromPage);\n\nsayHelloToBackend();\n\n// The backend waits to install the global hook until notified by the content script.\n// In the event of a page reload, the content script might be loaded before the backend is injected.\n// Because of this we need to poll the backend until it has been initialized.\nif (!backendInitialized) {\n  const intervalID = setInterval(() => {\n    if (backendInitialized || backendDisconnected) {\n      clearInterval(intervalID);\n    } else {\n      sayHelloToBackend();\n    }\n  }, 500);\n}\n"
  },
  {
    "path": "shells/browser/shared/src/inject.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/* global chrome */\n\nexport default function inject(scriptName: string, done: ?Function) {\n  const source = `\n  // the prototype stuff is in case document.createElement has been modified\n  (function () {\n    var script = document.constructor.prototype.createElement.call(document, 'script');\n    script.src = \"${scriptName}\";\n    script.charset = \"utf-8\";\n    document.documentElement.appendChild(script);\n    script.parentNode.removeChild(script);\n  })()\n  `;\n\n  chrome.devtools.inspectedWindow.eval(source, function(response, error) {\n    if (error) {\n      console.log(error);\n    }\n\n    if (typeof done === 'function') {\n      done();\n    }\n  });\n}\n"
  },
  {
    "path": "shells/browser/shared/src/injectGlobalHook.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\n/* global chrome */\n\nimport nullthrows from 'nullthrows';\nimport { installHook } from 'src/hook';\n\nfunction injectCode(code) {\n  const script = document.createElement('script');\n  script.textContent = code;\n\n  // This script runs before the <head> element is created,\n  // so we add the script to <html> instead.\n  nullthrows(document.documentElement).appendChild(script);\n  nullthrows(script.parentNode).removeChild(script);\n}\n\nlet lastDetectionResult;\n\n// We want to detect when a renderer attaches, and notify the \"background page\"\n// (which is shared between tabs and can highlight the React icon).\n// Currently we are in \"content script\" context, so we can't listen to the hook directly\n// (it will be injected directly into the page).\n// So instead, the hook will use postMessage() to pass message to us here.\n// And when this happens, we'll send a message to the \"background page\".\nwindow.addEventListener('message', function(evt) {\n  if (evt.source === window && evt.data && evt.data.source === 'relay-devtools-detector') {\n    lastDetectionResult = {\n      hasDetectedReact: true\n    };\n    chrome.runtime.sendMessage(lastDetectionResult);\n  }\n});\n\n// NOTE: Firefox WebExtensions content scripts are still alive and not re-injected\n// while navigating the history to a document that has not been destroyed yet,\n// replay the last detection result if the content script is active and the\n// document has been hidden and shown again.\nwindow.addEventListener('pageshow', function(evt) {\n  if (!lastDetectionResult || evt.target !== window.document) {\n    return;\n  }\n  chrome.runtime.sendMessage(lastDetectionResult);\n});\n\nconst detectRelay = `\nwindow.__RELAY_DEVTOOLS_HOOK__.on('environment', function(evt) {\n  window.postMessage({\n    source: 'relay-devtools-detector',\n  }, '*');\n});\n`;\n\n// Inject a `__RELAY_DEVTOOLS_HOOK__` global so that Relay can detect that the\n// devtools are installed (and skip its suggestion to install the devtools).\ninjectCode(';(' + installHook.toString() + '(window))' + detectRelay);\n"
  },
  {
    "path": "shells/browser/shared/src/main.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/* global chrome */\n\nimport { createElement } from 'react';\nimport { unstable_createRoot as createRoot, flushSync } from 'react-dom';\nimport Bridge from 'src/bridge';\nimport Store from 'src/devtools/store';\nimport inject from './inject';\nimport { createViewElementSource } from './utils';\nimport DevTools from 'src/devtools/DevTools';\n\nlet panelCreated = false;\n\nfunction createPanelIfReactLoaded() {\n  if (panelCreated) {\n    return;\n  }\n\n  chrome.devtools.inspectedWindow.eval(\n    'window.__RELAY_DEVTOOLS_HOOK__ && window.__RELAY_DEVTOOLS_HOOK__.environments.size > 0',\n    (pageHasRelay, error) => {\n      if (!pageHasRelay || panelCreated) {\n        return;\n      }\n\n      panelCreated = true;\n\n      clearInterval(loadCheckInterval);\n\n      let bridge = null;\n      let store = null;\n\n      let cloneStyleTags = null;\n      let render = null;\n      let root = null;\n      let currentPanel = null;\n\n      const tabId = chrome.devtools.inspectedWindow.tabId;\n\n      function initBridgeAndStore() {\n        const port = chrome.runtime.connect({\n          name: '' + tabId\n        });\n        // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,\n        // so it makes no sense to handle it here.\n\n        bridge = new Bridge({\n          listen(fn) {\n            const listener = message => fn(message);\n            // Store the reference so that we unsubscribe from the same object.\n            const portOnMessage = port.onMessage;\n            portOnMessage.addListener(listener);\n            return () => {\n              portOnMessage.removeListener(listener);\n            };\n          },\n          send(event: string, payload: any, transferable?: Array<any>) {\n            port.postMessage({ event, payload }, transferable);\n          }\n        });\n\n        store = new Store(bridge);\n\n        // Initialize the backend only once the Store has been initialized.\n        // Otherwise the Store may miss important initial tree op codes.\n        inject(chrome.runtime.getURL('build/backend.js'));\n\n        const viewElementSourceFunction = createViewElementSource(bridge, store);\n\n        render = () => {\n          console.log('Rendering...');\n          if (root) {\n            root.render(\n              createElement(DevTools, {\n                bridge,\n                // showTabBar: true,\n                store,\n                // viewElementSourceFunction,\n                rootContainer: currentPanel.container\n              })\n            );\n          }\n        };\n\n        render();\n      }\n\n      cloneStyleTags = () => {\n        const linkTags = [];\n        for (const linkTag of document.getElementsByTagName('link')) {\n          if (linkTag.rel === 'stylesheet') {\n            const newLinkTag = document.createElement('link');\n            for (const attribute of linkTag.attributes) {\n              newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);\n            }\n            linkTags.push(newLinkTag);\n          }\n        }\n        return linkTags;\n      };\n\n      initBridgeAndStore();\n\n      function ensureInitialHTMLIsCleared(container) {\n        if (container._hasInitialHTMLBeenCleared) {\n          return;\n        }\n        container.innerHTML = '';\n        container._hasInitialHTMLBeenCleared = true;\n      }\n\n      chrome.devtools.panels.create('proto*', '', 'index.html', panel => {\n        panel.onShown.addListener(listenPanel => {\n          if (currentPanel === listenPanel) {\n            return;\n          }\n          currentPanel = listenPanel;\n\n          if (listenPanel.container != null) {\n            listenPanel.injectStyles(cloneStyleTags);\n            ensureInitialHTMLIsCleared(listenPanel.container);\n            root = createRoot(listenPanel.container);\n            render();\n          }\n        });\n        panel.onHidden.addListener(() => {\n          // TODO: Stop highlighting and stuff.\n        });\n      });\n\n      chrome.devtools.network.onNavigated.removeListener(checkPageForReact);\n\n      // Shutdown bridge before a new page is loaded.\n      chrome.webNavigation.onBeforeNavigate.addListener(function onBeforeNavigate(details) {\n        // Ignore navigation events from other tabs (or from within frames).\n        if (details.tabId !== tabId || details.frameId !== 0) {\n          return;\n        }\n\n        // `bridge.shutdown()` will remove all listeners we added, so we don't have to.\n        bridge.shutdown();\n      });\n\n      // Re-initialize DevTools panel when a new page is loaded.\n      chrome.devtools.network.onNavigated.addListener(function onNavigated() {\n        // It's easiest to recreate the DevTools panel (to clean up potential stale state).\n        // We can revisit this in the future as a small optimization.\n        flushSync(() => {\n          root.unmount(() => {\n            initBridgeAndStore();\n          });\n        });\n      });\n    }\n  );\n}\n\n// Load (or reload) the DevTools extension when the user navigates to a new page.\nfunction checkPageForReact() {\n  createPanelIfReactLoaded();\n}\n\nchrome.devtools.network.onNavigated.addListener(checkPageForReact);\n\n// Check to see if React has loaded once per second in case React is added\n// after page load\nconst loadCheckInterval = setInterval(function() {\n  createPanelIfReactLoaded();\n}, 1000);\n\ncreatePanelIfReactLoaded();\n"
  },
  {
    "path": "shells/browser/shared/src/utils.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/* global chrome */\n\nexport function createViewElementSource(bridge: Bridge, store: Store) {\n  return function viewElementSource(id) {\n    const rendererID = store.getRendererIDForElement(id);\n    if (rendererID != null) {\n      // Ask the renderer interface to determine the component function,\n      // and store it as a global variable on the window\n      bridge.send('viewElementSource', { id, rendererID });\n\n      setTimeout(() => {\n        // Ask Chrome to display the location of the component function,\n        // assuming the renderer found one.\n        chrome.devtools.inspectedWindow.eval(`\n          if (window.$type != null) {\n            inspect(window.$type);\n          }\n        `);\n      }, 100);\n    }\n  };\n}\n"
  },
  {
    "path": "shells/browser/shared/view/App.jsx",
    "content": "/** @format */\nimport React, { useEffect, useState } from 'react';\n\nconst port = chrome.runtime.connect({ name: 'test' });\n\nconst App = () => {\n  const [tree, setTree] = useState();\n  // const [history, setHistory] = useState([]);\n  // const [count, setCount] = useState(1);\n\n  // function is receiving fibernode state changes from backend and is saving that data to tree hook\n  useEffect(() => {\n    port.postMessage({\n      name: 'connect',\n      tabID: chrome.devtools.inspectedWindow.tabId\n    });\n\n    port.onMessage.addListener(message => {\n      if (message.length === 3) {\n        setTree(message);\n      }\n    });\n  }, []);\n  return <div> </div>;\n};\n\nexport default App;\n"
  },
  {
    "path": "shells/browser/shared/view/index.js",
    "content": "/** @format */\n\nimport React from 'react';\nimport { render } from 'react-dom';\n\nimport App from './App.jsx';\nimport styles from './styles.scss';\n\n/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// Portal target container.\nwindow.container = document.getElementById('container');\n\nlet hasInjectedStyles = false;\n\n// DevTools styles are injected into the top-level document head (where the main React app is rendered).\n// This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled.\nwindow.injectStyles = getLinkTags => {\n  if (!hasInjectedStyles) {\n    hasInjectedStyles = true;\n\n    const linkTags = getLinkTags();\n\n    for (const linkTag of linkTags) {\n      document.head.appendChild(linkTag);\n    }\n  }\n};\n\nrender(<App />, document.getElementById('root'));\n"
  },
  {
    "path": "shells/browser/shared/view/styles.scss",
    "content": "/** @format */\n\n//***********************************\n//*********   VARIABLES   ***********\n//***********************************\n\n//***********************************\n//**********   GENERAL   ************\n//***********************************\n\n.button {\n  border-radius: 5px;\n  border-color: gray;\n  border-width: 3px;\n}\n.snapshot-nav button {\n  margin-right: 12px;\n}\n\n#container {\n  padding: 10px 0 0 10px;\n}\n.navigation {\n  padding: 0;\n  overflow: auto;\n}\n.navigation .tabs {\n  margin: 0 0.75em;\n}\nbutton.button.is-small.is-link {\n  margin-left: 10px;\n}\n.column {\n  border-right: 1px solid #e4e0e0;\n  height: 90vh;\n}\n\n#timeline-mini-col {\n  border-right: none !important;\n}\n\n#snapshot-info-col {\n  border-right: none !important;\n}\n\n.scrollable {\n  overflow: scroll;\n}\n\n//***********************************\n//**********    STORE    ************\n//***********************************\n\n.slider-textcolor {\n  color: #060606;\n  font-weight: bold;\n  margin: 20px 0;\n}\n\n//***** MENU *****\n.type {\n  background: lightblue;\n}\n\n.menu-list {\n  width: 100%;\n  font-size: 10px;\n}\n.menu-list a {\n  word-break: break-all;\n}\n.records {\n  width: 100%;\n  margin-left: 2em;\n}\n\n.records:first-child {\n  margin-left: 0em;\n  border-bottom: 1px grey solid;\n}\n\n//***** STORE DISPLAY *****\n.display-box {\n  border-radius: 5px;\n  width: 100%;\n  font-size: 10px;\n}\n\n.key {\n  font-weight: bold;\n  word-wrap: break-word;\n}\n\n.logo img {\n  height: 30px;\n}\n\n.value {\n  word-wrap: break-word;\n}\n\n.snapshots {\n  padding: 0 10px;\n}\n.snapshot-nav {\n  margin-top: 30px;\n}\n.input-range {\n  margin: 35px 0px;\n}\n.tabs.is-toggle li.is-active a {\n  background-color: #00d1b2;\n  border-color: #00d1b2;\n  color: #fff;\n  z-index: 1;\n}\n.input-range__slider {\n  appearance: none;\n  background: #00d1b2;\n  border: 1px solid #0bc3a8;\n  border-radius: 100%;\n  cursor: pointer;\n  display: block;\n  height: 1rem;\n  margin-left: -0.5rem;\n  margin-top: -0.65rem;\n  outline: none;\n  position: absolute;\n  top: 50%;\n  transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;\n  width: 1rem;\n}\n.input-range__slider:active {\n  transform: scale(1.3);\n}\n.input-range__slider:focus {\n  box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2);\n}\n.input-range--disabled .input-range__slider {\n  background: #cccccc;\n  border: 1px solid #cccccc;\n  box-shadow: none;\n  transform: none;\n}\n\n.input-range__slider-container {\n  transition: left 0.3s ease-out;\n}\n\n.input-range__label {\n  color: #aaaaaa;\n  font-family: \"Helvetica Neue\", san-serif;\n  font-size: 0.8rem;\n  transform: translateZ(0);\n  white-space: nowrap;\n}\n\n.input-range__label--min,\n.input-range__label--max {\n  bottom: -1.4rem;\n  position: absolute;\n}\n\n.input-range__label--min {\n  left: 0;\n}\n\n.input-range__label--max {\n  right: 0;\n}\n\n.input-range__label--value {\n  position: absolute;\n  top: -1.8rem;\n}\n\n.input-range__label-container {\n  left: -50%;\n  position: relative;\n}\n.input-range__label--max .input-range__label-container {\n  left: 50%;\n}\n\n.input-range__track {\n  background: #eeeeee;\n  border-radius: 0.3rem;\n  cursor: pointer;\n  display: block;\n  height: 0.3rem;\n  position: relative;\n  transition: left 0.3s ease-out, width 0.3s ease-out;\n}\n.input-range--disabled .input-range__track {\n  background: #eeeeee;\n}\n\n.input-range__track--background {\n  left: 0;\n  margin-top: -0.15rem;\n  position: absolute;\n  right: 0;\n  top: 50%;\n}\n\n.input-range__track--active {\n  background: #3f51b5;\n}\n\n.input-range {\n  height: 1rem;\n  position: relative;\n  width: 100%;\n}\n.type {\n  background: lightblue;\n}\n\n.menu-list {\n  width: 100%;\n}\n\n.record-line {\n  border-bottom: 1px grey solid;\n}\n\n.records {\n  width: 100%;\n  margin-left: 2em;\n}\n\n.records:first-child {\n  margin-left: 0em;\n}\n\n.snapshots .column {\n  height: auto;\n}\n\n@media screen and (max-width: 768px) {\n  .column {\n    height: auto;\n  }\n  .column.is-half-mobile {\n    padding-top: 0;\n  }\n  .snapshots {\n    .column {\n      align-items: center;\n      height: auto;\n    }\n  }\n  .snapshot-nav {\n    margin-top: 0;\n    width: 100%;\n  }\n  .input-range {\n    width: 70%;\n    margin: 25px 0;\n  }\n}\n\n//768 - 1020 beside snapshots get rid of columns and multi-line// added\n\n//issue now between 712ish 988\n// @media screen and (min-width: 769px) and (max-width: 1020px) {\n//   .column {\n//     height: auto;\n//   }\n//   .column.is-half-mobile {\n//     padding-top: 0;\n//   }\n//   .snapshots {\n//     .column {\n//       align-items: center;\n//       height: auto;\n//     }\n//   }\n//   .snapshot-nav {\n//     margin-top: 0;\n//     width: 100%;\n//   }\n//   .input-range {\n//     width: 70%;\n//     margin: 25px 0;\n//   }\n// }\n"
  },
  {
    "path": "shells/browser/shared/webpack.backend.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst { resolve } = require('path');\nconst { DefinePlugin } = require('webpack');\nconst {\n  getGitHubIssuesURL,\n  getGitHubURL,\n  getInternalDevToolsFeedbackGroup,\n  getVersionString\n} = require('../../utils');\n\nconst NODE_ENV = process.env.NODE_ENV;\nif (!NODE_ENV) {\n  console.error('NODE_ENV not set');\n  process.exit(1);\n}\n\nconst __DEV__ = NODE_ENV === 'development';\n\nconst GITHUB_URL = getGitHubURL();\nconst DEVTOOLS_VERSION = getVersionString();\nconst GITHUB_ISSUES_URL = getGitHubIssuesURL();\nconst DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup();\n\nmodule.exports = {\n  mode: __DEV__ ? 'development' : 'production',\n  devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,\n  entry: {\n    backend: './src/backend.js'\n  },\n  output: {\n    path: __dirname + '/build',\n    filename: '[name].js'\n  },\n  resolve: {\n    alias: {\n      src: resolve(__dirname, '../../../src')\n    }\n  },\n  plugins: [\n    new DefinePlugin({\n      __DEV__: true,\n      'process.env.DEVTOOLS_VERSION': `\"${DEVTOOLS_VERSION}\"`,\n      'process.env.GITHUB_URL': `\"${GITHUB_URL}\"`,\n      'process.env.GITHUB_ISSUES_URL': `\"${GITHUB_ISSUES_URL}\"`,\n      'process.env.DEVTOOLS_FEEDBACK_GROUP': `\"${DEVTOOLS_FEEDBACK_GROUP}\"`\n    })\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        options: {\n          configFile: resolve(__dirname, '../../../babel.config.js')\n        }\n      },\n      {\n        test: /.(css|scss)$/,\n        use: ['style-loader', 'css-loader', 'sass-loader']\n      }\n    ]\n  }\n};\n"
  },
  {
    "path": "shells/browser/shared/webpack.config.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst { resolve } = require('path');\nconst { DefinePlugin } = require('webpack');\nconst {\n  getGitHubIssuesURL,\n  getGitHubURL,\n  getInternalDevToolsFeedbackGroup,\n  getVersionString\n} = require('../../utils');\n\nconst NODE_ENV = process.env.NODE_ENV;\nif (!NODE_ENV) {\n  console.error('NODE_ENV not set');\n  process.exit(1);\n}\n\nconst __DEV__ = NODE_ENV === 'development';\n\nconst GITHUB_URL = getGitHubURL();\nconst DEVTOOLS_VERSION = getVersionString();\nconst GITHUB_ISSUES_URL = getGitHubIssuesURL();\nconst DEVTOOLS_FEEDBACK_GROUP = getInternalDevToolsFeedbackGroup();\n\nmodule.exports = {\n  mode: __DEV__ ? 'development' : 'production',\n  devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,\n  entry: {\n    background: './src/background.js',\n    contentScript: './src/contentScript.js',\n    injectGlobalHook: './src/injectGlobalHook.js',\n    index: './view/index.js',\n    main: './src/main.js'\n  },\n  output: {\n    path: __dirname + '/build',\n    filename: '[name].js'\n  },\n  resolve: {\n    alias: {\n      src: resolve(__dirname, '../../../src')\n    }\n  },\n  plugins: [\n    new DefinePlugin({\n      __DEV__: false,\n      'process.env.DEVTOOLS_VERSION': `\"${DEVTOOLS_VERSION}\"`,\n      'process.env.GITHUB_URL': `\"${GITHUB_URL}\"`,\n      'process.env.GITHUB_ISSUES_URL': `\"${GITHUB_ISSUES_URL}\"`,\n      'process.env.DEVTOOLS_FEEDBACK_GROUP': `\"${DEVTOOLS_FEEDBACK_GROUP}\"`\n    })\n  ],\n  module: {\n    rules: [\n      {\n        test: /.jsx?$/,\n        exclude: /node_modules/,\n        loader: 'babel-loader',\n        options: {\n          configFile: resolve(__dirname, '../../../babel.config.js')\n        }\n      },\n      {\n        test: /.(css|scss)$/,\n        use: ['style-loader', 'css-loader', 'sass-loader']\n      }\n    ]\n  }\n};\n"
  },
  {
    "path": "shells/utils.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst { execSync } = require('child_process');\nconst { readFileSync, existsSync } = require('fs');\nconst { resolve } = require('path');\n\nfunction getCommit() {\n  if (existsSync(resolve(__dirname, '../.git'))) {\n    return execSync('git show -s --format=%h')\n      .toString()\n      .trim();\n  }\n  return execSync('hg id -i')\n    .toString()\n    .trim();\n}\n\nfunction getGitHubURL() {\n  return 'https://github.com/relayjs/relay-devtools';\n}\n\nfunction getGitHubIssuesURL() {\n  return 'https://github.com/relayjs/relay-devtools/issues/new';\n}\n\nfunction getInternalDevToolsFeedbackGroup() {\n  return 'https://fburl.com/ieftwi8l';\n}\n\nfunction getVersionString() {\n  const packageVersion = JSON.parse(readFileSync(resolve(__dirname, '../package.json'))).version;\n\n  const commit = getCommit();\n\n  return `${packageVersion}-${commit}`;\n}\n\nmodule.exports = {\n  getCommit,\n  getGitHubIssuesURL,\n  getGitHubURL,\n  getInternalDevToolsFeedbackGroup,\n  getVersionString\n};\n"
  },
  {
    "path": "src/backend/EnvironmentWrapper.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport type { DevToolsHook, RelayEnvironment, EnvironmentWrapper } from './types';\n\nexport function attach(\n  hook: DevToolsHook,\n  rendererID: number,\n  environment: RelayEnvironment,\n  global: Object\n): EnvironmentWrapper {\n  let pendingEventsQueue = [];\n  const store = environment.getStore();\n\n  const originalLog = environment.__log;\n  environment.__log = event => {\n    originalLog(event);\n    // TODO(damassart): Make this a modular function\n    if (pendingEventsQueue !== null) {\n      pendingEventsQueue.push(event);\n    } else {\n      hook.emit('environment.event', {\n        id: rendererID,\n        data: event,\n        eventType: 'environment'\n      });\n    }\n  };\n\n  const storeOriginalLog = store.__log;\n  // TODO(damassart): Make this cleaner\n  store.__log = event => {\n    if (storeOriginalLog !== null) {\n      storeOriginalLog(event);\n    }\n    switch (event.name) {\n      case 'store.gc':\n        // references is a Set, but we can't serialize Sets,\n        // so we convert references to an Array\n        event.references = Array.from(event.references);\n        hook.emit('environment.event', {\n          id: rendererID,\n          data: event,\n          eventType: 'store'\n        });\n        break;\n      case 'store.notify.complete':\n        event.invalidatedRecordIDs = Array.from(event.invalidatedRecordIDs);\n        hook.emit('environment.event', {\n          id: rendererID,\n          data: event,\n          eventType: 'store'\n        });\n        break;\n      default:\n        hook.emit('environment.event', {\n          id: rendererID,\n          data: event,\n          eventType: 'store'\n        });\n        break;\n    }\n  };\n\n  function cleanup() {\n    // We don't patch any methods so there is no cleanup.\n    environment.__log = originalLog;\n    store.__log = storeOriginalLog;\n  }\n\n  function sendStoreRecords() {\n    const records = store.getSource().toJSON();\n    hook.emit('environment.store', {\n      name: 'refresh.store',\n      id: rendererID,\n      records\n    });\n  }\n\n  function flushInitialOperations() {\n    // TODO(damassart): Make this a modular function\n    if (pendingEventsQueue != null) {\n      pendingEventsQueue.forEach(pendingEvent => {\n        hook.emit('environment.event', {\n          id: rendererID,\n          envName: environment.configName,\n          data: pendingEvent,\n          eventType: 'environment'\n        });\n      });\n      pendingEventsQueue = null;\n    }\n    this.sendStoreRecords();\n  }\n\n  return {\n    cleanup,\n    sendStoreRecords,\n    flushInitialOperations\n  };\n}\n"
  },
  {
    "path": "src/backend/agent.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport EventEmitter from 'events';\nimport type { BackendBridge } from 'src/bridge';\n\nimport type { EnvironmentID, EnvironmentWrapper } from './types';\n\nexport default class Agent extends EventEmitter<{|\n  shutdown: [],\n  refreshStore: []\n|}> {\n  _bridge: BackendBridge;\n  _recordChangeDescriptions: boolean = false;\n  _environmentWrappers: {\n    [key: EnvironmentID]: EnvironmentWrapper\n  } = {};\n\n  constructor(bridge: BackendBridge) {\n    super();\n\n    this._bridge = bridge;\n\n    bridge.addListener('shutdown', this.shutdown);\n    bridge.addListener('refreshStore', this.refreshStore);\n  }\n\n  get environmentWrappers(): {\n    [key: EnvironmentID]: EnvironmentWrapper\n  } {\n    return this._environmentWrappers;\n  }\n\n  shutdown = () => {\n    // Clean up the overlay if visible, and associated events.\n    this.emit('shutdown');\n  };\n\n  refreshStore = (id: EnvironmentID) => {\n    const wrapper = this._environmentWrappers[id];\n    wrapper && wrapper.sendStoreRecords();\n  };\n\n  onEnvironmentInitialized = (data: mixed) => {\n    this._bridge.send('environmentInitialized', [data]);\n  };\n\n  setEnvironmentWrapper = (id: number, environmentWrapper: EnvironmentWrapper) => {\n    this._environmentWrappers[id] = environmentWrapper;\n  };\n\n  onStoreData = (data: mixed) => {\n    this._bridge.send('storeRecords', [data]);\n  };\n\n  onEnvironmentEvent = (data: mixed) => {\n    this._bridge.send('events', [data]);\n  };\n}\n"
  },
  {
    "path": "src/backend/index.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport type { DevToolsHook, RelayEnvironment, EnvironmentWrapper } from './types';\nimport type Agent from './agent';\n\nimport { attach } from './EnvironmentWrapper';\n\nexport function initBackend(hook: DevToolsHook, agent: Agent, global: Object): () => void {\n  const subs = [\n    hook.sub('environment.event', data => {\n      agent.onEnvironmentEvent(data);\n    }),\n    hook.sub('environment.store', data => {\n      agent.onStoreData(data);\n    }),\n    hook.sub(\n      'environment-attached',\n      ({\n        id,\n        environment,\n        environmentWrapper\n      }: {\n        id: number,\n        environment: RelayEnvironment,\n        environmentWrapper: EnvironmentWrapper\n      }) => {\n        agent.setEnvironmentWrapper(id, environmentWrapper);\n        agent.onEnvironmentInitialized({\n          id: id,\n          environmentName: environment.configName\n        });\n        // Now that the Store and the renderer interface are connected,\n        // it's time to flush the pending operation codes to the frontend.\n        environmentWrapper.flushInitialOperations();\n      }\n    )\n  ];\n\n  const attachEnvironment = (id: number, environment: RelayEnvironment) => {\n    let environmentWrapper = hook.environmentWrappers.get(id);\n\n    // Inject any not-yet-injected renderers (if we didn't reload-and-profile)\n    if (!environmentWrapper) {\n      environmentWrapper = attach(hook, id, environment, global);\n      hook.environmentWrappers.set(id, environmentWrapper);\n    }\n\n    // Notify the DevTools frontend about new renderers.\n    hook.emit('environment-attached', {\n      id,\n      environment,\n      environmentWrapper\n    });\n  };\n\n  // Connect renderers that have already injected themselves.\n  hook.environments.forEach((environment, id) => {\n    attachEnvironment(id, environment);\n  });\n\n  // Connect any new renderers that injected themselves.\n  subs.push(\n    hook.sub(\n      'environment',\n      ({ id, environment }: { id: number, environment: RelayEnvironment }) => {\n        attachEnvironment(id, environment);\n      }\n    )\n  );\n\n  return () => {\n    subs.forEach(fn => fn());\n  };\n}\n"
  },
  {
    "path": "src/backend/types.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nexport type EnvironmentID = number;\n\nexport type RelayRecordSource = {\n  getRecordIDs: () => string,\n  get: (id: string) => any,\n  toJSON: () => any\n};\n\nexport type RelayStore = {\n  getSource: () => RelayRecordSource,\n  __log: (event: Object) => void\n};\n\nexport type RelayEnvironment = {\n  execute: (options: any) => any,\n  configName: ?string,\n  getStore: () => RelayStore,\n  __log: (event: Object) => void\n};\n\nexport type EnvironmentWrapper = {\n  flushInitialOperations: () => void,\n  sendStoreRecords: () => void,\n  cleanup: () => void\n};\n\nexport type Handler = (data: any) => void;\n\nexport type DevToolsHook = {\n  registerEnvironment: (env: RelayEnvironment) => number | null,\n  // listeners: { [key: string]: Array<Handler> },\n  environmentWrappers: Map<EnvironmentID, EnvironmentWrapper>,\n  environments: Map<EnvironmentID, RelayEnvironment>,\n\n  emit: (event: string, data: any) => void,\n  on: (event: string, handler: Handler) => void,\n  off: (event: string, handler: Handler) => void,\n  // reactDevtoolsAgent?: ?Object,\n  sub: (event: string, handler: Handler) => () => void\n};\n"
  },
  {
    "path": "src/backend/utils.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nexport function copyWithSet(\n  obj: Object | Array<any>,\n  path: Array<number | string>,\n  value: any,\n  index: number = 0\n): Object | Array<any> {\n  console.log('[utils] copyWithSet()', obj, path, index, value);\n  if (index >= path.length) {\n    return value;\n  }\n  const key = parseInt(path[index]);\n  const updated = Array.isArray(obj) ? obj.slice() : { ...obj };\n  updated[key] = copyWithSet(obj[key], path, value, index + 1);\n  return updated;\n}\n"
  },
  {
    "path": "src/bridge.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport EventEmitter from 'events';\n\nimport type { EnvironmentInfo, EventData, StoreData, Wall } from './types';\n\nconst BATCH_DURATION = 100;\n\ntype Message = {|\n  event: string,\n  payload: any\n|};\n\ntype BackendEvents = {|\n  events: [Array<EventData>],\n  shutdown: [],\n  environmentInitialized: [Array<EnvironmentInfo>],\n  storeRecords: [Array<StoreData>]\n|};\n\ntype FrontendEvents = {|\n  refreshStore: [number]\n|};\nclass Bridge<OutgoingEvents: Object, IncomingEvents: Object> extends EventEmitter<{|\n  ...IncomingEvents,\n  ...OutgoingEvents\n|}> {\n  _isShutdown: boolean = false;\n  _messageQueue: Array<any> = [];\n  _timeoutID: TimeoutID | null = null;\n  _wall: Wall;\n  _wallUnlisten: Function | null = null;\n\n  constructor(wall: Wall) {\n    super();\n\n    this._wall = wall;\n\n    this._wallUnlisten =\n      wall.listen((message: Message) => {\n        (this: any).emit(message.event, message.payload);\n      }) || null;\n  }\n\n  send(event: string, payload: any, transferable?: Array<any>) {\n    if (this._isShutdown) {\n      console.warn(`Cannot send message \"${event}\" through a Bridge that has been shutdown.`);\n      return;\n    }\n\n    // When we receive a message:\n    // - we add it to our queue of messages to be sent\n    // - if there hasn't been a message recently, we set a timer for 0 ms in\n    //   the future, allowing all messages created in the same tick to be sent\n    //   together\n    // - if there *has* been a message flushed in the last BATCH_DURATION ms\n    //   (or we're waiting for our setTimeout-0 to fire), then _timeoutID will\n    //   be set, and we'll simply add to the queue and wait for that\n    this._messageQueue.push(event, payload, transferable);\n    if (!this._timeoutID) {\n      this._timeoutID = setTimeout(this._flush, 0);\n    }\n  }\n\n  shutdown() {\n    if (this._isShutdown) {\n      console.warn('Bridge was already shutdown.');\n      return;\n    }\n\n    // Queue the shutdown outgoing message for subscribers.\n    this.send('shutdown');\n\n    // Mark this bridge as destroyed, i.e. disable its public API.\n    this._isShutdown = true;\n\n    // Disable the API inherited from EventEmitter that can add more listeners and send more messages.\n    (this: any).addListener = function() {};\n    this.emit = function() {};\n    // NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter.\n\n    // Unsubscribe this bridge incoming message listeners to be sure, and so they don't have to do that.\n    this.removeAllListeners();\n\n    // Stop accepting and emitting incoming messages from the wall.\n    const wallUnlisten = this._wallUnlisten;\n    if (wallUnlisten) {\n      wallUnlisten();\n    }\n\n    // Synchronously flush all queued outgoing messages.\n    // At this step the subscribers' code may run in this call stack.\n    do {\n      this._flush();\n    } while (this._messageQueue.length);\n\n    // Make sure once again that there is no dangling timer.\n    clearTimeout(this._timeoutID);\n    this._timeoutID = null;\n  }\n\n  _flush = () => {\n    // This method is used after the bridge is marked as destroyed in shutdown sequence,\n    // so we do not bail out if the bridge marked as destroyed.\n    // It is a private method that the bridge ensures is only called at the right times.\n\n    clearTimeout(this._timeoutID);\n    this._timeoutID = null;\n\n    if (this._messageQueue.length) {\n      for (let i = 0; i < this._messageQueue.length; i += 3) {\n        this._wall.send(\n          this._messageQueue[i],\n          this._messageQueue[i + 1],\n          this._messageQueue[i + 2]\n        );\n      }\n      this._messageQueue.length = 0;\n\n      // Check again for queued messages in BATCH_DURATION ms. This will keep\n      // flushing in a loop as long as messages continue to be added. Once no\n      // more are, the timer expires.\n      this._timeoutID = setTimeout(this._flush, BATCH_DURATION);\n    }\n  };\n}\n\nexport type BackendBridge = Bridge<BackendEvents, FrontendEvents>;\nexport type FrontendBridge = Bridge<FrontendEvents, BackendEvents>;\n\nexport default Bridge;\n"
  },
  {
    "path": "src/devtools/DevTools.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\n// Reach styles need to come before any component styles.\n// This makes overridding the styles simpler.\n\nimport React, { useState, useCallback, useEffect } from 'react';\nimport type { FrontendBridge } from 'src/bridge';\nimport Store from './store';\nimport { BridgeContext, StoreContext } from './context';\nimport NetworkDisplayer from './view/NetworkDisplayer';\nimport StoreTimeline from './view/StoreTimeline';\n\n// export type TabID = 'network' | 'settings' | 'store-inspector';\nexport type ViewElementSource = (id: number) => void;\n\nexport type Props = {|\n  bridge: FrontendBridge,\n  // defaultTab?: TabID,\n  // showTabBar?: boolean,\n  store: Store,\n  viewElementSourceFunction?: ?ViewElementSource,\n  viewElementSourceRequiresFileLocation?: boolean,\n\n  // This property is used only by the web extension target.\n  // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.\n  // This is done to save space within the app.\n  // Because of this, the extension needs to be able to change which tab is active/rendered.\n  // overrideTab?: TabID,\n\n  // TODO: Cleanup multi-tabs in webextensions\n  // To avoid potential multi-root trickiness, the web extension uses portals to render tabs.\n  // The root <DevTools> app is rendered in the top-level extension window,\n  // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.\n  rootContainer?: Element,\n  // networkPortalContainer?: Element,\n  settingsPortalContainer?: Element,\n  storeInspectorPortalContainer?: Element\n|};\n\nconst networkTab = {\n  id: ('network': TabID),\n  icon: 'network',\n  label: 'Network',\n  title: 'Relay Network'\n};\nconst storeInspectorTab = {\n  id: ('store-inspector': TabID),\n  icon: 'store-inspector',\n  label: 'Store',\n  title: 'Relay Store'\n};\n\nconst tabs = [networkTab, storeInspectorTab];\n\nexport default function DevTools({\n  bridge,\n  rootContainer,\n  networkPortalContainer,\n  storeInspectorPortalContainer,\n  settingsPortalContainer,\n  store,\n  viewElementSourceFunction,\n  viewElementSourceRequiresFileLocation = false\n}: Props) {\n  const [environmentIDs, setEnvironmentIDs] = useState(store.getEnvironmentIDs());\n  const [currentEnvID, setCurrentEnvID] = useState(environmentIDs[0]);\n  const [selector, setSelector] = useState('Store');\n\n  const setEnv = useCallback(() => {\n    const ids = store.getEnvironmentIDs();\n    if (currentEnvID === undefined) {\n      const firstKey = ids[0];\n      setCurrentEnvID(firstKey);\n    }\n    setEnvironmentIDs(ids);\n  }, [store, currentEnvID]);\n\n  useEffect(() => {\n    setEnv();\n    store.addListener('environmentInitialized', setEnv);\n    return () => {\n      store.removeListener('environmentInitialized', setEnv);\n    };\n  }, [store, setEnv]);\n\n  function handleTabClick(e, tab) {\n    setSelector(tab);\n  }\n\n  const handleChange = useCallback(e => {\n    setCurrentEnvID(parseInt(e.target.value));\n  }, []);\n\n  console.log('currentenvid before render', currentEnvID);\n\n  return (\n    <BridgeContext.Provider value={bridge}>\n      <StoreContext.Provider value={store}>\n        <div className=\"navigation\">\n          <form className=\"env-select select is-small is-pulled-left\">\n            <select className=\"env-select\" onChange={handleChange}>\n              {environmentIDs.map(id => {\n                return (\n                  <option key={id} value={id}>\n                    {store.getEnvironmentName(id) || id}\n                  </option>\n                );\n              })}\n            </select>\n          </form>\n          <div className=\"tabs is-toggle is-small is-pulled-left\">\n            <ul>\n              <li className={selector === 'Store' && 'is-active'}>\n                <a id=\"storeSelector\" onClick={e => handleTabClick(e, 'Store')}>\n                  <span className=\"icon is-small\">\n                    <i className=\"fas fa-database\"></i>\n                  </span>\n                  <span>Store</span>\n                </a>\n              </li>\n              <li className={selector === 'Network' && 'is-active'}>\n                <a\n                  id=\"networkSelector\"\n                  onClick={e => {\n                    handleTabClick(e, 'Network');\n                  }}\n                >\n                  <span className=\"icon is-small\">\n                    <i className=\"fas fa-network-wired\"></i>\n                  </span>\n                  <span>Network</span>\n                </a>\n              </li>\n            </ul>\n          </div>\n          <div className=\"logo is-pulled-right\">\n            <a href=\"https://github.com/oslabs-beta/protostar-relay\" target=\"_blank\">\n              <img src=\"../../assets/protorelay.png\"></img>\n            </a>\n          </div>\n        </div>\n        <div className={selector === 'Store' ? 'columns mb-0 is-multiline is-mobile' : 'is-hidden'}>\n          {currentEnvID && (\n            <StoreTimeline\n              currentEnvID={currentEnvID}\n              portalContainer={storeInspectorPortalContainer}\n            />\n          )}\n        </div>\n        <div className={selector === 'Network' ? 'columns mb-0 is-mobile' : 'is-hidden'}>\n          {currentEnvID && <NetworkDisplayer currentEnvID={currentEnvID} />}\n        </div>\n      </StoreContext.Provider>\n    </BridgeContext.Provider>\n  );\n}\n"
  },
  {
    "path": "src/devtools/context.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport { createContext } from 'react';\n\nimport type { FrontendBridge } from 'src/bridge';\nimport type Store from 'store';\n\nexport const BridgeContext = createContext<FrontendBridge>(((null: any): FrontendBridge));\nBridgeContext.displayName = 'BridgeContext';\n\nexport const StoreContext = createContext<Store>(((null: any): Store));\nStoreContext.displayName = 'StoreContext';\n"
  },
  {
    "path": "src/devtools/store.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\nconst __DEBUG__ = true;\n\nimport EventEmitter from 'events';\nimport type { FrontendBridge } from 'src/bridge';\nimport type {\n  DataID,\n  LogEvent,\n  EventData,\n  EnvironmentInfo,\n  StoreData,\n  StoreRecords,\n  Record\n} from '../types';\n\nconst debug = (methodName, ...args) => {\n  if (__DEBUG__) {\n    console.log(\n      `%cStore %c${methodName}`,\n      'color: green; font-weight: bold;',\n      'font-weight: bold;',\n      ...args\n    );\n  }\n};\n\nconst storeEventNames = [\n  'queryresource.fetch',\n  'store.publish',\n  'store.restore',\n  'store.gc',\n  'store.snapshot',\n  'store.notify.complete',\n  'store.notify.start'\n];\n\n/**\n * The store is the single source of truth for updates from the backend.\n * ContextProviders can subscribe to the Store for specific things they want to provide.\n */\nexport default class Store extends EventEmitter<{|\n  collapseNodesByDefault: [],\n  componentFilters: [],\n  environmentInitialized: [],\n  mutated: [],\n  storeDataReceived: [],\n  allEventsReceived: [],\n  recordChangeDescriptions: [],\n  roots: []\n|}> {\n  _bridge: FrontendBridge;\n\n  _environmentEventsMap: Map<number, Array<LogEvent>> = new Map();\n  _environmentNames: Map<number, string> = new Map();\n  _environmentStoreData: Map<number, StoreRecords> = new Map();\n  _environmentStoreOptimisticData: Map<number, StoreRecords> = new Map();\n  _environmentAllEvents: Map<number, Array<LogEvent>> = new Map();\n  _recordedRequests: Map<number, Map<number, LogEvent>> = new Map();\n  _isRecording: boolean = false;\n  _importEnvID: ?number = null;\n\n  constructor(bridge: FrontendBridge) {\n    super();\n    this._bridge = bridge;\n    bridge.addListener('events', this.onBridgeEvents);\n    bridge.addListener('shutdown', this.onBridgeShutdown);\n    bridge.addListener('environmentInitialized', this.onBridgeEnvironmentInit);\n    bridge.addListener('storeRecords', this.onBridgeStoreSnapshot);\n    bridge.addListener('mutationComplete', this.setEnvironmentEvents);\n    bridge.addListener('all', this.setEnvironmentEvents);\n  }\n\n  getAllEventsArray(): $ReadOnlyArray<LogEvent> {\n    const allEvents = [];\n    this._environmentAllEvents.forEach((value, _) => allEvents.push(...value));\n    return allEvents;\n  }\n\n  setAllEventsMap(environmentID: number, events: Array<LogEvent>) {\n    this._environmentAllEvents.set(environmentID, events);\n    this.emit('allEventsReceived');\n  }\n\n  getAllEventsMap(): Map<number, Array<LogEvent>> {\n    return this._environmentAllEvents;\n  }\n\n  getEvents(environmentID: number): ?$ReadOnlyArray<LogEvent> {\n    return this._environmentAllEvents.get(environmentID);\n  }\n\n  getAllEnvironmentEvents(): $ReadOnlyArray<LogEvent> {\n    const allEnvironmentEvents = [];\n    this._environmentEventsMap.forEach((value, _) => allEnvironmentEvents.push(...value));\n    return allEnvironmentEvents;\n  }\n\n  getEnvironmentEvents(environmentID: number): ?$ReadOnlyArray<LogEvent> {\n    return this._environmentEventsMap.get(environmentID);\n  }\n\n  getEnvironmentIDs(): $ReadOnlyArray<number> {\n    return Array.from(this._environmentNames.keys());\n  }\n\n  getImportEnvID(): ?number {\n    return this._importEnvID;\n  }\n\n  setImportEnvID(envID: ?number) {\n    this._importEnvID = envID;\n    this.emit('allEventsReceived');\n  }\n\n  getEnvironmentName(environmentID: number): ?string {\n    return this._environmentNames.get(environmentID);\n  }\n\n  getRecords(environmentID: number): ?StoreRecords {\n    return this._environmentStoreData.get(environmentID);\n  }\n\n  getRecordIDs(environmentID: number): ?$ReadOnlyArray<string> {\n    const storeRecords = this._environmentStoreData.get(environmentID);\n    return storeRecords ? Object.keys(storeRecords) : null;\n  }\n\n  removeRecord(environmentID: number, recordID: string) {\n    const storeRecords = this._environmentStoreData.get(environmentID);\n    if (storeRecords != null) {\n      delete storeRecords[recordID];\n    }\n  }\n\n  getAllRecords(): ?$ReadOnlyArray<StoreRecords> {\n    return Array.from(this._environmentStoreData.values());\n  }\n\n  getOptimisticUpdates(environmentID: number): ?StoreRecords {\n    return this._environmentStoreOptimisticData.get(environmentID);\n  }\n\n  mergeRecords(id: number, newRecords: ?StoreRecords) {\n    if (newRecords == null) {\n      return;\n    }\n    const oldRecords = this._environmentStoreData.get(id);\n    if (oldRecords == null) {\n      this._environmentStoreData.set(id, newRecords);\n      return;\n    }\n    const dataIDs = Object.keys(newRecords);\n\n    for (let ii = 0; ii < dataIDs.length; ii++) {\n      const dataID = dataIDs[ii];\n      const oldRecord = oldRecords[dataID];\n      const newRecord = newRecords[dataID];\n      if (oldRecord && newRecord) {\n        let updated: Record | null = null;\n        const keys = Object.keys(newRecord);\n        for (let iii = 0; iii < keys.length; iii++) {\n          const key = keys[iii];\n          if (updated || oldRecord[key] !== newRecord[key]) {\n            updated = updated !== null ? updated : { ...oldRecord };\n            updated[key] = newRecord[key];\n          }\n        }\n        updated = updated !== null ? updated : oldRecord;\n        if (updated !== newRecord) {\n          oldRecords[dataID] = updated;\n        }\n      } else if (oldRecord == null) {\n        oldRecords[dataID] = newRecord;\n      } else if (newRecord == null) {\n        delete oldRecords[dataID];\n      }\n    }\n    this._environmentStoreData.set(id, oldRecords);\n  }\n\n  mergeOptimisticRecords(id: number, newRecords: ?StoreRecords) {\n    if (newRecords == null) {\n      return;\n    }\n    const oldRecords = this._environmentStoreOptimisticData.get(id);\n    if (oldRecords == null) {\n      this._environmentStoreOptimisticData.set(id, newRecords);\n      return;\n    }\n    const dataIDs = Object.keys(newRecords);\n\n    for (let ii = 0; ii < dataIDs.length; ii++) {\n      const dataID = dataIDs[ii];\n      const oldRecord = oldRecords[dataID];\n      const newRecord = newRecords[dataID];\n      if (oldRecord && newRecord) {\n        let updated: Record | null = null;\n        const keys = Object.keys(newRecord);\n        for (let iii = 0; iii < keys.length; iii++) {\n          const key = keys[iii];\n          if (updated || oldRecord[key] !== newRecord[key]) {\n            updated = updated !== null ? updated : { ...oldRecord };\n            updated[key] = newRecord[key];\n          }\n        }\n        updated = updated !== null ? updated : oldRecord;\n        if (updated !== newRecord) {\n          oldRecords[dataID] = updated;\n        }\n      } else if (oldRecord == null) {\n        oldRecords[dataID] = newRecord;\n      } else if (newRecord == null) {\n        delete oldRecords[dataID];\n      }\n    }\n    this._environmentStoreOptimisticData.set(id, oldRecords);\n  }\n\n  onBridgeStoreSnapshot = (data: Array<StoreData>) => {\n    for (const { id, records } of data) {\n      this._environmentStoreData.set(id, records);\n      this.emit('storeDataReceived');\n    }\n  };\n\n  setStoreEvents = (id: number, data: LogEvent) => {\n    switch (data.name) {\n      case 'store.publish':\n        this.mergeRecords(id, data.source);\n        if (data.optimistic) {\n          this.mergeOptimisticRecords(id, data.source);\n        }\n        break;\n      case 'store.restore':\n        this.clearOptimisticUpdates(id);\n        break;\n      case 'store.gc':\n        this.garbageCollectRecords(id, data.references);\n        break;\n      default:\n        break;\n    }\n    this.emit('storeDataReceived');\n  };\n\n  setEnvironmentEvents = (id: number, data: LogEvent) => {\n    const arr = this._environmentEventsMap.get(id);\n    if (arr) {\n      arr.push(data);\n    } else {\n      this._environmentEventsMap.set(id, [data]);\n    }\n    this.emit('mutated');\n    if (data.name === 'execute.complete') {\n      this.emit('mutationComplete');\n    }\n  };\n\n  appendInformationToRequest = (id: number, data: LogEvent) => {\n    switch (data.name) {\n      case 'execute.start':\n        const requestArr = this._recordedRequests.get(id);\n        if (requestArr) {\n          requestArr.set(data.transactionID, data);\n        } else {\n          const newRequest = new Map<number, LogEvent>();\n          newRequest.set(data.transactionID, data);\n          this._recordedRequests.set(id, newRequest);\n        }\n        break;\n      case 'execute.next':\n      case 'execute.info':\n      case 'execute.complete':\n      case 'execute.error':\n      case 'execute.unsubscribe':\n        const requests = this._recordedRequests.get(id);\n        if (requests) {\n          const request = requests.get(data.transactionID);\n          if (request && request.name === 'execute.start') {\n            data.params = request.params;\n            data.variables = request.variables;\n          }\n        }\n        break;\n      default:\n        break;\n    }\n  };\n\n  startRecording = () => {\n    this._isRecording = true;\n    this.clearAllEvents();\n  };\n\n  stopRecording = () => {\n    this._isRecording = false;\n  };\n\n  onBridgeEvents = (events: Array<EventData>) => {\n    for (const { id, data, eventType } of events) {\n      if (this._isRecording) {\n        const allEvents = this._environmentAllEvents.get(id);\n        if (allEvents) {\n          if (data.name === 'store.gc') {\n            const records = this.getRecords(id);\n            if (records != null) {\n              data.gcRecords = {};\n              data.references = Object.keys(records)\n                .filter(recID => recID != null && !data.references.includes(recID))\n                .map(recID => {\n                  data.gcRecords[recID] = records[recID];\n                  return recID;\n                });\n            }\n          } else if (data.name === 'store.notify.complete') {\n            const records = this.getRecords(id);\n            if (records != null) {\n              data.invalidatedRecords = {};\n              data.updatedRecords = {};\n              Object.keys(data.updatedRecordIDs).forEach(recID => {\n                data.updatedRecords[recID] = { ...records[recID] };\n              });\n              data.invalidatedRecordIDs.forEach(\n                recID => (data.invalidatedRecords[recID] = { ...records[recID] })\n              );\n            }\n          } else if (data.name.startsWith('execute')) {\n            this.appendInformationToRequest(id, data);\n          }\n          allEvents.push(data);\n        } else {\n          this._environmentAllEvents.set(id, [data]);\n        }\n        this.emit('allEventsReceived');\n      }\n      if (eventType === 'store') {\n        this.setStoreEvents(id, data);\n      } else if (eventType === 'environment') {\n        this.setEnvironmentEvents(id, data);\n      }\n    }\n  };\n\n  onBridgeEnvironmentInit = (data: Array<EnvironmentInfo>) => {\n    for (const { id, environmentName } of data) {\n      this._environmentNames.set(id, environmentName);\n    }\n    this.emit('environmentInitialized');\n  };\n\n  clearOptimisticUpdates = (envID: number) => {\n    this._environmentStoreOptimisticData.delete(envID);\n  };\n\n  garbageCollectRecords = (envID: number, references: $ReadOnlyArray<DataID>) => {\n    if (references.length === 0) {\n      this._environmentStoreData.delete(envID);\n    } else {\n      const storeIDs = this.getRecordIDs(envID);\n      if (storeIDs == null) {\n        return;\n      }\n      for (const dataID of storeIDs) {\n        if (!references.includes(dataID)) {\n          this.removeRecord(envID, dataID);\n        }\n      }\n    }\n  };\n\n  clearAllEvents = () => {\n    this._environmentAllEvents.forEach((_, key) => this.clearEvents(key));\n    this.emit('allEventsReceived');\n  };\n\n  clearEvents = (environmentID: number) => {\n    this._environmentAllEvents.delete(environmentID);\n  };\n\n  clearAllNetworkEvents = () => {\n    this._environmentEventsMap.forEach((_, key) => this.clearNetworkEvents(key));\n    this.emit('mutated');\n  };\n\n  clearNetworkEvents = (environmentID: number) => {\n    const completed = new Set();\n    let networkEventArray = this._environmentEventsMap.get(environmentID);\n    if (networkEventArray !== undefined && networkEventArray.length > 0) {\n      for (const event of networkEventArray) {\n        if (\n          event.name === 'execute.complete' ||\n          event.name === 'execute.error' ||\n          event.name === 'execute.unsubscribe'\n        ) {\n          completed.add(event.transactionID);\n        }\n      }\n      networkEventArray = networkEventArray.filter(\n        event =>\n          storeEventNames.includes(event.name) &&\n          event.transactionID != null &&\n          !completed.has(event.transactionID)\n      );\n      this._environmentEventsMap.set(environmentID, networkEventArray);\n      this.emit('mutated');\n    }\n  };\n\n  onBridgeShutdown = () => {\n    if (__DEBUG__) {\n      debug('onBridgeShutdown', 'unsubscribing from Bridge');\n    }\n\n    this._bridge.removeListener('events', this.onBridgeEvents);\n    this._bridge.removeListener('shutdown', this.onBridgeShutdown);\n    this._bridge.removeListener('environmentInitialized', this.onBridgeEnvironmentInit);\n    this._bridge.removeListener('storeRecords', this.onBridgeStoreSnapshot);\n  };\n}\n"
  },
  {
    "path": "src/devtools/utils.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nexport function deepCopyFunction(inObject: any) {\n  if (typeof inObject !== 'object' || inObject === null) {\n    return inObject;\n  }\n\n  if (Array.isArray(inObject)) {\n    const outObject = [];\n    for (let i = 0; i < inObject.length; i++) {\n      const value = inObject[i];\n      outObject[i] = deepCopyFunction(value);\n    }\n    return outObject;\n  } else if (inObject instanceof Map) {\n    const outObject = new Map<mixed, mixed>();\n    inObject.forEach((val, key) => {\n      outObject.set(key, deepCopyFunction(val));\n    });\n    return outObject;\n  } else {\n    const outObject = {};\n    for (const key in inObject) {\n      const value = inObject[key];\n      if (typeof key === 'string' && key != null) {\n        outObject[key] = deepCopyFunction(value);\n      }\n    }\n    return outObject;\n  }\n}\n\nexport function debounce(func, wait) {\n  let timeout = null;\n  return function() {\n    const newfunc = () => {\n      timeout = null;\n      func.apply(this, arguments);\n    };\n    clearTimeout(timeout);\n    timeout = setTimeout(newfunc, wait);\n  };\n}\n"
  },
  {
    "path": "src/devtools/view/Components/EnvironmentSelector.js",
    "content": "import React, { useState, useCallback, useEffect } from 'react';\n\nfunction EnvironmentSelector(props) {\n  const [selectEnv, setSelectEnv] = useState('');\n\n  return (\n    <form className=\"env-select\">\n      <select name=\"environment\">{dropdownEnv}</select>\n    </form>\n  );\n}\n\nexport default EnvironmentSelector;\n"
  },
  {
    "path": "src/devtools/view/Components/Record.js",
    "content": "import React from 'react';\n\nfunction Record(props) {\n  /* Maps through array and recursively calls component if the props[value] is an object, \n  otherwise, it will store a key/value pair */\n  const records = Object.keys(props).map(key => {\n    return typeof props[key] === 'object' ? (\n      <div className=\"nestedObject\" key={key}>\n        <span className=\"key\">{key}: </span>\n        <Record {...props[key]} />\n      </div>\n    ) : (\n      <div className=\"objectProperty\" key={key}>\n        <span className=\"key\">{key}: </span>\n        <span className=\"value\">{JSON.stringify(props[key])}</span>\n      </div>\n    );\n  });\n\n  return <div className=\"records\">{records}</div>;\n}\n\nexport default Record;\n"
  },
  {
    "path": "src/devtools/view/Components/SnapshotLinks.js",
    "content": "import React, { useState } from 'react';\n\nconst SnapshotLinks = ({ timeline, currentEnvID, handleSnapshot }) => {\n  const [active, setActive] = useState(null);\n  // 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.\n  return (\n    <div>\n      <aside className=\"menu\">\n        <ul className=\"menu-list\">\n          {timeline[currentEnvID].map((item, i) => (\n            <li\n              key={i}\n              onClick={() => {\n                handleSnapshot(i);\n                setActive(i);\n              }}\n            >\n              <a href=\"#\" key={i} className={active === i && 'is-active'}>\n                {item.date.toLocaleTimeString()}: {item.label}\n              </a>\n            </li>\n          ))}\n        </ul>\n      </aside>\n    </div>\n  );\n};\n\nexport default SnapshotLinks;\n"
  },
  {
    "path": "src/devtools/view/NetworkDisplayer.js",
    "content": "import React, { useState, useEffect, useContext } from 'react';\nimport { StoreContext } from '../context';\nimport Record from './Components/Record';\nimport { execute } from 'graphql';\nimport { debounce } from '../utils';\n\n//iterates over each event and joins events based on transactionID and sorts by type\nconst combineEvents = events => {\n  const combinedEvents = {};\n  const eventTypes = {};\n  //join events by transactionID\n  events.forEach(event => {\n    const tempObj = {};\n    if (event.name === 'execute.start') {\n      tempObj.request = event.params;\n      tempObj.variables = event.variables;\n    } else if (event.name === 'execute.info') {\n      tempObj.info = event.info;\n    } else if (event.name === 'execute.next') {\n      tempObj.response = event.response;\n    } else if (event.name === 'execute.complete') {\n      // tempObj.complete = true\n    }\n    combinedEvents[event.transactionID]\n      ? (combinedEvents[event.transactionID] = Object.assign(\n          combinedEvents[event.transactionID],\n          tempObj\n        ))\n      : (combinedEvents[event.transactionID] = tempObj);\n  });\n\n  //sort by type\n  Object.keys(combinedEvents).forEach(transactionID => {\n    const op = combinedEvents[transactionID].request.operationKind;\n    eventTypes[op]\n      ? (eventTypes[op] = Object.assign(eventTypes[op], {\n          [transactionID]: combinedEvents[transactionID]\n        }))\n      : (eventTypes[op] = { [transactionID]: combinedEvents[transactionID] });\n  });\n\n  return eventTypes;\n};\n\n//generates a list of elements for the menu and the events listing\nconst generateElementList = (events, searchResults, selection, handleMenuClick) => {\n  const eventMenu = [];\n  const eventsList = [];\n\n  //for each event - add to menu list\n  for (let type in events) {\n    //creates an array of menu items for all events belonging to a given type\n    const typeList = [];\n    for (let id in events[type]) {\n      //filter out results based on search input\n      if (new RegExp(searchResults, 'i').test(JSON.stringify(events[type][id]))) {\n        typeList.push(\n          <li>\n            <a\n              id={id}\n              className={selection === id && 'is-active'}\n              onClick={e => {\n                handleMenuClick(e, id);\n              }}\n            >\n              {events[type][id].request.name}\n            </a>\n          </li>\n        );\n        //creates an array of elements for all events\n        eventsList.push(\n          <div\n            id={id}\n            className={`${\n              selection !== id && selection !== type && selection !== ''\n                ? 'is-hidden'\n                : 'record-line'\n            }`}\n          >\n            <Record {...events[type][id]} />\n          </div>\n        );\n      }\n    }\n\n    //pushes the new type element with child events to the typeList component array\n    eventMenu.push(\n      <li>\n        <a\n          id={type}\n          className={selection === type && 'is-active'}\n          onClick={e => {\n            handleMenuClick(e, type);\n          }}\n        >\n          {type}\n        </a>\n        <ul>{typeList}</ul>\n      </li>\n    );\n  }\n  return { eventMenu, eventsList };\n};\n\nconst NetworkDisplayer = ({ currentEnvID }) => {\n  const [selection, setSelection] = useState('');\n  const [events, setEvents] = useState([]);\n  const [searchResults, setSearchResults] = useState('');\n  const store = useContext(StoreContext);\n\n  useEffect(() => {\n    //on mutation all store events are pulled and processed with events state updated\n    const onMutated = () => {\n      setEvents(combineEvents(store._environmentEventsMap.get(currentEnvID) || []));\n    };\n    store.addListener('mutated', onMutated);\n\n    return () => {\n      store.removeListener('mutated', onMutated);\n    };\n  }, [store]);\n\n  //handle type menu click events\n  function handleMenuClick(e, id) {\n    //set new selection\n    setSelection(id);\n  }\n\n  //shows you the entire network\n  function handleReset(e) {\n    //remove selection;\n    setSelection('');\n  }\n\n  //updates search results\n  const debounced = debounce(val => setSearchResults(val), 300);\n  function handleSearch(e) {\n    //debounce search\n    debounced(e.target.value);\n  }\n\n  //generate menu list and events list\n  const { eventMenu, eventsList } = generateElementList(\n    events,\n    searchResults,\n    selection,\n    handleMenuClick\n  );\n\n  return (\n    <React.Fragment>\n      <div className=\"column is-one-third scrollable\">\n        <p class=\"control has-icons-left is-flex ml-2\">\n          <input\n            className=\"input is-small is-primary mt-2\"\n            type=\"text\"\n            placeholder=\"Search\"\n            onChange={e => {\n              handleSearch(e);\n            }}\n          ></input>\n          <button\n            className=\"button is-small is-link my-2\"\n            onClick={e => {\n              handleReset(e);\n            }}\n          >\n            Reset\n          </button>\n          <span class=\"icon is-left mt-2\">\n            <i class=\"fas fa-search\"></i>\n          </span>\n        </p>\n        <aside className=\"menu\">\n          <p className=\"menu-label ml-2\">Event List</p>\n          <ul className=\"menu-list\">{eventMenu}</ul>\n        </aside>\n      </div>\n      <div className=\"column scrollable\">\n        <div className=\"display-box\">{eventsList}</div>\n      </div>\n    </React.Fragment>\n  );\n};\n\nexport default NetworkDisplayer;\n"
  },
  {
    "path": "src/devtools/view/StoreDisplayer.js",
    "content": "import React, { useState } from 'react';\nimport Record from './Components/Record';\nimport { debounce } from '../utils';\n\n//update record list to current selection\nfunction updateRecords(store, selection) {\n  if (store) {\n    if (selection === '') {\n      return store;\n      //id selected - filter out everything except selected id\n    } else if (selection[0] === 'i') {\n      const id = selection.slice(3);\n      return Object.keys(store).reduce((newRL, key) => {\n        if (store[key].__id === id) newRL[key] = store[key];\n        return newRL;\n      }, {});\n      //type selected - filter out everything except selected type\n    } else {\n      const type = selection.slice(5);\n      return Object.keys(store).reduce((newRL, key) => {\n        if (store[key].__typename === type) newRL[key] = store[key];\n        return newRL;\n      }, {});\n    }\n  }\n}\n\n//generate list of menu elements\nfunction generateComponentsList(store, searchResults, recordsList, selection, handleMenuClick) {\n  //create menu list of all types\n  const menuList = {};\n  const typeList = [];\n\n  for (let id in store) {\n    const record = store[id];\n    menuList[record.__typename]\n      ? menuList[record.__typename].push(record.__id)\n      : (menuList[record.__typename] = [record.__id]);\n  }\n  //loop through each type and generate menu item\n\n  for (let type in menuList) {\n    //creates an array of elements for all ids belonging to a given type\n\n    const idList = menuList[type]\n      .filter(id => new RegExp(searchResults, 'i').test(JSON.stringify(recordsList[id])))\n      .map(id => {\n        return (\n          <li key={id}>\n            <a\n              id={'id-' + id}\n              className={selection === 'id-' + id && 'is-active'}\n              onClick={() => {\n                handleMenuClick('id-' + id);\n              }}\n            >\n              {id}\n            </a>\n          </li>\n        );\n      });\n    //pushes the new type element with child ids to the typeList component array\n    if (idList.length !== 0) {\n      typeList.push(\n        <li key={type}>\n          <a\n            id={'type-' + type}\n            className={selection === 'type-' + type && 'is-active'}\n            onClick={() => {\n              handleMenuClick('type-' + type);\n            }}\n          >\n            {type}\n          </a>\n          <ul>{idList}</ul>\n        </li>\n      );\n    }\n  }\n  return typeList;\n}\n\nconst StoreDisplayer = ({ store }) => {\n  const [recordsList, setRecordsList] = useState({});\n  const [selection, setSelection] = useState('');\n  const [searchResults, setSearchResults] = useState('');\n\n  React.useEffect(() => {\n    //initialize store\n    setRecordsList(store);\n  }, [store]);\n\n  //handle menu click events\n  function handleMenuClick(selection) {\n    //set new selection\n    setSelection(selection);\n    //update display with current selection\n    setRecordsList(updateRecords(store, selection));\n  }\n\n  //shows you the entire store\n  function handleReset(e) {\n    //remove selection\n    setSelection('');\n    //reset back to original store\n    setRecordsList(store);\n  }\n\n  //updates search results\n  const debounced = debounce(val => {\n    setSelection('');\n    setRecordsList(store);\n    setSearchResults(val);\n  }, 300);\n  function handleSearch(e) {\n    //debounce search\n    debounced(e.target.value);\n  }\n  //generates the menu element list\n\n  //verify recordsList is not undefined and then generate list of components\n  const typeList =\n    recordsList === undefined\n      ? []\n      : generateComponentsList(store, searchResults, recordsList, selection, handleMenuClick);\n\n  return (\n    <React.Fragment>\n      <div className=\"column is-half-mobile scrollable\">\n        <p class=\"control has-icons-left is-flex\">\n          <input\n            className=\"input is-small is-primary\"\n            type=\"text\"\n            placeholder=\"Search\"\n            onChange={e => {\n              handleSearch(e);\n            }}\n          ></input>\n          <button\n            className=\"button is-small is-link\"\n            onClick={e => {\n              handleReset(e);\n            }}\n          >\n            Reset\n          </button>\n          <span class=\"icon is-left\">\n            <i class=\"fas fa-search\"></i>\n          </span>\n        </p>\n        <aside className=\"menu\">\n          <p className=\"menu-label mt-1\">Record List</p>\n          <ul className=\"menu-list\">{typeList}</ul>\n        </aside>\n      </div>\n      <div className=\"column is-half-mobile scrollable\">\n        <div className=\"display-box\">\n          <Record {...recordsList} />\n        </div>\n      </div>\n    </React.Fragment>\n  );\n};\n\nexport default StoreDisplayer;\n"
  },
  {
    "path": "src/devtools/view/StoreTimeline.js",
    "content": "import React, { useState, useContext, useEffect } from 'react';\nimport InputRange from 'react-input-range';\nimport { BridgeContext, StoreContext } from '../context';\nimport StoreDisplayer from './StoreDisplayer';\nimport SnapshotLinks from './Components/SnapshotLinks';\n\nconst StoreTimeline = ({ currentEnvID }) => {\n  const store = useContext(StoreContext);\n  const bridge = useContext(BridgeContext);\n  const [snapshotIndex, setSnapshotIndex] = useState(0);\n  const [timelineLabel, setTimelineLabel] = useState('');\n  const [liveStore, setLiveStore] = useState({});\n  // Each envId has an array of orbject built up for loading snapshots via the handleClick\n  const [timeline, setTimeline] = useState({\n    [currentEnvID]: [\n      {\n        label: 'at startup',\n        date: new Date(),\n        storage: liveStore\n      }\n    ]\n  });\n\n  // build snapshot object and insert into timeline\n  const handleClick = e => {\n    e.preventDefault();\n    const timelineInsert = {};\n    const timeStamp = new Date();\n    timelineInsert.label = timelineLabel;\n    timelineInsert.date = timeStamp;\n    timelineInsert.storage = liveStore;\n    const newTimeline = timeline[currentEnvID].concat([timelineInsert]);\n    setTimeline({ ...timeline, [currentEnvID]: newTimeline });\n    setTimelineLabel('');\n    setSnapshotIndex(newTimeline.length);\n  };\n\n  const handleSnapshot = index => {\n    setSnapshotIndex(index);\n  };\n\n  const updateStoreHelper = storeObj => {\n    setLiveStore(storeObj);\n  };\n\n  // triggering refresh of store on completed mutation\n  React.useEffect(() => {\n    const refreshLiveStore = () => {\n      bridge.send('refreshStore', currentEnvID);\n    };\n    const refreshEvents = () => {\n      const allRecords = store.getRecords(currentEnvID);\n      updateStoreHelper(allRecords);\n    };\n\n    store.addListener('storeDataReceived', refreshEvents);\n    store.addListener('allEventsReceived', refreshEvents);\n    store.addListener('mutationComplete', refreshLiveStore);\n\n    return () => {\n      store.removeListener('mutationComplete', refreshLiveStore);\n      store.removeListener('storeDataReceived', refreshEvents);\n      store.removeListener('allEventsReceived', refreshEvents);\n    };\n  }, [store]);\n\n  React.useEffect(() => {\n    const allRecords = store.getRecords(currentEnvID);\n    setLiveStore(allRecords);\n\n    if (!timeline[currentEnvID]) {\n      const newTimeline = {\n        ...timeline,\n        [currentEnvID]: [\n          {\n            label: 'current',\n            date: new Date(),\n            storage: allRecords\n          }\n        ]\n      };\n      setTimeline(newTimeline);\n      setSnapshotIndex(1);\n    } else {\n      setSnapshotIndex(timeline[currentEnvID].length);\n    }\n  }, [currentEnvID]);\n\n  console.log(\n    'showing livestore',\n    !timeline[currentEnvID] ||\n      !timeline[currentEnvID][snapshotIndex] ||\n      snapshotIndex === timeline[currentEnvID].length\n  );\n\n  return (\n    <React.Fragment>\n      <div className=\"column is-full-mobile is-one-quarter-desktop\">\n        <div className=\"display-box\">\n          <div className=\"snapshot-wrapper is-flex ml-2\">\n            <input\n              type=\"text\"\n              className=\"input is-small snapshot-btn is-primary\"\n              value={timelineLabel}\n              onChange={e => setTimelineLabel(e.target.value)}\n              placeholder=\"take a store snapshot\"\n            ></input>\n            <button className=\"button is-small is-link\" onClick={e => handleClick(e)}>\n              Snapshot\n            </button>\n          </div>\n        </div>\n        <div className=\"snapshots\">\n          <div\n            className=\"timeline-nav column is-full-desktop is-flex-mobile\"\n            id=\"timeline-mini-col\"\n          >\n            <InputRange\n              maxValue={timeline[currentEnvID] ? timeline[currentEnvID].length : 0}\n              minValue={0}\n              value={snapshotIndex}\n              onChange={value => setSnapshotIndex(value)}\n            />\n            <div className=\"snapshot-nav has-text-centered has-text-right-mobile\">\n              <button\n                class=\"button is-small is-info is-light\"\n                onClick={() => {\n                  if (snapshotIndex !== 0) setSnapshotIndex(snapshotIndex - 1);\n                }}\n              >\n                <span className=\"icon is-medium\">\n                  <i className=\"fas fa-fast-backward\"></i>\n                </span>\n              </button>\n              <button\n                class=\"button is-small is-info is-light\"\n                onClick={() => setSnapshotIndex(timeline[currentEnvID].length)}\n              >\n                Current\n              </button>\n              <button\n                class=\"button is-small is-info is-light\"\n                onClick={() => {\n                  if (snapshotIndex !== timeline[currentEnvID].length)\n                    setSnapshotIndex(snapshotIndex + 1);\n                }}\n              >\n                <span className=\"icon is-medium\">\n                  <i className=\"fas fa-fast-forward\"></i>\n                </span>\n              </button>\n            </div>\n          </div>\n          <div\n            className=\"snapshot-info is-size-7 column is-full-desktop pt-0\"\n            id=\"snapshot-info-col\"\n          >\n            {timeline[currentEnvID] && (\n              <SnapshotLinks\n                currentEnvID={currentEnvID}\n                handleSnapshot={handleSnapshot}\n                timeline={timeline}\n              />\n            )}\n          </div>\n        </div>\n      </div>\n      <StoreDisplayer\n        store={\n          !timeline[currentEnvID] ||\n          !timeline[currentEnvID][snapshotIndex] ||\n          snapshotIndex === timeline[currentEnvID].length\n            ? liveStore\n            : timeline[currentEnvID][snapshotIndex].storage\n        }\n      />\n    </React.Fragment>\n  );\n};\n\nexport default StoreTimeline;\n"
  },
  {
    "path": "src/hook.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\n/**\n * Install the hook on window, which is an event emitter.\n * Note because Chrome content scripts cannot directly modify the window object,\n * we are evaling this function by inserting a script tag.\n * That's why we have to inline the whole event emitter implementation here.\n */\n\nimport type { DevToolsHook } from 'src/backend/types';\n\ndeclare var window: any;\n\nexport function installHook(target: any): DevToolsHook | null {\n  if (target.hasOwnProperty('__RELAY_DEVTOOLS_HOOK__')) {\n    return null;\n  }\n  const listeners = {};\n  const environments = new Map();\n\n  let uidCounter = 0;\n\n  function registerEnvironment(environment) {\n    const id = ++uidCounter;\n    environments.set(id, environment);\n\n    hook.emit('environment', { id, environment });\n\n    return id;\n  }\n\n  function sub(event, fn) {\n    hook.on(event, fn);\n    return () => hook.off(event, fn);\n  }\n\n  function on(event, fn) {\n    if (!listeners[event]) {\n      listeners[event] = [];\n    }\n    listeners[event].push(fn);\n  }\n\n  function off(event, fn) {\n    if (!listeners[event]) {\n      return;\n    }\n    const index = listeners[event].indexOf(fn);\n    if (index !== -1) {\n      listeners[event].splice(index, 1);\n    }\n    if (!listeners[event].length) {\n      delete listeners[event];\n    }\n  }\n\n  function emit(event, data) {\n    if (listeners[event]) {\n      listeners[event].map(fn => fn(data));\n    }\n  }\n\n  const environmentWrappers = new Map();\n\n  const hook: DevToolsHook = {\n    registerEnvironment,\n    environmentWrappers,\n    // listeners,\n    environments,\n\n    emit,\n    // inject,\n    on,\n    off,\n    sub\n  };\n\n  Object.defineProperty(\n    target,\n    '__RELAY_DEVTOOLS_HOOK__',\n    ({\n      // This property needs to be configurable for the test environment,\n      // else we won't be able to delete and recreate it beween tests.\n      configurable: __DEV__,\n      enumerable: false,\n      get() {\n        return hook;\n      }\n    }: Object)\n  );\n\n  return hook;\n}\n"
  },
  {
    "path": "src/types.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nexport type Wall = {|\n  // `listen` returns the \"unlisten\" function.\n  listen: (fn: Function) => Function,\n  send: (event: string, payload: any, transferable?: Array<any>) => void\n|};\n\nexport type Record = { [key: string]: mixed, ... };\nexport type DataID = string;\nexport type UpdatedRecords = { [dataID: DataID]: boolean, ... };\n\nexport type StoreRecords = { [DataID]: ?Record, ... };\n\n// Copied from relay\nexport type LogEvent =\n  | {|\n      +name: 'queryresource.fetch',\n      +operation: $FlowFixMe,\n      // FetchPolicy from relay-experimental\n      +fetchPolicy: string,\n      // RenderPolicy from relay-experimental\n      +renderPolicy: string,\n      +hasFullQuery: boolean,\n      +shouldFetch: boolean\n    |}\n  | {|\n      +name: 'store.publish',\n      +source: any,\n      +optimistic: boolean\n    |}\n  | {|\n      +name: 'store.gc',\n      references: Array<DataID>,\n      gcRecords: StoreRecords\n    |}\n  | {|\n      +name: 'store.restore'\n    |}\n  | {|\n      +name: 'store.snapshot'\n    |}\n  | {|\n      +name: 'store.notify.start'\n    |}\n  | {|\n      +name: 'store.notify.complete',\n      +updatedRecordIDs: UpdatedRecords,\n      +invalidatedRecordIDs: Array<DataID>,\n      updatedRecords: StoreRecords,\n      invalidatedRecords: StoreRecords\n    |}\n  | {|\n      +name: 'execute.info',\n      +transactionID: number,\n      +info: mixed,\n      params: $FlowFixMe,\n      variables: $FlowFixMe\n    |}\n  | {|\n      +name: 'execute.start',\n      +transactionID: number,\n      +params: $FlowFixMe,\n      +variables: $FlowFixMe\n    |}\n  | {|\n      +name: 'execute.next',\n      +transactionID: number,\n      +response: $FlowFixMe,\n      params: $FlowFixMe,\n      variables: $FlowFixMe\n    |}\n  | {|\n      +name: 'execute.error',\n      +transactionID: number,\n      +error: Error,\n      params: $FlowFixMe,\n      variables: $FlowFixMe\n    |}\n  | {|\n      +name: 'execute.complete',\n      +transactionID: number,\n      params: $FlowFixMe,\n      variables: $FlowFixMe\n    |}\n  | {|\n      +name: 'execute.unsubscribe',\n      +transactionID: number,\n      params: $FlowFixMe,\n      variables: $FlowFixMe\n    |};\n\nexport type EventData = {|\n  +id: number,\n  +data: LogEvent,\n  +source: StoreRecords,\n  +eventType: string\n|};\n\nexport type StoreData = {|\n  +name: string,\n  +id: number,\n  +records: StoreRecords\n|};\n\nexport type EnvironmentInfo = {|\n  +id: number,\n  +environmentName: string\n|};\n"
  }
]