diff --git a/desktop/plugins/hermesdebuggerrn/ChromeDevTools.tsx b/desktop/plugins/hermesdebuggerrn/ChromeDevTools.tsx
new file mode 100644
index 000000000..bc4a040f5
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/ChromeDevTools.tsx
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ */
+
+import React from 'react';
+
+import electron from 'electron';
+
+const devToolsNodeId = (url: string) =>
+ `hermes-chromedevtools-out-of-react-node-${url.replace(
+ /[^a-zA-Z0-9]+/g,
+ '-',
+ )}`;
+
+// TODO: build abstractionf or this: T62306732
+const TARGET_CONTAINER_ID = 'flipper-out-of-contents-container'; // should be a hook in the future
+
+function createDevToolsNode(url: string): HTMLElement {
+ const existing = findDevToolsNode(url);
+ if (existing) {
+ return existing;
+ }
+
+ // It is necessary to activate chrome devtools in electron
+ electron.remote.getCurrentWindow().webContents.toggleDevTools();
+ electron.remote.getCurrentWindow().webContents.closeDevTools();
+
+ const wrapper = document.createElement('div');
+ wrapper.id = devToolsNodeId(url);
+ wrapper.style.height = '100%';
+ wrapper.style.width = '100%';
+
+ const iframe = document.createElement('webview');
+ iframe.style.height = '100%';
+ iframe.style.width = '100%';
+
+ // // HACK: chrome-devtools:// is blocked by the sandbox but devtools:// isn't for some reason.
+ iframe.src = url.replace(/^chrome-/, '');
+
+ wrapper.appendChild(iframe);
+ document.getElementById(TARGET_CONTAINER_ID)!.appendChild(wrapper);
+ return wrapper;
+}
+
+function findDevToolsNode(url: string): HTMLElement | null {
+ return document.querySelector('#' + devToolsNodeId(url));
+}
+
+function attachDevTools(devToolsNode: HTMLElement) {
+ devToolsNode.style.display = 'block';
+ document.getElementById(TARGET_CONTAINER_ID)!.style.display = 'block';
+}
+
+function detachDevTools(devToolsNode: HTMLElement | null) {
+ document.getElementById(TARGET_CONTAINER_ID)!.style.display = 'none';
+
+ if (devToolsNode) {
+ devToolsNode.style.display = 'none';
+ }
+}
+
+type ChromeDevToolsProps = {
+ url: string;
+};
+
+export default class ChromeDevTools extends React.Component<
+ ChromeDevToolsProps
+> {
+ createDevTools(url: string) {
+ const devToolsNode = createDevToolsNode(url);
+ attachDevTools(devToolsNode);
+ }
+
+ hideDevTools(_url: string) {
+ detachDevTools(findDevToolsNode(this.props.url));
+ }
+
+ componentDidMount() {
+ this.createDevTools(this.props.url);
+ }
+
+ componentWillUnmount() {
+ this.hideDevTools(this.props.url);
+ }
+
+ componentDidUpdate(prevProps: ChromeDevToolsProps) {
+ const oldUrl = prevProps.url;
+ const newUrl = this.props.url;
+ if (oldUrl != newUrl) {
+ this.hideDevTools(oldUrl);
+ this.createDevTools(newUrl);
+ }
+ }
+
+ render() {
+ return
;
+ }
+}
diff --git a/desktop/plugins/hermesdebuggerrn/ErrorScreen.tsx b/desktop/plugins/hermesdebuggerrn/ErrorScreen.tsx
new file mode 100644
index 000000000..d84083947
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/ErrorScreen.tsx
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ */
+
+import React from 'react';
+import {styled, FlexColumn, FlexRow, Text, Glyph, colors} from 'flipper';
+
+const Container = styled(FlexColumn)({
+ height: '100%',
+ width: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: colors.light02,
+});
+
+const Welcome = styled(FlexColumn)({
+ width: 460,
+ background: colors.white,
+ borderRadius: 10,
+ boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
+ overflow: 'hidden',
+ transition: '0.6s all ease-out',
+});
+
+const Title = styled(Text)({
+ fontSize: 24,
+ fontWeight: 300,
+ textAlign: 'center',
+ color: colors.light50,
+ marginTop: 16,
+ marginBottom: 16,
+});
+
+const Item = styled(FlexRow)({
+ padding: 10,
+ alignItems: 'center',
+ borderTop: `1px solid ${colors.light10}`,
+});
+
+const ItemTitle = styled(Text)({
+ color: colors.light50,
+ fontSize: 14,
+ lineHeight: '20px',
+});
+
+const Bold = styled(Text)({
+ fontWeight: 600,
+});
+
+const Icon = styled(Glyph)({
+ marginRight: 11,
+ marginLeft: 6,
+});
+
+// As more known failures are found, add them to this list with better error information.
+const KNOWN_FAILURE_MESSAGES: Record<
+ string,
+ Record<'message' | 'hint', string>
+> = {
+ 'Failed to fetch': {
+ // This is the error that is returned specifcally when Metro is turned off.
+ message: 'Metro disconnected.',
+ hint: 'Please check that metro is running and Flipper can connect to it.',
+ },
+ default: {
+ // All we really know in this case is that we can't connect to metro.
+ // Do not try and be more specific here.
+ message: 'Cannot connect to Metro.',
+ hint: 'Please check that metro is running and Flipper can connect to it.',
+ },
+};
+
+function getReason(error: Error) {
+ let failure_message = KNOWN_FAILURE_MESSAGES.default;
+ if (error != null && KNOWN_FAILURE_MESSAGES[error.message]) {
+ failure_message = KNOWN_FAILURE_MESSAGES[error.message];
+ }
+
+ return (
+
+ {failure_message.message}
+ {failure_message.hint}
+
+ );
+}
+
+type Props = Readonly<{
+ error: Error;
+}>;
+
+export default function ErrorScreen(props: Props) {
+ return (
+
+
+ Hermes Debugger Error
+ -
+
+ {getReason(props.error)}
+
+
+
+ );
+}
diff --git a/desktop/plugins/hermesdebuggerrn/LaunchScreen.tsx b/desktop/plugins/hermesdebuggerrn/LaunchScreen.tsx
new file mode 100644
index 000000000..eb9fa0b76
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/LaunchScreen.tsx
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ */
+
+import React from 'react';
+import {styled, FlexColumn, FlexRow, Text, Glyph, colors} from 'flipper';
+
+const Container = styled(FlexColumn)({
+ height: '100%',
+ width: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: colors.light02,
+});
+
+const Welcome = styled(FlexColumn)({
+ width: 460,
+ background: colors.white,
+ borderRadius: 10,
+ boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
+ overflow: 'hidden',
+ transition: '0.6s all ease-out',
+});
+
+const Title = styled(Text)({
+ fontSize: 24,
+ fontWeight: 300,
+ textAlign: 'center',
+ color: colors.light50,
+ marginTop: 16,
+ marginBottom: 16,
+});
+
+const Item = styled(FlexRow)({
+ padding: 10,
+ alignItems: 'center',
+ borderTop: `1px solid ${colors.light10}`,
+});
+
+const ItemTitle = styled(Text)({
+ color: colors.light50,
+ fontSize: 14,
+ lineHeight: '20px',
+});
+
+const Bold = styled(Text)({
+ fontWeight: 600,
+});
+
+const Icon = styled(Glyph)({
+ marginRight: 11,
+ marginLeft: 6,
+});
+
+export default function LaunchScreen() {
+ return (
+
+
+ Hermes Debugger
+ -
+
+
+
+ Metro is connected but no Hermes apps were found.{' '}
+ Open a React Native screen with Hermes enabled to connect. Note:
+ you may need to reload the app in order to reconnect the device to
+ Metro.
+
+
+
+
+
+ );
+}
diff --git a/desktop/plugins/hermesdebuggerrn/SelectScreen.tsx b/desktop/plugins/hermesdebuggerrn/SelectScreen.tsx
new file mode 100644
index 000000000..102411d16
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/SelectScreen.tsx
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ */
+
+import React from 'react';
+import {styled, FlexColumn, FlexRow, Text, Glyph, colors} from 'flipper';
+import {Target, Targets} from './index';
+
+const Container = styled(FlexColumn)({
+ height: '100%',
+ width: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: colors.light02,
+});
+
+const Welcome = styled(FlexColumn)({
+ width: 460,
+ background: colors.white,
+ borderRadius: 10,
+ boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
+ overflow: 'hidden',
+ transition: '0.6s all ease-out',
+});
+
+const Title = styled(Text)({
+ fontSize: 24,
+ fontWeight: 300,
+ textAlign: 'center',
+ color: colors.light50,
+ marginTop: 16,
+ marginBottom: 16,
+});
+
+const Item = styled(FlexRow)({
+ padding: 10,
+ alignItems: 'center',
+ borderTop: `1px solid ${colors.light10}`,
+});
+
+const ItemTitle = styled(Text)({
+ color: colors.light50,
+ fontSize: 14,
+ lineHeight: '20px',
+});
+
+const Icon = styled(Glyph)({
+ marginRight: 11,
+ marginLeft: 6,
+});
+
+type Props = {
+ readonly targets: Targets;
+ readonly onSelect: (target: Target) => void;
+};
+
+export default function SelectScreen(props: Props) {
+ return (
+
+
+ Hermes Debugger Select
+ -
+
+ Please select a target:
+
+
+ {props.targets.map((target) => {
+ return (
+ - props.onSelect(target)}>
+
+
+ {target.title}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/desktop/plugins/hermesdebuggerrn/index.tsx b/desktop/plugins/hermesdebuggerrn/index.tsx
new file mode 100644
index 000000000..250cb3d80
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/index.tsx
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ */
+
+import React from 'react';
+import {FlipperDevicePlugin, Device} from 'flipper';
+import LaunchScreen from './LaunchScreen';
+import SelectScreen from './SelectScreen';
+import ErrorScreen from './ErrorScreen';
+import ChromeDevTools from './ChromeDevTools';
+
+const POLL_SECS = 5 * 1000;
+const METRO_HOST = 'http://localhost:8081';
+
+export type Target = Readonly<{
+ id: string;
+ description: string;
+ title: string;
+ faviconUrl: string;
+ devtoolsFrontendUrl: string;
+ type: string;
+ webSocketDebuggerUrl: string;
+ vm: string;
+}>;
+
+export type Targets = ReadonlyArray;
+
+type State = Readonly<{
+ targets?: Targets | null;
+ selectedTarget?: Target | null;
+ error?: Error | null;
+}>;
+
+export default class extends FlipperDevicePlugin {
+ static title = 'Hermes Debugger';
+ static id = 'Hermesdebuggerrn';
+ static icon = 'code';
+
+ static supportsDevice(device: Device) {
+ return !device.isArchived && device.os === 'Metro';
+ }
+
+ state: State = {
+ targets: null,
+ selectedTarget: null,
+ error: null,
+ };
+
+ poll?: NodeJS.Timeout;
+
+ componentDidMount() {
+ // This is a pretty basic polling mechnaism. We ask Metro every POLL_SECS what the
+ // current available targets are and only handle a few basic state transitions.
+ this.poll = setInterval(this.checkDebugTargets, POLL_SECS);
+ this.checkDebugTargets();
+ }
+
+ componentWillUnmount() {
+ if (this.poll) {
+ clearInterval(this.poll);
+ }
+ }
+
+ checkDebugTargets = () => {
+ fetch(`${METRO_HOST}/json`)
+ .then((res) => res.json())
+ .then(
+ (result) => {
+ // We only want to use the Chrome Reload targets.
+ const targets = result.filter(
+ (target: any) =>
+ target.title ===
+ 'React Native Experimental (Improved Chrome Reloads)',
+ );
+
+ // Find the currently selected target.
+ // If the current selectedTarget isn't returned, clear it.
+ let currentlySelected = null;
+ if (this.state.selectedTarget != null) {
+ for (const target of result) {
+ if (
+ this.state.selectedTarget?.webSocketDebuggerUrl ===
+ target.webSocketDebuggerUrl
+ ) {
+ currentlySelected = this.state.selectedTarget;
+ }
+ }
+ }
+
+ // Auto-select the first target if there is one,
+ // but don't change the one that's already selected.
+ const selectedTarget =
+ currentlySelected == null && targets.length === 1
+ ? targets[0]
+ : currentlySelected;
+
+ this.setState({
+ error: null,
+ targets,
+ selectedTarget,
+ });
+ },
+ (error) => {
+ this.setState({
+ targets: null,
+ selectedTarget: null,
+ error,
+ });
+ },
+ );
+ };
+
+ handleSelect = (selectedTarget: Target) => this.setState({selectedTarget});
+
+ render() {
+ const {error, selectedTarget, targets} = this.state;
+
+ if (selectedTarget) {
+ return ;
+ } else if (targets != null && targets.length === 0) {
+ return ;
+ } else if (targets != null && targets.length > 0) {
+ return ;
+ } else if (error != null) {
+ return ;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/desktop/plugins/hermesdebuggerrn/package.json b/desktop/plugins/hermesdebuggerrn/package.json
new file mode 100644
index 000000000..3c36f9a9f
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "flipper-plugin-hermesdebuggerrn",
+ "version": "1.0.0",
+ "main": "index.tsx",
+ "license": "MIT",
+ "title": "Hermes Debugger (RN)",
+ "icon": "apps",
+ "keywords": ["flipper-plugin"],
+ "bugs": {
+ "email": "rickhanlonii@fb.com"
+ }
+}
diff --git a/desktop/plugins/hermesdebuggerrn/yarn.lock b/desktop/plugins/hermesdebuggerrn/yarn.lock
new file mode 100644
index 000000000..fb57ccd13
--- /dev/null
+++ b/desktop/plugins/hermesdebuggerrn/yarn.lock
@@ -0,0 +1,4 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+