diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index c105f20cc8449..7bd636b80e448 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -86,6 +86,7 @@ function reload() { bridge: ((bridge: any): Bridge), showTabBar: true, store: ((store: any): Store), + warnIfLegacyBackendDetected: true, viewElementSourceFunction, viewElementSourceRequiresFileLocation: true, }) diff --git a/shells/dev/src/devtools.js b/shells/dev/src/devtools.js index 80d8bb38ccb1c..2afed0c208637 100644 --- a/shells/dev/src/devtools.js +++ b/shells/dev/src/devtools.js @@ -77,6 +77,7 @@ inject('dist/app.js', () => { browserTheme: 'light', showTabBar: true, store, + warnIfLegacyBackendDetected: true, }) ); batch.then(() => { diff --git a/src/bridge.js b/src/bridge.js index c5ed0f4a0379c..23b887af7fc7e 100644 --- a/src/bridge.js +++ b/src/bridge.js @@ -128,6 +128,12 @@ export default class Bridge extends EventEmitter<{| }) || null; } + // Listening directly to the wall isn't advised. + // It can be used to listen for legacy (v3) messages (since they use a different format). + get wall(): Wall { + return this._wall; + } + send(event: string, payload: any, transferable?: Array) { if (this._isShutdown) { console.warn( diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index 7a04ee46bc831..f8d9bcec9186b 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -18,6 +18,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext'; import { ProfilerContextController } from './Profiler/ProfilerContext'; import { ModalDialogContextController } from './ModalDialog'; import ReactLogo from './ReactLogo'; +import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected'; import styles from './DevTools.css'; @@ -38,6 +39,7 @@ export type Props = {| defaultTab?: TabID, showTabBar?: boolean, store: Store, + warnIfLegacyBackendDetected?: boolean, viewElementSourceFunction?: ?ViewElementSource, viewElementSourceRequiresFileLocation?: boolean, @@ -80,6 +82,7 @@ export default function DevTools({ settingsPortalContainer, showTabBar = false, store, + warnIfLegacyBackendDetected = false, viewElementSourceFunction, viewElementSourceRequiresFileLocation = false, }: Props) { @@ -143,6 +146,7 @@ export default function DevTools({ + {warnIfLegacyBackendDetected && } diff --git a/src/devtools/views/ModalDialog.js b/src/devtools/views/ModalDialog.js index b9ca5cc974696..2cceb9a49e034 100644 --- a/src/devtools/views/ModalDialog.js +++ b/src/devtools/views/ModalDialog.js @@ -18,6 +18,7 @@ type DIALOG_ACTION_HIDE = {| |}; type DIALOG_ACTION_SHOW = {| type: 'SHOW', + canBeDismissed?: boolean, content: React$Node, title?: React$Node | null, |}; @@ -27,6 +28,7 @@ type Action = DIALOG_ACTION_HIDE | DIALOG_ACTION_SHOW; type Dispatch = (action: Action) => void; type State = {| + canBeDismissed: boolean, content: React$Node | null, isVisible: boolean, title: React$Node | null, @@ -46,12 +48,14 @@ function dialogReducer(state, action) { switch (action.type) { case 'HIDE': return { + canBeDismissed: true, content: null, isVisible: false, title: null, }; case 'SHOW': return { + canBeDismissed: action.canBeDismissed !== false, content: action.content, isVisible: true, title: action.title || null, @@ -67,6 +71,7 @@ type Props = {| function ModalDialogContextController({ children }: Props) { const [state, dispatch] = useReducer(dialogReducer, { + canBeDismissed: true, content: null, isVisible: false, title: null, @@ -74,6 +79,7 @@ function ModalDialogContextController({ children }: Props) { const value = useMemo( () => ({ + canBeDismissed: state.canBeDismissed, content: state.content, isVisible: state.isVisible, title: state.title, @@ -95,10 +101,14 @@ function ModalDialog(_: {||}) { } function ModalDialogImpl(_: {||}) { - const { content, dispatch, title } = useContext(ModalDialogContext); - const dismissModal = useCallback(() => dispatch({ type: 'HIDE' }), [ - dispatch, - ]); + const { canBeDismissed, content, dispatch, title } = useContext( + ModalDialogContext + ); + const dismissModal = useCallback(() => { + if (canBeDismissed) { + dispatch({ type: 'HIDE' }); + } + }, [canBeDismissed, dispatch]); const modalRef = useRef(null); useModalDismissSignal(modalRef, dismissModal); @@ -108,11 +118,13 @@ function ModalDialogImpl(_: {||}) {
{title !== null &&
{title}
} {content} -
- -
+ {canBeDismissed && ( +
+ +
+ )}
); diff --git a/src/devtools/views/WarnIfLegacyBackendDetected.css b/src/devtools/views/WarnIfLegacyBackendDetected.css new file mode 100644 index 0000000000000..e1c3817309f6c --- /dev/null +++ b/src/devtools/views/WarnIfLegacyBackendDetected.css @@ -0,0 +1,6 @@ +.Command { + background-color: var(--color-dimmest); + padding: 0.25rem 0.5rem; + display: block; + border-radius: 0.125rem; +} diff --git a/src/devtools/views/WarnIfLegacyBackendDetected.js b/src/devtools/views/WarnIfLegacyBackendDetected.js new file mode 100644 index 0000000000000..79dd8978cdcc2 --- /dev/null +++ b/src/devtools/views/WarnIfLegacyBackendDetected.js @@ -0,0 +1,60 @@ +// @flow + +import React, { Fragment, useContext, useEffect } from 'react'; +import { BridgeContext } from './context'; +import { ModalDialogContext } from './ModalDialog'; + +import styles from './WarnIfLegacyBackendDetected.css'; + +export default function WarnIfLegacyBackendDetected(_: {||}) { + const bridge = useContext(BridgeContext); + const { dispatch } = useContext(ModalDialogContext); + + // Detect pairing with legacy v3 backend. + // We do this by listening to a message that it broadcasts but the v4 backend doesn't. + // In this case the frontend should show upgrade instructions. + useEffect(() => { + // Wall.listen returns a cleanup function + let unlisten = bridge.wall.listen(event => { + switch (event.type) { + case 'call': + case 'event': + case 'many-events': + // Any of these types indicate the v3 backend. + dispatch({ + canBeDismissed: false, + type: 'SHOW', + title: + 'React DevTools v4 is incompatible with this version of React', + content: , + }); + + if (typeof unlisten === 'function') { + unlisten(); + unlisten = null; + } + break; + default: + break; + } + }); + + return () => { + if (typeof unlisten === 'function') { + unlisten(); + unlisten = null; + } + }; + }, [bridge, dispatch]); + + return null; +} + +function InvalidBackendDetected(_: {||}) { + return ( + +

Either upgrade React or install React DevTools v3:

+ npm install -d react-devtools@^3 +
+ ); +}