Initial commit
This commit is contained in:
7
.babelrc
Normal file
7
.babelrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"plugins": [
|
||||
["@babel/plugin-transform-flow-strip-types"],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": false }]
|
||||
],
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"]
|
||||
}
|
||||
31
.flowconfig
Normal file
31
.flowconfig
Normal file
@@ -0,0 +1,31 @@
|
||||
[ignore]
|
||||
.*/react/node_modules/.*
|
||||
.*/electron/node_modules/.*
|
||||
.*node_modules/archiver-utils
|
||||
.*node_modules/babel.*
|
||||
.*node_modules/browserify-zlib/.*
|
||||
.*node_modules/classnames.*
|
||||
.*node_modules/gh-pages/.*
|
||||
.*node_modules/invariant/.*
|
||||
.*node_modules/json-loader.*
|
||||
.*node_modules/json5.*
|
||||
.*node_modules/node-libs-browser.*
|
||||
.*node_modules/webpack.*
|
||||
.*node_modules/fbjs/flow.*
|
||||
.*node_modules/web-ext.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
./flow.js
|
||||
|
||||
[lints]
|
||||
|
||||
[options]
|
||||
esproposal.class_instance_fields=enable
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore
|
||||
module.name_mapper='^src' ->'<PROJECT_ROOT>/src'
|
||||
|
||||
[strict]
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/shells/chrome.crx
|
||||
/shells/chrome.pem
|
||||
/shells/firefox/*.xpi
|
||||
build
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
11
flow.js
Normal file
11
flow.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
declare module 'events' {
|
||||
declare class EventEmitter {
|
||||
addListener: (type: string, fn: Function) => void;
|
||||
emit: (type: string, data: any) => void;
|
||||
removeListener: (type: string, fn: Function) => void;
|
||||
}
|
||||
|
||||
declare export default typeof EventEmitter;
|
||||
}
|
||||
80
package.json
Normal file
80
package.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.1.6",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"adm-zip": "^0.4.7",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "6",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"classnames": "2.2.1",
|
||||
"cli-spinners": "^1.0.0",
|
||||
"clipboard-js": "^0.3.3",
|
||||
"css-loader": "^1.0.1",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"es6-symbol": "3.0.2",
|
||||
"eslint": "2.10.2",
|
||||
"eslint-plugin-react": "5.1.1",
|
||||
"events": "^3.0.0",
|
||||
"fbjs": "0.5.1",
|
||||
"fbjs-scripts": "0.7.0",
|
||||
"flow-bin": "^0.91.0",
|
||||
"fs-extra": "^3.0.1",
|
||||
"gh-pages": "^1.0.0",
|
||||
"immutable": "3.7.6",
|
||||
"jest": "22.1.4",
|
||||
"log-update": "^2.0.0",
|
||||
"lru-cache": "^4.1.3",
|
||||
"memoize-one": "^3.1.1",
|
||||
"node-libs-browser": "0.5.3",
|
||||
"nullthrows": "^1.0.0",
|
||||
"object-assign": "4.0.1",
|
||||
"prop-types": "^15.6.1",
|
||||
"react": "^16.8.0-alpha.1",
|
||||
"react-color": "^2.11.7",
|
||||
"react-dom": "^16.8.0-alpha.1",
|
||||
"react-portal": "^3.1.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.1.1",
|
||||
"semver": "^5.5.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.26.0",
|
||||
"webpack-cli": "^3.1.2"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": "facebook/react-devtools",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build:extension": "yarn run build:extension:chrome && yarn run build:extension:firefox",
|
||||
"build:extension:chrome": "node ./shells/chrome/build",
|
||||
"build:extension:firefox": "node ./shells/firefox/build",
|
||||
"build:standalone": "cd packages/react-devtools-core && yarn run build",
|
||||
"deploy": "cd ./shells/theme-preview && ./build.sh && gh-pages -d .",
|
||||
"lint": "eslint .",
|
||||
"start": "cd ./shells/dev && open ./index.html && webpack --config webpack.config.js --watch",
|
||||
"test": "jest",
|
||||
"test:chrome": "node ./shells/chrome/test",
|
||||
"test:firefox": "node ./shells/firefox/test",
|
||||
"test:standalone": "cd packages/react-devtools && yarn start",
|
||||
"typecheck": "flow check"
|
||||
},
|
||||
"jest": {
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/shells"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"chrome-launch": "^1.1.4",
|
||||
"firefox-profile": "^1.0.2",
|
||||
"lerna": "^2.8.0",
|
||||
"web-ext": "^1.10.1"
|
||||
}
|
||||
}
|
||||
6
shells/browser/chrome/manifest.json
Normal file
6
shells/browser/chrome/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "React v16 DevTools",
|
||||
"version": "0.1",
|
||||
"description": "DevTools for React version 16.0+",
|
||||
"manifest_version": 2
|
||||
}
|
||||
36
shells/browser/webpack.backend.js
Normal file
36
shells/browser/webpack.backend.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {readFileSync} = require('fs');
|
||||
const {resolve} = require('path');
|
||||
|
||||
const __DEV__ = process.env.NODE_ENV !== 'production';
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
filename: '[name].js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: JSON.parse(readFileSync(resolve(__dirname, '../../.babelrc'))),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
63
shells/browser/webpack.config.js
Normal file
63
shells/browser/webpack.config.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {readFileSync} = require('fs');
|
||||
const {resolve} = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const __DEV__ = process.env.NODE_ENV !== 'production';
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
|
||||
entry: {
|
||||
background: './src/background.js',
|
||||
contentScript: './src/contentScript.js',
|
||||
inject: './src/GlobalHook.js',
|
||||
main: './src/main.js',
|
||||
panel: './src/panel.js',
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
filename: '[name].js',
|
||||
},
|
||||
plugins: __DEV__ ? [] : [
|
||||
// Ensure we get production React
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: JSON.parse(readFileSync(resolve(__dirname, '../../.babelrc'))),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader',
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
modules: true,
|
||||
localIdentName: '[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
150
shells/createConfig.js
Normal file
150
shells/createConfig.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
|
||||
module.exports = (config, target = { chrome: 52, firefox: 48 }) => {
|
||||
const bubleOptions = {
|
||||
target,
|
||||
objectAssign: 'Object.assign',
|
||||
transforms: {
|
||||
forOf: false,
|
||||
modules: false
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
resolve: {
|
||||
alias: {
|
||||
src: path.resolve(__dirname, '../src'),
|
||||
views: path.resolve(__dirname, '../src/devtools/views'),
|
||||
components: path.resolve(__dirname, '../src/devtools/components'),
|
||||
filters: path.resolve(__dirname, '../src/devtools/filters')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'buble-loader',
|
||||
exclude: /node_modules|vue\/dist|vuex\/dist/,
|
||||
options: bubleOptions
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
preserveWhitespace: false
|
||||
},
|
||||
transpileOptions: bubleOptions
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'postcss-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'postcss-loader',
|
||||
'stylus-loader',
|
||||
{
|
||||
loader: 'style-resources-loader',
|
||||
options: {
|
||||
patterns: [
|
||||
path.resolve(__dirname, '../src/devtools/style/imports.styl')
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|woff2)$/,
|
||||
loader: 'url-loader?limit=0'
|
||||
}
|
||||
]
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
...(process.env.VUE_DEVTOOL_TEST ? [] : [new FriendlyErrorsPlugin()]),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.RELEASE_CHANNEL': JSON.stringify(process.env.RELEASE_CHANNEL || 'stable')
|
||||
})
|
||||
],
|
||||
devServer: {
|
||||
port: process.env.PORT
|
||||
},
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const UglifyPlugin = require('uglifyjs-webpack-plugin')
|
||||
baseConfig.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
})
|
||||
)
|
||||
baseConfig.optimization = {
|
||||
minimizer: [
|
||||
new UglifyPlugin({
|
||||
exclude: /backend/,
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
// turn off flags with small gains to speed up minification
|
||||
arrows: false,
|
||||
collapse_vars: false, // 0.3kb
|
||||
comparisons: false,
|
||||
computed_props: false,
|
||||
hoist_funs: false,
|
||||
hoist_props: false,
|
||||
hoist_vars: false,
|
||||
inline: false,
|
||||
loops: false,
|
||||
negate_iife: false,
|
||||
properties: false,
|
||||
reduce_funcs: false,
|
||||
reduce_vars: false,
|
||||
switches: false,
|
||||
toplevel: false,
|
||||
typeofs: false,
|
||||
|
||||
// a few flags with noticable gains/speed ratio
|
||||
// numbers based on out of the box vendor bundle
|
||||
booleans: true, // 0.7kb
|
||||
if_return: true, // 0.4kb
|
||||
sequences: true, // 0.7kb
|
||||
unused: true, // 2.3kb
|
||||
|
||||
// required features to drop conditional branches
|
||||
conditionals: true,
|
||||
dead_code: true,
|
||||
evaluate: true
|
||||
},
|
||||
mangle: {
|
||||
safari10: true
|
||||
}
|
||||
},
|
||||
sourceMap: false,
|
||||
cache: true,
|
||||
parallel: true
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return merge(baseConfig, config)
|
||||
}
|
||||
6
shells/dev/app/App.css
Normal file
6
shells/dev/app/App.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.App {
|
||||
/* GitHub.com frontend fonts */
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
54
shells/dev/app/ElementTypes/index.js
Normal file
54
shells/dev/app/ElementTypes/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
forwardRef,
|
||||
lazy,
|
||||
memo,
|
||||
Component,
|
||||
ConcurrentMode,
|
||||
Fragment,
|
||||
Profiler,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
} from 'react';
|
||||
|
||||
class ClassComponent extends Component {
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function FunctionComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MemoFunctionComponent = memo(FunctionComponent);
|
||||
|
||||
const ForwardRefComponent = forwardRef((props, ref) => (
|
||||
<ClassComponent ref={ref} {...props} />
|
||||
));
|
||||
|
||||
const LazyComponent = lazy(() => Promise.resolve({
|
||||
default: FunctionComponent,
|
||||
}));
|
||||
|
||||
export default function ElementTypes() {
|
||||
return (
|
||||
<Profiler id="test" onRender={() => {}}>
|
||||
<Fragment>
|
||||
<StrictMode>
|
||||
<ConcurrentMode>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ClassComponent />
|
||||
<FunctionComponent />
|
||||
<MemoFunctionComponent />
|
||||
<ForwardRefComponent />
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
</ConcurrentMode>
|
||||
</StrictMode>
|
||||
</Fragment>
|
||||
</Profiler>
|
||||
)
|
||||
}
|
||||
22
shells/dev/app/ToDoList/List.css
Normal file
22
shells/dev/app/ToDoList/List.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.Header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.Input {
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
padding: 0.25rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.List {
|
||||
margin: 0.5rem 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
94
shells/dev/app/ToDoList/List.js
Normal file
94
shells/dev/app/ToDoList/List.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import ListItem from './ListItem';
|
||||
import styles from './List.css';
|
||||
|
||||
export type Item = {|
|
||||
id: number,
|
||||
isComplete: boolean,
|
||||
text: string,
|
||||
|};
|
||||
|
||||
type Props = {||};
|
||||
|
||||
export default function List({}: Props) {
|
||||
const [newItemText, setNewItemText] = useState<string>('');
|
||||
const [items, setItems] = useState<Array<Item>>([
|
||||
{id: 1, isComplete: true, text: "First"},
|
||||
{id: 2, isComplete: true, text: "Second"},
|
||||
{id: 3, isComplete: false, text: "Third"},
|
||||
]);
|
||||
const [uid, setUID] = useState<number>(4);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (newItemText !== '') {
|
||||
setItems([...items, {
|
||||
id: uid,
|
||||
isComplete: false,
|
||||
text: newItemText,
|
||||
}]);
|
||||
setUID(uid + 1);
|
||||
setNewItemText('');
|
||||
}
|
||||
}, [newItemText]);
|
||||
|
||||
const handleKeyPress = useCallback(event => {
|
||||
if (event.key === 'Enter') {
|
||||
handleClick();
|
||||
}
|
||||
}, [handleClick]);
|
||||
|
||||
const handleChange = useCallback(event => {
|
||||
setNewItemText(event.currentTarget.value);
|
||||
}, [setNewItemText]);
|
||||
|
||||
const removeItem = useCallback(itemToRemove => {
|
||||
setItems(
|
||||
items.filter(item => item !== itemToRemove)
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const toggleItem = useCallback(itemToToggle => {
|
||||
const index = items.indexOf(itemToToggle);
|
||||
|
||||
setItems(
|
||||
items
|
||||
.slice(0, index)
|
||||
.concat({
|
||||
...itemToToggle,
|
||||
isComplete: !itemToToggle.isComplete,
|
||||
})
|
||||
.concat(items.slice(index + 1))
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Header}>List</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New list item..."
|
||||
className={styles.Input}
|
||||
value={newItemText}
|
||||
onChange={handleChange}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
disabled={newItemText === ''}
|
||||
onClick={handleClick}
|
||||
>➕</button>
|
||||
<ul className={styles.List}>
|
||||
{items.map(item => (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
removeItem={removeItem}
|
||||
toggleItem={toggleItem}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
23
shells/dev/app/ToDoList/ListItem.css
Normal file
23
shells/dev/app/ToDoList/ListItem.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.ListItem {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.Input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Label {
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
color: #555;
|
||||
}
|
||||
.Label:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
padding: 0.25rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
36
shells/dev/app/ToDoList/ListItem.js
Normal file
36
shells/dev/app/ToDoList/ListItem.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import styles from './ListItem.css';
|
||||
|
||||
import type {Item} from './List';
|
||||
|
||||
type Props = {|
|
||||
item: Item,
|
||||
removeItem: (item: Item) => void,
|
||||
toggleItem: (item: Item) => void,
|
||||
|};
|
||||
|
||||
export default function ListItem({ item, removeItem, toggleItem }: Props) {
|
||||
const handleDelete = useCallback(() => {
|
||||
removeItem(item);
|
||||
}, [item, removeItem]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
toggleItem(item);
|
||||
}, [item, toggleItem]);
|
||||
|
||||
return (
|
||||
<li className={styles.ListItem}>
|
||||
<button className={styles.IconButton} onClick={handleDelete}>🗑</button>
|
||||
<label className={styles.Label}>
|
||||
<input
|
||||
className={styles.Input}
|
||||
checked={item.isComplete}
|
||||
onChange={handleToggle}
|
||||
type="checkbox"
|
||||
/> {item.text}
|
||||
</label>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
5
shells/dev/app/ToDoList/index.js
Normal file
5
shells/dev/app/ToDoList/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import List from './List';
|
||||
|
||||
export default List;
|
||||
15
shells/dev/app/app.js
Normal file
15
shells/dev/app/app.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import List from './ToDoList';
|
||||
import ElementTypes from './ElementTypes';
|
||||
import styles from './App.css';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className={styles.App}>
|
||||
<List />
|
||||
<ElementTypes />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
shells/dev/app/index.js
Normal file
11
shells/dev/app/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @flow */
|
||||
|
||||
import { createElement } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
render(createElement(App), container);
|
||||
|
||||
((document.body: any): HTMLBodyElement).appendChild(container);
|
||||
44
shells/dev/index.html
Normal file
44
shells/dev/index.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>React DevTools</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
#target {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
#devtools {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
max-height: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- React test app (shells/dev/app) is injected here -->
|
||||
<!-- DevTools backend (shells/dev/src) is injected here -->
|
||||
<!-- global "hook" is defined on the iframe's contentWindow -->
|
||||
<iframe id="target"></iframe>
|
||||
|
||||
<!-- DevTools frontend UI (shells/dev/src) renders here -->
|
||||
<div id="devtools"></div>
|
||||
|
||||
<!-- This script installs the hook, injects the backend, and renders the DevTools UI -->
|
||||
<script src="build/devtools.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
shells/dev/src/backend.js
Normal file
21
shells/dev/src/backend.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @flow */
|
||||
|
||||
import Agent from 'src/backend/agent';
|
||||
import Bridge from 'src/bridge'
|
||||
import { initBackend } from 'src/backend';
|
||||
|
||||
const bridge = new Bridge({
|
||||
listen (fn) {
|
||||
window.addEventListener('message', event => {
|
||||
fn(event.data);
|
||||
});
|
||||
},
|
||||
send (data) {
|
||||
window.parent.postMessage(data, '*')
|
||||
}
|
||||
})
|
||||
|
||||
const agent = new Agent();
|
||||
agent.addBridge(bridge);
|
||||
|
||||
initBackend(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, agent);
|
||||
52
shells/dev/src/devtools.js
Normal file
52
shells/dev/src/devtools.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/** @flow */
|
||||
|
||||
import { createElement } from 'react';
|
||||
import {render } from 'react-dom';
|
||||
import Bridge from 'src/bridge';
|
||||
import { installHook } from 'src/hook';
|
||||
import { initDevTools } from 'src/devtools';
|
||||
import App from 'src/devtools/views/App';
|
||||
|
||||
const iframe = ((document.getElementById('target'): any): HTMLIFrameElement);
|
||||
|
||||
const {contentDocument, contentWindow} = iframe;
|
||||
|
||||
installHook(contentWindow);
|
||||
|
||||
initDevTools({
|
||||
connect(cb) {
|
||||
inject('./build/backend.js', () => {
|
||||
const bridge = new Bridge({
|
||||
listen(fn) {
|
||||
contentWindow.parent.addEventListener('message', ({ data }) => {
|
||||
fn(data)
|
||||
});
|
||||
},
|
||||
send(data) {
|
||||
contentWindow.postMessage(data, '*');
|
||||
}
|
||||
});
|
||||
|
||||
cb(bridge);
|
||||
|
||||
render(
|
||||
createElement(App, {bridge}),
|
||||
((document.getElementById('devtools'): any): HTMLElement),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
onReload(reloadFn) {
|
||||
iframe.onload = reloadFn;
|
||||
}
|
||||
});
|
||||
|
||||
inject('./build/app.js');
|
||||
|
||||
function inject(sourcePath, callback) {
|
||||
const script = contentDocument.createElement('script')
|
||||
script.onload = callback;
|
||||
script.src = sourcePath;
|
||||
|
||||
((contentDocument.body: any): HTMLBodyElement).appendChild(script);
|
||||
}
|
||||
48
shells/dev/webpack.config.js
Normal file
48
shells/dev/webpack.config.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const {readFileSync} = require('fs');
|
||||
const {resolve} = require('path');
|
||||
|
||||
// TODO Share Webpack configs like alias
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
devtool: false,
|
||||
entry: {
|
||||
app: './app/index.js',
|
||||
backend: './src/backend.js',
|
||||
devtools: './src/devtools.js',
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
filename: '[name].js',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
src: resolve(__dirname, '../../src'),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: JSON.parse(readFileSync(resolve(__dirname, '../../.babelrc'))),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader',
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
modules: true,
|
||||
localIdentName: '[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
159
src/backend/agent.js
Normal file
159
src/backend/agent.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// @flow
|
||||
|
||||
import nullthrows from 'nullthrows';
|
||||
import EventEmitter from 'events';
|
||||
import {guid} from './utils';
|
||||
import {ElementTypeOtherOrUnknown} from 'src/devtools/types';
|
||||
|
||||
import type {Fiber, RendererData, RendererID, RendererInterface} from './types';
|
||||
import type {Bridge} from '../types';
|
||||
import type {Element} from 'src/devtools/types';
|
||||
|
||||
export default class Agent extends EventEmitter {
|
||||
fiberToID: WeakMap<Fiber, string> = new WeakMap();
|
||||
idToElement: Map<string, Element> = new Map();
|
||||
idToFiber: Map<string, Fiber> = new Map();
|
||||
idToRendererData: Map<string, RendererData> = new Map();
|
||||
idToRendererID: Map<string, RendererID> = new Map();
|
||||
rendererInterfaces: {[key: RendererID]: RendererInterface} = {};
|
||||
roots: Set<RendererID> = new Set();
|
||||
|
||||
addBridge(bridge: Bridge) {
|
||||
// TODO Listen to bridge for things like selection.
|
||||
// bridge.on('...'), this...);
|
||||
|
||||
this.addListener('root', id => bridge.send('root', id));
|
||||
this.addListener('mount', data => bridge.send('mount', data));
|
||||
this.addListener('update', data => bridge.send('update', data));
|
||||
this.addListener('unmount', data => bridge.send('unmount', data));
|
||||
// TODO Add other methods for e.g. profiling.
|
||||
}
|
||||
|
||||
setRendererInterface(rendererID: RendererID, rendererInterface: RendererInterface) {
|
||||
this.rendererInterfaces[rendererID] = rendererInterface;
|
||||
}
|
||||
|
||||
_getId(fiber: Fiber): string {
|
||||
if (typeof fiber !== 'object' || !fiber) {
|
||||
return fiber;
|
||||
}
|
||||
if (!this.fiberToID.has(fiber)) {
|
||||
this.fiberToID.set(fiber, guid());
|
||||
this.idToFiber.set(
|
||||
nullthrows(this.fiberToID.get(fiber)),
|
||||
fiber
|
||||
);
|
||||
}
|
||||
return nullthrows(this.fiberToID.get(fiber));
|
||||
}
|
||||
|
||||
_crawl(parent: Element, id: string): void {
|
||||
const data: RendererData = ((this.idToRendererData.get(id): any): RendererData);
|
||||
if (data.type === ElementTypeOtherOrUnknown) {
|
||||
data.children.forEach(childFiber => {
|
||||
if (childFiber !== null) {
|
||||
this._crawl(parent, this._getId(childFiber))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
parent.children.push(id);
|
||||
|
||||
this._createOrUpdateElement(id, data);
|
||||
}
|
||||
}
|
||||
|
||||
_createOrUpdateElement(id: string, data: RendererData): void {
|
||||
const prevElement: ?Element = this.idToElement.get(id);
|
||||
const nextElement: Element = {
|
||||
id,
|
||||
key: data.key,
|
||||
displayName: data.displayName,
|
||||
children: [],
|
||||
type: data.type,
|
||||
};
|
||||
|
||||
this.idToElement.set(id, nextElement);
|
||||
|
||||
data.children.forEach(childFiber => {
|
||||
if (childFiber !== null) {
|
||||
this._crawl(nextElement, this._getId(childFiber))
|
||||
}
|
||||
});
|
||||
|
||||
if (prevElement == null) {
|
||||
console.log('%cAgent%c emit("mount")', 'color: blue; font-weight: bold;', 'font-weight: bold;', id, nextElement)
|
||||
this.emit('mount', nextElement);
|
||||
} else if (!areElementsEqual(prevElement, nextElement)) {
|
||||
console.log('%cAgent%c emit("update")', 'color: blue; font-weight: bold;', 'font-weight: bold;', id, nextElement)
|
||||
this.emit('update', nextElement);
|
||||
}
|
||||
}
|
||||
|
||||
onHookMount = ({data, fiber, renderer}: {data: RendererData, fiber: Fiber, renderer: RendererID}) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
this.idToRendererData.set(id, data);
|
||||
this.idToRendererID.set(id, renderer);
|
||||
};
|
||||
|
||||
onHookRootCommitted = ({data, fiber, renderer}: {data: RendererData, fiber: Fiber, renderer: RendererID}) => {
|
||||
const id = this._getId(fiber);
|
||||
console.log('%cAgent%c onHookRootCommitted()', 'color: blue; font-weight: bold;', 'font-weight: bold;', id);
|
||||
|
||||
this._createOrUpdateElement(id, data);
|
||||
|
||||
if (!this.roots.has(id)) {
|
||||
this.roots.add(id);
|
||||
|
||||
this.emit('root', id);
|
||||
}
|
||||
};
|
||||
|
||||
onHookUnmount = ({fiber}: {fiber: Fiber}) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
if (this.roots.has(id)) {
|
||||
this.roots.delete(id);
|
||||
this.emit('rootUnmounted', id);
|
||||
}
|
||||
|
||||
if (this.idToElement.has(id)) {
|
||||
this.idToElement.delete(id);
|
||||
console.log('%cAgent%c emit("unmount")', 'color: blue; font-weight: bold;', 'font-weight: bold;', id)
|
||||
this.emit('unmount', id);
|
||||
}
|
||||
|
||||
this.fiberToID.delete(fiber);
|
||||
this.idToRendererData.delete(id);
|
||||
this.idToRendererID.delete(id);
|
||||
};
|
||||
|
||||
onHookUpdate = ({data, fiber}: {data: RendererData, fiber: Fiber}) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
this.idToRendererData.set(id, data);
|
||||
};
|
||||
}
|
||||
|
||||
function areElementsEqual(prevElement: ?Element, nextElement: ?Element): boolean {
|
||||
if (!prevElement || !nextElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevElement.key !== nextElement.key ||
|
||||
prevElement.displayName !== nextElement.displayName ||
|
||||
prevElement.children.length !== nextElement.children.length ||
|
||||
prevElement.type !== nextElement.type
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < prevElement.children.length; i++) {
|
||||
if (prevElement.children[i] !== nextElement.children[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
51
src/backend/index.js
Normal file
51
src/backend/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
|
||||
import type {Hook} from './types';
|
||||
import type {Bridge} from '../types';
|
||||
import Agent from './agent';
|
||||
|
||||
import { attach } from './renderer';
|
||||
|
||||
export function initBackend(hook: Hook, agent: Agent): void {
|
||||
const subs = [
|
||||
hook.sub('renderer-attached', ({id, renderer, rendererInterface}) => {
|
||||
agent.setRendererInterface(id, rendererInterface);
|
||||
rendererInterface.walkTree();
|
||||
}),
|
||||
|
||||
hook.sub('rootCommitted', agent.onHookRootCommitted),
|
||||
hook.sub('mount', agent.onHookMount),
|
||||
hook.sub('update', agent.onHookUpdate),
|
||||
hook.sub('unmount', agent.onHookUnmount),
|
||||
|
||||
// TODO Add additional subscriptions required for profiling mode
|
||||
];
|
||||
|
||||
const attachRenderer = (id, renderer) => {
|
||||
const rendererInterface = attach(hook, id, renderer);
|
||||
hook.rendererInterfaces[id] = rendererInterface;
|
||||
hook.emit('renderer-attached', {
|
||||
id,
|
||||
renderer,
|
||||
rendererInterface
|
||||
});
|
||||
};
|
||||
|
||||
// Connect renderers that have already injected themselves.
|
||||
for (let id in hook.renderers) {
|
||||
const renderer = hook.renderers[id];
|
||||
attachRenderer(id, renderer);
|
||||
}
|
||||
|
||||
// Connect any new renderers that injected themselves.
|
||||
hook.on('renderer', ({id, renderer}) => {
|
||||
attachRenderer(id, renderer);
|
||||
});
|
||||
|
||||
hook.emit('react-devtools', agent);
|
||||
hook.reactDevtoolsAgent = agent;
|
||||
agent.addListener('shutdown', () => {
|
||||
subs.forEach(fn => fn());
|
||||
hook.reactDevtoolsAgent = null;
|
||||
});
|
||||
};
|
||||
718
src/backend/renderer.js
Normal file
718
src/backend/renderer.js
Normal file
@@ -0,0 +1,718 @@
|
||||
// @flow
|
||||
|
||||
import {gte} from 'semver';
|
||||
import {
|
||||
ElementTypeClassOrFunction,
|
||||
ElementTypeContext,
|
||||
ElementTypeForwardRef,
|
||||
ElementTypeMemo,
|
||||
ElementTypeOtherOrUnknown,
|
||||
ElementTypeProfiler,
|
||||
ElementTypeSuspense,
|
||||
} from 'src/devtools/types';
|
||||
import { getDisplayName } from './utils';
|
||||
|
||||
import type {Fiber, Hook, ReactRenderer, RendererData, RendererInterface} from './types';
|
||||
|
||||
// TODO: If we're profiling, process update
|
||||
// TODO: Throttle process update to app tree
|
||||
|
||||
// Taken from ReactElement.
|
||||
function resolveDefaultProps(Component: any, baseProps: Object): Object {
|
||||
if (Component && Component.defaultProps) {
|
||||
// Resolve default props. Taken from ReactElement
|
||||
const props = Object.assign({}, baseProps);
|
||||
const defaultProps = Component.defaultProps;
|
||||
for (const propName in defaultProps) {
|
||||
if (props[propName] === undefined) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
return baseProps;
|
||||
}
|
||||
|
||||
function getInternalReactConstants(version) {
|
||||
const ReactSymbols = {
|
||||
CONCURRENT_MODE_NUMBER: 0xeacf,
|
||||
CONCURRENT_MODE_SYMBOL_STRING: 'Symbol(react.concurrent_mode)',
|
||||
DEPRECATED_ASYNC_MODE_SYMBOL_STRING: 'Symbol(react.async_mode)',
|
||||
CONTEXT_CONSUMER_NUMBER: 0xeace,
|
||||
CONTEXT_CONSUMER_SYMBOL_STRING: 'Symbol(react.context)',
|
||||
CONTEXT_PROVIDER_NUMBER: 0xeacd,
|
||||
CONTEXT_PROVIDER_SYMBOL_STRING: 'Symbol(react.provider)',
|
||||
FORWARD_REF_NUMBER: 0xead0,
|
||||
FORWARD_REF_SYMBOL_STRING: 'Symbol(react.forward_ref)',
|
||||
MEMO_NUMBER: 0xead3,
|
||||
MEMO_SYMBOL_STRING: 'Symbol(react.memo)',
|
||||
PROFILER_NUMBER: 0xead2,
|
||||
PROFILER_SYMBOL_STRING: 'Symbol(react.profiler)',
|
||||
STRICT_MODE_NUMBER: 0xeacc,
|
||||
STRICT_MODE_SYMBOL_STRING: 'Symbol(react.strict_mode)',
|
||||
SUSPENSE_NUMBER: 0xead1,
|
||||
SUSPENSE_SYMBOL_STRING: 'Symbol(react.suspense)',
|
||||
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: 'Symbol(react.placeholder)',
|
||||
};
|
||||
|
||||
const ReactTypeOfSideEffect = {
|
||||
PerformedWork: 1,
|
||||
};
|
||||
|
||||
let ReactTypeOfWork;
|
||||
|
||||
// **********************************************************
|
||||
// The section below is copied from files in React repo.
|
||||
// Keep it in sync, and add version guards if it changes.
|
||||
// **********************************************************
|
||||
if (gte(version, '16.6.0-beta.0')) {
|
||||
ReactTypeOfWork = {
|
||||
ClassComponent: 1,
|
||||
ContextConsumer: 9,
|
||||
ContextProvider: 10,
|
||||
CoroutineComponent: -1, // Removed
|
||||
CoroutineHandlerPhase: -1, // Removed
|
||||
ForwardRef: 11,
|
||||
Fragment: 7,
|
||||
FunctionComponent: 0,
|
||||
HostComponent: 5,
|
||||
HostPortal: 4,
|
||||
HostRoot: 3,
|
||||
HostText: 6,
|
||||
IncompleteClassComponent: 17,
|
||||
IndeterminateComponent: 2,
|
||||
LazyComponent: 16,
|
||||
MemoComponent: 14,
|
||||
Mode: 8,
|
||||
Profiler: 12,
|
||||
SimpleMemoComponent: 15,
|
||||
SuspenseComponent: 13,
|
||||
YieldComponent: -1, // Removed
|
||||
};
|
||||
} else if (gte(version, '16.4.3-alpha')) {
|
||||
ReactTypeOfWork = {
|
||||
ClassComponent: 2,
|
||||
ContextConsumer: 11,
|
||||
ContextProvider: 12,
|
||||
CoroutineComponent: -1, // Removed
|
||||
CoroutineHandlerPhase: -1, // Removed
|
||||
ForwardRef: 13,
|
||||
Fragment: 9,
|
||||
FunctionComponent: 0,
|
||||
HostComponent: 7,
|
||||
HostPortal: 6,
|
||||
HostRoot: 5,
|
||||
HostText: 8,
|
||||
IncompleteClassComponent: -1, // Doesn't exist yet
|
||||
IndeterminateComponent: 4,
|
||||
LazyComponent: -1, // Doesn't exist yet
|
||||
MemoComponent: -1, // Doesn't exist yet
|
||||
Mode: 10,
|
||||
Profiler: 15,
|
||||
SimpleMemoComponent: -1, // Doesn't exist yet
|
||||
SuspenseComponent: 16,
|
||||
YieldComponent: -1, // Removed
|
||||
};
|
||||
} else {
|
||||
ReactTypeOfWork = {
|
||||
ClassComponent: 2,
|
||||
ContextConsumer: 12,
|
||||
ContextProvider: 13,
|
||||
CoroutineComponent: 7,
|
||||
CoroutineHandlerPhase: 8,
|
||||
ForwardRef: 14,
|
||||
Fragment: 10,
|
||||
FunctionComponent: 1,
|
||||
HostComponent: 5,
|
||||
HostPortal: 4,
|
||||
HostRoot: 3,
|
||||
HostText: 6,
|
||||
IncompleteClassComponent: -1, // Doesn't exist yet
|
||||
IndeterminateComponent: 0,
|
||||
LazyComponent: -1, // Doesn't exist yet
|
||||
MemoComponent: -1, // Doesn't exist yet
|
||||
Mode: 11,
|
||||
Profiler: 15,
|
||||
SimpleMemoComponent: -1, // Doesn't exist yet
|
||||
SuspenseComponent: 16,
|
||||
YieldComponent: 9,
|
||||
};
|
||||
}
|
||||
// **********************************************************
|
||||
// End of copied code.
|
||||
// **********************************************************
|
||||
|
||||
return {
|
||||
ReactTypeOfWork,
|
||||
ReactSymbols,
|
||||
ReactTypeOfSideEffect,
|
||||
};
|
||||
}
|
||||
|
||||
export function attach(
|
||||
hook: Hook,
|
||||
rendererID: string,
|
||||
renderer: ReactRenderer,
|
||||
): RendererInterface {
|
||||
const {overrideProps} = renderer;
|
||||
const {ReactTypeOfWork, ReactSymbols, ReactTypeOfSideEffect} = getInternalReactConstants(renderer.version);
|
||||
const {PerformedWork} = ReactTypeOfSideEffect;
|
||||
const {
|
||||
FunctionComponent,
|
||||
ClassComponent,
|
||||
ContextConsumer,
|
||||
Fragment,
|
||||
ForwardRef,
|
||||
HostRoot,
|
||||
HostPortal,
|
||||
HostComponent,
|
||||
HostText,
|
||||
IncompleteClassComponent,
|
||||
IndeterminateComponent,
|
||||
MemoComponent,
|
||||
SimpleMemoComponent,
|
||||
} = ReactTypeOfWork;
|
||||
const {
|
||||
CONCURRENT_MODE_NUMBER,
|
||||
CONCURRENT_MODE_SYMBOL_STRING,
|
||||
DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
|
||||
CONTEXT_CONSUMER_NUMBER,
|
||||
CONTEXT_CONSUMER_SYMBOL_STRING,
|
||||
CONTEXT_PROVIDER_NUMBER,
|
||||
CONTEXT_PROVIDER_SYMBOL_STRING,
|
||||
PROFILER_NUMBER,
|
||||
PROFILER_SYMBOL_STRING,
|
||||
STRICT_MODE_NUMBER,
|
||||
STRICT_MODE_SYMBOL_STRING,
|
||||
SUSPENSE_NUMBER,
|
||||
SUSPENSE_SYMBOL_STRING,
|
||||
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
|
||||
} = ReactSymbols;
|
||||
|
||||
const primaryFibers: WeakSet<Fiber> = new WeakSet();
|
||||
|
||||
// TODO: we might want to change the data structure
|
||||
// once we no longer suppport Stack versions of `getData`.
|
||||
// TODO: Keep in sync with getElementType()
|
||||
function getRendererDataFromFiber(fiber: Fiber): RendererData {
|
||||
const { elementType, type, key, tag } = fiber;
|
||||
|
||||
// This is to support lazy components with a Promise as the type.
|
||||
// see https://github.com/facebook/react/pull/13397
|
||||
let resolvedType = type;
|
||||
if (typeof type === 'object' && type !== null) {
|
||||
if (typeof type.then === 'function') {
|
||||
resolvedType = type._reactResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Suspense has some special behavior when timed out that we have to handle.
|
||||
// see https://github.com/facebook/react/pull/13823
|
||||
let isTimedOutSuspense: boolean = false;
|
||||
|
||||
let rendererData: RendererData = ((null: any): RendererData);
|
||||
let displayName: string = ((null: any): string);
|
||||
let resolvedContext: any = null;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case FunctionComponent:
|
||||
case IncompleteClassComponent:
|
||||
case IndeterminateComponent:
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: getDisplayName(resolvedType),
|
||||
key,
|
||||
type: ElementTypeClassOrFunction,
|
||||
};
|
||||
break;
|
||||
case ForwardRef:
|
||||
const functionName = getDisplayName(resolvedType.render, '');
|
||||
displayName = resolvedType.displayName || (
|
||||
functionName !== ''
|
||||
? `ForwardRef(${functionName})`
|
||||
: 'ForwardRef'
|
||||
);
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeForwardRef,
|
||||
};
|
||||
break;
|
||||
case HostRoot:
|
||||
case HostPortal:
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
// Not displayed in Elements tree
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
if (elementType.displayName) {
|
||||
displayName = elementType.displayName;
|
||||
} else {
|
||||
displayName = type.displayName || type.name;
|
||||
displayName = displayName ? `Memo(${displayName})` : 'Memo';
|
||||
}
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeMemo,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
const symbolOrNumber = typeof type === 'object' && type !== null
|
||||
? type.$$typeof
|
||||
: type;
|
||||
|
||||
// $FlowFixMe facebook/flow/issues/2362
|
||||
const switchValue = typeof symbolOrNumber === 'symbol'
|
||||
? symbolOrNumber.toString()
|
||||
: symbolOrNumber;
|
||||
|
||||
switch (switchValue) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
// Not displayed in Elements tree
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_PROVIDER_SYMBOL_STRING:
|
||||
// 16.3.0 exposed the context object as "context"
|
||||
// PR #12501 changed it to "_context" for 16.3.1+
|
||||
resolvedContext = fiber.type._context || fiber.type.context;
|
||||
displayName = `${resolvedContext.displayName || 'Context'}.Provider`;
|
||||
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeContext,
|
||||
};
|
||||
break;
|
||||
case CONTEXT_CONSUMER_NUMBER:
|
||||
case CONTEXT_CONSUMER_SYMBOL_STRING:
|
||||
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
|
||||
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
|
||||
resolvedContext = fiber.type._context || fiber.type;
|
||||
|
||||
// NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
|
||||
// If you change the name, figure out a more resilient way to detect it.
|
||||
displayName = `${resolvedContext.displayName || 'Context'}.Consumer`;
|
||||
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeContext,
|
||||
};
|
||||
break;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
// Suspense components only have a non-null memoizedState if they're timed-out.
|
||||
isTimedOutSuspense = fiber.memoizedState !== null;
|
||||
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: 'Suspense',
|
||||
key,
|
||||
type: ElementTypeSuspense,
|
||||
};
|
||||
break;
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: `Profiler(${fiber.memoizedProps.id})`,
|
||||
key,
|
||||
type: ElementTypeProfiler,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// Unknown element type.
|
||||
// This may mean a new element type that has not yet been added to DevTools.
|
||||
rendererData = {
|
||||
children: [],
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const {children} = rendererData;
|
||||
if (isTimedOutSuspense) {
|
||||
// The behavior of timed-out Suspense trees is unique.
|
||||
// Rather than unmount the timed out content (and possibly lose important state),
|
||||
// React re-parents this content within a hidden Fragment while the fallback is showing.
|
||||
// This behavior doesn't need to be observable in the DevTools though.
|
||||
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
|
||||
// The easiest fix is to strip out the intermediate Fragment fibers,
|
||||
// so the Elements panel and Profiler don't need to special case them.
|
||||
const primaryChildFragment = fiber.child;
|
||||
const primaryChild = primaryChildFragment.child;
|
||||
const fallbackChildFragment = primaryChildFragment.sibling;
|
||||
const fallbackChild = fallbackChildFragment.child;
|
||||
children.push(primaryChild);
|
||||
children.push(fallbackChild);
|
||||
} else {
|
||||
let child = fiber.child;
|
||||
while (child) {
|
||||
children.push(getPrimaryFiber(child));
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
return rendererData;
|
||||
}
|
||||
|
||||
// This is a slightly annoying indirection.
|
||||
// It is currently necessary because DevTools wants to use unique objects as keys for instances.
|
||||
// However fibers have two versions.
|
||||
// We use this set to remember first encountered fiber for each conceptual instance.
|
||||
function getPrimaryFiber(fiber: Fiber): Fiber {
|
||||
if (primaryFibers.has(fiber)) {
|
||||
return fiber;
|
||||
}
|
||||
const {alternate} = fiber;
|
||||
if (alternate != null && primaryFibers.has(alternate)) {
|
||||
return alternate;
|
||||
}
|
||||
primaryFibers.add(fiber);
|
||||
return fiber;
|
||||
}
|
||||
|
||||
function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
|
||||
switch (nextFiber.tag) {
|
||||
case ClassComponent:
|
||||
case FunctionComponent:
|
||||
case ContextConsumer:
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
// For types that execute user code, we check PerformedWork effect.
|
||||
// We don't reflect bailouts (either referential or sCU) in DevTools.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (nextFiber.effectTag & PerformedWork) === PerformedWork;
|
||||
// Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
|
||||
// so it won't get highlighted with React 16.3.0 to 16.3.2.
|
||||
default:
|
||||
// For host components and other types, we compare inputs
|
||||
// to determine whether something is an update.
|
||||
return (
|
||||
prevFiber.memoizedProps !== nextFiber.memoizedProps ||
|
||||
prevFiber.memoizedState !== nextFiber.memoizedState ||
|
||||
prevFiber.ref !== nextFiber.ref
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function haveProfilerTimesChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
|
||||
return (
|
||||
prevFiber.actualDuration !== undefined && // Short-circuit check for non-profiling builds
|
||||
(
|
||||
prevFiber.actualDuration !== nextFiber.actualDuration ||
|
||||
prevFiber.actualStartTime !== nextFiber.actualStartTime ||
|
||||
prevFiber.treeBaseDuration !== nextFiber.treeBaseDuration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let pendingEvents = [];
|
||||
|
||||
// TODO: Do we still need to send events like this?
|
||||
function flushPendingEvents() {
|
||||
const events = pendingEvents;
|
||||
pendingEvents = [];
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
hook.emit(event.type, event);
|
||||
}
|
||||
}
|
||||
|
||||
function enqueueMount(fiber) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data: getRendererDataFromFiber(fiber),
|
||||
renderer: rendererID,
|
||||
type: 'mount',
|
||||
});
|
||||
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
if (isRoot) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
renderer: rendererID,
|
||||
type: 'root',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function enqueueUpdateIfNecessary(fiber, hasChildOrderChanged) {
|
||||
const data = getRendererDataFromFiber(fiber);
|
||||
|
||||
if (
|
||||
!hasChildOrderChanged &&
|
||||
!hasDataChanged(fiber.alternate, fiber)
|
||||
) {
|
||||
// If only timing information has changed, we still need to update the nodes.
|
||||
// But we can do it in a faster way since we know it's safe to skip the children.
|
||||
// It's also important to avoid emitting an "update" signal for the node in this case,
|
||||
// Since that would indicate to the Profiler that it was part of the "commit" when it wasn't.
|
||||
if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data,
|
||||
renderer: rendererID,
|
||||
type: 'updateProfileTimes',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data,
|
||||
renderer: rendererID,
|
||||
type: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
function enqueueUnmount(fiber) {
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
const primaryFiber = getPrimaryFiber(fiber);
|
||||
const event = {
|
||||
fiber: primaryFiber,
|
||||
renderer: rendererID,
|
||||
type: 'unmount',
|
||||
};
|
||||
if (isRoot) {
|
||||
pendingEvents.push(event);
|
||||
} else {
|
||||
// Non-root fibers are deleted during the commit phase.
|
||||
// They are deleted in the child-first order. However
|
||||
// DevTools currently expects deletions to be parent-first.
|
||||
// This is why we unshift deletions rather than push them.
|
||||
pendingEvents.unshift(event);
|
||||
}
|
||||
primaryFibers.delete(primaryFiber);
|
||||
}
|
||||
|
||||
function markRootCommitted(fiber) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data: getRendererDataFromFiber(fiber),
|
||||
renderer: rendererID,
|
||||
type: 'rootCommitted',
|
||||
});
|
||||
}
|
||||
|
||||
function mountFiber(fiber) {
|
||||
// Depth-first.
|
||||
// Logs mounting of children first, parents later.
|
||||
let node = fiber;
|
||||
outer: while (true) {
|
||||
if (node.child) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
enqueueMount(node);
|
||||
if (node == fiber) {
|
||||
return;
|
||||
}
|
||||
if (node.sibling) {
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
continue;
|
||||
}
|
||||
while (node.return) {
|
||||
node = node.return;
|
||||
enqueueMount(node);
|
||||
if (node == fiber) {
|
||||
return;
|
||||
}
|
||||
if (node.sibling) {
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiber(nextFiber, prevFiber) {
|
||||
// Suspense components only have a non-null memoizedState if they're timed-out.
|
||||
const isTimedOutSuspense = (
|
||||
nextFiber.tag === ReactTypeOfWork.SuspenseComponent &&
|
||||
nextFiber.memoizedState !== null
|
||||
);
|
||||
|
||||
if (isTimedOutSuspense) {
|
||||
// The behavior of timed-out Suspense trees is unique.
|
||||
// Rather than unmount the timed out content (and possibly lose important state),
|
||||
// React re-parents this content within a hidden Fragment while the fallback is showing.
|
||||
// This behavior doesn't need to be observable in the DevTools though.
|
||||
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
|
||||
// The easiest fix is to strip out the intermediate Fragment fibers,
|
||||
// so the Elements panel and Profiler don't need to special case them.
|
||||
const primaryChildFragment = nextFiber.child;
|
||||
const fallbackChildFragment = primaryChildFragment.sibling;
|
||||
const fallbackChild = fallbackChildFragment.child;
|
||||
// The primary, hidden child is never actually updated in this case,
|
||||
// so we can skip any updates to its tree.
|
||||
// We only need to track updates to the Fallback UI for now.
|
||||
if (fallbackChild.alternate) {
|
||||
updateFiber(fallbackChild, fallbackChild.alternate);
|
||||
} else {
|
||||
mountFiber(fallbackChild);
|
||||
}
|
||||
enqueueUpdateIfNecessary(nextFiber, false);
|
||||
} else {
|
||||
let hasChildOrderChanged = false;
|
||||
if (nextFiber.child !== prevFiber.child) {
|
||||
// If the first child is different, we need to traverse them.
|
||||
// Each next child will be either a new child (mount) or an alternate (update).
|
||||
let nextChild = nextFiber.child;
|
||||
let prevChildAtSameIndex = prevFiber.child;
|
||||
while (nextChild) {
|
||||
// We already know children will be referentially different because
|
||||
// they are either new mounts or alternates of previous children.
|
||||
// Schedule updates and mounts depending on whether alternates exist.
|
||||
// We don't track deletions here because they are reported separately.
|
||||
if (nextChild.alternate) {
|
||||
const prevChild = nextChild.alternate;
|
||||
updateFiber(nextChild, prevChild);
|
||||
// However we also keep track if the order of the children matches
|
||||
// the previous order. They are always different referentially, but
|
||||
// if the instances line up conceptually we'll want to know that.
|
||||
if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) {
|
||||
hasChildOrderChanged = true;
|
||||
}
|
||||
} else {
|
||||
mountFiber(nextChild);
|
||||
if (!hasChildOrderChanged) {
|
||||
hasChildOrderChanged = true;
|
||||
}
|
||||
}
|
||||
// Try the next child.
|
||||
nextChild = nextChild.sibling;
|
||||
// Advance the pointer in the previous list so that we can
|
||||
// keep comparing if they line up.
|
||||
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
|
||||
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
|
||||
}
|
||||
}
|
||||
// If we have no more children, but used to, they don't line up.
|
||||
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
|
||||
hasChildOrderChanged = true;
|
||||
}
|
||||
}
|
||||
enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
// We don't patch any methods so there is no cleanup.
|
||||
}
|
||||
|
||||
function walkTree() {
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
// Hydrate all the roots for the first time.
|
||||
mountFiber(root.current);
|
||||
markRootCommitted(root.current);
|
||||
});
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
function handleCommitFiberUnmount(fiber) {
|
||||
// This is not recursive.
|
||||
// We can't traverse fibers after unmounting so instead
|
||||
// we rely on React telling us about each unmount.
|
||||
enqueueUnmount(fiber);
|
||||
}
|
||||
|
||||
function handleCommitFiberRoot(root) {
|
||||
const current = root.current;
|
||||
const alternate = current.alternate;
|
||||
|
||||
if (alternate) {
|
||||
// TODO: relying on this seems a bit fishy.
|
||||
const wasMounted = alternate.memoizedState != null && alternate.memoizedState.element != null;
|
||||
const isMounted = current.memoizedState != null && current.memoizedState.element != null;
|
||||
if (!wasMounted && isMounted) {
|
||||
// Mount a new root.
|
||||
mountFiber(current);
|
||||
} else if (wasMounted && isMounted) {
|
||||
// Update an existing root.
|
||||
updateFiber(current, alternate);
|
||||
} else if (wasMounted && !isMounted) {
|
||||
// Unmount an existing root.
|
||||
enqueueUnmount(current);
|
||||
}
|
||||
} else {
|
||||
// Mount a new root.
|
||||
mountFiber(current);
|
||||
}
|
||||
markRootCommitted(current);
|
||||
// We're done here.
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
// The naming is confusing.
|
||||
// They deal with opaque nodes (fibers), not elements.
|
||||
function getNativeFromReactElement(fiber) {
|
||||
try {
|
||||
const primaryFiber = fiber;
|
||||
const hostInstance = renderer.findHostInstanceByFiber(primaryFiber);
|
||||
return hostInstance;
|
||||
} catch (err) {
|
||||
// The fiber might have unmounted by now.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getReactElementFromNative(hostInstance) {
|
||||
const fiber = renderer.findFiberByHostInstance(hostInstance);
|
||||
if (fiber != null) {
|
||||
// TODO: type fibers.
|
||||
const primaryFiber = getPrimaryFiber((fiber: any));
|
||||
return primaryFiber;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
getNativeFromReactElement,
|
||||
getReactElementFromNative,
|
||||
handleCommitFiberRoot,
|
||||
handleCommitFiberUnmount,
|
||||
cleanup,
|
||||
walkTree,
|
||||
renderer,
|
||||
};
|
||||
}
|
||||
77
src/backend/types.js
Normal file
77
src/backend/types.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// @flow
|
||||
|
||||
import type {ElementType} from 'src/devtools/types';
|
||||
|
||||
type BundleType =
|
||||
| 0 // PROD
|
||||
| 1; // DEV
|
||||
|
||||
type CompositeUpdater = {
|
||||
canUpdate: boolean,
|
||||
setInProps: ?(path: Array<string>, value: any) => void,
|
||||
setInState: ?(path: Array<string>, value: any) => void,
|
||||
setInContext: ?(path: Array<string>, value: any) => void,
|
||||
};
|
||||
|
||||
type NativeUpdater = {
|
||||
setNativeProps: ?(nativeProps: {[key: string]: any}) => void,
|
||||
};
|
||||
|
||||
// TODO: Better type for Fiber
|
||||
export type Fiber = Object;
|
||||
|
||||
// TODO: If it's useful for the frontend to know which types of data an Element has
|
||||
// (e.g. props, state, context, hooks) then we could add a bitmask field for this
|
||||
// to keep the number of attributes small.
|
||||
export type RendererData = {|
|
||||
key: React$Key | null,
|
||||
displayName: string | null,
|
||||
children: Array<Fiber>,
|
||||
type: ElementType,
|
||||
|};
|
||||
|
||||
export type Interaction = {|
|
||||
name: string,
|
||||
timestamp: number,
|
||||
|};
|
||||
|
||||
export type NativeType = {};
|
||||
export type RendererID = string;
|
||||
|
||||
export type ReactRenderer = {
|
||||
findHostInstanceByFiber: (fiber: Object) => ?NativeType,
|
||||
findFiberByHostInstance: (hostInstance: NativeType) => ?Fiber,
|
||||
version: string,
|
||||
bundleType: BundleType,
|
||||
overrideProps?: ?(fiber: Object, path: Array<string | number>, value: any) => void,
|
||||
};
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
getNativeFromReactElement?: ?(component: Fiber) => ?NativeType,
|
||||
getReactElementFromNative?: ?(component: NativeType) => ?Fiber,
|
||||
handleCommitFiberRoot: (fiber: Object) => void,
|
||||
renderer: ReactRenderer | null,
|
||||
walkTree: () => void,
|
||||
};
|
||||
|
||||
export type Handler = (data: any) => void;
|
||||
|
||||
export type Hook = {
|
||||
listeners: {[key: string]: Array<Handler>},
|
||||
rendererInterfaces: {[key: string]: RendererInterface},
|
||||
renderers: {[key: string]: ReactRenderer},
|
||||
|
||||
emit: (evt: string, data: any) => void,
|
||||
getFiberRoots: (rendererID : string) => Set<Object>,
|
||||
inject: (renderer: ReactRenderer) => string | null,
|
||||
on: (evt: string, handler: Handler) => void,
|
||||
off: (evt: string, handler: Handler) => void,
|
||||
reactDevtoolsAgent?: ?Object,
|
||||
sub: (evt: string, handler: Handler) => () => void,
|
||||
|
||||
// React uses these methods.
|
||||
checkDCE: (fn: Function) => void,
|
||||
onCommitFiberUnmount: (rendererID: RendererID, fiber: Object) => void,
|
||||
onCommitFiberRoot: (rendererID: RendererID, fiber: Object) => void,
|
||||
};
|
||||
47
src/backend/utils.js
Normal file
47
src/backend/utils.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// @flow
|
||||
|
||||
const FB_MODULE_RE = /^(.*) \[from (.*)\]$/;
|
||||
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
|
||||
|
||||
export function getDisplayName(type: Function, fallbackName: string = 'Unknown'): string {
|
||||
const nameFromCache = cachedDisplayNames.get(type);
|
||||
if (nameFromCache != null) {
|
||||
return nameFromCache;
|
||||
}
|
||||
|
||||
let displayName;
|
||||
|
||||
// The displayName property is not guaranteed to be a string.
|
||||
// It's only safe to use for our purposes if it's a string.
|
||||
// github.com/facebook/react-devtools/issues/803
|
||||
if (typeof type.displayName === 'string') {
|
||||
displayName = type.displayName;
|
||||
}
|
||||
|
||||
if (!displayName) {
|
||||
displayName = type.name || fallbackName;
|
||||
}
|
||||
|
||||
// Facebook-specific hack to turn "Image [from Image.react]" into just "Image".
|
||||
// We need displayName with module name for error reports but it clutters the DevTools.
|
||||
const match = displayName.match(FB_MODULE_RE);
|
||||
if (match) {
|
||||
const componentName = match[1];
|
||||
const moduleName = match[2];
|
||||
if (componentName && moduleName) {
|
||||
if (
|
||||
moduleName === componentName ||
|
||||
moduleName.startsWith(componentName + '.')
|
||||
) {
|
||||
displayName = componentName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedDisplayNames.set(type, displayName);
|
||||
return displayName;
|
||||
}
|
||||
|
||||
export function guid(): string {
|
||||
return 'g' + Math.random().toString(16).substr(2);
|
||||
}
|
||||
91
src/bridge.js
Normal file
91
src/bridge.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import type {Wall} from './types';
|
||||
|
||||
const BATCH_DURATION = 100;
|
||||
|
||||
type Message = {|
|
||||
event: string,
|
||||
payload: any,
|
||||
|};
|
||||
|
||||
export default class Bridge extends EventEmitter {
|
||||
_messageQueue: Array<Message> = [];
|
||||
_time: number | null = null;
|
||||
_timeoutID: TimeoutID | null = null;
|
||||
|
||||
wall: Wall;
|
||||
|
||||
constructor (wall: Wall) {
|
||||
super();
|
||||
|
||||
this.wall = wall;
|
||||
wall.listen(messages => {
|
||||
if (Array.isArray(messages)) {
|
||||
messages.forEach(message => this._emit(message))
|
||||
} else {
|
||||
this._emit(messages)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {*} payload
|
||||
*/
|
||||
|
||||
send (event: string, payload: any) {
|
||||
const time = this._time;
|
||||
|
||||
if (time === null) {
|
||||
this.wall.send([{ event, payload }])
|
||||
this._time = Date.now()
|
||||
} else {
|
||||
this._messageQueue.push({
|
||||
event,
|
||||
payload
|
||||
})
|
||||
|
||||
const now = Date.now()
|
||||
if (now - time > BATCH_DURATION) {
|
||||
this._flush()
|
||||
} else {
|
||||
this._timeoutID = setTimeout(() => this._flush(), BATCH_DURATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the devtools background page.
|
||||
*
|
||||
* @param {String} message
|
||||
*/
|
||||
|
||||
log (message: string): void {
|
||||
this.send('log', message)
|
||||
}
|
||||
|
||||
_flush () {
|
||||
if (this._messageQueue.length) {
|
||||
this.wall.send(this._messageQueue)
|
||||
}
|
||||
if (this._timeoutID !== null) {
|
||||
clearTimeout(this._timeoutID)
|
||||
this._timeoutID = null;
|
||||
}
|
||||
this._messageQueue = []
|
||||
this._time = null
|
||||
}
|
||||
|
||||
_emit (message: string | Message) {
|
||||
if (typeof message === 'string') {
|
||||
this.emit(message)
|
||||
} else {
|
||||
this.emit(message.event, message.payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/devtools/index.js
Normal file
14
src/devtools/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import type { Bridge } from '../types';
|
||||
|
||||
type Shell = {|
|
||||
connect: (callback: Function) => void,
|
||||
onReload: (reloadFn: Function) => void,
|
||||
|};
|
||||
|
||||
export function initDevTools(shell: Shell) {
|
||||
shell.connect((bridge: Bridge) => {
|
||||
// TODO ...
|
||||
});
|
||||
}
|
||||
78
src/devtools/store.js
Normal file
78
src/devtools/store.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import type { Element } from './types';
|
||||
import type { Bridge } from '../types';
|
||||
|
||||
/**
|
||||
* The store is the single source of truth for updates from the backend.
|
||||
* ContextProviders can subscribe to the Store for specific things they want to provide.
|
||||
*/
|
||||
class Store extends EventEmitter {
|
||||
_idToElement: Map<string, Element> = new Map();
|
||||
_idToParentID: Map<string, string> = new Map();
|
||||
|
||||
roots: Set<string> = new Set();
|
||||
|
||||
constructor(bridge: Bridge) {
|
||||
super();
|
||||
|
||||
bridge.on('root', this.onBridgeRoot);
|
||||
bridge.on('mount', this.onBridgeMount);
|
||||
bridge.on('update', this.onBridgeUpdated);
|
||||
bridge.on('unmount', this.onBridgeUnmounted);
|
||||
}
|
||||
|
||||
getElement(id: string) {
|
||||
return this._idToElement.get(id);
|
||||
}
|
||||
|
||||
geParent(id: string) {
|
||||
return this._idToParentID.get(id);
|
||||
}
|
||||
|
||||
onBridgeMount = (element: Element) => {
|
||||
const {id} = element;
|
||||
console.log('%cStore%c onBridgeMount()', 'color: red; font-weight: bold;', 'font-weight: bold;', element);
|
||||
this._idToElement.set(id, element);
|
||||
|
||||
element.children.forEach(childID => {
|
||||
this._idToParentID.set(childID, id);
|
||||
});
|
||||
|
||||
this.emit(id);
|
||||
};
|
||||
|
||||
onBridgeRoot = (id: string) => {
|
||||
console.log('%cStore%c onBridgeRoot()', 'color: red; font-weight: bold;', 'font-weight: bold;', id);
|
||||
if (!this.roots.has(id)) {
|
||||
this.roots.add(id);
|
||||
this.emit('roots');
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeUnmounted = (id: string) => {
|
||||
console.log('%cStore%c onBridgeUnmounted()', 'color: red; font-weight: bold;', 'font-weight: bold;', id);
|
||||
this._idToElement.delete(id);
|
||||
|
||||
if (this._idToParentID.has(id)) {
|
||||
this._idToParentID.delete(id);
|
||||
}
|
||||
|
||||
if (this.roots.has(id)) {
|
||||
this.roots.delete(id);
|
||||
this.emit('roots');
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeUpdated = (element: Element) => {
|
||||
const {id} = element;
|
||||
console.log('%cStore%c onBridgeUpdated()', 'color: red; font-weight: bold;', 'font-weight: bold;', element);
|
||||
this._idToElement.set(id, element);
|
||||
|
||||
this.emit(id);
|
||||
};
|
||||
}
|
||||
|
||||
export default Store;
|
||||
38
src/devtools/types.js
Normal file
38
src/devtools/types.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
|
||||
export const ElementTypeClassOrFunction = 1;
|
||||
export const ElementTypeContext = 2;
|
||||
export const ElementTypeForwardRef = 3;
|
||||
export const ElementTypeMemo = 4;
|
||||
export const ElementTypeOtherOrUnknown = 5;
|
||||
export const ElementTypeProfiler = 6;
|
||||
export const ElementTypeSuspense = 7;
|
||||
|
||||
export type ElementType =
|
||||
| 1
|
||||
| 2
|
||||
| 3
|
||||
| 4
|
||||
| 5
|
||||
| 6
|
||||
| 7;
|
||||
|
||||
// TODO: Add profiling node
|
||||
|
||||
export type Element = {|
|
||||
id: string,
|
||||
type: ElementType,
|
||||
key: React$Key | null,
|
||||
displayName: string | null,
|
||||
children: Array<string>,
|
||||
|};
|
||||
|
||||
export type InspectedElement = {|
|
||||
id: string,
|
||||
context: Object | null,
|
||||
hooks: Object | null,
|
||||
props: Object | null,
|
||||
state: Object | null,
|
||||
canEditProps: boolean,
|
||||
source: Object,
|
||||
|};
|
||||
49
src/devtools/views/App.css
Normal file
49
src/devtools/views/App.css
Normal file
@@ -0,0 +1,49 @@
|
||||
:root {
|
||||
/* TODO */
|
||||
--color-base00: #ffffff;
|
||||
--color-base01: #f3f3f3;
|
||||
--color-base02: #eeeeee;
|
||||
--color-base03: #dadada;
|
||||
--color-base04: #333333;
|
||||
--color-base05: #5a5a5a;
|
||||
--color-special00: #8155cb;
|
||||
--color-special01: #222222;
|
||||
--color-special02: #1a1aa6;
|
||||
--color-special03: #c80000;
|
||||
--color-special04: #236e25;
|
||||
--color-special05: #aa0d91;
|
||||
--color-special06: #ef6632;
|
||||
--color-special07: #777d88;
|
||||
--color-state00: #0088FA;
|
||||
--color-state01: #dadada;
|
||||
--color-state02: #ffffff;
|
||||
--color-state03: #ebf1fb;
|
||||
--color-state04: #FFFF00;
|
||||
--color-state05: #222222;
|
||||
--color-state06: #222222;
|
||||
|
||||
--color-component: var(--color-special00);
|
||||
--color-tree-tag: var(--color-base04);
|
||||
--color-tree-attr-name: var(--color-special06);
|
||||
--color-tree-attr-value: var(--color-special02);
|
||||
|
||||
--line-height-tree-compact: 1.5;
|
||||
--line-height-tree-normal: 1.5;
|
||||
|
||||
/* GitHub.com system fonts */
|
||||
--font-family-monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
--font-size-tree-compact: 12px;
|
||||
--font-size-tree-normal: 14px;
|
||||
}
|
||||
|
||||
.App {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
25
src/devtools/views/App.js
Normal file
25
src/devtools/views/App.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @flow
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import Store from '../store';
|
||||
import Tree from './Tree';
|
||||
import {StoreContext} from './contexts';
|
||||
import styles from './App.css';
|
||||
|
||||
import type {Bridge} from '../../types';
|
||||
|
||||
type Props = {|
|
||||
bridge: Bridge,
|
||||
|};
|
||||
|
||||
export default function App({ bridge }: Props) {
|
||||
const store = useMemo(() => new Store(bridge), []);
|
||||
|
||||
return (
|
||||
<StoreContext.Provider value={store}>
|
||||
<div className={styles.App}>
|
||||
<Tree />
|
||||
</div>
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
}
|
||||
46
src/devtools/views/Element.css
Normal file
46
src/devtools/views/Element.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.Element {
|
||||
width: 100%;
|
||||
border-radius: 0.25em;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.Element:hover {
|
||||
background-color: var(--color-state03);
|
||||
}
|
||||
|
||||
.ArrowClosed,
|
||||
.ArrowOpen {
|
||||
position: absolute;
|
||||
margin-left: -1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23777d88' d='M8 5v14l11-7z'/%3E%3Cpath fill='none' d='M0 24V0h24v24H0z'/%3E%3C/svg%3E");
|
||||
background-size: 12px 12px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.ArrowOpen {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.Component {
|
||||
color: var(--color-component);
|
||||
}
|
||||
.Component:before {
|
||||
white-space: nowrap;
|
||||
content: "<";
|
||||
color: var(--color-tree-tag);
|
||||
}
|
||||
.Component:after {
|
||||
white-space: nowrap;
|
||||
content: ">";
|
||||
color: var(--color-tree-tag);
|
||||
}
|
||||
|
||||
.AttributeName {
|
||||
color: var(--color-tree-attr-name);
|
||||
}
|
||||
|
||||
.AttributeValue {
|
||||
color: var(--color-tree-attr-value);
|
||||
}
|
||||
43
src/devtools/views/Element.js
Normal file
43
src/devtools/views/Element.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
|
||||
import React, {Fragment, useContext} from 'react';
|
||||
import {useElement} from './hooks';
|
||||
import {StoreContext} from './contexts';
|
||||
|
||||
import styles from './Element.css';
|
||||
|
||||
type Props = {|
|
||||
depth: number,
|
||||
id: string,
|
||||
|};
|
||||
|
||||
export default function Element({depth, id}: Props) {
|
||||
const store = useContext(StoreContext);
|
||||
const element = useElement(store, id);
|
||||
|
||||
const {children, displayName, key} = element;
|
||||
|
||||
// TODO: Toggle open/close state
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Element} style={{paddingLeft: `${1 + depth}rem`}}>
|
||||
{children.length > 0 && (
|
||||
<span className={styles.ArrowOpen}></span>
|
||||
)}
|
||||
|
||||
<span className={styles.Component}>
|
||||
{displayName}{key && (
|
||||
<Fragment>
|
||||
<span className={styles.AttributeName}>key</span>=<span className={styles.AttributeValue}>"{key}"</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{children.map(childID => (
|
||||
<Element key={childID} depth={depth + 1} id={childID} />
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
11
src/devtools/views/Tree.css
Normal file
11
src/devtools/views/Tree.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.Tree {
|
||||
width: 100%;
|
||||
padding: 0.25rem;
|
||||
overflow: auto;
|
||||
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-tree-compact);
|
||||
|
||||
/* TODO */
|
||||
line-height: var(--line-height-tree-compact);
|
||||
}
|
||||
34
src/devtools/views/Tree.js
Normal file
34
src/devtools/views/Tree.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @flow
|
||||
|
||||
import React, {useContext} from 'react';
|
||||
import Element from './Element';
|
||||
import {StoreContext} from './contexts';
|
||||
import {useElement, useRoots} from './hooks';
|
||||
|
||||
import styles from './Tree.css';
|
||||
|
||||
type TreeProps = {||};
|
||||
|
||||
export default function Tree({}: TreeProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const roots = useRoots(store);
|
||||
|
||||
return (
|
||||
<div className={styles.Tree}>
|
||||
{roots.map(id => <Root key={id} id={id} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type RootProps = {|
|
||||
id: string,
|
||||
|};
|
||||
|
||||
function Root({id}: RootProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const element = useElement(store, id);
|
||||
|
||||
return element.children.map(childID => (
|
||||
<Element key={childID} depth={0} id={childID} />
|
||||
));
|
||||
}
|
||||
8
src/devtools/views/contexts.js
Normal file
8
src/devtools/views/contexts.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
import Store from '../store';
|
||||
|
||||
export const RootsContext = createContext<Array<string>>([]);
|
||||
export const StoreContext = createContext<Store>(((null: any): Store));
|
||||
30
src/devtools/views/hooks.js
Normal file
30
src/devtools/views/hooks.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
|
||||
import {createContext, useLayoutEffect, useRef, useState} from 'react';
|
||||
|
||||
import type {Element} from '../types';
|
||||
import Store from '../store';
|
||||
|
||||
export function useElement(store: Store, id: string): Element {
|
||||
const [element, setElement] = useState<Element>(((store.getElement(id): any): Element));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handler = () => setElement(((store.getElement(id): any): Element));
|
||||
store.addListener(id, handler);
|
||||
return () => store.removeListener(id, handler);
|
||||
}, [store, id]);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export function useRoots(store: Store): Array<string> {
|
||||
const [roots, setRoots] = useState<Array<string>>(Array.from(store.roots));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handler = () => setRoots(Array.from(store.roots));
|
||||
store.addListener('roots', handler);
|
||||
return () => store.removeListener('roots', handler);
|
||||
}, [store]);
|
||||
|
||||
return roots;
|
||||
}
|
||||
182
src/hook.js
Normal file
182
src/hook.js
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Install the hook on window, which is an event emitter.
|
||||
* Note because Chrome content scripts cannot directly modify the window object,
|
||||
* we are evaling this function by inserting a script tag.
|
||||
* That's why we have to inline the whole event emitter implementation here.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Hook} from './types';
|
||||
|
||||
declare var window: any;
|
||||
|
||||
export function installHook(target: any): Hook {
|
||||
if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) return
|
||||
|
||||
function detectReactBuildType(renderer) {
|
||||
try {
|
||||
if (typeof renderer.version === 'string') {
|
||||
if (renderer.bundleType > 0) {
|
||||
// This is not a production build.
|
||||
// We are currently only using 0 (PROD) and 1 (DEV)
|
||||
// but might add 2 (PROFILE) in the future.
|
||||
return 'development';
|
||||
}
|
||||
|
||||
// React 16 uses flat bundles. If we report the bundle as production
|
||||
// version, it means we also minified and envified it ourselves.
|
||||
return 'production';
|
||||
|
||||
// Note: There is still a risk that the CommonJS entry point has not
|
||||
// been envified or uglified. In this case the user would have *both*
|
||||
// development and production bundle, but only the prod one would run.
|
||||
// This would be really bad. We have a separate check for this because
|
||||
// it happens *outside* of the renderer injection. See `checkDCE` below.
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return 'production';
|
||||
}
|
||||
|
||||
function checkDCE(fn: Function) {
|
||||
// This runs for production versions of React.
|
||||
// Needs to be super safe.
|
||||
try {
|
||||
const toString = Function.prototype.toString;
|
||||
const code = toString.call(fn);
|
||||
|
||||
// This is a string embedded in the passed function under DEV-only
|
||||
// condition. However the function executes only in PROD. Therefore,
|
||||
// if we see it, dead code elimination did not work.
|
||||
if (code.indexOf('^_^') > -1) {
|
||||
// Remember to report during next injection.
|
||||
hasDetectedBadDCE = true;
|
||||
|
||||
// Bonus: throw an exception hoping that it gets picked up by a reporting system.
|
||||
// Not synchronously so that it doesn't break the calling code.
|
||||
setTimeout(function() {
|
||||
throw new Error(
|
||||
'React is running in production mode, but dead code ' +
|
||||
'elimination has not been applied. Read how to correctly ' +
|
||||
'configure React for production: ' +
|
||||
'https://fb.me/react-perf-use-the-production-build'
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function inject(renderer) {
|
||||
const id = Math.random().toString(16).slice(2);
|
||||
renderers[id] = renderer;
|
||||
|
||||
const reactBuildType = hasDetectedBadDCE ? 'deadcode' : detectReactBuildType(renderer);
|
||||
|
||||
hook.emit('renderer', {id, renderer, reactBuildType});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
let hasDetectedBadDCE = false;
|
||||
|
||||
function sub(event, fn) {
|
||||
hook.on(event, fn);
|
||||
return () => hook.off(event, fn);
|
||||
}
|
||||
|
||||
function on(event, fn) {
|
||||
if (!listeners[event]) {
|
||||
listeners[event] = [];
|
||||
}
|
||||
listeners[event].push(fn);
|
||||
}
|
||||
|
||||
function off(event, fn) {
|
||||
if (!listeners[event]) {
|
||||
return;
|
||||
}
|
||||
const index = listeners[event].indexOf(fn);
|
||||
if (index !== -1) {
|
||||
listeners[event].splice(index, 1);
|
||||
}
|
||||
if (!listeners[event].length) {
|
||||
delete listeners[event];
|
||||
}
|
||||
}
|
||||
|
||||
function emit(event, data) {
|
||||
if (listeners[event]) {
|
||||
listeners[event].map(fn => fn(data));
|
||||
}
|
||||
}
|
||||
|
||||
function getFiberRoots(rendererID) {
|
||||
const roots = fiberRoots;
|
||||
if (!roots[rendererID]) {
|
||||
roots[rendererID] = new Set();
|
||||
}
|
||||
return roots[rendererID];
|
||||
}
|
||||
|
||||
function onCommitFiberUnmount(rendererID, fiber) {
|
||||
// TODO: can we use hook for roots too?
|
||||
if (rendererInterfaces[rendererID]) {
|
||||
rendererInterfaces[rendererID].handleCommitFiberUnmount(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
function onCommitFiberRoot(rendererID, root) {
|
||||
const mountedRoots = hook.getFiberRoots(rendererID);
|
||||
const current = root.current;
|
||||
const isKnownRoot = mountedRoots.has(root);
|
||||
const isUnmounting = current.memoizedState == null || current.memoizedState.element == null;
|
||||
|
||||
// Keep track of mounted roots so we can hydrate when DevTools connect.
|
||||
if (!isKnownRoot && !isUnmounting) {
|
||||
mountedRoots.add(root);
|
||||
} else if (isKnownRoot && isUnmounting) {
|
||||
mountedRoots.delete(root);
|
||||
}
|
||||
if (rendererInterfaces[rendererID]) {
|
||||
rendererInterfaces[rendererID].handleCommitFiberRoot(root);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: More meaningful names for "rendererInterfaces" and "renderers".
|
||||
const fiberRoots = {};
|
||||
const rendererInterfaces = {};
|
||||
const listeners = {};
|
||||
const renderers = {};
|
||||
|
||||
const hook: Hook = ({
|
||||
rendererInterfaces,
|
||||
listeners,
|
||||
renderers,
|
||||
|
||||
emit,
|
||||
getFiberRoots,
|
||||
inject,
|
||||
on,
|
||||
off,
|
||||
sub,
|
||||
|
||||
// This is a legacy flag.
|
||||
// React v16 checks the hook for this to ensure DevTools is new enough.
|
||||
supportsFiber: true,
|
||||
|
||||
// React calls these methods.
|
||||
checkDCE,
|
||||
onCommitFiberUnmount,
|
||||
onCommitFiberRoot,
|
||||
});
|
||||
|
||||
Object.defineProperty(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', ({
|
||||
enumerable: false,
|
||||
get () {
|
||||
return hook
|
||||
}
|
||||
}: Object));
|
||||
|
||||
return hook;
|
||||
}
|
||||
12
src/types.js
Normal file
12
src/types.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
|
||||
// TODO
|
||||
export type Bridge = any;
|
||||
|
||||
// TODO
|
||||
export type Hook = any;
|
||||
|
||||
export type Wall = {|
|
||||
listen: (fn: Function) => void,
|
||||
send: (data: any) => void,
|
||||
|};
|
||||
Reference in New Issue
Block a user