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}{' '} + +
+ ); + } + apps.push( + + + + ); + } + case 6: + // memo + function LabelComponent({label}) { + return ; + } + const AnonymousMemoized = React.memo(({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 ; + } + } + } + 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}{' '} + +
+ ); + } + } + 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} + )}{' '} + +
+ ); + } +} +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 = ( +

+ React canary{' '} + + {commitSha} + +

+ ); + } 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). +

+ + diff --git a/packages/react-devtools-extensions/deploy.js b/packages/react-devtools-extensions/deploy.js new file mode 100644 index 0000000000000..c9bfdaf6af73f --- /dev/null +++ b/packages/react-devtools-extensions/deploy.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +'use strict'; + +const {exec, execSync} = require('child_process'); +const {readFileSync, writeFileSync} = require('fs'); +const {join} = require('path'); + +const main = async buildId => { + const root = join(__dirname, buildId); + const buildPath = join(root, 'build'); + + execSync(`node ${join(root, './build')}`, { + cwd: __dirname, + env: { + ...process.env, + NODE_ENV: 'production', + }, + stdio: 'inherit', + }); + + await exec(`cp ${join(root, 'now.json')} ${join(buildPath, 'now.json')}`, { + cwd: root, + }); + + const file = readFileSync(join(root, 'now.json')); + const json = JSON.parse(file); + const alias = json.alias[0]; + + const commit = execSync('git rev-parse HEAD') + .toString() + .trim() + .substr(0, 7); + + let date = new Date(); + date = `${date.toLocaleDateString()} – ${date.toLocaleTimeString()}`; + + const installationInstructions = + buildId === 'chrome' + ? readFileSync(join(__dirname, 'deploy.chrome.html')) + : readFileSync(join(__dirname, 'deploy.firefox.html')); + + let html = readFileSync(join(__dirname, 'deploy.html')).toString(); + html = html.replace(/%commit%/g, commit); + html = html.replace(/%date%/g, date); + html = html.replace(/%installation%/, installationInstructions); + + writeFileSync(join(buildPath, 'index.html'), html); + + await exec(`now deploy && now alias ${alias}`, { + cwd: buildPath, + stdio: 'inherit', + }); + + console.log(`Deployed to https://${alias}.now.sh`); +}; + +module.exports = main; diff --git a/packages/react-devtools-extensions/firefox/README.md b/packages/react-devtools-extensions/firefox/README.md new file mode 100644 index 0000000000000..c4bbc8d2de6c6 --- /dev/null +++ b/packages/react-devtools-extensions/firefox/README.md @@ -0,0 +1,12 @@ +# The Firefox extension + +The source code for this extension has moved to `shells/webextension`. + +Modify the source code there and then rebuild this extension by running `node build` from this directory or `yarn run build:extension:firefox` from the root directory. + +## Testing in Firefox + + 1. Build the extension: `node build` + 1. Follow the on-screen instructions. + +You can test upcoming releases of Firefox by downloading the Beta or Nightly build from the [Firefox releases](https://www.mozilla.org/en-US/firefox/channel/desktop/) page and then following the on-screen instructions after building. diff --git a/packages/react-devtools-extensions/firefox/build.js b/packages/react-devtools-extensions/firefox/build.js new file mode 100644 index 0000000000000..3ff45547a90f2 --- /dev/null +++ b/packages/react-devtools-extensions/firefox/build.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const build = require('../build'); + +const main = async () => { + await build('firefox'); + + console.log(chalk.green('\nThe Firefox extension has been built!')); + console.log(chalk.green('You can test this build by running:')); + console.log(chalk.gray('\n# From the react-devtools root directory:')); + console.log('yarn run test:firefox'); + console.log( + chalk.gray('\n# You can also test against upcoming Firefox releases.') + ); + console.log( + chalk.gray( + '# First download a release from https://www.mozilla.org/en-US/firefox/channel/desktop/' + ) + ); + console.log( + chalk.gray( + '# And then tell web-ext which release to use (eg firefoxdeveloperedition, nightly, beta):' + ) + ); + console.log('WEB_EXT_FIREFOX=nightly yarn run test:firefox'); + console.log(chalk.gray('\n# You can test against older versions too:')); + console.log( + 'WEB_EXT_FIREFOX=/Applications/Firefox52.app/Contents/MacOS/firefox-bin yarn run test:firefox' + ); +}; + +main(); + +module.exports = {main}; diff --git a/packages/react-devtools-extensions/firefox/deploy.js b/packages/react-devtools-extensions/firefox/deploy.js new file mode 100644 index 0000000000000..49d83ead46123 --- /dev/null +++ b/packages/react-devtools-extensions/firefox/deploy.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +'use strict'; + +const deploy = require('../deploy'); + +const main = async () => await deploy('firefox'); + +main(); diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json new file mode 100644 index 0000000000000..77ea161a7a632 --- /dev/null +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -0,0 +1,56 @@ +{ + "manifest_version": 2, + "name": "React Developer Tools", + "description": "Adds React debugging tools to the Firefox Developer Tools.", + "version": "4.0.6", + + "applications": { + "gecko": { + "id": "@react-devtools", + "strict_min_version": "54.0" + } + }, + + "icons": { + "16": "icons/16-production.png", + "32": "icons/32-production.png", + "48": "icons/48-production.png", + "128": "icons/128-production.png" + }, + + "browser_action": { + "default_icon": { + "16": "icons/16-disabled.png", + "32": "icons/32-disabled.png", + "48": "icons/48-disabled.png", + "128": "icons/128-disabled.png" + }, + + "default_popup": "popups/disabled.html", + "browser_style": true + }, + + "devtools_page": "main.html", + + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "web_accessible_resources": [ + "main.html", + "panel.html", + "build/backend.js", + "build/renderer.js" + ], + + "background": { + "scripts": ["build/background.js"] + }, + + "permissions": ["file:///*", "http://*/*", "https://*/*"], + + "content_scripts": [ + { + "matches": [""], + "js": ["build/injectGlobalHook.js"], + "run_at": "document_start" + } + ] +} diff --git a/packages/react-devtools-extensions/firefox/now.json b/packages/react-devtools-extensions/firefox/now.json new file mode 100644 index 0000000000000..5e61bb442f567 --- /dev/null +++ b/packages/react-devtools-extensions/firefox/now.json @@ -0,0 +1,5 @@ +{ + "name": "react-devtools-experimental-firefox", + "alias": ["react-devtools-experimental-firefox"], + "files": ["index.html", "ReactDevTools.zip"] +} diff --git a/packages/react-devtools-extensions/firefox/test.js b/packages/react-devtools-extensions/firefox/test.js new file mode 100644 index 0000000000000..754c2f05e79b7 --- /dev/null +++ b/packages/react-devtools-extensions/firefox/test.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +'use strict'; + +const {exec} = require('child-process-promise'); +const {Finder} = require('firefox-profile'); +const {resolve} = require('path'); + +const EXTENSION_PATH = resolve('./firefox/build/unpacked'); +const START_URL = 'https://facebook.github.io/react/'; + +const main = async () => { + const finder = new Finder(); + + // Use default Firefox profile for testing purposes. + // This prevents users from having to re-login-to sites before testing. + const findPathPromise = new Promise((resolvePromise, rejectPromise) => { + finder.getPath('default', (error, profile) => { + if (error) { + rejectPromise(error); + } else { + resolvePromise(profile); + } + }); + }); + + const options = [ + `--source-dir=${EXTENSION_PATH}`, + `--start-url=${START_URL}`, + '--browser-console', + ]; + + try { + const path = await findPathPromise; + const trimmedPath = path.replace(' ', '\\ '); + options.push(`--firefox-profile=${trimmedPath}`); + } catch (err) { + console.warn('Could not find default profile, using temporary profile.'); + } + + try { + await exec(`web-ext run ${options.join(' ')}`); + } catch (err) { + console.error('`web-ext run` failed', err.stdout, err.stderr); + } +}; + +main(); diff --git a/packages/react-devtools-extensions/flow-typed/jest.js b/packages/react-devtools-extensions/flow-typed/jest.js new file mode 100644 index 0000000000000..f8cd1b90be611 --- /dev/null +++ b/packages/react-devtools-extensions/flow-typed/jest.js @@ -0,0 +1,1190 @@ +'use strict'; + +type JestMockFn, TReturn> = { + (...args: TArguments): TReturn, + /** + * An object for introspecting mock calls + */ + mock: { + /** + * An array that represents all calls that have been made into this mock + * function. Each call is represented by an array of arguments that were + * passed during the call. + */ + calls: Array, + /** + * An array that contains all the object instances that have been + * instantiated from this mock function. + */ + instances: Array, + /** + * An array that contains all the object results that have been + * returned by this mock function call + */ + results: Array<{isThrow: boolean, value: TReturn}>, + }, + /** + * Resets all information stored in the mockFn.mock.calls and + * mockFn.mock.instances arrays. Often this is useful when you want to clean + * up a mock's usage data between two assertions. + */ + mockClear(): void, + /** + * Resets all information stored in the mock. This is useful when you want to + * completely restore a mock back to its initial state. + */ + mockReset(): void, + /** + * Removes the mock and restores the initial implementation. This is useful + * when you want to mock functions in certain test cases and restore the + * original implementation in others. Beware that mockFn.mockRestore only + * works when mock was created with jest.spyOn. Thus you have to take care of + * restoration yourself when manually assigning jest.fn(). + */ + mockRestore(): void, + /** + * Accepts a function that should be used as the implementation of the mock. + * The mock itself will still record all calls that go into and instances + * that come from itself -- the only difference is that the implementation + * will also be executed when the mock is called. + */ + mockImplementation( + fn: (...args: TArguments) => TReturn + ): JestMockFn, + /** + * Accepts a function that will be used as an implementation of the mock for + * one call to the mocked function. Can be chained so that multiple function + * calls produce different results. + */ + mockImplementationOnce( + fn: (...args: TArguments) => TReturn + ): JestMockFn, + /** + * Accepts a string to use in test result output in place of "jest.fn()" to + * indicate which mock function is being referenced. + */ + mockName(name: string): JestMockFn, + /** + * Just a simple sugar function for returning `this` + */ + mockReturnThis(): void, + /** + * Accepts a value that will be returned whenever the mock function is called. + */ + mockReturnValue(value: TReturn): JestMockFn, + /** + * Sugar for only returning a value once inside your mock + */ + mockReturnValueOnce(value: TReturn): JestMockFn, + /** + * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) + */ + mockResolvedValue(value: TReturn): JestMockFn>, + /** + * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) + */ + mockResolvedValueOnce( + value: TReturn + ): JestMockFn>, + /** + * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) + */ + mockRejectedValue(value: TReturn): JestMockFn>, + /** + * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) + */ + mockRejectedValueOnce(value: TReturn): JestMockFn>, +}; + +type JestAsymmetricEqualityType = { + /** + * A custom Jasmine equality tester + */ + asymmetricMatch(value: mixed): boolean, +}; + +type JestCallsType = { + allArgs(): mixed, + all(): mixed, + any(): boolean, + count(): number, + first(): mixed, + mostRecent(): mixed, + reset(): void, +}; + +type JestClockType = { + install(): void, + mockDate(date: Date): void, + tick(milliseconds?: number): void, + uninstall(): void, +}; + +type JestMatcherResult = { + message?: string | (() => string), + pass: boolean, +}; + +type JestMatcher = ( + actual: any, + expected: any +) => JestMatcherResult | Promise; + +type JestPromiseType = { + /** + * Use rejects to unwrap the reason of a rejected promise so any other + * matcher can be chained. If the promise is fulfilled the assertion fails. + */ + // eslint-disable-next-line no-use-before-define + rejects: JestExpectType, + /** + * Use resolves to unwrap the value of a fulfilled promise so any other + * matcher can be chained. If the promise is rejected the assertion fails. + */ + // eslint-disable-next-line no-use-before-define + resolves: JestExpectType, +}; + +/** + * Jest allows functions and classes to be used as test names in test() and + * describe() + */ +type JestTestName = string | Function; + +/** + * Plugin: jest-styled-components + */ + +type JestStyledComponentsMatcherValue = + | string + | JestAsymmetricEqualityType + | RegExp + | typeof undefined; + +type JestStyledComponentsMatcherOptions = { + media?: string, + modifier?: string, + supports?: string, +}; + +type JestStyledComponentsMatchersType = { + toHaveStyleRule( + property: string, + value: JestStyledComponentsMatcherValue, + options?: JestStyledComponentsMatcherOptions + ): void, +}; + +/** + * Plugin: jest-enzyme + */ +type EnzymeMatchersType = { + // 5.x + toBeEmpty(): void, + toBePresent(): void, + // 6.x + toBeChecked(): void, + toBeDisabled(): void, + toBeEmptyRender(): void, + toContainMatchingElement(selector: string): void, + toContainMatchingElements(n: number, selector: string): void, + toContainExactlyOneMatchingElement(selector: string): void, + toContainReact(element: React$Element): void, + toExist(): void, + toHaveClassName(className: string): void, + toHaveHTML(html: string): void, + toHaveProp: ((propKey: string, propValue?: any) => void) & + ((props: {}) => void), + toHaveRef(refName: string): void, + toHaveState: ((stateKey: string, stateValue?: any) => void) & + ((state: {}) => void), + toHaveStyle: ((styleKey: string, styleValue?: any) => void) & + ((style: {}) => void), + toHaveTagName(tagName: string): void, + toHaveText(text: string): void, + toHaveValue(value: any): void, + toIncludeText(text: string): void, + toMatchElement( + element: React$Element, + options?: {|ignoreProps?: boolean, verbose?: boolean|} + ): void, + toMatchSelector(selector: string): void, + // 7.x + toHaveDisplayName(name: string): void, +}; + +// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers +type DomTestingLibraryType = { + toBeDisabled(): void, + toBeEmpty(): void, + toBeInTheDocument(): void, + toBeVisible(): void, + toContainElement(element: HTMLElement | null): void, + toContainHTML(htmlText: string): void, + toHaveAttribute(name: string, expectedValue?: string): void, + toHaveClass(...classNames: string[]): void, + toHaveFocus(): void, + toHaveFormValues(expectedValues: {[name: string]: any}): void, + toHaveStyle(css: string): void, + toHaveTextContent( + content: string | RegExp, + options?: {normalizeWhitespace: boolean} + ): void, + toBeInTheDOM(): void, +}; + +// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers +type JestJQueryMatchersType = { + toExist(): void, + toHaveLength(len: number): void, + toHaveId(id: string): void, + toHaveClass(className: string): void, + toHaveTag(tag: string): void, + toHaveAttr(key: string, val?: any): void, + toHaveProp(key: string, val?: any): void, + toHaveText(text: string | RegExp): void, + toHaveData(key: string, val?: any): void, + toHaveValue(val: any): void, + toHaveCss(css: {[key: string]: any}): void, + toBeChecked(): void, + toBeDisabled(): void, + toBeEmpty(): void, + toBeHidden(): void, + toBeSelected(): void, + toBeVisible(): void, + toBeFocused(): void, + toBeInDom(): void, + toBeMatchedBy(sel: string): void, + toHaveDescendant(sel: string): void, + toHaveDescendantWithText(sel: string, text: string | RegExp): void, +}; + +// Jest Extended Matchers: https://github.com/jest-community/jest-extended +type JestExtendedMatchersType = { + /** + * Note: Currently unimplemented + * Passing assertion + * + * @param {String} message + */ + // pass(message: string): void; + + /** + * Note: Currently unimplemented + * Failing assertion + * + * @param {String} message + */ + // fail(message: string): void; + + /** + * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. + */ + toBeEmpty(): void, + + /** + * Use .toBeOneOf when checking if a value is a member of a given Array. + * @param {Array.<*>} members + */ + toBeOneOf(members: any[]): void, + + /** + * Use `.toBeNil` when checking a value is `null` or `undefined`. + */ + toBeNil(): void, + + /** + * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. + * @param {Function} predicate + */ + toSatisfy(predicate: (n: any) => boolean): void, + + /** + * Use `.toBeArray` when checking if a value is an `Array`. + */ + toBeArray(): void, + + /** + * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. + * @param {Number} x + */ + toBeArrayOfSize(x: number): void, + + /** + * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. + * @param {Array.<*>} members + */ + toIncludeAllMembers(members: any[]): void, + + /** + * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. + * @param {Array.<*>} members + */ + toIncludeAnyMembers(members: any[]): void, + + /** + * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. + * @param {Function} predicate + */ + toSatisfyAll(predicate: (n: any) => boolean): void, + + /** + * Use `.toBeBoolean` when checking if a value is a `Boolean`. + */ + toBeBoolean(): void, + + /** + * Use `.toBeTrue` when checking a value is equal (===) to `true`. + */ + toBeTrue(): void, + + /** + * Use `.toBeFalse` when checking a value is equal (===) to `false`. + */ + toBeFalse(): void, + + /** + * Use .toBeDate when checking if a value is a Date. + */ + toBeDate(): void, + + /** + * Use `.toBeFunction` when checking if a value is a `Function`. + */ + toBeFunction(): void, + + /** + * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. + * + * Note: Required Jest version >22 + * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same + * + * @param {Mock} mock + */ + toHaveBeenCalledBefore(mock: JestMockFn): void, + + /** + * Use `.toBeNumber` when checking if a value is a `Number`. + */ + toBeNumber(): void, + + /** + * Use `.toBeNaN` when checking a value is `NaN`. + */ + toBeNaN(): void, + + /** + * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. + */ + toBeFinite(): void, + + /** + * Use `.toBePositive` when checking if a value is a positive `Number`. + */ + toBePositive(): void, + + /** + * Use `.toBeNegative` when checking if a value is a negative `Number`. + */ + toBeNegative(): void, + + /** + * Use `.toBeEven` when checking if a value is an even `Number`. + */ + toBeEven(): void, + + /** + * Use `.toBeOdd` when checking if a value is an odd `Number`. + */ + toBeOdd(): void, + + /** + * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). + * + * @param {Number} start + * @param {Number} end + */ + toBeWithin(start: number, end: number): void, + + /** + * Use `.toBeObject` when checking if a value is an `Object`. + */ + toBeObject(): void, + + /** + * Use `.toContainKey` when checking if an object contains the provided key. + * + * @param {String} key + */ + toContainKey(key: string): void, + + /** + * Use `.toContainKeys` when checking if an object has all of the provided keys. + * + * @param {Array.} keys + */ + toContainKeys(keys: string[]): void, + + /** + * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. + * + * @param {Array.} keys + */ + toContainAllKeys(keys: string[]): void, + + /** + * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. + * + * @param {Array.} keys + */ + toContainAnyKeys(keys: string[]): void, + + /** + * Use `.toContainValue` when checking if an object contains the provided value. + * + * @param {*} value + */ + toContainValue(value: any): void, + + /** + * Use `.toContainValues` when checking if an object contains all of the provided values. + * + * @param {Array.<*>} values + */ + toContainValues(values: any[]): void, + + /** + * Use `.toContainAllValues` when checking if an object only contains all of the provided values. + * + * @param {Array.<*>} values + */ + toContainAllValues(values: any[]): void, + + /** + * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. + * + * @param {Array.<*>} values + */ + toContainAnyValues(values: any[]): void, + + /** + * Use `.toContainEntry` when checking if an object contains the provided entry. + * + * @param {Array.} entry + */ + toContainEntry(entry: [string, string]): void, + + /** + * Use `.toContainEntries` when checking if an object contains all of the provided entries. + * + * @param {Array.>} entries + */ + toContainEntries(entries: [string, string][]): void, + + /** + * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. + * + * @param {Array.>} entries + */ + toContainAllEntries(entries: [string, string][]): void, + + /** + * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. + * + * @param {Array.>} entries + */ + toContainAnyEntries(entries: [string, string][]): void, + + /** + * Use `.toBeExtensible` when checking if an object is extensible. + */ + toBeExtensible(): void, + + /** + * Use `.toBeFrozen` when checking if an object is frozen. + */ + toBeFrozen(): void, + + /** + * Use `.toBeSealed` when checking if an object is sealed. + */ + toBeSealed(): void, + + /** + * Use `.toBeString` when checking if a value is a `String`. + */ + toBeString(): void, + + /** + * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. + * + * @param {String} string + */ + toEqualCaseInsensitive(string: string): void, + + /** + * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. + * + * @param {String} prefix + */ + toStartWith(prefix: string): void, + + /** + * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. + * + * @param {String} suffix + */ + toEndWith(suffix: string): void, + + /** + * Use `.toInclude` when checking if a `String` includes the given `String` substring. + * + * @param {String} substring + */ + toInclude(substring: string): void, + + /** + * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. + * + * @param {String} substring + * @param {Number} times + */ + toIncludeRepeated(substring: string, times: number): void, + + /** + * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. + * + * @param {Array.} substring + */ + toIncludeMultiple(substring: string[]): void, +}; + +interface JestExpectType { + not: JestExpectType & + EnzymeMatchersType & + DomTestingLibraryType & + JestJQueryMatchersType & + JestStyledComponentsMatchersType & + JestExtendedMatchersType; + /** + * If you have a mock function, you can use .lastCalledWith to test what + * arguments it was last called with. + */ + lastCalledWith(...args: Array): void; + /** + * toBe just checks that a value is what you expect. It uses === to check + * strict equality. + */ + toBe(value: any): void; + /** + * Use .toBeCalledWith to ensure that a mock function was called with + * specific arguments. + */ + toBeCalledWith(...args: Array): void; + /** + * Using exact equality with floating point numbers is a bad idea. Rounding + * means that intuitive things fail. + */ + toBeCloseTo(num: number, delta: any): void; + /** + * Use .toBeDefined to check that a variable is not undefined. + */ + toBeDefined(): void; + /** + * Use .toBeFalsy when you don't care what a value is, you just want to + * ensure a value is false in a boolean context. + */ + toBeFalsy(): void; + /** + * To compare floating point numbers, you can use toBeGreaterThan. + */ + toBeGreaterThan(number: number): void; + /** + * To compare floating point numbers, you can use toBeGreaterThanOrEqual. + */ + toBeGreaterThanOrEqual(number: number): void; + /** + * To compare floating point numbers, you can use toBeLessThan. + */ + toBeLessThan(number: number): void; + /** + * To compare floating point numbers, you can use toBeLessThanOrEqual. + */ + toBeLessThanOrEqual(number: number): void; + /** + * Use .toBeInstanceOf(Class) to check that an object is an instance of a + * class. + */ + toBeInstanceOf(cls: Class<*>): void; + /** + * .toBeNull() is the same as .toBe(null) but the error messages are a bit + * nicer. + */ + toBeNull(): void; + /** + * Use .toBeTruthy when you don't care what a value is, you just want to + * ensure a value is true in a boolean context. + */ + toBeTruthy(): void; + /** + * Use .toBeUndefined to check that a variable is undefined. + */ + toBeUndefined(): void; + /** + * Use .toContain when you want to check that an item is in a list. For + * testing the items in the list, this uses ===, a strict equality check. + */ + toContain(item: any): void; + /** + * Use .toContainEqual when you want to check that an item is in a list. For + * testing the items in the list, this matcher recursively checks the + * equality of all fields, rather than checking for object identity. + */ + toContainEqual(item: any): void; + /** + * Use .toEqual when you want to check that two objects have the same value. + * This matcher recursively checks the equality of all fields, rather than + * checking for object identity. + */ + toEqual(value: any): void; + /** + * Use .toHaveBeenCalled to ensure that a mock function got called. + */ + toHaveBeenCalled(): void; + toBeCalled(): void; + /** + * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact + * number of times. + */ + toHaveBeenCalledTimes(number: number): void; + toBeCalledTimes(number: number): void; + /** + * + */ + toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; + nthCalledWith(nthCall: number, ...args: Array): void; + /** + * + */ + toHaveReturned(): void; + toReturn(): void; + /** + * + */ + toHaveReturnedTimes(number: number): void; + toReturnTimes(number: number): void; + /** + * + */ + toHaveReturnedWith(value: any): void; + toReturnWith(value: any): void; + /** + * + */ + toHaveLastReturnedWith(value: any): void; + lastReturnedWith(value: any): void; + /** + * + */ + toHaveNthReturnedWith(nthCall: number, value: any): void; + nthReturnedWith(nthCall: number, value: any): void; + /** + * Use .toHaveBeenCalledWith to ensure that a mock function was called with + * specific arguments. + */ + toHaveBeenCalledWith(...args: Array): void; + toBeCalledWith(...args: Array): void; + /** + * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called + * with specific arguments. + */ + toHaveBeenLastCalledWith(...args: Array): void; + lastCalledWith(...args: Array): void; + /** + * Check that an object has a .length property and it is set to a certain + * numeric value. + */ + toHaveLength(number: number): void; + /** + * + */ + toHaveProperty(propPath: string, value?: any): void; + /** + * Use .toMatch to check that a string matches a regular expression or string. + */ + toMatch(regexpOrString: RegExp | string): void; + /** + * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. + */ + toMatchObject(object: Object | Array): void; + /** + * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. + */ + toStrictEqual(value: any): void; + /** + * This ensures that an Object matches the most recent snapshot. + */ + toMatchSnapshot(propertyMatchers?: any, name?: string): void; + /** + * This ensures that an Object matches the most recent snapshot. + */ + toMatchSnapshot(name: string): void; + + toMatchInlineSnapshot(snapshot?: string): void; + toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void; + /** + * Use .toThrow to test that a function throws when it is called. + * If you want to test that a specific error gets thrown, you can provide an + * argument to toThrow. The argument can be a string for the error message, + * a class for the error, or a regex that should match the error. + * + * Alias: .toThrowError + */ + toThrow(message?: string | Error | Class | RegExp): void; + toThrowError(message?: string | Error | Class | RegExp): void; + /** + * Use .toThrowErrorMatchingSnapshot to test that a function throws a error + * matching the most recent snapshot when it is called. + */ + toThrowErrorMatchingSnapshot(): void; + toThrowErrorMatchingInlineSnapshot(snapshot?: string): void; +} + +type JestObjectType = { + /** + * Disables automatic mocking in the module loader. + * + * After this method is called, all `require()`s will return the real + * versions of each module (rather than a mocked version). + */ + disableAutomock(): JestObjectType, + /** + * An un-hoisted version of disableAutomock + */ + autoMockOff(): JestObjectType, + /** + * Enables automatic mocking in the module loader. + */ + enableAutomock(): JestObjectType, + /** + * An un-hoisted version of enableAutomock + */ + autoMockOn(): JestObjectType, + /** + * Clears the mock.calls and mock.instances properties of all mocks. + * Equivalent to calling .mockClear() on every mocked function. + */ + clearAllMocks(): JestObjectType, + /** + * Resets the state of all mocks. Equivalent to calling .mockReset() on every + * mocked function. + */ + resetAllMocks(): JestObjectType, + /** + * Restores all mocks back to their original value. + */ + restoreAllMocks(): JestObjectType, + /** + * Removes any pending timers from the timer system. + */ + clearAllTimers(): void, + /** + * Returns the number of fake timers still left to run. + */ + getTimerCount(): number, + /** + * The same as `mock` but not moved to the top of the expectation by + * babel-jest. + */ + doMock(moduleName: string, moduleFactory?: any): JestObjectType, + /** + * The same as `unmock` but not moved to the top of the expectation by + * babel-jest. + */ + dontMock(moduleName: string): JestObjectType, + /** + * Returns a new, unused mock function. Optionally takes a mock + * implementation. + */ + fn, TReturn>( + implementation?: (...args: TArguments) => TReturn + ): JestMockFn, + /** + * Determines if the given function is a mocked function. + */ + isMockFunction(fn: Function): boolean, + /** + * Given the name of a module, use the automatic mocking system to generate a + * mocked version of the module for you. + */ + genMockFromModule(moduleName: string): any, + /** + * Mocks a module with an auto-mocked version when it is being required. + * + * The second argument can be used to specify an explicit module factory that + * is being run instead of using Jest's automocking feature. + * + * The third argument can be used to create virtual mocks -- mocks of modules + * that don't exist anywhere in the system. + */ + mock( + moduleName: string, + moduleFactory?: any, + options?: Object + ): JestObjectType, + /** + * Returns the actual module instead of a mock, bypassing all checks on + * whether the module should receive a mock implementation or not. + */ + requireActual(moduleName: string): any, + /** + * Returns a mock module instead of the actual module, bypassing all checks + * on whether the module should be required normally or not. + */ + requireMock(moduleName: string): any, + /** + * Resets the module registry - the cache of all required modules. This is + * useful to isolate modules where local state might conflict between tests. + */ + resetModules(): JestObjectType, + + /** + * Creates a sandbox registry for the modules that are loaded inside the + * callback function. This is useful to isolate specific modules for every + * test so that local module state doesn't conflict between tests. + */ + isolateModules(fn: () => void): JestObjectType, + + /** + * Exhausts the micro-task queue (usually interfaced in node via + * process.nextTick). + */ + runAllTicks(): void, + /** + * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), + * setInterval(), and setImmediate()). + */ + runAllTimers(): void, + /** + * Exhausts all tasks queued by setImmediate(). + */ + runAllImmediates(): void, + /** + * Executes only the macro task queue (i.e. all tasks queued by setTimeout() + * or setInterval() and setImmediate()). + */ + advanceTimersByTime(msToRun: number): void, + /** + * Executes only the macro task queue (i.e. all tasks queued by setTimeout() + * or setInterval() and setImmediate()). + * + * Renamed to `advanceTimersByTime`. + */ + runTimersToTime(msToRun: number): void, + /** + * Executes only the macro-tasks that are currently pending (i.e., only the + * tasks that have been queued by setTimeout() or setInterval() up to this + * point) + */ + runOnlyPendingTimers(): void, + /** + * Explicitly supplies the mock object that the module system should return + * for the specified module. Note: It is recommended to use jest.mock() + * instead. + */ + setMock(moduleName: string, moduleExports: any): JestObjectType, + /** + * Indicates that the module system should never return a mocked version of + * the specified module from require() (e.g. that it should always return the + * real module). + */ + unmock(moduleName: string): JestObjectType, + /** + * Instructs Jest to use fake versions of the standard timer functions + * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, + * setImmediate and clearImmediate). + */ + useFakeTimers(): JestObjectType, + /** + * Instructs Jest to use the real versions of the standard timer functions. + */ + useRealTimers(): JestObjectType, + /** + * Creates a mock function similar to jest.fn but also tracks calls to + * object[methodName]. + */ + spyOn( + object: Object, + methodName: string, + accessType?: 'get' | 'set' + ): JestMockFn, + /** + * Set the default timeout interval for tests and before/after hooks in milliseconds. + * Note: The default timeout interval is 5 seconds if this method is not called. + */ + setTimeout(timeout: number): JestObjectType, +}; + +type JestSpyType = { + calls: JestCallsType, +}; + +/** Runs this function after every test inside this context */ +declare function afterEach( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; +/** Runs this function before every test inside this context */ +declare function beforeEach( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; +/** Runs this function after all tests have finished inside this context */ +declare function afterAll( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; +/** Runs this function before any tests have started inside this context */ +declare function beforeAll( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; + +/** A context for grouping tests together */ +declare var describe: { + /** + * Creates a block that groups together several related tests in one "test suite" + */ + (name: JestTestName, fn: () => void): void, + + /** + * Only run this describe block + */ + only(name: JestTestName, fn: () => void): void, + + /** + * Skip running this describe block + */ + skip(name: JestTestName, fn: () => void): void, + + /** + * each runs this test against array of argument arrays per each run + * + * @param {table} table of Test + */ + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, +}; + +/** An individual test unit */ +declare var it: { + /** + * An individual test unit + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + ( + name: JestTestName, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void, + + /** + * Only run this test + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + only( + name: JestTestName, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): { + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, + }, + + /** + * Skip running this test + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + skip( + name: JestTestName, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void, + + /** + * Highlight planned tests in the summary output + * + * @param {String} Name of Test to do + */ + todo(name: string): void, + + /** + * Run the test concurrently + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + concurrent( + name: JestTestName, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void, + + /** + * each runs this test against array of argument arrays per each run + * + * @param {table} table of Test + */ + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, +}; + +declare function fit( + name: JestTestName, + fn: (done: () => void) => ?Promise, + timeout?: number +): void; +/** An individual test unit */ +declare var test: typeof it; +/** A disabled group of tests */ +declare var xdescribe: typeof describe; +/** A focused group of tests */ +declare var fdescribe: typeof describe; +/** A disabled individual test */ +declare var xit: typeof it; +/** A disabled individual test */ +declare var xtest: typeof it; + +type JestPrettyFormatColors = { + comment: {close: string, open: string}, + content: {close: string, open: string}, + prop: {close: string, open: string}, + tag: {close: string, open: string}, + value: {close: string, open: string}, +}; + +type JestPrettyFormatIndent = string => string; +// eslint-disable-next-line no-unused-vars +type JestPrettyFormatRefs = Array; +type JestPrettyFormatPrint = any => string; +// eslint-disable-next-line no-unused-vars +type JestPrettyFormatStringOrNull = string | null; + +type JestPrettyFormatOptions = {| + callToJSON: boolean, + edgeSpacing: string, + escapeRegex: boolean, + highlight: boolean, + indent: number, + maxDepth: number, + min: boolean, + // eslint-disable-next-line no-use-before-define + plugins: JestPrettyFormatPlugins, + printFunctionName: boolean, + spacing: string, + theme: {| + comment: string, + content: string, + prop: string, + tag: string, + value: string, + |}, +|}; + +type JestPrettyFormatPlugin = { + print: ( + val: any, + serialize: JestPrettyFormatPrint, + indent: JestPrettyFormatIndent, + opts: JestPrettyFormatOptions, + colors: JestPrettyFormatColors + ) => string, + test: any => boolean, +}; + +type JestPrettyFormatPlugins = Array; + +/** The expect function is used every time you want to test a value */ +declare var expect: { + /** The object that you want to make assertions against */ + ( + value: any + ): JestExpectType & + JestPromiseType & + EnzymeMatchersType & + DomTestingLibraryType & + JestJQueryMatchersType & + JestStyledComponentsMatchersType & + JestExtendedMatchersType, + + /** Add additional Jasmine matchers to Jest's roster */ + extend(matchers: {[name: string]: JestMatcher}): void, + /** Add a module that formats application-specific data structures. */ + addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, + assertions(expectedAssertions: number): void, + hasAssertions(): void, + any(value: mixed): JestAsymmetricEqualityType, + anything(): any, + arrayContaining(value: Array): Array, + objectContaining(value: Object): Object, + /** Matches any received string that contains the exact expected string. */ + stringContaining(value: string): string, + stringMatching(value: string | RegExp): string, + not: { + arrayContaining: (value: $ReadOnlyArray) => Array, + objectContaining: (value: {}) => Object, + stringContaining: (value: string) => string, + stringMatching: (value: string | RegExp) => string, + }, +}; + +// TODO handle return type +// http://jasmine.github.io/2.4/introduction.html#section-Spies +declare function spyOn(value: mixed, method: string): Object; + +/** Holds all functions related to manipulating test runner */ +declare var jest: JestObjectType; + +/** + * The global Jasmine object, this is generally not exposed as the public API, + * using features inside here could break in later versions of Jest. + */ +declare var jasmine: { + DEFAULT_TIMEOUT_INTERVAL: number, + any(value: mixed): JestAsymmetricEqualityType, + anything(): any, + arrayContaining(value: Array): Array, + clock(): JestClockType, + createSpy(name: string): JestSpyType, + createSpyObj( + baseName: string, + methodNames: Array + ): {[methodName: string]: JestSpyType}, + objectContaining(value: Object): Object, + stringMatching(value: string): string, +}; diff --git a/packages/react-devtools-extensions/flow-typed/npm/react-test-renderer_v16.x.x.js b/packages/react-devtools-extensions/flow-typed/npm/react-test-renderer_v16.x.x.js new file mode 100644 index 0000000000000..cd2ae506d2ef5 --- /dev/null +++ b/packages/react-devtools-extensions/flow-typed/npm/react-test-renderer_v16.x.x.js @@ -0,0 +1,84 @@ +// flow-typed signature: b6bb53397d83d2d821e258cc73818d1b +// flow-typed version: 9c71eca8ef/react-test-renderer_v16.x.x/flow_>=v0.47.x + +// Type definitions for react-test-renderer 16.x.x +// Ported from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer + +'use strict'; + +type ReactComponentInstance = React$Component; + +type ReactTestRendererJSON = { + type: string, + props: {[propName: string]: any}, + children: null | ReactTestRendererJSON[], +}; + +type ReactTestRendererTree = ReactTestRendererJSON & { + nodeType: 'component' | 'host', + instance: ?ReactComponentInstance, + rendered: null | ReactTestRendererTree, +}; + +type ReactTestInstance = { + instance: ?ReactComponentInstance, + type: string, + props: {[propName: string]: any}, + parent: null | ReactTestInstance, + children: Array, + + find(predicate: (node: ReactTestInstance) => boolean): ReactTestInstance, + findByType(type: React$ElementType): ReactTestInstance, + findByProps(props: {[propName: string]: any}): ReactTestInstance, + + findAll( + predicate: (node: ReactTestInstance) => boolean, + options?: {deep: boolean} + ): ReactTestInstance[], + findAllByType( + type: React$ElementType, + options?: {deep: boolean} + ): ReactTestInstance[], + findAllByProps( + props: {[propName: string]: any}, + options?: {deep: boolean} + ): ReactTestInstance[], +}; + +type TestRendererOptions = { + createNodeMock(element: React$Element): any, +}; + +declare module 'react-test-renderer' { + // eslint-disable-next-line no-inner-declarations + declare export type ReactTestRenderer = { + toJSON(): null | ReactTestRendererJSON, + toTree(): null | ReactTestRendererTree, + unmount(nextElement?: React$Element): void, + update(nextElement: React$Element): void, + getInstance(): ?ReactComponentInstance, + root: ReactTestInstance, + }; + + declare type Thenable = { + then(resolve: () => mixed, reject?: () => mixed): mixed, + }; + + declare function create( + nextElement: React$Element, + options?: TestRendererOptions + ): ReactTestRenderer; + + declare function act(callback: () => ?Thenable): Thenable; +} + +declare module 'react-test-renderer/shallow' { + declare export default class ShallowRenderer { + static createRenderer(): ShallowRenderer; + getMountedInstance(): ReactTestInstance; + getRenderOutput>(): E; + getRenderOutput(): React$Element; + render(element: React$Element, context?: any): void; + unmount(): void; + } +} diff --git a/packages/react-devtools-extensions/icons/128-deadcode.png b/packages/react-devtools-extensions/icons/128-deadcode.png new file mode 100644 index 0000000000000..b6ecc88e13ac0 Binary files /dev/null and b/packages/react-devtools-extensions/icons/128-deadcode.png differ diff --git a/packages/react-devtools-extensions/icons/128-development.png b/packages/react-devtools-extensions/icons/128-development.png new file mode 100644 index 0000000000000..b6ecc88e13ac0 Binary files /dev/null and b/packages/react-devtools-extensions/icons/128-development.png differ diff --git a/packages/react-devtools-extensions/icons/128-disabled.png b/packages/react-devtools-extensions/icons/128-disabled.png new file mode 100644 index 0000000000000..67b1c9a31a6df Binary files /dev/null and b/packages/react-devtools-extensions/icons/128-disabled.png differ diff --git a/packages/react-devtools-extensions/icons/128-outdated.png b/packages/react-devtools-extensions/icons/128-outdated.png new file mode 100644 index 0000000000000..05792b762878c Binary files /dev/null and b/packages/react-devtools-extensions/icons/128-outdated.png differ diff --git a/packages/react-devtools-extensions/icons/128-production.png b/packages/react-devtools-extensions/icons/128-production.png new file mode 100644 index 0000000000000..b9327946441f5 Binary files /dev/null and b/packages/react-devtools-extensions/icons/128-production.png differ diff --git a/packages/react-devtools-extensions/icons/128-unminified.png b/packages/react-devtools-extensions/icons/128-unminified.png new file mode 100644 index 0000000000000..b6ecc88e13ac0 Binary files /dev/null and b/packages/react-devtools-extensions/icons/128-unminified.png differ diff --git a/packages/react-devtools-extensions/icons/16-deadcode.png b/packages/react-devtools-extensions/icons/16-deadcode.png new file mode 100644 index 0000000000000..33d99798e07b2 Binary files /dev/null and b/packages/react-devtools-extensions/icons/16-deadcode.png differ diff --git a/packages/react-devtools-extensions/icons/16-development.png b/packages/react-devtools-extensions/icons/16-development.png new file mode 100644 index 0000000000000..33d99798e07b2 Binary files /dev/null and b/packages/react-devtools-extensions/icons/16-development.png differ diff --git a/packages/react-devtools-extensions/icons/16-disabled.png b/packages/react-devtools-extensions/icons/16-disabled.png new file mode 100644 index 0000000000000..2f0317ea3fd6b Binary files /dev/null and b/packages/react-devtools-extensions/icons/16-disabled.png differ diff --git a/packages/react-devtools-extensions/icons/16-outdated.png b/packages/react-devtools-extensions/icons/16-outdated.png new file mode 100644 index 0000000000000..aa42bfe0b4e0c Binary files /dev/null and b/packages/react-devtools-extensions/icons/16-outdated.png differ diff --git a/packages/react-devtools-extensions/icons/16-production.png b/packages/react-devtools-extensions/icons/16-production.png new file mode 100644 index 0000000000000..1c253dca5d56d Binary files /dev/null and b/packages/react-devtools-extensions/icons/16-production.png differ diff --git a/packages/react-devtools-extensions/icons/16-unminified.png b/packages/react-devtools-extensions/icons/16-unminified.png new file mode 100644 index 0000000000000..33d99798e07b2 Binary files /dev/null and b/packages/react-devtools-extensions/icons/16-unminified.png differ diff --git a/packages/react-devtools-extensions/icons/32-deadcode.png b/packages/react-devtools-extensions/icons/32-deadcode.png new file mode 100644 index 0000000000000..c4a6bda3e6a6d Binary files /dev/null and b/packages/react-devtools-extensions/icons/32-deadcode.png differ diff --git a/packages/react-devtools-extensions/icons/32-development.png b/packages/react-devtools-extensions/icons/32-development.png new file mode 100644 index 0000000000000..c4a6bda3e6a6d Binary files /dev/null and b/packages/react-devtools-extensions/icons/32-development.png differ diff --git a/packages/react-devtools-extensions/icons/32-disabled.png b/packages/react-devtools-extensions/icons/32-disabled.png new file mode 100644 index 0000000000000..7c75045323888 Binary files /dev/null and b/packages/react-devtools-extensions/icons/32-disabled.png differ diff --git a/packages/react-devtools-extensions/icons/32-outdated.png b/packages/react-devtools-extensions/icons/32-outdated.png new file mode 100644 index 0000000000000..6eae901bf0588 Binary files /dev/null and b/packages/react-devtools-extensions/icons/32-outdated.png differ diff --git a/packages/react-devtools-extensions/icons/32-production.png b/packages/react-devtools-extensions/icons/32-production.png new file mode 100644 index 0000000000000..9192719e5b50f Binary files /dev/null and b/packages/react-devtools-extensions/icons/32-production.png differ diff --git a/packages/react-devtools-extensions/icons/32-unminified.png b/packages/react-devtools-extensions/icons/32-unminified.png new file mode 100644 index 0000000000000..c4a6bda3e6a6d Binary files /dev/null and b/packages/react-devtools-extensions/icons/32-unminified.png differ diff --git a/packages/react-devtools-extensions/icons/48-deadcode.png b/packages/react-devtools-extensions/icons/48-deadcode.png new file mode 100644 index 0000000000000..f3224021a560c Binary files /dev/null and b/packages/react-devtools-extensions/icons/48-deadcode.png differ diff --git a/packages/react-devtools-extensions/icons/48-development.png b/packages/react-devtools-extensions/icons/48-development.png new file mode 100644 index 0000000000000..f3224021a560c Binary files /dev/null and b/packages/react-devtools-extensions/icons/48-development.png differ diff --git a/packages/react-devtools-extensions/icons/48-disabled.png b/packages/react-devtools-extensions/icons/48-disabled.png new file mode 100644 index 0000000000000..372b6e00e88a8 Binary files /dev/null and b/packages/react-devtools-extensions/icons/48-disabled.png differ diff --git a/packages/react-devtools-extensions/icons/48-outdated.png b/packages/react-devtools-extensions/icons/48-outdated.png new file mode 100644 index 0000000000000..342fedea1ed50 Binary files /dev/null and b/packages/react-devtools-extensions/icons/48-outdated.png differ diff --git a/packages/react-devtools-extensions/icons/48-production.png b/packages/react-devtools-extensions/icons/48-production.png new file mode 100644 index 0000000000000..9aac93a622bfa Binary files /dev/null and b/packages/react-devtools-extensions/icons/48-production.png differ diff --git a/packages/react-devtools-extensions/icons/48-unminified.png b/packages/react-devtools-extensions/icons/48-unminified.png new file mode 100644 index 0000000000000..f3224021a560c Binary files /dev/null and b/packages/react-devtools-extensions/icons/48-unminified.png differ diff --git a/packages/react-devtools-extensions/icons/deadcode.svg b/packages/react-devtools-extensions/icons/deadcode.svg new file mode 100644 index 0000000000000..ccd6e669061f7 --- /dev/null +++ b/packages/react-devtools-extensions/icons/deadcode.svg @@ -0,0 +1 @@ +development780780 \ No newline at end of file diff --git a/packages/react-devtools-extensions/icons/development.svg b/packages/react-devtools-extensions/icons/development.svg new file mode 100644 index 0000000000000..ccd6e669061f7 --- /dev/null +++ b/packages/react-devtools-extensions/icons/development.svg @@ -0,0 +1 @@ +development780780 \ No newline at end of file diff --git a/packages/react-devtools-extensions/icons/disabled.svg b/packages/react-devtools-extensions/icons/disabled.svg new file mode 100644 index 0000000000000..73c2bb51cdbbc --- /dev/null +++ b/packages/react-devtools-extensions/icons/disabled.svg @@ -0,0 +1 @@ +disabled \ No newline at end of file diff --git a/packages/react-devtools-extensions/icons/outdated.svg b/packages/react-devtools-extensions/icons/outdated.svg new file mode 100644 index 0000000000000..03b83c1eb559b --- /dev/null +++ b/packages/react-devtools-extensions/icons/outdated.svg @@ -0,0 +1 @@ +outdated \ No newline at end of file diff --git a/packages/react-devtools-extensions/icons/production.svg b/packages/react-devtools-extensions/icons/production.svg new file mode 100644 index 0000000000000..1e974f5131012 --- /dev/null +++ b/packages/react-devtools-extensions/icons/production.svg @@ -0,0 +1 @@ +production \ No newline at end of file diff --git a/packages/react-devtools-extensions/main.html b/packages/react-devtools-extensions/main.html new file mode 100644 index 0000000000000..f1c96d4a7a094 --- /dev/null +++ b/packages/react-devtools-extensions/main.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json new file mode 100644 index 0000000000000..06a2dcd2df344 --- /dev/null +++ b/packages/react-devtools-extensions/package.json @@ -0,0 +1,43 @@ +{ + + "name": "react-devtools-extensions", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox", + "build:dev": "cross-env NODE_ENV=development yarn run build:chrome && yarn run build:firefox", + "build:chrome": "cross-env NODE_ENV=production node ./chrome/build", + "build:chrome:crx": "cross-env NODE_ENV=production node ./chrome/build --crx", + "build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build", + "build:firefox": "cross-env NODE_ENV=production node ./firefox/build", + "build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build", + "test:chrome": "node ./chrome/test", + "test:firefox": "node ./firefox/test" + }, + "devDependencies": { + "@babel/core": "^7.1.6", + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-transform-flow-strip-types": "^7.1.6", + "@babel/plugin-transform-react-jsx-source": "^7.2.0", + "@babel/preset-env": "^7.1.6", + "@babel/preset-flow": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "archiver": "^3.0.0", + "babel-core": "^7.0.0-bridge", + "babel-eslint": "^9.0.0", + "babel-jest": "^24.7.1", + "babel-loader": "^8.0.4", + "chrome-launch": "^1.1.4", + "child-process-promise": "^2.2.1", + "css-loader": "^1.0.1", + "firefox-profile": "^1.0.2", + "node-libs-browser": "0.5.3", + "nullthrows": "^1.0.0", + "raw-loader": "^3.1.0", + "style-loader": "^0.23.1", + "web-ext": "^3.0.0", + "webpack": "^4.26.0", + "webpack-cli": "^3.1.2", + "webpack-dev-server": "^3.3.1" + } +} diff --git a/packages/react-devtools-extensions/panel.html b/packages/react-devtools-extensions/panel.html new file mode 100644 index 0000000000000..60fd1bdf13ff2 --- /dev/null +++ b/packages/react-devtools-extensions/panel.html @@ -0,0 +1,32 @@ + + + + + + + + +
Unable to find React on the page.
+ + + diff --git a/packages/react-devtools-extensions/popups/deadcode.html b/packages/react-devtools-extensions/popups/deadcode.html new file mode 100644 index 0000000000000..cdd4c278e86cc --- /dev/null +++ b/packages/react-devtools-extensions/popups/deadcode.html @@ -0,0 +1,32 @@ + + +

