Skip to content

Commit

Permalink
New NPM package react-devtools-inline (#363)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Aug 5, 2019
1 parent 6f1e283 commit dc8580e
Show file tree
Hide file tree
Showing 21 changed files with 440 additions and 116 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ shells/browser/firefox/build
shells/browser/shared/build
shells/dev/dist
packages/react-devtools-core/dist
packages/react-devtools-inline/dist
vendor
*.js.snap

Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
/shells/browser/firefox/*.pem
/shells/browser/shared/build
/packages/react-devtools-core/dist
/packages/react-devtools-inline/dist
/shells/dev/dist
build
/node_modules
/packages/react-devtools-core/node_modules/
/packages/react-devtools/node_modules/
/packages/react-devtools-core/node_modules
/packages/react-devtools-inline/node_modules
/packages/react-devtools/node_modules
npm-debug.log
yarn-error.log
.DS_Store
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"scripts": {
"build:core:backend": "cd ./packages/react-devtools-core && yarn build:backend",
"build:core:standalone": "cd ./packages/react-devtools-core && yarn build:standalone",
"build:core": "cd ./packages/react-devtools-core && yarn build",
"build:inline": "cd ./packages/react-devtools-inline && yarn build",
"build:demo": "cd ./shells/dev && cross-env NODE_ENV=development cross-env TARGET=remote webpack --config webpack.config.js",
"build:extension": "cross-env NODE_ENV=production yarn run build:extension:chrome && yarn run build:extension:firefox",
"build:extension:dev": "cross-env NODE_ENV=development yarn run build:extension:chrome && yarn run build:extension:firefox",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-devtools-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-devtools-core",
"version": "4.0.0-alpha.6",
"version": "4.0.0-alpha.7",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"main": "./dist/backend.js",
Expand Down
134 changes: 134 additions & 0 deletions packages/react-devtools-inline/README.md
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',
},
'*'
);
};
```
1 change: 1 addition & 0 deletions packages/react-devtools-inline/backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/backend');
1 change: 1 addition & 0 deletions packages/react-devtools-inline/frontend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/frontend');
25 changes: 25 additions & 0 deletions packages/react-devtools-inline/package.json
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"
}
}
96 changes: 96 additions & 0 deletions packages/react-devtools-inline/src/backend.js
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);
}
6 changes: 6 additions & 0 deletions packages/react-devtools-inline/src/constants.js
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';
68 changes: 68 additions & 0 deletions packages/react-devtools-inline/src/frontend.js
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;
}
Loading

0 comments on commit dc8580e

Please sign in to comment.