diff --git a/.circleci/config.yml b/.circleci/config.yml
index 311665d0f3df3..535563556ee95 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -158,6 +158,16 @@ jobs:
- *run_yarn
- run: yarn test-build --maxWorkers=2
+ test_build_devtools:
+ docker: *docker
+ environment: *environment
+ steps:
+ - checkout
+ - attach_workspace: *attach_workspace
+ - *restore_yarn_cache
+ - *run_yarn
+ - run: yarn test-build-devtools --maxWorkers=2
+
test_dom_fixtures:
docker: *docker
environment: *environment
@@ -231,6 +241,9 @@ workflows:
- test_build_prod:
requires:
- build
+ - test_build_devtools:
+ requires:
+ - build
- test_dom_fixtures:
requires:
- build
diff --git a/.eslintignore b/.eslintignore
index d95f2bf247e3c..62ca593965173 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -12,3 +12,10 @@ scripts/bench/benchmarks/**/*.js
# React repository clone
scripts/bench/remote-repo/
+
+packages/react-devtools-core/dist
+packages/react-devtools-extensions/chrome/build
+packages/react-devtools-extensions/firefox/build
+packages/react-devtools-extensions/shared/build
+packages/react-devtools-inline/dist
+packages/react-devtools-shell/dist
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 53de0c289077e..902e6c9c8838b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,14 @@ chrome-user-data
.vscode
*.swp
*.swo
+
+packages/react-devtools-core/dist
+packages/react-devtools-extensions/chrome/build
+packages/react-devtools-extensions/chrome/*.crx
+packages/react-devtools-extensions/chrome/*.pem
+packages/react-devtools-extensions/firefox/build
+packages/react-devtools-extensions/firefox/*.xpi
+packages/react-devtools-extensions/firefox/*.pem
+packages/react-devtools-extensions/shared/build
+packages/react-devtools-inline/dist
+packages/react-devtools-shell/dist
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000000..bea24210eca5e
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,6 @@
+packages/react-devtools-core/dist
+packages/react-devtools-extensions/chrome/build
+packages/react-devtools-extensions/firefox/build
+packages/react-devtools-extensions/shared/build
+packages/react-devtools-inline/dist
+packages/react-devtools-shell/dist
\ No newline at end of file
diff --git a/fixtures/devtools/regression/14.9.html b/fixtures/devtools/regression/14.9.html
new file mode 100644
index 0000000000000..524ff196881f5
--- /dev/null
+++ b/fixtures/devtools/regression/14.9.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 14.9
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/14.9.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.0.html b/fixtures/devtools/regression/15.0.html
new file mode 100644
index 0000000000000..fde26012c6bbe
--- /dev/null
+++ b/fixtures/devtools/regression/15.0.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.0
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.0.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.1.html b/fixtures/devtools/regression/15.1.html
new file mode 100644
index 0000000000000..49813d80a19d6
--- /dev/null
+++ b/fixtures/devtools/regression/15.1.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.1
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.1.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.2.html b/fixtures/devtools/regression/15.2.html
new file mode 100644
index 0000000000000..2be6f7c41aae3
--- /dev/null
+++ b/fixtures/devtools/regression/15.2.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.2
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.2.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.3.html b/fixtures/devtools/regression/15.3.html
new file mode 100644
index 0000000000000..cc5c3960d2a44
--- /dev/null
+++ b/fixtures/devtools/regression/15.3.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.3
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.3.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.4.html b/fixtures/devtools/regression/15.4.html
new file mode 100644
index 0000000000000..93b425a644b6f
--- /dev/null
+++ b/fixtures/devtools/regression/15.4.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.4
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.4.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.5.html b/fixtures/devtools/regression/15.5.html
new file mode 100644
index 0000000000000..75f722ccabfe6
--- /dev/null
+++ b/fixtures/devtools/regression/15.5.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.5
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.5.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/15.6.html b/fixtures/devtools/regression/15.6.html
new file mode 100644
index 0000000000000..0ec03ee51d123
--- /dev/null
+++ b/fixtures/devtools/regression/15.6.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 15.6
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/15.6.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.0.html b/fixtures/devtools/regression/16.0.html
new file mode 100644
index 0000000000000..1e1403372ca43
--- /dev/null
+++ b/fixtures/devtools/regression/16.0.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 16.0
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.0.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.1.html b/fixtures/devtools/regression/16.1.html
new file mode 100644
index 0000000000000..a6131e9ae1ce9
--- /dev/null
+++ b/fixtures/devtools/regression/16.1.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 16.1
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.1.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.2.html b/fixtures/devtools/regression/16.2.html
new file mode 100644
index 0000000000000..4d0468d343428
--- /dev/null
+++ b/fixtures/devtools/regression/16.2.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 16.2
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.2.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.3.html b/fixtures/devtools/regression/16.3.html
new file mode 100644
index 0000000000000..335adaeffead1
--- /dev/null
+++ b/fixtures/devtools/regression/16.3.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 16.3
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.3.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.4.html b/fixtures/devtools/regression/16.4.html
new file mode 100644
index 0000000000000..8a881ea3fe059
--- /dev/null
+++ b/fixtures/devtools/regression/16.4.html
@@ -0,0 +1,38 @@
+
+
+
+
+ React 16.4
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.4.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.5.html b/fixtures/devtools/regression/16.5.html
new file mode 100644
index 0000000000000..ad91102b1b609
--- /dev/null
+++ b/fixtures/devtools/regression/16.5.html
@@ -0,0 +1,40 @@
+
+
+
+
+ React 16.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.5.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.6.html b/fixtures/devtools/regression/16.6.html
new file mode 100644
index 0000000000000..aa8ae33837e57
--- /dev/null
+++ b/fixtures/devtools/regression/16.6.html
@@ -0,0 +1,41 @@
+
+
+
+
+ React 16.6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.6.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/16.7.html b/fixtures/devtools/regression/16.7.html
new file mode 100644
index 0000000000000..76fb9823014c2
--- /dev/null
+++ b/fixtures/devtools/regression/16.7.html
@@ -0,0 +1,41 @@
+
+
+
+
+ React 16.7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/16.7.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/canary.html b/fixtures/devtools/regression/canary.html
new file mode 100644
index 0000000000000..88cadef4ccaf6
--- /dev/null
+++ b/fixtures/devtools/regression/canary.html
@@ -0,0 +1,41 @@
+
+
+
+
+ React canary
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/canary.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/index.html b/fixtures/devtools/regression/index.html
new file mode 100644
index 0000000000000..125a8e8baf9b9
--- /dev/null
+++ b/fixtures/devtools/regression/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+ React DevTools regression test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/next.html b/fixtures/devtools/regression/next.html
new file mode 100644
index 0000000000000..4ac0751a0c22d
--- /dev/null
+++ b/fixtures/devtools/regression/next.html
@@ -0,0 +1,41 @@
+
+
+
+
+ React next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If you are seeing this message, you are likely viewing this file using the
file
protocol which does not support cross origin requests.
+
+ Use the
server
script instead:
+
+
node ./fixtures/devtools/regression/server.js
+
open http://localhost:3000/next.html
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fixtures/devtools/regression/server.js b/fixtures/devtools/regression/server.js
new file mode 100755
index 0000000000000..34853173a01ee
--- /dev/null
+++ b/fixtures/devtools/regression/server.js
@@ -0,0 +1,16 @@
+#!/usr/bin/env node
+
+const finalhandler = require('finalhandler');
+const http = require('http');
+const serveStatic = require('serve-static');
+
+// Serve fixtures folder
+const serve = serveStatic(__dirname, {index: 'index.html'});
+
+// Create server
+const server = http.createServer(function onRequest(req, res) {
+ serve(req, res, finalhandler(req, res));
+});
+
+// Listen
+server.listen(3000);
diff --git a/fixtures/devtools/regression/shared.js b/fixtures/devtools/regression/shared.js
new file mode 100644
index 0000000000000..055ba3d65e553
--- /dev/null
+++ b/fixtures/devtools/regression/shared.js
@@ -0,0 +1,328 @@
+/* eslint-disable no-fallthrough, react/react-in-jsx-scope, react/jsx-no-undef */
+/* global React ReactCache ReactDOM SchedulerTracing ScheduleTracing */
+
+const apps = [];
+
+const pieces = React.version.split('.');
+const major =
+ pieces[0] === '0' ? parseInt(pieces[1], 10) : parseInt(pieces[0], 10);
+const minor =
+ pieces[0] === '0' ? parseInt(pieces[2], 10) : parseInt(pieces[1], 10);
+
+// Convenience wrapper to organize API features in DevTools.
+function Feature({children, label, version}) {
+ return (
+
+
+ {label}
+ {version}
+
+ {children}
+
+ );
+}
+
+// Simplify interaction tracing for tests below.
+let trace = null;
+if (typeof SchedulerTracing !== 'undefined') {
+ trace = SchedulerTracing.unstable_trace;
+} else if (typeof ScheduleTracing !== 'undefined') {
+ trace = ScheduleTracing.unstable_trace;
+} else {
+ trace = (_, __, callback) => callback();
+}
+
+// https://github.com/facebook/react/blob/master/CHANGELOG.md
+switch (major) {
+ case 16:
+ switch (minor) {
+ case 7:
+ if (typeof React.useState === 'function') {
+ // Hooks
+ function Hooks() {
+ const [count, setCount] = React.useState(0);
+ const incrementCount = React.useCallback(
+ () => setCount(count + 1),
+ [count]
+ );
+ return (
+
+ count: {count}{' '}
+ increment
+
+ );
+ }
+ apps.push(
+
+
+
+ );
+ }
+ case 6:
+ // memo
+ function LabelComponent({label}) {
+ return {label} ;
+ }
+ const AnonymousMemoized = React.memo(({label}) => (
+ {label}
+ ));
+ const Memoized = React.memo(LabelComponent);
+ const CustomMemoized = React.memo(LabelComponent);
+ CustomMemoized.displayName = 'MemoizedLabelFunction';
+ apps.push(
+
+
+
+
+
+ );
+
+ // Suspense
+ const loadResource = ([text, ms]) => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve(text);
+ }, ms);
+ });
+ };
+ const getResourceKey = ([text, ms]) => text;
+ const Resource = ReactCache.unstable_createResource(
+ loadResource,
+ getResourceKey
+ );
+ class Suspending extends React.Component {
+ state = {useSuspense: false};
+ useSuspense = () => this.setState({useSuspense: true});
+ render() {
+ if (this.state.useSuspense) {
+ const text = Resource.read(['loaded', 2000]);
+ return text;
+ } else {
+ return load data ;
+ }
+ }
+ }
+ apps.push(
+
+ loading...}>
+
+
+
+ );
+
+ // lazy
+ const LazyWithDefaultProps = React.lazy(
+ () =>
+ new Promise(resolve => {
+ function FooWithDefaultProps(props) {
+ return (
+
+ {props.greeting}, {props.name}
+
+ );
+ }
+ FooWithDefaultProps.defaultProps = {
+ name: 'World',
+ greeting: 'Bonjour',
+ };
+ resolve({
+ default: FooWithDefaultProps,
+ });
+ })
+ );
+ apps.push(
+
+ loading...}>
+
+
+
+ );
+ case 5:
+ case 4:
+ // unstable_Profiler
+ class ProfilerChild extends React.Component {
+ state = {count: 0};
+ incrementCount = () =>
+ this.setState(prevState => ({count: prevState.count + 1}));
+ render() {
+ return (
+
+ count: {this.state.count}{' '}
+ increment
+
+ );
+ }
+ }
+ const onRender = (...args) => {};
+ const Profiler = React.unstable_Profiler || React.Profiler;
+ apps.push(
+
+
+
+
+
+ );
+ case 3:
+ // createContext()
+ const LocaleContext = React.createContext();
+ LocaleContext.displayName = 'LocaleContext';
+ const ThemeContext = React.createContext();
+ apps.push(
+
+
+
+ {theme => theme: {theme}
}
+
+
+
+
+ {locale => locale: {locale}
}
+
+
+
+ );
+
+ // forwardRef()
+ const AnonymousFunction = React.forwardRef((props, ref) => (
+ {props.children}
+ ));
+ const NamedFunction = React.forwardRef(function named(props, ref) {
+ return {props.children}
;
+ });
+ const CustomName = React.forwardRef((props, ref) => (
+ {props.children}
+ ));
+ CustomName.displayName = 'CustomNameForwardRef';
+ apps.push(
+
+ AnonymousFunction
+ NamedFunction
+ CustomName
+
+ );
+
+ // StrictMode
+ class StrictModeChild extends React.Component {
+ render() {
+ return 'StrictModeChild';
+ }
+ }
+ apps.push(
+
+
+
+
+
+ );
+
+ // unstable_AsyncMode (later renamed to unstable_ConcurrentMode, then ConcurrentMode)
+ const ConcurrentMode =
+ React.ConcurrentMode ||
+ React.unstable_ConcurrentMode ||
+ React.unstable_AsyncMode;
+ apps.push(
+
+
+
+ unstable_AsyncMode was added in 16.3, renamed to
+ unstable_ConcurrentMode in 16.5, and then renamed to
+ ConcurrentMode in 16.7
+
+
+
+ );
+ case 2:
+ // Fragment
+ apps.push(
+
+
+ one
+ two
+
+
+ );
+ case 1:
+ case 0:
+ default:
+ break;
+ }
+ break;
+ case 15:
+ break;
+ case 14:
+ break;
+ default:
+ break;
+}
+
+function Even() {
+ return (even) ;
+}
+
+// Simple stateful app shared by all React versions
+class SimpleApp extends React.Component {
+ state = {count: 0};
+ incrementCount = () => {
+ const updaterFn = prevState => ({count: prevState.count + 1});
+ trace('Updating count', performance.now(), () => this.setState(updaterFn));
+ };
+ render() {
+ const {count} = this.state;
+ return (
+
+ {count % 2 === 0 ? (
+
+ count: {count}
+
+ ) : (
+ count: {count}
+ )}{' '}
+ increment
+
+ );
+ }
+}
+apps.push(
+
+
+
+);
+
+// This component, with the version prop, helps organize DevTools at a glance.
+function TopLevelWrapperForDevTools({version}) {
+ let header = React {version} ;
+ if (version.includes('canary')) {
+ const commitSha = version.match(/.+canary-(.+)/)[1];
+ header = (
+
+ );
+ } else if (version.includes('alpha')) {
+ header = React next ;
+ }
+
+ return (
+
+ {header}
+ {apps}
+
+ );
+}
+TopLevelWrapperForDevTools.displayName = 'React';
+
+ReactDOM.render(
+ ,
+ document.getElementById('root')
+);
diff --git a/fixtures/devtools/regression/styles.css b/fixtures/devtools/regression/styles.css
new file mode 100644
index 0000000000000..6cbaaa5c0149a
--- /dev/null
+++ b/fixtures/devtools/regression/styles.css
@@ -0,0 +1,37 @@
+body {
+ font-family: sans-serif;
+ font-size: 12px;
+}
+
+h1 {
+ margin: 0;
+ font-size: 20px;
+}
+
+h2 {
+ margin: 1rem 0 0;
+}
+
+iframe {
+ border: 1px solid #ddd;
+ border-radius: 0.5rem;
+}
+
+code {
+ white-space: nowrap;
+}
+
+.Feature {
+ margin: 1rem 0;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 1rem;
+}
+.FeatureHeader {
+ font-size: 16px;
+ margin-bottom: 0.5rem;
+}
+.FeatureCode {
+ background-color: #eee;
+ padding: 0.25rem;
+ border-radius: 0.25rem;
+}
diff --git a/fixtures/devtools/standalone/index.html b/fixtures/devtools/standalone/index.html
new file mode 100644
index 0000000000000..28255cb67ee6c
--- /dev/null
+++ b/fixtures/devtools/standalone/index.html
@@ -0,0 +1,313 @@
+
+
+
+
+ TODO List
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 28cc06d5dda9b..2f2bda553eaa7 100644
--- a/package.json
+++ b/package.json
@@ -48,10 +48,10 @@
"error-stack-parser": "^2.0.2",
"eslint": "^6.1.0",
"eslint-config-fbjs": "^1.1.1",
+ "eslint-plugin-babel": "^5.3.0",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-for-of-loops": "^1.0.0",
- "eslint-plugin-babel": "^5.3.0",
"eslint-plugin-react": "^6.7.1",
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "^0.8.3",
@@ -62,8 +62,8 @@
"google-closure-compiler": "20190301.0.0",
"gzip-size": "^3.0.0",
"jasmine-check": "^1.0.0-rc.0",
- "jest": "^23.1.0",
- "jest-diff": "^23.0.1",
+ "jest": "^23",
+ "jest-diff": "^23",
"jest-snapshot-serializer-raw": "^1.1.0",
"minimatch": "^3.0.4",
"minimist": "^1.2.0",
@@ -109,6 +109,8 @@
"test-prod-build": "yarn test-build-prod",
"test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
"test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
+ "test-build-devtools": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build-devtools.js",
+ "debug-test-build-devtools": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.build-devtools.js",
"test-dom-fixture": "cd fixtures/dom && yarn && yarn prestart && yarn test",
"flow": "node ./scripts/tasks/flow.js",
"flow-ci": "node ./scripts/tasks/flow-ci.js",
diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js
index c4a65ac5ba37d..994745b8a75ae 100644
--- a/packages/react-debug-tools/src/ReactDebugHooks.js
+++ b/packages/react-debug-tools/src/ReactDebugHooks.js
@@ -253,14 +253,14 @@ const Dispatcher: DispatcherType = {
// Inspect
-type HooksNode = {
+export type HooksNode = {
id: number | null,
isStateEditable: boolean,
name: string,
value: mixed,
subHooks: Array,
};
-type HooksTree = Array;
+export type HooksTree = Array;
// Don't assume
//
diff --git a/packages/react-devtools-core/README.md b/packages/react-devtools-core/README.md
index 6f45a6f2ea44b..73d05c351f159 100644
--- a/packages/react-devtools-core/README.md
+++ b/packages/react-devtools-core/README.md
@@ -12,20 +12,17 @@ This is similar requiring the `react-devtools` package, but provides several con
```js
const { connectToDevTools } = require("react-devtools-core");
-connectToDevTools({
- // Config options
-});
-
+connectToDevTools(config);
```
Run `connectToDevTools()` in the same context as React to set up a connection to DevTools.
Be sure to run this function *before* importing e.g. `react`, `react-dom`, `react-native`.
-The `options` object may contain:
+The `config` object may contain:
* `host: string` (defaults to "localhost") - Websocket will connect to this host.
* `port: number` (defaults to `8097`) - Websocket will connect to this port.
* `websocket: Websocket` - Custom websocked to use. Overrides `host` and `port` settings if provided.
-* `resolveNativeStyle: (style: number) => ?Object` - Used by the React Native style plug-in.
+* `resolveRNStyle: (style: number) => ?Object` - Used by the React Native style plug-in.
* `isAppActive: () => boolean` - If provided, DevTools will poll this method and wait until it returns true before connecting to React.
## `react-devtools-core/standalone`
@@ -41,4 +38,16 @@ require("react-devtools-core/standalone")
.startServer(port);
```
-Reference the `react-devtools` package for a complete integration example.
\ No newline at end of file
+Reference the `react-devtools` package for a complete integration example.
+
+## Development
+
+Watch for changes made to the backend entry point and rebuild:
+```sh
+yarn start:backend
+```
+
+Watch for changes made to the standalone UI entry point and rebuild:
+```sh
+yarn start:standalone
+```
\ No newline at end of file
diff --git a/packages/react-devtools-core/backend.js b/packages/react-devtools-core/backend.js
new file mode 100644
index 0000000000000..2c2a32d45125b
--- /dev/null
+++ b/packages/react-devtools-core/backend.js
@@ -0,0 +1 @@
+module.exports = require('./dist/backend');
diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json
index d71b62e8248d4..4970dcf263977 100644
--- a/packages/react-devtools-core/package.json
+++ b/packages/react-devtools-core/package.json
@@ -1,5 +1,34 @@
{
- "private": true,
"name": "react-devtools-core",
- "version": "0.0.0"
+ "version": "4.0.6",
+ "description": "Use react-devtools outside of the browser",
+ "license": "MIT",
+ "main": "./dist/backend.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/facebook/react.git",
+ "directory": "packages/react-devtools-core"
+ },
+ "files": [
+ "dist",
+ "backend.js",
+ "build-info.json",
+ "standalone.js"
+ ],
+ "scripts": {
+ "build": "yarn build:backend && yarn build:standalone",
+ "build:backend": "cross-env NODE_ENV=production webpack --config webpack.backend.js",
+ "build:standalone": "cross-env NODE_ENV=production webpack --config webpack.standalone.js",
+ "prepublish": "yarn run build",
+ "start:backend": "cross-env NODE_ENV=development webpack --config webpack.backend.js --watch",
+ "start:standalone": "cross-env NODE_ENV=development webpack --config webpack.standalone.js --watch"
+ },
+ "dependencies": {
+ "es6-symbol": "^3",
+ "shell-quote": "^1.6.1",
+ "ws": "^7"
+ },
+ "devDependencies": {
+ "cross-env": "^3.1.4"
+ }
}
diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js
new file mode 100644
index 0000000000000..e84a07fd3e16f
--- /dev/null
+++ b/packages/react-devtools-core/src/backend.js
@@ -0,0 +1,284 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import Agent from 'react-devtools-shared/src/backend/agent';
+import Bridge from 'react-devtools-shared/src/bridge';
+import {installHook} from 'react-devtools-shared/src/hook';
+import {initBackend} from 'react-devtools-shared/src/backend';
+import {__DEBUG__} from 'react-devtools-shared/src/constants';
+import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
+import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
+
+import type {BackendBridge} from 'react-devtools-shared/src/bridge';
+import type {ComponentFilter} from 'react-devtools-shared/src/types';
+import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
+import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
+
+type ConnectOptions = {
+ host?: string,
+ nativeStyleEditorValidAttributes?: $ReadOnlyArray,
+ port?: number,
+ resolveRNStyle?: ResolveNativeStyle,
+ isAppActive?: () => boolean,
+ websocket?: ?WebSocket,
+};
+
+installHook(window);
+
+const hook: DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
+
+let savedComponentFilters: Array<
+ ComponentFilter,
+> = getDefaultComponentFilters();
+
+function debug(methodName: string, ...args) {
+ if (__DEBUG__) {
+ console.log(
+ `%c[core/backend] %c${methodName}`,
+ 'color: teal; font-weight: bold;',
+ 'font-weight: bold;',
+ ...args,
+ );
+ }
+}
+
+export function connectToDevTools(options: ?ConnectOptions) {
+ const {
+ host = 'localhost',
+ nativeStyleEditorValidAttributes,
+ port = 8097,
+ websocket,
+ resolveRNStyle = null,
+ isAppActive = () => true,
+ } =
+ options || {};
+
+ let retryTimeoutID: TimeoutID | null = null;
+
+ function scheduleRetry() {
+ if (retryTimeoutID === null) {
+ // Two seconds because RN had issues with quick retries.
+ retryTimeoutID = setTimeout(() => connectToDevTools(options), 2000);
+ }
+ }
+
+ if (!isAppActive()) {
+ // If the app is in background, maybe retry later.
+ // Don't actually attempt to connect until we're in foreground.
+ scheduleRetry();
+ return;
+ }
+
+ let bridge: BackendBridge | null = null;
+
+ const messageListeners = [];
+ const uri = 'ws://' + host + ':' + port;
+
+ // If existing websocket is passed, use it.
+ // This is necessary to support our custom integrations.
+ // See D6251744.
+ const ws = websocket ? websocket : new window.WebSocket(uri);
+ ws.onclose = handleClose;
+ ws.onerror = handleFailed;
+ ws.onmessage = handleMessage;
+ ws.onopen = function() {
+ bridge = new Bridge({
+ listen(fn) {
+ messageListeners.push(fn);
+ return () => {
+ const index = messageListeners.indexOf(fn);
+ if (index >= 0) {
+ messageListeners.splice(index, 1);
+ }
+ };
+ },
+ send(event: string, payload: any, transferable?: Array) {
+ if (ws.readyState === ws.OPEN) {
+ if (__DEBUG__) {
+ debug('wall.send()', event, payload);
+ }
+
+ ws.send(JSON.stringify({event, payload}));
+ } else {
+ if (__DEBUG__) {
+ debug(
+ 'wall.send()',
+ 'Shutting down bridge because of closed WebSocket connection',
+ );
+ }
+
+ if (bridge !== null) {
+ bridge.emit('shutdown');
+ }
+
+ scheduleRetry();
+ }
+ },
+ });
+ bridge.addListener(
+ 'inspectElement',
+ ({id, rendererID}: {id: number, rendererID: number}) => {
+ const renderer = agent.rendererInterfaces[rendererID];
+ if (renderer != null) {
+ // Send event for RN to highlight.
+ const nodes: ?Array = renderer.findNativeNodesForFiberID(
+ id,
+ );
+ if (nodes != null && nodes[0] != null) {
+ agent.emit('showNativeHighlight', nodes[0]);
+ }
+ }
+ },
+ );
+ bridge.addListener(
+ 'updateComponentFilters',
+ (componentFilters: Array) => {
+ // Save filter changes in memory, in case DevTools is reloaded.
+ // In that case, the renderer will already be using the updated values.
+ // We'll lose these in between backend reloads but that can't be helped.
+ savedComponentFilters = componentFilters;
+ },
+ );
+
+ // The renderer interface doesn't read saved component filters directly,
+ // because they are generally stored in localStorage within the context of the extension.
+ // Because of this it relies on the extension to pass filters.
+ // In the case of the standalone DevTools being used with a website,
+ // saved filters are injected along with the backend script tag so we shouldn't override them here.
+ // This injection strategy doesn't work for React Native though.
+ // Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution.
+ // So for now we just fall back to using the default filters...
+ if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
+ bridge.send('overrideComponentFilters', savedComponentFilters);
+ }
+
+ // TODO (npm-packages) Warn if "isBackendStorageAPISupported"
+ const agent = new Agent(bridge);
+ agent.addListener('shutdown', () => {
+ // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
+ // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
+ hook.emit('shutdown');
+ });
+
+ initBackend(hook, agent, window);
+
+ // Setup React Native style editor if the environment supports it.
+ if (resolveRNStyle != null || hook.resolveRNStyle != null) {
+ setupNativeStyleEditor(
+ bridge,
+ agent,
+ ((resolveRNStyle || hook.resolveRNStyle: any): ResolveNativeStyle),
+ nativeStyleEditorValidAttributes ||
+ hook.nativeStyleEditorValidAttributes ||
+ null,
+ );
+ } else {
+ // Otherwise listen to detect if the environment later supports it.
+ // For example, Flipper does not eagerly inject these values.
+ // Instead it relies on the React Native Inspector to lazily inject them.
+ let lazyResolveRNStyle;
+ let lazyNativeStyleEditorValidAttributes;
+
+ const initAfterTick = () => {
+ if (bridge !== null) {
+ setupNativeStyleEditor(
+ bridge,
+ agent,
+ lazyResolveRNStyle,
+ lazyNativeStyleEditorValidAttributes,
+ );
+ }
+ };
+
+ if (!hook.hasOwnProperty('resolveRNStyle')) {
+ Object.defineProperty(
+ hook,
+ 'resolveRNStyle',
+ ({
+ enumerable: false,
+ get() {
+ return lazyResolveRNStyle;
+ },
+ set(value) {
+ lazyResolveRNStyle = value;
+ initAfterTick();
+ },
+ }: Object),
+ );
+ }
+ if (!hook.hasOwnProperty('nativeStyleEditorValidAttributes')) {
+ Object.defineProperty(
+ hook,
+ 'nativeStyleEditorValidAttributes',
+ ({
+ enumerable: false,
+ get() {
+ return lazyNativeStyleEditorValidAttributes;
+ },
+ set(value) {
+ lazyNativeStyleEditorValidAttributes = value;
+ initAfterTick();
+ },
+ }: Object),
+ );
+ }
+ }
+ };
+
+ function handleClose() {
+ if (__DEBUG__) {
+ debug('WebSocket.onclose');
+ }
+
+ if (bridge !== null) {
+ bridge.emit('shutdown');
+ }
+
+ scheduleRetry();
+ }
+
+ function handleFailed() {
+ if (__DEBUG__) {
+ debug('WebSocket.onerror');
+ }
+
+ scheduleRetry();
+ }
+
+ function handleMessage(event) {
+ let data;
+ try {
+ if (typeof event.data === 'string') {
+ data = JSON.parse(event.data);
+ if (__DEBUG__) {
+ debug('WebSocket.onmessage', data);
+ }
+ } else {
+ throw Error();
+ }
+ } catch (e) {
+ console.error(
+ '[React DevTools] Failed to parse JSON: ' + (event.data: any),
+ );
+ return;
+ }
+ messageListeners.forEach(fn => {
+ try {
+ fn(data);
+ } catch (error) {
+ // jsc doesn't play so well with tracebacks that go into eval'd code,
+ // so the stack trace here will stop at the `eval()` call. Getting the
+ // message that caused the error is the best we can do for now.
+ console.log('[React DevTools] Error calling listener', data);
+ console.log('error:', error);
+ throw error;
+ }
+ });
+ }
+}
diff --git a/packages/react-devtools-core/src/editor.js b/packages/react-devtools-core/src/editor.js
new file mode 100644
index 0000000000000..bbe3fa63d9857
--- /dev/null
+++ b/packages/react-devtools-core/src/editor.js
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import {existsSync} from 'fs';
+import {basename, join, isAbsolute} from 'path';
+import {execSync, spawn} from 'child_process';
+import {parse} from 'shell-quote';
+
+function isTerminalEditor(editor: string): boolean {
+ switch (editor) {
+ case 'vim':
+ case 'emacs':
+ case 'nano':
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Map from full process name to binary that starts the process
+// We can't just re-use full process name, because it will spawn a new instance
+// of the app every time
+const COMMON_EDITORS = {
+ '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
+ '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
+ '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
+ '/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
+ '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
+ '/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
+ '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
+ '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
+};
+
+function getArgumentsForLineNumber(
+ editor: string,
+ filePath: string,
+ lineNumber: number,
+): Array {
+ switch (basename(editor)) {
+ case 'vim':
+ case 'mvim':
+ return [filePath, '+' + lineNumber];
+ case 'atom':
+ case 'Atom':
+ case 'Atom Beta':
+ case 'subl':
+ case 'sublime':
+ case 'wstorm':
+ case 'appcode':
+ case 'charm':
+ case 'idea':
+ return [filePath + ':' + lineNumber];
+ case 'joe':
+ case 'emacs':
+ case 'emacsclient':
+ return ['+' + lineNumber, filePath];
+ case 'rmate':
+ case 'mate':
+ case 'mine':
+ return ['--line', lineNumber + '', filePath];
+ case 'code':
+ return ['-g', filePath + ':' + lineNumber];
+ default:
+ // For all others, drop the lineNumber until we have
+ // a mapping above, since providing the lineNumber incorrectly
+ // can result in errors or confusing behavior.
+ return [filePath];
+ }
+}
+
+function guessEditor(): Array {
+ // Explicit config always wins
+ if (process.env.REACT_EDITOR) {
+ return parse(process.env.REACT_EDITOR);
+ }
+
+ // Using `ps x` on OSX we can find out which editor is currently running.
+ // Potentially we could use similar technique for Windows and Linux
+ if (process.platform === 'darwin') {
+ try {
+ const output = execSync('ps x').toString();
+ const processNames = Object.keys(COMMON_EDITORS);
+ for (let i = 0; i < processNames.length; i++) {
+ const processName = processNames[i];
+ if (output.indexOf(processName) !== -1) {
+ return [COMMON_EDITORS[processName]];
+ }
+ }
+ } catch (error) {
+ // Ignore...
+ }
+ }
+
+ // Last resort, use old skool env vars
+ if (process.env.VISUAL) {
+ return [process.env.VISUAL];
+ } else if (process.env.EDITOR) {
+ return [process.env.EDITOR];
+ }
+
+ return [];
+}
+
+let childProcess = null;
+
+export function getValidFilePath(
+ maybeRelativePath: string,
+ absoluteProjectRoots: Array,
+): string | null {
+ // We use relative paths at Facebook with deterministic builds.
+ // This is why our internal tooling calls React DevTools with absoluteProjectRoots.
+ // If the filename is absolute then we don't need to care about this.
+ if (isAbsolute(maybeRelativePath)) {
+ if (existsSync(maybeRelativePath)) {
+ return maybeRelativePath;
+ }
+ } else {
+ for (let i = 0; i < absoluteProjectRoots.length; i++) {
+ const projectRoot = absoluteProjectRoots[i];
+ const joinedPath = join(projectRoot, maybeRelativePath);
+ if (existsSync(joinedPath)) {
+ return joinedPath;
+ }
+ }
+ }
+
+ return null;
+}
+
+export function doesFilePathExist(
+ maybeRelativePath: string,
+ absoluteProjectRoots: Array,
+): boolean {
+ return getValidFilePath(maybeRelativePath, absoluteProjectRoots) !== null;
+}
+
+export function launchEditor(
+ maybeRelativePath: string,
+ lineNumber: number,
+ absoluteProjectRoots: Array,
+) {
+ const filePath = getValidFilePath(maybeRelativePath, absoluteProjectRoots);
+ if (filePath === null) {
+ return;
+ }
+
+ // Sanitize lineNumber to prevent malicious use on win32
+ // via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
+ if (lineNumber && isNaN(lineNumber)) {
+ return;
+ }
+
+ let [editor, ...args] = guessEditor();
+ if (!editor) {
+ return;
+ }
+
+ if (lineNumber) {
+ args = args.concat(getArgumentsForLineNumber(editor, filePath, lineNumber));
+ } else {
+ args.push(filePath);
+ }
+
+ if (childProcess && isTerminalEditor(editor)) {
+ // There's an existing editor process already and it's attached
+ // to the terminal, so go kill it. Otherwise two separate editor
+ // instances attach to the stdin/stdout which gets confusing.
+ childProcess.kill('SIGKILL');
+ }
+
+ if (process.platform === 'win32') {
+ // On Windows, launch the editor in a shell because spawn can only
+ // launch .exe files.
+ childProcess = spawn('cmd.exe', ['/C', editor].concat(args), {
+ stdio: 'inherit',
+ });
+ } else {
+ childProcess = spawn(editor, args, {stdio: 'inherit'});
+ }
+ childProcess.on('error', function() {});
+ childProcess.on('exit', function(errorCode) {
+ childProcess = null;
+ });
+}
diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js
new file mode 100644
index 0000000000000..1516fa1c77b51
--- /dev/null
+++ b/packages/react-devtools-core/src/standalone.js
@@ -0,0 +1,326 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import {createElement} from 'react';
+import {
+ // $FlowFixMe Flow does not yet know about flushSync()
+ flushSync,
+ // $FlowFixMe Flow does not yet know about createRoot()
+ unstable_createRoot as createRoot,
+} from 'react-dom';
+import Bridge from 'react-devtools-shared/src/bridge';
+import Store from 'react-devtools-shared/src/devtools/store';
+import {
+ getSavedComponentFilters,
+ getAppendComponentStack,
+} from 'react-devtools-shared/src/utils';
+import {Server} from 'ws';
+import {join} from 'path';
+import {readFileSync} from 'fs';
+import {installHook} from 'react-devtools-shared/src/hook';
+import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
+import {doesFilePathExist, launchEditor} from './editor';
+import {__DEBUG__} from 'react-devtools-shared/src/constants';
+
+import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import type {InspectedElement} from 'react-devtools-shared/src/devtools/views/Components/types';
+
+installHook(window);
+
+export type StatusListener = (message: string) => void;
+
+let node: HTMLElement = ((null: any): HTMLElement);
+let nodeWaitingToConnectHTML: string = '';
+let projectRoots: Array = [];
+let statusListener: StatusListener = (message: string) => {};
+
+// Unlike browser extension users, people using the standalone have actively installed version 4,
+// So we probably don't need to show them a changelog notice.
+// We should give embedded users (e.g. Nuclide, Sonar) a way of showing this dialog though.
+let showWelcomeToTheNewDevToolsDialog: boolean = false;
+
+function setContentDOMNode(value: HTMLElement) {
+ node = value;
+
+ // Save so we can restore the exact waiting message between sessions.
+ nodeWaitingToConnectHTML = node.innerHTML;
+
+ return DevtoolsUI;
+}
+
+function setProjectRoots(value: Array) {
+ projectRoots = value;
+}
+
+function setStatusListener(value: StatusListener) {
+ statusListener = value;
+ return DevtoolsUI;
+}
+
+function setShowWelcomeToTheNewDevToolsDialog(value: boolean) {
+ showWelcomeToTheNewDevToolsDialog = value;
+ return DevtoolsUI;
+}
+
+let bridge: FrontendBridge | null = null;
+let store: Store | null = null;
+let root = null;
+
+const log = (...args) => console.log('[React DevTools]', ...args);
+log.warn = (...args) => console.warn('[React DevTools]', ...args);
+log.error = (...args) => console.error('[React DevTools]', ...args);
+
+function debug(methodName: string, ...args) {
+ if (__DEBUG__) {
+ console.log(
+ `%c[core/standalone] %c${methodName}`,
+ 'color: teal; font-weight: bold;',
+ 'font-weight: bold;',
+ ...args,
+ );
+ }
+}
+
+function safeUnmount() {
+ flushSync(() => {
+ if (root !== null) {
+ root.unmount();
+ }
+ });
+ root = null;
+}
+
+function reload() {
+ safeUnmount();
+
+ node.innerHTML = '';
+
+ setTimeout(() => {
+ root = createRoot(node);
+ root.render(
+ createElement(DevTools, {
+ bridge: ((bridge: any): FrontendBridge),
+ canViewElementSourceFunction,
+ showTabBar: true,
+ showWelcomeToTheNewDevToolsDialog,
+ store: ((store: any): Store),
+ warnIfLegacyBackendDetected: true,
+ viewElementSourceFunction,
+ }),
+ );
+ }, 100);
+}
+
+function canViewElementSourceFunction(
+ inspectedElement: InspectedElement,
+): boolean {
+ if (
+ inspectedElement.canViewSource === false ||
+ inspectedElement.source === null
+ ) {
+ return false;
+ }
+
+ const {source} = inspectedElement;
+
+ return doesFilePathExist(source.fileName, projectRoots);
+}
+
+function viewElementSourceFunction(
+ id: number,
+ inspectedElement: InspectedElement,
+): void {
+ const {source} = inspectedElement;
+ if (source !== null) {
+ launchEditor(source.fileName, source.lineNumber, projectRoots);
+ } else {
+ log.error('Cannot inspect element', id);
+ }
+}
+
+function onDisconnected() {
+ safeUnmount();
+
+ node.innerHTML = nodeWaitingToConnectHTML;
+}
+
+function onError({code, message}) {
+ safeUnmount();
+
+ if (code === 'EADDRINUSE') {
+ node.innerHTML = `
Another instance of DevTools is running `;
+ } else {
+ node.innerHTML = `
Unknown error (${message}) `;
+ }
+}
+
+function initialize(socket: WebSocket) {
+ const listeners = [];
+ socket.onmessage = event => {
+ let data;
+ try {
+ if (typeof event.data === 'string') {
+ data = JSON.parse(event.data);
+
+ if (__DEBUG__) {
+ debug('WebSocket.onmessage', data);
+ }
+ } else {
+ throw Error();
+ }
+ } catch (e) {
+ log.error('Failed to parse JSON', event.data);
+ return;
+ }
+ listeners.forEach(fn => {
+ try {
+ fn(data);
+ } catch (error) {
+ log.error('Error calling listener', data);
+ throw error;
+ }
+ });
+ };
+
+ bridge = new Bridge({
+ listen(fn) {
+ listeners.push(fn);
+ return () => {
+ const index = listeners.indexOf(fn);
+ if (index >= 0) {
+ listeners.splice(index, 1);
+ }
+ };
+ },
+ send(event: string, payload: any, transferable?: Array) {
+ if (socket.readyState === socket.OPEN) {
+ socket.send(JSON.stringify({event, payload}));
+ }
+ },
+ });
+ ((bridge: any): FrontendBridge).addListener('shutdown', () => {
+ socket.close();
+ });
+
+ store = new Store(bridge, {supportsNativeInspection: false});
+
+ log('Connected');
+ reload();
+}
+
+let startServerTimeoutID: TimeoutID | null = null;
+
+function connectToSocket(socket: WebSocket) {
+ socket.onerror = err => {
+ onDisconnected();
+ log.error('Error with websocket connection', err);
+ };
+ socket.onclose = () => {
+ onDisconnected();
+ log('Connection to RN closed');
+ };
+ initialize(socket);
+
+ return {
+ close: function() {
+ onDisconnected();
+ },
+ };
+}
+
+function startServer(port?: number = 8097) {
+ const httpServer = require('http').createServer();
+ const server = new Server({server: httpServer});
+ let connected: WebSocket | null = null;
+ server.on('connection', (socket: WebSocket) => {
+ if (connected !== null) {
+ connected.close();
+ log.warn(
+ 'Only one connection allowed at a time.',
+ 'Closing the previous connection',
+ );
+ }
+ connected = socket;
+ socket.onerror = error => {
+ connected = null;
+ onDisconnected();
+ log.error('Error with websocket connection', error);
+ };
+ socket.onclose = () => {
+ connected = null;
+ onDisconnected();
+ log('Connection to RN closed');
+ };
+ initialize(socket);
+ });
+
+ server.on('error', event => {
+ onError(event);
+ log.error('Failed to start the DevTools server', event);
+ startServerTimeoutID = setTimeout(() => startServer(port), 1000);
+ });
+
+ httpServer.on('request', (request, response) => {
+ // Serve a file that immediately sets up the connection.
+ const backendFile = readFileSync(join(__dirname, 'backend.js'));
+
+ // The renderer interface doesn't read saved component filters directly,
+ // because they are generally stored in localStorage within the context of the extension.
+ // Because of this it relies on the extension to pass filters, so include them wth the response here.
+ // This will ensure that saved filters are shared across different web pages.
+ const savedPreferencesString = `
+ window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
+ getSavedComponentFilters(),
+ )};
+ window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
+ getAppendComponentStack(),
+ )};`;
+
+ response.end(
+ savedPreferencesString +
+ '\n;' +
+ backendFile.toString() +
+ '\n;' +
+ 'ReactDevToolsBackend.connectToDevTools();',
+ );
+ });
+
+ httpServer.on('error', event => {
+ onError(event);
+ statusListener('Failed to start the server.');
+ startServerTimeoutID = setTimeout(() => startServer(port), 1000);
+ });
+
+ httpServer.listen(port, () => {
+ statusListener('The server is listening on the port ' + port + '.');
+ });
+
+ return {
+ close: function() {
+ connected = null;
+ onDisconnected();
+ if (startServerTimeoutID !== null) {
+ clearTimeout(startServerTimeoutID);
+ }
+ server.close();
+ httpServer.close();
+ },
+ };
+}
+
+const DevtoolsUI = {
+ connectToSocket,
+ setContentDOMNode,
+ setProjectRoots,
+ setShowWelcomeToTheNewDevToolsDialog,
+ setStatusListener,
+ startServer,
+};
+
+export default DevtoolsUI;
diff --git a/packages/react-devtools-core/standalone.js b/packages/react-devtools-core/standalone.js
new file mode 100644
index 0000000000000..fe55aa11d5d41
--- /dev/null
+++ b/packages/react-devtools-core/standalone.js
@@ -0,0 +1 @@
+module.exports = require('./dist/standalone');
diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js
new file mode 100644
index 0000000000000..3a27421d94e84
--- /dev/null
+++ b/packages/react-devtools-core/webpack.backend.js
@@ -0,0 +1,67 @@
+const {resolve} = require('path');
+const {DefinePlugin} = require('webpack');
+const {
+ getGitHubURL,
+ getVersionString,
+} = require('react-devtools-extensions/utils');
+
+const NODE_ENV = process.env.NODE_ENV;
+if (!NODE_ENV) {
+ console.error('NODE_ENV not set');
+ process.exit(1);
+}
+
+const builtModulesDir = resolve(__dirname, '..', '..', 'build', 'node_modules');
+
+const __DEV__ = NODE_ENV === 'development';
+
+const GITHUB_URL = getGitHubURL();
+const DEVTOOLS_VERSION = getVersionString();
+
+module.exports = {
+ mode: __DEV__ ? 'development' : 'production',
+ devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
+ entry: {
+ backend: './src/backend.js',
+ },
+ output: {
+ path: __dirname + '/dist',
+ filename: '[name].js',
+
+ // This name is important; standalone references it in order to connect.
+ library: 'ReactDevToolsBackend',
+ libraryTarget: 'umd',
+ },
+ resolve: {
+ alias: {
+ react: resolve(builtModulesDir, 'react'),
+ 'react-dom': resolve(builtModulesDir, 'react-dom'),
+ 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
+ 'react-is': resolve(builtModulesDir, 'react-is'),
+ scheduler: resolve(builtModulesDir, 'scheduler'),
+ },
+ },
+ plugins: [
+ new DefinePlugin({
+ __DEV__: true,
+ 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
+ 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
+ }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ options: {
+ configFile: resolve(
+ __dirname,
+ '..',
+ 'react-devtools-shared',
+ 'babel.config.js',
+ ),
+ },
+ },
+ ],
+ },
+};
diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js
new file mode 100644
index 0000000000000..d14e6514ba934
--- /dev/null
+++ b/packages/react-devtools-core/webpack.standalone.js
@@ -0,0 +1,93 @@
+const {resolve} = require('path');
+const {DefinePlugin} = require('webpack');
+const {
+ getGitHubURL,
+ getVersionString,
+} = require('react-devtools-extensions/utils');
+
+const NODE_ENV = process.env.NODE_ENV;
+if (!NODE_ENV) {
+ console.error('NODE_ENV not set');
+ process.exit(1);
+}
+
+const builtModulesDir = resolve(__dirname, '..', '..', 'build', 'node_modules');
+
+const __DEV__ = NODE_ENV === 'development';
+
+const GITHUB_URL = getGitHubURL();
+const DEVTOOLS_VERSION = getVersionString();
+
+module.exports = {
+ mode: __DEV__ ? 'development' : 'production',
+ devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
+ target: 'electron-main',
+ entry: {
+ standalone: './src/standalone.js',
+ },
+ output: {
+ path: __dirname + '/dist',
+ filename: '[name].js',
+ library: '[name]',
+ libraryTarget: 'commonjs2',
+ },
+ resolve: {
+ alias: {
+ react: resolve(builtModulesDir, 'react'),
+ 'react-dom': resolve(builtModulesDir, 'react-dom'),
+ 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
+ 'react-is': resolve(builtModulesDir, 'react-is'),
+ scheduler: resolve(builtModulesDir, 'scheduler'),
+ },
+ },
+ node: {
+ // Don't replace __dirname!
+ // This would break the standalone DevTools ability to load the backend.
+ // see https://github.com/facebook/react-devtools/issues/1269
+ __dirname: false,
+ },
+ plugins: [
+ new DefinePlugin({
+ __DEV__: false,
+ '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,
+ '..',
+ 'react-devtools-shared',
+ 'babel.config.js',
+ ),
+ },
+ },
+ {
+ test: /\.css$/,
+ use: [
+ {
+ loader: 'style-loader',
+ },
+ {
+ loader: 'css-loader',
+ options: {
+ // WARNING It's important that we disable CSS source maps for production builds.
+ // This causes style-loader to insert styles via a
+
+
+
+ React DevTools pre-release
+
+
+
+ Created on %date% from
+ %commit%
+
+
+
+ This is a preview build of the React DevTools extension .
+
+
+ Installation instructions
+ %installation%
+
+ If you already have the React DevTools extension installed, you will need to temporarily disable or remove it in order to install this prerelease build.
+
+
+ Bug reports
+
+ Please report bugs as GitHub issues .
+ Please include all of the info required to reproduce the bug (e.g. links, code, instructions).
+
+
+