New NPM package react-devtools-inline (#363)

This commit is contained in:
Brian Vaughn 2019-08-05 10:09:26 -07:00 committed by GitHub
parent 6f1e283b76
commit dc8580e64d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 440 additions and 116 deletions

View File

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

6
.gitignore vendored
View File

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

View File

@ -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",

View File

@ -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",

View File

@ -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',
},
'*'
);
};
```

View File

@ -0,0 +1 @@
module.exports = require('./dist/backend');

View File

@ -0,0 +1 @@
module.exports = require('./dist/frontend');

View File

@ -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"
}
}

View File

@ -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);
}

View File

@ -0,0 +1,6 @@
/** @flow */
export const MESSAGE_TYPE_GET_SAVED_PREFERENCES =
'React::DevTools::getSavedPreferences';
export const MESSAGE_TYPE_SAVED_PREFERENCES =
'React::DevTools::savedPreferences';

View File

@ -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;
}

View File

@ -0,0 +1,74 @@
const { resolve } = require('path');
const { DefinePlugin } = require('webpack');
const { getGitHubURL, getVersionString } = require('../../shells/utils');
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
console.error('NODE_ENV not set');
process.exit(1);
}
const __DEV__ = true; // NODE_ENV === 'development';
const GITHUB_URL = getGitHubURL();
const DEVTOOLS_VERSION = getVersionString();
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: false,
entry: {
backend: './src/backend.js',
frontend: './src/frontend.js',
},
output: {
path: __dirname + '/dist',
filename: '[name].js',
library: '[name]',
libraryTarget: 'commonjs2',
},
resolve: {
alias: {
src: resolve(__dirname, '../../src'),
},
},
externals: {
react: 'react',
'react-dom': 'react-dom',
scheduler: 'scheduler',
},
plugins: [
new DefinePlugin({
__DEV__: __DEV__,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
'process.env.NODE_ENV': `"${NODE_ENV}"`,
}),
],
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
configFile: resolve(__dirname, '../../babel.config.js'),
},
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: true,
localIdentName: '[local]___[hash:base64:5]',
},
},
],
},
],
},
};

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools",
"version": "4.0.0-alpha.6",
"version": "4.0.0-alpha.7",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"repository": {
@ -25,7 +25,7 @@
"electron": "^5.0.0",
"ip": "^1.1.4",
"minimist": "^1.2.0",
"react-devtools-core": "4.0.0-alpha.6",
"react-devtools-core": "4.0.0-alpha.7",
"update-notifier": "^2.1.0"
}
}

View File

@ -63,7 +63,6 @@ function createPanelIfReactLoaded() {
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let settingsPortalContainer = null;
let cloneStyleTags = null;
let mostRecentOverrideTab = null;
@ -142,7 +141,6 @@ function createPanelIfReactLoaded() {
componentsPortalContainer,
overrideTab,
profilerPortalContainer,
settingsPortalContainer,
showTabBar: false,
showWelcomeToTheNewDevToolsDialog: true,
store,

View File

@ -1,37 +0,0 @@
/** @flow */
import Agent from 'src/backend/agent';
import Bridge from 'src/bridge';
import { initBackend } from 'src/backend';
import setupNativeStyleEditor from 'src/backend/NativeStyleEditor/setupNativeStyleEditor';
const bridge = new Bridge({
listen(fn) {
const listener = event => {
fn(event.data);
};
window.addEventListener('message', listener);
return () => {
window.removeEventListener('message', listener);
};
},
send(event: string, payload: any, transferable?: Array<any>) {
window.parent.postMessage({ event, payload }, '*', transferable);
},
});
const agent = new Agent(bridge);
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
initBackend(hook, agent, window.parent);
// 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
);
}

View File

@ -3,24 +3,21 @@
import { createElement } from 'react';
// $FlowFixMe Flow does not yet know about createRoot()
import { unstable_createRoot as createRoot } from 'react-dom';
import Bridge from 'src/bridge';
import { installHook } from 'src/hook';
import {
activate as activateBackend,
initialize as initializeBackend,
} from 'react-devtools-inline/backend';
import { initialize as initializeFrontend } from 'react-devtools-inline/frontend';
import { initDevTools } from 'src/devtools';
import Store from 'src/devtools/store';
import DevTools from 'src/devtools/views/DevTools';
import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
const iframe = ((document.getElementById('target'): any): HTMLIFrameElement);
const { contentDocument, contentWindow } = iframe;
// The renderer interface can't read saved component filters directly,
// because they are stored in localStorage within the context of the extension.
// Instead it relies on the extension to pass filters through.
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getSavedComponentFilters();
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = getAppendComponentStack();
// Helps with positioning Overlay UI.
contentWindow.__REACT_DEVTOOLS_TARGET_WINDOW__ = window;
installHook(contentWindow);
initializeBackend(contentWindow);
const container = ((document.getElementById('devtools'): any): HTMLElement);
@ -48,46 +45,21 @@ mountButton.addEventListener('click', function() {
inject('dist/app.js', () => {
initDevTools({
connect(cb) {
const bridge = new Bridge({
listen(fn) {
const listener = ({ data }) => {
fn(data);
};
// Preserve the reference to the window we subscribe to, so we can unsubscribe from it when required.
const contentWindowParent = contentWindow.parent;
contentWindowParent.addEventListener('message', listener);
return () => {
contentWindowParent.removeEventListener('message', listener);
};
},
send(event: string, payload: any, transferable?: Array<any>) {
contentWindow.postMessage({ event, payload }, '*', transferable);
},
});
const DevTools = initializeFrontend(contentWindow);
cb(bridge);
const store = new Store(bridge);
// Activate the backend only once the DevTools frontend Store has been initialized.
// Otherwise the Store may miss important initial tree op codes.
activateBackend(contentWindow);
const root = createRoot(container);
const batch = root.createBatch();
batch.render(
root.render(
createElement(DevTools, {
bridge,
browserTheme: 'light',
showTabBar: true,
showWelcomeToTheNewDevToolsDialog: true,
store,
warnIfLegacyBackendDetected: true,
})
);
batch.then(() => {
batch.commit();
// Initialize the backend only once the DevTools frontend Store has been initialized.
// Otherwise the Store may miss important initial tree op codes.
inject('dist/backend.js');
});
},
onReload(reloadFn) {

View File

@ -16,6 +16,8 @@ if (!TARGET) {
const __DEV__ = NODE_ENV === 'development';
const root = resolve(__dirname, '../..');
const GITHUB_URL = getGitHubURL();
const DEVTOOLS_VERSION = getVersionString();
@ -24,12 +26,15 @@ const config = {
devtool: false,
entry: {
app: './app/index.js',
backend: './src/backend.js',
devtools: './src/devtools.js',
},
resolve: {
alias: {
src: resolve(__dirname, '../../src'),
'react-devtools-inline': resolve(
root,
'packages/react-devtools-inline/src/'
),
src: resolve(root, 'src'),
},
},
plugins: [
@ -45,7 +50,7 @@ const config = {
test: /\.js$/,
loader: 'babel-loader',
options: {
configFile: require.resolve('../../babel.config.js'),
configFile: resolve(root, 'babel.config.js'),
},
},
{

View File

@ -153,20 +153,11 @@ export default class Overlay {
constructor() {
// Find the root window, because overlays are positioned relative to it.
let currentWindow = window;
while (currentWindow !== currentWindow.parent) {
currentWindow = currentWindow.parent;
}
let currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
this.window = currentWindow;
// When opened in shells/dev, the tooltip should be bound by the app iframe, not by the topmost window.
let tipBoundsWindow = window;
while (
tipBoundsWindow !== tipBoundsWindow.parent &&
!tipBoundsWindow.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')
) {
tipBoundsWindow = tipBoundsWindow.parent;
}
let tipBoundsWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
this.tipBoundsWindow = tipBoundsWindow;
const doc = currentWindow.document;

View File

@ -114,7 +114,7 @@ export default class Store extends EventEmitter<{|
// These options may be initially set by a confiugraiton option when constructing the Store.
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
_supportsNativeInspection: boolean = false;
_supportsNativeInspection: boolean = true;
_supportsProfiling: boolean = false;
_supportsReloadAndProfile: boolean = false;

View File

@ -59,7 +59,6 @@ export type Props = {|
// but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
componentsPortalContainer?: Element,
profilerPortalContainer?: Element,
settingsPortalContainer?: Element,
|};
const componentsTab = {
@ -85,7 +84,6 @@ export default function DevTools({
componentsPortalContainer,
overrideTab,
profilerPortalContainer,
settingsPortalContainer,
showTabBar = false,
showWelcomeToTheNewDevToolsDialog = false,
store,
@ -113,7 +111,6 @@ export default function DevTools({
browserTheme={browserTheme}
componentsPortalContainer={componentsPortalContainer}
profilerPortalContainer={profilerPortalContainer}
settingsPortalContainer={settingsPortalContainer}
>
<ViewElementSourceContext.Provider value={viewElementSource}>
<TreeContextController>

View File

@ -41,7 +41,6 @@ type Props = {|
children: React$Node,
componentsPortalContainer?: Element,
profilerPortalContainer?: Element,
settingsPortalContainer?: Element,
|};
function SettingsContextController({
@ -49,7 +48,6 @@ function SettingsContextController({
children,
componentsPortalContainer,
profilerPortalContainer,
settingsPortalContainer,
}: Props) {
const bridge = useContext(BridgeContext);
@ -82,18 +80,8 @@ function SettingsContextController({
.documentElement: any): HTMLElement)
);
}
if (settingsPortalContainer != null) {
array.push(
((settingsPortalContainer.ownerDocument
.documentElement: any): HTMLElement)
);
}
return array;
}, [
componentsPortalContainer,
profilerPortalContainer,
settingsPortalContainer,
]);
}, [componentsPortalContainer, profilerPortalContainer]);
const computedStyle = getComputedStyle((document.body: any));
const comfortableLineHeight = parseInt(