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 + +