-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New NPM package react-devtools-inline (#363)
- Loading branch information
Brian Vaughn
authored
Aug 5, 2019
1 parent
6f1e283
commit dc8580e
Showing
21 changed files
with
440 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# `react-devtools-inline` | ||
|
||
React DevTools implementation for embedding within a browser-based IDE (e.g. [CodeSandbox](https://codesandbox.io/), [StackBlitz](https://stackblitz.com/)). | ||
|
||
This is a low-level package. If you're looking for the standalone DevTools app, **use the `react-devtools` package instead.** | ||
|
||
## Usage | ||
|
||
This package exports two entry points: a frontend (to be run in the main `window`) and a backend (to be installed and run within an `iframe`<sup>1</sup>). | ||
|
||
The frontend and backend can be initialized in any order, but **the backend must not be activated until after the frontend has been initialized**. Because of this, the simplest sequence is: | ||
|
||
1. Frontend (DevTools interface) initialized in the main `window`. | ||
1. Backend initialized in an `iframe`. | ||
1. Backend activated. | ||
|
||
<sup>1</sup> Sandboxed iframes are supported. | ||
|
||
## API | ||
|
||
### `react-devtools-inline/backend` | ||
|
||
* **`initialize(contentWindow)`** - | ||
Installs the global hook on the window. This hook is how React and DevTools communicate. **This method must be called before React is loaded.** (This means before any `import` or `require` statements!) | ||
* **`activate(contentWindow)`** - | ||
Lets the backend know when the frontend is ready. It should not be called until after the frontend has been initialized, else the frontend might miss important tree-initialization events. | ||
|
||
```js | ||
import { activate, initialize } from 'react-devtools-inline/backend'; | ||
|
||
// Call this before importing React (or any other packages that might import React). | ||
initialize(); | ||
|
||
// Call this only once the frontend has been initialized. | ||
activate(); | ||
``` | ||
|
||
### `react-devtools-inline/frontend` | ||
|
||
* **`initialize(contentWindow)`** - | ||
Configures the DevTools interface to listen to the `window` the backend was injected into. This method returns a React component that can be rendered directly. | ||
|
||
```js | ||
import { initialize } from 'react-devtools-inline/frontend'; | ||
|
||
// This should be the iframe the backend hook has been installed in. | ||
const iframe = document.getElementById(frameID); | ||
const contentWindow = iframe.contentWindow; | ||
|
||
// This returns a React component that can be rendered into your app. | ||
// <DevTools {...props} /> | ||
const DevTools = initialize(contentWindow); | ||
``` | ||
|
||
## Examples | ||
|
||
### Configuring a same-origin `iframe` | ||
|
||
The simplest way to use this package is to install the hook from the parent `window`. This is possible if the `iframe` is not sandboxed and there are no cross-origin restrictions. | ||
|
||
```js | ||
import { | ||
activate as activateBackend, | ||
initialize as initializeBackend | ||
} from 'react-devtools-inline/backend'; | ||
import { initialize as initializeFrontend } from 'react-devtools-inline/frontend'; | ||
|
||
// The React app you want to inspect with DevTools is running within this iframe: | ||
const iframe = document.getElementById('target'); | ||
const { contentWindow } = iframe; | ||
|
||
// Installs the global hook into the iframe. | ||
// This be called before React is loaded into that frame. | ||
initializeBackend(contentWindow); | ||
|
||
// React application can be injected into <iframe> at any time now... | ||
|
||
// Initialize DevTools UI to listen to the hook we just installed. | ||
// This returns a React component we can render anywhere in the parent window. | ||
const DevTools = initializeFrontend(contentWindow); | ||
|
||
// <DevTools /> interface can be rendered in the parent window at any time now... | ||
|
||
// Let the backend know the frontend is ready and listening. | ||
activateBackend(contentWindow); | ||
``` | ||
|
||
### Configuring a sandboxed `iframe` | ||
|
||
Sandboxed `iframe`s are also supported but require more complex initialization. | ||
|
||
**`iframe.html`** | ||
```js | ||
import { activate, initialize } from 'react-devtools-inline/backend'; | ||
|
||
// The DevTooks hook needs to be installed before React is even required! | ||
// The safest way to do this is probably to install it in a separate script tag. | ||
initialize(window); | ||
|
||
// Wait for the frontend to let us know that it's ready. | ||
window.addEventListener('message', ({ data }) => { | ||
switch (data.type) { | ||
case 'activate': | ||
activate(window); | ||
break; | ||
default: | ||
break; | ||
} | ||
}); | ||
``` | ||
|
||
**`main-window.html`** | ||
```js | ||
import { initialize } from 'react-devtools-inline/frontend'; | ||
|
||
const iframe = document.getElementById('target'); | ||
const { contentWindow } = iframe; | ||
|
||
// Initialize DevTools UI to listen to the iframe. | ||
// This returns a React component we can render anywhere in the main window. | ||
const DevTools = initialize(contentWindow); | ||
|
||
// Let the backend know to initialize itself. | ||
// We can't do this directly because the iframe is sandboxed. | ||
// Only initialize the backend once the DevTools frontend has been initialized. | ||
iframe.onload = () => { | ||
contentWindow.postMessage( | ||
{ | ||
type: 'activate', | ||
}, | ||
'*' | ||
); | ||
}; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./dist/backend'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./dist/frontend'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "react-devtools-inline", | ||
"version": "4.0.0-alpha.7", | ||
"description": "Embed react-devtools within a website", | ||
"license": "MIT", | ||
"main": "./dist/backend.js", | ||
"repository": { | ||
"url": "https://github.com/bvaughn/react-devtools-experimental.git", | ||
"type": "git" | ||
}, | ||
"files": [ | ||
"dist", | ||
"backend.js", | ||
"frontend.js" | ||
], | ||
"scripts": { | ||
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js", | ||
"prepublish": "yarn run build", | ||
"start": "cross-env NODE_ENV=development webpack --config webpack.config.js --watch" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"cross-env": "^3.1.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/** @flow */ | ||
|
||
import Agent from 'src/backend/agent'; | ||
import Bridge from 'src/bridge'; | ||
import { initBackend } from 'src/backend'; | ||
import { installHook } from 'src/hook'; | ||
import setupNativeStyleEditor from 'src/backend/NativeStyleEditor/setupNativeStyleEditor'; | ||
import { | ||
MESSAGE_TYPE_GET_SAVED_PREFERENCES, | ||
MESSAGE_TYPE_SAVED_PREFERENCES, | ||
} from './constants'; | ||
|
||
function startActivation(contentWindow: window) { | ||
const { parent } = contentWindow; | ||
|
||
const onMessage = ({ data }) => { | ||
switch (data.type) { | ||
case MESSAGE_TYPE_SAVED_PREFERENCES: | ||
// This is the only message we're listening for, | ||
// so it's safe to cleanup after we've received it. | ||
contentWindow.removeEventListener('message', onMessage); | ||
|
||
const { appendComponentStack, componentFilters } = data; | ||
|
||
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack; | ||
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; | ||
|
||
// TRICKY | ||
// The backend entry point may be required in the context of an iframe or the parent window. | ||
// If it's required within the parent window, store the saved values on it as well, | ||
// since the injected renderer interface will read from window. | ||
// Technically we don't need to store them on the contentWindow in this case, | ||
// but it doesn't really hurt anything to store them there too. | ||
if (contentWindow !== window) { | ||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack; | ||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; | ||
} | ||
|
||
finishActivation(contentWindow); | ||
break; | ||
default: | ||
break; | ||
} | ||
}; | ||
|
||
contentWindow.addEventListener('message', onMessage); | ||
|
||
// The backend may be unable to read saved preferences directly, | ||
// because they are stored in localStorage within the context of the extension (on the frontend). | ||
// Instead it relies on the extension to pass preferences through. | ||
// Because we might be in a sandboxed iframe, we have to ask for them by way of postMessage(). | ||
parent.postMessage({ type: MESSAGE_TYPE_GET_SAVED_PREFERENCES }, '*'); | ||
} | ||
|
||
function finishActivation(contentWindow: window) { | ||
const { parent } = contentWindow; | ||
|
||
const bridge = new Bridge({ | ||
listen(fn) { | ||
const onMessage = event => { | ||
fn(event.data); | ||
}; | ||
contentWindow.addEventListener('message', onMessage); | ||
return () => { | ||
contentWindow.removeEventListener('message', onMessage); | ||
}; | ||
}, | ||
send(event: string, payload: any, transferable?: Array<any>) { | ||
parent.postMessage({ event, payload }, '*', transferable); | ||
}, | ||
}); | ||
|
||
const agent = new Agent(bridge); | ||
|
||
const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__; | ||
|
||
initBackend(hook, agent, contentWindow); | ||
|
||
// Setup React Native style editor if a renderer like react-native-web has injected it. | ||
if (!!hook.resolveRNStyle) { | ||
setupNativeStyleEditor( | ||
bridge, | ||
agent, | ||
hook.resolveRNStyle, | ||
hook.nativeStyleEditorValidAttributes | ||
); | ||
} | ||
} | ||
|
||
export function activate(contentWindow: window): void { | ||
startActivation(contentWindow); | ||
} | ||
|
||
export function initialize(contentWindow: window): void { | ||
installHook(contentWindow); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/** @flow */ | ||
|
||
export const MESSAGE_TYPE_GET_SAVED_PREFERENCES = | ||
'React::DevTools::getSavedPreferences'; | ||
export const MESSAGE_TYPE_SAVED_PREFERENCES = | ||
'React::DevTools::savedPreferences'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/** @flow */ | ||
|
||
import React, { forwardRef } from 'react'; | ||
import Bridge from 'src/bridge'; | ||
import Store from 'src/devtools/store'; | ||
import DevTools from 'src/devtools/views/DevTools'; | ||
import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils'; | ||
import { | ||
MESSAGE_TYPE_GET_SAVED_PREFERENCES, | ||
MESSAGE_TYPE_SAVED_PREFERENCES, | ||
} from './constants'; | ||
|
||
import type { FrontendBridge } from 'src/bridge'; | ||
import type { Props } from 'src/devtools/views/DevTools'; | ||
|
||
export function initialize( | ||
contentWindow: window | ||
): React$AbstractComponent<Props, mixed> { | ||
const onMessage = ({ data, origin, source }) => { | ||
switch (data.type) { | ||
case MESSAGE_TYPE_GET_SAVED_PREFERENCES: | ||
// This is the only message we're listening for, | ||
// so it's safe to cleanup after we've received it. | ||
window.removeEventListener('message', onMessage); | ||
|
||
// The renderer interface can't read saved preferences directly, | ||
// because they are stored in localStorage within the context of the extension. | ||
// Instead it relies on the extension to pass them through. | ||
contentWindow.postMessage( | ||
{ | ||
type: MESSAGE_TYPE_SAVED_PREFERENCES, | ||
appendComponentStack: getAppendComponentStack(), | ||
componentFilters: getSavedComponentFilters(), | ||
}, | ||
'*' | ||
); | ||
break; | ||
default: | ||
break; | ||
} | ||
}; | ||
|
||
window.addEventListener('message', onMessage); | ||
|
||
const bridge: FrontendBridge = new Bridge({ | ||
listen(fn) { | ||
const onMessage = ({ data }) => { | ||
fn(data); | ||
}; | ||
window.addEventListener('message', onMessage); | ||
return () => { | ||
window.removeEventListener('message', onMessage); | ||
}; | ||
}, | ||
send(event: string, payload: any, transferable?: Array<any>) { | ||
contentWindow.postMessage({ event, payload }, '*', transferable); | ||
}, | ||
}); | ||
|
||
const store: Store = new Store(bridge); | ||
|
||
const ForwardRef = forwardRef<Props, mixed>((props, ref) => ( | ||
<DevTools ref={ref} bridge={bridge} store={store} {...props} /> | ||
)); | ||
ForwardRef.displayName = 'DevTools'; | ||
|
||
return ForwardRef; | ||
} |
Oops, something went wrong.