Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5880d7bd66 |
@@ -8,17 +8,16 @@ aliases:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
- &restore_yarn_cache
|
||||
restore_cache:
|
||||
name: Restore yarn cache
|
||||
key: v2-node-{{ arch }}-{{ checksum "yarn.lock" }}-yarn
|
||||
|
||||
- &restore_node_modules
|
||||
restore_cache:
|
||||
name: Restore node_modules cache
|
||||
keys:
|
||||
- v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-modules
|
||||
|
||||
- &TEST_PARALLELISM 20
|
||||
- v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
- v2-node-{{ arch }}-{{ .Branch }}-
|
||||
- v2-node-{{ arch }}-
|
||||
- &run_yarn
|
||||
run:
|
||||
name: Install Packages
|
||||
command: yarn --frozen-lockfile
|
||||
|
||||
- &attach_workspace
|
||||
at: build
|
||||
@@ -29,7 +28,8 @@ aliases:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: node ./scripts/rollup/consolidateBundleSizes.js
|
||||
- run: ./scripts/circleci/pack_and_store_artifact.sh
|
||||
- store_artifacts:
|
||||
@@ -58,26 +58,12 @@ jobs:
|
||||
name: Nodejs Version
|
||||
command: node --version
|
||||
- *restore_yarn_cache
|
||||
- run:
|
||||
name: Install Packages
|
||||
command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
- *run_yarn
|
||||
- save_cache:
|
||||
# Store the yarn cache globally for all lock files with this same
|
||||
# checksum. This will speed up the setup job for all PRs where the
|
||||
# lockfile is the same.
|
||||
name: Save yarn cache for future installs
|
||||
key: v2-node-{{ arch }}-{{ checksum "yarn.lock" }}-yarn
|
||||
name: Save node_modules cache
|
||||
key: v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- save_cache:
|
||||
# Store node_modules for all jobs in this workflow so that they don't
|
||||
# need to each run a yarn install for each job. This will speed up
|
||||
# all jobs run on this branch with the same lockfile.
|
||||
name: Save node_modules cache
|
||||
# This cache key is per branch, a yarn install in setup is required.
|
||||
key: v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-modules
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
yarn_lint:
|
||||
docker: *docker
|
||||
@@ -85,7 +71,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: node ./scripts/prettier/index
|
||||
- run: node ./scripts/tasks/eslint
|
||||
- run: ./scripts/circleci/check_license.sh
|
||||
@@ -98,136 +85,138 @@ jobs:
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: node ./scripts/tasks/flow-ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=stable --ci
|
||||
|
||||
yarn_test:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_www:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-classic --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_www_variant:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-classic --variant --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_prod_www:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-classic --prod --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_prod_www_variant:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-classic --prod --variant --ci
|
||||
|
||||
yarn_test_www:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-modern --ci
|
||||
|
||||
yarn_test_www_variant:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-modern --variant --ci
|
||||
|
||||
yarn_test_prod_www:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-modern --prod --ci
|
||||
|
||||
yarn_test_prod_www_variant:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=www-modern --prod --variant --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_persistent:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=stable --persistent --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_prod:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=stable --prod --ci
|
||||
|
||||
yarn_test_prod:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=experimental --prod --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_build:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
parallelism: 20
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
@@ -253,7 +242,8 @@ jobs:
|
||||
parallelism: 20
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: experimental
|
||||
@@ -276,14 +266,12 @@ jobs:
|
||||
build_devtools_and_process_artifacts:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: 20
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *restore_node_modules
|
||||
- run:
|
||||
name: Install Packages
|
||||
command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: experimental
|
||||
@@ -291,46 +279,6 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: ./build/devtools.tgz
|
||||
|
||||
build_devtools_scheduling_profiler:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *restore_node_modules
|
||||
- run:
|
||||
name: Install Packages
|
||||
command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
- run:
|
||||
name: Build and archive
|
||||
command: |
|
||||
mkdir -p build/devtools
|
||||
cd packages/react-devtools-scheduling-profiler
|
||||
yarn build
|
||||
cd dist
|
||||
tar -zcvf ../../../build/devtools-scheduling-profiler.tgz .
|
||||
- store_artifacts:
|
||||
path: ./build/devtools-scheduling-profiler.tgz
|
||||
- persist_to_workspace:
|
||||
root: packages/react-devtools-scheduling-profiler
|
||||
paths:
|
||||
- dist
|
||||
|
||||
deploy_devtools_scheduling_profiler:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: packages/react-devtools-scheduling-profiler
|
||||
- *restore_node_modules
|
||||
- run:
|
||||
name: Deploy
|
||||
command: |
|
||||
cd packages/react-devtools-scheduling-profiler
|
||||
yarn vercel deploy dist --prod --confirm --token $SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN
|
||||
|
||||
# These jobs are named differently so we can distinguish the stable and
|
||||
# and experimental artifacts
|
||||
process_artifacts: *process_artifacts
|
||||
@@ -342,7 +290,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
# This runs in the process_artifacts job, too, but it's faster to run
|
||||
# this step in both jobs instead of running the jobs sequentially
|
||||
- run: node ./scripts/rollup/consolidateBundleSizes.js
|
||||
@@ -357,7 +306,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
# This runs in the process_artifacts job, too, but it's faster to run
|
||||
# this step in both jobs instead of running the jobs sequentially
|
||||
- run: node ./scripts/rollup/consolidateBundleSizes.js
|
||||
@@ -372,7 +322,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn lint-build
|
||||
- run: scripts/circleci/check_minified_errors.sh
|
||||
|
||||
@@ -382,7 +333,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
@@ -392,21 +344,21 @@ jobs:
|
||||
RELEASE_CHANNEL_stable_yarn_test_build:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=stable --build --ci
|
||||
|
||||
yarn_test_build:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=experimental --build --ci
|
||||
|
||||
yarn_test_build_devtools:
|
||||
@@ -415,7 +367,8 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --project=devtools --build --ci
|
||||
|
||||
RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
|
||||
@@ -424,7 +377,7 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- run:
|
||||
name: Run DOM fixture tests
|
||||
environment:
|
||||
@@ -440,7 +393,8 @@ jobs:
|
||||
environment: *environment
|
||||
steps:
|
||||
- checkout
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
name: Run fuzz tests
|
||||
command: |
|
||||
@@ -450,21 +404,21 @@ jobs:
|
||||
RELEASE_CHANNEL_stable_yarn_test_build_prod:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=stable --build --prod --ci
|
||||
|
||||
yarn_test_build_prod:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: *TEST_PARALLELISM
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_node_modules
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run: yarn test --release-channel=experimental --build --prod --ci
|
||||
|
||||
workflows:
|
||||
@@ -563,19 +517,10 @@ workflows:
|
||||
- yarn_test_build_devtools:
|
||||
requires:
|
||||
- yarn_build
|
||||
- build_devtools_and_process_artifacts:
|
||||
requires:
|
||||
- yarn_build
|
||||
- build_devtools_scheduling_profiler:
|
||||
requires:
|
||||
- yarn_build
|
||||
- deploy_devtools_scheduling_profiler:
|
||||
requires:
|
||||
- build_devtools_scheduling_profiler
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
# FIXME: Temporarily disabled to unblock master.
|
||||
# - build_devtools_and_process_artifacts:
|
||||
# requires:
|
||||
# - yarn_build
|
||||
|
||||
fuzz_tests:
|
||||
triggers:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"packages": ["packages/react", "packages/react-dom", "packages/scheduler"],
|
||||
"buildCommand": "build --type=NODE react/index,react-dom/index,react-dom/server,react-dom/test-utils,scheduler/index,scheduler/tracing",
|
||||
"buildCommand": "build --type=NODE react/index,react-dom/index,react-dom/server,scheduler/index,scheduler/tracing",
|
||||
"publishDirectory": {
|
||||
"react": "build/node_modules/react",
|
||||
"react-dom": "build/node_modules/react-dom",
|
||||
|
||||
@@ -18,6 +18,4 @@ 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
|
||||
packages/react-devtools-scheduling-profiler/dist
|
||||
packages/react-devtools-scheduling-profiler/static
|
||||
packages/react-devtools-shell/dist
|
||||
29
.eslintrc.js
29
.eslintrc.js
@@ -16,13 +16,7 @@ module.exports = {
|
||||
// Stop ESLint from looking for a configuration file in parent folders
|
||||
root: true,
|
||||
|
||||
plugins: [
|
||||
'jest',
|
||||
'no-for-of-loops',
|
||||
'no-function-declare-after-return',
|
||||
'react',
|
||||
'react-internal',
|
||||
],
|
||||
plugins: ['jest', 'no-for-of-loops', 'react', 'react-internal'],
|
||||
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
@@ -97,9 +91,6 @@ module.exports = {
|
||||
// You can disable this rule for code that isn't shipped (e.g. build scripts and tests).
|
||||
'no-for-of-loops/no-for-of-loops': ERROR,
|
||||
|
||||
// Prevent function declarations after return statements
|
||||
'no-function-declare-after-return/no-function-declare-after-return': ERROR,
|
||||
|
||||
// CUSTOM RULES
|
||||
// the second argument of warning/invariant should be a literal string
|
||||
'react-internal/no-primitive-constructors': ERROR,
|
||||
@@ -108,18 +99,6 @@ module.exports = {
|
||||
'react-internal/warning-args': ERROR,
|
||||
'react-internal/no-production-logging': ERROR,
|
||||
'react-internal/no-cross-fork-imports': ERROR,
|
||||
'react-internal/no-cross-fork-types': [
|
||||
ERROR,
|
||||
{
|
||||
old: [
|
||||
'firstEffect',
|
||||
'nextEffect',
|
||||
// Disabled because it's also used by the Hook type.
|
||||
// 'lastEffect',
|
||||
],
|
||||
new: ['subtreeFlags'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
overrides: [
|
||||
@@ -187,12 +166,6 @@ module.exports = {
|
||||
__webpack_require__: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/scheduler/**/*.js'],
|
||||
globals: {
|
||||
TaskController: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
globals: {
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,5 +34,4 @@ packages/react-devtools-extensions/firefox/*.pem
|
||||
packages/react-devtools-extensions/shared/build
|
||||
packages/react-devtools-extensions/.tempUserDataDir
|
||||
packages/react-devtools-inline/dist
|
||||
packages/react-devtools-shell/dist
|
||||
packages/react-devtools-scheduling-profiler/dist
|
||||
packages/react-devtools-shell/dist
|
||||
3
.mailmap
3
.mailmap
@@ -127,8 +127,6 @@ Rainer Oviir <roviir@gmail.com> <raineroviir@rainers-MacBook-Pro.local>
|
||||
Ray <ray@tomo.im>
|
||||
Richard Feldman <richard.t.feldman@gmail.com> <richard@noredink.com>
|
||||
Richard Livesey <Livesey7@hotmail.co.uk>
|
||||
Rick Hanlon <rickhanlonii@gmail.com>
|
||||
Rick Hanlon <rickhanlonii@gmail.com> <rickhanlonii@fb.com>
|
||||
Rob Arnold <robarnold@cs.cmu.edu>
|
||||
Robert Binna <rbinna@gmail.com> <speedskater@users.noreply.github.com>
|
||||
Robin Frischmann <robin@rofrischmann.de>
|
||||
@@ -147,7 +145,6 @@ Steven Luscher <react@steveluscher.com> <github@steveluscher.com>
|
||||
Steven Luscher <react@steveluscher.com> <steveluscher@fb.com>
|
||||
Steven Luscher <react@steveluscher.com> <steveluscher@instagram.com>
|
||||
Steven Luscher <react@steveluscher.com> <steveluscher@users.noreply.github.com>
|
||||
Seth Webster <sethwebster@gmail.com> <sethwebster@fb.com>
|
||||
Stoyan Stefanov <ssttoo@ymail.com>
|
||||
Tengfei Guo <terryr3rd@yeah.net> <tfguo369@gmail.com>
|
||||
Thomas Aylott <oblivious@subtlegradient.com> <aylott@fb.com>
|
||||
|
||||
@@ -3,6 +3,4 @@ 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
|
||||
packages/react-devtools-scheduling-profiler/dist
|
||||
packages/react-devtools-scheduling-profiler/static
|
||||
packages/react-devtools-shell/dist
|
||||
@@ -56,7 +56,7 @@ You'll notice that we used an HTML-like syntax; [we call it JSX](https://reactjs
|
||||
|
||||
## Contributing
|
||||
|
||||
The main purpose of this repository is to continue evolving React core, making it faster and easier to use. Development of React happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving React.
|
||||
The main purpose of this repository is to continue to evolve React core, making it faster and easier to use. Development of React happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving React.
|
||||
|
||||
### [Code of Conduct](https://code.fb.com/codeofconduct)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-core": "^6.24.0",
|
||||
"babel-loader": "^6.4.1",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"react": "link:../../build/node_modules/react",
|
||||
"react-art": "link:../../build/node_modules/react-art/",
|
||||
"react-dom": "link:../../build/node_modules/react-dom",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
context: __dirname,
|
||||
@@ -11,10 +12,10 @@ module.exports = {
|
||||
exclude: /node_modules/,
|
||||
query: {
|
||||
presets: [
|
||||
require.resolve('@babel/preset-env'),
|
||||
require.resolve('@babel/preset-react'),
|
||||
require.resolve('babel-preset-es2015'),
|
||||
require.resolve('babel-preset-react'),
|
||||
],
|
||||
plugins: [require.resolve('@babel/plugin-proposal-class-properties')],
|
||||
plugins: [require.resolve('babel-plugin-transform-class-properties')],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3223,31 +3223,6 @@
|
||||
| `end=(null)`| (initial)| `<null>` |
|
||||
| `end=(undefined)`| (initial)| `<null>` |
|
||||
|
||||
## `enterKeyHint` (on `<input>` inside `<div>`)
|
||||
| Test Case | Flags | Result |
|
||||
| --- | --- | --- |
|
||||
| `enterKeyHint=(string)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(empty string)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(array with string)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(empty array)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(object)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(numeric string)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(-1)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(0)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(integer)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(NaN)`| (initial, warning)| `<empty string>` |
|
||||
| `enterKeyHint=(float)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(true)`| (initial, warning)| `<empty string>` |
|
||||
| `enterKeyHint=(false)`| (initial, warning)| `<empty string>` |
|
||||
| `enterKeyHint=(string 'true')`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(string 'false')`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(string 'on')`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(string 'off')`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(symbol)`| (initial, warning)| `<empty string>` |
|
||||
| `enterKeyHint=(function)`| (initial, warning)| `<empty string>` |
|
||||
| `enterKeyHint=(null)`| (initial)| `<empty string>` |
|
||||
| `enterKeyHint=(undefined)`| (initial)| `<empty string>` |
|
||||
|
||||
## `exponent` (on `<feFuncA>` inside `<svg>`)
|
||||
| Test Case | Flags | Result |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -556,11 +556,6 @@ const attributes = [
|
||||
tagName: 'animate',
|
||||
read: getSVGAttribute('end'),
|
||||
},
|
||||
{
|
||||
name: 'enterKeyHint',
|
||||
tagName: 'input',
|
||||
read: getProperty('enterKeyHint'),
|
||||
},
|
||||
{
|
||||
name: 'exponent',
|
||||
read: getSVGProperty('exponent'),
|
||||
|
||||
@@ -208,70 +208,6 @@
|
||||
return <ChildComponent customObject={new Custom()} />;
|
||||
}
|
||||
|
||||
const baseInheritedKeys = Object.create(Object.prototype, {
|
||||
enumerableStringBase: {
|
||||
value: 1,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
[Symbol('enumerableSymbolBase')]: {
|
||||
value: 1,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
nonEnumerableStringBase: {
|
||||
value: 1,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
[Symbol('nonEnumerableSymbolBase')]: {
|
||||
value: 1,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
const inheritedKeys = Object.create(baseInheritedKeys, {
|
||||
enumerableString: {
|
||||
value: 2,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
nonEnumerableString: {
|
||||
value: 3,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
123: {
|
||||
value: 3,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
[Symbol('nonEnumerableSymbol')]: {
|
||||
value: 2,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
[Symbol('enumerableSymbol')]: {
|
||||
value: 3,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
function InheritedKeys() {
|
||||
return <ChildComponent data={inheritedKeys} />;
|
||||
}
|
||||
|
||||
const object = {
|
||||
string: "abc",
|
||||
longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890",
|
||||
@@ -358,7 +294,6 @@
|
||||
<ObjectProps />
|
||||
<UnserializableProps />
|
||||
<CustomObject />
|
||||
<InheritedKeys />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -324,9 +324,9 @@ export default function Fibers({fibers, show, graphSettings, ...rest}) {
|
||||
) : (
|
||||
<small>Committed</small>
|
||||
)}
|
||||
{fiber.flags && [
|
||||
{fiber.effectTag && [
|
||||
<br key="br" />,
|
||||
<small key="small">Effect: {fiber.flags}</small>,
|
||||
<small key="small">Effect: {fiber.effectTag}</small>,
|
||||
]}
|
||||
</div>
|
||||
</Vertex>,
|
||||
|
||||
@@ -37,7 +37,7 @@ function getFriendlyTag(tag) {
|
||||
}
|
||||
}
|
||||
|
||||
function getFriendlyEffect(flags) {
|
||||
function getFriendlyEffect(effectTag) {
|
||||
const effects = {
|
||||
1: 'Performed Work',
|
||||
2: 'Placement',
|
||||
@@ -49,7 +49,7 @@ function getFriendlyEffect(flags) {
|
||||
128: 'Ref',
|
||||
};
|
||||
return Object.keys(effects)
|
||||
.filter(flag => flag & flags)
|
||||
.filter(flag => flag & effectTag)
|
||||
.map(flag => effects[flag])
|
||||
.join(' & ');
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export default function describeFibers(rootFiber, workInProgress) {
|
||||
...fiber,
|
||||
id: id,
|
||||
tag: getFriendlyTag(fiber.tag),
|
||||
flags: getFriendlyEffect(fiber.flags),
|
||||
effectTag: getFriendlyEffect(fiber.effectTag),
|
||||
type: fiber.type && '<' + (fiber.type.name || fiber.type) + '>',
|
||||
stateNode: `[${typeof fiber.stateNode}]`,
|
||||
return: acknowledgeFiber(fiber.return),
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
EXTEND_ESLINT=true
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
@@ -1 +0,0 @@
|
||||
src/*/node_modules
|
||||
27
fixtures/nesting/.gitignore
vendored
27
fixtures/nesting/.gitignore
vendored
@@ -1,27 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# copies of shared
|
||||
src/*/shared
|
||||
src/*/node_modules
|
||||
@@ -1,163 +0,0 @@
|
||||
# Nested React Demo
|
||||
|
||||
This is a demo of how you can configure a build system to serve **two different versions of React** side by side in the same app. This is not optimal, and should only be used as a compromise to prevent your app from getting stuck on an old version of React.
|
||||
|
||||
## You Probably Don't Need This
|
||||
|
||||
Note that **this approach is meant to be an escape hatch, not the norm**.
|
||||
|
||||
Normally, we encourage you to use a single version of React across your whole app. When you need to upgrade React, it is better to try to upgrade it all at once. We try to keep breaking changes between versions to the minimum, and often there are automatic scripts ("codemods") that can assist you with migration. You can always find the migration information for any release on [our blog](https://reactjs.org/blog/).
|
||||
|
||||
Using a single version of React removes a lot of complexity. It is also essential to ensure the best experience for your users who don't have to download the code twice. Always prefer using one React.
|
||||
|
||||
## What Is This For?
|
||||
|
||||
However, for some apps that have been in production for many years, upgrading all screens at once may be prohibitively difficult. For example, React components written in 2014 may still rely on [the unofficial legacy context API](https://reactjs.org/docs/legacy-context.html) (not to be confused with the modern one), and are not always maintained.
|
||||
|
||||
Traditionally, this meant that if a legacy API is deprecated, you would be stuck on the old version of React forever. That prevents your whole app from receiving improvements and bugfixes. This repository demonstrates a hybrid approach. It shows how you can use a newer version of React for some parts of your app, while **lazy-loading an older version of React** for the parts that haven't been migrated yet.
|
||||
|
||||
This approach is inherently more complex, and should be used as a last resort when you can't upgrade.
|
||||
|
||||
## Version Requirements
|
||||
|
||||
This demo uses two different versions of React: React 17 for "modern" components (in `src/modern`), and React 16.8 for "legacy" components (in `src/legacy`).
|
||||
|
||||
**We still recommend upgrading your whole app to React 17 in one piece.** The React 17 release intentionally has minimal breaking changes so that it's easier to upgrade to. In particular, React 17 solves some problems with nesting related to event propagation that earlier versions of React did not handle well. We expect that this nesting demo may not be as useful today as during a future migration from React 17 to the future major versions where some of the long-deprecated APIs may be removed.
|
||||
|
||||
However, if you're already stuck on an old version of React, you may found this approach useful today. If you remove a Hook call from `src/shared/Clock.js`, you can downgrade the legacy React all the way down to React 16.3. If you then remove Context API usage from `src/legacy/createLegacyRoot.js`, you can further downgrade the legacy React version, but keep in mind that the usage of third-party libraries included in this demo (React Router and React Redux) may need to be adjusted or removed.
|
||||
|
||||
## Installation
|
||||
|
||||
To run this demo, open its folder in Terminal and execute:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
If you want to test the production build, you can run instead:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build
|
||||
npx serve -s build
|
||||
```
|
||||
|
||||
This sample app uses client-side routing and consists of two routes:
|
||||
|
||||
- `/` renders a page which uses a newer version of React. (In the production build, you can verify that only one version of React is being loaded when this route is rendered.)
|
||||
- `/about` renders a page which uses an older version of React for a part of its tree. (In the production build, you can verify that both versions of React are loaded from different chunks.)
|
||||
|
||||
**The purpose of this demo is to show some nuances of such setup:**
|
||||
|
||||
- How to install two versions of React in a single app with npm side by side.
|
||||
- How to avoid the ["invalid Hook call" error](https://github.com/facebook/react/issues/13991) while nesting React trees.
|
||||
- How to pass context between different versions of React.
|
||||
- How to lazy-load the second React bundle so it's only loaded on the screens that use it.
|
||||
- How to do all of this without a special bundler configuration.
|
||||
|
||||
## How It Works
|
||||
|
||||
File structure is extremely important in this demo. It has a direct effect on which code is going to use which version of React. This particular demo is using Create React App without ejecting, so **it doesn't rely on any bundler plugins or configuration**. The principle of this demo is portable to other setups.
|
||||
|
||||
### Dependencies
|
||||
|
||||
We will use three different `package.json`s: one for non-React code at the root, and two in the respective `src/legacy` and `src/modern` folders that specify the React dependencies:
|
||||
|
||||
- **`package.json`**: The root `package.json` is a place for build dependencies (such as `react-scripts`) and any React-agnostic libraries (for example, `lodash`, `immer`, or `redux`). We do **not** include React or any React-related libraries in this file.
|
||||
- **`src/legacy/package.json`**: This is where we declare the `react` and `react-dom` dependencies for the "legacy" trees. In this demo, we're using React 16.8 (although, as noted above, we could downgrade it further below). This is **also** where we specify any third-party libraries that use React. For example, we include `react-router` and `react-redux` in this example.
|
||||
- **`src/modern/package.json`**: This is where we declare the `react` and `react-dom` dependencies for the "modern" trees. In this demo, we're using React 17. Here, we also specify third-party dependencies that use React and are used from the modern part of our app. This is why we *also* have `react-router` and `react-redux` in this file. (Their versions don't strictly have to match their `legacy` counterparts, but features that rely on context may require workarounds if they differ.)
|
||||
|
||||
The `scripts` in the root `package.json` are set up so that when you run `npm install` in it, it also runs `npm intall` in both `src/legacy` and `src/modern` folders.
|
||||
|
||||
**Note:** This demo is set up to use a few third-party dependencies (React Router and Redux). These are not essential, and you can remove them from the demo. They are included so we can show how to make them work with this approach.
|
||||
|
||||
### Folders
|
||||
|
||||
There are a few key folders in this example:
|
||||
|
||||
- **`src`**: Root of the source tree. At this level (or below it, except for the special folders noted below), you can put any logic that's agnostic of React. For example, in this demo we have `src/index.js` which is the app's entry point, and `src/store.js` which exports a Redux store. These regular modules only execute once, and are **not** duplicated between the bundles.
|
||||
- **`src/legacy`**: This is where all the code using the older version of React should go. This includes React components and Hooks, and general product code that is **only** used by the legacy trees.
|
||||
- **`src/modern`**: This is where all the code using the newer version of React should go. This includes React components and Hooks, and general product code that is **only** used by the modern trees.
|
||||
- **`src/shared`**: You may have some components or Hooks that you wish to use from both modern and legacy subtrees. The build process is set up so that **everything inside `src/shared` gets copied by a file watcher** into both `src/legacy/shared` and `src/modern/shared` on every change. This lets you write a component or a Hook once, but reuse it in both places.
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Loading two Reacts on the same page is bad for the user experience, so you should strive to push this as far as possible from the critical path of your app. For example, if there is a dialog that is less commonly used, or a route that is rarely visited, those are better candidates for staying on an older version of React than parts of your homepage.
|
||||
|
||||
To encourage only loading the older React when necessary, this demo includes a helper that works similarly to `React.lazy`. For example, `src/modern/AboutPage.js`, simplified, looks like this:
|
||||
|
||||
```js
|
||||
import lazyLegacyRoot from './lazyLegacyRoot';
|
||||
|
||||
// Lazy-load a component from the bundle using legacy React.
|
||||
const Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));
|
||||
|
||||
function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<h3>This component is rendered by React ({React.version}).</h3>
|
||||
<Greeting />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
As a result, only if the `AboutPage` (and as a result, `<Greeting />`) gets rendered, we will load the bundle containing the legacy React and the legacy `Greeting` component. Like with `React.lazy()`, we wrap it in `<Suspense>` to specify the loading indicator:
|
||||
|
||||
```js
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<AboutPage />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
If the legacy component is only rendered conditionally, we won't load the second React until it's shown:
|
||||
|
||||
```js
|
||||
<>
|
||||
<button onClick={() => setShowGreeting(true)}>
|
||||
Say hi
|
||||
</button>
|
||||
{showGreeting && (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Greeting />
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
```
|
||||
|
||||
|
||||
The implementation of the `src/modern/lazyLegacyRoot.js` helper is included so you can tweak it and customize it to your needs. Remember to test lazy loading with the production builds because the bundler may not optimize it in development.
|
||||
|
||||
### Context
|
||||
|
||||
If you have nested trees managed by different versions of React, the inner tree won't "see" the outer tree's Context.
|
||||
|
||||
This breaks third-party libraries like React Redux or React Router, as well as any of your own usage of Context (for example, for theming).
|
||||
|
||||
To solve this problem, we read all the Contexts we care about in the outer tree, pass them to the inner tree, and then wrap the inner tree in the corresponding Providers. You can see this in action in two files:
|
||||
|
||||
* `src/modern/lazyLegacyRoot.js`: Look for `useContext` calls, and how their results are combined into a single object that is passed through. **You can read more Contexts there** if your app requires them.
|
||||
* `src/legacy/createLegacyRoot.js`: Look for the `Bridge` component which receives that object and wraps its children with the appropriate Context Providers. **You can wrap them with more Providers there** if your app requires them.
|
||||
|
||||
Note that, generally saying, this approach is somewhat fragile, especially because some libraries may not expose their Contexts officially or consider their structure private. You may be able to expose private Contexts by using a tool like [patch-package](https://www.npmjs.com/package/patch-package), but remember to keep all the versions pinned because even a patch release of a third-party library may change the behavior.
|
||||
|
||||
### Nesting Direction
|
||||
|
||||
In this demo, we use an older React inside an app managed by the newer React. However, we could rename the folders and apply the same approach in the other direction.
|
||||
|
||||
### Event Propagation
|
||||
|
||||
Note that before React 17, `event.stopPropagation()` in the inner React tree does not prevent the event propagation to the outer React tree. This may cause unexpected behavior when extracting a UI tree like a dialog to use a separate React. This is because prior to React 17, both Reacts would attach the event listener at the `document` level. React 17 fixes this by attaching handlers to the roots. We strongly recommend upgrading to React 17 before considering the nesting strategy for future upgrades.
|
||||
|
||||
### Gotchas
|
||||
|
||||
This setup is unusual, so it has a few gotchas.
|
||||
|
||||
* Don't add `package.json` to the `src/shared` folder. For example, if you want to use an npm React component inside `src/shared`, you should add it to both `src/modern/package.json` and `src/legacy/package.json` instead. You can use different versions of it but make sure your code works with both of them — and that it works with both Reacts!
|
||||
* Don't use React outside of the `src/modern`, `src/legacy`, or `src/shared`. Don't add React-related libraries outside of `src/modern/package.json` or `src/legacy/package.json`.
|
||||
* Remember that `src/shared` is where you write shared components, but the files you write there are automatically copied into `src/modern/shared` and `src/legacy/shared`, **from which you should import them**. Both of the target directories are in `.gitignore`. Importing directly from `src/shared` **will not work** because it is ambiguous what `react` refers to in that folder.
|
||||
* Keep in mind that any code in `src/shared` gets duplicated between the legacy and the modern bundles. Code that should not be duplicated needs to be anywhere else in `src` (but you can't use React there since the version is ambiguous).
|
||||
* You'll want to exclude `src/*/node_modules` from your linter's configuration, as this demo does in `.eslintignorerc`.
|
||||
|
||||
This setup is complicated, and we don't recommend it for most apps. However, we believe it is important to offer it as an option for apps that would otherwise get left behind. There might be ways to simplify it with a layer of tooling, but this example is intentionally showing the low-level mechanism that other tools may build on.
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "react-nesting-example",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react-scripts": "3.4.1",
|
||||
"redux": "^4.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "run-p install:*",
|
||||
"install:legacy": "cd src/legacy && npm install",
|
||||
"install:modern": "cd src/modern && npm install",
|
||||
"copy:legacy": "cpx 'src/shared/**' 'src/legacy/shared/'",
|
||||
"copy:modern": "cpx 'src/shared/**' 'src/modern/shared/'",
|
||||
"watch:legacy": "cpx 'src/shared/**' 'src/legacy/shared/' --watch --no-initial",
|
||||
"watch:modern": "cpx 'src/shared/**' 'src/modern/shared/' --watch --no-initial",
|
||||
"prebuild": "run-p copy:*",
|
||||
"prestart": "run-p copy:*",
|
||||
"start": "run-p start-app watch:*",
|
||||
"start-app": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
import './modern/index';
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Component} from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {connect} from 'react-redux';
|
||||
import {store} from '../store';
|
||||
|
||||
import ThemeContext from './shared/ThemeContext';
|
||||
import Clock from './shared/Clock';
|
||||
|
||||
store.subscribe(() => {
|
||||
console.log('Counter:', store.getState());
|
||||
});
|
||||
|
||||
class AboutSection extends Component {
|
||||
componentDidMount() {
|
||||
// The modern app is wrapped in StrictMode,
|
||||
// but the legacy bits can still use old APIs.
|
||||
findDOMNode(this);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => (
|
||||
<div style={{border: '1px dashed black', padding: 20}}>
|
||||
<h3>src/legacy/Greeting.js</h3>
|
||||
<h4 style={{color: theme}}>
|
||||
This component is rendered by the nested React ({React.version}).
|
||||
</h4>
|
||||
<Clock />
|
||||
<p>
|
||||
Counter: {this.props.counter}{' '}
|
||||
<button onClick={() => this.props.dispatch({type: 'increment'})}>
|
||||
+
|
||||
</button>
|
||||
</p>
|
||||
<b>
|
||||
<Link to="/">Go to Home</Link>
|
||||
</b>
|
||||
</div>
|
||||
)}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {counter: state};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AboutSection);
|
||||
@@ -1,43 +0,0 @@
|
||||
/* eslint-disable react/jsx-pascal-case */
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ThemeContext from './shared/ThemeContext';
|
||||
|
||||
// Note: this is a semi-private API, but it's ok to use it
|
||||
// if we never inspect the values, and only pass them through.
|
||||
import {__RouterContext} from 'react-router';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
// Pass through every context required by this tree.
|
||||
// The context object is populated in src/modern/withLegacyRoot.
|
||||
function Bridge({children, context}) {
|
||||
return (
|
||||
<ThemeContext.Provider value={context.theme}>
|
||||
<__RouterContext.Provider value={context.router}>
|
||||
{/*
|
||||
If we used the newer react-redux@7.x in the legacy/package.json,
|
||||
we woud instead import {ReactReduxContext} from 'react-redux'
|
||||
and render <ReactReduxContext.Provider value={context.reactRedux}>.
|
||||
*/}
|
||||
<Provider store={context.reactRedux.store}>{children}</Provider>
|
||||
</__RouterContext.Provider>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function createLegacyRoot(container) {
|
||||
return {
|
||||
render(Component, props, context) {
|
||||
ReactDOM.render(
|
||||
<Bridge context={context}>
|
||||
<Component {...props} />
|
||||
</Bridge>,
|
||||
container
|
||||
);
|
||||
},
|
||||
unmount() {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "react-nesting-example-legacy",
|
||||
"dependencies": {
|
||||
"react": "16.8",
|
||||
"react-dom": "16.8",
|
||||
"react-redux": "4.4.10",
|
||||
"react-router-dom": "5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import ThemeContext from './shared/ThemeContext';
|
||||
import lazyLegacyRoot from './lazyLegacyRoot';
|
||||
|
||||
// Lazy-load a component from the bundle using legacy React.
|
||||
const Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));
|
||||
|
||||
function AboutPage({counter, dispatch}) {
|
||||
const theme = useContext(ThemeContext);
|
||||
return (
|
||||
<>
|
||||
<h2>src/modern/AboutPage.js</h2>
|
||||
<h3 style={{color: theme}}>
|
||||
This component is rendered by the outer React ({React.version}).
|
||||
</h3>
|
||||
<Greeting />
|
||||
<br />
|
||||
<p>
|
||||
Counter: {counter}{' '}
|
||||
<button onClick={() => dispatch({type: 'increment'})}>+</button>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {counter: state};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AboutPage);
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import {useState, Suspense} from 'react';
|
||||
import {BrowserRouter, Switch, Route} from 'react-router-dom';
|
||||
|
||||
import HomePage from './HomePage';
|
||||
import AboutPage from './AboutPage';
|
||||
import ThemeContext from './shared/ThemeContext';
|
||||
|
||||
export default function App() {
|
||||
const [theme, setTheme] = useState('slategrey');
|
||||
|
||||
function handleToggleClick() {
|
||||
if (theme === 'slategrey') {
|
||||
setTheme('hotpink');
|
||||
} else {
|
||||
setTheme('slategrey');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<div style={{fontFamily: 'sans-serif'}}>
|
||||
<div
|
||||
style={{
|
||||
margin: 20,
|
||||
padding: 20,
|
||||
border: '1px solid black',
|
||||
minHeight: 300,
|
||||
}}>
|
||||
<button onClick={handleToggleClick}>Toggle Theme Context</button>
|
||||
<br />
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Switch>
|
||||
<Route path="/about">
|
||||
<AboutPage />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<HomePage />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeContext.Provider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
function Spinner() {
|
||||
return null;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import ThemeContext from './shared/ThemeContext';
|
||||
import Clock from './shared/Clock';
|
||||
|
||||
export default function HomePage({counter, dispatch}) {
|
||||
const theme = useContext(ThemeContext);
|
||||
return (
|
||||
<>
|
||||
<h2>src/modern/HomePage.js</h2>
|
||||
<h3 style={{color: theme}}>
|
||||
This component is rendered by the outer React ({React.version}).
|
||||
</h3>
|
||||
<Clock />
|
||||
<b>
|
||||
<Link to="/about">Go to About</Link>
|
||||
</b>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import {StrictMode} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import App from './App';
|
||||
import {store} from '../store';
|
||||
|
||||
ReactDOM.render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
@@ -1,94 +0,0 @@
|
||||
import React from 'react';
|
||||
import {useContext, useMemo, useRef, useState, useLayoutEffect} from 'react';
|
||||
import {__RouterContext} from 'react-router';
|
||||
import {ReactReduxContext} from 'react-redux';
|
||||
|
||||
import ThemeContext from './shared/ThemeContext';
|
||||
|
||||
let rendererModule = {
|
||||
status: 'pending',
|
||||
promise: null,
|
||||
result: null,
|
||||
};
|
||||
|
||||
export default function lazyLegacyRoot(getLegacyComponent) {
|
||||
let componentModule = {
|
||||
status: 'pending',
|
||||
promise: null,
|
||||
result: null,
|
||||
};
|
||||
|
||||
return function Wrapper(props) {
|
||||
const createLegacyRoot = readModule(rendererModule, () =>
|
||||
import('../legacy/createLegacyRoot')
|
||||
).default;
|
||||
const Component = readModule(componentModule, getLegacyComponent).default;
|
||||
const containerRef = useRef(null);
|
||||
const rootRef = useRef(null);
|
||||
|
||||
// Populate every contexts we want the legacy subtree to see.
|
||||
// Then in src/legacy/createLegacyRoot we will apply them.
|
||||
const theme = useContext(ThemeContext);
|
||||
const router = useContext(__RouterContext);
|
||||
const reactRedux = useContext(ReactReduxContext);
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
router,
|
||||
reactRedux,
|
||||
}),
|
||||
[theme, router, reactRedux]
|
||||
);
|
||||
|
||||
// Create/unmount.
|
||||
useLayoutEffect(() => {
|
||||
if (!rootRef.current) {
|
||||
rootRef.current = createLegacyRoot(containerRef.current);
|
||||
}
|
||||
const root = rootRef.current;
|
||||
return () => {
|
||||
root.unmount();
|
||||
};
|
||||
}, [createLegacyRoot]);
|
||||
|
||||
// Mount/update.
|
||||
useLayoutEffect(() => {
|
||||
if (rootRef.current) {
|
||||
rootRef.current.render(Component, props, context);
|
||||
}
|
||||
}, [Component, props, context]);
|
||||
|
||||
return <div style={{display: 'contents'}} ref={containerRef} />;
|
||||
};
|
||||
}
|
||||
|
||||
// This is similar to React.lazy, but implemented manually.
|
||||
// We use this to Suspend rendering of this component until
|
||||
// we fetch the component and the legacy React to render it.
|
||||
function readModule(record, createPromise) {
|
||||
if (record.status === 'fulfilled') {
|
||||
return record.result;
|
||||
}
|
||||
if (record.status === 'rejected') {
|
||||
throw record.result;
|
||||
}
|
||||
if (!record.promise) {
|
||||
record.promise = createPromise().then(
|
||||
value => {
|
||||
if (record.status === 'pending') {
|
||||
record.status = 'fulfilled';
|
||||
record.promise = null;
|
||||
record.result = value;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if (record.status === 'pending') {
|
||||
record.status = 'rejected';
|
||||
record.promise = null;
|
||||
record.result = error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
throw record.promise;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "react-nesting-example-modern",
|
||||
"dependencies": {
|
||||
"react": "0.0.0-3d0895557",
|
||||
"react-dom": "0.0.0-3d0895557",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router-dom": "5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import useTime from './useTime';
|
||||
|
||||
export default function Clock() {
|
||||
const time = useTime();
|
||||
return <p>Time: {time}</p>;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createContext} from 'react';
|
||||
|
||||
const ThemeContext = createContext(null);
|
||||
|
||||
export default ThemeContext;
|
||||
@@ -1,12 +0,0 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
export default function useTimer() {
|
||||
const [value, setValue] = useState(() => new Date());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setValue(new Date());
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return value.toLocaleTimeString();
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import {createStore} from 'redux';
|
||||
|
||||
function reducer(state = 0, action) {
|
||||
switch (action.type) {
|
||||
case 'increment':
|
||||
return state + 1;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Because this file is declared above both Modern and Legacy folders,
|
||||
// we can import this from either folder without duplicating the object.
|
||||
export const store = createStore(reducer);
|
||||
@@ -28,9 +28,12 @@ export default class Chrome extends Component {
|
||||
<div>
|
||||
<ThemeToggleButton
|
||||
onChange={theme => {
|
||||
React.startTransition(() => {
|
||||
this.setState({theme});
|
||||
});
|
||||
React.unstable_withSuspenseConfig(
|
||||
() => {
|
||||
this.setState({theme});
|
||||
},
|
||||
{timeoutMs: 6000}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
65
package.json
65
package.json
@@ -4,36 +4,35 @@
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.10.5",
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/eslint-parser": "^7.11.4",
|
||||
"@babel/helper-module-imports": "^7.10.4",
|
||||
"@babel/parser": "^7.11.3",
|
||||
"@babel/plugin-external-helpers": "^7.10.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-jsx": "^7.10.4",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.10.4",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.10.4",
|
||||
"@babel/plugin-transform-block-scoped-functions": "^7.10.4",
|
||||
"@babel/plugin-transform-block-scoping": "^7.11.1",
|
||||
"@babel/plugin-transform-classes": "^7.10.4",
|
||||
"@babel/plugin-transform-computed-properties": "^7.10.4",
|
||||
"@babel/plugin-transform-destructuring": "^7.10.4",
|
||||
"@babel/plugin-transform-for-of": "^7.10.4",
|
||||
"@babel/plugin-transform-literals": "^7.10.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
|
||||
"@babel/plugin-transform-object-super": "^7.10.4",
|
||||
"@babel/plugin-transform-parameters": "^7.10.5",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.10.4",
|
||||
"@babel/plugin-transform-spread": "^7.11.0",
|
||||
"@babel/plugin-transform-template-literals": "^7.10.5",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@babel/traverse": "^7.11.0",
|
||||
"@babel/cli": "^7.8.0",
|
||||
"@babel/code-frame": "^7.8.0",
|
||||
"@babel/core": "^7.8.0",
|
||||
"@babel/helper-module-imports": "^7.8.0",
|
||||
"@babel/parser": "^7.8.0",
|
||||
"@babel/plugin-external-helpers": "^7.8.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.8.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.8.0",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.8.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.8.0",
|
||||
"@babel/plugin-transform-block-scoped-functions": "^7.8.0",
|
||||
"@babel/plugin-transform-block-scoping": "^7.8.0",
|
||||
"@babel/plugin-transform-classes": "^7.8.0",
|
||||
"@babel/plugin-transform-computed-properties": "^7.8.0",
|
||||
"@babel/plugin-transform-destructuring": "^7.8.0",
|
||||
"@babel/plugin-transform-for-of": "^7.8.0",
|
||||
"@babel/plugin-transform-literals": "^7.8.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.8.0",
|
||||
"@babel/plugin-transform-object-super": "^7.8.0",
|
||||
"@babel/plugin-transform-parameters": "^7.8.0",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.8.0",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.8.0",
|
||||
"@babel/plugin-transform-spread": "^7.8.0",
|
||||
"@babel/plugin-transform-template-literals": "^7.8.0",
|
||||
"@babel/preset-flow": "^7.8.0",
|
||||
"@babel/preset-react": "^7.8.0",
|
||||
"@babel/traverse": "^7.8.0",
|
||||
"@mattiasbuelens/web-streams-polyfill": "^0.3.2",
|
||||
"art": "0.10.1",
|
||||
"babel-eslint": "^10.0.3",
|
||||
@@ -47,14 +46,13 @@
|
||||
"create-react-class": "^15.6.3",
|
||||
"danger": "^9.2.10",
|
||||
"error-stack-parser": "^2.0.6",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-config-fbjs": "^1.1.1",
|
||||
"eslint-config-prettier": "^6.9.0",
|
||||
"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-no-function-declare-after-return": "^1.0.0",
|
||||
"eslint-plugin-react": "^6.7.1",
|
||||
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
|
||||
"fbjs-scripts": "1.2.0",
|
||||
@@ -68,7 +66,6 @@
|
||||
"jest": "^25.2.7",
|
||||
"jest-cli": "^25.2.7",
|
||||
"jest-diff": "^25.2.6",
|
||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||
"jest-snapshot-serializer-raw": "^1.1.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimist": "^1.2.3",
|
||||
@@ -104,7 +101,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./scripts/rollup/build.js",
|
||||
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh --type=NODE",
|
||||
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer --type=NODE",
|
||||
"linc": "node ./scripts/tasks/linc.js",
|
||||
"lint": "node ./scripts/tasks/eslint.js",
|
||||
"lint-build": "node ./scripts/rollup/validate/index.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "create-subscription",
|
||||
"description": "utility for subscribing to external data sources inside React components",
|
||||
"version": "17.0.0",
|
||||
"version": "16.13.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
@@ -15,7 +15,7 @@
|
||||
"cjs/"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0"
|
||||
"react": "^16.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rxjs": "^5.5.6"
|
||||
|
||||
@@ -234,10 +234,6 @@ export function blur({relatedTarget} = {}) {
|
||||
return new FocusEvent('blur', {relatedTarget});
|
||||
}
|
||||
|
||||
export function focusOut({relatedTarget} = {}) {
|
||||
return new FocusEvent('focusout', {relatedTarget, bubbles: true});
|
||||
}
|
||||
|
||||
export function click(payload) {
|
||||
return createMouseEvent('click', {
|
||||
button: buttonType.primary,
|
||||
@@ -263,10 +259,6 @@ export function focus({relatedTarget} = {}) {
|
||||
return new FocusEvent('focus', {relatedTarget});
|
||||
}
|
||||
|
||||
export function focusIn({relatedTarget} = {}) {
|
||||
return new FocusEvent('focusin', {relatedTarget, bubbles: true});
|
||||
}
|
||||
|
||||
export function scroll() {
|
||||
return createEvent('scroll');
|
||||
}
|
||||
|
||||
@@ -22,14 +22,12 @@ const createEventTarget = node => ({
|
||||
*/
|
||||
blur(payload) {
|
||||
node.dispatchEvent(domEvents.blur(payload));
|
||||
node.dispatchEvent(domEvents.focusOut(payload));
|
||||
},
|
||||
click(payload) {
|
||||
node.dispatchEvent(domEvents.click(payload));
|
||||
},
|
||||
focus(payload) {
|
||||
node.dispatchEvent(domEvents.focus(payload));
|
||||
node.dispatchEvent(domEvents.focusIn(payload));
|
||||
node.focus();
|
||||
},
|
||||
keydown(payload) {
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
## 4.1.2
|
||||
* Fix a crash with the TypeScript 4.x parser. ([@eps1lon](https://github.com/eps1lon) in [#19815](https://github.com/facebook/react/pull/19815))
|
||||
|
||||
## 4.1.1
|
||||
* Improve support for optional chaining. ([@pfongkye](https://github.com/pfongkye) in [#19680](https://github.com/facebook/react/pull/19680))
|
||||
* Fix a false positive warning for TypeScript parameters. ([@NeoRaider](https://github.com/NeoRaider) in [#19751](https://github.com/facebook/react/pull/19751))
|
||||
|
||||
## 4.1.0
|
||||
* **New Violations:** Warn when dependencies change on every render. ([@captbaritone](https://github.com/captbaritone) in [#19590](https://github.com/facebook/react/pull/19590))
|
||||
|
||||
## 4.0.8
|
||||
* Fixes TypeScript `typeof` annotation to not be considered a dependency. ([@delca85](https://github.com/delca85) in [#19316](https://github.com/facebook/react/pull/19316))
|
||||
|
||||
## 4.0.7
|
||||
* Fixes an overly coarse dependency suggestion. ([@gaearon](https://github.com/gaearon) in [#19313](https://github.com/facebook/react/pull/19313))
|
||||
|
||||
## 4.0.6
|
||||
* Fix crashes and other bugs related to optional chaining. ([@gaearon](https://github.com/gaearon) in [#19273](https://github.com/facebook/react/pull/19273) and [#19275](https://github.com/facebook/react/pull/19275))
|
||||
|
||||
## 4.0.5
|
||||
* Fix a crash when the dependency array has an empty element. ([@yeonjuan](https://github.com/yeonjuan) in [#19145](https://github.com/facebook/react/pull/19145))
|
||||
* Fix a false positive warning that occurs with optional chaining. ([@fredvollmer](https://github.com/fredvollmer) in [#19061](https://github.com/facebook/react/pull/19061))
|
||||
|
||||
## 4.0.4
|
||||
* Fix a false positive warning that occurs with optional chaining. ([@fredvollmer](https://github.com/fredvollmer) in [#19061](https://github.com/facebook/react/pull/19061))
|
||||
* Support nullish coalescing and optional chaining. ([@yanneves](https://github.com/yanneves) in [#19008](https://github.com/facebook/react/pull/19008))
|
||||
|
||||
## 4.0.3
|
||||
* Remove the heuristic that checks all Hooks ending with `Effect` due to too many false positives. ([@gaearon](https://github.com/gaearon) in [#19004](https://github.com/facebook/react/pull/19004))
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -752,7 +752,9 @@ const tests = {
|
||||
errors: [
|
||||
conditionalError('useState'),
|
||||
conditionalError('useState'),
|
||||
conditionalError('useState'),
|
||||
// TODO: ideally this *should* warn, but ESLint
|
||||
// doesn't plan full support for ?? until it advances.
|
||||
// conditionalError('useState'),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "4.2.0",
|
||||
"version": "4.0.5",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
@@ -32,8 +32,6 @@
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser-v2": "npm:@typescript-eslint/parser@^2.26.0",
|
||||
"@typescript-eslint/parser-v3": "npm:@typescript-eslint/parser@^3.10.0",
|
||||
"@typescript-eslint/parser-v4": "npm:@typescript-eslint/parser@^4.1.0"
|
||||
"@typescript-eslint/parser": "^2.26.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"jest": "^23.0.1 || ^24.0.0 || ^25.1.0",
|
||||
"scheduler": "^0.20.0"
|
||||
"scheduler": "^0.15.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jest-react",
|
||||
"version": "0.12.0",
|
||||
"version": "0.11.1",
|
||||
"description": "Jest matchers and utilities for testing React components.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -20,8 +20,8 @@
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"jest": "^23.0.1 || ^24.0.0 || ^25.1.0",
|
||||
"react": "^17.0.0",
|
||||
"react-test-renderer": "^17.0.0"
|
||||
"react": "^16.0.0",
|
||||
"react-test-renderer": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react-art",
|
||||
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
|
||||
"version": "17.0.0",
|
||||
"version": "16.13.1",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,10 +26,10 @@
|
||||
"create-react-class": "^15.6.2",
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"scheduler": "^0.20.0"
|
||||
"scheduler": "^0.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "17.0.0"
|
||||
"react": "^16.13.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
|
||||
24
packages/react-art/src/ReactARTHostConfig.js
vendored
24
packages/react-art/src/ReactARTHostConfig.js
vendored
@@ -10,6 +10,10 @@ import Mode from 'art/modes/current';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
|
||||
import type {
|
||||
ReactEventResponder,
|
||||
ReactEventResponderInstance,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
const pooledTransform = new Transform();
|
||||
|
||||
@@ -425,6 +429,22 @@ export function clearContainer(container) {
|
||||
// TODO Implement this
|
||||
}
|
||||
|
||||
export function DEPRECATED_mountResponderInstance(
|
||||
responder: ReactEventResponder<any, any>,
|
||||
responderInstance: ReactEventResponderInstance<any, any>,
|
||||
props: Object,
|
||||
state: Object,
|
||||
instance: Object,
|
||||
) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function DEPRECATED_unmountResponderInstance(
|
||||
responderInstance: ReactEventResponderInstance<any, any>,
|
||||
): void {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function getFundamentalComponentInstance(fundamentalInstance) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
@@ -449,6 +469,10 @@ export function getInstanceFromNode(node) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function removeInstanceEventHandles(instance) {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function isOpaqueHydratingObject(value: mixed): boolean {
|
||||
throw new Error('Not yet implemented');
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
"umd/"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0"
|
||||
"react": "^16.3.0-alpha.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0"
|
||||
"react": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0"
|
||||
"react": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"error-stack-parser": "^2.0.2",
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
ReactProviderType,
|
||||
ReactEventResponder,
|
||||
ReactEventResponderListener,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {
|
||||
Fiber,
|
||||
@@ -20,6 +22,7 @@ import type {
|
||||
} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
|
||||
|
||||
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
|
||||
import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';
|
||||
|
||||
import ErrorStackParser from 'error-stack-parser';
|
||||
@@ -61,6 +64,10 @@ type Hook = {
|
||||
next: Hook | null,
|
||||
};
|
||||
|
||||
type TimeoutConfig = {|
|
||||
timeoutMs: number,
|
||||
|};
|
||||
|
||||
function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||
// This initializes a cache of all primitive hooks so that the top
|
||||
// most stack frames added by calling the primitive hook can be removed.
|
||||
@@ -79,6 +86,16 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||
Dispatcher.useDebugValue(null);
|
||||
Dispatcher.useCallback(() => {});
|
||||
Dispatcher.useMemo(() => null);
|
||||
Dispatcher.useMutableSource(
|
||||
{
|
||||
_source: {},
|
||||
_getVersion: () => 1,
|
||||
_workInProgressVersionPrimary: null,
|
||||
_workInProgressVersionSecondary: null,
|
||||
},
|
||||
() => null,
|
||||
() => () => {},
|
||||
);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
@@ -253,7 +270,25 @@ function useMutableSource<Source, Snapshot>(
|
||||
return value;
|
||||
}
|
||||
|
||||
function useTransition(): [(() => void) => void, boolean] {
|
||||
function useResponder(
|
||||
responder: ReactEventResponder<any, any>,
|
||||
listenerProps: Object,
|
||||
): ReactEventResponderListener<any, any> {
|
||||
// Don't put the actual event responder object in, just its displayName
|
||||
const value = {
|
||||
responder: responder.displayName || 'EventResponder',
|
||||
props: listenerProps,
|
||||
};
|
||||
hookLog.push({primitive: 'Responder', stackError: new Error(), value});
|
||||
return {
|
||||
responder,
|
||||
props: listenerProps,
|
||||
};
|
||||
}
|
||||
|
||||
function useTransition(
|
||||
config: SuspenseConfig | null | void,
|
||||
): [(() => void) => void, boolean] {
|
||||
// useTransition() composes multiple hooks internally.
|
||||
// Advance the current hook index the same number of times
|
||||
// so that subsequent hooks have the right memoized state.
|
||||
@@ -262,12 +297,12 @@ function useTransition(): [(() => void) => void, boolean] {
|
||||
hookLog.push({
|
||||
primitive: 'Transition',
|
||||
stackError: new Error(),
|
||||
value: undefined,
|
||||
value: config,
|
||||
});
|
||||
return [callback => {}, false];
|
||||
}
|
||||
|
||||
function useDeferredValue<T>(value: T): T {
|
||||
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
|
||||
// useDeferredValue() composes multiple hooks internally.
|
||||
// Advance the current hook index the same number of times
|
||||
// so that subsequent hooks have the right memoized state.
|
||||
@@ -310,6 +345,7 @@ const Dispatcher: DispatcherType = {
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
useResponder,
|
||||
useTransition,
|
||||
useMutableSource,
|
||||
useDeferredValue,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDebugTools;
|
||||
|
||||
describe('ReactHooksInspection', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
|
||||
React = require('react');
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should inspect a simple useResponder hook', () => {
|
||||
const TestResponder = React.DEPRECATED_createResponder('TestResponder', {});
|
||||
|
||||
function Foo(props) {
|
||||
const listener = React.DEPRECATED_useResponder(TestResponder, {
|
||||
preventDefault: false,
|
||||
});
|
||||
return <div DEPRECATED_flareListeners={listener}>Hello world</div>;
|
||||
}
|
||||
const tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
isStateEditable: false,
|
||||
id: 0,
|
||||
name: 'Responder',
|
||||
value: {props: {preventDefault: false}, responder: 'TestResponder'},
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -284,7 +284,7 @@ describe('ReactHooksInspection', () => {
|
||||
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
|
||||
'2. You might be breaking the Rules of Hooks\n' +
|
||||
'3. You might have more than one copy of React in the same app\n' +
|
||||
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
);
|
||||
|
||||
expect(getterCalls).toBe(1);
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
act = ReactTestRenderer.unstable_concurrentAct;
|
||||
act = ReactTestRenderer.act;
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
@@ -787,7 +787,7 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
|
||||
'2. You might be breaking the Rules of Hooks\n' +
|
||||
'3. You might have more than one copy of React in the same app\n' +
|
||||
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
);
|
||||
|
||||
expect(getterCalls).toBe(1);
|
||||
@@ -848,9 +848,9 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||
|
||||
if (__EXPERIMENTAL__) {
|
||||
it('should support composite useMutableSource hook', () => {
|
||||
const mutableSource = React.unstable_createMutableSource({}, () => 1);
|
||||
const mutableSource = React.createMutableSource({}, () => 1);
|
||||
function Foo(props) {
|
||||
React.unstable_useMutableSource(
|
||||
React.useMutableSource(
|
||||
mutableSource,
|
||||
() => 'snapshot',
|
||||
() => {},
|
||||
|
||||
@@ -21,7 +21,6 @@ Be sure to run this function *before* importing e.g. `react`, `react-dom`, `reac
|
||||
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.
|
||||
* `useHttps: boolean` (defaults to `false`) - Websocked should use a secure protocol (wss).
|
||||
* `websocket: Websocket` - Custom websocket to use. Overrides `host` and `port` settings if provided.
|
||||
* `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.
|
||||
@@ -39,24 +38,6 @@ require("react-devtools-core/standalone")
|
||||
.startServer(port);
|
||||
```
|
||||
|
||||
Renders DevTools interface into a DOM node over SSL using a custom host name (Default is localhost).
|
||||
|
||||
```js
|
||||
const host = 'dev.server.com';
|
||||
const options = {
|
||||
key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
|
||||
cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
|
||||
};
|
||||
|
||||
|
||||
require("react-devtools-core/standalone")
|
||||
.setContentDOMNode(document.getElementById("container"))
|
||||
.setStatusListener(status => {
|
||||
// This callback is optional...
|
||||
})
|
||||
.startServer(port, host, options);
|
||||
```
|
||||
|
||||
Reference the `react-devtools` package for a complete integration example.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "4.8.2",
|
||||
"version": "4.7.0",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
||||
5
packages/react-devtools-core/src/backend.js
vendored
5
packages/react-devtools-core/src/backend.js
vendored
@@ -24,7 +24,6 @@ type ConnectOptions = {
|
||||
host?: string,
|
||||
nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
|
||||
port?: number,
|
||||
useHttps?: boolean,
|
||||
resolveRNStyle?: ResolveNativeStyle,
|
||||
isAppActive?: () => boolean,
|
||||
websocket?: ?WebSocket,
|
||||
@@ -56,14 +55,12 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
const {
|
||||
host = 'localhost',
|
||||
nativeStyleEditorValidAttributes,
|
||||
useHttps = false,
|
||||
port = 8097,
|
||||
websocket,
|
||||
resolveRNStyle = null,
|
||||
isAppActive = () => true,
|
||||
} = options || {};
|
||||
|
||||
const protocol = useHttps ? 'wss' : 'ws';
|
||||
let retryTimeoutID: TimeoutID | null = null;
|
||||
|
||||
function scheduleRetry() {
|
||||
@@ -83,7 +80,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
let bridge: BackendBridge | null = null;
|
||||
|
||||
const messageListeners = [];
|
||||
const uri = protocol + '://' + host + ':' + port;
|
||||
const uri = 'ws://' + host + ':' + port;
|
||||
|
||||
// If existing websocket is passed, use it.
|
||||
// This is necessary to support our custom integrations.
|
||||
|
||||
20
packages/react-devtools-core/src/standalone.js
vendored
20
packages/react-devtools-core/src/standalone.js
vendored
@@ -242,20 +242,8 @@ function connectToSocket(socket: WebSocket) {
|
||||
};
|
||||
}
|
||||
|
||||
type ServerOptions = {
|
||||
key?: string,
|
||||
cert?: string,
|
||||
};
|
||||
|
||||
function startServer(
|
||||
port?: number = 8097,
|
||||
host?: string = 'localhost',
|
||||
httpsOptions?: ServerOptions,
|
||||
) {
|
||||
const useHttps = !!httpsOptions;
|
||||
const httpServer = useHttps
|
||||
? require('https').createServer(httpsOptions)
|
||||
: require('http').createServer();
|
||||
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) => {
|
||||
@@ -310,9 +298,7 @@ function startServer(
|
||||
'\n;' +
|
||||
backendFile.toString() +
|
||||
'\n;' +
|
||||
`ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
|
||||
useHttps ? 'true' : 'false'
|
||||
}});`,
|
||||
'ReactDevToolsBackend.connectToDevTools();',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,14 +17,9 @@ const __DEV__ = NODE_ENV === 'development';
|
||||
|
||||
const DEVTOOLS_VERSION = getVersionString();
|
||||
|
||||
// This targets RN/Hermes.
|
||||
process.env.BABEL_CONFIG_ADDITIONAL_TARGETS = JSON.stringify({
|
||||
ie: '11',
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ const DEVTOOLS_VERSION = getVersionString();
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
|
||||
target: 'electron-main',
|
||||
entry: {
|
||||
standalone: './src/standalone.js',
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||
"version": "4.8.2",
|
||||
"version_name": "4.8.2",
|
||||
"version": "4.7.0",
|
||||
"version_name": "4.7.0",
|
||||
|
||||
"minimum_chrome_version": "49",
|
||||
|
||||
@@ -32,14 +32,7 @@
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/renderer.js",
|
||||
"build/background.js.map",
|
||||
"build/contentScript.js.map",
|
||||
"build/injectGlobalHook.js.map",
|
||||
"build/main.js.map",
|
||||
"build/panel.js.map",
|
||||
"build/renderer.js.map",
|
||||
"build/react_devtools_backend.js.map"
|
||||
"build/renderer.js"
|
||||
],
|
||||
|
||||
"background": {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
|
||||
"version": "4.8.2",
|
||||
"version_name": "4.8.2",
|
||||
"version": "4.7.0",
|
||||
"version_name": "4.7.0",
|
||||
|
||||
"minimum_chrome_version": "49",
|
||||
|
||||
@@ -32,14 +32,7 @@
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/renderer.js",
|
||||
"build/background.js.map",
|
||||
"build/contentScript.js.map",
|
||||
"build/injectGlobalHook.js.map",
|
||||
"build/main.js.map",
|
||||
"build/panel.js.map",
|
||||
"build/renderer.js.map",
|
||||
"build/react_devtools_backend.js.map"
|
||||
"build/renderer.js"
|
||||
],
|
||||
|
||||
"background": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
||||
"version": "4.8.2",
|
||||
"version": "4.7.0",
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
@@ -37,14 +37,7 @@
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/renderer.js",
|
||||
"build/background.js.map",
|
||||
"build/contentScript.js.map",
|
||||
"build/injectGlobalHook.js.map",
|
||||
"build/main.js.map",
|
||||
"build/panel.js.map",
|
||||
"build/renderer.js.map",
|
||||
"build/react_devtools_backend.js.map"
|
||||
"build/renderer.js"
|
||||
],
|
||||
|
||||
"background": {
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"test:edge": "node ./edge/test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@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",
|
||||
@@ -44,8 +44,5 @@
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"web-ext": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const location = ln.href;
|
||||
ln.onclick = function() {
|
||||
chrome.tabs.create({active: true, url: location});
|
||||
return false;
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function setup(hook) {
|
||||
initBackend(hook, agent, window);
|
||||
|
||||
// Let the frontend know that the backend has attached listeners and is ready for messages.
|
||||
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
|
||||
// This covers the case of of syncing saved values after reloading/navigating while DevTools remain open.
|
||||
bridge.send('extensionBackendInitialized');
|
||||
|
||||
// Setup React Native style editor if a renderer like react-native-web has injected it.
|
||||
|
||||
@@ -98,7 +98,18 @@ chrome.runtime.onMessage.addListener((request, sender) => {
|
||||
// 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.
|
||||
setIconAndPopup(request.reactBuildType, sender.tab.id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -296,7 +296,7 @@ function createPanelIfReactLoaded() {
|
||||
let needsToSyncElementSelection = false;
|
||||
|
||||
chrome.devtools.panels.create(
|
||||
isChrome ? '⚛️ Components' : 'Components',
|
||||
isChrome ? '⚛ Components' : 'Components',
|
||||
'',
|
||||
'panel.html',
|
||||
extensionPanel => {
|
||||
@@ -326,7 +326,7 @@ function createPanelIfReactLoaded() {
|
||||
);
|
||||
|
||||
chrome.devtools.panels.create(
|
||||
isChrome ? '⚛️ Profiler' : 'Profiler',
|
||||
isChrome ? '⚛ Profiler' : 'Profiler',
|
||||
'',
|
||||
'panel.html',
|
||||
extensionPanel => {
|
||||
|
||||
7
packages/react-devtools-extensions/utils.js
vendored
7
packages/react-devtools-extensions/utils.js
vendored
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {execSync} = require('child_process');
|
||||
const {readFileSync} = require('fs');
|
||||
const {resolve} = require('path');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const {resolve} = require('path');
|
||||
const {DefinePlugin} = require('webpack');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const {GITHUB_URL, getVersionString} = require('./utils');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
@@ -18,7 +19,7 @@ const DEVTOOLS_VERSION = getVersionString();
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
},
|
||||
@@ -40,7 +41,14 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
compress: {drop_debugger: false},
|
||||
output: {comments: true},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
|
||||
@@ -18,7 +18,7 @@ const DEVTOOLS_VERSION = getVersionString();
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
|
||||
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
|
||||
entry: {
|
||||
background: './src/background.js',
|
||||
contentScript: './src/contentScript.js',
|
||||
@@ -44,9 +44,6 @@ module.exports = {
|
||||
scheduler: resolve(builtModulesDir, 'scheduler'),
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
__DEV__: false,
|
||||
|
||||
@@ -8,7 +8,7 @@ This is a low-level package. If you're looking for the standalone DevTools app,
|
||||
|
||||
This package exports two entry points: a frontend (to be run in the main `window`) and a backend (to be installed and run within an `iframe`<sup>1</sup>).
|
||||
|
||||
The frontend and backend can be initialized in any order, but **the backend must not be activated until the frontend initialization has completed**. Because of this, the simplest sequence is:
|
||||
The frontend and backend can be initialized in any order, but **the backend must not be activated until after the frontend has been initialized**. Because of this, the simplest sequence is:
|
||||
|
||||
1. Frontend (DevTools interface) initialized in the main `window`.
|
||||
1. Backend initialized in an `iframe`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "4.8.2",
|
||||
"version": "4.7.0",
|
||||
"description": "Embed react-devtools within a website",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
@@ -22,13 +22,13 @@
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@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-loader": "^8.0.4",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const {resolve} = require('path');
|
||||
const {DefinePlugin} = require('webpack');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const {
|
||||
GITHUB_URL,
|
||||
getVersionString,
|
||||
@@ -17,7 +18,7 @@ const DEVTOOLS_VERSION = getVersionString();
|
||||
|
||||
module.exports = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'eval-cheap-source-map' : 'source-map',
|
||||
devtool: false,
|
||||
entry: {
|
||||
backend: './src/backend.js',
|
||||
frontend: './src/frontend.js',
|
||||
@@ -37,7 +38,14 @@ module.exports = {
|
||||
scheduler: 'scheduler',
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
compress: {drop_debugger: false},
|
||||
output: {comments: true},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Experimental React Concurrent Mode Profiler
|
||||
|
||||
https://react-devtools-scheduling-profiler.vercel.app/
|
||||
|
||||
## Setting up continuous deployment with CircleCI and Vercel
|
||||
|
||||
These instructions are intended for internal use, but may be useful if you are setting up a custom production deployment of the scheduling profiler.
|
||||
|
||||
1. Create a Vercel token at https://vercel.com/account/tokens.
|
||||
2. Configure CircleCI:
|
||||
1. In CircleCI, navigate to the repository's Project Settings.
|
||||
2. In the Advanced tab, ensure that "Pass secrets to builds from forked pull requests" is set to false.
|
||||
3. In the Environment Variables tab, add the Vercel token as a new `SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN` environment variable.
|
||||
|
||||
The Vercel project will be created when the deploy job runs.
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {execSync} = require('child_process');
|
||||
const {readFileSync} = require('fs');
|
||||
const {resolve} = require('path');
|
||||
|
||||
function getGitCommit() {
|
||||
try {
|
||||
return execSync('git show -s --format=%h')
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (error) {
|
||||
// Mozilla runs this command from a git archive.
|
||||
// In that context, there is no Git revision.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getVersionString() {
|
||||
const packageVersion = JSON.parse(
|
||||
readFileSync(resolve(__dirname, './package.json')),
|
||||
).version;
|
||||
|
||||
const commit = getGitCommit();
|
||||
|
||||
return `${packageVersion}-${commit}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getVersionString,
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "react-devtools-scheduling-profiler",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production cross-env TARGET=remote webpack --config webpack.config.js",
|
||||
"start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elg/speedscope": "1.9.0-a6f84db",
|
||||
"clipboard-js": "^0.3.6",
|
||||
"memoize-one": "^5.1.1",
|
||||
"nullthrows": "^1.1.1",
|
||||
"pretty-ms": "^7.0.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"regenerator-runtime": "^0.13.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.1",
|
||||
"@reach/menu-button": "^0.11.2",
|
||||
"@reach/tooltip": "^0.11.2",
|
||||
"babel-loader": "^8.1.0",
|
||||
"css-loader": "^4.2.1",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"style-loader": "^1.2.1",
|
||||
"url-loader": "^4.1.0",
|
||||
"vercel": "^20.1.0",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"worker-loader": "^3.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.DevTools {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.TabContent {
|
||||
flex: 1 1 100%;
|
||||
overflow: auto;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.DevTools, .DevTools * {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: var(--font-smoothing);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Reach styles need to come before any component styles.
|
||||
// This makes overriding the styles simpler.
|
||||
import '@reach/menu-button/styles.css';
|
||||
import '@reach/tooltip/styles.css';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {SchedulingProfiler} from './SchedulingProfiler';
|
||||
import {useBrowserTheme, useDisplayDensity} from './hooks';
|
||||
|
||||
import styles from './App.css';
|
||||
import 'react-devtools-shared/src/devtools/views/root.css';
|
||||
|
||||
export default function App() {
|
||||
useBrowserTheme();
|
||||
useDisplayDensity();
|
||||
|
||||
return (
|
||||
<div className={styles.DevTools}>
|
||||
<div className={styles.TabContent}>
|
||||
<SchedulingProfiler />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.CanvasPage {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
/**
|
||||
* 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 type {
|
||||
Point,
|
||||
HorizontalPanAndZoomViewOnChangeCallback,
|
||||
} from './view-base';
|
||||
import type {
|
||||
ReactHoverContextInfo,
|
||||
ReactProfilerData,
|
||||
ReactMeasure,
|
||||
} from './types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {copy} from 'clipboard-js';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
import {
|
||||
HorizontalPanAndZoomView,
|
||||
ResizableSplitView,
|
||||
Surface,
|
||||
VerticalScrollView,
|
||||
View,
|
||||
createComposedLayout,
|
||||
lastViewTakesUpRemainingSpaceLayout,
|
||||
useCanvasInteraction,
|
||||
verticallyStackedLayout,
|
||||
zeroPoint,
|
||||
} from './view-base';
|
||||
import {
|
||||
FlamechartView,
|
||||
ReactEventsView,
|
||||
ReactMeasuresView,
|
||||
TimeAxisMarkersView,
|
||||
UserTimingMarksView,
|
||||
} from './content-views';
|
||||
import {COLORS} from './content-views/constants';
|
||||
|
||||
import EventTooltip from './EventTooltip';
|
||||
import ContextMenu from './context/ContextMenu';
|
||||
import ContextMenuItem from './context/ContextMenuItem';
|
||||
import useContextMenu from './context/useContextMenu';
|
||||
import {getBatchRange} from './utils/getBatchRange';
|
||||
|
||||
import styles from './CanvasPage.css';
|
||||
|
||||
const CONTEXT_MENU_ID = 'canvas';
|
||||
|
||||
type Props = {|
|
||||
profilerData: ReactProfilerData,
|
||||
|};
|
||||
|
||||
function CanvasPage({profilerData}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={styles.CanvasPage}
|
||||
style={{backgroundColor: COLORS.BACKGROUND}}>
|
||||
<AutoSizer>
|
||||
{({height, width}: {height: number, width: number}) => (
|
||||
<AutoSizedCanvas data={profilerData} height={height} width={width} />
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
|
||||
const {batchUID, duration, timestamp, type} = measure;
|
||||
|
||||
const [startTime, stopTime] = getBatchRange(batchUID, data);
|
||||
|
||||
copy(
|
||||
JSON.stringify({
|
||||
type,
|
||||
timestamp: prettyMilliseconds(timestamp),
|
||||
duration: prettyMilliseconds(duration),
|
||||
batchDuration: prettyMilliseconds(stopTime - startTime),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const zoomToBatch = (
|
||||
data: ReactProfilerData,
|
||||
measure: ReactMeasure,
|
||||
syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
|
||||
) => {
|
||||
const {batchUID} = measure;
|
||||
const [startTime, stopTime] = getBatchRange(batchUID, data);
|
||||
syncedHorizontalPanAndZoomViews.forEach(syncedView =>
|
||||
// Using time as range works because the views' intrinsic content size is
|
||||
// based on time.
|
||||
syncedView.zoomToRange(startTime, stopTime),
|
||||
);
|
||||
};
|
||||
|
||||
type AutoSizedCanvasProps = {|
|
||||
data: ReactProfilerData,
|
||||
height: number,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
|
||||
const [mouseLocation, setMouseLocation] = useState<Point>(zeroPoint); // DOM coordinates
|
||||
const [
|
||||
hoveredEvent,
|
||||
setHoveredEvent,
|
||||
] = useState<ReactHoverContextInfo | null>(null);
|
||||
|
||||
const surfaceRef = useRef(new Surface());
|
||||
const userTimingMarksViewRef = useRef(null);
|
||||
const reactEventsViewRef = useRef(null);
|
||||
const reactMeasuresViewRef = useRef(null);
|
||||
const flamechartViewRef = useRef(null);
|
||||
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const surface = surfaceRef.current;
|
||||
const defaultFrame = {origin: zeroPoint, size: {width, height}};
|
||||
|
||||
// Clear synced views
|
||||
syncedHorizontalPanAndZoomViewsRef.current = [];
|
||||
|
||||
const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
|
||||
newState,
|
||||
triggeringView?: HorizontalPanAndZoomView,
|
||||
) => {
|
||||
syncedHorizontalPanAndZoomViewsRef.current.forEach(
|
||||
syncedView =>
|
||||
triggeringView !== syncedView && syncedView.setScrollState(newState),
|
||||
);
|
||||
};
|
||||
|
||||
// Top content
|
||||
|
||||
const topContentStack = new View(
|
||||
surface,
|
||||
defaultFrame,
|
||||
verticallyStackedLayout,
|
||||
);
|
||||
|
||||
const axisMarkersView = new TimeAxisMarkersView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data.duration,
|
||||
);
|
||||
topContentStack.addSubview(axisMarkersView);
|
||||
|
||||
if (data.otherUserTimingMarks.length > 0) {
|
||||
const userTimingMarksView = new UserTimingMarksView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data.otherUserTimingMarks,
|
||||
data.duration,
|
||||
);
|
||||
userTimingMarksViewRef.current = userTimingMarksView;
|
||||
topContentStack.addSubview(userTimingMarksView);
|
||||
}
|
||||
|
||||
const reactEventsView = new ReactEventsView(surface, defaultFrame, data);
|
||||
reactEventsViewRef.current = reactEventsView;
|
||||
topContentStack.addSubview(reactEventsView);
|
||||
|
||||
const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
topContentStack,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(
|
||||
topContentHorizontalPanAndZoomView,
|
||||
);
|
||||
|
||||
// Resizable content
|
||||
|
||||
const reactMeasuresView = new ReactMeasuresView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data,
|
||||
);
|
||||
reactMeasuresViewRef.current = reactMeasuresView;
|
||||
const reactMeasuresVerticalScrollView = new VerticalScrollView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
reactMeasuresView,
|
||||
);
|
||||
const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
reactMeasuresVerticalScrollView,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(
|
||||
reactMeasuresHorizontalPanAndZoomView,
|
||||
);
|
||||
|
||||
const flamechartView = new FlamechartView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data.flamechart,
|
||||
data.duration,
|
||||
);
|
||||
flamechartViewRef.current = flamechartView;
|
||||
const flamechartVerticalScrollView = new VerticalScrollView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
flamechartView,
|
||||
);
|
||||
const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
flamechartVerticalScrollView,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(
|
||||
flamechartHorizontalPanAndZoomView,
|
||||
);
|
||||
|
||||
const resizableContentStack = new ResizableSplitView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
reactMeasuresHorizontalPanAndZoomView,
|
||||
flamechartHorizontalPanAndZoomView,
|
||||
);
|
||||
|
||||
const rootView = new View(
|
||||
surface,
|
||||
defaultFrame,
|
||||
createComposedLayout(
|
||||
verticallyStackedLayout,
|
||||
lastViewTakesUpRemainingSpaceLayout,
|
||||
),
|
||||
);
|
||||
rootView.addSubview(topContentHorizontalPanAndZoomView);
|
||||
rootView.addSubview(resizableContentStack);
|
||||
|
||||
surfaceRef.current.rootView = rootView;
|
||||
}, [data]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
surfaceRef.current.setCanvas(canvasRef.current, {width, height});
|
||||
}
|
||||
}, [width, height]);
|
||||
|
||||
const interactor = useCallback(interaction => {
|
||||
if (canvasRef.current === null) {
|
||||
return;
|
||||
}
|
||||
surfaceRef.current.handleInteraction(interaction);
|
||||
// Defer drawing to canvas until React's commit phase, to avoid drawing
|
||||
// twice and to ensure that both the canvas and DOM elements managed by
|
||||
// React are in sync.
|
||||
setMouseLocation({
|
||||
x: interaction.payload.event.x,
|
||||
y: interaction.payload.event.y,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useCanvasInteraction(canvasRef, interactor);
|
||||
|
||||
useContextMenu({
|
||||
data: {
|
||||
data,
|
||||
hoveredEvent,
|
||||
},
|
||||
id: CONTEXT_MENU_ID,
|
||||
onChange: setIsContextMenuShown,
|
||||
ref: canvasRef,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const {current: userTimingMarksView} = userTimingMarksViewRef;
|
||||
if (userTimingMarksView) {
|
||||
userTimingMarksView.onHover = userTimingMark => {
|
||||
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
|
||||
setHoveredEvent({
|
||||
userTimingMark,
|
||||
event: null,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
data,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const {current: reactEventsView} = reactEventsViewRef;
|
||||
if (reactEventsView) {
|
||||
reactEventsView.onHover = event => {
|
||||
if (!hoveredEvent || hoveredEvent.event !== event) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
event,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
data,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const {current: reactMeasuresView} = reactMeasuresViewRef;
|
||||
if (reactMeasuresView) {
|
||||
reactMeasuresView.onHover = measure => {
|
||||
if (!hoveredEvent || hoveredEvent.measure !== measure) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
event: null,
|
||||
flamechartStackFrame: null,
|
||||
measure,
|
||||
data,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const {current: flamechartView} = flamechartViewRef;
|
||||
if (flamechartView) {
|
||||
flamechartView.setOnHover(flamechartStackFrame => {
|
||||
if (
|
||||
!hoveredEvent ||
|
||||
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
|
||||
) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
event: null,
|
||||
flamechartStackFrame,
|
||||
measure: null,
|
||||
data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [
|
||||
hoveredEvent,
|
||||
data, // Attach onHover callbacks when views are re-created on data change
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const {current: userTimingMarksView} = userTimingMarksViewRef;
|
||||
if (userTimingMarksView) {
|
||||
userTimingMarksView.setHoveredMark(
|
||||
hoveredEvent ? hoveredEvent.userTimingMark : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: reactEventsView} = reactEventsViewRef;
|
||||
if (reactEventsView) {
|
||||
reactEventsView.setHoveredEvent(hoveredEvent ? hoveredEvent.event : null);
|
||||
}
|
||||
|
||||
const {current: reactMeasuresView} = reactMeasuresViewRef;
|
||||
if (reactMeasuresView) {
|
||||
reactMeasuresView.setHoveredMeasure(
|
||||
hoveredEvent ? hoveredEvent.measure : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: flamechartView} = flamechartViewRef;
|
||||
if (flamechartView) {
|
||||
flamechartView.setHoveredFlamechartStackFrame(
|
||||
hoveredEvent ? hoveredEvent.flamechartStackFrame : null,
|
||||
);
|
||||
}
|
||||
}, [hoveredEvent]);
|
||||
|
||||
// Draw to canvas in React's commit phase
|
||||
useLayoutEffect(() => {
|
||||
surfaceRef.current.displayIfNeeded();
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<canvas ref={canvasRef} height={height} width={width} />
|
||||
<ContextMenu id={CONTEXT_MENU_ID}>
|
||||
{contextData => {
|
||||
if (contextData.hoveredEvent == null) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
event,
|
||||
flamechartStackFrame,
|
||||
measure,
|
||||
} = contextData.hoveredEvent;
|
||||
return (
|
||||
<Fragment>
|
||||
{event !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(event.componentName)}
|
||||
title="Copy component name">
|
||||
Copy component name
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{event !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(event.componentStack)}
|
||||
title="Copy component stack">
|
||||
Copy component stack
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{measure !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
zoomToBatch(
|
||||
contextData.data,
|
||||
measure,
|
||||
syncedHorizontalPanAndZoomViewsRef.current,
|
||||
)
|
||||
}
|
||||
title="Zoom to batch">
|
||||
Zoom to batch
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{measure !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copySummary(contextData.data, measure)}
|
||||
title="Copy summary">
|
||||
Copy summary
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{flamechartStackFrame !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(flamechartStackFrame.scriptUrl)}
|
||||
title="Copy file path">
|
||||
Copy file path
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{flamechartStackFrame !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
copy(
|
||||
`line ${flamechartStackFrame.locationLine ??
|
||||
''}, column ${flamechartStackFrame.locationColumn ??
|
||||
''}`,
|
||||
)
|
||||
}
|
||||
title="Copy location">
|
||||
Copy location
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
</ContextMenu>
|
||||
{!isContextMenuShown && (
|
||||
<EventTooltip
|
||||
data={data}
|
||||
hoveredEvent={hoveredEvent}
|
||||
origin={mouseLocation}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default CanvasPage;
|
||||
@@ -1,78 +0,0 @@
|
||||
.Tooltip {
|
||||
position: fixed;
|
||||
display: inline-block;
|
||||
border-radius: 0.125rem;
|
||||
max-width: 300px;
|
||||
padding: 0.25rem;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.Divider {
|
||||
height: 1px;
|
||||
background-color: #aaa;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.DetailsGrid {
|
||||
display: grid;
|
||||
padding-top: 5px;
|
||||
grid-gap: 2px 5px;
|
||||
grid-template-columns: min-content auto;
|
||||
}
|
||||
|
||||
.DetailsGridLabel {
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.DetailsGridURL {
|
||||
word-break: break-all;
|
||||
max-height: 50vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.FlamechartStackFrameName {
|
||||
word-break: break-word;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.ComponentName {
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.ComponentStack {
|
||||
overflow: hidden;
|
||||
max-width: 35em;
|
||||
max-height: 10em;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
180deg,
|
||||
#fff,
|
||||
#fff 5em,
|
||||
transparent
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
#fff,
|
||||
#fff 5em,
|
||||
transparent
|
||||
);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ReactMeasureLabel {
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.UserTimingLabel {
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/**
|
||||
* 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 type {Point} from './view-base';
|
||||
import type {
|
||||
FlamechartStackFrame,
|
||||
ReactEvent,
|
||||
ReactHoverContextInfo,
|
||||
ReactMeasure,
|
||||
ReactProfilerData,
|
||||
Return,
|
||||
UserTimingMark,
|
||||
} from './types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useRef} from 'react';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
import {COLORS} from './content-views/constants';
|
||||
import {getBatchRange} from './utils/getBatchRange';
|
||||
import useSmartTooltip from './utils/useSmartTooltip';
|
||||
import styles from './EventTooltip.css';
|
||||
|
||||
type Props = {|
|
||||
data: ReactProfilerData,
|
||||
hoveredEvent: ReactHoverContextInfo | null,
|
||||
origin: Point,
|
||||
|};
|
||||
|
||||
function formatTimestamp(ms) {
|
||||
return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms';
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3});
|
||||
}
|
||||
|
||||
function trimmedString(string: string, length: number): string {
|
||||
if (string.length > length) {
|
||||
return `${string.substr(0, length - 1)}…`;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
function getReactEventLabel(type): string | null {
|
||||
switch (type) {
|
||||
case 'schedule-render':
|
||||
return 'render scheduled';
|
||||
case 'schedule-state-update':
|
||||
return 'state update scheduled';
|
||||
case 'schedule-force-update':
|
||||
return 'force update scheduled';
|
||||
case 'suspense-suspend':
|
||||
return 'suspended';
|
||||
case 'suspense-resolved':
|
||||
return 'suspense resolved';
|
||||
case 'suspense-rejected':
|
||||
return 'suspense rejected';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getReactEventColor(event: ReactEvent): string | null {
|
||||
switch (event.type) {
|
||||
case 'schedule-render':
|
||||
return COLORS.REACT_SCHEDULE_HOVER;
|
||||
case 'schedule-state-update':
|
||||
case 'schedule-force-update':
|
||||
return event.isCascading
|
||||
? COLORS.REACT_SCHEDULE_CASCADING_HOVER
|
||||
: COLORS.REACT_SCHEDULE_HOVER;
|
||||
case 'suspense-suspend':
|
||||
case 'suspense-resolved':
|
||||
case 'suspense-rejected':
|
||||
return COLORS.REACT_SUSPEND_HOVER;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getReactMeasureLabel(type): string | null {
|
||||
switch (type) {
|
||||
case 'commit':
|
||||
return 'commit';
|
||||
case 'render-idle':
|
||||
return 'idle';
|
||||
case 'render':
|
||||
return 'render';
|
||||
case 'layout-effects':
|
||||
return 'layout effects';
|
||||
case 'passive-effects':
|
||||
return 'passive effects';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function EventTooltip({data, hoveredEvent, origin}: Props) {
|
||||
const tooltipRef = useSmartTooltip({
|
||||
mouseX: origin.x,
|
||||
mouseY: origin.y,
|
||||
});
|
||||
|
||||
if (hoveredEvent === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent;
|
||||
|
||||
if (event !== null) {
|
||||
return <TooltipReactEvent event={event} tooltipRef={tooltipRef} />;
|
||||
} else if (measure !== null) {
|
||||
return (
|
||||
<TooltipReactMeasure
|
||||
data={data}
|
||||
measure={measure}
|
||||
tooltipRef={tooltipRef}
|
||||
/>
|
||||
);
|
||||
} else if (flamechartStackFrame !== null) {
|
||||
return (
|
||||
<TooltipFlamechartNode
|
||||
stackFrame={flamechartStackFrame}
|
||||
tooltipRef={tooltipRef}
|
||||
/>
|
||||
);
|
||||
} else if (userTimingMark !== null) {
|
||||
return (
|
||||
<TooltipUserTimingMark mark={userTimingMark} tooltipRef={tooltipRef} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatComponentStack(componentStack: string): string {
|
||||
const lines = componentStack.split('\n').map(line => line.trim());
|
||||
lines.shift();
|
||||
|
||||
if (lines.length > 5) {
|
||||
return lines.slice(0, 5).join('\n') + '\n...';
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const TooltipFlamechartNode = ({
|
||||
stackFrame,
|
||||
tooltipRef,
|
||||
}: {
|
||||
stackFrame: FlamechartStackFrame,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const {
|
||||
name,
|
||||
timestamp,
|
||||
duration,
|
||||
scriptUrl,
|
||||
locationLine,
|
||||
locationColumn,
|
||||
} = stackFrame;
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
{formatDuration(duration)}
|
||||
<span className={styles.FlamechartStackFrameName}>{name}</span>
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
{scriptUrl && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Script URL:</div>
|
||||
<div className={styles.DetailsGridURL}>{scriptUrl}</div>
|
||||
</>
|
||||
)}
|
||||
{(locationLine !== undefined || locationColumn !== undefined) && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Location:</div>
|
||||
<div>
|
||||
line {locationLine}, column {locationColumn}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipReactEvent = ({
|
||||
event,
|
||||
tooltipRef,
|
||||
}: {
|
||||
event: ReactEvent,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const label = getReactEventLabel(event.type);
|
||||
const color = getReactEventColor(event);
|
||||
if (!label || !color) {
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected event type "%s"', event.type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const {componentName, componentStack, timestamp} = event;
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
{componentName && (
|
||||
<span className={styles.ComponentName} style={{color}}>
|
||||
{trimmedString(componentName, 768)}
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
{componentStack && (
|
||||
<Fragment>
|
||||
<div className={styles.DetailsGridLabel}>Component stack:</div>
|
||||
<pre className={styles.ComponentStack}>
|
||||
{formatComponentStack(componentStack)}
|
||||
</pre>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipReactMeasure = ({
|
||||
data,
|
||||
measure,
|
||||
tooltipRef,
|
||||
}: {
|
||||
data: ReactProfilerData,
|
||||
measure: ReactMeasure,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const label = getReactMeasureLabel(measure.type);
|
||||
if (!label) {
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected measure type "%s"', measure.type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const {batchUID, duration, timestamp, lanes} = measure;
|
||||
const [startTime, stopTime] = getBatchRange(batchUID, data);
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
{formatDuration(duration)}
|
||||
<span className={styles.ReactMeasureLabel}>{label}</span>
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
<div className={styles.DetailsGridLabel}>Batch duration:</div>
|
||||
<div>{formatDuration(stopTime - startTime)}</div>
|
||||
<div className={styles.DetailsGridLabel}>
|
||||
Lane{lanes.length === 1 ? '' : 's'}:
|
||||
</div>
|
||||
<div>{lanes.join(', ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipUserTimingMark = ({
|
||||
mark,
|
||||
tooltipRef,
|
||||
}: {
|
||||
mark: UserTimingMark,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const {name, timestamp} = mark;
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
<span className={styles.UserTimingLabel}>{name}</span>
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications
|
||||
*/
|
||||
.Input {
|
||||
position: absolute !important;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* 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 * as React from 'react';
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
import Button from 'react-devtools-shared/src/devtools/views/Button';
|
||||
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
|
||||
|
||||
import styles from './ImportButton.css';
|
||||
|
||||
type Props = {|
|
||||
onFileSelect: (file: File) => void,
|
||||
|};
|
||||
|
||||
export default function ImportButton({onFileSelect}: Props) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleFiles = useCallback(() => {
|
||||
const input = inputRef.current;
|
||||
if (input === null) {
|
||||
return;
|
||||
}
|
||||
if (input.files.length > 0) {
|
||||
onFileSelect(input.files[0]);
|
||||
}
|
||||
// Reset input element to allow the same file to be re-imported
|
||||
input.value = '';
|
||||
}, [onFileSelect]);
|
||||
|
||||
const uploadData = useCallback(() => {
|
||||
if (inputRef.current !== null) {
|
||||
inputRef.current.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={styles.Input}
|
||||
type="file"
|
||||
onChange={handleFiles}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<Button onClick={uploadData} title="Load profile...">
|
||||
<ButtonIcon type="import" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
.SchedulingProfiler {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-sans-normal);
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.SchedulingProfiler, .SchedulingProfiler * {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: var(--font-smoothing);
|
||||
}
|
||||
|
||||
.Content {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Paragraph {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ErrorMessage {
|
||||
margin: 0.5rem 0;
|
||||
color: var(--color-dim);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
|
||||
.Row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: wrap;
|
||||
}
|
||||
|
||||
.EmptyStateContainer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Header {
|
||||
font-size: var(--font-size-sans-large);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.Toolbar {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.25rem;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.VRule {
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
border-left: 1px solid var(--color-border);
|
||||
padding-left: 0.25rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.Spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Link {
|
||||
color: var(--color-button);
|
||||
}
|
||||
|
||||
.ScreenshotWrapper {
|
||||
max-width: 30rem;
|
||||
padding: 0 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.Screenshot {
|
||||
width: 100%;
|
||||
border-radius: 0.4em;
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.AppName {
|
||||
font-size: var(--font-size-sans-large);
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 350px) {
|
||||
.AppName {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 600px) {
|
||||
.ScreenshotWrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/**
|
||||
* 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 type {Resource} from 'react-devtools-shared/src/devtools/cache';
|
||||
import type {ReactProfilerData} from './types';
|
||||
import type {ImportWorkerOutputData} from './import-worker/import.worker';
|
||||
|
||||
import * as React from 'react';
|
||||
import {Suspense, useCallback, useState} from 'react';
|
||||
import {createResource} from 'react-devtools-shared/src/devtools/cache';
|
||||
import ReactLogo from 'react-devtools-shared/src/devtools/views/ReactLogo';
|
||||
|
||||
import ImportButton from './ImportButton';
|
||||
import CanvasPage from './CanvasPage';
|
||||
import ImportWorker from './import-worker/import.worker';
|
||||
|
||||
import profilerBrowser from './assets/profilerBrowser.png';
|
||||
import styles from './SchedulingProfiler.css';
|
||||
|
||||
type DataResource = Resource<void, File, ReactProfilerData | Error>;
|
||||
|
||||
function createDataResourceFromImportedFile(file: File): DataResource {
|
||||
return createResource(
|
||||
() => {
|
||||
return new Promise<ReactProfilerData | Error>((resolve, reject) => {
|
||||
const worker: Worker = new (ImportWorker: any)();
|
||||
|
||||
worker.onmessage = function(event) {
|
||||
const data = ((event.data: any): ImportWorkerOutputData);
|
||||
switch (data.status) {
|
||||
case 'SUCCESS':
|
||||
resolve(data.processedData);
|
||||
break;
|
||||
case 'INVALID_PROFILE_ERROR':
|
||||
resolve(data.error);
|
||||
break;
|
||||
case 'UNEXPECTED_ERROR':
|
||||
reject(data.error);
|
||||
break;
|
||||
}
|
||||
worker.terminate();
|
||||
};
|
||||
|
||||
worker.postMessage({file});
|
||||
});
|
||||
},
|
||||
() => file,
|
||||
{useWeakMap: true},
|
||||
);
|
||||
}
|
||||
|
||||
export function SchedulingProfiler(_: {||}) {
|
||||
const [dataResource, setDataResource] = useState<DataResource | null>(null);
|
||||
|
||||
const handleFileSelect = useCallback((file: File) => {
|
||||
setDataResource(createDataResourceFromImportedFile(file));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.SchedulingProfiler}>
|
||||
<div className={styles.Toolbar}>
|
||||
<ReactLogo />
|
||||
<span className={styles.AppName}>Concurrent Mode Profiler</span>
|
||||
<div className={styles.VRule} />
|
||||
<ImportButton onFileSelect={handleFileSelect} />
|
||||
<div className={styles.Spacer} />
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
{dataResource ? (
|
||||
<Suspense fallback={<ProcessingData />}>
|
||||
<DataResourceComponent
|
||||
dataResource={dataResource}
|
||||
onFileSelect={handleFileSelect}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<Welcome onFileSelect={handleFileSelect} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
|
||||
<div className={styles.EmptyStateContainer}>
|
||||
<div className={styles.ScreenshotWrapper}>
|
||||
<img
|
||||
src={profilerBrowser}
|
||||
className={styles.Screenshot}
|
||||
alt="Profiler screenshot"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.Header}>Welcome!</div>
|
||||
<div className={styles.Row}>
|
||||
Click the import button
|
||||
<ImportButton onFileSelect={onFileSelect} /> to import a Chrome
|
||||
performance profile.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProcessingData = () => (
|
||||
<div className={styles.EmptyStateContainer}>
|
||||
<div className={styles.Header}>Processing data...</div>
|
||||
<div className={styles.Row}>This should only take a minute.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CouldNotLoadProfile = ({error, onFileSelect}) => (
|
||||
<div className={styles.EmptyStateContainer}>
|
||||
<div className={styles.Header}>Could not load profile</div>
|
||||
{error.message && (
|
||||
<div className={styles.Row}>
|
||||
<div className={styles.ErrorMessage}>{error.message}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.Row}>
|
||||
Try importing
|
||||
<ImportButton onFileSelect={onFileSelect} />
|
||||
another Chrome performance profile.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DataResourceComponent = ({
|
||||
dataResource,
|
||||
onFileSelect,
|
||||
}: {|
|
||||
dataResource: DataResource,
|
||||
onFileSelect: (file: File) => void,
|
||||
|}) => {
|
||||
const dataOrError = dataResource.read();
|
||||
if (dataOrError instanceof Error) {
|
||||
return (
|
||||
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
|
||||
);
|
||||
}
|
||||
return <CanvasPage profilerData={dataOrError} />;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,378 +0,0 @@
|
||||
/**
|
||||
* 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 type {
|
||||
Flamechart,
|
||||
FlamechartStackFrame,
|
||||
FlamechartStackLayer,
|
||||
} from '../types';
|
||||
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
|
||||
|
||||
import {
|
||||
ColorView,
|
||||
Surface,
|
||||
View,
|
||||
layeredLayout,
|
||||
rectContainsPoint,
|
||||
rectEqualToRect,
|
||||
intersectionOfRects,
|
||||
rectIntersectsRect,
|
||||
verticallyStackedLayout,
|
||||
} from '../view-base';
|
||||
import {
|
||||
durationToWidth,
|
||||
positioningScaleFactor,
|
||||
timestampToPosition,
|
||||
} from './utils/positioning';
|
||||
import {
|
||||
COLORS,
|
||||
FLAMECHART_FONT_SIZE,
|
||||
FLAMECHART_FRAME_HEIGHT,
|
||||
FLAMECHART_TEXT_PADDING,
|
||||
COLOR_HOVER_DIM_DELTA,
|
||||
BORDER_SIZE,
|
||||
} from './constants';
|
||||
import {ColorGenerator, dimmedColor, hslaColorToString} from './utils/colors';
|
||||
|
||||
// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399
|
||||
const colorGenerator = new ColorGenerator(
|
||||
{min: 30, max: 330},
|
||||
{min: 50, max: 80, count: 3},
|
||||
85,
|
||||
);
|
||||
colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100});
|
||||
|
||||
function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) {
|
||||
return colorGenerator.colorForID(scriptUrl ?? '');
|
||||
}
|
||||
|
||||
function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string {
|
||||
const color = defaultHslaColorForStackFrame(stackFrame);
|
||||
return hslaColorToString(color);
|
||||
}
|
||||
|
||||
function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
|
||||
const color = dimmedColor(
|
||||
defaultHslaColorForStackFrame(stackFrame),
|
||||
COLOR_HOVER_DIM_DELTA,
|
||||
);
|
||||
return hslaColorToString(color);
|
||||
}
|
||||
|
||||
const cachedFlamechartTextWidths = new Map();
|
||||
const trimFlamechartText = (
|
||||
context: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
width: number,
|
||||
) => {
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
|
||||
|
||||
let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
|
||||
if (measuredWidth == null) {
|
||||
measuredWidth = context.measureText(trimmedText).width;
|
||||
cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
|
||||
}
|
||||
|
||||
if (measuredWidth <= width) {
|
||||
return trimmedText;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
class FlamechartStackLayerView extends View {
|
||||
/** Layer to display */
|
||||
_stackLayer: FlamechartStackLayer;
|
||||
|
||||
/** A set of `stackLayer`'s frames, for efficient lookup. */
|
||||
_stackFrameSet: Set<FlamechartStackFrame>;
|
||||
|
||||
_intrinsicSize: Size;
|
||||
|
||||
_hoveredStackFrame: FlamechartStackFrame | null = null;
|
||||
_onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
stackLayer: FlamechartStackLayer,
|
||||
duration: number,
|
||||
) {
|
||||
super(surface, frame);
|
||||
this._stackLayer = stackLayer;
|
||||
this._stackFrameSet = new Set(stackLayer);
|
||||
this._intrinsicSize = {
|
||||
width: duration,
|
||||
height: FLAMECHART_FRAME_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredFlamechartStackFrame(
|
||||
hoveredStackFrame: FlamechartStackFrame | null,
|
||||
) {
|
||||
if (this._hoveredStackFrame === hoveredStackFrame) {
|
||||
return; // We're already hovering over this frame
|
||||
}
|
||||
|
||||
// Only care about frames displayed by this view.
|
||||
const stackFrameToSet =
|
||||
hoveredStackFrame && this._stackFrameSet.has(hoveredStackFrame)
|
||||
? hoveredStackFrame
|
||||
: null;
|
||||
if (this._hoveredStackFrame === stackFrameToSet) {
|
||||
return; // Resulting state is unchanged
|
||||
}
|
||||
this._hoveredStackFrame = stackFrameToSet;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {
|
||||
frame,
|
||||
_stackLayer,
|
||||
_hoveredStackFrame,
|
||||
_intrinsicSize,
|
||||
visibleArea,
|
||||
} = this;
|
||||
|
||||
context.fillStyle = COLORS.BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
context.textAlign = 'left';
|
||||
context.textBaseline = 'middle';
|
||||
context.font = `${FLAMECHART_FONT_SIZE}px sans-serif`;
|
||||
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
|
||||
for (let i = 0; i < _stackLayer.length; i++) {
|
||||
const stackFrame = _stackLayer[i];
|
||||
const {name, timestamp, duration} = stackFrame;
|
||||
|
||||
const width = durationToWidth(duration, scaleFactor);
|
||||
if (width < 1) {
|
||||
continue; // Too small to render at this zoom level
|
||||
}
|
||||
|
||||
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
|
||||
const nodeRect: Rect = {
|
||||
origin: {x, y: frame.origin.y},
|
||||
size: {
|
||||
width: Math.floor(width - BORDER_SIZE),
|
||||
height: Math.floor(FLAMECHART_FRAME_HEIGHT - BORDER_SIZE),
|
||||
},
|
||||
};
|
||||
if (!rectIntersectsRect(nodeRect, visibleArea)) {
|
||||
continue; // Not in view
|
||||
}
|
||||
|
||||
const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];
|
||||
context.fillStyle = showHoverHighlight
|
||||
? hoverColorForStackFrame(stackFrame)
|
||||
: defaultColorForStackFrame(stackFrame);
|
||||
|
||||
const drawableRect = intersectionOfRects(nodeRect, visibleArea);
|
||||
context.fillRect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
|
||||
if (width > FLAMECHART_TEXT_PADDING * 2) {
|
||||
const trimmedName = trimFlamechartText(
|
||||
context,
|
||||
name,
|
||||
width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0),
|
||||
);
|
||||
|
||||
if (trimmedName !== null) {
|
||||
context.fillStyle = COLORS.PRIORITY_LABEL;
|
||||
|
||||
// Prevent text from being drawn outside `viewableArea`
|
||||
const textOverflowsViewableArea = !rectEqualToRect(
|
||||
drawableRect,
|
||||
nodeRect,
|
||||
);
|
||||
if (textOverflowsViewableArea) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.rect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
context.closePath();
|
||||
context.clip();
|
||||
}
|
||||
|
||||
context.fillText(
|
||||
trimmedName,
|
||||
nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0),
|
||||
nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
|
||||
);
|
||||
|
||||
if (textOverflowsViewableArea) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this;
|
||||
const {location} = interaction.payload;
|
||||
if (!_onHover || !rectContainsPoint(location, visibleArea)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the node being hovered over.
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
let startIndex = 0;
|
||||
let stopIndex = _stackLayer.length - 1;
|
||||
while (startIndex <= stopIndex) {
|
||||
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
|
||||
const flamechartStackFrame = _stackLayer[currentIndex];
|
||||
const {timestamp, duration} = flamechartStackFrame;
|
||||
|
||||
const width = durationToWidth(duration, scaleFactor);
|
||||
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
|
||||
if (x <= location.x && x + width >= location.x) {
|
||||
_onHover(flamechartStackFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
if (x > location.x) {
|
||||
stopIndex = currentIndex - 1;
|
||||
} else {
|
||||
startIndex = currentIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
_onHover(null);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FlamechartView extends View {
|
||||
_flamechartRowViews: FlamechartStackLayerView[] = [];
|
||||
|
||||
/** Container view that vertically stacks flamechart rows */
|
||||
_verticalStackView: View;
|
||||
|
||||
_hoveredStackFrame: FlamechartStackFrame | null = null;
|
||||
_onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
flamechart: Flamechart,
|
||||
duration: number,
|
||||
) {
|
||||
super(surface, frame, layeredLayout);
|
||||
this.setDataAndUpdateSubviews(flamechart, duration);
|
||||
}
|
||||
|
||||
setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) {
|
||||
const {surface, frame, _onHover, _hoveredStackFrame} = this;
|
||||
|
||||
// Clear existing rows on data update
|
||||
if (this._verticalStackView) {
|
||||
this.removeAllSubviews();
|
||||
this._flamechartRowViews = [];
|
||||
}
|
||||
|
||||
this._verticalStackView = new View(surface, frame, verticallyStackedLayout);
|
||||
this._flamechartRowViews = flamechart.map(stackLayer => {
|
||||
const rowView = new FlamechartStackLayerView(
|
||||
surface,
|
||||
frame,
|
||||
stackLayer,
|
||||
duration,
|
||||
);
|
||||
this._verticalStackView.addSubview(rowView);
|
||||
|
||||
// Update states
|
||||
rowView._onHover = _onHover;
|
||||
rowView.setHoveredFlamechartStackFrame(_hoveredStackFrame);
|
||||
return rowView;
|
||||
});
|
||||
|
||||
// Add a plain background view to prevent gaps from appearing between
|
||||
// flamechartRowViews.
|
||||
const colorView = new ColorView(surface, frame, COLORS.BACKGROUND);
|
||||
this.addSubview(colorView);
|
||||
this.addSubview(this._verticalStackView);
|
||||
}
|
||||
|
||||
setHoveredFlamechartStackFrame(
|
||||
hoveredStackFrame: FlamechartStackFrame | null,
|
||||
) {
|
||||
this._hoveredStackFrame = hoveredStackFrame;
|
||||
this._flamechartRowViews.forEach(rowView =>
|
||||
rowView.setHoveredFlamechartStackFrame(hoveredStackFrame),
|
||||
);
|
||||
}
|
||||
|
||||
setOnHover(onHover: (node: FlamechartStackFrame | null) => void) {
|
||||
this._onHover = onHover;
|
||||
this._flamechartRowViews.forEach(rowView => (rowView._onHover = onHover));
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
// Ignore the wishes of the background color view
|
||||
return this._verticalStackView.desiredSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {_onHover, visibleArea} = this;
|
||||
if (!_onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
// Clear out any hovered flamechart stack frame
|
||||
_onHover(null);
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
/**
|
||||
* 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 type {ReactEvent, ReactProfilerData} from '../types';
|
||||
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
|
||||
|
||||
import {
|
||||
positioningScaleFactor,
|
||||
timestampToPosition,
|
||||
positionToTimestamp,
|
||||
widthToDuration,
|
||||
} from './utils/positioning';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
rectContainsPoint,
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
import {
|
||||
COLORS,
|
||||
EVENT_ROW_PADDING,
|
||||
EVENT_DIAMETER,
|
||||
BORDER_SIZE,
|
||||
} from './constants';
|
||||
|
||||
const EVENT_ROW_HEIGHT_FIXED =
|
||||
EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
|
||||
|
||||
function isSuspenseEvent(event: ReactEvent): boolean %checks {
|
||||
return (
|
||||
event.type === 'suspense-suspend' ||
|
||||
event.type === 'suspense-resolved' ||
|
||||
event.type === 'suspense-rejected'
|
||||
);
|
||||
}
|
||||
|
||||
export class ReactEventsView extends View {
|
||||
_profilerData: ReactProfilerData;
|
||||
_intrinsicSize: Size;
|
||||
|
||||
_hoveredEvent: ReactEvent | null = null;
|
||||
onHover: ((event: ReactEvent | null) => void) | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
super(surface, frame);
|
||||
this._profilerData = profilerData;
|
||||
|
||||
this._intrinsicSize = {
|
||||
width: this._profilerData.duration,
|
||||
height: EVENT_ROW_HEIGHT_FIXED,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredEvent(hoveredEvent: ReactEvent | null) {
|
||||
if (this._hoveredEvent === hoveredEvent) {
|
||||
return;
|
||||
}
|
||||
this._hoveredEvent = hoveredEvent;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `ReactEvent` as a circle in the canvas.
|
||||
*/
|
||||
_drawSingleReactEvent(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: Rect,
|
||||
event: ReactEvent,
|
||||
baseY: number,
|
||||
scaleFactor: number,
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame} = this;
|
||||
const {timestamp, type} = event;
|
||||
|
||||
const x = timestampToPosition(timestamp, scaleFactor, frame);
|
||||
const radius = EVENT_DIAMETER / 2;
|
||||
const eventRect: Rect = {
|
||||
origin: {
|
||||
x: x - radius,
|
||||
y: baseY,
|
||||
},
|
||||
size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER},
|
||||
};
|
||||
if (!rectIntersectsRect(eventRect, rect)) {
|
||||
return; // Not in view
|
||||
}
|
||||
|
||||
let fillStyle = null;
|
||||
|
||||
switch (type) {
|
||||
case 'schedule-render':
|
||||
case 'schedule-state-update':
|
||||
case 'schedule-force-update':
|
||||
if (event.isCascading) {
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SCHEDULE_CASCADING_HOVER
|
||||
: COLORS.REACT_SCHEDULE_CASCADING;
|
||||
} else {
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SCHEDULE_HOVER
|
||||
: COLORS.REACT_SCHEDULE;
|
||||
}
|
||||
break;
|
||||
case 'suspense-suspend':
|
||||
case 'suspense-resolved':
|
||||
case 'suspense-rejected':
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SUSPEND_HOVER
|
||||
: COLORS.REACT_SUSPEND;
|
||||
break;
|
||||
default:
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected event type "%s"', type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (fillStyle !== null) {
|
||||
const y = eventRect.origin.y + radius;
|
||||
|
||||
context.beginPath();
|
||||
context.fillStyle = fillStyle;
|
||||
context.arc(x, y, radius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {
|
||||
frame,
|
||||
_profilerData: {events},
|
||||
_hoveredEvent,
|
||||
visibleArea,
|
||||
} = this;
|
||||
|
||||
context.fillStyle = COLORS.BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
// Draw events
|
||||
const baseY = frame.origin.y + EVENT_ROW_PADDING;
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
|
||||
const highlightedEvents: ReactEvent[] = [];
|
||||
|
||||
events.forEach(event => {
|
||||
if (
|
||||
event === _hoveredEvent ||
|
||||
(_hoveredEvent &&
|
||||
isSuspenseEvent(event) &&
|
||||
isSuspenseEvent(_hoveredEvent) &&
|
||||
event.id === _hoveredEvent.id)
|
||||
) {
|
||||
highlightedEvents.push(event);
|
||||
return;
|
||||
}
|
||||
this._drawSingleReactEvent(
|
||||
context,
|
||||
visibleArea,
|
||||
event,
|
||||
baseY,
|
||||
scaleFactor,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Draw the highlighted items on top so they stand out.
|
||||
// This is helpful if there are multiple (overlapping) items close to each other.
|
||||
highlightedEvents.forEach(event => {
|
||||
this._drawSingleReactEvent(
|
||||
context,
|
||||
visibleArea,
|
||||
event,
|
||||
baseY,
|
||||
scaleFactor,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// Render bottom border.
|
||||
// Propose border rect, check if intersects with `rect`, draw intersection.
|
||||
const borderFrame: Rect = {
|
||||
origin: {
|
||||
x: frame.origin.x,
|
||||
y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: BORDER_SIZE,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {frame, onHover, visibleArea} = this;
|
||||
if (!onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
_profilerData: {events},
|
||||
} = this;
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
const eventTimestampAllowance = widthToDuration(
|
||||
EVENT_DIAMETER / 2,
|
||||
scaleFactor,
|
||||
);
|
||||
|
||||
// Because data ranges may overlap, we want to find the last intersecting item.
|
||||
// This will always be the one on "top" (the one the user is hovering over).
|
||||
for (let index = events.length - 1; index >= 0; index--) {
|
||||
const event = events[index];
|
||||
const {timestamp} = event;
|
||||
|
||||
if (
|
||||
timestamp - eventTimestampAllowance <= hoverTimestamp &&
|
||||
hoverTimestamp <= timestamp + eventTimestampAllowance
|
||||
) {
|
||||
onHover(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onHover(null);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
/**
|
||||
* 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 type {ReactLane, ReactMeasure, ReactProfilerData} from '../types';
|
||||
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
|
||||
|
||||
import {
|
||||
durationToWidth,
|
||||
positioningScaleFactor,
|
||||
positionToTimestamp,
|
||||
timestampToPosition,
|
||||
} from './utils/positioning';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
rectContainsPoint,
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
|
||||
import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
|
||||
import {REACT_TOTAL_NUM_LANES} from '../constants';
|
||||
|
||||
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
|
||||
|
||||
function getMeasuresForLane(
|
||||
allMeasures: ReactMeasure[],
|
||||
lane: ReactLane,
|
||||
): ReactMeasure[] {
|
||||
return allMeasures.filter(measure => measure.lanes.includes(lane));
|
||||
}
|
||||
|
||||
export class ReactMeasuresView extends View {
|
||||
_profilerData: ReactProfilerData;
|
||||
_intrinsicSize: Size;
|
||||
|
||||
_lanesToRender: ReactLane[];
|
||||
_laneToMeasures: Map<ReactLane, ReactMeasure[]>;
|
||||
|
||||
_hoveredMeasure: ReactMeasure | null = null;
|
||||
onHover: ((measure: ReactMeasure | null) => void) | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
super(surface, frame);
|
||||
this._profilerData = profilerData;
|
||||
this._performPreflightComputations();
|
||||
}
|
||||
|
||||
_performPreflightComputations() {
|
||||
this._lanesToRender = [];
|
||||
this._laneToMeasures = new Map<ReactLane, ReactMeasure[]>();
|
||||
|
||||
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
|
||||
const measuresForLane = getMeasuresForLane(
|
||||
this._profilerData.measures,
|
||||
lane,
|
||||
);
|
||||
// Only show lanes with measures
|
||||
if (measuresForLane.length) {
|
||||
this._lanesToRender.push(lane);
|
||||
this._laneToMeasures.set(lane, measuresForLane);
|
||||
}
|
||||
}
|
||||
|
||||
this._intrinsicSize = {
|
||||
width: this._profilerData.duration,
|
||||
height: this._lanesToRender.length * REACT_LANE_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredMeasure(hoveredMeasure: ReactMeasure | null) {
|
||||
if (this._hoveredMeasure === hoveredMeasure) {
|
||||
return;
|
||||
}
|
||||
this._hoveredMeasure = hoveredMeasure;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `ReactMeasure` as a bar in the canvas.
|
||||
*/
|
||||
_drawSingleReactMeasure(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: Rect,
|
||||
measure: ReactMeasure,
|
||||
baseY: number,
|
||||
scaleFactor: number,
|
||||
showGroupHighlight: boolean,
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame} = this;
|
||||
const {timestamp, type, duration} = measure;
|
||||
|
||||
let fillStyle = null;
|
||||
let hoveredFillStyle = null;
|
||||
let groupSelectedFillStyle = null;
|
||||
|
||||
// We could change the max to 0 and just skip over rendering anything that small,
|
||||
// but this has the effect of making the chart look very empty when zoomed out.
|
||||
// So long as perf is okay- it might be best to err on the side of showing things.
|
||||
const width = durationToWidth(duration, scaleFactor);
|
||||
if (width <= 0) {
|
||||
return; // Too small to render at this zoom level
|
||||
}
|
||||
|
||||
const x = timestampToPosition(timestamp, scaleFactor, frame);
|
||||
const measureRect: Rect = {
|
||||
origin: {x, y: baseY},
|
||||
size: {width, height: REACT_MEASURE_HEIGHT},
|
||||
};
|
||||
if (!rectIntersectsRect(measureRect, rect)) {
|
||||
return; // Not in view
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'commit':
|
||||
fillStyle = COLORS.REACT_COMMIT;
|
||||
hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED;
|
||||
break;
|
||||
case 'render-idle':
|
||||
// We could render idle time as diagonal hashes.
|
||||
// This looks nicer when zoomed in, but not so nice when zoomed out.
|
||||
// color = context.createPattern(getIdlePattern(), 'repeat');
|
||||
fillStyle = COLORS.REACT_IDLE;
|
||||
hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED;
|
||||
break;
|
||||
case 'render':
|
||||
fillStyle = COLORS.REACT_RENDER;
|
||||
hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED;
|
||||
break;
|
||||
case 'layout-effects':
|
||||
fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
|
||||
hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED;
|
||||
break;
|
||||
case 'passive-effects':
|
||||
fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
|
||||
hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected measure type "${type}"`);
|
||||
}
|
||||
|
||||
const drawableRect = intersectionOfRects(measureRect, rect);
|
||||
context.fillStyle = showHoverHighlight
|
||||
? hoveredFillStyle
|
||||
: showGroupHighlight
|
||||
? groupSelectedFillStyle
|
||||
: fillStyle;
|
||||
context.fillRect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {
|
||||
frame,
|
||||
_hoveredMeasure,
|
||||
_lanesToRender,
|
||||
_laneToMeasures,
|
||||
visibleArea,
|
||||
} = this;
|
||||
|
||||
context.fillStyle = COLORS.PRIORITY_BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
|
||||
for (let i = 0; i < _lanesToRender.length; i++) {
|
||||
const lane = _lanesToRender[i];
|
||||
const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
|
||||
const measuresForLane = _laneToMeasures.get(lane);
|
||||
|
||||
if (!measuresForLane) {
|
||||
throw new Error(
|
||||
'No measures found for a React lane! This is a bug in this profiler tool. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
// Draw measures
|
||||
for (let j = 0; j < measuresForLane.length; j++) {
|
||||
const measure = measuresForLane[j];
|
||||
const showHoverHighlight = _hoveredMeasure === measure;
|
||||
const showGroupHighlight =
|
||||
!!_hoveredMeasure && _hoveredMeasure.batchUID === measure.batchUID;
|
||||
|
||||
this._drawSingleReactMeasure(
|
||||
context,
|
||||
visibleArea,
|
||||
measure,
|
||||
baseY,
|
||||
scaleFactor,
|
||||
showGroupHighlight,
|
||||
showHoverHighlight,
|
||||
);
|
||||
}
|
||||
|
||||
// Render bottom border
|
||||
const borderFrame: Rect = {
|
||||
origin: {
|
||||
x: frame.origin.x,
|
||||
y: frame.origin.y + (i + 1) * REACT_LANE_HEIGHT - BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: BORDER_SIZE,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(
|
||||
borderFrame,
|
||||
visibleArea,
|
||||
);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {
|
||||
frame,
|
||||
_intrinsicSize,
|
||||
_lanesToRender,
|
||||
_laneToMeasures,
|
||||
onHover,
|
||||
visibleArea,
|
||||
} = this;
|
||||
if (!onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Identify the lane being hovered over
|
||||
const adjustedCanvasMouseY = location.y - frame.origin.y;
|
||||
const renderedLaneIndex = Math.floor(
|
||||
adjustedCanvasMouseY / REACT_LANE_HEIGHT,
|
||||
);
|
||||
if (renderedLaneIndex < 0 || renderedLaneIndex >= _lanesToRender.length) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
const lane = _lanesToRender[renderedLaneIndex];
|
||||
|
||||
// Find the measure in `lane` being hovered over.
|
||||
//
|
||||
// Because data ranges may overlap, we want to find the last intersecting item.
|
||||
// This will always be the one on "top" (the one the user is hovering over).
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
const measures = _laneToMeasures.get(lane);
|
||||
if (!measures) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = measures.length - 1; index >= 0; index--) {
|
||||
const measure = measures[index];
|
||||
const {duration, timestamp} = measure;
|
||||
|
||||
if (
|
||||
hoverTimestamp >= timestamp &&
|
||||
hoverTimestamp <= timestamp + duration
|
||||
) {
|
||||
onHover(measure);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onHover(null);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/**
|
||||
* 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 type {Rect, Size} from '../view-base';
|
||||
|
||||
import {
|
||||
durationToWidth,
|
||||
positioningScaleFactor,
|
||||
positionToTimestamp,
|
||||
timestampToPosition,
|
||||
} from './utils/positioning';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
import {
|
||||
COLORS,
|
||||
INTERVAL_TIMES,
|
||||
LABEL_SIZE,
|
||||
MARKER_FONT_SIZE,
|
||||
MARKER_HEIGHT,
|
||||
MARKER_TEXT_PADDING,
|
||||
MARKER_TICK_HEIGHT,
|
||||
MIN_INTERVAL_SIZE_PX,
|
||||
BORDER_SIZE,
|
||||
} from './constants';
|
||||
|
||||
const HEADER_HEIGHT_FIXED = MARKER_HEIGHT + BORDER_SIZE;
|
||||
const LABEL_FIXED_WIDTH = LABEL_SIZE + BORDER_SIZE;
|
||||
|
||||
export class TimeAxisMarkersView extends View {
|
||||
_totalDuration: number;
|
||||
_intrinsicSize: Size;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, totalDuration: number) {
|
||||
super(surface, frame);
|
||||
this._totalDuration = totalDuration;
|
||||
this._intrinsicSize = {
|
||||
width: this._totalDuration,
|
||||
height: HEADER_HEIGHT_FIXED,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
// Time mark intervals vary based on the current zoom range and the time it represents.
|
||||
// In Chrome, these seem to range from 70-140 pixels wide.
|
||||
// Time wise, they represent intervals of e.g. 1s, 500ms, 200ms, 100ms, 50ms, 20ms.
|
||||
// Based on zoom, we should determine which amount to actually show.
|
||||
_getTimeTickInterval(scaleFactor: number): number {
|
||||
for (let i = 0; i < INTERVAL_TIMES.length; i++) {
|
||||
const currentInterval = INTERVAL_TIMES[i];
|
||||
const intervalWidth = durationToWidth(currentInterval, scaleFactor);
|
||||
if (intervalWidth > MIN_INTERVAL_SIZE_PX) {
|
||||
return currentInterval;
|
||||
}
|
||||
}
|
||||
return INTERVAL_TIMES[0];
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {frame, _intrinsicSize, visibleArea} = this;
|
||||
const clippedFrame = {
|
||||
origin: frame.origin,
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: _intrinsicSize.height,
|
||||
},
|
||||
};
|
||||
const drawableRect = intersectionOfRects(clippedFrame, visibleArea);
|
||||
|
||||
// Clear background
|
||||
context.fillStyle = COLORS.BACKGROUND;
|
||||
context.fillRect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
_intrinsicSize.width,
|
||||
clippedFrame,
|
||||
);
|
||||
const interval = this._getTimeTickInterval(scaleFactor);
|
||||
const firstIntervalTimestamp =
|
||||
Math.ceil(
|
||||
positionToTimestamp(
|
||||
drawableRect.origin.x - LABEL_FIXED_WIDTH,
|
||||
scaleFactor,
|
||||
clippedFrame,
|
||||
) / interval,
|
||||
) * interval;
|
||||
|
||||
for (
|
||||
let markerTimestamp = firstIntervalTimestamp;
|
||||
true;
|
||||
markerTimestamp += interval
|
||||
) {
|
||||
if (markerTimestamp <= 0) {
|
||||
continue; // Timestamps < are probably a bug; markers at 0 are ugly.
|
||||
}
|
||||
|
||||
const x = timestampToPosition(markerTimestamp, scaleFactor, clippedFrame);
|
||||
if (x > drawableRect.origin.x + drawableRect.size.width) {
|
||||
break; // Not in view
|
||||
}
|
||||
|
||||
const markerLabel = Math.round(markerTimestamp);
|
||||
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
x,
|
||||
drawableRect.origin.y + MARKER_HEIGHT - MARKER_TICK_HEIGHT,
|
||||
BORDER_SIZE,
|
||||
MARKER_TICK_HEIGHT,
|
||||
);
|
||||
|
||||
context.fillStyle = COLORS.TIME_MARKER_LABEL;
|
||||
context.textAlign = 'right';
|
||||
context.textBaseline = 'middle';
|
||||
context.font = `${MARKER_FONT_SIZE}px sans-serif`;
|
||||
context.fillText(
|
||||
`${markerLabel}ms`,
|
||||
x - MARKER_TEXT_PADDING,
|
||||
MARKER_HEIGHT / 2,
|
||||
);
|
||||
}
|
||||
|
||||
// Render bottom border.
|
||||
// Propose border rect, check if intersects with `rect`, draw intersection.
|
||||
const borderFrame: Rect = {
|
||||
origin: {
|
||||
x: clippedFrame.origin.x,
|
||||
y: clippedFrame.origin.y + clippedFrame.size.height - BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: clippedFrame.size.width,
|
||||
height: BORDER_SIZE,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
/**
|
||||
* 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 type {UserTimingMark} from '../types';
|
||||
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
|
||||
|
||||
import {
|
||||
positioningScaleFactor,
|
||||
timestampToPosition,
|
||||
positionToTimestamp,
|
||||
widthToDuration,
|
||||
} from './utils/positioning';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
rectContainsPoint,
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
import {
|
||||
COLORS,
|
||||
EVENT_ROW_PADDING,
|
||||
USER_TIMING_MARK_SIZE,
|
||||
BORDER_SIZE,
|
||||
} from './constants';
|
||||
|
||||
const ROW_HEIGHT_FIXED =
|
||||
EVENT_ROW_PADDING + USER_TIMING_MARK_SIZE + EVENT_ROW_PADDING;
|
||||
|
||||
export class UserTimingMarksView extends View {
|
||||
_marks: UserTimingMark[];
|
||||
_intrinsicSize: Size;
|
||||
|
||||
_hoveredMark: UserTimingMark | null = null;
|
||||
onHover: ((mark: UserTimingMark | null) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
marks: UserTimingMark[],
|
||||
duration: number,
|
||||
) {
|
||||
super(surface, frame);
|
||||
this._marks = marks;
|
||||
|
||||
this._intrinsicSize = {
|
||||
width: duration,
|
||||
height: ROW_HEIGHT_FIXED,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredMark(hoveredMark: UserTimingMark | null) {
|
||||
if (this._hoveredMark === hoveredMark) {
|
||||
return;
|
||||
}
|
||||
this._hoveredMark = hoveredMark;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `UserTimingMark` as a circle in the canvas.
|
||||
*/
|
||||
_drawSingleMark(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: Rect,
|
||||
mark: UserTimingMark,
|
||||
baseY: number,
|
||||
scaleFactor: number,
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame} = this;
|
||||
const {timestamp} = mark;
|
||||
|
||||
const x = timestampToPosition(timestamp, scaleFactor, frame);
|
||||
const size = USER_TIMING_MARK_SIZE;
|
||||
const halfSize = size / 2;
|
||||
|
||||
const markRect: Rect = {
|
||||
origin: {
|
||||
x: x - halfSize,
|
||||
y: baseY,
|
||||
},
|
||||
size: {width: size, height: size},
|
||||
};
|
||||
if (!rectIntersectsRect(markRect, rect)) {
|
||||
return; // Not in view
|
||||
}
|
||||
|
||||
const fillStyle = showHoverHighlight
|
||||
? COLORS.USER_TIMING_HOVER
|
||||
: COLORS.USER_TIMING;
|
||||
|
||||
if (fillStyle !== null) {
|
||||
const y = baseY + halfSize;
|
||||
|
||||
context.beginPath();
|
||||
context.fillStyle = fillStyle;
|
||||
context.moveTo(x, y - halfSize);
|
||||
context.lineTo(x + halfSize, y);
|
||||
context.lineTo(x, y + halfSize);
|
||||
context.lineTo(x - halfSize, y);
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {frame, _marks, _hoveredMark, visibleArea} = this;
|
||||
|
||||
context.fillStyle = COLORS.BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
// Draw marks
|
||||
const baseY = frame.origin.y + EVENT_ROW_PADDING;
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
|
||||
_marks.forEach(mark => {
|
||||
if (mark === _hoveredMark) {
|
||||
return;
|
||||
}
|
||||
this._drawSingleMark(
|
||||
context,
|
||||
visibleArea,
|
||||
mark,
|
||||
baseY,
|
||||
scaleFactor,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Draw the hovered and/or selected items on top so they stand out.
|
||||
// This is helpful if there are multiple (overlapping) items close to each other.
|
||||
if (_hoveredMark !== null) {
|
||||
this._drawSingleMark(
|
||||
context,
|
||||
visibleArea,
|
||||
_hoveredMark,
|
||||
baseY,
|
||||
scaleFactor,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Render bottom border.
|
||||
// Propose border rect, check if intersects with `rect`, draw intersection.
|
||||
const borderFrame: Rect = {
|
||||
origin: {
|
||||
x: frame.origin.x,
|
||||
y: frame.origin.y + ROW_HEIGHT_FIXED - BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: BORDER_SIZE,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {frame, onHover, visibleArea} = this;
|
||||
if (!onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const {_marks} = this;
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
const markTimestampAllowance = widthToDuration(
|
||||
USER_TIMING_MARK_SIZE / 2,
|
||||
scaleFactor,
|
||||
);
|
||||
|
||||
// Because data ranges may overlap, we want to find the last intersecting item.
|
||||
// This will always be the one on "top" (the one the user is hovering over).
|
||||
for (let index = _marks.length - 1; index >= 0; index--) {
|
||||
const mark = _marks[index];
|
||||
const {timestamp} = mark;
|
||||
|
||||
if (
|
||||
timestamp - markTimestampAllowance <= hoverTimestamp &&
|
||||
hoverTimestamp <= timestamp + markTimestampAllowance
|
||||
) {
|
||||
onHover(mark);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onHover(null);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export const LABEL_SIZE = 80;
|
||||
export const LABEL_FONT_SIZE = 11;
|
||||
export const MARKER_HEIGHT = 20;
|
||||
export const MARKER_TICK_HEIGHT = 8;
|
||||
export const MARKER_FONT_SIZE = 10;
|
||||
export const MARKER_TEXT_PADDING = 8;
|
||||
export const COLOR_HOVER_DIM_DELTA = 5;
|
||||
|
||||
export const INTERVAL_TIMES = [
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
500,
|
||||
1000,
|
||||
2000,
|
||||
5000,
|
||||
];
|
||||
export const MIN_INTERVAL_SIZE_PX = 70;
|
||||
|
||||
export const EVENT_ROW_PADDING = 4;
|
||||
export const EVENT_DIAMETER = 6;
|
||||
export const USER_TIMING_MARK_SIZE = 8;
|
||||
export const REACT_MEASURE_HEIGHT = 9;
|
||||
export const BORDER_SIZE = 1;
|
||||
|
||||
export const FLAMECHART_FONT_SIZE = 10;
|
||||
export const FLAMECHART_FRAME_HEIGHT = 16;
|
||||
export const FLAMECHART_TEXT_PADDING = 3;
|
||||
|
||||
export const COLORS = Object.freeze({
|
||||
BACKGROUND: '#ffffff',
|
||||
PRIORITY_BACKGROUND: '#ededf0',
|
||||
PRIORITY_BORDER: '#d7d7db',
|
||||
PRIORITY_LABEL: '#272727',
|
||||
USER_TIMING: '#c9cacd',
|
||||
USER_TIMING_HOVER: '#93959a',
|
||||
REACT_IDLE: '#edf6ff',
|
||||
REACT_IDLE_SELECTED: '#EDF6FF',
|
||||
REACT_IDLE_HOVER: '#EDF6FF',
|
||||
REACT_RENDER: '#9fc3f3',
|
||||
REACT_RENDER_SELECTED: '#64A9F5',
|
||||
REACT_RENDER_HOVER: '#2683E2',
|
||||
REACT_COMMIT: '#ff718e',
|
||||
REACT_COMMIT_SELECTED: '#FF5277',
|
||||
REACT_COMMIT_HOVER: '#ed0030',
|
||||
REACT_LAYOUT_EFFECTS: '#c88ff0',
|
||||
REACT_LAYOUT_EFFECTS_SELECTED: '#934FC1',
|
||||
REACT_LAYOUT_EFFECTS_HOVER: '#601593',
|
||||
REACT_PASSIVE_EFFECTS: '#c88ff0',
|
||||
REACT_PASSIVE_EFFECTS_SELECTED: '#934FC1',
|
||||
REACT_PASSIVE_EFFECTS_HOVER: '#601593',
|
||||
REACT_SCHEDULE: '#9fc3f3',
|
||||
REACT_SCHEDULE_HOVER: '#2683E2',
|
||||
REACT_SCHEDULE_CASCADING: '#ff718e',
|
||||
REACT_SCHEDULE_CASCADING_HOVER: '#ed0030',
|
||||
REACT_SUSPEND: '#a6e59f',
|
||||
REACT_SUSPEND_HOVER: '#13bc00',
|
||||
REACT_WORK_BORDER: '#ffffff',
|
||||
TIME_MARKER_LABEL: '#18212b',
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from './FlamechartView';
|
||||
export * from './ReactEventsView';
|
||||
export * from './ReactMeasuresView';
|
||||
export * from './TimeAxisMarkersView';
|
||||
export * from './UserTimingMarksView';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user