+ This page includes an extra development build of React. 🚧 +

+

+ The React build on this page includes both development and production versions because dead code elimination has not been applied correctly. +
+
+ This makes its size larger, and causes React to run slower. +
+
+ Make sure to set up dead code elimination before deployment. +

+
+

+ Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. +

diff --git a/packages/react-devtools-extensions/popups/development.html b/packages/react-devtools-extensions/popups/development.html new file mode 100644 index 0000000000000..3d092a5eb69f8 --- /dev/null +++ b/packages/react-devtools-extensions/popups/development.html @@ -0,0 +1,28 @@ + + +

+ This page is using the development build of React. 🚧 +

+

+ Note that the development build is not suitable for production. +
+ Make sure to use the production build before deployment. +

+
+

+ Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. +

diff --git a/packages/react-devtools-extensions/popups/disabled.html b/packages/react-devtools-extensions/popups/disabled.html new file mode 100644 index 0000000000000..a89b178d49cf8 --- /dev/null +++ b/packages/react-devtools-extensions/popups/disabled.html @@ -0,0 +1,21 @@ + + +

+ This page doesn’t appear to be using React. +
+ If this seems wrong, follow the troubleshooting instructions. +

diff --git a/packages/react-devtools-extensions/popups/outdated.html b/packages/react-devtools-extensions/popups/outdated.html new file mode 100644 index 0000000000000..8f3f3f9508766 --- /dev/null +++ b/packages/react-devtools-extensions/popups/outdated.html @@ -0,0 +1,29 @@ + + +

+ This page is using an outdated version of React. ⌛ +

+

+ We recommend updating React to ensure that you receive important bugfixes and performance improvements. +
+
+ You can find the upgrade instructions on the React blog. +

+
+

+ Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. +

diff --git a/packages/react-devtools-extensions/popups/production.html b/packages/react-devtools-extensions/popups/production.html new file mode 100644 index 0000000000000..7b92841c6c75c --- /dev/null +++ b/packages/react-devtools-extensions/popups/production.html @@ -0,0 +1,21 @@ + + +

+ This page is using the production build of React. ✅ +
+ Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. +

diff --git a/packages/react-devtools-extensions/popups/shared.js b/packages/react-devtools-extensions/popups/shared.js new file mode 100644 index 0000000000000..ddb0456e5bbf3 --- /dev/null +++ b/packages/react-devtools-extensions/popups/shared.js @@ -0,0 +1,24 @@ +/* globals chrome */ + +'use strict'; + +document.addEventListener('DOMContentLoaded', function() { + // Make links work + const links = document.getElementsByTagName('a'); + for (let i = 0; i < links.length; i++) { + (function() { + const ln = links[i]; + const location = ln.href; + ln.onclick = function() { + chrome.tabs.create({active: true, url: location}); + }; + })(); + } + + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=428044 + document.body.style.opacity = 0; + document.body.style.transition = 'opacity ease-out .4s'; + requestAnimationFrame(function() { + document.body.style.opacity = 1; + }); +}); diff --git a/packages/react-devtools-extensions/popups/unminified.html b/packages/react-devtools-extensions/popups/unminified.html new file mode 100644 index 0000000000000..c53885f5098c3 --- /dev/null +++ b/packages/react-devtools-extensions/popups/unminified.html @@ -0,0 +1,31 @@ + + +

+ This page is using an unminified build of React. 🚧 +

+

+ The React build on this page appears to be unminified. +
+ This makes its size larger, and causes React to run slower. +
+
+ Make sure to set up minification before deployment. +

+
+

+ Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. +

diff --git a/packages/react-devtools-extensions/src/backend.js b/packages/react-devtools-extensions/src/backend.js new file mode 100644 index 0000000000000..072e8de09f0ef --- /dev/null +++ b/packages/react-devtools-extensions/src/backend.js @@ -0,0 +1,79 @@ +// Do not use imports or top-level requires here! +// Running module factories is intentionally delayed until we know the hook exists. +// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039 + +/** @flow */ + +'use strict'; + +function welcome(event) { + if ( + event.source !== window || + event.data.source !== 'react-devtools-content-script' + ) { + return; + } + + window.removeEventListener('message', welcome); + + setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__); +} + +window.addEventListener('message', welcome); + +function setup(hook) { + const Agent = require('react-devtools-shared/src/backend/agent').default; + const Bridge = require('react-devtools-shared/src/bridge').default; + const {initBackend} = require('react-devtools-shared/src/backend'); + const setupNativeStyleEditor = require('react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor') + .default; + + const bridge = new Bridge({ + listen(fn) { + const listener = event => { + if ( + event.source !== window || + !event.data || + event.data.source !== 'react-devtools-content-script' || + !event.data.payload + ) { + return; + } + fn(event.data.payload); + }; + window.addEventListener('message', listener); + return () => { + window.removeEventListener('message', listener); + }; + }, + send(event: string, payload: any, transferable?: Array) { + window.postMessage( + { + source: 'react-devtools-bridge', + payload: {event, payload}, + }, + '*', + transferable, + ); + }, + }); + + 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 a renderer like react-native-web has injected it. + if (hook.resolveRNStyle) { + setupNativeStyleEditor( + bridge, + agent, + hook.resolveRNStyle, + hook.nativeStyleEditorValidAttributes, + ); + } +} diff --git a/packages/react-devtools-extensions/src/background.js b/packages/react-devtools-extensions/src/background.js new file mode 100644 index 0000000000000..389ca1310a910 --- /dev/null +++ b/packages/react-devtools-extensions/src/background.js @@ -0,0 +1,115 @@ +/* global chrome */ + +'use strict'; + +const ports = {}; + +const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0; + +chrome.runtime.onConnect.addListener(function(port) { + let tab = null; + let name = null; + if (isNumeric(port.name)) { + tab = port.name; + name = 'devtools'; + installContentScript(+port.name); + } else { + tab = port.sender.tab.id; + name = 'content-script'; + } + + if (!ports[tab]) { + ports[tab] = { + devtools: null, + 'content-script': null, + }; + } + ports[tab][name] = port; + + if (ports[tab].devtools && ports[tab]['content-script']) { + doublePipe(ports[tab].devtools, ports[tab]['content-script']); + } +}); + +function isNumeric(str: string): boolean { + return +str + '' === str; +} + +function installContentScript(tabId: number) { + chrome.tabs.executeScript( + tabId, + {file: '/build/contentScript.js'}, + function() {}, + ); +} + +function doublePipe(one, two) { + one.onMessage.addListener(lOne); + function lOne(message) { + two.postMessage(message); + } + two.onMessage.addListener(lTwo); + function lTwo(message) { + one.postMessage(message); + } + function shutdown() { + one.onMessage.removeListener(lOne); + two.onMessage.removeListener(lTwo); + one.disconnect(); + two.disconnect(); + } + one.onDisconnect.addListener(shutdown); + two.onDisconnect.addListener(shutdown); +} + +function setIconAndPopup(reactBuildType, tabId) { + chrome.browserAction.setIcon({ + tabId: tabId, + path: { + '16': 'icons/16-' + reactBuildType + '.png', + '32': 'icons/32-' + reactBuildType + '.png', + '48': 'icons/48-' + reactBuildType + '.png', + '128': 'icons/128-' + reactBuildType + '.png', + }, + }); + chrome.browserAction.setPopup({ + tabId: tabId, + popup: 'popups/' + reactBuildType + '.html', + }); +} + +// Listen to URL changes on the active tab and reset the DevTools icon. +// This prevents non-disabled icons from sticking in Firefox. +// Don't listen to this event in Chrome though. +// It fires more frequently, often after onMessage() has been called. +if (IS_FIREFOX) { + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (tab.active && changeInfo.status === 'loading') { + setIconAndPopup('disabled', tabId); + } + }); +} + +chrome.runtime.onMessage.addListener((request, sender) => { + if (sender.tab) { + // This is sent from the hook content script. + // It tells us a renderer has attached. + if (request.hasDetectedReact) { + // We use browserAction instead of pageAction because this lets us + // display a custom default popup when React is *not* detected. + // It is specified in the manifest. + let reactBuildType = request.reactBuildType; + if (sender.url.indexOf('facebook.github.io/react') !== -1) { + // Cheat: We use the development version on the website because + // it is better for interactive examples. However we're going + // to get misguided bug reports if the extension highlights it + // as using the dev version. We're just going to special case + // our own documentation and cheat. It is acceptable to use dev + // version of React in React docs, but not in any other case. + reactBuildType = 'production'; + } + + setIconAndPopup(reactBuildType, sender.tab.id); + } + } +}); diff --git a/packages/react-devtools-extensions/src/contentScript.js b/packages/react-devtools-extensions/src/contentScript.js new file mode 100644 index 0000000000000..c914c6e7b3dfc --- /dev/null +++ b/packages/react-devtools-extensions/src/contentScript.js @@ -0,0 +1,79 @@ +/* global chrome */ + +'use strict'; + +let backendDisconnected: boolean = false; +let backendInitialized: boolean = false; + +function sayHelloToBackend() { + window.postMessage( + { + source: 'react-devtools-content-script', + hello: true, + }, + '*', + ); +} + +function handleMessageFromDevtools(message) { + window.postMessage( + { + source: 'react-devtools-content-script', + payload: message, + }, + '*', + ); +} + +function handleMessageFromPage(evt) { + if ( + evt.source === window && + evt.data && + evt.data.source === 'react-devtools-bridge' + ) { + backendInitialized = true; + + port.postMessage(evt.data.payload); + } +} + +function handleDisconnect() { + backendDisconnected = true; + + window.removeEventListener('message', handleMessageFromPage); + + window.postMessage( + { + source: 'react-devtools-content-script', + payload: { + type: 'event', + event: 'shutdown', + }, + }, + '*', + ); +} + +// proxy from main page to devtools (via the background page) +const port = chrome.runtime.connect({ + name: 'content-script', +}); +port.onMessage.addListener(handleMessageFromDevtools); +port.onDisconnect.addListener(handleDisconnect); + +window.addEventListener('message', handleMessageFromPage); + +sayHelloToBackend(); + +// The backend waits to install the global hook until notified by the content script. +// In the event of a page reload, the content script might be loaded before the backend is injected. +// Because of this we need to poll the backend until it has been initialized. +if (!backendInitialized) { + const intervalID = setInterval(() => { + if (backendInitialized || backendDisconnected) { + clearInterval(intervalID); + } else { + sayHelloToBackend(); + } + }, 500); +} diff --git a/packages/react-devtools-extensions/src/inject.js b/packages/react-devtools-extensions/src/inject.js new file mode 100644 index 0000000000000..938e2cf7eb227 --- /dev/null +++ b/packages/react-devtools-extensions/src/inject.js @@ -0,0 +1,24 @@ +/* global chrome */ + +export default function inject(scriptName: string, done: ?Function) { + const source = ` + // the prototype stuff is in case document.createElement has been modified + (function () { + var script = document.constructor.prototype.createElement.call(document, 'script'); + script.src = "${scriptName}"; + script.charset = "utf-8"; + document.documentElement.appendChild(script); + script.parentNode.removeChild(script); + })() + `; + + chrome.devtools.inspectedWindow.eval(source, function(response, error) { + if (error) { + console.log(error); + } + + if (typeof done === 'function') { + done(); + } + }); +} diff --git a/packages/react-devtools-extensions/src/injectGlobalHook.js b/packages/react-devtools-extensions/src/injectGlobalHook.js new file mode 100644 index 0000000000000..b56f9a299b378 --- /dev/null +++ b/packages/react-devtools-extensions/src/injectGlobalHook.js @@ -0,0 +1,89 @@ +/* global chrome */ + +import nullthrows from 'nullthrows'; +import {installHook} from 'react-devtools-shared/src/hook'; +import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants'; +import {sessionStorageGetItem} from 'react-devtools-shared/src/storage'; + +function injectCode(code) { + const script = document.createElement('script'); + script.textContent = code; + + // This script runs before the element is created, + // so we add the script to instead. + nullthrows(document.documentElement).appendChild(script); + nullthrows(script.parentNode).removeChild(script); +} + +let lastDetectionResult; + +// We want to detect when a renderer attaches, and notify the "background page" +// (which is shared between tabs and can highlight the React icon). +// Currently we are in "content script" context, so we can't listen to the hook directly +// (it will be injected directly into the page). +// So instead, the hook will use postMessage() to pass message to us here. +// And when this happens, we'll send a message to the "background page". +window.addEventListener('message', function(evt) { + if ( + evt.source === window && + evt.data && + evt.data.source === 'react-devtools-detector' + ) { + lastDetectionResult = { + hasDetectedReact: true, + reactBuildType: evt.data.reactBuildType, + }; + chrome.runtime.sendMessage(lastDetectionResult); + } +}); + +// NOTE: Firefox WebExtensions content scripts are still alive and not re-injected +// while navigating the history to a document that has not been destroyed yet, +// replay the last detection result if the content script is active and the +// document has been hidden and shown again. +window.addEventListener('pageshow', function(evt) { + if (!lastDetectionResult || evt.target !== window.document) { + return; + } + chrome.runtime.sendMessage(lastDetectionResult); +}); + +const detectReact = ` +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function(evt) { + window.postMessage({ + source: 'react-devtools-detector', + reactBuildType: evt.reactBuildType, + }, '*'); +}); +`; +const saveNativeValues = ` +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeObjectCreate = Object.create; +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeMap = Map; +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap; +window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set; +`; + +// If we have just reloaded to profile, we need to inject the renderer interface before the app loads. +if (sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') { + const rendererURL = chrome.runtime.getURL('build/renderer.js'); + let rendererCode; + + // We need to inject in time to catch the initial mount. + // This means we need to synchronously read the renderer code itself, + // and synchronously inject it into the page. + // There are very few ways to actually do this. + // This seems to be the best approach. + const request = new XMLHttpRequest(); + request.addEventListener('load', function() { + rendererCode = this.responseText; + }); + request.open('GET', rendererURL, false); + request.send(); + injectCode(rendererCode); +} + +// Inject a `__REACT_DEVTOOLS_GLOBAL_HOOK__` global so that React can detect that the +// devtools are installed (and skip its suggestion to install the devtools). +injectCode( + ';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact, +); diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js new file mode 100644 index 0000000000000..7d786e0321917 --- /dev/null +++ b/packages/react-devtools-extensions/src/main.js @@ -0,0 +1,322 @@ +/* global chrome */ + +import {createElement} from 'react'; +import {unstable_createRoot as createRoot, flushSync} from 'react-dom'; +import Bridge from 'react-devtools-shared/src/bridge'; +import Store from 'react-devtools-shared/src/devtools/store'; +import inject from './inject'; +import { + createViewElementSource, + getBrowserName, + getBrowserTheme, +} from './utils'; +import { + getSavedComponentFilters, + getAppendComponentStack, +} from 'react-devtools-shared/src/utils'; +import { + localStorageGetItem, + localStorageRemoveItem, + localStorageSetItem, +} from 'react-devtools-shared/src/storage'; +import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; + +const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = + 'React::DevTools::supportsProfiling'; + +const isChrome = getBrowserName() === 'Chrome'; + +let panelCreated = false; + +// The renderer interface can't read saved component filters directly, +// because they are stored in localStorage within the context of the extension. +// Instead it relies on the extension to pass filters through. +function syncSavedPreferences() { + const componentFilters = getSavedComponentFilters(); + chrome.devtools.inspectedWindow.eval( + `window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( + componentFilters, + )};`, + ); + + const appendComponentStack = getAppendComponentStack(); + chrome.devtools.inspectedWindow.eval( + `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify( + appendComponentStack, + )};`, + ); +} + +syncSavedPreferences(); + +function createPanelIfReactLoaded() { + if (panelCreated) { + return; + } + + chrome.devtools.inspectedWindow.eval( + 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0', + function(pageHasReact, error) { + if (!pageHasReact || panelCreated) { + return; + } + + panelCreated = true; + + clearInterval(loadCheckInterval); + + let bridge = null; + let store = null; + + let profilingData = null; + + let componentsPortalContainer = null; + let profilerPortalContainer = null; + + let cloneStyleTags = null; + let mostRecentOverrideTab = null; + let render = null; + let root = null; + + const tabId = chrome.devtools.inspectedWindow.tabId; + + function initBridgeAndStore() { + const port = chrome.runtime.connect({ + name: '' + tabId, + }); + // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation, + // so it makes no sense to handle it here. + + bridge = new Bridge({ + listen(fn) { + const listener = message => fn(message); + // Store the reference so that we unsubscribe from the same object. + const portOnMessage = port.onMessage; + portOnMessage.addListener(listener); + return () => { + portOnMessage.removeListener(listener); + }; + }, + send(event: string, payload: any, transferable?: Array) { + port.postMessage({event, payload}, transferable); + }, + }); + bridge.addListener('reloadAppForProfiling', () => { + localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); + chrome.devtools.inspectedWindow.eval('window.location.reload();'); + }); + bridge.addListener('syncSelectionToNativeElementsPanel', () => { + setBrowserSelectionFromReact(); + }); + + // This flag lets us tip the Store off early that we expect to be profiling. + // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab, + // after a user has clicked the "reload and profile" button. + let isProfiling = false; + let supportsProfiling = false; + if ( + localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true' + ) { + supportsProfiling = true; + isProfiling = true; + localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY); + } + + if (store !== null) { + profilingData = store.profilerStore.profilingData; + } + + store = new Store(bridge, { + isProfiling, + supportsReloadAndProfile: isChrome, + supportsProfiling, + }); + store.profilerStore.profilingData = profilingData; + + // Initialize the backend only once the Store has been initialized. + // Otherwise the Store may miss important initial tree op codes. + inject(chrome.runtime.getURL('build/backend.js')); + + const viewElementSourceFunction = createViewElementSource( + bridge, + store, + ); + + root = createRoot(document.createElement('div')); + + render = (overrideTab = mostRecentOverrideTab) => { + mostRecentOverrideTab = overrideTab; + + root.render( + createElement(DevTools, { + bridge, + browserTheme: getBrowserTheme(), + componentsPortalContainer, + overrideTab, + profilerPortalContainer, + showTabBar: false, + showWelcomeToTheNewDevToolsDialog: true, + store, + viewElementSourceFunction, + }), + ); + }; + + render(); + } + + cloneStyleTags = () => { + const linkTags = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let linkTag of document.getElementsByTagName('link')) { + if (linkTag.rel === 'stylesheet') { + const newLinkTag = document.createElement('link'); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let attribute of linkTag.attributes) { + newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue); + } + linkTags.push(newLinkTag); + } + } + return linkTags; + }; + + initBridgeAndStore(); + + function ensureInitialHTMLIsCleared(container) { + if (container._hasInitialHTMLBeenCleared) { + return; + } + container.innerHTML = ''; + container._hasInitialHTMLBeenCleared = true; + } + + function setBrowserSelectionFromReact() { + // This is currently only called on demand when you press "view DOM". + // In the future, if Chrome adds an inspect() that doesn't switch tabs, + // we could make this happen automatically when you select another component. + chrome.devtools.inspectedWindow.eval( + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + + 'false', + (didSelectionChange, evalError) => { + if (evalError) { + console.error(evalError); + } + }, + ); + } + + function setReactSelectionFromBrowser() { + // When the user chooses a different node in the browser Elements tab, + // copy it over to the hook object so that we can sync the selection. + chrome.devtools.inspectedWindow.eval( + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + + 'false', + (didSelectionChange, evalError) => { + if (evalError) { + console.error(evalError); + } else if (didSelectionChange) { + // Remember to sync the selection next time we show Components tab. + needsToSyncElementSelection = true; + } + }, + ); + } + + setReactSelectionFromBrowser(); + chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { + setReactSelectionFromBrowser(); + }); + + let currentPanel = null; + let needsToSyncElementSelection = false; + + chrome.devtools.panels.create( + isChrome ? '⚛ Components' : 'Components', + '', + 'panel.html', + extensionPanel => { + extensionPanel.onShown.addListener(panel => { + if (needsToSyncElementSelection) { + needsToSyncElementSelection = false; + bridge.send('syncSelectionFromNativeElementsPanel'); + } + + if (currentPanel === panel) { + return; + } + + currentPanel = panel; + componentsPortalContainer = panel.container; + + if (componentsPortalContainer != null) { + ensureInitialHTMLIsCleared(componentsPortalContainer); + render('components'); + panel.injectStyles(cloneStyleTags); + } + }); + extensionPanel.onHidden.addListener(panel => { + // TODO: Stop highlighting and stuff. + }); + }, + ); + + chrome.devtools.panels.create( + isChrome ? '⚛ Profiler' : 'Profiler', + '', + 'panel.html', + extensionPanel => { + extensionPanel.onShown.addListener(panel => { + if (currentPanel === panel) { + return; + } + + currentPanel = panel; + profilerPortalContainer = panel.container; + + if (profilerPortalContainer != null) { + ensureInitialHTMLIsCleared(profilerPortalContainer); + render('profiler'); + panel.injectStyles(cloneStyleTags); + } + }); + }, + ); + + chrome.devtools.network.onNavigated.removeListener(checkPageForReact); + + // Re-initialize DevTools panel when a new page is loaded. + chrome.devtools.network.onNavigated.addListener(function onNavigated() { + // Re-initialize saved filters on navigation, + // since global values stored on window get reset in this case. + syncSavedPreferences(); + + // It's easiest to recreate the DevTools panel (to clean up potential stale state). + // We can revisit this in the future as a small optimization. + flushSync(() => { + root.unmount(() => { + initBridgeAndStore(); + }); + }); + }); + }, + ); +} + +// Load (or reload) the DevTools extension when the user navigates to a new page. +function checkPageForReact() { + syncSavedPreferences(); + createPanelIfReactLoaded(); +} + +chrome.devtools.network.onNavigated.addListener(checkPageForReact); + +// Check to see if React has loaded once per second in case React is added +// after page load +const loadCheckInterval = setInterval(function() { + createPanelIfReactLoaded(); +}, 1000); + +createPanelIfReactLoaded(); diff --git a/packages/react-devtools-extensions/src/panel.js b/packages/react-devtools-extensions/src/panel.js new file mode 100644 index 0000000000000..de7d8a68fc5ff --- /dev/null +++ b/packages/react-devtools-extensions/src/panel.js @@ -0,0 +1,19 @@ +// Portal target container. +window.container = document.getElementById('container'); + +let hasInjectedStyles = false; + +// DevTools styles are injected into the top-level document head (where the main React app is rendered). +// This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled. +window.injectStyles = getLinkTags => { + if (!hasInjectedStyles) { + hasInjectedStyles = true; + + const linkTags = getLinkTags(); + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let linkTag of linkTags) { + document.head.appendChild(linkTag); + } + } +}; diff --git a/packages/react-devtools-extensions/src/renderer.js b/packages/react-devtools-extensions/src/renderer.js new file mode 100644 index 0000000000000..933fd5d6b8a6a --- /dev/null +++ b/packages/react-devtools-extensions/src/renderer.js @@ -0,0 +1,26 @@ +/** + * In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts. + * Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself, + * So this entry point (one of the web_accessible_resources) provcides a way to eagerly inject it. + * The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early. + * The normal case (not a reload-and-profile) will not make use of this entry point though. + * + * @flow + */ + +import {attach} from 'react-devtools-shared/src/backend/renderer'; + +Object.defineProperty( + window, + '__REACT_DEVTOOLS_ATTACH__', + ({ + enumerable: false, + // This property needs to be configurable to allow third-party integrations + // to attach their own renderer. Note that using third-party integrations + // is not officially supported. Use at your own risk. + configurable: true, + get() { + return attach; + }, + }: Object), +); diff --git a/packages/react-devtools-extensions/src/utils.js b/packages/react-devtools-extensions/src/utils.js new file mode 100644 index 0000000000000..c8d4003115a85 --- /dev/null +++ b/packages/react-devtools-extensions/src/utils.js @@ -0,0 +1,51 @@ +/* global chrome */ + +const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0; + +export function createViewElementSource(bridge: Bridge, store: Store) { + return function viewElementSource(id) { + const rendererID = store.getRendererIDForElement(id); + if (rendererID != null) { + // Ask the renderer interface to determine the component function, + // and store it as a global variable on the window + bridge.send('viewElementSource', {id, rendererID}); + + setTimeout(() => { + // Ask Chrome to display the location of the component function, + // assuming the renderer found one. + chrome.devtools.inspectedWindow.eval(` + if (window.$type != null) { + inspect(window.$type); + } + `); + }, 100); + } + }; +} + +export type BrowserName = 'Chrome' | 'Firefox'; + +export function getBrowserName(): BrowserName { + return IS_CHROME ? 'Chrome' : 'Firefox'; +} + +export type BrowserTheme = 'dark' | 'light'; + +export function getBrowserTheme(): BrowserTheme { + if (IS_CHROME) { + // chrome.devtools.panels added in Chrome 18. + // chrome.devtools.panels.themeName added in Chrome 54. + return chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light'; + } else { + // chrome.devtools.panels.themeName added in Firefox 55. + // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName + if (chrome.devtools && chrome.devtools.panels) { + switch (chrome.devtools.panels.themeName) { + case 'dark': + return 'dark'; + default: + return 'light'; + } + } + } +} diff --git a/packages/react-devtools-extensions/utils.js b/packages/react-devtools-extensions/utils.js new file mode 100644 index 0000000000000..1af974e701783 --- /dev/null +++ b/packages/react-devtools-extensions/utils.js @@ -0,0 +1,39 @@ +const {execSync} = require('child_process'); +const {readFileSync} = require('fs'); +const {resolve} = require('path'); + +function getGitCommit() { + return execSync('git show -s --format=%h') + .toString() + .trim(); +} + +function getGitHubURL() { + // TODO potentially replace this with an fb.me URL (assuming it can forward the query params) + const url = execSync('git remote get-url origin') + .toString() + .trim(); + + if (url.startsWith('https://')) { + return url.replace('.git', ''); + } else { + return url + .replace(':', '/') + .replace('git@', 'https://') + .replace('.git', ''); + } +} + +function getVersionString() { + const packageVersion = JSON.parse( + readFileSync( + resolve(__dirname, '..', 'react-devtools-core', './package.json'), + ), + ).version; + + const commit = getGitCommit(); + + return `${packageVersion}-${commit}`; +} + +module.exports = {getGitCommit, getGitHubURL, getVersionString}; diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js new file mode 100644 index 0000000000000..03911a31290b2 --- /dev/null +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -0,0 +1,62 @@ +'use strict'; + +const {resolve} = require('path'); +const {DefinePlugin} = require('webpack'); +const {getGitHubURL, getVersionString} = require('./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 + '/build', + filename: '[name].js', + }, + resolve: { + alias: { + react: resolve(builtModulesDir, 'react'), + 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), + 'react-dom': resolve(builtModulesDir, 'react-dom'), + '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-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js new file mode 100644 index 0000000000000..8a362f82fd3db --- /dev/null +++ b/packages/react-devtools-extensions/webpack.config.js @@ -0,0 +1,84 @@ +'use strict'; + +const {resolve} = require('path'); +const {DefinePlugin} = require('webpack'); +const {getGitHubURL, getVersionString} = require('./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: { + background: './src/background.js', + contentScript: './src/contentScript.js', + injectGlobalHook: './src/injectGlobalHook.js', + main: './src/main.js', + panel: './src/panel.js', + renderer: './src/renderer.js', + }, + output: { + path: __dirname + '/build', + filename: '[name].js', + }, + resolve: { + alias: { + react: resolve(builtModulesDir, 'react'), + 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), + 'react-dom': resolve(builtModulesDir, 'react-dom'), + 'react-is': resolve(builtModulesDir, 'react-is'), + scheduler: resolve(builtModulesDir, 'scheduler'), + }, + }, + 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: { + sourceMap: true, + modules: true, + localIdentName: '[local]___[hash:base64:5]', + }, + }, + ], + }, + ], + }, +}; diff --git a/packages/react-devtools-inline/README.md b/packages/react-devtools-inline/README.md index 33cc4afa8b3d8..2847bac8a9394 100644 --- a/packages/react-devtools-inline/README.md +++ b/packages/react-devtools-inline/README.md @@ -21,24 +21,32 @@ The frontend and backend can be initialized in any order, but **the backend must ### `react-devtools-inline/backend` * **`initialize(contentWindow)`** - -Installs the global hook on the window. This hook is how React and DevTools communicate. **This method must be called before React is loaded.** (This means before any `import` or `require` statements!) +Installs the global hook on the window. This hook is how React and DevTools communicate. **This method must be called before React is loaded.**2 * **`activate(contentWindow)`** - Lets the backend know when the frontend is ready. It should not be called until after the frontend has been initialized, else the frontend might miss important tree-initialization events. ```js import { activate, initialize } from 'react-devtools-inline/backend'; +// This should be the iframe the React application is running in. +const iframe = document.getElementById(frameID); +const contentWindow = iframe.contentWindow; + // Call this before importing React (or any other packages that might import React). -initialize(); +initialize(contentWindow); + +// Initialize the frontend... // Call this only once the frontend has been initialized. -activate(); +activate(contentWindow); ``` +2 The backend must be initialized before React is loaded. (This means before any `import` or `require` statements or ` + + \ No newline at end of file diff --git a/packages/react-devtools-shell/now.json b/packages/react-devtools-shell/now.json new file mode 100644 index 0000000000000..7c0805c3b1656 --- /dev/null +++ b/packages/react-devtools-shell/now.json @@ -0,0 +1,5 @@ +{ + "name": "react-devtools-experimental", + "alias": ["react-devtools-experimental"], + "files": ["index.html", "dist"] +} diff --git a/packages/react-devtools-shell/package.json b/packages/react-devtools-shell/package.json new file mode 100644 index 0000000000000..b3f1420065c33 --- /dev/null +++ b/packages/react-devtools-shell/package.json @@ -0,0 +1,34 @@ +{ + "private": true, + "name": "react-devtools-shell", + "version": "0.0.0", + "scripts": { + "build": "cross-env NODE_ENV=development cross-env TARGET=remote webpack --config webpack.config.js", + "deploy": "yarn run build && now deploy && now alias react-devtools-experimental", + "start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open" + }, + "dependencies": { + "immutable": "^4.0.0-rc.12", + "react-native-web": "^0.11.5" + }, + "devDependencies": { + "@babel/core": "^7.1.6", + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-transform-flow-strip-types": "^7.1.6", + "@babel/plugin-transform-react-jsx-source": "^7.2.0", + "@babel/preset-env": "^7.1.6", + "@babel/preset-flow": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "babel-core": "^7.0.0-bridge", + "babel-eslint": "^9.0.0", + "babel-jest": "^24.7.1", + "babel-loader": "^8.0.4", + "cross-env": "^3.1.4", + "css-loader": "^1.0.1", + "raw-loader": "^3.1.0", + "style-loader": "^0.23.1", + "webpack": "^4.26.0", + "webpack-cli": "^3.1.2", + "webpack-dev-server": "^3.3.1" + } +} diff --git a/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js b/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js new file mode 100644 index 0000000000000..bc6b05fa9d774 --- /dev/null +++ b/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js @@ -0,0 +1,45 @@ +/** + * 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 React, {Fragment} from 'react'; + +function wrapWithHoc(Component, index) { + function HOC() { + return ; + } + + // $FlowFixMe + const displayName = Component.displayName || Component.name; + + HOC.displayName = `withHoc${index}(${displayName})`; + return HOC; +} + +function wrapWithNested(Component, times) { + for (let i = 0; i < times; i++) { + Component = wrapWithHoc(Component, i); + } + + return Component; +} + +function Nested() { + return
Deeply nested div
; +} + +const DeeplyNested = wrapWithNested(Nested, 100); + +export default function DeeplyNestedComponents() { + return ( + +

Deeply nested component

+ +
+ ); +} diff --git a/packages/react-devtools-shell/src/app/EditableProps/index.js b/packages/react-devtools-shell/src/app/EditableProps/index.js new file mode 100644 index 0000000000000..220a58c17be32 --- /dev/null +++ b/packages/react-devtools-shell/src/app/EditableProps/index.js @@ -0,0 +1,169 @@ +/** + * 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 React, { + createContext, + Component, + forwardRef, + Fragment, + memo, + useCallback, + useDebugValue, + useEffect, + useReducer, + useState, +} from 'react'; + +const initialData = {foo: 'FOO', bar: 'BAR'}; + +function reducer(state, action) { + switch (action.type) { + case 'swap': + return {foo: state.bar, bar: state.foo}; + default: + throw new Error(); + } +} + +type StatefulFunctionProps = {|name: string|}; + +function StatefulFunction({name}: StatefulFunctionProps) { + const [count, updateCount] = useState(0); + const debouncedCount = useDebounce(count, 1000); + const handleUpdateCountClick = useCallback(() => updateCount(count + 1), [ + count, + ]); + + const [data, dispatch] = useReducer(reducer, initialData); + const handleUpdateReducerClick = useCallback( + () => dispatch({type: 'swap'}), + [], + ); + + return ( +
    +
  • Name: {name}
  • +
  • + +
  • +
  • + Reducer state: foo "{data.foo}", bar "{data.bar}" +
  • +
  • + +
  • +
+ ); +} + +const BoolContext = createContext(true); +BoolContext.displayName = 'BoolContext'; + +type Props = {|name: string, toggle: boolean|}; +type State = {|cities: Array, state: string|}; + +class StatefulClass extends Component { + static contextType = BoolContext; + + state: State = { + cities: ['San Francisco', 'San Jose'], + state: 'California', + }; + + handleChange = ({target}) => + this.setState({ + state: target.value, + }); + + render() { + return ( +
    +
  • Name: {this.props.name}
  • +
  • Toggle: {this.props.toggle ? 'true' : 'false'}
  • +
  • + State: +
  • +
  • Cities: {this.state.cities.join(', ')}
  • +
  • Context: {this.context ? 'true' : 'false'}
  • +
+ ); + } +} + +const MemoizedStatefulClass = memo(StatefulClass); +const MemoizedStatefulFunction = memo(StatefulFunction); + +const ForwardRef = forwardRef<{|name: string|}, HTMLUListElement>( + ({name}, ref) => { + const [count, updateCount] = useState(0); + const debouncedCount = useDebounce(count, 1000); + const handleUpdateCountClick = useCallback(() => updateCount(count + 1), [ + count, + ]); + return ( +
    +
  • Name: {name}
  • +
  • + +
  • +
+ ); + }, +); + +export default function EditableProps() { + return ( + +

Editable props

+ Class + + Function + + Memoized Class + + Memoized Function + + Forward Ref + +
+ ); +} + +// Below copied from https://usehooks.com/ +function useDebounce(value, delay) { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = useState(value); + + // Show the value in DevTools + useDebugValue(debouncedValue); + + useEffect( + () => { + // Update debounced value after delay + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Cancel the timeout if value changes (also on delay change or unmount) + // This is how we prevent debounced value from updating if value is changed ... + // .. within the delay period. Timeout gets cleared and restarted. + return () => { + clearTimeout(handler); + }; + }, + [value, delay], // Only re-call effect if value or delay changes + ); + + return debouncedValue; +} +// Above copied from https://usehooks.com/ diff --git a/packages/react-devtools-shell/src/app/ElementTypes/index.js b/packages/react-devtools-shell/src/app/ElementTypes/index.js new file mode 100644 index 0000000000000..b66d9eb9989fe --- /dev/null +++ b/packages/react-devtools-shell/src/app/ElementTypes/index.js @@ -0,0 +1,67 @@ +/** + * 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 React, { + createContext, + forwardRef, + lazy, + memo, + Component, + Fragment, + // $FlowFixMe Flow doesn't know about the Profiler import yet + Profiler, + StrictMode, + Suspense, +} from 'react'; + +const Context = createContext('abc'); +Context.displayName = 'ExampleContext'; + +class ClassComponent extends Component { + render() { + return null; + } +} + +function FunctionComponent() { + return null; +} + +const MemoFunctionComponent = memo(FunctionComponent); + +const ForwardRefComponent = forwardRef((props, ref) => ( + +)); + +const LazyComponent = lazy(() => + Promise.resolve({ + default: FunctionComponent, + }), +); + +export default function ElementTypes() { + return ( + {}}> + + + {value => null} + + + Loading...}> + + + + + + + + + + ); +} diff --git a/packages/react-devtools-shell/src/app/Hydration/index.js b/packages/react-devtools-shell/src/app/Hydration/index.js new file mode 100644 index 0000000000000..914bf54ccc071 --- /dev/null +++ b/packages/react-devtools-shell/src/app/Hydration/index.js @@ -0,0 +1,140 @@ +/** + * 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 React, {Fragment, useDebugValue, useState} from 'react'; + +const div = document.createElement('div'); +const exmapleFunction = () => {}; +const typedArray = new Uint8Array(3); +typedArray[0] = 1; +typedArray[1] = 2; +typedArray[2] = 3; + +const arrayOfArrays = [ + [['a', 'b', 'c'], ['d', 'e', 'f'], ['h', 'i', 'j']], + [['k', 'l', 'm'], ['n', 'o', 'p'], ['q', 'r', 's']], + [['t', 'u', 'v'], ['w', 'x', 'y'], ['z']], + [], +]; + +const objectOfObjects = { + foo: { + a: 1, + b: 2, + c: 3, + }, + bar: { + e: 4, + f: 5, + g: 6, + }, + baz: { + h: 7, + i: 8, + j: 9, + }, + qux: {}, +}; + +function useOuterFoo() { + useDebugValue({ + debugA: { + debugB: { + debugC: 'abc', + }, + }, + }); + useState({ + valueA: { + valueB: { + valueC: 'abc', + }, + }, + }); + return useInnerFoo(); +} + +function useInnerFoo() { + const [value] = useState([[['a', 'b', 'c']]]); + return value; +} + +function useOuterBar() { + useDebugValue({ + debugA: { + debugB: { + debugC: 'abc', + }, + }, + }); + return useInnerBar(); +} + +function useInnerBar() { + useDebugValue({ + debugA: { + debugB: { + debugC: 'abc', + }, + }, + }); + const [count] = useState(123); + return count; +} + +function useOuterBaz() { + return useInnerBaz(); +} + +function useInnerBaz() { + const [count] = useState(123); + return count; +} + +export default function Hydration() { + return ( + +

Hydration

+ } + array_buffer={typedArray.buffer} + typed_array={typedArray} + date={new Date()} + array={arrayOfArrays} + object={objectOfObjects} + /> + +
+ ); +} + +function DehydratableProps({array, object}: any) { + return ( +
    +
  • array: {JSON.stringify(array, null, 2)}
  • +
  • object: {JSON.stringify(object, null, 2)}
  • +
+ ); +} + +function DeepHooks(props: any) { + const foo = useOuterFoo(); + const bar = useOuterBar(); + const baz = useOuterBaz(); + return ( +
    +
  • foo: {foo}
  • +
  • bar: {bar}
  • +
  • baz: {baz}
  • +
+ ); +} diff --git a/packages/react-devtools-shell/src/app/Iframe/index.js b/packages/react-devtools-shell/src/app/Iframe/index.js new file mode 100644 index 0000000000000..394b0d680f9c4 --- /dev/null +++ b/packages/react-devtools-shell/src/app/Iframe/index.js @@ -0,0 +1,69 @@ +/** @flow */ + +import React, {Fragment} from 'react'; +import ReactDOM from 'react-dom'; + +export default function Iframe() { + return ( + +

Iframe

+
+ + + +
+
+ ); +} + +const iframeStyle = {border: '2px solid #eee', height: 80}; + +function Frame(props) { + const [element, setElement] = React.useState(null); + + const ref = React.useRef(); + + React.useLayoutEffect(function() { + const iframe = ref.current; + + if (iframe) { + const html = ` + + + +
+ + + `; + + const document = iframe.contentDocument; + + document.open(); + document.write(html); + document.close(); + + setElement(document.getElementById('root')); + } + }, []); + + return ( + +